319473b1d8
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>
331 lines
9.5 KiB
Python
331 lines
9.5 KiB
Python
#!/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)
|