This is page 6 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/kicad_interface.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | KiCAD Python Interface Script for Model Context Protocol
4 |
5 | This script handles communication between the MCP TypeScript server
6 | and KiCAD's Python API (pcbnew). It receives commands via stdin as
7 | JSON and returns responses via stdout also as JSON.
8 | """
9 |
10 | import sys
11 | import json
12 | import traceback
13 | import logging
14 | import os
15 | from typing import Dict, Any, Optional
16 |
17 | # Import tool schemas and resource definitions
18 | from schemas.tool_schemas import TOOL_SCHEMAS
19 | from resources.resource_definitions import RESOURCE_DEFINITIONS, handle_resource_read
20 |
21 | # Configure logging
22 | log_dir = os.path.join(os.path.expanduser('~'), '.kicad-mcp', 'logs')
23 | os.makedirs(log_dir, exist_ok=True)
24 | log_file = os.path.join(log_dir, 'kicad_interface.log')
25 |
26 | logging.basicConfig(
27 | level=logging.DEBUG,
28 | format='%(asctime)s [%(levelname)s] %(message)s',
29 | handlers=[
30 | logging.FileHandler(log_file),
31 | logging.StreamHandler(sys.stderr)
32 | ]
33 | )
34 | logger = logging.getLogger('kicad_interface')
35 |
36 | # Log Python environment details
37 | logger.info(f"Python version: {sys.version}")
38 | logger.info(f"Python executable: {sys.executable}")
39 | logger.info(f"Platform: {sys.platform}")
40 | logger.info(f"Working directory: {os.getcwd()}")
41 |
42 | # Windows-specific diagnostics
43 | if sys.platform == 'win32':
44 | logger.info("=== Windows Environment Diagnostics ===")
45 | logger.info(f"PYTHONPATH: {os.environ.get('PYTHONPATH', 'NOT SET')}")
46 | logger.info(f"PATH: {os.environ.get('PATH', 'NOT SET')[:200]}...") # Truncate PATH
47 |
48 | # Check for common KiCAD installations
49 | common_kicad_paths = [
50 | r"C:\Program Files\KiCad",
51 | r"C:\Program Files (x86)\KiCad"
52 | ]
53 |
54 | found_kicad = False
55 | for base_path in common_kicad_paths:
56 | if os.path.exists(base_path):
57 | logger.info(f"Found KiCAD installation at: {base_path}")
58 | # List versions
59 | try:
60 | versions = [d for d in os.listdir(base_path) if os.path.isdir(os.path.join(base_path, d))]
61 | logger.info(f" Versions found: {', '.join(versions)}")
62 | for version in versions:
63 | python_path = os.path.join(base_path, version, 'lib', 'python3', 'dist-packages')
64 | if os.path.exists(python_path):
65 | logger.info(f" ✓ Python path exists: {python_path}")
66 | found_kicad = True
67 | else:
68 | logger.warning(f" ✗ Python path missing: {python_path}")
69 | except Exception as e:
70 | logger.warning(f" Could not list versions: {e}")
71 |
72 | if not found_kicad:
73 | logger.warning("No KiCAD installations found in standard locations!")
74 | logger.warning("Please ensure KiCAD 9.0+ is installed from https://www.kicad.org/download/windows/")
75 |
76 | logger.info("========================================")
77 |
78 | # Add utils directory to path for imports
79 | utils_dir = os.path.join(os.path.dirname(__file__))
80 | if utils_dir not in sys.path:
81 | sys.path.insert(0, utils_dir)
82 |
83 | # Import platform helper and add KiCAD paths
84 | from utils.platform_helper import PlatformHelper
85 | from utils.kicad_process import check_and_launch_kicad, KiCADProcessManager
86 |
87 | logger.info(f"Detecting KiCAD Python paths for {PlatformHelper.get_platform_name()}...")
88 | paths_added = PlatformHelper.add_kicad_to_python_path()
89 |
90 | if paths_added:
91 | logger.info("Successfully added KiCAD Python paths to sys.path")
92 | else:
93 | logger.warning("No KiCAD Python paths found - attempting to import pcbnew from system path")
94 |
95 | logger.info(f"Current Python path: {sys.path}")
96 |
97 | # Check if auto-launch is enabled
98 | AUTO_LAUNCH_KICAD = os.environ.get("KICAD_AUTO_LAUNCH", "false").lower() == "true"
99 | if AUTO_LAUNCH_KICAD:
100 | logger.info("KiCAD auto-launch enabled")
101 |
102 | # Check which backend to use
103 | # KICAD_BACKEND can be: 'auto', 'ipc', or 'swig'
104 | KICAD_BACKEND = os.environ.get("KICAD_BACKEND", "auto").lower()
105 | logger.info(f"KiCAD backend preference: {KICAD_BACKEND}")
106 |
107 | # Try to use IPC backend first if available and preferred
108 | USE_IPC_BACKEND = False
109 | ipc_backend = None
110 |
111 | if KICAD_BACKEND in ('auto', 'ipc'):
112 | try:
113 | logger.info("Checking IPC backend availability...")
114 | from kicad_api.ipc_backend import IPCBackend
115 |
116 | # Try to connect to running KiCAD
117 | ipc_backend = IPCBackend()
118 | if ipc_backend.connect():
119 | USE_IPC_BACKEND = True
120 | logger.info(f"✓ Using IPC backend - real-time UI sync enabled!")
121 | logger.info(f" KiCAD version: {ipc_backend.get_version()}")
122 | else:
123 | logger.info("IPC backend available but KiCAD not running with IPC enabled")
124 | ipc_backend = None
125 | except ImportError:
126 | logger.info("IPC backend not available (kicad-python not installed)")
127 | except Exception as e:
128 | logger.info(f"IPC backend connection failed: {e}")
129 | ipc_backend = None
130 |
131 | # Fall back to SWIG backend if IPC not available
132 | if not USE_IPC_BACKEND and KICAD_BACKEND != 'ipc':
133 | # Import KiCAD's Python API (SWIG)
134 | try:
135 | logger.info("Attempting to import pcbnew module (SWIG backend)...")
136 | import pcbnew # type: ignore
137 | logger.info(f"Successfully imported pcbnew module from: {pcbnew.__file__}")
138 | logger.info(f"pcbnew version: {pcbnew.GetBuildVersion()}")
139 | logger.warning("Using SWIG backend - changes require manual reload in KiCAD UI")
140 | except ImportError as e:
141 | logger.error(f"Failed to import pcbnew module: {e}")
142 | logger.error(f"Current sys.path: {sys.path}")
143 |
144 | # Platform-specific help message
145 | help_message = ""
146 | if sys.platform == 'win32':
147 | help_message = """
148 | Windows Troubleshooting:
149 | 1. Verify KiCAD is installed: C:\\Program Files\\KiCad\\9.0
150 | 2. Check PYTHONPATH environment variable points to:
151 | C:\\Program Files\\KiCad\\9.0\\lib\\python3\\dist-packages
152 | 3. Test with: "C:\\Program Files\\KiCad\\9.0\\bin\\python.exe" -c "import pcbnew"
153 | 4. Log file location: %USERPROFILE%\\.kicad-mcp\\logs\\kicad_interface.log
154 | 5. Run setup-windows.ps1 for automatic configuration
155 | """
156 | elif sys.platform == 'darwin':
157 | help_message = """
158 | macOS Troubleshooting:
159 | 1. Verify KiCAD is installed: /Applications/KiCad/KiCad.app
160 | 2. Check PYTHONPATH points to KiCAD's Python packages
161 | 3. Run: python3 -c "import pcbnew" to test
162 | """
163 | else: # Linux
164 | help_message = """
165 | Linux Troubleshooting:
166 | 1. Verify KiCAD is installed: apt list --installed | grep kicad
167 | 2. Check: /usr/lib/kicad/lib/python3/dist-packages exists
168 | 3. Test: python3 -c "import pcbnew"
169 | """
170 |
171 | logger.error(help_message)
172 |
173 | error_response = {
174 | "success": False,
175 | "message": "Failed to import pcbnew module - KiCAD Python API not found",
176 | "errorDetails": f"Error: {str(e)}\n\n{help_message}\n\nPython sys.path:\n{chr(10).join(sys.path)}"
177 | }
178 | print(json.dumps(error_response))
179 | sys.exit(1)
180 | except Exception as e:
181 | logger.error(f"Unexpected error importing pcbnew: {e}")
182 | logger.error(traceback.format_exc())
183 | error_response = {
184 | "success": False,
185 | "message": "Error importing pcbnew module",
186 | "errorDetails": str(e)
187 | }
188 | print(json.dumps(error_response))
189 | sys.exit(1)
190 |
191 | # If IPC-only mode requested but not available, exit with error
192 | elif KICAD_BACKEND == 'ipc' and not USE_IPC_BACKEND:
193 | error_response = {
194 | "success": False,
195 | "message": "IPC backend requested but not available",
196 | "errorDetails": "KiCAD must be running with IPC API enabled. Enable at: Preferences > Plugins > Enable IPC API Server"
197 | }
198 | print(json.dumps(error_response))
199 | sys.exit(1)
200 |
201 | # Import command handlers
202 | try:
203 | logger.info("Importing command handlers...")
204 | from commands.project import ProjectCommands
205 | from commands.board import BoardCommands
206 | from commands.component import ComponentCommands
207 | from commands.routing import RoutingCommands
208 | from commands.design_rules import DesignRuleCommands
209 | from commands.export import ExportCommands
210 | from commands.schematic import SchematicManager
211 | from commands.component_schematic import ComponentManager
212 | from commands.connection_schematic import ConnectionManager
213 | from commands.library_schematic import LibraryManager as SchematicLibraryManager
214 | from commands.library import LibraryManager as FootprintLibraryManager, LibraryCommands
215 | logger.info("Successfully imported all command handlers")
216 | except ImportError as e:
217 | logger.error(f"Failed to import command handlers: {e}")
218 | error_response = {
219 | "success": False,
220 | "message": "Failed to import command handlers",
221 | "errorDetails": str(e)
222 | }
223 | print(json.dumps(error_response))
224 | sys.exit(1)
225 |
226 | class KiCADInterface:
227 | """Main interface class to handle KiCAD operations"""
228 |
229 | def __init__(self):
230 | """Initialize the interface and command handlers"""
231 | self.board = None
232 | self.project_filename = None
233 | self.use_ipc = USE_IPC_BACKEND
234 | self.ipc_backend = ipc_backend
235 | self.ipc_board_api = None
236 |
237 | if self.use_ipc:
238 | logger.info("Initializing with IPC backend (real-time UI sync enabled)")
239 | try:
240 | self.ipc_board_api = self.ipc_backend.get_board()
241 | logger.info("✓ Got IPC board API")
242 | except Exception as e:
243 | logger.warning(f"Could not get IPC board API: {e}")
244 | else:
245 | logger.info("Initializing with SWIG backend")
246 |
247 | logger.info("Initializing command handlers...")
248 |
249 | # Initialize footprint library manager
250 | self.footprint_library = FootprintLibraryManager()
251 |
252 | # Initialize command handlers
253 | self.project_commands = ProjectCommands(self.board)
254 | self.board_commands = BoardCommands(self.board)
255 | self.component_commands = ComponentCommands(self.board, self.footprint_library)
256 | self.routing_commands = RoutingCommands(self.board)
257 | self.design_rule_commands = DesignRuleCommands(self.board)
258 | self.export_commands = ExportCommands(self.board)
259 | self.library_commands = LibraryCommands(self.footprint_library)
260 |
261 | # Schematic-related classes don't need board reference
262 | # as they operate directly on schematic files
263 |
264 | # Command routing dictionary
265 | self.command_routes = {
266 | # Project commands
267 | "create_project": self.project_commands.create_project,
268 | "open_project": self.project_commands.open_project,
269 | "save_project": self.project_commands.save_project,
270 | "get_project_info": self.project_commands.get_project_info,
271 |
272 | # Board commands
273 | "set_board_size": self.board_commands.set_board_size,
274 | "add_layer": self.board_commands.add_layer,
275 | "set_active_layer": self.board_commands.set_active_layer,
276 | "get_board_info": self.board_commands.get_board_info,
277 | "get_layer_list": self.board_commands.get_layer_list,
278 | "get_board_2d_view": self.board_commands.get_board_2d_view,
279 | "add_board_outline": self.board_commands.add_board_outline,
280 | "add_mounting_hole": self.board_commands.add_mounting_hole,
281 | "add_text": self.board_commands.add_text,
282 | "add_board_text": self.board_commands.add_text, # Alias for TypeScript tool
283 |
284 | # Component commands
285 | "place_component": self.component_commands.place_component,
286 | "move_component": self.component_commands.move_component,
287 | "rotate_component": self.component_commands.rotate_component,
288 | "delete_component": self.component_commands.delete_component,
289 | "edit_component": self.component_commands.edit_component,
290 | "get_component_properties": self.component_commands.get_component_properties,
291 | "get_component_list": self.component_commands.get_component_list,
292 | "place_component_array": self.component_commands.place_component_array,
293 | "align_components": self.component_commands.align_components,
294 | "duplicate_component": self.component_commands.duplicate_component,
295 |
296 | # Routing commands
297 | "add_net": self.routing_commands.add_net,
298 | "route_trace": self.routing_commands.route_trace,
299 | "add_via": self.routing_commands.add_via,
300 | "delete_trace": self.routing_commands.delete_trace,
301 | "get_nets_list": self.routing_commands.get_nets_list,
302 | "create_netclass": self.routing_commands.create_netclass,
303 | "add_copper_pour": self.routing_commands.add_copper_pour,
304 | "route_differential_pair": self.routing_commands.route_differential_pair,
305 | "refill_zones": self._handle_refill_zones,
306 |
307 | # Design rule commands
308 | "set_design_rules": self.design_rule_commands.set_design_rules,
309 | "get_design_rules": self.design_rule_commands.get_design_rules,
310 | "run_drc": self.design_rule_commands.run_drc,
311 | "get_drc_violations": self.design_rule_commands.get_drc_violations,
312 |
313 | # Export commands
314 | "export_gerber": self.export_commands.export_gerber,
315 | "export_pdf": self.export_commands.export_pdf,
316 | "export_svg": self.export_commands.export_svg,
317 | "export_3d": self.export_commands.export_3d,
318 | "export_bom": self.export_commands.export_bom,
319 |
320 | # Library commands (footprint management)
321 | "list_libraries": self.library_commands.list_libraries,
322 | "search_footprints": self.library_commands.search_footprints,
323 | "list_library_footprints": self.library_commands.list_library_footprints,
324 | "get_footprint_info": self.library_commands.get_footprint_info,
325 |
326 | # Schematic commands
327 | "create_schematic": self._handle_create_schematic,
328 | "load_schematic": self._handle_load_schematic,
329 | "add_schematic_component": self._handle_add_schematic_component,
330 | "add_schematic_wire": self._handle_add_schematic_wire,
331 | "add_schematic_connection": self._handle_add_schematic_connection,
332 | "add_schematic_net_label": self._handle_add_schematic_net_label,
333 | "connect_to_net": self._handle_connect_to_net,
334 | "get_net_connections": self._handle_get_net_connections,
335 | "generate_netlist": self._handle_generate_netlist,
336 | "list_schematic_libraries": self._handle_list_schematic_libraries,
337 | "export_schematic_pdf": self._handle_export_schematic_pdf,
338 |
339 | # UI/Process management commands
340 | "check_kicad_ui": self._handle_check_kicad_ui,
341 | "launch_kicad_ui": self._handle_launch_kicad_ui,
342 |
343 | # IPC-specific commands (real-time operations)
344 | "get_backend_info": self._handle_get_backend_info,
345 | "ipc_add_track": self._handle_ipc_add_track,
346 | "ipc_add_via": self._handle_ipc_add_via,
347 | "ipc_add_text": self._handle_ipc_add_text,
348 | "ipc_list_components": self._handle_ipc_list_components,
349 | "ipc_get_tracks": self._handle_ipc_get_tracks,
350 | "ipc_get_vias": self._handle_ipc_get_vias,
351 | "ipc_save_board": self._handle_ipc_save_board
352 | }
353 |
354 | logger.info(f"KiCAD interface initialized (backend: {'IPC' if self.use_ipc else 'SWIG'})")
355 |
356 | # Commands that can be handled via IPC for real-time updates
357 | IPC_CAPABLE_COMMANDS = {
358 | # Routing commands
359 | "route_trace": "_ipc_route_trace",
360 | "add_via": "_ipc_add_via",
361 | "add_net": "_ipc_add_net",
362 | "delete_trace": "_ipc_delete_trace",
363 | "get_nets_list": "_ipc_get_nets_list",
364 | # Zone commands
365 | "add_copper_pour": "_ipc_add_copper_pour",
366 | "refill_zones": "_ipc_refill_zones",
367 | # Board commands
368 | "add_text": "_ipc_add_text",
369 | "add_board_text": "_ipc_add_text",
370 | "set_board_size": "_ipc_set_board_size",
371 | "get_board_info": "_ipc_get_board_info",
372 | "add_board_outline": "_ipc_add_board_outline",
373 | "add_mounting_hole": "_ipc_add_mounting_hole",
374 | "get_layer_list": "_ipc_get_layer_list",
375 | # Component commands
376 | "place_component": "_ipc_place_component",
377 | "move_component": "_ipc_move_component",
378 | "rotate_component": "_ipc_rotate_component",
379 | "delete_component": "_ipc_delete_component",
380 | "get_component_list": "_ipc_get_component_list",
381 | "get_component_properties": "_ipc_get_component_properties",
382 | # Save command
383 | "save_project": "_ipc_save_project",
384 | }
385 |
386 | def handle_command(self, command: str, params: Dict[str, Any]) -> Dict[str, Any]:
387 | """Route command to appropriate handler, preferring IPC when available"""
388 | logger.info(f"Handling command: {command}")
389 | logger.debug(f"Command parameters: {params}")
390 |
391 | try:
392 | # Check if we can use IPC for this command (real-time UI sync)
393 | if self.use_ipc and self.ipc_board_api and command in self.IPC_CAPABLE_COMMANDS:
394 | ipc_handler_name = self.IPC_CAPABLE_COMMANDS[command]
395 | ipc_handler = getattr(self, ipc_handler_name, None)
396 |
397 | if ipc_handler:
398 | logger.info(f"Using IPC backend for {command} (real-time sync)")
399 | result = ipc_handler(params)
400 |
401 | # Add indicator that IPC was used
402 | if isinstance(result, dict):
403 | result["_backend"] = "ipc"
404 | result["_realtime"] = True
405 |
406 | logger.debug(f"IPC command result: {result}")
407 | return result
408 |
409 | # Fall back to SWIG-based handler
410 | if self.use_ipc and command in self.IPC_CAPABLE_COMMANDS:
411 | logger.warning(f"IPC handler not available for {command}, falling back to SWIG (deprecated)")
412 |
413 | # Get the handler for the command
414 | handler = self.command_routes.get(command)
415 |
416 | if handler:
417 | # Execute the command
418 | result = handler(params)
419 | logger.debug(f"Command result: {result}")
420 |
421 | # Add backend indicator
422 | if isinstance(result, dict):
423 | result["_backend"] = "swig"
424 | result["_realtime"] = False
425 |
426 | # Update board reference if command was successful
427 | if result.get("success", False):
428 | if command == "create_project" or command == "open_project":
429 | logger.info("Updating board reference...")
430 | # Get board from the project commands handler
431 | self.board = self.project_commands.board
432 | self._update_command_handlers()
433 |
434 | return result
435 | else:
436 | logger.error(f"Unknown command: {command}")
437 | return {
438 | "success": False,
439 | "message": f"Unknown command: {command}",
440 | "errorDetails": "The specified command is not supported"
441 | }
442 |
443 | except Exception as e:
444 | # Get the full traceback
445 | traceback_str = traceback.format_exc()
446 | logger.error(f"Error handling command {command}: {str(e)}\n{traceback_str}")
447 | return {
448 | "success": False,
449 | "message": f"Error handling command: {command}",
450 | "errorDetails": f"{str(e)}\n{traceback_str}"
451 | }
452 |
453 | def _update_command_handlers(self):
454 | """Update board reference in all command handlers"""
455 | logger.debug("Updating board reference in command handlers")
456 | self.project_commands.board = self.board
457 | self.board_commands.board = self.board
458 | self.component_commands.board = self.board
459 | self.routing_commands.board = self.board
460 | self.design_rule_commands.board = self.board
461 | self.export_commands.board = self.board
462 |
463 | # Schematic command handlers
464 | def _handle_create_schematic(self, params):
465 | """Create a new schematic"""
466 | logger.info("Creating schematic")
467 | try:
468 | # Support multiple parameter naming conventions for compatibility:
469 | # - TypeScript tools use: name, path
470 | # - Python schema uses: filename, title
471 | # - Legacy uses: projectName, path, metadata
472 | project_name = (
473 | params.get("projectName") or
474 | params.get("name") or
475 | params.get("title")
476 | )
477 |
478 | # Handle filename parameter - it may contain full path
479 | filename = params.get("filename")
480 | if filename:
481 | # If filename provided, extract name and path from it
482 | if filename.endswith('.kicad_sch'):
483 | filename = filename[:-10] # Remove .kicad_sch extension
484 | path = os.path.dirname(filename) or "."
485 | project_name = project_name or os.path.basename(filename)
486 | else:
487 | path = params.get("path", ".")
488 | metadata = params.get("metadata", {})
489 |
490 | if not project_name:
491 | return {
492 | "success": False,
493 | "message": "Schematic name is required. Provide 'name', 'projectName', or 'filename' parameter."
494 | }
495 |
496 | schematic = SchematicManager.create_schematic(project_name, metadata)
497 | file_path = f"{path}/{project_name}.kicad_sch"
498 | success = SchematicManager.save_schematic(schematic, file_path)
499 |
500 | return {"success": success, "file_path": file_path}
501 | except Exception as e:
502 | logger.error(f"Error creating schematic: {str(e)}")
503 | return {"success": False, "message": str(e)}
504 |
505 | def _handle_load_schematic(self, params):
506 | """Load an existing schematic"""
507 | logger.info("Loading schematic")
508 | try:
509 | filename = params.get("filename")
510 |
511 | if not filename:
512 | return {"success": False, "message": "Filename is required"}
513 |
514 | schematic = SchematicManager.load_schematic(filename)
515 | success = schematic is not None
516 |
517 | if success:
518 | metadata = SchematicManager.get_schematic_metadata(schematic)
519 | return {"success": success, "metadata": metadata}
520 | else:
521 | return {"success": False, "message": "Failed to load schematic"}
522 | except Exception as e:
523 | logger.error(f"Error loading schematic: {str(e)}")
524 | return {"success": False, "message": str(e)}
525 |
526 | def _handle_add_schematic_component(self, params):
527 | """Add a component to a schematic"""
528 | logger.info("Adding component to schematic")
529 | try:
530 | schematic_path = params.get("schematicPath")
531 | component = params.get("component", {})
532 |
533 | if not schematic_path:
534 | return {"success": False, "message": "Schematic path is required"}
535 | if not component:
536 | return {"success": False, "message": "Component definition is required"}
537 |
538 | schematic = SchematicManager.load_schematic(schematic_path)
539 | if not schematic:
540 | return {"success": False, "message": "Failed to load schematic"}
541 |
542 | component_obj = ComponentManager.add_component(schematic, component)
543 | success = component_obj is not None
544 |
545 | if success:
546 | SchematicManager.save_schematic(schematic, schematic_path)
547 | return {"success": True}
548 | else:
549 | return {"success": False, "message": "Failed to add component"}
550 | except Exception as e:
551 | logger.error(f"Error adding component to schematic: {str(e)}")
552 | return {"success": False, "message": str(e)}
553 |
554 | def _handle_add_schematic_wire(self, params):
555 | """Add a wire to a schematic"""
556 | logger.info("Adding wire to schematic")
557 | try:
558 | schematic_path = params.get("schematicPath")
559 | start_point = params.get("startPoint")
560 | end_point = params.get("endPoint")
561 |
562 | if not schematic_path:
563 | return {"success": False, "message": "Schematic path is required"}
564 | if not start_point or not end_point:
565 | return {"success": False, "message": "Start and end points are required"}
566 |
567 | schematic = SchematicManager.load_schematic(schematic_path)
568 | if not schematic:
569 | return {"success": False, "message": "Failed to load schematic"}
570 |
571 | wire = ConnectionManager.add_wire(schematic, start_point, end_point)
572 | success = wire is not None
573 |
574 | if success:
575 | SchematicManager.save_schematic(schematic, schematic_path)
576 | return {"success": True}
577 | else:
578 | return {"success": False, "message": "Failed to add wire"}
579 | except Exception as e:
580 | logger.error(f"Error adding wire to schematic: {str(e)}")
581 | return {"success": False, "message": str(e)}
582 |
583 | def _handle_list_schematic_libraries(self, params):
584 | """List available symbol libraries"""
585 | logger.info("Listing schematic libraries")
586 | try:
587 | search_paths = params.get("searchPaths")
588 |
589 | libraries = LibraryManager.list_available_libraries(search_paths)
590 | return {"success": True, "libraries": libraries}
591 | except Exception as e:
592 | logger.error(f"Error listing schematic libraries: {str(e)}")
593 | return {"success": False, "message": str(e)}
594 |
595 | def _handle_export_schematic_pdf(self, params):
596 | """Export schematic to PDF"""
597 | logger.info("Exporting schematic to PDF")
598 | try:
599 | schematic_path = params.get("schematicPath")
600 | output_path = params.get("outputPath")
601 |
602 | if not schematic_path:
603 | return {"success": False, "message": "Schematic path is required"}
604 | if not output_path:
605 | return {"success": False, "message": "Output path is required"}
606 |
607 | import subprocess
608 | result = subprocess.run(
609 | ["kicad-cli", "sch", "export", "pdf", "--output", output_path, schematic_path],
610 | capture_output=True,
611 | text=True
612 | )
613 |
614 | success = result.returncode == 0
615 | message = result.stderr if not success else ""
616 |
617 | return {"success": success, "message": message}
618 | except Exception as e:
619 | logger.error(f"Error exporting schematic to PDF: {str(e)}")
620 | return {"success": False, "message": str(e)}
621 |
622 | def _handle_add_schematic_connection(self, params):
623 | """Add a pin-to-pin connection in schematic"""
624 | logger.info("Adding pin-to-pin connection in schematic")
625 | try:
626 | schematic_path = params.get("schematicPath")
627 | source_ref = params.get("sourceRef")
628 | source_pin = params.get("sourcePin")
629 | target_ref = params.get("targetRef")
630 | target_pin = params.get("targetPin")
631 |
632 | if not all([schematic_path, source_ref, source_pin, target_ref, target_pin]):
633 | return {"success": False, "message": "Missing required parameters"}
634 |
635 | schematic = SchematicManager.load_schematic(schematic_path)
636 | if not schematic:
637 | return {"success": False, "message": "Failed to load schematic"}
638 |
639 | success = ConnectionManager.add_connection(schematic, source_ref, source_pin, target_ref, target_pin)
640 |
641 | if success:
642 | SchematicManager.save_schematic(schematic, schematic_path)
643 | return {"success": True}
644 | else:
645 | return {"success": False, "message": "Failed to add connection"}
646 | except Exception as e:
647 | logger.error(f"Error adding schematic connection: {str(e)}")
648 | return {"success": False, "message": str(e)}
649 |
650 | def _handle_add_schematic_net_label(self, params):
651 | """Add a net label to schematic"""
652 | logger.info("Adding net label to schematic")
653 | try:
654 | schematic_path = params.get("schematicPath")
655 | net_name = params.get("netName")
656 | position = params.get("position")
657 |
658 | if not all([schematic_path, net_name, position]):
659 | return {"success": False, "message": "Missing required parameters"}
660 |
661 | schematic = SchematicManager.load_schematic(schematic_path)
662 | if not schematic:
663 | return {"success": False, "message": "Failed to load schematic"}
664 |
665 | label = ConnectionManager.add_net_label(schematic, net_name, position)
666 |
667 | if label:
668 | SchematicManager.save_schematic(schematic, schematic_path)
669 | return {"success": True}
670 | else:
671 | return {"success": False, "message": "Failed to add net label"}
672 | except Exception as e:
673 | logger.error(f"Error adding net label: {str(e)}")
674 | return {"success": False, "message": str(e)}
675 |
676 | def _handle_connect_to_net(self, params):
677 | """Connect a component pin to a named net"""
678 | logger.info("Connecting component pin to net")
679 | try:
680 | schematic_path = params.get("schematicPath")
681 | component_ref = params.get("componentRef")
682 | pin_name = params.get("pinName")
683 | net_name = params.get("netName")
684 |
685 | if not all([schematic_path, component_ref, pin_name, net_name]):
686 | return {"success": False, "message": "Missing required parameters"}
687 |
688 | schematic = SchematicManager.load_schematic(schematic_path)
689 | if not schematic:
690 | return {"success": False, "message": "Failed to load schematic"}
691 |
692 | success = ConnectionManager.connect_to_net(schematic, component_ref, pin_name, net_name)
693 |
694 | if success:
695 | SchematicManager.save_schematic(schematic, schematic_path)
696 | return {"success": True}
697 | else:
698 | return {"success": False, "message": "Failed to connect to net"}
699 | except Exception as e:
700 | logger.error(f"Error connecting to net: {str(e)}")
701 | return {"success": False, "message": str(e)}
702 |
703 | def _handle_get_net_connections(self, params):
704 | """Get all connections for a named net"""
705 | logger.info("Getting net connections")
706 | try:
707 | schematic_path = params.get("schematicPath")
708 | net_name = params.get("netName")
709 |
710 | if not all([schematic_path, net_name]):
711 | return {"success": False, "message": "Missing required parameters"}
712 |
713 | schematic = SchematicManager.load_schematic(schematic_path)
714 | if not schematic:
715 | return {"success": False, "message": "Failed to load schematic"}
716 |
717 | connections = ConnectionManager.get_net_connections(schematic, net_name)
718 | return {"success": True, "connections": connections}
719 | except Exception as e:
720 | logger.error(f"Error getting net connections: {str(e)}")
721 | return {"success": False, "message": str(e)}
722 |
723 | def _handle_generate_netlist(self, params):
724 | """Generate netlist from schematic"""
725 | logger.info("Generating netlist from schematic")
726 | try:
727 | schematic_path = params.get("schematicPath")
728 |
729 | if not schematic_path:
730 | return {"success": False, "message": "Schematic path is required"}
731 |
732 | schematic = SchematicManager.load_schematic(schematic_path)
733 | if not schematic:
734 | return {"success": False, "message": "Failed to load schematic"}
735 |
736 | netlist = ConnectionManager.generate_netlist(schematic)
737 | return {"success": True, "netlist": netlist}
738 | except Exception as e:
739 | logger.error(f"Error generating netlist: {str(e)}")
740 | return {"success": False, "message": str(e)}
741 |
742 | def _handle_check_kicad_ui(self, params):
743 | """Check if KiCAD UI is running"""
744 | logger.info("Checking if KiCAD UI is running")
745 | try:
746 | manager = KiCADProcessManager()
747 | is_running = manager.is_running()
748 | processes = manager.get_process_info() if is_running else []
749 |
750 | return {
751 | "success": True,
752 | "running": is_running,
753 | "processes": processes,
754 | "message": "KiCAD is running" if is_running else "KiCAD is not running"
755 | }
756 | except Exception as e:
757 | logger.error(f"Error checking KiCAD UI status: {str(e)}")
758 | return {"success": False, "message": str(e)}
759 |
760 | def _handle_launch_kicad_ui(self, params):
761 | """Launch KiCAD UI"""
762 | logger.info("Launching KiCAD UI")
763 | try:
764 | project_path = params.get("projectPath")
765 | auto_launch = params.get("autoLaunch", AUTO_LAUNCH_KICAD)
766 |
767 | # Convert project path to Path object if provided
768 | from pathlib import Path
769 | path_obj = Path(project_path) if project_path else None
770 |
771 | result = check_and_launch_kicad(path_obj, auto_launch)
772 |
773 | return {
774 | "success": True,
775 | **result
776 | }
777 | except Exception as e:
778 | logger.error(f"Error launching KiCAD UI: {str(e)}")
779 | return {"success": False, "message": str(e)}
780 |
781 | def _handle_refill_zones(self, params):
782 | """Refill all copper pour zones on the board"""
783 | logger.info("Refilling zones")
784 | try:
785 | if not self.board:
786 | return {
787 | "success": False,
788 | "message": "No board is loaded",
789 | "errorDetails": "Load or create a board first"
790 | }
791 |
792 | # Use pcbnew's zone filler for SWIG backend
793 | filler = pcbnew.ZONE_FILLER(self.board)
794 | zones = self.board.Zones()
795 | filler.Fill(zones)
796 |
797 | return {
798 | "success": True,
799 | "message": "Zones refilled successfully",
800 | "zoneCount": zones.size() if hasattr(zones, 'size') else len(list(zones))
801 | }
802 | except Exception as e:
803 | logger.error(f"Error refilling zones: {str(e)}")
804 | return {"success": False, "message": str(e)}
805 |
806 | # =========================================================================
807 | # IPC Backend handlers - these provide real-time UI synchronization
808 | # These methods are called automatically when IPC is available
809 | # =========================================================================
810 |
811 | def _ipc_route_trace(self, params):
812 | """IPC handler for route_trace - adds track with real-time UI update"""
813 | try:
814 | # Extract parameters matching the existing route_trace interface
815 | start = params.get("start", {})
816 | end = params.get("end", {})
817 | layer = params.get("layer", "F.Cu")
818 | width = params.get("width", 0.25)
819 | net = params.get("net")
820 |
821 | # Handle both dict format and direct x/y
822 | start_x = start.get("x", 0) if isinstance(start, dict) else params.get("startX", 0)
823 | start_y = start.get("y", 0) if isinstance(start, dict) else params.get("startY", 0)
824 | end_x = end.get("x", 0) if isinstance(end, dict) else params.get("endX", 0)
825 | end_y = end.get("y", 0) if isinstance(end, dict) else params.get("endY", 0)
826 |
827 | success = self.ipc_board_api.add_track(
828 | start_x=start_x,
829 | start_y=start_y,
830 | end_x=end_x,
831 | end_y=end_y,
832 | width=width,
833 | layer=layer,
834 | net_name=net
835 | )
836 |
837 | return {
838 | "success": success,
839 | "message": "Added trace (visible in KiCAD UI)" if success else "Failed to add trace",
840 | "trace": {
841 | "start": {"x": start_x, "y": start_y, "unit": "mm"},
842 | "end": {"x": end_x, "y": end_y, "unit": "mm"},
843 | "layer": layer,
844 | "width": width,
845 | "net": net
846 | }
847 | }
848 | except Exception as e:
849 | logger.error(f"IPC route_trace error: {e}")
850 | return {"success": False, "message": str(e)}
851 |
852 | def _ipc_add_via(self, params):
853 | """IPC handler for add_via - adds via with real-time UI update"""
854 | try:
855 | position = params.get("position", {})
856 | x = position.get("x", 0) if isinstance(position, dict) else params.get("x", 0)
857 | y = position.get("y", 0) if isinstance(position, dict) else params.get("y", 0)
858 |
859 | size = params.get("size", 0.8)
860 | drill = params.get("drill", 0.4)
861 | net = params.get("net")
862 | from_layer = params.get("from_layer", "F.Cu")
863 | to_layer = params.get("to_layer", "B.Cu")
864 |
865 | success = self.ipc_board_api.add_via(
866 | x=x,
867 | y=y,
868 | diameter=size,
869 | drill=drill,
870 | net_name=net,
871 | via_type="through"
872 | )
873 |
874 | return {
875 | "success": success,
876 | "message": "Added via (visible in KiCAD UI)" if success else "Failed to add via",
877 | "via": {
878 | "position": {"x": x, "y": y, "unit": "mm"},
879 | "size": size,
880 | "drill": drill,
881 | "from_layer": from_layer,
882 | "to_layer": to_layer,
883 | "net": net
884 | }
885 | }
886 | except Exception as e:
887 | logger.error(f"IPC add_via error: {e}")
888 | return {"success": False, "message": str(e)}
889 |
890 | def _ipc_add_net(self, params):
891 | """IPC handler for add_net"""
892 | # Note: Net creation via IPC is limited - nets are typically created
893 | # when components are placed. Return success for compatibility.
894 | name = params.get("name")
895 | logger.info(f"IPC add_net: {name} (nets auto-created with components)")
896 | return {
897 | "success": True,
898 | "message": f"Net '{name}' will be created when components are connected",
899 | "net": {"name": name}
900 | }
901 |
902 | def _ipc_add_copper_pour(self, params):
903 | """IPC handler for add_copper_pour - adds zone with real-time UI update"""
904 | try:
905 | layer = params.get("layer", "F.Cu")
906 | net = params.get("net")
907 | clearance = params.get("clearance", 0.5)
908 | min_width = params.get("minWidth", 0.25)
909 | points = params.get("points", [])
910 | priority = params.get("priority", 0)
911 | fill_type = params.get("fillType", "solid")
912 | name = params.get("name", "")
913 |
914 | if not points or len(points) < 3:
915 | return {
916 | "success": False,
917 | "message": "At least 3 points are required for copper pour outline"
918 | }
919 |
920 | # Convert points format if needed (handle both {x, y} and {x, y, unit})
921 | formatted_points = []
922 | for point in points:
923 | formatted_points.append({
924 | "x": point.get("x", 0),
925 | "y": point.get("y", 0)
926 | })
927 |
928 | success = self.ipc_board_api.add_zone(
929 | points=formatted_points,
930 | layer=layer,
931 | net_name=net,
932 | clearance=clearance,
933 | min_thickness=min_width,
934 | priority=priority,
935 | fill_mode=fill_type,
936 | name=name
937 | )
938 |
939 | return {
940 | "success": success,
941 | "message": "Added copper pour (visible in KiCAD UI)" if success else "Failed to add copper pour",
942 | "pour": {
943 | "layer": layer,
944 | "net": net,
945 | "clearance": clearance,
946 | "minWidth": min_width,
947 | "priority": priority,
948 | "fillType": fill_type,
949 | "pointCount": len(points)
950 | }
951 | }
952 | except Exception as e:
953 | logger.error(f"IPC add_copper_pour error: {e}")
954 | return {"success": False, "message": str(e)}
955 |
956 | def _ipc_refill_zones(self, params):
957 | """IPC handler for refill_zones - refills all zones with real-time UI update"""
958 | try:
959 | success = self.ipc_board_api.refill_zones()
960 |
961 | return {
962 | "success": success,
963 | "message": "Zones refilled (visible in KiCAD UI)" if success else "Failed to refill zones"
964 | }
965 | except Exception as e:
966 | logger.error(f"IPC refill_zones error: {e}")
967 | return {"success": False, "message": str(e)}
968 |
969 | def _ipc_add_text(self, params):
970 | """IPC handler for add_text/add_board_text - adds text with real-time UI update"""
971 | try:
972 | text = params.get("text", "")
973 | position = params.get("position", {})
974 | x = position.get("x", 0) if isinstance(position, dict) else params.get("x", 0)
975 | y = position.get("y", 0) if isinstance(position, dict) else params.get("y", 0)
976 | layer = params.get("layer", "F.SilkS")
977 | size = params.get("size", 1.0)
978 | rotation = params.get("rotation", 0)
979 |
980 | success = self.ipc_board_api.add_text(
981 | text=text,
982 | x=x,
983 | y=y,
984 | layer=layer,
985 | size=size,
986 | rotation=rotation
987 | )
988 |
989 | return {
990 | "success": success,
991 | "message": f"Added text '{text}' (visible in KiCAD UI)" if success else "Failed to add text"
992 | }
993 | except Exception as e:
994 | logger.error(f"IPC add_text error: {e}")
995 | return {"success": False, "message": str(e)}
996 |
997 | def _ipc_set_board_size(self, params):
998 | """IPC handler for set_board_size"""
999 | try:
1000 | width = params.get("width", 100)
1001 | height = params.get("height", 100)
1002 | unit = params.get("unit", "mm")
1003 |
1004 | success = self.ipc_board_api.set_size(width, height, unit)
1005 |
1006 | return {
1007 | "success": success,
1008 | "message": f"Board size set to {width}x{height} {unit} (visible in KiCAD UI)" if success else "Failed to set board size",
1009 | "boardSize": {"width": width, "height": height, "unit": unit}
1010 | }
1011 | except Exception as e:
1012 | logger.error(f"IPC set_board_size error: {e}")
1013 | return {"success": False, "message": str(e)}
1014 |
1015 | def _ipc_get_board_info(self, params):
1016 | """IPC handler for get_board_info"""
1017 | try:
1018 | size = self.ipc_board_api.get_size()
1019 | components = self.ipc_board_api.list_components()
1020 | tracks = self.ipc_board_api.get_tracks()
1021 | vias = self.ipc_board_api.get_vias()
1022 | nets = self.ipc_board_api.get_nets()
1023 |
1024 | return {
1025 | "success": True,
1026 | "boardInfo": {
1027 | "size": size,
1028 | "componentCount": len(components),
1029 | "trackCount": len(tracks),
1030 | "viaCount": len(vias),
1031 | "netCount": len(nets),
1032 | "backend": "ipc",
1033 | "realtime": True
1034 | }
1035 | }
1036 | except Exception as e:
1037 | logger.error(f"IPC get_board_info error: {e}")
1038 | return {"success": False, "message": str(e)}
1039 |
1040 | def _ipc_place_component(self, params):
1041 | """IPC handler for place_component - places component with real-time UI update"""
1042 | try:
1043 | reference = params.get("reference", params.get("componentId", ""))
1044 | footprint = params.get("footprint", "")
1045 | position = params.get("position", {})
1046 | x = position.get("x", 0) if isinstance(position, dict) else params.get("x", 0)
1047 | y = position.get("y", 0) if isinstance(position, dict) else params.get("y", 0)
1048 | rotation = params.get("rotation", 0)
1049 | layer = params.get("layer", "F.Cu")
1050 | value = params.get("value", "")
1051 |
1052 | success = self.ipc_board_api.place_component(
1053 | reference=reference,
1054 | footprint=footprint,
1055 | x=x,
1056 | y=y,
1057 | rotation=rotation,
1058 | layer=layer,
1059 | value=value
1060 | )
1061 |
1062 | return {
1063 | "success": success,
1064 | "message": f"Placed component {reference} (visible in KiCAD UI)" if success else "Failed to place component",
1065 | "component": {
1066 | "reference": reference,
1067 | "footprint": footprint,
1068 | "position": {"x": x, "y": y, "unit": "mm"},
1069 | "rotation": rotation,
1070 | "layer": layer
1071 | }
1072 | }
1073 | except Exception as e:
1074 | logger.error(f"IPC place_component error: {e}")
1075 | return {"success": False, "message": str(e)}
1076 |
1077 | def _ipc_move_component(self, params):
1078 | """IPC handler for move_component - moves component with real-time UI update"""
1079 | try:
1080 | reference = params.get("reference", params.get("componentId", ""))
1081 | position = params.get("position", {})
1082 | x = position.get("x", 0) if isinstance(position, dict) else params.get("x", 0)
1083 | y = position.get("y", 0) if isinstance(position, dict) else params.get("y", 0)
1084 | rotation = params.get("rotation")
1085 |
1086 | success = self.ipc_board_api.move_component(
1087 | reference=reference,
1088 | x=x,
1089 | y=y,
1090 | rotation=rotation
1091 | )
1092 |
1093 | return {
1094 | "success": success,
1095 | "message": f"Moved component {reference} (visible in KiCAD UI)" if success else "Failed to move component"
1096 | }
1097 | except Exception as e:
1098 | logger.error(f"IPC move_component error: {e}")
1099 | return {"success": False, "message": str(e)}
1100 |
1101 | def _ipc_delete_component(self, params):
1102 | """IPC handler for delete_component - deletes component with real-time UI update"""
1103 | try:
1104 | reference = params.get("reference", params.get("componentId", ""))
1105 |
1106 | success = self.ipc_board_api.delete_component(reference=reference)
1107 |
1108 | return {
1109 | "success": success,
1110 | "message": f"Deleted component {reference} (visible in KiCAD UI)" if success else "Failed to delete component"
1111 | }
1112 | except Exception as e:
1113 | logger.error(f"IPC delete_component error: {e}")
1114 | return {"success": False, "message": str(e)}
1115 |
1116 | def _ipc_get_component_list(self, params):
1117 | """IPC handler for get_component_list"""
1118 | try:
1119 | components = self.ipc_board_api.list_components()
1120 |
1121 | return {
1122 | "success": True,
1123 | "components": components,
1124 | "count": len(components)
1125 | }
1126 | except Exception as e:
1127 | logger.error(f"IPC get_component_list error: {e}")
1128 | return {"success": False, "message": str(e)}
1129 |
1130 | def _ipc_save_project(self, params):
1131 | """IPC handler for save_project"""
1132 | try:
1133 | success = self.ipc_board_api.save()
1134 |
1135 | return {
1136 | "success": success,
1137 | "message": "Project saved" if success else "Failed to save project"
1138 | }
1139 | except Exception as e:
1140 | logger.error(f"IPC save_project error: {e}")
1141 | return {"success": False, "message": str(e)}
1142 |
1143 | def _ipc_delete_trace(self, params):
1144 | """IPC handler for delete_trace - Note: IPC doesn't support direct trace deletion yet"""
1145 | # IPC API doesn't have a direct delete track method
1146 | # Fall back to SWIG for this operation
1147 | logger.info("delete_trace: Falling back to SWIG (IPC doesn't support trace deletion)")
1148 | return self.routing_commands.delete_trace(params)
1149 |
1150 | def _ipc_get_nets_list(self, params):
1151 | """IPC handler for get_nets_list - gets nets with real-time data"""
1152 | try:
1153 | nets = self.ipc_board_api.get_nets()
1154 |
1155 | return {
1156 | "success": True,
1157 | "nets": nets,
1158 | "count": len(nets)
1159 | }
1160 | except Exception as e:
1161 | logger.error(f"IPC get_nets_list error: {e}")
1162 | return {"success": False, "message": str(e)}
1163 |
1164 | def _ipc_add_board_outline(self, params):
1165 | """IPC handler for add_board_outline - adds board edge with real-time UI update"""
1166 | try:
1167 | from kipy.board_types import BoardSegment
1168 | from kipy.geometry import Vector2
1169 | from kipy.util.units import from_mm
1170 | from kipy.proto.board.board_types_pb2 import BoardLayer
1171 |
1172 | board = self.ipc_board_api._get_board()
1173 |
1174 | points = params.get("points", [])
1175 | width = params.get("width", 0.1)
1176 |
1177 | if len(points) < 2:
1178 | return {"success": False, "message": "At least 2 points required for board outline"}
1179 |
1180 | commit = board.begin_commit()
1181 | lines_created = 0
1182 |
1183 | # Create line segments connecting the points
1184 | for i in range(len(points)):
1185 | start = points[i]
1186 | end = points[(i + 1) % len(points)] # Wrap around to close the outline
1187 |
1188 | segment = BoardSegment()
1189 | segment.start = Vector2.from_xy(from_mm(start.get("x", 0)), from_mm(start.get("y", 0)))
1190 | segment.end = Vector2.from_xy(from_mm(end.get("x", 0)), from_mm(end.get("y", 0)))
1191 | segment.layer = BoardLayer.BL_Edge_Cuts
1192 | segment.attributes.stroke.width = from_mm(width)
1193 |
1194 | board.create_items(segment)
1195 | lines_created += 1
1196 |
1197 | board.push_commit(commit, "Added board outline")
1198 |
1199 | return {
1200 | "success": True,
1201 | "message": f"Added board outline with {lines_created} segments (visible in KiCAD UI)",
1202 | "segments": lines_created
1203 | }
1204 | except Exception as e:
1205 | logger.error(f"IPC add_board_outline error: {e}")
1206 | return {"success": False, "message": str(e)}
1207 |
1208 | def _ipc_add_mounting_hole(self, params):
1209 | """IPC handler for add_mounting_hole - adds mounting hole with real-time UI update"""
1210 | try:
1211 | from kipy.board_types import BoardCircle
1212 | from kipy.geometry import Vector2
1213 | from kipy.util.units import from_mm
1214 | from kipy.proto.board.board_types_pb2 import BoardLayer
1215 |
1216 | board = self.ipc_board_api._get_board()
1217 |
1218 | x = params.get("x", 0)
1219 | y = params.get("y", 0)
1220 | diameter = params.get("diameter", 3.2) # M3 hole default
1221 |
1222 | commit = board.begin_commit()
1223 |
1224 | # Create circle on Edge.Cuts layer for the hole
1225 | circle = BoardCircle()
1226 | circle.center = Vector2.from_xy(from_mm(x), from_mm(y))
1227 | circle.radius = from_mm(diameter / 2)
1228 | circle.layer = BoardLayer.BL_Edge_Cuts
1229 | circle.attributes.stroke.width = from_mm(0.1)
1230 |
1231 | board.create_items(circle)
1232 | board.push_commit(commit, f"Added mounting hole at ({x}, {y})")
1233 |
1234 | return {
1235 | "success": True,
1236 | "message": f"Added mounting hole at ({x}, {y}) mm (visible in KiCAD UI)",
1237 | "hole": {
1238 | "position": {"x": x, "y": y},
1239 | "diameter": diameter
1240 | }
1241 | }
1242 | except Exception as e:
1243 | logger.error(f"IPC add_mounting_hole error: {e}")
1244 | return {"success": False, "message": str(e)}
1245 |
1246 | def _ipc_get_layer_list(self, params):
1247 | """IPC handler for get_layer_list - gets enabled layers"""
1248 | try:
1249 | layers = self.ipc_board_api.get_enabled_layers()
1250 |
1251 | return {
1252 | "success": True,
1253 | "layers": layers,
1254 | "count": len(layers)
1255 | }
1256 | except Exception as e:
1257 | logger.error(f"IPC get_layer_list error: {e}")
1258 | return {"success": False, "message": str(e)}
1259 |
1260 | def _ipc_rotate_component(self, params):
1261 | """IPC handler for rotate_component - rotates component with real-time UI update"""
1262 | try:
1263 | reference = params.get("reference", params.get("componentId", ""))
1264 | angle = params.get("angle", params.get("rotation", 90))
1265 |
1266 | # Get current component to find its position
1267 | components = self.ipc_board_api.list_components()
1268 | target = None
1269 | for comp in components:
1270 | if comp.get("reference") == reference:
1271 | target = comp
1272 | break
1273 |
1274 | if not target:
1275 | return {"success": False, "message": f"Component {reference} not found"}
1276 |
1277 | # Calculate new rotation
1278 | current_rotation = target.get("rotation", 0)
1279 | new_rotation = (current_rotation + angle) % 360
1280 |
1281 | # Use move_component with new rotation (position stays the same)
1282 | success = self.ipc_board_api.move_component(
1283 | reference=reference,
1284 | x=target.get("position", {}).get("x", 0),
1285 | y=target.get("position", {}).get("y", 0),
1286 | rotation=new_rotation
1287 | )
1288 |
1289 | return {
1290 | "success": success,
1291 | "message": f"Rotated component {reference} by {angle}° (visible in KiCAD UI)" if success else "Failed to rotate component",
1292 | "newRotation": new_rotation
1293 | }
1294 | except Exception as e:
1295 | logger.error(f"IPC rotate_component error: {e}")
1296 | return {"success": False, "message": str(e)}
1297 |
1298 | def _ipc_get_component_properties(self, params):
1299 | """IPC handler for get_component_properties - gets detailed component info"""
1300 | try:
1301 | reference = params.get("reference", params.get("componentId", ""))
1302 |
1303 | components = self.ipc_board_api.list_components()
1304 | target = None
1305 | for comp in components:
1306 | if comp.get("reference") == reference:
1307 | target = comp
1308 | break
1309 |
1310 | if not target:
1311 | return {"success": False, "message": f"Component {reference} not found"}
1312 |
1313 | return {
1314 | "success": True,
1315 | "component": target
1316 | }
1317 | except Exception as e:
1318 | logger.error(f"IPC get_component_properties error: {e}")
1319 | return {"success": False, "message": str(e)}
1320 |
1321 | # =========================================================================
1322 | # Legacy IPC command handlers (explicit ipc_* commands)
1323 | # =========================================================================
1324 |
1325 | def _handle_get_backend_info(self, params):
1326 | """Get information about the current backend"""
1327 | return {
1328 | "success": True,
1329 | "backend": "ipc" if self.use_ipc else "swig",
1330 | "realtime_sync": self.use_ipc,
1331 | "ipc_connected": self.ipc_backend.is_connected() if self.ipc_backend else False,
1332 | "version": self.ipc_backend.get_version() if self.ipc_backend else "N/A",
1333 | "message": "Using IPC backend with real-time UI sync" if self.use_ipc else "Using SWIG backend (requires manual reload)"
1334 | }
1335 |
1336 | def _handle_ipc_add_track(self, params):
1337 | """Add a track using IPC backend (real-time)"""
1338 | if not self.use_ipc or not self.ipc_board_api:
1339 | return {"success": False, "message": "IPC backend not available"}
1340 |
1341 | try:
1342 | success = self.ipc_board_api.add_track(
1343 | start_x=params.get("startX", 0),
1344 | start_y=params.get("startY", 0),
1345 | end_x=params.get("endX", 0),
1346 | end_y=params.get("endY", 0),
1347 | width=params.get("width", 0.25),
1348 | layer=params.get("layer", "F.Cu"),
1349 | net_name=params.get("net")
1350 | )
1351 | return {
1352 | "success": success,
1353 | "message": "Track added (visible in KiCAD UI)" if success else "Failed to add track",
1354 | "realtime": True
1355 | }
1356 | except Exception as e:
1357 | logger.error(f"Error adding track via IPC: {e}")
1358 | return {"success": False, "message": str(e)}
1359 |
1360 | def _handle_ipc_add_via(self, params):
1361 | """Add a via using IPC backend (real-time)"""
1362 | if not self.use_ipc or not self.ipc_board_api:
1363 | return {"success": False, "message": "IPC backend not available"}
1364 |
1365 | try:
1366 | success = self.ipc_board_api.add_via(
1367 | x=params.get("x", 0),
1368 | y=params.get("y", 0),
1369 | diameter=params.get("diameter", 0.8),
1370 | drill=params.get("drill", 0.4),
1371 | net_name=params.get("net"),
1372 | via_type=params.get("type", "through")
1373 | )
1374 | return {
1375 | "success": success,
1376 | "message": "Via added (visible in KiCAD UI)" if success else "Failed to add via",
1377 | "realtime": True
1378 | }
1379 | except Exception as e:
1380 | logger.error(f"Error adding via via IPC: {e}")
1381 | return {"success": False, "message": str(e)}
1382 |
1383 | def _handle_ipc_add_text(self, params):
1384 | """Add text using IPC backend (real-time)"""
1385 | if not self.use_ipc or not self.ipc_board_api:
1386 | return {"success": False, "message": "IPC backend not available"}
1387 |
1388 | try:
1389 | success = self.ipc_board_api.add_text(
1390 | text=params.get("text", ""),
1391 | x=params.get("x", 0),
1392 | y=params.get("y", 0),
1393 | layer=params.get("layer", "F.SilkS"),
1394 | size=params.get("size", 1.0),
1395 | rotation=params.get("rotation", 0)
1396 | )
1397 | return {
1398 | "success": success,
1399 | "message": "Text added (visible in KiCAD UI)" if success else "Failed to add text",
1400 | "realtime": True
1401 | }
1402 | except Exception as e:
1403 | logger.error(f"Error adding text via IPC: {e}")
1404 | return {"success": False, "message": str(e)}
1405 |
1406 | def _handle_ipc_list_components(self, params):
1407 | """List components using IPC backend"""
1408 | if not self.use_ipc or not self.ipc_board_api:
1409 | return {"success": False, "message": "IPC backend not available"}
1410 |
1411 | try:
1412 | components = self.ipc_board_api.list_components()
1413 | return {
1414 | "success": True,
1415 | "components": components,
1416 | "count": len(components)
1417 | }
1418 | except Exception as e:
1419 | logger.error(f"Error listing components via IPC: {e}")
1420 | return {"success": False, "message": str(e)}
1421 |
1422 | def _handle_ipc_get_tracks(self, params):
1423 | """Get tracks using IPC backend"""
1424 | if not self.use_ipc or not self.ipc_board_api:
1425 | return {"success": False, "message": "IPC backend not available"}
1426 |
1427 | try:
1428 | tracks = self.ipc_board_api.get_tracks()
1429 | return {
1430 | "success": True,
1431 | "tracks": tracks,
1432 | "count": len(tracks)
1433 | }
1434 | except Exception as e:
1435 | logger.error(f"Error getting tracks via IPC: {e}")
1436 | return {"success": False, "message": str(e)}
1437 |
1438 | def _handle_ipc_get_vias(self, params):
1439 | """Get vias using IPC backend"""
1440 | if not self.use_ipc or not self.ipc_board_api:
1441 | return {"success": False, "message": "IPC backend not available"}
1442 |
1443 | try:
1444 | vias = self.ipc_board_api.get_vias()
1445 | return {
1446 | "success": True,
1447 | "vias": vias,
1448 | "count": len(vias)
1449 | }
1450 | except Exception as e:
1451 | logger.error(f"Error getting vias via IPC: {e}")
1452 | return {"success": False, "message": str(e)}
1453 |
1454 | def _handle_ipc_save_board(self, params):
1455 | """Save board using IPC backend"""
1456 | if not self.use_ipc or not self.ipc_board_api:
1457 | return {"success": False, "message": "IPC backend not available"}
1458 |
1459 | try:
1460 | success = self.ipc_board_api.save()
1461 | return {
1462 | "success": success,
1463 | "message": "Board saved" if success else "Failed to save board"
1464 | }
1465 | except Exception as e:
1466 | logger.error(f"Error saving board via IPC: {e}")
1467 | return {"success": False, "message": str(e)}
1468 |
1469 |
1470 | def main():
1471 | """Main entry point"""
1472 | logger.info("Starting KiCAD interface...")
1473 | interface = KiCADInterface()
1474 |
1475 | try:
1476 | logger.info("Processing commands from stdin...")
1477 | # Process commands from stdin
1478 | for line in sys.stdin:
1479 | try:
1480 | # Parse command
1481 | logger.debug(f"Received input: {line.strip()}")
1482 | command_data = json.loads(line)
1483 |
1484 | # Check if this is JSON-RPC 2.0 format
1485 | if 'jsonrpc' in command_data and command_data['jsonrpc'] == '2.0':
1486 | logger.info("Detected JSON-RPC 2.0 format message")
1487 | method = command_data.get('method')
1488 | params = command_data.get('params', {})
1489 | request_id = command_data.get('id')
1490 |
1491 | # Handle MCP protocol methods
1492 | if method == 'initialize':
1493 | logger.info("Handling MCP initialize")
1494 | response = {
1495 | 'jsonrpc': '2.0',
1496 | 'id': request_id,
1497 | 'result': {
1498 | 'protocolVersion': '2025-06-18',
1499 | 'capabilities': {
1500 | 'tools': {
1501 | 'listChanged': True
1502 | },
1503 | 'resources': {
1504 | 'subscribe': False,
1505 | 'listChanged': True
1506 | }
1507 | },
1508 | 'serverInfo': {
1509 | 'name': 'kicad-mcp-server',
1510 | 'title': 'KiCAD PCB Design Assistant',
1511 | 'version': '2.1.0-alpha'
1512 | },
1513 | 'instructions': 'AI-assisted PCB design with KiCAD. Use tools to create projects, design boards, place components, route traces, and export manufacturing files.'
1514 | }
1515 | }
1516 | elif method == 'tools/list':
1517 | logger.info("Handling MCP tools/list")
1518 | # Return list of available tools with proper schemas
1519 | tools = []
1520 | for cmd_name in interface.command_routes.keys():
1521 | # Get schema from TOOL_SCHEMAS if available
1522 | if cmd_name in TOOL_SCHEMAS:
1523 | tool_def = TOOL_SCHEMAS[cmd_name].copy()
1524 | tools.append(tool_def)
1525 | else:
1526 | # Fallback for tools without schemas
1527 | logger.warning(f"No schema defined for tool: {cmd_name}")
1528 | tools.append({
1529 | 'name': cmd_name,
1530 | 'description': f'KiCAD command: {cmd_name}',
1531 | 'inputSchema': {
1532 | 'type': 'object',
1533 | 'properties': {}
1534 | }
1535 | })
1536 |
1537 | logger.info(f"Returning {len(tools)} tools")
1538 | response = {
1539 | 'jsonrpc': '2.0',
1540 | 'id': request_id,
1541 | 'result': {
1542 | 'tools': tools
1543 | }
1544 | }
1545 | elif method == 'tools/call':
1546 | logger.info("Handling MCP tools/call")
1547 | tool_name = params.get('name')
1548 | tool_params = params.get('arguments', {})
1549 |
1550 | # Execute the command
1551 | result = interface.handle_command(tool_name, tool_params)
1552 |
1553 | response = {
1554 | 'jsonrpc': '2.0',
1555 | 'id': request_id,
1556 | 'result': {
1557 | 'content': [
1558 | {
1559 | 'type': 'text',
1560 | 'text': json.dumps(result)
1561 | }
1562 | ]
1563 | }
1564 | }
1565 | elif method == 'resources/list':
1566 | logger.info("Handling MCP resources/list")
1567 | # Return list of available resources
1568 | response = {
1569 | 'jsonrpc': '2.0',
1570 | 'id': request_id,
1571 | 'result': {
1572 | 'resources': RESOURCE_DEFINITIONS
1573 | }
1574 | }
1575 | elif method == 'resources/read':
1576 | logger.info("Handling MCP resources/read")
1577 | resource_uri = params.get('uri')
1578 |
1579 | if not resource_uri:
1580 | response = {
1581 | 'jsonrpc': '2.0',
1582 | 'id': request_id,
1583 | 'error': {
1584 | 'code': -32602,
1585 | 'message': 'Missing required parameter: uri'
1586 | }
1587 | }
1588 | else:
1589 | # Read the resource
1590 | resource_data = handle_resource_read(resource_uri, interface)
1591 |
1592 | response = {
1593 | 'jsonrpc': '2.0',
1594 | 'id': request_id,
1595 | 'result': resource_data
1596 | }
1597 | else:
1598 | logger.error(f"Unknown JSON-RPC method: {method}")
1599 | response = {
1600 | 'jsonrpc': '2.0',
1601 | 'id': request_id,
1602 | 'error': {
1603 | 'code': -32601,
1604 | 'message': f'Method not found: {method}'
1605 | }
1606 | }
1607 | else:
1608 | # Handle legacy custom format
1609 | logger.info("Detected custom format message")
1610 | command = command_data.get("command")
1611 | params = command_data.get("params", {})
1612 |
1613 | if not command:
1614 | logger.error("Missing command field")
1615 | response = {
1616 | "success": False,
1617 | "message": "Missing command",
1618 | "errorDetails": "The command field is required"
1619 | }
1620 | else:
1621 | # Handle command
1622 | response = interface.handle_command(command, params)
1623 |
1624 | # Send response
1625 | logger.debug(f"Sending response: {response}")
1626 | print(json.dumps(response))
1627 | sys.stdout.flush()
1628 |
1629 | except json.JSONDecodeError as e:
1630 | logger.error(f"Invalid JSON input: {str(e)}")
1631 | response = {
1632 | "success": False,
1633 | "message": "Invalid JSON input",
1634 | "errorDetails": str(e)
1635 | }
1636 | print(json.dumps(response))
1637 | sys.stdout.flush()
1638 |
1639 | except KeyboardInterrupt:
1640 | logger.info("KiCAD interface stopped")
1641 | sys.exit(0)
1642 |
1643 | except Exception as e:
1644 | logger.error(f"Unexpected error: {str(e)}\n{traceback.format_exc()}")
1645 | sys.exit(1)
1646 |
1647 | if __name__ == "__main__":
1648 | main()
1649 |
```