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/repair-wire-clearance.py
"""Local trace detours around existing drills; candidate only, no auto-install.
Preserve all source identities/widths/layers/pad endpoints. Propose small arc
detours on straight trace segments, accepting only exact-geometry changes that
do not worsen other drill, copper, edge, hole or same-net contact clearances.
"""
from route_geometry import *
from collections import defaultdict
from shapely.strtree import STRtree
from shapely.ops import nearest_points
import time
c=load_circuit("dist/index/circuit.json");root=connectivity(c)
src={e["source_trace_id"]:e for e in c if e["type"]=="source_trace"}
ports={e["pcb_port_id"]:e for e in c if e["type"]=="pcb_port"}
def net(t):
s=src[t["source_trace_id"]];ids=s.get("connected_source_port_ids",[])+s.get("connected_source_net_ids",[])
return root(ids[0])
def xy(p):return round(p["x"],6),round(p["y"],6)
def sig(n,l,a,b):return n,l,tuple(sorted([a,b]))
routes=json.loads(Path("routing/complete-routes.json").read_text())
segments=[];seen={};refs=defaultdict(list)
for t in routes:
n=net(t)
for i,(a,b) in enumerate(zip(t["route"],t["route"][1:])):
if a["route_type"]!=b["route_type"] or a["route_type"]!="wire" or a["layer"]!=b["layer"]:continue
if math.dist(xy(a),xy(b))<1e-7:continue
k=sig(n,a["layer"],xy(a),xy(b));refs[k].append((t,i))
if k in seen:continue
seen[k]=len(segments)
segments.append(dict(key=k,net=n,layer=a["layer"],width=max(a["width"],b["width"]),points=[xy(a),xy(b)]))
def shape(s):return LineString(s["points"]).buffer(s["width"]/2,quad_segs=16)
metal=[dict(net=s["net"],layers=[s["layer"]],shape=shape(s)) for s in segments]
for e in c:
if e["type"] not in ["pcb_smtpad","pcb_plated_hole"]:continue
p=ports.get(e.get("pcb_port_id"));n=root(p["source_port_id"]) if p else "NC:"+e[e["type"]+"_id"]
metal.append(dict(net=n,layers=e.get("layers",[e.get("layer")]),
shape=geometry(e,"outer_" if e["type"]=="pcb_plated_hole" else "")))
traces={e["pcb_trace_id"]:e for e in c if e["type"]=="pcb_trace"}
vias=[dict(x=e["x"],y=e["y"],net=net({"source_trace_id":e.get("source_trace_id",traces.get(e.get("pcb_trace_id"),{}).get("source_trace_id"))}))
for e in c if e["type"]=="pcb_via"]
holes=[geometry(e,"hole_") for e in c if e["type"]=="pcb_hole"]
tree=STRtree([m["shape"] for m in metal])
def bad_pairs():
return [(v,int(i)) for v in vias for i in tree.query(Point(v["x"],v["y"]).buffer(.35))
if int(i)<len(segments) and metal[int(i)]["net"]!=v["net"]
and Point(v["x"],v["y"]).distance(metal[int(i)]["shape"])<.35-.0002]
def valid(i,points,target):
s=segments[i];old=metal[i]["shape"];g=LineString(points).buffer(s["width"]/2,quad_segs=16)
if Point(target["x"],target["y"]).distance(g)<.353:return False
if max(abs(b) for b in g.bounds)>20.95:return False
if any(g.distance(h)<.202 for h in holes):return False
for j in tree.query(g.buffer(.103)):
j=int(j);m=metal[j]
if j==i or s["layer"] not in m["layers"]:continue
if m["net"]!=s["net"] and g.distance(m["shape"])<.1001:return False
for v in vias:
p=Point(v["x"],v["y"])
if v["net"]!=s["net"]:
if p.distance(g)<min(.353,p.distance(old))-.000001:return False
elif p.distance(old)<=.225 and p.distance(g)>.2249:return False
# Preserve own-net junctions which do not occur at the segment endpoints.
for j in tree.query(old):
j=int(j);m=metal[j]
if j==i or s["layer"] not in m["layers"] or m["net"]!=s["net"]:continue
if old.intersects(m["shape"]) and not g.intersects(m["shape"]):return False
radius=2.85 if s["layer"]=="top" else 2.6 if s["layer"]=="bottom" else 0
if radius and any(g.intersection(Point(x,y).buffer(radius)).difference(old.buffer(.0001)).area>1e-7
for x in [-15.5,15.5] for y in [-15.5,15.5]):return False
return True
before=len(bad_pairs());changes={};moves=[];start=time.monotonic()
for v,i in bad_pairs():
if time.monotonic()-start>45:break
s=segments[i]
if i in changes:continue
a,b=s["points"];dx,dy=b[0]-a[0],b[1]-a[1];length=math.hypot(dx,dy)
if length<.05:continue
ux,uy=dx/length,dy/length
vx,vy=v["x"],v["y"]
projection=(vx-a[0])*ux+(vy-a[1])*uy
signed=(a[0]-vx)*(-uy)+(a[1]-vy)*ux
# Route on the existing side of the drill; do not cross the phase/net.
side=1 if signed>=0 else -1
for margin in [.003,.006,.01,.02,.04]:
radius=.35+s["width"]/2+margin
if abs(signed)>=radius:continue
along=math.sqrt(radius*radius-signed*signed)
lo,hi=projection-along,projection+along
if lo<=.001 or hi>=length-.001:continue
angle=math.acos(abs(signed)/radius)
arc=[]
for j in range(9):
t=-angle+j*(2*angle/8)
xx=radius*math.sin(t);yy=side*radius*math.cos(t)
arc.append((round(vx+xx*ux-yy*uy,6),round(vy+xx*uy+yy*ux,6)))
points=[a,*arc,b]
if not valid(i,points,v):continue
changes[i]=points;metal[i]["shape"]=LineString(points).buffer(s["width"]/2,quad_segs=16)
moves.append(dict(via=[vx,vy],net=s["net"],layer=s["layer"],old=[a,b],new=points))
tree=STRtree([m["shape"] for m in metal]);break
vertex_changes={};vertex_moves=[]
for v,i in bad_pairs():
if time.monotonic()-start>45:break
if i in changes:continue
s=segments[i]
for old in list(s["points"]):
vertex=(s["net"],s["layer"],old)
if vertex in vertex_changes or math.dist(old,(v["x"],v["y"]))>.9:continue
if any(q["net"]==s["net"] and math.dist(old,(q["x"],q["y"]))<1e-5 for q in vias):continue
attached=[j for j,z in enumerate(segments) if j not in changes and z["net"]==s["net"]
and z["layer"]==s["layer"] and old in z["points"]]
moved=False
for r in [.005,.01,.015,.02,.025,.03,.04,.05,.06]:
if moved:break
for angle in range(16):
target=(round(old[0]+r*math.cos(angle*math.pi/8),6),round(old[1]+r*math.sin(angle*math.pi/8),6))
proposals={j:[target if p==old else p for p in segments[j]["points"]] for j in attached}
if not all(valid(j,pts,v) for j,pts in proposals.items()):continue
vertex_changes[vertex]=target
for j,pts in proposals.items():
segments[j]["points"]=pts;metal[j]["shape"]=LineString(pts).buffer(segments[j]["width"]/2,quad_segs=16)
vertex_moves.append(dict(net=s["net"],layer=s["layer"],old=old,new=target,segments=len(attached)))
tree=STRtree([m["shape"] for m in metal]);moved=True;break
if moved:break
for t in routes:
original=t["route"];out=[];n=net(t)
for a,b in zip(original,original[1:]):
out.append(a)
if a["route_type"]!=b["route_type"] or a["route_type"]!="wire" or a["layer"]!=b["layer"]:continue
k=sig(n,a["layer"],xy(a),xy(b));i=seen.get(k)
if i not in changes:continue
points=changes[i]
if math.dist(points[0],xy(a))>1e-6:points=list(reversed(points))
out.extend(dict(route_type="wire",layer=a["layer"],width=max(a["width"],b["width"]),x=x,y=y)
for x,y in points[1:-1])
if original:out.append(original[-1])
for p in out:
if p["route_type"]=="wire":
target=vertex_changes.get((n,p["layer"],xy(p)))
if target:p["x"],p["y"]=target
t["route"]=out
after=len(bad_pairs())
Path("routing/wire-clearance-candidate.json").write_text(json.dumps(routes,indent=2)+"\n")
report=dict(seconds=round(time.monotonic()-start,2),trackFindingsBefore=before,trackFindingsAfter=after,
detours=moves,vertexMoves=vertex_moves,notInstalled=True)
Path("reports/wire-repair-candidate.json").write_text(json.dumps(report,indent=2)+"\n")
print(json.dumps({k:v for k,v in report.items() if k not in ["detours","vertexMoves"]}|{"detours":len(moves),"vertexMoves":len(vertex_moves)}))