@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,674 @@
|
|
|
1
|
+
"""Schematic editing tools for KiCad MCP Server."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import uuid
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ..server import mcp
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _get_date_string() -> str:
|
|
11
|
+
"""Get current date in ISO format."""
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
return datetime.now().strftime("%Y-%m-%d")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# KiCad standard library symbols mapping
|
|
18
|
+
KICAD_STANDARD_SYMBOLS = {
|
|
19
|
+
"ESP32-S3-WROOM-1": {
|
|
20
|
+
"library": "RF_Module",
|
|
21
|
+
"symbol": "ESP32-S3-WROOM-1",
|
|
22
|
+
},
|
|
23
|
+
"SSD1306": {
|
|
24
|
+
"library": "Display_Graphic",
|
|
25
|
+
"symbol": "OLED-128O064D",
|
|
26
|
+
},
|
|
27
|
+
"MPU6050": {
|
|
28
|
+
"library": "Sensor_Motion",
|
|
29
|
+
"symbol": "MPU-6050",
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Pin definitions for common symbols (fallback when library not found)
|
|
34
|
+
SYMBOL_PINS = {
|
|
35
|
+
"Device:R": [(1, "passive", ""), (2, "passive", "")],
|
|
36
|
+
"Device:LED": [(1, "passive", "K"), (2, "passive", "A")],
|
|
37
|
+
"Device:C": [(1, "passive", ""), (2, "passive", "")],
|
|
38
|
+
"RF_Module:ESP32-S3-WROOM-1": [(1, "input", "GPIO0"), (2, "input", "GPIO1")],
|
|
39
|
+
"Display_Graphic:OLED-128O064D": [(1, "input", "GND"), (2, "input", "SCL")],
|
|
40
|
+
"Sensor_Motion:MPU-6050": [(1, "input", "VDD"), (2, "input", "GND")],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_pins_for_symbol(library_name: str, symbol_name: str) -> list[tuple[int, str, str]]:
|
|
45
|
+
"""Get pin definitions for a symbol."""
|
|
46
|
+
lib_id = f"{library_name}:{symbol_name}"
|
|
47
|
+
if lib_id in SYMBOL_PINS:
|
|
48
|
+
return SYMBOL_PINS[lib_id]
|
|
49
|
+
return [(1, "passive", ""), (2, "passive", "")]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _find_symbol_library_file(library_name: str) -> Path | None:
|
|
53
|
+
"""Find the .kicad_sym file for a KiCad symbol library."""
|
|
54
|
+
from ..utils.kicad_version import get_kicad_symbol_dir
|
|
55
|
+
|
|
56
|
+
sym_dir = get_kicad_symbol_dir()
|
|
57
|
+
if sym_dir:
|
|
58
|
+
sym_path = sym_dir / f"{library_name}.kicad_sym"
|
|
59
|
+
if sym_path.exists():
|
|
60
|
+
return sym_path
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _extract_symbol_from_kicad_sym(kicad_sym_path: Path, symbol_name: str) -> str | None:
|
|
65
|
+
"""Extract a complete symbol block from a .kicad_sym library file."""
|
|
66
|
+
try:
|
|
67
|
+
content = kicad_sym_path.read_text(encoding="utf-8", errors="replace")
|
|
68
|
+
except OSError:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
# Match the top-level symbol definition (not sub-symbols like "R_0_1")
|
|
72
|
+
pattern = r'\(symbol\s+"' + re.escape(symbol_name) + r'"(?!\w)'
|
|
73
|
+
match = re.search(pattern, content)
|
|
74
|
+
if not match:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
# Extract balanced block using parenthesis counting
|
|
78
|
+
start = match.start()
|
|
79
|
+
depth = 0
|
|
80
|
+
for i in range(start, len(content)):
|
|
81
|
+
if content[i] == "(":
|
|
82
|
+
depth += 1
|
|
83
|
+
elif content[i] == ")":
|
|
84
|
+
depth -= 1
|
|
85
|
+
if depth == 0:
|
|
86
|
+
return content[start : i + 1]
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _convert_symbol_to_lib_symbols_format(
|
|
91
|
+
symbol_block: str, library_name: str, symbol_name: str
|
|
92
|
+
) -> str:
|
|
93
|
+
"""Convert a symbol block from .kicad_sym to lib_symbols format.
|
|
94
|
+
|
|
95
|
+
Key changes:
|
|
96
|
+
- Prefix symbol name with library: "R" -> "Device:R"
|
|
97
|
+
- Strip kicad_sym-specific attributes: show_name, do_not_autoplace,
|
|
98
|
+
in_pos_files, duplicate_pin_numbers_are_jumpers
|
|
99
|
+
- Keep: exclude_from_sim, in_bom, on_board, embedded_fonts
|
|
100
|
+
- Normalize indentation
|
|
101
|
+
"""
|
|
102
|
+
result = symbol_block
|
|
103
|
+
|
|
104
|
+
# Prefix the symbol name with library name
|
|
105
|
+
result = re.sub(
|
|
106
|
+
r'\(symbol\s+"' + re.escape(symbol_name) + r'"',
|
|
107
|
+
f'(symbol "{library_name}:{symbol_name}"',
|
|
108
|
+
result,
|
|
109
|
+
count=1,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Also prefix sub-symbol references (e.g. "R_0_1" stays as-is since
|
|
113
|
+
# they are nested and don't need the library prefix)
|
|
114
|
+
|
|
115
|
+
# Strip kicad_sym-specific attributes
|
|
116
|
+
result = re.sub(r"\n?\s*\(show_name\s+\w+\)", "", result)
|
|
117
|
+
result = re.sub(r"\n?\s*\(do_not_autoplace\s+\w+\)", "", result)
|
|
118
|
+
result = re.sub(r"\n?\s*\(in_pos_files\s+\w+\)", "", result)
|
|
119
|
+
result = re.sub(r"\n?\s*\(duplicate_pin_numbers_are_jumpers\s+\w+\)", "", result)
|
|
120
|
+
|
|
121
|
+
# Normalize indentation: strip leading whitespace and re-indent with tabs
|
|
122
|
+
lines = result.split("\n")
|
|
123
|
+
# Find minimum indentation (ignoring empty lines)
|
|
124
|
+
min_indent = float("inf")
|
|
125
|
+
for line in lines:
|
|
126
|
+
stripped = line.lstrip()
|
|
127
|
+
if stripped:
|
|
128
|
+
indent = len(line) - len(stripped)
|
|
129
|
+
min_indent = min(min_indent, indent)
|
|
130
|
+
|
|
131
|
+
if min_indent == float("inf"):
|
|
132
|
+
min_indent = 0
|
|
133
|
+
|
|
134
|
+
normalized = []
|
|
135
|
+
for line in lines:
|
|
136
|
+
stripped = line.strip()
|
|
137
|
+
if not stripped:
|
|
138
|
+
normalized.append("")
|
|
139
|
+
continue
|
|
140
|
+
original_indent = len(line) - len(line.lstrip())
|
|
141
|
+
# Convert to tab-based indentation (base = 2 tabs inside lib_symbols)
|
|
142
|
+
tab_depth = max(0, (original_indent - min_indent)) // 2
|
|
143
|
+
normalized.append("\t\t" + "\t" * tab_depth + stripped)
|
|
144
|
+
|
|
145
|
+
return "\n".join(normalized)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _symbol_exists_in_lib_symbols(content: str, lib_id: str) -> bool:
|
|
149
|
+
"""Check if a symbol definition already exists in the lib_symbols section."""
|
|
150
|
+
ls_match = re.search(r"\(lib_symbols\b", content)
|
|
151
|
+
if not ls_match:
|
|
152
|
+
return False
|
|
153
|
+
# Search from lib_symbols start for the symbol
|
|
154
|
+
section = content[ls_match.start() :]
|
|
155
|
+
return bool(re.search(r'\(symbol\s+"' + re.escape(lib_id) + r'"', section))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _insert_symbol_into_lib_symbols(content: str, symbol_text: str) -> str:
|
|
159
|
+
"""Insert a symbol definition into the lib_symbols section of a schematic."""
|
|
160
|
+
ls_match = re.search(r"\(lib_symbols\b", content)
|
|
161
|
+
if not ls_match:
|
|
162
|
+
# No lib_symbols section - create one before the first schematic element
|
|
163
|
+
insert_pattern = r"(\n\t\((?:symbol|wire|label|global_label|junction|text|sheet|sheet_instances)\b)"
|
|
164
|
+
m = re.search(insert_pattern, content)
|
|
165
|
+
if m:
|
|
166
|
+
insert_pos = m.start()
|
|
167
|
+
new_section = f"\n\t(lib_symbols\n{symbol_text}\n\t)"
|
|
168
|
+
return content[:insert_pos] + new_section + content[insert_pos:]
|
|
169
|
+
# Last resort: insert before final closing paren
|
|
170
|
+
stripped = content.rstrip()
|
|
171
|
+
if stripped.endswith(")"):
|
|
172
|
+
return stripped[:-1] + f"\n\t(lib_symbols\n{symbol_text}\n\t)\n)\n"
|
|
173
|
+
return content
|
|
174
|
+
|
|
175
|
+
# lib_symbols exists - find its closing paren
|
|
176
|
+
start = ls_match.start()
|
|
177
|
+
depth = 0
|
|
178
|
+
end_pos = start
|
|
179
|
+
for i in range(start, len(content)):
|
|
180
|
+
if content[i] == "(":
|
|
181
|
+
depth += 1
|
|
182
|
+
elif content[i] == ")":
|
|
183
|
+
depth -= 1
|
|
184
|
+
if depth == 0:
|
|
185
|
+
end_pos = i
|
|
186
|
+
break
|
|
187
|
+
|
|
188
|
+
# Insert before the closing paren of lib_symbols
|
|
189
|
+
return content[:end_pos] + "\n" + symbol_text + "\n" + content[end_pos:]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _extract_pins_from_symbol_block(symbol_block: str) -> list[tuple[str, str, str]]:
|
|
193
|
+
"""Extract pin numbers, types, and names from a symbol definition."""
|
|
194
|
+
pins = []
|
|
195
|
+
pin_pattern = re.compile(
|
|
196
|
+
r"\(pin\s+(\w+)\s+\w+\s+[\s\S]*?"
|
|
197
|
+
r'\(name\s+"([^"]*)"[\s\S]*?\)'
|
|
198
|
+
r'\s*\(number\s+"([^"]*)"',
|
|
199
|
+
)
|
|
200
|
+
for m in pin_pattern.finditer(symbol_block):
|
|
201
|
+
elec_type = m.group(1)
|
|
202
|
+
pin_name = m.group(2)
|
|
203
|
+
pin_number = m.group(3)
|
|
204
|
+
pins.append((pin_number, elec_type, pin_name))
|
|
205
|
+
return pins
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
# Pin anchor geometry (for label placement)
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
_PIN_GEOM_RE = re.compile(
|
|
213
|
+
r"\(pin\s+(?P<etype>\w+)\s+\w+\s+"
|
|
214
|
+
r"\(at\s+(?P<x>-?[\d.]+)\s+(?P<y>-?[\d.]+)\s+(?P<angle>-?[\d.]+)\)"
|
|
215
|
+
r"[\s\S]*?"
|
|
216
|
+
r'\(number\s+"(?P<number>[^"]*)"'
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _lib_symbols_block(content: str) -> str:
|
|
221
|
+
"""Text of the (lib_symbols ...) section, or ''."""
|
|
222
|
+
m = re.search(r"\(lib_symbols\b", content)
|
|
223
|
+
if not m:
|
|
224
|
+
return ""
|
|
225
|
+
start = m.start()
|
|
226
|
+
depth = 0
|
|
227
|
+
for i in range(start, len(content)):
|
|
228
|
+
if content[i] == "(":
|
|
229
|
+
depth += 1
|
|
230
|
+
elif content[i] == ")":
|
|
231
|
+
depth -= 1
|
|
232
|
+
if depth == 0:
|
|
233
|
+
return content[start : i + 1]
|
|
234
|
+
return ""
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _symbol_def_block(lib_block: str, lib_id: str) -> str | None:
|
|
238
|
+
m = re.search(r'\(symbol\s+"' + re.escape(lib_id) + r'"(?!\w)', lib_block)
|
|
239
|
+
if not m:
|
|
240
|
+
return None
|
|
241
|
+
start = m.start()
|
|
242
|
+
depth = 0
|
|
243
|
+
for i in range(start, len(lib_block)):
|
|
244
|
+
if lib_block[i] == "(":
|
|
245
|
+
depth += 1
|
|
246
|
+
elif lib_block[i] == ")":
|
|
247
|
+
depth -= 1
|
|
248
|
+
if depth == 0:
|
|
249
|
+
return lib_block[start : i + 1]
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _lib_pin_offset(symbol_block: str, lib_id: str, unit: int, pin: str) -> tuple[float, float] | None:
|
|
254
|
+
"""Lib-space offset of one pin, honouring the unit sub-symbol filter.
|
|
255
|
+
|
|
256
|
+
Sub-symbols are named "<lib_id>_<unit>_<convert>" (or the bare
|
|
257
|
+
"<name>_<unit>_<convert>" form); unit 0 = common to all units.
|
|
258
|
+
"""
|
|
259
|
+
base = lib_id.split(":")[-1]
|
|
260
|
+
for sub_m in re.finditer(r'\(symbol\s+"([^"]+)"', symbol_block):
|
|
261
|
+
sub_name = sub_m.group(1)
|
|
262
|
+
if sub_name == lib_id:
|
|
263
|
+
continue
|
|
264
|
+
sub_unit = None
|
|
265
|
+
for prefix in (lib_id, base):
|
|
266
|
+
mm = re.match(re.escape(prefix) + r"_(\d+)_(\d+)$", sub_name)
|
|
267
|
+
if mm:
|
|
268
|
+
sub_unit = int(mm.group(1))
|
|
269
|
+
break
|
|
270
|
+
if sub_unit is None or sub_unit not in (0, unit):
|
|
271
|
+
continue
|
|
272
|
+
# balanced span of this sub-symbol block
|
|
273
|
+
start = sub_m.start()
|
|
274
|
+
depth = 0
|
|
275
|
+
for i in range(start, len(symbol_block)):
|
|
276
|
+
if symbol_block[i] == "(":
|
|
277
|
+
depth += 1
|
|
278
|
+
elif symbol_block[i] == ")":
|
|
279
|
+
depth -= 1
|
|
280
|
+
if depth == 0:
|
|
281
|
+
break
|
|
282
|
+
for pm in _PIN_GEOM_RE.finditer(symbol_block[start : i + 1]):
|
|
283
|
+
if pm.group("number") == pin:
|
|
284
|
+
return float(pm.group("x")), float(pm.group("y"))
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _pin_anchor(content: str, reference: str, pin: str) -> tuple[float, float, int] | None:
|
|
289
|
+
"""Absolute (x, y) and outward angle of a placed component's pin.
|
|
290
|
+
|
|
291
|
+
Symbol libraries use a Y-UP axis while schematic sheets store Y growing
|
|
292
|
+
DOWNWARD, so the pin offset's Y is negated on placement; the returned
|
|
293
|
+
angle (0/90/180/270) points away from the symbol body so labels fan out.
|
|
294
|
+
"""
|
|
295
|
+
lib = _lib_symbols_block(content)
|
|
296
|
+
inst_re = re.compile(
|
|
297
|
+
r'\(symbol\s+\(lib_id\s+"(?P<lib_id>[^"]+)"\)\s+'
|
|
298
|
+
r"\(at\s+(?P<x>-?[\d.]+)\s+(?P<y>-?[\d.]+)\s+(?P<angle>-?[\d.]+)\)\s+"
|
|
299
|
+
r"\(unit\s+(?P<unit>\d+)\)"
|
|
300
|
+
)
|
|
301
|
+
body = content.replace(lib, "") if lib else content
|
|
302
|
+
for m in inst_re.finditer(body):
|
|
303
|
+
tail = body[m.start() : m.start() + 4000]
|
|
304
|
+
ref_m = re.search(r'\(property\s+"Reference"\s+"([^"]*)"', tail)
|
|
305
|
+
if not ref_m or ref_m.group(1) != reference:
|
|
306
|
+
continue
|
|
307
|
+
ix, iy, iangle, unit = (
|
|
308
|
+
float(m.group("x")),
|
|
309
|
+
float(m.group("y")),
|
|
310
|
+
float(m.group("angle")),
|
|
311
|
+
int(m.group("unit")),
|
|
312
|
+
)
|
|
313
|
+
block = _symbol_def_block(lib, m.group("lib_id"))
|
|
314
|
+
if block is None:
|
|
315
|
+
return None
|
|
316
|
+
offset = _lib_pin_offset(block, m.group("lib_id"), unit, pin)
|
|
317
|
+
if offset is None:
|
|
318
|
+
return None
|
|
319
|
+
dx, dy = offset
|
|
320
|
+
r = int(round(iangle)) % 360
|
|
321
|
+
if r == 90:
|
|
322
|
+
dx, dy = -dy, dx
|
|
323
|
+
elif r == 180:
|
|
324
|
+
dx, dy = -dx, -dy
|
|
325
|
+
elif r == 270:
|
|
326
|
+
dx, dy = dy, -dx
|
|
327
|
+
sx, sy = dx, -dy # lib Y-UP -> sheet Y-DOWN
|
|
328
|
+
if abs(sx) >= abs(sy):
|
|
329
|
+
angle = 0 if sx >= 0 else 180
|
|
330
|
+
else:
|
|
331
|
+
angle = 90 if sy < 0 else 270
|
|
332
|
+
return ix + sx, iy + sy, angle
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _append_top_level(content: str, entry: str) -> str:
|
|
337
|
+
stripped = content.rstrip()
|
|
338
|
+
if not stripped.endswith(")"):
|
|
339
|
+
return content + "\n" + entry + "\n"
|
|
340
|
+
return stripped[:-1] + entry + "\n)\n"
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
_LABEL_SHAPES = ("input", "output", "bidirectional", "tri_state", "passive")
|
|
344
|
+
|
|
345
|
+
_JUSTIFY = {0: "left", 180: "right", 90: "bottom", 270: "top"}
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _resolve_label_target(
|
|
349
|
+
file_path: str, reference: str, pin: str, x: float | None, y: float | None
|
|
350
|
+
) -> tuple[str, float, float, int] | str:
|
|
351
|
+
"""Common target resolution for label tools: exact pin anchor by default,
|
|
352
|
+
explicit coordinates when pin lookups are not applicable."""
|
|
353
|
+
content = Path(file_path).read_text(encoding="utf-8", errors="replace")
|
|
354
|
+
if x is not None and y is not None:
|
|
355
|
+
return content, x, y, 0
|
|
356
|
+
anchor = _pin_anchor(content, reference, pin)
|
|
357
|
+
if anchor is None:
|
|
358
|
+
return (
|
|
359
|
+
f"❌ Pin '{pin}' not found on component '{reference}' — place the "
|
|
360
|
+
f"component first, or pass explicit x/y coordinates."
|
|
361
|
+
)
|
|
362
|
+
return content, anchor[0], anchor[1], anchor[2]
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@mcp.tool()
|
|
366
|
+
async def add_hierarchical_label(
|
|
367
|
+
file_path: str,
|
|
368
|
+
reference: str,
|
|
369
|
+
pin: str,
|
|
370
|
+
label_name: str = "",
|
|
371
|
+
shape: str = "input",
|
|
372
|
+
x: float | None = None,
|
|
373
|
+
y: float | None = None,
|
|
374
|
+
) -> str:
|
|
375
|
+
"""Add a hierarchical label on a component pin (cross-sheet connection).
|
|
376
|
+
|
|
377
|
+
Hierarchical labels are how signals cross sheet boundaries in KiCad: a
|
|
378
|
+
label placed in a sub-sheet appears as a same-named pin on the parent
|
|
379
|
+
sheet's sheet symbol, connecting the two. Placing them by hand for
|
|
380
|
+
every cross-sheet signal is the classic tedium this automates.
|
|
381
|
+
|
|
382
|
+
The label anchors EXACTLY on the pin (computed from the embedded
|
|
383
|
+
symbol geometry incl. rotation and the library-vs-sheet Y flip), so
|
|
384
|
+
the connection is electrically valid — verify with generate_netlist.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
file_path: Path to the .kicad_sch file (the SUB-sheet for signals
|
|
388
|
+
going up to its parent).
|
|
389
|
+
reference: Component reference (e.g. 'U1').
|
|
390
|
+
pin: Pin number.
|
|
391
|
+
label_name: Label text; defaults to the reference+pin when empty.
|
|
392
|
+
shape: Electrical direction: input, output, bidirectional,
|
|
393
|
+
tri_state or passive (ERC uses this).
|
|
394
|
+
x/y: Explicit anchor coordinates (overrides reference/pin).
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
Confirmation with the exact anchor position.
|
|
398
|
+
"""
|
|
399
|
+
if shape not in _LABEL_SHAPES:
|
|
400
|
+
return f"❌ Invalid shape '{shape}'. Options: {', '.join(_LABEL_SHAPES)}"
|
|
401
|
+
try:
|
|
402
|
+
path = Path(file_path)
|
|
403
|
+
if not path.exists():
|
|
404
|
+
return f"Error: File {file_path} does not exist"
|
|
405
|
+
|
|
406
|
+
resolved = _resolve_label_target(file_path, reference, pin, x, y)
|
|
407
|
+
if isinstance(resolved, str):
|
|
408
|
+
return resolved
|
|
409
|
+
content, ax, ay, angle = resolved
|
|
410
|
+
|
|
411
|
+
name = label_name or f"{reference}.{pin}"
|
|
412
|
+
justify = _JUSTIFY.get(angle, "left")
|
|
413
|
+
entry = (
|
|
414
|
+
f'\t(hierarchical_label "{name}" (shape {shape}) (at {round(ax, 3)} {round(ay, 3)} {angle})\n'
|
|
415
|
+
f"\t\t(effects (font (size 1.27 1.27)) (justify {justify}))\n"
|
|
416
|
+
f'\t\t(uuid "{uuid.uuid4()}")\n'
|
|
417
|
+
f"\t)\n"
|
|
418
|
+
)
|
|
419
|
+
path.write_text(_append_top_level(content, entry), encoding="utf-8")
|
|
420
|
+
return (
|
|
421
|
+
f"✅ Hierarchical label '{name}' ({shape}) anchored on {reference}.{pin} "
|
|
422
|
+
f"at ({round(ax, 3)}, {round(ay, 3)}) angle {angle}°"
|
|
423
|
+
)
|
|
424
|
+
except Exception as e:
|
|
425
|
+
import traceback
|
|
426
|
+
|
|
427
|
+
return f"Error adding hierarchical label: {e}\n\n{traceback.format_exc()}"
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@mcp.tool()
|
|
431
|
+
async def add_global_label(
|
|
432
|
+
file_path: str,
|
|
433
|
+
reference: str,
|
|
434
|
+
pin: str,
|
|
435
|
+
label_name: str,
|
|
436
|
+
shape: str = "input",
|
|
437
|
+
x: float | None = None,
|
|
438
|
+
y: float | None = None,
|
|
439
|
+
) -> str:
|
|
440
|
+
"""Add a global label on a component pin (project-wide connection).
|
|
441
|
+
|
|
442
|
+
Global labels connect same-named signals anywhere in the whole
|
|
443
|
+
hierarchy regardless of sheets — useful for power-style rails and
|
|
444
|
+
cross-cutting signals where a hierarchical parent/child chain would
|
|
445
|
+
be awkward.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
file_path: Path to the .kicad_sch file.
|
|
449
|
+
reference: Component reference (e.g. 'U1').
|
|
450
|
+
pin: Pin number.
|
|
451
|
+
label_name: Global net name (required — globals are named nets).
|
|
452
|
+
shape: Electrical direction: input, output, bidirectional,
|
|
453
|
+
tri_state or passive.
|
|
454
|
+
x/y: Explicit anchor coordinates (overrides reference/pin).
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
Confirmation with the exact anchor position.
|
|
458
|
+
"""
|
|
459
|
+
if shape not in _LABEL_SHAPES:
|
|
460
|
+
return f"❌ Invalid shape '{shape}'. Options: {', '.join(_LABEL_SHAPES)}"
|
|
461
|
+
try:
|
|
462
|
+
path = Path(file_path)
|
|
463
|
+
if not path.exists():
|
|
464
|
+
return f"Error: File {file_path} does not exist"
|
|
465
|
+
|
|
466
|
+
resolved = _resolve_label_target(file_path, reference, pin, x, y)
|
|
467
|
+
if isinstance(resolved, str):
|
|
468
|
+
return resolved
|
|
469
|
+
content, ax, ay, angle = resolved
|
|
470
|
+
|
|
471
|
+
justify = _JUSTIFY.get(angle, "left")
|
|
472
|
+
entry = (
|
|
473
|
+
f'\t(global_label "{label_name}" (shape {shape}) (at {round(ax, 3)} {round(ay, 3)} {angle})\n'
|
|
474
|
+
f"\t\t(effects (font (size 1.27 1.27)) (justify {justify}))\n"
|
|
475
|
+
f'\t\t(uuid "{uuid.uuid4()}")\n'
|
|
476
|
+
f'\t\t(property "Intersheetrefs" "${{INTERSHEET_REFS}}" (at 0 0 0)\n'
|
|
477
|
+
f"\t\t\t(effects (font (size 1.27 1.27)) hide)\n"
|
|
478
|
+
f"\t\t)\n"
|
|
479
|
+
f"\t)\n"
|
|
480
|
+
)
|
|
481
|
+
path.write_text(_append_top_level(content, entry), encoding="utf-8")
|
|
482
|
+
return (
|
|
483
|
+
f"✅ Global label '{label_name}' ({shape}) anchored on {reference}.{pin} "
|
|
484
|
+
f"at ({round(ax, 3)}, {round(ay, 3)}) angle {angle}°"
|
|
485
|
+
)
|
|
486
|
+
except Exception as e:
|
|
487
|
+
import traceback
|
|
488
|
+
|
|
489
|
+
return f"Error adding global label: {e}\n\n{traceback.format_exc()}"
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
@mcp.tool()
|
|
493
|
+
async def add_component_from_library(
|
|
494
|
+
file_path: str,
|
|
495
|
+
library_name: str,
|
|
496
|
+
symbol_name: str,
|
|
497
|
+
reference: str,
|
|
498
|
+
value: str,
|
|
499
|
+
footprint: str = "",
|
|
500
|
+
x: float = 100,
|
|
501
|
+
y: float = 100,
|
|
502
|
+
unit: int = 1,
|
|
503
|
+
) -> str:
|
|
504
|
+
"""Add a component from KiCad's built-in library to the schematic."""
|
|
505
|
+
try:
|
|
506
|
+
path = Path(file_path)
|
|
507
|
+
if not path.exists():
|
|
508
|
+
return f"Error: File {file_path} does not exist"
|
|
509
|
+
|
|
510
|
+
content = path.read_text()
|
|
511
|
+
comp_uuid = str(uuid.uuid4())
|
|
512
|
+
lib_id = f"{library_name}:{symbol_name}"
|
|
513
|
+
|
|
514
|
+
# --- Insert symbol definition into lib_symbols if needed ---
|
|
515
|
+
lib_inserted = False
|
|
516
|
+
lib_file = _find_symbol_library_file(library_name)
|
|
517
|
+
if lib_file:
|
|
518
|
+
symbol_block = _extract_symbol_from_kicad_sym(lib_file, symbol_name)
|
|
519
|
+
if symbol_block:
|
|
520
|
+
if not _symbol_exists_in_lib_symbols(content, lib_id):
|
|
521
|
+
converted = _convert_symbol_to_lib_symbols_format(
|
|
522
|
+
symbol_block, library_name, symbol_name
|
|
523
|
+
)
|
|
524
|
+
content = _insert_symbol_into_lib_symbols(content, converted)
|
|
525
|
+
lib_inserted = True
|
|
526
|
+
|
|
527
|
+
# --- Determine pins ---
|
|
528
|
+
pins = []
|
|
529
|
+
if lib_file:
|
|
530
|
+
symbol_block = _extract_symbol_from_kicad_sym(lib_file, symbol_name)
|
|
531
|
+
if symbol_block:
|
|
532
|
+
pins = _extract_pins_from_symbol_block(symbol_block)
|
|
533
|
+
if not pins:
|
|
534
|
+
pins = get_pins_for_symbol(library_name, symbol_name)
|
|
535
|
+
|
|
536
|
+
pin_entries = []
|
|
537
|
+
for pin_num, _pin_type, _pin_name in pins:
|
|
538
|
+
pin_uuid = str(uuid.uuid4())
|
|
539
|
+
pin_entries.append(f' (pin "{pin_num}" (uuid "{pin_uuid}"))')
|
|
540
|
+
pins_str = "\n".join(pin_entries) if pin_entries else ""
|
|
541
|
+
|
|
542
|
+
# --- Insert component instance ---
|
|
543
|
+
# NOTE: schematic symbol-instance (property ...) blocks must NOT contain
|
|
544
|
+
# a nested (uuid ...) field in this KiCad 9 file format (generator_version
|
|
545
|
+
# "9.0", version 20250114) -- confirmed against KiCad's own
|
|
546
|
+
# template/Arduino_Mega/Arduino_Mega.kicad_sch. Adding one is silently
|
|
547
|
+
# accepted by lenient parsers (sexpdata, kicad-skip) but makes KiCad's own
|
|
548
|
+
# strict loader (kicad-cli and the GUI) refuse to open the file at all.
|
|
549
|
+
component_entry = f''' (symbol (lib_id "{lib_id}") (at {x} {y} 0) (unit {unit})
|
|
550
|
+
(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no)
|
|
551
|
+
(uuid "{comp_uuid}")
|
|
552
|
+
(property "Reference" "{reference}" (at {x} {y - 5} 0)
|
|
553
|
+
(effects (font (size 1.27 1.27)))
|
|
554
|
+
)
|
|
555
|
+
(property "Value" "{value}" (at {x} {y + 2.54} 0)
|
|
556
|
+
(effects (font (size 1.27 1.27)))
|
|
557
|
+
)
|
|
558
|
+
(property "Footprint" "{footprint}" (at {x} {y + 5.08} 0)
|
|
559
|
+
(effects (font (size 1.27 1.27)) (hide yes))
|
|
560
|
+
)
|
|
561
|
+
{pins_str}
|
|
562
|
+
)'''
|
|
563
|
+
|
|
564
|
+
# Refuse to write a symbol instance whose lib_id has no matching
|
|
565
|
+
# definition in (lib_symbols) — that yields a broken symbol in KiCad.
|
|
566
|
+
if not lib_inserted:
|
|
567
|
+
return (f"❌ Cannot add component: symbol definition for "
|
|
568
|
+
f'"{lib_id}" is unavailable (library "{library_name}" not '
|
|
569
|
+
f"found, or symbol not in it). Install the library or pick a "
|
|
570
|
+
f"symbol that exists.")
|
|
571
|
+
|
|
572
|
+
if content.rstrip().endswith(")"):
|
|
573
|
+
content = content.rstrip()
|
|
574
|
+
if content.endswith(")"):
|
|
575
|
+
content = content[:-1] + component_entry + "\n)\n"
|
|
576
|
+
else:
|
|
577
|
+
content = content + "\n" + component_entry + "\n"
|
|
578
|
+
else:
|
|
579
|
+
content = content + "\n" + component_entry + "\n"
|
|
580
|
+
|
|
581
|
+
path.write_text(content)
|
|
582
|
+
|
|
583
|
+
return f"""✅ Component added successfully!
|
|
584
|
+
|
|
585
|
+
**File:** {file_path}
|
|
586
|
+
**Library:** {library_name}
|
|
587
|
+
**Symbol:** {symbol_name}
|
|
588
|
+
**Reference:** {reference}
|
|
589
|
+
**Value:** {value}
|
|
590
|
+
**Position:** ({x}, {y})
|
|
591
|
+
**Lib ID:** {lib_id}
|
|
592
|
+
**Symbol definition:** inserted into lib_symbols
|
|
593
|
+
**Pins:** {len(pins)} pins detected
|
|
594
|
+
|
|
595
|
+
"""
|
|
596
|
+
|
|
597
|
+
except Exception as e:
|
|
598
|
+
import traceback
|
|
599
|
+
|
|
600
|
+
return f"Error adding component: {e}\n\n{traceback.format_exc()}"
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
@mcp.tool()
|
|
604
|
+
async def add_wire(
|
|
605
|
+
file_path: str,
|
|
606
|
+
points: list[tuple[float, float]],
|
|
607
|
+
) -> str:
|
|
608
|
+
"""Add a wire (connection line) to the schematic."""
|
|
609
|
+
try:
|
|
610
|
+
path = Path(file_path)
|
|
611
|
+
if not path.exists():
|
|
612
|
+
return f"Error: File {file_path} does not exist"
|
|
613
|
+
|
|
614
|
+
content = path.read_text()
|
|
615
|
+
pts_str = " ".join([f"(xy {x} {y})" for x, y in points])
|
|
616
|
+
wire_entry = f""" (wire (pts {pts_str})
|
|
617
|
+
(stroke (width 0) (type default))
|
|
618
|
+
(uuid "{uuid.uuid4()}")
|
|
619
|
+
)"""
|
|
620
|
+
|
|
621
|
+
if content.rstrip().endswith(")"):
|
|
622
|
+
content = content.rstrip()
|
|
623
|
+
if content.endswith(")"):
|
|
624
|
+
content = content[:-1] + wire_entry + "\n)\n"
|
|
625
|
+
else:
|
|
626
|
+
content = content + "\n" + wire_entry + "\n)"
|
|
627
|
+
else:
|
|
628
|
+
content = content + "\n" + wire_entry + "\n"
|
|
629
|
+
|
|
630
|
+
path.write_text(content)
|
|
631
|
+
|
|
632
|
+
return "✅ Wire added"
|
|
633
|
+
except Exception as e:
|
|
634
|
+
import traceback
|
|
635
|
+
|
|
636
|
+
return f"Error adding wire: {e}\n\n{traceback.format_exc()}"
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
@mcp.tool()
|
|
640
|
+
async def add_label(
|
|
641
|
+
file_path: str,
|
|
642
|
+
text: str,
|
|
643
|
+
x: float,
|
|
644
|
+
y: float,
|
|
645
|
+
orientation: float = 0,
|
|
646
|
+
) -> str:
|
|
647
|
+
"""Add a local label (text label) to the schematic."""
|
|
648
|
+
try:
|
|
649
|
+
path = Path(file_path)
|
|
650
|
+
if not path.exists():
|
|
651
|
+
return f"Error: File {file_path} does not exist"
|
|
652
|
+
|
|
653
|
+
content = path.read_text()
|
|
654
|
+
label_uuid = str(uuid.uuid4())
|
|
655
|
+
label_entry = f""" (label "{text}" (at {x} {y} {orientation})
|
|
656
|
+
(effects (font (size 1.27 1.27)) (justify left))
|
|
657
|
+
(uuid "{label_uuid}")
|
|
658
|
+
)"""
|
|
659
|
+
|
|
660
|
+
if content.rstrip().endswith(")"):
|
|
661
|
+
content = content.rstrip()
|
|
662
|
+
if content.endswith(")"):
|
|
663
|
+
content = content[:-1] + label_entry + "\n)\n"
|
|
664
|
+
else:
|
|
665
|
+
content = content + "\n" + label_entry + "\n)"
|
|
666
|
+
else:
|
|
667
|
+
content = content + "\n" + label_entry + "\n"
|
|
668
|
+
|
|
669
|
+
path.write_text(content)
|
|
670
|
+
return f"✅ Label '{text}' added at ({x}, {y})"
|
|
671
|
+
except Exception as e:
|
|
672
|
+
import traceback
|
|
673
|
+
|
|
674
|
+
return f"Error adding label: {e}\n\n{traceback.format_exc()}"
|