feat: Implement IPC backend for real-time UI synchronization
Add KiCAD IPC API backend using kicad-python library for real-time communication with KiCAD 9.0+. Changes now appear instantly in KiCAD UI without manual reload. Key changes: - Implement IPCBackend and IPCBoardAPI classes for IPC communication - Auto-detect IPC availability and fall back to SWIG when unavailable - Route existing commands (route_trace, add_via, place_component, etc.) through IPC automatically when available - Add transaction support for proper undo/redo - Add socket path auto-detection for Linux (/tmp/kicad/api.sock) New commands: - get_backend_info: Check which backend is active Supported IPC operations: - Board: set_size, get_board_info, save - Routing: route_trace, add_via, add_net - Components: place, move, delete, list - Text: add_text, add_board_text SWIG backend is now deprecated and will be removed when KiCAD 10.0 drops SWIG support. Requires: kicad-python>=0.5.0, KiCAD 9.0+ with IPC enabled 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
330
python/test_ipc_backend.py
Normal file
330
python/test_ipc_backend.py
Normal file
@@ -0,0 +1,330 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for KiCAD IPC Backend
|
||||
|
||||
This script tests the real-time UI synchronization capabilities
|
||||
of the IPC backend. Run this while KiCAD is open with a board.
|
||||
|
||||
Prerequisites:
|
||||
1. KiCAD 9.0+ must be running
|
||||
2. IPC API must be enabled: Preferences > Plugins > Enable IPC API Server
|
||||
3. A board should be open in the PCB editor
|
||||
|
||||
Usage:
|
||||
./venv/bin/python python/test_ipc_backend.py
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import logging
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def test_connection():
|
||||
"""Test basic IPC connection to KiCAD."""
|
||||
print("\n" + "="*60)
|
||||
print("TEST 1: IPC Connection")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
from kicad_api.ipc_backend import IPCBackend
|
||||
|
||||
backend = IPCBackend()
|
||||
print("✓ IPCBackend created")
|
||||
|
||||
if backend.connect():
|
||||
print(f"✓ Connected to KiCAD via IPC")
|
||||
print(f" Version: {backend.get_version()}")
|
||||
return backend
|
||||
else:
|
||||
print("✗ Failed to connect to KiCAD")
|
||||
return None
|
||||
|
||||
except ImportError as e:
|
||||
print(f"✗ kicad-python not installed: {e}")
|
||||
print(" Install with: pip install kicad-python")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"✗ Connection failed: {e}")
|
||||
print("\nMake sure:")
|
||||
print(" 1. KiCAD is running")
|
||||
print(" 2. IPC API is enabled (Preferences > Plugins > Enable IPC API Server)")
|
||||
print(" 3. A board is open in the PCB editor")
|
||||
return None
|
||||
|
||||
|
||||
def test_board_access(backend):
|
||||
"""Test board access and component listing."""
|
||||
print("\n" + "="*60)
|
||||
print("TEST 2: Board Access")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
board_api = backend.get_board()
|
||||
print("✓ Got board API")
|
||||
|
||||
# List components
|
||||
components = board_api.list_components()
|
||||
print(f"✓ Found {len(components)} components on board")
|
||||
|
||||
if components:
|
||||
print("\n First 5 components:")
|
||||
for comp in components[:5]:
|
||||
ref = comp.get('reference', 'N/A')
|
||||
val = comp.get('value', 'N/A')
|
||||
pos = comp.get('position', {})
|
||||
x = pos.get('x', 0)
|
||||
y = pos.get('y', 0)
|
||||
print(f" - {ref}: {val} @ ({x:.2f}, {y:.2f}) mm")
|
||||
|
||||
return board_api
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to access board: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def test_board_info(board_api):
|
||||
"""Test getting board information."""
|
||||
print("\n" + "="*60)
|
||||
print("TEST 3: Board Information")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
# Get board size
|
||||
size = board_api.get_size()
|
||||
print(f"✓ Board size: {size.get('width', 0):.2f} x {size.get('height', 0):.2f} mm")
|
||||
|
||||
# Get enabled layers
|
||||
try:
|
||||
layers = board_api.get_enabled_layers()
|
||||
print(f"✓ Enabled layers: {len(layers)}")
|
||||
if layers:
|
||||
print(f" Layers: {', '.join(layers[:5])}...")
|
||||
except Exception as e:
|
||||
print(f" (Layer info not available: {e})")
|
||||
|
||||
# Get nets
|
||||
nets = board_api.get_nets()
|
||||
print(f"✓ Found {len(nets)} nets")
|
||||
if nets:
|
||||
print(f" First 5 nets: {', '.join([n.get('name', '') for n in nets[:5]])}")
|
||||
|
||||
# Get tracks
|
||||
tracks = board_api.get_tracks()
|
||||
print(f"✓ Found {len(tracks)} tracks")
|
||||
|
||||
# Get vias
|
||||
vias = board_api.get_vias()
|
||||
print(f"✓ Found {len(vias)} vias")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to get board info: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_realtime_track(board_api, interactive=False):
|
||||
"""Test adding a track in real-time (appears immediately in KiCAD UI)."""
|
||||
print("\n" + "="*60)
|
||||
print("TEST 4: Real-time Track Addition")
|
||||
print("="*60)
|
||||
|
||||
print("\nThis test will add a track that appears IMMEDIATELY in KiCAD UI.")
|
||||
print("Watch the KiCAD window!")
|
||||
|
||||
if interactive:
|
||||
response = input("\nProceed with adding a test track? [y/N]: ").strip().lower()
|
||||
if response != 'y':
|
||||
print("Skipped track test")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Add a track
|
||||
success = board_api.add_track(
|
||||
start_x=100.0,
|
||||
start_y=100.0,
|
||||
end_x=120.0,
|
||||
end_y=100.0,
|
||||
width=0.25,
|
||||
layer="F.Cu"
|
||||
)
|
||||
|
||||
if success:
|
||||
print("✓ Track added! Check the KiCAD window - it should appear at (100, 100) mm")
|
||||
print(" Track: (100, 100) -> (120, 100) mm, width 0.25mm on F.Cu")
|
||||
else:
|
||||
print("✗ Failed to add track")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error adding track: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_realtime_via(board_api, interactive=False):
|
||||
"""Test adding a via in real-time (appears immediately in KiCAD UI)."""
|
||||
print("\n" + "="*60)
|
||||
print("TEST 5: Real-time Via Addition")
|
||||
print("="*60)
|
||||
|
||||
print("\nThis test will add a via that appears IMMEDIATELY in KiCAD UI.")
|
||||
print("Watch the KiCAD window!")
|
||||
|
||||
if interactive:
|
||||
response = input("\nProceed with adding a test via? [y/N]: ").strip().lower()
|
||||
if response != 'y':
|
||||
print("Skipped via test")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Add a via
|
||||
success = board_api.add_via(
|
||||
x=120.0,
|
||||
y=100.0,
|
||||
diameter=0.8,
|
||||
drill=0.4,
|
||||
via_type="through"
|
||||
)
|
||||
|
||||
if success:
|
||||
print("✓ Via added! Check the KiCAD window - it should appear at (120, 100) mm")
|
||||
print(" Via: diameter 0.8mm, drill 0.4mm")
|
||||
else:
|
||||
print("✗ Failed to add via")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error adding via: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_realtime_text(board_api, interactive=False):
|
||||
"""Test adding text in real-time."""
|
||||
print("\n" + "="*60)
|
||||
print("TEST 6: Real-time Text Addition")
|
||||
print("="*60)
|
||||
|
||||
print("\nThis test will add text that appears IMMEDIATELY in KiCAD UI.")
|
||||
|
||||
if interactive:
|
||||
response = input("\nProceed with adding test text? [y/N]: ").strip().lower()
|
||||
if response != 'y':
|
||||
print("Skipped text test")
|
||||
return False
|
||||
|
||||
try:
|
||||
success = board_api.add_text(
|
||||
text="MCP Test",
|
||||
x=100.0,
|
||||
y=95.0,
|
||||
layer="F.SilkS",
|
||||
size=1.0
|
||||
)
|
||||
|
||||
if success:
|
||||
print("✓ Text added! Check the KiCAD window - should show 'MCP Test' at (100, 95) mm")
|
||||
else:
|
||||
print("✗ Failed to add text")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error adding text: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_selection(board_api, interactive=False):
|
||||
"""Test getting the current selection from KiCAD UI."""
|
||||
print("\n" + "="*60)
|
||||
print("TEST 7: UI Selection")
|
||||
print("="*60)
|
||||
|
||||
if interactive:
|
||||
print("\nSelect some items in KiCAD, then press Enter...")
|
||||
input()
|
||||
else:
|
||||
print("\nReading current selection...")
|
||||
|
||||
try:
|
||||
selection = board_api.get_selection()
|
||||
print(f"✓ Found {len(selection)} selected items")
|
||||
|
||||
for item in selection[:10]:
|
||||
print(f" - {item.get('type', 'Unknown')} (ID: {item.get('id', 'N/A')})")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to get selection: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def run_all_tests(interactive=False):
|
||||
"""Run all IPC backend tests."""
|
||||
print("\n" + "="*60)
|
||||
print("KiCAD IPC Backend Test Suite")
|
||||
print("="*60)
|
||||
print("\nThis script tests real-time communication with KiCAD via IPC API.")
|
||||
print("Make sure KiCAD is running with a board open.\n")
|
||||
|
||||
# Test connection
|
||||
backend = test_connection()
|
||||
if not backend:
|
||||
print("\n" + "="*60)
|
||||
print("TESTS FAILED: Could not connect to KiCAD")
|
||||
print("="*60)
|
||||
return False
|
||||
|
||||
# Test board access
|
||||
board_api = test_board_access(backend)
|
||||
if not board_api:
|
||||
print("\n" + "="*60)
|
||||
print("TESTS FAILED: Could not access board")
|
||||
print("="*60)
|
||||
return False
|
||||
|
||||
# Test board info
|
||||
test_board_info(board_api)
|
||||
|
||||
# Test real-time modifications
|
||||
test_realtime_track(board_api, interactive)
|
||||
test_realtime_via(board_api, interactive)
|
||||
test_realtime_text(board_api, interactive)
|
||||
|
||||
# Test selection
|
||||
test_selection(board_api, interactive)
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("TESTS COMPLETE")
|
||||
print("="*60)
|
||||
print("\nThe IPC backend is working! Changes appear in real-time.")
|
||||
print("No manual reload required - this is the power of the IPC API!")
|
||||
|
||||
# Cleanup
|
||||
backend.disconnect()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description='Test KiCAD IPC Backend')
|
||||
parser.add_argument('-i', '--interactive', action='store_true',
|
||||
help='Run in interactive mode (prompts before modifications)')
|
||||
args = parser.parse_args()
|
||||
|
||||
success = run_all_tests(interactive=args.interactive)
|
||||
sys.exit(0 if success else 1)
|
||||
Reference in New Issue
Block a user