fix: Rewrite DynamicSymbolLoader to prevent schematic file corruption (#40)
Fixes Issue #26 - add_schematic_component corrupts .kicad_sch files Complete rewrite of DynamicSymbolLoader to use text manipulation instead of sexpdata: - Preserves KiCAD file formatting perfectly - Correctly handles KiCAD 9 symbol naming conventions (library prefix for top-level, no prefix for sub-symbols) - Resolves parent-child symbol dependencies with (extends) - Adds symbol caching for performance - Simplifies component addition workflow Tested on KiCAD 9.0.7 with R, C, LED, ESP32, and transistor components. Compatible with recent Windows fixes (UUID generation, IPC, JLCPCB). Co-authored-by: FlowSync0 <249798311+FlowSync0@users.noreply.github.com>
This commit is contained in:
@@ -2,403 +2,345 @@
|
|||||||
Dynamic Symbol Loader for KiCad Schematics
|
Dynamic Symbol Loader for KiCad Schematics
|
||||||
|
|
||||||
Loads symbols from .kicad_sym library files and injects them into schematics
|
Loads symbols from .kicad_sym library files and injects them into schematics
|
||||||
on-the-fly, eliminating the need for static templates.
|
on-the-fly using TEXT MANIPULATION (not sexpdata) to preserve file formatting.
|
||||||
|
|
||||||
This enables access to all ~10,000+ KiCad symbols dynamically.
|
This enables access to all ~10,000+ KiCad symbols dynamically.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
import sexpdata
|
|
||||||
from sexpdata import Symbol
|
|
||||||
|
|
||||||
logger = logging.getLogger('kicad_interface')
|
logger = logging.getLogger('kicad_interface')
|
||||||
|
|
||||||
|
|
||||||
class DynamicSymbolLoader:
|
class DynamicSymbolLoader:
|
||||||
"""
|
"""
|
||||||
Dynamically loads symbols from KiCad library files and injects them into schematics
|
Dynamically loads symbols from KiCad library files and injects them into schematics.
|
||||||
|
|
||||||
Workflow:
|
Uses raw text manipulation instead of sexpdata to avoid corrupting the KiCad file format.
|
||||||
1. Parse .kicad_sym library file to extract symbol definition
|
|
||||||
2. Inject symbol definition into schematic's lib_symbols section
|
Key rules for KiCad 9 .kicad_sch format:
|
||||||
3. Create an offscreen template instance that can be cloned
|
- Top-level symbols in lib_symbols must have library prefix: (symbol "Device:R" ...)
|
||||||
4. Clone the template to create actual component instances
|
- Sub-symbols must NOT have library prefix: (symbol "R_0_1" ...), (symbol "R_1_1" ...)
|
||||||
|
- Parent symbols must appear BEFORE child symbols that use (extends ...)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initialize the dynamic symbol loader"""
|
self.symbol_cache = {} # Cache: "lib:symbol" -> raw text block
|
||||||
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]:
|
def find_kicad_symbol_libraries(self) -> List[Path]:
|
||||||
"""
|
"""Find all KiCad symbol library directories"""
|
||||||
Find all KiCad symbol library directories
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of paths to symbol library directories
|
|
||||||
"""
|
|
||||||
possible_paths = [
|
possible_paths = [
|
||||||
# Linux
|
|
||||||
Path("/usr/share/kicad/symbols"),
|
Path("/usr/share/kicad/symbols"),
|
||||||
Path("/usr/local/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/9.0/share/kicad/symbols"),
|
||||||
Path("C:/Program Files/KiCad/8.0/share/kicad/symbols"),
|
Path("C:/Program Files/KiCad/8.0/share/kicad/symbols"),
|
||||||
# macOS
|
|
||||||
Path("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols"),
|
Path("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols"),
|
||||||
# User libraries
|
|
||||||
Path.home() / ".local" / "share" / "kicad" / "9.0" / "symbols",
|
Path.home() / ".local" / "share" / "kicad" / "9.0" / "symbols",
|
||||||
Path.home() / ".local" / "share" / "kicad" / "8.0" / "symbols",
|
|
||||||
Path.home() / "Documents" / "KiCad" / "9.0" / "3rdparty" / "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']:
|
for env_var in ['KICAD9_SYMBOL_DIR', 'KICAD8_SYMBOL_DIR', 'KICAD_SYMBOL_DIR']:
|
||||||
if env_var in os.environ:
|
if env_var in os.environ:
|
||||||
possible_paths.insert(0, Path(os.environ[env_var]))
|
possible_paths.insert(0, Path(os.environ[env_var]))
|
||||||
|
|
||||||
found_paths = []
|
return [p for p in possible_paths if p.exists() and p.is_dir()]
|
||||||
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]:
|
def find_library_file(self, library_name: str) -> Optional[Path]:
|
||||||
"""
|
"""Find the .kicad_sym file for a given library name"""
|
||||||
Find the .kicad_sym file for a given library name
|
for lib_dir in self.find_kicad_symbol_libraries():
|
||||||
|
|
||||||
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"
|
lib_file = lib_dir / f"{library_name}.kicad_sym"
|
||||||
if lib_file.exists():
|
if lib_file.exists():
|
||||||
logger.debug(f"Found library file: {lib_file}")
|
|
||||||
return lib_file
|
return lib_file
|
||||||
|
|
||||||
logger.warning(f"Library file not found: {library_name}.kicad_sym")
|
logger.warning(f"Library file not found: {library_name}.kicad_sym")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def parse_library_file(self, library_path: Path) -> List:
|
def _extract_symbol_block(self, text: str, symbol_name: str) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Parse a .kicad_sym file into S-expression data structure
|
Extract a complete symbol block from a library or schematic file by matching
|
||||||
|
parentheses depth. Returns the raw text of the symbol definition.
|
||||||
Args:
|
|
||||||
library_path: Path to .kicad_sym file
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Parsed S-expression data
|
|
||||||
"""
|
"""
|
||||||
# Check cache first
|
lines = text.split('\n')
|
||||||
cache_key = str(library_path)
|
start = None
|
||||||
if cache_key in self.library_cache:
|
for i, line in enumerate(lines):
|
||||||
logger.debug(f"Using cached library data for: {library_path.name}")
|
stripped = line.strip()
|
||||||
return self.library_cache[cache_key]
|
# Match exact symbol name (not sub-symbols like Name_0_1)
|
||||||
|
if stripped.startswith(f'(symbol "{symbol_name}"') and \
|
||||||
|
not re.match(r'.*_\d+_\d+"', stripped):
|
||||||
|
start = i
|
||||||
|
break
|
||||||
|
|
||||||
logger.info(f"Parsing library file: {library_path}")
|
if start is None:
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
depth = 0
|
||||||
with open(library_path, 'r', encoding='utf-8') as f:
|
end = None
|
||||||
content = f.read()
|
for i in range(start, len(lines)):
|
||||||
|
for ch in lines[i]:
|
||||||
|
if ch == '(':
|
||||||
|
depth += 1
|
||||||
|
elif ch == ')':
|
||||||
|
depth -= 1
|
||||||
|
if depth == 0:
|
||||||
|
end = i
|
||||||
|
break
|
||||||
|
if end is not None:
|
||||||
|
break
|
||||||
|
|
||||||
# Parse S-expression
|
if end is None:
|
||||||
parsed = sexpdata.loads(content)
|
return None
|
||||||
|
|
||||||
# Cache the result
|
return '\n'.join(lines[start:end + 1])
|
||||||
self.library_cache[cache_key] = parsed
|
|
||||||
|
|
||||||
logger.debug(f"Successfully parsed library: {library_path.name}")
|
def extract_symbol_from_library(self, library_name: str, symbol_name: str) -> Optional[str]:
|
||||||
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
|
Extract a symbol definition from a KiCad .kicad_sym library file.
|
||||||
|
Returns the raw text block, ready to be injected into a schematic.
|
||||||
|
|
||||||
Args:
|
The returned block has:
|
||||||
library_path: Path to .kicad_sym file
|
- Top-level name prefixed with library: (symbol "Library:Name" ...)
|
||||||
symbol_name: Name of symbol to extract (e.g., "R", "LED")
|
- Sub-symbol names WITHOUT prefix: (symbol "Name_0_1" ...)
|
||||||
|
|
||||||
Returns:
|
|
||||||
Symbol definition as S-expression list, or None if not found
|
|
||||||
"""
|
"""
|
||||||
cache_key = f"{library_path.name}:{symbol_name}"
|
cache_key = f"{library_name}:{symbol_name}"
|
||||||
if cache_key in self.symbol_cache:
|
if cache_key in self.symbol_cache:
|
||||||
logger.debug(f"Using cached symbol: {cache_key}")
|
|
||||||
return self.symbol_cache[cache_key]
|
return self.symbol_cache[cache_key]
|
||||||
|
|
||||||
parsed_lib = self.parse_library_file(library_path)
|
lib_path = self.find_library_file(library_name)
|
||||||
|
if not lib_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
|
return None
|
||||||
|
|
||||||
|
with open(lib_path, 'r', encoding='utf-8') as f:
|
||||||
|
lib_content = f.read()
|
||||||
|
|
||||||
|
block = self._extract_symbol_block(lib_content, symbol_name)
|
||||||
|
if block is None:
|
||||||
|
logger.warning(f"Symbol '{symbol_name}' not found in {library_name}.kicad_sym")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Check if this symbol uses (extends "ParentName")
|
||||||
|
extends_match = re.search(r'\(extends "([^"]+)"\)', block)
|
||||||
|
parent_block = None
|
||||||
|
if extends_match:
|
||||||
|
parent_name = extends_match.group(1)
|
||||||
|
logger.info(f"Symbol {symbol_name} extends {parent_name}, extracting parent too")
|
||||||
|
parent_block = self._extract_symbol_block(lib_content, parent_name)
|
||||||
|
if parent_block:
|
||||||
|
# Prefix parent top-level name with library
|
||||||
|
parent_block = parent_block.replace(
|
||||||
|
f'(symbol "{parent_name}"',
|
||||||
|
f'(symbol "{library_name}:{parent_name}"',
|
||||||
|
1 # Only first occurrence (top-level)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prefix top-level symbol name with library
|
||||||
|
full_name = f"{library_name}:{symbol_name}"
|
||||||
|
block = block.replace(
|
||||||
|
f'(symbol "{symbol_name}"',
|
||||||
|
f'(symbol "{full_name}"',
|
||||||
|
1 # Only first occurrence (top-level)
|
||||||
|
)
|
||||||
|
# Sub-symbols like "Name_0_1" keep their short names (already correct from library)
|
||||||
|
|
||||||
|
# Combine parent + child if extends is used
|
||||||
|
if parent_block:
|
||||||
|
result = parent_block + '\n' + block
|
||||||
|
else:
|
||||||
|
result = block
|
||||||
|
|
||||||
|
self.symbol_cache[cache_key] = result
|
||||||
|
logger.info(f"Extracted symbol {full_name} ({len(result)} chars)")
|
||||||
|
return result
|
||||||
|
|
||||||
def inject_symbol_into_schematic(self, schematic_path: Path, library_name: str, symbol_name: str) -> bool:
|
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
|
Inject a symbol definition into a schematic's lib_symbols section.
|
||||||
|
Uses text manipulation to preserve file formatting.
|
||||||
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:
|
full_name = f"{library_name}:{symbol_name}"
|
||||||
# 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:
|
with open(schematic_path, 'r', encoding='utf-8') as f:
|
||||||
sch_content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
sch_data = sexpdata.loads(sch_content)
|
# Check if symbol already exists
|
||||||
|
if f'(symbol "{full_name}"' in content:
|
||||||
# 4. Find the lib_symbols section
|
logger.info(f"Symbol {full_name} already exists in schematic")
|
||||||
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
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
# Extract symbol from library
|
||||||
logger.error(f"Error injecting symbol into schematic: {e}")
|
symbol_block = self.extract_symbol_from_library(library_name, symbol_name)
|
||||||
raise
|
if not symbol_block:
|
||||||
|
raise ValueError(f"Symbol '{symbol_name}' not found in library '{library_name}'")
|
||||||
|
|
||||||
def create_template_instance(self, schematic_path: Path, library_name: str, symbol_name: str,
|
# Indent the block to match lib_symbols indentation (4 spaces for top-level)
|
||||||
template_ref: Optional[str] = None) -> str:
|
indented_lines = []
|
||||||
"""
|
for line in symbol_block.split('\n'):
|
||||||
Create an offscreen template instance of a symbol that can be cloned
|
# Add 4-space indent for the content inside lib_symbols
|
||||||
|
indented_lines.append(' ' + line if line.strip() else line)
|
||||||
|
indented_block = '\n'.join(indented_lines)
|
||||||
|
|
||||||
Args:
|
# Find the end of lib_symbols section to insert before closing )
|
||||||
schematic_path: Path to .kicad_sch file
|
lines = content.split('\n')
|
||||||
library_name: Library name (e.g., "Device")
|
lib_sym_start = None
|
||||||
symbol_name: Symbol name (e.g., "R")
|
lib_sym_end = None
|
||||||
template_ref: Optional custom reference (defaults to _TEMPLATE_{LIBRARY}_{SYMBOL})
|
depth = 0
|
||||||
|
|
||||||
Returns:
|
for i, line in enumerate(lines):
|
||||||
Template reference name
|
if '(lib_symbols' in line and lib_sym_start is None:
|
||||||
"""
|
lib_sym_start = i
|
||||||
try:
|
depth = 0
|
||||||
if template_ref is None:
|
for ch in line:
|
||||||
# Clean up library and symbol names for reference
|
if ch == '(':
|
||||||
lib_clean = library_name.replace('-', '_').replace('.', '_')
|
depth += 1
|
||||||
sym_clean = symbol_name.replace('-', '_').replace('.', '_')
|
elif ch == ')':
|
||||||
template_ref = f"_TEMPLATE_{lib_clean}_{sym_clean}"
|
depth -= 1
|
||||||
|
continue
|
||||||
# Read schematic
|
if lib_sym_start is not None and lib_sym_end is None:
|
||||||
with open(schematic_path, 'r', encoding='utf-8') as f:
|
for ch in line:
|
||||||
sch_content = f.read()
|
if ch == '(':
|
||||||
|
depth += 1
|
||||||
sch_data = sexpdata.loads(sch_content)
|
elif ch == ')':
|
||||||
|
depth -= 1
|
||||||
# Check if template already exists
|
if depth == 0:
|
||||||
for item in sch_data:
|
lib_sym_end = i
|
||||||
if isinstance(item, list) and len(item) > 0 and item[0] == Symbol('symbol'):
|
break
|
||||||
# Find Reference property
|
if lib_sym_end is not None:
|
||||||
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
|
break
|
||||||
|
|
||||||
if sheet_instances_index is None:
|
if lib_sym_end is None:
|
||||||
raise ValueError("No sheet_instances section found in schematic")
|
raise ValueError("No lib_symbols section found in schematic")
|
||||||
|
|
||||||
# Create template symbol instance
|
# Insert the symbol block just before the closing ) of lib_symbols
|
||||||
|
lines.insert(lib_sym_end, indented_block)
|
||||||
|
|
||||||
|
with open(schematic_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write('\n'.join(lines))
|
||||||
|
|
||||||
|
logger.info(f"Injected symbol {full_name} into {schematic_path.name}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def create_component_instance(self, schematic_path: Path, library_name: str,
|
||||||
|
symbol_name: str, reference: str,
|
||||||
|
value: str = "", x: float = 0, y: float = 0) -> bool:
|
||||||
|
"""
|
||||||
|
Add a component instance to the schematic.
|
||||||
|
This creates the (symbol ...) block with lib_id reference.
|
||||||
|
"""
|
||||||
full_lib_id = f"{library_name}:{symbol_name}"
|
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())
|
new_uuid = str(uuid.uuid4())
|
||||||
|
|
||||||
# Build the symbol instance S-expression
|
instance_block = f''' (symbol (lib_id "{full_lib_id}") (at {x} {y} 0) (unit 1)
|
||||||
template_instance = [
|
(in_bom yes) (on_board yes) (dnp no)
|
||||||
Symbol('symbol'),
|
(uuid "{new_uuid}")
|
||||||
[Symbol('lib_id'), full_lib_id],
|
(property "Reference" "{reference}" (at {x} {y - 2.54} 0)
|
||||||
[Symbol('at'), -100, y_offset, 0],
|
(effects (font (size 1.27 1.27)))
|
||||||
[Symbol('unit'), 1],
|
)
|
||||||
[Symbol('in_bom'), Symbol('no')],
|
(property "Value" "{value or symbol_name}" (at {x} {y + 2.54} 0)
|
||||||
[Symbol('on_board'), Symbol('no')],
|
(effects (font (size 1.27 1.27)))
|
||||||
[Symbol('dnp'), Symbol('yes')],
|
)
|
||||||
[Symbol('uuid'), new_uuid],
|
(property "Footprint" "" (at {x} {y} 0)
|
||||||
[Symbol('property'), "Reference", template_ref,
|
(effects (font (size 1.27 1.27)) (hide yes))
|
||||||
[Symbol('at'), -100, y_offset - 2.54, 0],
|
)
|
||||||
[Symbol('effects'), [Symbol('font'), [Symbol('size'), 1.27, 1.27]]]
|
(property "Datasheet" "~" (at {x} {y} 0)
|
||||||
],
|
(effects (font (size 1.27 1.27)) (hide yes))
|
||||||
[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
|
with open(schematic_path, 'r', encoding='utf-8') as f:
|
||||||
sch_data.insert(sheet_instances_index, template_instance)
|
content = f.read()
|
||||||
|
|
||||||
|
# Insert before (sheet_instances or at end before final )
|
||||||
|
lines = content.split('\n')
|
||||||
|
insert_pos = None
|
||||||
|
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if '(sheet_instances' in line:
|
||||||
|
insert_pos = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if insert_pos is None:
|
||||||
|
# Insert before the last closing parenthesis
|
||||||
|
for i in range(len(lines) - 1, -1, -1):
|
||||||
|
if lines[i].strip() == ')':
|
||||||
|
insert_pos = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if insert_pos is None:
|
||||||
|
raise ValueError("Could not find insertion point in schematic")
|
||||||
|
|
||||||
|
lines.insert(insert_pos, instance_block)
|
||||||
|
|
||||||
# Write back
|
|
||||||
with open(schematic_path, 'w', encoding='utf-8') as f:
|
with open(schematic_path, 'w', encoding='utf-8') as f:
|
||||||
output = sexpdata.dumps(sch_data)
|
f.write('\n'.join(lines))
|
||||||
f.write(output)
|
|
||||||
|
|
||||||
logger.info(f"Created template instance: {template_ref} at y={y_offset}")
|
logger.info(f"Added component instance {reference} ({full_lib_id}) at ({x}, {y})")
|
||||||
return template_ref
|
return True
|
||||||
|
|
||||||
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:
|
def load_symbol_dynamically(self, schematic_path: Path, library_name: str, symbol_name: str) -> str:
|
||||||
"""
|
"""
|
||||||
Complete workflow: inject symbol and create template instance
|
Complete workflow: inject symbol definition and create a template instance.
|
||||||
|
Returns a template reference name.
|
||||||
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}")
|
logger.info(f"Loading symbol dynamically: {library_name}:{symbol_name}")
|
||||||
|
|
||||||
# Step 1: Inject symbol definition into lib_symbols
|
# Step 1: Inject symbol definition into lib_symbols
|
||||||
self.inject_symbol_into_schematic(schematic_path, library_name, symbol_name)
|
self.inject_symbol_into_schematic(schematic_path, library_name, symbol_name)
|
||||||
|
|
||||||
# Step 2: Create template instance
|
# Step 2: Create an offscreen template instance
|
||||||
template_ref = self.create_template_instance(schematic_path, library_name, symbol_name)
|
lib_clean = library_name.replace('-', '_').replace('.', '_')
|
||||||
|
sym_clean = symbol_name.replace('-', '_').replace('.', '_')
|
||||||
|
template_ref = f"_TEMPLATE_{lib_clean}_{sym_clean}"
|
||||||
|
|
||||||
logger.info(f"Symbol loaded successfully. Template reference: {template_ref}")
|
self.create_component_instance(
|
||||||
|
schematic_path, library_name, symbol_name,
|
||||||
|
reference=template_ref, value=symbol_name,
|
||||||
|
x=-200, y=-200
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Symbol loaded. Template reference: {template_ref}")
|
||||||
return template_ref
|
return template_ref
|
||||||
|
|
||||||
|
def add_component(self, schematic_path: Path, library_name: str, symbol_name: str,
|
||||||
|
reference: str, value: str = "", x: float = 0, y: float = 0) -> bool:
|
||||||
|
"""
|
||||||
|
High-level: ensure symbol definition exists in schematic, then add an instance.
|
||||||
|
This is the main entry point for adding components.
|
||||||
|
"""
|
||||||
|
# Ensure symbol definition is in lib_symbols
|
||||||
|
self.inject_symbol_into_schematic(schematic_path, library_name, symbol_name)
|
||||||
|
|
||||||
|
# Add the component instance
|
||||||
|
return self.create_component_instance(
|
||||||
|
schematic_path, library_name, symbol_name,
|
||||||
|
reference=reference, value=value, x=x, y=y
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Test the dynamic symbol loader
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
loader = DynamicSymbolLoader()
|
loader = DynamicSymbolLoader()
|
||||||
|
|
||||||
print("\n=== Testing Dynamic Symbol Loader ===\n")
|
print("\n=== Testing Dynamic Symbol Loader (Text-based) ===\n")
|
||||||
|
|
||||||
# Test 1: Find library directories
|
|
||||||
print("1. Finding KiCad symbol library directories...")
|
print("1. Finding KiCad symbol library directories...")
|
||||||
lib_dirs = loader.find_kicad_symbol_libraries()
|
lib_dirs = loader.find_kicad_symbol_libraries()
|
||||||
print(f" Found {len(lib_dirs)} directories:")
|
print(f" Found {len(lib_dirs)} directories")
|
||||||
for lib_dir in lib_dirs:
|
|
||||||
print(f" - {lib_dir}")
|
|
||||||
|
|
||||||
# Test 2: Find Device library
|
print("\n2. Extracting symbols...")
|
||||||
print("\n2. Finding Device.kicad_sym library file...")
|
for lib, sym in [('Device', 'R'), ('Device', 'C'), ('Device', 'LED'), ('Device', 'Q_NMOS')]:
|
||||||
device_lib = loader.find_library_file("Device")
|
block = loader.extract_symbol_from_library(lib, sym)
|
||||||
if device_lib:
|
if block:
|
||||||
print(f" ✓ Found: {device_lib}")
|
print(f" OK: {lib}:{sym} ({len(block)} chars)")
|
||||||
else:
|
else:
|
||||||
print(" ✗ Not found")
|
print(f" FAIL: {lib}:{sym}")
|
||||||
exit(1)
|
|
||||||
|
|
||||||
# Test 3: Parse library file
|
print("\n3. Testing extends resolution...")
|
||||||
print("\n3. Parsing Device.kicad_sym...")
|
block = loader.extract_symbol_from_library('Regulator_Switching', 'LM2596S-5')
|
||||||
parsed = loader.parse_library_file(device_lib)
|
if block and 'LM2596S-12' in block:
|
||||||
print(f" ✓ Parsed successfully ({len(parsed)} top-level items)")
|
print(f" OK: LM2596S-5 includes parent LM2596S-12 ({len(block)} chars)")
|
||||||
|
|
||||||
# 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:
|
else:
|
||||||
print(f" ✗ Failed: {symbol}")
|
print(f" FAIL: extends not resolved")
|
||||||
|
|
||||||
print("\n✓ All basic tests passed!")
|
print("\nAll tests passed!")
|
||||||
|
|||||||
@@ -556,10 +556,11 @@ class KiCADInterface:
|
|||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_add_schematic_component(self, params):
|
def _handle_add_schematic_component(self, params):
|
||||||
"""Add a component to a schematic with dynamic symbol loading support"""
|
"""Add a component to a schematic using text-based injection (no sexpdata)"""
|
||||||
logger.info("Adding component to schematic")
|
logger.info("Adding component to schematic")
|
||||||
try:
|
try:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from commands.dynamic_symbol_loader import DynamicSymbolLoader
|
||||||
|
|
||||||
schematic_path = params.get("schematicPath")
|
schematic_path = params.get("schematicPath")
|
||||||
component = params.get("component", {})
|
component = params.get("component", {})
|
||||||
@@ -569,86 +570,29 @@ class KiCADInterface:
|
|||||||
if not component:
|
if not component:
|
||||||
return {"success": False, "message": "Component definition is required"}
|
return {"success": False, "message": "Component definition is required"}
|
||||||
|
|
||||||
# Convert to Path object for dynamic loader
|
|
||||||
schematic_path_obj = Path(schematic_path)
|
|
||||||
|
|
||||||
# Load schematic
|
|
||||||
schematic = SchematicManager.load_schematic(schematic_path)
|
|
||||||
if not schematic:
|
|
||||||
return {"success": False, "message": "Failed to load schematic"}
|
|
||||||
|
|
||||||
# Check if component type requires dynamic loading
|
|
||||||
comp_type = component.get('type', 'R')
|
comp_type = component.get('type', 'R')
|
||||||
library = component.get('library', 'Device')
|
library = component.get('library', 'Device')
|
||||||
|
reference = component.get('reference', 'X?')
|
||||||
# Check if template exists in static templates
|
value = component.get('value', comp_type)
|
||||||
template_ref = ComponentManager.TEMPLATE_MAP.get(comp_type)
|
x = component.get('x', 0)
|
||||||
needs_dynamic_loading = False
|
y = component.get('y', 0)
|
||||||
|
|
||||||
if template_ref:
|
|
||||||
# Check if template exists in schematic
|
|
||||||
if not hasattr(schematic.symbol, template_ref):
|
|
||||||
needs_dynamic_loading = True
|
|
||||||
logger.info(f"Static template {template_ref} not found in schematic, will try dynamic loading")
|
|
||||||
else:
|
|
||||||
# Not in static map, definitely needs dynamic loading
|
|
||||||
needs_dynamic_loading = True
|
|
||||||
logger.info(f"Component type {comp_type} not in static templates, will use dynamic loading")
|
|
||||||
|
|
||||||
# If dynamic loading is needed and available
|
|
||||||
if needs_dynamic_loading:
|
|
||||||
try:
|
|
||||||
from commands.dynamic_symbol_loader import DynamicSymbolLoader
|
|
||||||
|
|
||||||
loader = DynamicSymbolLoader()
|
loader = DynamicSymbolLoader()
|
||||||
|
loader.add_component(
|
||||||
|
Path(schematic_path), library, comp_type,
|
||||||
|
reference=reference, value=value, x=x, y=y
|
||||||
|
)
|
||||||
|
|
||||||
# Save current schematic first to preserve any changes
|
return {
|
||||||
SchematicManager.save_schematic(schematic, schematic_path)
|
|
||||||
logger.info("Saved schematic before dynamic loading")
|
|
||||||
|
|
||||||
# Dynamically load the symbol (injects into file and creates template)
|
|
||||||
logger.info(f"Dynamically loading symbol: {library}:{comp_type}")
|
|
||||||
template_ref = loader.load_symbol_dynamically(schematic_path_obj, library, comp_type)
|
|
||||||
logger.info(f"Dynamic loading successful. Template ref: {template_ref}")
|
|
||||||
|
|
||||||
# Reload schematic to get the newly injected symbol
|
|
||||||
schematic = SchematicManager.load_schematic(schematic_path)
|
|
||||||
if not schematic:
|
|
||||||
return {"success": False, "message": "Failed to reload schematic after dynamic loading"}
|
|
||||||
logger.info("Reloaded schematic with new symbol definition")
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
logger.warning("Dynamic symbol loader not available, falling back to static templates")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Dynamic loading failed: {e}")
|
|
||||||
logger.warning("Falling back to static templates")
|
|
||||||
|
|
||||||
# Add component (now with template available in schematic)
|
|
||||||
component_obj = ComponentManager.add_component(schematic, component, schematic_path_obj)
|
|
||||||
success = component_obj is not None
|
|
||||||
|
|
||||||
if success:
|
|
||||||
SchematicManager.save_schematic(schematic, schematic_path)
|
|
||||||
|
|
||||||
# Prepare response with dynamic loading info
|
|
||||||
response = {
|
|
||||||
"success": True,
|
"success": True,
|
||||||
"component_reference": component.get('reference', 'unknown'),
|
"component_reference": reference,
|
||||||
"dynamic_loading_used": needs_dynamic_loading
|
"symbol_source": f"{library}:{comp_type}"
|
||||||
}
|
}
|
||||||
|
|
||||||
if needs_dynamic_loading:
|
|
||||||
response["symbol_source"] = f"{library}:{comp_type}"
|
|
||||||
response["template_reference"] = template_ref if 'template_ref' in locals() else "unknown"
|
|
||||||
|
|
||||||
return response
|
|
||||||
else:
|
|
||||||
return {"success": False, "message": "Failed to add component"}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error adding component to schematic: {str(e)}")
|
logger.error(f"Error adding component to schematic: {str(e)}")
|
||||||
import traceback
|
import traceback
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e), "errorDetails": traceback.format_exc()}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_add_schematic_wire(self, params):
|
def _handle_add_schematic_wire(self, params):
|
||||||
"""Add a wire to a schematic using WireManager"""
|
"""Add a wire to a schematic using WireManager"""
|
||||||
|
|||||||
Reference in New Issue
Block a user