""" Design Rule Check (DRC) tools for KiCad MCP server. Combines basic DRC checking via kicad-cli with advanced rule set management for different PCB technologies (standard, HDI, RF, automotive). """ import json import logging import os import tempfile from typing import Any from mckicad.server import mcp from mckicad.utils.file_utils import get_project_files from mckicad.utils.secure_subprocess import run_kicad_command logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- # Technology-specific manufacturing constraints and rule definitions # --------------------------------------------------------------------------- _MANUFACTURING_CONSTRAINTS: dict[str, dict[str, Any]] = { "standard": { "min_track_width_mm": 0.15, "min_clearance_mm": 0.15, "min_via_drill_mm": 0.3, "min_via_diameter_mm": 0.6, "min_annular_ring_mm": 0.15, "min_drill_size_mm": 0.2, "max_layer_count": 6, "min_board_thickness_mm": 0.8, "max_board_thickness_mm": 3.2, "copper_weights_oz": [0.5, 1.0, 2.0], "min_silk_width_mm": 0.15, "min_silk_clearance_mm": 0.15, "min_courtyard_clearance_mm": 0.25, }, "hdi": { "min_track_width_mm": 0.075, "min_clearance_mm": 0.075, "min_via_drill_mm": 0.1, "min_via_diameter_mm": 0.25, "min_annular_ring_mm": 0.075, "min_drill_size_mm": 0.1, "max_layer_count": 20, "min_board_thickness_mm": 0.4, "max_board_thickness_mm": 3.2, "copper_weights_oz": [0.33, 0.5, 1.0], "min_silk_width_mm": 0.1, "min_silk_clearance_mm": 0.1, "min_courtyard_clearance_mm": 0.15, "microvia_supported": True, "sequential_buildup": True, }, "rf": { "min_track_width_mm": 0.127, "min_clearance_mm": 0.2, "min_via_drill_mm": 0.25, "min_via_diameter_mm": 0.5, "min_annular_ring_mm": 0.125, "min_drill_size_mm": 0.2, "max_layer_count": 8, "min_board_thickness_mm": 0.8, "max_board_thickness_mm": 3.2, "copper_weights_oz": [0.5, 1.0], "min_silk_width_mm": 0.15, "min_silk_clearance_mm": 0.15, "min_courtyard_clearance_mm": 0.25, "controlled_impedance": True, "via_stitching_pitch_mm": 2.0, }, "automotive": { "min_track_width_mm": 0.2, "min_clearance_mm": 0.25, "min_via_drill_mm": 0.35, "min_via_diameter_mm": 0.7, "min_annular_ring_mm": 0.175, "min_drill_size_mm": 0.3, "max_layer_count": 8, "min_board_thickness_mm": 1.0, "max_board_thickness_mm": 3.2, "copper_weights_oz": [1.0, 2.0, 3.0], "min_silk_width_mm": 0.2, "min_silk_clearance_mm": 0.2, "min_courtyard_clearance_mm": 0.5, "temp_range_c": [-40, 125], "vibration_rated": True, }, } _TECHNOLOGY_RECOMMENDATIONS: dict[str, list[str]] = { "standard": [ "Maintain 0.15mm minimum track width for cost-effective manufacturing", "Use 0.15mm clearance for reliable production yields", "Consider 6-layer maximum for standard processes", ], "hdi": [ "Use microvias for high-density routing", "Maintain controlled impedance for signal integrity", "Consider sequential build-up for complex designs", ], "rf": [ "Maintain consistent dielectric properties", "Use ground via stitching for EMI control", "Control trace geometry for impedance matching", ], "automotive": [ "Design for extended temperature range operation (-40 to +125 C)", "Increase clearances for vibration resistance", "Use thermal management for high-power components", ], } _APPLICABLE_STANDARDS: dict[str, list[str]] = { "standard": ["IPC-2221", "IPC-2222"], "hdi": ["IPC-2226", "IPC-6016"], "rf": ["IPC-2221", "IPC-2141"], "automotive": ["ISO 26262", "AEC-Q100"], } def _build_rule_set(name: str, technology: str, description: str) -> dict[str, Any]: """Build a rule set from manufacturing constraints for a given technology.""" tech = technology.lower() if tech not in _MANUFACTURING_CONSTRAINTS: tech = "standard" constraints = _MANUFACTURING_CONSTRAINTS[tech] rules = [] rules.append({ "name": f"{name}_clearance", "type": "clearance", "severity": "error", "constraint": {"min_mm": constraints["min_clearance_mm"]}, "enabled": True, }) rules.append({ "name": f"{name}_track_width", "type": "track_width", "severity": "error", "constraint": {"min_mm": constraints["min_track_width_mm"]}, "enabled": True, }) rules.append({ "name": f"{name}_via_size", "type": "via_size", "severity": "error", "constraint": { "min_drill_mm": constraints["min_via_drill_mm"], "min_diameter_mm": constraints["min_via_diameter_mm"], }, "enabled": True, }) rules.append({ "name": f"{name}_annular_ring", "type": "annular_ring", "severity": "error", "constraint": {"min_mm": constraints["min_annular_ring_mm"]}, "enabled": True, }) rules.append({ "name": f"{name}_drill_size", "type": "drill_size", "severity": "warning", "constraint": {"min_mm": constraints["min_drill_size_mm"]}, "enabled": True, }) rules.append({ "name": f"{name}_silk_clearance", "type": "silk_clearance", "severity": "warning", "constraint": { "min_width_mm": constraints["min_silk_width_mm"], "min_clearance_mm": constraints["min_silk_clearance_mm"], }, "enabled": True, }) rules.append({ "name": f"{name}_courtyard", "type": "courtyard_clearance", "severity": "warning", "constraint": {"min_mm": constraints["min_courtyard_clearance_mm"]}, "enabled": True, }) return { "name": name, "technology": tech, "description": description or f"{tech.upper()} PCB rules for {name}", "rules": rules, "rule_count": len(rules), } def _rules_to_kicad_format(rule_set: dict[str, Any]) -> str: """Convert a rule set to KiCad DRC rule text format.""" lines = [ f"# KiCad DRC Rules: {rule_set['name']}", f"# Technology: {rule_set['technology']}", f"# {rule_set['description']}", "", ] for rule in rule_set["rules"]: if not rule.get("enabled", True): continue constraint = rule["constraint"] rule_type = rule["type"] lines.append(f"(rule \"{rule['name']}\"") lines.append(f" (severity {rule['severity']})") if rule_type == "clearance": lines.append(f" (constraint clearance (min {constraint['min_mm']}mm))") elif rule_type == "track_width": lines.append(f" (constraint track_width (min {constraint['min_mm']}mm))") elif rule_type == "via_size": lines.append(f" (constraint via_diameter (min {constraint['min_diameter_mm']}mm))") lines.append(f" (constraint hole_size (min {constraint['min_drill_mm']}mm))") elif rule_type == "annular_ring": lines.append(f" (constraint annular_width (min {constraint['min_mm']}mm))") elif rule_type == "drill_size": lines.append(f" (constraint hole_size (min {constraint['min_mm']}mm))") elif rule_type == "silk_clearance": lines.append(f" (constraint silk_clearance (min {constraint['min_clearance_mm']}mm))") elif rule_type == "courtyard_clearance": lines.append(f" (constraint courtyard_clearance (min {constraint['min_mm']}mm))") lines.append(")") lines.append("") return "\n".join(lines) # --------------------------------------------------------------------------- # MCP Tool definitions # --------------------------------------------------------------------------- @mcp.tool() def run_drc_check(project_path: str) -> dict[str, Any]: """Run a Design Rule Check on a KiCad PCB using kicad-cli. Locates the .kicad_pcb file for the given project, runs DRC via ``kicad-cli pcb drc``, and parses the JSON report to extract violation counts and categories. Args: project_path: Absolute path to the .kicad_pro file. Returns: Dictionary with violation count, categorised violations, and the raw violation list from KiCad. """ logger.info("Running DRC check for project: %s", project_path) if not os.path.exists(project_path): logger.warning("Project not found: %s", project_path) return {"success": False, "data": None, "error": f"Project not found: {project_path}"} # Locate the PCB file files = get_project_files(project_path) if "pcb" not in files: logger.warning("PCB file not found in project: %s", project_path) return {"success": False, "data": None, "error": "PCB file not found in project"} pcb_file = files["pcb"] logger.info("Found PCB file: %s", pcb_file) try: with tempfile.TemporaryDirectory(prefix="mckicad_drc_") as temp_dir: output_file = os.path.join(temp_dir, "drc_report.json") result = run_kicad_command( command_args=[ "pcb", "drc", "--format", "json", "--output", output_file, pcb_file, ], input_files=[pcb_file], output_files=[output_file], ) # kicad-cli may return non-zero when violations exist, so we # check for the output file rather than just the return code. if not os.path.exists(output_file): error_msg = result.stderr.strip() if result.stderr else "DRC report file not created" logger.error("DRC report not created: %s", error_msg) return {"success": False, "data": None, "error": error_msg} with open(output_file) as f: try: drc_report = json.load(f) except json.JSONDecodeError as exc: logger.error("Failed to parse DRC JSON report: %s", exc) return {"success": False, "data": None, "error": "Failed to parse DRC report JSON"} violations = drc_report.get("violations", []) violation_count = len(violations) # Categorise violations by message violation_categories: dict[str, int] = {} for v in violations: msg = v.get("message", "Unknown") violation_categories[msg] = violation_categories.get(msg, 0) + 1 logger.info("DRC completed: %d violation(s)", violation_count) return { "success": True, "data": { "pcb_file": pcb_file, "total_violations": violation_count, "violation_categories": violation_categories, "violations": violations, }, "error": None, } except Exception as e: logger.error("DRC check failed: %s", e, exc_info=True) return {"success": False, "data": None, "error": str(e)} @mcp.tool() def create_drc_rule_set( name: str, technology: str = "standard", description: str = "", ) -> dict[str, Any]: """Create a DRC rule set optimised for a specific PCB technology. Generates a collection of manufacturing rules (clearance, track width, via size, annular ring, drill size, silk, courtyard) tuned for the requested technology tier. Args: name: Human-readable name for the rule set (e.g. "MyBoard_Rules"). technology: One of "standard", "hdi", "rf", or "automotive". description: Optional free-text description. Returns: Dictionary containing the generated rules, their parameters, and the technology profile used. """ logger.info("Creating DRC rule set '%s' for technology '%s'", name, technology) tech = technology.lower() if tech not in _MANUFACTURING_CONSTRAINTS: return { "success": False, "data": None, "error": ( f"Unknown technology: {technology}. " f"Valid options: {list(_MANUFACTURING_CONSTRAINTS.keys())}" ), } try: rule_set = _build_rule_set(name, tech, description) logger.info("Created rule set '%s' with %d rules", name, rule_set["rule_count"]) return {"success": True, "data": rule_set, "error": None} except Exception as e: logger.error("Failed to create rule set '%s': %s", name, e) return {"success": False, "data": None, "error": str(e)} @mcp.tool() def export_kicad_drc_rules( rule_set_name: str = "Standard", technology: str = "standard", ) -> dict[str, Any]: """Export DRC rules in KiCad-compatible text format. Builds a rule set for the given technology and serialises it to the KiCad custom DRC rule syntax that can be pasted into a project's design rules file. Args: rule_set_name: Name to assign to the exported rule set. technology: Technology profile ("standard", "hdi", "rf", "automotive"). Returns: Dictionary containing the KiCad-format rule text and metadata. """ logger.info("Exporting KiCad DRC rules for '%s' (%s)", rule_set_name, technology) tech = technology.lower() if tech not in _MANUFACTURING_CONSTRAINTS: return { "success": False, "data": None, "error": ( f"Unknown technology: {technology}. " f"Valid options: {list(_MANUFACTURING_CONSTRAINTS.keys())}" ), } try: rule_set = _build_rule_set(rule_set_name, tech, "") kicad_text = _rules_to_kicad_format(rule_set) active_count = sum(1 for r in rule_set["rules"] if r.get("enabled", True)) return { "success": True, "data": { "rule_set_name": rule_set_name, "technology": tech, "kicad_rules": kicad_text, "rule_count": rule_set["rule_count"], "active_rules": active_count, "usage": "Copy the kicad_rules text into your project's custom DRC rules file", }, "error": None, } except Exception as e: logger.error("Failed to export DRC rules: %s", e) return {"success": False, "data": None, "error": str(e)} @mcp.tool() def get_manufacturing_constraints(technology: str = "standard") -> dict[str, Any]: """Get manufacturing constraints and design guidelines for a PCB technology. Returns the numeric manufacturing limits (minimum track width, clearance, via size, etc.) along with design recommendations and applicable industry standards for the chosen technology tier. Args: technology: Technology profile ("standard", "hdi", "rf", "automotive"). Returns: Dictionary with constraints, recommendations, and applicable standards. """ logger.info("Getting manufacturing constraints for technology: %s", technology) tech = technology.lower() if tech not in _MANUFACTURING_CONSTRAINTS: return { "success": False, "data": None, "error": ( f"Unknown technology: {technology}. " f"Valid options: {list(_MANUFACTURING_CONSTRAINTS.keys())}" ), } return { "success": True, "data": { "technology": tech, "constraints": _MANUFACTURING_CONSTRAINTS[tech], "recommendations": _TECHNOLOGY_RECOMMENDATIONS.get(tech, []), "applicable_standards": _APPLICABLE_STANDARDS.get(tech, []), }, "error": None, }