d43c7976ad
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)
247 lines
7.0 KiB
Python
Executable File
247 lines
7.0 KiB
Python
Executable File
#!/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)
|