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:
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)
|
||||
}
|
||||
Reference in New Issue
Block a user