imrishabh18/rp2040-motor-controller
An RP2040-based NEMA17 stepper-motor controller with USB-C programming and PD motor-power negotiation, DRV8825 dual H-bridge drive, current/temperature telemetry, protected power filtering, status RGB LED, buzzer alarm, and optional magnetic encoder.
- Version
- 1.0.41
- License
- unset
- Stars
- 3
scripts/import-freerouting.py
"""Turn a Specctra session into source-associated tscircuit routing paths.
No routing clearance error is suppressed; the result is an unvalidated candidate.
"""
from freerouting_geometry import *
import heapq,collections
from shapely.ops import unary_union
from shapely.strtree import STRtree
text=Path(sys.argv[1] if len(sys.argv)>1 else 'work/freerouting/routed.ses').read_text().replace('(string_quote ")','(string_quote DQUOTE)')
tokens=re.findall(r'"(?:[^"\\]|\\.)*"|[()]|[^\s()]+',text)
stack=[];tree=None
for t in tokens:
if t=='(':
obj=[]
if stack:stack[-1].append(obj)
stack.append(obj)
elif t==')':tree=stack.pop()
else:stack[-1].append(t.strip('"'))
def entries(x,k):return [e for e in x if isinstance(e,list) and e and e[0]==k]
def one(x,k):return next(iter(entries(x,k)),None)
routes=one(tree,'routes');res=one(routes,'resolution');scale={'um':.001,'mm':1,'mil':.0254,'inch':25.4}[res[1]]/float(res[2]);print('Resolution',res,'scale',scale)
manual_vias=[e for e in j if e['type']=='pcb_via']
def snap(x,y):
for v in manual_vias:
if math.hypot(x-v['x'],y-v['y'])<.0002:return (v['x'],v['y'])
return (x,y)
network=one(routes,'network_out');wires=collections.defaultdict(list);vias=collections.defaultdict(list)
for n in entries(network,'net'):
net=n[1]
for w in entries(n,'wire'):
path=one(w,'path');layer=path[1];width=float(path[2])*scale;nums=list(map(float,path[3:]));pts=[snap(x*scale,y*scale) for x,y in zip(nums[::2],nums[1::2])]
for a,b in zip(pts,pts[1:]):
if math.dist(a,b)>1e-7:wires[net].append((LineString([a,b]),layer,width))
for v in entries(n,'via'):vias[net].append((*snap(float(v[2])*scale,float(v[3])*scale),.45 if v[1]=='VFIX' else .5))
# Fixed routes, which may not be included in a session's output, remain authoritative.
for e in j:
if e['type']=='pcb_trace' and '_SENSE_' not in st[e['source_trace_id']].get('name',''):
for a,b,l,w in trace_segments(e['route']):wires[enet(e)].append((LineString([a,b]),l,w))
elif e['type']=='pcb_via':vias[enet(e)].append((e['x'],e['y'],e['outer_diameter']))
allpads=collections.defaultdict(list)
for e in j:
if e['type'] in ['pcb_smtpad','pcb_plated_hole']:
allpads[enet(e)].append((e,geom(e,'outer_' if e['type']=='pcb_plated_hole' else '')))
portpoints={e['pcb_port_id']:(e['x'],e['y'],e.get('layers',['top'])) for e in pp.values()}
connections=collections.defaultdict(list)
for t in plan['traces']:connections[enet(pp[t['connectsTo'][0]])].append(t)
result=[];failed=[];graphstats=[]
for net,traces in connections.items():
graph=collections.defaultdict(dict);viamap={};padmap={};nodes_by_layer=collections.defaultdict(set)
def node(x,y,l):return (round(x,6),round(y,6),l)
def add(a,b,w,kind='wire',extra=None):
if a==b:return
cost=math.dist(a[:2],b[:2]) if kind=='wire' else 1.6
val=(cost,w,kind,extra)
if b not in graph[a] or graph[a][b][1]<w:graph[a][b]=val;graph[b][a]=val
nodes_by_layer[a[2]].add(a);nodes_by_layer[b[2]].add(b)
for x,y,d in vias[net]:
ns=[node(x,y,l) for l in layers]
for a,b in zip(ns,ns[1:]):add(a,b,0,'via',d)
for n in ns:viamap[n]=d
for e,g in allpads[net]:
if e['type']=='pcb_plated_hole':
ns=[node(e['x'],e['y'],l) for l in e['layers']]
for a,b in zip(ns,ns[1:]):add(a,b,0,'pad',e['pcb_plated_hole_id'])
# Build a noded wire graph and explicitly insert all via and pad connection points.
for layer in layers:
wl=[(g,w) for g,l,w in wires[net] if l==layer]
if not wl:continue
union=unary_union([g for g,w in wl]);lines=[union] if union.geom_type=='LineString' else list(union.geoms)
geos=[g for g,w in wl];stree=STRtree(geos)
specials=list(nodes_by_layer[layer]);padlinks=[]
padgeos=[(e,g) for e,g in allpads[net] if layer in e.get('layers',[e.get('layer')])]
for line in lines:
coords=list(line.coords)
for a,b in zip(coords,coords[1:]):
seg=LineString([a,b]);mid=seg.interpolate(.5,normalized=True)
widths=[wl[i][1] for i in stree.query(seg.buffer(1e-5)) if geos[i].distance(mid)<1e-5];width=max(widths or [.15]);cuts=[(0,node(*a,layer)),(seg.length,node(*b,layer))]
for v in specials:
q=Point(v[:2])
if seg.distance(q)<1e-5:cuts.append((seg.project(q),v))
for e,g in padgeos:
if seg.distance(g)>width/2+1e-5:continue
center=Point(e['x'],e['y']);dist=seg.project(center);proj=seg.interpolate(dist);pn=node(proj.x,proj.y,layer);pc=node(e['x'],e['y'],layer)
# Only bridge inside existing pad/track copper. Final DRC still checks it.
bridge=LineString([pc[:2],pn[:2]])
if not g.buffer(1e-4).union(seg.buffer(width/2+1e-4)).covers(bridge):continue
cuts.append((dist,pn));padlinks.append((pc,pn,min(width,.15)))
cuts.sort(key=lambda p:p[0])
for (_,aa),(_,bb) in zip(cuts,cuts[1:]):add(aa,bb,width)
for a,b,w in padlinks:add(a,b,w)
def shortest(start,end):
q=[(0,start)];cost={start:0};prev={}
while q:
d,a=heapq.heappop(q)
if d>cost[a]+1e-9:continue
if a==end:
path=[end]
while path[-1]!=start:path.append(prev[path[-1]])
return list(reversed(path))
for b,(length,w,k,extra) in graph[a].items():
nd=d+length
if nd<cost.get(b,float('inf')):cost[b]=nd;prev[b]=a;heapq.heappush(q,(nd,b))
return None
for t in traces:
start,end=[{**pp[pid],'layer':pp[pid]['layers'][0]} for pid in t['connectsTo']];a=node(start['x'],start['y'],start['layer']);b=node(end['x'],end['y'],end['layer'])
path=shortest(a,b)
if not path:failed.append({'trace':t['pcb_trace_id'],'net':net,'ports':t['connectsTo'],'start':a,'end':b,'startDegree':len(graph[a]),'endDegree':len(graph[b])});continue
route=[]
for a,b in zip(path,path[1:]):
_,w,kind,extra=graph[a][b]
if kind=='wire':
for pt0 in [a,b]:route.append({'route_type':'wire','x':pt0[0],'y':pt0[1],'layer':pt0[2],'width':w})
elif kind=='via':
# Collapse adjacent layer hops at the same physical through-via.
if route and route[-1].get('route_type')=='via' and route[-1]['x']==a[0] and route[-1]['y']==a[1]:continue
route.append({'route_type':'via','x':a[0],'y':a[1],'from_layer':'top','to_layer':'bottom','via_diameter':extra,'via_hole_diameter':.3})
else:route.append({'route_type':'through_pad','start':{'x':a[0],'y':a[1]},'end':{'x':b[0],'y':b[1]},'start_layer':a[2],'end_layer':b[2],'pcb_plated_hole_id':extra,'width':.15})
# Leading/trailing wire records let core associate source ports with the route.
route.insert(0,{'route_type':'wire','x':start['x'],'y':start['y'],'layer':start['layer'],'width':next((r['width'] for r in route if r['route_type']=='wire'),.15)})
route.append({'route_type':'wire','x':end['x'],'y':end['y'],'layer':end['layer'],'width':next((r['width'] for r in reversed(route) if r['route_type']=='wire'),.15)})
result.append({**t,'route':route})
graphstats.append({'net':net,'nodes':len(graph),'wires':len(wires[net]),'vias':len(vias[net]),'connections':len(traces)})
Path('reports/freerouting-import.json').write_text(json.dumps({'imported':len(result),'failed':failed,'graphStats':graphstats},indent=2));print('Imported',len(result),'failed',len(failed));print(json.dumps(failed[:10],indent=2))
out=Path(sys.argv[2] if len(sys.argv)>2 else 'dist/freerouting');out.mkdir(parents=True,exist_ok=True);(out/'route-plan.json').write_text(json.dumps({**plan,'traces':result},indent=2))
if failed:sys.exit(1)
