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-additional-dfm.py

"""Read-only additional DFM review of emitted copper, not a release gate.

Checks explicit tracks/lands/drills, not Gerber mask/paste or copper pours.
Uses JLCPCB's published 1 oz rules. Findings are not automatically waived.
"""
from route_geometry import *
from collections import Counter, defaultdict
from shapely.ops import unary_union
import hashlib

path = Path("dist/index/circuit.json")
c = json.loads(path.read_text())
root = connectivity(c)
sources = {e["source_component_id"]: e["name"] for e in c if e["type"] == "source_component"}
parts = {e["pcb_component_id"]: sources[e["source_component_id"]] 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_traces = {e["source_trace_id"]: e for e in c if e["type"] == "source_trace"}

def net_for(e):
    port = ports.get(e.get("pcb_port_id"))
    if port:
        return root(port["source_port_id"])
    trace = source_traces.get(e.get("source_trace_id"))
    if trace:
        ids = trace.get("connected_source_port_ids", []) + trace.get("connected_source_net_ids", [])
        if ids:
            return root(ids[0])
    return "UNCONNECTED:" + e.get("pcb_plated_hole_id", e.get("pcb_smtpad_id", "unknown"))

tracks = defaultdict(list)
for e in c:
    if e["type"] != "pcb_trace":
        continue
    for a, b in zip(e["route"], e["route"][1:]):
        if a["route_type"] == b["route_type"] == "wire" and a["layer"] == b["layer"]:
            if math.dist((a["x"], a["y"]), (b["x"], b["y"])) > 1e-8:
                tracks[net_for(e), a["layer"]].append(LineString([(a["x"], a["y"]), (b["x"], b["y"])])
                    .buffer(max(a["width"], b["width"]) / 2, quad_segs=32))
tracks = {k: unary_union(v) for k, v in tracks.items()}
pth = [e for e in c if e["type"] == "pcb_plated_hole"]
smt = [e for e in c if e["type"] == "pcb_smtpad"]
findings = []
for e in pth:
    drill = geometry(e, "hole_")
    for (net, layer), metal in tracks.items():
        if net == net_for(e):
            continue
        required = .3 if layer in ["inner1", "inner2"] else .28
        distance = drill.distance(metal)
        if distance < required - .0002:
            findings.append(dict(rule="pth-hole-to-foreign-track", hole=e["pcb_plated_hole_id"],
                part=parts[e["pcb_component_id"]], x=e["x"], y=e["y"], layer=layer,
                foreignNet=net, clearance=round(distance, 6), required=required))
for i, e in enumerate(pth):
    for f in pth[i+1:]:
        distance = geometry(e, "hole_").distance(geometry(f, "hole_"))
        if distance < .45 - .0002:
            findings.append(dict(rule="pth-hole-to-pth-hole", a=e["pcb_plated_hole_id"],
                b=f["pcb_plated_hole_id"], clearance=round(distance, 6), required=.45))
for i, e in enumerate(smt):
    for f in smt[i+1:]:
        if e["layer"] != f["layer"] or net_for(e) == net_for(f):
            continue
        distance = geometry(e).distance(geometry(f))
        if distance < .15 - .0002:
            findings.append(dict(rule="foreign-smd-pad-to-pad", a=e["pcb_smtpad_id"],
                aPart=parts[e["pcb_component_id"]], b=f["pcb_smtpad_id"],
                bPart=parts[f["pcb_component_id"]], layer=e["layer"],
                clearance=round(distance, 6), required=.15))
silk = [e for e in c if e["type"] == "pcb_silkscreen_text"]
small_silk = [dict(text=e["text"], fontSize=e["font_size"], layer=e["layer"])
              for e in silk if e["font_size"] < 1]
old_audit_path = Path("reports/fabrication-audit.json")
fabrication_audit = json.loads(old_audit_path.read_text()) if old_audit_path.exists() else None
report = dict(circuitSha256=hashlib.sha256(path.read_bytes()).hexdigest(),
    ruleSource="https://jlcpcb.com/capabilities/pcb-capabilities", copperWeightAssumption="1 oz",
    caveat="Additional explicit-metal checks only; excludes pours, mask, paste, mixed via/PTH drill spacing and actual production CAM.",
    counts=dict(Counter(f["rule"] for f in findings)), findings=findings,
    silkscreenTextCount=len(silk), silkscreenBelowPublished1mmHeight=len(small_silk), smallSilkscreen=small_silk,
    fabricationAuditStale=None if fabrication_audit is None else fabrication_audit["circuitJsonSha256"] != hashlib.sha256(path.read_bytes()).hexdigest(),
    oldFabricationPartCount=None if fabrication_audit is None else fabrication_audit["assemblyComponents"])
Path("reports/additional-dfm-review.json").write_text(json.dumps(report, indent=2) + "\n")
print(json.dumps({k: v for k, v in report.items() if k not in ["findings", "smallSilkscreen"]}))
for f in findings[:12]:
    print(json.dumps(f))