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/source_design_rules.py
"""Apply/audit native geometry against the source-aligned board design policy."""
import argparse
import hashlib
import json
from pathlib import Path
import pcbnew
def policy():
p = json.loads(Path('board-standards.json').read_text())['fabrication']
g = p['ground_plane']
if p['minimum_trace_width_mm'] < .15 or g['clearance_mm'] < .30 or g['board_edge_margin_mm'] < .50:
raise ValueError('User-required 0.15 mm track / 0.30 mm GND clearance / 0.50 mm GND edge floors must not be lowered')
if g['net'] != 'GND' or g['layer'] != 'inner1':
raise ValueError('Expected the source GND_PLANE on inner1')
return p
def ground_outline(board, p):
outline = pcbnew.SHAPE_POLY_SET()
if not board.GetBoardPolygonOutlines(outline, False, None, False, False) or outline.OutlineCount() != 1:
raise ValueError('Expected one valid board perimeter')
outline.Deflate(pcbnew.FromMM(p['ground_plane']['board_edge_margin_mm']),
pcbnew.CORNER_STRATEGY_ROUND_ALL_CORNERS, pcbnew.FromMM(.001))
if outline.OutlineCount() != 1:
raise ValueError('Ground inset splits the plane; geometry review required')
return outline
def apply_ground_policy(board, p):
allowed = ground_outline(board, p)
grounds = [z for z in board.Zones() if not z.GetIsRuleArea() and z.GetNetname() == 'GND']
main = [z for z in grounds if z.GetLayer() == pcbnew.In1_Cu]
if len(main) != 1:
raise ValueError('Expected exactly one native inner1 GND plane')
for z in grounds:
z.SetLocalClearance(pcbnew.FromMM(p['ground_plane']['clearance_mm']))
if z is main[0]:
# SetOutline stores a raw pointer in KiCad. Copy into the
# zone-owned polygon so a local Python polygon cannot dangle.
z.Outline().RemoveAllContours()
z.Outline().Append(allowed)
z.SetZoneName('GND_PLANE')
z.SetMinThickness(pcbnew.FromMM(p['ground_plane']['minimum_fill_width_mm']))
else:
clipped = pcbnew.SHAPE_POLY_SET(z.Outline())
clipped.BooleanIntersection(allowed)
z.Outline().RemoveAllContours()
z.Outline().Append(clipped)
def audit(board, p):
errors = []
tracks = [t for t in board.GetTracks() if not isinstance(t, pcbnew.PCB_VIA)]
thin = [t.m_Uuid.AsString() for t in tracks if t.GetWidth() < pcbnew.FromMM(p['minimum_trace_width_mm'])]
if thin: errors.append(f'{len(thin)} tracks below {p["minimum_trace_width_mm"]} mm')
allowed = ground_outline(board, p)
rows = []
for z in board.Zones():
if z.GetIsRuleArea() or z.GetNetname() != 'GND': continue
outside = pcbnew.SHAPE_POLY_SET(z.Outline())
outside.BooleanSubtract(allowed)
clearance = pcbnew.ToMM(z.GetLocalClearance())
outside_area = outside.Area()/1e12
if clearance < p['ground_plane']['clearance_mm'] - 1e-9:
errors.append(f'GND zone {z.m_Uuid.AsString()} clearance {clearance} mm is below policy')
if outside_area > 1e-8:
errors.append(f'GND zone {z.m_Uuid.AsString()} extends outside the required edge inset')
rows.append({'uuid': z.m_Uuid.AsString(), 'layer': pcbnew.LayerName(z.GetLayer()), 'name': z.GetZoneName(),
'clearance_mm': clearance, 'outline_outside_required_inset_mm2': outside_area})
if len([r for r in rows if r['layer'] == 'In1.Cu' and r['name'] == 'GND_PLANE']) != 1:
errors.append('Missing/ambiguous native GND_PLANE')
return {'minimum_actual_track_width_mm': min((pcbnew.ToMM(t.GetWidth()) for t in tracks), default=None),
'track_count': len(tracks), 'thin_track_uuids': thin, 'ground_zones': rows, 'errors': errors}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('mode', choices=['apply', 'check'])
parser.add_argument('board', type=Path)
parser.add_argument('output', type=Path)
args = parser.parse_args()
import wx
app = wx.App(False)
p = policy()
board = pcbnew.LoadBoard(str(args.board.resolve()))
if args.mode == 'apply':
if args.output.resolve() == args.board.resolve(): raise ValueError('Use a separate output board')
apply_ground_policy(board, p)
pcbnew.ZONE_FILLER(board).Fill(board.Zones())
pcbnew.SaveBoard(str(args.output.resolve()), board)
else:
result = audit(board, p)
result.update(board_sha256=hashlib.sha256(args.board.read_bytes()).hexdigest(),
standards_sha256=hashlib.sha256(Path('board-standards.json').read_bytes()).hexdigest(),
checker_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest())
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(result, indent=2)+'\n')
print(json.dumps(result, indent=2))
if result['errors']: raise SystemExit(1)
if __name__ == '__main__': main()