astra/nema17-stepper-motor-controller-spring-clamp-rp2040

A 4-layer RP2040 controller board that negotiates USB‑C PD motor power and drives a NEMA17 stepper via a DRV8825, with current sensing, thermal shutdown, RGB status LED, buzzer alarm, optional AS5600 encoder, protection/filtering, and test points.

Version
1.4.0
License
unset
Stars
0

scripts/audit-ground-copper.py

import json,collections,hashlib,sys
from pathlib import Path
from shapely import make_valid
from shapely.geometry import Point,Polygon,LineString,box
from shapely.affinity import rotate,translate
from shapely.strtree import STRtree
input_path=Path(sys.argv[1] if len(sys.argv)>1 else 'dist/index/circuit.json')
j=json.loads(input_path.read_text()); parent={}
def root(x):
 parent.setdefault(x,x)
 if parent[x]!=x:parent[x]=root(parent[x])
 return parent[x]
def join(a,b):parent[root(a)]=root(b)
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 x in ids[1:]:join(ids[0],x)
gnd=root(next(e['source_net_id'] for e in j if e['type']=='source_net' and e['name']=='GND'))
ports={e['pcb_port_id']:root(e['source_port_id'])==gnd for e in j if e['type']=='pcb_port' and e.get('source_port_id')}
traces={e['source_trace_id']:any(root(x)==gnd for x in e['connected_source_port_ids']+e['connected_source_net_ids']) for e in j if e['type']=='source_trace'}
ground_vias={(round(r['x'],6),round(r['y'],6)) for e in j if e['type']=='pcb_trace' and traces.get(e.get('source_trace_id')) for r in e['route'] if r['route_type']=='via'}
shapes=[]; meta=[]; barrels=[]
def add(g,l,id):
 if g.is_empty:return
 if g.geom_type in ("MultiPolygon", "GeometryCollection"):
  for piece in g.geoms:add(piece,l,id)
  return
 shapes.append(g);meta.append((l,id))
def pad(e,outer=False,hole=False):
 prefix='hole_' if hole else 'outer_' if outer else ''
 if e['shape']=='circle':g=Point(0,0).buffer(e.get(prefix+'diameter',2*e.get('radius',0))/2,quad_segs=64)
 else:
  w,h=e[prefix+'width'],e[prefix+'height']; r=min(w,h)/2 if 'pill' in e['shape'] else e.get('corner_radius',0)
  if r:
   if abs(w-2*r)<1e-8 or abs(h-2*r)<1e-8:g=LineString([(-w/2+r,-h/2+r),(w/2-r,h/2-r)]).buffer(r,quad_segs=64)
   else:g=box(-w/2+r,-h/2+r,w/2-r,h/2-r).buffer(r,quad_segs=64)
  else:g=box(-w/2,-h/2,w/2,h/2)
 return translate(rotate(g,e.get('ccw_rotation',0),origin=(0,0)),e['x'],e['y'])
for e in j:
 t=e['type'];id=e.get(t+'_id')
 if t=='pcb_copper_pour' and root(e['source_net_id'])==gnd:
  b=e['brep_shape']; xy=lambda r:[(v['x'],v['y']) for v in r['vertices']]
  g=Polygon(xy(b['outer_ring']),[xy(r) for r in b['inner_rings']]);g=make_valid(g);add(g,e['layer'],id)
 elif t=='pcb_smtpad' and ports.get(e.get('pcb_port_id')):add(pad(e),e['layer'],id)
 elif t=='pcb_plated_hole' and ports.get(e.get('pcb_port_id')):
  ids=[];g=pad(e,outer=True).difference(pad(e,hole=True))
  for l in e['layers']:ids.append(len(shapes));add(g,l,id)
  barrels.append(ids)
 elif t=='pcb_via' and (traces.get(e.get('source_trace_id')) or (round(e['x'],6),round(e['y'],6)) in ground_vias):
  g=Point(e['x'],e['y']).buffer(e['outer_diameter']/2,quad_segs=64).difference(Point(e['x'],e['y']).buffer(e['hole_diameter']/2,quad_segs=64));ids=[]
  for l in e['layers']:ids.append(len(shapes));add(g,l,id)
  barrels.append(ids)
 elif t=='pcb_trace' and traces.get(e.get('source_trace_id')):
  for a,b in zip(e['route'],e['route'][1:]):
   if a['route_type']=='wire' and b['route_type']=='wire' and a['layer']==b['layer']:
    add(LineString([(a['x'],a['y']),(b['x'],b['y'])]).buffer(min(a['width'],b['width'])/2,quad_segs=64),a['layer'],id)
parent={}
for i in range(len(shapes)):root(i)
for ids in barrels:
 for i in ids[1:]:join(ids[0],i)
for l in ['top','inner1','inner2','bottom']:
 indices=[i for i,m in enumerate(meta) if m[0]==l];tree=STRtree([shapes[i] for i in indices])
 for i in indices:
  for k in tree.query(shapes[i].buffer(1e-7)):
   other=indices[k]
   if shapes[i].distance(shapes[other])<=1e-7:join(i,other)
groups=collections.defaultdict(list)
for i in range(len(shapes)):groups[root(i)].append(i)
result={'circuitJsonSha256':hashlib.sha256(input_path.read_bytes()).hexdigest(),'groundCopperConnectedComponents':len(groups),'groundPourCount':sum(e['type']=='pcb_copper_pour' for e in j),'geometryPieceCount':len(shapes),'groups':[{'pieces':len(v),'areaSumMm2':sum(shapes[i].area for i in v),'elements':sorted(set(meta[i][1] for i in v))} for v in sorted(groups.values(),key=len,reverse=True)]}
Path('reports').mkdir(exist_ok=True)
Path('reports/ground-copper-connectivity.json').write_text(json.dumps(result,indent=2)+'\n')
print(json.dumps({**result,'groups':[dict(g,elements=g['elements'] if g['pieces']<30 else ['main ground network']) for g in result['groups']]},indent=2))

if len(groups) != 1:sys.exit("Ground copper is not one connected network")