Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJ4eAB6zjQT49RVwpgfKdt
80 lines
3.5 KiB
Python
80 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
||
"""v6 T16 — square up the 4 mounting holes and give them unique references.
|
||
|
||
Found 25-Sep: the board (140 x 100 mm, Edge.Cuts x 60.19–200.19, y 40.32–140.32) had
|
||
MH5 (65.2, 45.3) MH6 (65.2, 135.3) <- west pair, same position since v4/v5 (proven)
|
||
MH6 (195.5, 45.0) MH6 (195.5, 135.5) <- east pair off by 0.3 / 0.2 / 0.5 mm
|
||
so the pattern was 130.3 x 90.0 on the west and 90.5 on the east, and three holes shared "MH6".
|
||
|
||
Fix: keep the proven west pair and mirror it -> a clean 130.0 x 90.0 mm rectangle, every hole
|
||
~5.0 mm from its two edges, centred on the board:
|
||
MH5 (65.2, 45.3) MH6 (65.2, 135.3) MH7 (195.2, 45.3) MH8 (195.2, 135.3)
|
||
|
||
Only the footprint (at …) line and the Reference value change; the holes keep their UUIDs.
|
||
|
||
python3 T16.py hbox-watchdog-v6.kicad_pcb # dry run
|
||
python3 T16.py hbox-watchdog-v6.kicad_pcb --apply # writes .T16bak
|
||
"""
|
||
import re, sys, shutil
|
||
|
||
TARGET = [ # (current ref, current x, y) -> (new ref, x, y)
|
||
(('MH5', 65.2, 45.3), ('MH5', 65.2, 45.3)),
|
||
(('MH6', 65.2, 135.3), ('MH6', 65.2, 135.3)),
|
||
(('MH6', 195.5, 45.0), ('MH7', 195.2, 45.3)),
|
||
(('MH6', 195.5, 135.5), ('MH8', 195.2, 135.3)),
|
||
]
|
||
|
||
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 holes(s):
|
||
out = []
|
||
for m in re.finditer(r'\n\t\(footprint "MountingHole:[^"]*"', s):
|
||
a = m.start() + 1; e = span(s, a); b = s[a:e]
|
||
at = re.search(r'\n\t\t\(at (-?[\d.]+) (-?[\d.]+)([^)]*)\)', b)
|
||
ref = re.search(r'\(property "Reference" "([^"]*)"', b).group(1)
|
||
out.append(dict(a=a, e=e, ref=ref, x=float(at.group(1)), y=float(at.group(2)), rot=at.group(3)))
|
||
return out
|
||
|
||
def main():
|
||
path = sys.argv[1]; apply = '--apply' in sys.argv
|
||
s0 = open(path).read(); s = s0; ch = []
|
||
hs = holes(s0)
|
||
assert len(hs) == 4, f'expected 4 mounting holes, found {len(hs)}'
|
||
edits = []
|
||
for (r0, x0, y0), (r1, x1, y1) in TARGET:
|
||
h = [h for h in hs if abs(h['x'] - x1) < 1e-3 and abs(h['y'] - y1) < 1e-3 and h['ref'] == r1]
|
||
if h: continue # already in place
|
||
h = [h for h in hs if abs(h['x'] - x0) < 1e-3 and abs(h['y'] - y0) < 1e-3 and h['ref'] == r0]
|
||
if len(h) != 1: raise SystemExit(f'cannot identify hole {r0} at ({x0},{y0})')
|
||
edits.append((h[0], r1, x1, y1))
|
||
for h, r1, x1, y1 in sorted(edits, key=lambda t: -t[0]['a']):
|
||
b = s[h['a']:h['e']]
|
||
b = re.sub(r'\n\t\t\(at (-?[\d.]+) (-?[\d.]+)([^)]*)\)', f'\n\t\t(at {x1:g} {y1:g}{h["rot"]})', b, count=1)
|
||
b = b.replace(f'(property "Reference" "{h["ref"]}"', f'(property "Reference" "{r1}"', 1)
|
||
s = s[:h['a']] + b + s[h['e']:]
|
||
ch.append(f'{h["ref"]} ({h["x"]:g},{h["y"]:g}) -> {r1} ({x1:g},{y1:g})')
|
||
hs1 = holes(s)
|
||
refs = [h['ref'] for h in hs1]
|
||
assert sorted(refs) == ['MH5', 'MH6', 'MH7', 'MH8'], refs
|
||
xs = sorted({h['x'] for h in hs1}); ys = sorted({h['y'] for h in hs1})
|
||
assert len(xs) == 2 and len(ys) == 2, 'holes not on a rectangle'
|
||
print('\n'.join(ch) or 'nothing to do (already applied)')
|
||
print(f'pattern {xs[1]-xs[0]:.2f} x {ys[1]-ys[0]:.2f} mm')
|
||
if ch and apply:
|
||
shutil.copy2(path, path + '.T16bak'); open(path, 'w').write(s); print('APPLIED (backup .T16bak)')
|
||
elif ch:
|
||
print('DRY RUN — re-run with --apply')
|
||
|
||
if __name__ == '__main__':
|
||
main()
|