Drogheda/scripts/2026-09-25_v6_T15b_dangling_stubs.py
2026-09-25 18:59:26 +00:00

66 lines
3.0 KiB
Python

#!/usr/bin/env python3
"""v6 T15b — delete the 4 dangling track stubs DRC reported on 25-Sep (warnings track_dangling).
Each target is identified by net + layer + the dangling end DRC printed + the reported length;
the script refuses to delete anything that doesn't match exactly one segment.
python3 T15b.py hbox-watchdog-v6.kicad_pcb # dry run
python3 T15b.py hbox-watchdog-v6.kicad_pcb --apply # writes .T15bbak
"""
import re, sys, math, shutil
STUBS = [ # (net, layer, dangling end x, y, length mm) — from the 25-Sep DRC report
('/3V3_ETH', 'F.Cu', 91.7375, 56.0800, 0.3889),
('/VEH5V', 'F.Cu', 187.5000, 116.1100, 0.7071),
('/RS485_B', 'B.Cu', 145.3500, 131.1682, 0.2107),
('/DOUT_4G', 'In2.Cu', 158.4000, 80.1000, 1.6000),
# second DRC pass: the RS485_B stub was a 3-segment chain off a THT pad going nowhere
('/RS485_B', 'B.Cu', 146.2000, 132.0182, 1.2021),
('/RS485_B', 'B.Cu', 146.2000, 132.3075, 0.2894),
]
def span(s, i):
d = 0; instr = False; j = i
while True:
c = s[j]
if c == '"' and s[j-1] != '\\': instr = not instr
elif not instr:
if c == '(': d += 1
elif c == ')':
d -= 1
if d == 0: return j + 1
j += 1
def main():
path = sys.argv[1]; apply = '--apply' in sys.argv
s0 = open(path).read(); cuts = []; log = []
segs = []
for m in re.finditer(r'\n\t\(segment\b', s0):
a = m.start() + 1; e = span(s0, a); b = s0[a:e]
st = tuple(map(float, re.search(r'\(start (-?[\d.]+) (-?[\d.]+)\)', b).groups()))
en = tuple(map(float, re.search(r'\(end (-?[\d.]+) (-?[\d.]+)\)', b).groups()))
segs.append(dict(a=a, e=e, st=st, en=en, l=re.search(r'\(layer "([^"]+)"\)', b).group(1),
n=re.search(r'\(net "([^"]*)"\)', b).group(1)))
for net, lay, x, y, L in STUBS:
hit = [g for g in segs if g['n'] == net and g['l'] == lay
and any(math.hypot(px - x, py - y) < 0.005 for px, py in (g['st'], g['en']))
and abs(math.dist(g['st'], g['en']) - L) < 0.002]
if not hit:
log.append(f'{net} {lay} @({x},{y}): not found (already removed?)'); continue
if len(hit) > 1: raise SystemExit(f'{net}: {len(hit)} candidates, refusing')
cuts.append(hit[0]); log.append(f'delete {net} {lay} {hit[0]["st"]}->{hit[0]["en"]} ({L} mm)')
s = s0
for g in sorted(cuts, key=lambda g: -g['a']):
s = s[:g['a'] - 1] + s[g['e']:] # also drops the preceding newline
t = re.sub(r'"(?:[^"\\]|\\.)*"', '', s); assert t.count('(') == t.count(')'), 'unbalanced'
assert len(re.findall(r'\n\t\(footprint "', s)) == len(re.findall(r'\n\t\(footprint "', s0))
print('\n'.join(log))
if not cuts: print('nothing to do'); return
if apply:
shutil.copy2(path, path + '.T15bbak'); open(path, 'w').write(s); print('APPLIED (backup .T15bbak)')
else:
print('DRY RUN — re-run with --apply')
if __name__ == '__main__':
main()