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