#!/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)