#!/usr/bin/env python3 """ EinarBoard 3.0 — Phase B.2: placement revision per Leo's review (12-Ago). - Rip ALL tracks/vias of the 24 old socket nets + /NRST (reroute in C) - Endpoint-rip stubs at old pad sites of every moved footprint (this + last round) - Delete Blackpill bay silk (labels row + title + outline gfx) - Pack R85/R86/R87/R88/D11 east beside Q1/Q5; buck cluster U12+L8+C100+C101 below C43 - U16 + full support ring -> freed bay (~119,63.5); LDO cluster -> tight block; J11 north; LEDs south of J11; SW2 -> beside H4; TPs -> north strip - MH7-12 reference text -> 1.0mm; version silk 2.0->3.0, HBOX v4->v5 """ import re, sys, math, uuid, collections CUR = 'hbox-watchdog-v5.kicad_pcb' ORIG = 'pcb.orig' src = open(CUR, encoding='utf-8').read() orig = open(ORIG, encoding='utf-8').read() assert '"U16"' in src, 'phase B output expected' if 'STM32F411 BLACKPILL' not in src: print('ABORT: bay silk already gone (B.2 already ran?)'); sys.exit(1) def NU(): return str(uuid.uuid4()) def endof(s, start): d=0; ins=False; esc=False for k in range(start, len(s)): c=s[k] if esc: esc=False; continue if c=='\\': esc=True; continue if c=='"': ins=not ins; continue if not ins: if c=='(': d+=1 elif c==')': d-=1 if d==0: return k+1 return -1 def rotpt(lx, ly, deg): r = math.radians((-deg) % 360) # ROTSGN=-1 convention baked in return (lx*math.cos(r) - ly*math.sin(r), lx*math.sin(r) + ly*math.cos(r)) def parse_fps(s): out={} for m in re.finditer(r'\(footprint\s+"([^"]+)"', s): a=m.start(); e=endof(s,a); blk=s[a:e] rm=re.search(r'\(property "Reference"\s+"([^"]+)"', blk) am=re.search(r'\(at ([\-0-9.]+) ([\-0-9.]+)( [\-0-9.]+)?\)', blk) if rm and am and rm.group(1) not in out: out[rm.group(1)]=dict(ref=rm.group(1), lib=m.group(1), x=float(am.group(1)), y=float(am.group(2)), rot=float((am.group(3) or '0')), span=(a,e), blk=blk) return out def pads_of(blk): out=[] for m in re.finditer(r'\(pad\s+"([^"]*)"\s+(np_thru_hole|thru_hole|smd)\s+(\w+)', blk): a=m.start(); e=endof(blk,a); p=blk[a:e] am=re.search(r'\(at ([\-0-9.]+) ([\-0-9.]+)( [\-0-9.]+)?\)', p) nm=re.search(r'\(net "((?:[^"\\]|\\.)*)"\)', p) sm=re.search(r'\(size ([\-0-9.]+) ([\-0-9.]+)\)', p) out.append(dict(num=m.group(1), lx=float(am.group(1)), ly=float(am.group(2)), net=nm.group(1) if nm else None, sx=float(sm.group(1)) if sm else 1, sy=float(sm.group(2)) if sm else 1)) return out def pad_world(f, p): dx,dy = rotpt(p['lx'], p['ly'], f['rot']) return (f['x']+dx, f['y']+dy) FP = parse_fps(src); FPO = parse_fps(orig) # ---------------- 1) net rip: socket nets + /NRST ---------------- RIP_NETS = {'/BAT_SENSE','/PWR_CTRL','/GNSS_TX','/GNSS_RX','/5V_SENSE','/IGN_SENSE','/FAN_PWM', '/EXT_LED','/NTC_TEMP','/ETH_INT','/ETH_CS','/ETH_SCK','/ETH_MISO','/ETH_MOSI','/ETH_RST', '/CHRG_EN','/CHRG_STAT','/CHRG_FAULT','/BYP_CTRL','/USART1_TX','/USART1_RX','/I2C_SCL', '/I2C_SDA','/SER_IRQ','/NRST'} del_spans=[]; stats=collections.Counter() for m in re.finditer(r'\(segment\b|\(via\b|\(arc\b', src): a=m.start(); e=endof(src,a); blk=src[a:e] nm=re.search(r'\(net "((?:[^"\\]|\\.)*)"\)', blk) if nm and nm.group(1) in RIP_NETS: del_spans.append((a,e)); stats['net-rip '+m.group(0)[1:]]+=1 # ---------------- 2) endpoint rip at old pad sites ---------------- old_sites=[] for ref in ('J1','J2','P6','Y1','Y2','C83','C90','C91','C96','C102','R96','R97'): f=FPO.get(ref) if f: for p in pads_of(f['blk']): old_sites.append(pad_world(f,p)) for ref in ('R85','R86','R87','R88','D11','L8','U12','C100','C101'): f=FP.get(ref) if f: for p in pads_of(f['blk']): old_sites.append(pad_world(f,p)) def near_site(x,y,tol=0.15): return any(abs(x-sx)<=tol and abs(y-sy)<=tol for sx,sy in old_sites) ripped=set(a for a,e in del_spans) for m in re.finditer(r'\(segment\b|\(via\b', src): a=m.start() if a in ripped: continue e=endof(src,a); blk=src[a:e] if m.group(0)=='(via': am=re.search(r'\(at ([\-0-9.]+) ([\-0-9.]+)\)', blk) pts=[(float(am.group(1)),float(am.group(2)))] else: pts=[] sm=re.search(r'\(start ([\-0-9.]+) ([\-0-9.]+)\)', blk); em=re.search(r'\(end ([\-0-9.]+) ([\-0-9.]+)\)', blk) if sm: pts.append((float(sm.group(1)),float(sm.group(2)))) if em: pts.append((float(em.group(1)),float(em.group(2)))) if any(near_site(x,y) for x,y in pts): del_spans.append((a,e)); stats['endpoint-rip']+=1 # ---------------- 3) bay silk deletion + version text patches ---------------- text_patches=[] BAY=(98,49,162,79) for m in re.finditer(r'\(gr_(text|rect|line|poly|circle)\b', src): a=m.start(); e=endof(src,a); blk=src[a:e] if 'Silk' not in blk: continue coords=[(float(mm.group(1)),float(mm.group(2))) for mm in re.finditer(r'\((?:at|start|end|xy|center) ([\-0-9.]+) ([\-0-9.]+)', blk)] if not coords: continue inside=all(BAY[0]=0: text_patches.append((i+1, i+1+len(old), new)) # ---------------- 4) footprint moves ---------------- MOVES = {} # ref -> (x, y) new position (rot preserved) # packed east row beside Q1/Q5 at y 63.9 (all rot preserved: R's are rot90, D11 rot90) MOVES.update({'R88':(140.0,63.9), 'R87':(142.5,63.9), 'D11':(145.6,63.9), 'R85':(148.7,63.9), 'R86':(151.2,63.9)}) # buck cluster below C43 (C43 at 146.95,109.31; keep loop tight) MOVES.update({'U12':(144.0,116.5), 'L8':(145.0,123.5), 'C100':(139.2,116.5), 'C101':(139.2,120.0)}) # J11 4.5mm north MOVES['J11']=(71.11,87.5) # LEDs + resistors south of J11 MOVES.update({'D17':(64.5,97.6), 'D18':(64.5,100.2), 'R102':(67.94,97.6), 'R103':(67.94,100.2)}) # SW2 beside H4 (bottom edge service row) MOVES['SW2']=(84.0,134.3) # U16 into freed bay + support ring UX,UY=119.0,63.5 MOVES['U16']=(UX,UY) MOVES.update({ 'C105':(111.4,56.6), 'C106':(126.6,56.6), 'C107':(111.4,70.4), 'C108':(126.6,70.4), 'C110':(114.5,56.0), 'C111':(119.0,71.9), 'C104':(110.6,66.5), 'R99':(110.6,60.5), 'R104':(114.5,71.9), 'C109':(123.5,71.9), 'C112':(126.0,73.9), 'R98':(127.4,60.5), 'Y3':(133.0,66.8), 'C113':(129.4,70.2), 'C114':(133.0,70.2), 'Y4':(133.0,58.4), 'C115':(129.4,61.4), 'C116':(133.0,61.9), 'TP3':(108.0,52.0), 'TP4':(110.8,52.0), 'TP5':(113.6,52.0), 'TP6':(116.4,52.0), 'TP7':(119.2,52.0), }) # LDO tight block in freed old-U16 zone (between J11 and new U16) MOVES.update({'U17':(90.5,69.5), 'D12':(96.3,67.0), 'D13':(96.3,70.5), 'C117':(87.0,74.5), 'C118':(90.5,74.5)}) for ref,(nx,ny) in MOVES.items(): f=FP[ref] del_spans.append(f['span']) nb=re.sub(r'\(at [\-0-9. ]+\)', f'(at {nx:g} {ny:g}{(" %g"%f["rot"]) if f["rot"] else ""})', f['blk'], count=1) MOVES[ref]=(nx,ny,nb) stats['moved']+=1 # ---------------- 5) MH7-12 reference text size ---------------- mh_patches=[] for ref in ('MH7','MH8','MH9','MH10','MH11','MH12'): f=FP[ref] a,e=f['span'] blk=f['blk'] pm=re.search(r'\(property "Reference"\s+"'+ref+r'"', blk) pe=endof(blk, pm.start()) ptxt=blk[pm.start():pe] ptxt2=re.sub(r'\(size [0-9.]+ [0-9.]+\)', '(size 1 1)', ptxt) if '(thickness' in ptxt2: ptxt2=re.sub(r'\(thickness [0-9.]+\)', '(thickness 0.15)', ptxt2) else: ptxt2=ptxt2.replace('(size 1 1)', '(size 1 1)\n\t\t\t\t\t(thickness 0.15)') nb=blk[:pm.start()]+ptxt2+blk[pe:] del_spans.append(f['span']); MOVES[ref]=(f['x'],f['y'],nb); stats['mh-text']+=1 # ---------------- splice ---------------- new=src 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]) ss=sorted([(a,b) for a,b,_ in ops]) for (a1,b1),(a2,b2) in zip(ss,ss[1:]): assert b1<=a2, f'overlap {a1,b1} {a2,b2}' for a,b,t in ops: if t=='': a0=a while a0>0 and new[a0-1] in ' \t\n': a0-=1 new=new[:a0]+'\n\t'+new[b:] else: new=new[:a]+t+new[b:] end=new.rfind(')') blocks=[v[2] for v in MOVES.values()] new=new[:end]+'\t'+'\n\t'.join(blocks)+'\n'+new[end:] open(CUR,'w',encoding='utf-8').write(new) print(f'WROTE {CUR}: {len(src)} -> {len(new)}; balance={new.count("(")==new.count(")")}') for k,v in sorted(stats.items()): print(f' {k}: {v}')