#!/usr/bin/env python3 """v6 T1 - delete MOD1, J7, J8, SW4G1 from hbox-watchdog-v6 (sch symbols + on-pin labels, pcb footprints). The v5/v6 schematic has ZERO wires: connectivity is 100% labels placed on pin coordinates. So deleting a symbol = delete its (symbol ...) block + the (label ...) blocks sitting on its pins. Usage: python3 2026-08-24_v6_T1_delete_sockets.py [--apply] Default is a DRY RUN that prints exactly what would be removed. """ import re, sys, os, shutil KI = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '03-Architecture-Hardware', 'kicad') SCH = os.path.join(KI, 'hbox-watchdog-v6.kicad_sch') PCB = os.path.join(KI, 'hbox-watchdog-v6.kicad_pcb') APPLY = '--apply' in sys.argv REFS = ['MOD1', 'J7', 'J8', 'SW4G1'] # (name, x, y) of every label sitting on a pin of a deleted symbol - surveyed 2026-08-24 LABELS = [ # MOD1 (simpleRTK2B Pro socket) ('3V3', 484.378, 94.488), ('SW_5V', 484.378, 86.868), ('GND', 484.378, 99.568), ('GND', 484.378, 102.108), ('GND', 484.378, 104.648), ('GNSS_RX', 453.898, 86.868), ('GNSS_TX', 453.898, 89.408), ('DOUT_4G', 453.898, 91.948), ('USART1_RX', 453.898, 94.488), # J7 (XBee row 1) ('3V3_MODEM', 398.272, 39.116), ('DOUT_4G', 398.272, 41.656), ('DIN_4G', 398.272, 44.196), ('GND', 398.272, 46.736), ('MODEM_RST', 398.272, 49.276), ('USB_VBUS', 398.272, 51.816), ('USB_DP', 398.272, 54.356), ('USB_DM', 398.272, 56.896), ('GND', 398.272, 61.976), # J8 (XBee row 2) ('GND', 420.878, 39.624), ('GND', 420.878, 52.324), ('3V3_MODEM', 420.878, 54.864), ('MODEM_LED', 420.198, 57.334), ('GND', 420.878, 62.484), # SW4G1 (modem reset button) ('MODEM_RST', 17.780, 276.860), ('GND', 40.640, 275.590), ] def spans(text, opener): """Yield (start, end) byte spans of every top-level s-expression beginning with `opener`.""" out = [] for m in re.finditer(re.escape(opener), text): i = m.start(); depth = 0; instr = False; esc = False; j = i while j < len(text): ch = text[j] if esc: esc = False elif ch == '\\': esc = True elif ch == '"': instr = not instr elif not instr: if ch == '(': depth += 1 elif ch == ')': depth -= 1 if depth == 0: out.append((i, j + 1)); break j += 1 return out def cut(text, spans_to_cut): for a, b in sorted(spans_to_cut, reverse=True): # also swallow a single trailing newline + leading tabs so the file stays tidy while a > 0 and text[a-1] in '\t': a -= 1 if a > 0 and text[a-1] == '\n': a -= 1 text = text[:a] + text[b:] return text # ---------------- schematic ---------------- sch = open(SCH, encoding='utf-8').read() kill, report = [], [] for a, b in spans(sch, '\n\t(symbol\n'): blk = sch[a:b] m = re.search(r'\(property "Reference" "([^"]+)"', blk) if m and m.group(1) in REFS: lib = re.search(r'\(lib_id "([^"]+)"', blk) kill.append((a, b)); report.append(f" symbol {m.group(1):<8} {lib.group(1) if lib else '?':<44} {b-a:>6} B") lab_spans = [] for kind in ('(label ', '(global_label ', '(hierarchical_label '): lab_spans += spans(sch, '\n\t' + kind) found = set() for a, b in lab_spans: blk = sch[a:b] m = re.match(r'\s*\((?:label|global_label|hierarchical_label) "([^"]+)"\s*\n\s*\(at ([-\d.]+) ([-\d.]+)', blk) if not m: continue name, x, y = m.group(1), float(m.group(2)), float(m.group(3)) for (ln, lx, ly) in LABELS: if name == ln and abs(x-lx) < 0.01 and abs(y-ly) < 0.01: kill.append((a, b)); found.add((ln, lx, ly)) report.append(f" label {name:<14} ({x}, {y})") break missing = [l for l in LABELS if l not in found] print(f"--- SCHEMATIC {os.path.basename(SCH)} ---") print("\n".join(report)) print(f" => {len(kill)} blocks ({len(REFS)} symbols + {len(found)}/{len(LABELS)} labels)") if missing: print(" !! LABELS NOT FOUND (investigate before applying):") for l in missing: print(" ", l) new_sch = cut(sch, kill) # ---------------- pcb ---------------- pcb = open(PCB, encoding='utf-8').read() pkill, preport = [], [] for a, b in spans(pcb, '\n\t(footprint '): blk = pcb[a:b] m = re.search(r'\(property "Reference" "([^"]+)"', blk) if m and m.group(1) in REFS: npad = len(re.findall(r'\n\t\t\(pad ', blk)) pkill.append((a, b)); preport.append(f" footprint {m.group(1):<8} pads={npad:<3} {b-a:>7} B") print(f"\n--- PCB {os.path.basename(PCB)} ---") print("\n".join(preport)) print(f" => {len(pkill)} footprints") new_pcb = cut(pcb, pkill) # nets that lose members -> report orphaned copper for the layout phase orphan_nets = ['/DOUT_4G', '/DIN_4G', '/MODEM_RST', '/MODEM_LED', '/3V3_MODEM', '/USB_VBUS', '/USB_DP', '/USB_DM', '/GNSS_RX', '/GNSS_TX', '/USART1_RX', '/SW_5V'] print("\n--- copper still on affected nets (clean up in the layout phase) ---") for n in orphan_nets: seg = len(re.findall(r'\(segment(?:(?!\(segment).)*?\(net "' + re.escape(n) + r'"\)', new_pcb, re.S)) via = len(re.findall(r'\(via(?:(?!\(via).)*?\(net "' + re.escape(n) + r'"\)', new_pcb, re.S)) print(f" {n:<14} segments={seg:<4} vias={via}") def balanced(t): d = 0; instr = False; esc = False for ch in t: if esc: esc = False; continue if ch == '\\': esc = True; continue if ch == '"': instr = not instr; continue if instr: continue if ch == '(': d += 1 elif ch == ')': d -= 1 if d < 0: return None return d print(f"\nparen balance sch={balanced(new_sch)} pcb={balanced(new_pcb)}") if not APPLY: print("\nDRY RUN - nothing written. Re-run with --apply to commit the edit.") sys.exit(0) if missing: sys.exit("REFUSING to apply: some labels were not found.") if balanced(new_sch) != 0 or balanced(new_pcb) != 0: sys.exit("REFUSING to apply: paren imbalance.") shutil.copy2(SCH, SCH + '.T1bak'); shutil.copy2(PCB, PCB + '.T1bak') open(SCH, 'w', encoding='utf-8').write(new_sch) open(PCB, 'w', encoding='utf-8').write(new_pcb) print("\nAPPLIED. Backups: *.T1bak")