kicad-sch-api 0.0.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.
Potentially problematic release.
This version of kicad-sch-api might be problematic. Click here for more details.
- kicad_sch_api/__init__.py +112 -0
- kicad_sch_api/core/__init__.py +23 -0
- kicad_sch_api/core/components.py +652 -0
- kicad_sch_api/core/formatter.py +312 -0
- kicad_sch_api/core/parser.py +434 -0
- kicad_sch_api/core/schematic.py +478 -0
- kicad_sch_api/core/types.py +369 -0
- kicad_sch_api/library/__init__.py +10 -0
- kicad_sch_api/library/cache.py +548 -0
- kicad_sch_api/mcp/__init__.py +5 -0
- kicad_sch_api/mcp/server.py +500 -0
- kicad_sch_api/py.typed +1 -0
- kicad_sch_api/utils/__init__.py +15 -0
- kicad_sch_api/utils/validation.py +447 -0
- kicad_sch_api-0.0.1.dist-info/METADATA +226 -0
- kicad_sch_api-0.0.1.dist-info/RECORD +20 -0
- kicad_sch_api-0.0.1.dist-info/WHEEL +5 -0
- kicad_sch_api-0.0.1.dist-info/entry_points.txt +2 -0
- kicad_sch_api-0.0.1.dist-info/licenses/LICENSE +21 -0
- kicad_sch_api-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core data types for KiCAD schematic manipulation.
|
|
3
|
+
|
|
4
|
+
This module defines the fundamental data structures used throughout kicad-sch-api,
|
|
5
|
+
providing a clean, type-safe interface for working with schematic elements.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
11
|
+
from uuid import uuid4
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Point:
|
|
16
|
+
"""2D point with x,y coordinates in mm."""
|
|
17
|
+
|
|
18
|
+
x: float
|
|
19
|
+
y: float
|
|
20
|
+
|
|
21
|
+
def __post_init__(self):
|
|
22
|
+
# Ensure coordinates are float
|
|
23
|
+
object.__setattr__(self, "x", float(self.x))
|
|
24
|
+
object.__setattr__(self, "y", float(self.y))
|
|
25
|
+
|
|
26
|
+
def distance_to(self, other: "Point") -> float:
|
|
27
|
+
"""Calculate distance to another point."""
|
|
28
|
+
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
|
|
29
|
+
|
|
30
|
+
def offset(self, dx: float, dy: float) -> "Point":
|
|
31
|
+
"""Create new point offset by dx, dy."""
|
|
32
|
+
return Point(self.x + dx, self.y + dy)
|
|
33
|
+
|
|
34
|
+
def __str__(self) -> str:
|
|
35
|
+
return f"({self.x:.3f}, {self.y:.3f})"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class Rectangle:
|
|
40
|
+
"""Rectangle defined by two corner points."""
|
|
41
|
+
|
|
42
|
+
top_left: Point
|
|
43
|
+
bottom_right: Point
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def width(self) -> float:
|
|
47
|
+
"""Rectangle width."""
|
|
48
|
+
return abs(self.bottom_right.x - self.top_left.x)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def height(self) -> float:
|
|
52
|
+
"""Rectangle height."""
|
|
53
|
+
return abs(self.bottom_right.y - self.top_left.y)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def center(self) -> Point:
|
|
57
|
+
"""Rectangle center point."""
|
|
58
|
+
return Point(
|
|
59
|
+
(self.top_left.x + self.bottom_right.x) / 2, (self.top_left.y + self.bottom_right.y) / 2
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def contains(self, point: Point) -> bool:
|
|
63
|
+
"""Check if point is inside rectangle."""
|
|
64
|
+
return (
|
|
65
|
+
self.top_left.x <= point.x <= self.bottom_right.x
|
|
66
|
+
and self.top_left.y <= point.y <= self.bottom_right.y
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class PinType(Enum):
|
|
71
|
+
"""KiCAD pin electrical types."""
|
|
72
|
+
|
|
73
|
+
INPUT = "input"
|
|
74
|
+
OUTPUT = "output"
|
|
75
|
+
BIDIRECTIONAL = "bidirectional"
|
|
76
|
+
TRISTATE = "tri_state"
|
|
77
|
+
PASSIVE = "passive"
|
|
78
|
+
FREE = "free"
|
|
79
|
+
UNSPECIFIED = "unspecified"
|
|
80
|
+
POWER_IN = "power_in"
|
|
81
|
+
POWER_OUT = "power_out"
|
|
82
|
+
OPEN_COLLECTOR = "open_collector"
|
|
83
|
+
OPEN_EMITTER = "open_emitter"
|
|
84
|
+
NO_CONNECT = "no_connect"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class PinShape(Enum):
|
|
88
|
+
"""KiCAD pin graphical shapes."""
|
|
89
|
+
|
|
90
|
+
LINE = "line"
|
|
91
|
+
INVERTED = "inverted"
|
|
92
|
+
CLOCK = "clock"
|
|
93
|
+
INVERTED_CLOCK = "inverted_clock"
|
|
94
|
+
INPUT_LOW = "input_low"
|
|
95
|
+
CLOCK_LOW = "clock_low"
|
|
96
|
+
OUTPUT_LOW = "output_low"
|
|
97
|
+
EDGE_CLOCK_HIGH = "edge_clock_high"
|
|
98
|
+
NON_LOGIC = "non_logic"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class SchematicPin:
|
|
103
|
+
"""Pin definition for schematic symbols."""
|
|
104
|
+
|
|
105
|
+
number: str
|
|
106
|
+
name: str
|
|
107
|
+
position: Point
|
|
108
|
+
pin_type: PinType = PinType.PASSIVE
|
|
109
|
+
pin_shape: PinShape = PinShape.LINE
|
|
110
|
+
length: float = 2.54 # Standard pin length in mm
|
|
111
|
+
rotation: float = 0.0 # Rotation in degrees
|
|
112
|
+
|
|
113
|
+
def __post_init__(self):
|
|
114
|
+
# Ensure types are correct
|
|
115
|
+
self.pin_type = PinType(self.pin_type) if isinstance(self.pin_type, str) else self.pin_type
|
|
116
|
+
self.pin_shape = (
|
|
117
|
+
PinShape(self.pin_shape) if isinstance(self.pin_shape, str) else self.pin_shape
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class SchematicSymbol:
|
|
123
|
+
"""Component symbol in a schematic."""
|
|
124
|
+
|
|
125
|
+
uuid: str
|
|
126
|
+
lib_id: str # e.g., "Device:R"
|
|
127
|
+
position: Point
|
|
128
|
+
reference: str # e.g., "R1"
|
|
129
|
+
value: str = ""
|
|
130
|
+
footprint: Optional[str] = None
|
|
131
|
+
properties: Dict[str, str] = field(default_factory=dict)
|
|
132
|
+
pins: List[SchematicPin] = field(default_factory=list)
|
|
133
|
+
rotation: float = 0.0
|
|
134
|
+
in_bom: bool = True
|
|
135
|
+
on_board: bool = True
|
|
136
|
+
unit: int = 1
|
|
137
|
+
|
|
138
|
+
def __post_init__(self):
|
|
139
|
+
# Generate UUID if not provided
|
|
140
|
+
if not self.uuid:
|
|
141
|
+
self.uuid = str(uuid4())
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def library(self) -> str:
|
|
145
|
+
"""Extract library name from lib_id."""
|
|
146
|
+
return self.lib_id.split(":")[0] if ":" in self.lib_id else ""
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def symbol_name(self) -> str:
|
|
150
|
+
"""Extract symbol name from lib_id."""
|
|
151
|
+
return self.lib_id.split(":")[-1] if ":" in self.lib_id else self.lib_id
|
|
152
|
+
|
|
153
|
+
def get_pin(self, pin_number: str) -> Optional[SchematicPin]:
|
|
154
|
+
"""Get pin by number."""
|
|
155
|
+
for pin in self.pins:
|
|
156
|
+
if pin.number == pin_number:
|
|
157
|
+
return pin
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
def get_pin_position(self, pin_number: str) -> Optional[Point]:
|
|
161
|
+
"""Get absolute position of a pin."""
|
|
162
|
+
pin = self.get_pin(pin_number)
|
|
163
|
+
if not pin:
|
|
164
|
+
return None
|
|
165
|
+
# TODO: Apply rotation and symbol position transformation
|
|
166
|
+
return Point(self.position.x + pin.position.x, self.position.y + pin.position.y)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class WireType(Enum):
|
|
170
|
+
"""Wire types in KiCAD schematics."""
|
|
171
|
+
|
|
172
|
+
WIRE = "wire"
|
|
173
|
+
BUS = "bus"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@dataclass
|
|
177
|
+
class Wire:
|
|
178
|
+
"""Wire connection in schematic."""
|
|
179
|
+
|
|
180
|
+
uuid: str
|
|
181
|
+
start: Point
|
|
182
|
+
end: Point
|
|
183
|
+
wire_type: WireType = WireType.WIRE
|
|
184
|
+
stroke_width: float = 0.0
|
|
185
|
+
|
|
186
|
+
def __post_init__(self):
|
|
187
|
+
if not self.uuid:
|
|
188
|
+
self.uuid = str(uuid4())
|
|
189
|
+
|
|
190
|
+
self.wire_type = (
|
|
191
|
+
WireType(self.wire_type) if isinstance(self.wire_type, str) else self.wire_type
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def length(self) -> float:
|
|
196
|
+
"""Wire length."""
|
|
197
|
+
return self.start.distance_to(self.end)
|
|
198
|
+
|
|
199
|
+
def is_horizontal(self) -> bool:
|
|
200
|
+
"""Check if wire is horizontal."""
|
|
201
|
+
return abs(self.start.y - self.end.y) < 0.001
|
|
202
|
+
|
|
203
|
+
def is_vertical(self) -> bool:
|
|
204
|
+
"""Check if wire is vertical."""
|
|
205
|
+
return abs(self.start.x - self.end.x) < 0.001
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@dataclass
|
|
209
|
+
class Junction:
|
|
210
|
+
"""Junction point where multiple wires meet."""
|
|
211
|
+
|
|
212
|
+
uuid: str
|
|
213
|
+
position: Point
|
|
214
|
+
diameter: float = 1.27 # Standard junction diameter
|
|
215
|
+
|
|
216
|
+
def __post_init__(self):
|
|
217
|
+
if not self.uuid:
|
|
218
|
+
self.uuid = str(uuid4())
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class LabelType(Enum):
|
|
222
|
+
"""Label types in KiCAD schematics."""
|
|
223
|
+
|
|
224
|
+
LOCAL = "label"
|
|
225
|
+
GLOBAL = "global_label"
|
|
226
|
+
HIERARCHICAL = "hierarchical_label"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@dataclass
|
|
230
|
+
class Label:
|
|
231
|
+
"""Text label in schematic."""
|
|
232
|
+
|
|
233
|
+
uuid: str
|
|
234
|
+
position: Point
|
|
235
|
+
text: str
|
|
236
|
+
label_type: LabelType = LabelType.LOCAL
|
|
237
|
+
rotation: float = 0.0
|
|
238
|
+
size: float = 1.27
|
|
239
|
+
|
|
240
|
+
def __post_init__(self):
|
|
241
|
+
if not self.uuid:
|
|
242
|
+
self.uuid = str(uuid4())
|
|
243
|
+
|
|
244
|
+
self.label_type = (
|
|
245
|
+
LabelType(self.label_type) if isinstance(self.label_type, str) else self.label_type
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@dataclass
|
|
250
|
+
class Net:
|
|
251
|
+
"""Electrical net connecting components."""
|
|
252
|
+
|
|
253
|
+
name: str
|
|
254
|
+
components: List[Tuple[str, str]] = field(default_factory=list) # (reference, pin) tuples
|
|
255
|
+
wires: List[str] = field(default_factory=list) # Wire UUIDs
|
|
256
|
+
labels: List[str] = field(default_factory=list) # Label UUIDs
|
|
257
|
+
|
|
258
|
+
def add_connection(self, reference: str, pin: str):
|
|
259
|
+
"""Add component pin to net."""
|
|
260
|
+
connection = (reference, pin)
|
|
261
|
+
if connection not in self.components:
|
|
262
|
+
self.components.append(connection)
|
|
263
|
+
|
|
264
|
+
def remove_connection(self, reference: str, pin: str):
|
|
265
|
+
"""Remove component pin from net."""
|
|
266
|
+
connection = (reference, pin)
|
|
267
|
+
if connection in self.components:
|
|
268
|
+
self.components.remove(connection)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@dataclass
|
|
272
|
+
class Sheet:
|
|
273
|
+
"""Hierarchical sheet in schematic."""
|
|
274
|
+
|
|
275
|
+
uuid: str
|
|
276
|
+
position: Point
|
|
277
|
+
size: Point # Width, height
|
|
278
|
+
name: str
|
|
279
|
+
filename: str
|
|
280
|
+
pins: List["SheetPin"] = field(default_factory=list)
|
|
281
|
+
|
|
282
|
+
def __post_init__(self):
|
|
283
|
+
if not self.uuid:
|
|
284
|
+
self.uuid = str(uuid4())
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@dataclass
|
|
288
|
+
class SheetPin:
|
|
289
|
+
"""Pin on hierarchical sheet."""
|
|
290
|
+
|
|
291
|
+
uuid: str
|
|
292
|
+
name: str
|
|
293
|
+
position: Point
|
|
294
|
+
pin_type: PinType = PinType.BIDIRECTIONAL
|
|
295
|
+
size: float = 1.27
|
|
296
|
+
|
|
297
|
+
def __post_init__(self):
|
|
298
|
+
if not self.uuid:
|
|
299
|
+
self.uuid = str(uuid4())
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@dataclass
|
|
303
|
+
class SymbolInstance:
|
|
304
|
+
"""Instance of a symbol from library."""
|
|
305
|
+
|
|
306
|
+
path: str # Hierarchical path
|
|
307
|
+
reference: str
|
|
308
|
+
unit: int = 1
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@dataclass
|
|
312
|
+
class TitleBlock:
|
|
313
|
+
"""Title block information."""
|
|
314
|
+
|
|
315
|
+
title: str = ""
|
|
316
|
+
company: str = ""
|
|
317
|
+
revision: str = ""
|
|
318
|
+
date: str = ""
|
|
319
|
+
size: str = "A4"
|
|
320
|
+
comments: Dict[int, str] = field(default_factory=dict)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@dataclass
|
|
324
|
+
class Schematic:
|
|
325
|
+
"""Complete schematic data structure."""
|
|
326
|
+
|
|
327
|
+
version: Optional[str] = None
|
|
328
|
+
generator: Optional[str] = None
|
|
329
|
+
uuid: Optional[str] = None
|
|
330
|
+
title_block: TitleBlock = field(default_factory=TitleBlock)
|
|
331
|
+
components: List[SchematicSymbol] = field(default_factory=list)
|
|
332
|
+
wires: List[Wire] = field(default_factory=list)
|
|
333
|
+
junctions: List[Junction] = field(default_factory=list)
|
|
334
|
+
labels: List[Label] = field(default_factory=list)
|
|
335
|
+
nets: List[Net] = field(default_factory=list)
|
|
336
|
+
sheets: List[Sheet] = field(default_factory=list)
|
|
337
|
+
lib_symbols: Dict[str, Any] = field(default_factory=dict)
|
|
338
|
+
|
|
339
|
+
def __post_init__(self):
|
|
340
|
+
if not self.uuid:
|
|
341
|
+
self.uuid = str(uuid4())
|
|
342
|
+
|
|
343
|
+
def get_component(self, reference: str) -> Optional[SchematicSymbol]:
|
|
344
|
+
"""Get component by reference."""
|
|
345
|
+
for component in self.components:
|
|
346
|
+
if component.reference == reference:
|
|
347
|
+
return component
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
def get_net(self, name: str) -> Optional[Net]:
|
|
351
|
+
"""Get net by name."""
|
|
352
|
+
for net in self.nets:
|
|
353
|
+
if net.name == name:
|
|
354
|
+
return net
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
def component_count(self) -> int:
|
|
358
|
+
"""Get total number of components."""
|
|
359
|
+
return len(self.components)
|
|
360
|
+
|
|
361
|
+
def connection_count(self) -> int:
|
|
362
|
+
"""Get total number of connections (wires + net connections)."""
|
|
363
|
+
return len(self.wires) + sum(len(net.components) for net in self.nets)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# Type aliases for convenience
|
|
367
|
+
ComponentDict = Dict[str, Any] # Raw component data from parser
|
|
368
|
+
WireDict = Dict[str, Any] # Raw wire data from parser
|
|
369
|
+
SchematicDict = Dict[str, Any] # Raw schematic data from parser
|