mohan-bee/flight-controller-v2

A 64 mm STM32F405 flight-controller PCB with ICM-42688 IMU, SPI flash, USB-C/ESD, battery buck and 3.3 V regulation, four motor outputs, service I/O, and PETG standoff mounting hardware.

Version
0.1.0
License
ISC
Stars
0

tools/audit-connectivity.py

from pathlib import Path
import runpy, json
from shapely.geometry import Polygon
from shapely.strtree import STRtree
from shapely.validation import make_valid

env=runpy.run_path(str(Path(__file__).with_name('audit-copper.py')))
c=env['c'];root=env['root'];shapes=env['shapes']
for p in (x for x in c if x['type']=='pcb_copper_pour'):
    assert p['shape']=='brep'
    b=p['brep_shape']
    polygon=Polygon([(v['x'],v['y']) for v in b['outer_ring']['vertices']], [[(v['x'],v['y']) for v in r['vertices']] for r in b['inner_rings']])
    if not polygon.is_valid:polygon=make_valid(polygon)
    net=next(x['subcircuit_connectivity_map_key'] for x in c if x['type']=='source_net' and x['source_net_id']==p['source_net_id'])
    shapes.append(dict(geom=polygon,net=net,layers={p['layer']},id=p['pcb_copper_pour_id'],kind='pour'))
parent=list(range(len(shapes)))
def find(i):
    while i!=parent[i]:parent[i]=parent[parent[i]];i=parent[i]
    return i
def merge(i,j):parent[find(j)]=find(i)
tree=STRtree([s['geom'] for s in shapes])
shorts=[]
for i,a in enumerate(shapes):
    for j in tree.query(a['geom'].buffer(.00005)):
        j=int(j)
        if j<=i:continue
        b=shapes[j]
        if not a['layers'] & b['layers']:continue
        if a['geom'].distance(b['geom'])>.00005:continue
        if a['net']==b['net']:merge(i,j)
        elif 'pour' in (a['kind'],b['kind']) and a['geom'].intersection(b['geom']).area>1e-7:
            shorts.append([a['id'],b['id']])
pad_to_index={s['id']:i for i,s in enumerate(shapes) if s['kind']=='pad'}
port_to_index={p['pcb_port_id']:pad_to_index[p[p['type']+'_id']] for p in c if p['type'] in ['pcb_smtpad','pcb_plated_hole'] and p.get('pcb_port_id')}
source_to_index={p['source_port_id']:port_to_index[p['pcb_port_id']] for p in c if p['type']=='pcb_port' and p['pcb_port_id'] in port_to_index}
for comp in (x for x in c if x['type']=='source_component'):
    for internal in comp.get('internally_connected_source_port_ids',[]):
        ids=[source_to_index[p] for p in internal if p in source_to_index]
        for i in ids[1:]:merge(ids[0],i)
net_groups={};unconnected=[]
sc={x['source_component_id']:x['name'] for x in c if x['type']=='source_component'}
for sp in (x for x in c if x['type']=='source_port'):
    key=sp.get('subcircuit_connectivity_map_key')
    if not key or sp['source_port_id'] not in source_to_index:continue
    i=source_to_index[sp['source_port_id']]
    net_groups.setdefault(key,{}).setdefault(find(i),[]).append(sc[sp['source_component_id']]+'.'+sp['name'])
for key,groups in net_groups.items():
    if len(groups)>1:unconnected.append(dict(net=env['netnames'].get(key,key),groups=list(groups.values())))
report=dict(status='pass' if not shorts and not unconnected else 'fail',checked_nets=len(net_groups),checked_pads=sum(len(g) for gs in net_groups.values() for g in gs.values()),unconnected=unconnected,pour_shorts=shorts,ground_groups=len(next(gs for k,gs in net_groups.items() if env['netnames'].get(k)=='GND')),
scope='Physical copper connectivity from pad/trace/via/pour polygons on four layers, including declared internal connector connections. 0.00005 mm contact tolerance. Does not test electrical operation or firmware.')
(root/'validation/release/copper-connectivity.json').write_text(json.dumps(report,indent=2)+'\n')
print(json.dumps(report,indent=2))
assert report['status']=='pass'