Drogheda: T12 (MFF2 repoint as a replayable script) + kicad submodule bump
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.
This commit is contained in:
parent
564c3e450d
commit
33b62d7052
@ -1 +1 @@
|
||||
Subproject commit 63dfbaa351c645ba825674a7c668b29e92a8c2c3
|
||||
Subproject commit 94917b1993b6003efb653483a305794130d395b3
|
||||
227
scripts/2026-09-15_v6_T12_sim_dual_footprint.py
Normal file
227
scripts/2026-09-15_v6_T12_sim_dual_footprint.py
Normal file
@ -0,0 +1,227 @@
|
||||
#!/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.')
|
||||
Loading…
Reference in New Issue
Block a user