kicad-mcp/kicad_mcp/utils/drc_history.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

183 lines
5.8 KiB
Python

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