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-routing-copper.py

#!/usr/bin/env python3
"""Import SES copper from a world-aligned routing copy, preserving original footprints."""

import argparse
import json
from pathlib import Path

import pcbnew
import wx
from critical_copper import load_paths, is_branch_track, check


def pad_geometry(board):
    return sorted(
        (fp.GetReference(), pad.GetNumber(), pad.GetPosition().x,
         pad.GetPosition().y, pad.GetOrientationDegrees(), pad.GetLayerSet().FmtHex(),
         pad.GetNetname())
        for fp in board.GetFootprints() for pad in fp.Pads()
    )


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("original_board", type=Path)
    parser.add_argument("routing_base", type=Path)
    parser.add_argument("session", type=Path)
    parser.add_argument("output", type=Path)
    parser.add_argument("--restore-critical-branches", action="store_true")
    args = parser.parse_args()
    app = wx.App(False)
    board = pcbnew.LoadBoard(str(args.original_board.resolve()))
    routed = pcbnew.LoadBoard(str(args.routing_base.resolve()))
    if board is None or routed is None:
        raise SystemExit("Cannot load original PCB or routing copy")
    original_pads, routing_pads = pad_geometry(board), pad_geometry(routed)
    if not pcbnew.ImportSpecctraSES(routed, str(args.session.resolve())):
        raise SystemExit("Specctra session import failed")
    after_routing = pad_geometry(routed)
    # Specctra's 0.1 um grid rounds package translations by up to 100 nm.
    # Allow only 100 nm in this disposable routing copy; original pads stay exact.
    mismatches = [(a, z) for a, z in zip(routing_pads, after_routing)
                  if a[:2] != z[:2] or a[4:] != z[4:]
                  or abs(a[2] - z[2]) > 100 or abs(a[3] - z[3]) > 100]
    if len(routing_pads) != len(after_routing):
        raise SystemExit("Router changed the pad count")
    if mismatches:
        print(json.dumps({"routing_pad_differences": mismatches[:8]}, indent=2))
        raise SystemExit("Router changed footprint placement or pin identity")
    paths = load_paths(board) if args.restore_critical_branches else []
    if paths and check(board, paths)["errors"]:
        raise SystemExit("Original critical copper is incomplete or already has premature joins")
    preserved = [t for t in board.GetTracks() if is_branch_track(t, paths)]
    held = list(board.GetTracks())
    for track in held:
        board.Remove(track)
    routed_tracks = list(routed.GetTracks())
    if args.restore_critical_branches:
        # SES contains routes around the reservations; native source provides
        # the completed branches. Reject duplicates instead of overlapping them.
        if any(is_branch_track(t, paths) for t in routed_tracks):
            raise SystemExit("SES unexpectedly contains reserved critical copper")
        routed_tracks.extend(preserved)
    held.extend(routed_tracks)
    for track in routed_tracks:
        net = board.FindNet(track.GetNetname())
        if net is None:
            raise SystemExit(f"Unknown routed net: {track.GetNetname()}")
        if isinstance(track, pcbnew.PCB_VIA):
            item = pcbnew.PCB_VIA(board)
            item.SetPosition(track.GetPosition())
            item.SetWidth(track.GetWidth(pcbnew.F_Cu))
            item.SetDrill(track.GetDrillValue())
            item.SetLayerPair(pcbnew.F_Cu, pcbnew.B_Cu)
        elif type(track) is pcbnew.PCB_TRACK:
            item = pcbnew.PCB_TRACK(board)
            item.SetStart(track.GetStart())
            item.SetEnd(track.GetEnd())
            item.SetWidth(track.GetWidth())
            item.SetLayer(track.GetLayer())
        else:
            raise SystemExit(f"Unsupported routed copper type: {type(track).__name__}")
        item.SetNet(net)
        board.Add(item)
        held.append(item)
    if pad_geometry(board) != original_pads:
        raise SystemExit("Copper import changed the original footprint geometry")
    pcbnew.ZONE_FILLER(board).Fill(board.Zones())
    board.BuildConnectivity()
    if paths:
        problems = check(board, paths)["errors"]
        if problems:
            raise SystemExit(f"Imported routing violated terminal-only joins: {problems}")
    args.output.parent.mkdir(parents=True, exist_ok=True)
    pcbnew.SaveBoard(str(args.output.resolve()), board)
    print(json.dumps({
        "original_pads_preserved": len(original_pads),
        "tracks_and_vias": len(board.GetTracks()),
        "unconnected": board.GetConnectivity().GetUnconnectedCount(True),
        "native_drc_still_required": True,
    }, indent=2))


if __name__ == "__main__":
    main()