Wire native SES applier and headless DSN export into FreeRouting
Some checks are pending
CI / Lint and Format (push) Waiting to run
CI / Test Python 3.11 on macos-latest (push) Waiting to run
CI / Test Python 3.12 on macos-latest (push) Waiting to run
CI / Test Python 3.13 on macos-latest (push) Waiting to run
CI / Test Python 3.10 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.11 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.12 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.13 on ubuntu-latest (push) Waiting to run
CI / Security Scan (push) Waiting to run
CI / Build Package (push) Blocked by required conditions

Replace the two broken kicad-cli specctra subprocess calls:

- import_ses_to_kicad now uses apply_ses_to_board (native, headless)
  instead of 'kicad-cli pcb import specctra-ses', logging segment/via/
  net counts and warning on nets missing from the board.
- export_dsn_from_kicad runs KiCad's bundled Python calling
  pcbnew.ExportSpecctraDSN (works headless, no wxApp) instead of
  'kicad-cli pcb export specctra-dsn'.

find_kicad_python locates a pcbnew-capable interpreter cross-platform:
the current interpreter, then the macOS KiCad framework Python, then a
python3 on PATH.
This commit is contained in:
Ryan Malloy 2026-07-12 11:10:34 -06:00
parent 00fb40fbf4
commit dbbb627278

View File

@ -9,10 +9,12 @@ FreeRouting: https://www.freerouting.app/
GitHub: https://github.com/freerouting/freerouting
"""
import glob
import logging
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import time
from typing import Any
@ -20,6 +22,7 @@ from typing import Any
from kipy.board_types import BoardLayer
from .ipc_client import kicad_ipc_session
from .ses_apply import apply_ses_to_board
logger = logging.getLogger(__name__)
@ -29,6 +32,56 @@ class FreeRoutingError(Exception):
pass
def find_kicad_python() -> str | None:
"""Locate a Python interpreter that can ``import pcbnew``.
KiCad 10's ``kicad-cli`` has no Specctra subcommands, so DSN export runs
through KiCad's bundled Python calling ``pcbnew.ExportSpecctraDSN`` (which
works headless, unlike the GUI-only SES importer).
Order of preference:
1. The current interpreter, if ``pcbnew`` is already importable.
2. KiCad's bundled framework Python (macOS app bundle).
3. A plain ``python3`` on PATH that can import ``pcbnew`` (Linux/Windows
distro installs put ``pcbnew`` on the system Python's path).
"""
# 1. Current interpreter.
try:
import pcbnew # noqa: F401
return sys.executable
except Exception:
pass
candidates: list[str] = []
# 2. macOS bundled framework Python.
candidates.extend(
sorted(
glob.glob(
"/Applications/KiCad/KiCad.app/Contents/Frameworks/"
"Python.framework/Versions/*/bin/python3"
)
)
)
# 3. Plain python3 on PATH.
candidates.append("python3")
for candidate in candidates:
try:
result = subprocess.run(
[candidate, "-c", "import pcbnew"],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode == 0:
return candidate
except (OSError, subprocess.SubprocessError):
continue
return None
class FreeRoutingEngine:
"""
Engine for automated PCB routing using FreeRouting.
@ -172,7 +225,11 @@ class FreeRoutingEngine:
routing_options: dict[str, Any] | None = None
) -> bool:
"""
Export DSN file from KiCad board using KiCad CLI.
Export DSN file from KiCad board.
KiCad 10's ``kicad-cli`` has no ``specctra-dsn`` subcommand, so this
invokes KiCad's bundled Python running ``pcbnew.ExportSpecctraDSN``,
which works headless (no wxApp / display required).
Args:
board_path: Path to .kicad_pcb file
@ -182,19 +239,28 @@ class FreeRoutingEngine:
Returns:
True if export successful
"""
try:
# Use KiCad CLI to export DSN
cmd = [
"kicad-cli", "pcb", "export", "specctra-dsn",
"--output", dsn_output_path,
board_path
]
python_exe = find_kicad_python()
if python_exe is None:
logger.error(
"DSN export failed: no Python interpreter with 'pcbnew' found "
"(install KiCad and ensure pcbnew is importable)"
)
return False
# Minimal headless export script: load the board, write the DSN.
script = (
"import sys, pcbnew\n"
"board = pcbnew.LoadBoard(sys.argv[1])\n"
"ok = pcbnew.ExportSpecctraDSN(board, sys.argv[2])\n"
"sys.exit(0 if ok else 1)\n"
)
try:
result = subprocess.run(
cmd,
[python_exe, "-c", script, board_path, dsn_output_path],
capture_output=True,
text=True,
timeout=60
timeout=120,
)
if result.returncode == 0 and os.path.isfile(dsn_output_path):
@ -337,8 +403,13 @@ class FreeRoutingEngine:
"""
Import SES routing results back into KiCad board.
Uses the native, headless :func:`apply_ses_to_board` applier instead of
``kicad-cli pcb import specctra-ses`` (which does not exist in KiCad 10)
or ``pcbnew.ImportSpecctraSES`` (which requires a GUI display). Routed
wires and vias are injected directly into the ``.kicad_pcb``.
Args:
board_path: Path to .kicad_pcb file
board_path: Path to .kicad_pcb file (updated in place)
ses_path: Path to SES file with routing results
backup_original: Whether to backup original board file
@ -353,30 +424,22 @@ class FreeRoutingEngine:
shutil.copy2(board_path, backup_path)
logger.info(f"Original board backed up to: {backup_path}")
# Use KiCad CLI to import SES file
cmd = [
"kicad-cli", "pcb", "import", "specctra-ses",
"--output", board_path,
ses_path
]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=60
# Apply the SES natively (in place).
result = apply_ses_to_board(board_path, ses_path, board_path)
logger.info(
"SES applied to %s: %d segments, %d vias across %d nets",
board_path,
result["segments_added"],
result["vias_added"],
result["nets_routed"],
)
if result["unknown_nets"]:
logger.warning(
"SES referenced nets not on the board (skipped): %s",
", ".join(result["unknown_nets"]),
)
return True
if result.returncode == 0:
logger.info(f"SES imported successfully to: {board_path}")
return True
else:
logger.error(f"SES import failed: {result.stderr}")
return False
except subprocess.TimeoutExpired:
logger.error("SES import timed out")
return False
except Exception as e:
logger.error(f"Error importing SES: {e}")
return False