imrishabh18/rp2040-motor-controller
An RP2040-based NEMA17 stepper-motor controller with USB-C programming and PD motor-power negotiation, DRV8825 dual H-bridge drive, current/temperature telemetry, protected power filtering, status RGB LED, buzzer alarm, and optional magnetic encoder.
- Version
- 1.0.42
- License
- unset
- Stars
- 3
scripts/audit-exported-clearances.py
"""Check NPTH clearances in the delivered Gerbers and Excellon, not just JSON."""
import math
import hashlib
import json
import re
import zipfile
from pathlib import Path
from shapely.geometry import Point, Polygon
from gerber_geometry import gerber_geometry
root = Path(__file__).resolve().parents[1]
circuit_file = root / 'dist/index/circuit.json'
archive = root / 'dist/index/gerbers.zip'
circuit = json.loads(circuit_file.read_text())
rules = json.loads((root / 'engineering/jlcpcb-clearance-rules.json').read_text())['minimumMm']
npths = [e for e in circuit if e['type'] == 'pcb_hole']
board = next(e for e in circuit if e['type'] == 'pcb_board')
outline = Polygon([(p['x'], p['y']) for p in board['outline']])
checks = []
with zipfile.ZipFile(archive) as z:
drill = z.read('drill_npth.drl').decode()
assert re.search(r'^METRIC$', drill, re.M) and 'G90' in drill
tools = {int(n): float(d) for n, d in re.findall(r'^T(\d+)C([\d.]+)', drill, re.M)}
selected = None; actual = []
for line in drill.splitlines():
if match := re.fullmatch(r'T(\d+)', line):
selected = tools[int(match[1])]
elif match := re.fullmatch(r'X(-?[\d.]+)Y(-?[\d.]+)', line):
assert selected is not None
actual.append((float(match[1]), float(match[2]), selected))
expected = [(round(e['x'], 4), round(e['y'], 4), round(e['hole_diameter'], 6)) for e in npths]
assert sorted(actual) == sorted(expected), 'Delivered NPTH drill positions/sizes differ from PCB'
# Verify the eight connector retaining slots survived the Gerber/Excellon
# exporter with supplier widths, lengths, positions, and orientation.
plated = z.read('drill-L1-L4.drl').decode()
ptools = {int(n): float(d) for n,d in re.findall(r'^T(\d+)C([\d.]+)', plated, re.M)}
selected = None; start = None; slots = []
for line in plated.splitlines():
if match := re.fullmatch(r'T(\d+)', line):selected = ptools[int(match[1])]
elif match := re.fullmatch(r'X(-?[\d.]+)Y(-?[\d.]+)', line):start = (float(match[1]),float(match[2]))
elif match := re.fullmatch(r'G85X(-?[\d.]+)Y(-?[\d.]+)', line):
assert start is not None and selected is not None
end = (float(match[1]),float(match[2]))
slots.append(((start[0]+end[0])/2,(start[1]+end[1])/2,
selected,math.dist(start,end)+selected))
usb_sources = {e['source_component_id'] for e in circuit if e['type']=='source_component' and e['name'] in ['J_USB','J_MOTOR_USB']}
usb_pcbs = {e['pcb_component_id'] for e in circuit if e['type']=='pcb_component' and e['source_component_id'] in usb_sources}
expected_slots = [e for e in circuit if e['type']=='pcb_plated_hole' and e.get('pcb_component_id') in usb_pcbs]
assert len(slots)==len(expected_slots)==8
for e in expected_slots:
assert e['shape']=='pill' and e.get('ccw_rotation',0)%180==0
# This exporter rounds G85 endpoints to 0.001 mm (starts: 0.0001 mm).
match = [a for a in slots if math.dist(a[:2],(e['x'],e['y']))<.0005]
assert len(match)==1
assert abs(match[0][2]-e['hole_width'])<.00001 and abs(match[0][3]-e['hole_height'])<.0006
for layer, filename in [('top', 'F_Cu.gbr'), ('inner1', 'In1_Cu.gbr'),
('inner2', 'In2_Cu.gbr'), ('bottom', 'B_Cu.gbr')]:
copper = gerber_geometry(z.read(filename).decode())
for e in npths:
hole = Point(e['x'], e['y']).buffer(e['hole_diameter'] / 2, quad_segs=64)
gap = copper.distance(hole)
checks.append({'hole': e['pcb_hole_id'], 'layer': layer,
'distanceMm': gap, 'requiredMm': rules['npthToCopper']})
edge_gap = copper.distance(outline.boundary)
assert outline.covers(copper) and edge_gap >= rules['copperToBoardEdge'] - 1e-6
checks.append({'boardEdge': True, 'layer': layer, 'distanceMm': edge_gap,
'requiredMm': rules['copperToBoardEdge']})
violations = [c for c in checks if c['distanceMm'] < c['requiredMm'] - 1e-6]
report = {'circuitJsonSha256': hashlib.sha256(circuit_file.read_bytes()).hexdigest(),
'gerbersSha256': hashlib.sha256(archive.read_bytes()).hexdigest(),
'npthDrillsVerified': len(actual), 'usbPlatedSlotsVerified': len(expected_slots), 'checks': checks, 'violations': violations}
(root / 'reports/exported-clearances.json').write_text(json.dumps(report, indent=2) + '\n')
print(json.dumps(report, indent=2))
assert not violations, 'Exported copper fails clearance checks'
