@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,902 @@
|
|
|
1
|
+
"""Schematic file parser wrapper using kicad-skip."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..utils.file_handlers import validate_kicad_file
|
|
9
|
+
|
|
10
|
+
# KiCad-defined boolean flags (fixed set, not user-extensible)
|
|
11
|
+
KICAD_SYMBOL_FLAGS = ("dnp", "in_bom", "on_board", "exclude_from_sim")
|
|
12
|
+
KICAD_FLAG_DEFAULTS = {"dnp": False, "in_bom": True, "on_board": True, "exclude_from_sim": False}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class SchematicComponent:
|
|
17
|
+
"""Component from schematic file."""
|
|
18
|
+
|
|
19
|
+
reference: str
|
|
20
|
+
value: str
|
|
21
|
+
library_id: str
|
|
22
|
+
footprint: str | None = None
|
|
23
|
+
properties: dict[str, str] = field(default_factory=dict)
|
|
24
|
+
position: tuple[float, float] = (0.0, 0.0)
|
|
25
|
+
unit: int | None = None
|
|
26
|
+
pins: list[dict[str, Any]] = field(default_factory=list)
|
|
27
|
+
flags: dict[str, bool] = field(default_factory=lambda: dict(KICAD_FLAG_DEFAULTS))
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_kicad_skip(cls, data: dict[str, Any]) -> "SchematicComponent":
|
|
31
|
+
"""Create from kicad-skip data structure."""
|
|
32
|
+
# Extract properties
|
|
33
|
+
properties = {}
|
|
34
|
+
for prop in data.get("properties", []):
|
|
35
|
+
key = prop.get("key", "")
|
|
36
|
+
value = prop.get("value", "")
|
|
37
|
+
if key and value:
|
|
38
|
+
properties[key] = value
|
|
39
|
+
|
|
40
|
+
# Get position
|
|
41
|
+
at = data.get("at", {})
|
|
42
|
+
position = (float(at.get("x", 0)), float(at.get("y", 0)))
|
|
43
|
+
|
|
44
|
+
# Get unit
|
|
45
|
+
unit = data.get("unit")
|
|
46
|
+
|
|
47
|
+
return cls(
|
|
48
|
+
reference=data.get("reference", ""),
|
|
49
|
+
value=properties.get("Value", data.get("value", "")),
|
|
50
|
+
library_id=data.get("lib_id", ""),
|
|
51
|
+
footprint=properties.get("Footprint"),
|
|
52
|
+
properties=properties,
|
|
53
|
+
position=position,
|
|
54
|
+
unit=unit,
|
|
55
|
+
pins=data.get("pins", []),
|
|
56
|
+
flags=data.get("flags", dict(KICAD_FLAG_DEFAULTS)),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class SchematicNet:
|
|
62
|
+
"""Net from schematic file."""
|
|
63
|
+
|
|
64
|
+
name: str
|
|
65
|
+
code: int
|
|
66
|
+
node_count: int = 0
|
|
67
|
+
pins: list[str] = field(default_factory=list)
|
|
68
|
+
type: str = "unknown"
|
|
69
|
+
position: tuple[float, float] = (0.0, 0.0)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_kicad_skip(cls, data: dict[str, Any]) -> "SchematicNet":
|
|
73
|
+
"""Create from kicad-skip data structure."""
|
|
74
|
+
return cls(
|
|
75
|
+
name=data.get("name", ""),
|
|
76
|
+
code=data.get("code", 0),
|
|
77
|
+
type=data.get("type", "unknown"),
|
|
78
|
+
position=data.get("position", (0.0, 0.0)),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class SchematicPin:
|
|
84
|
+
"""Pin definition from symbol."""
|
|
85
|
+
|
|
86
|
+
number: str
|
|
87
|
+
name: str
|
|
88
|
+
type: str
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_kicad_skip(cls, data: dict[str, Any]) -> "SchematicPin":
|
|
92
|
+
"""Create from kicad-skip data structure."""
|
|
93
|
+
return cls(
|
|
94
|
+
number=data.get("number", ""),
|
|
95
|
+
name=data.get("name", ""),
|
|
96
|
+
type=data.get("electrical_type", ""),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _read_file_with_encoding_fallback(file_path: Path) -> str:
|
|
101
|
+
"""Read file with multiple encoding fallback support.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
file_path: Path to the file to read
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
File content as string
|
|
108
|
+
|
|
109
|
+
Note:
|
|
110
|
+
Tries multiple encodings to handle different KiCad file formats
|
|
111
|
+
and potential encoding issues.
|
|
112
|
+
"""
|
|
113
|
+
encodings = ['utf-8', 'latin-1', 'cp1252', 'iso-8859-1']
|
|
114
|
+
content = None
|
|
115
|
+
|
|
116
|
+
for encoding in encodings:
|
|
117
|
+
try:
|
|
118
|
+
content = file_path.read_text(encoding=encoding)
|
|
119
|
+
break
|
|
120
|
+
except UnicodeDecodeError:
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
if content is None:
|
|
124
|
+
# Last resort: read with error handling
|
|
125
|
+
content = file_path.read_text(encoding='utf-8', errors='ignore')
|
|
126
|
+
|
|
127
|
+
return content
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _merge_duplicate_references(
|
|
131
|
+
components: list[SchematicComponent],
|
|
132
|
+
) -> list[SchematicComponent]:
|
|
133
|
+
"""Merge multi-unit symbol instances that share a reference.
|
|
134
|
+
|
|
135
|
+
KiCad stores each unit of a multi-unit symbol as its own symbol block
|
|
136
|
+
(often spread across sub-sheets), but the physical component is one
|
|
137
|
+
entity — netlist lookups and per-reference reports expect it once. Pin
|
|
138
|
+
metadata from all units is unioned by pin number; the first instance's
|
|
139
|
+
value/footprint wins (units share them by construction).
|
|
140
|
+
"""
|
|
141
|
+
merged: dict[str, SchematicComponent] = {}
|
|
142
|
+
for comp in components:
|
|
143
|
+
key = comp.reference or f"__unannotated_{len(merged)}"
|
|
144
|
+
existing = merged.get(key)
|
|
145
|
+
if existing is None:
|
|
146
|
+
merged[key] = comp
|
|
147
|
+
continue
|
|
148
|
+
known_pins = {str(p.get("number", "")) for p in existing.pins}
|
|
149
|
+
for p in comp.pins:
|
|
150
|
+
if str(p.get("number", "")) not in known_pins:
|
|
151
|
+
existing.pins.append(p)
|
|
152
|
+
return list(merged.values())
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class SchematicParser:
|
|
156
|
+
"""Parser for KiCad schematic files (.kicad_sch)."""
|
|
157
|
+
|
|
158
|
+
def __init__(self, file_path: str) -> None:
|
|
159
|
+
"""Initialize parser with schematic file.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
file_path: Path to .kicad_sch file
|
|
163
|
+
|
|
164
|
+
Raises:
|
|
165
|
+
FileNotFoundError: If file doesn't exist
|
|
166
|
+
ValueError: If file is not a .kicad_sch file
|
|
167
|
+
"""
|
|
168
|
+
self.file_path = validate_kicad_file(file_path, ".kicad_sch")
|
|
169
|
+
self._data: dict[str, Any] | None = None
|
|
170
|
+
self._lib_symbols_lookup: dict[str, dict[str, dict[str, str]]] = {}
|
|
171
|
+
|
|
172
|
+
def _parse_file(self) -> dict[str, Any]:
|
|
173
|
+
"""Parse the schematic file.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Parsed data structure
|
|
177
|
+
|
|
178
|
+
Note:
|
|
179
|
+
This is a simplified parser. For production use, integrate kicad-skip
|
|
180
|
+
or use kicad-netlist for proper parsing.
|
|
181
|
+
"""
|
|
182
|
+
if self._data is not None:
|
|
183
|
+
return self._data
|
|
184
|
+
|
|
185
|
+
# Simple text-based parser for .kicad_sch (S-expression format)
|
|
186
|
+
# In production, use kicad-skip library
|
|
187
|
+
|
|
188
|
+
# Read file with encoding fallback support
|
|
189
|
+
content = _read_file_with_encoding_fallback(self.file_path)
|
|
190
|
+
|
|
191
|
+
# Parse lib_symbols first so _parse_components can use it
|
|
192
|
+
self._lib_symbols_lookup = self._parse_lib_symbols(content)
|
|
193
|
+
|
|
194
|
+
# Extract basic information using regex patterns
|
|
195
|
+
# This is a simplified implementation
|
|
196
|
+
self._data = {
|
|
197
|
+
"path": str(self.file_path),
|
|
198
|
+
"title_block": self._parse_title_block(content),
|
|
199
|
+
"components": self._parse_components(content),
|
|
200
|
+
"nets": self._parse_nets(content),
|
|
201
|
+
"sheets": self._parse_sheets(content),
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return self._data
|
|
205
|
+
|
|
206
|
+
def _parse_lib_symbols(self, content: str) -> dict[str, dict[str, dict[str, str]]]:
|
|
207
|
+
"""Parse lib_symbols section to extract pin names and electrical types.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
{lib_id: {pin_number: {"name": str, "electrical_type": str}}}
|
|
211
|
+
"""
|
|
212
|
+
result = {}
|
|
213
|
+
|
|
214
|
+
# Find the (lib_symbols ...) block
|
|
215
|
+
ls_match = re.search(r'\(lib_symbols\b', content)
|
|
216
|
+
if not ls_match:
|
|
217
|
+
return result
|
|
218
|
+
|
|
219
|
+
# Extract the full lib_symbols block by counting parens
|
|
220
|
+
start = ls_match.start()
|
|
221
|
+
depth = 0
|
|
222
|
+
i = start
|
|
223
|
+
while i < len(content):
|
|
224
|
+
if content[i] == '(':
|
|
225
|
+
depth += 1
|
|
226
|
+
elif content[i] == ')':
|
|
227
|
+
depth -= 1
|
|
228
|
+
if depth == 0:
|
|
229
|
+
break
|
|
230
|
+
i += 1
|
|
231
|
+
ls_block = content[start:i + 1]
|
|
232
|
+
|
|
233
|
+
# Find top-level symbols (contain ":" like "Device:R") and extract their blocks
|
|
234
|
+
lines = ls_block.split('\n')
|
|
235
|
+
li = 0
|
|
236
|
+
while li < len(lines):
|
|
237
|
+
line = lines[li].strip()
|
|
238
|
+
top_match = re.match(r'\(symbol\s+"([^"]*:[^"]*)"', line)
|
|
239
|
+
if top_match:
|
|
240
|
+
lib_id = top_match.group(1)
|
|
241
|
+
# Extract full block for this top-level symbol
|
|
242
|
+
sym_lines = []
|
|
243
|
+
depth = 0
|
|
244
|
+
j = li
|
|
245
|
+
while j < len(lines):
|
|
246
|
+
cur = lines[j]
|
|
247
|
+
depth += cur.count('(') - cur.count(')')
|
|
248
|
+
sym_lines.append(cur)
|
|
249
|
+
if depth == 0 and len(sym_lines) > 1:
|
|
250
|
+
break
|
|
251
|
+
j += 1
|
|
252
|
+
sym_block = '\n'.join(sym_lines)
|
|
253
|
+
|
|
254
|
+
# Extract pins from the entire symbol block (including sub-symbols)
|
|
255
|
+
# Pin format: (pin <elec_type> <graphic> ... (name "X") (number "N"))
|
|
256
|
+
pin_pattern = re.compile(
|
|
257
|
+
r'\(pin\s+(\w+)\s+\w+\s[\s\S]*?'
|
|
258
|
+
r'\(name\s+"([^"]*)"[\s\S]*?\)'
|
|
259
|
+
r'\s*\(number\s+"([^"]*)"',
|
|
260
|
+
)
|
|
261
|
+
pins = {}
|
|
262
|
+
for pin_match in pin_pattern.finditer(sym_block):
|
|
263
|
+
elec_type = pin_match.group(1)
|
|
264
|
+
name = pin_match.group(2)
|
|
265
|
+
number = pin_match.group(3)
|
|
266
|
+
pins[number] = {
|
|
267
|
+
"name": name,
|
|
268
|
+
"electrical_type": elec_type,
|
|
269
|
+
}
|
|
270
|
+
if pins:
|
|
271
|
+
result[lib_id] = pins
|
|
272
|
+
|
|
273
|
+
li = j + 1
|
|
274
|
+
else:
|
|
275
|
+
li += 1
|
|
276
|
+
|
|
277
|
+
return result
|
|
278
|
+
|
|
279
|
+
def _parse_title_block(self, content: str) -> dict[str, str]:
|
|
280
|
+
"""Parse title block from schematic."""
|
|
281
|
+
title_block = {
|
|
282
|
+
"title": "",
|
|
283
|
+
"date": "",
|
|
284
|
+
"rev": "",
|
|
285
|
+
"company": "",
|
|
286
|
+
"comment": "",
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
# Extract title block values using regex
|
|
290
|
+
patterns = {
|
|
291
|
+
"title": r'title\s+"([^"]*)"',
|
|
292
|
+
"date": r'date\s+"([^"]*)"',
|
|
293
|
+
"rev": r'rev\s+"([^"]*)"',
|
|
294
|
+
"company": r'company\s+"([^"]*)"',
|
|
295
|
+
"comment": r'comment\s+\d+\s+"([^"]*)"',
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for key, pattern in patterns.items():
|
|
299
|
+
match = re.search(pattern, content)
|
|
300
|
+
if match:
|
|
301
|
+
title_block[key] = match.group(1)
|
|
302
|
+
|
|
303
|
+
return title_block
|
|
304
|
+
|
|
305
|
+
def _parse_components(self, content: str) -> list[dict[str, Any]]:
|
|
306
|
+
"""Parse components from schematic."""
|
|
307
|
+
components = []
|
|
308
|
+
|
|
309
|
+
# Find all symbol instances
|
|
310
|
+
# Pattern: (symbol (at x y rotation) (lib_id "...") ... (property "Reference" "...") ...)
|
|
311
|
+
# Match from (symbol to the closing )
|
|
312
|
+
lines = content.split('\n')
|
|
313
|
+
i = 0
|
|
314
|
+
while i < len(lines):
|
|
315
|
+
line = lines[i].strip()
|
|
316
|
+
if line.startswith('(symbol') and not line.startswith('(symbol "'):
|
|
317
|
+
# Found a symbol instance, find the entire block
|
|
318
|
+
symbol_block = []
|
|
319
|
+
depth = 0
|
|
320
|
+
j = i
|
|
321
|
+
while j < len(lines):
|
|
322
|
+
current_line = lines[j]
|
|
323
|
+
# Count parentheses
|
|
324
|
+
depth += current_line.count('(') - current_line.count(')')
|
|
325
|
+
symbol_block.append(current_line)
|
|
326
|
+
if depth == 0 and len(symbol_block) > 1:
|
|
327
|
+
break
|
|
328
|
+
j += 1
|
|
329
|
+
|
|
330
|
+
block_text = '\n'.join(symbol_block)
|
|
331
|
+
|
|
332
|
+
# Extract lib_id
|
|
333
|
+
lib_id_match = re.search(r'\(lib_id\s+"([^"]+)"', block_text)
|
|
334
|
+
if not lib_id_match:
|
|
335
|
+
i = j + 1
|
|
336
|
+
continue
|
|
337
|
+
|
|
338
|
+
lib_id = lib_id_match.group(1)
|
|
339
|
+
|
|
340
|
+
# Extract position (at x y rotation)
|
|
341
|
+
at_match = re.search(r'^\s*\(at\s+([\d.]+)\s+([\d.]+)\s+(\d+)\)', block_text, re.MULTILINE)
|
|
342
|
+
if at_match:
|
|
343
|
+
x = float(at_match.group(1))
|
|
344
|
+
y = float(at_match.group(2))
|
|
345
|
+
_ = float(at_match.group(3))
|
|
346
|
+
else:
|
|
347
|
+
x, y, _ = 0.0, 0.0, 0.0
|
|
348
|
+
|
|
349
|
+
# Extract reference
|
|
350
|
+
ref_match = re.search(r'\(property\s+"Reference"\s+"([^"]+)"', block_text)
|
|
351
|
+
reference = ref_match.group(1) if ref_match else ""
|
|
352
|
+
|
|
353
|
+
# Extract value
|
|
354
|
+
value_match = re.search(r'\(property\s+"Value"\s+"([^"]+)"', block_text)
|
|
355
|
+
value = value_match.group(1) if value_match else ""
|
|
356
|
+
|
|
357
|
+
# Extract footprint
|
|
358
|
+
fp_match = re.search(r'\(property\s+"Footprint"\s+"([^"]+)"', block_text)
|
|
359
|
+
footprint = fp_match.group(1) if fp_match else None
|
|
360
|
+
|
|
361
|
+
# Extract pins and enrich with lib_symbols data
|
|
362
|
+
pins = []
|
|
363
|
+
lib_pin_data = self._lib_symbols_lookup.get(lib_id, {})
|
|
364
|
+
for pin_match in re.finditer(r'\(pin\s+"([^"]+)"', block_text):
|
|
365
|
+
pin_num = pin_match.group(1)
|
|
366
|
+
pin_info = lib_pin_data.get(pin_num, {})
|
|
367
|
+
pins.append({
|
|
368
|
+
"number": pin_num,
|
|
369
|
+
"name": pin_info.get("name", ""),
|
|
370
|
+
"electrical_type": pin_info.get("electrical_type", ""),
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
# Extract component flags
|
|
374
|
+
flags = dict(KICAD_FLAG_DEFAULTS)
|
|
375
|
+
for flag in KICAD_SYMBOL_FLAGS:
|
|
376
|
+
if f"({flag} yes)" in block_text:
|
|
377
|
+
flags[flag] = True
|
|
378
|
+
elif f"({flag} no)" in block_text:
|
|
379
|
+
flags[flag] = False
|
|
380
|
+
|
|
381
|
+
# Skip library symbols (no reference)
|
|
382
|
+
if reference and not reference.startswith('#'):
|
|
383
|
+
# Build properties
|
|
384
|
+
properties = [
|
|
385
|
+
{"key": "Reference", "value": reference},
|
|
386
|
+
{"key": "Value", "value": value},
|
|
387
|
+
]
|
|
388
|
+
if footprint:
|
|
389
|
+
properties.append({"key": "Footprint", "value": footprint})
|
|
390
|
+
|
|
391
|
+
components.append({
|
|
392
|
+
"lib_id": lib_id,
|
|
393
|
+
"reference": reference,
|
|
394
|
+
"value": value,
|
|
395
|
+
"properties": properties,
|
|
396
|
+
"at": {"x": x, "y": y},
|
|
397
|
+
"pins": pins,
|
|
398
|
+
"flags": flags,
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
i = j + 1
|
|
402
|
+
else:
|
|
403
|
+
i += 1
|
|
404
|
+
|
|
405
|
+
return components
|
|
406
|
+
|
|
407
|
+
def _parse_nets(self, content: str) -> list[dict[str, Any]]:
|
|
408
|
+
"""Parse nets from schematic."""
|
|
409
|
+
nets = {}
|
|
410
|
+
|
|
411
|
+
# KiCad 9.0+ uses global_label, label, and wire to define nets
|
|
412
|
+
# Extract global labels
|
|
413
|
+
global_label_pattern = r'\(global_label\s+"([^"]+)"[\s\S]*?\(at\s+([\d.]+)\s+([\d.]+)[^\)]*\)'
|
|
414
|
+
for match in re.finditer(global_label_pattern, content):
|
|
415
|
+
name = match.group(1)
|
|
416
|
+
x = float(match.group(2))
|
|
417
|
+
y = float(match.group(3))
|
|
418
|
+
nets[name] = {
|
|
419
|
+
"name": name,
|
|
420
|
+
"code": len(nets),
|
|
421
|
+
"type": "global",
|
|
422
|
+
"position": (x, y),
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
# Extract local labels
|
|
426
|
+
label_pattern = r'\(label\s+"([^"]+)"[\s\S]*?\(at\s+([\d.]+)\s+([\d.]+)[^\)]*\)'
|
|
427
|
+
for match in re.finditer(label_pattern, content):
|
|
428
|
+
name = match.group(1)
|
|
429
|
+
if name not in nets: # Avoid duplicates
|
|
430
|
+
x = float(match.group(2))
|
|
431
|
+
y = float(match.group(3))
|
|
432
|
+
nets[name] = {
|
|
433
|
+
"name": name,
|
|
434
|
+
"code": len(nets),
|
|
435
|
+
"type": "local",
|
|
436
|
+
"position": (x, y),
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
# Extract hierarchical labels (connections to parent/child sheets)
|
|
440
|
+
h_label_pattern = r'\(hierarchical_label\s+"([^"]+)"[\s\S]*?\(at\s+([\d.]+)\s+([\d.]+)[^\)]*\)'
|
|
441
|
+
for match in re.finditer(h_label_pattern, content):
|
|
442
|
+
name = match.group(1)
|
|
443
|
+
if name not in nets: # Avoid duplicates
|
|
444
|
+
x = float(match.group(2))
|
|
445
|
+
y = float(match.group(3))
|
|
446
|
+
nets[name] = {
|
|
447
|
+
"name": name,
|
|
448
|
+
"code": len(nets),
|
|
449
|
+
"type": "hierarchical",
|
|
450
|
+
"position": (x, y),
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
# Extract power port labels (like +3V3, GND, etc.)
|
|
454
|
+
power_pattern = r'\(symbol\s+\(lib_id\s+"power:([^"]+)"[\s\S]*?\(at\s+([\d.]+)\s+([\d.]+)'
|
|
455
|
+
for match in re.finditer(power_pattern, content):
|
|
456
|
+
name = match.group(1)
|
|
457
|
+
if name not in nets: # Avoid duplicates
|
|
458
|
+
x = float(match.group(2))
|
|
459
|
+
y = float(match.group(3))
|
|
460
|
+
nets[name] = {
|
|
461
|
+
"name": name,
|
|
462
|
+
"code": len(nets),
|
|
463
|
+
"type": "power",
|
|
464
|
+
"position": (x, y),
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return list(nets.values())
|
|
468
|
+
|
|
469
|
+
def _parse_sheets(self, content: str) -> list[dict[str, str]]:
|
|
470
|
+
"""Parse hierarchical sheets."""
|
|
471
|
+
sheets = []
|
|
472
|
+
|
|
473
|
+
# Match sheet blocks and extract Sheetname/Sheetfile properties
|
|
474
|
+
# KiCad 10+ adds extra attributes between (size ...) and (property ...),
|
|
475
|
+
# so we use a flexible pattern with [\s\S]*? to skip them.
|
|
476
|
+
sheet_pattern = (
|
|
477
|
+
r'\(sheet\s+\(at\s+[\d.]+\s+[\d.]+\)\s*'
|
|
478
|
+
r'\(size\s+[\d.]+\s+[\d.]+\)'
|
|
479
|
+
r'[\s\S]*?'
|
|
480
|
+
r'\(property\s+"Sheetname"\s+"([^"]+)"'
|
|
481
|
+
r'[\s\S]*?'
|
|
482
|
+
r'\(property\s+"Sheetfile"\s+"([^"]+)"'
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
for match in re.finditer(sheet_pattern, content):
|
|
486
|
+
sheets.append({
|
|
487
|
+
"name": match.group(1),
|
|
488
|
+
"file": match.group(2),
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
return sheets
|
|
492
|
+
|
|
493
|
+
def get_components(self, recursive: bool = False) -> list[SchematicComponent]:
|
|
494
|
+
"""Get all components from schematic.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
recursive: Also include components from hierarchical sub-sheets,
|
|
498
|
+
following Sheetfile references (depth-first, cycle-safe).
|
|
499
|
+
Real designs keep most components in sub-sheets, so callers
|
|
500
|
+
that cross-reference a full-hierarchy netlist must pass True.
|
|
501
|
+
|
|
502
|
+
Returns:
|
|
503
|
+
List of components
|
|
504
|
+
"""
|
|
505
|
+
data = self._parse_file()
|
|
506
|
+
components = [SchematicComponent.from_kicad_skip(c) for c in data["components"]]
|
|
507
|
+
if recursive:
|
|
508
|
+
seen = {str(self.file_path.resolve())}
|
|
509
|
+
for sheet in data["sheets"]:
|
|
510
|
+
components.extend(self._collect_subsheet_components(sheet["file"], seen))
|
|
511
|
+
components = _merge_duplicate_references(components)
|
|
512
|
+
return components
|
|
513
|
+
|
|
514
|
+
def _collect_subsheet_components(
|
|
515
|
+
self, sheet_file: str, seen: set[str]
|
|
516
|
+
) -> list[SchematicComponent]:
|
|
517
|
+
"""Recursively collect components from a referenced sub-sheet.
|
|
518
|
+
|
|
519
|
+
Missing or unparsable sub-sheets are skipped (a broken reference
|
|
520
|
+
must not wipe out the parent's components).
|
|
521
|
+
"""
|
|
522
|
+
sub_path = self.file_path.parent / sheet_file
|
|
523
|
+
try:
|
|
524
|
+
key = str(sub_path.resolve())
|
|
525
|
+
except OSError:
|
|
526
|
+
return []
|
|
527
|
+
if key in seen or not sub_path.is_file():
|
|
528
|
+
return []
|
|
529
|
+
seen.add(key)
|
|
530
|
+
try:
|
|
531
|
+
sub = SchematicParser(str(sub_path))
|
|
532
|
+
sub_data = sub._parse_file()
|
|
533
|
+
except (FileNotFoundError, ValueError, OSError):
|
|
534
|
+
return []
|
|
535
|
+
components = [SchematicComponent.from_kicad_skip(c) for c in sub_data["components"]]
|
|
536
|
+
for sheet in sub_data["sheets"]:
|
|
537
|
+
components.extend(sub._collect_subsheet_components(sheet["file"], seen))
|
|
538
|
+
return components
|
|
539
|
+
|
|
540
|
+
def get_nets(self) -> list[SchematicNet]:
|
|
541
|
+
"""Get all nets from schematic.
|
|
542
|
+
|
|
543
|
+
Returns:
|
|
544
|
+
List of nets
|
|
545
|
+
"""
|
|
546
|
+
data = self._parse_file()
|
|
547
|
+
return [SchematicNet.from_kicad_skip(n) for n in data["nets"]]
|
|
548
|
+
|
|
549
|
+
def get_title_block(self) -> dict[str, str]:
|
|
550
|
+
"""Get title block information.
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
Dictionary with title, date, rev, company, comment
|
|
554
|
+
"""
|
|
555
|
+
data = self._parse_file()
|
|
556
|
+
return data["title_block"]
|
|
557
|
+
|
|
558
|
+
def get_sheets(self) -> list[dict[str, str]]:
|
|
559
|
+
"""Get hierarchical sheets.
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
List of sheet information
|
|
563
|
+
"""
|
|
564
|
+
data = self._parse_file()
|
|
565
|
+
return data["sheets"]
|
|
566
|
+
|
|
567
|
+
def get_component_by_reference(self, reference: str) -> SchematicComponent | None:
|
|
568
|
+
"""Get a component by its reference designator.
|
|
569
|
+
|
|
570
|
+
Args:
|
|
571
|
+
reference: Component reference (e.g., "R1", "U1")
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
Component if found, None otherwise
|
|
575
|
+
"""
|
|
576
|
+
for component in self.get_components():
|
|
577
|
+
if component.reference == reference:
|
|
578
|
+
return component
|
|
579
|
+
return None
|
|
580
|
+
|
|
581
|
+
def search_components(self, pattern: str) -> list[SchematicComponent]:
|
|
582
|
+
"""Search for components by pattern.
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
pattern: Search pattern (matches reference, value, or library_id)
|
|
586
|
+
|
|
587
|
+
Returns:
|
|
588
|
+
List of matching components
|
|
589
|
+
"""
|
|
590
|
+
import re
|
|
591
|
+
|
|
592
|
+
regex = re.compile(pattern, re.IGNORECASE)
|
|
593
|
+
results = []
|
|
594
|
+
|
|
595
|
+
for component in self.get_components():
|
|
596
|
+
if (
|
|
597
|
+
regex.search(component.reference)
|
|
598
|
+
or regex.search(component.value)
|
|
599
|
+
or regex.search(component.library_id)
|
|
600
|
+
):
|
|
601
|
+
results.append(component)
|
|
602
|
+
|
|
603
|
+
return results
|
|
604
|
+
|
|
605
|
+
def get_component_connections(self, reference: str) -> dict[str, Any]:
|
|
606
|
+
"""Get all network connections for a component.
|
|
607
|
+
|
|
608
|
+
Args:
|
|
609
|
+
reference: Component reference (e.g., "R16")
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
Dictionary with connection information:
|
|
613
|
+
{
|
|
614
|
+
"nets": ["net_name1", "net_name2"],
|
|
615
|
+
"labels": ["label1", "label2"],
|
|
616
|
+
"connected_components": ["comp1", "comp2"]
|
|
617
|
+
}
|
|
618
|
+
"""
|
|
619
|
+
content = _read_file_with_encoding_fallback(self.file_path)
|
|
620
|
+
|
|
621
|
+
# Find the component instance
|
|
622
|
+
comp_pattern = rf'\(symbol\s+[\s\S]*?\(property\s+"Reference"\s+"{re.escape(reference)}"'
|
|
623
|
+
comp_match = re.search(comp_pattern, content, re.DOTALL)
|
|
624
|
+
|
|
625
|
+
if not comp_match:
|
|
626
|
+
return {"error": f"Component {reference} not found"}
|
|
627
|
+
|
|
628
|
+
# Extract the component block (from symbol to closing paren)
|
|
629
|
+
start = comp_match.start()
|
|
630
|
+
depth = 0
|
|
631
|
+
i = start
|
|
632
|
+
while i < len(content):
|
|
633
|
+
if content[i] == '(':
|
|
634
|
+
depth += 1
|
|
635
|
+
elif content[i] == ')':
|
|
636
|
+
depth -= 1
|
|
637
|
+
if depth == 0:
|
|
638
|
+
break
|
|
639
|
+
i += 1
|
|
640
|
+
|
|
641
|
+
comp_block = content[start:i]
|
|
642
|
+
|
|
643
|
+
# Find all pins in this component
|
|
644
|
+
pins = []
|
|
645
|
+
for pin_match in re.finditer(r'\(pin\s+"([^"]+)"', comp_block):
|
|
646
|
+
pins.append(pin_match.group(1))
|
|
647
|
+
|
|
648
|
+
# Search for connections by finding wires near the component position
|
|
649
|
+
comp = self.get_component_by_reference(reference)
|
|
650
|
+
if not comp:
|
|
651
|
+
return {"error": f"Component {reference} not found"}
|
|
652
|
+
|
|
653
|
+
cx, cy = comp.position
|
|
654
|
+
|
|
655
|
+
# Find all labels and global labels
|
|
656
|
+
labels = []
|
|
657
|
+
for label_match in re.finditer(r'\((?:global_)?label\s+"([^"]+)"[\s\S]*?\(at\s+([\d.]+)\s+([\d.]+)', content):
|
|
658
|
+
label_name = label_match.group(1)
|
|
659
|
+
lx, ly = float(label_match.group(2)), float(label_match.group(3))
|
|
660
|
+
dist = ((lx - cx)**2 + (ly - cy)**2)**0.5
|
|
661
|
+
labels.append({"name": label_name, "position": (lx, ly), "distance": dist})
|
|
662
|
+
|
|
663
|
+
# Find hierarchical labels
|
|
664
|
+
for label_match in re.finditer(r'\(hierarchical_label\s+"([^"]+)"[\s\S]*?\(at\s+([\d.]+)\s+([\d.]+)', content):
|
|
665
|
+
label_name = label_match.group(1)
|
|
666
|
+
lx, ly = float(label_match.group(2)), float(label_match.group(3))
|
|
667
|
+
dist = ((lx - cx)**2 + (ly - cy)**2)**0.5
|
|
668
|
+
labels.append({"name": label_name, "position": (lx, ly), "distance": dist})
|
|
669
|
+
|
|
670
|
+
# Filter to nearby labels (within 20mm)
|
|
671
|
+
nearby_labels = [label for label in labels if label["distance"] < 20]
|
|
672
|
+
|
|
673
|
+
# Find nearby components (within 15mm)
|
|
674
|
+
components = self.get_components()
|
|
675
|
+
nearby_comps = []
|
|
676
|
+
for c in components:
|
|
677
|
+
if c.reference == reference:
|
|
678
|
+
continue
|
|
679
|
+
dist = ((c.position[0] - cx)**2 + (c.position[1] - cy)**2)**0.5
|
|
680
|
+
if dist < 15:
|
|
681
|
+
nearby_comps.append({
|
|
682
|
+
"reference": c.reference,
|
|
683
|
+
"value": c.value,
|
|
684
|
+
"distance": dist,
|
|
685
|
+
"position": c.position
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
return {
|
|
689
|
+
"component": reference,
|
|
690
|
+
"position": comp.position,
|
|
691
|
+
"pins": pins,
|
|
692
|
+
"nearby_labels": nearby_labels[:10],
|
|
693
|
+
"nearby_components": nearby_comps[:10],
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
def trace_net(self, reference: str, pin_number: str | None = None) -> dict[str, Any]:
|
|
697
|
+
"""Trace network connections from a component pin.
|
|
698
|
+
|
|
699
|
+
Args:
|
|
700
|
+
reference: Component reference (e.g., "R16")
|
|
701
|
+
pin_number: Optional pin number to trace (if None, trace all pins)
|
|
702
|
+
|
|
703
|
+
Returns:
|
|
704
|
+
Network trace information
|
|
705
|
+
"""
|
|
706
|
+
connections = self.get_component_connections(reference)
|
|
707
|
+
|
|
708
|
+
if "error" in connections:
|
|
709
|
+
return connections
|
|
710
|
+
|
|
711
|
+
# Analyze the nearby data to infer network connections
|
|
712
|
+
inferred_nets = []
|
|
713
|
+
|
|
714
|
+
# Check for hierarchical labels that indicate function
|
|
715
|
+
for label in connections["nearby_labels"]:
|
|
716
|
+
label_name = label["name"]
|
|
717
|
+
if any(keyword in label_name.upper() for keyword in
|
|
718
|
+
["I2C", "SCL", "SDA", "SMBUS", "PMIC", "GPIO", "EN", "INT"]):
|
|
719
|
+
inferred_nets.append({
|
|
720
|
+
"name": label_name,
|
|
721
|
+
"type": "signal",
|
|
722
|
+
"distance": label["distance"]
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
return {
|
|
726
|
+
"component": reference,
|
|
727
|
+
"position": connections["position"],
|
|
728
|
+
"inferred_connections": inferred_nets,
|
|
729
|
+
"nearby_components": connections["nearby_components"][:5],
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
def build_wire_network(self) -> dict[tuple[float, float], list[tuple[float, float]]]:
|
|
733
|
+
"""Build a graph of wire connections.
|
|
734
|
+
|
|
735
|
+
Returns:
|
|
736
|
+
Dictionary mapping each point to its connected neighbors
|
|
737
|
+
"""
|
|
738
|
+
import re
|
|
739
|
+
|
|
740
|
+
content = _read_file_with_encoding_fallback(self.file_path)
|
|
741
|
+
|
|
742
|
+
# Find all wire segments
|
|
743
|
+
wire_pattern = r'\(wire\s+\(pts\s+\(xy\s+([\d.]+)\s+([\d.]+)\)\s+\(xy\s+([\d.]+)\s+([\d.]+)\)'
|
|
744
|
+
|
|
745
|
+
network = {}
|
|
746
|
+
|
|
747
|
+
for match in re.finditer(wire_pattern, content):
|
|
748
|
+
x1, y1 = float(match.group(1)), float(match.group(2))
|
|
749
|
+
x2, y2 = float(match.group(3)), float(match.group(4))
|
|
750
|
+
|
|
751
|
+
p1 = (x1, y1)
|
|
752
|
+
p2 = (x2, y2)
|
|
753
|
+
|
|
754
|
+
if p1 not in network:
|
|
755
|
+
network[p1] = []
|
|
756
|
+
if p2 not in network:
|
|
757
|
+
network[p2] = []
|
|
758
|
+
|
|
759
|
+
network[p1].append(p2)
|
|
760
|
+
network[p2].append(p1)
|
|
761
|
+
|
|
762
|
+
# Find junctions and merge connections
|
|
763
|
+
junction_pattern = r'\(junction\s+\(at\s+([\d.]+)\s+([\d.]+)'
|
|
764
|
+
for match in re.finditer(junction_pattern, content):
|
|
765
|
+
jx, jy = float(match.group(1)), float(match.group(2))
|
|
766
|
+
# For a junction, all wires meeting at this point should be connected
|
|
767
|
+
# Find all wire endpoints at this position (with small tolerance)
|
|
768
|
+
tolerance = 0.01 # 0.01mm tolerance
|
|
769
|
+
connected_points = [p for p in network if
|
|
770
|
+
abs(p[0] - jx) < tolerance and abs(p[1] - jy) < tolerance]
|
|
771
|
+
|
|
772
|
+
# Merge all connections at junction
|
|
773
|
+
all_neighbors = set()
|
|
774
|
+
for p in connected_points:
|
|
775
|
+
all_neighbors.update(network[p])
|
|
776
|
+
|
|
777
|
+
for p in connected_points:
|
|
778
|
+
network[p] = list(all_neighbors)
|
|
779
|
+
|
|
780
|
+
return network
|
|
781
|
+
|
|
782
|
+
def trace_wire_network(self, reference: str, max_depth: int = 20) -> dict[str, Any]:
|
|
783
|
+
"""Trace wire connections from a component.
|
|
784
|
+
|
|
785
|
+
Args:
|
|
786
|
+
reference: Component reference (e.g., "R16")
|
|
787
|
+
max_depth: Maximum connection depth to trace
|
|
788
|
+
|
|
789
|
+
Returns:
|
|
790
|
+
Dictionary with traced connections and labels
|
|
791
|
+
"""
|
|
792
|
+
import re
|
|
793
|
+
|
|
794
|
+
# Get component position
|
|
795
|
+
comp = self.get_component_by_reference(reference)
|
|
796
|
+
if not comp:
|
|
797
|
+
return {"error": f"Component {reference} not found"}
|
|
798
|
+
|
|
799
|
+
cx, cy = comp.position
|
|
800
|
+
|
|
801
|
+
# Build wire network
|
|
802
|
+
network = self.build_wire_network()
|
|
803
|
+
|
|
804
|
+
# Find all labels
|
|
805
|
+
content = _read_file_with_encoding_fallback(self.file_path)
|
|
806
|
+
|
|
807
|
+
# Find hierarchical labels
|
|
808
|
+
h_labels = []
|
|
809
|
+
for label_match in re.finditer(
|
|
810
|
+
r'\(hierarchical_label\s+"([^"]+)"[\s\S]*?\(at\s+([\d.]+)\s+([\d.]+)',
|
|
811
|
+
content
|
|
812
|
+
):
|
|
813
|
+
name = label_match.group(1)
|
|
814
|
+
lx, ly = float(label_match.group(2)), float(label_match.group(3))
|
|
815
|
+
h_labels.append({"name": name, "position": (lx, ly)})
|
|
816
|
+
|
|
817
|
+
# Find global labels
|
|
818
|
+
g_labels = []
|
|
819
|
+
for label_match in re.finditer(
|
|
820
|
+
r'\(global_label\s+"([^"]+)"[\s\S]*?\(at\s+([\d.]+)\s+([\d.]+)',
|
|
821
|
+
content
|
|
822
|
+
):
|
|
823
|
+
name = label_match.group(1)
|
|
824
|
+
lx, ly = float(label_match.group(2)), float(label_match.group(3))
|
|
825
|
+
g_labels.append({"name": name, "position": (lx, ly)})
|
|
826
|
+
|
|
827
|
+
all_labels = h_labels + g_labels
|
|
828
|
+
|
|
829
|
+
# Start from component position and trace
|
|
830
|
+
max_tolerance = 20.0 # 20mm max tolerance (for components with pin offset)
|
|
831
|
+
start_point = None
|
|
832
|
+
min_dist = float('inf')
|
|
833
|
+
|
|
834
|
+
# Find the nearest wire endpoint to component
|
|
835
|
+
for point in network:
|
|
836
|
+
dist = ((point[0] - cx)**2 + (point[1] - cy)**2)**0.5
|
|
837
|
+
if dist < min_dist:
|
|
838
|
+
min_dist = dist
|
|
839
|
+
start_point = point
|
|
840
|
+
|
|
841
|
+
# Only proceed if the nearest point is within tolerance
|
|
842
|
+
if min_dist > max_tolerance:
|
|
843
|
+
return {
|
|
844
|
+
"component": reference,
|
|
845
|
+
"position": comp.position,
|
|
846
|
+
"connected_labels": [],
|
|
847
|
+
"trace_path": [],
|
|
848
|
+
"error": f"No wire found within {max_tolerance}mm (nearest: {min_dist:.2f}mm)",
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
# BFS trace through network
|
|
852
|
+
visited = set()
|
|
853
|
+
queue = [start_point]
|
|
854
|
+
trace_path = []
|
|
855
|
+
connected_labels = []
|
|
856
|
+
|
|
857
|
+
while queue and len(visited) < max_depth:
|
|
858
|
+
point = queue.pop(0)
|
|
859
|
+
if point in visited:
|
|
860
|
+
continue
|
|
861
|
+
visited.add(point)
|
|
862
|
+
|
|
863
|
+
trace_path.append(point)
|
|
864
|
+
|
|
865
|
+
# Check if this point is near a label
|
|
866
|
+
label_tolerance = 5.0 # 5mm tolerance for label matching
|
|
867
|
+
for label in all_labels:
|
|
868
|
+
lx, ly = label["position"]
|
|
869
|
+
dist = ((point[0] - lx)**2 + (point[1] - ly)**2)**0.5
|
|
870
|
+
if dist < label_tolerance and not any(lbl["name"] == label["name"] for lbl in connected_labels):
|
|
871
|
+
connected_labels.append({
|
|
872
|
+
"name": label["name"],
|
|
873
|
+
"position": label["position"],
|
|
874
|
+
"distance": dist,
|
|
875
|
+
})
|
|
876
|
+
|
|
877
|
+
# Add neighbors to queue
|
|
878
|
+
if point in network:
|
|
879
|
+
for neighbor in network[point]:
|
|
880
|
+
if neighbor not in visited:
|
|
881
|
+
queue.append(neighbor)
|
|
882
|
+
|
|
883
|
+
# Find nearby power symbols
|
|
884
|
+
power_tolerance = 15.0 # 15mm tolerance for power symbols
|
|
885
|
+
power_pattern = r'\(symbol\s+\(lib_id\s+"power:([^"]+)"[\s\S]*?\(at\s+([\d.]+)\s+([\d.]+)'
|
|
886
|
+
for match in re.finditer(power_pattern, content):
|
|
887
|
+
power_name = match.group(1)
|
|
888
|
+
px, py = float(match.group(2)), float(match.group(3))
|
|
889
|
+
dist = ((px - cx)**2 + (py - cy)**2)**0.5
|
|
890
|
+
if dist < power_tolerance:
|
|
891
|
+
connected_labels.append({
|
|
892
|
+
"name": f"POWER:{power_name}",
|
|
893
|
+
"position": (px, py),
|
|
894
|
+
"distance": dist,
|
|
895
|
+
})
|
|
896
|
+
|
|
897
|
+
return {
|
|
898
|
+
"component": reference,
|
|
899
|
+
"position": comp.position,
|
|
900
|
+
"connected_labels": connected_labels,
|
|
901
|
+
"trace_path": trace_path,
|
|
902
|
+
}
|