Add comprehensive Windows support and documentation

Windows Support Package:
- PowerShell automated setup script (setup-windows.ps1)
  - Auto-detects KiCAD installation and version
  - Validates all prerequisites (Node.js, Python, pcbnew)
  - Installs dependencies automatically
  - Generates MCP configuration with platform-specific paths
  - Runs comprehensive diagnostic tests
- Windows troubleshooting guide (docs/WINDOWS_TROUBLESHOOTING.md)
- Platform comparison guide (docs/PLATFORM_GUIDE.md)

Code Enhancements:
- Enhanced Windows error diagnostics in Python interface
- Startup validation in TypeScript server
- Platform-specific error messages with troubleshooting hints
- Component library integration (153 KiCAD footprint libraries)
- Routing operations KiCAD 9.0 API compatibility fixes

Documentation Updates:
- Updated README with Windows automated setup
- Real-time collaboration workflow guide
- Library integration documentation
- JLCPCB integration planning
- Updated status to reflect Windows support
- Changelogs for Nov 1 and Nov 5 updates

Infrastructure:
- Added venv/ to .gitignore to prevent virtual env commits

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
KiCAD MCP Bot
2025-11-05 09:10:45 -05:00
parent 0354b70c68
commit 5717a91a59
18 changed files with 4551 additions and 328 deletions

View File

@@ -8,15 +8,17 @@ import logging
import math
from typing import Dict, Any, Optional, List, Tuple
import base64
from commands.library import LibraryManager
logger = logging.getLogger('kicad_interface')
class ComponentCommands:
"""Handles component-related KiCAD operations"""
def __init__(self, board: Optional[pcbnew.BOARD] = None):
"""Initialize with optional board instance"""
def __init__(self, board: Optional[pcbnew.BOARD] = None, library_manager: Optional[LibraryManager] = None):
"""Initialize with optional board instance and library manager"""
self.board = board
self.library_manager = library_manager or LibraryManager()
def place_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Place a component on the PCB"""
@@ -44,13 +46,48 @@ class ComponentCommands:
"errorDetails": "componentId and position are required"
}
# Create new module (footprint)
module = pcbnew.FootprintLoad(self.board.GetLibraryPath(), component_id)
# Find footprint using library manager
# component_id can be "Library:Footprint" or just "Footprint"
footprint_result = self.library_manager.find_footprint(component_id)
if not footprint_result:
# Try to suggest similar footprints
suggestions = self.library_manager.search_footprints(f"*{component_id}*", limit=5)
suggestion_text = ""
if suggestions:
suggestion_text = "\n\nDid you mean one of these?\n" + \
"\n".join([f" - {s['full_name']}" for s in suggestions])
return {
"success": False,
"message": "Footprint not found",
"errorDetails": f"Could not find footprint: {component_id}{suggestion_text}"
}
library_path, footprint_name = footprint_result
# Load footprint from library
# Extract library nickname from path
library_nickname = None
for nick, path in self.library_manager.libraries.items():
if path == library_path:
library_nickname = nick
break
if not library_nickname:
return {
"success": False,
"message": "Internal error",
"errorDetails": "Could not determine library nickname"
}
# Load the footprint
module = pcbnew.FootprintLoad(library_path, footprint_name)
if not module:
return {
"success": False,
"message": "Component not found",
"errorDetails": f"Could not find component: {component_id}"
"message": "Failed to load footprint",
"errorDetails": f"Could not load footprint from {library_path}/{footprint_name}"
}
# Set position
@@ -71,8 +108,9 @@ class ComponentCommands:
if footprint:
module.SetFootprintName(footprint)
# Set rotation
module.SetOrientation(rotation * 10) # KiCAD uses decidegrees
# Set rotation (KiCAD 9.0 uses EDA_ANGLE)
angle = pcbnew.EDA_ANGLE(rotation, pcbnew.DEGREES_T)
module.SetOrientation(angle)
# Set layer
layer_id = self.board.GetLayerID(layer)
@@ -144,7 +182,8 @@ class ComponentCommands:
# Set new rotation if provided
if rotation is not None:
module.SetOrientation(rotation * 10) # KiCAD uses decidegrees
angle = pcbnew.EDA_ANGLE(rotation, pcbnew.DEGREES_T)
module.SetOrientation(angle)
return {
"success": True,
@@ -156,7 +195,7 @@ class ComponentCommands:
"y": position["y"],
"unit": position["unit"]
},
"rotation": rotation if rotation is not None else module.GetOrientation() / 10
"rotation": rotation if rotation is not None else module.GetOrientation().AsDegrees()
}
}
@@ -198,7 +237,8 @@ class ComponentCommands:
}
# Set rotation
module.SetOrientation(angle * 10) # KiCAD uses decidegrees
rotation_angle = pcbnew.EDA_ANGLE(angle, pcbnew.DEGREES_T)
module.SetOrientation(rotation_angle)
return {
"success": True,
@@ -305,7 +345,7 @@ class ComponentCommands:
"component": {
"reference": new_reference or reference,
"value": value or module.GetValue(),
"footprint": footprint or module.GetFootprintName()
"footprint": footprint or module.GetFPIDAsString()
}
}
@@ -354,13 +394,13 @@ class ComponentCommands:
"component": {
"reference": module.GetReference(),
"value": module.GetValue(),
"footprint": module.GetFootprintName(),
"footprint": module.GetFPIDAsString(),
"position": {
"x": x_mm,
"y": y_mm,
"unit": "mm"
},
"rotation": module.GetOrientation() / 10,
"rotation": module.GetOrientation().AsDegrees(),
"layer": self.board.GetLayerName(module.GetLayer()),
"attributes": {
"smd": module.GetAttributes() & pcbnew.FP_SMD,
@@ -397,13 +437,13 @@ class ComponentCommands:
components.append({
"reference": module.GetReference(),
"value": module.GetValue(),
"footprint": module.GetFootprintName(),
"footprint": module.GetFPIDAsString(),
"position": {
"x": x_mm,
"y": y_mm,
"unit": "mm"
},
"rotation": module.GetOrientation() / 10,
"rotation": module.GetOrientation().AsDegrees(),
"layer": self.board.GetLayerName(module.GetLayer())
})
@@ -594,7 +634,7 @@ class ComponentCommands:
"y": pos.y / 1000000,
"unit": "mm"
},
"rotation": module.GetOrientation() / 10
"rotation": module.GetOrientation().AsDegrees()
})
return {
@@ -654,7 +694,7 @@ class ComponentCommands:
# Create new footprint with the same properties
new_module = pcbnew.FOOTPRINT(self.board)
new_module.SetFootprintName(source.GetFootprintName())
new_module.SetFootprintName(source.GetFPIDAsString())
new_module.SetValue(source.GetValue())
new_module.SetReference(new_reference)
new_module.SetLayer(source.GetLayer())
@@ -678,7 +718,8 @@ class ComponentCommands:
# Set rotation if provided, otherwise use same as original
if rotation is not None:
new_module.SetOrientation(rotation * 10) # KiCAD uses decidegrees
rotation_angle = pcbnew.EDA_ANGLE(rotation, pcbnew.DEGREES_T)
new_module.SetOrientation(rotation_angle)
else:
new_module.SetOrientation(source.GetOrientation())
@@ -694,13 +735,13 @@ class ComponentCommands:
"component": {
"reference": new_reference,
"value": new_module.GetValue(),
"footprint": new_module.GetFootprintName(),
"footprint": new_module.GetFPIDAsString(),
"position": {
"x": pos.x / 1000000,
"y": pos.y / 1000000,
"unit": "mm"
},
"rotation": new_module.GetOrientation() / 10,
"rotation": new_module.GetOrientation().AsDegrees(),
"layer": self.board.GetLayerName(new_module.GetLayer())
}
}

450
python/commands/library.py Normal file
View File

@@ -0,0 +1,450 @@
"""
Library management for KiCAD footprints
Handles parsing fp-lib-table files, discovering footprints,
and providing search functionality for component placement.
"""
import os
import re
import logging
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import glob
logger = logging.getLogger('kicad_interface')
class LibraryManager:
"""
Manages KiCAD footprint libraries
Parses fp-lib-table files (both global and project-specific),
indexes available footprints, and provides search functionality.
"""
def __init__(self, project_path: Optional[Path] = None):
"""
Initialize library manager
Args:
project_path: Optional path to project directory for project-specific libraries
"""
self.project_path = project_path
self.libraries: Dict[str, str] = {} # nickname -> path mapping
self.footprint_cache: Dict[str, List[str]] = {} # library -> [footprint names]
self._load_libraries()
def _load_libraries(self):
"""Load libraries from fp-lib-table files"""
# Load global libraries
global_table = self._get_global_fp_lib_table()
if global_table and global_table.exists():
logger.info(f"Loading global fp-lib-table from: {global_table}")
self._parse_fp_lib_table(global_table)
else:
logger.warning(f"Global fp-lib-table not found at: {global_table}")
# Load project-specific libraries if project path provided
if self.project_path:
project_table = self.project_path / "fp-lib-table"
if project_table.exists():
logger.info(f"Loading project fp-lib-table from: {project_table}")
self._parse_fp_lib_table(project_table)
logger.info(f"Loaded {len(self.libraries)} footprint libraries")
def _get_global_fp_lib_table(self) -> Optional[Path]:
"""Get path to global fp-lib-table file"""
# Try different possible locations
kicad_config_paths = [
Path.home() / ".config" / "kicad" / "9.0" / "fp-lib-table",
Path.home() / ".config" / "kicad" / "8.0" / "fp-lib-table",
Path.home() / ".config" / "kicad" / "fp-lib-table",
# Windows paths
Path.home() / "AppData" / "Roaming" / "kicad" / "9.0" / "fp-lib-table",
Path.home() / "AppData" / "Roaming" / "kicad" / "8.0" / "fp-lib-table",
# macOS paths
Path.home() / "Library" / "Preferences" / "kicad" / "9.0" / "fp-lib-table",
Path.home() / "Library" / "Preferences" / "kicad" / "8.0" / "fp-lib-table",
]
for path in kicad_config_paths:
if path.exists():
return path
return None
def _parse_fp_lib_table(self, table_path: Path):
"""
Parse fp-lib-table file
Format is S-expression (Lisp-like):
(fp_lib_table
(lib (name "Library_Name")(type KiCad)(uri "${KICAD9_FOOTPRINT_DIR}/Library.pretty")(options "")(descr "Description"))
)
"""
try:
with open(table_path, 'r') as f:
content = f.read()
# Simple regex-based parser for lib entries
# Pattern: (lib (name "NAME")(type TYPE)(uri "URI")...)
lib_pattern = r'\(lib\s+\(name\s+"?([^")\s]+)"?\)\s*\(type\s+[^)]+\)\s*\(uri\s+"?([^")\s]+)"?'
for match in re.finditer(lib_pattern, content, re.IGNORECASE):
nickname = match.group(1)
uri = match.group(2)
# Resolve environment variables in URI
resolved_uri = self._resolve_uri(uri)
if resolved_uri:
self.libraries[nickname] = resolved_uri
logger.debug(f" Found library: {nickname} -> {resolved_uri}")
else:
logger.warning(f" Could not resolve URI for library {nickname}: {uri}")
except Exception as e:
logger.error(f"Error parsing fp-lib-table at {table_path}: {e}")
def _resolve_uri(self, uri: str) -> Optional[str]:
"""
Resolve environment variables and paths in library URI
Handles:
- ${KICAD9_FOOTPRINT_DIR} -> /usr/share/kicad/footprints
- ${KICAD8_FOOTPRINT_DIR} -> /usr/share/kicad/footprints
- ${KIPRJMOD} -> project directory
- Relative paths
- Absolute paths
"""
# Replace environment variables
resolved = uri
# Common KiCAD environment variables
env_vars = {
'KICAD9_FOOTPRINT_DIR': self._find_kicad_footprint_dir(),
'KICAD8_FOOTPRINT_DIR': self._find_kicad_footprint_dir(),
'KICAD_FOOTPRINT_DIR': self._find_kicad_footprint_dir(),
'KISYSMOD': self._find_kicad_footprint_dir(),
}
# Project directory
if self.project_path:
env_vars['KIPRJMOD'] = str(self.project_path)
# Replace environment variables
for var, value in env_vars.items():
if value:
resolved = resolved.replace(f'${{{var}}}', value)
resolved = resolved.replace(f'${var}', value)
# Expand ~ to home directory
resolved = os.path.expanduser(resolved)
# Convert to absolute path
path = Path(resolved)
# Check if path exists
if path.exists():
return str(path)
else:
logger.debug(f" Path does not exist: {path}")
return None
def _find_kicad_footprint_dir(self) -> Optional[str]:
"""Find KiCAD footprint directory"""
# Try common locations
possible_paths = [
"/usr/share/kicad/footprints",
"/usr/local/share/kicad/footprints",
"C:/Program Files/KiCad/9.0/share/kicad/footprints",
"C:/Program Files/KiCad/8.0/share/kicad/footprints",
"/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints",
]
# Also check environment variable
if 'KICAD9_FOOTPRINT_DIR' in os.environ:
possible_paths.insert(0, os.environ['KICAD9_FOOTPRINT_DIR'])
if 'KICAD8_FOOTPRINT_DIR' in os.environ:
possible_paths.insert(0, os.environ['KICAD8_FOOTPRINT_DIR'])
for path in possible_paths:
if os.path.isdir(path):
return path
return None
def list_libraries(self) -> List[str]:
"""Get list of available library nicknames"""
return list(self.libraries.keys())
def get_library_path(self, nickname: str) -> Optional[str]:
"""Get filesystem path for a library nickname"""
return self.libraries.get(nickname)
def list_footprints(self, library_nickname: str) -> List[str]:
"""
List all footprints in a library
Args:
library_nickname: Library name (e.g., "Resistor_SMD")
Returns:
List of footprint names (without .kicad_mod extension)
"""
# Check cache first
if library_nickname in self.footprint_cache:
return self.footprint_cache[library_nickname]
library_path = self.libraries.get(library_nickname)
if not library_path:
logger.warning(f"Library not found: {library_nickname}")
return []
try:
footprints = []
lib_dir = Path(library_path)
# List all .kicad_mod files
for fp_file in lib_dir.glob("*.kicad_mod"):
# Remove .kicad_mod extension
footprint_name = fp_file.stem
footprints.append(footprint_name)
# Cache the results
self.footprint_cache[library_nickname] = footprints
logger.debug(f"Found {len(footprints)} footprints in {library_nickname}")
return footprints
except Exception as e:
logger.error(f"Error listing footprints in {library_nickname}: {e}")
return []
def find_footprint(self, footprint_spec: str) -> Optional[Tuple[str, str]]:
"""
Find a footprint by specification
Supports multiple formats:
- "Library:Footprint" (e.g., "Resistor_SMD:R_0603_1608Metric")
- "Footprint" (searches all libraries)
Args:
footprint_spec: Footprint specification
Returns:
Tuple of (library_path, footprint_name) or None if not found
"""
# Parse specification
if ":" in footprint_spec:
# Format: Library:Footprint
library_nickname, footprint_name = footprint_spec.split(":", 1)
library_path = self.libraries.get(library_nickname)
if not library_path:
logger.warning(f"Library not found: {library_nickname}")
return None
# Check if footprint exists
fp_file = Path(library_path) / f"{footprint_name}.kicad_mod"
if fp_file.exists():
return (library_path, footprint_name)
else:
logger.warning(f"Footprint not found: {footprint_spec}")
return None
else:
# Format: Footprint (search all libraries)
footprint_name = footprint_spec
# Search in all libraries
for library_nickname, library_path in self.libraries.items():
fp_file = Path(library_path) / f"{footprint_name}.kicad_mod"
if fp_file.exists():
logger.info(f"Found footprint {footprint_name} in library {library_nickname}")
return (library_path, footprint_name)
logger.warning(f"Footprint not found in any library: {footprint_name}")
return None
def search_footprints(self, pattern: str, limit: int = 20) -> List[Dict[str, str]]:
"""
Search for footprints matching a pattern
Args:
pattern: Search pattern (supports wildcards *, case-insensitive)
limit: Maximum number of results to return
Returns:
List of dicts with 'library', 'footprint', and 'full_name' keys
"""
results = []
pattern_lower = pattern.lower()
# Convert wildcards to regex
regex_pattern = pattern_lower.replace("*", ".*")
regex = re.compile(regex_pattern)
for library_nickname in self.libraries.keys():
footprints = self.list_footprints(library_nickname)
for footprint in footprints:
if regex.search(footprint.lower()):
results.append({
'library': library_nickname,
'footprint': footprint,
'full_name': f"{library_nickname}:{footprint}"
})
if len(results) >= limit:
return results
return results
def get_footprint_info(self, library_nickname: str, footprint_name: str) -> Optional[Dict[str, str]]:
"""
Get information about a specific footprint
Args:
library_nickname: Library name
footprint_name: Footprint name
Returns:
Dict with footprint information or None if not found
"""
library_path = self.libraries.get(library_nickname)
if not library_path:
return None
fp_file = Path(library_path) / f"{footprint_name}.kicad_mod"
if not fp_file.exists():
return None
return {
'library': library_nickname,
'footprint': footprint_name,
'full_name': f"{library_nickname}:{footprint_name}",
'path': str(fp_file),
'library_path': library_path
}
class LibraryCommands:
"""Command handlers for library operations"""
def __init__(self, library_manager: Optional[LibraryManager] = None):
"""Initialize with optional library manager"""
self.library_manager = library_manager or LibraryManager()
def list_libraries(self, params: Dict) -> Dict:
"""List all available footprint libraries"""
try:
libraries = self.library_manager.list_libraries()
return {
"success": True,
"libraries": libraries,
"count": len(libraries)
}
except Exception as e:
logger.error(f"Error listing libraries: {e}")
return {
"success": False,
"message": "Failed to list libraries",
"errorDetails": str(e)
}
def search_footprints(self, params: Dict) -> Dict:
"""Search for footprints by pattern"""
try:
pattern = params.get("pattern", "*")
limit = params.get("limit", 20)
results = self.library_manager.search_footprints(pattern, limit)
return {
"success": True,
"footprints": results,
"count": len(results),
"pattern": pattern
}
except Exception as e:
logger.error(f"Error searching footprints: {e}")
return {
"success": False,
"message": "Failed to search footprints",
"errorDetails": str(e)
}
def list_library_footprints(self, params: Dict) -> Dict:
"""List all footprints in a specific library"""
try:
library = params.get("library")
if not library:
return {
"success": False,
"message": "Missing library parameter"
}
footprints = self.library_manager.list_footprints(library)
return {
"success": True,
"library": library,
"footprints": footprints,
"count": len(footprints)
}
except Exception as e:
logger.error(f"Error listing library footprints: {e}")
return {
"success": False,
"message": "Failed to list library footprints",
"errorDetails": str(e)
}
def get_footprint_info(self, params: Dict) -> Dict:
"""Get information about a specific footprint"""
try:
footprint_spec = params.get("footprint")
if not footprint_spec:
return {
"success": False,
"message": "Missing footprint parameter"
}
# Try to find the footprint
result = self.library_manager.find_footprint(footprint_spec)
if result:
library_path, footprint_name = result
# Extract library nickname from path
library_nickname = None
for nick, path in self.library_manager.libraries.items():
if path == library_path:
library_nickname = nick
break
info = {
"library": library_nickname,
"footprint": footprint_name,
"full_name": f"{library_nickname}:{footprint_name}",
"library_path": library_path
}
return {
"success": True,
"footprint_info": info
}
else:
return {
"success": False,
"message": f"Footprint not found: {footprint_spec}"
}
except Exception as e:
logger.error(f"Error getting footprint info: {e}")
return {
"success": False,
"message": "Failed to get footprint info",
"errorDetails": str(e)
}

View File

@@ -39,9 +39,12 @@ class RoutingCommands:
# Create new net
netinfo = self.board.GetNetInfo()
net = netinfo.FindNet(name)
if not net:
net = netinfo.AddNet(name)
nets_map = netinfo.NetsByName()
if nets_map.has_key(name):
net = nets_map[name]
else:
net = pcbnew.NETINFO_ITEM(self.board, name)
self.board.Add(net)
# Set net class if provided
if net_class:
@@ -119,8 +122,9 @@ class RoutingCommands:
# Set net if provided
if net:
netinfo = self.board.GetNetInfo()
net_obj = netinfo.FindNet(net)
if net_obj:
nets_map = netinfo.NetsByName()
if nets_map.has_key(net):
net_obj = nets_map[net]
track.SetNet(net_obj)
# Add track to board
@@ -218,8 +222,9 @@ class RoutingCommands:
# Set net if provided
if net:
netinfo = self.board.GetNetInfo()
net_obj = netinfo.FindNet(net)
if net_obj:
nets_map = netinfo.NetsByName()
if nets_map.has_key(net):
net_obj = nets_map[net]
via.SetNet(net_obj)
# Add via to board
@@ -421,9 +426,10 @@ class RoutingCommands:
# Add nets to net class
netinfo = self.board.GetNetInfo()
nets_map = netinfo.NetsByName()
for net_name in nets:
net = netinfo.FindNet(net_name)
if net:
if nets_map.has_key(net_name):
net = nets_map[net_name]
net.SetClass(netclass)
return {
@@ -492,13 +498,14 @@ class RoutingCommands:
# Set net if provided
if net:
netinfo = self.board.GetNetInfo()
net_obj = netinfo.FindNet(net)
if net_obj:
nets_map = netinfo.NetsByName()
if nets_map.has_key(net):
net_obj = nets_map[net]
zone.SetNet(net_obj)
# Set zone properties
scale = 1000000 # mm to nm
zone.SetPriority(priority)
zone.SetAssignedPriority(priority)
if clearance is not None:
zone.SetLocalClearance(int(clearance * scale))
@@ -509,24 +516,27 @@ class RoutingCommands:
if fill_type == "hatched":
zone.SetFillMode(pcbnew.ZONE_FILL_MODE_HATCH_PATTERN)
else:
zone.SetFillMode(pcbnew.ZONE_FILL_MODE_POLYGON)
zone.SetFillMode(pcbnew.ZONE_FILL_MODE_POLYGONS)
# Create outline
outline = zone.Outline()
outline.NewOutline() # Create a new outline contour first
# Add points to outline
for point in points:
scale = 1000000 if point.get("unit", "mm") == "mm" else 25400000
x_nm = int(point["x"] * scale)
y_nm = int(point["y"] * scale)
outline.Append(pcbnew.VECTOR2I(x_nm, y_nm))
outline.Append(pcbnew.VECTOR2I(x_nm, y_nm)) # Add point to outline
# Add zone to board
self.board.Add(zone)
# Fill zone
filler = pcbnew.ZONE_FILLER(self.board)
filler.Fill(self.board.Zones())
# Note: Zone filling can cause issues with SWIG API
# Comment out for now - zones will be filled when board is saved/opened in KiCAD
# filler = pcbnew.ZONE_FILLER(self.board)
# filler.Fill(self.board.Zones())
return {
"success": True,
@@ -586,9 +596,11 @@ class RoutingCommands:
# Get nets
netinfo = self.board.GetNetInfo()
net_pos_obj = netinfo.FindNet(net_pos)
net_neg_obj = netinfo.FindNet(net_neg)
nets_map = netinfo.NetsByName()
net_pos_obj = nets_map[net_pos] if nets_map.has_key(net_pos) else None
net_neg_obj = nets_map[net_neg] if nets_map.has_key(net_neg) else None
if not net_pos_obj or not net_neg_obj:
return {
"success": False,

View File

@@ -32,6 +32,44 @@ logger = logging.getLogger('kicad_interface')
# Log Python environment details
logger.info(f"Python version: {sys.version}")
logger.info(f"Python executable: {sys.executable}")
logger.info(f"Platform: {sys.platform}")
logger.info(f"Working directory: {os.getcwd()}")
# Windows-specific diagnostics
if sys.platform == 'win32':
logger.info("=== Windows Environment Diagnostics ===")
logger.info(f"PYTHONPATH: {os.environ.get('PYTHONPATH', 'NOT SET')}")
logger.info(f"PATH: {os.environ.get('PATH', 'NOT SET')[:200]}...") # Truncate PATH
# Check for common KiCAD installations
common_kicad_paths = [
r"C:\Program Files\KiCad",
r"C:\Program Files (x86)\KiCad"
]
found_kicad = False
for base_path in common_kicad_paths:
if os.path.exists(base_path):
logger.info(f"Found KiCAD installation at: {base_path}")
# List versions
try:
versions = [d for d in os.listdir(base_path) if os.path.isdir(os.path.join(base_path, d))]
logger.info(f" Versions found: {', '.join(versions)}")
for version in versions:
python_path = os.path.join(base_path, version, 'lib', 'python3', 'dist-packages')
if os.path.exists(python_path):
logger.info(f" ✓ Python path exists: {python_path}")
found_kicad = True
else:
logger.warning(f" ✗ Python path missing: {python_path}")
except Exception as e:
logger.warning(f" Could not list versions: {e}")
if not found_kicad:
logger.warning("No KiCAD installations found in standard locations!")
logger.warning("Please ensure KiCAD 9.0+ is installed from https://www.kicad.org/download/windows/")
logger.info("========================================")
# Add utils directory to path for imports
utils_dir = os.path.join(os.path.dirname(__file__))
@@ -66,10 +104,40 @@ try:
except ImportError as e:
logger.error(f"Failed to import pcbnew module: {e}")
logger.error(f"Current sys.path: {sys.path}")
# Platform-specific help message
help_message = ""
if sys.platform == 'win32':
help_message = """
Windows Troubleshooting:
1. Verify KiCAD is installed: C:\\Program Files\\KiCad\\9.0
2. Check PYTHONPATH environment variable points to:
C:\\Program Files\\KiCad\\9.0\\lib\\python3\\dist-packages
3. Test with: "C:\\Program Files\\KiCad\\9.0\\bin\\python.exe" -c "import pcbnew"
4. Log file location: %USERPROFILE%\\.kicad-mcp\\logs\\kicad_interface.log
5. Run setup-windows.ps1 for automatic configuration
"""
elif sys.platform == 'darwin':
help_message = """
macOS Troubleshooting:
1. Verify KiCAD is installed: /Applications/KiCad/KiCad.app
2. Check PYTHONPATH points to KiCAD's Python packages
3. Run: python3 -c "import pcbnew" to test
"""
else: # Linux
help_message = """
Linux Troubleshooting:
1. Verify KiCAD is installed: apt list --installed | grep kicad
2. Check: /usr/lib/kicad/lib/python3/dist-packages exists
3. Test: python3 -c "import pcbnew"
"""
logger.error(help_message)
error_response = {
"success": False,
"message": "Failed to import pcbnew module",
"errorDetails": f"Error: {str(e)}\nPython path: {sys.path}"
"message": "Failed to import pcbnew module - KiCAD Python API not found",
"errorDetails": f"Error: {str(e)}\n\n{help_message}\n\nPython sys.path:\n{chr(10).join(sys.path)}"
}
print(json.dumps(error_response))
sys.exit(1)
@@ -96,7 +164,8 @@ try:
from commands.schematic import SchematicManager
from commands.component_schematic import ComponentManager
from commands.connection_schematic import ConnectionManager
from commands.library_schematic import LibraryManager
from commands.library_schematic import LibraryManager as SchematicLibraryManager
from commands.library import LibraryManager as FootprintLibraryManager, LibraryCommands
logger.info("Successfully imported all command handlers")
except ImportError as e:
logger.error(f"Failed to import command handlers: {e}")
@@ -110,22 +179,26 @@ except ImportError as e:
class KiCADInterface:
"""Main interface class to handle KiCAD operations"""
def __init__(self):
"""Initialize the interface and command handlers"""
self.board = None
self.project_filename = None
logger.info("Initializing command handlers...")
# Initialize footprint library manager
self.footprint_library = FootprintLibraryManager()
# Initialize command handlers
self.project_commands = ProjectCommands(self.board)
self.board_commands = BoardCommands(self.board)
self.component_commands = ComponentCommands(self.board)
self.component_commands = ComponentCommands(self.board, self.footprint_library)
self.routing_commands = RoutingCommands(self.board)
self.design_rule_commands = DesignRuleCommands(self.board)
self.export_commands = ExportCommands(self.board)
self.library_commands = LibraryCommands(self.footprint_library)
# Schematic-related classes don't need board reference
# as they operate directly on schematic files
@@ -183,7 +256,13 @@ class KiCADInterface:
"export_svg": self.export_commands.export_svg,
"export_3d": self.export_commands.export_3d,
"export_bom": self.export_commands.export_bom,
# Library commands (footprint management)
"list_libraries": self.library_commands.list_libraries,
"search_footprints": self.library_commands.search_footprints,
"list_library_footprints": self.library_commands.list_library_footprints,
"get_footprint_info": self.library_commands.get_footprint_info,
# Schematic commands
"create_schematic": self._handle_create_schematic,
"load_schematic": self._handle_load_schematic,