From 100f64186d7d38dea9aeb5c136e3f0c48d83ebab Mon Sep 17 00:00:00 2001 From: Lama Date: Thu, 20 Mar 2025 02:41:52 -0400 Subject: [PATCH] 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. --- README.md | 3 + docs/drc_guide.md | 126 ++++++++++ kicad_mcp/prompts/drc_prompt.py | 49 ++++ kicad_mcp/resources/drc_resource.py | 261 ++++++++++++++++++++ kicad_mcp/server.py | 8 +- kicad_mcp/tools/drc_tools.py | 370 ++++++++++++++++++++++++++++ kicad_mcp/utils/drc_history.py | 182 ++++++++++++++ 7 files changed, 998 insertions(+), 1 deletion(-) create mode 100644 docs/drc_guide.md create mode 100644 kicad_mcp/prompts/drc_prompt.py create mode 100644 kicad_mcp/resources/drc_resource.py create mode 100644 kicad_mcp/tools/drc_tools.py create mode 100644 kicad_mcp/utils/drc_history.py diff --git a/README.md b/README.md index 2c33455..e39db2d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/drc_guide.md b/docs/drc_guide.md new file mode 100644 index 0000000..12ad952 --- /dev/null +++ b/docs/drc_guide.md @@ -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 diff --git a/kicad_mcp/prompts/drc_prompt.py b/kicad_mcp/prompts/drc_prompt.py new file mode 100644 index 0000000..52d3570 --- /dev/null +++ b/kicad_mcp/prompts/drc_prompt.py @@ -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. + """ diff --git a/kicad_mcp/resources/drc_resource.py b/kicad_mcp/resources/drc_resource.py new file mode 100644 index 0000000..9f39cec --- /dev/null +++ b/kicad_mcp/resources/drc_resource.py @@ -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 diff --git a/kicad_mcp/server.py b/kicad_mcp/server.py index 25dbe01..bd59488 100644 --- a/kicad_mcp/server.py +++ b/kicad_mcp/server.py @@ -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 diff --git a/kicad_mcp/tools/drc_tools.py b/kicad_mcp/tools/drc_tools.py new file mode 100644 index 0000000..3361f0f --- /dev/null +++ b/kicad_mcp/tools/drc_tools.py @@ -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 diff --git a/kicad_mcp/utils/drc_history.py b/kicad_mcp/utils/drc_history.py new file mode 100644 index 0000000..fef59ce --- /dev/null +++ b/kicad_mcp/utils/drc_history.py @@ -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