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-clearance-placement.py

"""Propose small component translations for pad-limited drill clearances.

No edits installed. Keep all pads, pins, original copper and via sizes. Reject
new pad/track/drill gaps and any lost existing same-net pad contact. A normal
build and full emitted-metal regression audit are still required afterwards.
"""
from route_geometry import *
from shapely.strtree import STRtree
from collections import defaultdict
import sys

c=load_circuit("dist/index/circuit.json");root=connectivity(c)
sources={e["source_component_id"]:e for e in c if e["type"]=="source_component"}
components={e["pcb_component_id"]:e for e in c if e["type"]=="pcb_component"}
by_name={sources[e["source_component_id"]]["name"]:e for e in components.values()}
ports={e["pcb_port_id"]:e for e in c if e["type"]=="pcb_port"}
traces={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"}
def net(t):
    s=traces[t["source_trace_id"]]
    return root((s.get("connected_source_port_ids",[])+s.get("connected_source_net_ids",[]))[0])
metal=[];seen=set();indices=defaultdict(list)
for e in c:
    if e["type"]=="pcb_smtpad":
        p=ports.get(e.get("pcb_port_id"));n=root(p["source_port_id"]) if p else None
        i=len(metal);indices[e["pcb_component_id"]].append(i)
        metal.append(dict(net=n,layer=e["layer"],shape=geometry(e),part=e["pcb_component_id"],pad=True))
    if e["type"]=="pcb_trace" and e.get("source_trace_id"):
        n=net(e)
        for a,b in zip(e["route"],e["route"][1:]):
            if a["route_type"]!=b["route_type"] or a["route_type"]!="wire" or a["layer"]!=b["layer"]:continue
            points=tuple(sorted([(a["x"],a["y"]),(b["x"],b["y"])]));k=n,a["layer"],points
            if k in seen or math.dist(*points)<1e-7:continue
            seen.add(k);metal.append(dict(net=n,layer=a["layer"],shape=LineString(points).buffer(max(a["width"],b["width"])/2,quad_segs=16),part=None,pad=False))
vias=[dict(net=net({"source_trace_id":e.get("source_trace_id",pcb_traces.get(e.get("pcb_trace_id"),{}).get("source_trace_id"))}),
           center=Point(e["x"],e["y"])) for e in c if e["type"]=="pcb_via"]
holes=[geometry(e,"hole_") for e in c if e["type"]=="pcb_hole"]
targets={"C_SV_H":(.02,0),"C_D50":(0,.03),"PE2":(0,-.03),"U_PD":(-.02,0),
         "U_ETH":(-.02,0),"U_MOTOR":(0,-.04),"C_B_AVCC":(0,-.02),
         "R_PD_STATUS_PULLUP":(-.03,0),"Q_VBUS":(-.03,0),"C_PD_ADC":(0,.03)}
diagnostic=len(sys.argv)>3
if diagnostic:targets={sys.argv[1]:tuple(map(float,sys.argv[2:4]))}
accepted=[];rejected=[]
for name,preferred in targets.items():
    component=by_name[name];own=indices[component["pcb_component_id"]]
    tree=STRtree([m["shape"] for m in metal]);contacts={}
    for i in own:
        contacts[i]=[int(j) for j in tree.query(metal[i]["shape"]) if metal[int(j)]["net"]==metal[i]["net"]
                     and metal[int(j)]["layer"]==metal[i]["layer"] and int(j) not in own
                     and metal[int(j)]["shape"].intersects(metal[i]["shape"])]
    offsets=[preferred]+[(r*math.cos(a*math.pi/8),r*math.sin(a*math.pi/8))
                       for r in [.015,.02,.03,.04,.05,.06,.08,.1] for a in range(16)]
    if diagnostic:offsets=[preferred]
    for dx,dy in offsets:
        shapes={i:translate(metal[i]["shape"],xoff=dx,yoff=dy) for i in own};valid=True
        for i,g in shapes.items():
            m=metal[i]
            if max(abs(v) for v in g.bounds)>20.95 or any(g.distance(h)<.2 for h in holes):valid=False;break
            for j in tree.query(g.buffer(.151)):
                j=int(j);other=metal[j]
                if j in own or m["net"]==other["net"] or m["layer"]!=other["layer"]:continue
                required=.15 if other["pad"] else .1
                if g.distance(other["shape"])<required-.0001:
                    if diagnostic:print(json.dumps(dict(reason="foreign metal",part=sources[components[other["part"]]["source_component_id"]]["name"] if other["part"] else "track",distance=g.distance(other["shape"]),required=required)))
                    valid=False;break
            if not valid:break
            for v in vias:
                if v["net"]!=m["net"] and v["center"].distance(g)<.351:
                    if diagnostic:print(json.dumps(dict(reason="drill",x=v["center"].x,y=v["center"].y,distance=v["center"].distance(g))))
                    valid=False;break
            if not valid or any(not g.intersects(metal[j]["shape"]) for j in contacts[i]):
                if diagnostic and valid:print("lost existing pad contact")
                valid=False;break
        if not valid:continue
        center={"x":round(component["center"]["x"]+dx,6),"y":round(component["center"]["y"]+dy,6)}
        accepted.append(dict(name=name,from_=component["center"],to=center,delta=[round(dx,6),round(dy,6)],
                             rotation=component["rotation"],layer=component["layer"]))
        for i,g in shapes.items():metal[i]["shape"]=g
        break
    else:rejected.append(name)
report=dict(accepted=accepted,rejected=rejected,notInstalled=True,
            scope="Exact pad/track/hole clearance and existing contacts; native courtyard/DRC and final copper checks still mandatory")
if not diagnostic:Path("reports/placement-clearance-candidate.json").write_text(json.dumps(report,indent=2)+"\n")
print(json.dumps(report))