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