mohan-bee/flight-controller-v2
A 64 mm STM32F405 flight-controller PCB with ICM-42688 IMU, SPI flash, USB-C/ESD, battery buck and 3.3 V regulation, four motor outputs, service I/O, and PETG standoff mounting hardware.
- Version
- 0.1.0
- License
- ISC
- Stars
- 0
tools/audit-assembly.py
import json, math, itertools
from pathlib import Path
import numpy as np
board=Path(__file__).resolve().parents[1]
root=board
c=json.loads((board/'dist/index/circuit.json').read_text())
sp={x['source_port_id']:x for x in c if x['type']=='source_port'}
ports={x['pcb_port_id']:sp[x['source_port_id']] for x in c if x['type']=='pcb_port'}
pcs={x['source_component_id']:x for x in c if x['type']=='pcb_component'}
rows=[];errors=[]
for comp in (x for x in c if x['type']=='source_component'):
pcb=pcs[comp['source_component_id']]
if pcb.get('do_not_place'):continue
part=comp['supplier_part_numbers']['jlcpcb'][0]
supplier=json.loads((root/'validation/supplier-data'/(part+'-product.json')).read_text())['result']
f=supplier['packageDetail']['dataStr'];h=f['head'];ox,oy=float(h['x']),float(h['y'])
pads=[]
for raw in f['shape']:
p=raw.split('~')
if p[0]=='PAD':pads.append(dict(number=p[8],x=(float(p[2])-ox)*.254,y=(oy-float(p[3]))*.254,width=float(p[4])*.254,height=float(p[5])*.254))
symbols={}
for raw in supplier['dataStr']['shape']:
if not raw.startswith('P~'):continue
chunks=raw.split('^^')
symbols[chunks[0].split('~')[3]]=chunks[3].split('~')[4].upper()
authored=[]
for p in c:
if p['type'] not in ['pcb_smtpad','pcb_plated_hole'] or p.get('pcb_component_id')!=pcb['pcb_component_id']:continue
port=ports[p['pcb_port_id']]
keys=set(port['port_hints'])
keys.add(str(port.get('pin_number','')))
if comp['ftype']=='simple_led':
role='anode' if any(k in keys for k in ['anode','pos']) else 'cathode'
allowed=['A','ANODE','+'] if role=='anode' else ['K','C','CATHODE','-']
semantic={number for number,label in symbols.items() if label in allowed}
if semantic:keys=semantic
candidates=[i for i,q in enumerate(pads) if q['number'] in keys]
if not candidates:errors.append([comp['name'],'Unmatched pad',sorted(keys)]);continue
authored.append((p,candidates))
best=None
for angle in [0,90,180,270]:
rad=math.radians(angle);rot=np.array([[math.cos(rad),-math.sin(rad)],[math.sin(rad),math.cos(rad)]])
for assignment in itertools.product(*(choices for p,choices in authored)):
if len(set(assignment))!=len(assignment):continue
target=np.array([[p['x'],p['y']] for p,choices in authored])
origin=np.array([[pads[i]['x'],pads[i]['y']] for i in assignment])
shifted=origin@rot.T
translation=np.mean(target-shifted,axis=0)
distance=np.linalg.norm(target-shifted-translation,axis=1)
score=float(np.max(distance))
if best is None or score<best[0]-1e-10:best=(score,angle,translation,assignment)
assert best,comp['name']
score,angle,translation,assignment=best
limit=.16 if comp['ftype'] in ['simple_resistor','simple_capacitor','simple_led'] else .055
if score>limit:errors.append([comp['name'],'Pad-center mismatch',score])
rows.append(dict(refdes=comp['name'],supplier=part,mpn=supplier['dataStr']['head']['c_para'].get('Manufacturer Part',supplier['title']),manufacturer=supplier['dataStr']['head']['c_para'].get('Manufacturer'),package=supplier['packageDetail']['title'],rotation_degrees=angle,x_mm=float(translation[0]),y_mm=float(translation[1]),max_pad_center_error_mm=score,matched_pads=len(authored),value=comp.get('display_resistance',comp.get('display_capacitance',comp.get('display_inductance',comp.get('manufacturer_part_number',supplier['title']))))))
report=dict(status='pass' if not errors else 'fail',parts=rows,errors=errors,method='Labeled supplier pad centers fitted at 0/90/180/270 degrees to authored PCB pad centers. LED pins mapped by anode/cathode roles. Coordinates use the exact selected supplier footprint origin. Nonpolar passive land patterns permit up to 0.16 mm pad-center difference; IC/connector limit 0.055 mm.')
(board/'validation/release/assembly-pose-audit.json').write_text(json.dumps(report,indent=2)+'\n')
print(report['status'],len(rows),'parts; errors:',errors)
print([p for p in rows if p['refdes'] in ['J_USB','D_LED','U1','U2']])
assert not errors