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/plan-coupled-via-fix.py
"""Candidate-only joint edit of one control via and its surrounding doglegs.
Keep all pads/terminals, widths and layers. Move the entire nearby unanchored
dogleg vertices consistently; validate actual candidate metal before emitting.
The normal build and independent continuity/drill audits remain mandatory.
"""
from route_geometry import *
from shapely.strtree import STRtree
from collections import defaultdict
import copy
c=load_circuit("dist/index/circuit.json");root=connectivity(c)
sources={e["source_trace_id"]:e for e in c if e["type"]=="source_trace"}
pcb_traces={e["pcb_trace_id"]:e for e in c if e["type"]=="pcb_trace"}
ports={e["pcb_port_id"]:e for e in c if e["type"]=="pcb_port"}
def net(t):
s=sources[t["source_trace_id"]]
return root((s.get("connected_source_port_ids",[])+s.get("connected_source_net_ids",[]))[0])
def xy(p):return round(p["x"],6),round(p["y"],6)
routes=json.loads(Path("routing/complete-routes.json").read_text())
vias=[dict(x=e["x"],y=e["y"],net=net({"source_trace_id":e.get("source_trace_id",pcb_traces.get(e.get("pcb_trace_id"),{}).get("source_trace_id"))})) for e in c if e["type"]=="pcb_via"]
pad_metal=[];anchors=set()
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 None
layers=e.get("layers",[e.get("layer")])
pad_metal.append(dict(net=n,layers=layers,shape=geometry(e,"outer_" if e["type"]=="pcb_plated_hole" else "")))
if p:
for l in layers:anchors.add((n,l,xy(p)))
for v in vias:
for l in LAYERS:anchors.add((v["net"],l,xy(v)))
holes=[geometry(e,"hole_") for e in c if e["type"]=="pcb_hole"]
def collect(rr):
out={}
for t in rr:
n=net(t)
for a,b in zip(t["route"],t["route"][1:]):
if a["route_type"]!=b["route_type"] or a["route_type"]!="wire" or a["layer"]!=b["layer"] or math.dist(xy(a),xy(b))<1e-7:continue
k=n,a["layer"],tuple(sorted([xy(a),xy(b)]))
out[k]=dict(net=n,layers=[a["layer"]],shape=LineString([xy(a),xy(b)]).buffer(max(a["width"],b["width"])/2,quad_segs=32))
return out
old_segments=collect(routes)
old=(5.2251,-7.4842);v=next(v for v in vias if math.dist(xy(v),old)<1e-7)
accepted=None;rejections=[]
for dx,dy in [(0.02,0.03),(0.015,0.035),(0.02,0.04),(0.03,0.03),(0.03,0.04),(0.04,0.04)]:
rr=copy.deepcopy(routes);new=(round(old[0]+dx,6),round(old[1]+dy,6));point_moves={}
for t in rr:
n=net(t)
for p in t["route"]:
point=xy(p)
if n==v["net"] and point==old:target=new
elif p["route_type"]=="wire" and n!=v["net"] and math.dist(point,old)<.6 and (n,p["layer"],point) not in anchors:
target=(round(point[0]+dx,6),round(point[1]+dy,6))
else:continue
point_moves[n,p.get("layer"),point]=target;p["x"],p["y"]=target
segments=collect(rr);changed=[g for k,g in segments.items() if k not in old_segments]
metal=list(segments.values())+pad_metal;tree=STRtree([m["shape"] for m in metal]);reason=None
center=Point(new)
for j in tree.query(center.buffer(.353)):
m=metal[int(j)]
if m["net"]!=v["net"] and center.distance(m["shape"])<.351:reason="new drill to foreign metal";break
if reason is None and any(q is not v and math.dist(new,xy(q))<.551 for q in vias):reason="other via"
if reason is None:
for m in changed:
g=m["shape"]
if max(abs(b) for b in g.bounds)>20.95 or any(g.distance(h)<.2 for h in holes):reason="edge/hole";break
for j in tree.query(g.buffer(.1)):
q=metal[int(j)]
if m["net"]!=q["net"] and set(m["layers"])&set(q["layers"]) and g.distance(q["shape"])<.1-.00005:reason="copper clearance";break
if reason:break
for q in vias:
p=Point(new if q is v else xy(q))
if q["net"]==m["net"] or p.distance(g)>=.35-.0002:continue
# Only retained, byte-identical old metal may remain in an
# inherited drill exclusion. No new metal gets that waiver.
original=[z["shape"] for z in old_segments.values() if z["net"]==m["net"] and z["layers"]==m["layers"] and z["shape"].distance(p)<.36]
from shapely.ops import unary_union
added=g.intersection(p.buffer(.35-.0002)).difference(unary_union(original).buffer(.00005))
if added.area>1e-7:reason="other drill";break
if reason:break
if reason:rejections.append(dict(delta=[dx,dy],reason=reason));continue
accepted=dict(old=old,new=new,shiftedVertices=len(point_moves),changedSegments=len(changed))
Path("routing/coupled-via-candidate.json").write_text(json.dumps(rr,indent=2)+"\n")
break
report=dict(accepted=accepted,rejections=rejections,notInstalled=True)
Path("reports/coupled-via-candidate.json").write_text(json.dumps(report,indent=2)+"\n")
print(json.dumps(report))