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)
This commit is contained in:
@@ -33,6 +33,15 @@ 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
|
||||
@@ -124,6 +133,29 @@ def main():
|
||||
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
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SN-L00 Command-Line Autoroute Pipeline
|
||||
Run from terminal: python3 scripts/autoroute_cli.py
|
||||
|
||||
Requires: kicad-cli or pcbnew Python module available
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
import sys
|
||||
|
||||
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")
|
||||
FREEROUTING_JAR = "/tmp/freerouting.jar"
|
||||
FREEROUTING_URL = "https://github.com/freerouting/freerouting/releases/download/v2.0.1/freerouting-2.0.1.jar"
|
||||
|
||||
def run(cmd, desc, cwd=None):
|
||||
"""Run command with description"""
|
||||
print(f" {desc}...")
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, cwd=cwd)
|
||||
if result.returncode != 0:
|
||||
print(f" ERROR: {result.stderr}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def main():
|
||||
print("=" * 50)
|
||||
print("SN-L00 Command-Line Autoroute Pipeline")
|
||||
print("=" * 50)
|
||||
|
||||
# Check for kicad-cli
|
||||
kicad_cli = None
|
||||
for path in ["/usr/bin/kicad-cli", "/usr/local/bin/kicad-cli",
|
||||
"/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli"]:
|
||||
if os.path.exists(path):
|
||||
kicad_cli = path
|
||||
break
|
||||
|
||||
if not kicad_cli:
|
||||
# Try to find in PATH
|
||||
result = subprocess.run(["which", "kicad-cli"], capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
kicad_cli = result.stdout.strip()
|
||||
|
||||
if not kicad_cli:
|
||||
print("ERROR: kicad-cli not found. Please run in KiCad scripting console instead:")
|
||||
print(" exec(open('scripts/autoroute_full.py').read())")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Using: {kicad_cli}")
|
||||
|
||||
# Step 1: Run placement script in KiCad
|
||||
print("\n[1/4] Placing components...")
|
||||
# kicad-cli pcb export dsn also runs any embedded scripts, but we need
|
||||
# to place first. For now, assume placement was done or use Python API
|
||||
print(" (Run place_8hp.py in KiCad first, or components should already be placed)")
|
||||
|
||||
# Step 2: Export DSN
|
||||
print("\n[2/4] Exporting DSN...")
|
||||
if not run([kicad_cli, "pcb", "export", "dsn", "-o", DSN_FILE, PCB_FILE],
|
||||
"Exporting Specctra DSN"):
|
||||
sys.exit(1)
|
||||
print(f" Created: {DSN_FILE}")
|
||||
|
||||
# Step 3: Download Freerouting if needed
|
||||
print("\n[3/4] Running Freerouting...")
|
||||
if not os.path.exists(FREEROUTING_JAR):
|
||||
print(" Downloading Freerouting...")
|
||||
if not run(["curl", "-L", "-o", FREEROUTING_JAR, FREEROUTING_URL],
|
||||
"Downloading"):
|
||||
sys.exit(1)
|
||||
|
||||
# Run 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
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f" Freerouting error: {result.stderr}")
|
||||
sys.exit(1)
|
||||
|
||||
# Parse output for timing
|
||||
for line in result.stdout.split('\n') + result.stderr.split('\n'):
|
||||
if 'completed' in line.lower():
|
||||
print(f" {line.split(']')[-1].strip()}")
|
||||
|
||||
print(f" Created: {SES_FILE}")
|
||||
|
||||
# Step 4: Import SES
|
||||
print("\n[4/4] Importing routed session...")
|
||||
if not run([kicad_cli, "pcb", "import", "ses", "-i", SES_FILE, PCB_FILE],
|
||||
"Importing Specctra session"):
|
||||
# kicad-cli might not support import, manual step needed
|
||||
print(" NOTE: Import via kicad-cli may not be supported.")
|
||||
print(" In KiCad: File → Import → Specctra Session")
|
||||
else:
|
||||
print(f" Imported routes into: {PCB_FILE}")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("DONE! Open PCB in KiCad and run DRC to verify.")
|
||||
print("=" * 50)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+293
@@ -0,0 +1,293 @@
|
||||
#!/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()
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SN-L00 Component Placement Script for 6HP Eurorack
|
||||
Board at origin (0,0) to (28,100)
|
||||
Run in KiCad PCB Editor: Tools → Scripting Console
|
||||
Then: exec(open('scripts/place_6hp.py').read())
|
||||
"""
|
||||
|
||||
import pcbnew
|
||||
|
||||
board = pcbnew.GetBoard()
|
||||
|
||||
def mm(val):
|
||||
return pcbnew.FromMM(val)
|
||||
|
||||
def place(x, y):
|
||||
return pcbnew.VECTOR2I(mm(x), mm(y))
|
||||
|
||||
# Component positions for 6HP (28mm x 100mm) layout
|
||||
# Board origin at (0,0), components placed relative to that
|
||||
placements = {
|
||||
# Top - OLED display (rotated 90°, 13mm wide when rotated)
|
||||
"MOD3": (14, 18, 90),
|
||||
|
||||
# Audio jacks
|
||||
"J3": (7, 40, 0), # RETURN_IN
|
||||
"J4": (21, 40, 0), # TRIG_OUT
|
||||
|
||||
# Button
|
||||
"SW1": (14, 54, 0),
|
||||
|
||||
# RP2040-Zero module
|
||||
"MOD2": (14, 70, 0),
|
||||
|
||||
# Signal conditioning ICs
|
||||
"U2": (6, 85, 0), # 74LVC1G17
|
||||
"U4": (22, 85, 0), # MCP6001
|
||||
|
||||
# Resistors
|
||||
"R2": (3, 90, 0),
|
||||
"R3": (7, 90, 0),
|
||||
"R4": (11, 90, 0),
|
||||
"R5": (15, 90, 0),
|
||||
"R6": (19, 90, 0),
|
||||
"R7": (23, 90, 0),
|
||||
|
||||
# Decoupling caps
|
||||
"C4": (3, 85, 90),
|
||||
"C5": (11, 85, 90),
|
||||
"C6": (25, 85, 90),
|
||||
|
||||
# LED
|
||||
"D5": (25, 90, 0),
|
||||
|
||||
# Protection diodes
|
||||
"D3": (6, 94, 0),
|
||||
"D4": (22, 94, 0),
|
||||
|
||||
# Power section
|
||||
"J2": (14, 94, 0), # Eurorack power header
|
||||
"D2": (3, 94, 90), # Protection diode
|
||||
"U3": (25, 94, 180), # LDO - moved right
|
||||
"C2": (3, 90, 90), # Input cap
|
||||
"C3": (25, 90, 90), # Output cap - moved right
|
||||
}
|
||||
|
||||
print("Placing components for 6HP Eurorack layout...")
|
||||
placed = 0
|
||||
not_found = []
|
||||
|
||||
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" {ref} -> ({x}, {y}) rot={rot}°")
|
||||
else:
|
||||
not_found.append(ref)
|
||||
|
||||
print(f"\nPlaced {placed} components")
|
||||
if not_found:
|
||||
print(f"Not found: {', '.join(not_found)}")
|
||||
|
||||
pcbnew.Refresh()
|
||||
print("\nDone! Save (Ctrl+S), then export DSN for routing.")
|
||||
Reference in New Issue
Block a user