Add Design Rule Check (DRC) functionality to KiCad MCP server

This commit implements comprehensive DRC support including:
- DRC check tool integration with both pcbnew Python module and CLI fallback
- Detailed DRC reports as resources with violation categorization
- Historical tracking of DRC results with visual trend analysis
- Comparison between current and previous DRC runs
- New prompt templates for fixing violations and custom design rules
- Full documentation in drc_guide.md

The DRC system helps users track their progress over time, focusing on the
most critical design rule violations as they improve their PCB designs.
This commit is contained in:
Lama 2025-03-20 02:41:52 -04:00
parent b72918daa5
commit 100f64186d
7 changed files with 998 additions and 1 deletions

View File

@ -131,11 +131,14 @@ The KiCad MCP Server provides several capabilities:
- `kicad://projects` - List all KiCad projects - `kicad://projects` - List all KiCad projects
- `kicad://project/{path}` - Get details about a specific project - `kicad://project/{path}` - Get details about a specific project
- `kicad://schematic/{path}` - Extract information from a schematic file - `kicad://schematic/{path}` - Extract information from a schematic file
- `kicad://drc/{path}` - Get a detailed Design Rule Check report for a PCB
- `kicad://drc/history/{path}` - Get DRC history report with trend analysis
### Tools ### Tools
- Project management tools (find projects, get structure, open in KiCad) - Project management tools (find projects, get structure, open in KiCad)
- Analysis tools (validate projects, generate thumbnails) - Analysis tools (validate projects, generate thumbnails)
- Export tools (extract bill of materials) - Export tools (extract bill of materials)
- Design Rule Check tools (run DRC checks, get detailed violation reports, track DRC history and improvements over time)
### Prompts ### Prompts
- Create new component guide - Create new component guide

126
docs/drc_guide.md Normal file
View File

@ -0,0 +1,126 @@
# KiCad Design Rule Check (DRC) Guide
This guide explains how to use the Design Rule Check (DRC) features in the KiCad MCP Server.
## Overview
The Design Rule Check (DRC) functionality allows you to:
1. Run DRC checks on your KiCad PCB designs
2. Get detailed reports of violations
3. Track your progress over time as you fix issues
4. Compare current results with previous checks
## Using DRC Features
### Running a DRC Check
To run a DRC check on your KiCad project:
1. In Claude Desktop, select the KiCad MCP server from the tools dropdown
2. Use the `run_drc_check` tool with your project path:
```
Please run a DRC check on my project at /Users/username/Documents/KiCad/my_project/my_project.kicad_pro
```
The tool will:
- Analyze your PCB design for rule violations
- Generate a comprehensive report
- Save the results to your DRC history
- Compare with previous runs (if available)
### Viewing DRC Reports
There are two ways to view DRC information:
#### Current DRC Report
To view the current DRC status of your project:
```
Show me the DRC report for /Users/username/Documents/KiCad/my_project/my_project.kicad_pro
```
This will load the `kicad://drc/project_path` resource, showing:
- Total number of violations
- Categorized list of issues
- Violation details with locations
- Recommendations for fixing common issues
#### DRC History
To see how your project's DRC status has changed over time:
```
Show me the DRC history for /Users/username/Documents/KiCad/my_project/my_project.kicad_pro
```
This will load the `kicad://drc/history/project_path` resource, showing:
- A visual trend of violations over time
- Table of all DRC check runs
- Comparison between first and most recent checks
- Progress indicators
### Getting Help with DRC Issues
If you need help understanding or fixing DRC violations, use the "Fix DRC Violations" prompt template:
1. Click on the prompt templates button in Claude Desktop
2. Select "Fix DRC Violations"
3. Fill in the specifics about your DRC issues
4. Claude will provide guidance on resolving the violations
## Understanding DRC Results
### Violation Categories
Common DRC violation categories include:
| Category | Description | Common Fixes |
|----------|-------------|--------------|
| Clearance | Items are too close together | Increase spacing, reroute traces |
| Track Width | Traces are too narrow | Increase trace width, reconsider current requirements |
| Annular Ring | Via rings are too small | Increase via size, adjust PCB manufacturing settings |
| Drill Size | Holes are too small | Increase drill diameter, check manufacturing capabilities |
| Silkscreen | Silkscreen conflicts with pads | Adjust silkscreen position, resize text |
| Courtyard | Component courtyards overlap | Adjust component placement, reduce footprint sizes |
### Progress Tracking
The DRC history feature tracks your progress over time, helping you:
- Identify if you're making progress (reducing violations)
- Spot when new violations are introduced
- Focus on resolving the most common issues
- Document your design improvements
## Advanced Usage
### Custom Design Rules
If your project has specific requirements, you can use the "Custom Design Rules" prompt template to get help creating specialized rules for:
- High-voltage circuits
- High-current paths
- RF design constraints
- Specialized manufacturing requirements
### Integrating with KiCad
The DRC checks are designed to work alongside KiCad's built-in DRC tool:
1. Run the MCP server's DRC check to get an overview and track progress
2. Use KiCad's built-in DRC for interactive fixes (which highlight the exact location in the editor)
3. Re-run the MCP server's DRC to verify your fixes and update the history
## Troubleshooting
### DRC Check Fails
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
4. Try using the full absolute path to your project file

View File

@ -0,0 +1,49 @@
"""
DRC prompt templates for KiCad PCB design.
"""
from mcp.server.fastmcp import FastMCP
def register_drc_prompts(mcp: FastMCP) -> None:
"""Register DRC prompt templates with the MCP server.
Args:
mcp: The FastMCP server instance
"""
@mcp.prompt()
def fix_drc_violations() -> str:
"""Prompt for assistance with fixing DRC violations."""
return """
I'm trying to fix DRC (Design Rule Check) violations in my KiCad PCB design. I need help with:
1. Understanding what these DRC errors mean
2. Knowing how to fix each type of violation
3. Best practices for preventing DRC issues in future designs
Here are the specific DRC errors I'm seeing (please list errors from your DRC report, or use the kicad://drc/path_to_project resource to see your full DRC report):
[list your DRC errors here]
Please help me understand these errors and provide step-by-step guidance on fixing them.
"""
@mcp.prompt()
def custom_design_rules() -> str:
"""Prompt for assistance with creating custom design rules."""
return """
I want to create custom design rules for my KiCad PCB. My project has the following requirements:
1. [Describe your project's specific requirements]
2. [List any special considerations like high voltage, high current, RF, etc.]
3. [Mention any manufacturing constraints]
Please help me set up appropriate design rules for my KiCad project, including:
- Minimum trace width and clearance settings
- Via size and drill constraints
- Layer stack considerations
- Other important design rules
Explain how to configure these rules in KiCad and how to verify they're being applied correctly.
"""

View File

@ -0,0 +1,261 @@
"""
Design Rule Check (DRC) resources for KiCad PCB files.
"""
import os
import json
import tempfile
from typing import Dict, Any, List
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
# Create logger for this module
logger = Logger()
def register_drc_resources(mcp: FastMCP) -> None:
"""Register DRC resources with the MCP server.
Args:
mcp: The FastMCP server instance
"""
@mcp.resource("kicad://drc/history/{project_path}")
def get_drc_history_report(project_path: str) -> str:
"""Get a formatted DRC history report for a KiCad project.
Args:
project_path: Path to the KiCad project file (.kicad_pro)
Returns:
Markdown-formatted DRC history report
"""
logger.info(f"Generating DRC history report for project: {project_path}")
if not os.path.exists(project_path):
return f"Project not found: {project_path}"
# Get history entries
history_entries = get_drc_history(project_path)
if not history_entries:
return "# DRC History\n\nNo DRC history available for this project. Run a DRC check first."
# Format results as Markdown
project_name = os.path.basename(project_path)[:-10] # Remove .kicad_pro
report = f"# DRC History for {project_name}\n\n"
# Add trend visualization
if len(history_entries) >= 2:
report += "## Trend\n\n"
# Create a simple ASCII chart of violations over time
report += "```\n"
report += "Violations\n"
# Find min/max for scaling
max_violations = max(entry.get("total_violations", 0) for entry in history_entries)
if max_violations < 10:
max_violations = 10 # Minimum scale
# Generate chart (10 rows high)
for i in range(10, 0, -1):
threshold = (i / 10) * max_violations
report += f"{int(threshold):4d} |"
for entry in reversed(history_entries): # Oldest to newest
violations = entry.get("total_violations", 0)
if violations >= threshold:
report += "*"
else:
report += " "
report += "\n"
# Add x-axis
report += " " + "-" * len(history_entries) + "\n"
report += " "
# Add dates (shortened)
for entry in reversed(history_entries):
date = entry.get("datetime", "")
if date:
# Just show month/day
shortened = date.split(" ")[0].split("-")[-2:]
report += shortened[-2][0] # First letter of month
report += "\n```\n"
# Add history table
report += "## History Entries\n\n"
report += "| Date | Time | Violations | Categories |\n"
report += "| ---- | ---- | ---------- | ---------- |\n"
for entry in history_entries:
date_time = entry.get("datetime", "Unknown")
if " " in date_time:
date, time = date_time.split(" ")
else:
date, time = date_time, ""
violations = entry.get("total_violations", 0)
categories = entry.get("violation_categories", {})
category_count = len(categories)
report += f"| {date} | {time} | {violations} | {category_count} |\n"
# Add detailed information about the most recent run
if history_entries:
most_recent = history_entries[0]
report += "\n## Most Recent Check Details\n\n"
report += f"**Date:** {most_recent.get('datetime', 'Unknown')}\n\n"
report += f"**Total Violations:** {most_recent.get('total_violations', 0)}\n\n"
categories = most_recent.get("violation_categories", {})
if categories:
report += "**Violation Categories:**\n\n"
for category, count in categories.items():
report += f"- {category}: {count}\n"
# Add comparison with first run if available
if len(history_entries) > 1:
first_run = history_entries[-1]
first_violations = first_run.get("total_violations", 0)
current_violations = most_recent.get("total_violations", 0)
report += "\n## Progress Since First Check\n\n"
report += f"**First Check Date:** {first_run.get('datetime', 'Unknown')}\n"
report += f"**First Check Violations:** {first_violations}\n"
report += f"**Current Violations:** {current_violations}\n"
if first_violations > current_violations:
fixed = first_violations - current_violations
report += f"**Progress:** You've fixed {fixed} violations! 🎉\n"
elif first_violations < current_violations:
added = current_violations - first_violations
report += f"**Alert:** {added} new violations have been introduced since the first check.\n"
else:
report += "**Status:** The number of violations has remained the same since the first check.\n"
return report
@mcp.resource("kicad://drc/{project_path}")
def get_drc_report(project_path: str) -> str:
"""Get a formatted DRC report for a KiCad project.
Args:
project_path: Path to the KiCad project file (.kicad_pro)
Returns:
Markdown-formatted DRC report
"""
logger.info(f"Generating DRC report for project: {project_path}")
if not os.path.exists(project_path):
return f"Project not found: {project_path}"
# Get PCB file from project
files = get_project_files(project_path)
if "pcb" not in files:
return "PCB file not found in project"
pcb_file = files["pcb"]
logger.info(f"Found PCB file: {pcb_file}")
# Try to run DRC via command line
drc_results = run_drc_via_cli(pcb_file)
if not drc_results["success"]:
error_message = drc_results.get("error", "Unknown error")
return f"# DRC Check Failed\n\nError: {error_message}"
# Format results as Markdown
project_name = os.path.basename(project_path)[:-10] # Remove .kicad_pro
pcb_name = os.path.basename(pcb_file)
report = f"# Design Rule Check Report for {project_name}\n\n"
report += f"PCB file: `{pcb_name}`\n\n"
# Add summary
total_violations = drc_results.get("total_violations", 0)
report += f"## Summary\n\n"
if total_violations == 0:
report += "✅ **No DRC violations found**\n\n"
else:
report += f"❌ **{total_violations} DRC violations found**\n\n"
# Add violation categories
categories = drc_results.get("violation_categories", {})
if categories:
report += "## Violation Categories\n\n"
for category, count in categories.items():
report += f"- **{category}**: {count} violations\n"
report += "\n"
# Add detailed violations
violations = drc_results.get("violations", [])
if violations:
report += "## Detailed Violations\n\n"
# Limit to first 50 violations to keep the report manageable
displayed_violations = violations[:50]
for i, violation in enumerate(displayed_violations, 1):
message = violation.get("message", "Unknown error")
severity = violation.get("severity", "error")
# Extract location information if available
location = violation.get("location", {})
x = location.get("x", 0)
y = location.get("y", 0)
report += f"### Violation {i}\n\n"
report += f"- **Type**: {message}\n"
report += f"- **Severity**: {severity}\n"
if x != 0 or y != 0:
report += f"- **Location**: X={x:.2f}mm, Y={y:.2f}mm\n"
report += "\n"
if len(violations) > 50:
report += f"*...and {len(violations) - 50} more violations (use the `run_drc_check` tool for complete results)*\n\n"
# Add recommendations
report += "## Recommendations\n\n"
if total_violations == 0:
report += "Your PCB design passes all design rule checks. It's ready for manufacturing!\n\n"
else:
report += "To fix these violations:\n\n"
report += "1. Open your PCB in KiCad's PCB Editor\n"
report += "2. Run the DRC by clicking the 'Inspect → Design Rules Checker' menu item\n"
report += "3. Click on each error in the DRC window to locate it on the PCB\n"
report += "4. Fix the issue according to the error message\n"
report += "5. Re-run DRC to verify your fixes\n\n"
# Add common solutions for frequent error types
if categories:
most_common_error = max(categories.items(), key=lambda x: x[1])[0]
report += "### Common Solutions\n\n"
if "clearance" in most_common_error.lower():
report += "**For clearance violations:**\n"
report += "- Reroute traces to maintain minimum clearance requirements\n"
report += "- Check layer stackup and adjust clearance rules if necessary\n"
report += "- Consider adjusting trace widths\n\n"
elif "width" in most_common_error.lower():
report += "**For width violations:**\n"
report += "- Increase trace widths to meet minimum requirements\n"
report += "- Check current requirements for your traces\n\n"
elif "drill" in most_common_error.lower():
report += "**For drill violations:**\n"
report += "- Adjust hole sizes to meet manufacturing constraints\n"
report += "- Check via settings\n\n"
return report

View File

@ -6,21 +6,24 @@ from mcp.server.fastmcp import FastMCP
# Import resource handlers # Import resource handlers
from kicad_mcp.resources.projects import register_project_resources from kicad_mcp.resources.projects import register_project_resources
from kicad_mcp.resources.files import register_file_resources from kicad_mcp.resources.files import register_file_resources
from kicad_mcp.resources.drc_resources import register_drc_resources
# Import tool handlers # Import tool handlers
from kicad_mcp.tools.project_tools import register_project_tools from kicad_mcp.tools.project_tools import register_project_tools
from kicad_mcp.tools.analysis_tools import register_analysis_tools from kicad_mcp.tools.analysis_tools import register_analysis_tools
from kicad_mcp.tools.export_tools import register_export_tools from kicad_mcp.tools.export_tools import register_export_tools
from kicad_mcp.tools.drc_tools import register_drc_tools
# Import prompt handlers # Import prompt handlers
from kicad_mcp.prompts.templates import register_prompts from kicad_mcp.prompts.templates import register_prompts
from kicad_mcp.prompts.drc_prompt import register_drc_prompts
# Import utils # Import utils
from kicad_mcp.utils.logger import Logger from kicad_mcp.utils.logger import Logger
from kicad_mcp.utils.python_path import setup_kicad_python_path from kicad_mcp.utils.python_path import setup_kicad_python_path
# Create logger for this module # Create logger for this module
logger = Logger(log_dir="logs") logger = Logger()
def create_server() -> FastMCP: def create_server() -> FastMCP:
"""Create and configure the KiCad MCP server.""" """Create and configure the KiCad MCP server."""
@ -42,16 +45,19 @@ def create_server() -> FastMCP:
logger.debug("Registering resources...") logger.debug("Registering resources...")
register_project_resources(mcp) register_project_resources(mcp)
register_file_resources(mcp) register_file_resources(mcp)
register_drc_resources(mcp)
# Register tools # Register tools
logger.debug("Registering tools...") logger.debug("Registering tools...")
register_project_tools(mcp) register_project_tools(mcp)
register_analysis_tools(mcp) register_analysis_tools(mcp)
register_export_tools(mcp) register_export_tools(mcp)
register_drc_tools(mcp, kicad_modules_available)
# Register prompts # Register prompts
logger.debug("Registering prompts...") logger.debug("Registering prompts...")
register_prompts(mcp) register_prompts(mcp)
register_drc_prompts(mcp)
logger.info("Server initialization complete") logger.info("Server initialization complete")
return mcp return mcp

View File

@ -0,0 +1,370 @@
"""
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 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
# Create logger for this module
logger = Logger()
def register_drc_tools(mcp: FastMCP, kicad_modules_available: bool = False) -> 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()
def get_drc_history_tool(project_path: str) -> Dict[str, Any]:
"""Get the DRC check history for a KiCad project.
Args:
project_path: Path to the KiCad project file (.kicad_pro)
Returns:
Dictionary with DRC history entries
"""
logger.info(f"Getting DRC history for project: {project_path}")
if not os.path.exists(project_path):
logger.error(f"Project not found: {project_path}")
return {"success": False, "error": f"Project not found: {project_path}"}
# Get history entries
history_entries = get_drc_history(project_path)
# Calculate trend information
trend = None
if len(history_entries) >= 2:
first = history_entries[-1] # Oldest entry
last = history_entries[0] # Newest entry
first_violations = first.get("total_violations", 0)
last_violations = last.get("total_violations", 0)
if first_violations > last_violations:
trend = "improving"
elif first_violations < last_violations:
trend = "degrading"
else:
trend = "stable"
return {
"success": True,
"project_path": project_path,
"history_entries": history_entries,
"entry_count": len(history_entries),
"trend": trend
}
@mcp.tool()
async def run_drc_check(project_path: str, ctx: Context) -> Dict[str, Any]:
"""Run a Design Rule Check on a KiCad PCB file.
Args:
project_path: Path to the KiCad project file (.kicad_pro)
Returns:
Dictionary with DRC results and statistics
"""
logger.info(f"Running DRC check for project: {project_path}")
if not os.path.exists(project_path):
logger.error(f"Project not found: {project_path}")
return {"success": False, "error": f"Project not found: {project_path}"}
# Get PCB file from project
files = get_project_files(project_path)
if "pcb" not in files:
logger.error("PCB file not found in project")
return {"success": False, "error": "PCB file not found in project"}
pcb_file = files["pcb"]
logger.info(f"Found PCB file: {pcb_file}")
# Report progress to user
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
# 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)
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.")
# 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 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

View File

@ -0,0 +1,182 @@
"""
Utilities for tracking DRC history for KiCad projects.
This will allow users to compare DRC results over time.
"""
import os
import json
import time
from datetime import datetime
from typing import Dict, List, Any, Optional
from pathlib import Path
from kicad_mcp.utils.logger import Logger
# Create logger for this module
logger = Logger()
# Directory for storing DRC history
DRC_HISTORY_DIR = os.path.expanduser("~/.kicad_mcp/drc_history")
def ensure_history_dir() -> None:
"""Ensure the DRC history directory exists."""
os.makedirs(DRC_HISTORY_DIR, exist_ok=True)
def get_project_history_path(project_path: str) -> str:
"""Get the path to the DRC history file for a project.
Args:
project_path: Path to the KiCad project file
Returns:
Path to the project's DRC history file
"""
# Create a safe filename from the project path
project_hash = hash(project_path) & 0xffffffff # Ensure positive hash
basename = os.path.basename(project_path)
history_filename = f"{basename}_{project_hash}_drc_history.json"
return os.path.join(DRC_HISTORY_DIR, history_filename)
def save_drc_result(project_path: str, drc_result: Dict[str, Any]) -> None:
"""Save a DRC result to the project's history.
Args:
project_path: Path to the KiCad project file
drc_result: DRC result dictionary
"""
ensure_history_dir()
history_path = get_project_history_path(project_path)
# Create a history entry
timestamp = time.time()
formatted_time = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
history_entry = {
"timestamp": timestamp,
"datetime": formatted_time,
"total_violations": drc_result.get("total_violations", 0),
"violation_categories": drc_result.get("violation_categories", {})
}
# Load existing history or create new
if os.path.exists(history_path):
try:
with open(history_path, 'r') as f:
history = json.load(f)
except (json.JSONDecodeError, IOError) as e:
logger.error(f"Error loading DRC history: {str(e)}")
history = {"project_path": project_path, "entries": []}
else:
history = {"project_path": project_path, "entries": []}
# Add new entry and save
history["entries"].append(history_entry)
# Keep only the last 10 entries to avoid excessive storage
if len(history["entries"]) > 10:
history["entries"] = sorted(
history["entries"],
key=lambda x: x["timestamp"],
reverse=True
)[:10]
try:
with open(history_path, 'w') as f:
json.dump(history, f, indent=2)
logger.info(f"Saved DRC history entry to {history_path}")
except IOError as e:
logger.error(f"Error saving DRC history: {str(e)}")
def get_drc_history(project_path: str) -> List[Dict[str, Any]]:
"""Get the DRC history for a project.
Args:
project_path: Path to the KiCad project file
Returns:
List of DRC history entries, sorted by timestamp (newest first)
"""
history_path = get_project_history_path(project_path)
if not os.path.exists(history_path):
logger.info(f"No DRC history found for {project_path}")
return []
try:
with open(history_path, 'r') as f:
history = json.load(f)
# Sort entries by timestamp (newest first)
entries = sorted(
history.get("entries", []),
key=lambda x: x.get("timestamp", 0),
reverse=True
)
return entries
except (json.JSONDecodeError, IOError) as e:
logger.error(f"Error reading DRC history: {str(e)}")
return []
def compare_with_previous(project_path: str, current_result: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Compare current DRC result with the previous one.
Args:
project_path: Path to the KiCad project file
current_result: Current DRC result dictionary
Returns:
Comparison dictionary or None if no history exists
"""
history = get_drc_history(project_path)
if not history or len(history) < 2: # Need at least one previous entry
return None
previous = history[0] # Most recent entry
current_violations = current_result.get("total_violations", 0)
previous_violations = previous.get("total_violations", 0)
# Compare violation categories
current_categories = current_result.get("violation_categories", {})
previous_categories = previous.get("violation_categories", {})
# Find new categories
new_categories = {}
for category, count in current_categories.items():
if category not in previous_categories:
new_categories[category] = count
# Find resolved categories
resolved_categories = {}
for category, count in previous_categories.items():
if category not in current_categories:
resolved_categories[category] = count
# Find changed categories
changed_categories = {}
for category, count in current_categories.items():
if category in previous_categories and count != previous_categories[category]:
changed_categories[category] = {
"current": count,
"previous": previous_categories[category],
"change": count - previous_categories[category]
}
comparison = {
"current_violations": current_violations,
"previous_violations": previous_violations,
"change": current_violations - previous_violations,
"previous_datetime": previous.get("datetime", "unknown"),
"new_categories": new_categories,
"resolved_categories": resolved_categories,
"changed_categories": changed_categories
}
return comparison