kicad-mcp/kicad_mcp/resources/drc_resource.py
Lama 100f64186d 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.
2025-03-20 02:41:52 -04:00

262 lines
11 KiB
Python

"""
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