abse/museair-usb
USB-C–powered ESP32-C3 controller board with AP63203 buck conversion, AP2112K 3.3V regulation, I²C SEN62 sensor interface, status LED, buttons, UART/I²C headers, and extensive decoupling.
- Version
- 0.5.7
- License
- unset
- Stars
- 0
scripts/verify-production.py
"""Audit manufacturing board/BOM/CPL using KiCad's bundled Python pcbnew.
Run from project directory after exports. The report binds source, native
board and production files by checksum; it does not claim hardware testing.
"""
from pathlib import Path
import csv,json,hashlib,math,zipfile
import wx
wx_app=wx.App(False)
import pcbnew as p
root=Path(__file__).resolve().parent.parent
cs=json.loads((root/'dist/index/circuit.json').read_text())
b=p.LoadBoard(str(root/'fabrication-review/museair-usb.kicad_pcb'))
fmap={f.GetReference():f for f in b.GetFootprints()}
sc={x['source_component_id']:x for x in cs if x['type']=='source_component'}
nets={x['subcircuit_connectivity_map_key']:x['name'] for x in cs if x['type']=='source_net'}
expected={}
for x in cs:
if x['type']=='source_port' and x.get('source_component_id') in sc and 'pin_number' in x:
expected[(sc[x['source_component_id']]['name'],str(x['pin_number']))]=nets.get(x.get('subcircuit_connectivity_map_key'),'')
pad_count=0
for f in b.GetFootprints():
for pad in f.Pads():
key=(f.GetReference(),pad.GetNumber())
if key in expected:
assert pad.GetNetname()==expected[key],(key,pad.GetNetname(),expected[key])
pad_count+=1
if f.GetReference() in ('J4','J5'):
assert all(not pad.GetLayerSet().Contains(p.F_Paste) for pad in f.Pads())
origin=b.GetDesignSettings().GetAuxOrigin()
assert (p.ToMM(origin.x),p.ToMM(origin.y))==(77,135)
with (root/'order/cpl-jlcpcb.csv').open() as h:cpl=list(csv.DictReader(h))
with (root/'order/bom-jlcpcb.csv').open() as h:bom=list(csv.DictReader(h))
bom_refs={ref for row in bom for ref in row['Designator'].split(',')}
assert len(cpl)==33 and len(bom)==17
assert bom_refs=={row['Designator'] for row in cpl}
for row in cpl:
f=fmap[row['Designator']];pos=f.GetPosition()
assert abs(float(row['Mid X'].removesuffix('mm'))-p.ToMM(pos.x-origin.x))<0.00006,row
assert abs(float(row['Mid Y'].removesuffix('mm'))-p.ToMM(origin.y-pos.y))<0.00006,row
assert abs((float(row['Rotation'])-f.GetOrientationDegrees()+180)%360-180)<1e-6,row
assert row['Layer']=='top' and f.GetLayer()==p.F_Cu
# The separately purchased sensor is tied into the carrier. Verify its PCB
# carrier drill pattern, and prove it is absent from SMT assembly files.
sensor=fmap['U3'];assert sensor.GetLayer()==p.B_Cu
npth=[pad for pad in sensor.Pads() if pad.GetAttribute()==p.PAD_ATTRIB_NPTH]
assert len(npth)==4 and all(abs(p.ToMM(pad.GetDrillSize().x)-2.4)<1e-6 for pad in npth)
centers={(round(p.ToMM(pad.GetPosition().x)-100,4),round(100-p.ToMM(pad.GetPosition().y),4)) for pad in npth}
assert centers=={(-18,-24),(-18,19),(18,-24),(18,19)},centers
assert 'U3' not in bom_refs and 'U3' not in {r['Designator'] for r in cpl}
assert all(row['LCSC Part #']!='C3020625' for row in bom)
assert abs(p.ToMM(sensor.GetPosition().x)-100)<1e-6 and abs(p.ToMM(sensor.GetPosition().y)-102.5)<1e-6
cad=next(x for x in cs if x['type']=='cad_component' and sc[x['source_component_id']]['name']=='U3')
assert cad['position']=={'x':0,'y':-6,'z':-0.8},cad
model=json.loads((root/'mechanical/model-validation.json').read_text())
assert model['status']=='pass' and model['carrier_brep_valid']
assert model['minimum_nominal_sensor_to_pcb_gap_mm']==4
assert abs(model['sensor_bounds_mm'][1][2]+4)<1e-5
assert set(map(tuple,model['pcb_top_view_holes_mm']))==centers
drills=[t for t in b.GetTracks() if isinstance(t,p.PCB_VIA)]
assert all(t.GetDrillValue()>=p.FromMM(0.3) for t in drills)
assert all(math.hypot(a.GetPosition().x-c.GetPosition().x,a.GetPosition().y-c.GetPosition().y)-(a.GetDrillValue()+c.GetDrillValue())/2>=p.FromMM(0.2)-1000 for i,a in enumerate(drills) for c in drills[i+1:])
drc=json.loads((root/'validation/kicad-drc.json').read_text())
assert not drc['violations'] and not drc['unconnected_items']
gerbers=root/'order/gerbers'
files=[f for f in gerbers.iterdir() if f.suffix.lower() in {'.gtl','.gbl','.gts','.gbs','.gto','.gbo','.gtp','.gbp','.gm1','.drl','.gbrjob'}]
assert len([f for f in files if f.suffix=='.drl'])==2
assert {'gtl','gbl','gts','gbs','gto','gbo','gtp','gbp','gm1'}<={f.suffix[1:] for f in files}
with zipfile.ZipFile(root/'order/gerbers.zip','w',zipfile.ZIP_DEFLATED) as z:
for f in sorted(files):z.write(f,f.name)
report={'status':'pass','critical_native_pad_net_assignments_checked':pad_count,'placed_parts':len(cpl),'unique_jlcpcb_parts':len(bom),'origin_native_mm':[77,135],'origin_production':'lower-left, X right, Y up','cpl_positions_and_rotations_match_native':True,'bare_contacts_without_paste':True,'kicad_violations':0,'unconnected_items':0,'hardware_tested':False,'carrier_native_drill_pattern_matches_cad':True,'sensor_excluded_from_jlc_assembly':True,'sensor_mechanical_fit_physically_tested':False}
bound=[root/'validation/esp-regulator-heat-design.json',root/'imports/AP63203WU_7.tsx',root/'imports/SWPA4030S4R7MT.tsx',root/'imports/ProgrammingHeader.tsx',root/'scripts/repair-routing.mjs',root/'validation/programming-header-design.json',root/'index.circuit.tsx',root/'validation/antenna-and-sheet-revision.json',root/'validation/schematic-annotations.json',root/'validation/schematic-only-final-correction.json',root/'validation/tsci-cli-recheck.json',root/'validation/tscircuit-routing-recheck.json',root/'scripts/prepare-kicad.py',root/'scripts/verify.mjs',root/'components/JLCParts.tsx',root/'imports/SM06B_GHS_TB_LF__SN_.tsx',root/'dist/index/circuit.json',root/'fabrication-review/museair-usb.kicad_pcb',root/'validation/kicad-drc.json',root/'validation/tscircuit-checks.json',root/'validation/stock-live.json',root/'validation/supplier-specification-audit.json',root/'validation/native-cad-alignment.json',root/'order/bom-jlcpcb.csv',root/'order/cpl-jlcpcb.csv',root/'order/gerbers.zip',root/'components/SEN62.tsx',root/'mechanical/model-validation.json',root/'models/SEN62AssemblyModel.ts',root/'mechanical/sen62-assembly.glb',root/'mechanical/sen62-assembly-native.step',root/'validation/sensor-model-four-mm.json',root/'mechanical/sen62-carrier.step',root/'mechanical/sen62-carrier.stl',root/'mechanical/mounting-drawing.svg',*files,*sorted((root/'preview').glob('schematic-*.svg'))]
report['sha256']={str(f.relative_to(root)):hashlib.sha256(f.read_bytes()).hexdigest() for f in bound}
(root/'validation/production-audit.json').write_text(json.dumps(report,indent=2)+'\n')
print('PASS: native netlist, CPL, BOM, drill spacing, paste and independent DRC; saved Gerber ZIP')