AnasSarkiz/NEMA14_CH32X035G8U6
A NEMA stepper motor is shown with proposed M3 screws/spacers, four wired motor leads and splices, and a JST SHR-04V-S connector.
- Version
- 1.12.2
- License
- ISC
- Stars
- 0
scripts/route-usb-boot.py
"""Finish explicitly selected connections on saved copper.
A collision-aware grid assists the selected manual repairs; unchanged routes are retained.
All results must pass the independent tscircuit DRC and Gerber short checks.
"""
import json,math,pathlib,heapq,time
import numpy as np
from PIL import Image,ImageDraw
ROOT=pathlib.Path(__file__).resolve().parents[1];path=ROOT/'artifacts/board.circuit.json';j=json.loads(path.read_text())
STEP=.05;LOW=-17.5;N=701
src={e['source_component_id']:e for e in j if e['type']=='source_component'}
sp={e['source_port_id']:e for e in j if e['type']=='source_port'}
ports={e['pcb_port_id']:e for e in j if e['type']=='pcb_port'}
comp={e['pcb_component_id']:e for e in j if e['type']=='pcb_component'}
nets={e['subcircuit_connectivity_map_key']:e for e in j if e['type']=='source_net'}
traces={e['subcircuit_connectivity_map_key']:e['source_trace_id'] for e in j if e['type']=='source_trace'}
pads=[e for e in j if e['type'] in ['pcb_smtpad','pcb_plated_hole']]
def xy(x,y):return (round((x-LOW)/STEP),round((y-LOW)/STEP))
def world(p):return (LOW+p[0]*STEP,LOW+p[1]*STEP)
def key(e):
if e['type'] in ['pcb_smtpad','pcb_plated_hole']:
p=ports.get(e.get('pcb_port_id'),{});return sp.get(p.get('source_port_id'),{}).get('subcircuit_connectivity_map_key')
return e.get('subcircuit_connectivity_map_key')
def findport(cname,pname):
return next(p for p in ports.values() if src[comp[p['pcb_component_id']]['source_component_id']]['name']==cname and sp[p['source_port_id']]['name']==pname)
def drawrect(draw,x,y,w,h,r=0):draw.rectangle([xy(x-w/2-r,y-h/2-r),xy(x+w/2+r,y+h/2+r)],fill=1)
def circle(draw,x,y,r):draw.ellipse([xy(x-r,y-r),xy(x+r,y+r)],fill=1)
def obstacle(net,width,via=False):
imgs=[Image.new('L',(N,N)) for _ in range(2)];ds=[ImageDraw.Draw(i) for i in imgs]
margin=.15+(.25 if via else width/2)+.018
for e in j:
typ=e['type']
if typ in ['pcb_smtpad','pcb_plated_hole']:
if key(e)==net and not via:continue
w=e.get('width',e.get('outer_width',e.get('outer_diameter',e.get('radius',0)*2)));h=e.get('height',e.get('outer_height',e.get('outer_diameter',e.get('radius',0)*2)))
if abs(e.get('ccw_rotation',0)%180-90)<.01:w,h=h,w
ls=[0 if l=='top' else 1 for l in e.get('layers',[e.get('layer','top')])]
for l in ls:drawrect(ds[l],e['x'],e['y'],w,h,margin)
elif typ=='pcb_trace' and key(e)!=net:
rr=e['route']
for a,b in zip(rr,rr[1:]):
if a['route_type']!='wire' or b['route_type']!='wire' or a['layer']!=b['layer']:continue
r=margin+max(a['width'],b['width'])/2;d=ds[0 if a['layer']=='top' else 1]
d.line([xy(a['x'],a['y']),xy(b['x'],b['y'])],fill=1,width=math.ceil(2*r/STEP))
circle(d,a['x'],a['y'],r);circle(d,b['x'],b['y'],r)
elif typ=='pcb_via':
if key(e)==net and not via:continue
r=.72 if via else e['outer_diameter']/2+margin
for d in ds:circle(d,e['x'],e['y'],r)
elif typ=='pcb_keepout':
c=e['center']
for d in ds:drawrect(d,c['x'],c['y'],e.get('width',e.get('radius',0)*2),e.get('height',e.get('radius',0)*2),.15+(.25 if via else width/2)+.018)
elif typ=='pcb_hole':
for d in ds:circle(d,e['x'],e['y'],e['hole_diameter']/2+.2+(.25 if via else width/2)+.018)
border=.3+(.25 if via else width/2)
for d in ds:
b=math.ceil(border/STEP);d.rectangle([0,0,N-1,b],fill=1);d.rectangle([0,N-1-b,N-1,N-1],fill=1);d.rectangle([0,0,b,N-1],fill=1);d.rectangle([N-1-b,0,N-1,N-1],fill=1)
return [np.asarray(im,dtype=np.bool_) for im in imgs]
assert not any(e.get('pcb_trace_id','').startswith('usb_boot_route_') for e in j), 'BOOT routes already present; this script adds branches once'
repairs=[]
original=[e for e in j if e['type'].startswith('pcb_')]
for cname,pname in [('J_BOOT','USB_DP'),('R_USB_BOOT','pin2'),('R_USB_BOOT','pin1')]:
p=findport(cname,pname);net=sp[p['source_port_id']]['subcircuit_connectivity_map_key']
width=.16
goals={}
for e in j:
if e['type']=='pcb_trace' and key(e)==net:
for a in e['route']:
if a['route_type']=='wire':gx,gy=xy(a['x'],a['y']);goals[(gx,gy,0 if a['layer']=='top' else 1)]=(a['x'],a['y'])
if not goals:
for pp in ports.values():
if pp['pcb_port_id']!=p['pcb_port_id'] and sp[pp['source_port_id']].get('subcircuit_connectivity_map_key')==net:
gx,gy=xy(pp['x'],pp['y']);goals[(gx,gy,0)]=(pp['x'],pp['y'])
if any(e.get('pcb_port_id')==pp['pcb_port_id'] and e['type']=='pcb_plated_hole' for e in pads):goals[(gx,gy,1)]=(pp['x'],pp['y'])
# Remove the source pad itself from candidate targets.
goals={g:v for g,v in goals.items() if math.hypot(v[0]-p['x'],v[1]-p['y'])>.25}
if not goals:raise RuntimeError('No target copper for '+cname+'.'+pname)
blocked=obstacle(net,width);vb=obstacle(net,width,True);start=(*xy(p['x'],p['y']),0)
pts=np.asarray([[g[0],g[1],g[2]] for g in goals]);lo=pts[:,:2].min(axis=0);hi=pts[:,:2].max(axis=0)
def heuristic(s):return max(lo[0]-s[0],0,s[0]-hi[0])+max(lo[1]-s[1],0,s[1]-hi[1])
heap=[(heuristic(start),0,start)];best={start:0};prev={};end=None;expanded=0
while heap:
_,cost,s=heapq.heappop(heap)
if cost!=best.get(s):continue
if s in goals:end=s;break
expanded+=1
if expanded>600000:break
x,y,l=s
options=[(x+dx,y+dy,l,1.41421356 if dx and dy else 1) for dx,dy in [(1,0),(-1,0),(0,1),(0,-1),(1,1),(1,-1),(-1,1),(-1,-1)]]
if not vb[0][y,x] and not vb[1][y,x]:options.append((x,y,1-l,35))
for nx,ny,nl,dc in options:
if not(0<=nx<N and 0<=ny<N) or blocked[nl][ny,nx]:continue
if nl==l and nx!=x and ny!=y and (blocked[l][y,nx] or blocked[l][ny,x]):continue
t=(nx,ny,nl);nc=cost+dc
if nc+1e-8<best.get(t,float('inf')):best[t]=nc;prev[t]=s;heapq.heappush(heap,(nc+heuristic(t),nc,t))
if end is None:pathlib.Path('/tmp/repair-debug.json').write_text(json.dumps(j));raise RuntimeError(f'No collision-free repair for {cname}.{pname} after {expanded} nodes')
nodes=[end]
while nodes[-1]!=start:nodes.append(prev[nodes[-1]])
nodes.reverse()
# Retain turns and layer changes; simplify only exact collinear runs.
slim=[nodes[0]]
for i in range(1,len(nodes)-1):
a,b,c=nodes[i-1:i+2]
if a[2]!=b[2] or b[2]!=c[2] or (b[0]-a[0],b[1]-a[1])!=(c[0]-b[0],c[1]-b[1]):slim.append(b)
slim.append(nodes[-1]);route=[];newvias=[]
for i,s in enumerate(slim):
x,y=world(s);layer='top' if s[2]==0 else 'bottom'
if i==0:x,y=p['x'],p['y']
if i==len(slim)-1:x,y=goals[end]
if route and route[-1]['layer']!=layer:
old=route[-1]['layer'];route.append({'route_type':'via','x':x,'y':y,'from_layer':old,'to_layer':layer,'via_diameter':.5,'via_hole_diameter':.25})
newvias.append((x,y))
route.append({'route_type':'wire','x':x,'y':y,'width':width,'layer':layer})
if p['pcb_port_id']:route[0]['start_pcb_port_id']=p['pcb_port_id']
tid=f'usb_boot_route_{cname}_{pname}'
j.append({'type':'pcb_trace','pcb_trace_id':tid,'route':route,**({'source_net_id':nets[net]['source_net_id']} if nets.get(net,{}).get('source_net_id') else {}),'source_trace_id':traces[net],'subcircuit_connectivity_map_key':net,'subcircuit_id':'subcircuit_source_group_0','pcb_port_ids':[p['pcb_port_id']] if p['pcb_port_id'] else []})
for i,(x,y) in enumerate(newvias):j.append({'type':'pcb_via','pcb_via_id':tid+'_via'+str(i),'pcb_trace_id':tid,'source_trace_id':traces[net],**({'source_net_id':nets[net]['source_net_id']} if nets.get(net,{}).get('source_net_id') else {}),'subcircuit_connectivity_map_key':net,'subcircuit_id':'subcircuit_source_group_0','x':x,'y':y,'hole_diameter':.25,'outer_diameter':.5,'layers':['top','bottom']})
repairs.append({'connection':cname+'.'+pname,'net':nets.get(net,{}).get('name','USB_BOOT'),'points':len(route),'vias':len(newvias),'expandedNodes':expanded,'route':route})
print(repairs[-1]['connection'],nets.get(net,{}).get('name','USB_BOOT'),len(route),'points',len(newvias),'vias',expanded,'searched nodes',flush=True)
assert all(e in j for e in original),'Saved copper was altered'
for e in j:
if e['type']=='pcb_via':e['tented_on_bottom']=True;e['tented_on_top']=True
path.write_text(json.dumps(j,indent=2)+'\n')
(ROOT/'artifacts/validation/usb-boot-routing.json').write_text(json.dumps(repairs,indent=2)+'\n')