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/seed-pg-routing.py
"""Retain existing copper by physical pin identity and queue only new branches.
This is a candidate, not a validated plan. New branches must be routed and all
native, physical connectivity and exported copper checks must pass afterward.
"""
import copy, json, re
from pathlib import Path
from shapely.geometry import Point, LineString
from copper_geometry import trace_segments
folder=Path('dist/pg-sense');baseline=Path('reports/pg-sense-2026-10-22/baseline')
old=json.loads((baseline/'circuit.json').read_text());new=json.loads((folder/'unrouted.json').read_text())
plan=json.loads((baseline/'route-plan.json').read_text());srj=json.loads((folder/'routing-input.json').read_text())
def lookup(j):
components={e['source_component_id']:e['name'] for e in j if e['type']=='source_component'}
for e in j:
if e['type']=='pcb_component' and e.get('source_component_id') not in components:
components[e.get('source_component_id')]='via:'+str(e['center'])
ports={e['source_port_id']:(components[e['source_component_id']],e.get('pin_number'),e['name']) for e in j if e['type']=='source_port'}
return ports,{e['pcb_port_id']:ports[e['source_port_id']] for e in j if e['type']=='pcb_port' and e.get('source_port_id') in ports}
os,op=lookup(old);ns,np=lookup(new);mapping={}
for a,b in [(os,ns),(op,np)]:
reverse={v:k for k,v in b.items()}
assert len(reverse)==len(b)
for id,value in a.items():
assert value in reverse,('Removed physical pin',value)
mapping[id]=reverse[value]
def trace_key(e,ports,j):
nets={n['source_net_id']:n['name'] for n in j if n['type']=='source_net'}
return (e.get('name'),tuple(sorted(ports[p] for p in e.get('connected_source_port_ids',[]))),tuple(sorted(nets[n] for n in e.get('connected_source_net_ids',[]))))
nt={trace_key(e,ns,new):e['source_trace_id'] for e in new if e['type']=='source_trace'}
for e in old:
if e['type']=='source_trace':mapping[e['source_trace_id']]=nt[trace_key(e,os,old)]
for e in old:
if e['type']=='source_net':
choices=[n for n in new if n['type']=='source_net' and n['name']==e['name']]
assert len(choices)==1
mapping[e['source_net_id']]=choices[0]['source_net_id']
pattern=re.compile(r'(?:source_(?:port|trace|net)_\d+|pcb_port_\d+)')
def rewrite(v):
if isinstance(v,str):return pattern.sub(lambda m:mapping.get(m[0],m[0]),v)
if isinstance(v,list):return [rewrite(x) for x in v]
if isinstance(v,dict):return {k:rewrite(x) for k,x in v.items()}
return v
traces=rewrite(plan['traces'])
oldpoints={e['pcb_port_id']:e for e in old if e['type']=='pcb_port'}
points={e['pcb_port_id']:e for e in new if e['type']=='pcb_port'}
for id,e in oldpoints.items():
if id not in mapping:continue
n=points[mapping[id]]
assert abs(e['x']-n['x'])+abs(e['y']-n['y'])<1e-7 and e['layers']==n['layers'],('Moved legacy pin',op.get(id))
parent={}
def root(x):
parent.setdefault(x,x)
if parent[x]!=x:parent[x]=root(parent[x])
return parent[x]
for e in new:
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 p in ids[1:]:parent[root(p)]=root(ids[0])
st={e['source_trace_id']:e for e in new if e['type']=='source_trace'}
def net(t):
if t.get('source_net_id'):return root(t['source_net_id'])
s=st[t['source_trace_id']];return root((s.get('connected_source_port_ids',[])+s.get('connected_source_net_ids',[]))[0])
reverse={(v[0],v[1]):k for k,v in np.items()}
spnet={id:root(e['source_port_id']) for id,e in points.items() if e.get('source_port_id')}
connections={}
for c in srj['connections']:
for p in c['pointsToConnect']:
if p.get('pcb_port_id'):connections.setdefault(spnet[p['pcb_port_id']],[]).append(c)
def assign(t):
candidates=connections.get(net(t),[])
assert candidates,('No routing net',t)
t['connection_name']='__'.join(dict.fromkeys(c['name'] for c in candidates))
for t in traces:assign(t)
new_traces=[]
def branch(name,part,pin,target=None):
port=reverse[part,pin];p=points[port];s=next(e for e in st.values() if e.get('name')==name)
t={'type':'pcb_trace','pcb_trace_id':'pg_'+name.lower(),'source_trace_id':s['source_trace_id'],'connectsTo':[port]}
if target:
end=reverse[target];q=points[end];x,y,layer=q['x'],q['y'],q['layers'][0];t['connectsTo'].append(end)
else:
candidates=[]
for other in traces:
if net(other)!=spnet[port]:continue
for a,b,layer,width in trace_segments(other['route']):
line=LineString([a,b]);q=line.interpolate(line.project(Point(p['x'],p['y'])))
candidates.append((Point(p['x'],p['y']).distance(q)+(0 if layer=='top' else .8),q.x,q.y,layer))
assert candidates,('No retained backbone',part,pin)
_,x,y,layer=min(candidates)
t['route']=[dict(route_type='wire',x=p['x'],y=p['y'],layer=p['layers'][0],width=.15),dict(route_type='wire',x=x,y=y,layer=layer,width=.15)]
assign(t);t['connection_name']=next((c['name'] for c in srj['connections'] if c.get('source_trace_id')==t['source_trace_id']),t['connection_name']);new_traces.append(t)
branch('PD_PG_SENSE','U_PD_PG',6)
branch('PD_PG_SUPPLY_SENSE','U_PD_PG',1)
branch('PD_PG_SUPPLY_DISCHARGE','R_PD_VDD_PD',1,('U_PD_PG',1))
branch('PD_PG_SUPPLY_DISCHARGE_GND','R_PD_VDD_PD',2)
branch('PD_PG_FUNCTION','U_PD_PG',3)
branch('PD_PG_GROUND','U_PD_PG',2)
branch('PD_PG_LOGIC_SUPPLY','U_PD_PG',5)
branch('PD_PG_DECOUPLING','C_PD_PG',1,('U_PD_PG',5))
branch('PD_PG_DECOUPLING_GND','C_PD_PG',2)
branch('PD_GOOD','U_PD_PG',4,('U1',9))
branch('PD_GOOD_PULLDOWN','R_PD_GOOD_PD',1,('U_PD_PG',4))
branch('PD_GOOD_PULLDOWN_GND','R_PD_GOOD_PD',2)
(folder/'route-plan.json').write_text(json.dumps({'expectedInput':srj,'traces':traces+new_traces},indent=2))
(folder/'new-routes.json').write_text(json.dumps([t['pcb_trace_id'] for t in new_traces],indent=2))
print(json.dumps({'retainedRoutes':len(traces),'newRoutes':len(new_traces),'allLegacyPinGeometryPreserved':True},indent=2))
