diff --git a/README.md b/README.md index f29f497..ca0cdcf 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ This guide will help you set up a Model Context Protocol (MCP) server for KiCad. - macOS, Windows, or Linux with KiCad installed - Python 3.10 or higher +- KiCad 9.0 or higher - Claude Desktop (or another MCP client) - Basic familiarity with the terminal @@ -154,9 +155,10 @@ The KiCad MCP Server provides several key features, each with detailed documenta - **BOM Management**: Analyze and export Bills of Materials - *Example:* "Generate a BOM for my smart watch project" → Creates a detailed bill of materials -- **Design Rule Checking**: Run DRC checks and track your progress over time + - **Design Rule Checking**: Run DRC checks and track your progress over time - *Example:* "Run DRC on my power supply board and compare to last week" → Shows progress in fixing violations - + - *KiCad 9.0+ Compatible:* Uses the new KiCad CLI or IPC API automatically + - **PCB Visualization**: Generate visual representations of your PCB layouts - *Example:* "Show me a thumbnail of my audio amplifier PCB" → Displays a visual render of the board diff --git a/docs/drc_guide.md b/docs/drc_guide.md index 12ad952..e8382e8 100644 --- a/docs/drc_guide.md +++ b/docs/drc_guide.md @@ -11,6 +11,24 @@ The Design Rule Check (DRC) functionality allows you to: 3. Track your progress over time as you fix issues 4. Compare current results with previous checks +## KiCad 9.0+ Compatibility + +**Important Update**: With KiCad 9.0+, the DRC functionality has been reimplemented to work with the new KiCad APIs. The server now supports two methods for running DRC: + +1. **KiCad CLI Method** (Recommended) - Uses the `kicad-cli` command-line tool to run DRC checks without requiring a running instance of KiCad. + +2. **IPC API Method** - Connects to a running instance of KiCad through the new IPC API using the `kicad-python` package. + +The server automatically selects the best available method based on your KiCad installation. + +## Prerequisites + +For optimal DRC functionality with KiCad 9.0+, you should have: + +- KiCad 9.0 or newer installed +- `kicad-cli` available in your system PATH (included with KiCad 9.0+) +- For IPC API functionality: the `kicad-python` package installed (`pip install kicad-python`) + ## Using DRC Features ### Running a DRC Check @@ -25,6 +43,7 @@ Please run a DRC check on my project at /Users/username/Documents/KiCad/my_proje ``` The tool will: +- Automatically select the best available method (CLI or IPC API) - Analyze your PCB design for rule violations - Generate a comprehensive report - Save the results to your DRC history @@ -122,5 +141,19 @@ If the DRC check fails to run: 1. Ensure your KiCad project exists at the specified path 2. Verify that the project contains a PCB file (.kicad_pcb) -3. Check that the KiCad installation is detected correctly +3. Check your KiCad installation: + - For CLI method: Verify `kicad-cli` is in your PATH or in a standard installation location + - For IPC API method: Make sure KiCad is running with the API server enabled in Preferences > Plugins 4. Try using the full absolute path to your project file + +### Method Selection Issues + +If you want to force a specific DRC method: + +1. **CLI Method**: Ensure `kicad-cli` is available in your PATH +2. **IPC API Method**: + - Install the `kicad-python` package + - Launch KiCad before running the DRC check + - Enable the API server in KiCad preferences + +If you continue to experience issues, check the server logs for more detailed error information. diff --git a/kicad_mcp/resources/drc_resources.py b/kicad_mcp/resources/drc_resources.py index 9f39cec..2752dba 100644 --- a/kicad_mcp/resources/drc_resources.py +++ b/kicad_mcp/resources/drc_resources.py @@ -10,7 +10,7 @@ from mcp.server.fastmcp import FastMCP from kicad_mcp.utils.file_utils import get_project_files from kicad_mcp.utils.logger import Logger from kicad_mcp.utils.drc_history import get_drc_history -from kicad_mcp.tools.drc_tools import run_drc_via_cli +from kicad_mcp.tools.drc_impl.cli_drc import run_drc_via_cli # Create logger for this module logger = Logger() diff --git a/kicad_mcp/tools/drc_impl/__init__.py b/kicad_mcp/tools/drc_impl/__init__.py new file mode 100644 index 0000000..a9774f5 --- /dev/null +++ b/kicad_mcp/tools/drc_impl/__init__.py @@ -0,0 +1,3 @@ +""" +DRC implementations for different KiCad API approaches. +""" diff --git a/kicad_mcp/tools/drc_impl/cli_drc.py b/kicad_mcp/tools/drc_impl/cli_drc.py new file mode 100644 index 0000000..6325ff4 --- /dev/null +++ b/kicad_mcp/tools/drc_impl/cli_drc.py @@ -0,0 +1,169 @@ +""" +Design Rule Check (DRC) implementation using KiCad command-line interface. +""" +import os +import json +import subprocess +import tempfile +from typing import Dict, Any, Optional +from mcp.server.fastmcp import Context + +from kicad_mcp.utils.logger import Logger +from kicad_mcp.config import system + +# Create logger for this module +logger = Logger() + +async def run_drc_via_cli(pcb_file: str, ctx: Context) -> Dict[str, Any]: + """Run DRC using KiCad command line tools. + + Args: + pcb_file: Path to the PCB file (.kicad_pcb) + ctx: MCP context for progress reporting + + Returns: + Dictionary with DRC results + """ + results = { + "success": False, + "method": "cli", + "pcb_file": pcb_file + } + + try: + # Create a temporary directory for the output + with tempfile.TemporaryDirectory() as temp_dir: + # Output file for DRC report + output_file = os.path.join(temp_dir, "drc_report.json") + + # Find kicad-cli executable + kicad_cli = find_kicad_cli() + if not kicad_cli: + logger.error("kicad-cli not found in PATH or common installation locations") + results["error"] = "kicad-cli not found. Please ensure KiCad 9.0+ is installed and kicad-cli is available." + return results + + # Report progress + await ctx.report_progress(50, 100) + ctx.info("Running DRC using KiCad CLI...") + + # Build the DRC command + cmd = [ + kicad_cli, + "pcb", + "drc", + "--format", "json", + "--output", output_file, + pcb_file + ] + + logger.info(f"Running command: {' '.join(cmd)}") + process = subprocess.run(cmd, capture_output=True, text=True) + + # Check if the command was successful + if process.returncode != 0: + logger.error(f"DRC command failed with code {process.returncode}") + logger.error(f"Error output: {process.stderr}") + results["error"] = f"DRC command failed: {process.stderr}" + return results + + # Check if the output file was created + if not os.path.exists(output_file): + logger.error("DRC report file not created") + results["error"] = "DRC report file not created" + return results + + # Read the DRC report + with open(output_file, 'r') as f: + try: + drc_report = json.load(f) + except json.JSONDecodeError: + logger.error("Failed to parse DRC report JSON") + results["error"] = "Failed to parse DRC report JSON" + return results + + # Process the DRC report + violations = drc_report.get("violations", []) + violation_count = len(violations) + logger.info(f"DRC completed with {violation_count} violations") + await ctx.report_progress(70, 100) + ctx.info(f"DRC completed with {violation_count} violations") + + # Categorize violations by type + error_types = {} + for violation in violations: + error_type = violation.get("message", "Unknown") + if error_type not in error_types: + error_types[error_type] = 0 + error_types[error_type] += 1 + + # Create success response + results = { + "success": True, + "method": "cli", + "pcb_file": pcb_file, + "total_violations": violation_count, + "violation_categories": error_types, + "violations": violations + } + + await ctx.report_progress(90, 100) + return results + + except Exception as e: + logger.error(f"Error in CLI DRC: {str(e)}", exc_info=True) + results["error"] = f"Error in CLI DRC: {str(e)}" + return results + + +def find_kicad_cli() -> Optional[str]: + """Find the kicad-cli executable in the system PATH. + + Returns: + Path to kicad-cli if found, None otherwise + """ + # Check if kicad-cli is in PATH + try: + if system == "Windows": + # On Windows, check for kicad-cli.exe + result = subprocess.run(["where", "kicad-cli.exe"], capture_output=True, text=True) + if result.returncode == 0: + return result.stdout.strip().split("\n")[0] + else: + # On Unix-like systems, use which + result = subprocess.run(["which", "kicad-cli"], capture_output=True, text=True) + if result.returncode == 0: + return result.stdout.strip() + + except Exception as e: + logger.error(f"Error finding kicad-cli: {str(e)}") + + # If we get here, kicad-cli is not in PATH + # Try common installation locations + if system == "Windows": + # Common Windows installation path + potential_paths = [ + r"C:\Program Files\KiCad\bin\kicad-cli.exe", + r"C:\Program Files (x86)\KiCad\bin\kicad-cli.exe" + ] + elif system == "Darwin": # macOS + # Common macOS installation paths + potential_paths = [ + "/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli", + "/Applications/KiCad/kicad-cli" + ] + else: # Linux and other Unix-like systems + # Common Linux installation paths + potential_paths = [ + "/usr/bin/kicad-cli", + "/usr/local/bin/kicad-cli", + "/opt/kicad/bin/kicad-cli" + ] + + # Check each potential path + for path in potential_paths: + if os.path.exists(path) and os.access(path, os.X_OK): + return path + + # If still not found, return None + return None diff --git a/kicad_mcp/tools/drc_impl/ipc_drc.py b/kicad_mcp/tools/drc_impl/ipc_drc.py new file mode 100644 index 0000000..3ebe1b8 --- /dev/null +++ b/kicad_mcp/tools/drc_impl/ipc_drc.py @@ -0,0 +1,163 @@ +""" +Design Rule Check (DRC) implementation using the KiCad IPC API. +""" +import os +from typing import Dict, Any +from mcp.server.fastmcp import Context + +from kicad_mcp.utils.logger import Logger +from kicad_mcp.utils.kicad_api_detection import check_ipc_api_environment + +# Create logger for this module +logger = Logger() + +async def run_drc_with_ipc_api(pcb_file: str, ctx: Context) -> Dict[str, Any]: + """Run DRC using the KiCad IPC API (kicad-python). + This requires a running instance of KiCad with the IPC API enabled. + + Args: + pcb_file: Path to the PCB file (.kicad_pcb) + ctx: MCP context for progress reporting + + Returns: + Dictionary with DRC results + """ + try: + # Import the kicad-python modules + import kipy + from kipy.board_types import DrcExclusion, DrcSeverity + logger.info("Successfully imported kipy modules") + + # Check if we're running in a KiCad IPC plugin environment + is_plugin, socket_path = check_ipc_api_environment() + + # Connect to KiCad + await ctx.report_progress(20, 100) + ctx.info("Connecting to KiCad...") + + if is_plugin: + # When running as a plugin, let kipy use environment variables + kicad = kipy.KiCad() + else: + # When running standalone, try to connect to KiCad + if socket_path: + kicad = kipy.KiCad(socket_path=socket_path) + else: + # Try with default socket path + kicad = kipy.KiCad() + + # Get the currently open board + await ctx.report_progress(30, 100) + ctx.info("Getting board...") + + # Check which board to use + current_boards = await kicad.get_open_documents("board") + + # If we have an open board, check if it's the one we want + use_current_board = False + board_doc = None + + if current_boards: + for doc in current_boards: + if doc.file_path and os.path.normpath(doc.file_path) == os.path.normpath(pcb_file): + board_doc = doc + use_current_board = True + break + + # If the board isn't open, see if we can open it + if not use_current_board: + ctx.info(f"Opening board file: {pcb_file}") + try: + # Try to open the board + doc = await kicad.open_document(pcb_file) + board_doc = doc + except Exception as e: + logger.error(f"Error opening board: {str(e)}") + return { + "success": False, + "method": "ipc", + "error": f"Failed to open board file: {str(e)}" + } + + # Get the board + board = await kicad.board.get_board(board_doc.uuid) + + # Run DRC + await ctx.report_progress(50, 100) + ctx.info("Running DRC check...") + + # Define which severities to include + severity_filter = DrcSeverity.ERROR | DrcSeverity.WARNING + + # Run DRC on the board + drc_report = await kicad.board.run_drc( + board.uuid, + severity_filter=severity_filter, + exclusions=DrcExclusion.NONE # Include all violations + ) + + # Process results + await ctx.report_progress(70, 100) + ctx.info("Processing DRC results...") + + # Get all violations + violations = drc_report.violations + violation_count = len(violations) + + logger.info(f"DRC completed with {violation_count} violations") + ctx.info(f"DRC completed with {violation_count} violations") + + # Process the violations + drc_errors = [] + error_types = {} + + for violation in violations: + # Get violation details + severity = str(violation.severity) + message = violation.message + + # Extract location + location = { + "x": violation.location.x if hasattr(violation, 'location') and violation.location else 0, + "y": violation.location.y if hasattr(violation, 'location') and violation.location else 0 + } + + error_info = { + "severity": severity, + "message": message, + "location": location + } + + drc_errors.append(error_info) + + # Count by type + if message not in error_types: + error_types[message] = 0 + error_types[message] += 1 + + # Create result + results = { + "success": True, + "method": "ipc", + "pcb_file": pcb_file, + "total_violations": violation_count, + "violation_categories": error_types, + "violations": drc_errors + } + + return results + + except ImportError as e: + logger.error(f"Failed to import kipy modules: {str(e)}") + return { + "success": False, + "method": "ipc", + "error": f"Failed to import kipy modules: {str(e)}" + } + except Exception as e: + logger.error(f"Error in IPC API DRC: {str(e)}", exc_info=True) + return { + "success": False, + "method": "ipc", + "error": f"Error in IPC API DRC: {str(e)}" + } diff --git a/kicad_mcp/tools/drc_tools.py b/kicad_mcp/tools/drc_tools.py index 3361f0f..c7a4b4a 100644 --- a/kicad_mcp/tools/drc_tools.py +++ b/kicad_mcp/tools/drc_tools.py @@ -2,26 +2,26 @@ Design Rule Check (DRC) tools for KiCad PCB files. """ import os -import json -import subprocess -import tempfile -from typing import Dict, Any, List, Optional, Tuple +from typing import Dict, Any from mcp.server.fastmcp import FastMCP, Context from kicad_mcp.utils.file_utils import get_project_files from kicad_mcp.utils.logger import Logger from kicad_mcp.utils.drc_history import save_drc_result, get_drc_history, compare_with_previous -from kicad_mcp.config import KICAD_APP_PATH, system +from kicad_mcp.utils.kicad_api_detection import get_best_api_approach + +# Import implementations +from kicad_mcp.tools.drc_impl.cli_drc import run_drc_via_cli +from kicad_mcp.tools.drc_impl.ipc_drc import run_drc_with_ipc_api # Create logger for this module logger = Logger() -def register_drc_tools(mcp: FastMCP, kicad_modules_available: bool = False) -> None: +def register_drc_tools(mcp: FastMCP) -> None: """Register DRC tools with the MCP server. Args: mcp: The FastMCP server instance - kicad_modules_available: Whether KiCad Python modules are available """ @mcp.tool() @@ -73,6 +73,7 @@ def register_drc_tools(mcp: FastMCP, kicad_modules_available: bool = False) -> N Args: project_path: Path to the KiCad project file (.kicad_pro) + ctx: MCP context for progress reporting Returns: Dictionary with DRC results and statistics @@ -96,38 +97,35 @@ def register_drc_tools(mcp: FastMCP, kicad_modules_available: bool = False) -> N await ctx.report_progress(10, 100) ctx.info(f"Starting DRC check on {os.path.basename(pcb_file)}") - # Try to use pcbnew if available - if kicad_modules_available: - try: - drc_results = await run_drc_with_pcbnew(pcb_file, ctx) - if drc_results["success"]: - # Save results to history - save_drc_result(project_path, drc_results) - - # Add comparison with previous run - comparison = compare_with_previous(project_path, drc_results) - if comparison: - drc_results["comparison"] = comparison - - if comparison["change"] < 0: - ctx.info(f"Great progress! You've fixed {abs(comparison['change'])} DRC violations since the last check.") - elif comparison["change"] > 0: - ctx.info(f"Found {comparison['change']} new DRC violations since the last check.") - else: - ctx.info(f"No change in the number of DRC violations since the last check.") - - return drc_results - except Exception as e: - logger.error(f"Error running DRC with pcbnew: {str(e)}", exc_info=True) - ctx.info(f"Error running DRC with pcbnew: {str(e)}") - # Fall back to CLI method if pcbnew fails + # Get app context and determine which approach to use + app_context = ctx.request_context.lifespan_context + api_approach = getattr(app_context, 'api_approach', get_best_api_approach()) - # Fall back to command line DRC check - logger.info("Attempting DRC check via command line") - await ctx.report_progress(30, 100) - drc_results = run_drc_via_cli(pcb_file) + # Run DRC using the appropriate approach + drc_results = None - if drc_results["success"]: + if api_approach == "cli": + # Use CLI approach (kicad-cli) + logger.info("Using kicad-cli for DRC") + ctx.info("Using KiCad CLI for DRC check...") + drc_results = await run_drc_via_cli(pcb_file, ctx) + + elif api_approach == "ipc": + # Use IPC API approach (kicad-python) + logger.info("Using IPC API for DRC") + ctx.info("Using KiCad IPC API for DRC check...") + drc_results = await run_drc_with_ipc_api(pcb_file, ctx) + + else: + # No API available + logger.error("No KiCad API available for DRC") + return { + "success": False, + "error": "No KiCad API available for DRC. Please install KiCad 9.0 or later." + } + + # Process and save results if successful + if drc_results and drc_results.get("success", False): # Save results to history save_drc_result(project_path, drc_results) @@ -146,225 +144,7 @@ def register_drc_tools(mcp: FastMCP, kicad_modules_available: bool = False) -> N # Complete progress await ctx.report_progress(100, 100) - return drc_results - - -async def run_drc_with_pcbnew(pcb_file: str, ctx: Context) -> Dict[str, Any]: - """Run DRC using the pcbnew Python module. - - Args: - pcb_file: Path to the PCB file (.kicad_pcb) - ctx: MCP context for progress reporting - - Returns: - Dictionary with DRC results - """ - try: - import pcbnew - logger.info("Successfully imported pcbnew module") - - # Load the board - board = pcbnew.LoadBoard(pcb_file) - if not board: - logger.error("Failed to load PCB file") - return {"success": False, "error": "Failed to load PCB file"} - - await ctx.report_progress(40, 100) - ctx.info("PCB file loaded, running DRC checks...") - - # Create a DRC runner - drc = pcbnew.DRC(board) - drc.SetViolationHandler(pcbnew.DRC_ITEM_LIST()) - - # Run the DRC - drc.Run() - await ctx.report_progress(70, 100) - - # Get the violations - violations = drc.GetViolations() - violation_count = violations.GetCount() - - logger.info(f"DRC completed with {violation_count} violations") - ctx.info(f"DRC completed with {violation_count} violations") - - # Process the violations - drc_errors = [] - for i in range(violation_count): - violation = violations.GetItem(i) - error_info = { - "severity": violation.GetSeverity(), - "message": violation.GetErrorMessage(), - "location": { - "x": violation.GetPointA().x / 1000000.0, # Convert to mm - "y": violation.GetPointA().y / 1000000.0 - } - } - drc_errors.append(error_info) - - await ctx.report_progress(90, 100) - - # Categorize violations by type - error_types = {} - for error in drc_errors: - error_type = error["message"] - if error_type not in error_types: - error_types[error_type] = 0 - error_types[error_type] += 1 - - # Create summary - results = { - "success": True, - "method": "pcbnew", - "pcb_file": pcb_file, - "total_violations": violation_count, - "violation_categories": error_types, - "violations": drc_errors + return drc_results or { + "success": False, + "error": "DRC check failed with an unknown error" } - - return results - - except ImportError as e: - logger.error(f"Failed to import pcbnew: {str(e)}") - raise - except Exception as e: - logger.error(f"Error in pcbnew DRC: {str(e)}", exc_info=True) - raise - - -def run_drc_via_cli(pcb_file: str) -> Dict[str, Any]: - """Run DRC using KiCad command line tools. - This is a fallback method when pcbnew Python module is not available. - - Args: - pcb_file: Path to the PCB file (.kicad_pcb) - - Returns: - Dictionary with DRC results - """ - results = { - "success": False, - "method": "cli", - "pcb_file": pcb_file - } - - try: - # Create a temporary directory for the output - with tempfile.TemporaryDirectory() as temp_dir: - # The command to run DRC depends on the operating system - if system == "Darwin": # macOS - # Path to KiCad command line tools on macOS - pcbnew_cli = os.path.join(KICAD_APP_PATH, "Contents/MacOS/pcbnew_cli") - - # Check if the CLI tool exists - if not os.path.exists(pcbnew_cli): - logger.error(f"pcbnew_cli not found at {pcbnew_cli}") - results["error"] = f"pcbnew_cli not found at {pcbnew_cli}" - return results - - # Output file for DRC report - output_file = os.path.join(temp_dir, "drc_report.json") - - # Run the DRC command - cmd = [ - pcbnew_cli, - "--run-drc", - "--output-json", output_file, - pcb_file - ] - - elif system == "Windows": - # Path to KiCad command line tools on Windows - pcbnew_cli = os.path.join(KICAD_APP_PATH, "bin", "pcbnew_cli.exe") - - # Check if the CLI tool exists - if not os.path.exists(pcbnew_cli): - logger.error(f"pcbnew_cli not found at {pcbnew_cli}") - results["error"] = f"pcbnew_cli not found at {pcbnew_cli}" - return results - - # Output file for DRC report - output_file = os.path.join(temp_dir, "drc_report.json") - - # Run the DRC command - cmd = [ - pcbnew_cli, - "--run-drc", - "--output-json", output_file, - pcb_file - ] - - elif system == "Linux": - # Path to KiCad command line tools on Linux - pcbnew_cli = "pcbnew_cli" # Assume it's in the PATH - - # Output file for DRC report - output_file = os.path.join(temp_dir, "drc_report.json") - - # Run the DRC command - cmd = [ - pcbnew_cli, - "--run-drc", - "--output-json", output_file, - pcb_file - ] - - else: - results["error"] = f"Unsupported operating system: {system}" - return results - - logger.info(f"Running command: {' '.join(cmd)}") - process = subprocess.run(cmd, capture_output=True, text=True) - - # Check if the command was successful - if process.returncode != 0: - logger.error(f"DRC command failed with code {process.returncode}") - logger.error(f"Error output: {process.stderr}") - results["error"] = f"DRC command failed: {process.stderr}" - return results - - # Check if the output file was created - if not os.path.exists(output_file): - logger.error("DRC report file not created") - results["error"] = "DRC report file not created" - return results - - # Read the DRC report - with open(output_file, 'r') as f: - try: - drc_report = json.load(f) - except json.JSONDecodeError: - logger.error("Failed to parse DRC report JSON") - results["error"] = "Failed to parse DRC report JSON" - return results - - # Process the DRC report - violation_count = len(drc_report.get("violations", [])) - logger.info(f"DRC completed with {violation_count} violations") - - # Extract violations - violations = drc_report.get("violations", []) - - # Categorize violations by type - error_types = {} - for violation in violations: - error_type = violation.get("message", "Unknown") - if error_type not in error_types: - error_types[error_type] = 0 - error_types[error_type] += 1 - - # Create success response - results = { - "success": True, - "method": "cli", - "pcb_file": pcb_file, - "total_violations": violation_count, - "violation_categories": error_types, - "violations": violations - } - - return results - - except Exception as e: - logger.error(f"Error in CLI DRC: {str(e)}", exc_info=True) - results["error"] = f"Error in CLI DRC: {str(e)}" - return results diff --git a/kicad_mcp/utils/kicad_api_detection.py b/kicad_mcp/utils/kicad_api_detection.py new file mode 100644 index 0000000..2780b4e --- /dev/null +++ b/kicad_mcp/utils/kicad_api_detection.py @@ -0,0 +1,135 @@ +""" +Utility functions for detecting and selecting available KiCad API approaches. +""" +import os +import subprocess +import shutil +from typing import Tuple, Optional, Literal + +from kicad_mcp.utils.logger import Logger +from kicad_mcp.config import system + +# Create logger for this module +logger = Logger() + +def check_for_cli_api() -> bool: + """Check if KiCad CLI API is available. + + Returns: + True if KiCad CLI is available, False otherwise + """ + try: + # Check if kicad-cli is in PATH + if system == "Windows": + # On Windows, check for kicad-cli.exe + kicad_cli = shutil.which("kicad-cli.exe") + else: + # On Unix-like systems + kicad_cli = shutil.which("kicad-cli") + + if kicad_cli: + # Verify it's a working kicad-cli + if system == "Windows": + cmd = [kicad_cli, "--version"] + else: + cmd = [kicad_cli, "--version"] + + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode == 0: + logger.info(f"Found working kicad-cli: {kicad_cli}") + return True + + # Check common installation locations if not found in PATH + if system == "Windows": + # Common Windows installation paths + potential_paths = [ + r"C:\Program Files\KiCad\bin\kicad-cli.exe", + r"C:\Program Files (x86)\KiCad\bin\kicad-cli.exe" + ] + elif system == "Darwin": # macOS + # Common macOS installation paths + potential_paths = [ + "/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli", + "/Applications/KiCad/kicad-cli" + ] + else: # Linux + # Common Linux installation paths + potential_paths = [ + "/usr/bin/kicad-cli", + "/usr/local/bin/kicad-cli", + "/opt/kicad/bin/kicad-cli" + ] + + # Check each potential path + for path in potential_paths: + if os.path.exists(path) and os.access(path, os.X_OK): + logger.info(f"Found kicad-cli at common location: {path}") + return True + + logger.info("KiCad CLI API is not available") + return False + + except Exception as e: + logger.error(f"Error checking for KiCad CLI API: {str(e)}") + return False + + +def check_for_ipc_api() -> bool: + """Check if KiCad IPC API (kicad-python) is available. + + Returns: + True if KiCad IPC API is available, False otherwise + """ + try: + # Try to import the kipy module + import kipy + logger.info("KiCad IPC API (kicad-python) is available") + return True + except ImportError: + logger.info("KiCad IPC API (kicad-python) is not available") + return False + except Exception as e: + logger.error(f"Error checking for KiCad IPC API: {str(e)}") + return False + + +def check_ipc_api_environment() -> Tuple[bool, Optional[str]]: + """Check if we're running in a KiCad IPC plugin environment. + + Returns: + Tuple of (is_plugin, socket_path) + """ + # Check for environment variables that would indicate we're a plugin + is_plugin = os.environ.get("KICAD_PLUGIN_ENV") is not None + + # Check for socket path in environment + socket_path = os.environ.get("KICAD_SOCKET_PATH") + + if is_plugin: + logger.info("Running as a KiCad plugin") + elif socket_path: + logger.info(f"KiCad IPC socket path found: {socket_path}") + + return (is_plugin, socket_path) + + +def get_best_api_approach() -> Literal["cli", "ipc", "none"]: + """Determine the best available KiCad API approach. + + Returns: + String indicating which API approach to use: + - "cli": Use KiCad command-line interface + - "ipc": Use KiCad IPC API (kicad-python) + - "none": No API available + """ + # Check for IPC API first (preferred if available) + if check_for_ipc_api(): + return "ipc" + + # Check for CLI API next + if check_for_cli_api(): + return "cli" + + # No API available + logger.warning("No KiCad API available") + return "none" diff --git a/requirements.txt b/requirements.txt index 455d29a..73818dc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ mcp[cli] httpx pytest pandas +kicad-python