#!/usr/bin/env python3 """ SN-L00 Headless Autoroute Pipeline Fully automated: place → export DSN → freeroute → import SES → DRC Usage: # Install dependencies first: sudo apt install xvfb pip install kigadgets # Run: python3 scripts/autoroute_headless.py Based on: https://github.com/atait/kicad-python """ import os import sys import subprocess import tempfile SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) KICAD_DIR = os.path.dirname(SCRIPT_DIR) PCB_FILE = os.path.join(KICAD_DIR, "SN-L00.kicad_pcb") DSN_FILE = os.path.join(KICAD_DIR, "SN-L00.dsn") SES_FILE = os.path.join(KICAD_DIR, "SN-L00.ses") DRC_FILE = os.path.join(KICAD_DIR, "DRC.rpt") FREEROUTING_JAR = "/tmp/freerouting.jar" # Component positions for 8HP (40mm x 100mm) layout v2 PLACEMENTS = { "MOD3": (20, 15, 90), "J3": (10, 35, 0), "J4": (30, 35, 0), "SW1": (20, 48, 0), "D5": (32, 48, 0), "MOD2": (20, 62, 0), "U2": (8, 78, 0), "U4": (32, 78, 0), "C4": (4, 78, 90), "C5": (14, 78, 90), "C6": (28, 82, 90), "D3": (4, 82, 0), "D4": (36, 78, 0), "R2": (4, 86, 90), "R3": (10, 86, 90), "R4": (16, 86, 90), "R5": (24, 86, 90), "R6": (30, 86, 90), "R7": (36, 86, 90), "J2": (20, 88, 0), "D2": (4, 92, 0), "U3": (32, 94, 180), "C2": (10, 94, 90), "C3": (26, 94, 90), } def check_dependencies(): """Check if required tools are available.""" missing = [] # Check xvfb if subprocess.run(["which", "xvfb-run"], capture_output=True).returncode != 0: missing.append("xvfb (sudo apt install xvfb)") # Check java if subprocess.run(["which", "java"], capture_output=True).returncode != 0: missing.append("java (sudo apt install default-jre)") # Check kicad-cli if subprocess.run(["which", "kicad-cli"], capture_output=True).returncode != 0: missing.append("kicad-cli (install KiCad)") if missing: print("Missing dependencies:") for dep in missing: print(f" - {dep}") sys.exit(1) def download_freerouting(): """Download Freerouting if not present.""" 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) def run_pcbnew_script(script_content): """Run a Python script inside pcbnew using xvfb.""" with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: f.write(script_content) script_path = f.name try: # Run pcbnew with xvfb, execute script, then exit # This is a workaround since pcbnew doesn't have a direct script execution mode result = subprocess.run( ["xvfb-run", "-a", "python3", "-c", f""" import sys sys.path.insert(0, '/usr/lib/python3/dist-packages') try: import pcbnew exec(open('{script_path}').read()) except Exception as e: print(f"Error: {{e}}") sys.exit(1) """], capture_output=True, text=True, timeout=120 ) print(result.stdout) if result.stderr: print(result.stderr) return result.returncode == 0 finally: os.unlink(script_path) def place_and_export(): """Place components and export DSN using pcbnew API.""" script = f''' import pcbnew board = pcbnew.LoadBoard("{PCB_FILE}") def mm(val): return pcbnew.FromMM(val) def place(x, y): return pcbnew.VECTOR2I(mm(x), mm(y)) # Update board outline 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") break # Delete tracks tracks = list(board.GetTracks()) for track in tracks: board.Delete(track) print(f"Deleted {{len(tracks)}} tracks") # Place components placements = {repr(PLACEMENTS)} 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") # Save board board.Save("{PCB_FILE}") print("Saved board") # Export DSN try: result = pcbnew.ExportSpecctraDSN("{DSN_FILE}") if result: print("Exported DSN") else: print("DSN export returned False - trying alternate method") # Try with board parameter result = pcbnew.ExportSpecctraDSN(board, "{DSN_FILE}") print(f"Alternate export result: {{result}}") except Exception as e: print(f"DSN export error: {{e}}") ''' return run_pcbnew_script(script) def run_freerouting(): """Run Freerouting autorouter.""" print("Running Freerouting...") result = subprocess.run( ["java", "-jar", FREEROUTING_JAR, "-de", DSN_FILE, "-do", SES_FILE, "-mp", "200", "-mt", "1", "-oit"], capture_output=True, text=True, cwd=KICAD_DIR ) for line in (result.stdout + result.stderr).split('\n'): if 'completed' in line.lower() or 'error' in line.lower(): print(f" {line.split(']')[-1].strip()}") return os.path.exists(SES_FILE) def import_ses(): """Import SES file using pcbnew API.""" script = f''' import pcbnew board = pcbnew.LoadBoard("{PCB_FILE}") try: result = pcbnew.ImportSpecctraSES("{SES_FILE}") if result: print("Imported SES") board.Save("{PCB_FILE}") print("Saved board") else: print("SES import returned False - trying alternate method") result = pcbnew.ImportSpecctraSES(board, "{SES_FILE}") if result: board.Save("{PCB_FILE}") print("Saved board") except Exception as e: print(f"SES import error: {{e}}") ''' return run_pcbnew_script(script) def run_drc(): """Run DRC using kicad-cli.""" print("Running DRC...") result = subprocess.run( ["kicad-cli", "pcb", "drc", "--severity-all", "--units", "mm", "-o", DRC_FILE, PCB_FILE], capture_output=True, text=True ) print(result.stdout) # Count errors 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"DRC: {errors} errors, {warnings} warnings") return errors == 0 return False def main(): print("=" * 50) print("SN-L00 Headless Autoroute Pipeline") print("=" * 50) check_dependencies() download_freerouting() print("\n[1/4] Placing components and exporting DSN...") if not place_and_export(): print("Failed to place/export") # Fall back to manual DSN check if not os.path.exists(DSN_FILE): print(f"ERROR: {DSN_FILE} not found") print("Export manually: File → Export → Specctra DSN") sys.exit(1) print("\n[2/4] Running Freerouting...") if not run_freerouting(): print("Freerouting failed") sys.exit(1) print("\n[3/4] Importing routed session...") if not import_ses(): print("SES import failed - import manually in KiCad") print("\n[4/4] Running DRC...") success = run_drc() print("\n" + "=" * 50) if success: print("SUCCESS! Board routed with no DRC errors.") else: print("Done. Check DRC.rpt for remaining issues.") print("=" * 50) if __name__ == "__main__": main()