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-plane-escapes.py
#!/usr/bin/env python3
"""Add clearance-checked short escapes to the two intended internal planes.
Unreachable lands remain in the netlist for the router. This never removes a
connection or changes a net; native DRC and filled-plane connectivity are still
required after routing. Kelvin source lands and branches are reserved.
"""
import argparse
import json
import math
from pathlib import Path
import pcbnew
import wx
from critical_copper import load_paths, check, reservation_polygons, vector, xy, distance_to_segment
def connected_to_through_copper(board, pad):
if pad.GetDrillSize().x:
return True
# KiCad returns the entire connected copper island, including remote vias.
for item in board.GetConnectivity().GetConnectedItems(pad):
# Connectivity collections expose vias through the PCB_TRACK base proxy.
if item.Type() == pcbnew.PCB_VIA_T:
return True
if isinstance(item, pcbnew.PAD) and item.GetDrillSize().x:
return True
return False
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('input', type=Path)
parser.add_argument('output', type=Path)
args = parser.parse_args()
app = wx.App(False)
board = pcbnew.LoadBoard(str(args.input.resolve()))
paths = load_paths(board)
if check(board, paths)['errors']:
raise SystemExit('Kelvin input is not isolated')
excluded = {p['start_pad'].m_Uuid.AsString() for p in paths}
reservations = []
for path in paths:
for polygon in reservation_polygons(path):
poly = pcbnew.SHAPE_POLY_SET()
poly.NewOutline()
for point in polygon:
q = vector(point)
poly.Append(q.x, q.y)
reservations.append(poly)
pads = [p for fp in board.GetFootprints() for p in fp.Pads()]
pad_shapes = [(p, p.GetEffectiveShape(pcbnew.F_Cu)) for p in pads if p.IsOnLayer(pcbnew.F_Cu)]
outline = pcbnew.SHAPE_POLY_SET()
if not board.GetBoardPolygonOutlines(outline, False, None, False, False):
raise SystemExit('Invalid board outline')
boundary = outline.Outline(0)
vertices = [xy(boundary.CPoint(i)) for i in range(boundary.PointCount())]
clearance = pcbnew.FromMM(.10)
added, skipped = [], []
held = []
board.BuildConnectivity()
for pad in pads:
if pad.GetNetname() not in ('GND', 'V3V3') or not pad.IsOnLayer(pcbnew.F_Cu):
continue
if pad.m_Uuid.AsString() in excluded or connected_to_through_copper(board, pad):
continue
start = pad.GetPosition()
cx, cy = xy(start)
tracks = list(board.GetTracks())
track_shapes = [(t, t.GetEffectiveShape()) for t in tracks if t.IsOnLayer(pcbnew.F_Cu)]
candidate = None
for distance in (.8, 1.0, 1.25, 1.5, 1.75, 2.0):
for angle in range(0, 360, 45):
a = math.radians(angle)
q = vector((cx+distance*math.cos(a), cy+distance*math.sin(a)))
if not outline.Contains(q) or any(distance_to_segment(xy(q),a,b)<.5 for a,b in zip(vertices,vertices[1:]+vertices[:1])):
continue
route = pcbnew.SHAPE_SEGMENT(start, q, pcbnew.FromMM(.2))
if any(shape.Collide(q, pcbnew.FromMM(.4)) for _, shape in pad_shapes):
continue # No new via-in-pad exceptions, including on this net.
if any(p.GetNetCode()!=pad.GetNetCode() and shape.Collide(route, clearance) for p,shape in pad_shapes):
continue
if any(t.GetNetCode()!=pad.GetNetCode() and shape.Collide(route, clearance) for t,shape in track_shapes):
continue
if any(shape.Collide(route, 0) or shape.Collide(q, pcbnew.FromMM(.3)) for shape in reservations):
continue
conflict = False
for t in tracks:
if isinstance(t, pcbnew.PCB_VIA):
if t.GetEffectiveShape().Collide(q, pcbnew.FromMM(.4)):
conflict=True; break
elif t.GetNetCode()!=pad.GetNetCode() and t.GetEffectiveShape().Collide(q, pcbnew.FromMM(.4)):
conflict=True; break
if conflict:
continue
candidate=q
break
if candidate is not None:
break
reference = pad.GetParentFootprint().GetReference()+'.'+pad.GetNumber()
if candidate is None:
skipped.append(reference)
continue
via=pcbnew.PCB_VIA(board)
via.SetPosition(candidate); via.SetWidth(pcbnew.FromMM(.6)); via.SetDrill(pcbnew.FromMM(.3))
via.SetLayerPair(pcbnew.F_Cu,pcbnew.B_Cu); via.SetNet(pad.GetNet()); board.Add(via)
track=pcbnew.PCB_TRACK(board)
track.SetStart(start);track.SetEnd(candidate);track.SetWidth(pcbnew.FromMM(.2))
track.SetLayer(pcbnew.F_Cu);track.SetNet(pad.GetNet());board.Add(track)
held.extend([via,track])
added.append({'pin':reference,'net':pad.GetNetname(),'via_mm':xy(candidate),'escape_mm':math.dist(xy(start),xy(candidate))})
board.BuildConnectivity()
errors=check(board,paths)['errors']
if errors:
raise SystemExit(f'Plane escape violated Kelvin isolation: {errors}')
pcbnew.SaveBoard(str(args.output.resolve()),board)
report={'added':added,'remaining_for_router':skipped,'native_drc_required':True}
args.output.with_suffix('.escapes.json').write_text(json.dumps(report,indent=2)+'\n')
print(json.dumps({'added':len(added),'remaining_for_router':skipped}))
if __name__ == '__main__':
main()