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,610 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python Code Generator for KiCad Schematics.
|
|
3
|
+
|
|
4
|
+
This module converts loaded KiCad schematic objects into executable Python code
|
|
5
|
+
that uses the kicad-sch-api library to recreate the schematic.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from jinja2 import Environment, PackageLoader, select_autoescape
|
|
15
|
+
JINJA2_AVAILABLE = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
JINJA2_AVAILABLE = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CodeGenerationError(Exception):
|
|
21
|
+
"""Error during Python code generation."""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TemplateNotFoundError(CodeGenerationError):
|
|
26
|
+
"""Template file not found."""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PythonCodeGenerator:
|
|
31
|
+
"""
|
|
32
|
+
Generate executable Python code from KiCad schematics.
|
|
33
|
+
|
|
34
|
+
This class converts loaded Schematic objects into executable Python
|
|
35
|
+
code that uses the kicad-sch-api to recreate the schematic.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
template: Template style ('minimal', 'default', 'verbose', 'documented')
|
|
39
|
+
format_code: Whether to format code with Black
|
|
40
|
+
add_comments: Whether to add explanatory comments
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
template: str = 'default',
|
|
46
|
+
format_code: bool = True,
|
|
47
|
+
add_comments: bool = True
|
|
48
|
+
):
|
|
49
|
+
"""
|
|
50
|
+
Initialize code generator.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
template: Template style to use ('minimal', 'default', 'verbose', 'documented')
|
|
54
|
+
format_code: Format output with Black (if available)
|
|
55
|
+
add_comments: Add explanatory comments
|
|
56
|
+
"""
|
|
57
|
+
self.template = template
|
|
58
|
+
self.format_code = format_code
|
|
59
|
+
self.add_comments = add_comments
|
|
60
|
+
|
|
61
|
+
# Initialize Jinja2 environment if available
|
|
62
|
+
if JINJA2_AVAILABLE:
|
|
63
|
+
self.jinja_env = Environment(
|
|
64
|
+
loader=PackageLoader('kicad_sch_api', 'exporters/templates'),
|
|
65
|
+
trim_blocks=True,
|
|
66
|
+
lstrip_blocks=True,
|
|
67
|
+
autoescape=select_autoescape(['html', 'xml'])
|
|
68
|
+
)
|
|
69
|
+
# Register custom filters
|
|
70
|
+
self.jinja_env.filters['sanitize'] = self._sanitize_variable_name
|
|
71
|
+
else:
|
|
72
|
+
self.jinja_env = None
|
|
73
|
+
|
|
74
|
+
def generate(
|
|
75
|
+
self,
|
|
76
|
+
schematic, # Type: Schematic (avoid circular import)
|
|
77
|
+
include_hierarchy: bool = True,
|
|
78
|
+
output_path: Optional[Path] = None
|
|
79
|
+
) -> str:
|
|
80
|
+
"""
|
|
81
|
+
Generate Python code from schematic.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
schematic: Loaded Schematic object
|
|
85
|
+
include_hierarchy: Include hierarchical sheets
|
|
86
|
+
output_path: Optional output file path
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Generated Python code as string
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
CodeGenerationError: If code generation fails
|
|
93
|
+
TemplateNotFoundError: If template doesn't exist
|
|
94
|
+
"""
|
|
95
|
+
# Extract all schematic data
|
|
96
|
+
data = self._extract_schematic_data(schematic, include_hierarchy)
|
|
97
|
+
|
|
98
|
+
# Generate code using template or fallback
|
|
99
|
+
if self.jinja_env and self.template != 'minimal':
|
|
100
|
+
code = self._generate_with_template(data)
|
|
101
|
+
else:
|
|
102
|
+
# Use simple string-based generation for minimal or if Jinja2 unavailable
|
|
103
|
+
code = self._generate_minimal(data)
|
|
104
|
+
|
|
105
|
+
# Format code
|
|
106
|
+
if self.format_code:
|
|
107
|
+
code = self._format_with_black(code)
|
|
108
|
+
|
|
109
|
+
# Validate syntax
|
|
110
|
+
self._validate_syntax(code)
|
|
111
|
+
|
|
112
|
+
# Write to file if path provided
|
|
113
|
+
if output_path:
|
|
114
|
+
output_path = Path(output_path)
|
|
115
|
+
output_path.write_text(code, encoding='utf-8')
|
|
116
|
+
# Make executable on Unix-like systems
|
|
117
|
+
try:
|
|
118
|
+
output_path.chmod(0o755)
|
|
119
|
+
except Exception:
|
|
120
|
+
pass # Windows doesn't support chmod
|
|
121
|
+
|
|
122
|
+
return code
|
|
123
|
+
|
|
124
|
+
def _extract_schematic_data(
|
|
125
|
+
self,
|
|
126
|
+
schematic,
|
|
127
|
+
include_hierarchy: bool
|
|
128
|
+
) -> Dict[str, Any]:
|
|
129
|
+
"""
|
|
130
|
+
Extract all data from schematic for code generation.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
schematic: Schematic to extract from
|
|
134
|
+
include_hierarchy: Include hierarchical sheets
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Dictionary with all template data
|
|
138
|
+
"""
|
|
139
|
+
return {
|
|
140
|
+
'metadata': self._extract_metadata(schematic),
|
|
141
|
+
'components': self._extract_components(schematic),
|
|
142
|
+
'wires': self._extract_wires(schematic),
|
|
143
|
+
'labels': self._extract_labels(schematic),
|
|
144
|
+
'sheets': self._extract_sheets(schematic) if include_hierarchy else [],
|
|
145
|
+
'options': {
|
|
146
|
+
'add_comments': self.add_comments,
|
|
147
|
+
'include_hierarchy': include_hierarchy
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
def _extract_metadata(self, schematic) -> Dict[str, Any]:
|
|
152
|
+
"""Extract schematic metadata."""
|
|
153
|
+
import kicad_sch_api
|
|
154
|
+
|
|
155
|
+
# Get project name from schematic
|
|
156
|
+
name = getattr(schematic, 'name', None) or 'untitled'
|
|
157
|
+
|
|
158
|
+
# Get title from title block if available
|
|
159
|
+
title = ''
|
|
160
|
+
if hasattr(schematic, 'title_block') and schematic.title_block:
|
|
161
|
+
title = getattr(schematic.title_block, 'title', '')
|
|
162
|
+
|
|
163
|
+
if not title:
|
|
164
|
+
title = name
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
'name': name,
|
|
168
|
+
'title': title,
|
|
169
|
+
'version': kicad_sch_api.__version__,
|
|
170
|
+
'date': datetime.now().isoformat(),
|
|
171
|
+
'source_file': str(schematic.filepath) if hasattr(schematic, 'filepath') and schematic.filepath else 'unknown'
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
def _extract_components(self, schematic) -> List[Dict[str, Any]]:
|
|
175
|
+
"""
|
|
176
|
+
Extract component data.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
List of component dictionaries
|
|
180
|
+
"""
|
|
181
|
+
components = []
|
|
182
|
+
|
|
183
|
+
# Access components collection
|
|
184
|
+
comp_collection = schematic.components if hasattr(schematic, 'components') else []
|
|
185
|
+
|
|
186
|
+
for comp in comp_collection:
|
|
187
|
+
# Extract component properties
|
|
188
|
+
ref = getattr(comp, 'reference', '') or getattr(comp, 'ref', '')
|
|
189
|
+
lib_id = getattr(comp, 'lib_id', '') or getattr(comp, 'symbol', '')
|
|
190
|
+
value = getattr(comp, 'value', '')
|
|
191
|
+
footprint = getattr(comp, 'footprint', '')
|
|
192
|
+
|
|
193
|
+
# Get position
|
|
194
|
+
pos = getattr(comp, 'position', None)
|
|
195
|
+
if pos:
|
|
196
|
+
x = getattr(pos, 'x', 0.0)
|
|
197
|
+
y = getattr(pos, 'y', 0.0)
|
|
198
|
+
else:
|
|
199
|
+
x, y = 0.0, 0.0
|
|
200
|
+
|
|
201
|
+
# Get rotation
|
|
202
|
+
rotation = getattr(comp, 'rotation', 0)
|
|
203
|
+
|
|
204
|
+
comp_data = {
|
|
205
|
+
'ref': ref,
|
|
206
|
+
'variable': self._sanitize_variable_name(ref),
|
|
207
|
+
'lib_id': lib_id,
|
|
208
|
+
'value': value,
|
|
209
|
+
'footprint': footprint,
|
|
210
|
+
'x': x,
|
|
211
|
+
'y': y,
|
|
212
|
+
'rotation': rotation,
|
|
213
|
+
'properties': self._extract_custom_properties(comp)
|
|
214
|
+
}
|
|
215
|
+
components.append(comp_data)
|
|
216
|
+
|
|
217
|
+
return components
|
|
218
|
+
|
|
219
|
+
def _extract_wires(self, schematic) -> List[Dict[str, Any]]:
|
|
220
|
+
"""
|
|
221
|
+
Extract wire data.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
List of wire dictionaries
|
|
225
|
+
"""
|
|
226
|
+
wires = []
|
|
227
|
+
|
|
228
|
+
# Access wires collection
|
|
229
|
+
wire_collection = schematic.wires if hasattr(schematic, 'wires') else []
|
|
230
|
+
|
|
231
|
+
for wire in wire_collection:
|
|
232
|
+
# Get start and end points
|
|
233
|
+
start = getattr(wire, 'start', None)
|
|
234
|
+
end = getattr(wire, 'end', None)
|
|
235
|
+
|
|
236
|
+
if start and end:
|
|
237
|
+
wire_data = {
|
|
238
|
+
'start_x': getattr(start, 'x', 0.0),
|
|
239
|
+
'start_y': getattr(start, 'y', 0.0),
|
|
240
|
+
'end_x': getattr(end, 'x', 0.0),
|
|
241
|
+
'end_y': getattr(end, 'y', 0.0),
|
|
242
|
+
'style': getattr(wire, 'style', 'solid')
|
|
243
|
+
}
|
|
244
|
+
wires.append(wire_data)
|
|
245
|
+
|
|
246
|
+
return wires
|
|
247
|
+
|
|
248
|
+
def _extract_labels(self, schematic) -> List[Dict[str, Any]]:
|
|
249
|
+
"""
|
|
250
|
+
Extract label data.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
List of label dictionaries
|
|
254
|
+
"""
|
|
255
|
+
labels = []
|
|
256
|
+
|
|
257
|
+
# Access labels collection
|
|
258
|
+
label_collection = schematic.labels if hasattr(schematic, 'labels') else []
|
|
259
|
+
|
|
260
|
+
for label in label_collection:
|
|
261
|
+
# Get label properties
|
|
262
|
+
text = getattr(label, 'text', '')
|
|
263
|
+
pos = getattr(label, 'position', None)
|
|
264
|
+
|
|
265
|
+
if pos:
|
|
266
|
+
x = getattr(pos, 'x', 0.0)
|
|
267
|
+
y = getattr(pos, 'y', 0.0)
|
|
268
|
+
else:
|
|
269
|
+
x, y = 0.0, 0.0
|
|
270
|
+
|
|
271
|
+
label_type = getattr(label, 'label_type', 'local')
|
|
272
|
+
rotation = getattr(label, 'rotation', 0)
|
|
273
|
+
|
|
274
|
+
label_data = {
|
|
275
|
+
'text': text,
|
|
276
|
+
'x': x,
|
|
277
|
+
'y': y,
|
|
278
|
+
'type': label_type,
|
|
279
|
+
'rotation': rotation
|
|
280
|
+
}
|
|
281
|
+
labels.append(label_data)
|
|
282
|
+
|
|
283
|
+
return labels
|
|
284
|
+
|
|
285
|
+
def _extract_sheets(self, schematic) -> List[Dict[str, Any]]:
|
|
286
|
+
"""
|
|
287
|
+
Extract hierarchical sheet data.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
List of sheet dictionaries
|
|
291
|
+
"""
|
|
292
|
+
sheets = []
|
|
293
|
+
|
|
294
|
+
# Access sheets collection if available
|
|
295
|
+
if not hasattr(schematic, 'sheets'):
|
|
296
|
+
return sheets
|
|
297
|
+
|
|
298
|
+
# SheetManager doesn't support direct iteration
|
|
299
|
+
# For now, return empty list - hierarchical support is Phase 3
|
|
300
|
+
# TODO: Implement when SheetManager has proper iteration support
|
|
301
|
+
return sheets
|
|
302
|
+
|
|
303
|
+
# The code below is for future when SheetManager is iterable:
|
|
304
|
+
for sheet in getattr(schematic.sheets, 'data', []):
|
|
305
|
+
# Get sheet properties
|
|
306
|
+
name = getattr(sheet, 'name', '')
|
|
307
|
+
filename = getattr(sheet, 'filename', '')
|
|
308
|
+
|
|
309
|
+
pos = getattr(sheet, 'position', None)
|
|
310
|
+
size = getattr(sheet, 'size', None)
|
|
311
|
+
|
|
312
|
+
if pos:
|
|
313
|
+
x = getattr(pos, 'x', 0.0)
|
|
314
|
+
y = getattr(pos, 'y', 0.0)
|
|
315
|
+
else:
|
|
316
|
+
x, y = 0.0, 0.0
|
|
317
|
+
|
|
318
|
+
if size:
|
|
319
|
+
width = getattr(size, 'width', 100.0)
|
|
320
|
+
height = getattr(size, 'height', 100.0)
|
|
321
|
+
else:
|
|
322
|
+
width, height = 100.0, 100.0
|
|
323
|
+
|
|
324
|
+
# Extract pins
|
|
325
|
+
pins = []
|
|
326
|
+
if hasattr(sheet, 'pins'):
|
|
327
|
+
for pin in sheet.pins:
|
|
328
|
+
pin_pos = getattr(pin, 'position', None)
|
|
329
|
+
pins.append({
|
|
330
|
+
'name': getattr(pin, 'name', ''),
|
|
331
|
+
'type': getattr(pin, 'pin_type', 'input'),
|
|
332
|
+
'x': getattr(pin_pos, 'x', 0.0) if pin_pos else 0.0,
|
|
333
|
+
'y': getattr(pin_pos, 'y', 0.0) if pin_pos else 0.0
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
sheet_data = {
|
|
337
|
+
'name': name,
|
|
338
|
+
'filename': filename,
|
|
339
|
+
'x': x,
|
|
340
|
+
'y': y,
|
|
341
|
+
'width': width,
|
|
342
|
+
'height': height,
|
|
343
|
+
'pins': pins
|
|
344
|
+
}
|
|
345
|
+
sheets.append(sheet_data)
|
|
346
|
+
|
|
347
|
+
return sheets
|
|
348
|
+
|
|
349
|
+
def _extract_custom_properties(self, component) -> List[Dict[str, str]]:
|
|
350
|
+
"""
|
|
351
|
+
Extract custom component properties.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
List of property dictionaries
|
|
355
|
+
"""
|
|
356
|
+
# Standard properties to exclude
|
|
357
|
+
standard_props = {
|
|
358
|
+
'Reference', 'Value', 'Footprint', 'Datasheet',
|
|
359
|
+
'ki_keywords', 'ki_description', 'ki_fp_filters',
|
|
360
|
+
'Description' # Also exclude Description as it's often auto-generated
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
properties = []
|
|
364
|
+
|
|
365
|
+
# Get properties if available
|
|
366
|
+
if hasattr(component, 'properties'):
|
|
367
|
+
comp_props = component.properties
|
|
368
|
+
if isinstance(comp_props, dict):
|
|
369
|
+
for prop_name, prop_value in comp_props.items():
|
|
370
|
+
# Skip internal properties (start with __)
|
|
371
|
+
if prop_name.startswith('__'):
|
|
372
|
+
continue
|
|
373
|
+
# Skip standard properties
|
|
374
|
+
if prop_name in standard_props:
|
|
375
|
+
continue
|
|
376
|
+
# Skip if value contains non-serializable content
|
|
377
|
+
str_value = str(prop_value)
|
|
378
|
+
if 'Symbol(' in str_value or '[Symbol(' in str_value:
|
|
379
|
+
continue
|
|
380
|
+
|
|
381
|
+
properties.append({
|
|
382
|
+
'name': prop_name,
|
|
383
|
+
'value': str_value
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
return properties
|
|
387
|
+
|
|
388
|
+
@staticmethod
|
|
389
|
+
def _sanitize_variable_name(name: str) -> str:
|
|
390
|
+
"""
|
|
391
|
+
Convert reference/name to valid Python variable name.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
name: Original name (R1, 3V3, etc.)
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
Sanitized variable name (r1, _3v3, etc.)
|
|
398
|
+
|
|
399
|
+
Examples:
|
|
400
|
+
>>> PythonCodeGenerator._sanitize_variable_name('R1')
|
|
401
|
+
'r1'
|
|
402
|
+
>>> PythonCodeGenerator._sanitize_variable_name('3V3')
|
|
403
|
+
'_3v3'
|
|
404
|
+
>>> PythonCodeGenerator._sanitize_variable_name('U$1')
|
|
405
|
+
'u_1'
|
|
406
|
+
"""
|
|
407
|
+
import keyword
|
|
408
|
+
|
|
409
|
+
# Handle special power net cases
|
|
410
|
+
power_nets = {
|
|
411
|
+
'3V3': '_3v3',
|
|
412
|
+
'3.3V': '_3v3',
|
|
413
|
+
'+3V3': '_3v3',
|
|
414
|
+
'+3.3V': '_3v3',
|
|
415
|
+
'5V': '_5v',
|
|
416
|
+
'+5V': '_5v',
|
|
417
|
+
'12V': '_12v',
|
|
418
|
+
'+12V': '_12v',
|
|
419
|
+
'VCC': 'vcc',
|
|
420
|
+
'VDD': 'vdd',
|
|
421
|
+
'GND': 'gnd',
|
|
422
|
+
'VSS': 'vss'
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if name in power_nets:
|
|
426
|
+
return power_nets[name]
|
|
427
|
+
|
|
428
|
+
# Convert to lowercase
|
|
429
|
+
var_name = name.lower()
|
|
430
|
+
|
|
431
|
+
# Replace invalid characters
|
|
432
|
+
var_name = var_name.replace('$', '_')
|
|
433
|
+
var_name = var_name.replace('+', 'p')
|
|
434
|
+
var_name = var_name.replace('-', 'n')
|
|
435
|
+
var_name = var_name.replace('.', '_')
|
|
436
|
+
var_name = re.sub(r'[^a-z0-9_]', '_', var_name)
|
|
437
|
+
|
|
438
|
+
# Remove consecutive underscores
|
|
439
|
+
var_name = re.sub(r'_+', '_', var_name)
|
|
440
|
+
|
|
441
|
+
# Strip leading/trailing underscores
|
|
442
|
+
var_name = var_name.strip('_')
|
|
443
|
+
|
|
444
|
+
# Prefix if starts with digit or is empty
|
|
445
|
+
if not var_name or var_name[0].isdigit():
|
|
446
|
+
var_name = '_' + var_name
|
|
447
|
+
|
|
448
|
+
# Ensure not a Python keyword
|
|
449
|
+
if keyword.iskeyword(var_name):
|
|
450
|
+
var_name = var_name + '_'
|
|
451
|
+
|
|
452
|
+
return var_name
|
|
453
|
+
|
|
454
|
+
def _generate_with_template(self, data: Dict[str, Any]) -> str:
|
|
455
|
+
"""
|
|
456
|
+
Generate code using Jinja2 template.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
data: Template data
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
Generated code
|
|
463
|
+
|
|
464
|
+
Raises:
|
|
465
|
+
TemplateNotFoundError: If template not found
|
|
466
|
+
"""
|
|
467
|
+
try:
|
|
468
|
+
template = self.jinja_env.get_template(f'{self.template}.py.jinja2')
|
|
469
|
+
code = template.render(**data)
|
|
470
|
+
return code
|
|
471
|
+
except Exception as e:
|
|
472
|
+
raise TemplateNotFoundError(
|
|
473
|
+
f"Template '{self.template}' not found or invalid: {e}"
|
|
474
|
+
) from e
|
|
475
|
+
|
|
476
|
+
def _generate_minimal(self, data: Dict[str, Any]) -> str:
|
|
477
|
+
"""
|
|
478
|
+
Generate minimal Python code without templates.
|
|
479
|
+
|
|
480
|
+
This is a fallback when Jinja2 is not available or for minimal template.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
data: Extracted schematic data
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
Generated Python code
|
|
487
|
+
"""
|
|
488
|
+
lines = []
|
|
489
|
+
|
|
490
|
+
# Header
|
|
491
|
+
lines.append("#!/usr/bin/env python3")
|
|
492
|
+
lines.append('"""')
|
|
493
|
+
lines.append(f"{data['metadata']['title']}")
|
|
494
|
+
lines.append("")
|
|
495
|
+
lines.append(f"Generated from: {data['metadata']['source_file']}")
|
|
496
|
+
lines.append(f"Generated by: kicad-sch-api v{data['metadata']['version']}")
|
|
497
|
+
lines.append('"""')
|
|
498
|
+
lines.append("")
|
|
499
|
+
lines.append("import kicad_sch_api as ksa")
|
|
500
|
+
lines.append("")
|
|
501
|
+
|
|
502
|
+
# Function definition
|
|
503
|
+
func_name = self._sanitize_variable_name(data['metadata']['name'])
|
|
504
|
+
lines.append(f"def create_{func_name}():")
|
|
505
|
+
lines.append(f' """Create {data["metadata"]["title"]} schematic."""')
|
|
506
|
+
lines.append("")
|
|
507
|
+
|
|
508
|
+
# Create schematic
|
|
509
|
+
lines.append(" # Create schematic")
|
|
510
|
+
lines.append(f" sch = ksa.create_schematic('{data['metadata']['name']}')")
|
|
511
|
+
lines.append("")
|
|
512
|
+
|
|
513
|
+
# Add components
|
|
514
|
+
if data['components']:
|
|
515
|
+
lines.append(" # Add components")
|
|
516
|
+
for comp in data['components']:
|
|
517
|
+
lines.append(f" {comp['variable']} = sch.components.add(")
|
|
518
|
+
lines.append(f" '{comp['lib_id']}',")
|
|
519
|
+
lines.append(f" reference='{comp['ref']}',")
|
|
520
|
+
lines.append(f" value='{comp['value']}',")
|
|
521
|
+
lines.append(f" position=({comp['x']}, {comp['y']})")
|
|
522
|
+
if comp['rotation'] != 0:
|
|
523
|
+
lines.append(f" rotation={comp['rotation']}")
|
|
524
|
+
lines.append(" )")
|
|
525
|
+
if comp['footprint']:
|
|
526
|
+
lines.append(f" {comp['variable']}.footprint = '{comp['footprint']}'")
|
|
527
|
+
for prop in comp['properties']:
|
|
528
|
+
lines.append(f" {comp['variable']}.set_property('{prop['name']}', '{prop['value']}')")
|
|
529
|
+
lines.append("")
|
|
530
|
+
|
|
531
|
+
# Add wires
|
|
532
|
+
if data['wires']:
|
|
533
|
+
lines.append(" # Add wires")
|
|
534
|
+
for wire in data['wires']:
|
|
535
|
+
lines.append(f" sch.add_wire(")
|
|
536
|
+
lines.append(f" start=({wire['start_x']}, {wire['start_y']}),")
|
|
537
|
+
lines.append(f" end=({wire['end_x']}, {wire['end_y']})")
|
|
538
|
+
lines.append(" )")
|
|
539
|
+
lines.append("")
|
|
540
|
+
|
|
541
|
+
# Add labels
|
|
542
|
+
if data['labels']:
|
|
543
|
+
lines.append(" # Add labels")
|
|
544
|
+
for label in data['labels']:
|
|
545
|
+
lines.append(f" sch.add_label(")
|
|
546
|
+
lines.append(f" '{label['text']}',")
|
|
547
|
+
lines.append(f" position=({label['x']}, {label['y']})")
|
|
548
|
+
lines.append(" )")
|
|
549
|
+
lines.append("")
|
|
550
|
+
|
|
551
|
+
# Return
|
|
552
|
+
lines.append(" return sch")
|
|
553
|
+
lines.append("")
|
|
554
|
+
lines.append("")
|
|
555
|
+
|
|
556
|
+
# Main block
|
|
557
|
+
lines.append("if __name__ == '__main__':")
|
|
558
|
+
lines.append(f" schematic = create_{func_name}()")
|
|
559
|
+
lines.append(f" schematic.save('{data['metadata']['name']}.kicad_sch')")
|
|
560
|
+
lines.append(f" print('✅ Schematic generated: {data['metadata']['name']}.kicad_sch')")
|
|
561
|
+
lines.append("")
|
|
562
|
+
|
|
563
|
+
return '\n'.join(lines)
|
|
564
|
+
|
|
565
|
+
def _format_with_black(self, code: str) -> str:
|
|
566
|
+
"""
|
|
567
|
+
Format code using Black formatter.
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
code: Unformatted Python code
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
Formatted code (or original if Black unavailable)
|
|
574
|
+
"""
|
|
575
|
+
try:
|
|
576
|
+
import black
|
|
577
|
+
|
|
578
|
+
mode = black.Mode(
|
|
579
|
+
target_versions={black.TargetVersion.PY38},
|
|
580
|
+
line_length=88,
|
|
581
|
+
string_normalization=True
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
formatted = black.format_str(code, mode=mode)
|
|
585
|
+
return formatted
|
|
586
|
+
|
|
587
|
+
except ImportError:
|
|
588
|
+
# Black not available, return unformatted
|
|
589
|
+
return code
|
|
590
|
+
|
|
591
|
+
except Exception:
|
|
592
|
+
# Black failed, return unformatted
|
|
593
|
+
return code
|
|
594
|
+
|
|
595
|
+
def _validate_syntax(self, code: str) -> None:
|
|
596
|
+
"""
|
|
597
|
+
Validate generated code syntax.
|
|
598
|
+
|
|
599
|
+
Args:
|
|
600
|
+
code: Generated Python code
|
|
601
|
+
|
|
602
|
+
Raises:
|
|
603
|
+
CodeGenerationError: If code has syntax errors
|
|
604
|
+
"""
|
|
605
|
+
try:
|
|
606
|
+
compile(code, '<generated>', 'exec')
|
|
607
|
+
except SyntaxError as e:
|
|
608
|
+
raise CodeGenerationError(
|
|
609
|
+
f"Generated code has syntax error at line {e.lineno}: {e.msg}"
|
|
610
|
+
) from e
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
{{ metadata.title }}
|
|
4
|
+
|
|
5
|
+
Generated from: {{ metadata.source_file }}
|
|
6
|
+
Generated by: kicad-sch-api v{{ metadata.version }}
|
|
7
|
+
Date: {{ metadata.date }}
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import kicad_sch_api as ksa
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_{{ metadata.name|sanitize }}():
|
|
14
|
+
"""Create {{ metadata.title }} schematic."""
|
|
15
|
+
|
|
16
|
+
# Create schematic
|
|
17
|
+
sch = ksa.create_schematic('{{ metadata.name }}')
|
|
18
|
+
|
|
19
|
+
{% if components %}
|
|
20
|
+
# Add components
|
|
21
|
+
{% for comp in components %}
|
|
22
|
+
{{ comp.variable }} = sch.components.add(
|
|
23
|
+
'{{ comp.lib_id }}',
|
|
24
|
+
reference='{{ comp.ref }}',
|
|
25
|
+
value='{{ comp.value }}',
|
|
26
|
+
position=({{ comp.x }}, {{ comp.y }}){% if comp.rotation != 0 %},
|
|
27
|
+
rotation={{ comp.rotation }}{% endif %}
|
|
28
|
+
|
|
29
|
+
)
|
|
30
|
+
{%- if comp.footprint %}
|
|
31
|
+
{{ comp.variable }}.footprint = '{{ comp.footprint }}'
|
|
32
|
+
{%- endif %}
|
|
33
|
+
{%- for prop in comp.properties %}
|
|
34
|
+
{{ comp.variable }}.set_property('{{ prop.name }}', '{{ prop.value }}')
|
|
35
|
+
{%- endfor %}
|
|
36
|
+
|
|
37
|
+
{% endfor %}
|
|
38
|
+
{% endif %}
|
|
39
|
+
{%- if wires %}
|
|
40
|
+
# Add wires
|
|
41
|
+
{% for wire in wires %}
|
|
42
|
+
sch.add_wire(
|
|
43
|
+
start=({{ wire.start_x }}, {{ wire.start_y }}),
|
|
44
|
+
end=({{ wire.end_x }}, {{ wire.end_y }})
|
|
45
|
+
)
|
|
46
|
+
{% endfor %}
|
|
47
|
+
|
|
48
|
+
{% endif %}
|
|
49
|
+
{%- if labels %}
|
|
50
|
+
# Add labels
|
|
51
|
+
{% for label in labels %}
|
|
52
|
+
sch.add_label(
|
|
53
|
+
'{{ label.text }}',
|
|
54
|
+
position=({{ label.x }}, {{ label.y }})
|
|
55
|
+
)
|
|
56
|
+
{% endfor %}
|
|
57
|
+
|
|
58
|
+
{% endif %}
|
|
59
|
+
return sch
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == '__main__':
|
|
63
|
+
schematic = create_{{ metadata.name|sanitize }}()
|
|
64
|
+
schematic.save('{{ metadata.name }}.kicad_sch')
|
|
65
|
+
print('✅ Schematic generated: {{ metadata.name }}.kicad_sch')
|