design_rules.py•17.9 kB
"""
Design rules command implementations for KiCAD interface
"""
import os
import pcbnew
import logging
from typing import Dict, Any, Optional, List, Tuple
logger = logging.getLogger('kicad_interface')
class DesignRuleCommands:
"""Handles design rule checking and configuration"""
def __init__(self, board: Optional[pcbnew.BOARD] = None):
"""Initialize with optional board instance"""
self.board = board
def set_design_rules(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Set design rules for the PCB"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
design_settings = self.board.GetDesignSettings()
# Convert mm to nanometers for KiCAD internal units
scale = 1000000 # mm to nm
# Set clearance
if "clearance" in params:
design_settings.m_MinClearance = int(params["clearance"] * scale)
# KiCAD 9.0: Use SetCustom* methods instead of SetCurrent* (which were removed)
# Track if we set any custom track/via values
custom_values_set = False
if "trackWidth" in params:
design_settings.SetCustomTrackWidth(int(params["trackWidth"] * scale))
custom_values_set = True
# Via settings
if "viaDiameter" in params:
design_settings.SetCustomViaSize(int(params["viaDiameter"] * scale))
custom_values_set = True
if "viaDrill" in params:
design_settings.SetCustomViaDrill(int(params["viaDrill"] * scale))
custom_values_set = True
# KiCAD 9.0: Activate custom track/via values so they become the current values
if custom_values_set:
design_settings.UseCustomTrackViaSize(True)
# Set micro via settings (use properties - methods removed in KiCAD 9.0)
if "microViaDiameter" in params:
design_settings.m_MicroViasMinSize = int(params["microViaDiameter"] * scale)
if "microViaDrill" in params:
design_settings.m_MicroViasMinDrill = int(params["microViaDrill"] * scale)
# Set minimum values
if "minTrackWidth" in params:
design_settings.m_TrackMinWidth = int(params["minTrackWidth"] * scale)
if "minViaDiameter" in params:
design_settings.m_ViasMinSize = int(params["minViaDiameter"] * scale)
# KiCAD 9.0: m_ViasMinDrill removed - use m_MinThroughDrill instead
if "minViaDrill" in params:
design_settings.m_MinThroughDrill = int(params["minViaDrill"] * scale)
if "minMicroViaDiameter" in params:
design_settings.m_MicroViasMinSize = int(params["minMicroViaDiameter"] * scale)
if "minMicroViaDrill" in params:
design_settings.m_MicroViasMinDrill = int(params["minMicroViaDrill"] * scale)
# KiCAD 9.0: m_MinHoleDiameter removed - use m_MinThroughDrill
if "minHoleDiameter" in params:
design_settings.m_MinThroughDrill = int(params["minHoleDiameter"] * scale)
# KiCAD 9.0: Added hole clearance settings
if "holeClearance" in params:
design_settings.m_HoleClearance = int(params["holeClearance"] * scale)
if "holeToHoleMin" in params:
design_settings.m_HoleToHoleMin = int(params["holeToHoleMin"] * scale)
# Build response with KiCAD 9.0 compatible properties
# After UseCustomTrackViaSize(True), GetCurrent* returns the custom values
response_rules = {
"clearance": design_settings.m_MinClearance / scale,
"trackWidth": design_settings.GetCurrentTrackWidth() / scale,
"viaDiameter": design_settings.GetCurrentViaSize() / scale,
"viaDrill": design_settings.GetCurrentViaDrill() / scale,
"microViaDiameter": design_settings.m_MicroViasMinSize / scale,
"microViaDrill": design_settings.m_MicroViasMinDrill / scale,
"minTrackWidth": design_settings.m_TrackMinWidth / scale,
"minViaDiameter": design_settings.m_ViasMinSize / scale,
"minThroughDrill": design_settings.m_MinThroughDrill / scale,
"minMicroViaDiameter": design_settings.m_MicroViasMinSize / scale,
"minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale,
"holeClearance": design_settings.m_HoleClearance / scale,
"holeToHoleMin": design_settings.m_HoleToHoleMin / scale,
"viasMinAnnularWidth": design_settings.m_ViasMinAnnularWidth / scale
}
return {
"success": True,
"message": "Updated design rules",
"rules": response_rules
}
except Exception as e:
logger.error(f"Error setting design rules: {str(e)}")
return {
"success": False,
"message": "Failed to set design rules",
"errorDetails": str(e)
}
def get_design_rules(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get current design rules - KiCAD 9.0 compatible"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
design_settings = self.board.GetDesignSettings()
scale = 1000000 # nm to mm
# Build rules dict with KiCAD 9.0 compatible properties
rules = {
# Core clearance and track settings
"clearance": design_settings.m_MinClearance / scale,
"trackWidth": design_settings.GetCurrentTrackWidth() / scale,
"minTrackWidth": design_settings.m_TrackMinWidth / scale,
# Via settings (current values from methods)
"viaDiameter": design_settings.GetCurrentViaSize() / scale,
"viaDrill": design_settings.GetCurrentViaDrill() / scale,
# Via minimum values
"minViaDiameter": design_settings.m_ViasMinSize / scale,
"viasMinAnnularWidth": design_settings.m_ViasMinAnnularWidth / scale,
# Micro via settings
"microViaDiameter": design_settings.m_MicroViasMinSize / scale,
"microViaDrill": design_settings.m_MicroViasMinDrill / scale,
"minMicroViaDiameter": design_settings.m_MicroViasMinSize / scale,
"minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale,
# KiCAD 9.0: Hole and drill settings (replaces removed m_ViasMinDrill and m_MinHoleDiameter)
"minThroughDrill": design_settings.m_MinThroughDrill / scale,
"holeClearance": design_settings.m_HoleClearance / scale,
"holeToHoleMin": design_settings.m_HoleToHoleMin / scale,
# Other constraints
"copperEdgeClearance": design_settings.m_CopperEdgeClearance / scale,
"silkClearance": design_settings.m_SilkClearance / scale,
}
return {
"success": True,
"rules": rules
}
except Exception as e:
logger.error(f"Error getting design rules: {str(e)}")
return {
"success": False,
"message": "Failed to get design rules",
"errorDetails": str(e)
}
def run_drc(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Run Design Rule Check using kicad-cli"""
import subprocess
import json
import tempfile
import platform
import shutil
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
report_path = params.get("reportPath")
# Get the board file path
board_file = self.board.GetFileName()
if not board_file or not os.path.exists(board_file):
return {
"success": False,
"message": "Board file not found",
"errorDetails": "Cannot run DRC without a saved board file"
}
# Find kicad-cli executable
kicad_cli = self._find_kicad_cli()
if not kicad_cli:
return {
"success": False,
"message": "kicad-cli not found",
"errorDetails": "KiCAD CLI tool not found in system. Install KiCAD 8.0+ or set PATH."
}
# Create temporary JSON output file
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp:
json_output = tmp.name
try:
# Build command
cmd = [
kicad_cli,
'pcb',
'drc',
'--format', 'json',
'--output', json_output,
'--units', 'mm',
board_file
]
logger.info(f"Running DRC command: {' '.join(cmd)}")
# Run DRC
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=600 # 10 minute timeout for large boards (21MB PCB needs time)
)
if result.returncode != 0:
logger.error(f"DRC command failed: {result.stderr}")
return {
"success": False,
"message": "DRC command failed",
"errorDetails": result.stderr
}
# Read JSON output
with open(json_output, 'r', encoding='utf-8') as f:
drc_data = json.load(f)
# Parse violations from kicad-cli output
violations = []
violation_counts = {}
severity_counts = {"error": 0, "warning": 0, "info": 0}
for violation in drc_data.get('violations', []):
vtype = violation.get("type", "unknown")
vseverity = violation.get("severity", "error")
violations.append({
"type": vtype,
"severity": vseverity,
"message": violation.get("description", ""),
"location": {
"x": violation.get("x", 0),
"y": violation.get("y", 0),
"unit": "mm"
}
})
# Count violations by type
violation_counts[vtype] = violation_counts.get(vtype, 0) + 1
# Count by severity
if vseverity in severity_counts:
severity_counts[vseverity] += 1
# Determine where to save the violations file
board_dir = os.path.dirname(board_file)
board_name = os.path.splitext(os.path.basename(board_file))[0]
violations_file = os.path.join(board_dir, f"{board_name}_drc_violations.json")
# Always save violations to JSON file (for large result sets)
with open(violations_file, 'w', encoding='utf-8') as f:
json.dump({
"board": board_file,
"timestamp": drc_data.get("date", "unknown"),
"total_violations": len(violations),
"violation_counts": violation_counts,
"severity_counts": severity_counts,
"violations": violations
}, f, indent=2)
# Save text report if requested
if report_path:
report_path = os.path.abspath(os.path.expanduser(report_path))
cmd_report = [
kicad_cli,
'pcb',
'drc',
'--format', 'report',
'--output', report_path,
'--units', 'mm',
board_file
]
subprocess.run(cmd_report, capture_output=True, timeout=600)
# Return summary only (not full violations list)
return {
"success": True,
"message": f"Found {len(violations)} DRC violations",
"summary": {
"total": len(violations),
"by_severity": severity_counts,
"by_type": violation_counts
},
"violationsFile": violations_file,
"reportPath": report_path if report_path else None
}
finally:
# Clean up temp JSON file
if os.path.exists(json_output):
os.unlink(json_output)
except subprocess.TimeoutExpired:
logger.error("DRC command timed out")
return {
"success": False,
"message": "DRC command timed out",
"errorDetails": "Command took longer than 600 seconds (10 minutes)"
}
except Exception as e:
logger.error(f"Error running DRC: {str(e)}")
return {
"success": False,
"message": "Failed to run DRC",
"errorDetails": str(e)
}
def _find_kicad_cli(self) -> Optional[str]:
"""Find kicad-cli executable"""
import platform
import shutil
# Try system PATH first
cli_name = "kicad-cli.exe" if platform.system() == "Windows" else "kicad-cli"
cli_path = shutil.which(cli_name)
if cli_path:
return cli_path
# Try common installation paths (version-specific)
if platform.system() == "Windows":
common_paths = [
r"C:\Program Files\KiCad\10.0\bin\kicad-cli.exe",
r"C:\Program Files\KiCad\9.0\bin\kicad-cli.exe",
r"C:\Program Files\KiCad\8.0\bin\kicad-cli.exe",
r"C:\Program Files (x86)\KiCad\10.0\bin\kicad-cli.exe",
r"C:\Program Files (x86)\KiCad\9.0\bin\kicad-cli.exe",
r"C:\Program Files (x86)\KiCad\8.0\bin\kicad-cli.exe",
r"C:\Program Files\KiCad\bin\kicad-cli.exe",
]
for path in common_paths:
if os.path.exists(path):
return path
elif platform.system() == "Darwin": # macOS
common_paths = [
"/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli",
"/usr/local/bin/kicad-cli",
]
for path in common_paths:
if os.path.exists(path):
return path
else: # Linux
common_paths = [
"/usr/bin/kicad-cli",
"/usr/local/bin/kicad-cli",
]
for path in common_paths:
if os.path.exists(path):
return path
return None
def get_drc_violations(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get list of DRC violations"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
severity = params.get("severity", "all")
# Get DRC markers
violations = []
for marker in self.board.GetDRCMarkers():
violation = {
"type": marker.GetErrorCode(),
"severity": "error", # KiCAD DRC markers are always errors
"message": marker.GetDescription(),
"location": {
"x": marker.GetPos().x / 1000000,
"y": marker.GetPos().y / 1000000,
"unit": "mm"
}
}
# Filter by severity if specified
if severity == "all" or severity == violation["severity"]:
violations.append(violation)
return {
"success": True,
"violations": violations
}
except Exception as e:
logger.error(f"Error getting DRC violations: {str(e)}")
return {
"success": False,
"message": "Failed to get DRC violations",
"errorDetails": str(e)
}