mohan-bee/rc-link-breakaway-panel
A four-layer RC link panel combining an STM32/E28 2.4‑GHz transmitter with USB-RP2040 socket and an STM32/E28 receiver providing six buffered PWM servo outputs, regulated 3.3‑V power, headers, controls, and protection/decoupling.
- Version
- 0.1.5
- License
- unset
- 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())))
grounds={k:len(gs) for k,gs in net_groups.items() if k in {n['subcircuit_connectivity_map_key'] for n in c if n['type']=='source_net' and n.get('is_ground')}}
assert len(grounds)==2 and all(count==1 for count in grounds.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_networks=grounds,
scope='Physical copper connectivity from pad/trace/via/pour polygons on all four layers, including declared internal connector connections. 0.00005 mm contact tolerance. Does not test electrical operation or firmware.')
directory=env['report_directory']/'release'
directory.mkdir(parents=True,exist_ok=True)
(directory/'copper-connectivity.json').write_text(json.dumps(report,indent=2)+'\n')
print(json.dumps(report,indent=2))
assert report['status']=='pass'