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/review-round2-geometry.py
"""Additional read-only coverage: mixed drills and populated signal via-in-pad.
Distances are geometry evidence, not an assertion of CAM acceptance. The
published JLC table does not separately define mixed via/PTH pair spacing.
"""
from route_geometry import *
from shapely.ops import unary_union
import hashlib
import sys
source = Path("dist/index/circuit.json")
c = load_circuit(str(source))
out = Path(sys.argv[1])
root = connectivity(c)
parts = {e["source_component_id"]: e for e in c if e["type"] == "source_component"}
placed = {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"}
source_ports = {e["source_port_id"]: e for e in c if e["type"] == "source_port"}
vias = [e for e in c if e["type"] == "pcb_via"]
pths = [e for e in c if e["type"] == "pcb_plated_hole"]
npths = [e for e in c if e["type"] == "pcb_hole"]
lands = [e for e in c if e["type"] == "pcb_smtpad"]
def part(e):
return parts[placed[e["pcb_component_id"]]["source_component_id"]]
def drill(e):
if e["type"] == "pcb_via":
return Point(e["x"], e["y"]).buffer(e["hole_diameter"] / 2, quad_segs=32)
return geometry(e, "hole_" if "hole_diameter" in e or "hole_width" in e else "")
mixed = []
for v in vias:
for h in pths:
distance = drill(v).distance(drill(h))
if distance < .45:
mixed.append(dict(via=v["pcb_via_id"], x=v["x"], y=v["y"],
pth=h["pcb_plated_hole_id"], part=part(h)["name"], gapMm=round(distance, 6)))
via_in_populated_pad = []
for v in vias:
hole = drill(v)
for land in lands:
component = part(land)
if component.get("ftype") == "simple_test_point" or component.get("is_dnp"):
continue
if component.get("supplier_part_numbers", {}).get("jlcpcb") is None:
continue
area = hole.intersection(geometry(land)).area
if area < 1e-7:
continue
p = source_ports.get(ports.get(land.get("pcb_port_id"), {}).get("source_port_id"), {})
via_in_populated_pad.append(dict(via=v["pcb_via_id"], x=v["x"], y=v["y"],
part=component["name"], pin=p.get("pin_number"), pinName=p.get("name"),
layer=land["layer"], overlapAreaMm2=round(area, 6),
fullDrillWithinLand=geometry(land).covers(hole),
viaMaskFields={k: value for k, value in v.items() if "mask" in k or "tent" in k or "fill" in k}))
silk = [e for e in c if e["type"] == "pcb_silkscreen_text"]
outside_anchors = [dict(text=e["text"], layer=e["layer"], position=e["anchor_position"])
for e in silk if max(abs(e["anchor_position"][a]) for a in ["x", "y"]) > 21.15]
jack = next(e for e in placed.values() if parts[e["source_component_id"]]["name"] == "J_ETH")
jack_envelope = box(jack["center"]["x"] - jack["width"]/2,
jack["center"]["y"] - jack["height"]/2,
jack["center"]["x"] + jack["width"]/2,
jack["center"]["y"] + jack["height"]/2)
pour_overlap = {}
for layer in LAYERS:
copper = []
for e in c:
if e["type"] != "pcb_copper_pour" or e["layer"] != layer:
continue
b = e["brep_shape"]
ring = lambda r: [(v["x"], v["y"]) for v in r["vertices"]]
copper.append(Polygon(ring(b["outer_ring"]), [ring(r) for r in b.get("inner_rings", [])]).buffer(0))
pour_overlap[layer] = round(unary_union(copper).intersection(jack_envelope).area, 4)
report = dict(circuitSha256=hashlib.sha256(source.read_bytes()).hexdigest(),
mixedDrillPairsBelowConservative045mm=len(mixed), mixedDrillPairs=mixed,
mixedDrillCaveat="CAM confirmation needed: published via-via and PTH-PTH limits differ; mixed-pair rule not separately specified.",
populatedPadDrillIntersections=via_in_populated_pad,
viaInPadCaveat="Thermal pads may deliberately contain vias; signal-pad intersections need solder-process/filled-capped-via review. No fill process is inferred from the model.",
minimumViaToNpthMm=min(drill(v).distance(drill(h)) for v in vias for h in npths),
jackPcbEnvelopePourOverlapMm2=pour_overlap,
jackPourCaveat="Intersects the emitted PCB component envelope, not an independently measured magnetics-core volume; all emitted pours are BOARD_GND. Review the actual jack exclusion region against the WIZnet guide.",
silkscreenTexts=len(silk), smallTextCount=sum(e["font_size"] < 1 for e in silk),
silkscreenAnchorsOutsideOutline=outside_anchors)
(out / "expanded-geometry.json").write_text(json.dumps(report, indent=2) + "\n")
print(json.dumps({k: v for k, v in report.items() if k not in ["mixedDrillPairs", "populatedPadDrillIntersections"]}))
print(json.dumps(via_in_populated_pad))