This is page 5 of 6. Use http://codebase.md/mixelpixx/kicad-mcp-server?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .github
│ └── workflows
│ └── ci.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG_2025-10-26.md
├── CHANGELOG_2025-11-01.md
├── CHANGELOG_2025-11-05.md
├── CHANGELOG_2025-11-30.md
├── config
│ ├── claude-desktop-config.json
│ ├── default-config.json
│ ├── linux-config.example.json
│ ├── macos-config.example.json
│ └── windows-config.example.json
├── CONTRIBUTING.md
├── docs
│ ├── BUILD_AND_TEST_SESSION.md
│ ├── CLIENT_CONFIGURATION.md
│ ├── IPC_API_MIGRATION_PLAN.md
│ ├── IPC_BACKEND_STATUS.md
│ ├── JLCPCB_INTEGRATION_PLAN.md
│ ├── KNOWN_ISSUES.md
│ ├── LIBRARY_INTEGRATION.md
│ ├── LINUX_COMPATIBILITY_AUDIT.md
│ ├── PLATFORM_GUIDE.md
│ ├── REALTIME_WORKFLOW.md
│ ├── ROADMAP.md
│ ├── STATUS_SUMMARY.md
│ ├── UI_AUTO_LAUNCH.md
│ ├── VISUAL_FEEDBACK.md
│ ├── WEEK1_SESSION1_SUMMARY.md
│ ├── WEEK1_SESSION2_SUMMARY.md
│ └── WINDOWS_TROUBLESHOOTING.md
├── LICENSE
├── package-json.json
├── package-lock.json
├── package.json
├── pytest.ini
├── python
│ ├── commands
│ │ ├── __init__.py
│ │ ├── board
│ │ │ ├── __init__.py
│ │ │ ├── layers.py
│ │ │ ├── outline.py
│ │ │ ├── size.py
│ │ │ └── view.py
│ │ ├── board.py
│ │ ├── component_schematic.py
│ │ ├── component.py
│ │ ├── connection_schematic.py
│ │ ├── design_rules.py
│ │ ├── export.py
│ │ ├── library_schematic.py
│ │ ├── library.py
│ │ ├── project.py
│ │ ├── routing.py
│ │ └── schematic.py
│ ├── kicad_api
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── factory.py
│ │ ├── ipc_backend.py
│ │ └── swig_backend.py
│ ├── kicad_interface.py
│ ├── requirements.txt
│ ├── resources
│ │ ├── __init__.py
│ │ └── resource_definitions.py
│ ├── schemas
│ │ ├── __init__.py
│ │ └── tool_schemas.py
│ ├── test_ipc_backend.py
│ └── utils
│ ├── __init__.py
│ ├── kicad_process.py
│ └── platform_helper.py
├── README.md
├── requirements-dev.txt
├── requirements.txt
├── scripts
│ ├── auto_refresh_kicad.sh
│ └── install-linux.sh
├── setup-windows.ps1
├── src
│ ├── config.ts
│ ├── index.ts
│ ├── kicad-server.ts
│ ├── logger.ts
│ ├── prompts
│ │ ├── component.ts
│ │ ├── design.ts
│ │ ├── index.ts
│ │ └── routing.ts
│ ├── resources
│ │ ├── board.ts
│ │ ├── component.ts
│ │ ├── index.ts
│ │ ├── library.ts
│ │ └── project.ts
│ ├── server.ts
│ ├── tools
│ │ ├── board.ts
│ │ ├── component.ts
│ │ ├── component.txt
│ │ ├── design-rules.ts
│ │ ├── export.ts
│ │ ├── index.ts
│ │ ├── library.ts
│ │ ├── project.ts
│ │ ├── routing.ts
│ │ ├── schematic.ts
│ │ └── ui.ts
│ └── utils
│ └── resource-helpers.ts
├── tests
│ ├── __init__.py
│ └── test_platform_helper.py
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/python/commands/component.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Component-related command implementations for KiCAD interface
3 | """
4 |
5 | import os
6 | import pcbnew
7 | import logging
8 | import math
9 | from typing import Dict, Any, Optional, List, Tuple
10 | import base64
11 | from commands.library import LibraryManager
12 |
13 | logger = logging.getLogger('kicad_interface')
14 |
15 | class ComponentCommands:
16 | """Handles component-related KiCAD operations"""
17 |
18 | def __init__(self, board: Optional[pcbnew.BOARD] = None, library_manager: Optional[LibraryManager] = None):
19 | """Initialize with optional board instance and library manager"""
20 | self.board = board
21 | self.library_manager = library_manager or LibraryManager()
22 |
23 | def place_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
24 | """Place a component on the PCB"""
25 | try:
26 | if not self.board:
27 | return {
28 | "success": False,
29 | "message": "No board is loaded",
30 | "errorDetails": "Load or create a board first"
31 | }
32 |
33 | # Get parameters
34 | component_id = params.get("componentId")
35 | position = params.get("position")
36 | reference = params.get("reference")
37 | value = params.get("value")
38 | footprint = params.get("footprint")
39 | rotation = params.get("rotation", 0)
40 | layer = params.get("layer", "F.Cu")
41 |
42 | if not component_id or not position:
43 | return {
44 | "success": False,
45 | "message": "Missing parameters",
46 | "errorDetails": "componentId and position are required"
47 | }
48 |
49 | # Find footprint using library manager
50 | # component_id can be "Library:Footprint" or just "Footprint"
51 | footprint_result = self.library_manager.find_footprint(component_id)
52 |
53 | if not footprint_result:
54 | # Try to suggest similar footprints
55 | suggestions = self.library_manager.search_footprints(f"*{component_id}*", limit=5)
56 | suggestion_text = ""
57 | if suggestions:
58 | suggestion_text = "\n\nDid you mean one of these?\n" + \
59 | "\n".join([f" - {s['full_name']}" for s in suggestions])
60 |
61 | return {
62 | "success": False,
63 | "message": "Footprint not found",
64 | "errorDetails": f"Could not find footprint: {component_id}{suggestion_text}"
65 | }
66 |
67 | library_path, footprint_name = footprint_result
68 |
69 | # Load footprint from library
70 | # Extract library nickname from path
71 | library_nickname = None
72 | for nick, path in self.library_manager.libraries.items():
73 | if path == library_path:
74 | library_nickname = nick
75 | break
76 |
77 | if not library_nickname:
78 | return {
79 | "success": False,
80 | "message": "Internal error",
81 | "errorDetails": "Could not determine library nickname"
82 | }
83 |
84 | # Load the footprint
85 | module = pcbnew.FootprintLoad(library_path, footprint_name)
86 | if not module:
87 | return {
88 | "success": False,
89 | "message": "Failed to load footprint",
90 | "errorDetails": f"Could not load footprint from {library_path}/{footprint_name}"
91 | }
92 |
93 | # Set position
94 | scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
95 | x_nm = int(position["x"] * scale)
96 | y_nm = int(position["y"] * scale)
97 | module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
98 |
99 | # Set reference if provided
100 | if reference:
101 | module.SetReference(reference)
102 |
103 | # Set value if provided
104 | if value:
105 | module.SetValue(value)
106 |
107 | # Set footprint if provided
108 | if footprint:
109 | module.SetFootprintName(footprint)
110 |
111 | # Set rotation (KiCAD 9.0 uses EDA_ANGLE)
112 | angle = pcbnew.EDA_ANGLE(rotation, pcbnew.DEGREES_T)
113 | module.SetOrientation(angle)
114 |
115 | # Set layer
116 | layer_id = self.board.GetLayerID(layer)
117 | if layer_id >= 0:
118 | module.SetLayer(layer_id)
119 |
120 | # Add to board
121 | self.board.Add(module)
122 |
123 | return {
124 | "success": True,
125 | "message": f"Placed component: {component_id}",
126 | "component": {
127 | "reference": module.GetReference(),
128 | "value": module.GetValue(),
129 | "position": {
130 | "x": position["x"],
131 | "y": position["y"],
132 | "unit": position["unit"]
133 | },
134 | "rotation": rotation,
135 | "layer": layer
136 | }
137 | }
138 |
139 | except Exception as e:
140 | logger.error(f"Error placing component: {str(e)}")
141 | return {
142 | "success": False,
143 | "message": "Failed to place component",
144 | "errorDetails": str(e)
145 | }
146 |
147 | def move_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
148 | """Move an existing component to a new position"""
149 | try:
150 | if not self.board:
151 | return {
152 | "success": False,
153 | "message": "No board is loaded",
154 | "errorDetails": "Load or create a board first"
155 | }
156 |
157 | reference = params.get("reference")
158 | position = params.get("position")
159 | rotation = params.get("rotation")
160 |
161 | if not reference or not position:
162 | return {
163 | "success": False,
164 | "message": "Missing parameters",
165 | "errorDetails": "reference and position are required"
166 | }
167 |
168 | # Find the component
169 | module = self.board.FindFootprintByReference(reference)
170 | if not module:
171 | return {
172 | "success": False,
173 | "message": "Component not found",
174 | "errorDetails": f"Could not find component: {reference}"
175 | }
176 |
177 | # Set new position
178 | scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
179 | x_nm = int(position["x"] * scale)
180 | y_nm = int(position["y"] * scale)
181 | module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
182 |
183 | # Set new rotation if provided
184 | if rotation is not None:
185 | angle = pcbnew.EDA_ANGLE(rotation, pcbnew.DEGREES_T)
186 | module.SetOrientation(angle)
187 |
188 | return {
189 | "success": True,
190 | "message": f"Moved component: {reference}",
191 | "component": {
192 | "reference": reference,
193 | "position": {
194 | "x": position["x"],
195 | "y": position["y"],
196 | "unit": position["unit"]
197 | },
198 | "rotation": rotation if rotation is not None else module.GetOrientation().AsDegrees()
199 | }
200 | }
201 |
202 | except Exception as e:
203 | logger.error(f"Error moving component: {str(e)}")
204 | return {
205 | "success": False,
206 | "message": "Failed to move component",
207 | "errorDetails": str(e)
208 | }
209 |
210 | def rotate_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
211 | """Rotate an existing component"""
212 | try:
213 | if not self.board:
214 | return {
215 | "success": False,
216 | "message": "No board is loaded",
217 | "errorDetails": "Load or create a board first"
218 | }
219 |
220 | reference = params.get("reference")
221 | angle = params.get("angle")
222 |
223 | if not reference or angle is None:
224 | return {
225 | "success": False,
226 | "message": "Missing parameters",
227 | "errorDetails": "reference and angle are required"
228 | }
229 |
230 | # Find the component
231 | module = self.board.FindFootprintByReference(reference)
232 | if not module:
233 | return {
234 | "success": False,
235 | "message": "Component not found",
236 | "errorDetails": f"Could not find component: {reference}"
237 | }
238 |
239 | # Set rotation
240 | rotation_angle = pcbnew.EDA_ANGLE(angle, pcbnew.DEGREES_T)
241 | module.SetOrientation(rotation_angle)
242 |
243 | return {
244 | "success": True,
245 | "message": f"Rotated component: {reference}",
246 | "component": {
247 | "reference": reference,
248 | "rotation": angle
249 | }
250 | }
251 |
252 | except Exception as e:
253 | logger.error(f"Error rotating component: {str(e)}")
254 | return {
255 | "success": False,
256 | "message": "Failed to rotate component",
257 | "errorDetails": str(e)
258 | }
259 |
260 | def delete_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
261 | """Delete a component from the PCB"""
262 | try:
263 | if not self.board:
264 | return {
265 | "success": False,
266 | "message": "No board is loaded",
267 | "errorDetails": "Load or create a board first"
268 | }
269 |
270 | reference = params.get("reference")
271 | if not reference:
272 | return {
273 | "success": False,
274 | "message": "Missing reference",
275 | "errorDetails": "reference parameter is required"
276 | }
277 |
278 | # Find the component
279 | module = self.board.FindFootprintByReference(reference)
280 | if not module:
281 | return {
282 | "success": False,
283 | "message": "Component not found",
284 | "errorDetails": f"Could not find component: {reference}"
285 | }
286 |
287 | # Remove from board
288 | self.board.Remove(module)
289 |
290 | return {
291 | "success": True,
292 | "message": f"Deleted component: {reference}"
293 | }
294 |
295 | except Exception as e:
296 | logger.error(f"Error deleting component: {str(e)}")
297 | return {
298 | "success": False,
299 | "message": "Failed to delete component",
300 | "errorDetails": str(e)
301 | }
302 |
303 | def edit_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
304 | """Edit the properties of an existing component"""
305 | try:
306 | if not self.board:
307 | return {
308 | "success": False,
309 | "message": "No board is loaded",
310 | "errorDetails": "Load or create a board first"
311 | }
312 |
313 | reference = params.get("reference")
314 | new_reference = params.get("newReference")
315 | value = params.get("value")
316 | footprint = params.get("footprint")
317 |
318 | if not reference:
319 | return {
320 | "success": False,
321 | "message": "Missing reference",
322 | "errorDetails": "reference parameter is required"
323 | }
324 |
325 | # Find the component
326 | module = self.board.FindFootprintByReference(reference)
327 | if not module:
328 | return {
329 | "success": False,
330 | "message": "Component not found",
331 | "errorDetails": f"Could not find component: {reference}"
332 | }
333 |
334 | # Update properties
335 | if new_reference:
336 | module.SetReference(new_reference)
337 | if value:
338 | module.SetValue(value)
339 | if footprint:
340 | module.SetFootprintName(footprint)
341 |
342 | return {
343 | "success": True,
344 | "message": f"Updated component: {reference}",
345 | "component": {
346 | "reference": new_reference or reference,
347 | "value": value or module.GetValue(),
348 | "footprint": footprint or module.GetFPIDAsString()
349 | }
350 | }
351 |
352 | except Exception as e:
353 | logger.error(f"Error editing component: {str(e)}")
354 | return {
355 | "success": False,
356 | "message": "Failed to edit component",
357 | "errorDetails": str(e)
358 | }
359 |
360 | def get_component_properties(self, params: Dict[str, Any]) -> Dict[str, Any]:
361 | """Get detailed properties of a component"""
362 | try:
363 | if not self.board:
364 | return {
365 | "success": False,
366 | "message": "No board is loaded",
367 | "errorDetails": "Load or create a board first"
368 | }
369 |
370 | reference = params.get("reference")
371 | if not reference:
372 | return {
373 | "success": False,
374 | "message": "Missing reference",
375 | "errorDetails": "reference parameter is required"
376 | }
377 |
378 | # Find the component
379 | module = self.board.FindFootprintByReference(reference)
380 | if not module:
381 | return {
382 | "success": False,
383 | "message": "Component not found",
384 | "errorDetails": f"Could not find component: {reference}"
385 | }
386 |
387 | # Get position in mm
388 | pos = module.GetPosition()
389 | x_mm = pos.x / 1000000
390 | y_mm = pos.y / 1000000
391 |
392 | return {
393 | "success": True,
394 | "component": {
395 | "reference": module.GetReference(),
396 | "value": module.GetValue(),
397 | "footprint": module.GetFPIDAsString(),
398 | "position": {
399 | "x": x_mm,
400 | "y": y_mm,
401 | "unit": "mm"
402 | },
403 | "rotation": module.GetOrientation().AsDegrees(),
404 | "layer": self.board.GetLayerName(module.GetLayer()),
405 | "attributes": {
406 | "smd": module.GetAttributes() & pcbnew.FP_SMD,
407 | "through_hole": module.GetAttributes() & pcbnew.FP_THROUGH_HOLE,
408 | "board_only": module.GetAttributes() & pcbnew.FP_BOARD_ONLY
409 | }
410 | }
411 | }
412 |
413 | except Exception as e:
414 | logger.error(f"Error getting component properties: {str(e)}")
415 | return {
416 | "success": False,
417 | "message": "Failed to get component properties",
418 | "errorDetails": str(e)
419 | }
420 |
421 | def get_component_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
422 | """Get a list of all components on the board"""
423 | try:
424 | if not self.board:
425 | return {
426 | "success": False,
427 | "message": "No board is loaded",
428 | "errorDetails": "Load or create a board first"
429 | }
430 |
431 | components = []
432 | for module in self.board.GetFootprints():
433 | pos = module.GetPosition()
434 | x_mm = pos.x / 1000000
435 | y_mm = pos.y / 1000000
436 |
437 | components.append({
438 | "reference": module.GetReference(),
439 | "value": module.GetValue(),
440 | "footprint": module.GetFPIDAsString(),
441 | "position": {
442 | "x": x_mm,
443 | "y": y_mm,
444 | "unit": "mm"
445 | },
446 | "rotation": module.GetOrientation().AsDegrees(),
447 | "layer": self.board.GetLayerName(module.GetLayer())
448 | })
449 |
450 | return {
451 | "success": True,
452 | "components": components
453 | }
454 |
455 | except Exception as e:
456 | logger.error(f"Error getting component list: {str(e)}")
457 | return {
458 | "success": False,
459 | "message": "Failed to get component list",
460 | "errorDetails": str(e)
461 | }
462 |
463 | def place_component_array(self, params: Dict[str, Any]) -> Dict[str, Any]:
464 | """Place an array of components in a grid or circular pattern"""
465 | try:
466 | if not self.board:
467 | return {
468 | "success": False,
469 | "message": "No board is loaded",
470 | "errorDetails": "Load or create a board first"
471 | }
472 |
473 | component_id = params.get("componentId")
474 | pattern = params.get("pattern", "grid") # grid or circular
475 | count = params.get("count")
476 | reference_prefix = params.get("referencePrefix", "U")
477 | value = params.get("value")
478 |
479 | if not component_id or not count:
480 | return {
481 | "success": False,
482 | "message": "Missing parameters",
483 | "errorDetails": "componentId and count are required"
484 | }
485 |
486 | if pattern == "grid":
487 | start_position = params.get("startPosition")
488 | rows = params.get("rows")
489 | columns = params.get("columns")
490 | spacing_x = params.get("spacingX")
491 | spacing_y = params.get("spacingY")
492 | rotation = params.get("rotation", 0)
493 | layer = params.get("layer", "F.Cu")
494 |
495 | if not start_position or not rows or not columns or not spacing_x or not spacing_y:
496 | return {
497 | "success": False,
498 | "message": "Missing grid parameters",
499 | "errorDetails": "For grid pattern, startPosition, rows, columns, spacingX, and spacingY are required"
500 | }
501 |
502 | if rows * columns != count:
503 | return {
504 | "success": False,
505 | "message": "Invalid grid parameters",
506 | "errorDetails": "rows * columns must equal count"
507 | }
508 |
509 | placed_components = self._place_grid_array(
510 | component_id,
511 | start_position,
512 | rows,
513 | columns,
514 | spacing_x,
515 | spacing_y,
516 | reference_prefix,
517 | value,
518 | rotation,
519 | layer
520 | )
521 |
522 | elif pattern == "circular":
523 | center = params.get("center")
524 | radius = params.get("radius")
525 | angle_start = params.get("angleStart", 0)
526 | angle_step = params.get("angleStep")
527 | rotation_offset = params.get("rotationOffset", 0)
528 | layer = params.get("layer", "F.Cu")
529 |
530 | if not center or not radius or not angle_step:
531 | return {
532 | "success": False,
533 | "message": "Missing circular parameters",
534 | "errorDetails": "For circular pattern, center, radius, and angleStep are required"
535 | }
536 |
537 | placed_components = self._place_circular_array(
538 | component_id,
539 | center,
540 | radius,
541 | count,
542 | angle_start,
543 | angle_step,
544 | reference_prefix,
545 | value,
546 | rotation_offset,
547 | layer
548 | )
549 |
550 | else:
551 | return {
552 | "success": False,
553 | "message": "Invalid pattern",
554 | "errorDetails": "Pattern must be 'grid' or 'circular'"
555 | }
556 |
557 | return {
558 | "success": True,
559 | "message": f"Placed {count} components in {pattern} pattern",
560 | "components": placed_components
561 | }
562 |
563 | except Exception as e:
564 | logger.error(f"Error placing component array: {str(e)}")
565 | return {
566 | "success": False,
567 | "message": "Failed to place component array",
568 | "errorDetails": str(e)
569 | }
570 |
571 | def align_components(self, params: Dict[str, Any]) -> Dict[str, Any]:
572 | """Align multiple components along a line or distribute them evenly"""
573 | try:
574 | if not self.board:
575 | return {
576 | "success": False,
577 | "message": "No board is loaded",
578 | "errorDetails": "Load or create a board first"
579 | }
580 |
581 | references = params.get("references", [])
582 | alignment = params.get("alignment", "horizontal") # horizontal, vertical, or edge
583 | distribution = params.get("distribution", "none") # none, equal, or spacing
584 | spacing = params.get("spacing")
585 |
586 | if not references or len(references) < 2:
587 | return {
588 | "success": False,
589 | "message": "Missing references",
590 | "errorDetails": "At least two component references are required"
591 | }
592 |
593 | # Find all referenced components
594 | components = []
595 | for ref in references:
596 | module = self.board.FindFootprintByReference(ref)
597 | if not module:
598 | return {
599 | "success": False,
600 | "message": "Component not found",
601 | "errorDetails": f"Could not find component: {ref}"
602 | }
603 | components.append(module)
604 |
605 | # Perform alignment based on selected option
606 | if alignment == "horizontal":
607 | self._align_components_horizontally(components, distribution, spacing)
608 | elif alignment == "vertical":
609 | self._align_components_vertically(components, distribution, spacing)
610 | elif alignment == "edge":
611 | edge = params.get("edge")
612 | if not edge:
613 | return {
614 | "success": False,
615 | "message": "Missing edge parameter",
616 | "errorDetails": "Edge parameter is required for edge alignment"
617 | }
618 | self._align_components_to_edge(components, edge)
619 | else:
620 | return {
621 | "success": False,
622 | "message": "Invalid alignment option",
623 | "errorDetails": "Alignment must be 'horizontal', 'vertical', or 'edge'"
624 | }
625 |
626 | # Prepare result data
627 | aligned_components = []
628 | for module in components:
629 | pos = module.GetPosition()
630 | aligned_components.append({
631 | "reference": module.GetReference(),
632 | "position": {
633 | "x": pos.x / 1000000,
634 | "y": pos.y / 1000000,
635 | "unit": "mm"
636 | },
637 | "rotation": module.GetOrientation().AsDegrees()
638 | })
639 |
640 | return {
641 | "success": True,
642 | "message": f"Aligned {len(components)} components",
643 | "alignment": alignment,
644 | "distribution": distribution,
645 | "components": aligned_components
646 | }
647 |
648 | except Exception as e:
649 | logger.error(f"Error aligning components: {str(e)}")
650 | return {
651 | "success": False,
652 | "message": "Failed to align components",
653 | "errorDetails": str(e)
654 | }
655 |
656 | def duplicate_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
657 | """Duplicate an existing component"""
658 | try:
659 | if not self.board:
660 | return {
661 | "success": False,
662 | "message": "No board is loaded",
663 | "errorDetails": "Load or create a board first"
664 | }
665 |
666 | reference = params.get("reference")
667 | new_reference = params.get("newReference")
668 | position = params.get("position")
669 | rotation = params.get("rotation")
670 |
671 | if not reference or not new_reference:
672 | return {
673 | "success": False,
674 | "message": "Missing parameters",
675 | "errorDetails": "reference and newReference are required"
676 | }
677 |
678 | # Find the source component
679 | source = self.board.FindFootprintByReference(reference)
680 | if not source:
681 | return {
682 | "success": False,
683 | "message": "Component not found",
684 | "errorDetails": f"Could not find component: {reference}"
685 | }
686 |
687 | # Check if new reference already exists
688 | if self.board.FindFootprintByReference(new_reference):
689 | return {
690 | "success": False,
691 | "message": "Reference already exists",
692 | "errorDetails": f"A component with reference {new_reference} already exists"
693 | }
694 |
695 | # Create new footprint with the same properties
696 | new_module = pcbnew.FOOTPRINT(self.board)
697 | new_module.SetFootprintName(source.GetFPIDAsString())
698 | new_module.SetValue(source.GetValue())
699 | new_module.SetReference(new_reference)
700 | new_module.SetLayer(source.GetLayer())
701 |
702 | # Copy pads and other items
703 | for pad in source.Pads():
704 | new_pad = pcbnew.PAD(new_module)
705 | new_pad.Copy(pad)
706 | new_module.Add(new_pad)
707 |
708 | # Set position if provided, otherwise use offset from original
709 | if position:
710 | scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000
711 | x_nm = int(position["x"] * scale)
712 | y_nm = int(position["y"] * scale)
713 | new_module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
714 | else:
715 | # Offset by 5mm
716 | source_pos = source.GetPosition()
717 | new_module.SetPosition(pcbnew.VECTOR2I(source_pos.x + 5000000, source_pos.y))
718 |
719 | # Set rotation if provided, otherwise use same as original
720 | if rotation is not None:
721 | rotation_angle = pcbnew.EDA_ANGLE(rotation, pcbnew.DEGREES_T)
722 | new_module.SetOrientation(rotation_angle)
723 | else:
724 | new_module.SetOrientation(source.GetOrientation())
725 |
726 | # Add to board
727 | self.board.Add(new_module)
728 |
729 | # Get final position in mm
730 | pos = new_module.GetPosition()
731 |
732 | return {
733 | "success": True,
734 | "message": f"Duplicated component {reference} to {new_reference}",
735 | "component": {
736 | "reference": new_reference,
737 | "value": new_module.GetValue(),
738 | "footprint": new_module.GetFPIDAsString(),
739 | "position": {
740 | "x": pos.x / 1000000,
741 | "y": pos.y / 1000000,
742 | "unit": "mm"
743 | },
744 | "rotation": new_module.GetOrientation().AsDegrees(),
745 | "layer": self.board.GetLayerName(new_module.GetLayer())
746 | }
747 | }
748 |
749 | except Exception as e:
750 | logger.error(f"Error duplicating component: {str(e)}")
751 | return {
752 | "success": False,
753 | "message": "Failed to duplicate component",
754 | "errorDetails": str(e)
755 | }
756 |
757 | def _place_grid_array(self, component_id: str, start_position: Dict[str, Any],
758 | rows: int, columns: int, spacing_x: float, spacing_y: float,
759 | reference_prefix: str, value: str, rotation: float, layer: str) -> List[Dict[str, Any]]:
760 | """Place components in a grid pattern and return the list of placed components"""
761 | placed = []
762 |
763 | # Convert spacing to nm
764 | unit = start_position.get("unit", "mm")
765 | scale = 1000000 if unit == "mm" else 25400000 # mm or inch to nm
766 | spacing_x_nm = int(spacing_x * scale)
767 | spacing_y_nm = int(spacing_y * scale)
768 |
769 | # Get layer ID
770 | layer_id = self.board.GetLayerID(layer)
771 |
772 | for row in range(rows):
773 | for col in range(columns):
774 | # Calculate position
775 | x = start_position["x"] + (col * spacing_x)
776 | y = start_position["y"] + (row * spacing_y)
777 |
778 | # Generate reference
779 | index = row * columns + col + 1
780 | component_reference = f"{reference_prefix}{index}"
781 |
782 | # Place component
783 | result = self.place_component({
784 | "componentId": component_id,
785 | "position": {"x": x, "y": y, "unit": unit},
786 | "reference": component_reference,
787 | "value": value,
788 | "rotation": rotation,
789 | "layer": layer
790 | })
791 |
792 | if result["success"]:
793 | placed.append(result["component"])
794 |
795 | return placed
796 |
797 | def _place_circular_array(self, component_id: str, center: Dict[str, Any],
798 | radius: float, count: int, angle_start: float,
799 | angle_step: float, reference_prefix: str,
800 | value: str, rotation_offset: float, layer: str) -> List[Dict[str, Any]]:
801 | """Place components in a circular pattern and return the list of placed components"""
802 | placed = []
803 |
804 | # Get unit
805 | unit = center.get("unit", "mm")
806 |
807 | for i in range(count):
808 | # Calculate angle for this component
809 | angle = angle_start + (i * angle_step)
810 | angle_rad = math.radians(angle)
811 |
812 | # Calculate position
813 | x = center["x"] + (radius * math.cos(angle_rad))
814 | y = center["y"] + (radius * math.sin(angle_rad))
815 |
816 | # Generate reference
817 | component_reference = f"{reference_prefix}{i+1}"
818 |
819 | # Calculate rotation (pointing outward from center)
820 | component_rotation = angle + rotation_offset
821 |
822 | # Place component
823 | result = self.place_component({
824 | "componentId": component_id,
825 | "position": {"x": x, "y": y, "unit": unit},
826 | "reference": component_reference,
827 | "value": value,
828 | "rotation": component_rotation,
829 | "layer": layer
830 | })
831 |
832 | if result["success"]:
833 | placed.append(result["component"])
834 |
835 | return placed
836 |
837 | def _align_components_horizontally(self, components: List[pcbnew.FOOTPRINT],
838 | distribution: str, spacing: Optional[float]) -> None:
839 | """Align components horizontally and optionally distribute them"""
840 | if not components:
841 | return
842 |
843 | # Find the average Y coordinate
844 | y_sum = sum(module.GetPosition().y for module in components)
845 | y_avg = y_sum // len(components)
846 |
847 | # Sort components by X position
848 | components.sort(key=lambda m: m.GetPosition().x)
849 |
850 | # Set Y coordinate for all components
851 | for module in components:
852 | pos = module.GetPosition()
853 | module.SetPosition(pcbnew.VECTOR2I(pos.x, y_avg))
854 |
855 | # Handle distribution if requested
856 | if distribution == "equal" and len(components) > 1:
857 | # Get leftmost and rightmost X coordinates
858 | x_min = components[0].GetPosition().x
859 | x_max = components[-1].GetPosition().x
860 |
861 | # Calculate equal spacing
862 | total_space = x_max - x_min
863 | spacing_nm = total_space // (len(components) - 1)
864 |
865 | # Set X positions with equal spacing
866 | for i in range(1, len(components) - 1):
867 | pos = components[i].GetPosition()
868 | new_x = x_min + (i * spacing_nm)
869 | components[i].SetPosition(pcbnew.VECTOR2I(new_x, pos.y))
870 |
871 | elif distribution == "spacing" and spacing is not None:
872 | # Convert spacing to nanometers
873 | spacing_nm = int(spacing * 1000000) # assuming mm
874 |
875 | # Set X positions with the specified spacing
876 | x_current = components[0].GetPosition().x
877 | for i in range(1, len(components)):
878 | pos = components[i].GetPosition()
879 | x_current += spacing_nm
880 | components[i].SetPosition(pcbnew.VECTOR2I(x_current, pos.y))
881 |
882 | def _align_components_vertically(self, components: List[pcbnew.FOOTPRINT],
883 | distribution: str, spacing: Optional[float]) -> None:
884 | """Align components vertically and optionally distribute them"""
885 | if not components:
886 | return
887 |
888 | # Find the average X coordinate
889 | x_sum = sum(module.GetPosition().x for module in components)
890 | x_avg = x_sum // len(components)
891 |
892 | # Sort components by Y position
893 | components.sort(key=lambda m: m.GetPosition().y)
894 |
895 | # Set X coordinate for all components
896 | for module in components:
897 | pos = module.GetPosition()
898 | module.SetPosition(pcbnew.VECTOR2I(x_avg, pos.y))
899 |
900 | # Handle distribution if requested
901 | if distribution == "equal" and len(components) > 1:
902 | # Get topmost and bottommost Y coordinates
903 | y_min = components[0].GetPosition().y
904 | y_max = components[-1].GetPosition().y
905 |
906 | # Calculate equal spacing
907 | total_space = y_max - y_min
908 | spacing_nm = total_space // (len(components) - 1)
909 |
910 | # Set Y positions with equal spacing
911 | for i in range(1, len(components) - 1):
912 | pos = components[i].GetPosition()
913 | new_y = y_min + (i * spacing_nm)
914 | components[i].SetPosition(pcbnew.VECTOR2I(pos.x, new_y))
915 |
916 | elif distribution == "spacing" and spacing is not None:
917 | # Convert spacing to nanometers
918 | spacing_nm = int(spacing * 1000000) # assuming mm
919 |
920 | # Set Y positions with the specified spacing
921 | y_current = components[0].GetPosition().y
922 | for i in range(1, len(components)):
923 | pos = components[i].GetPosition()
924 | y_current += spacing_nm
925 | components[i].SetPosition(pcbnew.VECTOR2I(pos.x, y_current))
926 |
927 | def _align_components_to_edge(self, components: List[pcbnew.FOOTPRINT], edge: str) -> None:
928 | """Align components to the specified edge of the board"""
929 | if not components:
930 | return
931 |
932 | # Get board bounds
933 | board_box = self.board.GetBoardEdgesBoundingBox()
934 | left = board_box.GetLeft()
935 | right = board_box.GetRight()
936 | top = board_box.GetTop()
937 | bottom = board_box.GetBottom()
938 |
939 | # Align based on specified edge
940 | if edge == "left":
941 | for module in components:
942 | pos = module.GetPosition()
943 | module.SetPosition(pcbnew.VECTOR2I(left + 2000000, pos.y)) # 2mm offset from edge
944 | elif edge == "right":
945 | for module in components:
946 | pos = module.GetPosition()
947 | module.SetPosition(pcbnew.VECTOR2I(right - 2000000, pos.y)) # 2mm offset from edge
948 | elif edge == "top":
949 | for module in components:
950 | pos = module.GetPosition()
951 | module.SetPosition(pcbnew.VECTOR2I(pos.x, top + 2000000)) # 2mm offset from edge
952 | elif edge == "bottom":
953 | for module in components:
954 | pos = module.GetPosition()
955 | module.SetPosition(pcbnew.VECTOR2I(pos.x, bottom - 2000000)) # 2mm offset from edge
956 | else:
957 | logger.warning(f"Unknown edge alignment: {edge}")
958 |
```
--------------------------------------------------------------------------------
/python/kicad_api/ipc_backend.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | IPC API Backend (KiCAD 9.0+)
3 |
4 | Uses the official kicad-python library for inter-process communication
5 | with a running KiCAD instance. This enables REAL-TIME UI synchronization.
6 |
7 | Note: Requires KiCAD to be running with IPC server enabled:
8 | Preferences > Plugins > Enable IPC API Server
9 |
10 | Key Benefits over SWIG:
11 | - Changes appear instantly in KiCAD UI (no reload needed)
12 | - Transaction support for undo/redo
13 | - Stable API that won't break between versions
14 | - Multi-language support
15 | """
16 | import logging
17 | import os
18 | from pathlib import Path
19 | from typing import Optional, Dict, Any, List, Callable
20 |
21 | from kicad_api.base import (
22 | KiCADBackend,
23 | BoardAPI,
24 | ConnectionError,
25 | APINotAvailableError
26 | )
27 |
28 | logger = logging.getLogger(__name__)
29 |
30 | # Unit conversion constant: KiCAD IPC uses nanometers internally
31 | MM_TO_NM = 1_000_000
32 | INCH_TO_NM = 25_400_000
33 |
34 |
35 | class IPCBackend(KiCADBackend):
36 | """
37 | KiCAD IPC API backend for real-time UI synchronization.
38 |
39 | Communicates with KiCAD via Protocol Buffers over UNIX sockets.
40 | Requires KiCAD 9.0+ to be running with IPC enabled.
41 |
42 | Changes made through this backend appear immediately in the KiCAD UI
43 | without requiring manual reload.
44 | """
45 |
46 | def __init__(self):
47 | self._kicad = None
48 | self._connected = False
49 | self._version = None
50 | self._on_change_callbacks: List[Callable] = []
51 |
52 | def connect(self, socket_path: Optional[str] = None) -> bool:
53 | """
54 | Connect to running KiCAD instance via IPC.
55 |
56 | Args:
57 | socket_path: Optional socket path. If not provided, will try common locations.
58 | Use format: ipc:///tmp/kicad/api.sock
59 |
60 | Returns:
61 | True if connection successful
62 |
63 | Raises:
64 | ConnectionError: If connection fails
65 | """
66 | try:
67 | # Import here to allow module to load even without kicad-python
68 | from kipy import KiCad
69 |
70 | logger.info("Connecting to KiCAD via IPC...")
71 |
72 | # Try to connect with provided path or auto-detect
73 | socket_paths_to_try = []
74 | if socket_path:
75 | socket_paths_to_try.append(socket_path)
76 | else:
77 | # Common socket locations
78 | socket_paths_to_try = [
79 | 'ipc:///tmp/kicad/api.sock', # Linux default
80 | f'ipc:///run/user/{os.getuid()}/kicad/api.sock', # XDG runtime
81 | None # Let kipy auto-detect
82 | ]
83 |
84 | last_error = None
85 | for path in socket_paths_to_try:
86 | try:
87 | if path:
88 | logger.debug(f"Trying socket path: {path}")
89 | self._kicad = KiCad(socket_path=path)
90 | else:
91 | logger.debug("Trying auto-detection")
92 | self._kicad = KiCad()
93 |
94 | # Verify connection with ping (ping returns None on success)
95 | self._kicad.ping()
96 | logger.info(f"Connected via socket: {path or 'auto-detected'}")
97 | break
98 | except Exception as e:
99 | last_error = e
100 | logger.debug(f"Failed to connect via {path}: {e}")
101 | continue
102 | else:
103 | # None of the paths worked
104 | raise ConnectionError(f"Could not connect to KiCAD IPC: {last_error}")
105 |
106 | # Get version info
107 | self._version = self._get_kicad_version()
108 | logger.info(f"Connected to KiCAD {self._version} via IPC")
109 | self._connected = True
110 | return True
111 |
112 | except ImportError as e:
113 | logger.error("kicad-python library not found")
114 | raise APINotAvailableError(
115 | "IPC backend requires kicad-python. "
116 | "Install with: pip install kicad-python"
117 | ) from e
118 | except Exception as e:
119 | logger.error(f"Failed to connect via IPC: {e}")
120 | logger.info(
121 | "Ensure KiCAD is running with IPC enabled: "
122 | "Preferences > Plugins > Enable IPC API Server"
123 | )
124 | raise ConnectionError(f"IPC connection failed: {e}") from e
125 |
126 | def _get_kicad_version(self) -> str:
127 | """Get KiCAD version string."""
128 | try:
129 | if self._kicad.check_version():
130 | return self._kicad.get_api_version()
131 | return "9.0+ (version mismatch)"
132 | except Exception:
133 | return "unknown"
134 |
135 | def disconnect(self) -> None:
136 | """Disconnect from KiCAD."""
137 | if self._kicad:
138 | self._kicad = None
139 | self._connected = False
140 | logger.info("Disconnected from KiCAD IPC")
141 |
142 | def is_connected(self) -> bool:
143 | """Check if connected to KiCAD."""
144 | if not self._connected or not self._kicad:
145 | return False
146 | try:
147 | # ping() returns None on success, raises on failure
148 | self._kicad.ping()
149 | return True
150 | except Exception:
151 | self._connected = False
152 | return False
153 |
154 | def get_version(self) -> str:
155 | """Get KiCAD version."""
156 | return self._version or "unknown"
157 |
158 | def register_change_callback(self, callback: Callable) -> None:
159 | """Register a callback to be called when changes are made."""
160 | self._on_change_callbacks.append(callback)
161 |
162 | def _notify_change(self, change_type: str, details: Dict[str, Any]) -> None:
163 | """Notify registered callbacks of a change."""
164 | for callback in self._on_change_callbacks:
165 | try:
166 | callback(change_type, details)
167 | except Exception as e:
168 | logger.warning(f"Change callback error: {e}")
169 |
170 | # Project Operations
171 | def create_project(self, path: Path, name: str) -> Dict[str, Any]:
172 | """
173 | Create a new KiCAD project.
174 |
175 | Note: The IPC API doesn't directly create projects.
176 | Projects must be created through the UI or file system.
177 | """
178 | if not self.is_connected():
179 | raise ConnectionError("Not connected to KiCAD")
180 |
181 | # IPC API doesn't have project creation - use file-based approach
182 | logger.warning("Project creation via IPC not fully supported - using hybrid approach")
183 |
184 | # For now, we'll return info about what needs to happen
185 | return {
186 | "success": False,
187 | "message": "Direct project creation not supported via IPC",
188 | "suggestion": "Open KiCAD and create a new project, or use SWIG backend"
189 | }
190 |
191 | def open_project(self, path: Path) -> Dict[str, Any]:
192 | """Open existing project via IPC."""
193 | if not self.is_connected():
194 | raise ConnectionError("Not connected to KiCAD")
195 |
196 | try:
197 | # Check for open documents
198 | documents = self._kicad.get_open_documents()
199 |
200 | # Look for matching project
201 | path_str = str(path)
202 | for doc in documents:
203 | if path_str in str(doc):
204 | return {
205 | "success": True,
206 | "message": f"Project already open: {path}",
207 | "path": str(path)
208 | }
209 |
210 | return {
211 | "success": False,
212 | "message": "Project not currently open in KiCAD",
213 | "suggestion": "Open the project in KiCAD first, then connect via IPC"
214 | }
215 |
216 | except Exception as e:
217 | logger.error(f"Failed to check project: {e}")
218 | return {
219 | "success": False,
220 | "message": "Failed to check project",
221 | "errorDetails": str(e)
222 | }
223 |
224 | def save_project(self, path: Optional[Path] = None) -> Dict[str, Any]:
225 | """Save current project via IPC."""
226 | if not self.is_connected():
227 | raise ConnectionError("Not connected to KiCAD")
228 |
229 | try:
230 | board = self._kicad.get_board()
231 | if path:
232 | board.save_as(str(path))
233 | else:
234 | board.save()
235 |
236 | self._notify_change("save", {"path": str(path) if path else "current"})
237 |
238 | return {
239 | "success": True,
240 | "message": "Project saved successfully"
241 | }
242 | except Exception as e:
243 | logger.error(f"Failed to save project: {e}")
244 | return {
245 | "success": False,
246 | "message": "Failed to save project",
247 | "errorDetails": str(e)
248 | }
249 |
250 | def close_project(self) -> None:
251 | """Close current project (not supported via IPC)."""
252 | logger.warning("Closing projects via IPC is not supported")
253 |
254 | # Board Operations
255 | def get_board(self) -> BoardAPI:
256 | """Get board API for real-time manipulation."""
257 | if not self.is_connected():
258 | raise ConnectionError("Not connected to KiCAD")
259 |
260 | return IPCBoardAPI(self._kicad, self._notify_change)
261 |
262 |
263 | class IPCBoardAPI(BoardAPI):
264 | """
265 | Board API implementation for IPC backend.
266 |
267 | All changes made through this API appear immediately in the KiCAD UI.
268 | Uses transactions for proper undo/redo support.
269 | """
270 |
271 | def __init__(self, kicad_instance, notify_callback: Callable):
272 | self._kicad = kicad_instance
273 | self._board = None
274 | self._notify = notify_callback
275 | self._current_commit = None
276 |
277 | def _get_board(self):
278 | """Get board instance, connecting if needed."""
279 | if self._board is None:
280 | try:
281 | self._board = self._kicad.get_board()
282 | except Exception as e:
283 | logger.error(f"Failed to get board: {e}")
284 | raise ConnectionError(f"No board open in KiCAD: {e}")
285 | return self._board
286 |
287 | def begin_transaction(self, description: str = "MCP Operation") -> None:
288 | """Begin a transaction for grouping operations into a single undo step."""
289 | board = self._get_board()
290 | self._current_commit = board.begin_commit()
291 | logger.debug(f"Started transaction: {description}")
292 |
293 | def commit_transaction(self, description: str = "MCP Operation") -> None:
294 | """Commit the current transaction."""
295 | if self._current_commit:
296 | board = self._get_board()
297 | board.push_commit(self._current_commit, description)
298 | self._current_commit = None
299 | logger.debug(f"Committed transaction: {description}")
300 |
301 | def rollback_transaction(self) -> None:
302 | """Roll back the current transaction."""
303 | if self._current_commit:
304 | board = self._get_board()
305 | board.drop_commit(self._current_commit)
306 | self._current_commit = None
307 | logger.debug("Rolled back transaction")
308 |
309 | def save(self) -> bool:
310 | """Save the board immediately."""
311 | try:
312 | board = self._get_board()
313 | board.save()
314 | self._notify("save", {})
315 | return True
316 | except Exception as e:
317 | logger.error(f"Failed to save board: {e}")
318 | return False
319 |
320 | def set_size(self, width: float, height: float, unit: str = "mm") -> bool:
321 | """
322 | Set board size.
323 |
324 | Note: Board size in KiCAD is typically defined by the board outline,
325 | not a direct size property. This method may need to create/modify
326 | the board outline.
327 | """
328 | try:
329 | from kipy.board_types import BoardRectangle
330 | from kipy.geometry import Vector2
331 | from kipy.util.units import from_mm
332 | from kipy.proto.board.board_types_pb2 import BoardLayer
333 |
334 | board = self._get_board()
335 |
336 | # Convert to nm
337 | if unit == "mm":
338 | w = from_mm(width)
339 | h = from_mm(height)
340 | else:
341 | w = int(width * INCH_TO_NM)
342 | h = int(height * INCH_TO_NM)
343 |
344 | # Create board outline rectangle on Edge.Cuts layer
345 | rect = BoardRectangle()
346 | rect.start = Vector2.from_xy(0, 0)
347 | rect.end = Vector2.from_xy(w, h)
348 | rect.layer = BoardLayer.BL_Edge_Cuts
349 | rect.width = from_mm(0.1) # Standard edge cut width
350 |
351 | # Begin transaction for undo support
352 | commit = board.begin_commit()
353 | board.create_items(rect)
354 | board.push_commit(commit, f"Set board size to {width}x{height} {unit}")
355 |
356 | self._notify("board_size", {"width": width, "height": height, "unit": unit})
357 |
358 | return True
359 |
360 | except Exception as e:
361 | logger.error(f"Failed to set board size: {e}")
362 | return False
363 |
364 | def get_size(self) -> Dict[str, float]:
365 | """Get current board size from bounding box."""
366 | try:
367 | board = self._get_board()
368 |
369 | # Get shapes on Edge.Cuts layer to determine board size
370 | shapes = board.get_shapes()
371 |
372 | if not shapes:
373 | return {"width": 0, "height": 0, "unit": "mm"}
374 |
375 | # Find bounding box of edge cuts
376 | from kipy.util.units import to_mm
377 |
378 | min_x = min_y = float('inf')
379 | max_x = max_y = float('-inf')
380 |
381 | for shape in shapes:
382 | # Check if on Edge.Cuts layer
383 | bbox = board.get_item_bounding_box(shape)
384 | if bbox:
385 | min_x = min(min_x, bbox.min.x)
386 | min_y = min(min_y, bbox.min.y)
387 | max_x = max(max_x, bbox.max.x)
388 | max_y = max(max_y, bbox.max.y)
389 |
390 | if min_x == float('inf'):
391 | return {"width": 0, "height": 0, "unit": "mm"}
392 |
393 | return {
394 | "width": to_mm(max_x - min_x),
395 | "height": to_mm(max_y - min_y),
396 | "unit": "mm"
397 | }
398 |
399 | except Exception as e:
400 | logger.error(f"Failed to get board size: {e}")
401 | return {"width": 0, "height": 0, "unit": "mm", "error": str(e)}
402 |
403 | def add_layer(self, layer_name: str, layer_type: str) -> bool:
404 | """Add layer to the board (layers are typically predefined in KiCAD)."""
405 | logger.warning("Layer management via IPC is limited - layers are predefined")
406 | return False
407 |
408 | def get_enabled_layers(self) -> List[str]:
409 | """Get list of enabled layers."""
410 | try:
411 | board = self._get_board()
412 | layers = board.get_enabled_layers()
413 | return [str(layer) for layer in layers]
414 | except Exception as e:
415 | logger.error(f"Failed to get enabled layers: {e}")
416 | return []
417 |
418 | def list_components(self) -> List[Dict[str, Any]]:
419 | """List all components (footprints) on the board."""
420 | try:
421 | from kipy.util.units import to_mm
422 |
423 | board = self._get_board()
424 | footprints = board.get_footprints()
425 |
426 | components = []
427 | for fp in footprints:
428 | try:
429 | pos = fp.position
430 | components.append({
431 | "reference": fp.reference_field.text.value if fp.reference_field else "",
432 | "value": fp.value_field.text.value if fp.value_field else "",
433 | "footprint": str(fp.definition.library_link) if fp.definition else "",
434 | "position": {
435 | "x": to_mm(pos.x) if pos else 0,
436 | "y": to_mm(pos.y) if pos else 0,
437 | "unit": "mm"
438 | },
439 | "rotation": fp.orientation.degrees if fp.orientation else 0,
440 | "layer": str(fp.layer) if hasattr(fp, 'layer') else "F.Cu",
441 | "id": str(fp.id) if hasattr(fp, 'id') else ""
442 | })
443 | except Exception as e:
444 | logger.warning(f"Error processing footprint: {e}")
445 | continue
446 |
447 | return components
448 |
449 | except Exception as e:
450 | logger.error(f"Failed to list components: {e}")
451 | return []
452 |
453 | def place_component(
454 | self,
455 | reference: str,
456 | footprint: str,
457 | x: float,
458 | y: float,
459 | rotation: float = 0,
460 | layer: str = "F.Cu",
461 | value: str = ""
462 | ) -> bool:
463 | """
464 | Place a component on the board.
465 |
466 | The component appears immediately in the KiCAD UI.
467 |
468 | This method uses a hybrid approach:
469 | 1. Load the footprint definition from the library using pcbnew (SWIG)
470 | 2. Place it on the board via IPC for real-time UI updates
471 |
472 | Args:
473 | reference: Component reference designator (e.g., "R1", "U1")
474 | footprint: Footprint path in format "Library:FootprintName" or just "FootprintName"
475 | x: X position in mm
476 | y: Y position in mm
477 | rotation: Rotation angle in degrees
478 | layer: Layer name ("F.Cu" for top, "B.Cu" for bottom)
479 | value: Component value (optional)
480 | """
481 | try:
482 | # First, try to load the footprint from library using pcbnew SWIG
483 | loaded_fp = self._load_footprint_from_library(footprint)
484 |
485 | if loaded_fp:
486 | # We have the footprint from the library - place it via SWIG
487 | # then sync to IPC for UI update
488 | return self._place_loaded_footprint(
489 | loaded_fp, reference, x, y, rotation, layer, value
490 | )
491 | else:
492 | # Fallback: Create a basic placeholder footprint via IPC
493 | logger.warning(f"Could not load footprint '{footprint}' from library, creating placeholder")
494 | return self._place_placeholder_footprint(
495 | reference, footprint, x, y, rotation, layer, value
496 | )
497 |
498 | except Exception as e:
499 | logger.error(f"Failed to place component: {e}")
500 | return False
501 |
502 | def _load_footprint_from_library(self, footprint_path: str):
503 | """
504 | Load a footprint from the library using pcbnew SWIG API.
505 |
506 | Args:
507 | footprint_path: Either "Library:FootprintName" or just "FootprintName"
508 |
509 | Returns:
510 | pcbnew.FOOTPRINT object or None if not found
511 | """
512 | try:
513 | import pcbnew
514 |
515 | # Parse library and footprint name
516 | if ':' in footprint_path:
517 | lib_name, fp_name = footprint_path.split(':', 1)
518 | else:
519 | # Try to find the footprint in all libraries
520 | lib_name = None
521 | fp_name = footprint_path
522 |
523 | # Get the footprint library table
524 | fp_lib_table = pcbnew.GetGlobalFootprintLib()
525 |
526 | if lib_name:
527 | # Load from specific library
528 | try:
529 | loaded_fp = pcbnew.FootprintLoad(fp_lib_table, lib_name, fp_name)
530 | if loaded_fp:
531 | logger.info(f"Loaded footprint '{fp_name}' from library '{lib_name}'")
532 | return loaded_fp
533 | except Exception as e:
534 | logger.warning(f"Could not load from {lib_name}: {e}")
535 | else:
536 | # Search all libraries for the footprint
537 | lib_names = fp_lib_table.GetLogicalLibs()
538 | for lib in lib_names:
539 | try:
540 | loaded_fp = pcbnew.FootprintLoad(fp_lib_table, lib, fp_name)
541 | if loaded_fp:
542 | logger.info(f"Found footprint '{fp_name}' in library '{lib}'")
543 | return loaded_fp
544 | except:
545 | continue
546 |
547 | logger.warning(f"Footprint '{footprint_path}' not found in any library")
548 | return None
549 |
550 | except ImportError:
551 | logger.warning("pcbnew not available - cannot load footprints from library")
552 | return None
553 | except Exception as e:
554 | logger.error(f"Error loading footprint from library: {e}")
555 | return None
556 |
557 | def _place_loaded_footprint(
558 | self,
559 | loaded_fp,
560 | reference: str,
561 | x: float,
562 | y: float,
563 | rotation: float,
564 | layer: str,
565 | value: str
566 | ) -> bool:
567 | """
568 | Place a loaded pcbnew footprint onto the board.
569 |
570 | Uses SWIG to add the footprint, then notifies for IPC sync.
571 | """
572 | try:
573 | import pcbnew
574 |
575 | # Get the board file path from IPC to load via pcbnew
576 | board = self._get_board()
577 |
578 | # Get the pcbnew board instance
579 | # We need to get the actual board file path
580 | project = board.get_project()
581 | board_path = None
582 |
583 | # Try to get the board path from kipy
584 | try:
585 | docs = self._kicad.get_open_documents()
586 | for doc in docs:
587 | if hasattr(doc, 'path') and str(doc.path).endswith('.kicad_pcb'):
588 | board_path = str(doc.path)
589 | break
590 | except Exception as e:
591 | logger.debug(f"Could not get board path from IPC: {e}")
592 |
593 | if board_path and os.path.exists(board_path):
594 | # Load board via pcbnew
595 | pcb_board = pcbnew.LoadBoard(board_path)
596 | else:
597 | # Try to get from pcbnew directly
598 | pcb_board = pcbnew.GetBoard()
599 |
600 | if not pcb_board:
601 | logger.error("Could not get pcbnew board instance")
602 | return self._place_placeholder_footprint(
603 | reference, "", x, y, rotation, layer, value
604 | )
605 |
606 | # Set footprint position and properties
607 | scale = MM_TO_NM
608 | loaded_fp.SetPosition(pcbnew.VECTOR2I(int(x * scale), int(y * scale)))
609 | loaded_fp.SetOrientationDegrees(rotation)
610 |
611 | # Set reference
612 | loaded_fp.SetReference(reference)
613 |
614 | # Set value if provided
615 | if value:
616 | loaded_fp.SetValue(value)
617 |
618 | # Set layer (flip if bottom)
619 | if layer == "B.Cu":
620 | if not loaded_fp.IsFlipped():
621 | loaded_fp.Flip(loaded_fp.GetPosition(), False)
622 |
623 | # Add to board
624 | pcb_board.Add(loaded_fp)
625 |
626 | # Save the board so IPC can see the changes
627 | pcbnew.SaveBoard(board_path, pcb_board)
628 |
629 | # Refresh IPC view
630 | try:
631 | board.revert() # Reload from disk to sync IPC
632 | except Exception as e:
633 | logger.debug(f"Could not refresh IPC board: {e}")
634 |
635 | self._notify("component_placed", {
636 | "reference": reference,
637 | "footprint": loaded_fp.GetFPIDAsString(),
638 | "position": {"x": x, "y": y},
639 | "rotation": rotation,
640 | "layer": layer,
641 | "loaded_from_library": True
642 | })
643 |
644 | logger.info(f"Placed component {reference} ({loaded_fp.GetFPIDAsString()}) at ({x}, {y}) mm")
645 | return True
646 |
647 | except Exception as e:
648 | logger.error(f"Error placing loaded footprint: {e}")
649 | # Fall back to placeholder
650 | return self._place_placeholder_footprint(
651 | reference, "", x, y, rotation, layer, value
652 | )
653 |
654 | def _place_placeholder_footprint(
655 | self,
656 | reference: str,
657 | footprint: str,
658 | x: float,
659 | y: float,
660 | rotation: float,
661 | layer: str,
662 | value: str
663 | ) -> bool:
664 | """
665 | Place a placeholder footprint when library loading fails.
666 |
667 | Creates a basic footprint via IPC with just reference/value fields.
668 | """
669 | try:
670 | from kipy.board_types import Footprint
671 | from kipy.geometry import Vector2, Angle
672 | from kipy.util.units import from_mm
673 | from kipy.proto.board.board_types_pb2 import BoardLayer
674 |
675 | board = self._get_board()
676 |
677 | # Create footprint
678 | fp = Footprint()
679 | fp.position = Vector2.from_xy(from_mm(x), from_mm(y))
680 | fp.orientation = Angle.from_degrees(rotation)
681 |
682 | # Set layer
683 | if layer == "B.Cu":
684 | fp.layer = BoardLayer.BL_B_Cu
685 | else:
686 | fp.layer = BoardLayer.BL_F_Cu
687 |
688 | # Set reference and value
689 | if fp.reference_field:
690 | fp.reference_field.text.value = reference
691 | if fp.value_field:
692 | fp.value_field.text.value = value if value else footprint
693 |
694 | # Begin transaction
695 | commit = board.begin_commit()
696 | board.create_items(fp)
697 | board.push_commit(commit, f"Placed component {reference}")
698 |
699 | self._notify("component_placed", {
700 | "reference": reference,
701 | "footprint": footprint,
702 | "position": {"x": x, "y": y},
703 | "rotation": rotation,
704 | "layer": layer,
705 | "loaded_from_library": False,
706 | "is_placeholder": True
707 | })
708 |
709 | logger.info(f"Placed placeholder component {reference} at ({x}, {y}) mm")
710 | return True
711 |
712 | except Exception as e:
713 | logger.error(f"Failed to place placeholder component: {e}")
714 | return False
715 |
716 | def move_component(self, reference: str, x: float, y: float, rotation: Optional[float] = None) -> bool:
717 | """Move a component to a new position (updates UI immediately)."""
718 | try:
719 | from kipy.geometry import Vector2, Angle
720 | from kipy.util.units import from_mm
721 |
722 | board = self._get_board()
723 | footprints = board.get_footprints()
724 |
725 | # Find the footprint by reference
726 | target_fp = None
727 | for fp in footprints:
728 | if fp.reference_field and fp.reference_field.text.value == reference:
729 | target_fp = fp
730 | break
731 |
732 | if not target_fp:
733 | logger.error(f"Component not found: {reference}")
734 | return False
735 |
736 | # Update position
737 | target_fp.position = Vector2.from_xy(from_mm(x), from_mm(y))
738 |
739 | if rotation is not None:
740 | target_fp.orientation = Angle.from_degrees(rotation)
741 |
742 | # Apply changes
743 | commit = board.begin_commit()
744 | board.update_items([target_fp])
745 | board.push_commit(commit, f"Moved component {reference}")
746 |
747 | self._notify("component_moved", {
748 | "reference": reference,
749 | "position": {"x": x, "y": y},
750 | "rotation": rotation
751 | })
752 |
753 | return True
754 |
755 | except Exception as e:
756 | logger.error(f"Failed to move component: {e}")
757 | return False
758 |
759 | def delete_component(self, reference: str) -> bool:
760 | """Delete a component from the board."""
761 | try:
762 | board = self._get_board()
763 | footprints = board.get_footprints()
764 |
765 | # Find the footprint by reference
766 | target_fp = None
767 | for fp in footprints:
768 | if fp.reference_field and fp.reference_field.text.value == reference:
769 | target_fp = fp
770 | break
771 |
772 | if not target_fp:
773 | logger.error(f"Component not found: {reference}")
774 | return False
775 |
776 | # Remove component
777 | commit = board.begin_commit()
778 | board.remove_items([target_fp])
779 | board.push_commit(commit, f"Deleted component {reference}")
780 |
781 | self._notify("component_deleted", {"reference": reference})
782 |
783 | return True
784 |
785 | except Exception as e:
786 | logger.error(f"Failed to delete component: {e}")
787 | return False
788 |
789 | def add_track(
790 | self,
791 | start_x: float,
792 | start_y: float,
793 | end_x: float,
794 | end_y: float,
795 | width: float = 0.25,
796 | layer: str = "F.Cu",
797 | net_name: Optional[str] = None
798 | ) -> bool:
799 | """
800 | Add a track (trace) to the board.
801 |
802 | The track appears immediately in the KiCAD UI.
803 | """
804 | try:
805 | from kipy.board_types import Track
806 | from kipy.geometry import Vector2
807 | from kipy.util.units import from_mm
808 | from kipy.proto.board.board_types_pb2 import BoardLayer
809 |
810 | board = self._get_board()
811 |
812 | # Create track
813 | track = Track()
814 | track.start = Vector2.from_xy(from_mm(start_x), from_mm(start_y))
815 | track.end = Vector2.from_xy(from_mm(end_x), from_mm(end_y))
816 | track.width = from_mm(width)
817 |
818 | # Set layer
819 | layer_map = {
820 | "F.Cu": BoardLayer.BL_F_Cu,
821 | "B.Cu": BoardLayer.BL_B_Cu,
822 | "In1.Cu": BoardLayer.BL_In1_Cu,
823 | "In2.Cu": BoardLayer.BL_In2_Cu,
824 | }
825 | track.layer = layer_map.get(layer, BoardLayer.BL_F_Cu)
826 |
827 | # Set net if specified
828 | if net_name:
829 | nets = board.get_nets()
830 | for net in nets:
831 | if net.name == net_name:
832 | track.net = net
833 | break
834 |
835 | # Add track with transaction
836 | commit = board.begin_commit()
837 | board.create_items(track)
838 | board.push_commit(commit, "Added track")
839 |
840 | self._notify("track_added", {
841 | "start": {"x": start_x, "y": start_y},
842 | "end": {"x": end_x, "y": end_y},
843 | "width": width,
844 | "layer": layer,
845 | "net": net_name
846 | })
847 |
848 | logger.info(f"Added track from ({start_x}, {start_y}) to ({end_x}, {end_y}) mm")
849 | return True
850 |
851 | except Exception as e:
852 | logger.error(f"Failed to add track: {e}")
853 | return False
854 |
855 | def add_via(
856 | self,
857 | x: float,
858 | y: float,
859 | diameter: float = 0.8,
860 | drill: float = 0.4,
861 | net_name: Optional[str] = None,
862 | via_type: str = "through"
863 | ) -> bool:
864 | """
865 | Add a via to the board.
866 |
867 | The via appears immediately in the KiCAD UI.
868 | """
869 | try:
870 | from kipy.board_types import Via
871 | from kipy.geometry import Vector2
872 | from kipy.util.units import from_mm
873 | from kipy.proto.board.board_types_pb2 import ViaType
874 |
875 | board = self._get_board()
876 |
877 | # Create via
878 | via = Via()
879 | via.position = Vector2.from_xy(from_mm(x), from_mm(y))
880 | via.diameter = from_mm(diameter)
881 | via.drill_diameter = from_mm(drill)
882 |
883 | # Set via type (enum values: VT_THROUGH=1, VT_BLIND_BURIED=2, VT_MICRO=3)
884 | type_map = {
885 | "through": ViaType.VT_THROUGH,
886 | "blind": ViaType.VT_BLIND_BURIED,
887 | "micro": ViaType.VT_MICRO,
888 | }
889 | via.type = type_map.get(via_type, ViaType.VT_THROUGH)
890 |
891 | # Set net if specified
892 | if net_name:
893 | nets = board.get_nets()
894 | for net in nets:
895 | if net.name == net_name:
896 | via.net = net
897 | break
898 |
899 | # Add via with transaction
900 | commit = board.begin_commit()
901 | board.create_items(via)
902 | board.push_commit(commit, "Added via")
903 |
904 | self._notify("via_added", {
905 | "position": {"x": x, "y": y},
906 | "diameter": diameter,
907 | "drill": drill,
908 | "net": net_name,
909 | "type": via_type
910 | })
911 |
912 | logger.info(f"Added via at ({x}, {y}) mm")
913 | return True
914 |
915 | except Exception as e:
916 | logger.error(f"Failed to add via: {e}")
917 | return False
918 |
919 | def add_text(
920 | self,
921 | text: str,
922 | x: float,
923 | y: float,
924 | layer: str = "F.SilkS",
925 | size: float = 1.0,
926 | rotation: float = 0
927 | ) -> bool:
928 | """Add text to the board."""
929 | try:
930 | from kipy.board_types import BoardText
931 | from kipy.geometry import Vector2, Angle
932 | from kipy.util.units import from_mm
933 | from kipy.proto.board.board_types_pb2 import BoardLayer
934 |
935 | board = self._get_board()
936 |
937 | # Create text
938 | board_text = BoardText()
939 | board_text.value = text
940 | board_text.position = Vector2.from_xy(from_mm(x), from_mm(y))
941 | board_text.angle = Angle.from_degrees(rotation)
942 |
943 | # Set layer
944 | layer_map = {
945 | "F.SilkS": BoardLayer.BL_F_SilkS,
946 | "B.SilkS": BoardLayer.BL_B_SilkS,
947 | "F.Cu": BoardLayer.BL_F_Cu,
948 | "B.Cu": BoardLayer.BL_B_Cu,
949 | }
950 | board_text.layer = layer_map.get(layer, BoardLayer.BL_F_SilkS)
951 |
952 | # Add text with transaction
953 | commit = board.begin_commit()
954 | board.create_items(board_text)
955 | board.push_commit(commit, f"Added text: {text}")
956 |
957 | self._notify("text_added", {
958 | "text": text,
959 | "position": {"x": x, "y": y},
960 | "layer": layer
961 | })
962 |
963 | return True
964 |
965 | except Exception as e:
966 | logger.error(f"Failed to add text: {e}")
967 | return False
968 |
969 | def get_tracks(self) -> List[Dict[str, Any]]:
970 | """Get all tracks on the board."""
971 | try:
972 | from kipy.util.units import to_mm
973 |
974 | board = self._get_board()
975 | tracks = board.get_tracks()
976 |
977 | result = []
978 | for track in tracks:
979 | try:
980 | result.append({
981 | "start": {
982 | "x": to_mm(track.start.x),
983 | "y": to_mm(track.start.y)
984 | },
985 | "end": {
986 | "x": to_mm(track.end.x),
987 | "y": to_mm(track.end.y)
988 | },
989 | "width": to_mm(track.width),
990 | "layer": str(track.layer),
991 | "net": track.net.name if track.net else "",
992 | "id": str(track.id) if hasattr(track, 'id') else ""
993 | })
994 | except Exception as e:
995 | logger.warning(f"Error processing track: {e}")
996 | continue
997 |
998 | return result
999 |
1000 | except Exception as e:
1001 | logger.error(f"Failed to get tracks: {e}")
1002 | return []
1003 |
1004 | def get_vias(self) -> List[Dict[str, Any]]:
1005 | """Get all vias on the board."""
1006 | try:
1007 | from kipy.util.units import to_mm
1008 |
1009 | board = self._get_board()
1010 | vias = board.get_vias()
1011 |
1012 | result = []
1013 | for via in vias:
1014 | try:
1015 | result.append({
1016 | "position": {
1017 | "x": to_mm(via.position.x),
1018 | "y": to_mm(via.position.y)
1019 | },
1020 | "diameter": to_mm(via.diameter),
1021 | "drill": to_mm(via.drill_diameter),
1022 | "net": via.net.name if via.net else "",
1023 | "type": str(via.type),
1024 | "id": str(via.id) if hasattr(via, 'id') else ""
1025 | })
1026 | except Exception as e:
1027 | logger.warning(f"Error processing via: {e}")
1028 | continue
1029 |
1030 | return result
1031 |
1032 | except Exception as e:
1033 | logger.error(f"Failed to get vias: {e}")
1034 | return []
1035 |
1036 | def get_nets(self) -> List[Dict[str, Any]]:
1037 | """Get all nets on the board."""
1038 | try:
1039 | board = self._get_board()
1040 | nets = board.get_nets()
1041 |
1042 | result = []
1043 | for net in nets:
1044 | try:
1045 | result.append({
1046 | "name": net.name,
1047 | "code": net.code if hasattr(net, 'code') else 0
1048 | })
1049 | except Exception as e:
1050 | logger.warning(f"Error processing net: {e}")
1051 | continue
1052 |
1053 | return result
1054 |
1055 | except Exception as e:
1056 | logger.error(f"Failed to get nets: {e}")
1057 | return []
1058 |
1059 | def add_zone(
1060 | self,
1061 | points: List[Dict[str, float]],
1062 | layer: str = "F.Cu",
1063 | net_name: Optional[str] = None,
1064 | clearance: float = 0.5,
1065 | min_thickness: float = 0.25,
1066 | priority: int = 0,
1067 | fill_mode: str = "solid",
1068 | name: str = ""
1069 | ) -> bool:
1070 | """
1071 | Add a copper pour zone to the board.
1072 |
1073 | The zone appears immediately in the KiCAD UI.
1074 |
1075 | Args:
1076 | points: List of points defining the zone outline, e.g. [{"x": 0, "y": 0}, ...]
1077 | layer: Layer name (F.Cu, B.Cu, etc.)
1078 | net_name: Net to connect the zone to (e.g., "GND")
1079 | clearance: Clearance from other copper in mm
1080 | min_thickness: Minimum copper thickness in mm
1081 | priority: Zone priority (higher = fills first)
1082 | fill_mode: "solid" or "hatched"
1083 | name: Optional zone name
1084 | """
1085 | try:
1086 | from kipy.board_types import Zone, ZoneFillMode, ZoneType
1087 | from kipy.geometry import PolyLine, PolyLineNode, Vector2
1088 | from kipy.util.units import from_mm
1089 | from kipy.proto.board.board_types_pb2 import BoardLayer
1090 |
1091 | board = self._get_board()
1092 |
1093 | if len(points) < 3:
1094 | logger.error("Zone requires at least 3 points")
1095 | return False
1096 |
1097 | # Create zone
1098 | zone = Zone()
1099 | zone.type = ZoneType.ZT_COPPER
1100 |
1101 | # Set layer
1102 | layer_map = {
1103 | "F.Cu": BoardLayer.BL_F_Cu,
1104 | "B.Cu": BoardLayer.BL_B_Cu,
1105 | "In1.Cu": BoardLayer.BL_In1_Cu,
1106 | "In2.Cu": BoardLayer.BL_In2_Cu,
1107 | "In3.Cu": BoardLayer.BL_In3_Cu,
1108 | "In4.Cu": BoardLayer.BL_In4_Cu,
1109 | }
1110 | zone.layers = [layer_map.get(layer, BoardLayer.BL_F_Cu)]
1111 |
1112 | # Set net if specified
1113 | if net_name:
1114 | nets = board.get_nets()
1115 | for net in nets:
1116 | if net.name == net_name:
1117 | zone.net = net
1118 | break
1119 |
1120 | # Set zone properties
1121 | zone.clearance = from_mm(clearance)
1122 | zone.min_thickness = from_mm(min_thickness)
1123 | zone.priority = priority
1124 |
1125 | if name:
1126 | zone.name = name
1127 |
1128 | # Set fill mode
1129 | if fill_mode == "hatched":
1130 | zone.fill_mode = ZoneFillMode.ZFM_HATCHED
1131 | else:
1132 | zone.fill_mode = ZoneFillMode.ZFM_SOLID
1133 |
1134 | # Create outline polyline
1135 | outline = PolyLine()
1136 | outline.closed = True
1137 |
1138 | for point in points:
1139 | x = point.get("x", 0)
1140 | y = point.get("y", 0)
1141 | node = PolyLineNode.from_xy(from_mm(x), from_mm(y))
1142 | outline.append(node)
1143 |
1144 | # Set the outline on the zone
1145 | # Note: Zone outline is set via the proto directly since kipy
1146 | # doesn't expose a direct setter for creating new zones
1147 | zone._proto.outline.polygons.add()
1148 | zone._proto.outline.polygons[0].outline.CopyFrom(outline._proto)
1149 |
1150 | # Add zone with transaction
1151 | commit = board.begin_commit()
1152 | board.create_items(zone)
1153 | board.push_commit(commit, f"Added copper zone on {layer}")
1154 |
1155 | self._notify("zone_added", {
1156 | "layer": layer,
1157 | "net": net_name,
1158 | "points": len(points),
1159 | "priority": priority
1160 | })
1161 |
1162 | logger.info(f"Added zone on {layer} with {len(points)} points")
1163 | return True
1164 |
1165 | except Exception as e:
1166 | logger.error(f"Failed to add zone: {e}")
1167 | return False
1168 |
1169 | def get_zones(self) -> List[Dict[str, Any]]:
1170 | """Get all zones on the board."""
1171 | try:
1172 | from kipy.util.units import to_mm
1173 |
1174 | board = self._get_board()
1175 | zones = board.get_zones()
1176 |
1177 | result = []
1178 | for zone in zones:
1179 | try:
1180 | result.append({
1181 | "name": zone.name if hasattr(zone, 'name') else "",
1182 | "net": zone.net.name if zone.net else "",
1183 | "priority": zone.priority if hasattr(zone, 'priority') else 0,
1184 | "layers": [str(l) for l in zone.layers] if hasattr(zone, 'layers') else [],
1185 | "filled": zone.filled if hasattr(zone, 'filled') else False,
1186 | "id": str(zone.id) if hasattr(zone, 'id') else ""
1187 | })
1188 | except Exception as e:
1189 | logger.warning(f"Error processing zone: {e}")
1190 | continue
1191 |
1192 | return result
1193 |
1194 | except Exception as e:
1195 | logger.error(f"Failed to get zones: {e}")
1196 | return []
1197 |
1198 | def refill_zones(self) -> bool:
1199 | """Refill all copper pour zones."""
1200 | try:
1201 | board = self._get_board()
1202 | board.refill_zones()
1203 | self._notify("zones_refilled", {})
1204 | return True
1205 | except Exception as e:
1206 | logger.error(f"Failed to refill zones: {e}")
1207 | return False
1208 |
1209 | def get_selection(self) -> List[Dict[str, Any]]:
1210 | """Get currently selected items in the KiCAD UI."""
1211 | try:
1212 | board = self._get_board()
1213 | selection = board.get_selection()
1214 |
1215 | result = []
1216 | for item in selection:
1217 | result.append({
1218 | "type": type(item).__name__,
1219 | "id": str(item.id) if hasattr(item, 'id') else ""
1220 | })
1221 |
1222 | return result
1223 | except Exception as e:
1224 | logger.error(f"Failed to get selection: {e}")
1225 | return []
1226 |
1227 | def clear_selection(self) -> bool:
1228 | """Clear the current selection in KiCAD UI."""
1229 | try:
1230 | board = self._get_board()
1231 | board.clear_selection()
1232 | return True
1233 | except Exception as e:
1234 | logger.error(f"Failed to clear selection: {e}")
1235 | return False
1236 |
1237 |
1238 | # Export for factory
1239 | __all__ = ['IPCBackend', 'IPCBoardAPI']
1240 |
```
--------------------------------------------------------------------------------
/python/schemas/tool_schemas.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Comprehensive tool schema definitions for all KiCAD MCP commands
3 |
4 | Following MCP 2025-06-18 specification for tool definitions.
5 | Each tool includes:
6 | - name: Unique identifier
7 | - title: Human-readable display name
8 | - description: Detailed explanation of what the tool does
9 | - inputSchema: JSON Schema for parameters
10 | - outputSchema: Optional JSON Schema for return values (structured content)
11 | """
12 |
13 | from typing import Dict, Any
14 |
15 | # =============================================================================
16 | # PROJECT TOOLS
17 | # =============================================================================
18 |
19 | PROJECT_TOOLS = [
20 | {
21 | "name": "create_project",
22 | "title": "Create New KiCAD Project",
23 | "description": "Creates a new KiCAD project with PCB board file and optional project configuration. Automatically creates project directory and initializes board with default settings.",
24 | "inputSchema": {
25 | "type": "object",
26 | "properties": {
27 | "projectName": {
28 | "type": "string",
29 | "description": "Name of the project (used for file naming)",
30 | "minLength": 1
31 | },
32 | "path": {
33 | "type": "string",
34 | "description": "Directory path where project will be created (defaults to current working directory)"
35 | },
36 | "template": {
37 | "type": "string",
38 | "description": "Optional path to template board file to copy settings from"
39 | }
40 | },
41 | "required": ["projectName"]
42 | }
43 | },
44 | {
45 | "name": "open_project",
46 | "title": "Open Existing KiCAD Project",
47 | "description": "Opens an existing KiCAD project file (.kicad_pro or .kicad_pcb) and loads the board into memory for manipulation.",
48 | "inputSchema": {
49 | "type": "object",
50 | "properties": {
51 | "filename": {
52 | "type": "string",
53 | "description": "Path to .kicad_pro or .kicad_pcb file"
54 | }
55 | },
56 | "required": ["filename"]
57 | }
58 | },
59 | {
60 | "name": "save_project",
61 | "title": "Save Current Project",
62 | "description": "Saves the current board to disk. Can optionally save to a new location.",
63 | "inputSchema": {
64 | "type": "object",
65 | "properties": {
66 | "filename": {
67 | "type": "string",
68 | "description": "Optional new path to save the board (if not provided, saves to current location)"
69 | }
70 | }
71 | }
72 | },
73 | {
74 | "name": "get_project_info",
75 | "title": "Get Project Information",
76 | "description": "Retrieves metadata and properties of the currently open project including name, paths, and board status.",
77 | "inputSchema": {
78 | "type": "object",
79 | "properties": {}
80 | }
81 | }
82 | ]
83 |
84 | # =============================================================================
85 | # BOARD TOOLS
86 | # =============================================================================
87 |
88 | BOARD_TOOLS = [
89 | {
90 | "name": "set_board_size",
91 | "title": "Set Board Dimensions",
92 | "description": "Sets the PCB board dimensions. The board outline must be added separately using add_board_outline.",
93 | "inputSchema": {
94 | "type": "object",
95 | "properties": {
96 | "width": {
97 | "type": "number",
98 | "description": "Board width in millimeters",
99 | "minimum": 1
100 | },
101 | "height": {
102 | "type": "number",
103 | "description": "Board height in millimeters",
104 | "minimum": 1
105 | }
106 | },
107 | "required": ["width", "height"]
108 | }
109 | },
110 | {
111 | "name": "add_board_outline",
112 | "title": "Add Board Outline",
113 | "description": "Adds a board outline shape (rectangle, rounded rectangle, circle, or polygon) on the Edge.Cuts layer.",
114 | "inputSchema": {
115 | "type": "object",
116 | "properties": {
117 | "shape": {
118 | "type": "string",
119 | "enum": ["rectangle", "rounded_rectangle", "circle", "polygon"],
120 | "description": "Shape type for the board outline"
121 | },
122 | "width": {
123 | "type": "number",
124 | "description": "Width in mm (for rectangle/rounded_rectangle)",
125 | "minimum": 1
126 | },
127 | "height": {
128 | "type": "number",
129 | "description": "Height in mm (for rectangle/rounded_rectangle)",
130 | "minimum": 1
131 | },
132 | "radius": {
133 | "type": "number",
134 | "description": "Radius in mm (for circle) or corner radius (for rounded_rectangle)",
135 | "minimum": 0
136 | },
137 | "points": {
138 | "type": "array",
139 | "description": "Array of [x, y] coordinates in mm (for polygon)",
140 | "items": {
141 | "type": "array",
142 | "items": {"type": "number"},
143 | "minItems": 2,
144 | "maxItems": 2
145 | },
146 | "minItems": 3
147 | }
148 | },
149 | "required": ["shape"]
150 | }
151 | },
152 | {
153 | "name": "add_layer",
154 | "title": "Add Custom Layer",
155 | "description": "Adds a new custom layer to the board stack (e.g., User.1, User.Comments).",
156 | "inputSchema": {
157 | "type": "object",
158 | "properties": {
159 | "layerName": {
160 | "type": "string",
161 | "description": "Name of the layer to add"
162 | },
163 | "layerType": {
164 | "type": "string",
165 | "enum": ["signal", "power", "mixed", "jumper"],
166 | "description": "Type of layer (for copper layers)"
167 | }
168 | },
169 | "required": ["layerName"]
170 | }
171 | },
172 | {
173 | "name": "set_active_layer",
174 | "title": "Set Active Layer",
175 | "description": "Sets the currently active layer for drawing operations.",
176 | "inputSchema": {
177 | "type": "object",
178 | "properties": {
179 | "layerName": {
180 | "type": "string",
181 | "description": "Name of the layer to make active (e.g., F.Cu, B.Cu, Edge.Cuts)"
182 | }
183 | },
184 | "required": ["layerName"]
185 | }
186 | },
187 | {
188 | "name": "get_layer_list",
189 | "title": "List Board Layers",
190 | "description": "Returns a list of all layers in the board with their properties.",
191 | "inputSchema": {
192 | "type": "object",
193 | "properties": {}
194 | }
195 | },
196 | {
197 | "name": "get_board_info",
198 | "title": "Get Board Information",
199 | "description": "Retrieves comprehensive board information including dimensions, layer count, component count, and design rules.",
200 | "inputSchema": {
201 | "type": "object",
202 | "properties": {}
203 | }
204 | },
205 | {
206 | "name": "get_board_2d_view",
207 | "title": "Render Board Preview",
208 | "description": "Generates a 2D visual representation of the current board state as a PNG image.",
209 | "inputSchema": {
210 | "type": "object",
211 | "properties": {
212 | "width": {
213 | "type": "number",
214 | "description": "Image width in pixels (default: 800)",
215 | "minimum": 100,
216 | "default": 800
217 | },
218 | "height": {
219 | "type": "number",
220 | "description": "Image height in pixels (default: 600)",
221 | "minimum": 100,
222 | "default": 600
223 | }
224 | }
225 | }
226 | },
227 | {
228 | "name": "add_mounting_hole",
229 | "title": "Add Mounting Hole",
230 | "description": "Adds a mounting hole (non-plated through hole) at the specified position with given diameter.",
231 | "inputSchema": {
232 | "type": "object",
233 | "properties": {
234 | "x": {
235 | "type": "number",
236 | "description": "X coordinate in millimeters"
237 | },
238 | "y": {
239 | "type": "number",
240 | "description": "Y coordinate in millimeters"
241 | },
242 | "diameter": {
243 | "type": "number",
244 | "description": "Hole diameter in millimeters",
245 | "minimum": 0.1
246 | }
247 | },
248 | "required": ["x", "y", "diameter"]
249 | }
250 | },
251 | {
252 | "name": "add_board_text",
253 | "title": "Add Text to Board",
254 | "description": "Adds text annotation to the board on a specified layer (e.g., F.SilkS for top silkscreen).",
255 | "inputSchema": {
256 | "type": "object",
257 | "properties": {
258 | "text": {
259 | "type": "string",
260 | "description": "Text content to add",
261 | "minLength": 1
262 | },
263 | "x": {
264 | "type": "number",
265 | "description": "X coordinate in millimeters"
266 | },
267 | "y": {
268 | "type": "number",
269 | "description": "Y coordinate in millimeters"
270 | },
271 | "layer": {
272 | "type": "string",
273 | "description": "Layer name (e.g., F.SilkS, B.SilkS, F.Cu)",
274 | "default": "F.SilkS"
275 | },
276 | "size": {
277 | "type": "number",
278 | "description": "Text size in millimeters",
279 | "minimum": 0.1,
280 | "default": 1.0
281 | },
282 | "thickness": {
283 | "type": "number",
284 | "description": "Text thickness in millimeters",
285 | "minimum": 0.01,
286 | "default": 0.15
287 | }
288 | },
289 | "required": ["text", "x", "y"]
290 | }
291 | }
292 | ]
293 |
294 | # =============================================================================
295 | # COMPONENT TOOLS
296 | # =============================================================================
297 |
298 | COMPONENT_TOOLS = [
299 | {
300 | "name": "place_component",
301 | "title": "Place Component",
302 | "description": "Places a component with specified footprint at given coordinates on the board.",
303 | "inputSchema": {
304 | "type": "object",
305 | "properties": {
306 | "reference": {
307 | "type": "string",
308 | "description": "Component reference designator (e.g., R1, C2, U3)"
309 | },
310 | "footprint": {
311 | "type": "string",
312 | "description": "Footprint library:name (e.g., Resistor_SMD:R_0805_2012Metric)"
313 | },
314 | "x": {
315 | "type": "number",
316 | "description": "X coordinate in millimeters"
317 | },
318 | "y": {
319 | "type": "number",
320 | "description": "Y coordinate in millimeters"
321 | },
322 | "rotation": {
323 | "type": "number",
324 | "description": "Rotation angle in degrees (0-360)",
325 | "minimum": 0,
326 | "maximum": 360,
327 | "default": 0
328 | },
329 | "layer": {
330 | "type": "string",
331 | "enum": ["F.Cu", "B.Cu"],
332 | "description": "Board layer (top or bottom)",
333 | "default": "F.Cu"
334 | }
335 | },
336 | "required": ["reference", "footprint", "x", "y"]
337 | }
338 | },
339 | {
340 | "name": "move_component",
341 | "title": "Move Component",
342 | "description": "Moves an existing component to a new position on the board.",
343 | "inputSchema": {
344 | "type": "object",
345 | "properties": {
346 | "reference": {
347 | "type": "string",
348 | "description": "Component reference designator"
349 | },
350 | "x": {
351 | "type": "number",
352 | "description": "New X coordinate in millimeters"
353 | },
354 | "y": {
355 | "type": "number",
356 | "description": "New Y coordinate in millimeters"
357 | }
358 | },
359 | "required": ["reference", "x", "y"]
360 | }
361 | },
362 | {
363 | "name": "rotate_component",
364 | "title": "Rotate Component",
365 | "description": "Rotates a component by specified angle. Rotation is cumulative with existing rotation.",
366 | "inputSchema": {
367 | "type": "object",
368 | "properties": {
369 | "reference": {
370 | "type": "string",
371 | "description": "Component reference designator"
372 | },
373 | "angle": {
374 | "type": "number",
375 | "description": "Rotation angle in degrees (positive = counterclockwise)"
376 | }
377 | },
378 | "required": ["reference", "angle"]
379 | }
380 | },
381 | {
382 | "name": "delete_component",
383 | "title": "Delete Component",
384 | "description": "Removes a component from the board.",
385 | "inputSchema": {
386 | "type": "object",
387 | "properties": {
388 | "reference": {
389 | "type": "string",
390 | "description": "Component reference designator"
391 | }
392 | },
393 | "required": ["reference"]
394 | }
395 | },
396 | {
397 | "name": "edit_component",
398 | "title": "Edit Component Properties",
399 | "description": "Modifies properties of an existing component (value, footprint, etc.).",
400 | "inputSchema": {
401 | "type": "object",
402 | "properties": {
403 | "reference": {
404 | "type": "string",
405 | "description": "Component reference designator"
406 | },
407 | "value": {
408 | "type": "string",
409 | "description": "New component value"
410 | },
411 | "footprint": {
412 | "type": "string",
413 | "description": "New footprint library:name"
414 | }
415 | },
416 | "required": ["reference"]
417 | }
418 | },
419 | {
420 | "name": "get_component_properties",
421 | "title": "Get Component Properties",
422 | "description": "Retrieves detailed properties of a specific component.",
423 | "inputSchema": {
424 | "type": "object",
425 | "properties": {
426 | "reference": {
427 | "type": "string",
428 | "description": "Component reference designator"
429 | }
430 | },
431 | "required": ["reference"]
432 | }
433 | },
434 | {
435 | "name": "get_component_list",
436 | "title": "List All Components",
437 | "description": "Returns a list of all components on the board with their properties.",
438 | "inputSchema": {
439 | "type": "object",
440 | "properties": {}
441 | }
442 | },
443 | {
444 | "name": "place_component_array",
445 | "title": "Place Component Array",
446 | "description": "Places multiple copies of a component in a grid or circular pattern.",
447 | "inputSchema": {
448 | "type": "object",
449 | "properties": {
450 | "referencePrefix": {
451 | "type": "string",
452 | "description": "Reference prefix (e.g., 'R' for R1, R2, R3...)"
453 | },
454 | "startNumber": {
455 | "type": "integer",
456 | "description": "Starting number for references",
457 | "minimum": 1,
458 | "default": 1
459 | },
460 | "footprint": {
461 | "type": "string",
462 | "description": "Footprint library:name"
463 | },
464 | "pattern": {
465 | "type": "string",
466 | "enum": ["grid", "circular"],
467 | "description": "Array pattern type"
468 | },
469 | "count": {
470 | "type": "integer",
471 | "description": "Total number of components to place",
472 | "minimum": 1
473 | },
474 | "startX": {
475 | "type": "number",
476 | "description": "Starting X coordinate in millimeters"
477 | },
478 | "startY": {
479 | "type": "number",
480 | "description": "Starting Y coordinate in millimeters"
481 | },
482 | "spacingX": {
483 | "type": "number",
484 | "description": "Horizontal spacing in mm (for grid pattern)"
485 | },
486 | "spacingY": {
487 | "type": "number",
488 | "description": "Vertical spacing in mm (for grid pattern)"
489 | },
490 | "radius": {
491 | "type": "number",
492 | "description": "Circle radius in mm (for circular pattern)"
493 | },
494 | "rows": {
495 | "type": "integer",
496 | "description": "Number of rows (for grid pattern)",
497 | "minimum": 1
498 | },
499 | "columns": {
500 | "type": "integer",
501 | "description": "Number of columns (for grid pattern)",
502 | "minimum": 1
503 | }
504 | },
505 | "required": ["referencePrefix", "footprint", "pattern", "count", "startX", "startY"]
506 | }
507 | },
508 | {
509 | "name": "align_components",
510 | "title": "Align Components",
511 | "description": "Aligns multiple components horizontally or vertically.",
512 | "inputSchema": {
513 | "type": "object",
514 | "properties": {
515 | "references": {
516 | "type": "array",
517 | "description": "Array of component reference designators to align",
518 | "items": {"type": "string"},
519 | "minItems": 2
520 | },
521 | "direction": {
522 | "type": "string",
523 | "enum": ["horizontal", "vertical"],
524 | "description": "Alignment direction"
525 | },
526 | "spacing": {
527 | "type": "number",
528 | "description": "Spacing between components in mm (optional, for even distribution)"
529 | }
530 | },
531 | "required": ["references", "direction"]
532 | }
533 | },
534 | {
535 | "name": "duplicate_component",
536 | "title": "Duplicate Component",
537 | "description": "Creates a copy of an existing component with new reference designator.",
538 | "inputSchema": {
539 | "type": "object",
540 | "properties": {
541 | "sourceReference": {
542 | "type": "string",
543 | "description": "Reference of component to duplicate"
544 | },
545 | "newReference": {
546 | "type": "string",
547 | "description": "Reference designator for the new component"
548 | },
549 | "offsetX": {
550 | "type": "number",
551 | "description": "X offset from original position in mm",
552 | "default": 0
553 | },
554 | "offsetY": {
555 | "type": "number",
556 | "description": "Y offset from original position in mm",
557 | "default": 0
558 | }
559 | },
560 | "required": ["sourceReference", "newReference"]
561 | }
562 | }
563 | ]
564 |
565 | # =============================================================================
566 | # ROUTING TOOLS
567 | # =============================================================================
568 |
569 | ROUTING_TOOLS = [
570 | {
571 | "name": "add_net",
572 | "title": "Create Electrical Net",
573 | "description": "Creates a new electrical net for signal routing.",
574 | "inputSchema": {
575 | "type": "object",
576 | "properties": {
577 | "netName": {
578 | "type": "string",
579 | "description": "Name of the net (e.g., VCC, GND, SDA)",
580 | "minLength": 1
581 | },
582 | "netClass": {
583 | "type": "string",
584 | "description": "Optional net class to assign (must exist first)"
585 | }
586 | },
587 | "required": ["netName"]
588 | }
589 | },
590 | {
591 | "name": "route_trace",
592 | "title": "Route PCB Trace",
593 | "description": "Routes a copper trace between two points or pads on a specified layer.",
594 | "inputSchema": {
595 | "type": "object",
596 | "properties": {
597 | "netName": {
598 | "type": "string",
599 | "description": "Net name for this trace"
600 | },
601 | "layer": {
602 | "type": "string",
603 | "description": "Layer to route on (e.g., F.Cu, B.Cu)",
604 | "default": "F.Cu"
605 | },
606 | "width": {
607 | "type": "number",
608 | "description": "Trace width in millimeters",
609 | "minimum": 0.1
610 | },
611 | "points": {
612 | "type": "array",
613 | "description": "Array of [x, y] waypoints in millimeters",
614 | "items": {
615 | "type": "array",
616 | "items": {"type": "number"},
617 | "minItems": 2,
618 | "maxItems": 2
619 | },
620 | "minItems": 2
621 | }
622 | },
623 | "required": ["points", "width"]
624 | }
625 | },
626 | {
627 | "name": "add_via",
628 | "title": "Add Via",
629 | "description": "Adds a via (plated through-hole) to connect traces between layers.",
630 | "inputSchema": {
631 | "type": "object",
632 | "properties": {
633 | "x": {
634 | "type": "number",
635 | "description": "X coordinate in millimeters"
636 | },
637 | "y": {
638 | "type": "number",
639 | "description": "Y coordinate in millimeters"
640 | },
641 | "diameter": {
642 | "type": "number",
643 | "description": "Via diameter in millimeters",
644 | "minimum": 0.1
645 | },
646 | "drill": {
647 | "type": "number",
648 | "description": "Drill diameter in millimeters",
649 | "minimum": 0.1
650 | },
651 | "netName": {
652 | "type": "string",
653 | "description": "Net name to assign to this via"
654 | }
655 | },
656 | "required": ["x", "y", "diameter", "drill"]
657 | }
658 | },
659 | {
660 | "name": "delete_trace",
661 | "title": "Delete Trace",
662 | "description": "Removes a trace or segment from the board.",
663 | "inputSchema": {
664 | "type": "object",
665 | "properties": {
666 | "traceId": {
667 | "type": "string",
668 | "description": "Identifier of the trace to delete"
669 | }
670 | },
671 | "required": ["traceId"]
672 | }
673 | },
674 | {
675 | "name": "get_nets_list",
676 | "title": "List All Nets",
677 | "description": "Returns a list of all electrical nets defined on the board.",
678 | "inputSchema": {
679 | "type": "object",
680 | "properties": {}
681 | }
682 | },
683 | {
684 | "name": "create_netclass",
685 | "title": "Create Net Class",
686 | "description": "Defines a net class with specific routing rules (trace width, clearance, etc.).",
687 | "inputSchema": {
688 | "type": "object",
689 | "properties": {
690 | "name": {
691 | "type": "string",
692 | "description": "Net class name",
693 | "minLength": 1
694 | },
695 | "traceWidth": {
696 | "type": "number",
697 | "description": "Default trace width in millimeters",
698 | "minimum": 0.1
699 | },
700 | "clearance": {
701 | "type": "number",
702 | "description": "Clearance in millimeters",
703 | "minimum": 0.1
704 | },
705 | "viaDiameter": {
706 | "type": "number",
707 | "description": "Via diameter in millimeters"
708 | },
709 | "viaDrill": {
710 | "type": "number",
711 | "description": "Via drill diameter in millimeters"
712 | }
713 | },
714 | "required": ["name", "traceWidth", "clearance"]
715 | }
716 | },
717 | {
718 | "name": "add_copper_pour",
719 | "title": "Add Copper Pour",
720 | "description": "Creates a copper pour/zone (typically for ground or power planes).",
721 | "inputSchema": {
722 | "type": "object",
723 | "properties": {
724 | "netName": {
725 | "type": "string",
726 | "description": "Net to connect this copper pour to (e.g., GND, VCC)"
727 | },
728 | "layer": {
729 | "type": "string",
730 | "description": "Layer for the copper pour (e.g., F.Cu, B.Cu)"
731 | },
732 | "priority": {
733 | "type": "integer",
734 | "description": "Pour priority (higher priorities fill first)",
735 | "minimum": 0,
736 | "default": 0
737 | },
738 | "clearance": {
739 | "type": "number",
740 | "description": "Clearance from other objects in millimeters",
741 | "minimum": 0.1
742 | },
743 | "outline": {
744 | "type": "array",
745 | "description": "Array of [x, y] points defining the pour boundary",
746 | "items": {
747 | "type": "array",
748 | "items": {"type": "number"},
749 | "minItems": 2,
750 | "maxItems": 2
751 | },
752 | "minItems": 3
753 | }
754 | },
755 | "required": ["netName", "layer", "outline"]
756 | }
757 | },
758 | {
759 | "name": "route_differential_pair",
760 | "title": "Route Differential Pair",
761 | "description": "Routes a differential signal pair with matched lengths and spacing.",
762 | "inputSchema": {
763 | "type": "object",
764 | "properties": {
765 | "positiveName": {
766 | "type": "string",
767 | "description": "Positive signal net name"
768 | },
769 | "negativeName": {
770 | "type": "string",
771 | "description": "Negative signal net name"
772 | },
773 | "layer": {
774 | "type": "string",
775 | "description": "Layer to route on"
776 | },
777 | "width": {
778 | "type": "number",
779 | "description": "Trace width in millimeters"
780 | },
781 | "gap": {
782 | "type": "number",
783 | "description": "Gap between traces in millimeters"
784 | },
785 | "points": {
786 | "type": "array",
787 | "description": "Waypoints for the pair routing",
788 | "items": {
789 | "type": "array",
790 | "items": {"type": "number"},
791 | "minItems": 2,
792 | "maxItems": 2
793 | },
794 | "minItems": 2
795 | }
796 | },
797 | "required": ["positiveName", "negativeName", "width", "gap", "points"]
798 | }
799 | }
800 | ]
801 |
802 | # =============================================================================
803 | # LIBRARY TOOLS
804 | # =============================================================================
805 |
806 | LIBRARY_TOOLS = [
807 | {
808 | "name": "list_libraries",
809 | "title": "List Footprint Libraries",
810 | "description": "Lists all available footprint libraries accessible to KiCAD.",
811 | "inputSchema": {
812 | "type": "object",
813 | "properties": {}
814 | }
815 | },
816 | {
817 | "name": "search_footprints",
818 | "title": "Search Footprints",
819 | "description": "Searches for footprints matching a query string across all libraries.",
820 | "inputSchema": {
821 | "type": "object",
822 | "properties": {
823 | "query": {
824 | "type": "string",
825 | "description": "Search query (e.g., '0805', 'SOIC', 'QFP')",
826 | "minLength": 1
827 | },
828 | "library": {
829 | "type": "string",
830 | "description": "Optional library to restrict search to"
831 | }
832 | },
833 | "required": ["query"]
834 | }
835 | },
836 | {
837 | "name": "list_library_footprints",
838 | "title": "List Footprints in Library",
839 | "description": "Lists all footprints available in a specific library.",
840 | "inputSchema": {
841 | "type": "object",
842 | "properties": {
843 | "library": {
844 | "type": "string",
845 | "description": "Library name (e.g., Resistor_SMD, Connector_PinHeader)",
846 | "minLength": 1
847 | }
848 | },
849 | "required": ["library"]
850 | }
851 | },
852 | {
853 | "name": "get_footprint_info",
854 | "title": "Get Footprint Details",
855 | "description": "Retrieves detailed information about a specific footprint including pad layout, dimensions, and description.",
856 | "inputSchema": {
857 | "type": "object",
858 | "properties": {
859 | "library": {
860 | "type": "string",
861 | "description": "Library name"
862 | },
863 | "footprint": {
864 | "type": "string",
865 | "description": "Footprint name"
866 | }
867 | },
868 | "required": ["library", "footprint"]
869 | }
870 | }
871 | ]
872 |
873 | # =============================================================================
874 | # DESIGN RULE TOOLS
875 | # =============================================================================
876 |
877 | DESIGN_RULE_TOOLS = [
878 | {
879 | "name": "set_design_rules",
880 | "title": "Set Design Rules",
881 | "description": "Configures board design rules including clearances, trace widths, and via sizes.",
882 | "inputSchema": {
883 | "type": "object",
884 | "properties": {
885 | "clearance": {
886 | "type": "number",
887 | "description": "Minimum clearance between copper in millimeters",
888 | "minimum": 0.1
889 | },
890 | "trackWidth": {
891 | "type": "number",
892 | "description": "Minimum track width in millimeters",
893 | "minimum": 0.1
894 | },
895 | "viaDiameter": {
896 | "type": "number",
897 | "description": "Minimum via diameter in millimeters"
898 | },
899 | "viaDrill": {
900 | "type": "number",
901 | "description": "Minimum via drill diameter in millimeters"
902 | },
903 | "microViaD iameter": {
904 | "type": "number",
905 | "description": "Minimum micro-via diameter in millimeters"
906 | }
907 | }
908 | }
909 | },
910 | {
911 | "name": "get_design_rules",
912 | "title": "Get Current Design Rules",
913 | "description": "Retrieves the currently configured design rules from the board.",
914 | "inputSchema": {
915 | "type": "object",
916 | "properties": {}
917 | }
918 | },
919 | {
920 | "name": "run_drc",
921 | "title": "Run Design Rule Check",
922 | "description": "Executes a design rule check (DRC) on the current board and reports violations.",
923 | "inputSchema": {
924 | "type": "object",
925 | "properties": {
926 | "includeWarnings": {
927 | "type": "boolean",
928 | "description": "Include warnings in addition to errors",
929 | "default": True
930 | }
931 | }
932 | }
933 | },
934 | {
935 | "name": "get_drc_violations",
936 | "title": "Get DRC Violations",
937 | "description": "Returns a list of design rule violations from the most recent DRC run.",
938 | "inputSchema": {
939 | "type": "object",
940 | "properties": {}
941 | }
942 | }
943 | ]
944 |
945 | # =============================================================================
946 | # EXPORT TOOLS
947 | # =============================================================================
948 |
949 | EXPORT_TOOLS = [
950 | {
951 | "name": "export_gerber",
952 | "title": "Export Gerber Files",
953 | "description": "Generates Gerber files for PCB fabrication (industry standard format).",
954 | "inputSchema": {
955 | "type": "object",
956 | "properties": {
957 | "outputPath": {
958 | "type": "string",
959 | "description": "Directory path for output files"
960 | },
961 | "layers": {
962 | "type": "array",
963 | "description": "List of layers to export (if not provided, exports all copper and mask layers)",
964 | "items": {"type": "string"}
965 | },
966 | "includeDrillFiles": {
967 | "type": "boolean",
968 | "description": "Include drill files (Excellon format)",
969 | "default": True
970 | }
971 | },
972 | "required": ["outputPath"]
973 | }
974 | },
975 | {
976 | "name": "export_pdf",
977 | "title": "Export PDF",
978 | "description": "Exports the board layout as a PDF document for documentation or review.",
979 | "inputSchema": {
980 | "type": "object",
981 | "properties": {
982 | "outputPath": {
983 | "type": "string",
984 | "description": "Path for output PDF file"
985 | },
986 | "layers": {
987 | "type": "array",
988 | "description": "Layers to include in PDF",
989 | "items": {"type": "string"}
990 | },
991 | "colorMode": {
992 | "type": "string",
993 | "enum": ["color", "black_white"],
994 | "description": "Color mode for output",
995 | "default": "color"
996 | }
997 | },
998 | "required": ["outputPath"]
999 | }
1000 | },
1001 | {
1002 | "name": "export_svg",
1003 | "title": "Export SVG",
1004 | "description": "Exports the board as Scalable Vector Graphics for documentation or web display.",
1005 | "inputSchema": {
1006 | "type": "object",
1007 | "properties": {
1008 | "outputPath": {
1009 | "type": "string",
1010 | "description": "Path for output SVG file"
1011 | },
1012 | "layers": {
1013 | "type": "array",
1014 | "description": "Layers to include in SVG",
1015 | "items": {"type": "string"}
1016 | }
1017 | },
1018 | "required": ["outputPath"]
1019 | }
1020 | },
1021 | {
1022 | "name": "export_3d",
1023 | "title": "Export 3D Model",
1024 | "description": "Exports a 3D model of the board in STEP or VRML format for mechanical CAD integration.",
1025 | "inputSchema": {
1026 | "type": "object",
1027 | "properties": {
1028 | "outputPath": {
1029 | "type": "string",
1030 | "description": "Path for output 3D file"
1031 | },
1032 | "format": {
1033 | "type": "string",
1034 | "enum": ["step", "vrml"],
1035 | "description": "3D model format",
1036 | "default": "step"
1037 | },
1038 | "includeComponents": {
1039 | "type": "boolean",
1040 | "description": "Include 3D component models",
1041 | "default": True
1042 | }
1043 | },
1044 | "required": ["outputPath"]
1045 | }
1046 | },
1047 | {
1048 | "name": "export_bom",
1049 | "title": "Export Bill of Materials",
1050 | "description": "Generates a bill of materials (BOM) listing all components with references, values, and footprints.",
1051 | "inputSchema": {
1052 | "type": "object",
1053 | "properties": {
1054 | "outputPath": {
1055 | "type": "string",
1056 | "description": "Path for output BOM file"
1057 | },
1058 | "format": {
1059 | "type": "string",
1060 | "enum": ["csv", "xml", "html"],
1061 | "description": "BOM output format",
1062 | "default": "csv"
1063 | },
1064 | "groupByValue": {
1065 | "type": "boolean",
1066 | "description": "Group components with same value together",
1067 | "default": True
1068 | }
1069 | },
1070 | "required": ["outputPath"]
1071 | }
1072 | }
1073 | ]
1074 |
1075 | # =============================================================================
1076 | # SCHEMATIC TOOLS
1077 | # =============================================================================
1078 |
1079 | SCHEMATIC_TOOLS = [
1080 | {
1081 | "name": "create_schematic",
1082 | "title": "Create New Schematic",
1083 | "description": "Creates a new KiCAD schematic file for circuit design.",
1084 | "inputSchema": {
1085 | "type": "object",
1086 | "properties": {
1087 | "filename": {
1088 | "type": "string",
1089 | "description": "Path for the new schematic file (.kicad_sch)"
1090 | },
1091 | "title": {
1092 | "type": "string",
1093 | "description": "Schematic title"
1094 | }
1095 | },
1096 | "required": ["filename"]
1097 | }
1098 | },
1099 | {
1100 | "name": "load_schematic",
1101 | "title": "Load Existing Schematic",
1102 | "description": "Opens an existing KiCAD schematic file for editing.",
1103 | "inputSchema": {
1104 | "type": "object",
1105 | "properties": {
1106 | "filename": {
1107 | "type": "string",
1108 | "description": "Path to schematic file (.kicad_sch)"
1109 | }
1110 | },
1111 | "required": ["filename"]
1112 | }
1113 | },
1114 | {
1115 | "name": "add_schematic_component",
1116 | "title": "Add Component to Schematic",
1117 | "description": "Places a symbol (resistor, capacitor, IC, etc.) on the schematic.",
1118 | "inputSchema": {
1119 | "type": "object",
1120 | "properties": {
1121 | "reference": {
1122 | "type": "string",
1123 | "description": "Reference designator (e.g., R1, C2, U3)"
1124 | },
1125 | "symbol": {
1126 | "type": "string",
1127 | "description": "Symbol library:name (e.g., Device:R, Device:C)"
1128 | },
1129 | "value": {
1130 | "type": "string",
1131 | "description": "Component value (e.g., 10k, 0.1uF)"
1132 | },
1133 | "x": {
1134 | "type": "number",
1135 | "description": "X coordinate on schematic"
1136 | },
1137 | "y": {
1138 | "type": "number",
1139 | "description": "Y coordinate on schematic"
1140 | }
1141 | },
1142 | "required": ["reference", "symbol", "x", "y"]
1143 | }
1144 | },
1145 | {
1146 | "name": "add_schematic_wire",
1147 | "title": "Connect Components",
1148 | "description": "Draws a wire connection between component pins on the schematic.",
1149 | "inputSchema": {
1150 | "type": "object",
1151 | "properties": {
1152 | "points": {
1153 | "type": "array",
1154 | "description": "Array of [x, y] waypoints for the wire",
1155 | "items": {
1156 | "type": "array",
1157 | "items": {"type": "number"},
1158 | "minItems": 2,
1159 | "maxItems": 2
1160 | },
1161 | "minItems": 2
1162 | }
1163 | },
1164 | "required": ["points"]
1165 | }
1166 | },
1167 | {
1168 | "name": "list_schematic_libraries",
1169 | "title": "List Symbol Libraries",
1170 | "description": "Lists all available symbol libraries for schematic design.",
1171 | "inputSchema": {
1172 | "type": "object",
1173 | "properties": {
1174 | "searchPaths": {
1175 | "type": "array",
1176 | "description": "Optional additional paths to search for libraries",
1177 | "items": {"type": "string"}
1178 | }
1179 | }
1180 | }
1181 | },
1182 | {
1183 | "name": "export_schematic_pdf",
1184 | "title": "Export Schematic to PDF",
1185 | "description": "Exports the schematic as a PDF document for printing or documentation.",
1186 | "inputSchema": {
1187 | "type": "object",
1188 | "properties": {
1189 | "schematicPath": {
1190 | "type": "string",
1191 | "description": "Path to schematic file"
1192 | },
1193 | "outputPath": {
1194 | "type": "string",
1195 | "description": "Path for output PDF"
1196 | }
1197 | },
1198 | "required": ["schematicPath", "outputPath"]
1199 | }
1200 | }
1201 | ]
1202 |
1203 | # =============================================================================
1204 | # UI/PROCESS TOOLS
1205 | # =============================================================================
1206 |
1207 | UI_TOOLS = [
1208 | {
1209 | "name": "check_kicad_ui",
1210 | "title": "Check KiCAD UI Status",
1211 | "description": "Checks if KiCAD user interface is currently running and returns process information.",
1212 | "inputSchema": {
1213 | "type": "object",
1214 | "properties": {}
1215 | }
1216 | },
1217 | {
1218 | "name": "launch_kicad_ui",
1219 | "title": "Launch KiCAD Application",
1220 | "description": "Opens the KiCAD graphical user interface, optionally with a specific project loaded.",
1221 | "inputSchema": {
1222 | "type": "object",
1223 | "properties": {
1224 | "projectPath": {
1225 | "type": "string",
1226 | "description": "Optional path to project file to open in UI"
1227 | },
1228 | "autoLaunch": {
1229 | "type": "boolean",
1230 | "description": "Whether to automatically launch if not running",
1231 | "default": True
1232 | }
1233 | }
1234 | }
1235 | }
1236 | ]
1237 |
1238 | # =============================================================================
1239 | # COMBINED TOOL SCHEMAS
1240 | # =============================================================================
1241 |
1242 | TOOL_SCHEMAS: Dict[str, Any] = {}
1243 |
1244 | # Combine all tool categories
1245 | for tool in (PROJECT_TOOLS + BOARD_TOOLS + COMPONENT_TOOLS + ROUTING_TOOLS +
1246 | LIBRARY_TOOLS + DESIGN_RULE_TOOLS + EXPORT_TOOLS +
1247 | SCHEMATIC_TOOLS + UI_TOOLS):
1248 | TOOL_SCHEMAS[tool["name"]] = tool
1249 |
1250 | # Total: 46 tools with comprehensive schemas
1251 |
```