
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.
183 lines
5.8 KiB
Python
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
|