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:
parent
b72918daa5
commit
100f64186d
@ -131,11 +131,14 @@ The KiCad MCP Server provides several capabilities:
|
||||
- `kicad://projects` - List all KiCad projects
|
||||
- `kicad://project/{path}` - Get details about a specific project
|
||||
- `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
|
||||
- Project management tools (find projects, get structure, open in KiCad)
|
||||
- Analysis tools (validate projects, generate thumbnails)
|
||||
- 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
|
||||
- Create new component guide
|
||||
|
126
docs/drc_guide.md
Normal file
126
docs/drc_guide.md
Normal 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
|
49
kicad_mcp/prompts/drc_prompt.py
Normal file
49
kicad_mcp/prompts/drc_prompt.py
Normal 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.
|
||||
"""
|
261
kicad_mcp/resources/drc_resource.py
Normal file
261
kicad_mcp/resources/drc_resource.py
Normal 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
|
@ -6,21 +6,24 @@ from mcp.server.fastmcp import FastMCP
|
||||
# Import resource handlers
|
||||
from kicad_mcp.resources.projects import register_project_resources
|
||||
from kicad_mcp.resources.files import register_file_resources
|
||||
from kicad_mcp.resources.drc_resources import register_drc_resources
|
||||
|
||||
# Import tool handlers
|
||||
from kicad_mcp.tools.project_tools import register_project_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.drc_tools import register_drc_tools
|
||||
|
||||
# Import prompt handlers
|
||||
from kicad_mcp.prompts.templates import register_prompts
|
||||
from kicad_mcp.prompts.drc_prompt import register_drc_prompts
|
||||
|
||||
# Import utils
|
||||
from kicad_mcp.utils.logger import Logger
|
||||
from kicad_mcp.utils.python_path import setup_kicad_python_path
|
||||
|
||||
# Create logger for this module
|
||||
logger = Logger(log_dir="logs")
|
||||
logger = Logger()
|
||||
|
||||
def create_server() -> FastMCP:
|
||||
"""Create and configure the KiCad MCP server."""
|
||||
@ -42,16 +45,19 @@ def create_server() -> FastMCP:
|
||||
logger.debug("Registering resources...")
|
||||
register_project_resources(mcp)
|
||||
register_file_resources(mcp)
|
||||
register_drc_resources(mcp)
|
||||
|
||||
# Register tools
|
||||
logger.debug("Registering tools...")
|
||||
register_project_tools(mcp)
|
||||
register_analysis_tools(mcp)
|
||||
register_export_tools(mcp)
|
||||
register_drc_tools(mcp, kicad_modules_available)
|
||||
|
||||
# Register prompts
|
||||
logger.debug("Registering prompts...")
|
||||
register_prompts(mcp)
|
||||
register_drc_prompts(mcp)
|
||||
|
||||
logger.info("Server initialization complete")
|
||||
return mcp
|
||||
|
370
kicad_mcp/tools/drc_tools.py
Normal file
370
kicad_mcp/tools/drc_tools.py
Normal 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
|
182
kicad_mcp/utils/drc_history.py
Normal file
182
kicad_mcp/utils/drc_history.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user