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 GitHub: https://github.com/freerouting/freerouting
""" """
import glob
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
import subprocess import subprocess
import sys
import tempfile import tempfile
import time import time
from typing import Any from typing import Any
@ -20,6 +22,7 @@ from typing import Any
from kipy.board_types import BoardLayer from kipy.board_types import BoardLayer
from .ipc_client import kicad_ipc_session from .ipc_client import kicad_ipc_session
from .ses_apply import apply_ses_to_board
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -29,6 +32,56 @@ class FreeRoutingError(Exception):
pass 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: class FreeRoutingEngine:
""" """
Engine for automated PCB routing using FreeRouting. Engine for automated PCB routing using FreeRouting.
@ -172,7 +225,11 @@ class FreeRoutingEngine:
routing_options: dict[str, Any] | None = None routing_options: dict[str, Any] | None = None
) -> bool: ) -> 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: Args:
board_path: Path to .kicad_pcb file board_path: Path to .kicad_pcb file
@ -182,19 +239,28 @@ class FreeRoutingEngine:
Returns: Returns:
True if export successful True if export successful
""" """
try: python_exe = find_kicad_python()
# Use KiCad CLI to export DSN if python_exe is None:
cmd = [ logger.error(
"kicad-cli", "pcb", "export", "specctra-dsn", "DSN export failed: no Python interpreter with 'pcbnew' found "
"--output", dsn_output_path, "(install KiCad and ensure pcbnew is importable)"
board_path )
] 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( result = subprocess.run(
cmd, [python_exe, "-c", script, board_path, dsn_output_path],
capture_output=True, capture_output=True,
text=True, text=True,
timeout=60 timeout=120,
) )
if result.returncode == 0 and os.path.isfile(dsn_output_path): 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. 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: 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 ses_path: Path to SES file with routing results
backup_original: Whether to backup original board file backup_original: Whether to backup original board file
@ -353,30 +424,22 @@ class FreeRoutingEngine:
shutil.copy2(board_path, backup_path) shutil.copy2(board_path, backup_path)
logger.info(f"Original board backed up to: {backup_path}") logger.info(f"Original board backed up to: {backup_path}")
# Use KiCad CLI to import SES file # Apply the SES natively (in place).
cmd = [ result = apply_ses_to_board(board_path, ses_path, board_path)
"kicad-cli", "pcb", "import", "specctra-ses", logger.info(
"--output", board_path, "SES applied to %s: %d segments, %d vias across %d nets",
ses_path board_path,
] result["segments_added"],
result["vias_added"],
result = subprocess.run( result["nets_routed"],
cmd,
capture_output=True,
text=True,
timeout=60
) )
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: except Exception as e:
logger.error(f"Error importing SES: {e}") logger.error(f"Error importing SES: {e}")
return False return False