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))