techmannih/nema34-mounted-stepper-controller

A USB-C PD-powered RP2040 motor controller with protected 48 V power, DRV8262 dual-bridge drive for a NEMA34 motor, regenerative-braking resistor, isolated sensing, hardware interlocks, and telemetry/status interfaces.

Version
0.3.113
License
unset
Stars
1

scripts/refine-copper-geometry.py

#!/usr/bin/env python3
"""Reproduce the 2026-10-20 copper ECO from the hash-locked main board.

Usage: KiCad Python scripts/refine-copper-geometry.py BASELINE OUTPUT
Source must be freshly compiled first. Never use this ECO to reroute a different
revision. All native checks, actual-fill screens and release exports follow it.
"""
import hashlib
import json
from pathlib import Path
import runpy
import sys
import pcbnew
import wx
from critical_copper import load_paths, add_pour_keepouts, check

BASELINE = '4e53cf5210003b327a1b4cd49f3c5c81ff3a0f5f44277b71c384c29bc95a5cb3'

def v(x, y):
    return pcbnew.VECTOR2I(round(x * 1e6), round(y * 1e6))

def main():
    src, dst = map(Path, sys.argv[1:3])
    if src.resolve() == dst.resolve():
        raise ValueError('Use a separate output path')
    if hashlib.sha256(src.read_bytes()).hexdigest() != BASELINE:
        raise ValueError('ECO input differs from reviewed main 8139bca')
    settings = json.loads(src.with_suffix('.kicad_pro').read_text())
    app = wx.App(False)
    board = pcbnew.LoadBoard(str(src.resolve()))
    pcbnew.KIID.SeedGenerator(20261020)
    tracks = {t.m_Uuid.AsString(): t for t in board.GetTracks()}
    def added_track(net, start, end, width=.15):
        t = pcbnew.PCB_TRACK(board)
        t.SetNet(board.FindNet(net)); t.SetLayer(pcbnew.F_Cu)
        t.SetWidth(pcbnew.FromMM(width)); t.SetStart(v(*start)); t.SetEnd(v(*end))
        board.Add(t)
    tracks['4d982e36-1430-4d61-a500-c5d15533ec31'].SetEnd(v(80.8,115.2))
    # Move the complete gate-via junction clear of a redundant pad-corner touch.
    old, new = v(93.5163,60.7126), v(93.6163,60.8126)
    for t in board.GetTracks():
        if t.GetNetname() != 'BLOCK_FAST_GATE': continue
        if isinstance(t, pcbnew.PCB_VIA):
            if t.GetPosition() == old: t.SetPosition(new)
        else:
            if t.GetStart() == old: t.SetStart(new)
            if t.GetEnd() == old: t.SetEnd(new)
    # Keep U12's existing direct pad-to-pad ground link; avoid grazing pad 1.
    tracks['8c894eed-461a-4ae8-9dc5-d16d548e7b07'].SetStart(v(100.64999,124.0))
    added_track('GND', (100.64999,124.0), (101.14999,124.5))
    # Source-matched outward chamfer, maintaining the 0.15 mm Kelvin branch.
    tracks['f9109076-03b8-4d85-a982-f5154b85651d'].SetEnd(v(134.218066,103.20811))
    tracks['110230f3-3a27-42e5-8ad8-982e99c602de'].SetStart(v(134.250066,103.50011))
    added_track('SENSE_B', (134.218066,103.20811), (134.150066,103.40011))
    added_track('SENSE_B', (134.150066,103.40011), (134.250066,103.50011))
    retained = []
    for zone in list(board.Zones()):
        if zone.GetIsRuleArea() and zone.GetZoneName() == 'KELVIN_B_HIGH':
            retained.append(zone); board.Remove(zone)
        if not zone.GetIsRuleArea() and zone.GetNetname() == 'GND' and zone.GetLayer() == pcbnew.In1_Cu:
            zone.SetMinThickness(pcbnew.FromMM(.25))
    paths = load_paths(board)
    add_pour_keepouts(board, [p for p in paths if p['name'] == 'KELVIN_B_HIGH'])
    consolidate = runpy.run_path(str(Path(__file__).with_name('reinforce-power-copper.py')))['consolidate_power_zones']
    result = consolidate(board)
    board.BuildConnectivity()
    filler = pcbnew.ZONE_FILLER(board)
    filler.Fill(board.Zones())
    isolation = check(board, paths)
    if isolation['errors']:
        raise ValueError(isolation['errors'])
    dst.parent.mkdir(parents=True, exist_ok=True)
    pcbnew.SaveBoard(str(dst.resolve()), board)
    settings['board']['design_settings']['rules'].update(min_connection=.09, min_groove_width=.09)
    dst.with_suffix('.kicad_pro').write_text(json.dumps(settings,indent=2)+'\n')
    print(json.dumps({'consolidated_power_zones':result,'kelvin_errors':isolation['errors'],
        'board_sha256':hashlib.sha256(dst.read_bytes()).hexdigest(),'output':str(dst)},indent=2))

if __name__ == '__main__': main()