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/check-copper.py

import json,math,re,sys,collections
from shapely.geometry import Point,Polygon,LineString,box
from shapely.affinity import rotate,translate
from shapely.ops import unary_union
from shapely.validation import make_valid
from shapely.strtree import STRtree
j=json.load(open(sys.argv[1])); sources={e['source_trace_id']:e for e in j if e['type']=='source_trace'};ports={e['pcb_port_id']:e for e in j if e['type']=='pcb_port'}
netports={}
for e in sources.values():
 for p in e.get('connected_source_port_ids',[]):
  ns=e.get('connected_source_net_ids',[])
  if ns:netports[p]=ns[0]
traces_by_id={e['pcb_trace_id']:e for e in j if e['type']=='pcb_trace'}
def net(e):
 if e['type']=='pcb_via':return net(traces_by_id[e['pcb_trace_id']])
 if e.get('source_net_id'):return e['source_net_id']
 if e.get('connection_name'):return e['connection_name']
 s=sources.get(e.get('source_trace_id'),{});ns=s.get('connected_source_net_ids',[])
 if ns:return ns[0]
 for p in s.get('connected_source_port_ids',[]):
  if p in netports:return netports[p]
 p=ports.get(e.get('pcb_port_id'),{}).get('source_port_id')
 return netports.get(p,'unconnected:'+e.get('pcb_smtpad_id',e.get('pcb_plated_hole_id','unknown')))
def shape(e,drill=False):
 if e.get('shape')=='brep':
  b=e['brep_shape'];return make_valid(Polygon([(p['x'],p['y']) for p in b['outer_ring']['vertices']], [[(p['x'],p['y']) for p in h['vertices']] for h in b.get('inner_rings',[])]))
 if 'points' in e:return make_valid(Polygon([(p['x'],p['y']) for p in e['points']]))
 x=e.get('x',e.get('center',{}).get('x'));y=e.get('y',e.get('center',{}).get('y'));s=e.get('hole_shape',e.get('shape','circle')) if drill else e.get('shape','circle')
 if drill:
  w=e.get('hole_width',e.get('hole_diameter'));h=e.get('hole_height',e.get('hole_diameter'))
 else:
  w=e.get('outer_width',e.get('width',e.get('outer_diameter',e.get('radius',0)*2)));h=e.get('outer_height',e.get('height',w))
 if s=='circle' or e['type']=='pcb_via':g=Point(0,0).buffer(w/2,resolution=128)
 elif 'pill' in s or s=='oval':
  r=min(w,h)/2;delta=abs(w-h)/2
  axis=[(-delta,0),(delta,0)] if w>=h else [(0,-delta),(0,delta)]
  g=LineString(axis).buffer(r,resolution=128) if delta>1e-9 else Point(0,0).buffer(r,resolution=128)
 else:g=box(-w/2,-h/2,w/2,h/2)
 return translate(rotate(g,e.get('ccw_rotation',e.get('rotation',0)),origin=(0,0)),xoff=x,yoff=y)
features=[];drills=[]
for e in j:
 t=e['type'];key=e.get('pcb_trace_id',e.get('pcb_smtpad_id',e.get('pcb_plated_hole_id',e.get('pcb_via_id',e.get('pcb_copper_pour_id',e.get('pcb_hole_id'))))))
 if t=='pcb_trace':
  bylayer=collections.defaultdict(list)
  for a,b in zip(e['route'],e['route'][1:]):
   layer=a.get('layer',b.get('layer'));w=max(a.get('width',0),b.get('width',0))
   if not layer or not w:continue
   if a.get('layer') and b.get('layer') and a['layer']!=b['layer']:raise RuntimeError('Unrepresented layer change '+key)
   if math.hypot(a['x']-b['x'],a['y']-b['y'])<1e-10:continue
   bylayer[layer].append(LineString([(a['x'],a['y']),(b['x'],b['y'])]).buffer(w/2,resolution=32))
  for l,gg in bylayer.items():features.append(dict(id=key,kind='trace',net=net(e),layer=l,geometry=unary_union(gg)))
 elif t in ['pcb_smtpad','pcb_plated_hole','pcb_via','pcb_copper_pour']:
  g=shape(e);layers=e.get('layers',[e.get('layer')]);kind={'pcb_smtpad':'pad','pcb_plated_hole':'pth','pcb_via':'via','pcb_copper_pour':'pour'}[t]
  if t in ['pcb_plated_hole','pcb_via']:
   hole=shape(e,True);drills.append(dict(id=key,kind=kind,geometry=hole));g=g.difference(hole)
  for l in layers:features.append(dict(id=key,kind=kind,net=net(e),layer=l,geometry=g))
 elif t=='pcb_hole':drills.append(dict(id=key,kind='npth',geometry=shape(dict(e,hole_diameter=e.get('hole_diameter',e.get('diameter'))),True)))
errors=[];min_gap=10
for layer in ['top','bottom']:
 fs=[f for f in features if f['layer']==layer];tree=STRtree([f['geometry'] for f in fs])
 for i,a in enumerate(fs):
  for k in tree.query(a['geometry'].buffer(.151)):
   k=int(k)
   if k<=i:continue
   b=fs[k]
   if a['net']==b['net']:continue
   kinds={a['kind'],b['kind']};req=.15 if 'via' in kinds and ('pad' in kinds or 'pth' in kinds or kinds=={'via'}) else (.15 if kinds<= {'pad','pth'} else .1)
   gap=a['geometry'].distance(b['geometry']);min_gap=min(min_gap,gap)
   if gap<req-.003:errors.append(dict(type='copper_clearance',layer=layer,ids=[a['id'],b['id']],nets=[a['net'],b['net']],gap=round(gap,6),required=req))
 for h in drills:
  if h['kind']!='npth':continue
  for k in tree.query(h['geometry'].buffer(.201)):
   a=fs[int(k)];gap=a['geometry'].distance(h['geometry'])
   if gap<.2-.003:errors.append(dict(type='npth_clearance',layer=layer,ids=[h['id'],a['id']],gap=round(gap,6),required=.2))
board=next(e for e in j if e['type']=='pcb_board');outline=Polygon([(p['x'],p['y']) for p in board['outline']])
for f in features:
 if not outline.buffer(-.297).covers(f['geometry']):errors.append(dict(type='board_edge_clearance',id=f['id'],layer=f['layer']))
for i,a in enumerate(drills):
 for b in drills[i+1:]:
  if a['kind']=='npth' or b['kind']=='npth':continue
  req=.2 if a['kind']==b['kind']=='via' else .45
  gap=a['geometry'].distance(b['geometry'])
  if gap<req-.003:errors.append(dict(type='plated_drill_clearance',ids=[a['id'],b['id']],gap=round(gap,6),required=req))
result=dict(features=len(features),drills=len(drills),minimum_different_net_copper_gap=round(min_gap,6),errors=errors)
json.dump(result,open('outputs/release/independent-copper-check.json','w'),indent=2)
print('Independent geometry:',len(features),'copper features;',len(drills),'drills;',len(errors),'violations')
print(collections.Counter(e['type'] for e in errors))
for e in errors[:20]:print(e)

if errors:sys.exit(1)