from skip import Schematic import os import uuid import logging from pathlib import Path from typing import Optional logger = logging.getLogger(__name__) # Import dynamic symbol loader try: from commands.dynamic_symbol_loader import DynamicSymbolLoader DYNAMIC_LOADING_AVAILABLE = True except ImportError: logger.warning("Dynamic symbol loader not available - falling back to template-only mode") DYNAMIC_LOADING_AVAILABLE = False class ComponentManager: """Manage components in a schematic""" # Initialize dynamic loader (class variable, shared across instances) _dynamic_loader = None @classmethod def get_dynamic_loader(cls): """Get or create dynamic symbol loader instance""" if cls._dynamic_loader is None and DYNAMIC_LOADING_AVAILABLE: cls._dynamic_loader = DynamicSymbolLoader() return cls._dynamic_loader # Template symbol references mapping component type to template reference TEMPLATE_MAP = { # Passives 'R': '_TEMPLATE_R', 'C': '_TEMPLATE_C', 'L': '_TEMPLATE_L', 'Y': '_TEMPLATE_Y', 'Crystal': '_TEMPLATE_Y', # Semiconductors 'D': '_TEMPLATE_D', 'LED': '_TEMPLATE_LED', 'Q': '_TEMPLATE_Q_NPN', 'Q_NPN': '_TEMPLATE_Q_NPN', 'Q_NMOS': '_TEMPLATE_Q_NMOS', 'MOSFET': '_TEMPLATE_Q_NMOS', # ICs 'U': '_TEMPLATE_U_OPAMP', 'OpAmp': '_TEMPLATE_U_OPAMP', 'IC': '_TEMPLATE_U_OPAMP', 'U_REG': '_TEMPLATE_U_REG', 'Regulator': '_TEMPLATE_U_REG', # Connectors 'J': '_TEMPLATE_J2', 'J2': '_TEMPLATE_J2', 'J4': '_TEMPLATE_J4', 'Conn_2': '_TEMPLATE_J2', 'Conn_4': '_TEMPLATE_J4', # Misc 'SW': '_TEMPLATE_SW', 'Button': '_TEMPLATE_SW', 'Switch': '_TEMPLATE_SW', } @classmethod def get_or_create_template(cls, schematic: Schematic, comp_type: str, library: Optional[str] = None) -> str: """ Get template reference for a component type, creating it dynamically if needed Args: schematic: Schematic object comp_type: Component type (e.g., 'R', 'LED', 'STM32F103C8Tx') library: Optional library name (defaults to 'Device' for common types) Returns: Template reference string (e.g., '_TEMPLATE_R' or '_TEMPLATE_Device_R') """ # 1. Check static template map first if comp_type in cls.TEMPLATE_MAP: template_ref = cls.TEMPLATE_MAP[comp_type] # Verify template exists in schematic if hasattr(schematic.symbol, template_ref): logger.debug(f"Using static template: {template_ref}") return template_ref # 2. Try dynamic loading if not DYNAMIC_LOADING_AVAILABLE: logger.warning(f"Component type '{comp_type}' not in static templates and dynamic loading unavailable") # Fall back to basic resistor template return '_TEMPLATE_R' loader = cls.get_dynamic_loader() if not loader: logger.warning("Dynamic loader unavailable, using fallback template") return '_TEMPLATE_R' # Determine library name if library is None: # Default library for common component types library = 'Device' # Most passives and basic components are in Device library try: # Get schematic file path # kicad-skip doesn't expose the file path directly, so we need to work around this # For now, we'll need the caller to pass the schematic path # TODO: Store schematic path in Schematic object or pass it separately logger.info(f"Attempting dynamic load: {library}:{comp_type}") # This is a limitation - we need the schematic file path # For now, return a fallback logger.warning("Dynamic loading requires schematic file path - feature not fully integrated yet") return cls.TEMPLATE_MAP.get(comp_type, '_TEMPLATE_R') except Exception as e: logger.error(f"Dynamic loading failed: {e}") return cls.TEMPLATE_MAP.get(comp_type, '_TEMPLATE_R') @staticmethod def add_component(schematic: Schematic, component_def: dict): """Add a component to the schematic by cloning from template""" try: logger.info(f"Adding component: type={component_def.get('type')}, ref={component_def.get('reference')}") logger.debug(f"Full component_def: {component_def}") # Get component type and determine template comp_type = component_def.get('type', 'R') library = component_def.get('library', None) # Optional library specification # Get template reference (static or dynamic) template_ref = ComponentManager.get_or_create_template(schematic, comp_type, library) # Check if schematic has template symbols if not hasattr(schematic.symbol, template_ref): logger.error(f"Template symbol {template_ref} not found in schematic. Available symbols: {[str(s.property.Reference.value) for s in schematic.symbol]}") raise ValueError(f"Template symbol {template_ref} not found. The schematic must be created from template_with_symbols.kicad_sch") # Get template symbol and clone it template_symbol = getattr(schematic.symbol, template_ref) new_symbol = template_symbol.clone() logger.debug(f"Cloned template symbol {template_ref}") # Set reference reference = component_def.get('reference', 'R?') new_symbol.property.Reference.value = reference logger.debug(f"Set reference to {reference}") # Set value if 'value' in component_def: new_symbol.property.Value.value = component_def['value'] logger.debug(f"Set value to {component_def['value']}") # Set footprint if 'footprint' in component_def: new_symbol.property.Footprint.value = component_def['footprint'] logger.debug(f"Set footprint to {component_def['footprint']}") # Set datasheet if 'datasheet' in component_def: new_symbol.property.Datasheet.value = component_def['datasheet'] # Set position x = component_def.get('x', 0) y = component_def.get('y', 0) rotation = component_def.get('rotation', 0) new_symbol.at.value = [x, y, rotation] logger.debug(f"Set position to ({x}, {y}, {rotation})") # Set BOM and board flags new_symbol.in_bom.value = component_def.get('in_bom', True) new_symbol.on_board.value = component_def.get('on_board', True) new_symbol.dnp.value = component_def.get('dnp', False) # Generate new UUID new_symbol.uuid.value = str(uuid.uuid4()) # Append to schematic schematic.symbol.append(new_symbol) logger.info(f"Successfully added component {reference} to schematic") return new_symbol except Exception as e: logger.error(f"Error adding component: {e}", exc_info=True) raise @staticmethod def remove_component(schematic: Schematic, component_ref: str): """Remove a component from the schematic by reference designator""" try: # kicad-skip doesn't have a direct remove_symbol method by reference. # We need to find the symbol and then remove it from the symbols list. symbol_to_remove = None for symbol in schematic.symbol: if symbol.reference == component_ref: symbol_to_remove = symbol break if symbol_to_remove: schematic.symbol.remove(symbol_to_remove) print(f"Removed component {component_ref} from schematic.") return True else: print(f"Component with reference {component_ref} not found.") return False except Exception as e: print(f"Error removing component {component_ref}: {e}") return False @staticmethod def update_component(schematic: Schematic, component_ref: str, new_properties: dict): """Update component properties by reference designator""" try: symbol_to_update = None for symbol in schematic.symbol: if symbol.reference == component_ref: symbol_to_update = symbol break if symbol_to_update: for key, value in new_properties.items(): if key in symbol_to_update.property: symbol_to_update.property[key].value = value else: # Add as a new property if it doesn't exist symbol_to_update.property.append(key, value) print(f"Updated properties for component {component_ref}.") return True else: print(f"Component with reference {component_ref} not found.") return False except Exception as e: print(f"Error updating component {component_ref}: {e}") return False @staticmethod def get_component(schematic: Schematic, component_ref: str): """Get a component by reference designator""" for symbol in schematic.symbol: if symbol.reference == component_ref: print(f"Found component with reference {component_ref}.") return symbol print(f"Component with reference {component_ref} not found.") return None @staticmethod def search_components(schematic: Schematic, query: str): """Search for components matching criteria (basic implementation)""" # This is a basic search, could be expanded to use regex or more complex logic matching_components = [] query_lower = query.lower() for symbol in schematic.symbol: if query_lower in symbol.reference.lower() or \ query_lower in symbol.name.lower() or \ (hasattr(symbol.property, 'Value') and query_lower in symbol.property.Value.value.lower()): matching_components.append(symbol) print(f"Found {len(matching_components)} components matching query '{query}'.") return matching_components @staticmethod def get_all_components(schematic: Schematic): """Get all components in schematic""" print(f"Retrieving all {len(schematic.symbol)} components.") return list(schematic.symbol) if __name__ == '__main__': # Example Usage (for testing) from schematic import SchematicManager # Assuming schematic.py is in the same directory # Create a new schematic test_sch = SchematicManager.create_schematic("ComponentTestSchematic") # Add components comp1_def = {"type": "R", "reference": "R1", "value": "10k", "x": 100, "y": 100} comp2_def = {"type": "C", "reference": "C1", "value": "0.1uF", "x": 200, "y": 100, "library": "Device"} comp3_def = {"type": "LED", "reference": "D1", "x": 300, "y": 100, "library": "Device", "properties": {"Color": "Red"}} comp1 = ComponentManager.add_component(test_sch, comp1_def) comp2 = ComponentManager.add_component(test_sch, comp2_def) comp3 = ComponentManager.add_component(test_sch, comp3_def) # Get a component retrieved_comp = ComponentManager.get_component(test_sch, "C1") if retrieved_comp: print(f"Retrieved component: {retrieved_comp.reference} ({retrieved_comp.value})") # Update a component ComponentManager.update_component(test_sch, "R1", {"value": "20k", "Tolerance": "5%"}) # Search components matching_comps = ComponentManager.search_components(test_sch, "100") # Search by position print(f"Search results for '100': {[c.reference for c in matching_comps]}") # Get all components all_comps = ComponentManager.get_all_components(test_sch) print(f"All components: {[c.reference for c in all_comps]}") # Remove a component ComponentManager.remove_component(test_sch, "D1") all_comps_after_remove = ComponentManager.get_all_components(test_sch) print(f"Components after removing D1: {[c.reference for c in all_comps_after_remove]}") # Save the schematic (optional) # SchematicManager.save_schematic(test_sch, "component_test.kicad_sch") # Clean up (if saved) # if os.path.exists("component_test.kicad_sch"): # os.remove("component_test.kicad_sch") # print("Cleaned up component_test.kicad_sch")