MustafaMulla29/rp2040-bldc-motor-controller-new

A USB‑C PD–powered RP2040 BLDC motor controller PCB with 3.3 V logic, MCT8329A sensorless commutation, three MOSFET half-bridges, protected power switching, USB data, controls, and motor connectors.

Version
0.3.82
License
unset
Stars
0

scripts/pcb-audit/audit-copper.py

"""Independent physical copper union; does not modify the generated artifact."""
import geometry as R
import json,sys,collections,hashlib,math
from pathlib import Path
from shapely.geometry import Point,LineString,Polygon
from shapely.strtree import STRtree
B=R.BASE;path=Path(sys.argv[1]) if len(sys.argv)>1 else B/'routed.circuit.json';raw=path.read_bytes();c=json.loads(raw)
of=lambda k:[x for x in c if x['type']==k]
nk={x['subcircuit_connectivity_map_key']:x['name'] for x in of('source_net')};ni={x['source_net_id']:x['name'] for x in of('source_net')}
sp={p['source_port_id']:p for p in of('source_port')};pp={p['pcb_port_id']:p for p in of('pcb_port')};sc={p['source_component_id']:p['name'] for p in of('source_component')};st={t['source_trace_id']:t for t in of('source_trace')}
nodes=[]
def add(net,shape,layers,kind,id,label=None,width=None):nodes.append(dict(net=net,shape=shape,layers=set(layers),kind=kind,id=id,label=label,width=width))
for p in of('pcb_smtpad')+of('pcb_plated_hole'):
 s=sp[pp[p['pcb_port_id']]['source_port_id']];net=nk.get(s.get('subcircuit_connectivity_map_key'),'NC_'+s['source_port_id']);label=sc[s['source_component_id']]+'.'+str(s['pin_number']);add(net,R.padshape(p),p.get('layers',[p.get('layer','top')]),'pad',p.get('pcb_smtpad_id',p.get('pcb_plated_hole_id')),label)
for t in of('pcb_trace'):
 net=ni.get(t.get('connection_name')) or nk.get(st.get(t.get('source_trace_id'),{}).get('subcircuit_connectivity_map_key'))
 if net is None:raise RuntimeError(('Unknown trace net',t['pcb_trace_id']))
 for i,(a,b) in enumerate(zip(t['route'],t['route'][1:])):
  if a['route_type']!=b['route_type'] or a['route_type']!='wire' or a['layer']!=b['layer'] or math.dist(R.xy(a),R.xy(b))<1e-8:continue
  w=min(a['width'],b['width']);add(net,LineString([R.xy(a),R.xy(b)]).buffer(w/2,quad_segs=12),[a['layer']],'trace',t['pcb_trace_id'],width=w)
for v in of('pcb_via'):
 net=nk.get(v.get('subcircuit_connectivity_map_key')) or ni.get(v.get('source_net_id'));assert net,(v,'no net')
 add(net,Point(v['x'],v['y']).buffer(v['outer_diameter']/2,quad_segs=32),v['layers'],'via',v['pcb_via_id'])
for p in of('pcb_copper_pour'):
 if p['shape']!='brep':raise RuntimeError(('Unhandled pour',p['shape']))
 b=p['brep_shape'];shape=Polygon([R.xy(v) for v in b['outer_ring']['vertices']],[[R.xy(v) for v in ring['vertices']] for ring in b.get('inner_rings',[])]);add(ni[p['source_net_id']],shape,[p['layer']],'pour',p['pcb_copper_pour_id'])
parent=list(range(len(nodes)))
def root(i):
 while i!=parent[i]:parent[i]=parent[parent[i]];i=parent[i]
 return i
def union(a,b):parent[root(a)]=root(b)
bylayer=collections.defaultdict(list)
for i,n in enumerate(nodes):
 for l in n['layers']:bylayer[(n['net'],l)].append(i)
for key,indices in bylayer.items():
 tree=STRtree([nodes[i]['shape'] for i in indices])
 for j,i in enumerate(indices):
  for jj in tree.query(nodes[i]['shape'].buffer(1e-6)):
   k=indices[int(jj)]
   if k>=i or root(i)==root(k):continue
   if nodes[i]['shape'].distance(nodes[k]['shape'])<=1e-6:union(i,k)
groups=collections.defaultdict(list)
for i,n in enumerate(nodes):groups[(n['net'],root(i))].append(i)
report=[]
for (net,r),indices in groups.items():
 ns=[nodes[i] for i in indices];pins=[n['label'] for n in ns if n['kind']=='pad'];report.append({'net':net,'pins':pins,'items':list(dict.fromkeys(n['id'] for n in ns)),'bounds':list(__import__('shapely').union_all([n['shape'] for n in ns]).bounds)})
bynet=collections.defaultdict(list)
for g in report:bynet[g['net']].append(g)
disconnected={n:[g for g in gs if g['pins']] for n,gs in bynet.items() if sum(bool(g['pins']) for g in gs)>1}
floating=[g for g in report if not g['pins']]
out={'sha256':hashlib.sha256(raw).hexdigest(),'disconnected':disconnected,'floating':floating,'groups':report}
(B/'copper-connectivity.json').write_text(json.dumps(out,indent=2)+'\n')
print('Disconnected nets',len(disconnected),'floating copper groups',len(floating))
for n,gs in disconnected.items():print(n,[g['pins'] for g in gs])
for g in floating:print('Floating',g['net'],g['items'][:5])