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