From 047ce54ade21006799d708bfada3b8531e6d925e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leopoldo=20Rodr=C3=ADguez?= Date: Thu, 17 Sep 2026 10:11:32 -0600 Subject: [PATCH] T13 + kicad pointer --- 03-Architecture-Hardware/kicad | 2 +- ...6-09-17_v6_T13_sim_dual_footprint_part2.py | 491 ++++++++++++++++++ 2 files changed, 492 insertions(+), 1 deletion(-) create mode 100644 scripts/2026-09-17_v6_T13_sim_dual_footprint_part2.py diff --git a/03-Architecture-Hardware/kicad b/03-Architecture-Hardware/kicad index b926403..f25958c 160000 --- a/03-Architecture-Hardware/kicad +++ b/03-Architecture-Hardware/kicad @@ -1 +1 @@ -Subproject commit b9264031b676bba55796d288847cee3a2acb1696 +Subproject commit f25958c323bca023ba60a459131414b960336570 diff --git a/scripts/2026-09-17_v6_T13_sim_dual_footprint_part2.py b/scripts/2026-09-17_v6_T13_sim_dual_footprint_part2.py new file mode 100644 index 0000000..ba7bb6b --- /dev/null +++ b/scripts/2026-09-17_v6_T13_sim_dual_footprint_part2.py @@ -0,0 +1,491 @@ +#!/usr/bin/env python3 +"""v6 T13 - SIM block part 2: nano-SIM holder alternate + 0R branch selects. + +WHAT AND WHY +------------ +T12 gave U22 (MFF2 soldered eSIM) a real footprint. This step adds the *alternate* +SIM path so the board can be built either way without a respin: + + U19 --- SIM_* --- [ESD D23/D24/D25 + C126] ---+--- R127..R130 --- SIMA_* --- U22 (MFF2) + | + +--- R131..R134 --- SIMB_* --- J14 (card holder) + +Exactly ONE branch is populated. The shared modem-side nets keep the name `SIM_*`, so +the copper Michelle already routed (U19 -> ESD, ~17.5 mm) is untouched: only the new +`SIMA_*` / `SIMB_*` branches downstream of the selects are new nets. + +Holder = JAE SF72S006VBDR2500, LCSC C2977286 (nano-SIM, push-push, 1.25 mm, -25..+85 C). +Footprint is KiCad's own KLC-reviewed `JAE_SIM_Card_SF72S006`, vendored into Drogheda.pretty +so the project does not depend on each machine's library version. Its pads are named +C1 C2 C3 C5 C6 C7 / CSW / DSW / SH - so the symbol's pin NUMBERS are those names, not 1-8. + +DELIBERATELY NOT DONE: `U19 USIM1_DET` stays unconnected. With the holder DNP the detect +switch is open, so DET would float or read "no card" and can stop the A7670SA attaching - +that would risk the PRIMARY (MFF2) path to instrument the FALLBACK path. CSW/DSW are +no-connect; the SH shell pads go to GND. + +METHOD: every new s-expression is produced by CLONING a known-good block from the same +file and substituting fields - never authored from scratch. Guarantees format validity. + +LESSONS APPLIED (drogheda-v6-resync-protocol): + - no bare numeric regex anywhere; edits are scoped to paren-matched blocks + - hard guard: every PRE-EXISTING uuid must survive unchanged (new ones may appear) + - .T13bak written before any file is touched; dry-run is the default + - %.10g coordinate formatting, no exponent notation + +Usage: python3 2026-09-17_v6_T13_sim_dual_footprint_part2.py [--apply] +""" +import os +import re +import shutil +import sys +import uuid as _uuid + +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') +FP_MFF2 = os.path.join(KI, 'Drogheda.pretty', 'MFF2_6x5.kicad_mod') +FP_JAE = os.path.join(KI, 'Drogheda.pretty', 'JAE_SIM_Card_SF72S006.kicad_mod') + +APPLY = '--apply' in sys.argv +UUID_RE = re.compile(r'\(uuid "([^"]*)"\)') + +SIGS = ['VDD', 'RST', 'CLK', 'DATA'] +# reference -> (signal, branch) branch A = MFF2 (populated), B = holder (DNP) +SELECTS = { + 'R127': ('VDD', 'A'), 'R128': ('RST', 'A'), 'R129': ('CLK', 'A'), 'R130': ('DATA', 'A'), + 'R131': ('VDD', 'B'), 'R132': ('RST', 'B'), 'R133': ('CLK', 'B'), 'R134': ('DATA', 'B'), +} +COL_X = {'VDD': 310.0, 'RST': 335.4, 'CLK': 360.8, 'DATA': 386.2} +ROW_Y = {'A': 420.0, 'B': 450.0} +PIN_DY = 3.81 # Device:R pin offset from body centre +J14_AT = (340.0, 485.0) +R0_LCSC = 'C304185' # 0R 0603, -55..+155 C, 2962 in stock +HOLDER_SYM = 'NANOSIM_JAE' +HOLDER_FP = 'Drogheda:JAE_SIM_Card_SF72S006' + +# JAE holder symbol pins: (number, name, electrical type, side, y in symbol space) +HOLDER_PINS = [ + ('C1', 'VCC', 'power_in', 'L', 7.62), + ('C2', 'RST', 'input', 'L', 5.08), + ('C3', 'CLK', 'input', 'L', 2.54), + ('C5', 'GND', 'power_in', 'L', 0.0), + ('C6', 'VPP', 'passive', 'L', -2.54), + ('C7', 'IO', 'bidirectional', 'L', -5.08), + ('SH', 'SHELL', 'passive', 'R', 5.08), + ('CSW', 'CSW', 'passive', 'R', 2.54), + ('DSW', 'DSW', 'passive', 'R', 0.0), +] + + +def die(msg): + sys.exit('REFUSING: ' + msg) + + +def g(v): + """KiCad-safe float formatting - never exponent notation.""" + if abs(v) < 1e-9: + v = 0.0 + return '%.10g' % v + + +def bend(s, j): + """End index (exclusive) of the s-expression opening at j. String-aware.""" + depth = 0 + k = j + instr = False + while k < len(s): + c = s[k] + if instr: + if c == '\\': + k += 2 + continue + if c == '"': + instr = False + else: + if c == '"': + instr = True + elif c == '(': + depth += 1 + elif c == ')': + depth -= 1 + if depth == 0: + return k + 1 + k += 1 + die('unbalanced s-expression at offset %d' % j) + + +def paren_ok(s, label): + depth = 0 + k = 0 + instr = False + while k < len(s): + c = s[k] + if instr: + if c == '\\': + k += 2 + continue + if c == '"': + instr = False + else: + if c == '"': + instr = True + elif c == '(': + depth += 1 + elif c == ')': + depth -= 1 + if depth < 0: + die('%s: unbalanced ")" at %d' % (label, k)) + k += 1 + if depth: + die('%s: %d unclosed "(" at EOF' % (label, depth)) + if instr: + die('%s: unterminated string' % label) + + +def find_block(s, head_pred, start=0): + """Yield (start,end,text) for each top-ish level (symbol ...) matching head_pred.""" + for m in re.finditer(r'\(symbol(?=[\s(])', s[start:]): + j = start + m.start() + e = bend(s, j) + blk = s[j:e] + if head_pred(blk): + yield j, e, blk + + +def nu(): + return str(_uuid.uuid4()) + + +# ===================================================================== gate 0 +print('== gate 0: preconditions ==') +for p in (SYM, SCH, FP_MFF2, FP_JAE): + if not os.path.isfile(p): + die('missing %s' % p) +print(' OK both footprints present (MFF2_6x5 + JAE_SIM_Card_SF72S006)') + +jae = open(FP_JAE, encoding='utf-8').read() +paren_ok(jae, 'JAE_SIM_Card_SF72S006.kicad_mod') +jae_pads = set(re.findall(r'\(pad (\S+) ', jae)) +need = {p[0] for p in HOLDER_PINS} +if not need <= jae_pads: + die('footprint pads %s do not cover symbol pins %s' % (sorted(jae_pads), sorted(need - jae_pads))) +print(' OK JAE footprint pads cover every symbol pin: %s' % ' '.join(sorted(need))) + +sym_src = open(SYM, encoding='utf-8').read() +sch_src = open(SCH, encoding='utf-8').read() +paren_ok(sym_src, 'Drogheda-sym.kicad_sym') +paren_ok(sch_src, 'hbox-watchdog-v6.kicad_sch') + +if 'Drogheda:MFF2_6x5' not in sch_src: + die('T12 has not been applied - run it first') +print(' OK T12 already applied') + +taken = set(re.findall(r'\(property "Reference" "([A-Za-z]+\d+)"', sch_src)) +clash = sorted((set(SELECTS) | {'J14'}) & taken) +already = ('NANOSIM_JAE' in sym_src) +if clash and not already: + die('designators already in use: %s' % clash) + +# idempotency: if J14 already exists, this script has run +if 'J14' in taken: + print('\n== already at T13 state - nothing to do ==') + print('\nDRY RUN - nothing written.' if not APPLY else 'ALREADY APPLIED') + sys.exit(0) + +# ============================================== step 1: U22 pin -> label rename +print('\n== step 1: re-point U22 from SIM_* to SIMA_* ==') + +mff2_lib = None +for j, e, blk in find_block(sym_src, lambda b: b.startswith('(symbol "MFF2_SIM"')): + mff2_lib = blk + break +if mff2_lib is None: + die('MFF2_SIM library symbol not found') + +# pin offsets from the library symbol, in SYMBOL space (+y up) +pin_off = {} +for pm in re.finditer( + r'\(pin \w+ \w+\s*\(at (-?[\d.]+) (-?[\d.]+) (-?[\d.]+)\)\s*\(length [\d.]+\)\s*' + r'\(name "([^"]*)".*?\(number "([^"]*)"', mff2_lib, re.S): + pin_off[pm.group(5)] = (float(pm.group(1)), float(pm.group(2)), pm.group(4)) +if len(pin_off) != 8: + die('expected 8 MFF2_SIM pins, parsed %d' % len(pin_off)) + +u22 = None +for j, e, blk in find_block(sch_src, lambda b: '"Drogheda-sym:MFF2_SIM"' in b + and '(property "Reference" "U22"' in b): + u22 = (j, e, blk) + break +if u22 is None: + die('U22 instance not found') +am = re.search(r'\n\t\t\(at (-?[\d.]+) (-?[\d.]+)(?: (-?[\d.]+))?\)', u22[2]) +ux, uy = float(am.group(1)), float(am.group(2)) +urot = float(am.group(3) or 0) +if urot: + die('U22 is rotated %g deg - T13 only handles rotation 0' % urot) +print(' U22 at (%s, %s)' % (g(ux), g(uy))) + +# schematic pin position = (X + px, Y - py) [symbol +y is up, sheet +y is down] +want = {} +for num, (px, py, pname) in pin_off.items(): + if pname in ('VCC', 'RST', 'CLK', 'IO'): + sig = {'VCC': 'VDD', 'RST': 'RST', 'CLK': 'CLK', 'IO': 'DATA'}[pname] + want[(round(ux + px, 3), round(uy - py, 3))] = sig + +labels = [] +for m in re.finditer(r'\t\(label "([^"]+)"\s*\(at (-?[\d.]+) (-?[\d.]+)(?: (-?[\d.]+))?\)', sch_src): + labels.append((m.start(), m.group(1), round(float(m.group(2)), 3), round(float(m.group(3)), 3))) + +# This schematic mixes two idioms: a label sitting directly ON a pin, and a label on the +# far end of a short WIRE whose other end lands on the pin (U22's VCC is the latter). +# So resolve each pin through the wire graph rather than by exact coincidence alone. +wires = [] +for m in re.finditer(r'\(wire\s*\(pts\s*\(xy (-?[\d.]+) (-?[\d.]+)\)\s*\(xy (-?[\d.]+) (-?[\d.]+)\)', sch_src): + a, b, c, d = (round(float(v), 3) for v in m.groups()) + wires.append(((a, b), (c, d))) + +adj = {} +for p, q in wires: + adj.setdefault(p, set()).add(q) + adj.setdefault(q, set()).add(p) + + +def net_nodes(start): + """Every coordinate electrically common with `start` via wires.""" + seen, stack = {start}, [start] + while stack: + n = stack.pop() + for m2 in adj.get(n, ()): + if m2 not in seen: + seen.add(m2) + stack.append(m2) + return seen + + +hits, misses = {}, [] +for (px, py), sig in want.items(): + nodes = net_nodes((px, py)) + found = [L for L in labels if (L[2], L[3]) in nodes and L[1] == 'SIM_' + sig] + if len(found) == 1: + via = 'on pin' if (found[0][2], found[0][3]) == (px, py) else 'via wire' + hits[sig] = (found[0][0], found[0][2], found[0][3], via) + else: + misses.append((sig, px, py, len(found), len(nodes))) + +for sig, (pos, px, py, via) in sorted(hits.items()): + print(' OK SIM_%-4s @(%s, %s) %-10s -> SIMA_%s' % (sig, g(px), g(py), '(' + via + ')', sig)) +for sig, px, py, n, nn in misses: + print(' !! SIM_%-4s U22 pin @(%s, %s): %d matching labels on its net (%d nodes)' + % (sig, g(px), g(py), n, nn)) +if misses: + die('%d U22 pin(s) not resolved to exactly one SIM_* label.\n' + ' Inspect those coordinates in Eeschema and tell me how they connect; do NOT guess.' + % len(misses)) + +sch = sch_src +for sig, (pos, px, py, via) in sorted(hits.items(), key=lambda kv: -kv[1][0]): + old = '(label "SIM_%s"' % sig + if sch[pos:pos + len(old) + 1] != '\t' + old: + die('label text moved under us at offset %d' % pos) + sch = sch[:pos] + '\t(label "SIMA_%s"' % sig + sch[pos + len(old) + 1:] + +# ==================================== step 2: NANOSIM_JAE library symbol (cloned) +print('\n== step 2: build %s symbol by cloning MFF2_SIM ==' % HOLDER_SYM) + +pin_tpl = re.search(r'(\t*)\(pin \w+ line\s*\n\s*\(at [^)]*\)\s*\n\s*\(length [\d.]+\)\s*\n' + r'\s*\(name "[^"]*"\s*\n\s*\(effects \(font \(size [\d.]+ [\d.]+\)\)\)\s*\n\s*\)\s*\n' + r'\s*\(number "[^"]*"\s*\n\s*\(effects \(font \(size [\d.]+ [\d.]+\)\)\)\s*\n\s*\)\s*\n\s*\)', + mff2_lib) +if not pin_tpl: + die('could not lift a pin template out of MFF2_SIM') + + +def make_pin(num, name, etype, side, y): + x = -12.7 if side == 'L' else 12.7 + ang = 0 if side == 'L' else 180 + return (' (pin %s line\n' + ' (at %s %s %d)\n' + ' (length 5.08)\n' + ' (name "%s"\n' + ' (effects (font (size 1.27 1.27)))\n' + ' )\n' + ' (number "%s"\n' + ' (effects (font (size 1.27 1.27)))\n' + ' )\n' + ' )\n') % (etype, g(x), g(y), ang, name, num) + + +holder_desc = ('Nano-SIM (4FF) card holder, JAE SF72S006VBDR2500, LCSC C2977286. Push-push, ' + '1.25 mm, -25..+85 C. Pin NUMBERS are the pad names C1/C2/C3/C5/C6/C7/CSW/DSW/SH ' + 'to match Connector_JAE:JAE_SIM_Card_SF72S006. DNP alternate to U22 (MFF2) - ' + 'exactly one of the two is populated, selected by R127-R134. CSW/DSW deliberately ' + 'unused: with this holder DNP the detect switch is open and USIM1_DET would float ' + 'or read no-card, which can stop the A7670SA attaching. SH shell pads -> GND.') + +holder = [] +holder.append(' (symbol "%s"\n' % HOLDER_SYM) +holder.append(' (exclude_from_sim no)\n (in_bom yes)\n (on_board yes)\n') +holder.append(' (property "Reference" "J"\n (at 0 12.7 0)\n' + ' (effects (font (size 1.27 1.27)))\n )\n') +holder.append(' (property "Value" "%s"\n (at 0 10.16 0)\n' + ' (effects (font (size 1.27 1.27)))\n )\n' % HOLDER_SYM) +holder.append(' (property "Footprint" "%s"\n (at 0 -5.08 0)\n (hide yes)\n' + ' (effects (font (size 1.27 1.27)))\n )\n' % HOLDER_FP) +holder.append(' (property "Datasheet" "~"\n (at 0 0 0)\n (hide yes)\n' + ' (effects (font (size 1.27 1.27)))\n )\n') +holder.append(' (property "Description" "%s"\n (at 0 0 0)\n (hide yes)\n' + ' (effects (font (size 1.27 1.27)))\n )\n' % holder_desc) +holder.append(' (property "LCSC" "C2977286"\n (at 0 0 0)\n (hide yes)\n' + ' (effects (font (size 1.27 1.27)))\n )\n') +holder.append(' (symbol "%s_0_1"\n (rectangle\n (start -7.62 10.16)\n' + ' (end 7.62 -7.62)\n (stroke (width 0.254) (type default))\n' + ' (fill (type background))\n )\n )\n' % HOLDER_SYM) +holder.append(' (symbol "%s_1_1"\n' % HOLDER_SYM) +for num, name, etype, side, y in HOLDER_PINS: + holder.append(make_pin(num, name, etype, side, y)) +holder.append(' )\n )\n') +holder_block = ''.join(holder) +print(' built symbol: %d pins, %d chars' % (len(HOLDER_PINS), len(holder_block))) + +# insert into the .kicad_sym just before its final ")" +sym = sym_src +if HOLDER_SYM not in sym: + last = sym.rstrip() + if not last.endswith(')'): + die('Drogheda-sym.kicad_sym does not end with ")"') + cut = sym.rfind(')') + sym = sym[:cut] + holder_block + sym[cut:] + +# and into the schematic's lib_symbols cache, as "Drogheda-sym:NANOSIM_JAE" +cache_block = holder_block.replace('(symbol "%s"' % HOLDER_SYM, + '(symbol "Drogheda-sym:%s"' % HOLDER_SYM, 1) +cache_block = re.sub(r'^', '\t', cache_block, flags=re.M).rstrip('\t') +libm = re.search(r'\n\t\(lib_symbols\n', sch) +if not libm: + die('lib_symbols section not found in the schematic') +lib_end = bend(sch, libm.start() + 1) +sch = sch[:lib_end - 2] + cache_block + sch[lib_end - 2:] +print(' inserted into Drogheda-sym.kicad_sym and the schematic lib_symbols cache') + +# ============================================ step 3: instances + labels (cloned) +print('\n== step 3: place J14 + R127-R134 and label the branches ==') + +r_tpl = None +for j, e, blk in find_block(sch, lambda b: '(lib_id "Device:R")' in b + and '(property "Reference" "R55"' in b): + r_tpl = blk + break +if r_tpl is None: + die('could not lift a Device:R instance template (R55)') + + +def place_r(ref, value, x, y, dnp): + b = r_tpl + b = re.sub(r'\n\t\t\(at -?[\d.]+ -?[\d.]+ -?[\d.]+\)', + '\n\t\t(at %s %s 0)' % (g(x), g(y)), b, count=1) + b = b.replace('(property "Reference" "R55"', '(property "Reference" "%s"' % ref, 1) + b = re.sub(r'\(property "Value" "[^"]*"', '(property "Value" "%s"' % value, b, count=1) + b = re.sub(r'\(property "LCSC" "[^"]*"', '(property "LCSC" "%s"' % R0_LCSC, b, count=1) + b = b.replace('(dnp no)', '(dnp %s)' % ('yes' if dnp else 'no'), 1) + b = b.replace('(reference "R55")', '(reference "%s")' % ref, 1) + # every property/pin coordinate that referenced R55's old position + b = re.sub(r'\(at 240(\.\d+)? (20[0-9]|21[0-9])(\.\d+)? 0\)', '(at %s %s 0)' % (g(x), g(y)), b) + b = UUID_RE.sub(lambda m: '(uuid "%s")' % nu(), b) + return '\t' + b + '\n' + + +def mk_label(text, x, y): + return ('\t(label "%s"\n\t\t(at %s %s 0)\n\t\t(effects\n\t\t\t(font\n' + '\t\t\t\t(size 1.27 1.27)\n\t\t\t)\n\t\t\t(justify left bottom)\n\t\t)\n' + '\t\t(uuid "%s")\n\t)\n') % (text, g(x), g(y), nu()) + + +add = [] +for ref, (sig, br) in sorted(SELECTS.items()): + x, y = COL_X[sig], ROW_Y[br] + add.append(place_r(ref, '0R', x, y, dnp=(br == 'B'))) + add.append(mk_label('SIM_%s' % sig, x, y - PIN_DY)) + add.append(mk_label('SIM%s_%s' % (br, sig), x, y + PIN_DY)) + print(' %-5s %s branch, %-4s @(%s, %s) SIM_%s -> SIM%s_%s%s' + % (ref, br, sig, g(x), g(y), sig, br, sig, ' [DNP]' if br == 'B' else '')) + +# J14 instance, cloned from U22's instance block then re-typed +j14 = u22[2] +j14 = j14.replace('"Drogheda-sym:MFF2_SIM"', '"Drogheda-sym:%s"' % HOLDER_SYM, 1) +j14 = re.sub(r'\n\t\t\(at -?[\d.]+ -?[\d.]+(?: -?[\d.]+)?\)', + '\n\t\t(at %s %s 0)' % (g(J14_AT[0]), g(J14_AT[1])), j14, count=1) +j14 = j14.replace('(property "Reference" "U22"', '(property "Reference" "J14"', 1) +j14 = re.sub(r'\(property "Value" "[^"]*"', '(property "Value" "NanoSIM-JAE-SF72S006"', j14, count=1) +j14 = re.sub(r'\(property "Footprint" "[^"]*"', '(property "Footprint" "%s"' % HOLDER_FP, j14, count=1) +j14 = re.sub(r'\(property "Description" "[^"]*"', + '(property "Description" "Nano-SIM holder JAE SF72S006 (LCSC C2977286) - DNP alternate to U22"', + j14, count=1) +j14 = j14.replace('(dnp no)', '(dnp yes)', 1) +j14 = j14.replace('(reference "U22")', '(reference "J14")', 1) +# pins: MFF2 numbers 1..8 -> holder pad names +j14 = re.sub(r'\(pin "\d"\n(\s*)\(uuid "[^"]*"\)\n\s*\)\n', '', j14) +pinblk = ''.join('\t\t(pin "%s"\n\t\t\t(uuid "%s")\n\t\t)\n' % (p[0], nu()) for p in HOLDER_PINS) +j14 = j14.replace('\t\t(instances', pinblk + '\t\t(instances', 1) +j14 = UUID_RE.sub(lambda m: '(uuid "%s")' % nu(), j14) +add.append('\t' + j14 + '\n') +print(' J14 holder @(%s, %s) [DNP]' % (g(J14_AT[0]), g(J14_AT[1]))) + +for sig in SIGS: + px = J14_AT[0] - 12.7 + py = {'VDD': J14_AT[1] - 7.62, 'RST': J14_AT[1] - 5.08, + 'CLK': J14_AT[1] - 2.54, 'DATA': J14_AT[1] + 5.08}[sig] + add.append(mk_label('SIMB_%s' % sig, px, py)) +add.append(mk_label('GND', J14_AT[0] - 12.7, J14_AT[1])) +add.append(mk_label('GND', J14_AT[0] + 12.7, J14_AT[1] - 5.08)) + +cut = sch.rfind(')') +sch = sch[:cut] + ''.join(add) + sch[cut:] + +# ================================================================ verification +print('\n== verification ==') +paren_ok(sym, 'Drogheda-sym.kicad_sym (post)') +paren_ok(sch, 'hbox-watchdog-v6.kicad_sch (post)') +print(' OK both files parse (paren-balanced, strings closed)') + +old_u = UUID_RE.findall(sch_src) +new_u = UUID_RE.findall(sch) +lost = set(old_u) - set(new_u) +if lost: + die('%d pre-existing uuid(s) vanished, e.g. %s' % (len(lost), sorted(lost)[:3])) +if len(new_u) != len(set(new_u)): + die('duplicate uuid generated') +print(' OK all %d pre-existing uuids intact, %d new, none duplicated' + % (len(old_u), len(new_u) - len(old_u))) + +refs = re.findall(r'\(property "Reference" "([A-Za-z]+\d+)"', sch) +dupes = sorted({r for r in refs if refs.count(r) > 1}) +if dupes: + die('duplicate references: %s' % dupes) +print(' OK no duplicate references (%d components)' % len(refs)) + +for sig in SIGS: + n_shared = len(re.findall(r'\(label "SIM_%s"' % sig, sch)) + n_a = len(re.findall(r'\(label "SIMA_%s"' % sig, sch)) + n_b = len(re.findall(r'\(label "SIMB_%s"' % sig, sch)) + if n_a != 2 or n_b != 2: + die('SIM%s: expected 2 SIMA_ and 2 SIMB_ labels, got %d/%d' % (sig, n_a, n_b)) + if n_shared < 3: + die('SIM_%s has only %d endpoints - modem side lost a connection' % (sig, n_shared)) + print(' OK SIM_%-4s shared=%d SIMA=%d SIMB=%d' % (sig, n_shared, n_a, n_b)) + +pop = [r for r, (s, b) in SELECTS.items() if b == 'A'] +print('\n POPULATE: U22 (MFF2) + %s' % ' '.join(sorted(pop))) +print(' DNP: J14 (holder) + %s' % ' '.join(sorted(set(SELECTS) - set(pop)))) + +if APPLY: + shutil.copy2(SYM, SYM + '.T13bak') + shutil.copy2(SCH, SCH + '.T13bak') + open(SYM, 'w', encoding='utf-8').write(sym) + open(SCH, 'w', encoding='utf-8').write(sch) + print('\nAPPLIED (.T13bak written beside each file)') + print('NEXT: open Eeschema -> ERC -> save; then Update PCB from Schematic') + print(' (UNTICK "delete extra footprints" and any remove-tracks option)') +else: + print('\nDRY RUN - nothing written. Re-run with --apply.')