@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,234 @@
|
|
|
1
|
+
"""KiCad netlist parser for accurate component network tracking."""
|
|
2
|
+
|
|
3
|
+
import xml.etree.ElementTree as ET
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class NetlistComponent:
|
|
11
|
+
"""Component from netlist file."""
|
|
12
|
+
|
|
13
|
+
reference: str
|
|
14
|
+
value: str
|
|
15
|
+
library: str
|
|
16
|
+
footprint: str | None = None
|
|
17
|
+
pins: dict[str, str] = field(default_factory=dict) # pin_number -> net_name
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class NetlistNet:
|
|
22
|
+
"""Net from netlist file."""
|
|
23
|
+
|
|
24
|
+
name: str
|
|
25
|
+
code: int
|
|
26
|
+
pins: list[tuple[str, str]] = field(default_factory=list) # (reference, pin_number)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class NetlistParser:
|
|
30
|
+
"""Parser for KiCad netlist files (.xml)."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, file_path: str) -> None:
|
|
33
|
+
"""Initialize parser with netlist file.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
file_path: Path to .xml netlist file
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
FileNotFoundError: If file doesn't exist
|
|
40
|
+
ValueError: If file is not a .xml file
|
|
41
|
+
"""
|
|
42
|
+
self.file_path = Path(file_path)
|
|
43
|
+
if not self.file_path.exists():
|
|
44
|
+
raise FileNotFoundError(f"Netlist file not found: {file_path}")
|
|
45
|
+
|
|
46
|
+
if self.file_path.suffix != ".xml":
|
|
47
|
+
raise ValueError(f"Netlist file must be .xml, got: {self.file_path.suffix}")
|
|
48
|
+
|
|
49
|
+
self._data: dict[str, Any] | None = None
|
|
50
|
+
|
|
51
|
+
def _parse_file(self) -> dict[str, Any]:
|
|
52
|
+
"""Parse the netlist XML file.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Parsed data structure
|
|
56
|
+
"""
|
|
57
|
+
if self._data is not None:
|
|
58
|
+
return self._data
|
|
59
|
+
|
|
60
|
+
tree = ET.parse(self.file_path)
|
|
61
|
+
root = tree.getroot()
|
|
62
|
+
|
|
63
|
+
# Parse components (KiCad 9.0+ uses <comp> not <component>)
|
|
64
|
+
components = {}
|
|
65
|
+
for comp in root.findall(".//comp"):
|
|
66
|
+
ref = comp.get("ref")
|
|
67
|
+
value = comp.findtext("value", "")
|
|
68
|
+
# KiCad XML: footprint is a direct <footprint>Lib:Footprint</footprint>
|
|
69
|
+
# element (NOT footprint/libpart), and libsource is an attribute-only
|
|
70
|
+
# element <libsource lib="..." part="..."/> (NOT libsource/libpart).
|
|
71
|
+
footprint = comp.findtext("footprint") or ""
|
|
72
|
+
libsource = comp.find("libsource")
|
|
73
|
+
library = libsource.get("lib", "") if libsource is not None else ""
|
|
74
|
+
|
|
75
|
+
# Pins will be populated from nets later
|
|
76
|
+
components[ref] = NetlistComponent(
|
|
77
|
+
reference=ref,
|
|
78
|
+
value=value,
|
|
79
|
+
library=library,
|
|
80
|
+
footprint=footprint,
|
|
81
|
+
pins={}, # Empty initially, will populate from nets
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Parse nets and populate component pins
|
|
85
|
+
nets = {}
|
|
86
|
+
for net in root.findall(".//net"):
|
|
87
|
+
code_attr = net.get("code")
|
|
88
|
+
code = int(code_attr) if code_attr and code_attr.lstrip("-").isdigit() else -1
|
|
89
|
+
name = net.get("name")
|
|
90
|
+
|
|
91
|
+
pins_list = []
|
|
92
|
+
for node in net.findall("node"):
|
|
93
|
+
ref = node.get("ref")
|
|
94
|
+
pin_num = node.get("pin")
|
|
95
|
+
if ref and pin_num:
|
|
96
|
+
pins_list.append((ref, pin_num))
|
|
97
|
+
|
|
98
|
+
# Populate component pins
|
|
99
|
+
if ref in components:
|
|
100
|
+
components[ref].pins[pin_num] = name
|
|
101
|
+
|
|
102
|
+
nets[name] = NetlistNet(name=name, code=code, pins=pins_list)
|
|
103
|
+
|
|
104
|
+
self._data = {
|
|
105
|
+
"components": components,
|
|
106
|
+
"nets": nets,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return self._data
|
|
110
|
+
|
|
111
|
+
def _get_net_name(self, root: ET.Element, net_code: str) -> str:
|
|
112
|
+
"""Get net name from net code.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
root: XML root element
|
|
116
|
+
net_code: Net code string (e.g., "3")
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Net name
|
|
120
|
+
"""
|
|
121
|
+
net_elem = root.find(f".//net[@code='{net_code}']")
|
|
122
|
+
if net_elem is not None:
|
|
123
|
+
return net_elem.get("name", f"net_{net_code}")
|
|
124
|
+
return f"net_{net_code}"
|
|
125
|
+
|
|
126
|
+
def get_components(self) -> dict[str, NetlistComponent]:
|
|
127
|
+
"""Get all components from netlist.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Dictionary mapping reference to component
|
|
131
|
+
"""
|
|
132
|
+
data = self._parse_file()
|
|
133
|
+
return data["components"]
|
|
134
|
+
|
|
135
|
+
def get_nets(self) -> dict[str, NetlistNet]:
|
|
136
|
+
"""Get all nets from netlist.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Dictionary mapping net name to net
|
|
140
|
+
"""
|
|
141
|
+
data = self._parse_file()
|
|
142
|
+
return data["nets"]
|
|
143
|
+
|
|
144
|
+
def get_component_nets(self, reference: str) -> dict[str, list[str]]:
|
|
145
|
+
"""Get all networks for a component.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
reference: Component reference (e.g., "R16")
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Dictionary mapping net name to list of connected pins
|
|
152
|
+
"""
|
|
153
|
+
components = self.get_components()
|
|
154
|
+
if reference not in components:
|
|
155
|
+
return {}
|
|
156
|
+
|
|
157
|
+
comp = components[reference]
|
|
158
|
+
net_pins = {}
|
|
159
|
+
|
|
160
|
+
for pin_num, net_name in comp.pins.items():
|
|
161
|
+
if net_name not in net_pins:
|
|
162
|
+
net_pins[net_name] = []
|
|
163
|
+
net_pins[net_name].append(pin_num)
|
|
164
|
+
|
|
165
|
+
return net_pins
|
|
166
|
+
|
|
167
|
+
def get_net_components(self, net_name: str) -> list[tuple[str, str]]:
|
|
168
|
+
"""Get all components connected to a net.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
net_name: Net name (e.g., "PMIC_I2C_SCL")
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
List of (reference, pin_number) tuples
|
|
175
|
+
"""
|
|
176
|
+
nets = self.get_nets()
|
|
177
|
+
if net_name not in nets:
|
|
178
|
+
return []
|
|
179
|
+
|
|
180
|
+
return nets[net_name].pins
|
|
181
|
+
|
|
182
|
+
def trace_connection(self, reference: str, pin_number: str | None = None) -> dict[str, Any]:
|
|
183
|
+
"""Trace connections from a component pin.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
reference: Component reference
|
|
187
|
+
pin_number: Optional pin number (if None, trace all pins)
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Connection information
|
|
191
|
+
"""
|
|
192
|
+
components = self.get_components()
|
|
193
|
+
if reference not in components:
|
|
194
|
+
return {"error": f"Component {reference} not found in netlist"}
|
|
195
|
+
|
|
196
|
+
comp = components[reference]
|
|
197
|
+
|
|
198
|
+
if pin_number:
|
|
199
|
+
# Trace specific pin
|
|
200
|
+
if pin_number not in comp.pins:
|
|
201
|
+
return {"error": f"Pin {pin_number} not found in component {reference}"}
|
|
202
|
+
|
|
203
|
+
net_name = comp.pins[pin_number]
|
|
204
|
+
nets = self.get_nets()
|
|
205
|
+
connected = nets[net_name].pins if net_name in nets else []
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
"component": reference,
|
|
209
|
+
"pin": pin_number,
|
|
210
|
+
"net": net_name,
|
|
211
|
+
"connected_to": [(ref, pin) for ref, pin in connected if ref != reference],
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
else:
|
|
215
|
+
# Trace all pins. A component can have several pins on the same net
|
|
216
|
+
# (relay contacts, multiple GND pins), so group as a list per net
|
|
217
|
+
# instead of overwriting earlier pins.
|
|
218
|
+
net_connections: dict[str, list[dict[str, Any]]] = {}
|
|
219
|
+
for pin_num, net_name in comp.pins.items():
|
|
220
|
+
nets = self.get_nets()
|
|
221
|
+
connected = nets[net_name].pins if net_name in nets else []
|
|
222
|
+
net_connections.setdefault(net_name, []).append(
|
|
223
|
+
{
|
|
224
|
+
"pin": pin_num,
|
|
225
|
+
"connected_to": [
|
|
226
|
+
(ref, pin) for ref, pin in connected if ref != reference
|
|
227
|
+
],
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
"component": reference,
|
|
233
|
+
"nets": net_connections,
|
|
234
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""PCB file parser wrapper using kicad-skip."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..utils.file_handlers import validate_kicad_file
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class PCBFootprint:
|
|
13
|
+
"""Footprint from PCB file."""
|
|
14
|
+
|
|
15
|
+
reference: str
|
|
16
|
+
footprint_id: str
|
|
17
|
+
value: str
|
|
18
|
+
layer: str = "F.Cu"
|
|
19
|
+
position: tuple[float, float] = (0.0, 0.0)
|
|
20
|
+
rotation: float = 0.0
|
|
21
|
+
pad_count: int = 0
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def from_dict(cls, data: dict[str, Any]) -> "PCBFootprint":
|
|
25
|
+
"""Create from parsed data structure."""
|
|
26
|
+
at = data.get("at", {})
|
|
27
|
+
position = (float(at.get("x", 0)), float(at.get("y", 0)))
|
|
28
|
+
rotation = float(at.get("rotation", 0))
|
|
29
|
+
|
|
30
|
+
return cls(
|
|
31
|
+
reference=data.get("reference", ""),
|
|
32
|
+
footprint_id=data.get("footprint_id", ""),
|
|
33
|
+
value=data.get("value", ""),
|
|
34
|
+
layer=data.get("layer", "F.Cu"),
|
|
35
|
+
position=position,
|
|
36
|
+
rotation=rotation,
|
|
37
|
+
pad_count=len(data.get("pads", [])),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _read_file_with_encoding_fallback(file_path: Path) -> str:
|
|
42
|
+
"""Read file with multiple encoding fallback support.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
file_path: Path to the file to read
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
File content as string
|
|
49
|
+
|
|
50
|
+
Note:
|
|
51
|
+
Tries multiple encodings to handle different KiCad file formats
|
|
52
|
+
and potential encoding issues.
|
|
53
|
+
"""
|
|
54
|
+
encodings = ['utf-8', 'latin-1', 'cp1252', 'iso-8859-1']
|
|
55
|
+
content = None
|
|
56
|
+
|
|
57
|
+
for encoding in encodings:
|
|
58
|
+
try:
|
|
59
|
+
content = file_path.read_text(encoding=encoding)
|
|
60
|
+
break
|
|
61
|
+
except UnicodeDecodeError:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
if content is None:
|
|
65
|
+
# Last resort: read with error handling
|
|
66
|
+
content = file_path.read_text(encoding='utf-8', errors='ignore')
|
|
67
|
+
|
|
68
|
+
return content
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class PCBParser:
|
|
72
|
+
"""Parser for KiCad PCB files (.kicad_pcb)."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, file_path: str) -> None:
|
|
75
|
+
"""Initialize parser with PCB file.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
file_path: Path to .kicad_pcb file
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
FileNotFoundError: If file doesn't exist
|
|
82
|
+
ValueError: If file is not a .kicad_pcb file
|
|
83
|
+
"""
|
|
84
|
+
self.file_path = validate_kicad_file(file_path, ".kicad_pcb")
|
|
85
|
+
self._data: dict[str, Any] | None = None
|
|
86
|
+
|
|
87
|
+
def _parse_file(self) -> dict[str, Any]:
|
|
88
|
+
"""Parse the PCB file.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Parsed data structure
|
|
92
|
+
"""
|
|
93
|
+
if self._data is not None:
|
|
94
|
+
return self._data
|
|
95
|
+
|
|
96
|
+
content = _read_file_with_encoding_fallback(self.file_path)
|
|
97
|
+
|
|
98
|
+
# Extract basic information
|
|
99
|
+
self._data = {
|
|
100
|
+
"path": str(self.file_path),
|
|
101
|
+
"general": self._parse_general(content),
|
|
102
|
+
"footprints": self._parse_footprints(content),
|
|
103
|
+
"tracks": self._parse_tracks(content),
|
|
104
|
+
"vias": self._parse_vias(content),
|
|
105
|
+
"zones": self._parse_zones(content),
|
|
106
|
+
"setup": self._parse_setup(content),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return self._data
|
|
110
|
+
|
|
111
|
+
def _parse_general(self, content: str) -> dict[str, Any]:
|
|
112
|
+
"""Parse general section."""
|
|
113
|
+
general = {"thickness": 1.6, "layers": 2}
|
|
114
|
+
|
|
115
|
+
# Extract thickness
|
|
116
|
+
thickness_match = re.search(r'thickness\s+([\d.]+)', content)
|
|
117
|
+
if thickness_match:
|
|
118
|
+
general["thickness"] = float(thickness_match.group(1))
|
|
119
|
+
|
|
120
|
+
# Count layers
|
|
121
|
+
layer_pattern = r'\(layer\s+"([^"]+)"'
|
|
122
|
+
layers = re.findall(layer_pattern, content)
|
|
123
|
+
# Filter for copper layers. A startswith("F."/"B.") check is wrong:
|
|
124
|
+
# F.SilkS / F.Paste / F.Mask / F.Fab / F.CrtYd / F.Adhes all start with
|
|
125
|
+
# "F." but are NOT copper. Only F.Cu, B.Cu and In<N>.Cu are copper.
|
|
126
|
+
copper_layers = [
|
|
127
|
+
layer for layer in set(layers)
|
|
128
|
+
if layer in ("F.Cu", "B.Cu") or re.match(r"In\d+\.Cu$", layer)
|
|
129
|
+
]
|
|
130
|
+
general["layers"] = len(copper_layers) or 2
|
|
131
|
+
|
|
132
|
+
return general
|
|
133
|
+
|
|
134
|
+
def _parse_footprints(self, content: str) -> list[dict[str, Any]]:
|
|
135
|
+
"""Parse footprints from PCB.
|
|
136
|
+
|
|
137
|
+
Uses multi-line regex to match KiCad footprint instances.
|
|
138
|
+
"""
|
|
139
|
+
footprints = []
|
|
140
|
+
|
|
141
|
+
# Simpler pattern: Match (footprint "..." followed by (at ... sometime later)
|
|
142
|
+
# We'll find the footprint line, then search forward for the (at ...) line
|
|
143
|
+
footprint_lines_pattern = r'\(footprint\s+"([^"]+)"'
|
|
144
|
+
|
|
145
|
+
for match in re.finditer(footprint_lines_pattern, content):
|
|
146
|
+
footprint_id = match.group(1)
|
|
147
|
+
start_pos = match.start()
|
|
148
|
+
|
|
149
|
+
# Search forward for the (at ...) line, within a reasonable distance
|
|
150
|
+
search_area = content[start_pos:start_pos + 500]
|
|
151
|
+
at_match = re.search(r'\(at\s+([\d.]+)\s+([\d.]+)(?:\s+([\d.\-]+))?\)', search_area)
|
|
152
|
+
|
|
153
|
+
if at_match:
|
|
154
|
+
x = float(at_match.group(1))
|
|
155
|
+
y = float(at_match.group(2))
|
|
156
|
+
rotation = float(at_match.group(3)) if at_match.group(3) else 0.0
|
|
157
|
+
|
|
158
|
+
# Extract a larger block for reference and value
|
|
159
|
+
# Find the matching closing parenthesis for the footprint
|
|
160
|
+
depth = 0
|
|
161
|
+
found_open = False
|
|
162
|
+
end_pos = start_pos
|
|
163
|
+
block_start = start_pos
|
|
164
|
+
|
|
165
|
+
# Find where the (footprint block starts (first '(' after the keyword)
|
|
166
|
+
while end_pos < len(content) and end_pos < start_pos + 20:
|
|
167
|
+
if content[end_pos] == '(':
|
|
168
|
+
found_open = True
|
|
169
|
+
block_start = end_pos
|
|
170
|
+
break
|
|
171
|
+
end_pos += 1
|
|
172
|
+
|
|
173
|
+
if found_open:
|
|
174
|
+
# Now find the matching closing parenthesis
|
|
175
|
+
depth = 1
|
|
176
|
+
end_pos = block_start + 1
|
|
177
|
+
while end_pos < len(content):
|
|
178
|
+
if content[end_pos] == '(':
|
|
179
|
+
depth += 1
|
|
180
|
+
elif content[end_pos] == ')':
|
|
181
|
+
depth -= 1
|
|
182
|
+
if depth == 0:
|
|
183
|
+
break
|
|
184
|
+
end_pos += 1
|
|
185
|
+
|
|
186
|
+
footprint_block = content[block_start:end_pos]
|
|
187
|
+
|
|
188
|
+
# Extract reference: KiCad 9+ uses (property "Reference" "..."),
|
|
189
|
+
# KiCad 4/5 used (fp_text reference "..."). Support both.
|
|
190
|
+
ref_match = (re.search(r'\(property\s+"Reference"\s+"([^"]+)"', footprint_block)
|
|
191
|
+
or re.search(r'\(fp_text\s+reference\s+"([^"]+)"', footprint_block))
|
|
192
|
+
reference = ref_match.group(1) if ref_match else ""
|
|
193
|
+
|
|
194
|
+
# Extract value (same version split)
|
|
195
|
+
val_match = (re.search(r'\(property\s+"Value"\s+"([^"]+)"', footprint_block)
|
|
196
|
+
or re.search(r'\(fp_text\s+value\s+"([^"]+)"', footprint_block))
|
|
197
|
+
value = val_match.group(1) if val_match else ""
|
|
198
|
+
|
|
199
|
+
# Extract layer (default to F.Cu if not found)
|
|
200
|
+
layer_match = re.search(r'\(layer\s+"([^"]+)"', footprint_block)
|
|
201
|
+
layer = layer_match.group(1) if layer_match else "F.Cu"
|
|
202
|
+
|
|
203
|
+
# Count pads
|
|
204
|
+
pad_count = len(re.findall(r'\(pad\s+', footprint_block))
|
|
205
|
+
|
|
206
|
+
footprints.append({
|
|
207
|
+
"footprint_id": footprint_id,
|
|
208
|
+
"reference": reference,
|
|
209
|
+
"value": value,
|
|
210
|
+
"layer": layer,
|
|
211
|
+
"at": {"x": x, "y": y, "rotation": rotation},
|
|
212
|
+
"pads": [],
|
|
213
|
+
"pad_count": pad_count,
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
return footprints
|
|
217
|
+
|
|
218
|
+
def _parse_tracks(self, content: str) -> list[dict[str, Any]]:
|
|
219
|
+
"""Parse track segments."""
|
|
220
|
+
tracks = []
|
|
221
|
+
|
|
222
|
+
# KiCad 10 format: (segment (start x y) (end x y) (width w) (layer "...") (net "name") (uuid "..."))
|
|
223
|
+
# The net field uses quoted string: (net "netname")
|
|
224
|
+
segment_pattern = (
|
|
225
|
+
r'\(segment\s+'
|
|
226
|
+
r'\(start\s+([\d.]+)\s+([\d.]+)\)\s+'
|
|
227
|
+
r'\(end\s+([\d.]+)\s+([\d.]+)\)\s+'
|
|
228
|
+
r'\(width\s+([\d.]+)\)\s+'
|
|
229
|
+
r'\(layer\s+"([^"]+)"\)\s+'
|
|
230
|
+
r'\(net\s+"([^"]*)"\)'
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
for match in re.finditer(segment_pattern, content):
|
|
234
|
+
x1, y1 = float(match.group(1)), float(match.group(2))
|
|
235
|
+
x2, y2 = float(match.group(3)), float(match.group(4))
|
|
236
|
+
dx, dy = x2 - x1, y2 - y1
|
|
237
|
+
tracks.append({
|
|
238
|
+
"start": {"x": x1, "y": y1},
|
|
239
|
+
"end": {"x": x2, "y": y2},
|
|
240
|
+
"width": float(match.group(5)),
|
|
241
|
+
"layer": match.group(6),
|
|
242
|
+
"net": match.group(7),
|
|
243
|
+
"length": (dx * dx + dy * dy) ** 0.5,
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
return tracks
|
|
247
|
+
|
|
248
|
+
def _parse_vias(self, content: str) -> list[dict[str, Any]]:
|
|
249
|
+
"""Parse vias."""
|
|
250
|
+
vias = []
|
|
251
|
+
|
|
252
|
+
# KiCad 10 format: (via (at x y) (size s) (drill d) (layers "L1" "L2") (free yes) (net "name") (uuid "..."))
|
|
253
|
+
via_pattern = (
|
|
254
|
+
r'\(via\s+'
|
|
255
|
+
r'\(at\s+([\d.]+)\s+([\d.]+)\)\s+'
|
|
256
|
+
r'\(size\s+([\d.]+)\)\s+'
|
|
257
|
+
r'\(drill\s+([\d.]+)\)\s+'
|
|
258
|
+
r'\(layers\s+"([^"]+)"\s+"([^"]+)"\)\s+'
|
|
259
|
+
r'(?:\(free\s+yes\)\s+)?'
|
|
260
|
+
r'(?:\(net\s+"([^"]*)"\)\s+)?'
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
for match in re.finditer(via_pattern, content):
|
|
264
|
+
vias.append({
|
|
265
|
+
"at": {"x": float(match.group(1)), "y": float(match.group(2))},
|
|
266
|
+
"size": float(match.group(3)),
|
|
267
|
+
"drill": float(match.group(4)),
|
|
268
|
+
"top_layer": match.group(5),
|
|
269
|
+
"bottom_layer": match.group(6),
|
|
270
|
+
"net": match.group(7) or "",
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
# Fallback: simpler pattern for single-layer via or older format
|
|
274
|
+
if not vias:
|
|
275
|
+
via_pattern_simple = r'\(via\s+\(at\s+([\d.]+)\s+([\d.]+)\)\s+\(size\s+([\d.]+)\)\s+\(drill\s+([\d.]+)'
|
|
276
|
+
for match in re.finditer(via_pattern_simple, content):
|
|
277
|
+
vias.append({
|
|
278
|
+
"at": {"x": float(match.group(1)), "y": float(match.group(2))},
|
|
279
|
+
"size": float(match.group(3)),
|
|
280
|
+
"drill": float(match.group(4)),
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
return vias
|
|
284
|
+
|
|
285
|
+
def _parse_zones(self, content: str) -> list[dict[str, Any]]:
|
|
286
|
+
"""Parse copper zones."""
|
|
287
|
+
zones = []
|
|
288
|
+
|
|
289
|
+
# KiCad 10 format: (zone (net "name") (layer "...") ...)
|
|
290
|
+
zone_pattern = r'\(zone\s+\(net\s+"([^"]+)"\)\s+\(layer\s+"([^"]+)"\)'
|
|
291
|
+
|
|
292
|
+
for match in re.finditer(zone_pattern, content):
|
|
293
|
+
zones.append({
|
|
294
|
+
"net_name": match.group(1),
|
|
295
|
+
"layer": match.group(2),
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
# Fallback: KiCad 9 format (net code + net_name)
|
|
299
|
+
if not zones:
|
|
300
|
+
zone_pattern_v9 = r'\(zone\s+\(net\s+(\d+)\)\s*\(net_name\s+"([^"]+)"'
|
|
301
|
+
for match in re.finditer(zone_pattern_v9, content):
|
|
302
|
+
zones.append({
|
|
303
|
+
"net_name": match.group(2),
|
|
304
|
+
"layer": "",
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
return zones
|
|
308
|
+
|
|
309
|
+
def _parse_setup(self, content: str) -> dict[str, Any]:
|
|
310
|
+
"""Parse setup section for design rules."""
|
|
311
|
+
setup = {
|
|
312
|
+
"trace_min": 0.2,
|
|
313
|
+
"via_min": 0.4,
|
|
314
|
+
"clearance": 0.15,
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
# Extract design rules
|
|
318
|
+
setup_match = re.search(
|
|
319
|
+
r'\(setup.*?\(min_resolution\s+([\d.]+)\)', content, re.DOTALL
|
|
320
|
+
)
|
|
321
|
+
if setup_match:
|
|
322
|
+
# Try to extract trace and via sizes
|
|
323
|
+
trace_match = re.search(
|
|
324
|
+
r'\(trace_min\s+([\d.]+)', content[: content.find(setup_match.group(0)) + 500]
|
|
325
|
+
)
|
|
326
|
+
if trace_match:
|
|
327
|
+
setup["trace_min"] = float(trace_match.group(1))
|
|
328
|
+
|
|
329
|
+
return setup
|
|
330
|
+
|
|
331
|
+
def get_footprints(self) -> list[PCBFootprint]:
|
|
332
|
+
"""Get all footprints from PCB.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
List of footprints
|
|
336
|
+
"""
|
|
337
|
+
data = self._parse_file()
|
|
338
|
+
return [PCBFootprint.from_dict(f) for f in data["footprints"]]
|
|
339
|
+
|
|
340
|
+
def get_statistics(self) -> dict[str, Any]:
|
|
341
|
+
"""Get PCB statistics.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Dictionary with statistics
|
|
345
|
+
"""
|
|
346
|
+
data = self._parse_file()
|
|
347
|
+
|
|
348
|
+
# Calculate board dimensions from footprint positions
|
|
349
|
+
footprints = self.get_footprints()
|
|
350
|
+
if footprints:
|
|
351
|
+
x_coords = [f.position[0] for f in footprints]
|
|
352
|
+
y_coords = [f.position[1] for f in footprints]
|
|
353
|
+
min_x, max_x = min(x_coords), max(x_coords)
|
|
354
|
+
min_y, max_y = min(y_coords), max(y_coords)
|
|
355
|
+
# Add margin for board edge
|
|
356
|
+
margin = 5.0
|
|
357
|
+
width = max_x - min_x + (2 * margin)
|
|
358
|
+
height = max_y - min_y + (2 * margin)
|
|
359
|
+
else:
|
|
360
|
+
width = height = 0.0
|
|
361
|
+
|
|
362
|
+
# Calculate total pads from pad_count field
|
|
363
|
+
total_pads = sum(f.get("pad_count", 0) for f in data["footprints"])
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
"total_footprints": len(data["footprints"]),
|
|
367
|
+
"total_pads": total_pads,
|
|
368
|
+
"total_tracks": len(data["tracks"]),
|
|
369
|
+
"total_vias": len(data["vias"]),
|
|
370
|
+
"total_zones": len(data["zones"]),
|
|
371
|
+
"board_width": width,
|
|
372
|
+
"board_height": height,
|
|
373
|
+
"layers": data["general"]["layers"],
|
|
374
|
+
"thickness": data["general"]["thickness"],
|
|
375
|
+
}
|