""" Dynamic Symbol Loader for KiCad Schematics Loads symbols from .kicad_sym library files and injects them into schematics on-the-fly, eliminating the need for static templates. This enables access to all ~10,000+ KiCad symbols dynamically. """ import os import uuid import logging from pathlib import Path from typing import Dict, List, Optional, Tuple import sexpdata from sexpdata import Symbol logger = logging.getLogger('kicad_interface') class DynamicSymbolLoader: """ Dynamically loads symbols from KiCad library files and injects them into schematics Workflow: 1. Parse .kicad_sym library file to extract symbol definition 2. Inject symbol definition into schematic's lib_symbols section 3. Create an offscreen template instance that can be cloned 4. Clone the template to create actual component instances """ def __init__(self): """Initialize the dynamic symbol loader""" self.library_cache = {} # Cache parsed library files: path -> parsed data self.symbol_cache = {} # Cache extracted symbols: "lib:symbol" -> symbol_def def find_kicad_symbol_libraries(self) -> List[Path]: """ Find all KiCad symbol library directories Returns: List of paths to symbol library directories """ possible_paths = [ # Linux Path("/usr/share/kicad/symbols"), Path("/usr/local/share/kicad/symbols"), # Windows Path("C:/Program Files/KiCad/9.0/share/kicad/symbols"), Path("C:/Program Files/KiCad/8.0/share/kicad/symbols"), # macOS Path("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols"), # User libraries Path.home() / ".local" / "share" / "kicad" / "9.0" / "symbols", Path.home() / ".local" / "share" / "kicad" / "8.0" / "symbols", Path.home() / "Documents" / "KiCad" / "9.0" / "3rdparty" / "symbols", ] # Check environment variables for env_var in ['KICAD9_SYMBOL_DIR', 'KICAD8_SYMBOL_DIR', 'KICAD_SYMBOL_DIR']: if env_var in os.environ: possible_paths.insert(0, Path(os.environ[env_var])) found_paths = [] for path in possible_paths: if path.exists() and path.is_dir(): found_paths.append(path) logger.info(f"Found KiCad symbol library directory: {path}") return found_paths def find_library_file(self, library_name: str) -> Optional[Path]: """ Find the .kicad_sym file for a given library name Args: library_name: Library name (e.g., "Device", "Connector_Generic") Returns: Path to .kicad_sym file or None if not found """ library_dirs = self.find_kicad_symbol_libraries() for lib_dir in library_dirs: lib_file = lib_dir / f"{library_name}.kicad_sym" if lib_file.exists(): logger.debug(f"Found library file: {lib_file}") return lib_file logger.warning(f"Library file not found: {library_name}.kicad_sym") return None def parse_library_file(self, library_path: Path) -> List: """ Parse a .kicad_sym file into S-expression data structure Args: library_path: Path to .kicad_sym file Returns: Parsed S-expression data """ # Check cache first cache_key = str(library_path) if cache_key in self.library_cache: logger.debug(f"Using cached library data for: {library_path.name}") return self.library_cache[cache_key] logger.info(f"Parsing library file: {library_path}") try: with open(library_path, 'r', encoding='utf-8') as f: content = f.read() # Parse S-expression parsed = sexpdata.loads(content) # Cache the result self.library_cache[cache_key] = parsed logger.debug(f"Successfully parsed library: {library_path.name}") return parsed except Exception as e: logger.error(f"Error parsing library file {library_path}: {e}") raise def extract_symbol_definition(self, library_path: Path, symbol_name: str) -> Optional[List]: """ Extract a specific symbol definition from a library file Args: library_path: Path to .kicad_sym file symbol_name: Name of symbol to extract (e.g., "R", "LED") Returns: Symbol definition as S-expression list, or None if not found """ cache_key = f"{library_path.name}:{symbol_name}" if cache_key in self.symbol_cache: logger.debug(f"Using cached symbol: {cache_key}") return self.symbol_cache[cache_key] parsed_lib = self.parse_library_file(library_path) # Library structure: (kicad_symbol_lib (version ...) (generator ...) (symbol ...) (symbol ...) ...) # We need to find the symbol with matching name for item in parsed_lib: if isinstance(item, list) and len(item) > 0: if item[0] == Symbol('symbol'): # Symbol structure: (symbol "Name" ...) if len(item) > 1 and isinstance(item[1], str): # Handle both "Device:R" and "R" formats item_name = item[1] if ':' in item_name: item_name = item_name.split(':')[1] if item_name == symbol_name: logger.info(f"Found symbol definition: {symbol_name}") # Cache and return self.symbol_cache[cache_key] = item return item logger.warning(f"Symbol '{symbol_name}' not found in {library_path.name}") return None def inject_symbol_into_schematic(self, schematic_path: Path, library_name: str, symbol_name: str) -> bool: """ Inject a symbol definition from a library into a schematic file Args: schematic_path: Path to .kicad_sch file to modify library_name: Source library name (e.g., "Device") symbol_name: Symbol to inject (e.g., "R") Returns: True if successful, False otherwise """ try: # 1. Find and parse the library file library_path = self.find_library_file(library_name) if not library_path: raise ValueError(f"Library not found: {library_name}") # 2. Extract the symbol definition symbol_def = self.extract_symbol_definition(library_path, symbol_name) if not symbol_def: raise ValueError(f"Symbol '{symbol_name}' not found in library '{library_name}'") # 3. Read the schematic file with open(schematic_path, 'r', encoding='utf-8') as f: sch_content = f.read() sch_data = sexpdata.loads(sch_content) # 4. Find the lib_symbols section lib_symbols_index = None for i, item in enumerate(sch_data): if isinstance(item, list) and len(item) > 0 and item[0] == Symbol('lib_symbols'): lib_symbols_index = i break if lib_symbols_index is None: raise ValueError("No lib_symbols section found in schematic") # 5. Check if symbol already exists in lib_symbols full_symbol_name = f"{library_name}:{symbol_name}" symbol_exists = False for item in sch_data[lib_symbols_index][1:]: # Skip the 'lib_symbols' symbol itself if isinstance(item, list) and len(item) > 1 and item[0] == Symbol('symbol'): if item[1] == full_symbol_name or item[1] == symbol_name: logger.info(f"Symbol {full_symbol_name} already exists in schematic") symbol_exists = True break if not symbol_exists: # 6. Inject the symbol definition # Need to update the symbol name to include library prefix modified_symbol_def = list(symbol_def) # Make a copy modified_symbol_def[1] = full_symbol_name # Update name to "Library:Symbol" sch_data[lib_symbols_index].append(modified_symbol_def) logger.info(f"Injected symbol {full_symbol_name} into schematic") # 7. Write the modified schematic back with open(schematic_path, 'w', encoding='utf-8') as f: output = sexpdata.dumps(sch_data) f.write(output) logger.info(f"Successfully injected symbol {full_symbol_name} into {schematic_path.name}") return True except Exception as e: logger.error(f"Error injecting symbol into schematic: {e}") raise def create_template_instance(self, schematic_path: Path, library_name: str, symbol_name: str, template_ref: Optional[str] = None) -> str: """ Create an offscreen template instance of a symbol that can be cloned Args: schematic_path: Path to .kicad_sch file library_name: Library name (e.g., "Device") symbol_name: Symbol name (e.g., "R") template_ref: Optional custom reference (defaults to _TEMPLATE_{LIBRARY}_{SYMBOL}) Returns: Template reference name """ try: if template_ref is None: # Clean up library and symbol names for reference lib_clean = library_name.replace('-', '_').replace('.', '_') sym_clean = symbol_name.replace('-', '_').replace('.', '_') template_ref = f"_TEMPLATE_{lib_clean}_{sym_clean}" # Read schematic with open(schematic_path, 'r', encoding='utf-8') as f: sch_content = f.read() sch_data = sexpdata.loads(sch_content) # Check if template already exists for item in sch_data: if isinstance(item, list) and len(item) > 0 and item[0] == Symbol('symbol'): # Find Reference property for prop in item: if isinstance(prop, list) and len(prop) > 2 and prop[0] == Symbol('property'): if prop[1] == "Reference" and prop[2] == template_ref: logger.info(f"Template instance {template_ref} already exists") return template_ref # Find sheet_instances index (we'll insert before this) sheet_instances_index = None for i, item in enumerate(sch_data): if isinstance(item, list) and len(item) > 0 and item[0] == Symbol('sheet_instances'): sheet_instances_index = i break if sheet_instances_index is None: raise ValueError("No sheet_instances section found in schematic") # Create template symbol instance full_lib_id = f"{library_name}:{symbol_name}" # Calculate y position based on existing templates template_count = sum(1 for item in sch_data if isinstance(item, list) and len(item) > 0 and item[0] == Symbol('symbol') and any(isinstance(p, list) and len(p) > 2 and p[0] == Symbol('property') and p[1] == "Reference" and str(p[2]).startswith('_TEMPLATE') for p in item)) y_offset = -100 - (template_count * 10) new_uuid = str(uuid.uuid4()) # Build the symbol instance S-expression template_instance = [ Symbol('symbol'), [Symbol('lib_id'), full_lib_id], [Symbol('at'), -100, y_offset, 0], [Symbol('unit'), 1], [Symbol('in_bom'), Symbol('no')], [Symbol('on_board'), Symbol('no')], [Symbol('dnp'), Symbol('yes')], [Symbol('uuid'), new_uuid], [Symbol('property'), "Reference", template_ref, [Symbol('at'), -100, y_offset - 2.54, 0], [Symbol('effects'), [Symbol('font'), [Symbol('size'), 1.27, 1.27]]] ], [Symbol('property'), "Value", symbol_name, [Symbol('at'), -100, y_offset + 2.54, 0], [Symbol('effects'), [Symbol('font'), [Symbol('size'), 1.27, 1.27]]] ], [Symbol('property'), "Footprint", "", [Symbol('at'), -100, y_offset, 0], [Symbol('effects'), [Symbol('font'), [Symbol('size'), 1.27, 1.27]], Symbol('hide')] ], [Symbol('property'), "Datasheet", "~", [Symbol('at'), -100, y_offset, 0], [Symbol('effects'), [Symbol('font'), [Symbol('size'), 1.27, 1.27]], Symbol('hide')] ], ] # Insert before sheet_instances sch_data.insert(sheet_instances_index, template_instance) # Write back with open(schematic_path, 'w', encoding='utf-8') as f: output = sexpdata.dumps(sch_data) f.write(output) logger.info(f"Created template instance: {template_ref} at y={y_offset}") return template_ref except Exception as e: logger.error(f"Error creating template instance: {e}") raise def load_symbol_dynamically(self, schematic_path: Path, library_name: str, symbol_name: str) -> str: """ Complete workflow: inject symbol and create template instance Args: schematic_path: Path to .kicad_sch file library_name: Library name (e.g., "Device") symbol_name: Symbol name (e.g., "R") Returns: Template reference that can be used with kicad-skip clone() """ logger.info(f"Loading symbol dynamically: {library_name}:{symbol_name}") # Step 1: Inject symbol definition into lib_symbols self.inject_symbol_into_schematic(schematic_path, library_name, symbol_name) # Step 2: Create template instance template_ref = self.create_template_instance(schematic_path, library_name, symbol_name) logger.info(f"Symbol loaded successfully. Template reference: {template_ref}") return template_ref if __name__ == '__main__': # Test the dynamic symbol loader logging.basicConfig(level=logging.INFO) loader = DynamicSymbolLoader() print("\n=== Testing Dynamic Symbol Loader ===\n") # Test 1: Find library directories print("1. Finding KiCad symbol library directories...") lib_dirs = loader.find_kicad_symbol_libraries() print(f" Found {len(lib_dirs)} directories:") for lib_dir in lib_dirs: print(f" - {lib_dir}") # Test 2: Find Device library print("\n2. Finding Device.kicad_sym library file...") device_lib = loader.find_library_file("Device") if device_lib: print(f" ✓ Found: {device_lib}") else: print(" ✗ Not found") exit(1) # Test 3: Parse library file print("\n3. Parsing Device.kicad_sym...") parsed = loader.parse_library_file(device_lib) print(f" ✓ Parsed successfully ({len(parsed)} top-level items)") # Test 4: Extract specific symbols print("\n4. Extracting symbol definitions...") for symbol in ['R', 'C', 'LED']: symbol_def = loader.extract_symbol_definition(device_lib, symbol) if symbol_def: print(f" ✓ Extracted: {symbol}") else: print(f" ✗ Failed: {symbol}") print("\n✓ All basic tests passed!")