diff --git a/03-Architecture-Hardware/kicad b/03-Architecture-Hardware/kicad index affb84b..bb78cb8 160000 --- a/03-Architecture-Hardware/kicad +++ b/03-Architecture-Hardware/kicad @@ -1 +1 @@ -Subproject commit affb84bccd06df085f25583d016c79494ac5c914 +Subproject commit bb78cb8888264a55cfa52187e36521bc50e21dd7 diff --git a/scripts/2026-09-25_v6_T14_sim_holder_primary.py b/scripts/2026-09-25_v6_T14_sim_holder_primary.py new file mode 100644 index 0000000..913c55e --- /dev/null +++ b/scripts/2026-09-25_v6_T14_sim_holder_primary.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""v6 T14 — nano-SIM holder becomes the PRIMARY SIM (Telcel will not supply MFF2). + +Context: T13 (17-Sep) built a dual-SIM block with the soldered MFF2 (U22) populated and +the JAE SF72S006 holder (J14) DNP. On 25-Sep Leo confirmed Telcel will NOT provide the +MFF2, so as-is the board would ship with jumpers to an empty footprint and no SIM socket. + +This script (schematic only — the PCB picks it up via Update PCB from Schematic): + 1. Flips population: J14 + R131..R134 -> populated ; U22 + R127..R130 -> DNP. + The MFF2 footprint stays on the board as a future option. + 2. Fixes the 5 ERC errors left by T13: + - no-connect flags on J14 C6 (VPP), CSW, DSW + - PWR_FLAG on /SIMB_VDD and /SIMA_VDD (both are fed through 0R jumpers, + so ERC could not see a driver) + +Method: clone-and-transform from existing blocks in the same file (PF19 for the flags, +an existing no_connect), fresh UUIDs, invariant guards. Dry-run by default. + + python3 2026-09-25_v6_T14_sim_holder_primary.py hbox-watchdog-v6.kicad_sch # dry run + python3 2026-09-25_v6_T14_sim_holder_primary.py hbox-watchdog-v6.kicad_sch --apply # writes .T14bak +""" +import re, sys, uuid, shutil + +POPULATE = ['J14', 'R131', 'R132', 'R133', 'R134'] +MAKE_DNP = ['U22', 'R127', 'R128', 'R129', 'R130'] +NO_CONNECT = [(327.3, 487.54), (352.7, 482.46), (352.7, 485.0)] # J14 C6, CSW, DSW +FLAGS = [('PF20', 310.0, 453.81, 'SIMB_VDD'), ('PF21', 310.0, 423.81, 'SIMA_VDD')] + +def span(s, i): + d = 0; instr = False; esc = False; j = i + while True: + c = s[j] + if esc: esc = False + elif c == '\\': esc = True + elif c == '"': instr = not instr + elif not instr: + if c == '(': d += 1 + elif c == ')': + d -= 1 + if d == 0: return j + 1 + j += 1 + +def balanced(s): + d = 0; instr = False; esc = False + for c in s: + if esc: esc = False; continue + if c == '\\': esc = True; continue + if c == '"': instr = not instr; continue + if instr: continue + if c == '(': d += 1 + elif c == ')': d -= 1 + if d < 0: return False + return d == 0 + +UUID = re.compile(r'\(uuid "([0-9a-f-]{36})"\)') + +def sym_block(s, ref): + m = re.search(r'\(property "Reference" "%s"\s' % re.escape(ref), s) + if not m: raise SystemExit('reference not found: ' + ref) + a = s.rfind('\n\t(symbol', 0, m.start()) + 1 + return a, span(s, a) + +def main(): + path = sys.argv[1]; apply = '--apply' in sys.argv + s0 = open(path).read(); s = s0 + assert balanced(s0), 'input not paren-balanced' + uuids0 = set(UUID.findall(s0)) + changes = [] + + # 1. population flip — only the symbol's own top-level (dnp …) token + for ref, want in [(r, 'no') for r in POPULATE] + [(r, 'yes') for r in MAKE_DNP]: + a, e = sym_block(s, ref); blk = s[a:e] + m = re.search(r'\n\t\t\(dnp (yes|no)\)', blk) + if not m: raise SystemExit('no dnp flag on ' + ref) + if m.group(1) != want: + blk2 = blk[:m.start()] + '\n\t\t(dnp %s)' % want + blk[m.end():] + s = s[:a] + blk2 + s[e:] + changes.append(f'{ref}: dnp {m.group(1)} -> {want}') + + # 2a. no-connect flags (skip any already present) + tmpl = re.search(r'\n\t\(no_connect\s*\(at [^)]*\)\s*\(uuid "[^"]+"\)\s*\)', s).group(0) + existing_nc = {(round(float(x), 2), round(float(y), 2)) + for x, y in re.findall(r'\(no_connect\s*\(at (-?[\d.]+) (-?[\d.]+)\)', s)} + add = '' + for x, y in NO_CONNECT: + if (round(x, 2), round(y, 2)) in existing_nc: continue + nb = re.sub(r'\(at [^)]*\)', f'(at {x:g} {y:g})', tmpl, count=1) + nb = UUID.sub(f'(uuid "{uuid.uuid4()}")', nb, count=1) + add += nb; changes.append(f'no_connect @({x:g},{y:g})') + + # 2b. PWR_FLAGs cloned from PF19 + a, e = sym_block(s, 'PF19'); pf = s[a:e] + ox, oy = map(float, re.search(r'\n\t\t\(at (-?[\d.]+) (-?[\d.]+) ', pf).groups()) + for ref, x, y, net in FLAGS: + if re.search(r'\(property "Reference" "%s"\s' % ref, s): continue + nb = pf + def mv(m): + px, py = float(m.group(1)), float(m.group(2)) + return f'(at {px - ox + x:g} {py - oy + y:g}{m.group(3)})' + nb = re.sub(r'\(at (-?[\d.]+) (-?[\d.]+)((?: -?[\d.]+)?)\)', mv, nb) + nb = nb.replace('"PF19"', f'"{ref}"') + nb = UUID.sub(lambda m: f'(uuid "{uuid.uuid4()}")', nb) + add += '\n' + nb; changes.append(f'{ref} PWR_FLAG on /{net} @({x:g},{y:g})') + + if add: + cut = s.rstrip().rfind('\n)') + s = s[:cut] + add + s[cut:] + + # guards + assert balanced(s), 'output not paren-balanced' + uuids1 = UUID.findall(s) + assert uuids0 <= set(uuids1), 'a pre-existing UUID disappeared' + assert len(uuids1) == len(set(uuids1)), 'duplicate UUID' + refs = re.findall(r'\n\t\t\(property "Reference" "([^"#][^"]*)"', s) + dup = {r for r in refs if refs.count(r) > 1} + assert not dup, f'duplicate references: {dup}' + for ref in POPULATE: + a, e = sym_block(s, ref); assert '\n\t\t(dnp no)' in s[a:e], ref + for ref in MAKE_DNP: + a, e = sym_block(s, ref); assert '\n\t\t(dnp yes)' in s[a:e], ref + + print(f'{len(changes)} change(s):'); [print(' ', c) for c in changes] + print(f'UUIDs: {len(uuids0)} kept, {len(uuids1) - len(uuids0)} new, 0 duplicated; paren-balanced') + if not changes: + print('nothing to do (already applied)'); return + if apply: + shutil.copy2(path, path + '.T14bak'); open(path, 'w').write(s); print('APPLIED (backup .T14bak)') + else: + print('DRY RUN — re-run with --apply') + +if __name__ == '__main__': + main()