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
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Network connectivity analysis for KiCAD schematics.
|
|
3
|
+
|
|
4
|
+
Implements comprehensive net tracing through wires, junctions, labels,
|
|
5
|
+
hierarchical connections, and power symbols.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
12
|
+
|
|
13
|
+
from .geometry import points_equal
|
|
14
|
+
from .types import Point, SchematicSymbol, Wire, Junction, Label, LabelType
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class PinConnection:
|
|
21
|
+
"""Represents a component pin in the connectivity graph."""
|
|
22
|
+
|
|
23
|
+
reference: str # Component reference (e.g., "R1")
|
|
24
|
+
pin_number: str # Pin number (e.g., "2")
|
|
25
|
+
position: Point # Absolute position of pin
|
|
26
|
+
|
|
27
|
+
def __hash__(self):
|
|
28
|
+
return hash((self.reference, self.pin_number))
|
|
29
|
+
|
|
30
|
+
def __eq__(self, other):
|
|
31
|
+
return (self.reference == other.reference and
|
|
32
|
+
self.pin_number == other.pin_number)
|
|
33
|
+
|
|
34
|
+
def __repr__(self):
|
|
35
|
+
return f"{self.reference}.{self.pin_number}@({self.position.x:.2f},{self.position.y:.2f})"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Net:
|
|
40
|
+
"""Represents an electrical net (connected set of pins/wires/labels)."""
|
|
41
|
+
|
|
42
|
+
name: Optional[str] = None # Net name (from label, or auto-generated)
|
|
43
|
+
pins: Set[PinConnection] = field(default_factory=set) # Connected pins
|
|
44
|
+
wires: Set[str] = field(default_factory=set) # Wire UUIDs
|
|
45
|
+
junctions: Set[str] = field(default_factory=set) # Junction UUIDs
|
|
46
|
+
labels: Set[str] = field(default_factory=set) # Label UUIDs
|
|
47
|
+
points: Set[Tuple[float, float]] = field(default_factory=set) # All connection points
|
|
48
|
+
|
|
49
|
+
def add_pin(self, pin: PinConnection):
|
|
50
|
+
"""Add a pin to this net."""
|
|
51
|
+
self.pins.add(pin)
|
|
52
|
+
self.points.add((pin.position.x, pin.position.y))
|
|
53
|
+
|
|
54
|
+
def merge(self, other: "Net"):
|
|
55
|
+
"""Merge another net into this one."""
|
|
56
|
+
self.pins.update(other.pins)
|
|
57
|
+
self.wires.update(other.wires)
|
|
58
|
+
self.junctions.update(other.junctions)
|
|
59
|
+
self.labels.update(other.labels)
|
|
60
|
+
self.points.update(other.points)
|
|
61
|
+
|
|
62
|
+
# Prefer named nets over unnamed
|
|
63
|
+
if other.name and not self.name:
|
|
64
|
+
self.name = other.name
|
|
65
|
+
|
|
66
|
+
def __repr__(self):
|
|
67
|
+
name_str = f"'{self.name}'" if self.name else "unnamed"
|
|
68
|
+
return f"Net({name_str}, {len(self.pins)} pins, {len(self.wires)} wires)"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ConnectivityAnalyzer:
|
|
72
|
+
"""
|
|
73
|
+
Analyzes schematic connectivity and builds electrical nets.
|
|
74
|
+
|
|
75
|
+
Traces connections through:
|
|
76
|
+
- Direct wire-to-pin connections
|
|
77
|
+
- Junction points connecting multiple wires
|
|
78
|
+
- Labels connecting separated wire segments
|
|
79
|
+
- Global labels (cross-schematic connections)
|
|
80
|
+
- Hierarchical labels (parent-child sheet connections)
|
|
81
|
+
- Power symbols (implicit global connections)
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, tolerance: float = 0.01):
|
|
85
|
+
"""
|
|
86
|
+
Initialize connectivity analyzer.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
tolerance: Position matching tolerance in mm (default: 0.01)
|
|
90
|
+
"""
|
|
91
|
+
self.tolerance = tolerance
|
|
92
|
+
self.nets: List[Net] = []
|
|
93
|
+
self._point_to_net: Dict[Tuple[float, float], Net] = {}
|
|
94
|
+
self._pin_to_net: Dict[PinConnection, Net] = {}
|
|
95
|
+
self._label_name_to_nets: Dict[str, List[Net]] = defaultdict(list)
|
|
96
|
+
|
|
97
|
+
logger.info(f"Initialized ConnectivityAnalyzer (tolerance={tolerance}mm)")
|
|
98
|
+
|
|
99
|
+
def analyze(self, schematic, hierarchical=True) -> List[Net]:
|
|
100
|
+
"""
|
|
101
|
+
Analyze schematic connectivity and return all nets.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
schematic: Schematic object to analyze (root schematic)
|
|
105
|
+
hierarchical: If True, also analyze child sheets (default: True)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
List of Net objects representing all electrical connections
|
|
109
|
+
"""
|
|
110
|
+
logger.info("Starting connectivity analysis...")
|
|
111
|
+
|
|
112
|
+
# Collect all schematics (parent + children if hierarchical)
|
|
113
|
+
if hierarchical:
|
|
114
|
+
schematics = self._load_hierarchical_schematics(schematic)
|
|
115
|
+
logger.info(f"Analyzing {len(schematics)} schematics (hierarchical)")
|
|
116
|
+
else:
|
|
117
|
+
schematics = [schematic]
|
|
118
|
+
|
|
119
|
+
# Step 1: Build component pin positions from all schematics
|
|
120
|
+
all_pin_positions = {}
|
|
121
|
+
for sch in schematics:
|
|
122
|
+
pin_positions = self._build_pin_positions(sch)
|
|
123
|
+
all_pin_positions.update(pin_positions)
|
|
124
|
+
logger.info(f"Found {len(all_pin_positions)} component pins across all sheets")
|
|
125
|
+
|
|
126
|
+
# Step 2: Create initial nets from wire-to-pin connections
|
|
127
|
+
for sch in schematics:
|
|
128
|
+
sch_pins = {pc: pos for pc, pos in all_pin_positions.items()
|
|
129
|
+
if any(c.reference == pc.reference for c in sch.components)}
|
|
130
|
+
self._trace_wire_connections(sch, sch_pins)
|
|
131
|
+
logger.info(f"Created {len(self.nets)} nets from wire connections")
|
|
132
|
+
|
|
133
|
+
# Step 3: Merge nets connected by junctions
|
|
134
|
+
for sch in schematics:
|
|
135
|
+
self._merge_junction_nets(sch)
|
|
136
|
+
logger.info(f"After junction merging: {len(self.nets)} nets")
|
|
137
|
+
|
|
138
|
+
# Step 4: Merge nets connected by local labels
|
|
139
|
+
for sch in schematics:
|
|
140
|
+
self._merge_label_nets(sch)
|
|
141
|
+
logger.info(f"After label merging: {len(self.nets)} nets")
|
|
142
|
+
|
|
143
|
+
# Step 5: Process hierarchical connections (sheet pins ↔ hierarchical labels)
|
|
144
|
+
if hierarchical and len(schematics) > 1:
|
|
145
|
+
self._process_hierarchical_connections(schematic, schematics)
|
|
146
|
+
logger.info(f"After hierarchical connections: {len(self.nets)} nets")
|
|
147
|
+
|
|
148
|
+
# Step 6: Process power symbols (implicit global connections across ALL sheets)
|
|
149
|
+
for sch in schematics:
|
|
150
|
+
self._process_power_symbols(sch)
|
|
151
|
+
logger.info(f"After power symbols: {len(self.nets)} nets")
|
|
152
|
+
|
|
153
|
+
# Step 7: Handle global labels
|
|
154
|
+
for sch in schematics:
|
|
155
|
+
self._process_global_labels(sch)
|
|
156
|
+
logger.info(f"After global labels: {len(self.nets)} nets")
|
|
157
|
+
|
|
158
|
+
# Step 8: Auto-generate net names for unnamed nets
|
|
159
|
+
self._generate_net_names()
|
|
160
|
+
|
|
161
|
+
logger.info(f"Connectivity analysis complete: {len(self.nets)} nets")
|
|
162
|
+
return self.nets
|
|
163
|
+
|
|
164
|
+
def _build_pin_positions(self, schematic) -> Dict[PinConnection, Point]:
|
|
165
|
+
"""
|
|
166
|
+
Build mapping of all component pins to their absolute positions.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
schematic: Schematic to analyze
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Dict mapping PinConnection to absolute Point
|
|
173
|
+
"""
|
|
174
|
+
from .pin_utils import list_component_pins
|
|
175
|
+
|
|
176
|
+
pin_positions = {}
|
|
177
|
+
|
|
178
|
+
for component in schematic.components:
|
|
179
|
+
# Get all pins for this component
|
|
180
|
+
pins = list_component_pins(component)
|
|
181
|
+
|
|
182
|
+
for pin_number, pin_position in pins:
|
|
183
|
+
if pin_position is not None:
|
|
184
|
+
pin_conn = PinConnection(
|
|
185
|
+
reference=component.reference,
|
|
186
|
+
pin_number=pin_number,
|
|
187
|
+
position=pin_position
|
|
188
|
+
)
|
|
189
|
+
pin_positions[pin_conn] = pin_position
|
|
190
|
+
logger.debug(f" {pin_conn}")
|
|
191
|
+
|
|
192
|
+
return pin_positions
|
|
193
|
+
|
|
194
|
+
def _trace_wire_connections(self, schematic, pin_positions: Dict[PinConnection, Point]):
|
|
195
|
+
"""
|
|
196
|
+
Create initial nets by tracing wire-to-pin connections.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
schematic: Schematic to analyze
|
|
200
|
+
pin_positions: Mapping of pins to positions
|
|
201
|
+
"""
|
|
202
|
+
for wire in schematic.wires:
|
|
203
|
+
# Get wire endpoints
|
|
204
|
+
wire_points = wire.points
|
|
205
|
+
if len(wire_points) < 2:
|
|
206
|
+
logger.warning(f"Wire {wire.uuid} has < 2 points, skipping")
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
# Find which pins connect to this wire
|
|
210
|
+
connected_pins = set()
|
|
211
|
+
|
|
212
|
+
for pin_conn, pin_pos in pin_positions.items():
|
|
213
|
+
# Check if pin connects to any point on the wire
|
|
214
|
+
for wire_point in wire_points:
|
|
215
|
+
if points_equal(wire_point, pin_pos, self.tolerance):
|
|
216
|
+
connected_pins.add(pin_conn)
|
|
217
|
+
logger.debug(f" Wire {wire.uuid} connects to {pin_conn}")
|
|
218
|
+
break
|
|
219
|
+
|
|
220
|
+
# Create or update net for this wire
|
|
221
|
+
# Always create a net for the wire, even if no pins connect yet
|
|
222
|
+
# (labels, junctions, or hierarchical connections may merge it later)
|
|
223
|
+
if connected_pins:
|
|
224
|
+
self._add_wire_to_net(wire, connected_pins, wire_points)
|
|
225
|
+
else:
|
|
226
|
+
# Create net for wire without pins (will be merged via labels/junctions)
|
|
227
|
+
net = Net()
|
|
228
|
+
net.wires.add(wire.uuid)
|
|
229
|
+
for point in wire_points:
|
|
230
|
+
net.points.add((point.x, point.y))
|
|
231
|
+
self._point_to_net[(point.x, point.y)] = net
|
|
232
|
+
self.nets.append(net)
|
|
233
|
+
logger.debug(f" Created net for wire {wire.uuid} without pins")
|
|
234
|
+
|
|
235
|
+
def _add_wire_to_net(self, wire: Wire, pins: Set[PinConnection], wire_points: List[Point]):
|
|
236
|
+
"""
|
|
237
|
+
Add wire and its connected pins to a net (create new or merge existing).
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
wire: Wire object
|
|
241
|
+
pins: Set of pins connected to this wire
|
|
242
|
+
wire_points: Points along the wire
|
|
243
|
+
"""
|
|
244
|
+
# Check if any of these pins are already in a net
|
|
245
|
+
existing_nets = set()
|
|
246
|
+
for pin in pins:
|
|
247
|
+
if pin in self._pin_to_net:
|
|
248
|
+
existing_nets.add(self._pin_to_net[pin])
|
|
249
|
+
|
|
250
|
+
if existing_nets:
|
|
251
|
+
# Merge all existing nets into the first one
|
|
252
|
+
primary_net = existing_nets.pop()
|
|
253
|
+
for other_net in existing_nets:
|
|
254
|
+
primary_net.merge(other_net)
|
|
255
|
+
self.nets.remove(other_net)
|
|
256
|
+
|
|
257
|
+
# Add new pins and wire to primary net
|
|
258
|
+
for pin in pins:
|
|
259
|
+
primary_net.add_pin(pin)
|
|
260
|
+
self._pin_to_net[pin] = primary_net
|
|
261
|
+
|
|
262
|
+
primary_net.wires.add(wire.uuid)
|
|
263
|
+
for point in wire_points:
|
|
264
|
+
primary_net.points.add((point.x, point.y))
|
|
265
|
+
self._point_to_net[(point.x, point.y)] = primary_net
|
|
266
|
+
else:
|
|
267
|
+
# Create new net
|
|
268
|
+
net = Net()
|
|
269
|
+
for pin in pins:
|
|
270
|
+
net.add_pin(pin)
|
|
271
|
+
self._pin_to_net[pin] = net
|
|
272
|
+
|
|
273
|
+
net.wires.add(wire.uuid)
|
|
274
|
+
for point in wire_points:
|
|
275
|
+
net.points.add((point.x, point.y))
|
|
276
|
+
self._point_to_net[(point.x, point.y)] = net
|
|
277
|
+
|
|
278
|
+
self.nets.append(net)
|
|
279
|
+
logger.debug(f" Created new {net}")
|
|
280
|
+
|
|
281
|
+
def _merge_junction_nets(self, schematic):
|
|
282
|
+
"""
|
|
283
|
+
Merge nets that are connected by junction points.
|
|
284
|
+
|
|
285
|
+
Also includes wires at the junction that don't connect to pins
|
|
286
|
+
(e.g., test point taps, voltage monitoring points).
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
schematic: Schematic to analyze
|
|
290
|
+
"""
|
|
291
|
+
for junction in schematic.junctions:
|
|
292
|
+
junc_pos = junction.position
|
|
293
|
+
|
|
294
|
+
# Find all nets that have points at this junction position
|
|
295
|
+
nets_at_junction = []
|
|
296
|
+
|
|
297
|
+
for net in self.nets:
|
|
298
|
+
for point in net.points:
|
|
299
|
+
if points_equal(Point(point[0], point[1]), junc_pos, self.tolerance):
|
|
300
|
+
if net not in nets_at_junction: # Avoid duplicates
|
|
301
|
+
nets_at_junction.append(net)
|
|
302
|
+
break
|
|
303
|
+
|
|
304
|
+
# Also find wires at this junction that aren't in any net yet
|
|
305
|
+
# (e.g., tap wires that don't connect to component pins)
|
|
306
|
+
unconnected_wires = []
|
|
307
|
+
for wire in schematic.wires:
|
|
308
|
+
# Check if wire has a point at junction
|
|
309
|
+
wire_at_junction = False
|
|
310
|
+
for wire_point in wire.points:
|
|
311
|
+
if points_equal(wire_point, junc_pos, self.tolerance):
|
|
312
|
+
wire_at_junction = True
|
|
313
|
+
break
|
|
314
|
+
|
|
315
|
+
if wire_at_junction:
|
|
316
|
+
# Check if this wire is already in a net
|
|
317
|
+
wire_in_net = False
|
|
318
|
+
for net in nets_at_junction:
|
|
319
|
+
if wire.uuid in net.wires:
|
|
320
|
+
wire_in_net = True
|
|
321
|
+
break
|
|
322
|
+
|
|
323
|
+
if not wire_in_net:
|
|
324
|
+
unconnected_wires.append(wire)
|
|
325
|
+
|
|
326
|
+
# If we have nets at junction, merge them and add unconnected wires
|
|
327
|
+
if len(nets_at_junction) >= 1:
|
|
328
|
+
primary_net = nets_at_junction[0]
|
|
329
|
+
|
|
330
|
+
# Merge other nets
|
|
331
|
+
for other_net in nets_at_junction[1:]:
|
|
332
|
+
primary_net.merge(other_net)
|
|
333
|
+
|
|
334
|
+
# Update all pin mappings
|
|
335
|
+
for pin in other_net.pins:
|
|
336
|
+
self._pin_to_net[pin] = primary_net
|
|
337
|
+
|
|
338
|
+
# Update all point mappings
|
|
339
|
+
for point in other_net.points:
|
|
340
|
+
self._point_to_net[point] = primary_net
|
|
341
|
+
|
|
342
|
+
self.nets.remove(other_net)
|
|
343
|
+
|
|
344
|
+
# Add unconnected wires to the primary net
|
|
345
|
+
for wire in unconnected_wires:
|
|
346
|
+
primary_net.wires.add(wire.uuid)
|
|
347
|
+
for point in wire.points:
|
|
348
|
+
primary_net.points.add((point.x, point.y))
|
|
349
|
+
self._point_to_net[(point.x, point.y)] = primary_net
|
|
350
|
+
logger.debug(f"Added unconnected wire {wire.uuid[:8]} to net at junction")
|
|
351
|
+
|
|
352
|
+
primary_net.junctions.add(junction.uuid)
|
|
353
|
+
|
|
354
|
+
def _merge_label_nets(self, schematic):
|
|
355
|
+
"""
|
|
356
|
+
Merge nets that are connected by labels with the same name.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
schematic: Schematic to analyze
|
|
360
|
+
"""
|
|
361
|
+
# Process local labels only (global labels handled separately)
|
|
362
|
+
local_labels = [label for label in schematic.labels
|
|
363
|
+
if hasattr(label, '_data') and label._data.label_type == LabelType.LOCAL]
|
|
364
|
+
|
|
365
|
+
for label in local_labels:
|
|
366
|
+
label_pos = label.position
|
|
367
|
+
|
|
368
|
+
# Find which net this label is on
|
|
369
|
+
net_for_label = None
|
|
370
|
+
for net in self.nets:
|
|
371
|
+
for point in net.points:
|
|
372
|
+
if points_equal(Point(point[0], point[1]), label_pos, self.tolerance):
|
|
373
|
+
net_for_label = net
|
|
374
|
+
break
|
|
375
|
+
if net_for_label:
|
|
376
|
+
break
|
|
377
|
+
|
|
378
|
+
if net_for_label:
|
|
379
|
+
# Set or merge net name
|
|
380
|
+
if not net_for_label.name:
|
|
381
|
+
net_for_label.name = label.text
|
|
382
|
+
logger.debug(f"Named {net_for_label} from label")
|
|
383
|
+
|
|
384
|
+
net_for_label.labels.add(label.uuid)
|
|
385
|
+
self._label_name_to_nets[label.text].append(net_for_label)
|
|
386
|
+
|
|
387
|
+
# Merge nets with the same label name
|
|
388
|
+
for label_name, nets_with_label in self._label_name_to_nets.items():
|
|
389
|
+
if len(nets_with_label) > 1:
|
|
390
|
+
logger.debug(f"Label '{label_name}' connects {len(nets_with_label)} nets")
|
|
391
|
+
|
|
392
|
+
primary_net = nets_with_label[0]
|
|
393
|
+
for other_net in nets_with_label[1:]:
|
|
394
|
+
if other_net in self.nets: # Check if not already merged
|
|
395
|
+
primary_net.merge(other_net)
|
|
396
|
+
|
|
397
|
+
# Update mappings
|
|
398
|
+
for pin in other_net.pins:
|
|
399
|
+
self._pin_to_net[pin] = primary_net
|
|
400
|
+
for point in other_net.points:
|
|
401
|
+
self._point_to_net[point] = primary_net
|
|
402
|
+
|
|
403
|
+
self.nets.remove(other_net)
|
|
404
|
+
|
|
405
|
+
def _process_power_symbols(self, schematic):
|
|
406
|
+
"""
|
|
407
|
+
Process power symbols and create implicit global connections.
|
|
408
|
+
|
|
409
|
+
Power symbols (like GND, VCC, +5V) create implicit global nets.
|
|
410
|
+
All power symbols with the same value are electrically connected,
|
|
411
|
+
even if they're not physically wired together.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
schematic: Schematic to analyze
|
|
415
|
+
"""
|
|
416
|
+
# Group power symbols by their value property
|
|
417
|
+
power_symbol_nets_by_value = defaultdict(list)
|
|
418
|
+
|
|
419
|
+
for component in schematic.components:
|
|
420
|
+
# Identify power symbols by lib_id pattern
|
|
421
|
+
if component.lib_id.startswith('power:'):
|
|
422
|
+
power_value = component.value
|
|
423
|
+
|
|
424
|
+
logger.debug(f"Found power symbol: {component.reference} (value={power_value})")
|
|
425
|
+
|
|
426
|
+
# Find the net this power symbol is connected to
|
|
427
|
+
# Power symbols have a single pin (usually pin "1")
|
|
428
|
+
power_pin_conn = None
|
|
429
|
+
for pin_conn in self._pin_to_net.keys():
|
|
430
|
+
if pin_conn.reference == component.reference:
|
|
431
|
+
power_pin_conn = pin_conn
|
|
432
|
+
break
|
|
433
|
+
|
|
434
|
+
if power_pin_conn:
|
|
435
|
+
net = self._pin_to_net[power_pin_conn]
|
|
436
|
+
power_symbol_nets_by_value[power_value].append(net)
|
|
437
|
+
logger.debug(f" Power symbol {component.reference} on net '{net.name}'")
|
|
438
|
+
|
|
439
|
+
# Merge all nets with the same power symbol value
|
|
440
|
+
for power_value, nets_to_merge in power_symbol_nets_by_value.items():
|
|
441
|
+
if len(nets_to_merge) > 1:
|
|
442
|
+
logger.debug(f"Merging {len(nets_to_merge)} nets for power symbol '{power_value}'")
|
|
443
|
+
|
|
444
|
+
# Merge all nets into the first one
|
|
445
|
+
primary_net = nets_to_merge[0]
|
|
446
|
+
|
|
447
|
+
# Set the net name from power symbol value
|
|
448
|
+
primary_net.name = power_value
|
|
449
|
+
|
|
450
|
+
for other_net in nets_to_merge[1:]:
|
|
451
|
+
if other_net in self.nets: # Check if not already merged
|
|
452
|
+
primary_net.merge(other_net)
|
|
453
|
+
|
|
454
|
+
# Update mappings
|
|
455
|
+
for pin in other_net.pins:
|
|
456
|
+
self._pin_to_net[pin] = primary_net
|
|
457
|
+
for point in other_net.points:
|
|
458
|
+
self._point_to_net[point] = primary_net
|
|
459
|
+
|
|
460
|
+
self.nets.remove(other_net)
|
|
461
|
+
elif len(nets_to_merge) == 1:
|
|
462
|
+
# Single power symbol - just name the net
|
|
463
|
+
nets_to_merge[0].name = power_value
|
|
464
|
+
|
|
465
|
+
def _process_global_labels(self, schematic):
|
|
466
|
+
"""
|
|
467
|
+
Process global labels (cross-schematic connections).
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
schematic: Schematic to analyze
|
|
471
|
+
"""
|
|
472
|
+
# TODO: Implement global label handling
|
|
473
|
+
# Global labels with same name should connect across schematics
|
|
474
|
+
pass
|
|
475
|
+
|
|
476
|
+
def _generate_net_names(self):
|
|
477
|
+
"""Generate names for nets that don't have explicit names."""
|
|
478
|
+
unnamed_counter = 1
|
|
479
|
+
|
|
480
|
+
for net in self.nets:
|
|
481
|
+
if not net.name:
|
|
482
|
+
# Try to name from connected component pins
|
|
483
|
+
if net.pins:
|
|
484
|
+
first_pin = next(iter(net.pins))
|
|
485
|
+
net.name = f"Net-({first_pin.reference}-Pad{first_pin.pin_number})"
|
|
486
|
+
else:
|
|
487
|
+
net.name = f"Net-(unnamed-{unnamed_counter})"
|
|
488
|
+
unnamed_counter += 1
|
|
489
|
+
|
|
490
|
+
def are_connected(self, ref1: str, pin1: str, ref2: str, pin2: str) -> bool:
|
|
491
|
+
"""
|
|
492
|
+
Check if two pins are electrically connected.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
ref1: First component reference
|
|
496
|
+
pin1: First pin number
|
|
497
|
+
ref2: Second component reference
|
|
498
|
+
pin2: Second pin number
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
True if pins are on the same net, False otherwise
|
|
502
|
+
"""
|
|
503
|
+
# Find pins in the connectivity graph
|
|
504
|
+
pin_conn1 = None
|
|
505
|
+
pin_conn2 = None
|
|
506
|
+
|
|
507
|
+
for pin in self._pin_to_net.keys():
|
|
508
|
+
if pin.reference == ref1 and pin.pin_number == pin1:
|
|
509
|
+
pin_conn1 = pin
|
|
510
|
+
if pin.reference == ref2 and pin.pin_number == pin2:
|
|
511
|
+
pin_conn2 = pin
|
|
512
|
+
|
|
513
|
+
if not pin_conn1 or not pin_conn2:
|
|
514
|
+
return False
|
|
515
|
+
|
|
516
|
+
# Check if both pins are in the same net
|
|
517
|
+
net1 = self._pin_to_net.get(pin_conn1)
|
|
518
|
+
net2 = self._pin_to_net.get(pin_conn2)
|
|
519
|
+
|
|
520
|
+
return net1 is not None and net1 is net2
|
|
521
|
+
|
|
522
|
+
def _load_hierarchical_schematics(self, root_schematic):
|
|
523
|
+
"""
|
|
524
|
+
Load root schematic and all child schematics.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
root_schematic: Root schematic object
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
List of all schematics (root + children)
|
|
531
|
+
"""
|
|
532
|
+
from pathlib import Path
|
|
533
|
+
|
|
534
|
+
schematics = [root_schematic]
|
|
535
|
+
|
|
536
|
+
# Check if root schematic has hierarchical sheets
|
|
537
|
+
if not hasattr(root_schematic, '_data') or 'sheets' not in root_schematic._data:
|
|
538
|
+
return schematics
|
|
539
|
+
|
|
540
|
+
sheets = root_schematic._data.get('sheets', [])
|
|
541
|
+
|
|
542
|
+
# Load each child schematic
|
|
543
|
+
root_path = Path(root_schematic.file_path) if root_schematic.file_path else None
|
|
544
|
+
|
|
545
|
+
for sheet in sheets:
|
|
546
|
+
sheet_filename = sheet.get('filename')
|
|
547
|
+
if not sheet_filename:
|
|
548
|
+
continue
|
|
549
|
+
|
|
550
|
+
# Build path to child schematic
|
|
551
|
+
if root_path:
|
|
552
|
+
child_path = root_path.parent / sheet_filename
|
|
553
|
+
else:
|
|
554
|
+
child_path = Path(sheet_filename)
|
|
555
|
+
|
|
556
|
+
if child_path.exists():
|
|
557
|
+
try:
|
|
558
|
+
# Import Schematic class - use absolute import to avoid circular dependency
|
|
559
|
+
import kicad_sch_api as ksa
|
|
560
|
+
child_sch = ksa.Schematic.load(str(child_path))
|
|
561
|
+
schematics.append(child_sch)
|
|
562
|
+
logger.info(f"Loaded child schematic: {sheet_filename}")
|
|
563
|
+
except Exception as e:
|
|
564
|
+
logger.warning(f"Could not load child schematic {sheet_filename}: {e}")
|
|
565
|
+
else:
|
|
566
|
+
logger.warning(f"Child schematic not found: {child_path}")
|
|
567
|
+
|
|
568
|
+
return schematics
|
|
569
|
+
|
|
570
|
+
def _process_hierarchical_connections(self, root_schematic, all_schematics):
|
|
571
|
+
"""
|
|
572
|
+
Process hierarchical connections between parent and child sheets.
|
|
573
|
+
|
|
574
|
+
Connects sheet pins in parent to hierarchical labels in child sheets.
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
root_schematic: Root schematic with hierarchical sheets
|
|
578
|
+
all_schematics: List of all schematics (root + children)
|
|
579
|
+
"""
|
|
580
|
+
from pathlib import Path
|
|
581
|
+
|
|
582
|
+
if not hasattr(root_schematic, '_data') or 'sheets' not in root_schematic._data:
|
|
583
|
+
return
|
|
584
|
+
|
|
585
|
+
sheets = root_schematic._data.get('sheets', [])
|
|
586
|
+
|
|
587
|
+
for sheet_data in sheets:
|
|
588
|
+
sheet_filename = sheet_data.get('filename')
|
|
589
|
+
sheet_pins = sheet_data.get('pins', [])
|
|
590
|
+
|
|
591
|
+
if not sheet_filename or not sheet_pins:
|
|
592
|
+
continue
|
|
593
|
+
|
|
594
|
+
# Find the child schematic
|
|
595
|
+
child_sch = None
|
|
596
|
+
for sch in all_schematics:
|
|
597
|
+
if sch.file_path and Path(sch.file_path).name == sheet_filename:
|
|
598
|
+
child_sch = sch
|
|
599
|
+
break
|
|
600
|
+
|
|
601
|
+
if not child_sch:
|
|
602
|
+
logger.warning(f"Child schematic not found for sheet: {sheet_filename}")
|
|
603
|
+
continue
|
|
604
|
+
|
|
605
|
+
# For each sheet pin, find matching hierarchical label in child
|
|
606
|
+
for pin_data in sheet_pins:
|
|
607
|
+
pin_name = pin_data.get('name')
|
|
608
|
+
pin_position = pin_data.get('position')
|
|
609
|
+
|
|
610
|
+
if not pin_name or not pin_position:
|
|
611
|
+
continue
|
|
612
|
+
|
|
613
|
+
pin_pos = Point(pin_position['x'], pin_position['y'])
|
|
614
|
+
|
|
615
|
+
# Find net at sheet pin position in parent
|
|
616
|
+
parent_net = None
|
|
617
|
+
for net in self.nets:
|
|
618
|
+
for point in net.points:
|
|
619
|
+
if points_equal(Point(point[0], point[1]), pin_pos, self.tolerance):
|
|
620
|
+
parent_net = net
|
|
621
|
+
break
|
|
622
|
+
if parent_net:
|
|
623
|
+
break
|
|
624
|
+
|
|
625
|
+
if not parent_net:
|
|
626
|
+
logger.debug(f"No net found at sheet pin '{pin_name}' position")
|
|
627
|
+
continue
|
|
628
|
+
|
|
629
|
+
# Find matching hierarchical label in child schematic
|
|
630
|
+
for hier_label in child_sch.hierarchical_labels:
|
|
631
|
+
if hier_label.text == pin_name:
|
|
632
|
+
label_pos = hier_label.position
|
|
633
|
+
|
|
634
|
+
# Find net at hierarchical label position in child
|
|
635
|
+
child_net = None
|
|
636
|
+
for net in self.nets:
|
|
637
|
+
for point in net.points:
|
|
638
|
+
if points_equal(Point(point[0], point[1]), label_pos, self.tolerance):
|
|
639
|
+
child_net = net
|
|
640
|
+
break
|
|
641
|
+
if child_net:
|
|
642
|
+
break
|
|
643
|
+
|
|
644
|
+
if child_net and child_net is not parent_net:
|
|
645
|
+
# Merge child net into parent net
|
|
646
|
+
logger.debug(f"Merging nets via hierarchical connection '{pin_name}'")
|
|
647
|
+
parent_net.merge(child_net)
|
|
648
|
+
|
|
649
|
+
# Update mappings
|
|
650
|
+
for pin in child_net.pins:
|
|
651
|
+
self._pin_to_net[pin] = parent_net
|
|
652
|
+
for point in child_net.points:
|
|
653
|
+
self._point_to_net[point] = parent_net
|
|
654
|
+
|
|
655
|
+
self.nets.remove(child_net)
|
|
656
|
+
|
|
657
|
+
break
|
|
658
|
+
|
|
659
|
+
def get_net_for_pin(self, reference: str, pin_number: str) -> Optional[Net]:
|
|
660
|
+
"""
|
|
661
|
+
Get the net connected to a specific pin.
|
|
662
|
+
|
|
663
|
+
Args:
|
|
664
|
+
reference: Component reference
|
|
665
|
+
pin_number: Pin number
|
|
666
|
+
|
|
667
|
+
Returns:
|
|
668
|
+
Net object if pin is connected, None otherwise
|
|
669
|
+
"""
|
|
670
|
+
for pin in self._pin_to_net.keys():
|
|
671
|
+
if pin.reference == reference and pin.pin_number == pin_number:
|
|
672
|
+
return self._pin_to_net[pin]
|
|
673
|
+
|
|
674
|
+
return None
|
|
675
|
+
|
|
676
|
+
def get_connected_pins(self, reference: str, pin_number: str) -> List[Tuple[str, str]]:
|
|
677
|
+
"""
|
|
678
|
+
Get all pins connected to a specific pin.
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
reference: Component reference
|
|
682
|
+
pin_number: Pin number
|
|
683
|
+
|
|
684
|
+
Returns:
|
|
685
|
+
List of (reference, pin_number) tuples for connected pins
|
|
686
|
+
"""
|
|
687
|
+
net = self.get_net_for_pin(reference, pin_number)
|
|
688
|
+
if not net:
|
|
689
|
+
return []
|
|
690
|
+
|
|
691
|
+
return [(pin.reference, pin.pin_number) for pin in net.pins
|
|
692
|
+
if not (pin.reference == reference and pin.pin_number == pin_number)]
|