fix: reconnect IPC backend after KiCad starts (#140)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
scorp508
2026-05-22 17:41:47 -04:00
committed by GitHub
parent f65eaf2798
commit 6113e0c27a
10 changed files with 927 additions and 102 deletions

View File

@@ -28,6 +28,22 @@ All notable changes to the KiCAD MCP Server project are documented here.
`.kicad_sch` path) for stateless callers, so project libraries can be
resolved without first calling `open_project`.
- **IPC backend runtime reconnect**: MCP no longer stays on SWIG for the
entire process when it starts before KiCAD. IPC-capable board tools now retry
the IPC connection when KiCAD is running, refresh the live board API when a
board becomes available, and report `_backend: "ipc"` when they actually use
the IPC path. `check_kicad_ui`, `launch_kicad_ui`, and `get_backend_info`
now include live backend status instead of only reflecting startup state.
- **Windows KiCAD Python discovery**: Windows startup now scans per-user KiCAD
installs under `%LOCALAPPDATA%\Programs\KiCad` in addition to machine-wide
installs under `C:\Program Files\KiCad` and `C:\Program Files (x86)\KiCad`,
so user-scope installs no longer require a manual `KICAD_PYTHON` override.
- **IPC board size on KiCAD 10**: `get_board_info` now handles KiCAD 10 IPC
`Box2` objects that expose `pos` / `size` instead of `min` / `max`, avoiding
a zero-size board result with an attribute error.
- **Schematic symbol lookup**: `get_schematic_component`,
`edit_schematic_component`, `set_schematic_component_property`,
`remove_schematic_component_property`, and `delete_schematic_component`
@@ -157,6 +173,9 @@ All notable changes to the KiCAD MCP Server project are documented here.
this out explicitly so agents know they can use it to inspect MPN,
Manufacturer, Distributor PN and other BOM fields without a separate call.
- `query_traces`: added to the IPC-capable board command path so trace reads
can use live KiCAD board data when IPC is connected.
### New MCP Prompt
- `component_sourcing_properties` — Guides the LLM through attaching BOM and
@@ -175,6 +194,10 @@ All notable changes to the KiCAD MCP Server project are documented here.
no-op removal, special-character escaping, UUID preservation, and the two
new convenience tools.
- `tests/test_backend_metadata.py`: regression coverage for backend metadata,
runtime IPC reconnect after KiCAD starts, IPC-backed `query_traces`, and
KiCAD 10 IPC `Box2` board-size compatibility.
### Removed
- `add_schematic_junction` MCP tool has been removed. Junctions are now

View File

@@ -169,6 +169,9 @@ We are currently implementing and testing the KiCAD 9.0 IPC API for real-time UI
- Changes made via MCP tools appear immediately in the KiCAD UI
- No manual reload required when IPC is active
- Hybrid backend: uses IPC when available, falls back to SWIG API
- IPC runtime reconnect: if MCP has fallen back to SWIG, IPC-capable board
tools retry IPC after KiCAD launches instead of staying on SWIG for the entire
session
- 20+ commands now support IPC including routing, component placement, and zone operations
Note: IPC features are under active development and testing. Enable IPC in KiCAD via Preferences > Plugins > Enable IPC API Server.
@@ -418,7 +421,7 @@ See [Freerouting Guide](docs/FREEROUTING_GUIDE.md) for setup and usage.
### Required Software
**KiCAD 9.0 or Higher**
**KiCAD 9.0 or higher**
- Download from [kicad.org/download](https://www.kicad.org/download/)
- Must include Python module (pcbnew)
@@ -463,7 +466,7 @@ Choose one:
### Linux (Ubuntu/Debian)
```bash
# Install KiCAD 9.0
# Install KiCAD 9.0 or higher
sudo add-apt-repository --yes ppa:kicad/kicad-9.0-releases
sudo apt-get update
sudo apt-get install -y kicad kicad-libraries
@@ -495,7 +498,9 @@ cd KiCAD-MCP-Server
The script will:
- Detect KiCAD installation
- Detect KiCAD installations, including both machine-wide installs under
`C:\Program Files\KiCad` and per-user installs under
`%LOCALAPPDATA%\Programs\KiCad`
- Verify prerequisites
- Install dependencies
- Build project
@@ -693,7 +698,8 @@ Edit configuration file:
**Platform-specific PYTHONPATH:**
- **Linux:** `/usr/lib/kicad/lib/python3/dist-packages`
- **Windows:** `C:\Program Files\KiCad\9.0\lib\python3\dist-packages`
- **Windows:** `C:\Program Files\KiCad\10.0\lib\python3\dist-packages` or
`%LOCALAPPDATA%\Programs\KiCad\10.0\lib\python3\dist-packages`
- **macOS:** `/Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages`
#### Linux Python Detection
@@ -1076,7 +1082,7 @@ npm run format
**Solutions:**
1. Run automated diagnostics: `.\setup-windows.ps1`
2. Verify Python path uses double backslashes: `C:\\Program Files\\KiCad\\9.0`
2. Verify Python path uses double backslashes: `C:\\Program Files\\KiCad\\10.0`
3. Check Windows Event Viewer for Node.js errors
4. See [Windows Troubleshooting Guide](docs/WINDOWS_TROUBLESHOOTING.md)
@@ -1118,7 +1124,7 @@ See [STATUS_SUMMARY.md](docs/STATUS_SUMMARY.md) for the complete status matrix a
**IPC Backend (Experimental):**
- Real-time UI synchronization via KiCAD 9.0 IPC API
- Real-time UI synchronization via the KiCAD IPC API
- 21 IPC-enabled commands with automatic SWIG fallback
- Hybrid footprint loading (SWIG for library access, IPC for placement)

View File

@@ -11,7 +11,13 @@
The IPC backend provides real-time UI synchronization with KiCAD 9.0+ via the official IPC API. When KiCAD is running with IPC enabled, commands can update the KiCAD UI immediately without requiring manual reload.
This feature is experimental and under active testing. The server uses a hybrid approach: IPC when available, automatic fallback to SWIG when IPC is not connected.
This feature is experimental and under active testing. The server uses a hybrid
approach: IPC when available, automatic fallback to SWIG when IPC is not
connected, and runtime reconnect when KiCAD becomes available after the MCP
server has already started. Tools with IPC implementations recheck IPC
availability at runtime and reconnect if KiCAD is available, which prevents the
MCP from staying in SWIG for the entire session after an initial failure to
connect to IPC.
## Key Differences
@@ -33,6 +39,7 @@ The following MCP commands have IPC handlers:
| `add_via` | `_ipc_add_via` | Implemented |
| `add_net` | `_ipc_add_net` | Implemented |
| `delete_trace` | `_ipc_delete_trace` | Falls back to SWIG |
| `query_traces` | `_ipc_query_traces` | Implemented |
| `get_nets_list` | `_ipc_get_nets_list` | Implemented |
| `add_copper_pour` | `_ipc_add_copper_pour` | Implemented |
| `refill_zones` | `_ipc_refill_zones` | Implemented |
@@ -59,6 +66,7 @@ The following MCP commands have IPC handlers:
- Auto-detect socket path (`/tmp/kicad/api.sock`)
- Version checking and validation
- Auto-fallback to SWIG when IPC unavailable
- Runtime reconnect from SWIG to IPC when KiCAD starts after MCP
- Change notification callbacks
**Board Operations:**
@@ -86,6 +94,7 @@ The following MCP commands have IPC handlers:
- Get all tracks
- Get all vias
- Get all nets
- Query traces from the live board with net, layer, and bounding-box filters
**Zone Operations:**
@@ -119,6 +128,30 @@ The following MCP commands have IPC handlers:
pip install kicad-python
```
### Runtime Backend Selection
At startup, the server attempts IPC first when `KICAD_BACKEND` is `auto` or
`ipc`. If KiCAD is not running yet, `auto` mode falls back to SWIG so file-based
operations can still work.
IPC-capable board tools now retry the IPC connection at runtime. This means the
following workflow can switch from SWIG to IPC without restarting the MCP
server:
1. Start the MCP server before KiCAD is running.
2. Launch KiCAD manually or with `launch_kicad_ui`.
3. Open a board with IPC enabled in KiCAD.
4. Call a normal IPC-capable board tool such as `get_board_info`,
`get_layer_list`, `get_component_list`, `get_nets_list`, or `query_traces`.
When the reconnect succeeds, tool responses include `_backend: "ipc"` and
`_realtime: true`. If IPC is still unavailable or no board API can be opened,
the server continues to use SWIG and reports `_backend: "swig"` for the result.
Use `check_kicad_ui`, `launch_kicad_ui`, or `get_backend_info` to inspect the
current live backend status. These responses include `backend`,
`realtime_sync`, and `ipc_connected`.
### Testing
Run the test script to verify IPC functionality:
@@ -164,7 +197,9 @@ Run the test script to verify IPC functionality:
2. **Project creation**: Not supported via IPC, uses file system
3. **Footprint library access**: Uses hybrid approach (SWIG loads from library, IPC places)
4. **Delete trace**: Falls back to SWIG (IPC API doesn't support direct deletion)
5. **Some operations may not work as expected**: This is experimental code
5. **Reconnect still requires IPC prerequisites**: Runtime reconnect only
succeeds when KiCAD is running with IPC enabled and a board is available
6. **Some operations may not work as expected**: This is experimental code
## Troubleshooting
@@ -173,6 +208,8 @@ Run the test script to verify IPC functionality:
- Ensure KiCAD is running
- Enable IPC API: `Preferences > Plugins > Enable IPC API Server`
- Check if a board is open
- If MCP started before KiCAD, call `check_kicad_ui` or retry an IPC-capable
board tool after KiCAD and the board are ready
### "kicad-python not found"

View File

@@ -71,7 +71,10 @@ Check if KiCAD is currently running.
"command": "/usr/bin/pcbnew /tmp/project.kicad_pcb"
}
],
"message": "KiCAD is running"
"message": "KiCAD is running",
"backend": "ipc",
"realtime_sync": true,
"ipc_connected": true
}
```
@@ -104,10 +107,26 @@ Launch KiCAD UI, optionally with a project file.
"launched": true,
"message": "KiCAD launched successfully",
"project": "/tmp/mcp_demo/New_Project.kicad_pcb",
"processes": [...]
"processes": [...],
"backend": "ipc",
"realtime_sync": true,
"ipc_connected": true
}
```
If IPC is not enabled, no board is open, or the IPC connection is otherwise not
available yet, these status fields report `backend: "swig"`,
`realtime_sync: false`, and `ipc_connected: false`.
### IPC Reconnect After Launch
When the MCP server starts before KiCAD, it may initially fall back to the SWIG
backend. `launch_kicad_ui` and `check_kicad_ui` now refresh the live backend
status after KiCAD is running, and normal IPC-capable board tools retry IPC on
their next call. Agents do not need to switch to special `ipc_*` tools; they can
retry standard tools such as `get_board_info`, `get_layer_list`,
`get_component_list`, `get_nets_list`, or `query_traces`.
---
## 🔄 Workflow Examples
@@ -383,16 +402,15 @@ await callKicadScript("launch_kicad_ui", {
- **Window Management:** Bring KiCAD to front, minimize/maximize
- **Multi-Instance:** Handle multiple KiCAD instances
- **IPC Integration:** Seamless integration with IPC backend
- **Status Notifications:** Push notifications when KiCAD state changes
- **Auto-Close:** Option to close KiCAD after operations complete
### IPC Mode (Coming Weeks 2-3)
### IPC Mode
When IPC backend is fully implemented:
When KiCAD is running with IPC enabled:
```
KiCAD runs in background → MCP connects via IPC → Real-time updates
KiCAD runs in background → MCP connects or reconnects via IPC → Real-time updates
No file reloading needed! Changes appear instantly.
```

View File

@@ -2,15 +2,19 @@
This document explains how to see changes made by the MCP server in the KiCAD UI in real-time or near-real-time.
## Current Status (Week 1 - SWIG Backend)
## Current Status
**Active Backend:** SWIG (legacy pcbnew Python API)
**Real-time Updates:** Not available yet
**IPC Backend:** Skeleton implemented, operations coming in Weeks 2-3
**Active Backend:** Hybrid SWIG/IPC
**Real-time Updates:** Available for IPC-backed commands when KiCAD IPC is connected
**SWIG Fallback:** File-based commands still require KiCAD to reload from disk
**IPC Re-detect:** IPC-enabled tools detect at runtime if IPC is available again and switch from SWIG to IPC.
---
## 🎯 Best Current Workflow (SWIG + Manual Reload)
## 🎯 Best Current Workflow (Hybrid IPC + SWIG)
### Setup
@@ -24,10 +28,11 @@ This document explains how to see changes made by the MCP server in the KiCAD UI
- Example: Add board outline, mounting holes, etc.
- Each operation saves the file automatically
3. **Reload in KiCAD UI**
- **Option A (Automatic):** KiCAD 8.0+ detects file changes and shows a reload prompt
- **Option B (Manual):** File → Revert to reload from disk
- **Keyboard shortcut:** None by default (but you can assign one)
3. **Check whether reload is needed**
- If the MCP response reports `_backend: "ipc"` and `_realtime: true`, the change should appear in KiCAD immediately.
- If the MCP response reports `_backend: "swig"` or `_realtime: false`, reload the board from disk.
- **Option A (Automatic):** KiCAD 8.0+ detects file changes and shows a reload prompt.
- **Option B (Manual):** File → Revert to reload from disk.
### Workflow Example
@@ -57,34 +62,51 @@ This document explains how to see changes made by the MCP server in the KiCAD UI
---
## 🔮 Future: IPC Backend (Weeks 2-3)
## IPC Backend: Real-Time Updates (Experimental)
When fully implemented, the IPC backend will provide **true real-time updates**:
When KiCAD is running with the IPC API enabled, supported MCP board tools can
use the IPC backend for **true real-time UI updates** instead of relying on
file save/reload.
### How It Will Work
### How It Works
```
Claude MCP → IPC Socket → Running KiCAD → Instant UI Update
```text
Agent → MCP → IPC connection → Running KiCAD → Instant UI Update
```
**No file reloading required** - changes appear as you make them!
**No file reloading is required** for commands that successfully use IPC. Tool
responses include `_backend: "ipc"` and `_realtime: true` when the IPC path was
used. If IPC is unavailable, the server falls back to SWIG and the manual reload
workflow still applies. If IPC becomes available during the session,
IPC-capable tools can reconnect and use IPC without restarting the MCP server.
### IPC Setup (When Available)
### IPC Setup
1. **Enable IPC in KiCAD**
- Preferences → Advanced Preferences
- Search for "IPC"
- Enable: "Enable IPC API Server"
- Restart KiCAD
1. Enable IPC in KiCAD:
- Preferences → Plugins → Enable IPC API Server
- Restart KiCAD if required
2. **Install kicad-python** (Already installed ✓)
2. Install `kicad-python`:
```bash
pip install kicad-python
```
3. **Configure MCP Server**
Add to your MCP config:
The default `auto` backend mode is recommended when you want SWIG fallback
plus runtime reconnect. To make the setting explicit, add:
```json
{
"env": {
"KICAD_BACKEND": "auto"
}
}
```
Use strict `ipc` mode only when you want startup to fail if IPC is not
available:
```json
{
@@ -94,20 +116,23 @@ Claude MCP → IPC Socket → Running KiCAD → Instant UI Update
}
```
4. **Start KiCAD first, then use MCP**
- Changes will appear in real-time
- No manual reloading needed
4. Start KiCAD and open a board, or use `launch_kicad_ui`.
The MCP server **can** start before KiCAD. In `auto` backend mode, IPC-capable
board tools retry IPC at runtime after KiCAD is available, so agents can keep
using standard tools such as `get_board_info`, `get_layer_list`,
`get_component_list`, `get_nets_list`, and `query_traces`.
### Current IPC Status
| Feature | Status |
| -------------------- | ----------- |
| Connection to KiCAD | ✅ Working |
| Version checking | ✅ Working |
| Project operations | ⏳ Week 2-3 |
| Board operations | ⏳ Week 2-3 |
| Component operations | ⏳ Week 2-3 |
| Routing operations | ⏳ Week 2-3 |
| Feature | Status |
| ------------------------ | ------------------------------------------------------------- |
| Connection to KiCAD | Working when KiCAD IPC is enabled |
| Board operations | Partially implemented via IPC |
| Component operations | Partially implemented / hybrid |
| Routing operations | Partially implemented via IPC |
| SWIG fallback | Used automatically in `auto` mode when IPC is unavailable |
| Runtime reconnect to IPC | Used automatically in `auto` mode for IPC-capable board tools |
---
@@ -153,7 +178,7 @@ The MCP server auto-saves after each operation, so changes are immediately avail
For complex changes (multiple components, routing, etc.):
1. Make the change
2. Reload in KiCAD
2. Confirm the change in KiCAD; reload only if the response used SWIG
3. Verify it looks correct
4. Proceed with next change
@@ -171,6 +196,15 @@ For complex changes (multiple components, routing, etc.):
**Cause:** MCP operation may have failed
**Solution:** Check the MCP response for success: true
### Changes Still Require Reload
**Cause:** The tool response reported `_backend: "swig"` or `_realtime: false`.
SWIG-backed commands write files directly and still require KiCAD to reload the board from disk.
**Solution:** Ensure KiCAD is running with IPC enabled and a board is open, then
retry the IPC-capable board tool. If IPC reconnect succeeds, the response will
report `_backend: "ipc"` and `_realtime: true`.
### File is Locked
**Cause:** KiCAD has the file open exclusively
@@ -181,13 +215,5 @@ For complex changes (multiple components, routing, etc.):
---
## 📅 Roadmap
**Current (Week 1):** SWIG backend with manual reload
**Week 2-3:** IPC backend implementation
**Week 4+:** Real-time collaboration features
---
**Last Updated:** 2025-10-26
**Version:** 2.0.0-alpha.1

View File

@@ -45,10 +45,12 @@ If the automated setup fails, continue with the manual troubleshooting below.
2. **Test pcbnew import manually:**
```powershell
& "C:\Program Files\KiCad\9.0\bin\python.exe" -c "import pcbnew; print(pcbnew.GetBuildVersion())"
& "C:\Program Files\KiCad\10.0\bin\python.exe" -c "import pcbnew; print(pcbnew.GetBuildVersion())"
```
**Expected:** Prints KiCAD version like `9.0.0`
Replace `10.0` with your installed KiCAD version if needed.
**Expected:** Prints KiCAD version like `10.0.0`
**If it fails:**
- KiCAD's Python module isn't installed
@@ -61,7 +63,7 @@ If the automated setup fails, continue with the manual troubleshooting below.
"mcpServers": {
"kicad": {
"env": {
"PYTHONPATH": "C:\\Program Files\\KiCad\\9.0\\lib\\python3\\dist-packages"
"PYTHONPATH": "C:\\Program Files\\KiCad\\10.0\\lib\\python3\\dist-packages"
}
}
}
@@ -74,25 +76,37 @@ If the automated setup fails, continue with the manual troubleshooting below.
**Symptom:** Log shows "No KiCAD installations found"
The server checks common Windows install locations, including both machine-wide
and per-user KiCAD installs:
- `%LOCALAPPDATA%\Programs\KiCad`
- `C:\Program Files\KiCad`
- `C:\Program Files (x86)\KiCad`
**Solution:**
1. **Check if KiCAD is installed:**
```powershell
Test-Path "C:\Program Files\KiCad\9.0"
Test-Path "C:\Program Files\KiCad\10.0"
Test-Path "$env:LOCALAPPDATA\Programs\KiCad\10.0"
```
Replace `10.0` with your installed KiCAD version if needed.
2. **If KiCAD is installed elsewhere:**
- Find your KiCAD installation directory
- Update PYTHONPATH in config to match your installation
- Example for version 8.0:
- Set `KICAD_PYTHON` to the bundled `python.exe`
- Update `PYTHONPATH` in config to match your installation if needed
- Example for a per-user 10.0 install:
```
"PYTHONPATH": "C:\\Program Files\\KiCad\\8.0\\lib\\python3\\dist-packages"
"KICAD_PYTHON": "C:\\Users\\YourName\\AppData\\Local\\Programs\\KiCad\\10.0\\bin\\python.exe",
"PYTHONPATH": "C:\\Users\\YourName\\AppData\\Local\\Programs\\KiCad\\10.0\\lib\\python3\\dist-packages"
```
3. **If KiCAD is not installed:**
- Download from https://www.kicad.org/download/windows/
- Install version 9.0 or higher
- Install KiCAD 9.0 or higher
- Use default installation path
---
@@ -162,7 +176,7 @@ If the automated setup fails, continue with the manual troubleshooting below.
1. **Install with KiCAD's Python:**
```powershell
& "C:\Program Files\KiCad\9.0\bin\python.exe" -m pip install -r requirements.txt
& "C:\Program Files\KiCad\10.0\bin\python.exe" -m pip install -r requirements.txt
```
2. **If pip is not available:**
@@ -172,10 +186,10 @@ If the automated setup fails, continue with the manual troubleshooting below.
Invoke-WebRequest -Uri https://bootstrap.pypa.io/get-pip.py -OutFile get-pip.py
# Install pip
& "C:\Program Files\KiCad\9.0\bin\python.exe" get-pip.py
& "C:\Program Files\KiCad\10.0\bin\python.exe" get-pip.py
# Then install requirements
& "C:\Program Files\KiCad\9.0\bin\python.exe" -m pip install -r requirements.txt
& "C:\Program Files\KiCad\10.0\bin\python.exe" -m pip install -r requirements.txt
```
---
@@ -232,7 +246,8 @@ If the automated setup fails, continue with the manual troubleshooting below.
**Solution:**
KiCAD MCP requires Python 3.10+. KiCAD 9.0 includes Python 3.11, which is perfect.
KiCAD MCP requires Python 3.10+. KiCAD 10.0 includes a compatible bundled Python,
and KiCAD 9.0+ is supported.
**Always use KiCAD's bundled Python:**
@@ -240,7 +255,7 @@ KiCAD MCP requires Python 3.10+. KiCAD 9.0 includes Python 3.11, which is perfec
{
"mcpServers": {
"kicad": {
"command": "C:\\Program Files\\KiCad\\9.0\\bin\\python.exe",
"command": "C:\\Program Files\\KiCad\\10.0\\bin\\python.exe",
"args": ["C:\\Users\\YourName\\KiCAD-MCP-Server\\python\\kicad_interface.py"]
}
}
@@ -264,7 +279,7 @@ Config location: `%APPDATA%\Claude\claude_desktop_config.json`
"command": "node",
"args": ["C:\\Users\\YourName\\KiCAD-MCP-Server\\dist\\index.js"],
"env": {
"PYTHONPATH": "C:\\Program Files\\KiCad\\9.0\\lib\\python3\\dist-packages",
"PYTHONPATH": "C:\\Program Files\\KiCad\\10.0\\lib\\python3\\dist-packages",
"NODE_ENV": "production",
"LOG_LEVEL": "info"
}
@@ -284,7 +299,7 @@ Config location: `%APPDATA%\Code\User\globalStorage\saoudrizwan.claude-dev\setti
"command": "node",
"args": ["C:\\Users\\YourName\\KiCAD-MCP-Server\\dist\\index.js"],
"env": {
"PYTHONPATH": "C:\\Program Files\\KiCad\\9.0\\lib\\python3\\dist-packages"
"PYTHONPATH": "C:\\Program Files\\KiCad\\10.0\\lib\\python3\\dist-packages"
},
"description": "KiCAD PCB Design Assistant"
}
@@ -300,10 +315,10 @@ If Node.js issues persist, run Python directly:
{
"mcpServers": {
"kicad": {
"command": "C:\\Program Files\\KiCad\\9.0\\bin\\python.exe",
"command": "C:\\Program Files\\KiCad\\10.0\\bin\\python.exe",
"args": ["C:\\Users\\YourName\\KiCAD-MCP-Server\\python\\kicad_interface.py"],
"env": {
"PYTHONPATH": "C:\\Program Files\\KiCad\\9.0\\lib\\python3\\dist-packages"
"PYTHONPATH": "C:\\Program Files\\KiCad\\10.0\\lib\\python3\\dist-packages"
}
}
}
@@ -317,7 +332,7 @@ If Node.js issues persist, run Python directly:
### Test 1: Verify KiCAD Python
```powershell
& "C:\Program Files\KiCad\9.0\bin\python.exe" -c @"
& "C:\Program Files\KiCad\10.0\bin\python.exe" -c @"
import sys
print(f'Python version: {sys.version}')
import pcbnew
@@ -330,7 +345,7 @@ Expected output:
```
Python version: 3.11.x ...
pcbnew version: 9.0.0
pcbnew version: 10.0.0
SUCCESS!
```
@@ -353,7 +368,7 @@ Test-Path .\dist\index.js # Should output: True
### Test 4: Run Server Manually
```powershell
$env:PYTHONPATH = "C:\Program Files\KiCad\9.0\lib\python3\dist-packages"
$env:PYTHONPATH = "C:\Program Files\KiCad\10.0\lib\python3\dist-packages"
node .\dist\index.js
```
@@ -390,20 +405,20 @@ Add to your MCP config:
### Check Python sys.path
```powershell
& "C:\Program Files\KiCad\9.0\bin\python.exe" -c @"
& "C:\Program Files\KiCad\10.0\bin\python.exe" -c @"
import sys
for path in sys.path:
print(path)
"@
```
Should include: `C:\Program Files\KiCad\9.0\lib\python3\dist-packages`
Should include: `C:\Program Files\KiCad\10.0\lib\python3\dist-packages`
### Test MCP Communication
```powershell
# Start server
$env:PYTHONPATH = "C:\Program Files\KiCad\9.0\lib\python3\dist-packages"
$env:PYTHONPATH = "C:\Program Files\KiCad\10.0\lib\python3\dist-packages"
$process = Start-Process -FilePath "node" -ArgumentList ".\dist\index.js" -NoNewWindow -PassThru
# Wait 3 seconds
@@ -474,7 +489,8 @@ If none of the above solutions work:
When everything works, you should have:
- [ ] KiCAD 9.0+ installed at `C:\Program Files\KiCad\9.0`
- [ ] KiCAD 9.0 or higher installed under a versioned KiCAD directory such as
`C:\Program Files\KiCad\10.0` or `%LOCALAPPDATA%\Programs\KiCad\10.0`
- [ ] Node.js 18+ installed and in PATH
- [ ] Python can import pcbnew successfully
- [ ] `npm run build` completes without errors

View File

@@ -371,10 +371,11 @@ class IPCBoardAPI(BoardAPI):
# Check if on Edge.Cuts layer
bbox = board.get_item_bounding_box(shape)
if bbox:
min_x = min(min_x, bbox.min.x)
min_y = min(min_y, bbox.min.y)
max_x = max(max_x, bbox.max.x)
max_y = max(max_y, bbox.max.y)
left, top, right, bottom = self._get_box2_extents(bbox)
min_x = min(min_x, left)
min_y = min(min_y, top)
max_x = max(max_x, right)
max_y = max(max_y, bottom)
if min_x == float("inf"):
return {"width": 0, "height": 0, "unit": "mm"}
@@ -385,6 +386,21 @@ class IPCBoardAPI(BoardAPI):
logger.error(f"Failed to get board size: {e}")
return {"width": 0, "height": 0, "unit": "mm", "error": str(e)}
@staticmethod
def _get_box2_extents(bbox: Any) -> tuple[float, float, float, float]:
"""Return left/top/right/bottom for kipy Box2 wrappers across versions."""
if hasattr(bbox, "min") and hasattr(bbox, "max"):
return bbox.min.x, bbox.min.y, bbox.max.x, bbox.max.y
if hasattr(bbox, "pos") and hasattr(bbox, "size"):
x1 = bbox.pos.x
y1 = bbox.pos.y
x2 = x1 + bbox.size.x
y2 = y1 + bbox.size.y
return min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2)
raise AttributeError("Unsupported Box2 shape: expected min/max or pos/size")
def add_layer(self, layer_name: str, layer_type: str) -> bool:
"""Add layer to the board (layers are typically predefined in KiCAD)."""
logger.warning("Layer management via IPC is limited - layers are predefined")

View File

@@ -496,6 +496,7 @@ class KiCADInterface:
"add_via": "_ipc_add_via",
"add_net": "_ipc_add_net",
"delete_trace": "_ipc_delete_trace",
"query_traces": "_ipc_query_traces",
"get_nets_list": "_ipc_get_nets_list",
# Zone commands
"add_copper_pour": "_ipc_add_copper_pour",
@@ -519,12 +520,99 @@ class KiCADInterface:
"save_project": "_ipc_save_project",
}
# Commands that are implemented by the explicit IPC command handlers in
# command_routes, rather than by the generic IPC_CAPABLE_COMMANDS fast path.
IPC_DIRECT_COMMANDS = {
"ipc_add_track",
"ipc_add_via",
"ipc_add_text",
"ipc_list_components",
"ipc_get_tracks",
"ipc_get_vias",
"ipc_save_board",
}
def _refresh_ipc_board_api(self) -> bool:
"""Refresh the IPC board API after KiCAD or a board becomes available."""
ipc_backend = getattr(self, "ipc_backend", None)
if not ipc_backend or not ipc_backend.is_connected():
self.ipc_board_api = None
return False
try:
self.ipc_board_api = ipc_backend.get_board()
return True
except Exception as e:
logger.warning(f"Connected to KiCAD IPC, but no board API is available yet: {e}")
self.ipc_board_api = None
return False
def _try_enable_ipc_backend(self, force: bool = False) -> bool:
"""Try to switch an already-running interface to IPC when KiCAD is available."""
if KICAD_BACKEND == "swig":
return False
ipc_backend = getattr(self, "ipc_backend", None)
if self.use_ipc and ipc_backend and ipc_backend.is_connected():
self._refresh_ipc_board_api()
return True
if not force and not KiCADProcessManager.is_running():
return False
try:
from kicad_api.ipc_backend import IPCBackend
backend = ipc_backend or IPCBackend()
if not backend.is_connected():
backend.connect()
self.ipc_backend = backend
self.use_ipc = True
self._refresh_ipc_board_api()
logger.info("Switched to IPC backend after KiCAD became available")
return True
except Exception as e:
logger.info(f"Runtime IPC connection not available: {e}")
return False
def _backend_status(self) -> Dict[str, Any]:
"""Return backend status fields for command responses."""
ipc_backend = getattr(self, "ipc_backend", None)
ipc_connected = ipc_backend.is_connected() if ipc_backend else False
return {
"backend": "ipc" if self.use_ipc and ipc_connected else "swig",
"realtime_sync": self.use_ipc and ipc_connected,
"ipc_connected": ipc_connected,
}
@staticmethod
def _normalize_ipc_layer_name(layer: Any) -> str:
"""Convert KiCad IPC layer enum strings to common layer names."""
layer_name = str(layer)
if layer_name.startswith("BL_"):
return layer_name[3:].replace("_", ".")
return layer_name
def _result_backend_for_command(self, command: str, result: Dict[str, Any]) -> str:
"""Return the backend label for a command result."""
if command in {"get_backend_info", "check_kicad_ui", "launch_kicad_ui"}:
return result.get("backend", "ipc" if self.use_ipc else "swig")
if command in self.IPC_DIRECT_COMMANDS:
return "ipc" if self.use_ipc else "unavailable"
return "swig"
def handle_command(self, command: str, params: Dict[str, Any]) -> Dict[str, Any]:
"""Route command to appropriate handler, preferring IPC when available"""
logger.info(f"Handling command: {command}")
logger.debug(f"Command parameters: {params}")
try:
if command in self.IPC_CAPABLE_COMMANDS:
self._try_enable_ipc_backend()
# Check if we can use IPC for this command (real-time UI sync)
if self.use_ipc and self.ipc_board_api and command in self.IPC_CAPABLE_COMMANDS:
ipc_handler_name = self.IPC_CAPABLE_COMMANDS[command]
@@ -558,8 +646,11 @@ class KiCADInterface:
# Add backend indicator
if isinstance(result, dict):
result["_backend"] = "swig"
result["_realtime"] = False
backend = self._result_backend_for_command(command, result)
result["_backend"] = backend
result["_realtime"] = bool(
backend == "ipc" and result.get("realtime", self.use_ipc)
)
# Update board reference if command was successful
if result.get("success", False):
@@ -5018,14 +5109,21 @@ class KiCADInterface:
logger.info("Checking if KiCAD UI is running")
try:
manager = KiCADProcessManager()
# `processes` is the single source of truth (from #173) so
# `running` can't disagree with it; and if KiCAD is up, opportunistically
# (re)connect the IPC backend (#140) so a session that started before
# KiCAD launched can fall up from SWIG to IPC.
processes = manager.get_process_info()
is_running = len(processes) > 0
if is_running:
self._try_enable_ipc_backend()
return {
"success": True,
"running": is_running,
"processes": processes,
"message": "KiCAD is running" if is_running else "KiCAD is not running",
**self._backend_status(),
}
except Exception as e:
logger.error(f"Error checking KiCAD UI status: {str(e)}")
@@ -5044,8 +5142,10 @@ class KiCADInterface:
path_obj = Path(project_path) if project_path else None
result = check_and_launch_kicad(path_obj, auto_launch)
if result.get("running"):
self._try_enable_ipc_backend(force=True)
return {"success": True, **result}
return {"success": True, **result, **self._backend_status()}
except Exception as e:
logger.error(f"Error launching KiCAD UI: {str(e)}")
return {"success": False, "message": str(e)}
@@ -5581,6 +5681,85 @@ print("ok")
logger.info("delete_trace: Falling back to SWIG (IPC doesn't support trace deletion)")
return self.routing_commands.delete_trace(params)
def _ipc_query_traces(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""IPC handler for query_traces - reads traces from the live KiCAD board."""
try:
net_name = params.get("net")
layer_filter = params.get("layer")
bbox = params.get("boundingBox")
include_vias = params.get("includeVias", False)
def point_in_bbox(point: Dict[str, Any]) -> bool:
if not bbox:
return True
unit_scale = 25.4 if bbox.get("unit", "mm") == "inch" else 1.0
x1 = bbox.get("x1", 0) * unit_scale
y1 = bbox.get("y1", 0) * unit_scale
x2 = bbox.get("x2", 0) * unit_scale
y2 = bbox.get("y2", 0) * unit_scale
low_x, high_x = sorted((x1, x2))
low_y, high_y = sorted((y1, y2))
return low_x <= point.get("x", 0) <= high_x and low_y <= point.get("y", 0) <= high_y
traces = []
for track in self.ipc_board_api.get_tracks():
if net_name and track.get("net") != net_name:
continue
layer = self._normalize_ipc_layer_name(track.get("layer", ""))
if layer_filter and layer != layer_filter:
continue
start = track.get("start", {})
end = track.get("end", {})
if bbox and not (point_in_bbox(start) or point_in_bbox(end)):
continue
start_with_unit = {**start, "unit": "mm"}
end_with_unit = {**end, "unit": "mm"}
dx = end.get("x", 0) - start.get("x", 0)
dy = end.get("y", 0) - start.get("y", 0)
traces.append(
{
"uuid": track.get("id", ""),
"net": track.get("net", ""),
"netCode": track.get("netCode", 0),
"layer": layer,
"width": track.get("width", 0),
"start": start_with_unit,
"end": end_with_unit,
"length": (dx**2 + dy**2) ** 0.5,
}
)
result = {"success": True, "traceCount": len(traces), "traces": traces}
if include_vias:
vias = []
for via in self.ipc_board_api.get_vias():
if net_name and via.get("net") != net_name:
continue
position = via.get("position", {})
if bbox and not point_in_bbox(position):
continue
vias.append(
{
"uuid": via.get("id", ""),
"position": {**position, "unit": "mm"},
"net": via.get("net", ""),
"netCode": via.get("netCode", 0),
"diameter": via.get("diameter", 0),
"drill": via.get("drill", 0),
}
)
result["viaCount"] = len(vias)
result["vias"] = vias
return result
except Exception as e:
logger.error(f"IPC query_traces error: {e}")
return {"success": False, "message": str(e)}
def _ipc_get_nets_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""IPC handler for get_nets_list - gets nets with real-time data"""
try:
@@ -5777,15 +5956,17 @@ print("ok")
def _handle_get_backend_info(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get information about the current backend"""
if KiCADProcessManager.is_running():
self._try_enable_ipc_backend()
status = self._backend_status()
ipc_backend = getattr(self, "ipc_backend", None)
return {
"success": True,
"backend": "ipc" if self.use_ipc else "swig",
"realtime_sync": self.use_ipc,
"ipc_connected": (self.ipc_backend.is_connected() if self.ipc_backend else False),
"version": self.ipc_backend.get_version() if self.ipc_backend else "N/A",
**status,
"version": ipc_backend.get_version() if ipc_backend else "N/A",
"message": (
"Using IPC backend with real-time UI sync"
if self.use_ipc
if status["backend"] == "ipc"
else "Using SWIG backend (requires manual reload)"
),
}

View File

@@ -6,7 +6,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import express from "express";
import { spawn, exec, execSync, ChildProcess } from "child_process";
import { existsSync } from "fs";
import { existsSync, readdirSync } from "fs";
import { join, dirname } from "path";
import { logger } from "./logger.js";
@@ -40,9 +40,41 @@ import { registerRoutingPrompts } from "./prompts/routing.js";
import { registerDesignPrompts } from "./prompts/design.js";
import { registerFootprintPrompts } from "./prompts/footprint.js";
function getWindowsKiCadPythonCandidates(): string[] {
const roots = [
process.env.LOCALAPPDATA ? join(process.env.LOCALAPPDATA, "Programs", "KiCad") : undefined,
"C:\\Program Files\\KiCad",
"C:\\Program Files (x86)\\KiCad",
].filter((root): root is string => Boolean(root));
const candidates: string[] = [];
for (const root of roots) {
if (!existsSync(root)) {
continue;
}
try {
const versionDirs = readdirSync(root, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
for (const versionDir of versionDirs) {
candidates.push(join(root, versionDir, "bin", "python.exe"));
}
} catch (error: any) {
logger.warn(`Failed to inspect KiCAD install directory ${root}: ${error.message}`);
}
}
return [...new Set(candidates)];
}
/**
* Find the Python executable to use
* Prioritizes virtual environment if available, falls back to system Python
* Find the Python executable to use.
* Prioritizes project venvs, then explicit overrides, then KiCAD-bundled Python
* before falling back to system Python.
*/
function findPythonExecutable(scriptPath: string): string {
const isWindows = process.platform === "win32";
@@ -73,11 +105,12 @@ function findPythonExecutable(scriptPath: string): string {
// Platform-specific KiCAD bundled Python detection
if (isWindows) {
// Windows: Always prefer KiCAD's bundled Python (pcbnew.pyd is compiled for it)
const kicadPython = "C:\\Program Files\\KiCad\\9.0\\bin\\python.exe";
if (existsSync(kicadPython)) {
logger.info(`Found KiCAD bundled Python at: ${kicadPython}`);
return kicadPython;
// Windows: Always prefer KiCAD's bundled Python (pcbnew.pyd is compiled for it).
for (const kicadPython of getWindowsKiCadPythonCandidates()) {
if (existsSync(kicadPython)) {
logger.info(`Found KiCAD bundled Python at: ${kicadPython}`);
return kicadPython;
}
}
} else if (isMac) {
// macOS: Try KiCAD's bundled Python (check multiple versions and locations)
@@ -259,10 +292,12 @@ export class KiCADMcpServer {
// Check if Python executable exists (for absolute paths) or is executable (for commands)
const isAbsolutePath =
pythonExe.startsWith("/") || pythonExe.startsWith("C:") || pythonExe.startsWith("\\");
let pythonExecutableAvailable = true;
if (isAbsolutePath) {
// Absolute path: use existsSync
if (!existsSync(pythonExe)) {
pythonExecutableAvailable = false;
errors.push(`Python executable not found: ${pythonExe}`);
if (isWindows) {
@@ -299,6 +334,7 @@ export class KiCADMcpServer {
logger.info(`Python version check passed: ${stdout.trim()}`);
} catch (error: any) {
pythonExecutableAvailable = false;
errors.push(`Python executable not found in PATH: ${pythonExe}`);
errors.push(`Error: ${error.message}`);
errors.push("Set KICAD_PYTHON environment variable to specify full path");
@@ -325,7 +361,7 @@ export class KiCADMcpServer {
}
// Try to test pcbnew import (quick validation)
if (existsSync(pythonExe) && existsSync(this.kicadScriptPath)) {
if (pythonExecutableAvailable && existsSync(this.kicadScriptPath)) {
logger.info("Validating pcbnew module access...");
const testCommand = `"${pythonExe}" -c "import pcbnew; print('OK')"`;

View File

@@ -0,0 +1,466 @@
"""Tests for backend metadata added by KiCADInterface.handle_command."""
import sys
import types
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "python"))
def _make_iface(command_routes, use_ipc=False):
from kicad_interface import KiCADInterface
iface = KiCADInterface.__new__(KiCADInterface)
iface.use_ipc = use_ipc
iface.ipc_backend = None
iface.ipc_board_api = None
iface.command_routes = command_routes
return iface
class _FakeIPCBoardAPI:
def get_size(self):
return {"width": 10, "height": 20, "unit": "mm"}
def list_components(self):
return []
def get_tracks(self):
return [
{
"id": "track-1",
"start": {"x": 0, "y": 0},
"end": {"x": 3, "y": 4},
"width": 0.25,
"layer": "BL_F_Cu",
"net": "N$1",
}
]
def get_vias(self):
return []
def get_nets(self):
return [{"name": "N$1", "code": 1}]
def get_enabled_layers(self):
return ["F.Cu", "B.Cu"]
class _FakeIPCBackend:
def __init__(self):
self.connected = False
def connect(self):
self.connected = True
return True
def is_connected(self):
return self.connected
def get_board(self):
return _FakeIPCBoardAPI()
def get_version(self):
return "9.0-test"
class _ConnectShouldNotBeCalledIPCBackend(_FakeIPCBackend):
def connect(self):
raise AssertionError("IPC reconnect should not be attempted")
class _FailingConnectIPCBackend(_FakeIPCBackend):
def connect(self):
raise RuntimeError("IPC unavailable")
class _NoBoardIPCBackend(_FakeIPCBackend):
def get_board(self):
raise RuntimeError("No board open")
class _FilteringIPCBoardAPI(_FakeIPCBoardAPI):
def get_tracks(self):
return [
{
"id": "track-1",
"start": {"x": 0, "y": 0},
"end": {"x": 3, "y": 4},
"width": 0.25,
"layer": "BL_F_Cu",
"net": "N$1",
"netCode": 1,
},
{
"id": "track-2",
"start": {"x": 10, "y": 10},
"end": {"x": 11, "y": 11},
"width": 0.2,
"layer": "BL_B_Cu",
"net": "N$2",
"netCode": 2,
},
]
def get_vias(self):
return [
{
"id": "via-1",
"position": {"x": 0.5, "y": 0.5},
"diameter": 0.8,
"drill": 0.4,
"net": "N$1",
"netCode": 1,
},
{
"id": "via-2",
"position": {"x": 20, "y": 20},
"diameter": 0.8,
"drill": 0.4,
"net": "N$2",
"netCode": 2,
},
]
class _Point:
def __init__(self, x, y):
self.x = x
self.y = y
class _BoxWithPosSize:
def __init__(self, x, y, width, height):
self.pos = _Point(x, y)
self.size = _Point(width, height)
class _BoxWithMinMax:
def __init__(self, min_x, min_y, max_x, max_y):
self.min = _Point(min_x, min_y)
self.max = _Point(max_x, max_y)
class _BoundingBoxBoard:
def __init__(self, boxes):
self._boxes = boxes
def get_shapes(self):
return list(range(len(self._boxes)))
def get_item_bounding_box(self, shape):
return self._boxes[shape]
def _stub_kipy_units(monkeypatch):
units_module = types.ModuleType("kipy.util.units")
units_module.to_mm = lambda nm: nm / 1_000_000
monkeypatch.setitem(sys.modules, "kipy", types.ModuleType("kipy"))
monkeypatch.setitem(sys.modules, "kipy.util", types.ModuleType("kipy.util"))
monkeypatch.setitem(sys.modules, "kipy.util.units", units_module)
def test_generic_command_is_tagged_as_swig():
iface = _make_iface(
{
"get_project_info": lambda params: {
"success": True,
"project": {"name": "demo"},
}
},
use_ipc=True,
)
result = iface.handle_command("get_project_info", {})
assert result["_backend"] == "swig"
assert result["_realtime"] is False
def test_explicit_ipc_command_is_tagged_as_ipc_when_ipc_is_active():
iface = _make_iface(
{
"ipc_add_track": lambda params: {
"success": True,
"message": "Track added",
"realtime": True,
}
},
use_ipc=True,
)
result = iface.handle_command("ipc_add_track", {})
assert result["_backend"] == "ipc"
assert result["_realtime"] is True
def test_backend_info_uses_reported_backend_for_metadata():
iface = _make_iface(
{
"get_backend_info": lambda params: {
"success": True,
"backend": "ipc",
"realtime_sync": True,
}
},
use_ipc=True,
)
result = iface.handle_command("get_backend_info", {})
assert result["_backend"] == "ipc"
assert result["_realtime"] is True
def test_ipc_capable_command_reconnects_when_kicad_is_running(monkeypatch):
import kicad_interface
monkeypatch.setattr(kicad_interface, "KICAD_BACKEND", "auto")
monkeypatch.setattr(kicad_interface.KiCADProcessManager, "is_running", lambda: True)
monkeypatch.setitem(
sys.modules,
"kicad_api.ipc_backend",
types.SimpleNamespace(IPCBackend=_FakeIPCBackend),
)
iface = _make_iface({}, use_ipc=False)
result = iface.handle_command("get_board_info", {})
assert result["success"] is True
assert result["_backend"] == "ipc"
assert result["_realtime"] is True
def test_ipc_capable_command_does_not_reconnect_in_strict_swig_mode(monkeypatch):
import kicad_interface
monkeypatch.setattr(kicad_interface, "KICAD_BACKEND", "swig")
monkeypatch.setattr(kicad_interface.KiCADProcessManager, "is_running", lambda: True)
monkeypatch.setitem(
sys.modules,
"kicad_api.ipc_backend",
types.SimpleNamespace(IPCBackend=_ConnectShouldNotBeCalledIPCBackend),
)
iface = _make_iface(
{
"get_board_info": lambda params: {
"success": True,
"board": {"filename": "demo.kicad_pcb"},
}
},
use_ipc=False,
)
result = iface.handle_command("get_board_info", {})
assert result["success"] is True
assert result["_backend"] == "swig"
assert result["_realtime"] is False
assert iface.ipc_board_api is None
def test_ipc_reconnect_failure_falls_back_to_swig(monkeypatch):
import kicad_interface
monkeypatch.setattr(kicad_interface, "KICAD_BACKEND", "auto")
monkeypatch.setattr(kicad_interface.KiCADProcessManager, "is_running", lambda: True)
monkeypatch.setitem(
sys.modules,
"kicad_api.ipc_backend",
types.SimpleNamespace(IPCBackend=_FailingConnectIPCBackend),
)
iface = _make_iface(
{
"get_board_info": lambda params: {
"success": True,
"board": {"filename": "demo.kicad_pcb"},
}
},
use_ipc=False,
)
result = iface.handle_command("get_board_info", {})
assert result["success"] is True
assert result["_backend"] == "swig"
assert result["_realtime"] is False
assert iface.use_ipc is False
assert iface.ipc_board_api is None
def test_connected_ipc_without_board_api_reports_status_but_board_tools_fallback(monkeypatch):
import kicad_interface
monkeypatch.setattr(kicad_interface, "KICAD_BACKEND", "auto")
monkeypatch.setattr(kicad_interface.KiCADProcessManager, "is_running", lambda: True)
monkeypatch.setitem(
sys.modules,
"kicad_api.ipc_backend",
types.SimpleNamespace(IPCBackend=_NoBoardIPCBackend),
)
iface = _make_iface(
{
"get_board_info": lambda params: {
"success": True,
"board": {"filename": "demo.kicad_pcb"},
}
},
use_ipc=False,
)
iface.command_routes["get_backend_info"] = iface._handle_get_backend_info
board_result = iface.handle_command("get_board_info", {})
backend_result = iface.handle_command("get_backend_info", {})
assert board_result["success"] is True
assert board_result["_backend"] == "swig"
assert board_result["_realtime"] is False
assert iface.use_ipc is True
assert iface.ipc_board_api is None
assert backend_result["success"] is True
assert backend_result["backend"] == "ipc"
assert backend_result["realtime_sync"] is True
assert backend_result["ipc_connected"] is True
assert backend_result["_backend"] == "ipc"
def test_ui_status_tools_report_live_ipc_backend_status(monkeypatch):
import kicad_interface
monkeypatch.setattr(kicad_interface, "KICAD_BACKEND", "auto")
monkeypatch.setattr(kicad_interface.KiCADProcessManager, "is_running", lambda self=None: True)
monkeypatch.setattr(
kicad_interface.KiCADProcessManager,
"get_process_info",
lambda self: [{"pid": "1234", "name": "pcbnew.exe", "command": "pcbnew.exe"}],
)
monkeypatch.setitem(
sys.modules,
"kicad_api.ipc_backend",
types.SimpleNamespace(IPCBackend=_FakeIPCBackend),
)
iface = _make_iface({}, use_ipc=False)
iface.ipc_backend = _FakeIPCBackend()
iface.command_routes = {
"check_kicad_ui": iface._handle_check_kicad_ui,
"launch_kicad_ui": iface._handle_launch_kicad_ui,
"get_backend_info": iface._handle_get_backend_info,
}
monkeypatch.setattr(
kicad_interface,
"check_and_launch_kicad",
lambda path_obj, auto_launch: {
"running": True,
"launched": True,
"processes": [{"pid": "1234", "name": "pcbnew.exe", "command": "pcbnew.exe"}],
"message": "KiCAD launched successfully",
"project": str(path_obj) if path_obj else None,
},
)
for command in ("check_kicad_ui", "launch_kicad_ui", "get_backend_info"):
result = iface.handle_command(command, {})
assert result["success"] is True
assert result["backend"] == "ipc"
assert result["realtime_sync"] is True
assert result["ipc_connected"] is True
assert result["_backend"] == "ipc"
assert result["_realtime"] is True
def test_query_traces_can_use_ipc_backend(monkeypatch):
import kicad_interface
monkeypatch.setattr(kicad_interface, "KICAD_BACKEND", "swig")
iface = _make_iface({}, use_ipc=True)
iface.ipc_board_api = _FakeIPCBoardAPI()
result = iface.handle_command(
"query_traces",
{"layer": "F.Cu", "boundingBox": {"x1": -1, "y1": -1, "x2": 1, "y2": 1}},
)
assert result["success"] is True
assert result["traceCount"] == 1
assert result["traces"][0]["layer"] == "F.Cu"
assert result["traces"][0]["length"] == 5
assert result["_backend"] == "ipc"
def test_query_traces_ipc_filters_and_vias(monkeypatch):
import kicad_interface
monkeypatch.setattr(kicad_interface, "KICAD_BACKEND", "swig")
iface = _make_iface({}, use_ipc=True)
iface.ipc_board_api = _FilteringIPCBoardAPI()
net_miss = iface.handle_command("query_traces", {"net": "NO_MATCH"})
layer_match = iface.handle_command("query_traces", {"layer": "B.Cu"})
reversed_bbox_with_vias = iface.handle_command(
"query_traces",
{
"net": "N$1",
"includeVias": True,
"boundingBox": {"x1": 1, "y1": 1, "x2": -1, "y2": -1},
},
)
assert net_miss["success"] is True
assert net_miss["traceCount"] == 0
assert net_miss["_backend"] == "ipc"
assert layer_match["success"] is True
assert layer_match["traceCount"] == 1
assert layer_match["traces"][0]["uuid"] == "track-2"
assert layer_match["traces"][0]["layer"] == "B.Cu"
assert reversed_bbox_with_vias["success"] is True
assert reversed_bbox_with_vias["traceCount"] == 1
assert reversed_bbox_with_vias["traces"][0]["uuid"] == "track-1"
assert reversed_bbox_with_vias["viaCount"] == 1
assert reversed_bbox_with_vias["vias"][0]["uuid"] == "via-1"
def test_ipc_board_size_supports_kicad_10_box2_pos_size(monkeypatch):
from kicad_api.ipc_backend import IPCBoardAPI
_stub_kipy_units(monkeypatch)
board_api = IPCBoardAPI(None, lambda *_args: None)
board_api._board = _BoundingBoxBoard(
[
_BoxWithPosSize(1_000_000, 2_000_000, 3_000_000, 4_000_000),
_BoxWithPosSize(0, 1_000_000, 2_000_000, 1_000_000),
]
)
result = board_api.get_size()
assert result == {"width": 4.0, "height": 5.0, "unit": "mm"}
def test_ipc_board_size_keeps_min_max_box2_compatibility(monkeypatch):
from kicad_api.ipc_backend import IPCBoardAPI
_stub_kipy_units(monkeypatch)
board_api = IPCBoardAPI(None, lambda *_args: None)
board_api._board = _BoundingBoxBoard(
[
_BoxWithMinMax(1_000_000, 2_000_000, 3_000_000, 4_000_000),
_BoxWithMinMax(0, 1_000_000, 2_000_000, 3_000_000),
]
)
result = board_api.get_size()
assert result == {"width": 3.0, "height": 3.0, "unit": "mm"}