From 23946a211a2117af5fd374ef9bd8ba3cbeffa703 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 6 Mar 2026 12:32:35 +0100 Subject: [PATCH] feat: add run_erc tool for schematic electrical rules checking --- python/kicad_interface.py | 83 ++++++++++++++++++++++++++++++++++ python/schemas/tool_schemas.py | 15 ++++++ src/tools/schematic.ts | 35 ++++++++++++++ 3 files changed, 133 insertions(+) diff --git a/python/kicad_interface.py b/python/kicad_interface.py index 0947d8f..6fc333e 100644 --- a/python/kicad_interface.py +++ b/python/kicad_interface.py @@ -378,6 +378,7 @@ class KiCADInterface: "connect_passthrough": self._handle_connect_passthrough, "get_schematic_pin_locations": self._handle_get_schematic_pin_locations, "get_net_connections": self._handle_get_net_connections, + "run_erc": self._handle_run_erc, "generate_netlist": self._handle_generate_netlist, "list_schematic_libraries": self._handle_list_schematic_libraries, "export_schematic_pdf": self._handle_export_schematic_pdf, @@ -1367,6 +1368,88 @@ class KiCADInterface: logger.error(f"Error getting net connections: {str(e)}") return {"success": False, "message": str(e)} + def _handle_run_erc(self, params): + """Run Electrical Rules Check on a schematic via kicad-cli""" + logger.info("Running ERC on schematic") + import subprocess + import tempfile + import os + + try: + schematic_path = params.get("schematicPath") + if not schematic_path or not os.path.exists(schematic_path): + return { + "success": False, + "message": "Schematic file not found", + "errorDetails": f"Path does not exist: {schematic_path}", + } + + kicad_cli = self.design_rule_commands._find_kicad_cli() + if not kicad_cli: + return { + "success": False, + "message": "kicad-cli not found", + "errorDetails": "Install KiCAD 8.0+ or add kicad-cli to PATH.", + } + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp: + json_output = tmp.name + + try: + cmd = [kicad_cli, "sch", "erc", "--format", "json", "--output", json_output, schematic_path] + logger.info(f"Running ERC command: {' '.join(cmd)}") + + result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) + + if result.returncode != 0: + logger.error(f"ERC command failed: {result.stderr}") + return { + "success": False, + "message": "ERC command failed", + "errorDetails": result.stderr, + } + + with open(json_output, "r", encoding="utf-8") as f: + erc_data = json.load(f) + + violations = [] + severity_counts = {"error": 0, "warning": 0, "info": 0} + + for v in erc_data.get("violations", []): + vseverity = v.get("severity", "error") + items = v.get("items", []) + loc = {} + if items and "pos" in items[0]: + loc = {"x": items[0]["pos"].get("x", 0), "y": items[0]["pos"].get("y", 0)} + violations.append({ + "type": v.get("type", "unknown"), + "severity": vseverity, + "message": v.get("description", ""), + "location": loc, + }) + if vseverity in severity_counts: + severity_counts[vseverity] += 1 + + return { + "success": True, + "message": f"ERC complete: {len(violations)} violation(s)", + "summary": { + "total": len(violations), + "by_severity": severity_counts, + }, + "violations": violations, + } + + finally: + if os.path.exists(json_output): + os.unlink(json_output) + + except subprocess.TimeoutExpired: + return {"success": False, "message": "ERC timed out after 120 seconds"} + except Exception as e: + logger.error(f"Error running ERC: {str(e)}") + return {"success": False, "message": str(e)} + def _handle_generate_netlist(self, params): """Generate netlist from schematic""" logger.info("Generating netlist from schematic") diff --git a/python/schemas/tool_schemas.py b/python/schemas/tool_schemas.py index 660e31b..5b90a83 100644 --- a/python/schemas/tool_schemas.py +++ b/python/schemas/tool_schemas.py @@ -1513,6 +1513,21 @@ SCHEMATIC_TOOLS = [ "required": ["schematicPath", "sourceRef", "targetRef"] } }, + { + "name": "run_erc", + "title": "Run Electrical Rules Check (ERC)", + "description": "Runs the KiCAD Electrical Rules Check (ERC) on a schematic via kicad-cli and returns all violations with type, severity, and location. Use this to verify the schematic is electrically correct before generating a netlist or exporting.", + "inputSchema": { + "type": "object", + "properties": { + "schematicPath": { + "type": "string", + "description": "Path to the .kicad_sch schematic file" + } + }, + "required": ["schematicPath"] + } + }, { "name": "generate_netlist",, "title": "Generate Netlist", diff --git a/src/tools/schematic.ts b/src/tools/schematic.ts index 65a6822..d81a5fd 100644 --- a/src/tools/schematic.ts +++ b/src/tools/schematic.ts @@ -445,6 +445,41 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp }, ); + // Run Electrical Rules Check (ERC) + server.tool( + "run_erc", + "Runs the KiCAD Electrical Rules Check (ERC) on a schematic and returns all violations. Use after wiring to verify the schematic before generating a netlist.", + { + schematicPath: z.string().describe("Path to the .kicad_sch schematic file"), + }, + async (args: { schematicPath: string }) => { + const result = await callKicadScript("run_erc", args); + if (result.success) { + const violations: any[] = result.violations || []; + const lines: string[] = [`ERC result: ${violations.length} violation(s)`]; + if (result.summary?.by_severity) { + const s = result.summary.by_severity; + lines.push(` Errors: ${s.error ?? 0} Warnings: ${s.warning ?? 0} Info: ${s.info ?? 0}`); + } + if (violations.length > 0) { + lines.push(""); + violations.slice(0, 30).forEach((v: any, i: number) => { + const loc = v.location && (v.location.x !== undefined) ? ` @ (${v.location.x}, ${v.location.y})` : ""; + lines.push(`${i + 1}. [${v.severity}] ${v.message}${loc}`); + }); + if (violations.length > 30) { + lines.push(`... and ${violations.length - 30} more`); + } + } + return { content: [{ type: "text", text: lines.join("\n") }] }; + } else { + return { + content: [{ type: "text", text: `ERC failed: ${result.message || "Unknown error"}${result.errorDetails ? "\n" + result.errorDetails : ""}` }], + }; + } + }, + ); + // Generate netlist server.tool( "generate_netlist",