Files
kicad-mcp-server/python/commands/component_schematic.py
KiCAD MCP Bot 4a543313eb feat: Expand schematic component support from 3 to 13 types + plan dynamic loading
Issue #26 Follow-up: The original fix only supported 3 component types (R, C, LED).
This expands the template-based approach to 13 types while planning for unlimited access.

**Expanded Template (Option 1 - Immediate Solution):**
- Added 10 new component types to template
- Passives: R, C, L, Crystal (4 types)
- Semiconductors: D, LED, Q_NPN, Q_NMOS (4 types)
- ICs: OpAmp, Voltage Regulator (2 types)
- Connectors: Conn_2pin, Conn_4pin (2 types)
- Misc: Switch/Button (1 type)

**Components Now Supported:**
1. Resistor (R)
2. Capacitor (C)
3. Inductor (L)
4. Crystal (Y)
5. Diode (D)
6. LED
7. NPN Transistor (Q_NPN)
8. N-Channel MOSFET (Q_NMOS)
9. Op-Amp (U)
10. Voltage Regulator (U_REG)
11. 2-pin Connector (J2)
12. 4-pin Connector (J4)
13. Push Button/Switch (SW)

**Implementation:**
- Created template_with_symbols_expanded.kicad_sch with all 13 types
- Updated TEMPLATE_MAP with comprehensive type mappings
- Updated project.py to use expanded template by default
- All tests passing (13/13 components added successfully)

**Future Plan (Option 2 - Dynamic Library Loading):**
- Documented comprehensive plan for accessing ALL KiCad symbols (~10,000+)
- Dynamic loading from .kicad_sym library files
- S-expression injection approach
- 6-8 week implementation timeline
- Maintains template fallback for compatibility

**Files Added:**
- python/templates/template_with_symbols_expanded.kicad_sch
- docs/DYNAMIC_LIBRARY_LOADING_PLAN.md

**Files Modified:**
- python/commands/component_schematic.py (expanded TEMPLATE_MAP)
- python/commands/project.py (use expanded template)

This provides immediate value (13 types vs 3) while we plan the long-term
solution of unlimited symbol access through dynamic library loading.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 09:54:51 -05:00

236 lines
9.7 KiB
Python

from skip import Schematic
import os
import uuid
import logging
logger = logging.getLogger(__name__)
class ComponentManager:
"""Manage components in a schematic"""
# 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',
}
@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')
template_ref = ComponentManager.TEMPLATE_MAP.get(comp_type, '_TEMPLATE_R')
# 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")