gokul/macro-six-usbc
USB-powered ATmega32U4 controller board with protected/switched 5 V power, six tactile keys, rotary encoder, reset/boot/ISP interfaces, power LED, and six daisy-chained SK6812 RGB LEDs driven through MOSFETs.
- Version
- 0.1.6
- License
- MIT
- Stars
- 0
manufacturing/verify-rotations.py
import json,math,re,pathlib,sys
j=json.load(open(sys.argv[1] if len(sys.argv)>1 else 'outputs/release/board.circuit.json'))
sources={e['source_component_id']:e for e in j if e['type']=='source_component'}
def centers(es,comp=None):
out={}
for p in es:
if p['type'] not in ['pcb_smtpad','pcb_plated_hole']:continue
if comp and p.get('pcb_component_id')!=comp:continue
hints=p.get('port_hints',[]);n=next((int(m.group(1)) for h in hints if (m:=re.fullmatch(r'(?:pin)?(\d+)',str(h)))),None)
if n is None:continue
if 'points' in p:
xx=[q['x'] for q in p['points']];yy=[q['y'] for q in p['points']];xy=((min(xx)+max(xx))/2,(min(yy)+max(yy))/2)
else:xy=(p['x'],p['y'])
out[n]=xy
return out
report=[]
for c in j:
if c['type']!='pcb_component' or c.get('do_not_place'):continue
s=sources[c['source_component_id']];part=s.get('supplier_part_numbers',{}).get('jlcpcb',[None])[0]
if not part:raise RuntimeError('No supplier '+s['name'])
sup=json.load(open('manufacturing/suppliers/supplier-'+part+'.json'));a=centers(j,c['pcb_component_id']);b=centers(sup);common=sorted(set(a)&set(b))
if len(common)<2:raise RuntimeError('Insufficient corresponding pads '+s['name'])
ax=sum(a[n][0] for n in common)/len(common);ay=sum(a[n][1] for n in common)/len(common)
bx=sum(b[n][0] for n in common)/len(common);by=sum(b[n][1] for n in common)/len(common)
dot=sum((a[n][0]-ax)*(b[n][0]-bx)+(a[n][1]-ay)*(b[n][1]-by) for n in common)
cross=sum((b[n][0]-bx)*(a[n][1]-ay)-(b[n][1]-by)*(a[n][0]-ax) for n in common)
angle=math.degrees(math.atan2(cross,dot))%360
deviation=abs((angle-c['rotation']+180)%360-180)
if deviation>0.1:raise RuntimeError(f'Wrong supplier frame {s["name"]}: {angle} vs {c["rotation"]}')
# Pad pitch may differ for generic nonpolar 0603s. Critical parts use exact geometry.
exact=s['name'].startswith(('U','Q','Y','SW','ENC','J','F','LED'))
rad=math.radians(angle);co=math.cos(rad);si=math.sin(rad)
residual=max(math.hypot(a[n][0]-ax-((b[n][0]-bx)*co-(b[n][1]-by)*si),a[n][1]-ay-((b[n][0]-bx)*si+(b[n][1]-by)*co)) for n in common)
if exact and residual>0.02:raise RuntimeError(f'Pad mapping mismatch {s["name"]}: {residual}')
report.append(dict(designator=s['name'],part=part,corresponding_numbered_pads=len(common),supplier_frame_rotation_deg=round(angle,5),placement_rotation_deg=c['rotation'],max_centered_pad_residual_mm=round(residual,6),exact_footprint=exact))
pathlib.Path('outputs/release/rotation-verification.json').write_text(json.dumps(report,indent=2))
print('Verified pad correspondence and rotation for',len(report),'placed components')