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:
@@ -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
450
python/commands/library.py
Normal 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)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user