AnasSarkiz/magnetic-shutter-remote-r8
A USB‑C rechargeable battery-powered ESP32‑C3 wireless controller with Li‑ion charging, protected 3.3‑V buck‑boost regulation, thermal/safety monitoring, status LEDs, buttons, programming UART, crystal clock, and 2.4‑GHz antenna matching network.
- Version
- 0.4.0-prototype
- License
- unset
- Stars
- 0
scripts/review-r9-declared-clearances.py
"""Check actual copper against R9's declared rules, independently of the router.
Board XY millimetres, +Z top. This supplements the fabrication-capability audit;
it does not replace process classification, connectivity or native checks.
"""
import argparse
import hashlib
import importlib.util
import json
import sys
from pathlib import Path
from shapely.strtree import STRtree
ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location('r9_declared_process', ROOT/'scripts/r9-filled-via-process.py')
PROCESS = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = PROCESS
SPEC.loader.exec_module(PROCESS)
EPS = PROCESS.GEOMETRY.EPS
def declared_measurements(copper, drills, board):
limits = {
'foreign_trace_copper': board['min_trace_to_pad_edge_clearance'],
'via_smt_copper': board['min_via_edge_to_pad_edge_clearance'],
'via_hole_spacing': board['min_via_hole_edge_to_via_hole_edge_clearance'],
}
measurements = []
def record(rule, first, second, gap):
measurements.append({'rule':rule, 'a':first, 'b':second,
'gap_mm':gap, 'minimum_mm':limits[rule]})
geometries = [feature.geometry for feature in copper]
index = STRtree(geometries)
for i, first in enumerate(copper):
for j in index.query(first.geometry.buffer(limits['foreign_trace_copper']+EPS)):
j = int(j)
if j <= i:
continue
second = copper[j]
if first.layer != second.layer or 'track' not in (first.kind,second.kind):
continue
if first.net is not None and first.net == second.net:
continue
record('foreign_trace_copper', first.id, second.id,
first.geometry.distance(second.geometry))
smt = [feature for feature in copper if feature.kind == 'smt']
pad_index = STRtree([feature.geometry for feature in smt])
vias = [drill for drill in drills if drill.kind == 'via']
for via in vias:
if via.outer is None:
raise ValueError('Via has no verified copper annulus')
for j in pad_index.query(via.outer.buffer(limits['via_smt_copper']+EPS)):
pad = smt[int(j)]
if pad.layer not in via.layers or via.filled_capped_pad_id == pad.id:
continue
# Net identity alone never exempts a via from a component land.
record('via_smt_copper', via.id, pad.id, via.outer.distance(pad.geometry))
for i, first in enumerate(vias):
for second in vias[:i]:
if not set(first.layers).intersection(second.layers):
continue
if first.geometry.equals(second.geometry) and first.net is not None and first.net == second.net:
continue # Multiple source routes share one physical same-net barrel.
gap = first.geometry.distance(second.geometry)
if gap <= limits['via_hole_spacing']+EPS:
record('via_hole_spacing', first.id, second.id, gap)
return measurements
def review(args):
raw = args.circuit.read_bytes()
circuit = json.loads(raw)
if any(element['type'].endswith('_error') for element in circuit):
raise ValueError('Declared-rule review requires an error-free native routed candidate')
if not any(element['type']=='pcb_trace' for element in circuit):
raise ValueError('Declared-rule review requires actual routed copper')
board = next(element for element in circuit if element['type']=='pcb_board')
copper, drills, widths, capped = PROCESS.classify_process(circuit,json.loads(args.process.read_text()))
measurements = declared_measurements(copper,drills,board)
failures = [row for row in measurements if row['gap_mm'] <= row['minimum_mm']+EPS]
report = {'circuit_sha256':hashlib.sha256(raw).hexdigest(), 'failures':failures,
'measurements':measurements,'required_filled_capped_vias':capped,
'curve_error_bound_mm':EPS,
'scope':'Actual nominal copper/drill geometry against declared board rules; supplier/physical qualification remains separate.'}
args.output.write_text(json.dumps(report,indent=2)+'\n')
print('R9 declared-rule clearance failures:',len(failures))
for row in failures[:15]:
print(row)
return bool(failures)
if __name__=='__main__':
parser=argparse.ArgumentParser()
parser.add_argument('--circuit',type=Path,default=ROOT/'dist/index/circuit.json')
parser.add_argument('--process',type=Path,default=ROOT/'fabrication/r9-process.json')
parser.add_argument('--output',type=Path,required=True)
raise SystemExit(review(parser.parse_args()))