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))