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