Drogheda/scripts/2026-08-24_v6_T2_k1_to_pc6.py
Claude (Cowork) d4795539e7 v6 T0-T2: fork hbox-watchdog-v6 from v5.5; delete MOD1/J7/J8/SW4G1; K1 pin10 PA9 -> PC6
T0  hbox-watchdog-v6.{sch,pcb,pro,dru} forked from v5.5 @ e6ac8cc (new files on main,
    same precedent as v4 -> v5). All internal project refs renamed, paren-balanced.
T1  Deleted the simpleRTK2B Pro socket (MOD1), both XBee sockets (J7/J8) and the modem
    reset button (SW4G1) from schematic and PCB, plus the 25 on-pin labels that carried
    their connectivity. The schematic has ZERO wires - connectivity is labels on pins -
    so no wire/junction surgery was needed.
T2  Relay K1 pin 10 (NO contact) moves from PA9 /USART1_TX to PC6 /USART6_TX, giving the
    MCU ownership of the modem RX line while K1 de-energised still feeds the receiver's
    GGA straight to the modem (MCU-dead fail-safe). PA9 freed -> no_connect. USART6_RX
    added on PC7. Retired /USART1_TX copper ripped (7 items).

Verified: /USART6_TX = U16.37 + K1.10 | /USART1_RX = U16.43 + Rdfu1.1 + K1.1 | PA9 free.
Nets awaiting T3/T4 silicon: /DOUT_4G, /MODEM_RST, /GNSS_TX, /GNSS_RX, /USART6_RX.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ4eAB6zjQT49RVwpgfKdt
2026-08-24 22:51:21 +00:00

135 lines
5.6 KiB
Python

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