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')