#!/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" # Mounting hole positions (M3, 3.2mm diameter) # Bottom holes moved up to avoid power section MOUNTING_HOLES = [ (5, 5), # Top left (35, 5), # Top right (5, 75), # Bottom left (above power section) (35, 75), # Bottom right (above power section) ] # 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 4b: Add mounting holes print("\n[4b/9] Adding mounting holes...") # Remove existing mounting holes first for drawing in list(board.GetDrawings()): if drawing.GetClass() == "PCB_SHAPE": if drawing.GetShape() == pcbnew.SHAPE_T_CIRCLE: # Check if it's a mounting hole (on Edge.Cuts, 3.2mm diameter) if drawing.GetLayer() == pcbnew.Edge_Cuts: radius = pcbnew.ToMM(drawing.GetRadius()) if 1.5 < radius < 1.7: # ~3.2mm diameter board.Delete(drawing) # Add new mounting holes for x, y in MOUNTING_HOLES: hole = pcbnew.PCB_SHAPE(board) hole.SetShape(pcbnew.SHAPE_T_CIRCLE) hole.SetCenter(place(x, y)) hole.SetEnd(place(x + 1.6, y)) # Radius = 1.6mm (3.2mm diameter) hole.SetLayer(pcbnew.Edge_Cuts) hole.SetWidth(mm(0.15)) board.Add(hole) print(f" Added {len(MOUNTING_HOLES)} mounting holes (M3, 3.2mm)") # 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)