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)