Files
SN-L00/hardware/kicad/scripts/autoroute_headless.py
T
florian.berthold d43c7976ad Add panel-to-PCB mounting system with M3 standoffs
Panel alignment:
- PCB offset 10mm from panel top
- Component holes aligned: OLED@25mm, jacks@45mm, button@58mm
- 4x M3 standoff holes at corners (5,5), (35,5), (5,75), (35,75)

Updates:
- Panel SVG and spec aligned with PCB layout
- Mounting holes added to PCB (Edge.Cuts layer)
- Regenerated Gerbers with mounting holes
- Updated autoroute.py to add mounting holes automatically

DRC: 0 unconnected, 7 cosmetic errors (courtyard overlaps)
2026-01-23 08:15:25 +01:00

294 lines
7.6 KiB
Python
Executable File

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