Skip to main content
Glama
library.py16.4 kB
""" 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) }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mixelpixx/KiCAD-MCP-Server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server