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/prepare-routing-rules.py

#!/usr/bin/env python3
"""Apply signal and power routing rules to the world-aligned Specctra export."""

from __future__ import annotations

from pathlib import Path
import re
import json
import sys


POWER_NETS = (
    "PD_VBUS",
    "EFUSE_IN",
    "VMOTOR",
    "MOTOR_A1",
    "MOTOR_A1_OUT",
    "MOTOR_B1_OUT",
    "MOTOR_A2",
    "MOTOR_B1",
    "MOTOR_B2",
    "SENSE_A",
    "SENSE_B",
    "BRAKE_RETURN",
    "TMC_12V",
    "DRIVER_BUCK_SW",
)


def wrapped_names(names: list[str], indent: str = "      ") -> str:
    # Wrap whole DSN identifiers. textwrap splits on hyphens even inside a
    # quoted NC-net name; Freerouting can then stop before reading the wiring
    # scope, silently losing every existing track and via.
    lines, line = [], ""
    for name in names:
        if line and len(line) + 1 + len(name) > 94 - len(indent):
            lines.append(line)
            line = name
        else:
            line = f"{line} {name}" if line else name
    if line:
        lines.append(line)
    return ("\n" + indent).join(lines)


def main() -> None:
    if len(sys.argv) != 3:
        raise SystemExit("usage: prepare-routing-rules.py INPUT.dsn OUTPUT.dsn")
    source = Path(sys.argv[1])
    output = Path(sys.argv[2])
    data = source.read_text()

    # Use the reviewed fabrication clearance, including the short dense-pad
    # escapes already checked in native KiCad. Power zones retain 0.16 mm.
    standards = json.loads(Path("board-standards.json").read_text())
    clearance_um = round(1000 * standards["fabrication"]["minimum_trace_space_mm"])
    data, replaced = re.subn(
        r"\(clearance (?:90|100|200)\)\n      \(clearance (?:22\.5|25|50) \(type smd_smd\)\)",
        f"(clearance {clearance_um})\n      (clearance 22.5 (type smd_smd))",
        data,
        count=1,
    )
    if replaced != 1:
        raise SystemExit("board-level routing rule not found")

    class_re = re.compile(
        r"    \(class kicad_default (?P<names>.*?)"
        r"(?P<body>\n      \(circuit\n        \(use_via .*?\n      \)\n"
        r"      \(rule\n        \(width (?:150|200)\)\n        \(clearance (?:90|100|200)\)\n      \)\n    \))",
        re.S,
    )
    match = class_re.search(data)
    if not match:
        raise SystemExit("kicad_default class not found")

    # Native parity boards include quoted names such as
    # "unconnected-(U34-PG-Pad4)". Preserve each identifier as one DSN token.
    names = re.findall(r'"(?:\\.|[^"\\])*"|[^\s]+', match.group("names"))
    unquoted = lambda name: json.loads(name) if name.startswith('"') else name
    missing = sorted(set(POWER_NETS) - {unquoted(name) for name in names})
    if missing:
        raise SystemExit(f"power nets missing from default class: {missing}")
    default_names = [name for name in names if unquoted(name) not in POWER_NETS]
    default_body = re.sub(r"\(width (?:150|200)\)", "(width 150)", match.group("body"))
    default_body = re.sub(r"\(clearance (?:90|100|200)\)", f"(clearance {clearance_um})", default_body)
    default_class = (
        "    (class kicad_default "
        + wrapped_names(default_names)
        + default_body
    )
    power_class = (
        "\n    (class POWER "
        + " ".join(POWER_NETS)
        + "\n      (circuit\n        (use_via \"Via[0-3]_600:300_um\")\n      )"
        # Dense QFN and PowerPAK pins need a short neck-down. The release
        # board reinforces these 0.50 mm connections with broad copper zones
        # and via arrays; forcing 2 mm uniformly cannot enter the packages.
        + f"\n      (rule\n        (width 500)\n        (clearance {clearance_um})\n      )\n    )"
    )
    data = data[: match.start()] + default_class + power_class + data[match.end() :]
    output.parent.mkdir(parents=True, exist_ok=True)
    output.write_text(data)
    print(
        f"wrote {output} with {len(default_names)} signal nets, "
        f"{len(POWER_NETS)} power nets; placements are unchanged"
    )


if __name__ == "__main__":
    main()