Fix drc issues - swap out pbnew for kicad cli and ipc
This commit is contained in:
parent
b1cb48ecf7
commit
a3613f273a
@ -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,8 +155,9 @@ 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
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
3
kicad_mcp/tools/drc_impl/__init__.py
Normal file
3
kicad_mcp/tools/drc_impl/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
DRC implementations for different KiCad API approaches.
|
||||
"""
|
169
kicad_mcp/tools/drc_impl/cli_drc.py
Normal file
169
kicad_mcp/tools/drc_impl/cli_drc.py
Normal file
@ -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
|
163
kicad_mcp/tools/drc_impl/ipc_drc.py
Normal file
163
kicad_mcp/tools/drc_impl/ipc_drc.py
Normal file
@ -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)}"
|
||||
}
|
@ -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)
|
||||
# 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())
|
||||
|
||||
# Add comparison with previous run
|
||||
comparison = compare_with_previous(project_path, drc_results)
|
||||
if comparison:
|
||||
drc_results["comparison"] = comparison
|
||||
# Run DRC using the appropriate approach
|
||||
drc_results = None
|
||||
|
||||
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.")
|
||||
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)
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
# 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)
|
||||
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."
|
||||
}
|
||||
|
||||
if drc_results["success"]:
|
||||
# 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
|
||||
|
135
kicad_mcp/utils/kicad_api_detection.py
Normal file
135
kicad_mcp/utils/kicad_api_detection.py
Normal file
@ -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"
|
@ -2,3 +2,4 @@ mcp[cli]
|
||||
httpx
|
||||
pytest
|
||||
pandas
|
||||
kicad-python
|
||||
|
Loading…
x
Reference in New Issue
Block a user