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/import-specctra-route.py

#!/usr/bin/env python3
"""Import a freerouting SES file into the matching KiCad PCB."""

from __future__ import annotations

import argparse
import json
from pathlib import Path
import re
import tempfile

import pcbnew
import wx


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("board", type=Path)
    parser.add_argument("session", type=Path)
    parser.add_argument("output", type=Path)
    parser.add_argument("--report", type=Path)
    parser.add_argument(
        "--restore-bottom-rotation",
        action="store_true",
        help=(
            "restore the 180 degree bottom-placement convention after routing a "
            "DSN prepared by prepare-r03-dsn.py"
        ),
    )
    args = parser.parse_args()

    app = wx.App(False)
    board = pcbnew.LoadBoard(str(args.board.resolve()))
    import_session = args.session.resolve()
    restored_bottom_placements = 0
    temporary_session: Path | None = None
    if args.restore_bottom_rotation:
        data = import_session.read_text()

        def restore_bottom_rotation(match: re.Match[str]) -> str:
            nonlocal restored_bottom_placements
            restored_bottom_placements += 1
            angle = (float(match.group("angle")) + 180.0) % 360.0
            angle_text = str(int(angle)) if angle.is_integer() else str(angle)
            return f'{match.group("prefix")}{angle_text}'

        data = re.sub(
            r'(?P<prefix>\(place\s+[^\s]+\s+[-.0-9]+\s+[-.0-9]+\s+back\s+)'
            r'(?P<angle>[-.0-9]+)',
            restore_bottom_rotation,
            data,
        )
        if restored_bottom_placements == 0:
            raise SystemExit("no bottom-side placements found in Specctra session")
        handle = tempfile.NamedTemporaryFile(
            mode="w",
            suffix=".ses",
            prefix="pd1180-import-",
            dir=args.output.resolve().parent,
            delete=False,
        )
        handle.write(data)
        handle.close()
        temporary_session = Path(handle.name)
        import_session = temporary_session

    try:
        if not pcbnew.ImportSpecctraSES(board, str(import_session)):
            raise SystemExit("Specctra SES import failed")
    finally:
        if temporary_session is not None:
            temporary_session.unlink(missing_ok=True)
    board.BuildConnectivity()
    args.output.parent.mkdir(parents=True, exist_ok=True)
    pcbnew.SaveBoard(str(args.output.resolve()), board)

    report = {
        "board": str(args.output.resolve()),
        "footprints": len(board.GetFootprints()),
        "pads": len(board.GetPads()),
        "tracks_and_vias": len(board.GetTracks()),
        "unconnected_count": board.GetConnectivity().GetUnconnectedCount(True),
        "restored_bottom_placements": restored_bottom_placements,
    }
    if args.report:
        args.report.write_text(json.dumps(report, indent=2) + "\n")
    print(json.dumps(report, indent=2))


if __name__ == "__main__":
    main()