199 lines
8.7 KiB
Python
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}')
|