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-manufacturing-legend.py
"""Find readable legend sites without changing component or copper placement."""
from route_geometry import *
from shapely.ops import unary_union
from shapely.prepared import prep
import sys
import hashlib
c=load_circuit("dist/index/circuit.json")
sources={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"}
obstacles={l:[] for l in ["top","bottom"]}
for e in c:
if e["type"] in ["pcb_smtpad","pcb_plated_hole"]:
g=geometry(e,"outer_" if e["type"]=="pcb_plated_hole" else "").buffer(.16)
for l in e.get("layers",[e.get("layer")]):
if l in obstacles:obstacles[l].append(g)
elif e["type"]=="pcb_hole":
for l in obstacles:obstacles[l].append(geometry(e,"hole_").buffer(.2))
elif e["type"]=="pcb_component" and not e.get("do_not_place"):
x,y=e["center"]["x"],e["center"]["y"]
obstacles[e["layer"]].append(box(x-e["width"]/2-.1,y-e["height"]/2-.1,x+e["width"]/2+.1,y+e["height"]/2+.1))
elif e["type"]=="pcb_courtyard_outline":
component=parts.get(e.get("pcb_component_id"))
if component and not component.get("do_not_place"):
points=[(p["x"],p["y"]) for p in e["outline"]]
if len(points)>=3:obstacles[component["layer"]].append(Polygon(points).buffer(.1))
elif e["type"]=="pcb_courtyard_rect":
component=parts.get(e.get("pcb_component_id"))
if component and not component.get("do_not_place"):
x,y=e["center"]["x"],e["center"]["y"]
obstacles[component["layer"]].append(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)).buffer(.1))
for l in obstacles:
for x in [-15.5,15.5]:
for y in [-15.5,15.5]:obstacles[l].append(Point(x,y).buffer(2.95 if l=="top" else 2.7))
# A 1.7 mm tscircuit2024 font has nominal 0.153 mm strokes (ratio 0.09).
# Exact glyph bounds and exported Gerber strokes are validated separately.
if "--audit" in sys.argv:
findings=[];labels=[]
numeric_path=Path("reports/round9-pin-glyph-bounds.json")
numeric=json.loads(numeric_path.read_text()) if numeric_path.exists() else None
if numeric:
assert numeric["circuitSha256"]==hashlib.sha256(Path("dist/index/circuit.json").read_bytes()).hexdigest()
for e in c:
if e["type"]!="pcb_silkscreen_text":continue
x,y=e["anchor_position"]["x"],e["anchor_position"]["y"];f=e["font_size"]
w=max(1,len(e["text"]))*f*.9
h=f
if numeric and e["pcb_silkscreen_text_id"] in numeric["labels"]:
ink=numeric["labels"][e["pcb_silkscreen_text_id"]]
assert ink["text"]==e["text"] and e["anchor_alignment"]=="center" and e.get("ccw_rotation",0)==0
w,h=ink["width"],ink["height"]
g=box(x-w/2,y-h/2,x+w/2,y+h/2)
if f<1.7-1e-6:findings.append(dict(text=e["text"],reason="font/stroke below production target"))
if max(abs(v) for v in g.bounds)>20.95:findings.append(dict(text=e["text"],reason="edge"))
if unary_union(obstacles[e["layer"]]).intersects(g):findings.append(dict(text=e["text"],reason="body/courtyard/pad/hole"))
if any(l==e["layer"] and g.distance(other)<.15 for l,other in labels):findings.append(dict(text=e["text"],reason="other label"))
labels.append((e["layer"],g))
report=dict(labels=len(labels),findings=findings,fontSizeMm=1.7,nominalStrokeMm=1.7*.09,
scope="Conservative text rectangles (numeric labels: hash-bound packaged ink-outline bounds including stroke) versus actual pads (+0.16), fitted component bodies/courtyards (+0.10), holes, screw envelopes and outline. No clearance reduction or copper changes.")
Path("reports/round5-legend-audit.json").write_text(json.dumps(report,indent=2)+"\n")
print(json.dumps(report));raise SystemExit(bool(findings))
specs=[("D","bottom",(-7,15.5),5),("PD","top",(7,15.5),6),
("M","bottom",(3,-16.2),3.5),("R","top",(14,1),4),
("FEL","top",(-18,19.45),5),("N17","bottom",(0,18),7)]
if "--motor-pins" in sys.argv:
specs=[(str(4-i),"bottom",(2*i,-17.05),5) for i in range(4)]
for e in c:
if e["type"]=="pcb_silkscreen_text" and e["text"] not in {"1","2","3","4"}:
x,y=e["anchor_position"]["x"],e["anchor_position"]["y"]
w=len(e["text"])*.9*e["font_size"];h=e["font_size"]
obstacles[e["layer"]].append(box(x-w/2,y-h/2,x+w/2,y+h/2).buffer(.15))
result=[]
for text,l,target,radius in specs:
w=max(1,len(text))*.9*1.7;h=1.7
blocked=prep(unary_union(obstacles[l]))
candidates=sorted((math.hypot(ix*.1,iy*.1),round(target[0]+ix*.1,3),round(target[1]+iy*.1,3))
for ix in range(-int(radius*10),int(radius*10)+1) for iy in range(-int(radius*10),int(radius*10)+1))
found=None
for distance,x,y in candidates:
if distance>radius:break
g=box(x-w/2,y-h/2,x+w/2,y+h/2)
if max(abs(v) for v in g.bounds)>20.9 or blocked.intersects(g):continue
found=dict(text=text,layer=l,x=x,y=y,fontSize=1.7,plannedBounds=list(g.bounds),distance=round(distance,3))
result.append(found);obstacles[l].append(g.buffer(.25));break
if not found:result.append(dict(text=text,layer=l,noSite=True))
print(json.dumps(result,indent=2))
Path("reports/round5-legend-plan.json").write_text(json.dumps(result,indent=2)+"\n")