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-round8-part-moves.py
"""Read-only: find drill-free component sites on the existing routed copper.
Every pad must still land fully across an actual same-net track. Candidate
placement is not installed, and no connectivity/clearance rule is relaxed.
"""
from route_geometry import *
from shapely.ops import unary_union
from shapely.strtree import STRtree
from shapely.geometry import MultiPoint
from collections import defaultdict
import os
new_fanout=os.environ.get("ALLOW_NEW_FANOUT")=="1"
search_radius=float(os.environ.get("PART_MOVE_RADIUS","1.5"))
search_pitch=float(os.environ.get("PART_MOVE_PITCH",".05"))
requested_names=set(filter(None,os.environ.get("PART_MOVE_NAMES","").split(",")))
ignore_tracks=os.environ.get('PART_MOVE_IGNORE_TRACKS')=='1'
c=load_circuit(os.environ.get('PART_MOVE_INPUT','dist/index/circuit.json'));root=connectivity(c)
source={e["source_component_id"]:e for e in c if e["type"]=="source_component"}
parts={e["pcb_component_id"]:e for e in c if e["type"]=="pcb_component"}
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"}
metal=[];courts={};pad_by_part=defaultdict(list);wires=defaultdict(list)
for e in c:
if e["type"] in ["pcb_smtpad","pcb_plated_hole"]:
p=ports.get(e.get("pcb_port_id"));n=root(p["source_port_id"]) if p else "NC"
g=geometry(e,"outer_" if e["type"]=="pcb_plated_hole" else "")
layers=e.get("layers",[e.get("layer")]);metal.append(dict(net=n,layers=layers,shape=g,margin=.1503,pid=e["pcb_component_id"]))
pad_by_part[e["pcb_component_id"]].append((e,n,g))
if e["type"]=="pcb_plated_hole":metal.append(dict(net="DRILL",layers=LAYERS,shape=geometry(e,"hole_"),margin=.2803))
elif e["type"]=="pcb_via":metal.append(dict(net="DRILL",layers=LAYERS,shape=Point(e["x"],e["y"]),margin=.331))
elif e["type"]=="pcb_hole":metal.append(dict(net="DRILL",layers=LAYERS,shape=geometry(e,"hole_"),margin=.2003))
elif e["type"]=="pcb_trace":
if ignore_tracks:continue
s=traces.get(e.get("source_trace_id"));ids=(s.get("connected_source_port_ids",[])+s.get("connected_source_net_ids",[])) if s else []
if not ids:continue
n=root(ids[0])
for a,b in zip(e["route"],e["route"][1:]):
if a["route_type"]==b["route_type"]=="wire" and a["layer"]==b["layer"] and math.dist((a["x"],a["y"]),(b["x"],b["y"]))>1e-7:
line=LineString([(a["x"],a["y"]),(b["x"],b["y"])])
width=max(a["width"],b["width"])
metal.append(dict(net=n,layers=[a["layer"]],shape=line.buffer(width/2),margin=.1003))
wires[n,a["layer"]].append((line,width))
elif e["type"].startswith("pcb_courtyard_"):
pid=e.get("pcb_component_id");p=parts.get(pid)
if not p or p.get("do_not_place"):continue
if e["type"]=="pcb_courtyard_outline":g=MultiPoint([(v["x"],v["y"]) for z in c if z["type"]=="pcb_courtyard_outline" and z.get("pcb_component_id")==pid for v in z["outline"]]).convex_hull
elif e["type"]=="pcb_courtyard_circle":g=Point(e["center"]["x"],e["center"]["y"]).buffer(e["radius"])
else:
x,y=e["center"]["x"],e["center"]["y"]
g=rotate(box(x-e["width"]/2,y-e["height"]/2,x+e["width"]/2,y+e["height"]/2),e.get("ccw_rotation",0),origin=(x,y))
courts[pid]=courts.get(pid,Polygon()).union(g)
# Deduplicate the repeated source-associated copper before spatial queries.
unique={ (o["net"],tuple(o["layers"]),o["shape"].wkb,o.get("pid"),o["margin"]):o for o in metal }
metal=list(unique.values());tree=STRtree([o["shape"] for o in metal])
targets=["C_B_VDD_CORE","C_B_AVCC","C_BUCK_OUT1","R_ETH_RAIL_AB","C_ETH_XI","Q_PD_ENABLE","C_U_3V3_IN","C_ETH_RXP_AC","C_ETH_TCT","U_STATUS_BUFFER","C_STATUS_BUFFER"]
targets=list(dict.fromkeys(targets+sorted(requested_names)))
result=[]
for name in targets:
if requested_names and name not in requested_names:continue
part=next(p for p in parts.values() if source[p["source_component_id"]]["name"]==name)
pid=part["pcb_component_id"];x0,y0=part["center"]["x"],part["center"]["y"];layer=part["layer"]
court=courts.get(pid,box(x0-part["width"]/2,y0-part["height"]/2,x0+part["width"]/2,y0+part["height"]/2))
candidates=[]
steps=int(search_radius/search_pitch)
offsets=sorted((math.hypot(dx,dy),dx*search_pitch,dy*search_pitch) for dx in range(-steps,steps+1) for dy in range(-steps,steps+1) if (dx,dy)!=(0,0))
# Quarter-turns preserve nonpolar passive/IC pin numbering, not net swaps.
for _,dx,dy in offsets:
for angle in [0,90,180,270]:
move=lambda g:translate(rotate(g,angle,origin=(x0,y0)),dx,dy)
body=move(court)
if max(abs(v) for v in body.bounds)>20.95:continue
if any(p!=pid and parts[p]["layer"]==layer and body.intersection(g).area>1e-8 for p,g in courts.items()):continue
hardware=2.85 if layer=="top" else 2.6
if any(body.distance(Point(x,y))<hardware for x in [-15.5,15.5] for y in [-15.5,15.5]):continue
good=True
for e,n,g in pad_by_part[pid]:
land=move(g)
for i in tree.query(land.buffer(.355)):
o=metal[int(i)]
if o.get("pid")==pid or layer not in o["layers"] or o["net"]==n:continue
if land.distance(o["shape"])<o["margin"]:good=False;break
if not good:break
# Full-width contact to unchanged metal, not an edge graze.
if not new_fanout and not any(not land.buffer(-width/2-.01).intersection(line).is_empty for line,width in wires[n,layer]):good=False;break
if good:
site=dict(x=round(x0+dx,6),y=round(y0+dy,6),rotation=(part["rotation"]+angle)%360,layer=layer,translationMm=round(math.hypot(dx,dy),4))
if not any(math.dist((s["x"],s["y"]),(site["x"],site["y"]))<.15 and s["rotation"]==site["rotation"] for s in candidates):candidates.append(site)
if len(candidates)>=5:break
result.append(dict(name=name,old=dict(x=x0,y=y0,rotation=part["rotation"],layer=layer),candidates=candidates))
print(json.dumps(result[-1]),flush=True)
Path("reports/round8-part-move-candidates.json").write_text(json.dumps(dict(notInstalled=True,requiresNewFanout=new_fanout,parts=result),indent=2)+"\n")