1ae49dc1bb
- 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
162 lines
4.5 KiB
Python
162 lines
4.5 KiB
Python
#!/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}")
|