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
This commit is contained in:
2026-01-23 07:59:50 +01:00
parent cd337b8718
commit 1ae49dc1bb
13 changed files with 15205 additions and 1905 deletions
+214
View File
@@ -0,0 +1,214 @@
#!/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)
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env python3
"""
SN-L00 Full Autoroute Pipeline for 8HP Eurorack
Run in KiCad PCB Editor: Tools → Scripting Console
Then: exec(open('scripts/autoroute_full.py').read())
This script:
1. Updates board outline to 8HP (40mm x 100mm)
2. Deletes existing tracks
3. Places all components
4. Exports DSN file
5. Runs Freerouting
6. Imports routed SES file
"""
import pcbnew
import subprocess
import os
import time
board = pcbnew.GetBoard()
board_path = board.GetFileName()
board_dir = os.path.dirname(board_path)
def mm(val):
return pcbnew.FromMM(val)
def place(x, y):
return pcbnew.VECTOR2I(mm(x), mm(y))
print("=" * 50)
print("SN-L00 Full Autoroute Pipeline")
print("=" * 50)
# Step 1: Update board outline to 8HP (40mm x 100mm)
print("\n[1/6] Updating board outline to 40mm x 100mm...")
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(" Board outline updated")
break
# Step 2: Delete all tracks
print("\n[2/6] Deleting existing tracks...")
tracks = list(board.GetTracks())
for track in tracks:
board.Delete(track)
print(f" Deleted {len(tracks)} tracks")
# Step 3: Place components (v2 - fixed spacing)
print("\n[3/6] Placing components...")
placements = {
# Top - OLED display (rotated 90°)
"MOD3": (20, 15, 90),
# Audio jacks - centered with good spacing
"J3": (10, 35, 0), # RETURN_IN
"J4": (30, 35, 0), # TRIG_OUT
# Button + LED
"SW1": (20, 48, 0),
"D5": (32, 48, 0), # LED near button
# RP2040-Zero module - centered
"MOD2": (20, 62, 0),
# Signal conditioning ICs - well spaced
"U2": (8, 78, 0), # 74LVC1G17
"U4": (32, 78, 0), # MCP6001
# Decoupling caps near ICs
"C4": (4, 78, 90),
"C5": (14, 78, 90),
"C6": (28, 82, 90),
# Protection diodes near jacks circuits
"D3": (4, 82, 0),
"D4": (36, 78, 0),
# Resistors - spread across middle
"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 moved up, components spread out
"J2": (20, 88, 0), # Eurorack power header (extends ~6mm down)
"D2": (4, 92, 0), # Protection diode
"U3": (32, 94, 180), # LDO - moved left from edge
"C2": (10, 94, 90), # Input cap
"C3": (26, 94, 90), # Output cap
}
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 4: Save and export DSN
print("\n[4/6] Saving board and exporting DSN...")
pcbnew.Refresh()
board.Save(board_path)
dsn_path = os.path.join(board_dir, "SN-L00.dsn")
ses_path = os.path.join(board_dir, "SN-L00.ses")
# Export DSN
if hasattr(pcbnew, 'ExportSpecctraDSN'):
pcbnew.ExportSpecctraDSN(dsn_path)
print(f" Exported: {dsn_path}")
else:
# Fallback for older KiCad versions
exporter = pcbnew.SPECCTRA_DB()
exporter.ExportPCB(dsn_path, False)
print(f" Exported: {dsn_path}")
# Step 5: Run Freerouting
print("\n[5/6] Running Freerouting...")
freerouting_jar = "/tmp/freerouting.jar"
if not os.path.exists(freerouting_jar):
print(" Downloading Freerouting...")
url = "https://github.com/freerouting/freerouting/releases/download/v2.0.1/freerouting-2.0.1.jar"
subprocess.run(["curl", "-L", "-o", freerouting_jar, url], check=True)
result = subprocess.run(
["java", "-jar", freerouting_jar,
"-de", dsn_path,
"-do", ses_path,
"-mp", "200",
"-mt", "1",
"-oit"],
capture_output=True,
text=True,
cwd=board_dir
)
if result.returncode == 0:
print(" Freerouting completed successfully")
else:
print(f" Freerouting error: {result.stderr}")
# Step 6: Import SES
print("\n[6/6] Importing routed session...")
if os.path.exists(ses_path):
if hasattr(pcbnew, 'ImportSpecctraSES'):
pcbnew.ImportSpecctraSES(ses_path)
print(f" Imported: {ses_path}")
else:
# Fallback
importer = pcbnew.SPECCTRA_DB()
importer.ImportSES(ses_path)
print(f" Imported: {ses_path}")
pcbnew.Refresh()
print("\n" + "=" * 50)
print("DONE! Save (Ctrl+S) and run DRC to verify.")
print("=" * 50)
else:
print(f" ERROR: SES file not found: {ses_path}")
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
# Freerouting automation script
# Usage: ./scripts/freeroute.sh
#
# Prerequisites: Export DSN from KiCad first (File → Export → Specctra DSN)
set -e
cd "$(dirname "$0")/.."
DSN_FILE="SN-L00.dsn"
SES_FILE="SN-L00.ses"
FREEROUTING_JAR="/tmp/freerouting.jar"
FREEROUTING_URL="https://github.com/freerouting/freerouting/releases/download/v2.0.1/freerouting-2.0.1.jar"
echo "=================================="
echo "SN-L00 Freerouting Automation"
echo "=================================="
# Check DSN exists
if [ ! -f "$DSN_FILE" ]; then
echo "ERROR: $DSN_FILE not found"
echo "Export from KiCad: File → Export → Specctra DSN"
exit 1
fi
# Download Freerouting if needed
if [ ! -f "$FREEROUTING_JAR" ]; then
echo "Downloading Freerouting..."
curl -L -o "$FREEROUTING_JAR" "$FREEROUTING_URL"
fi
# Run Freerouting
echo "Running Freerouting..."
java -jar "$FREEROUTING_JAR" \
-de "$DSN_FILE" \
-do "$SES_FILE" \
-mp 200 \
-mt 1 \
-oit
echo ""
echo "=================================="
echo "DONE! Import in KiCad:"
echo " File → Import → Specctra Session"
echo " Select: $SES_FILE"
echo "=================================="
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""
Move board and all components to origin (0,0)
Run in KiCad PCB Editor: Tools → Scripting Console
Then: exec(open('scripts/move_to_origin.py').read())
"""
import pcbnew
board = pcbnew.GetBoard()
# Get current board origin from edge cuts
bbox = board.GetBoardEdgesBoundingBox()
offset_x = bbox.GetX()
offset_y = bbox.GetY()
print(f"Current board origin: ({pcbnew.ToMM(offset_x):.2f}, {pcbnew.ToMM(offset_y):.2f}) mm")
if offset_x == 0 and offset_y == 0:
print("Board is already at origin!")
else:
# Move all footprints
for fp in board.GetFootprints():
pos = fp.GetPosition()
new_pos = pcbnew.VECTOR2I(pos.x - offset_x, pos.y - offset_y)
fp.SetPosition(new_pos)
# Move all drawings (including board outline)
for drawing in board.GetDrawings():
if hasattr(drawing, 'Move'):
drawing.Move(pcbnew.VECTOR2I(-offset_x, -offset_y))
# Move all tracks
for track in board.GetTracks():
track.Move(pcbnew.VECTOR2I(-offset_x, -offset_y))
# Move all zones
for zone in board.Zones():
zone.Move(pcbnew.VECTOR2I(-offset_x, -offset_y))
print(f"Moved everything by ({-pcbnew.ToMM(offset_x):.2f}, {-pcbnew.ToMM(offset_y):.2f}) mm")
print("Board is now at origin (0, 0)")
pcbnew.Refresh()
print("\nDone! Save (Ctrl+S), delete all tracks, then run place_6hp.py")
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""
SN-L00 Component Placement Script for 8HP Eurorack (40mm x 100mm)
Run in KiCad PCB Editor: Tools → Scripting Console
Then: exec(open('scripts/place_8hp.py').read())
"""
import pcbnew
board = pcbnew.GetBoard()
def mm(val):
return pcbnew.FromMM(val)
def place(x, y):
return pcbnew.VECTOR2I(mm(x), mm(y))
# First, update the board outline to 8HP (40mm x 100mm)
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("Updated board outline to 40mm x 100mm (8HP)")
# Delete all tracks for fresh routing
tracks = list(board.GetTracks())
for track in tracks:
board.Delete(track)
print(f"Deleted {len(tracks)} tracks")
# Component positions for 8HP (40mm x 100mm) layout v2
placements = {
"MOD3": (20, 15, 90), # OLED display
"J3": (10, 35, 0), # RETURN_IN jack
"J4": (30, 35, 0), # TRIG_OUT jack
"SW1": (20, 48, 0), # Button
"D5": (32, 48, 0), # LED
"MOD2": (20, 62, 0), # RP2040-Zero
"U2": (8, 78, 0), # 74LVC1G17
"U4": (32, 78, 0), # MCP6001
"C4": (4, 78, 90), # Decoupling
"C5": (14, 78, 90),
"C6": (28, 82, 90),
"D3": (4, 82, 0), # Protection diodes
"D4": (36, 78, 0),
"R2": (4, 86, 90), # Resistors
"R3": (10, 86, 90),
"R4": (16, 86, 90),
"R5": (24, 86, 90),
"R6": (30, 86, 90),
"R7": (36, 86, 90),
"J2": (20, 88, 0), # Eurorack power
"D2": (4, 92, 0),
"U3": (32, 94, 180), # LDO
"C2": (10, 94, 90),
"C3": (26, 94, 90),
}
print("\nPlacing components for 8HP Eurorack layout (v2)...")
placed = 0
not_found = []
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" {ref} -> ({x}, {y}) rot={rot}°")
else:
not_found.append(ref)
print(f"\nPlaced {placed} components")
if not_found:
print(f"Not found: {', '.join(not_found)}")
pcbnew.Refresh()
print("\nDone! Save (Ctrl+S), then run: ./scripts/route.sh")
+97
View File
@@ -0,0 +1,97 @@
#!/bin/bash
# Full routing and DRC workflow
# Usage: ./scripts/route.sh
#
# Prerequisites:
# 1. Run place_8hp.py in KiCad scripting console
# 2. Export DSN: File → Export → Specctra DSN
# 3. Save PCB
set -e
cd "$(dirname "$0")/.."
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"
echo "============================================"
echo "SN-L00 Routing Pipeline"
echo "============================================"
# Check DSN exists
if [ ! -f "$DSN_FILE" ]; then
echo ""
echo "ERROR: $DSN_FILE not found!"
echo ""
echo "In KiCad:"
echo " 1. Run: exec(open('scripts/place_8hp.py').read())"
echo " 2. Save: Ctrl+S"
echo " 3. Export: File → Export → Specctra DSN"
echo ""
exit 1
fi
# Download Freerouting if needed
if [ ! -f "$FREEROUTING_JAR" ]; then
echo "[1/3] Downloading Freerouting..."
curl -L -o "$FREEROUTING_JAR" "$FREEROUTING_URL"
else
echo "[1/3] Freerouting ready"
fi
# Run Freerouting
echo "[2/3] Running Freerouting..."
java -jar "$FREEROUTING_JAR" \
-de "$DSN_FILE" \
-do "$SES_FILE" \
-mp 200 \
-mt 1 \
-oit 2>&1 | grep -E "(completed|INFO.*Auto|ERROR)" || true
if [ ! -f "$SES_FILE" ]; then
echo "ERROR: Routing failed - no SES file created"
exit 1
fi
echo "[3/3] Running DRC..."
echo ""
echo "============================================"
echo "In KiCad: File → Import → Specctra Session"
echo "Select: $SES_FILE"
echo "Then save (Ctrl+S)"
echo "============================================"
echo ""
echo "Waiting for you to import SES and save..."
read -p "Press Enter when done: "
# Run DRC from CLI
kicad-cli pcb drc \
--severity-all \
--units mm \
--schematic-parity \
-o "$DRC_FILE" \
"$PCB_FILE" 2>&1
echo ""
echo "============================================"
echo "DRC Results:"
echo "============================================"
grep -E "^(Found|\*\*)" "$DRC_FILE" || cat "$DRC_FILE"
echo ""
# Count errors vs warnings
ERRORS=$(grep -c "error$" "$DRC_FILE" 2>/dev/null || echo "0")
WARNINGS=$(grep -c "warning$" "$DRC_FILE" 2>/dev/null || echo "0")
echo "Summary: $ERRORS errors, $WARNINGS warnings"
if [ "$ERRORS" -eq 0 ]; then
echo ""
echo "✓ No DRC errors - ready for manufacturing!"
else
echo ""
echo "Fix errors and re-run: ./scripts/route.sh"
fi