#!/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()