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
135 lines
5.6 KiB
Python
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')
|