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/test-critical-copper.py
#!/usr/bin/env python3
"""Native regressions for missing copper and premature same-net Kelvin joins."""
import unittest
import pcbnew
from critical_copper import check, vector, reservation_polygons, reserve_completed_branches, protect_seed_copper
class KelvinIsolationTest(unittest.TestCase):
def setUp(self):
self.board = pcbnew.BOARD()
self.net = pcbnew.NETINFO_ITEM(self.board, "GND")
self.board.Add(self.net)
self.held = [self.net]
self.pads = []
for name, position in (("U1", (0, 0)), ("R1", (5, 0))):
fp = pcbnew.FOOTPRINT(self.board)
fp.SetReference(name)
pad = pcbnew.PAD(fp)
pad.SetNumber("1")
pad.SetAttribute(pcbnew.PAD_ATTRIB_SMD)
pad.SetShape(pcbnew.PAD_SHAPE_RECT)
layers = pcbnew.LSET()
layers.AddLayer(pcbnew.F_Cu)
pad.SetLayerSet(layers)
pad.SetPosition(vector(position))
pad.SetSize(vector((0.8, 0.8)))
pad.SetNet(self.net)
fp.Add(pad)
self.board.Add(fp)
self.held.extend([fp, pad])
self.pads.append(pad)
self.branch = self.track((0, 0), (5, 0))
self.paths = [{"name": "RETURN", "net": "GND", "width": 0.15,
"segments": [((0, 0), (5, 0))], "start_pad": self.pads[0], "end_pad": self.pads[1]}]
def track(self, a, b):
track = pcbnew.PCB_TRACK(self.board)
track.SetStart(vector(a))
track.SetEnd(vector(b))
track.SetWidth(pcbnew.FromMM(0.15))
track.SetLayer(pcbnew.F_Cu)
track.SetNet(self.net)
self.board.Add(track)
self.held.append(track)
return track
def test_terminal_load_connection_is_allowed(self):
self.track((5, 0), (5, 2))
self.assertEqual(check(self.board, self.paths)["errors"], [])
def test_join_at_far_edge_of_ic_pad_fails(self):
self.track((0, .35), (0, 2))
self.assertIn("joins before", " ".join(check(self.board, self.paths)["errors"]))
def test_completed_branch_is_reserved_without_hiding_other_pins(self):
self.paths[0]["from"] = ["U1", "1"]
text = """ (component shared
(place U1 0 0 front 0 (PN Device))
(place U2 1000 1000 front 0 (PN Device))
)
(image shared
(pin Pad (rotate 180) 1 0 0)
(pin Pad (rotate 180) 2 0 1000)
)
(net GND (pins U1-1 U1-2 R1-1))
(wire (path F.Cu 150 0 0 5000 0)(net GND)(type route))
(wire (path F.Cu 150 5000 0 5000 2000)(net GND)(type route))
"""
result, removed = reserve_completed_branches(text, self.paths)
self.assertEqual(removed, {"RETURN": 1})
self.assertNotIn("U1-1", result)
self.assertIn("U1-2 R1-1", result)
self.assertIn("(pin Pad (rotate 180) 2 0 1000)", result)
self.assertIn("(component shared\n (place U2", result)
self.assertIn("(image U1_critical_reserved", result)
self.assertEqual(result.count("(pin Pad (rotate 180) 1 0 0)"), 1)
self.assertIn("5000 0 5000 2000", result)
self.assertEqual(check(self.board, self.paths)["errors"], [])
with self.assertRaises(ValueError):
reserve_completed_branches(result, self.paths)
def test_missing_branch_fails(self):
self.board.Remove(self.branch)
self.assertIn("source copper was removed", " ".join(check(self.board, self.paths)["errors"]))
def test_incremental_route_preserves_seed_but_can_move_other_copper(self):
source = '(wire (path F.Cu 150 0 0 5000 0)(net GND)(type route))'
other = '(wire (path F.Cu 150 5000 0 5000 2000)(net GND)(type route))'
result = protect_seed_copper(source+'\n'+other, self.board)
self.assertIn(source.replace('(type route)', '(type protect)'), result)
self.assertIn(other, result)
def test_same_ground_spur_before_terminal_fails(self):
self.track((2, 0), (2, 2))
self.assertIn("joins before", " ".join(check(self.board, self.paths)["errors"]))
def test_same_ground_plane_via_before_terminal_fails(self):
via = pcbnew.PCB_VIA(self.board)
via.SetPosition(vector((2, 0)))
via.SetWidth(pcbnew.FromMM(0.5))
via.SetDrill(pcbnew.FromMM(0.3))
via.SetLayerPair(pcbnew.F_Cu, pcbnew.B_Cu)
via.SetNet(self.net)
self.board.Add(via)
self.held.append(via)
self.assertIn("joins before", " ".join(check(self.board, self.paths)["errors"]))
def add_ground_fill(self, bottom):
zone = pcbnew.ZONE(self.board)
zone.SetLayer(pcbnew.F_Cu)
zone.SetNet(self.net)
polygon = pcbnew.SHAPE_POLY_SET()
polygon.NewOutline()
for point in ((1, bottom), (4, bottom), (4, 1), (1, 1)):
p = vector(point)
polygon.Append(p.x, p.y)
zone.SetFilledPolysList(pcbnew.F_Cu, polygon)
zone.SetIsFilled(True)
self.board.Add(zone)
self.held.extend([zone, polygon])
def test_ground_fill_touching_trace_edge_fails(self):
# The 0.15 mm trace overlaps this fill, although its center is outside.
self.add_ground_fill(0.05)
self.assertIn("joins before", " ".join(check(self.board, self.paths)["errors"]))
def test_ground_fill_with_positive_gap_is_allowed(self):
self.add_ground_fill(0.10)
self.assertEqual(check(self.board, self.paths)["errors"], [])
def test_fill_may_join_trace_only_inside_receiving_pad(self):
zone = pcbnew.ZONE(self.board)
zone.SetLayer(pcbnew.F_Cu)
zone.SetNet(self.net)
polygon = pcbnew.SHAPE_POLY_SET()
polygon.NewOutline()
for p in ((4.6, -1), (6, -1), (6, 1), (4.6, 1)):
q = vector(p)
polygon.Append(q.x, q.y)
zone.SetFilledPolysList(pcbnew.F_Cu, polygon)
zone.SetIsFilled(True)
self.board.Add(zone)
self.held.extend([zone, polygon])
self.assertEqual(check(self.board, self.paths)["errors"], [])
polygon.Move(vector((-.05, 0)))
zone.SetFilledPolysList(pcbnew.F_Cu, polygon)
self.assertIn("joins before", " ".join(check(self.board, self.paths)["errors"]))
def test_segment_reservations_merge_and_leave_terminal_accessible(self):
path = {**self.paths[0], "segments": [((0, 0), (2, 0)), ((2, 0), (5, 0))]}
outlines = list(reservation_polygons(path))
self.assertEqual(len(outlines), 1)
polygon = pcbnew.SHAPE_POLY_SET()
polygon.NewOutline()
for point in outlines[0]:
p = vector(point)
polygon.Append(p.x, p.y)
self.assertTrue(polygon.Contains(vector((2, 0))))
self.assertFalse(polygon.Contains(vector((5, 0))))
if __name__ == "__main__":
import wx
app = wx.App(False)
unittest.main()