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>
229 lines
8.8 KiB
Python
229 lines
8.8 KiB
Python
"""
|
|
Project-related command implementations for KiCAD interface
|
|
"""
|
|
|
|
import os
|
|
import pcbnew # type: ignore
|
|
import logging
|
|
import shutil
|
|
from typing import Dict, Any, Optional
|
|
|
|
logger = logging.getLogger('kicad_interface')
|
|
|
|
class ProjectCommands:
|
|
"""Handles project-related KiCAD operations"""
|
|
|
|
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
|
"""Initialize with optional board instance"""
|
|
self.board = board
|
|
|
|
def create_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Create a new KiCAD project"""
|
|
try:
|
|
# Accept both 'name' (from MCP tool) and 'projectName' (legacy)
|
|
project_name = params.get("name") or params.get("projectName", "New_Project")
|
|
path = params.get("path", os.getcwd())
|
|
template = params.get("template")
|
|
|
|
# Generate the full project path
|
|
project_path = os.path.join(path, project_name)
|
|
if not project_path.endswith(".kicad_pro"):
|
|
project_path += ".kicad_pro"
|
|
|
|
# Create project directory if it doesn't exist
|
|
os.makedirs(os.path.dirname(project_path), exist_ok=True)
|
|
|
|
# Create a new board
|
|
board = pcbnew.BOARD()
|
|
|
|
# Set project properties
|
|
board.GetTitleBlock().SetTitle(project_name)
|
|
|
|
# Set current date with proper parameter
|
|
from datetime import datetime
|
|
current_date = datetime.now().strftime("%Y-%m-%d")
|
|
board.GetTitleBlock().SetDate(current_date)
|
|
|
|
# If template is specified, try to load it
|
|
if template:
|
|
template_path = os.path.expanduser(template)
|
|
if os.path.exists(template_path):
|
|
template_board = pcbnew.LoadBoard(template_path)
|
|
# Copy settings from template
|
|
board.SetDesignSettings(template_board.GetDesignSettings())
|
|
board.SetLayerStack(template_board.GetLayerStack())
|
|
|
|
# Save the board
|
|
board_path = project_path.replace(".kicad_pro", ".kicad_pcb")
|
|
board.SetFileName(board_path)
|
|
pcbnew.SaveBoard(board_path, board)
|
|
|
|
# Create schematic from template (use expanded template with many component types)
|
|
schematic_path = project_path.replace(".kicad_pro", ".kicad_sch")
|
|
template_sch_path = os.path.join(
|
|
os.path.dirname(os.path.abspath(__file__)),
|
|
'..', 'templates', 'template_with_symbols_expanded.kicad_sch'
|
|
)
|
|
|
|
if os.path.exists(template_sch_path):
|
|
# Copy template schematic
|
|
shutil.copy(template_sch_path, schematic_path)
|
|
logger.info(f"Created schematic from template: {schematic_path}")
|
|
else:
|
|
# Fallback: create minimal schematic
|
|
logger.warning(f"Template not found at {template_sch_path}, creating minimal schematic")
|
|
with open(schematic_path, 'w') as f:
|
|
f.write(f'(kicad_sch (version 20230121) (generator "KiCAD-MCP-Server")\n\n')
|
|
f.write(f' (uuid 00000000-0000-0000-0000-000000000000)\n\n')
|
|
f.write(f' (paper "A4")\n\n')
|
|
f.write(f' (lib_symbols\n )\n\n')
|
|
f.write(f' (sheet_instances\n (path "/" (page "1"))\n )\n')
|
|
f.write(f')\n')
|
|
|
|
# Create project file with schematic reference
|
|
with open(project_path, 'w') as f:
|
|
f.write('{\n')
|
|
f.write(' "board": {\n')
|
|
f.write(f' "filename": "{os.path.basename(board_path)}"\n')
|
|
f.write(' },\n')
|
|
f.write(' "sheets": [\n')
|
|
f.write(f' ["root", "{os.path.basename(schematic_path)}"]\n')
|
|
f.write(' ]\n')
|
|
f.write('}\n')
|
|
|
|
self.board = board
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Created project: {project_name}",
|
|
"project": {
|
|
"name": project_name,
|
|
"path": project_path,
|
|
"boardPath": board_path,
|
|
"schematicPath": schematic_path
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error creating project: {str(e)}")
|
|
return {
|
|
"success": False,
|
|
"message": "Failed to create project",
|
|
"errorDetails": str(e)
|
|
}
|
|
|
|
def open_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Open an existing KiCAD project"""
|
|
try:
|
|
filename = params.get("filename")
|
|
if not filename:
|
|
return {
|
|
"success": False,
|
|
"message": "No filename provided",
|
|
"errorDetails": "The filename parameter is required"
|
|
}
|
|
|
|
# Expand user path and make absolute
|
|
filename = os.path.abspath(os.path.expanduser(filename))
|
|
|
|
# If it's a project file, get the board file
|
|
if filename.endswith(".kicad_pro"):
|
|
board_path = filename.replace(".kicad_pro", ".kicad_pcb")
|
|
else:
|
|
board_path = filename
|
|
|
|
# Load the board
|
|
board = pcbnew.LoadBoard(board_path)
|
|
self.board = board
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Opened project: {os.path.basename(board_path)}",
|
|
"project": {
|
|
"name": os.path.splitext(os.path.basename(board_path))[0],
|
|
"path": filename,
|
|
"boardPath": board_path
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error opening project: {str(e)}")
|
|
return {
|
|
"success": False,
|
|
"message": "Failed to open project",
|
|
"errorDetails": str(e)
|
|
}
|
|
|
|
def save_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Save the current KiCAD project"""
|
|
try:
|
|
if not self.board:
|
|
return {
|
|
"success": False,
|
|
"message": "No board is loaded",
|
|
"errorDetails": "Load or create a board first"
|
|
}
|
|
|
|
filename = params.get("filename")
|
|
if filename:
|
|
# Save to new location
|
|
filename = os.path.abspath(os.path.expanduser(filename))
|
|
self.board.SetFileName(filename)
|
|
|
|
# Save the board
|
|
pcbnew.SaveBoard(self.board.GetFileName(), self.board)
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Saved project to: {self.board.GetFileName()}",
|
|
"project": {
|
|
"name": os.path.splitext(os.path.basename(self.board.GetFileName()))[0],
|
|
"path": self.board.GetFileName()
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error saving project: {str(e)}")
|
|
return {
|
|
"success": False,
|
|
"message": "Failed to save project",
|
|
"errorDetails": str(e)
|
|
}
|
|
|
|
def get_project_info(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Get information about the current project"""
|
|
try:
|
|
if not self.board:
|
|
return {
|
|
"success": False,
|
|
"message": "No board is loaded",
|
|
"errorDetails": "Load or create a board first"
|
|
}
|
|
|
|
title_block = self.board.GetTitleBlock()
|
|
filename = self.board.GetFileName()
|
|
|
|
return {
|
|
"success": True,
|
|
"project": {
|
|
"name": os.path.splitext(os.path.basename(filename))[0],
|
|
"path": filename,
|
|
"title": title_block.GetTitle(),
|
|
"date": title_block.GetDate(),
|
|
"revision": title_block.GetRevision(),
|
|
"company": title_block.GetCompany(),
|
|
"comment1": title_block.GetComment(0),
|
|
"comment2": title_block.GetComment(1),
|
|
"comment3": title_block.GetComment(2),
|
|
"comment4": title_block.GetComment(3)
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting project info: {str(e)}")
|
|
return {
|
|
"success": False,
|
|
"message": "Failed to get project information",
|
|
"errorDetails": str(e)
|
|
}
|