@hybridlabor-api/bdb-hardware-pcb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -0
- package/config/mcp/antigravity.json +22 -0
- package/config/mcp/claude.json +22 -0
- package/config/mcp/codex.toml +25 -0
- package/docs/adr/ADR-001-KICAD-OPENSCAD-MCP-STRATEGY.md +334 -0
- package/docs/review_documentation.md +121 -0
- package/installer.js +358 -0
- package/mcp_servers/kicad-mcp-server/.env.example +22 -0
- package/mcp_servers/kicad-mcp-server/.github/workflows/ci.yml +36 -0
- package/mcp_servers/kicad-mcp-server/CLAUDE.md +487 -0
- package/mcp_servers/kicad-mcp-server/README.md +316 -0
- package/mcp_servers/kicad-mcp-server/docs/DEVICE_TREE.md +416 -0
- package/mcp_servers/kicad-mcp-server/docs/INSTALLATION.md +332 -0
- package/mcp_servers/kicad-mcp-server/docs/PIN_ANALYSIS.md +332 -0
- package/mcp_servers/kicad-mcp-server/docs/README.md +240 -0
- package/mcp_servers/kicad-mcp-server/docs/TESTING.md +613 -0
- package/mcp_servers/kicad-mcp-server/docs/VALIDATION.md +268 -0
- package/mcp_servers/kicad-mcp-server/pyproject.toml +96 -0
- package/mcp_servers/kicad-mcp-server/requirements-dev.txt +16 -0
- package/mcp_servers/kicad-mcp-server/requirements-test.txt +24 -0
- package/mcp_servers/kicad-mcp-server/requirements.txt +13 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__init__.py +3 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__main__.py +17 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/config.py +46 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/types.py +87 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/netlist_parser.py +234 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser.py +375 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser_kicad.py +327 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/schematic_parser.py +902 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/server.py +71 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/arduino/connectivity_test.cpp.j2 +189 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/atmega.dts.j2 +77 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/esp32.dts.j2 +77 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/nrf52.dts.j2 +77 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/stm32f4.dts.j2 +89 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/esp_idf/test_suite.c.j2 +340 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/pytest/test_connectivity.py.j2 +147 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/st_hal/hal_test.c.j2 +313 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_gpio_test.py.j2 +99 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_i2c_test.py.j2 +117 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_pinmux_test.py.j2 +43 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_spi_test.py.j2 +94 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_gpio_test.c.j2 +113 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_i2c_test.c.j2 +101 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_spi_test.c.j2 +94 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/unittest/test_schematic.py.j2 +172 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/__init__.py +36 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/device_tree.py +1187 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/hierarchical_analysis.py +211 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/netlist.py +320 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/parts_registry.py +142 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb.py +955 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb_layout.py +308 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pin_analysis.py +765 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/project.py +196 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic.py +319 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_editor.py +674 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_search.py +158 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/validation.py +866 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/visualization.py +225 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/file_handlers.py +65 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_cli.py +103 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_version.py +103 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/parts_registry.py +197 -0
- package/mcp_servers/kicad-mcp-server/tests/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/tests/examples/ESP32S3_TEST.md +219 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/README.md +65 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/example_pcb.kicad_pcb +177 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/example_schematic.kicad_sch +145 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/child.kicad_sch +24 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/root.kicad_sch +38 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_hierarchical_labels.py +195 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_kicad_cli.py +116 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_netlist_cache_path.py +29 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic.py +189 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic_hierarchy.py +69 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_visualization.py +136 -0
- package/mcp_servers/kicad-mcp-server/uv.lock +2873 -0
- package/mcp_servers/openscad-mcp-server/.dockerignore +9 -0
- package/mcp_servers/openscad-mcp-server/.github/workflows/test.yml +40 -0
- package/mcp_servers/openscad-mcp-server/Dockerfile +29 -0
- package/mcp_servers/openscad-mcp-server/LICENSE +21 -0
- package/mcp_servers/openscad-mcp-server/README.md +154 -0
- package/mcp_servers/openscad-mcp-server/docs/audit.md +56 -0
- package/mcp_servers/openscad-mcp-server/docs/docker.md +66 -0
- package/mcp_servers/openscad-mcp-server/docs/issue-followup.md +19 -0
- package/mcp_servers/openscad-mcp-server/docs/jetson.md +17 -0
- package/mcp_servers/openscad-mcp-server/glama.json +4 -0
- package/mcp_servers/openscad-mcp-server/legacy/README.md +15 -0
- package/mcp_servers/openscad-mcp-server/legacy/README.original.md +294 -0
- package/mcp_servers/openscad-mcp-server/legacy/implementation_plan.md +100 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/download_sam2_checkpoint.py +115 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/src/ai/sam_segmentation.py +209 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/src/models/threestudio_generator.py +231 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/src/workflow/image_to_model_pipeline.py +260 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/test_sam2_segmentation.py +96 -0
- package/mcp_servers/openscad-mcp-server/legacy/requirements.txt +57 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/README.md +39 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/ai-driven-code-generation.md +122 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/export-formats.md +76 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/ai/ai_service.py.md +51 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/main.py.md +63 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/models/code_generator.py.md +63 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/nlp/parameter_extractor.py.md +63 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/ai/natural-language-processing.md +78 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/nlp/parameter-extraction.md +173 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/export-formats.md +91 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/openscad-basics.md +66 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/primitive-testing.md +79 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/ai/ai_service.py +257 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/ai/gemini_api.py +161 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/ai/venice_api.py +203 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/config.py +121 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/main.py +1456 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/main.py.new +404 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/main_remote.py +401 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/code_generator.py +321 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/cuda_mvs.py +209 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/scad_templates/basic_shapes.scad +144 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/nlp/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/nlp/parameter_extractor.py +388 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/wrapper.py +418 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/__init__.py +1 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/printer_discovery.py +471 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/connection_manager.py +537 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_client.py +435 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_server.py +787 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/error_handling.py +415 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/testing/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/testing/primitive_tester.py +203 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/testing/test_primitives.py +98 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/__init__.py +1 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/cad_exporter.py +241 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/format_validator.py +206 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_exporter.py +140 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_repair.py +91 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_validator.py +123 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/headless_renderer.py +52 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/renderer.py +177 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/web_interface.py +639 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/workflow/image_approval.py +148 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/workflow/multi_view_to_model_pipeline.py +338 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_complete_workflow.py +374 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_cuda_mvs.py +191 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_gemini_api.py +168 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_image_approval.py +192 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_image_approval_workflow.py +251 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_image_to_model_pipeline.py +145 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_model_selection.py +41 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_multi_view_pipeline.py +290 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_primitives.sh +13 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_rabbit_direct.py +71 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_remote_cuda_mvs.py +283 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_venice_example.py +69 -0
- package/mcp_servers/openscad-mcp-server/pyproject.toml +33 -0
- package/mcp_servers/openscad-mcp-server/requirements.txt +2 -0
- package/mcp_servers/openscad-mcp-server/scad/simple_cube.scad +2 -0
- package/mcp_servers/openscad-mcp-server/scripts/test_docker.py +220 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__init__.py +3 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__main__.py +3 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/engine.py +94 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/geometry.py +242 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/server.py +245 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/service.py +190 -0
- package/mcp_servers/openscad-mcp-server/tests/conftest.py +17 -0
- package/mcp_servers/openscad-mcp-server/tests/test_engine.py +37 -0
- package/mcp_servers/openscad-mcp-server/tests/test_geometry.py +106 -0
- package/mcp_servers/openscad-mcp-server/tests/test_integration.py +172 -0
- package/mcp_servers/openscad-mcp-server/tests/test_transports.py +314 -0
- package/mcp_servers/openscad-mcp-server/uv.lock +1123 -0
- package/package.json +44 -0
- package/scripts/install_mcps.sh +86 -0
- package/scripts/openscad_wrapper.sh +62 -0
- package/scripts/run_kicad_mcp.sh +25 -0
- package/scripts/run_openscad_mcp.sh +41 -0
- package/scripts/test_mcp_connection.py +626 -0
- package/scripts/test_mcp_connection.sh +168 -0
- package/skills/code-first-hardware-design/SKILL.md +260 -0
- package/skills/pcb-constraint-definition/SKILL.md +236 -0
- package/skills/pcb-layout-routing-automation/SKILL.md +153 -0
- package/skills/pcb-validation-dfm-signoff/SKILL.md +194 -0
- package/skills/schematic-datasheet-analysis/SKILL.md +202 -0
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
"""Design Rule Checking (DRC) and Electrical Rules Check (ERC) tools for KiCad MCP Server."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import tempfile
|
|
6
|
+
import xml.etree.ElementTree as ET
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ..models.types import DRCError, ERCError
|
|
10
|
+
from ..server import mcp
|
|
11
|
+
from ..utils.kicad_cli import run_kicad_cli
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _temp_dir() -> Path:
|
|
15
|
+
return Path(tempfile.gettempdir())
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _find_root_schematic(sch_path: Path) -> Path | None:
|
|
19
|
+
"""If sch_path is a sub-sheet, return the root schematic instead.
|
|
20
|
+
|
|
21
|
+
KiCad convention: root schematic has the same stem as the .kicad_pro file.
|
|
22
|
+
kicad-cli silently drops unnamed local nets when exporting from a sub-sheet,
|
|
23
|
+
so we detect this and redirect to the root schematic.
|
|
24
|
+
"""
|
|
25
|
+
pro_files = list(sch_path.parent.glob("*.kicad_pro"))
|
|
26
|
+
if not pro_files:
|
|
27
|
+
return None
|
|
28
|
+
root_sch = pro_files[0].with_suffix(".kicad_sch")
|
|
29
|
+
if root_sch.exists() and root_sch.resolve() != sch_path.resolve():
|
|
30
|
+
return root_sch
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _parse_erc_report(report_path: Path) -> list[ERCError]:
|
|
35
|
+
"""Parse ERC report file (JSON or XML format).
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
report_path: Path to ERC report file
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
List of ERC errors/warnings
|
|
42
|
+
"""
|
|
43
|
+
errors = []
|
|
44
|
+
content = report_path.read_text(encoding="utf-8", errors="replace")
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
data = json.loads(content)
|
|
48
|
+
# KiCad 9+ ERC JSON: violations are a top-level list (there is no
|
|
49
|
+
# "sheets" key — reading data["sheets"] silently returned []).
|
|
50
|
+
for v in data.get("violations", []):
|
|
51
|
+
components = []
|
|
52
|
+
for item in v.get("items", []):
|
|
53
|
+
desc = item.get("description", "")
|
|
54
|
+
# Description looks like "Pin U1 of ...": pull the reference.
|
|
55
|
+
# Don't gate on the literal English word "Symbol" — localized
|
|
56
|
+
# KiCad uses other words and the ref would be missed.
|
|
57
|
+
m = re.search(r"\b(?:Pin|Symbol)\s+([A-Za-z]+\d+)", desc)
|
|
58
|
+
if m:
|
|
59
|
+
components.append(m.group(1))
|
|
60
|
+
errors.append(
|
|
61
|
+
ERCError(
|
|
62
|
+
severity=v.get("severity", "error"),
|
|
63
|
+
type=v.get("type", "unknown"),
|
|
64
|
+
description=v.get("description", ""),
|
|
65
|
+
components=components,
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
return errors
|
|
69
|
+
except (json.JSONDecodeError, KeyError):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
# Fallback: XML format
|
|
73
|
+
try:
|
|
74
|
+
root = ET.fromstring(content)
|
|
75
|
+
for violation in root.findall(".//violation"):
|
|
76
|
+
severity = violation.get("severity", "error")
|
|
77
|
+
error_type = violation.get("type", "unknown")
|
|
78
|
+
description = violation.get("description", "")
|
|
79
|
+
|
|
80
|
+
components = []
|
|
81
|
+
for comp in violation.findall(".//component"):
|
|
82
|
+
ref = comp.get("ref", "")
|
|
83
|
+
if ref:
|
|
84
|
+
components.append(ref)
|
|
85
|
+
|
|
86
|
+
errors.append(
|
|
87
|
+
ERCError(
|
|
88
|
+
severity=severity,
|
|
89
|
+
type=error_type,
|
|
90
|
+
description=description,
|
|
91
|
+
components=components,
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
except ET.ParseError as e:
|
|
95
|
+
errors.append(
|
|
96
|
+
ERCError(
|
|
97
|
+
severity="error",
|
|
98
|
+
type="parse_error",
|
|
99
|
+
description=f"Failed to parse ERC report: {str(e)}",
|
|
100
|
+
components=[],
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return errors
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _parse_drc_report(report_path: Path) -> list[DRCError]:
|
|
108
|
+
"""Parse DRC report file (JSON or XML format).
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
report_path: Path to DRC report file
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
List of DRC errors/warnings
|
|
115
|
+
"""
|
|
116
|
+
errors = []
|
|
117
|
+
content = report_path.read_text(encoding="utf-8", errors="replace")
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
data = json.loads(content)
|
|
121
|
+
# KiCad 9+ DRC JSON: violations are a top-level list (no "sheets" key —
|
|
122
|
+
# reading data["sheets"] returned [] and made run_drc report PASS with
|
|
123
|
+
# every violation silently dropped).
|
|
124
|
+
for v in data.get("violations", []):
|
|
125
|
+
x, y = 0.0, 0.0
|
|
126
|
+
items = v.get("items", [])
|
|
127
|
+
if items:
|
|
128
|
+
pos = items[0].get("pos", {})
|
|
129
|
+
x = float(pos.get("x", 0))
|
|
130
|
+
y = float(pos.get("y", 0))
|
|
131
|
+
|
|
132
|
+
errors.append(
|
|
133
|
+
DRCError(
|
|
134
|
+
severity=v.get("severity", "error"),
|
|
135
|
+
type=v.get("type", "unknown"),
|
|
136
|
+
description=v.get("description", ""),
|
|
137
|
+
location=(x, y),
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
return errors
|
|
141
|
+
except (json.JSONDecodeError, KeyError):
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
# Fallback: XML format
|
|
145
|
+
try:
|
|
146
|
+
root = ET.fromstring(content)
|
|
147
|
+
for violation in root.findall(".//violation"):
|
|
148
|
+
severity = violation.get("severity", "error")
|
|
149
|
+
error_type = violation.get("type", "unknown")
|
|
150
|
+
description = violation.get("description", "")
|
|
151
|
+
|
|
152
|
+
location_elem = violation.find(".//location")
|
|
153
|
+
x = float(location_elem.get("x", "0")) if location_elem is not None else 0.0
|
|
154
|
+
y = float(location_elem.get("y", "0")) if location_elem is not None else 0.0
|
|
155
|
+
|
|
156
|
+
errors.append(
|
|
157
|
+
DRCError(
|
|
158
|
+
severity=severity,
|
|
159
|
+
type=error_type,
|
|
160
|
+
description=description,
|
|
161
|
+
location=(x, y),
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
except ET.ParseError as e:
|
|
165
|
+
errors.append(
|
|
166
|
+
DRCError(
|
|
167
|
+
severity="error",
|
|
168
|
+
type="parse_error",
|
|
169
|
+
description=f"Failed to parse DRC report: {str(e)}",
|
|
170
|
+
location=(0.0, 0.0),
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return errors
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _filter_erc_errors(errors: list[ERCError], severity: str = "") -> list[ERCError]:
|
|
178
|
+
"""Filter ERC errors by severity.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
errors: List of ERC errors
|
|
182
|
+
severity: Severity filter ('error', 'warning', '') for all
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Filtered list of ERC errors
|
|
186
|
+
"""
|
|
187
|
+
if not severity:
|
|
188
|
+
return errors
|
|
189
|
+
|
|
190
|
+
return [e for e in errors if e.severity == severity]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _filter_drc_errors(errors: list[DRCError], violation_type: str = "") -> list[DRCError]:
|
|
194
|
+
"""Filter DRC errors by type.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
errors: List of DRC errors
|
|
198
|
+
violation_type: Type filter (e.g., 'clearance', 'spacing', '') for all
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Filtered list of DRC errors
|
|
202
|
+
"""
|
|
203
|
+
if not violation_type:
|
|
204
|
+
return errors
|
|
205
|
+
|
|
206
|
+
return [e for e in errors if e.type == violation_type]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@mcp.tool()
|
|
210
|
+
async def run_erc(
|
|
211
|
+
schematic_path: str,
|
|
212
|
+
) -> str:
|
|
213
|
+
"""Run Electrical Rules Check (ERC) on schematic.
|
|
214
|
+
|
|
215
|
+
Checks for:
|
|
216
|
+
- Unconnected pins
|
|
217
|
+
- Power conflicts
|
|
218
|
+
- Multiple outputs on same net
|
|
219
|
+
- Pin type mismatches
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
schematic_path: Path to .kicad_sch file
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
ERC report with violations and recommendations
|
|
226
|
+
"""
|
|
227
|
+
try:
|
|
228
|
+
sch_path = Path(schematic_path)
|
|
229
|
+
if not sch_path.exists():
|
|
230
|
+
return f"❌ **Schematic file not found:** {schematic_path}"
|
|
231
|
+
|
|
232
|
+
# Detect sub-sheet and redirect to root schematic
|
|
233
|
+
subsheet_note = ""
|
|
234
|
+
root_sch = _find_root_schematic(sch_path)
|
|
235
|
+
if root_sch:
|
|
236
|
+
subsheet_note = (
|
|
237
|
+
f"\n\n⚠️ **Note:** `{sch_path.name}` is a hierarchical sub-sheet. "
|
|
238
|
+
f"Switched to root schematic `{root_sch.name}` for complete ERC check."
|
|
239
|
+
)
|
|
240
|
+
sch_path = root_sch
|
|
241
|
+
|
|
242
|
+
# ERC report output path. run_erc redirects sub-sheets to the root
|
|
243
|
+
# schematic, so the report is named after the ROOT stem — not the
|
|
244
|
+
# (possibly sub-sheet) input stem, which would never match.
|
|
245
|
+
root = _find_root_schematic(sch_path) or sch_path
|
|
246
|
+
erc_report_path = _temp_dir() / (root.stem + "_erc.rpt")
|
|
247
|
+
|
|
248
|
+
# Run ERC using kicad-cli (KiCad 7+)
|
|
249
|
+
result = await run_kicad_cli(
|
|
250
|
+
[
|
|
251
|
+
"sch",
|
|
252
|
+
"erc",
|
|
253
|
+
"--format",
|
|
254
|
+
"json",
|
|
255
|
+
"--output",
|
|
256
|
+
str(erc_report_path),
|
|
257
|
+
str(sch_path),
|
|
258
|
+
],
|
|
259
|
+
timeout=60,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
stderr_text = result.stderr.decode("utf-8", errors="replace") if result.stderr else ""
|
|
263
|
+
|
|
264
|
+
if result.returncode != 0:
|
|
265
|
+
return f"""❌ **ERC Check Failed**
|
|
266
|
+
|
|
267
|
+
**Schematic:** {schematic_path}
|
|
268
|
+
|
|
269
|
+
**Error:**
|
|
270
|
+
```
|
|
271
|
+
{stderr_text}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Possible causes:**
|
|
275
|
+
- KiCad command line tools not available
|
|
276
|
+
- Invalid schematic file
|
|
277
|
+
- Missing project file (.kicad_pro)
|
|
278
|
+
|
|
279
|
+
**Manual steps:**
|
|
280
|
+
1. Open schematic in KiCad Eeschema
|
|
281
|
+
2. Run: Inspect → Electrical Rules Checker
|
|
282
|
+
3. Export report manually{subsheet_note}"""
|
|
283
|
+
|
|
284
|
+
# Parse ERC report
|
|
285
|
+
if not erc_report_path.exists():
|
|
286
|
+
# kicad-cli returned success but produced no report — do NOT claim
|
|
287
|
+
# "all pins connected" (that was never verified).
|
|
288
|
+
return f"""⚠️ **ERC Report Not Generated**
|
|
289
|
+
|
|
290
|
+
**Schematic:** {schematic_path}
|
|
291
|
+
|
|
292
|
+
kicad-cli exited cleanly but wrote no report file, so no checks could be
|
|
293
|
+
verified. Ensure KiCad 9+ is installed and the schematic has a matching
|
|
294
|
+
.kicad_pro project file.{subsheet_note}"""
|
|
295
|
+
|
|
296
|
+
# Parse and format results
|
|
297
|
+
try:
|
|
298
|
+
errors = _parse_erc_report(erc_report_path)
|
|
299
|
+
|
|
300
|
+
if not errors:
|
|
301
|
+
return f"""✅ **ERC Check Passed**
|
|
302
|
+
|
|
303
|
+
**Schematic:** {schematic_path}
|
|
304
|
+
|
|
305
|
+
No electrical violations detected!{subsheet_note}"""
|
|
306
|
+
|
|
307
|
+
# Count by severity
|
|
308
|
+
error_count = sum(1 for e in errors if e.severity == "error")
|
|
309
|
+
warning_count = sum(1 for e in errors if e.severity == "warning")
|
|
310
|
+
|
|
311
|
+
# Format violations
|
|
312
|
+
violations_table = "| Severity | Type | Description | Components |\n"
|
|
313
|
+
violations_table += "|----------|------|-------------|------------|\n"
|
|
314
|
+
|
|
315
|
+
for error in errors[:20]: # Limit to first 20
|
|
316
|
+
severity_icon = "❌" if error.severity == "error" else "⚠️"
|
|
317
|
+
components_str = ", ".join(error.components) if error.components else "N/A"
|
|
318
|
+
violations_table += f"| {severity_icon} {error.severity} | {error.type} | {error.description[:50]} | {components_str} |\n"
|
|
319
|
+
|
|
320
|
+
if len(errors) > 20:
|
|
321
|
+
violations_table += f"\n*... and {len(errors) - 20} more violations*\n"
|
|
322
|
+
|
|
323
|
+
return f"""❌ **ERC Violations Detected**
|
|
324
|
+
|
|
325
|
+
**Schematic:** {schematic_path}
|
|
326
|
+
**Total Violations:** {len(errors)}
|
|
327
|
+
- Errors: {error_count}
|
|
328
|
+
- Warnings: {warning_count}{subsheet_note}
|
|
329
|
+
|
|
330
|
+
## Violations
|
|
331
|
+
|
|
332
|
+
{violations_table}
|
|
333
|
+
|
|
334
|
+
## Recommendations
|
|
335
|
+
|
|
336
|
+
1. **Fix all errors** before proceeding to PCB layout
|
|
337
|
+
2. Review warnings - some may be acceptable
|
|
338
|
+
3. Re-run ERC after fixes
|
|
339
|
+
4. Use `get_erc_violations()` for detailed filtering
|
|
340
|
+
|
|
341
|
+
## Next Steps
|
|
342
|
+
|
|
343
|
+
- Run `get_erc_violations()` with filters to analyze specific issues
|
|
344
|
+
- Fix violations in KiCad Eeschema
|
|
345
|
+
- Re-run `run_erc()` to verify fixes"""
|
|
346
|
+
|
|
347
|
+
except Exception as e:
|
|
348
|
+
return f"""⚠️ **ERC Report Parse Error**
|
|
349
|
+
|
|
350
|
+
**Schematic:** {schematic_path}
|
|
351
|
+
|
|
352
|
+
ERC check completed but failed to parse report.
|
|
353
|
+
|
|
354
|
+
**Error:** {str(e)}
|
|
355
|
+
|
|
356
|
+
**Report location:** {erc_report_path}
|
|
357
|
+
|
|
358
|
+
You can manually inspect the report file or re-run ERC in KiCad GUI.{subsheet_note}"""
|
|
359
|
+
|
|
360
|
+
except FileNotFoundError as e:
|
|
361
|
+
# Distinguish kicad-cli missing (raised by subprocess) from a missing
|
|
362
|
+
# schematic file (already checked at the top of the function).
|
|
363
|
+
if "kicad-cli" in str(e):
|
|
364
|
+
return f"""❌ **kicad-cli Not Found**
|
|
365
|
+
|
|
366
|
+
**Schematic:** {schematic_path}
|
|
367
|
+
|
|
368
|
+
KiCad's command-line tool (kicad-cli) could not be located on PATH or in the
|
|
369
|
+
detected KiCad install directory. Install KiCad 7+ (https://www.kicad.org/),
|
|
370
|
+
or point the KICAD_CLI environment variable at the kicad-cli executable."""
|
|
371
|
+
return f"""❌ **File Not Found**
|
|
372
|
+
|
|
373
|
+
**Schematic:** {schematic_path}
|
|
374
|
+
|
|
375
|
+
{e}
|
|
376
|
+
Please check the file path and try again."""
|
|
377
|
+
except Exception as e:
|
|
378
|
+
import traceback
|
|
379
|
+
|
|
380
|
+
return f"""❌ **Unexpected Error**
|
|
381
|
+
|
|
382
|
+
**Schematic:** {schematic_path}
|
|
383
|
+
|
|
384
|
+
**Error:** {str(e)}
|
|
385
|
+
|
|
386
|
+
**Traceback:**
|
|
387
|
+
```
|
|
388
|
+
{traceback.format_exc()}
|
|
389
|
+
```"""
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@mcp.tool()
|
|
393
|
+
async def run_drc(
|
|
394
|
+
pcb_path: str,
|
|
395
|
+
) -> str:
|
|
396
|
+
"""Run Design Rules Check (DRC) on PCB.
|
|
397
|
+
|
|
398
|
+
Checks for:
|
|
399
|
+
- Clearance violations
|
|
400
|
+
- Track spacing violations
|
|
401
|
+
- Missing connections
|
|
402
|
+
- Pad/footprint overlaps
|
|
403
|
+
- Board edge constraints
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
pcb_path: Path to .kicad_pcb file
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
DRC report with violations and locations
|
|
410
|
+
"""
|
|
411
|
+
try:
|
|
412
|
+
pcb = Path(pcb_path)
|
|
413
|
+
if not pcb.exists():
|
|
414
|
+
return f"❌ **PCB file not found:** {pcb_path}"
|
|
415
|
+
|
|
416
|
+
# DRC report output path
|
|
417
|
+
drc_report_path = _temp_dir() / (pcb.stem + "_drc.rpt")
|
|
418
|
+
|
|
419
|
+
# Run DRC using kicad-cli (KiCad 7+)
|
|
420
|
+
result = await run_kicad_cli(
|
|
421
|
+
[
|
|
422
|
+
"pcb",
|
|
423
|
+
"drc",
|
|
424
|
+
"--format",
|
|
425
|
+
"json",
|
|
426
|
+
"--output",
|
|
427
|
+
str(drc_report_path),
|
|
428
|
+
str(pcb),
|
|
429
|
+
],
|
|
430
|
+
timeout=60,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
stderr_text = result.stderr.decode("utf-8", errors="replace") if result.stderr else ""
|
|
434
|
+
|
|
435
|
+
if result.returncode != 0:
|
|
436
|
+
return f"""❌ **DRC Check Failed**
|
|
437
|
+
|
|
438
|
+
**PCB:** {pcb_path}
|
|
439
|
+
|
|
440
|
+
**Error:**
|
|
441
|
+
```
|
|
442
|
+
{stderr_text}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Possible causes:**
|
|
446
|
+
- KiCad command line tools not available
|
|
447
|
+
- Invalid PCB file
|
|
448
|
+
- Missing design rules
|
|
449
|
+
|
|
450
|
+
**Manual steps:**
|
|
451
|
+
1. Open PCB in KiCad Pcbnew
|
|
452
|
+
2. Run: Inspect → Design Rules Checker
|
|
453
|
+
3. Export report manually"""
|
|
454
|
+
|
|
455
|
+
# Parse DRC report
|
|
456
|
+
if not drc_report_path.exists():
|
|
457
|
+
# No violations found
|
|
458
|
+
return f"""✅ **DRC Check Passed**
|
|
459
|
+
|
|
460
|
+
**PCB:** {pcb_path}
|
|
461
|
+
|
|
462
|
+
No design rule violations detected!
|
|
463
|
+
|
|
464
|
+
**Checked:**
|
|
465
|
+
- ✅ All clearance requirements met
|
|
466
|
+
- ✅ No spacing violations
|
|
467
|
+
- ✅ All connections routed
|
|
468
|
+
- ✅ No pad/footprint overlaps
|
|
469
|
+
- ✅ Board edge constraints satisfied"""
|
|
470
|
+
|
|
471
|
+
# Parse and format results
|
|
472
|
+
try:
|
|
473
|
+
errors = _parse_drc_report(drc_report_path)
|
|
474
|
+
|
|
475
|
+
if not errors:
|
|
476
|
+
return f"""✅ **DRC Check Passed**
|
|
477
|
+
|
|
478
|
+
**PCB:** {pcb_path}
|
|
479
|
+
|
|
480
|
+
No design rule violations detected!"""
|
|
481
|
+
|
|
482
|
+
# Count by severity
|
|
483
|
+
error_count = sum(1 for e in errors if e.severity == "error")
|
|
484
|
+
warning_count = sum(1 for e in errors if e.severity == "warning")
|
|
485
|
+
|
|
486
|
+
# Group by type
|
|
487
|
+
type_counts = {}
|
|
488
|
+
for error in errors:
|
|
489
|
+
type_counts[error.type] = type_counts.get(error.type, 0) + 1
|
|
490
|
+
|
|
491
|
+
# Format violations
|
|
492
|
+
violations_table = "| Severity | Type | Location | Description |\n"
|
|
493
|
+
violations_table += "|----------|------|----------|-------------|\n"
|
|
494
|
+
|
|
495
|
+
for error in errors[:20]: # Limit to first 20
|
|
496
|
+
severity_icon = "❌" if error.severity == "error" else "⚠️"
|
|
497
|
+
x, y = error.location
|
|
498
|
+
violations_table += f"| {severity_icon} {error.severity} | {error.type} | ({x:.2f}, {y:.2f}) | {error.description[:40]} |\n"
|
|
499
|
+
|
|
500
|
+
if len(errors) > 20:
|
|
501
|
+
violations_table += f"\n*... and {len(errors) - 20} more violations*\n"
|
|
502
|
+
|
|
503
|
+
# Format type summary
|
|
504
|
+
type_summary = "\n## Violation Summary\n\n"
|
|
505
|
+
for vtype, count in sorted(type_counts.items()):
|
|
506
|
+
type_summary += f"- **{vtype}**: {count} violations\n"
|
|
507
|
+
|
|
508
|
+
return f"""❌ **DRC Violations Detected**
|
|
509
|
+
|
|
510
|
+
**PCB:** {pcb_path}
|
|
511
|
+
**Total Violations:** {len(errors)}
|
|
512
|
+
- Errors: {error_count}
|
|
513
|
+
- Warnings: {warning_count}
|
|
514
|
+
|
|
515
|
+
{type_summary}
|
|
516
|
+
## Violations
|
|
517
|
+
|
|
518
|
+
{violations_table}
|
|
519
|
+
|
|
520
|
+
## Recommendations
|
|
521
|
+
|
|
522
|
+
1. **Fix all errors** before manufacturing
|
|
523
|
+
2. Review warnings - some may be acceptable
|
|
524
|
+
3. Pay attention to clearance violations
|
|
525
|
+
4. Re-run DRC after fixes
|
|
526
|
+
5. Use `get_drc_violations()` for detailed filtering
|
|
527
|
+
|
|
528
|
+
## Next Steps
|
|
529
|
+
|
|
530
|
+
- Run `get_drc_violations()` with filters to analyze specific issues
|
|
531
|
+
- Fix violations in KiCad Pcbnew
|
|
532
|
+
- Re-run `run_drc()` to verify fixes"""
|
|
533
|
+
|
|
534
|
+
except Exception as e:
|
|
535
|
+
return f"""⚠️ **DRC Report Parse Error**
|
|
536
|
+
|
|
537
|
+
**PCB:** {pcb_path}
|
|
538
|
+
|
|
539
|
+
DRC check completed but failed to parse report.
|
|
540
|
+
|
|
541
|
+
**Error:** {str(e)}
|
|
542
|
+
|
|
543
|
+
**Report location:** {drc_report_path}
|
|
544
|
+
|
|
545
|
+
You can manually inspect the report file or re-run DRC in KiCad GUI."""
|
|
546
|
+
|
|
547
|
+
except FileNotFoundError:
|
|
548
|
+
return f"""❌ **File Not Found**
|
|
549
|
+
|
|
550
|
+
**PCB:** {pcb_path}
|
|
551
|
+
|
|
552
|
+
Please check the file path and try again."""
|
|
553
|
+
except Exception as e:
|
|
554
|
+
import traceback
|
|
555
|
+
|
|
556
|
+
return f"""❌ **Unexpected Error**
|
|
557
|
+
|
|
558
|
+
**PCB:** {pcb_path}
|
|
559
|
+
|
|
560
|
+
**Error:** {str(e)}
|
|
561
|
+
|
|
562
|
+
**Traceback:**
|
|
563
|
+
```
|
|
564
|
+
{traceback.format_exc()}
|
|
565
|
+
```"""
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
@mcp.tool()
|
|
569
|
+
async def get_erc_violations(
|
|
570
|
+
schematic_path: str,
|
|
571
|
+
severity: str = "",
|
|
572
|
+
) -> str:
|
|
573
|
+
"""Get filtered ERC violations from schematic.
|
|
574
|
+
|
|
575
|
+
Args:
|
|
576
|
+
schematic_path: Path to .kicad_sch file
|
|
577
|
+
severity: Filter by severity ('error', 'warning', '') for all
|
|
578
|
+
|
|
579
|
+
Returns:
|
|
580
|
+
Filtered list of ERC violations
|
|
581
|
+
"""
|
|
582
|
+
try:
|
|
583
|
+
sch_path = Path(schematic_path)
|
|
584
|
+
if not sch_path.exists():
|
|
585
|
+
return f"❌ **Schematic file not found:** {schematic_path}"
|
|
586
|
+
|
|
587
|
+
# ERC report output path. run_erc redirects sub-sheets to the root
|
|
588
|
+
# schematic, so the report is named after the ROOT stem — not the
|
|
589
|
+
# (possibly sub-sheet) input stem, which would never match.
|
|
590
|
+
root = _find_root_schematic(sch_path) or sch_path
|
|
591
|
+
erc_report_path = _temp_dir() / (root.stem + "_erc.rpt")
|
|
592
|
+
|
|
593
|
+
if not erc_report_path.exists():
|
|
594
|
+
# Run ERC first
|
|
595
|
+
initial_result = await run_erc(schematic_path)
|
|
596
|
+
if "❌" in initial_result or "⚠️" in initial_result:
|
|
597
|
+
return initial_result
|
|
598
|
+
|
|
599
|
+
# Parse ERC report
|
|
600
|
+
errors = _parse_erc_report(erc_report_path)
|
|
601
|
+
|
|
602
|
+
if not errors:
|
|
603
|
+
return f"""✅ **No ERC Violations**
|
|
604
|
+
|
|
605
|
+
**Schematic:** {schematic_path}
|
|
606
|
+
|
|
607
|
+
No electrical violations detected."""
|
|
608
|
+
|
|
609
|
+
# Apply filter
|
|
610
|
+
filtered_errors = _filter_erc_errors(errors, severity)
|
|
611
|
+
|
|
612
|
+
if not filtered_errors:
|
|
613
|
+
return f"""✅ **No Matching Violations**
|
|
614
|
+
|
|
615
|
+
**Schematic:** {schematic_path}
|
|
616
|
+
**Filter:** severity = '{severity or 'all'}'
|
|
617
|
+
|
|
618
|
+
No violations match the specified filter."""
|
|
619
|
+
|
|
620
|
+
# Format results
|
|
621
|
+
result = f"""# ERC Violations
|
|
622
|
+
|
|
623
|
+
**Schematic:** {schematic_path}
|
|
624
|
+
**Filter:** severity = '{severity or 'all'}'
|
|
625
|
+
**Count:** {len(filtered_errors)}
|
|
626
|
+
|
|
627
|
+
## Violations
|
|
628
|
+
|
|
629
|
+
| Severity | Type | Description | Components |
|
|
630
|
+
|----------|------|-------------|------------|
|
|
631
|
+
"""
|
|
632
|
+
|
|
633
|
+
for error in filtered_errors:
|
|
634
|
+
severity_icon = "❌" if error.severity == "error" else "⚠️"
|
|
635
|
+
components_str = ", ".join(error.components) if error.components else "N/A"
|
|
636
|
+
result += f"| {severity_icon} {error.severity} | {error.type} | {error.description} | {components_str} |\n"
|
|
637
|
+
|
|
638
|
+
return result
|
|
639
|
+
|
|
640
|
+
except Exception as e:
|
|
641
|
+
import traceback
|
|
642
|
+
|
|
643
|
+
return f"""❌ **Error**
|
|
644
|
+
|
|
645
|
+
**Schematic:** {schematic_path}
|
|
646
|
+
|
|
647
|
+
**Error:** {str(e)}
|
|
648
|
+
|
|
649
|
+
**Traceback:**
|
|
650
|
+
```
|
|
651
|
+
{traceback.format_exc()}
|
|
652
|
+
```"""
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
@mcp.tool()
|
|
656
|
+
async def get_drc_violations(
|
|
657
|
+
pcb_path: str,
|
|
658
|
+
violation_type: str = "",
|
|
659
|
+
) -> str:
|
|
660
|
+
"""Get filtered DRC violations from PCB.
|
|
661
|
+
|
|
662
|
+
Args:
|
|
663
|
+
pcb_path: Path to .kicad_pcb file
|
|
664
|
+
violation_type: Filter by type (e.g., 'clearance', 'spacing', '') for all
|
|
665
|
+
|
|
666
|
+
Returns:
|
|
667
|
+
Filtered list of DRC violations
|
|
668
|
+
"""
|
|
669
|
+
try:
|
|
670
|
+
pcb = Path(pcb_path)
|
|
671
|
+
if not pcb.exists():
|
|
672
|
+
return f"❌ **PCB file not found:** {pcb_path}"
|
|
673
|
+
|
|
674
|
+
# DRC report output path
|
|
675
|
+
drc_report_path = _temp_dir() / (pcb.stem + "_drc.rpt")
|
|
676
|
+
|
|
677
|
+
if not drc_report_path.exists():
|
|
678
|
+
# Run DRC first
|
|
679
|
+
initial_result = await run_drc(pcb_path)
|
|
680
|
+
if "❌" in initial_result or "⚠️" in initial_result:
|
|
681
|
+
return initial_result
|
|
682
|
+
|
|
683
|
+
# Parse DRC report
|
|
684
|
+
errors = _parse_drc_report(drc_report_path)
|
|
685
|
+
|
|
686
|
+
if not errors:
|
|
687
|
+
return f"""✅ **No DRC Violations**
|
|
688
|
+
|
|
689
|
+
**PCB:** {pcb_path}
|
|
690
|
+
|
|
691
|
+
No design rule violations detected."""
|
|
692
|
+
|
|
693
|
+
# Apply filter
|
|
694
|
+
filtered_errors = _filter_drc_errors(errors, violation_type)
|
|
695
|
+
|
|
696
|
+
if not filtered_errors:
|
|
697
|
+
return f"""✅ **No Matching Violations**
|
|
698
|
+
|
|
699
|
+
**PCB:** {pcb_path}
|
|
700
|
+
**Filter:** type = '{violation_type or 'all'}'
|
|
701
|
+
|
|
702
|
+
No violations match the specified filter."""
|
|
703
|
+
|
|
704
|
+
# Format results
|
|
705
|
+
result = f"""# DRC Violations
|
|
706
|
+
|
|
707
|
+
**PCB:** {pcb_path}
|
|
708
|
+
**Filter:** type = '{violation_type or 'all'}'
|
|
709
|
+
**Count:** {len(filtered_errors)}
|
|
710
|
+
|
|
711
|
+
## Violations
|
|
712
|
+
|
|
713
|
+
| Severity | Type | Location | Description |
|
|
714
|
+
|----------|------|----------|-------------|
|
|
715
|
+
"""
|
|
716
|
+
|
|
717
|
+
for error in filtered_errors:
|
|
718
|
+
severity_icon = "❌" if error.severity == "error" else "⚠️"
|
|
719
|
+
x, y = error.location
|
|
720
|
+
result += f"| {severity_icon} {error.severity} | {error.type} | ({x:.2f}, {y:.2f}) | {error.description} |\n"
|
|
721
|
+
|
|
722
|
+
return result
|
|
723
|
+
|
|
724
|
+
except Exception as e:
|
|
725
|
+
import traceback
|
|
726
|
+
|
|
727
|
+
return f"""❌ **Error**
|
|
728
|
+
|
|
729
|
+
**PCB:** {pcb_path}
|
|
730
|
+
|
|
731
|
+
**Error:** {str(e)}
|
|
732
|
+
|
|
733
|
+
**Traceback:**
|
|
734
|
+
```
|
|
735
|
+
{traceback.format_exc()}
|
|
736
|
+
```"""
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
@mcp.tool()
|
|
740
|
+
async def export_erc_report(
|
|
741
|
+
schematic_path: str,
|
|
742
|
+
output_path: str = "",
|
|
743
|
+
) -> str:
|
|
744
|
+
"""Export ERC report to file.
|
|
745
|
+
|
|
746
|
+
Args:
|
|
747
|
+
schematic_path: Path to .kicad_sch file
|
|
748
|
+
output_path: Optional output file path (default: schematic_name_erc.txt)
|
|
749
|
+
|
|
750
|
+
Returns:
|
|
751
|
+
Confirmation with output file location
|
|
752
|
+
"""
|
|
753
|
+
try:
|
|
754
|
+
sch_path = Path(schematic_path)
|
|
755
|
+
if not sch_path.exists():
|
|
756
|
+
return f"❌ **Schematic file not found:** {schematic_path}"
|
|
757
|
+
|
|
758
|
+
# Run ERC first to generate report
|
|
759
|
+
initial_result = await run_erc(schematic_path)
|
|
760
|
+
|
|
761
|
+
if "❌" in initial_result:
|
|
762
|
+
return f"❌ **ERC Check Failed**\n\n{initial_result}"
|
|
763
|
+
|
|
764
|
+
# ERC report path
|
|
765
|
+
erc_report_path = _temp_dir() / (sch_path.stem + "_erc.rpt")
|
|
766
|
+
|
|
767
|
+
if not erc_report_path.exists():
|
|
768
|
+
return "❌ **ERC report not found.** Run ERC check first."
|
|
769
|
+
|
|
770
|
+
# Determine output path
|
|
771
|
+
if not output_path:
|
|
772
|
+
output_path = str(sch_path.parent / (sch_path.stem + "_erc_report.txt"))
|
|
773
|
+
|
|
774
|
+
output = Path(output_path)
|
|
775
|
+
|
|
776
|
+
# Copy report to output location
|
|
777
|
+
import shutil
|
|
778
|
+
|
|
779
|
+
shutil.copy(erc_report_path, output)
|
|
780
|
+
|
|
781
|
+
return f"""✅ **ERC Report Exported**
|
|
782
|
+
|
|
783
|
+
**Schematic:** {schematic_path}
|
|
784
|
+
**Output:** {output}
|
|
785
|
+
|
|
786
|
+
The ERC report has been successfully exported to the specified location."""
|
|
787
|
+
|
|
788
|
+
except Exception as e:
|
|
789
|
+
import traceback
|
|
790
|
+
|
|
791
|
+
return f"""❌ **Export Failed**
|
|
792
|
+
|
|
793
|
+
**Schematic:** {schematic_path}
|
|
794
|
+
**Output:** {output_path}
|
|
795
|
+
|
|
796
|
+
**Error:** {str(e)}
|
|
797
|
+
|
|
798
|
+
**Traceback:**
|
|
799
|
+
```
|
|
800
|
+
{traceback.format_exc()}
|
|
801
|
+
```"""
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
@mcp.tool()
|
|
805
|
+
async def export_drc_report(
|
|
806
|
+
pcb_path: str,
|
|
807
|
+
output_path: str = "",
|
|
808
|
+
) -> str:
|
|
809
|
+
"""Export DRC report to file.
|
|
810
|
+
|
|
811
|
+
Args:
|
|
812
|
+
pcb_path: Path to .kicad_pcb file
|
|
813
|
+
output_path: Optional output file path (default: pcb_name_drc.txt)
|
|
814
|
+
|
|
815
|
+
Returns:
|
|
816
|
+
Confirmation with output file location
|
|
817
|
+
"""
|
|
818
|
+
try:
|
|
819
|
+
pcb = Path(pcb_path)
|
|
820
|
+
if not pcb.exists():
|
|
821
|
+
return f"❌ **PCB file not found:** {pcb_path}"
|
|
822
|
+
|
|
823
|
+
# Run DRC first to generate report
|
|
824
|
+
initial_result = await run_drc(pcb_path)
|
|
825
|
+
|
|
826
|
+
if "❌" in initial_result:
|
|
827
|
+
return f"❌ **DRC Check Failed**\n\n{initial_result}"
|
|
828
|
+
|
|
829
|
+
# DRC report path
|
|
830
|
+
drc_report_path = _temp_dir() / (pcb.stem + "_drc.rpt")
|
|
831
|
+
|
|
832
|
+
if not drc_report_path.exists():
|
|
833
|
+
return "❌ **DRC report not found.** Run DRC check first."
|
|
834
|
+
|
|
835
|
+
# Determine output path
|
|
836
|
+
if not output_path:
|
|
837
|
+
output_path = str(pcb.parent / (pcb.stem + "_drc_report.txt"))
|
|
838
|
+
|
|
839
|
+
output = Path(output_path)
|
|
840
|
+
|
|
841
|
+
# Copy report to output location
|
|
842
|
+
import shutil
|
|
843
|
+
|
|
844
|
+
shutil.copy(drc_report_path, output)
|
|
845
|
+
|
|
846
|
+
return f"""✅ **DRC Report Exported**
|
|
847
|
+
|
|
848
|
+
**PCB:** {pcb_path}
|
|
849
|
+
**Output:** {output}
|
|
850
|
+
|
|
851
|
+
The DRC report has been successfully exported to the specified location."""
|
|
852
|
+
|
|
853
|
+
except Exception as e:
|
|
854
|
+
import traceback
|
|
855
|
+
|
|
856
|
+
return f"""❌ **Export Failed**
|
|
857
|
+
|
|
858
|
+
**PCB:** {pcb_path}
|
|
859
|
+
**Output:** {output_path}
|
|
860
|
+
|
|
861
|
+
**Error:** {str(e)}
|
|
862
|
+
|
|
863
|
+
**Traceback:**
|
|
864
|
+
```
|
|
865
|
+
{traceback.format_exc()}
|
|
866
|
+
```"""
|