AnasSarkiz/NEMA14_RP2040
A NEMA motor assembly concept with a controller board, four proposed M3 screws and spacers, motor lead splices, and a JST 4-pin connector harness.
- Version
- 1.13.0
- License
- ISC
- Stars
- 0
scripts/escape-inner-layer-cages.py
"""Reserve short inner1 signal escapes; refill and verify all ground regions afterward."""
import json,sys,math
from shapely.geometry import Point,LineString,box
from shapely.strtree import STRtree
import verify_supplier_connectivity as g
requested={q for q in sys.argv[1:] if not q.startswith('--')};requested_ref=next((q.split('=',1)[1] for q in sys.argv if q.startswith('--start-ref=')),None);reachable_path=next((q.split('=',1)[1] for q in sys.argv if q.startswith('--reachable-sites=')),None);reachable=__import__('numpy').load(reachable_path) if reachable_path else None;construction_layer=next((q.split('=',1)[1] for q in sys.argv if q.startswith('--layer=')),'inner1');assert construction_layer in ['inner1','inner2'];requested_width=float(next((q.split('=',1)[1] for q in sys.argv if q.startswith('--width=')),.16));assert .16<=requested_width<=.3;via_radius=.25 if requested_width>=.3 else .225;stack=['top','inner1','inner2','bottom'];board=next(e for e in g.j if e['type']=='pcb_board');assert board['num_layers']==4
receipts=[]
def build_index():
shapes=[];owners=[];physical_layers=[]
def add(e,geom,ls):shapes.append(geom);owners.append(e);physical_layers.append(ls)
for e in g.j:
typ=e['type']
if typ in ['pcb_smtpad','pcb_plated_hole']:add(e,g.geometry(e),e.get('layers',[e.get('layer','top')]))
elif typ=='pcb_via':add(e,Point(e['x'],e['y']).buffer(e['outer_diameter']/2),stack)
elif typ=='pcb_trace':
for a,b in zip(e['route'],e['route'][1:]):
if a['route_type']==b['route_type']=='wire' and a['layer']==b['layer']:add(e,LineString([(a['x'],a['y']),(b['x'],b['y'])]).buffer(max(a['width'],b['width'])/2),[a['layer']])
elif typ=='pcb_keepout':
c=e['center'];r=e['radius'];add(e,box(c['x']-r,c['y']-r,c['x']+r,c['y']+r),stack)
elif typ=='pcb_hole':add(e,Point(e['x'],e['y']).buffer(e['hole_diameter']/2),stack)
return STRtree(shapes),shapes,owners,physical_layers
tree,shapes,owners,physical_layers=build_index()
def clear(poly,layer,net,via=False):
if not box(-17.15,-17.15,17.15,17.15).covers(poly):return False
if g.nets.get(net)!='GND':
plan=json.loads((g.ROOT/'artifacts/engineering-fixes/usb-routing-plan.json').read_text())
for core in plan['cores']:
if via or layer==core['reference_layer']:
for track in core['tracks']:
if poly.distance(LineString(track['points']).buffer(.125))<.203:return False
for i in tree.query(poly.buffer(.253)):
e=owners[i];typ=e['type'];same=g.key(e)==net
if not via and layer not in physical_layers[i]:continue
if same and not via:continue
if typ=='pcb_copper_pour':continue
if via and same and typ=='pcb_trace':continue
margin=.003 if via and same and typ in ['pcb_smtpad','pcb_plated_hole'] else .103 if via and same and typ=='pcb_via' else .25 if typ=='pcb_hole' else .153
if poly.distance(shapes[i])<margin:return False
return True
for net,name in g.nets.items():
if name not in requested:continue
width=requested_width # Independent physical branch-cut/current screening remains mandatory.
assert construction_layer=='inner2' or (name in ['A_PLUS','A_MINUS','B_PLUS','B_MINUS'] and width>=.3) or name not in ['GND','PD_VBUS','LOGIC_IN','DATA_VBUS','BUCK_SW','BUZZER_SINK','A_PLUS','A_MINUS','B_PLUS','B_MINUS','SENSE1','SENSE2'],'Local inner1 escapes are restricted to low-current signals'
es,rs=g.groups(net);lands={}
for e,r in zip(es,rs):
if e.get('pcb_port_id'):lands.setdefault(r,[]).append(e)
if len(lands)<2:continue
phase=name in ['A_PLUS','A_MINUS','B_PLUS','B_MINUS']
existing_inner=sum(math.dist((a['x'],a['y']),(b['x'],b['y'])) for t in g.j if t['type']=='pcb_trace' and g.key(t)==net for a,b in zip(t['route'],t['route'][1:]) if a['route_type']==b['route_type']=='wire' and a['layer']==b['layer']=='inner1')
remaining_inner=8-existing_inner if phase and construction_layer=='inner1' else float('inf')
direct=None
for ra in lands:
for rb in lands:
if ra==rb:continue
va=[e for e,r in zip(es,rs) if r==ra and e['type']=='pcb_via'];vb=[e for e,r in zip(es,rs) if r==rb and e['type']=='pcb_via']
for aa in va:
for bb in vb:
a=(aa['x'],aa['y']);b=(bb['x'],bb['y'])
for pts in [[a,b],[a,(b[0],a[1]),b],[a,(a[0],b[1]),b]]:
length=LineString(pts).length
if length<=min(15,remaining_inner) and clear(LineString(pts).buffer(width/2),construction_layer,net) and (direct is None or length<direct[0]):direct=(length,aa,pts)
if direct:
length,v,pts=direct;uid='inner1_local_escape_'+name+'_direct'
while any(e.get('pcb_trace_id')==uid for e in g.j):uid+='_next'
base={k:v[k] for k in ['source_net_id','source_trace_id','subcircuit_connectivity_map_key','subcircuit_id'] if k in v}
g.j.append({'type':'pcb_trace','pcb_trace_id':uid,'route':[{'route_type':'wire','x':a,'y':b,'layer':construction_layer,'width':width} for a,b in pts],'pcb_port_ids':[],**base});rec={'net':name,'start_mm':pts[0],'end_mm':pts[-1],'length_mm':length,'connects_existing_actual_via_barrels':True};receipts.append(rec);print(rec,flush=True);tree,shapes,owners,physical_layers=build_index();continue
phase=name in ['A_PLUS','A_MINUS','B_PLUS','B_MINUS']
existing_inner=sum(math.dist((a['x'],a['y']),(b['x'],b['y'])) for t in g.j if t['type']=='pcb_trace' and g.key(t)==net for a,b in zip(t['route'],t['route'][1:]) if a['route_type']==b['route_type']=='wire' and a['layer']==b['layer']=='inner1')
remaining_inner=8-existing_inner if phase and construction_layer=='inner1' else float('inf')
for root in sorted(lands,key=lambda r:len(lands[r])):
if requested_ref and not any(g.src[g.sp[g.ports[p['pcb_port_id']]['source_port_id']]['source_component_id']]==requested_ref for p in lands[root]):continue
vs=[e for e,r in zip(es,rs) if r==root and e['type']=='pcb_via'];found=None
for v in vs:
origin=(v['x'],v['y']);candidates=[]
for ix in range(-40,41):
for iy in range(-40,41):
dist=math.hypot(ix*.1,iy*.1)
if .7<=dist<=(4 if phase else 2.5):candidates.append((dist,round(origin[0]+ix*.1,3),round(origin[1]+iy*.1,3)))
for _,x,y in sorted(candidates):
if reachable is not None and not reachable[round((y+17.5)/.025),round((x+17.5)/.025)]:continue
via=Point(x,y).buffer(via_radius)
if not clear(via,None,net,True):continue
paths=[[origin,(x,y)],[origin,(x,origin[1]),(x,y)],[origin,(origin[0],y),(x,y)]]
for pts in paths:
if LineString(pts).length>min(4 if phase else 3,remaining_inner):continue
if clear(LineString(pts).buffer(width/2),construction_layer,net):found=(v,x,y,pts);break
if found:break
if found:break
if not found:continue
v,x,y,pts=found;uid='inner1_local_escape_'+name+'_'+str(len(receipts))
while any(e.get('pcb_trace_id')==uid for e in g.j):uid+='_next'
base={k:v[k] for k in ['source_net_id','source_trace_id','subcircuit_connectivity_map_key','subcircuit_id'] if k in v}
g.j.append({'type':'pcb_trace','pcb_trace_id':uid,'route':[{'route_type':'wire','x':a,'y':b,'layer':construction_layer,'width':width} for a,b in pts],'pcb_port_ids':[],**base})
g.j.append({'type':'pcb_via','pcb_via_id':uid+'_via','x':x,'y':y,'outer_diameter':via_radius*2,'hole_diameter':.3,'layers':stack,'from_layer':'top','to_layer':'bottom','tented_on_top':True,'tented_on_bottom':True,**base})
rec={'net':name,'start_mm':pts[0],'end_mm':[x,y],'length_mm':LineString(pts).length,'ground_plane_requires_refill_and_connectivity_verification':True};receipts.append(rec);print(rec,flush=True)
tree,shapes,owners,physical_layers=build_index()
break
g.path.write_text(json.dumps(g.j,indent=2)+'\n');(g.ROOT/'artifacts/reference-feedback/local-inner1-escapes.json').write_text(json.dumps(receipts,indent=2)+'\n')