Drogheda/scripts/2026-08-24_v6_T3b_block_n.py
Claude (Cowork) 3494413eaf v6 T3: Block N - ZED-F9P (U18) + low-noise LDO (U21) placed and wired
Leo generated the vendor part with easyeda2kicad (C5120257) -> Drogheda-v6.kicad_sym /
.pretty / .3dshapes, now registered in sym-lib-table and fp-lib-table.

CROSS-CHECK: the EasyEDA/LCSC symbol independently CONFIRMS the datasheet transcription -
pads 1-54 match the u-blox pin assignment function for function, and the 21 Reserved pads
are identical. Pads 55-102 are 48 extra GND pads (the ground array under the module), which
our symbol was missing; T3a2 adds them stacked on the pin-1 coordinate so one GND label
serves all. Symbol is now 102/102 pins, footprint = Drogheda-v6:LCC-LGA-102_L22.0-W17.0_FM660.

Block N as wired (verified by rebuilding connectivity from label-on-pin geometry):
  GNSS_TX      U16.16 + U18.43        GNSS_RX  U16.17 + U18.42
  DOUT_4G      U18.26 (RXD2, RTCM in - modem end lands in T4)
  USART1_RX    U16.43 + Rdfu1.1 + U18.27 (TXD2 uplink; K1.1 also on this net per the PCB)
  GNSS_RESET_N U16.53 (PC12) + U18.49 + R105 10k pull-up to 3V3_GNSS
  GNSS_TIMEPULSE  U16.23 (PA7) + U18.53
  GNSS_ANT_DETECT U16.9 (PC1) + U18.4
  GNSS_ANT_SHORT_N U16.10 (PC2) + U18.6
  GNSS_RTK_STAT   U16.11 (PC3) + U18.20  <- hardware answer to are-corrections-being-used
  3V3_GNSS = U21.5 + U18.33/34 + C120-C124 + R105; U21 fed from SW_5V (stays MCU-gated)
  V_BCKP   = U18.36 on the UNGATED 3V3 so ephemeris survives a power-cycle
  33 no-connects: the 21 reserved pads plus V_USB, USB_DM/DP, ANT_OFF, SDA, SCL, TX_READY,
  D_SEL (open = UART mode), SAFEBOOT_N, EXTINT, GEOFENCE_STAT and the LDO NC/NR pin.

T3c fixes two coordinates the generators emitted as 3.552713679e-15 (i.e. zero); KiCad is
not guaranteed to accept exponent notation. See the script header for the lesson: a first
attempt used a bare numeric regex over the whole file and corrupted 264 UUIDs, because
segments such as 895e-4153 parse as scientific notation. Reverted from backup and redone
scoped to (at ...) forms only, with a UUID-integrity guard. Verified 2298 UUIDs, 0
malformed, 0 exponent coordinates.

STILL OPEN: the PCB has no U18/U21/passive footprints yet - that is a KiCad
Update-PCB-from-Schematic. And confirm the -04B datasheet pin assignment on paper.

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

191 lines
8.2 KiB
Python

#!/usr/bin/env python3
"""v6 T3b - Block N: place U18 (ZED-F9P) + U21 (LDO) and wire the GNSS island.
The schematic is label-only (zero wires), so "wiring" = placing labels on pin coordinates.
Sheet coords: x = X + px , y = Y - py (py is the symbol-internal Y, which points up).
Usage: python3 2026-08-24_v6_T3b_block_n.py [--apply]
"""
import os, sys, shutil, uuid, re
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')
LIB = os.path.join(KI, 'Drogheda-sym.kicad_sym')
APPLY = '--apply' in sys.argv
ROOT = '/561403b3-895e-4153-a7be-3876ae930d43'
U18X, U18Y = 470.0, 120.0 # ZED-F9P
U21X, U21Y = 390.0, 110.0 # LDO
P = 2.54
LEFT = [33,34,36,38,7,2,4,5,6,1,3,12,14,32,37,41,48]
RIGHT = [42,43,26,27,44,45,39,40,46,47,49,50,51,53,19,20]
RSVD = [8,9,10,11,13,15,16,17,18,21,22,23,24,25,28,29,30,31,35,52,54]
NC = '<NC>'
NET_L = {33:'3V3_GNSS', 34:'3V3_GNSS', 36:'3V3', 38:NC, 7:'VCC_RF_GNSS', 2:'RF_GNSS',
4:'GNSS_ANT_DETECT', 5:NC, 6:'GNSS_ANT_SHORT_N',
1:'GND', 3:'GND', 12:'GND', 14:'GND', 32:'GND', 37:'GND', 41:'GND', 48:'GND'}
NET_R = {42:'GNSS_RX', 43:'GNSS_TX', 26:'DOUT_4G', 27:'USART1_RX',
44:NC, 45:NC, 39:NC, 40:NC, 46:NC, 47:NC,
49:'GNSS_RESET_N', 50:NC, 51:NC, 53:'GNSS_TIMEPULSE', 19:NC, 20:'GNSS_RTK_STAT'}
# MCU-side labels: (net, x, y, rot, justify)
MCU = [('GNSS_ANT_DETECT', 482.6, 419.10, 180, 'right bottom'), # PC1
('GNSS_ANT_SHORT_N', 482.6, 421.64, 180, 'right bottom'), # PC2
('GNSS_RTK_STAT', 482.6, 424.18, 180, 'right bottom'), # PC3
('GNSS_RESET_N', 482.6, 447.04, 180, 'right bottom'), # PC12
('GNSS_TIMEPULSE', 518.16, 411.48, 0, 'left bottom')] # PA7
# passives: ref, value, x, y, pin1 net, pin2 net, footprint, lib
PASSIVES = [
('C119','10uF', 365.0, 110.0, 'SW_5V', 'GND', 'Capacitor_SMD:C_0805_2012Metric','Device:C'),
('C120','10uF', 415.0, 110.0, '3V3_GNSS', 'GND', 'Capacitor_SMD:C_0805_2012Metric','Device:C'),
('C121','100nF', 425.0, 110.0, '3V3_GNSS', 'GND', 'Capacitor_SMD:C_0402_1005Metric','Device:C'),
('C122','100nF', 365.0, 130.0, '3V3_GNSS', 'GND', 'Capacitor_SMD:C_0402_1005Metric','Device:C'),
('C123','100nF', 375.0, 130.0, '3V3_GNSS', 'GND', 'Capacitor_SMD:C_0402_1005Metric','Device:C'),
('C124','10uF', 355.0, 130.0, '3V3_GNSS', 'GND', 'Capacitor_SMD:C_0805_2012Metric','Device:C'),
('R105','10k', 415.0, 130.0, '3V3_GNSS', 'GNSS_RESET_N', 'Resistor_SMD:R_0603_1608Metric', 'Device:R'),
]
def f(v):
v = 0.0 if abs(v) < 1e-9 else v
return '%.10g' % v
def mklabel(name, x, y, rot, just):
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, f(x), f(y), rot, just, uuid.uuid4())
def mknc(x, y):
return '\n\t(no_connect\n\t\t(at %s %s)\n\t\t(uuid "%s")\n\t)' % (f(x), f(y), uuid.uuid4())
def mksym(lib_id, ref, val, x, y, fp, pins, extra=''):
props = [('Reference', ref, x-3, y-3, ''), ('Value', val, x+3, y-3, ''),
('Footprint', fp, x, y, '\n\t\t\t(hide yes)'), ('Datasheet', '~', x, y, '\n\t\t\t(hide yes)'),
('Description', '', x, y, '\n\t\t\t(hide yes)')]
if extra: props.append(('LCSC', extra, x, y, '\n\t\t\t(hide yes)'))
p = ''
for nm, v, px, py, hide in props:
p += ('\t\t(property "%s" "%s"\n\t\t\t(at %s %s 0)%s\n\t\t\t(show_name no)\n\t\t\t(do_not_autoplace no)\n'
'\t\t\t(effects\n\t\t\t\t(font\n\t\t\t\t\t(size 1.27 1.27)\n\t\t\t\t)\n\t\t\t)\n\t\t)\n'
) % (nm, v, f(px), f(py), hide)
pn = ''.join('\t\t(pin "%s"\n\t\t\t(uuid "%s")\n\t\t)\n' % (n, uuid.uuid4()) for n in pins)
return ('\n\t(symbol\n\t\t(lib_id "%s")\n\t\t(at %s %s 0)\n\t\t(unit 1)\n\t\t(body_style 1)\n'
'\t\t(exclude_from_sim no)\n\t\t(in_bom yes)\n\t\t(on_board yes)\n\t\t(in_pos_files yes)\n'
'\t\t(dnp no)\n\t\t(uuid "%s")\n%s%s\t\t(instances\n\t\t\t(project "hbox-watchdog-v6"\n'
'\t\t\t\t(path "%s"\n\t\t\t\t\t(reference "%s")\n\t\t\t\t\t(unit 1)\n\t\t\t\t)\n\t\t\t)\n\t\t)\n\t)'
) % (lib_id, f(x), f(y), uuid.uuid4(), p, pn, ROOT, ref)
sch = open(SCH, encoding='utf-8').read()
libsrc = open(LIB, encoding='utf-8').read()
# ---- A. copy the two symbol definitions into the schematic's lib_symbols cache ----
def grab(name):
i = libsrc.find(' (symbol "%s"\n' % name)
if i < 0: sys.exit('symbol %s not found in %s' % (name, LIB))
d = 0; instr = False; esc = False; j = libsrc.index('(', i)
k = j
while k < len(libsrc):
ch = libsrc[k]
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: break
k += 1
blk = libsrc[j:k+1]
return blk.replace('(symbol "%s"' % name, '(symbol "Drogheda-sym:%s"' % name, 1)
added_defs = ''
for nm in ('ZED-F9P', 'LDO_SOT23-5'):
if '(symbol "Drogheda-sym:%s"' % nm in sch:
sys.exit('REFUSING: %s already cached in the schematic' % nm)
added_defs += '\n\t\t' + grab(nm)
m = re.search(r'\n\t\(lib_symbols\n', sch)
if not m: sys.exit('lib_symbols block not found')
d = 0; instr = False; esc = False; k = sch.index('(', m.start())
start = k
while k < len(sch):
ch = sch[k]
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: break
k += 1
sch = sch[:k] + added_defs + '\n\t' + sch[k:]
# ---- B. symbols ----
add = ''
add += mksym('Drogheda-sym:ZED-F9P', 'U18', 'ZED-F9P-04B', U18X, U18Y,
'Drogheda-v6:LCC-LGA-102_L22.0-W17.0_FM660', [str(n) for n in range(1, 103)], 'C5120257')
add += mksym('Drogheda-sym:LDO_SOT23-5', 'U21', 'AP2112K-3.3', U21X, U21Y,
'Package_TO_SOT_SMD:SOT-23-5', ['1','2','3','4','5'], 'C51118')
for ref, val, x, y, n1, n2, fp, lb in PASSIVES:
add += mksym(lb, ref, val, x, y, fp, ['1','2'])
# ---- C. labels + no-connects ----
nlab = nnc = 0
def put(net, x, y, rot, just):
global add, nlab, nnc
if net == NC:
add += mknc(x, y); nnc += 1
else:
add += mklabel(net, x, y, rot, just); nlab += 1
y = U18Y - (len(LEFT)-1)*P/2
for n in LEFT:
put(NET_L[n], U18X-33.02, y, 180, 'right bottom'); y += P
y = U18Y - (len(RIGHT)-1)*P/2
for n in RIGHT:
put(NET_R[n], U18X+33.02, y, 0, 'left bottom'); y += P
x = U18X - (len(RSVD)-1)*P/2
for n in RSVD:
put(NC, x, U18Y+30.48, 0, 'left bottom'); x += P
# U21 pins: 1 VIN(-12.7,0) 3 EN(-12.7,-2.54) 5 VOUT(12.7,0) 4 NC(12.7,-2.54) 2 GND(0,-10.16)
put('SW_5V', U21X-12.7, U21Y, 180, 'right bottom')
put('SW_5V', U21X-12.7, U21Y+2.54, 180, 'right bottom')
put('3V3_GNSS', U21X+12.7, U21Y, 0, 'left bottom')
put(NC, U21X+12.7, U21Y+2.54, 0, 'left bottom')
put('GND', U21X, U21Y+10.16, 90, 'left bottom')
for ref, val, x, y, n1, n2, fp, lb in PASSIVES:
put(n1, x, y-3.81, 90, 'left bottom')
put(n2, x, y+3.81, 90, 'left bottom')
for net, x, y, rot, just in MCU:
put(net, x, y, rot, just)
end = sch.rstrip()
assert end.endswith(')')
sch = end[:-1].rstrip('\n') + add + '\n)\n'
print('U18 ZED-F9P @ (%s, %s) U21 LDO @ (%s, %s)' % (U18X, U18Y, U21X, U21Y))
print('symbols added : U18, U21, ' + ', '.join(p[0] for p in PASSIVES))
print('labels added : %d no_connects added: %d (21 reserved + %d unused)' % (nlab, nnc, nnc-21))
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('paren balance :', bal(sch))
if not APPLY:
print('\nDRY RUN - nothing written. Re-run with --apply.'); sys.exit(0)
if bal(sch) != 0: sys.exit('REFUSING: paren imbalance')
shutil.copy2(SCH, SCH + '.T3bbak')
open(SCH, 'w', encoding='utf-8').write(sch)
print('\nAPPLIED. Backup: hbox-watchdog-v6.kicad_sch.T3bbak')