From dbbb6272780491e971f1a2575bb6abbaa2dc0b8c Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sun, 12 Jul 2026 11:10:34 -0600 Subject: [PATCH] Wire native SES applier and headless DSN export into FreeRouting 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. --- src/mckicad/utils/freerouting.py | 129 +++++++++++++++++++++++-------- 1 file changed, 96 insertions(+), 33 deletions(-) diff --git a/src/mckicad/utils/freerouting.py b/src/mckicad/utils/freerouting.py index bf33752..8ae3d5e 100644 --- a/src/mckicad/utils/freerouting.py +++ b/src/mckicad/utils/freerouting.py @@ -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