scripts/2026-09-15_v6_T12_sim_dual_footprint.py moves the U22 edits into the T-script chain. Idempotent, dry-run by default, .T12bak backups, UUID invariant guard, and a gate-0 assertion on the MFF2 land pattern. Verified to reproduce the hand edits byte-for-byte. Part 2 (nano-SIM holder + 0R selects) pending the holder datasheet.
228 lines
8.4 KiB
Python
228 lines
8.4 KiB
Python
#!/usr/bin/env python3
|
|
"""v6 T12 - SIM block: MFF2 footprint repoint (part 1 of 2).
|
|
|
|
WHY THIS SCRIPT EXISTS
|
|
----------------------
|
|
U22 (MFF2 soldered eSIM, Block Q) shipped from T4 with a placeholder footprint field:
|
|
the literal string "Drogheda:MFF2_5x6_TBD", which resolved to nothing - Drogheda.pretty
|
|
held only the E-J1 socket. U22 was also absent from the board.
|
|
|
|
On 2026-09-15 the real land pattern was built as Drogheda.pretty/MFF2_6x5.kicad_mod
|
|
(geometry derived from ETSI TS 102 671 V19.0.0 section 6, cross-checked against the
|
|
ST ST4SIM-200M VFDFPN8 outline) and the three property fields were edited BY HAND.
|
|
Hand edits live outside the T-script replay chain, so a future v5 schematic change
|
|
would silently discard them. This script puts them back in the chain.
|
|
|
|
It is IDEMPOTENT: on an already-patched tree it reports 0 changes and exits 0.
|
|
|
|
NAMING: the old field said "5x6"; ETSI dimensions the package as E=6.00 x D=5.00,
|
|
so the footprint is MFF2_6x5.
|
|
|
|
PART 2 (pending, will be appended to this same script)
|
|
------------------------------------------------------
|
|
The nano-SIM holder alternate + eight 0R selects downstream of the existing ESD
|
|
(D23 SIM_DATA / D24 SIM_CLK / D25 SIM_RST) + DNP flags, so MFF2 and a card holder
|
|
can share the bus with exactly one populated. Blocked on the holder's land pattern.
|
|
|
|
LESSONS APPLIED (see drogheda-v6-resync-protocol)
|
|
-------------------------------------------------
|
|
- no bare numeric regex; every substitution is a complete quoted token or is scoped
|
|
to one paren-matched s-expression block
|
|
- hard invariant guard: aborts if ANY pre-existing UUID would change
|
|
- .T12bak written before any file is touched; dry-run is the default
|
|
|
|
Usage: python3 2026-09-15_v6_T12_sim_dual_footprint.py [--apply]
|
|
"""
|
|
import os
|
|
import re
|
|
import shutil
|
|
import sys
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
KI = os.path.join(HERE, '..', '03-Architecture-Hardware', 'kicad')
|
|
SYM = os.path.join(KI, 'Drogheda-sym.kicad_sym')
|
|
SCH = os.path.join(KI, 'hbox-watchdog-v6.kicad_sch')
|
|
FPF = os.path.join(KI, 'Drogheda.pretty', 'MFF2_6x5.kicad_mod')
|
|
|
|
APPLY = '--apply' in sys.argv
|
|
UUID_RE = re.compile(r'\(uuid "([^"]*)"\)')
|
|
|
|
OLD_FP = '"Drogheda:MFF2_5x6_TBD"'
|
|
NEW_FP = '"Drogheda:MFF2_6x5"'
|
|
|
|
OLD_DESC = (
|
|
'MFF2 / DFN-8 5x6 mm soldered M2M SIM (ETSI TS 102 671). Pads: 1=C1 VCC, 2=C2 RST, '
|
|
'3=C3 CLK, 4=C4 NC, 5=C5 GND, 6=C6 VPP (NC), 7=C7 I/O, 8=C8 NC. FOOTPRINT TBD - build '
|
|
'from the chosen carrier chip datasheet before layout. Carrier-supplied part, not on LCSC.'
|
|
)
|
|
NEW_DESC = (
|
|
'MFF2 / VFDFPN-8 6.0x5.0 mm soldered M2M eSIM (ETSI TS 102 671). Pads: 1=C1 VCC, 2=C2 RST, '
|
|
'3=C3 CLK, 4=C4 NC, 5=C5 GND, 6=C6 VPP (NC), 7=C7 I/O, 8=C8 NC - JEDEC DFN counterclockwise, '
|
|
'pin 1 top-left, matches ST ST4SIM-200M. Footprint Drogheda:MFF2_6x5 (2026-09-15). '
|
|
'Carrier-supplied part: NO LCSC number, exclude from the JLCPCB PCBA BOM, hand-solder or '
|
|
'consign. Verify pinout + die-pad exposure against the sourced carrier drawing before fab.'
|
|
)
|
|
U22_DESC = 'MFF2 eSIM (carrier-supplied, NO LCSC - do not order from JLCPCB)'
|
|
|
|
# pad number -> (x, y); size is 0.9 x 0.5 for all eight
|
|
EXPECTED_PADS = {
|
|
'1': (-2.85, -1.905), '2': (-2.85, -0.635), '3': (-2.85, 0.635), '4': (-2.85, 1.905),
|
|
'5': (2.85, 1.905), '6': (2.85, 0.635), '7': (2.85, -0.635), '8': (2.85, -1.905),
|
|
}
|
|
|
|
|
|
def die(msg):
|
|
sys.exit('REFUSING: ' + msg)
|
|
|
|
|
|
def balanced_end(s, j):
|
|
"""End index (exclusive) of the s-expression opening at j. String-aware."""
|
|
depth = 0
|
|
k = j
|
|
in_str = False
|
|
while k < len(s):
|
|
c = s[k]
|
|
if in_str:
|
|
if c == '\\':
|
|
k += 2
|
|
continue
|
|
if c == '"':
|
|
in_str = False
|
|
else:
|
|
if c == '"':
|
|
in_str = True
|
|
elif c == '(':
|
|
depth += 1
|
|
elif c == ')':
|
|
depth -= 1
|
|
if depth == 0:
|
|
return k + 1
|
|
k += 1
|
|
die('unbalanced s-expression starting at offset %d' % j)
|
|
|
|
|
|
def paren_ok(s, label):
|
|
depth = 0
|
|
k = 0
|
|
in_str = False
|
|
while k < len(s):
|
|
c = s[k]
|
|
if in_str:
|
|
if c == '\\':
|
|
k += 2
|
|
continue
|
|
if c == '"':
|
|
in_str = False
|
|
else:
|
|
if c == '"':
|
|
in_str = True
|
|
elif c == '(':
|
|
depth += 1
|
|
elif c == ')':
|
|
depth -= 1
|
|
if depth < 0:
|
|
die('%s: unbalanced ")" at offset %d' % (label, k))
|
|
k += 1
|
|
if depth != 0:
|
|
die('%s: %d unclosed "(" at EOF' % (label, depth))
|
|
if in_str:
|
|
die('%s: unterminated string at EOF' % label)
|
|
|
|
|
|
# ---------------------------------------------------------------- gate 0: footprint
|
|
print('== gate 0: the footprint file must exist and match the documented geometry ==')
|
|
if not os.path.isfile(FPF):
|
|
die('%s is missing - build the land pattern before running T12' % FPF)
|
|
fp_src = open(FPF, encoding='utf-8').read()
|
|
paren_ok(fp_src, 'MFF2_6x5.kicad_mod')
|
|
|
|
pads = dict(
|
|
(m.group(1), (float(m.group(2)), float(m.group(3))))
|
|
for m in re.finditer(r'\(pad "(\d)" smd rect \(at (-?[\d.]+) (-?[\d.]+)\) \(size 0\.9 0\.5\)', fp_src)
|
|
)
|
|
if pads != EXPECTED_PADS:
|
|
die('MFF2_6x5.kicad_mod pad geometry does not match the T12 reference.\n'
|
|
' found: %s\n expected: %s' % (sorted(pads.items()), sorted(EXPECTED_PADS.items())))
|
|
print(' OK 8 pads, 0.9 x 0.5, columns at X +/-2.85, rows at Y +/-1.905 / +/-0.635')
|
|
|
|
# ---------------------------------------------------------------- the edits
|
|
edits = 0
|
|
reports = []
|
|
|
|
for path in (SYM, SCH):
|
|
name = os.path.basename(path)
|
|
if not os.path.isfile(path):
|
|
die('%s is missing' % path)
|
|
src = open(path, encoding='utf-8').read()
|
|
paren_ok(src, name)
|
|
new = src
|
|
local = []
|
|
|
|
# (a) footprint field - complete quoted token only
|
|
n = new.count(OLD_FP)
|
|
if n:
|
|
new = new.replace(OLD_FP, NEW_FP)
|
|
local.append('%d x footprint field -> Drogheda:MFF2_6x5' % n)
|
|
|
|
# (b) library description (SYM: the symbol; SCH: the lib_symbols cache copy)
|
|
n = new.count(OLD_DESC)
|
|
if n:
|
|
new = new.replace(OLD_DESC, NEW_DESC)
|
|
local.append('%d x symbol description de-TBD-ed' % n)
|
|
|
|
# (c) U22 instance Description - scoped to U22's own paren-matched block
|
|
if name.endswith('.kicad_sch'):
|
|
targets = []
|
|
for m in re.finditer(r'\(symbol(?=[\s(])', new):
|
|
j = m.start()
|
|
e = balanced_end(new, j)
|
|
blk = new[j:e]
|
|
if '"Drogheda-sym:MFF2_SIM"' in blk and '(property "Reference" "U22"' in blk:
|
|
targets.append((j, e))
|
|
if len(targets) != 1:
|
|
die('expected exactly 1 U22 MFF2_SIM instance in %s, found %d' % (name, len(targets)))
|
|
j, e = targets[0]
|
|
blk = new[j:e]
|
|
if '(property "Description" ""' in blk:
|
|
blk2 = blk.replace('(property "Description" ""',
|
|
'(property "Description" "%s"' % U22_DESC, 1)
|
|
new = new[:j] + blk2 + new[e:]
|
|
local.append('1 x U22 instance Description set')
|
|
elif U22_DESC not in blk:
|
|
die('U22 Description is neither empty nor the T12 value - inspect by hand')
|
|
# sanity: U22 must carry no LCSC number (carrier-supplied part)
|
|
blk_now = new[j:balanced_end(new, j)]
|
|
mm = re.search(r'\(property "LCSC" "([^"]*)"', blk_now)
|
|
if mm and mm.group(1).strip():
|
|
die('U22 carries LCSC "%s" - it is a carrier-supplied part and must have none '
|
|
'(C730237 belongs to U23/U24, the 74LVC2T45s)' % mm.group(1))
|
|
|
|
if new == src:
|
|
reports.append('%-28s already at T12 state, nothing to do' % name)
|
|
continue
|
|
|
|
# invariants
|
|
if UUID_RE.findall(new) != UUID_RE.findall(src):
|
|
die('%s: a UUID would change' % name)
|
|
paren_ok(new, name + ' (post-edit)')
|
|
if OLD_FP in new or 'MFF2_5x6_TBD' in new:
|
|
die('%s: a TBD footprint reference survived the edit' % name)
|
|
|
|
reports.append('%-28s %s' % (name, '; '.join(local)))
|
|
edits += len(local)
|
|
if APPLY:
|
|
shutil.copy2(path, path + '.T12bak')
|
|
open(path, 'w', encoding='utf-8').write(new)
|
|
|
|
print()
|
|
print('== edits ==')
|
|
for r in reports:
|
|
print(' ' + r)
|
|
|
|
print()
|
|
print('total change groups: %d' % edits)
|
|
if APPLY:
|
|
print('APPLIED (.T12bak written beside each modified file)')
|
|
else:
|
|
print('DRY RUN - nothing written. Re-run with --apply.')
|