Files
SN-L00/hardware/kicad/scripts/autoroute.py
T
florian.berthold 1ae49dc1bb Add 8HP layout with fully automated routing pipeline
- Update PCB to 8HP format (40x100mm) with v2 component placement
- Add automated routing scripts (autoroute.py runs full pipeline headlessly)
- Update panel spec and SVG for 8HP dimensions
- Board routes in <1 second with 0 unconnected pads

Scripts:
- autoroute.py: Full CLI pipeline (place → export → route → import → DRC)
- autoroute_full.py: Same pipeline for KiCad scripting console
- place_8hp.py: Component placement only
- route.sh/freeroute.sh: Routing helpers
2026-01-23 07:59:50 +01:00

215 lines
5.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""
SN-L00 Fully Automated Routing Pipeline
Works headlessly on KiCad 9+
Usage: python3 scripts/autoroute.py
Pipeline:
1. Load board
2. Update outline to 8HP (40x100mm)
3. Delete existing tracks
4. Place components (v2 layout)
5. Export Specctra DSN
6. Run Freerouting autorouter
7. Import Specctra SES
8. Save board
9. Run DRC
"""
import os
import sys
import subprocess
# Paths
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
KICAD_DIR = os.path.dirname(SCRIPT_DIR)
os.chdir(KICAD_DIR)
PCB_FILE = "SN-L00.kicad_pcb"
DSN_FILE = "SN-L00.dsn"
SES_FILE = "SN-L00.ses"
DRC_FILE = "DRC.rpt"
FREEROUTING_JAR = "/tmp/freerouting.jar"
FREEROUTING_URL = "https://github.com/freerouting/freerouting/releases/download/v2.0.1/freerouting-2.0.1.jar"
# Component positions for 8HP (40mm x 100mm) layout v2
PLACEMENTS = {
# Top - OLED display
"MOD3": (20, 15, 90),
# Audio jacks
"J3": (10, 35, 0), # RETURN_IN
"J4": (30, 35, 0), # TRIG_OUT
# Button + LED
"SW1": (20, 48, 0),
"D5": (32, 48, 0),
# RP2040-Zero
"MOD2": (20, 62, 0),
# Signal conditioning
"U2": (8, 78, 0), # 74LVC1G17
"U4": (32, 78, 0), # MCP6001
# Decoupling caps
"C4": (4, 78, 90),
"C5": (14, 78, 90),
"C6": (28, 82, 90),
# Protection diodes
"D3": (4, 82, 0),
"D4": (36, 78, 0),
# Resistors
"R2": (4, 86, 90),
"R3": (10, 86, 90),
"R4": (16, 86, 90),
"R5": (24, 86, 90),
"R6": (30, 86, 90),
"R7": (36, 86, 90),
# Power section
"J2": (20, 88, 0), # Eurorack power
"D2": (4, 92, 0),
"U3": (32, 94, 180), # LDO
"C2": (10, 94, 90),
"C3": (26, 94, 90),
}
def main():
print("=" * 50)
print("SN-L00 Automated Routing Pipeline")
print("=" * 50)
# Import pcbnew
try:
import pcbnew
print(f"KiCad version: {pcbnew.Version()}")
except ImportError:
print("ERROR: pcbnew not available. Install KiCad or set PYTHONPATH.")
sys.exit(1)
# Helper functions
def mm(val):
return pcbnew.FromMM(val)
def place(x, y):
return pcbnew.VECTOR2I(mm(x), mm(y))
# Step 1: Load board
print("\n[1/9] Loading board...")
board = pcbnew.LoadBoard(PCB_FILE)
print(f" Loaded {len(list(board.GetFootprints()))} footprints")
# Step 2: Update outline
print("\n[2/9] Updating board outline to 40x100mm...")
for drawing in board.GetDrawings():
if drawing.GetClass() == "PCB_SHAPE":
if drawing.GetShape() == pcbnew.SHAPE_T_RECT:
drawing.SetStart(pcbnew.VECTOR2I(mm(0), mm(0)))
drawing.SetEnd(pcbnew.VECTOR2I(mm(40), mm(100)))
print(" Done")
break
# Step 3: Delete tracks
print("\n[3/9] Deleting existing tracks...")
tracks = list(board.GetTracks())
for track in tracks:
board.Delete(track)
print(f" Deleted {len(tracks)} tracks")
# Step 4: Place components
print("\n[4/9] Placing components...")
placed = 0
for ref, (x, y, rot) in PLACEMENTS.items():
fp = board.FindFootprintByReference(ref)
if fp:
fp.SetPosition(place(x, y))
fp.SetOrientationDegrees(rot)
placed += 1
print(f" Placed {placed} components")
# Step 5: Export DSN
print("\n[5/9] Exporting Specctra DSN...")
board.Save(PCB_FILE) # Save first
result = pcbnew.ExportSpecctraDSN(board, DSN_FILE)
if result and os.path.exists(DSN_FILE):
print(f" Created {DSN_FILE} ({os.path.getsize(DSN_FILE)} bytes)")
else:
print(" ERROR: DSN export failed")
sys.exit(1)
# Step 6: Download and run Freerouting
print("\n[6/9] Running Freerouting...")
if not os.path.exists(FREEROUTING_JAR):
print(" Downloading Freerouting...")
subprocess.run(["curl", "-sL", "-o", FREEROUTING_JAR, FREEROUTING_URL], check=True)
result = subprocess.run(
["java", "-jar", FREEROUTING_JAR,
"-de", DSN_FILE,
"-do", SES_FILE,
"-mp", "200",
"-mt", "1",
"-oit"],
capture_output=True,
text=True
)
# Parse output
for line in (result.stdout + result.stderr).split('\n'):
if 'auto-rout' in line.lower() or 'completed' in line.lower():
print(f" {line.split(']')[-1].strip()}")
if not os.path.exists(SES_FILE):
print(" ERROR: Freerouting failed")
sys.exit(1)
# Step 7: Import SES
print("\n[7/9] Importing routed session...")
# Reload board to get fresh state
board = pcbnew.LoadBoard(PCB_FILE)
result = pcbnew.ImportSpecctraSES(board, SES_FILE)
if result:
print(f" Imported {len(list(board.GetTracks()))} tracks")
else:
print(" WARNING: SES import returned False")
# Step 8: Save
print("\n[8/9] Saving board...")
board.Save(PCB_FILE)
print(f" Saved {PCB_FILE}")
# Step 9: Run DRC
print("\n[9/9] Running DRC...")
result = subprocess.run(
["kicad-cli", "pcb", "drc",
"--severity-all",
"--units", "mm",
"-o", DRC_FILE,
PCB_FILE],
capture_output=True,
text=True
)
# Parse DRC output
errors = 0
warnings = 0
if os.path.exists(DRC_FILE):
with open(DRC_FILE) as f:
content = f.read()
errors = content.count('; error')
warnings = content.count('; warning')
print(f" {errors} errors, {warnings} warnings")
# Summary
print("\n" + "=" * 50)
if errors == 0:
print("SUCCESS! Board routed with no DRC errors.")
else:
print(f"Done. {errors} DRC errors remain - see {DRC_FILE}")
print("=" * 50)
return errors == 0
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)