shibosoftwaredev/f1c100s-linux-nema17-stepper-controller
A four-layer NEMA 17 motor-controller PCB combining an Allwinner F1C100S Linux computer, USB‑PD/buck power system, W5500 Ethernet, STM32 real-time motion control, DRV8825 stepper driver, protection/temperature/current telemetry, USB interfaces, and local LED/buzzer diagnostics.
- Version
- 0.4.56
- License
- unset
- Stars
- 0
scripts/complete-review-clock-fanout.py
"""Draw the final short clock fanout through an off-grid legal clearance.
Constrained triangles describe the exact free copper polygon; every emitted
segment is then checked independently, including the imported Gerbers.
"""
from route_geometry import *
from shapely.ops import unary_union
from shapely.strtree import STRtree
import heapq,subprocess
c=load_circuit('routing/review-placement.circuit.json');root=connectivity(c)
mapping=json.loads(Path('routing/net-map.json').read_text())['roots']
ports={e['pcb_port_id']:e for e in c if e['type']=='pcb_port'}
physical=json.loads(Path('routing/bulk-top-closure-partial-physical-candidate.json').read_text())
plan=json.loads(Path('routing/bulk-top-clock-plan.json').read_text())
attempt=json.loads(Path('reports/bulk-top-closure-attempt.json').read_text())
remaining=plan['jobs'][len(attempt['routes']):]
assert remaining[0]['source']=='source_trace_222'
a=(13.271604046558629,-4.254637514057382);b=(12.664442,-.536658);width=.15;n='N162';layer='inner2'
blocked=[]
for e in c:
if e['type'] not in ['pcb_smtpad','pcb_plated_hole']:continue
if layer not in e.get('layers',[e.get('layer')]):continue
p=ports.get(e.get('pcb_port_id'));nn=mapping[root(p['source_port_id'])] if p else 'NC'
if nn!=n:blocked.append(geometry(e,'outer_' if e['type']=='pcb_plated_hole' else '').buffer(.1754))
if nn!=n and e['type']=='pcb_plated_hole':blocked.append(geometry(e,'hole_').buffer(.3754))
for e in c:
if e['type']=='pcb_hole':blocked.append(geometry(e,'hole_').buffer(.2754))
for w in physical['wires']:
if w['net']!=n and w['layer']==layer:blocked.append(LineString(w['points']).buffer(w['width']/2+.1754))
for v in physical['vias']:
if v['net']!=n:blocked.append(Point(v['x'],v['y']).buffer(.4291))
obstacles=unary_union(blocked)
free=box(-20.87,-20.87,20.87,20.87).difference(obstacles)
polygons=list(free.geoms) if hasattr(free,'geoms') else [free]
polygon=next(g for g in polygons if g.covers(Point(a)) and g.covers(Point(b)))
raw=subprocess.run(['/opt/homebrew/bin/bun','scripts/triangulate-review-polygon.mjs'],
input=json.dumps(dict(outer=list(polygon.exterior.coords),holes=[list(h.coords) for h in polygon.interiors])),
text=True,capture_output=True,check=True)
triangles=[Polygon(xy) for xy in json.loads(raw.stdout)]
assert all(polygon.buffer(1e-8).covers(t) for t in triangles)
tree=STRtree(triangles)
start=next(int(i) for i in tree.query(Point(a)) if triangles[int(i)].covers(Point(a)))
goal=next(int(i) for i in tree.query(Point(b)) if triangles[int(i)].covers(Point(b)))
edges={};adj={i:[] for i in range(len(triangles))}
for i,t in enumerate(triangles):
xy=list(t.exterior.coords)
for u,v in zip(xy,xy[1:]):
key=tuple(sorted([tuple(u),tuple(v)]))
if key in edges:
j=edges[key];p=((u[0]+v[0])/2,(u[1]+v[1])/2)
adj[i].append((j,p));adj[j].append((i,p))
else:edges[key]=i
queue=[(0,start)];costs={start:0};previous={}
while queue:
cost,i=heapq.heappop(queue)
if cost>costs[i]+1e-8:continue
if i==goal:break
for j,p in adj[i]:
delta=triangles[i].centroid.distance(triangles[j].centroid)
if cost+delta<costs.get(j,math.inf):costs[j]=cost+delta;previous[j]=(i,p);heapq.heappush(queue,(cost+delta,j))
assert goal in costs
points=[b];node=goal
while node!=start:node,p=previous[node];points.append(p)
points.append(a);points.reverse()
# Remove superfluous triangle crossings without cutting any obstacle.
simple=[points[0]];i=0
while i<len(points)-1:
j=max(j for j in range(i+1,len(points)) if polygon.covers(LineString([points[i],points[j]])))
simple.append(points[j]);i=j
wire=dict(net=n,layer=layer,width=width,points=simple)
assert not LineString(simple).intersects(obstacles)
physical['wires'].append(wire)
sources={}
for s in c:
if s['type']=='source_trace':
nn=mapping[root((s.get('connected_source_port_ids',[])+s.get('connected_source_net_ids',[]))[0])]
sources.setdefault(nn,s['source_trace_id'])
retained=[]
for i,w in enumerate(physical['wires']):
retained.append(dict(type='pcb_trace',pcb_trace_id=f'review_final_wire_{i}',source_trace_id=sources[w['net']],
route=[dict(route_type='wire',x=x,y=y,layer=w['layer'],width=w['width']) for x,y in w['points']]))
for i,v in enumerate(physical['vias']):
retained.append(dict(type='pcb_trace',pcb_trace_id=f'review_final_via_{i}',source_trace_id=sources[v['net']],
route=[dict(route_type='via',x=v['x'],y=v['y'],from_layer='top',to_layer='bottom',via_diameter=.45,via_hole_diameter=.3)]))
Path('routing/review-clock-fanout.json').write_text(json.dumps(dict(wires=[wire],vias=[]),indent=2)+'\n')
Path('routing/bulk-top-final-retained.json').write_text(json.dumps(retained,indent=2)+'\n')
Path('routing/bulk-top-final-plan.json').write_text(json.dumps({**plan,'retained':'routing/bulk-top-final-retained.json','jobs':remaining[1:]},indent=2)+'\n')
print(json.dumps(dict(manualClockFanout=simple,lengthMm=LineString(simple).length,remainingJobs=len(remaining)-1)))