kicad-sch-api 0.4.1__py3-none-any.whl → 0.5.1__py3-none-any.whl
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.
- kicad_sch_api/__init__.py +67 -2
- kicad_sch_api/cli/kicad_to_python.py +169 -0
- kicad_sch_api/collections/__init__.py +23 -8
- kicad_sch_api/collections/base.py +369 -59
- kicad_sch_api/collections/components.py +1376 -187
- kicad_sch_api/collections/junctions.py +129 -289
- kicad_sch_api/collections/labels.py +391 -287
- kicad_sch_api/collections/wires.py +202 -316
- kicad_sch_api/core/__init__.py +37 -2
- kicad_sch_api/core/component_bounds.py +34 -12
- kicad_sch_api/core/components.py +146 -7
- kicad_sch_api/core/config.py +25 -12
- kicad_sch_api/core/connectivity.py +692 -0
- kicad_sch_api/core/exceptions.py +175 -0
- kicad_sch_api/core/factories/element_factory.py +3 -1
- kicad_sch_api/core/formatter.py +24 -7
- kicad_sch_api/core/geometry.py +94 -5
- kicad_sch_api/core/managers/__init__.py +4 -0
- kicad_sch_api/core/managers/base.py +76 -0
- kicad_sch_api/core/managers/file_io.py +3 -1
- kicad_sch_api/core/managers/format_sync.py +3 -2
- kicad_sch_api/core/managers/graphics.py +3 -2
- kicad_sch_api/core/managers/hierarchy.py +661 -0
- kicad_sch_api/core/managers/metadata.py +4 -2
- kicad_sch_api/core/managers/sheet.py +52 -14
- kicad_sch_api/core/managers/text_elements.py +3 -2
- kicad_sch_api/core/managers/validation.py +3 -2
- kicad_sch_api/core/managers/wire.py +112 -54
- kicad_sch_api/core/parsing_utils.py +63 -0
- kicad_sch_api/core/pin_utils.py +103 -9
- kicad_sch_api/core/schematic.py +343 -29
- kicad_sch_api/core/types.py +79 -7
- kicad_sch_api/exporters/__init__.py +10 -0
- kicad_sch_api/exporters/python_generator.py +610 -0
- kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
- kicad_sch_api/geometry/__init__.py +15 -3
- kicad_sch_api/geometry/routing.py +211 -0
- kicad_sch_api/parsers/elements/label_parser.py +30 -8
- kicad_sch_api/parsers/elements/symbol_parser.py +255 -83
- kicad_sch_api/utils/logging.py +555 -0
- kicad_sch_api/utils/logging_decorators.py +587 -0
- kicad_sch_api/utils/validation.py +16 -22
- kicad_sch_api/wrappers/__init__.py +14 -0
- kicad_sch_api/wrappers/base.py +89 -0
- kicad_sch_api/wrappers/wire.py +198 -0
- kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
- kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
- kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
- {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
- mcp_server/__init__.py +34 -0
- mcp_server/example_logging_integration.py +506 -0
- mcp_server/models.py +252 -0
- mcp_server/server.py +357 -0
- mcp_server/tools/__init__.py +32 -0
- mcp_server/tools/component_tools.py +516 -0
- mcp_server/tools/connectivity_tools.py +532 -0
- mcp_server/tools/consolidated_tools.py +1216 -0
- mcp_server/tools/pin_discovery.py +333 -0
- mcp_server/utils/__init__.py +38 -0
- mcp_server/utils/logging.py +127 -0
- mcp_server/utils.py +36 -0
- kicad_sch_api-0.4.1.dist-info/METADATA +0 -491
- kicad_sch_api-0.4.1.dist-info/RECORD +0 -87
- kicad_sch_api-0.4.1.dist-info/entry_points.txt +0 -2
- {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -11,11 +11,12 @@ from pathlib import Path
|
|
|
11
11
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
12
12
|
|
|
13
13
|
from ..types import Point
|
|
14
|
+
from .base import BaseManager
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
class SheetManager:
|
|
19
|
+
class SheetManager(BaseManager):
|
|
19
20
|
"""
|
|
20
21
|
Manages hierarchical sheets and multi-sheet project coordination.
|
|
21
22
|
|
|
@@ -34,7 +35,7 @@ class SheetManager:
|
|
|
34
35
|
Args:
|
|
35
36
|
schematic_data: Reference to schematic data
|
|
36
37
|
"""
|
|
37
|
-
|
|
38
|
+
super().__init__(schematic_data)
|
|
38
39
|
|
|
39
40
|
def add_sheet(
|
|
40
41
|
self,
|
|
@@ -121,29 +122,30 @@ class SheetManager:
|
|
|
121
122
|
sheet_uuid: str,
|
|
122
123
|
name: str,
|
|
123
124
|
pin_type: str,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
justify: str = "left",
|
|
125
|
+
edge: str,
|
|
126
|
+
position_along_edge: float,
|
|
127
127
|
uuid_str: Optional[str] = None,
|
|
128
128
|
) -> Optional[str]:
|
|
129
129
|
"""
|
|
130
|
-
Add a pin to an existing sheet.
|
|
130
|
+
Add a pin to an existing sheet using edge-based positioning.
|
|
131
131
|
|
|
132
132
|
Args:
|
|
133
133
|
sheet_uuid: UUID of target sheet
|
|
134
134
|
name: Pin name
|
|
135
135
|
pin_type: Pin type (input, output, bidirectional, tri_state, passive)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
justify: Text justification (left, right, center)
|
|
136
|
+
edge: Edge to place pin on ("right", "bottom", "left", "top")
|
|
137
|
+
position_along_edge: Distance along edge from reference corner (mm)
|
|
139
138
|
uuid_str: Optional pin UUID
|
|
140
139
|
|
|
141
140
|
Returns:
|
|
142
141
|
UUID of created pin, or None if sheet not found
|
|
143
|
-
"""
|
|
144
|
-
if isinstance(position, tuple):
|
|
145
|
-
position = Point(position[0], position[1])
|
|
146
142
|
|
|
143
|
+
Edge positioning (clockwise from right):
|
|
144
|
+
- "right": rotation=0°, justify="right", position from top edge
|
|
145
|
+
- "bottom": rotation=270°, justify="left", position from left edge
|
|
146
|
+
- "left": rotation=180°, justify="left", position from bottom edge
|
|
147
|
+
- "top": rotation=90°, justify="right", position from left edge
|
|
148
|
+
"""
|
|
147
149
|
if uuid_str is None:
|
|
148
150
|
uuid_str = str(uuid.uuid4())
|
|
149
151
|
|
|
@@ -152,15 +154,49 @@ class SheetManager:
|
|
|
152
154
|
logger.warning(f"Invalid sheet pin type: {pin_type}. Using 'input'")
|
|
153
155
|
pin_type = "input"
|
|
154
156
|
|
|
157
|
+
valid_edges = ["right", "bottom", "left", "top"]
|
|
158
|
+
if edge not in valid_edges:
|
|
159
|
+
logger.error(f"Invalid edge: {edge}. Must be one of {valid_edges}")
|
|
160
|
+
return None
|
|
161
|
+
|
|
155
162
|
# Find the sheet
|
|
156
163
|
sheets = self._data.get("sheets", [])
|
|
157
164
|
for sheet in sheets:
|
|
158
165
|
if sheet.get("uuid") == sheet_uuid:
|
|
166
|
+
# Get sheet bounds
|
|
167
|
+
sheet_x = sheet["position"]["x"]
|
|
168
|
+
sheet_y = sheet["position"]["y"]
|
|
169
|
+
sheet_width = sheet["size"]["width"]
|
|
170
|
+
sheet_height = sheet["size"]["height"]
|
|
171
|
+
|
|
172
|
+
# Calculate position, rotation, and justification based on edge
|
|
173
|
+
# Clockwise: right (0°) → bottom (270°) → left (180°) → top (90°)
|
|
174
|
+
if edge == "right":
|
|
175
|
+
x = sheet_x + sheet_width
|
|
176
|
+
y = sheet_y + position_along_edge
|
|
177
|
+
rotation = 0
|
|
178
|
+
justify = "right"
|
|
179
|
+
elif edge == "bottom":
|
|
180
|
+
x = sheet_x + position_along_edge
|
|
181
|
+
y = sheet_y + sheet_height
|
|
182
|
+
rotation = 270
|
|
183
|
+
justify = "left"
|
|
184
|
+
elif edge == "left":
|
|
185
|
+
x = sheet_x
|
|
186
|
+
y = sheet_y + sheet_height - position_along_edge
|
|
187
|
+
rotation = 180
|
|
188
|
+
justify = "left"
|
|
189
|
+
elif edge == "top":
|
|
190
|
+
x = sheet_x + position_along_edge
|
|
191
|
+
y = sheet_y
|
|
192
|
+
rotation = 90
|
|
193
|
+
justify = "right"
|
|
194
|
+
|
|
159
195
|
pin_data = {
|
|
160
196
|
"uuid": uuid_str,
|
|
161
197
|
"name": name,
|
|
162
198
|
"pin_type": pin_type,
|
|
163
|
-
"position": {"x":
|
|
199
|
+
"position": {"x": x, "y": y},
|
|
164
200
|
"rotation": rotation,
|
|
165
201
|
"size": 1.27,
|
|
166
202
|
"justify": justify,
|
|
@@ -169,7 +205,9 @@ class SheetManager:
|
|
|
169
205
|
# Add to sheet's pins array (already initialized in add_sheet)
|
|
170
206
|
sheet["pins"].append(pin_data)
|
|
171
207
|
|
|
172
|
-
logger.debug(
|
|
208
|
+
logger.debug(
|
|
209
|
+
f"Added pin '{name}' to sheet {sheet_uuid} on {edge} edge at ({x}, {y})"
|
|
210
|
+
)
|
|
173
211
|
return uuid_str
|
|
174
212
|
|
|
175
213
|
logger.warning(f"Sheet not found: {sheet_uuid}")
|
|
@@ -11,11 +11,12 @@ import uuid
|
|
|
11
11
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
12
12
|
|
|
13
13
|
from ..types import Point
|
|
14
|
+
from .base import BaseManager
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
class TextElementManager:
|
|
19
|
+
class TextElementManager(BaseManager):
|
|
19
20
|
"""
|
|
20
21
|
Manages text elements and labeling in KiCAD schematics.
|
|
21
22
|
|
|
@@ -34,7 +35,7 @@ class TextElementManager:
|
|
|
34
35
|
Args:
|
|
35
36
|
schematic_data: Reference to schematic data
|
|
36
37
|
"""
|
|
37
|
-
|
|
38
|
+
super().__init__(schematic_data)
|
|
38
39
|
|
|
39
40
|
def add_label(
|
|
40
41
|
self,
|
|
@@ -11,11 +11,12 @@ from typing import Any, Dict, List, Optional, Set, Tuple
|
|
|
11
11
|
|
|
12
12
|
from ...utils.validation import ValidationError, ValidationIssue
|
|
13
13
|
from ..types import Point
|
|
14
|
+
from .base import BaseManager
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
class ValidationManager:
|
|
19
|
+
class ValidationManager(BaseManager):
|
|
19
20
|
"""
|
|
20
21
|
Comprehensive validation manager for schematic integrity.
|
|
21
22
|
|
|
@@ -39,7 +40,7 @@ class ValidationManager:
|
|
|
39
40
|
component_collection: Component collection for validation
|
|
40
41
|
wire_collection: Wire collection for connectivity analysis
|
|
41
42
|
"""
|
|
42
|
-
|
|
43
|
+
super().__init__(schematic_data)
|
|
43
44
|
self._components = component_collection
|
|
44
45
|
self._wires = wire_collection
|
|
45
46
|
self._validation_rules = self._initialize_validation_rules()
|
|
@@ -10,13 +10,15 @@ import uuid
|
|
|
10
10
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
11
11
|
|
|
12
12
|
from ...library.cache import get_symbol_cache
|
|
13
|
+
from ..connectivity import ConnectivityAnalyzer, Net
|
|
13
14
|
from ..types import Point, Wire, WireType
|
|
14
15
|
from ..wires import WireCollection
|
|
16
|
+
from .base import BaseManager
|
|
15
17
|
|
|
16
18
|
logger = logging.getLogger(__name__)
|
|
17
19
|
|
|
18
20
|
|
|
19
|
-
class WireManager:
|
|
21
|
+
class WireManager(BaseManager):
|
|
20
22
|
"""
|
|
21
23
|
Manages wire operations and pin connectivity in KiCAD schematics.
|
|
22
24
|
|
|
@@ -29,7 +31,11 @@ class WireManager:
|
|
|
29
31
|
"""
|
|
30
32
|
|
|
31
33
|
def __init__(
|
|
32
|
-
self,
|
|
34
|
+
self,
|
|
35
|
+
schematic_data: Dict[str, Any],
|
|
36
|
+
wire_collection: WireCollection,
|
|
37
|
+
component_collection,
|
|
38
|
+
schematic,
|
|
33
39
|
):
|
|
34
40
|
"""
|
|
35
41
|
Initialize WireManager.
|
|
@@ -38,12 +44,18 @@ class WireManager:
|
|
|
38
44
|
schematic_data: Reference to schematic data
|
|
39
45
|
wire_collection: Wire collection for management
|
|
40
46
|
component_collection: Component collection for pin lookups
|
|
47
|
+
schematic: Reference to parent Schematic object for connectivity analysis
|
|
41
48
|
"""
|
|
42
|
-
|
|
49
|
+
super().__init__(schematic_data)
|
|
43
50
|
self._wires = wire_collection
|
|
44
51
|
self._components = component_collection
|
|
52
|
+
self._schematic = schematic
|
|
45
53
|
self._symbol_cache = get_symbol_cache()
|
|
46
54
|
|
|
55
|
+
# Lazy-initialized connectivity analyzer (always hierarchical)
|
|
56
|
+
self._connectivity_analyzer: Optional[ConnectivityAnalyzer] = None
|
|
57
|
+
self._connectivity_valid = False
|
|
58
|
+
|
|
47
59
|
def add_wire(
|
|
48
60
|
self, start: Union[Point, Tuple[float, float]], end: Union[Point, Tuple[float, float]]
|
|
49
61
|
) -> str:
|
|
@@ -65,6 +77,9 @@ class WireManager:
|
|
|
65
77
|
# Use the wire collection to add the wire
|
|
66
78
|
wire_uuid = self._wires.add(start=start, end=end)
|
|
67
79
|
|
|
80
|
+
# Invalidate connectivity cache
|
|
81
|
+
self._invalidate_connectivity()
|
|
82
|
+
|
|
68
83
|
logger.debug(f"Added wire: {start} -> {end}")
|
|
69
84
|
return wire_uuid
|
|
70
85
|
|
|
@@ -92,6 +107,8 @@ class WireManager:
|
|
|
92
107
|
|
|
93
108
|
success = removed_from_collection or removed_from_data
|
|
94
109
|
if success:
|
|
110
|
+
# Invalidate connectivity cache
|
|
111
|
+
self._invalidate_connectivity()
|
|
95
112
|
logger.debug(f"Removed wire: {wire_uuid}")
|
|
96
113
|
|
|
97
114
|
return success
|
|
@@ -151,7 +168,8 @@ class WireManager:
|
|
|
151
168
|
"""
|
|
152
169
|
Get absolute position of a component pin.
|
|
153
170
|
|
|
154
|
-
|
|
171
|
+
Uses the same geometry transformation as the connectivity analyzer
|
|
172
|
+
to ensure consistent pin positions.
|
|
155
173
|
|
|
156
174
|
Args:
|
|
157
175
|
component_ref: Component reference (e.g., "R1")
|
|
@@ -160,27 +178,20 @@ class WireManager:
|
|
|
160
178
|
Returns:
|
|
161
179
|
Absolute pin position or None if not found
|
|
162
180
|
"""
|
|
181
|
+
from ..pin_utils import list_component_pins
|
|
182
|
+
|
|
163
183
|
# Find component
|
|
164
184
|
component = self._components.get(component_ref)
|
|
165
185
|
if not component:
|
|
166
186
|
logger.warning(f"Component not found: {component_ref}")
|
|
167
187
|
return None
|
|
168
188
|
|
|
169
|
-
#
|
|
170
|
-
|
|
171
|
-
if not symbol_def:
|
|
172
|
-
logger.warning(f"Symbol definition not found: {component.lib_id}")
|
|
173
|
-
return None
|
|
174
|
-
|
|
175
|
-
# Find pin in symbol definition
|
|
176
|
-
for pin in symbol_def.pins:
|
|
177
|
-
if pin.number == pin_number:
|
|
178
|
-
# Calculate absolute position
|
|
179
|
-
# Apply component rotation/mirroring if needed (simplified for now)
|
|
180
|
-
absolute_x = component.position.x + pin.position.x
|
|
181
|
-
absolute_y = component.position.y + pin.position.y
|
|
189
|
+
# Use pin_utils to get correct transformed positions
|
|
190
|
+
pins = list_component_pins(component)
|
|
182
191
|
|
|
183
|
-
|
|
192
|
+
for pin_num, pin_pos in pins:
|
|
193
|
+
if pin_num == pin_number:
|
|
194
|
+
return pin_pos
|
|
184
195
|
|
|
185
196
|
logger.warning(f"Pin {pin_number} not found on component {component_ref}")
|
|
186
197
|
return None
|
|
@@ -189,31 +200,24 @@ class WireManager:
|
|
|
189
200
|
"""
|
|
190
201
|
List all pins and their positions for a component.
|
|
191
202
|
|
|
203
|
+
Uses the same geometry transformation as the connectivity analyzer
|
|
204
|
+
to ensure consistent pin positions.
|
|
205
|
+
|
|
192
206
|
Args:
|
|
193
207
|
component_ref: Component reference
|
|
194
208
|
|
|
195
209
|
Returns:
|
|
196
210
|
List of (pin_number, absolute_position) tuples
|
|
197
211
|
"""
|
|
198
|
-
|
|
212
|
+
from ..pin_utils import list_component_pins
|
|
199
213
|
|
|
200
214
|
# Find component
|
|
201
215
|
component = self._components.get(component_ref)
|
|
202
216
|
if not component:
|
|
203
|
-
return
|
|
204
|
-
|
|
205
|
-
# Get symbol definition
|
|
206
|
-
symbol_def = self._symbol_cache.get_symbol(component.lib_id)
|
|
207
|
-
if not symbol_def:
|
|
208
|
-
return pins
|
|
209
|
-
|
|
210
|
-
# Calculate absolute positions for all pins
|
|
211
|
-
for pin in symbol_def.pins:
|
|
212
|
-
absolute_x = component.position.x + pin.position.x
|
|
213
|
-
absolute_y = component.position.y + pin.position.y
|
|
214
|
-
pins.append((pin.number, Point(absolute_x, absolute_y)))
|
|
217
|
+
return []
|
|
215
218
|
|
|
216
|
-
|
|
219
|
+
# Use pin_utils to get correct transformed positions
|
|
220
|
+
return list_component_pins(component)
|
|
217
221
|
|
|
218
222
|
def auto_route_pins(
|
|
219
223
|
self,
|
|
@@ -280,7 +284,14 @@ class WireManager:
|
|
|
280
284
|
self, component1_ref: str, pin1_number: str, component2_ref: str, pin2_number: str
|
|
281
285
|
) -> bool:
|
|
282
286
|
"""
|
|
283
|
-
Check if two pins are connected
|
|
287
|
+
Check if two pins are electrically connected.
|
|
288
|
+
|
|
289
|
+
Performs full connectivity analysis including:
|
|
290
|
+
- Direct wire connections
|
|
291
|
+
- Connections through junctions
|
|
292
|
+
- Connections through labels (local/global/hierarchical)
|
|
293
|
+
- Connections through power symbols
|
|
294
|
+
- Hierarchical sheet connections
|
|
284
295
|
|
|
285
296
|
Args:
|
|
286
297
|
component1_ref: First component reference
|
|
@@ -289,29 +300,15 @@ class WireManager:
|
|
|
289
300
|
pin2_number: Second component pin number
|
|
290
301
|
|
|
291
302
|
Returns:
|
|
292
|
-
True if pins are connected, False otherwise
|
|
303
|
+
True if pins are electrically connected, False otherwise
|
|
293
304
|
"""
|
|
294
|
-
|
|
295
|
-
|
|
305
|
+
self._ensure_connectivity()
|
|
306
|
+
|
|
307
|
+
if self._connectivity_analyzer:
|
|
308
|
+
return self._connectivity_analyzer.are_connected(
|
|
309
|
+
component1_ref, pin1_number, component2_ref, pin2_number
|
|
310
|
+
)
|
|
296
311
|
|
|
297
|
-
if pin1_pos is None or pin2_pos is None:
|
|
298
|
-
return False
|
|
299
|
-
|
|
300
|
-
# Check for direct wire connection
|
|
301
|
-
for wire in self._wires:
|
|
302
|
-
if (wire.start == pin1_pos and wire.end == pin2_pos) or (
|
|
303
|
-
wire.start == pin2_pos and wire.end == pin1_pos
|
|
304
|
-
):
|
|
305
|
-
return True
|
|
306
|
-
|
|
307
|
-
# TODO: Implement more sophisticated connectivity analysis
|
|
308
|
-
# NOTE: Current implementation only checks for direct wire connections between pins.
|
|
309
|
-
# A full implementation would:
|
|
310
|
-
# 1. Follow wire networks through junctions (connection points)
|
|
311
|
-
# 2. Trace through labels (global/hierarchical net connections)
|
|
312
|
-
# 3. Build a complete net connectivity graph
|
|
313
|
-
# This is a known limitation - use ValidationManager for full electrical rule checking.
|
|
314
|
-
# Priority: MEDIUM - Would enable better automated wiring validation
|
|
315
312
|
return False
|
|
316
313
|
|
|
317
314
|
def connect_pins_with_wire(
|
|
@@ -350,3 +347,64 @@ class WireManager:
|
|
|
350
347
|
"bus": len([w for w in self._wires if w.wire_type == WireType.BUS]),
|
|
351
348
|
},
|
|
352
349
|
}
|
|
350
|
+
|
|
351
|
+
def get_net_for_pin(self, component_ref: str, pin_number: str) -> Optional[Net]:
|
|
352
|
+
"""
|
|
353
|
+
Get the electrical net connected to a specific pin.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
component_ref: Component reference (e.g., "R1")
|
|
357
|
+
pin_number: Pin number
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
Net object if pin is connected, None otherwise
|
|
361
|
+
"""
|
|
362
|
+
self._ensure_connectivity()
|
|
363
|
+
|
|
364
|
+
if self._connectivity_analyzer:
|
|
365
|
+
return self._connectivity_analyzer.get_net_for_pin(component_ref, pin_number)
|
|
366
|
+
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
def get_connected_pins(self, component_ref: str, pin_number: str) -> List[Tuple[str, str]]:
|
|
370
|
+
"""
|
|
371
|
+
Get all pins electrically connected to a specific pin.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
component_ref: Component reference (e.g., "R1")
|
|
375
|
+
pin_number: Pin number
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
List of (reference, pin_number) tuples for all connected pins
|
|
379
|
+
"""
|
|
380
|
+
self._ensure_connectivity()
|
|
381
|
+
|
|
382
|
+
if self._connectivity_analyzer:
|
|
383
|
+
return self._connectivity_analyzer.get_connected_pins(component_ref, pin_number)
|
|
384
|
+
|
|
385
|
+
return []
|
|
386
|
+
|
|
387
|
+
def _ensure_connectivity(self):
|
|
388
|
+
"""
|
|
389
|
+
Ensure connectivity analysis is up-to-date.
|
|
390
|
+
|
|
391
|
+
Lazily initializes and runs connectivity analyzer on first query.
|
|
392
|
+
Re-runs analysis if schematic has changed since last analysis.
|
|
393
|
+
"""
|
|
394
|
+
if not self._connectivity_valid:
|
|
395
|
+
logger.debug("Running connectivity analysis (hierarchical)...")
|
|
396
|
+
self._connectivity_analyzer = ConnectivityAnalyzer()
|
|
397
|
+
self._connectivity_analyzer.analyze(self._schematic, hierarchical=True)
|
|
398
|
+
self._connectivity_valid = True
|
|
399
|
+
logger.debug("Connectivity analysis complete")
|
|
400
|
+
|
|
401
|
+
def _invalidate_connectivity(self):
|
|
402
|
+
"""
|
|
403
|
+
Invalidate connectivity cache.
|
|
404
|
+
|
|
405
|
+
Called when schematic changes that affect connectivity (wires, components, etc.).
|
|
406
|
+
Next connectivity query will trigger re-analysis.
|
|
407
|
+
"""
|
|
408
|
+
if self._connectivity_valid:
|
|
409
|
+
logger.debug("Invalidating connectivity cache")
|
|
410
|
+
self._connectivity_valid = False
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for parsing S-expression data.
|
|
3
|
+
|
|
4
|
+
This module contains helper functions used by various parsers
|
|
5
|
+
to handle common parsing patterns safely and consistently.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import sexpdata
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_bool_property(value: Any, default: bool = True) -> bool:
|
|
17
|
+
"""
|
|
18
|
+
Parse a boolean property from S-expression data.
|
|
19
|
+
|
|
20
|
+
Handles both sexpdata.Symbol and string types, converting yes/no to bool.
|
|
21
|
+
This is the canonical way to parse boolean properties from KiCad files.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
value: Value from S-expression (Symbol, str, bool, or None)
|
|
25
|
+
default: Default value if parsing fails or value is None
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
bool: Parsed boolean value
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
>>> parse_bool_property(sexpdata.Symbol('yes'))
|
|
32
|
+
True
|
|
33
|
+
>>> parse_bool_property('no')
|
|
34
|
+
False
|
|
35
|
+
>>> parse_bool_property(None, default=False)
|
|
36
|
+
False
|
|
37
|
+
>>> parse_bool_property('YES') # Case insensitive
|
|
38
|
+
True
|
|
39
|
+
|
|
40
|
+
Note:
|
|
41
|
+
This function was added to fix a critical bug where Symbol('yes') == 'yes'
|
|
42
|
+
returned False, causing properties like in_bom and on_board to be parsed
|
|
43
|
+
incorrectly.
|
|
44
|
+
"""
|
|
45
|
+
# If value is None, use default
|
|
46
|
+
if value is None:
|
|
47
|
+
return default
|
|
48
|
+
|
|
49
|
+
# Convert Symbol to string
|
|
50
|
+
if isinstance(value, sexpdata.Symbol):
|
|
51
|
+
value = str(value)
|
|
52
|
+
|
|
53
|
+
# Handle string values (case-insensitive)
|
|
54
|
+
if isinstance(value, str):
|
|
55
|
+
return value.lower() == "yes"
|
|
56
|
+
|
|
57
|
+
# Handle boolean values directly
|
|
58
|
+
if isinstance(value, bool):
|
|
59
|
+
return value
|
|
60
|
+
|
|
61
|
+
# Unexpected type - use default
|
|
62
|
+
logger.warning(f"Unexpected type for boolean property: {type(value)}, using default={default}")
|
|
63
|
+
return default
|
kicad_sch_api/core/pin_utils.py
CHANGED
|
@@ -66,14 +66,14 @@ def get_component_pin_position(component: SchematicSymbol, pin_number: str) -> O
|
|
|
66
66
|
|
|
67
67
|
# Look for pin in symbol definition
|
|
68
68
|
pins_found = []
|
|
69
|
-
for pin_def in symbol_def.
|
|
70
|
-
pins_found.append(pin_def.
|
|
71
|
-
if pin_def.
|
|
69
|
+
for pin_def in symbol_def.pins:
|
|
70
|
+
pins_found.append(pin_def.number)
|
|
71
|
+
if pin_def.number == pin_number:
|
|
72
72
|
logger.info(f" Found pin {pin_number} in symbol definition")
|
|
73
73
|
|
|
74
74
|
# Get pin position from definition
|
|
75
|
-
pin_x = pin_def.
|
|
76
|
-
pin_y = pin_def.
|
|
75
|
+
pin_x = pin_def.position.x
|
|
76
|
+
pin_y = pin_def.position.y
|
|
77
77
|
logger.info(f" Symbol pin position: ({pin_x}, {pin_y})")
|
|
78
78
|
|
|
79
79
|
# Apply component transformations
|
|
@@ -96,6 +96,100 @@ def get_component_pin_position(component: SchematicSymbol, pin_number: str) -> O
|
|
|
96
96
|
return None
|
|
97
97
|
|
|
98
98
|
|
|
99
|
+
def get_component_pin_info(
|
|
100
|
+
component: SchematicSymbol, pin_number: str
|
|
101
|
+
) -> Optional[Tuple[Point, float]]:
|
|
102
|
+
"""
|
|
103
|
+
Get the absolute position and rotation of a component pin.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
component: Component containing the pin
|
|
107
|
+
pin_number: Pin number to find
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Tuple of (absolute_position, absolute_rotation_degrees), or None if not found
|
|
111
|
+
"""
|
|
112
|
+
logger.info(f"Getting pin info for {component.reference} pin {pin_number}")
|
|
113
|
+
logger.info(f" Component position: ({component.position.x}, {component.position.y})")
|
|
114
|
+
component_rotation = getattr(component, "rotation", 0)
|
|
115
|
+
logger.info(f" Component rotation: {component_rotation}°")
|
|
116
|
+
logger.info(f" Component mirror: {getattr(component, 'mirror', None)}")
|
|
117
|
+
|
|
118
|
+
# First check if pin is already in component data
|
|
119
|
+
for pin in component.pins:
|
|
120
|
+
if pin.number == pin_number:
|
|
121
|
+
logger.info(f" Found pin {pin_number} in component data")
|
|
122
|
+
logger.info(f" Pin relative position: ({pin.position.x}, {pin.position.y})")
|
|
123
|
+
logger.info(f" Pin rotation: {pin.rotation}°")
|
|
124
|
+
|
|
125
|
+
# Apply component transformations to position
|
|
126
|
+
absolute_pos = apply_transformation(
|
|
127
|
+
(pin.position.x, pin.position.y),
|
|
128
|
+
component.position,
|
|
129
|
+
component_rotation,
|
|
130
|
+
getattr(component, "mirror", None),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Calculate absolute rotation (pin rotation + component rotation)
|
|
134
|
+
absolute_rotation = (pin.rotation + component_rotation) % 360
|
|
135
|
+
|
|
136
|
+
result_pos = Point(absolute_pos[0], absolute_pos[1])
|
|
137
|
+
logger.info(
|
|
138
|
+
f" Final absolute position: ({result_pos.x}, {result_pos.y}), rotation: {absolute_rotation}°"
|
|
139
|
+
)
|
|
140
|
+
return (result_pos, absolute_rotation)
|
|
141
|
+
|
|
142
|
+
# If not in component data, try to get from symbol library
|
|
143
|
+
logger.info(f" Pin {pin_number} not in component data, checking symbol library")
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
symbol_cache = get_symbol_cache()
|
|
147
|
+
symbol_def = symbol_cache.get_symbol(component.lib_id)
|
|
148
|
+
|
|
149
|
+
if not symbol_def:
|
|
150
|
+
logger.warning(f" Symbol definition not found for {component.lib_id}")
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
logger.info(f" Found symbol definition for {component.lib_id}")
|
|
154
|
+
|
|
155
|
+
# Look for pin in symbol definition
|
|
156
|
+
pins_found = []
|
|
157
|
+
for pin_def in symbol_def.pins:
|
|
158
|
+
pins_found.append(pin_def.number)
|
|
159
|
+
if pin_def.number == pin_number:
|
|
160
|
+
logger.info(f" Found pin {pin_number} in symbol definition")
|
|
161
|
+
|
|
162
|
+
# Get pin position and rotation from definition
|
|
163
|
+
pin_x = pin_def.position.x
|
|
164
|
+
pin_y = pin_def.position.y
|
|
165
|
+
pin_rotation = pin_def.rotation
|
|
166
|
+
logger.info(f" Symbol pin position: ({pin_x}, {pin_y}), rotation: {pin_rotation}°")
|
|
167
|
+
|
|
168
|
+
# Apply component transformations to position
|
|
169
|
+
absolute_pos = apply_transformation(
|
|
170
|
+
(pin_x, pin_y),
|
|
171
|
+
component.position,
|
|
172
|
+
component_rotation,
|
|
173
|
+
getattr(component, "mirror", None),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Calculate absolute rotation
|
|
177
|
+
absolute_rotation = (pin_rotation + component_rotation) % 360
|
|
178
|
+
|
|
179
|
+
result_pos = Point(absolute_pos[0], absolute_pos[1])
|
|
180
|
+
logger.info(
|
|
181
|
+
f" Final absolute position: ({result_pos.x}, {result_pos.y}), rotation: {absolute_rotation}°"
|
|
182
|
+
)
|
|
183
|
+
return (result_pos, absolute_rotation)
|
|
184
|
+
|
|
185
|
+
logger.warning(f" Pin {pin_number} not found in symbol. Available pins: {pins_found}")
|
|
186
|
+
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.error(f" Error accessing symbol cache: {e}")
|
|
189
|
+
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
|
|
99
193
|
def list_component_pins(component: SchematicSymbol) -> List[Tuple[str, Point]]:
|
|
100
194
|
"""
|
|
101
195
|
List all pins for a component with their absolute positions.
|
|
@@ -127,10 +221,10 @@ def list_component_pins(component: SchematicSymbol) -> List[Tuple[str, Point]]:
|
|
|
127
221
|
symbol_def = symbol_cache.get_symbol(component.lib_id)
|
|
128
222
|
|
|
129
223
|
if symbol_def:
|
|
130
|
-
for pin_def in symbol_def.
|
|
131
|
-
pin_number = pin_def.
|
|
132
|
-
pin_x = pin_def.
|
|
133
|
-
pin_y = pin_def.
|
|
224
|
+
for pin_def in symbol_def.pins:
|
|
225
|
+
pin_number = pin_def.number
|
|
226
|
+
pin_x = pin_def.position.x
|
|
227
|
+
pin_y = pin_def.position.y
|
|
134
228
|
|
|
135
229
|
absolute_pos = apply_transformation(
|
|
136
230
|
(pin_x, pin_y),
|