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-servo-power.py
"""Check the actual high-current routes, independently of the net-wide warning."""
import json, math, heapq
from pathlib import Path
root=Path(__file__).resolve().parents[1]
c=json.loads((root/'dist/index/circuit.json').read_text())
components={e['source_component_id']:e for e in c if e['type']=='source_component'}
source_ports={e['source_port_id']:e for e in c if e['type']=='source_port'}
ports={e['pcb_port_id']:e for e in c if e['type']=='pcb_port'}
source_traces={e['source_trace_id']:e for e in c if e['type']=='source_trace'}
traces=[e for e in c if e['type']=='pcb_trace']
nets={e['name']:e['subcircuit_connectivity_map_key'] for e in c if e['type']=='source_net'}
all_layers=['top','inner1','inner2','bottom']
headers={}
for p in ports.values():
sp=source_ports[p['source_port_id']]
name=components[sp['source_component_id']]['name']
if name=='J_RX_SERVO_POWER' or name.startswith('J_RX_PWM'):
headers[name,sp['pin_number']]=p
def node(layer,x,y):return layer,round(x,6),round(y,6)
# Conservative 25 um finished copper and copper resistivity at 85 C.
rho=1.724e-8*(1+.00393*(85-20))
reports={}
for name,pin in [('RX_V_SERVO',2),('RX_GND',3)]:
graph={}
def edge(a,b,resistance,length):
graph.setdefault(a,[]).append((b,resistance,length))
graph.setdefault(b,[]).append((a,resistance,length))
wide=[]
for t in traces:
if source_traces[t['source_trace_id']]['subcircuit_connectivity_map_key']!=nets[name]:continue
for a,b in zip(t['route'],t['route'][1:]):
if a['route_type']!='wire' or b['route_type']!='wire' or a['layer']!=b['layer']:continue
if a['width']<1.5-1e-6:continue
length=math.hypot(a['x']-b['x'],a['y']-b['y'])
resistance=rho*(length/1000)/((a['width']/1000)*25e-6)
edge(node(a['layer'],a['x'],a['y']),node(b['layer'],b['x'],b['y']),resistance,length)
wide.append(t['pcb_trace_id'])
for p in headers.values():
sp=source_ports[p['source_port_id']]
if sp.get('subcircuit_connectivity_map_key')!=nets[name]:continue
hole=next(e for e in c if e['type']=='pcb_plated_hole' and e.get('pcb_port_id')==p['pcb_port_id'])
assert hole['hole_diameter']>=1
assert set(hole['layers'])==set(all_layers)
# Full 1.6 mm travel through a 20 um plated 1 mm barrel, conservatively
# charged to each layer transition instead of using a zero-ohm shortcut.
barrel_r=rho*.0016/(math.pi*hole['hole_diameter']/1000*20e-6)
for layer in all_layers[1:]:edge(node('top',p['x'],p['y']),node(layer,p['x'],p['y']),barrel_r,0)
supply=headers['J_RX_SERVO_POWER',1 if name=='RX_V_SERVO' else 2]
origin=node('top',supply['x'],supply['y'])
distances={origin:(0,0)};queue=[(0,0,origin)]
while queue:
r,length,u=heapq.heappop(queue)
if r>distances[u][0]+1e-12:continue
for v,dr,dl in graph.get(u,[]):
if r+dr<distances.get(v,(float('inf'),0))[0]:
distances[v]=(r+dr,length+dl);heapq.heappush(queue,(r+dr,length+dl,v))
outputs=[]
for i in range(1,7):
p=headers[f'J_RX_PWM{i}',pin];target=node('top',p['x'],p['y'])
assert target in distances,(name,i,'No dedicated 1.5 mm path')
r,length=distances[target]
outputs.append(dict(output=i,length_mm=length,resistance_ohm=r,drop_at_2A_v=2*r))
reports[name]=dict(min_trace_width_mm=1.5,outputs=outputs,trace_ids=sorted(set(wide)))
loop_r=max(a['resistance_ohm']+b['resistance_ohm'] for a,b in zip(reports['RX_V_SERVO']['outputs'],reports['RX_GND']['outputs']))
assert 2*loop_r<.15,(loop_r,'Excess PCB-only supply/return drop')
warning_reviews=[]
for warning in (e for e in c if e['type']=='pcb_trace_warning'):
source=source_traces[warning['source_trace_id']]
assert source['subcircuit_connectivity_map_key']==nets['RX_GND']
pins=[source_ports[p] for p in source['connected_source_port_ids']]
assert len(pins)==2 and all(components[p['source_component_id']]['name'].startswith('J_RX_') for p in pins)
assert source['min_trace_thickness']==1.5
offender=next(t for t in traces if t['pcb_trace_id']==warning['pcb_trace_id'])
assert offender['source_trace_id']!=source['source_trace_id']
warning_reviews.append(dict(source_trace_id=source['source_trace_id'],reported_trace_id=offender['pcb_trace_id'],actual_reported_source=offender['source_trace_id'],classification='false positive: checker inspects unrelated branches on the entire ground net; dedicated 1.5 mm path verified'))
assert len(warning_reviews)==6,warning_reviews
report=dict(status='pass',design_limit_total_A=2,minimum_dedicated_path_mm=1.5,assumed_copper_um=25,assumed_barrel_plating_um=20,assumed_copper_temperature_C=85,worst_pcb_loop_resistance_ohm=loop_r,worst_pcb_drop_at_2A_v=2*loop_r,routes=reports,native_warning_review=warning_reviews,
limits='PCB-only resistance estimate, not a thermal certification. Excludes contact, cable and BEC drop; requires >=3 A contacts, regulated 5 V and <=2 A total including stall/transients. Physical current/temperature tests remain required.')
(root/'validation/servo-power-audit.json').write_text(json.dumps(report,indent=2)+'\n')
print(json.dumps({k:v for k,v in report.items() if k not in ['routes','native_warning_review']},indent=2))