#!/usr/bin/env python3 """v6 T2 - move K1 pin 10 (relay NO) from PA9 /USART1_TX to PC6 /USART6_TX. This is the one-net change that lets the MCU own the modem's RX line (AT commands, byte-exact GGA forwarding, $PHLG injection) while keeping the MCU-dead fail-safe: de-energised, K1's NC contact still feeds the receiver's GGA straight to the modem. Schematic (label-only connectivity): - rename USART1_TX @ (513.588, 182.372) -> USART6_TX [K1 pin 10] - delete USART1_TX @ (518.16, 416.56) + add a no_connect [U16 PA9, now free] - add USART6_TX @ (482.6, 431.8) [U16 PC6] - add USART6_RX @ (482.6, 434.34) [U16 PC7] PCB: - K1 pad 10 /USART1_TX -> /USART6_TX - U16 pad 37 -> /USART6_TX ; pad 38 -> /USART6_RX ; pad 42 -> unconnected - rip all copper on the retired /USART1_TX net (it joined PA9 to K1.10) Usage: python3 2026-08-24_v6_T2_k1_to_pc6.py [--apply] """ import re, sys, os, shutil, uuid 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 def span_from(text, i): d = 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 == '(': d += 1 elif ch == ')': d -= 1 if d == 0: return j + 1 j += 1 raise ValueError('unbalanced') def find_label(text, name, x, y): for m in re.finditer(r'\(label "' + re.escape(name) + r'"\s*\n\s*\(at ([-\d.]+) ([-\d.]+) (\d+)\)', text): if abs(float(m.group(1)) - x) < 0.01 and abs(float(m.group(2)) - y) < 0.01: return m.start(), span_from(text, m.start()), int(m.group(3)) return None def mklabel(name, x, y, rot, justify): return ('\n\t(label "%s"\n\t\t(at %s %s %d)\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 %s)\n\t\t)\n\t\t(uuid "%s")\n\t)') % (name, x, y, rot, justify, uuid.uuid4()) def mknc(x, y): return '\n\t(no_connect\n\t\t(at %s %s)\n\t\t(uuid "%s")\n\t)' % (x, y, uuid.uuid4()) sch = open(SCH, encoding='utf-8').read() log = [] # 1. K1 pin 10 label -> USART6_TX hit = find_label(sch, 'USART1_TX', 513.588, 182.372) if not hit: sys.exit('K1.10 USART1_TX label not found') a, b, rot = hit sch = sch[:a] + sch[a:b].replace('"USART1_TX"', '"USART6_TX"', 1) + sch[b:] log.append(' rename USART1_TX -> USART6_TX @ (513.588, 182.372) [K1 pin 10 / relay NO]') # 2. U16 PA9 label -> gone, no_connect in its place hit = find_label(sch, 'USART1_TX', 518.16, 416.56) if not hit: sys.exit('U16 PA9 USART1_TX label not found') a, b, rot = hit sch = sch[:a] + mknc('518.16', '416.56') + sch[b:] log.append(' delete USART1_TX @ (518.16, 416.56) -> no_connect [U16 PA9, freed -> test point later]') # 3+4. new labels on PC6 / PC7 (left column of U16: rot 180, justify right bottom) anchor = sch.find('\n\t(label "ETH_INT"') ins = span_from(sch, anchor + 1) add = mklabel('USART6_TX', '482.6', '431.8', 180, 'right bottom') + \ mklabel('USART6_RX', '482.6', '434.34', 180, 'right bottom') sch = sch[:ins] + add + sch[ins:] log.append(' add USART6_TX @ (482.6, 431.8) [U16 PC6 / USART6_TX]') log.append(' add USART6_RX @ (482.6, 434.34) [U16 PC7 / USART6_RX]') # ---------------- PCB ---------------- pcb = open(PCB, encoding='utf-8').read() plog = [] def set_pad_net(text, ref, pad, newnet): m = re.search(r'\n\t\(footprint (?:(?!\n\t\(footprint ).)*?\(property "Reference" "' + re.escape(ref) + r'"(?:(?!\n\t\(footprint ).)*', text, re.S) if not m: raise ValueError('footprint %s not found' % ref) seg = m.group(0) pm = re.search(r'\(pad "' + re.escape(pad) + r'"(?:(?!\(pad ").)*?\(net "([^"]*)"\)', seg, re.S) if not pm: raise ValueError('pad %s.%s not found' % (ref, pad)) old = pm.group(1) newseg = seg[:pm.start(1)] + newnet + seg[pm.end(1):] plog.append(' %-5s pad %-3s %-34s -> %s' % (ref, pad, old, newnet)) return text[:m.start()] + newseg + text[m.end():] pcb = set_pad_net(pcb, 'K1', '10', '/USART6_TX') pcb = set_pad_net(pcb, 'U16', '37', '/USART6_TX') pcb = set_pad_net(pcb, 'U16', '38', '/USART6_RX') pcb = set_pad_net(pcb, 'U16', '42', 'unconnected-(U16-PA9-Pad42)') # rip retired /USART1_TX copper cuts = [] for kw in ('\n\t(segment', '\n\t(via'): for m in re.finditer(re.escape(kw), pcb): e = span_from(pcb, m.start() + 1) if '(net "/USART1_TX")' in pcb[m.start():e]: cuts.append((m.start(), e)) for a, b in sorted(cuts, reverse=True): pcb = pcb[:a] + pcb[b:] plog.append(' ripped %d copper items on the retired /USART1_TX net' % len(cuts)) print('--- SCHEMATIC ---'); print('\n'.join(log)) print('--- PCB ---'); print('\n'.join(plog)) def bal(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 return d print('\nparen balance sch=%s pcb=%s' % (bal(sch), bal(pcb))) if not APPLY: print('\nDRY RUN - nothing written. Re-run with --apply.') sys.exit(0) if bal(sch) or bal(pcb): sys.exit('REFUSING: paren imbalance') shutil.copy2(SCH, SCH + '.T2bak'); shutil.copy2(PCB, PCB + '.T2bak') open(SCH, 'w', encoding='utf-8').write(sch) open(PCB, 'w', encoding='utf-8').write(pcb) print('\nAPPLIED. Backups: *.T2bak')