From 33b62d7052f14167f815e6ad1a7df7263ab88dc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leopoldo=20Rodr=C3=ADguez?= Date: Tue, 15 Sep 2026 10:58:36 -0600 Subject: [PATCH] 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. --- 03-Architecture-Hardware/kicad | 2 +- .../2026-09-15_v6_T12_sim_dual_footprint.py | 227 ++++++++++++++++++ 2 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 scripts/2026-09-15_v6_T12_sim_dual_footprint.py diff --git a/03-Architecture-Hardware/kicad b/03-Architecture-Hardware/kicad index 63dfbaa..94917b1 160000 --- a/03-Architecture-Hardware/kicad +++ b/03-Architecture-Hardware/kicad @@ -1 +1 @@ -Subproject commit 63dfbaa351c645ba825674a7c668b29e92a8c2c3 +Subproject commit 94917b1993b6003efb653483a305794130d395b3 diff --git a/scripts/2026-09-15_v6_T12_sim_dual_footprint.py b/scripts/2026-09-15_v6_T12_sim_dual_footprint.py new file mode 100644 index 0000000..a0fb411 --- /dev/null +++ b/scripts/2026-09-15_v6_T12_sim_dual_footprint.py @@ -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.')