techmannih/nema34-mounted-stepper-controller

A 48 V USB‑C PD 3.1 EPR NEMA‑34 stepper controller PCB combining STM32/TMC5160 control, external MOSFET motor bridges, protected power and regenerative braking, USB data, telemetry, diagnostics, status LEDs, and buzzer alarms.

Version
0.3.92
License
unset
Stars
1

scripts/generate-routing-report.py

#!/usr/bin/env python3
"""Generate the final route summary from KiCad evidence."""

from __future__ import annotations

import argparse
import json
from datetime import datetime, timezone
from pathlib import Path

import pcbnew


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("board", type=Path)
    parser.add_argument("drc", type=Path)
    parser.add_argument("manufacturing_report", type=Path)
    parser.add_argument("output", type=Path)
    parser.add_argument(
        "--manifest",
        type=Path,
        default=Path("docs/design-manifest.json"),
        help="supplier-backed fitted-parts manifest (default: docs/design-manifest.json)",
    )
    args = parser.parse_args()

    board = pcbnew.LoadBoard(str(args.board.resolve()))
    drc = json.loads(args.drc.read_text())
    manufacturing = json.loads(args.manufacturing_report.read_text())
    manifest = json.loads(args.manifest.read_text())
    tracks = list(board.GetTracks())
    vias = [item for item in tracks if isinstance(item, pcbnew.PCB_VIA)]
    route_segments = [item for item in tracks if not isinstance(item, pcbnew.PCB_VIA)]
    violations = drc.get("violations", [])
    unconnected = drc.get("unconnected_items", [])
    report = {
        "recorded_at": datetime.now(timezone.utc).isoformat(),
        "revision": json.loads(Path("hardware-contract.json").read_text())["revision"],
        "complete": not violations and not unconnected,
        "router": "Freerouting seed with KiCad-reviewed local completion and 2.4 mm power-corridor reinforcement",
        "board": str(args.board),
        "statistics": {
            "components_including_mounts_and_service_pads": len(board.GetFootprints()),
            "supplier_backed_fitted_components": len(manifest["parts"]),
            "service_testpads": sum(f.GetReference().startswith("TP_") for f in board.GetFootprints()),
            "mounting_holes": 4,
            "pads": len(board.GetPads()),
            "traces": len(route_segments),
            "vias": len(vias),
            "copper_pours": len(board.Zones()),
            "layers": board.GetCopperLayerCount(),
        },
        "kicad_drc": {
            "report": str(args.drc),
            "violations": len(violations),
            "unconnected_items": len(unconnected),
        },
        "manufacturing": {
            "report": str(args.manufacturing_report),
            "gerber_and_drill_files": len(manufacturing.get("gerber_files", [])),
            "archive": "dist/pd1180-epr-r0.3-manufacturing.zip",
        },
        "physical_validation_required": [
            "high-current copper temperature rise at target motor profile",
            "48 V EPR negotiation and fault recovery",
            "motor-control firmware and shutdown timing",
            "brake resistor energy and thermal sizing",
            "motor rear-face fit and shaft-magnet alignment",
            "conducted and radiated EMC",
        ],
    }
    args.output.parent.mkdir(parents=True, exist_ok=True)
    args.output.write_text(json.dumps(report, indent=2) + "\n")
    print(json.dumps(report, indent=2))


if __name__ == "__main__":
    main()