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/r9-filled-via-process.py
"""Validate required R9 filled/capped ground vias; no Circuit JSON edits.
Circuit/world XY in millimetres, +Z top. Gerbers cannot prove actual filling;
supplier process confirmation is an independent mandatory release gate.
"""
import json, math, importlib.util, sys, hashlib, argparse
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1]
sys.path.insert(0,str(ROOT/'scripts'))
spec=importlib.util.spec_from_file_location('r9_filled_geometry',ROOT/'scripts/manufacturing-audit.py')
GEOMETRY=importlib.util.module_from_spec(spec);sys.modules[spec.name]=GEOMETRY;spec.loader.exec_module(GEOMETRY)
def classify_process(circuit, process):
required=process['intentional_via_in_pad']
if required['required_process'] != 'epoxy_filled_copper_capped':
raise ValueError('R9 thermal vias require epoxy filling and plated copper caps')
component=next(e for e in circuit if e['type']=='source_component' and e['name']==required['component'])
if component['manufacturer_part_number']!='ESP32-C3FH4' or component['supplier_part_numbers']['jlcpcb']!=['C2858491']:
raise ValueError('Filled process is scoped to exact U1 bare chip')
pcb=next(e for e in circuit if e['type']=='pcb_component' and e['source_component_id']==component['source_component_id'])
pads=[e for e in circuit if e['type']=='pcb_smtpad' and e['pcb_component_id']==pcb['pcb_component_id'] and required['terminal'] in e['port_hints']]
if len(pads)!=1:raise ValueError('Thermal pad is not unique')
copper,drills,widths=GEOMETRY.make_features(circuit,'gerber')
pad=next(f for f in copper if f.id==pads[0]['pcb_smtpad_id'])
gnd=next(e for e in circuit if e['type']=='source_net' and e['name']==required['net'])
if required['net']!='GND' or pad.net!=gnd['subcircuit_connectivity_map_key']:
raise ValueError('Thermal pad must be physically on declared GND')
centers=required['centers_mm']
if len(centers)!=4 or len({tuple(c) for c in centers})!=4:raise ValueError('Require exactly four unique thermal vias')
vias=[e for e in circuit if e['type']=='pcb_via']
selected=[]
for x,y in centers:
matches=[v for v in vias if math.hypot(v['x']-x,v['y']-y)<1e-6]
if len(matches)!=1:raise ValueError('Declared thermal via missing or duplicated')
via=matches[0]
if sorted(via['layers'])!=['bottom','top'] or abs(via['hole_diameter']-.3)>1e-6 or abs(via['outer_diameter']-.45)>1e-6:
raise ValueError('Thermal vias must be 0.3/0.45mm through vias')
drill=next(d for d in drills if d.id==via['pcb_via_id'])
if drill.net!=pad.net or not pad.geometry.covers(drill.outer):
raise ValueError('Thermal via must be fully inside same-net exposed ground pad')
drill.filled_capped_pad_id=pad.id
selected.append(via['pcb_via_id'])
# Every via follows the user's two-layer fabrication geometry, not just
# the four intentional filled/capped exposed-pad vias.
for via in vias:
if sorted(via['layers']) != ['bottom', 'top'] or abs(via['hole_diameter']-.3)>1e-6 or abs(via['outer_diameter']-.45)>1e-6:
raise ValueError(f"Nonconforming through-via geometry: {via['pcb_via_id']}")
# Native board policy allows intentional via-in-pad; this independent check
# requires exact process membership for every actual SMT overlap.
for drill in drills:
if drill.kind!='via':continue
for smt in (f for f in copper if f.kind=='smt' and f.layer in drill.layers):
if drill.geometry.intersects(smt.geometry) and drill.filled_capped_pad_id!=smt.id:
raise ValueError(f'Unqualified via-in-pad: {drill.id}/{smt.id}')
return copper,drills,widths,selected
def review(args):
raw=args.circuit.read_bytes();circuit=json.loads(raw);process=json.loads(args.process.read_text())
native_errors=[e for e in circuit if e['type'].endswith('_error')]
if native_errors and not args.placement_only:
raise ValueError('Routed process review requires error-free native output; use --placement-only for scoped placement diagnostics')
copper,drills,widths,selected=classify_process(circuit,process)
report=GEOMETRY.audit(copper,drills,widths)
report.update({'circuit_sha256':hashlib.sha256(raw).hexdigest(),'required_filled_capped_vias':selected,'supplier_process_confirmed':process['intentional_via_in_pad']['supplier_processed_preview_confirmed'],'cost_quote_confirmed':process['intentional_via_in_pad']['cost_quote_confirmed'],'release_gate':'BLOCKED_PENDING_SUPPLIER_PROCESS_AND_COST_REVIEW','scope':'Local geometry conditional on the explicitly required filled/capped process. No physical fabrication or supplier acceptance is claimed.'})
report.update({'placement_only':args.placement_only,'native_error_count':len(native_errors),'routed_geometry_qualified':not args.placement_only and not native_errors and not report['failures']})
args.output.write_text(json.dumps(report,indent=2)+'\n')
print(f"R9 conditional manufacturing geometry: {len(report['failures'])} failures; {len(selected)} required filled/capped vias; supplier/cost gate pending")
return bool(report['failures'])
if __name__=='__main__':
p=argparse.ArgumentParser();p.add_argument('--circuit',type=Path,default=ROOT/'dist/index/circuit.json');p.add_argument('--process',type=Path,default=ROOT/'fabrication/r9-process.json');p.add_argument('--output',type=Path,required=True);p.add_argument('--placement-only',action='store_true')
raise SystemExit(review(p.parse_args()))