imrishabh18/rp2040-motor-controller

A library of PCB component definitions covering MOSFETs, motor drivers, USB-PD control, sensors, current monitors, transistors, diodes, resistors, speakers, connectors, and mirrored bottom-side logos, with pin mappings, footprints, and 3D models.

Version
1.0.55
License
unset
Stars
5

scripts/repair-top-local.py

"""Repair local fragments while preserving all unaffected route backbones."""
import json, math, subprocess, re, sys
from pathlib import Path

folder=Path('dist/top-assembly')
plan=json.loads((folder/'route-plan.json').read_text())
traces={t['pcb_trace_id']:t for t in plan['traces']}
changed={e['id'] for e in json.loads((folder/'changed-routes.json').read_text())}
leaves=set(json.loads((folder/'leaf-routes.json').read_text()))
bad=json.loads(Path('reports/top-pad-conflict-indices.json').read_text())
j=json.loads(Path('dist/pd-control/unrouted.json').read_text())
sc={e['source_component_id']:e['name'] for e in j if e['type']=='source_component'}
sp={e['source_port_id']:e for e in j if e['type']=='source_port'}
pp={e['pcb_port_id']:e for e in j if e['type']=='pcb_port'}
moved={'R_BUZZER_PD','R_ADC_B','U_CURRENT_A','R_PD_ENABLE_PU','R_STATUS_R','R_ADC_A','Q_PD_CFG1','C_ENCODER','C_ENCODER_BULK','U_PD_MON','C_PD_MON'}
parent={}
def root(id):
    parent.setdefault(id,id)
    if parent[id]!=id:parent[id]=root(parent[id])
    return parent[id]
for e in j:
    ids=e.get('connected_source_port_ids',[])+e.get('connected_source_net_ids',[]) if e['type']=='source_trace' else e.get('source_port_ids',[]) if e['type']=='source_component_internal_connection' else []
    for id in ids[1:]:parent[root(id)]=root(ids[0])
terminals={'J_MOTOR_USB':{'A4B9'},'F_MOTOR_VBUS':{'pin2'},'DRIVER':{'VMA','AOUT1','AOUT2','BOUT1','BOUT2','ISENA','ISENB'},'R_PHASE_A':{'pin2'},'R_PHASE_B':{'pin2'}}
motor_nets={root(id) for id,p in sp.items() if p['name'] in terminals.get(sc.get(p['source_component_id']),set())}
assert len(motor_nets)==11
sources={e['source_trace_id']:e for e in j if e['type']=='source_trace'}
def is_motor(t):
    s=sources.get(t.get('source_trace_id'),{})
    ids=s.get('connected_source_port_ids',[])+s.get('connected_source_net_ids',[]) or ([t['source_net_id']] if t.get('source_net_id') else [])
    return bool(ids) and root(ids[0]) in motor_nets
manifest={}
for id in sorted(changed|set(bad)):
    route=traces[id]['route'];n=len(route)
    if id in leaves or n<4:
        manifest[id]=[0,n-1];continue
    lo,hi=n-1,0
    for index in bad.get(id,[]): lo=min(lo,index);hi=max(hi,index)
    for endpoint_index in [0,n-1]:
        endpoint=route[endpoint_index]
        if id not in changed:continue
        for port in traces[id].get('connectsTo',[]):
            p=pp[port];source=sp.get(p.get('source_port_id'),{})
            if sc.get(source.get('source_component_id')) not in moved:continue
            if math.hypot(endpoint['x']-p['x'],endpoint['y']-p['y'])>1e-5:continue
            candidates=[k for k in range(1,n-1) if route[k]['route_type']=='wire']
            if not candidates:lo,hi=0,n-1;continue
            anchor=min(candidates,key=lambda k:math.hypot(route[k]['x']-endpoint['x'],route[k]['y']-endpoint['y']))
            if endpoint_index==0:lo=0;hi=max(hi,anchor)
            else:lo=min(lo,anchor);hi=n-1
    if lo>hi:lo,hi=0,n-1
    # Extend past nearby old vias and pad escapes before fixing the cut points.
    for direction in [-1,1]:
        k=lo if direction==-1 else hi;distance=0
        while 0<=k+direction<n and (distance<1.5 or route[k]['route_type']!='wire'):
            other=k+direction
            distance+=math.hypot(route[k]['x']-route[other]['x'],route[k]['y']-route[other]['y'])
            k=other
        if direction==-1:lo=k
        else:hi=k
    while lo>0 and route[lo]['route_type']!='wire':lo-=1
    while hi<n-1 and route[hi]['route_type']!='wire':hi+=1
    if lo==hi:lo=max(0,lo-1);hi=min(n-1,hi+1)
    manifest[id]=[lo,hi]
extra_path=Path('reports/local-extra-patches.json')
if extra_path.exists():manifest.update(json.loads(extra_path.read_text()))
Path('reports/local-patch-manifest.json').write_text(json.dumps(manifest,indent=2))
done=set(re.findall(r'^Routed (\S+)',Path('reports/local-patch-repair.log').read_text(),re.M)) if '--resume' in sys.argv else set()
pending=sorted((id for id in manifest if id not in done),key=lambda id:(id not in ['top_assembly_256'],id in leaves,math.hypot(traces[id]['route'][manifest[id][0]]['x']-traces[id]['route'][manifest[id][1]]['x'],traces[id]['route'][manifest[id][0]]['y']-traces[id]['route'][manifest[id][1]]['y'])))
with Path('reports/local-patch-repair.log').open('a' if '--resume' in sys.argv else 'w',buffering=1) as log:
    while pending:
        id=pending.pop(0);a,b=manifest[id];r=traces[id]['route']
        width=min(p['width'] for p in r[a:b+1] if p['route_type']=='wire')
        if id=='top_assembly_060':width=.6
        if id=='top_assembly_000':width=.15
        args=['.venv/bin/python','scripts/route-local.py',str(folder),id,str(width),'--shorten','--via-cost=35',f'--patch-from={a}',f'--patch-to={b}','--ripup='+','.join(pending),'--patch-manifest=reports/local-patch-manifest.json','--report-dir=reports/top-local']
        if id in ['top_assembly_142','top_assembly_256','top_assembly_258']:args.append('--debug')
        if id=='top_assembly_171':args += ['--debug','--grid-step=.025']
        if is_motor(traces[id]):args.append('--outer-only')
        if id=='top_assembly_000':args += ['--escape-width=.1','--escape-radius=.6']
        if id=='top_assembly_060':args += ['--escape-width=.3','--escape-radius=.7']
        print('LOCAL',id,'bounds',a,b,'of',len(r),'remaining',len(pending),file=log,flush=True)
        result=subprocess.run(args,stdout=log,stderr=subprocess.STDOUT)
        if result.returncode:raise SystemExit('Local repair failed: '+id)
    print('All local repairs complete',file=log,flush=True)