Drogheda/scripts/2026-08-12_einarboard30_phaseA_gen_blockP.py

631 lines
27 KiB
Python

#!/usr/bin/env python3
"""
EinarBoard 3.0 — Phase A generator: Block P (on-board STM32F411RET6) into hbox-watchdog-v5.kicad_sch
File-level edit per project standing rule (no KiCad MCP backend writes).
Surgical text splice: deletions by byte-span, insertions at anchors. Independent re-parse + netlist diff after.
Run: python3 gen_blockP.py <schematic path>
"""
import re, sys, math, uuid, collections, json
FN = sys.argv[1] if len(sys.argv) > 1 else 'hbox-watchdog-v5.kicad_sch'
src = open(FN, encoding='utf-8').read()
if 'STM32F411RETx' in src:
print('ABORT: U16/STM32F411RETx already present — idempotence guard'); sys.exit(1)
# ---------------- sexp parser with byte spans ----------------
def tokenize(s):
toks = []; i = 0; n = len(s)
while i < n:
c = s[i]
if c in ' \t\r\n': i += 1; continue
if c in '()': toks.append((c, i, i+1)); i += 1; continue
if c == '"':
j = i+1; buf = []
while j < n:
if s[j] == '\\' and j+1 < n: buf.append(s[j:j+2]); j += 2; continue
if s[j] == '"': break
buf.append(s[j]); j += 1
toks.append((('STR', ''.join(buf)), i, j+1)); i = j+1; continue
j = i
while j < n and s[j] not in ' \t\r\n()"': j += 1
toks.append((('ATOM', s[i:j]), i, j)); i = j
return toks
def parse(toks):
pos = [0]
def rd():
t, a, b = toks[pos[0]]
if t == '(':
pos[0] += 1; lst = []
while toks[pos[0]][0] != ')': lst.append(rd())
_, _, e = toks[pos[0]]; pos[0] += 1
return ('L', lst, a, e)
pos[0] += 1
return ('T', t, a, b)
out = []
while pos[0] < len(toks): out.append(rd())
return out
toks = tokenize(src)
doc = parse(toks)[0]
assert doc[0] == 'L'
def kids(e): return e[1] if e[0] == 'L' else []
def tag(e):
k = kids(e)
if k and k[0][0] == 'T': return k[0][1][1]
return None
def sval(e): return e[1][1] if e[0] == 'T' else None
def span(e): return (e[2], e[3])
def find_all(e, t): return [c for c in kids(e) if c[0] == 'L' and tag(c) == t]
def find1(e, t):
r = find_all(e, t); return r[0] if r else None
def fnum(e):
try: return float(sval(e))
except: return None
top = kids(doc)
root_uuid = sval(kids(find1(doc, 'uuid'))[1])
libsym_el = find1(doc, 'lib_symbols')
# ---------------- lib pin tables ----------------
def lib_pins(libname):
for s in find_all(libsym_el, 'symbol'):
if sval(kids(s)[1]) == libname:
pins = []
def walk(e):
for c in kids(e):
if c[0] == 'L':
if tag(c) == 'pin':
at = find1(c, 'at'); num = find1(c, 'number'); nm = find1(c, 'name')
pins.append(dict(x=fnum(kids(at)[1]), y=fnum(kids(at)[2]),
num=sval(kids(num)[1]), name=sval(kids(nm)[1]) if nm else ''))
else: walk(c)
walk(s)
return pins
raise KeyError(libname)
# ---------------- collect elements ----------------
symbols = find_all(doc, 'symbol')
wires = find_all(doc, 'wire')
labels = find_all(doc, 'label')
ncs = find_all(doc, 'no_connect')
juncs = find_all(doc, 'junction')
texts = find_all(doc, 'text')
def sym_info(s):
lid = sval(kids(find1(s, 'lib_id'))[1]); at = kids(find1(s, 'at'))
ref = value = fp = None
for p in find_all(s, 'property'):
pk = kids(p)
if sval(pk[1]) == 'Reference': ref = sval(pk[2])
elif sval(pk[1]) == 'Value': value = sval(pk[2])
elif sval(pk[1]) == 'Footprint': fp = sval(pk[2])
mir = find1(s, 'mirror')
return dict(lib_id=lid, ref=ref, value=value, fp=fp, x=fnum(at[1]), y=fnum(at[2]),
rot=(fnum(at[3]) if len(at) > 3 else 0) or 0,
mirror=sval(kids(mir)[1]) if mir else None, el=s)
SYMS = [sym_info(s) for s in symbols]
def by_ref(r):
m = [s for s in SYMS if s['ref'] == r]
assert len(m) == 1, f'ref {r}: {len(m)} matches'
return m[0]
def R2(v): return round(v, 2)
def pin_world(inst, pins=None):
pins = pins or lib_pins(inst['lib_id'])
out = {}
for p in pins:
x, y = p['x'], -p['y']
if inst['mirror'] == 'y': x = -x
if inst['mirror'] == 'x': y = -y
r = math.radians(inst['rot'])
out[p['num']] = (R2(inst['x'] + x*math.cos(r) + y*math.sin(r)),
R2(inst['y'] - x*math.sin(r) + y*math.cos(r)))
return out
adj = collections.defaultdict(set)
wire_pts = {}
for w in wires:
pts = [(R2(fnum(kids(xy)[1])), R2(fnum(kids(xy)[2]))) for xy in find_all(find1(w, 'pts'), 'xy')]
wire_pts[id(w)] = pts
for a, b in zip(pts, pts[1:]): adj[a].add(b); adj[b].add(a)
lab_at = collections.defaultdict(list) # pos -> [(name, element)]
for L in labels:
at = kids(find1(L, 'at'))
lab_at[(R2(fnum(at[1])), R2(fnum(at[2])))].append((sval(kids(L)[1]), L))
nc_at = {}
for n in ncs:
at = kids(find1(n, 'at')); nc_at[(R2(fnum(at[1])), R2(fnum(at[2])))] = n
junc_at = {}
for jn in juncs:
at = kids(find1(jn, 'at')); junc_at[(R2(fnum(at[1])), R2(fnum(at[2])))] = jn
# all pin positions of every symbol (for safety asserts + netlist)
ALLPINS = [] # (ref, pinnum, pos)
for s in SYMS:
try:
for num, p in pin_world(s).items(): ALLPINS.append((s['ref'], num, p))
except KeyError:
pass
pin_pos_map = collections.defaultdict(list)
for ref, num, p in ALLPINS: pin_pos_map[p].append((ref, num))
# ---------------- netlist (union-find over positions + label names) ----------------
def build_netlist(adjacency, labels_at, pinlist):
parent = {}
def find(a):
while parent.setdefault(a, a) != a:
parent[a] = parent[parent[a]]; a = parent[a]
return a
def union(a, b): parent[find(a)] = find(b)
for a, nbrs in adjacency.items():
for b in nbrs: union(('P', a), ('P', b))
for pos, lst in labels_at.items():
for nm, _ in lst: union(('P', pos), ('N', nm))
for ref, num, p in pinlist: union(('P', p), ('PIN', ref, num))
groups = collections.defaultdict(lambda: dict(names=set(), pins=set()))
for a in list(parent):
r = find(a); g = groups[r]
if a[0] == 'N': g['names'].add(a[1])
elif a[0] == 'PIN': g['pins'].add((a[1], a[2]))
nets = {}
for g in groups.values():
if not g['names'] and not g['pins']: continue
nm = '/'.join(sorted(g['names'])) if g['names'] else None
if nm: nets.setdefault(nm, set()).update(g['pins'])
return nets
NET_BEFORE = build_netlist(adj, lab_at, ALLPINS)
# ---------------- deletion set ----------------
J1 = by_ref('J1'); J2 = by_ref('J2'); P6 = by_ref('P6'); Rts1 = by_ref('Rts1'); Rtp1 = by_ref('Rtp1')
assert J1['value'] == 'Blackpill-L' and J2['value'] == 'Blackpill-R'
assert P6['lib_id'].endswith('Conn_01x04') and P6['value'] == 'FAN'
del_syms = [J1, J2, P6, Rts1, Rtp1]
del_sym_ids = {id(s['el']) for s in del_syms}
seed_pins = []
for s in del_syms: seed_pins += list(pin_world(s).values())
seed_set = set(seed_pins)
kept_pin_pos = {p for ref, num, p in ALLPINS if not any(by is s for s in [] )}
kept_pin_pos = set()
for s in SYMS:
if id(s['el']) in del_sym_ids: continue
try: kept_pin_pos.update(pin_world(s).values())
except KeyError: pass
# graph walk from seeds
gnodes = set()
stack = [p for p in seed_pins if p in adj]
while stack:
p = stack.pop()
if p in gnodes: continue
gnodes.add(p)
for q in adj[p]:
if q not in gnodes: stack.append(q)
# safety: deletion graph must not touch kept pins
touch = gnodes & kept_pin_pos
assert not touch, f'deletion graph touches kept pins at {sorted(touch)[:5]}'
del_spans = []
report = collections.defaultdict(list)
for s in del_syms:
del_spans.append(span(s['el'])); report['symbols_deleted'].append(f"{s['ref']} ({s['value']})")
for w in wires:
pts = wire_pts[id(w)]
if pts and all(p in gnodes for p in pts):
del_spans.append(span(w)); report['wires_deleted'].append(pts)
node_del = gnodes | seed_set
for pos, lst in lab_at.items():
if pos in node_del:
for nm, el in lst:
del_spans.append(span(el)); report['labels_deleted'].append((nm, pos))
# any FAN_TACH-family label anywhere (retired nets)
for pos, lst in lab_at.items():
if pos not in node_del:
for nm, el in lst:
if nm in ('FAN_TACH', 'FAN_TACH_RAW'):
del_spans.append(span(el)); report['labels_deleted'].append((nm, pos, 'tach-sweep'))
for pos, el in nc_at.items():
if pos in node_del:
del_spans.append(span(el)); report['nc_deleted'].append(pos)
for pos, el in junc_at.items():
if pos in node_del:
del_spans.append(span(el)); report['junctions_deleted'].append(pos)
# text patch: BLOCK N note (ETH_INT moved)
text_patches = []
for t in texts:
tv = sval(kids(t)[1])
if tv and 'INT PB2' in tv:
a, b = span(t)
text_patches.append((a, b, src[a:b].replace('INT PB2', 'INT PC0 (3.0: PB2 = BOOT1 strap — never repurpose)')))
report['texts_patched'].append('BLOCK N note: INT PB2 -> INT PC0')
# ---------------- templates from live file ----------------
def label_template(pos):
for nm, el in lab_at[pos]:
a, b = span(el); return src[a:b], nm
return None, None
# LEFT-side sample: label on J1 pin15 (NTC_TEMP); RIGHT-side: label on J2 pin1 (ETH_CS)
J1p = pin_world(J1); J2p = pin_world(J2)
tmplL, _ = label_template(J1p['15'])
tmplR, _ = label_template(J2p['1'])
assert tmplL and tmplR
def NU(): return str(uuid.uuid4())
def mk_label(name, x, y, side):
"""side: 'L' extends left, 'R' extends right, 'U' vertical up, 'D' vertical down"""
t = tmplL if side == 'L' else tmplR
# replace name, position, uuid in template
def rep(m):
return f'(at {x:g} {y:g} {ANG})'
ANG = {'L': 180, 'R': 0, 'U': 90, 'D': 270}[side]
out = re.sub(r'\(label "[^"]*"', f'(label "{name}"', t, count=1)
out = re.sub(r'\(at [^)]*\)', f'(at {x:g} {y:g} {ANG})', out, count=1)
out = re.sub(r'\(uuid "[^"]*"\)', f'(uuid "{NU()}")', out, count=1)
if side in ('R',):
out = re.sub(r'\(justify [^)]*\)', '(justify left bottom)', out, count=1)
elif side in ('L',):
out = re.sub(r'\(justify [^)]*\)', '(justify right bottom)', out, count=1)
else:
out = re.sub(r'\(justify [^)]*\)', '(justify left bottom)', out, count=1)
return out
def mk_nc(x, y):
return f'(no_connect\n\t\t(at {x:g} {y:g})\n\t\t(uuid "{NU()}")\n\t)'
def mk_text(txt, x, y):
e = texts[0]; a, b = span(e); t = src[a:b]
t = re.sub(r'^\(text "[^"]*(?:\\.[^"]*)*"', '(text ' + json.dumps(txt), t, count=1)
t = re.sub(r'\(at [^)]*\)', f'(at {x:g} {y:g} 0)', t, count=1)
t = re.sub(r'\(uuid "[^"]*"\)', f'(uuid "{NU()}")', t, count=1)
return t
# generic instance factory from template R20
R20 = by_ref('R20'); a, b = span(R20['el']); INST_TMPL = src[a:b]
def mk_instance(lib_id, ref, value, footprint, x, y, rot=0, pin_nums=None, extra_props=None):
t = INST_TMPL
t = re.sub(r'\(lib_id "[^"]*"\)', f'(lib_id "{lib_id}")', t, count=1)
t = re.sub(r'\(at [^)]*\)', f'(at {x:g} {y:g} {rot:g})', t, count=1) # first (at = instance
t = re.sub(r'\(property "Reference" "[^"]*"', f'(property "Reference" "{ref}"', t, count=1)
t = re.sub(r'\(property "Value" "[^"]*"', f'(property "Value" {json.dumps(value)}', t, count=1)
t = re.sub(r'\(property "Footprint" "[^"]*"', f'(property "Footprint" {json.dumps(footprint)}', t, count=1)
# property positions: drop to instance pos (KiCad re-autoplaces visually; positions cosmetic)
# fix property (at ...) occurrences AFTER the first: leave — cosmetic only.
# pins: template has pins "1","2" — rebuild pin uuid list for pin_nums
pin_nums = pin_nums or ['1', '2']
pin_block = '\n'.join(f'\t\t(pin "{p}"\n\t\t\t(uuid "{NU()}")\n\t\t)' for p in pin_nums)
t = re.sub(r'(\t\t\(pin "1".*?\)\n)+.*?\t\t\(pin "2"\n\t\t\t\(uuid "[^"]*"\)\n\t\t\)',
pin_block, t, count=1, flags=re.S)
t = re.sub(r'\(reference "[^"]*"\)', f'(reference "{ref}")', t, count=1)
t = re.sub(r'\(uuid "a2b3658d[^"]*"\)', f'(uuid "{NU()}")', t, count=1)
return t
# ---------------- MCU lib symbol (built from official pin map) ----------------
MCU_PINS = [
(1,'VBAT','power_in',-5.08,48.26,270),(2,'PC13','bidirectional',-17.78,-15.24,0),
(3,'PC14','bidirectional',-17.78,-17.78,0),(4,'PC15','bidirectional',-17.78,-20.32,0),
(5,'PH0','bidirectional',-17.78,30.48,0),(6,'PH1','bidirectional',-17.78,27.94,0),
(7,'NRST','input',-17.78,40.64,0),(8,'PC0','bidirectional',-17.78,17.78,0),
(9,'PC1','bidirectional',-17.78,15.24,0),(10,'PC2','bidirectional',-17.78,12.7,0),
(11,'PC3','bidirectional',-17.78,10.16,0),(12,'VSSA','power_in',2.54,-45.72,90),
(13,'VDDA','power_in',7.62,48.26,270),(14,'PA0','bidirectional',17.78,40.64,180),
(15,'PA1','bidirectional',17.78,38.1,180),(16,'PA2','bidirectional',17.78,35.56,180),
(17,'PA3','bidirectional',17.78,33.02,180),(18,'VSS','power_in',0,-45.72,90),
(19,'VDD','power_in',-2.54,48.26,270),(20,'PA4','bidirectional',17.78,30.48,180),
(21,'PA5','bidirectional',17.78,27.94,180),(22,'PA6','bidirectional',17.78,25.4,180),
(23,'PA7','bidirectional',17.78,22.86,180),(24,'PC4','bidirectional',-17.78,7.62,0),
(25,'PC5','bidirectional',-17.78,5.08,0),(26,'PB0','bidirectional',17.78,-2.54,180),
(27,'PB1','bidirectional',17.78,-5.08,180),(28,'PB2','bidirectional',17.78,-7.62,180),
(29,'PB10','bidirectional',17.78,-27.94,180),(30,'VCAP1','power_out',-17.78,-25.4,0),
(31,'VSS','passive',0,-45.72,90),(32,'VDD','power_in',0,48.26,270),
(33,'PB12','bidirectional',17.78,-30.48,180),(34,'PB13','bidirectional',17.78,-33.02,180),
(35,'PB14','bidirectional',17.78,-35.56,180),(36,'PB15','bidirectional',17.78,-38.1,180),
(37,'PC6','bidirectional',-17.78,2.54,0),(38,'PC7','bidirectional',-17.78,0,0),
(39,'PC8','bidirectional',-17.78,-2.54,0),(40,'PC9','bidirectional',-17.78,-5.08,0),
(41,'PA8','bidirectional',17.78,20.32,180),(42,'PA9','bidirectional',17.78,17.78,180),
(43,'PA10','bidirectional',17.78,15.24,180),(44,'PA11','bidirectional',17.78,12.7,180),
(45,'PA12','bidirectional',17.78,10.16,180),(46,'PA13','bidirectional',17.78,7.62,180),
(47,'VSS','passive',0,-45.72,90),(48,'VDD','power_in',2.54,48.26,270),
(49,'PA14','bidirectional',17.78,5.08,180),(50,'PA15','bidirectional',17.78,2.54,180),
(51,'PC10','bidirectional',-17.78,-7.62,0),(52,'PC11','bidirectional',-17.78,-10.16,0),
(53,'PC12','bidirectional',-17.78,-12.7,0),(54,'PD2','bidirectional',-17.78,22.86,0),
(55,'PB3','bidirectional',17.78,-10.16,180),(56,'PB4','bidirectional',17.78,-12.7,180),
(57,'PB5','bidirectional',17.78,-15.24,180),(58,'PB6','bidirectional',17.78,-17.78,180),
(59,'PB7','bidirectional',17.78,-20.32,180),(60,'BOOT0','input',-17.78,35.56,0),
(61,'PB8','bidirectional',17.78,-22.86,180),(62,'PB9','bidirectional',17.78,-25.4,180),
(63,'VSS','passive',0,-45.72,90),(64,'VDD','power_in',5.08,48.26,270),
]
def mcu_libsym():
pins_txt = []
for num, name, etype, x, y, ang in MCU_PINS:
hide = '\n\t\t\t\t\t(hide yes)' if etype == 'passive' and name == 'VSS' and num != 18 else ''
pins_txt.append(f'''\t\t\t\t(pin {etype} line
\t\t\t\t\t(at {x:g} {y:g} {ang}){hide}
\t\t\t\t\t(length 2.54)
\t\t\t\t\t(name "{name}"
\t\t\t\t\t\t(effects
\t\t\t\t\t\t\t(font
\t\t\t\t\t\t\t\t(size 1.27 1.27)
\t\t\t\t\t\t\t)
\t\t\t\t\t\t)
\t\t\t\t\t)
\t\t\t\t\t(number "{num}"
\t\t\t\t\t\t(effects
\t\t\t\t\t\t\t(font
\t\t\t\t\t\t\t\t(size 1.27 1.27)
\t\t\t\t\t\t\t)
\t\t\t\t\t\t)
\t\t\t\t\t)
\t\t\t\t)''')
pins = '\n'.join(pins_txt)
return f'''\t\t(symbol "MCU_ST_STM32F4:STM32F411RETx"
\t\t\t(pin_names
\t\t\t\t(offset 1.016)
\t\t\t)
\t\t\t(exclude_from_sim no)
\t\t\t(in_bom yes)
\t\t\t(on_board yes)
\t\t\t(property "Reference" "U"
\t\t\t\t(at -15.24 49.53 0)
\t\t\t\t(effects
\t\t\t\t\t(font
\t\t\t\t\t\t(size 1.27 1.27)
\t\t\t\t\t)
\t\t\t\t\t(justify left)
\t\t\t\t)
\t\t\t)
\t\t\t(property "Value" "STM32F411RETx"
\t\t\t\t(at -15.24 -46.99 0)
\t\t\t\t(effects
\t\t\t\t\t(font
\t\t\t\t\t\t(size 1.27 1.27)
\t\t\t\t\t)
\t\t\t\t\t(justify left)
\t\t\t\t)
\t\t\t)
\t\t\t(property "Footprint" "Package_QFP:LQFP-64_10x10mm_P0.5mm"
\t\t\t\t(at 0 0 0)
\t\t\t\t(effects
\t\t\t\t\t(font
\t\t\t\t\t\t(size 1.27 1.27)
\t\t\t\t\t)
\t\t\t\t\t(hide yes)
\t\t\t\t)
\t\t\t)
\t\t\t(property "Datasheet" "https://www.st.com/resource/en/datasheet/stm32f411re.pdf"
\t\t\t\t(at 0 0 0)
\t\t\t\t(effects
\t\t\t\t\t(font
\t\t\t\t\t\t(size 1.27 1.27)
\t\t\t\t\t)
\t\t\t\t\t(hide yes)
\t\t\t\t)
\t\t\t)
\t\t\t(symbol "STM32F411RETx_0_1"
\t\t\t\t(rectangle
\t\t\t\t\t(start -15.24 45.72)
\t\t\t\t\t(end 15.24 -43.18)
\t\t\t\t\t(stroke
\t\t\t\t\t\t(width 0.254)
\t\t\t\t\t\t(type default)
\t\t\t\t\t)
\t\t\t\t\t(fill
\t\t\t\t\t\t(type background)
\t\t\t\t\t)
\t\t\t\t)
\t\t\t)
\t\t\t(symbol "STM32F411RETx_1_1"
{pins}
\t\t\t)
\t\t)'''
# ---------------- Block P content ----------------
INS = [] # element text blocks to append at end of doc
def add(t): INS.append(t)
# U16 at grid
U16X, U16Y = 500.38, 434.34
mcu_world = {}
for num, name, etype, x, y, ang in MCU_PINS:
mcu_world[num] = (R2(U16X + x), R2(U16Y - y))
MCU_NETS = {1:'3V3',2:'LED_C13',3:'LSE_IN',4:'LSE_OUT',5:'HSE_IN',6:'HSE_OUT',7:'NRST',8:'ETH_INT',
12:'GND',13:'VDDA',14:'BAT_SENSE',15:'PWR_CTRL',16:'GNSS_TX',17:'GNSS_RX',18:'GND',19:'3V3',
20:'5V_SENSE',21:'IGN_SENSE',22:'FAN_PWM',26:'EXT_LED',27:'NTC_TEMP',28:'BOOT1_STRAP',30:'VCAP1',
32:'3V3',33:'ETH_CS',34:'ETH_SCK',35:'ETH_MISO',36:'ETH_MOSI',41:'CHRG_EN',42:'USART1_TX',
43:'USART1_RX',44:'DFU_DM',45:'DFU_DP',46:'SWDIO',48:'3V3',49:'SWCLK',50:'ETH_RST',
55:'CHRG_STAT',56:'CHRG_FAULT',57:'BYP_CTRL',58:'I2C_SCL',59:'I2C_SDA',60:'BOOT0',
61:'SER_IRQ',62:None,64:'3V3'}
MCU_NC = [9,10,11,23,24,25,29,37,38,39,40,51,52,53,54,62]
add(mk_instance('MCU_ST_STM32F4:STM32F411RETx', 'U16', 'STM32F411RET6',
'Package_QFP:LQFP-64_10x10mm_P0.5mm', U16X, U16Y, 0,
[str(n) for n, *_ in MCU_PINS]))
for num, name, etype, x, y, ang in MCU_PINS:
wx, wy = mcu_world[num]
net = MCU_NETS.get(num)
if num in MCU_NC:
add(mk_nc(wx, wy)); continue
if net is None: continue
side = {0: 'L', 180: 'R', 270: 'U', 90: 'D'}[ang]
if num in (31, 47, 63): continue # stacked VSS share pin18's point/label
add(mk_label(net, wx, wy, side))
# passives / support parts: label-on-pin style (v4 house pattern)
PART_LIBS = {}
def two_pin(lib_id, ref, value, fp, x, y, net1, net2, rot=0):
pins = lib_pins(lib_id)
inst = dict(lib_id=lib_id, x=x, y=y, rot=rot, mirror=None)
pw = pin_world(inst, pins)
nums = sorted(pw, key=lambda n: int(re.sub(r'\D', '', n) or 0))
add(mk_instance(lib_id, ref, value, fp, x, y, rot, nums))
for num, net in zip(nums, (net1, net2)):
px, py = pw[num]
side = 'R' if px > x else ('L' if px < x else ('U' if py < y else 'D'))
if net: add(mk_label(net, px, py, side))
FP_R = 'Resistor_SMD:R_0603_1608Metric'
FP_C = 'Capacitor_SMD:C_0603_1608Metric'
FP_C0805 = 'Capacitor_SMD:C_0805_2012Metric'
FP_LED = 'LED_SMD:LED_0603_1608Metric'
FP_TVS = 'Diode_SMD:D_SOD-323'
D2fp = by_ref('D2')['fp'] # SS34 footprint in use
Y1fp = by_ref('Y1')['fp'] # 25MHz crystal footprint in use
SW1fp = by_ref('SW1')['fp']
TP1fp = by_ref('TP1')['fp']
J9 = by_ref('J9')
C23fp = by_ref('C23')['fp']
P6fp_old = P6['fp'] or ''
P6fp_new = P6fp_old.replace('1x04', '1x02') if '1x04' in P6fp_old else P6fp_old
ROWA, ROWB = 502.92, 530.86
two_pin('Switch:SW_Push', 'SW2', 'BOOT0', SW1fp, 436.88, ROWA, '3V3', 'BOOT0')
two_pin('Device:R', 'R98', '10k', FP_R, 449.58, ROWA, 'BOOT0', 'GND')
two_pin('Device:R', 'R99', '10k', FP_R, 462.28, ROWA, 'BOOT1_STRAP', 'GND')
two_pin('Device:C', 'C104', '100nF', FP_C, 474.98, ROWA, 'NRST', 'GND')
for i, xx in enumerate([487.68, 500.38, 513.08, 525.78]):
two_pin('Device:C', f'C{105+i}', '100nF', FP_C, xx, ROWA, '3V3', 'GND')
two_pin('Device:C', 'C109', '100nF', FP_C, 538.48, ROWA, 'VDDA', 'GND')
two_pin('Device:C', 'C110', '4.7uF', FP_C, 551.18, ROWA, '3V3', 'GND')
two_pin('Device:C', 'C111', '4.7uF', FP_C, 563.88, ROWA, 'VCAP1', 'GND')
two_pin('Device:C', 'C112', '1uF', FP_C, 576.58, ROWA, 'VDDA', 'GND')
two_pin('Device:R', 'R104', '10R', FP_R, 589.28, ROWA, '3V3', 'VDDA')
two_pin('Device:Crystal', 'Y3', '25MHz', Y1fp, 436.88, ROWB, 'HSE_IN', 'HSE_OUT')
two_pin('Device:C', 'C113', '15pF', FP_C, 449.58, ROWB, 'HSE_IN', 'GND')
two_pin('Device:C', 'C114', '15pF', FP_C, 462.28, ROWB, 'HSE_OUT', 'GND')
two_pin('Device:Crystal', 'Y4', '32.768kHz', 'Crystal:Crystal_SMD_3215-2Pin_3.2x1.5mm',
474.98, ROWB, 'LSE_IN', 'LSE_OUT')
two_pin('Device:C', 'C115', '12pF', FP_C, 487.68, ROWB, 'LSE_IN', 'GND')
two_pin('Device:C', 'C116', '12pF', FP_C, 500.38, ROWB, 'LSE_OUT', 'GND')
# LEDs: Device:LED pin1=K pin2=A
two_pin('Device:R', 'R102', '1k', FP_R, 513.08, ROWB, '3V3', 'LED_C13_A')
two_pin('Device:LED', 'D17', 'LED C13 (blue)', FP_LED, 525.78, ROWB, 'LED_C13', 'LED_C13_A')
two_pin('Device:R', 'R103', '1k', FP_R, 538.48, ROWB, '3V3', 'LED_PWR_A')
two_pin('Device:LED', 'D18', 'LED PWR (green)', FP_LED, 551.18, ROWB, 'GND', 'LED_PWR_A')
for ref, net, xx in [('TP3', 'SWDIO', 563.88), ('TP4', 'SWCLK', 576.58), ('TP5', 'NRST', 589.28),
('TP6', 'GND', 601.98), ('TP7', '3V3', 614.68)]:
pins = lib_pins('Connector:TestPoint')
inst = dict(lib_id='Connector:TestPoint', x=xx, y=ROWB, rot=0, mirror=None)
pw = pin_world(inst, pins)
add(mk_instance('Connector:TestPoint', ref, net, TP1fp, xx, ROWB, 0, list(pw)))
(px, py) = list(pw.values())[0]
add(mk_label(net, px, py, 'D' if py > ROWB else 'U'))
# LDO block
two_pin('Device:D_Schottky', 'D12', 'SS34', D2fp, 627.38, ROWA, 'LDO_IN', 'SYS5V') # pin1=K pin2=A
two_pin('Device:D_Schottky', 'D13', 'SS34', D2fp, 640.08, ROWA, 'LDO_IN', 'DFU_VBUS')
u17pins = lib_pins('Regulator_Linear:AMS1117-3.3')
U17X, U17Y = 665.48, ROWA
inst = dict(lib_id='Regulator_Linear:AMS1117-3.3', x=U17X, y=U17Y, rot=0, mirror=None)
pw = pin_world(inst, u17pins)
add(mk_instance('Regulator_Linear:AMS1117-3.3', 'U17', 'AMS1117-3.3',
'Package_TO_SOT_SMD:SOT-223-3_TabPin2', U17X, U17Y, 0, [p['num'] for p in u17pins]))
name_by_num = {p['num']: p['name'].upper() for p in u17pins}
for num, (px, py) in pw.items():
nm = name_by_num[num]
net = 'LDO_IN' if nm in ('VI', 'VIN', 'IN') else ('3V3' if nm in ('VO', 'VOUT', 'OUT') else 'GND')
side = 'R' if px > U17X else ('L' if px < U17X else ('U' if py < U17Y else 'D'))
add(mk_label(net, px, py, side))
two_pin('Device:C', 'C117', '10uF', FP_C0805, 690.88, ROWA, 'LDO_IN', 'GND')
two_pin('Device:C', 'C118', '22uF/16V', C23fp or FP_C0805, 703.58, ROWA, '3V3', 'GND')
for ref, net, xx in [('PF12', 'LDO_IN', 716.28), ('PF13', 'DFU_VBUS', 728.98)]:
pins = lib_pins('power:PWR_FLAG')
inst = dict(lib_id='power:PWR_FLAG', x=xx, y=ROWA, rot=0, mirror=None)
pw = pin_world(inst, pins)
add(mk_instance('power:PWR_FLAG', ref, 'PWR_FLAG', '', xx, ROWA, 0, list(pw)))
(px, py) = list(pw.values())[0]
add(mk_label(net, px, py, 'D' if py > ROWA else 'U'))
# USB-C DFU port: clone J9 geometry+pattern with DFU_* nets
J9pins = pin_world(J9)
J11X, J11Y = 660.4, 430.53
NETMAP = {'USB_VBUS': 'DFU_VBUS', 'USB_DP': 'DFU_DP', 'USB_DM': 'DFU_DM',
'USB_CC1': 'DFU_CC1', 'USB_CC2': 'DFU_CC2'}
# what J9 has per pin: label at pin OR nc
a9, b9 = span(J9['el']); j9txt = src[a9:b9]
j9txt = re.sub(r'\(property "Reference" "J9"', '(property "Reference" "J11"', j9txt, count=1)
j9txt = re.sub(r'\(property "Value" "[^"]*"', '(property "Value" "USB-C DFU"', j9txt, count=1)
j9txt = re.sub(r'\(at ([\-0-9.]+) ([\-0-9.]+)( [\-0-9.]+)?\)',
lambda m, base=[0]: m.group(0), j9txt) # keep; we shift below numerically
# shift all coordinates by delta
DX, DY = R2(J11X - J9['x']), R2(J11Y - J9['y'])
def shift_coords(txt, dx, dy):
def r(m):
vals = m.group(1).split()
x = float(vals[0]) + dx; y = float(vals[1]) + dy
rest = (' ' + ' '.join(vals[2:])) if len(vals) > 2 else ''
return f'(at {x:g} {y:g}{rest})'
return re.sub(r'\(at ([^)]*)\)', r, txt)
j9txt = shift_coords(j9txt, DX, DY)
j9txt = re.sub(r'\(uuid "[^"]*"\)', lambda m: f'(uuid "{NU()}")', j9txt)
j9txt = re.sub(r'\(reference "J9"\)', '(reference "J11")', j9txt)
add(j9txt)
# labels/nc for J11 pins mirroring J9's connection style
for num, pos in J9pins.items():
npos = (R2(pos[0] + DX), R2(pos[1] + DY))
if pos in nc_at:
add(mk_nc(*npos)); continue
here = [nm for nm, _ in lab_at.get(pos, [])]
if not here and pos in adj:
# pin connected via a wire run in the original (e.g. SH -> wire -> GND label):
# resolve the net name by graph walk and label the cloned pin directly.
seen = {pos}; st = [pos]; found = None
while st and not found:
q = st.pop()
lbls = [nm for nm, _ in lab_at.get(q, [])]
if lbls: found = lbls[0]; break
for r_ in adj[q]:
if r_ not in seen: seen.add(r_); st.append(r_)
if found: here = [found]
if here:
nm = here[0]
newnm = NETMAP.get(nm, nm)
side = 'R' if pos[0] > J9['x'] else 'L'
add(mk_label(newnm, npos[0], npos[1], side))
# CC pulldowns + TVS
two_pin('Device:R', 'R100', '5.1k', FP_R, 704.85, 430.53, 'DFU_CC1', 'GND')
two_pin('Device:R', 'R101', '5.1k', FP_R, 717.55, 430.53, 'DFU_CC2', 'GND')
two_pin('Device:D_TVS', 'D14', 'PESD5V0S1BA', FP_TVS, 704.85, 458.47, 'DFU_DP', 'GND')
two_pin('Device:D_TVS', 'D15', 'PESD5V0S1BA', FP_TVS, 717.55, 458.47, 'DFU_DM', 'GND')
two_pin('Device:D_TVS', 'D16', 'PESD5V0S1BA', FP_TVS, 730.25, 458.47, 'DFU_VBUS', 'GND')
# 2-pin fan connector at old P6 site (reuse ref P6)
oldp = pin_world(P6)
np6x, np6y = P6['x'], P6['y']
two_pin('Connector_Generic:Conn_01x02', 'P6', 'FAN 2-pin', P6fp_new, np6x, np6y,
'5V_FAN', 'FAN_RET', rot=P6['rot'])
# notes
add(mk_text('BLOCK P - ON-BOARD MCU (EinarBoard 3.0). STM32F411RET6 LQFP64 replaces Blackpill+J1/J2. DFU: hold BOOT0, tap NRST -> USB. HSE 25MHz REQUIRED for ROM-bootloader USB clock (AN2606). PB2=BOOT1 strapped LOW via R99 - NEVER repurpose PB2. Bench flash: USB powers 3V3 domain only via D13 (SYS5V dark, K1 stays NC).', 420, 372))
add(mk_text('PC6/PC7 reserved: USART6 (3rd UART, future). Free: PA7, PB9, PB10, PC1-PC5, PC8-PC12, PD2. LED_C13 active-LOW (Blackpill parity). SWD: TP3-TP7 (SWDIO/SWCLK/NRST/GND/3V3).', 420, 377))
# ---------------- splice ----------------
new_src = src
# apply text patches + deletions together, sorted desc by start
ops = [(a, b, '') for (a, b) in del_spans] + [(a, b, t) for (a, b, t) in text_patches]
ops.sort(key=lambda o: -o[0])
spans_sorted = sorted([(a, b) for a, b, _ in ops])
for (a1, b1), (a2, b2) in zip(spans_sorted, spans_sorted[1:]):
assert b1 <= a2, f'overlapping spans {a1,b1} {a2,b2}'
for a, b, t in ops:
# widen deletion to swallow leading whitespace
a0 = a
while a0 > 0 and new_src[a0-1] in ' \t\n': a0 -= 1
if t == '':
new_src = new_src[:a0] + '\n\t' + new_src[b:]
else:
new_src = new_src[:a] + t + new_src[b:]
# insert lib symbol before lib_symbols closing paren
ls_a, ls_b = span(libsym_el)
# recompute shift: all deletions were AFTER lib_symbols? assert to be safe
assert all(a > ls_b for a, b, _ in ops), 'a deletion overlaps lib_symbols - unhandled'
new_src = new_src[:ls_b-1] + '\n' + mcu_libsym() + '\n\t' + new_src[ls_b-1:]
# append new elements before final closing paren
end = new_src.rfind(')')
body = '\n\t' + '\n\t'.join(INS) + '\n'
new_src = new_src[:end] + body + new_src[end:]
open(FN, 'w', encoding='utf-8').write(new_src)
print('WROTE', FN, len(new_src), 'bytes (was', len(src), ')')
for k, v in report.items():
print(f'-- {k}: {len(v)}')
for item in v[:40]: print(' ', item)