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

199 lines
8.7 KiB
Python

#!/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]<x<BAY[2] and BAY[1]<y<BAY[3] for x,y in coords)
if m.group(1)=='text':
tm=re.match(r'\(gr_text\s+"((?:[^"\\]|\\.)*)"', blk)
if inside or (tm and 'BLACKPILL' in tm.group(1).upper()):
del_spans.append((a,e)); stats['silk-del']+=1
elif inside:
del_spans.append((a,e)); stats['silk-gfx-del']+=1
for old,new in [('EINARBoard 2.0','EINARBoard 3.0'), ('HBOX v4 - 2026-07','HBOX v5 - 2026-08')]:
i=src.find(f'"{old}')
if i>=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}')