mohan-bee/gimbal-one-budget-pcb
A 4-layer STM32-based gimbal controller PCB with an onboard LSM6DS3TR-C IMU, three DRV8313 3-phase motor drivers, protected 3S battery input, 3.3 V buck regulation, and SWD/UART control interfaces.
- Version
- 0.1.0
- License
- unset
- Stars
- 0
audit-geometry.py
PCB
Schematic
"""Independent copper clearance audit of the saved router output (no pours)."""
import json,pathlib,math
from collections import defaultdict
from shapely.geometry import Point,LineString,box,Polygon
from shapely import affinity
from shapely.strtree import STRtree
r=pathlib.Path(__file__).resolve().parent;d=json.loads((r/'dist/controller/circuit.json').read_text())
ts=json.loads((r/'routing/checked-traces.json').read_text())
ref=json.loads((r/'routing/reference-input.json').read_text());pn={p['pcb_port_id']:c['name'] for c in ref['connections'] for p in c['pointsToConnect']}
names={x['source_net_id']:x['name'] for x in d if x['type']=='source_net'}
items=[]
def add(net,layer,g,tag):items.append((net,layer,g,tag))
for p in d:
if p['type'] not in ['pcb_smtpad','pcb_plated_hole','pcb_via']:continue
if p['type']=='pcb_via':
if not p.get('source_net_id'):continue
g=Point(p['x'],p['y']).buffer(p['outer_diameter']/2);net=p['source_net_id'];layers=['top','inner1','inner2','bottom']
else:
net=pn.get(p.get('pcb_port_id'),p.get('pcb_port_id'));layers=[p.get('layer','top')] if p['type']=='pcb_smtpad' else ['top','inner1','inner2','bottom']
w=p.get('width',p.get('outer_width',p.get('rect_pad_width',p.get('outer_diameter',p.get('radius',.5)*2))));h=p.get('height',p.get('outer_height',p.get('rect_pad_height',w)))
if p['shape']=='circle':g=Point(0,0).buffer(w/2)
elif p['shape']=='pill':
g=LineString([(0,-(h-w)/2),(0,(h-w)/2)]).buffer(w/2) if h>=w else LineString([(-(w-h)/2,0),((w-h)/2,0)]).buffer(h/2)
else:g=box(-w/2,-h/2,w/2,h/2)
g=affinity.translate(affinity.rotate(g,p.get('ccw_rotation',0)),p['x'],p['y'])
for layer in layers:add(net,layer,g,p.get('pcb_port_id',p.get('pcb_via_id')))
via_seen=set()
for t in ts:
n=t['connection_name']
for a,b in zip(t['route'],t['route'][1:]):
if a['route_type']==b['route_type']=='wire' and a['layer']==b['layer'] and math.hypot(a['x']-b['x'],a['y']-b['y'])>1e-8:
add(n,a['layer'],LineString([(a['x'],a['y']),(b['x'],b['y'])]).buffer(max(a['width'],b['width'])/2),t['pcb_trace_id'])
for p in t['route']:
key=(round(p['x'],5),round(p['y'],5))
if p['route_type']=='via' and key not in via_seen:
via_seen.add(key)
for layer in ['top','inner1','inner2','bottom']:add(n,layer,Point(p['x'],p['y']).buffer(p.get('outer_diameter',.45)/2),'via '+str(key))
issues={}
for layer in ['top','inner1','inner2','bottom']:
rows=[i for i in items if i[1]==layer];tree=STRtree([i[2] for i in rows])
for j,(n,_,g,tag) in enumerate(rows):
for k in tree.query(g.buffer(.099)):
if k<=j:continue
m,_,h,label=rows[k]
if n is None or m is None or n==m:continue
dist=g.distance(h)
if dist<.099:
key=(layer,tag,label)
if key not in issues or issues[key]['gap']>dist:issues[key]=dict(layer=layer,net1=names.get(n,n),net2=names.get(m,m),item1=tag,item2=label,gap=round(dist,6))
out=list(issues.values());(r/'power-geometry-audit.json').write_text(json.dumps(out,indent=2))
print('Independent clearance issues:',len(out));print(json.dumps(out[:25],indent=2))
# Check the actual top pour as well as traces; unused LSM pads must stay isolated.
keepout=box(23.1,-15.65,24.9,-14.35)
top_pours=[]
for p in d:
if p['type']=='pcb_copper_pour' and p['layer']=='top':
b=p['brep_shape'];ring=lambda ring:[(v['x'],v['y']) for v in ring['vertices']]
top_pours.append(Polygon(ring(b['outer_ring']),[ring(r) for r in b.get('inner_rings',[])]))
assert top_pours
for poly in top_pours:assert poly.intersection(keepout).area<1e-8,'Top copper inside sensor keepout'
for net,layer,g,tag in items:
if layer!='top':continue
if tag in ['pcb_port_124','pcb_port_125','pcb_port_126']:
assert all(g.distance(poly)>=.099 for poly in top_pours),f'Unused IMU pad {tag} contacts ground pour'
if str(tag).startswith('source_net') or str(tag).startswith('via ') or str(tag).startswith('budget_'):
assert not g.intersects(keepout),f'{tag} crosses sensor keepout'
assert not out,'Copper clearance issues'
(r/'imu-layout-validation.json').write_text(json.dumps({'status':'PASS','floatingPins':[9,10,11],'topCopperKeepoutMm':[1.8,1.3],'checks':['NC pad clearance to traces, vias, pads and top pours','top interior free of routed copper and pours','minimum 0.1 mm different-net copper clearance']},indent=2))
print('PASS: unused IMU pads isolated from pours and routed copper; sensor interior clear.')