feat: Week 1 complete - Linux support + IPC API prep
🎉 Major v2.0 rebuild kickoff - Week 1 accomplished! ## Highlights ### Cross-Platform Support 🌍 - ✅ Linux primary platform (Ubuntu/Debian tested) - ✅ Windows fully supported - ✅ macOS experimental support - ✅ Platform-agnostic path handling (XDG spec) - ✅ Auto-detection of KiCAD installation ### Infrastructure 🏗️ - ✅ GitHub Actions CI/CD pipeline - ✅ Pytest framework with 20+ tests - ✅ Pre-commit hooks (Black, MyPy, ESLint) - ✅ Automated Linux installation script - ✅ Enhanced npm scripts ### IPC API Migration Prep 🚀 - ✅ Comprehensive migration plan (30 pages) - ✅ Backend abstraction layer (800+ lines) - ✅ Factory pattern with auto-detection - ✅ SWIG backward compatibility wrapper - ✅ IPC backend skeleton ready ### Documentation 📚 - ✅ Updated README (Linux installation) - ✅ CONTRIBUTING.md guide - ✅ Linux compatibility audit - ✅ IPC API migration plan - ✅ Session summaries - ✅ Platform-specific config templates ## Files Changed - 27 files created - ~3,000 lines of code/docs - 8 comprehensive documentation pages - 20+ unit tests - 5 abstraction layer modules ## Next Steps - Week 2: IPC API migration (project.py → component.py → routing.py) - Migrate from deprecated SWIG to official IPC API - JLCPCB/Digikey integration prep 🤖 Generated with Claude Code https://claude.com/claude-code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
19
python/commands/__init__.py
Normal file
19
python/commands/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
KiCAD command implementations package
|
||||
"""
|
||||
|
||||
from .project import ProjectCommands
|
||||
from .board import BoardCommands
|
||||
from .component import ComponentCommands
|
||||
from .routing import RoutingCommands
|
||||
from .design_rules import DesignRuleCommands
|
||||
from .export import ExportCommands
|
||||
|
||||
__all__ = [
|
||||
'ProjectCommands',
|
||||
'BoardCommands',
|
||||
'ComponentCommands',
|
||||
'RoutingCommands',
|
||||
'DesignRuleCommands',
|
||||
'ExportCommands'
|
||||
]
|
||||
11
python/commands/board.py
Normal file
11
python/commands/board.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
Board-related command implementations for KiCAD interface
|
||||
|
||||
This file is maintained for backward compatibility.
|
||||
It imports and re-exports the BoardCommands class from the board package.
|
||||
"""
|
||||
|
||||
from commands.board import BoardCommands
|
||||
|
||||
# Re-export the BoardCommands class for backward compatibility
|
||||
__all__ = ['BoardCommands']
|
||||
77
python/commands/board/__init__.py
Normal file
77
python/commands/board/__init__.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Board-related command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import pcbnew
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
# Import specialized modules
|
||||
from .size import BoardSizeCommands
|
||||
from .layers import BoardLayerCommands
|
||||
from .outline import BoardOutlineCommands
|
||||
from .view import BoardViewCommands
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
|
||||
class BoardCommands:
|
||||
"""Handles board-related KiCAD operations"""
|
||||
|
||||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||||
"""Initialize with optional board instance"""
|
||||
self.board = board
|
||||
|
||||
# Initialize specialized command classes
|
||||
self.size_commands = BoardSizeCommands(board)
|
||||
self.layer_commands = BoardLayerCommands(board)
|
||||
self.outline_commands = BoardOutlineCommands(board)
|
||||
self.view_commands = BoardViewCommands(board)
|
||||
|
||||
# Delegate board size commands
|
||||
def set_board_size(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Set the size of the PCB board"""
|
||||
self.size_commands.board = self.board
|
||||
return self.size_commands.set_board_size(params)
|
||||
|
||||
# Delegate layer commands
|
||||
def add_layer(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add a new layer to the PCB"""
|
||||
self.layer_commands.board = self.board
|
||||
return self.layer_commands.add_layer(params)
|
||||
|
||||
def set_active_layer(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Set the active layer for PCB operations"""
|
||||
self.layer_commands.board = self.board
|
||||
return self.layer_commands.set_active_layer(params)
|
||||
|
||||
def get_layer_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get a list of all layers in the PCB"""
|
||||
self.layer_commands.board = self.board
|
||||
return self.layer_commands.get_layer_list(params)
|
||||
|
||||
# Delegate board outline commands
|
||||
def add_board_outline(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add a board outline to the PCB"""
|
||||
self.outline_commands.board = self.board
|
||||
return self.outline_commands.add_board_outline(params)
|
||||
|
||||
def add_mounting_hole(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add a mounting hole to the PCB"""
|
||||
self.outline_commands.board = self.board
|
||||
return self.outline_commands.add_mounting_hole(params)
|
||||
|
||||
def add_text(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add text annotation to the PCB"""
|
||||
self.outline_commands.board = self.board
|
||||
return self.outline_commands.add_text(params)
|
||||
|
||||
# Delegate view commands
|
||||
def get_board_info(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get information about the current board"""
|
||||
self.view_commands.board = self.board
|
||||
return self.view_commands.get_board_info(params)
|
||||
|
||||
def get_board_2d_view(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get a 2D image of the PCB"""
|
||||
self.view_commands.board = self.board
|
||||
return self.view_commands.get_board_2d_view(params)
|
||||
190
python/commands/board/layers.py
Normal file
190
python/commands/board/layers.py
Normal file
@@ -0,0 +1,190 @@
|
||||
"""
|
||||
Board layer command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import pcbnew
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
|
||||
class BoardLayerCommands:
|
||||
"""Handles board layer operations"""
|
||||
|
||||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||||
"""Initialize with optional board instance"""
|
||||
self.board = board
|
||||
|
||||
def add_layer(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add a new layer to the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
name = params.get("name")
|
||||
layer_type = params.get("type")
|
||||
position = params.get("position")
|
||||
number = params.get("number")
|
||||
|
||||
if not name or not layer_type or not position:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "name, type, and position are required"
|
||||
}
|
||||
|
||||
# Get layer stack
|
||||
layer_stack = self.board.GetLayerStack()
|
||||
|
||||
# Determine layer ID based on position and number
|
||||
layer_id = None
|
||||
if position == "inner":
|
||||
if number is None:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing layer number",
|
||||
"errorDetails": "number is required for inner layers"
|
||||
}
|
||||
layer_id = pcbnew.In1_Cu + (number - 1)
|
||||
elif position == "top":
|
||||
layer_id = pcbnew.F_Cu
|
||||
elif position == "bottom":
|
||||
layer_id = pcbnew.B_Cu
|
||||
|
||||
if layer_id is None:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid layer position",
|
||||
"errorDetails": "position must be 'top', 'bottom', or 'inner'"
|
||||
}
|
||||
|
||||
# Set layer properties
|
||||
layer_stack.SetLayerName(layer_id, name)
|
||||
layer_stack.SetLayerType(layer_id, self._get_layer_type(layer_type))
|
||||
|
||||
# Enable the layer
|
||||
self.board.SetLayerEnabled(layer_id, True)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Added layer: {name}",
|
||||
"layer": {
|
||||
"name": name,
|
||||
"type": layer_type,
|
||||
"position": position,
|
||||
"number": number
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding layer: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to add layer",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def set_active_layer(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Set the active layer for PCB operations"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
layer = params.get("layer")
|
||||
if not layer:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No layer specified",
|
||||
"errorDetails": "layer parameter is required"
|
||||
}
|
||||
|
||||
# Find layer ID by name
|
||||
layer_id = self.board.GetLayerID(layer)
|
||||
if layer_id < 0:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Layer not found",
|
||||
"errorDetails": f"Layer '{layer}' does not exist"
|
||||
}
|
||||
|
||||
# Set active layer
|
||||
self.board.SetActiveLayer(layer_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Set active layer to: {layer}",
|
||||
"layer": {
|
||||
"name": layer,
|
||||
"id": layer_id
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting active layer: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to set active layer",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def get_layer_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get a list of all layers in the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
layers = []
|
||||
for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
|
||||
if self.board.IsLayerEnabled(layer_id):
|
||||
layers.append({
|
||||
"name": self.board.GetLayerName(layer_id),
|
||||
"type": self._get_layer_type_name(self.board.GetLayerType(layer_id)),
|
||||
"id": layer_id,
|
||||
"isActive": layer_id == self.board.GetActiveLayer()
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"layers": layers
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting layer list: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to get layer list",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def _get_layer_type(self, type_name: str) -> int:
|
||||
"""Convert layer type name to KiCAD layer type constant"""
|
||||
type_map = {
|
||||
"copper": pcbnew.LT_SIGNAL,
|
||||
"technical": pcbnew.LT_SIGNAL,
|
||||
"user": pcbnew.LT_USER,
|
||||
"signal": pcbnew.LT_SIGNAL
|
||||
}
|
||||
return type_map.get(type_name.lower(), pcbnew.LT_SIGNAL)
|
||||
|
||||
def _get_layer_type_name(self, type_id: int) -> str:
|
||||
"""Convert KiCAD layer type constant to name"""
|
||||
type_map = {
|
||||
pcbnew.LT_SIGNAL: "signal",
|
||||
pcbnew.LT_POWER: "power",
|
||||
pcbnew.LT_MIXED: "mixed",
|
||||
pcbnew.LT_JUMPER: "jumper",
|
||||
pcbnew.LT_USER: "user"
|
||||
}
|
||||
return type_map.get(type_id, "unknown")
|
||||
413
python/commands/board/outline.py
Normal file
413
python/commands/board/outline.py
Normal file
@@ -0,0 +1,413 @@
|
||||
"""
|
||||
Board outline command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import pcbnew
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
|
||||
class BoardOutlineCommands:
|
||||
"""Handles board outline operations"""
|
||||
|
||||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||||
"""Initialize with optional board instance"""
|
||||
self.board = board
|
||||
|
||||
def add_board_outline(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add a board outline to the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
shape = params.get("shape", "rectangle")
|
||||
width = params.get("width")
|
||||
height = params.get("height")
|
||||
center_x = params.get("centerX", 0)
|
||||
center_y = params.get("centerY", 0)
|
||||
radius = params.get("radius")
|
||||
corner_radius = params.get("cornerRadius", 0)
|
||||
points = params.get("points", [])
|
||||
unit = params.get("unit", "mm")
|
||||
|
||||
if shape not in ["rectangle", "circle", "polygon", "rounded_rectangle"]:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid shape",
|
||||
"errorDetails": f"Shape '{shape}' not supported"
|
||||
}
|
||||
|
||||
# Convert to internal units (nanometers)
|
||||
scale = 1000000 if unit == "mm" else 25400000 # mm or inch to nm
|
||||
|
||||
# Create drawing for edge cuts
|
||||
edge_layer = self.board.GetLayerID("Edge.Cuts")
|
||||
|
||||
if shape == "rectangle":
|
||||
if width is None or height is None:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing dimensions",
|
||||
"errorDetails": "Both width and height are required for rectangle"
|
||||
}
|
||||
|
||||
width_nm = int(width * scale)
|
||||
height_nm = int(height * scale)
|
||||
center_x_nm = int(center_x * scale)
|
||||
center_y_nm = int(center_y * scale)
|
||||
|
||||
# Create rectangle
|
||||
top_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm - height_nm // 2)
|
||||
top_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm - height_nm // 2)
|
||||
bottom_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm + height_nm // 2)
|
||||
bottom_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm + height_nm // 2)
|
||||
|
||||
# Add lines for rectangle
|
||||
self._add_edge_line(top_left, top_right, edge_layer)
|
||||
self._add_edge_line(top_right, bottom_right, edge_layer)
|
||||
self._add_edge_line(bottom_right, bottom_left, edge_layer)
|
||||
self._add_edge_line(bottom_left, top_left, edge_layer)
|
||||
|
||||
elif shape == "rounded_rectangle":
|
||||
if width is None or height is None:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing dimensions",
|
||||
"errorDetails": "Both width and height are required for rounded rectangle"
|
||||
}
|
||||
|
||||
width_nm = int(width * scale)
|
||||
height_nm = int(height * scale)
|
||||
center_x_nm = int(center_x * scale)
|
||||
center_y_nm = int(center_y * scale)
|
||||
corner_radius_nm = int(corner_radius * scale)
|
||||
|
||||
# Create rounded rectangle
|
||||
self._add_rounded_rect(
|
||||
center_x_nm, center_y_nm,
|
||||
width_nm, height_nm,
|
||||
corner_radius_nm, edge_layer
|
||||
)
|
||||
|
||||
elif shape == "circle":
|
||||
if radius is None:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing radius",
|
||||
"errorDetails": "Radius is required for circle"
|
||||
}
|
||||
|
||||
center_x_nm = int(center_x * scale)
|
||||
center_y_nm = int(center_y * scale)
|
||||
radius_nm = int(radius * scale)
|
||||
|
||||
# Create circle
|
||||
circle = pcbnew.PCB_SHAPE(self.board)
|
||||
circle.SetShape(pcbnew.SHAPE_T_CIRCLE)
|
||||
circle.SetCenter(pcbnew.VECTOR2I(center_x_nm, center_y_nm))
|
||||
circle.SetEnd(pcbnew.VECTOR2I(center_x_nm + radius_nm, center_y_nm))
|
||||
circle.SetLayer(edge_layer)
|
||||
circle.SetWidth(0) # Zero width for edge cuts
|
||||
self.board.Add(circle)
|
||||
|
||||
elif shape == "polygon":
|
||||
if not points or len(points) < 3:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing points",
|
||||
"errorDetails": "At least 3 points are required for polygon"
|
||||
}
|
||||
|
||||
# Convert points to nm
|
||||
polygon_points = []
|
||||
for point in points:
|
||||
x_nm = int(point["x"] * scale)
|
||||
y_nm = int(point["y"] * scale)
|
||||
polygon_points.append(pcbnew.VECTOR2I(x_nm, y_nm))
|
||||
|
||||
# Add lines for polygon
|
||||
for i in range(len(polygon_points)):
|
||||
self._add_edge_line(
|
||||
polygon_points[i],
|
||||
polygon_points[(i + 1) % len(polygon_points)],
|
||||
edge_layer
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Added board outline: {shape}",
|
||||
"outline": {
|
||||
"shape": shape,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"center": {"x": center_x, "y": center_y, "unit": unit},
|
||||
"radius": radius,
|
||||
"cornerRadius": corner_radius,
|
||||
"points": points
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding board outline: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to add board outline",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def add_mounting_hole(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add a mounting hole to the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
position = params.get("position")
|
||||
diameter = params.get("diameter")
|
||||
pad_diameter = params.get("padDiameter")
|
||||
plated = params.get("plated", False)
|
||||
|
||||
if not position or not diameter:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "position and diameter are required"
|
||||
}
|
||||
|
||||
# Convert to internal units (nanometers)
|
||||
scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000 # mm or inch to nm
|
||||
x_nm = int(position["x"] * scale)
|
||||
y_nm = int(position["y"] * scale)
|
||||
diameter_nm = int(diameter * scale)
|
||||
pad_diameter_nm = int(pad_diameter * scale) if pad_diameter else diameter_nm + scale # 1mm larger by default
|
||||
|
||||
# Create footprint for mounting hole
|
||||
module = pcbnew.FOOTPRINT(self.board)
|
||||
module.SetReference(f"MH")
|
||||
module.SetValue(f"MountingHole_{diameter}mm")
|
||||
|
||||
# Create the pad for the hole
|
||||
pad = pcbnew.PAD(module)
|
||||
pad.SetNumber(1)
|
||||
pad.SetShape(pcbnew.PAD_SHAPE_CIRCLE)
|
||||
pad.SetAttribute(pcbnew.PAD_ATTRIB_PTH if plated else pcbnew.PAD_ATTRIB_NPTH)
|
||||
pad.SetSize(pcbnew.VECTOR2I(pad_diameter_nm, pad_diameter_nm))
|
||||
pad.SetDrillSize(pcbnew.VECTOR2I(diameter_nm, diameter_nm))
|
||||
pad.SetPosition(pcbnew.VECTOR2I(0, 0)) # Position relative to module
|
||||
module.Add(pad)
|
||||
|
||||
# Position the mounting hole
|
||||
module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
||||
|
||||
# Add to board
|
||||
self.board.Add(module)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Added mounting hole",
|
||||
"mountingHole": {
|
||||
"position": position,
|
||||
"diameter": diameter,
|
||||
"padDiameter": pad_diameter or diameter + 1,
|
||||
"plated": plated
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding mounting hole: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to add mounting hole",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def add_text(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add text annotation to the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
text = params.get("text")
|
||||
position = params.get("position")
|
||||
layer = params.get("layer", "F.SilkS")
|
||||
size = params.get("size", 1.0)
|
||||
thickness = params.get("thickness", 0.15)
|
||||
rotation = params.get("rotation", 0)
|
||||
mirror = params.get("mirror", False)
|
||||
|
||||
if not text or not position:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "text and position are required"
|
||||
}
|
||||
|
||||
# Convert to internal units (nanometers)
|
||||
scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000 # mm or inch to nm
|
||||
x_nm = int(position["x"] * scale)
|
||||
y_nm = int(position["y"] * scale)
|
||||
size_nm = int(size * scale)
|
||||
thickness_nm = int(thickness * scale)
|
||||
|
||||
# Get layer ID
|
||||
layer_id = self.board.GetLayerID(layer)
|
||||
if layer_id < 0:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid layer",
|
||||
"errorDetails": f"Layer '{layer}' does not exist"
|
||||
}
|
||||
|
||||
# Create text
|
||||
pcb_text = pcbnew.PCB_TEXT(self.board)
|
||||
pcb_text.SetText(text)
|
||||
pcb_text.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
||||
pcb_text.SetLayer(layer_id)
|
||||
pcb_text.SetTextSize(pcbnew.VECTOR2I(size_nm, size_nm))
|
||||
pcb_text.SetTextThickness(thickness_nm)
|
||||
pcb_text.SetTextAngle(rotation * 10) # KiCAD uses decidegrees
|
||||
pcb_text.SetMirrored(mirror)
|
||||
|
||||
# Add to board
|
||||
self.board.Add(pcb_text)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Added text annotation",
|
||||
"text": {
|
||||
"text": text,
|
||||
"position": position,
|
||||
"layer": layer,
|
||||
"size": size,
|
||||
"thickness": thickness,
|
||||
"rotation": rotation,
|
||||
"mirror": mirror
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding text: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to add text",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def _add_edge_line(self, start: pcbnew.VECTOR2I, end: pcbnew.VECTOR2I, layer: int) -> None:
|
||||
"""Add a line to the edge cuts layer"""
|
||||
line = pcbnew.PCB_SHAPE(self.board)
|
||||
line.SetShape(pcbnew.SHAPE_T_SEGMENT)
|
||||
line.SetStart(start)
|
||||
line.SetEnd(end)
|
||||
line.SetLayer(layer)
|
||||
line.SetWidth(0) # Zero width for edge cuts
|
||||
self.board.Add(line)
|
||||
|
||||
def _add_rounded_rect(self, center_x_nm: int, center_y_nm: int,
|
||||
width_nm: int, height_nm: int,
|
||||
radius_nm: int, layer: int) -> None:
|
||||
"""Add a rounded rectangle to the edge cuts layer"""
|
||||
if radius_nm <= 0:
|
||||
# If no radius, create regular rectangle
|
||||
top_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm - height_nm // 2)
|
||||
top_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm - height_nm // 2)
|
||||
bottom_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm + height_nm // 2)
|
||||
bottom_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm + height_nm // 2)
|
||||
|
||||
self._add_edge_line(top_left, top_right, layer)
|
||||
self._add_edge_line(top_right, bottom_right, layer)
|
||||
self._add_edge_line(bottom_right, bottom_left, layer)
|
||||
self._add_edge_line(bottom_left, top_left, layer)
|
||||
return
|
||||
|
||||
# Calculate corner centers
|
||||
half_width = width_nm // 2
|
||||
half_height = height_nm // 2
|
||||
|
||||
# Ensure radius is not larger than half the smallest dimension
|
||||
max_radius = min(half_width, half_height)
|
||||
if radius_nm > max_radius:
|
||||
radius_nm = max_radius
|
||||
|
||||
# Calculate corner centers
|
||||
top_left_center = pcbnew.VECTOR2I(
|
||||
center_x_nm - half_width + radius_nm,
|
||||
center_y_nm - half_height + radius_nm
|
||||
)
|
||||
top_right_center = pcbnew.VECTOR2I(
|
||||
center_x_nm + half_width - radius_nm,
|
||||
center_y_nm - half_height + radius_nm
|
||||
)
|
||||
bottom_right_center = pcbnew.VECTOR2I(
|
||||
center_x_nm + half_width - radius_nm,
|
||||
center_y_nm + half_height - radius_nm
|
||||
)
|
||||
bottom_left_center = pcbnew.VECTOR2I(
|
||||
center_x_nm - half_width + radius_nm,
|
||||
center_y_nm + half_height - radius_nm
|
||||
)
|
||||
|
||||
# Add arcs for corners
|
||||
self._add_corner_arc(top_left_center, radius_nm, 180, 270, layer)
|
||||
self._add_corner_arc(top_right_center, radius_nm, 270, 0, layer)
|
||||
self._add_corner_arc(bottom_right_center, radius_nm, 0, 90, layer)
|
||||
self._add_corner_arc(bottom_left_center, radius_nm, 90, 180, layer)
|
||||
|
||||
# Add lines for straight edges
|
||||
# Top edge
|
||||
self._add_edge_line(
|
||||
pcbnew.VECTOR2I(top_left_center.x, top_left_center.y - radius_nm),
|
||||
pcbnew.VECTOR2I(top_right_center.x, top_right_center.y - radius_nm),
|
||||
layer
|
||||
)
|
||||
# Right edge
|
||||
self._add_edge_line(
|
||||
pcbnew.VECTOR2I(top_right_center.x + radius_nm, top_right_center.y),
|
||||
pcbnew.VECTOR2I(bottom_right_center.x + radius_nm, bottom_right_center.y),
|
||||
layer
|
||||
)
|
||||
# Bottom edge
|
||||
self._add_edge_line(
|
||||
pcbnew.VECTOR2I(bottom_right_center.x, bottom_right_center.y + radius_nm),
|
||||
pcbnew.VECTOR2I(bottom_left_center.x, bottom_left_center.y + radius_nm),
|
||||
layer
|
||||
)
|
||||
# Left edge
|
||||
self._add_edge_line(
|
||||
pcbnew.VECTOR2I(bottom_left_center.x - radius_nm, bottom_left_center.y),
|
||||
pcbnew.VECTOR2I(top_left_center.x - radius_nm, top_left_center.y),
|
||||
layer
|
||||
)
|
||||
|
||||
def _add_corner_arc(self, center: pcbnew.VECTOR2I, radius: int,
|
||||
start_angle: float, end_angle: float, layer: int) -> None:
|
||||
"""Add an arc for a rounded corner"""
|
||||
# Create arc for corner
|
||||
arc = pcbnew.PCB_SHAPE(self.board)
|
||||
arc.SetShape(pcbnew.SHAPE_T_ARC)
|
||||
arc.SetCenter(center)
|
||||
|
||||
# Calculate start and end points
|
||||
start_x = center.x + int(radius * math.cos(math.radians(start_angle)))
|
||||
start_y = center.y + int(radius * math.sin(math.radians(start_angle)))
|
||||
end_x = center.x + int(radius * math.cos(math.radians(end_angle)))
|
||||
end_y = center.y + int(radius * math.sin(math.radians(end_angle)))
|
||||
|
||||
arc.SetStart(pcbnew.VECTOR2I(start_x, start_y))
|
||||
arc.SetEnd(pcbnew.VECTOR2I(end_x, end_y))
|
||||
arc.SetLayer(layer)
|
||||
arc.SetWidth(0) # Zero width for edge cuts
|
||||
self.board.Add(arc)
|
||||
67
python/commands/board/size.py
Normal file
67
python/commands/board/size.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
Board size command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import pcbnew
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
|
||||
class BoardSizeCommands:
|
||||
"""Handles board size operations"""
|
||||
|
||||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||||
"""Initialize with optional board instance"""
|
||||
self.board = board
|
||||
|
||||
def set_board_size(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Set the size of the PCB board"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
width = params.get("width")
|
||||
height = params.get("height")
|
||||
unit = params.get("unit", "mm")
|
||||
|
||||
if width is None or height is None:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing dimensions",
|
||||
"errorDetails": "Both width and height are required"
|
||||
}
|
||||
|
||||
# Convert to internal units (nanometers)
|
||||
scale = 1000000 if unit == "mm" else 25400000 # mm or inch to nm
|
||||
width_nm = int(width * scale)
|
||||
height_nm = int(height * scale)
|
||||
|
||||
# Set board size
|
||||
board_box = self.board.GetBoardEdgesBoundingBox()
|
||||
board_box.SetSize(pcbnew.VECTOR2I(width_nm, height_nm))
|
||||
|
||||
# Update board outline
|
||||
self.board.SetBoardEdgesBoundingBox(board_box)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Set board size to {width}x{height} {unit}",
|
||||
"size": {
|
||||
"width": width,
|
||||
"height": height,
|
||||
"unit": unit
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting board size: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to set board size",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
171
python/commands/board/view.py
Normal file
171
python/commands/board/view.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
Board view command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import os
|
||||
import pcbnew
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, List, Tuple
|
||||
from PIL import Image
|
||||
import io
|
||||
import base64
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
|
||||
class BoardViewCommands:
|
||||
"""Handles board viewing operations"""
|
||||
|
||||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||||
"""Initialize with optional board instance"""
|
||||
self.board = board
|
||||
|
||||
def get_board_info(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get information about the current board"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
# Get board dimensions
|
||||
board_box = self.board.GetBoardEdgesBoundingBox()
|
||||
width_nm = board_box.GetWidth()
|
||||
height_nm = board_box.GetHeight()
|
||||
|
||||
# Convert to mm
|
||||
width_mm = width_nm / 1000000
|
||||
height_mm = height_nm / 1000000
|
||||
|
||||
# Get layer information
|
||||
layers = []
|
||||
for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
|
||||
if self.board.IsLayerEnabled(layer_id):
|
||||
layers.append({
|
||||
"name": self.board.GetLayerName(layer_id),
|
||||
"type": self._get_layer_type_name(self.board.GetLayerType(layer_id)),
|
||||
"id": layer_id
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"board": {
|
||||
"filename": self.board.GetFileName(),
|
||||
"size": {
|
||||
"width": width_mm,
|
||||
"height": height_mm,
|
||||
"unit": "mm"
|
||||
},
|
||||
"layers": layers,
|
||||
"title": self.board.GetTitleBlock().GetTitle(),
|
||||
"activeLayer": self.board.GetActiveLayer()
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting board info: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to get board information",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def get_board_2d_view(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get a 2D image of the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
# Get parameters
|
||||
width = params.get("width", 800)
|
||||
height = params.get("height", 600)
|
||||
format = params.get("format", "png")
|
||||
layers = params.get("layers", [])
|
||||
|
||||
# Create plot controller
|
||||
plotter = pcbnew.PLOT_CONTROLLER(self.board)
|
||||
|
||||
# Set up plot options
|
||||
plot_opts = plotter.GetPlotOptions()
|
||||
plot_opts.SetOutputDirectory(os.path.dirname(self.board.GetFileName()))
|
||||
plot_opts.SetScale(1)
|
||||
plot_opts.SetMirror(False)
|
||||
plot_opts.SetExcludeEdgeLayer(False)
|
||||
plot_opts.SetPlotFrameRef(False)
|
||||
plot_opts.SetPlotValue(True)
|
||||
plot_opts.SetPlotReference(True)
|
||||
|
||||
# Plot to SVG first (for vector output)
|
||||
temp_svg = os.path.join(os.path.dirname(self.board.GetFileName()), "temp_view.svg")
|
||||
plotter.OpenPlotfile("temp_view", pcbnew.PLOT_FORMAT_SVG, "Temporary View")
|
||||
|
||||
# Plot specified layers or all enabled layers
|
||||
if layers:
|
||||
for layer_name in layers:
|
||||
layer_id = self.board.GetLayerID(layer_name)
|
||||
if layer_id >= 0 and self.board.IsLayerEnabled(layer_id):
|
||||
plotter.PlotLayer(layer_id)
|
||||
else:
|
||||
for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
|
||||
if self.board.IsLayerEnabled(layer_id):
|
||||
plotter.PlotLayer(layer_id)
|
||||
|
||||
plotter.ClosePlot()
|
||||
|
||||
# Convert SVG to requested format
|
||||
if format == "svg":
|
||||
with open(temp_svg, 'r') as f:
|
||||
svg_data = f.read()
|
||||
os.remove(temp_svg)
|
||||
return {
|
||||
"success": True,
|
||||
"imageData": svg_data,
|
||||
"format": "svg"
|
||||
}
|
||||
else:
|
||||
# Use PIL to convert SVG to PNG/JPG
|
||||
from cairosvg import svg2png
|
||||
png_data = svg2png(url=temp_svg, output_width=width, output_height=height)
|
||||
os.remove(temp_svg)
|
||||
|
||||
if format == "jpg":
|
||||
# Convert PNG to JPG
|
||||
img = Image.open(io.BytesIO(png_data))
|
||||
jpg_buffer = io.BytesIO()
|
||||
img.convert('RGB').save(jpg_buffer, format='JPEG')
|
||||
jpg_data = jpg_buffer.getvalue()
|
||||
return {
|
||||
"success": True,
|
||||
"imageData": base64.b64encode(jpg_data).decode('utf-8'),
|
||||
"format": "jpg"
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": True,
|
||||
"imageData": base64.b64encode(png_data).decode('utf-8'),
|
||||
"format": "png"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting board 2D view: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to get board 2D view",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def _get_layer_type_name(self, type_id: int) -> str:
|
||||
"""Convert KiCAD layer type constant to name"""
|
||||
type_map = {
|
||||
pcbnew.LT_SIGNAL: "signal",
|
||||
pcbnew.LT_POWER: "power",
|
||||
pcbnew.LT_MIXED: "mixed",
|
||||
pcbnew.LT_JUMPER: "jumper",
|
||||
pcbnew.LT_USER: "user"
|
||||
}
|
||||
return type_map.get(type_id, "unknown")
|
||||
916
python/commands/component.py
Normal file
916
python/commands/component.py
Normal file
@@ -0,0 +1,916 @@
|
||||
"""
|
||||
Component-related command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import os
|
||||
import pcbnew
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict, Any, Optional, List, Tuple
|
||||
import base64
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
|
||||
class ComponentCommands:
|
||||
"""Handles component-related KiCAD operations"""
|
||||
|
||||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||||
"""Initialize with optional board instance"""
|
||||
self.board = board
|
||||
|
||||
def place_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Place a component on the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
# Get parameters
|
||||
component_id = params.get("componentId")
|
||||
position = params.get("position")
|
||||
reference = params.get("reference")
|
||||
value = params.get("value")
|
||||
footprint = params.get("footprint")
|
||||
rotation = params.get("rotation", 0)
|
||||
layer = params.get("layer", "F.Cu")
|
||||
|
||||
if not component_id or not position:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "componentId and position are required"
|
||||
}
|
||||
|
||||
# Create new module (footprint)
|
||||
module = pcbnew.FootprintLoad(self.board.GetLibraryPath(), component_id)
|
||||
if not module:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Component not found",
|
||||
"errorDetails": f"Could not find component: {component_id}"
|
||||
}
|
||||
|
||||
# Set position
|
||||
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
|
||||
x_nm = int(position["x"] * scale)
|
||||
y_nm = int(position["y"] * scale)
|
||||
module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
||||
|
||||
# Set reference if provided
|
||||
if reference:
|
||||
module.SetReference(reference)
|
||||
|
||||
# Set value if provided
|
||||
if value:
|
||||
module.SetValue(value)
|
||||
|
||||
# Set footprint if provided
|
||||
if footprint:
|
||||
module.SetFootprintName(footprint)
|
||||
|
||||
# Set rotation
|
||||
module.SetOrientation(rotation * 10) # KiCAD uses decidegrees
|
||||
|
||||
# Set layer
|
||||
layer_id = self.board.GetLayerID(layer)
|
||||
if layer_id >= 0:
|
||||
module.SetLayer(layer_id)
|
||||
|
||||
# Add to board
|
||||
self.board.Add(module)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Placed component: {component_id}",
|
||||
"component": {
|
||||
"reference": module.GetReference(),
|
||||
"value": module.GetValue(),
|
||||
"position": {
|
||||
"x": position["x"],
|
||||
"y": position["y"],
|
||||
"unit": position["unit"]
|
||||
},
|
||||
"rotation": rotation,
|
||||
"layer": layer
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error placing component: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to place component",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def move_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Move an existing component to a new position"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
reference = params.get("reference")
|
||||
position = params.get("position")
|
||||
rotation = params.get("rotation")
|
||||
|
||||
if not reference or not position:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "reference and position are required"
|
||||
}
|
||||
|
||||
# Find the component
|
||||
module = self.board.FindFootprintByReference(reference)
|
||||
if not module:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Component not found",
|
||||
"errorDetails": f"Could not find component: {reference}"
|
||||
}
|
||||
|
||||
# Set new position
|
||||
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
|
||||
x_nm = int(position["x"] * scale)
|
||||
y_nm = int(position["y"] * scale)
|
||||
module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
||||
|
||||
# Set new rotation if provided
|
||||
if rotation is not None:
|
||||
module.SetOrientation(rotation * 10) # KiCAD uses decidegrees
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Moved component: {reference}",
|
||||
"component": {
|
||||
"reference": reference,
|
||||
"position": {
|
||||
"x": position["x"],
|
||||
"y": position["y"],
|
||||
"unit": position["unit"]
|
||||
},
|
||||
"rotation": rotation if rotation is not None else module.GetOrientation() / 10
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error moving component: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to move component",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def rotate_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Rotate an existing component"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
reference = params.get("reference")
|
||||
angle = params.get("angle")
|
||||
|
||||
if not reference or angle is None:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "reference and angle are required"
|
||||
}
|
||||
|
||||
# Find the component
|
||||
module = self.board.FindFootprintByReference(reference)
|
||||
if not module:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Component not found",
|
||||
"errorDetails": f"Could not find component: {reference}"
|
||||
}
|
||||
|
||||
# Set rotation
|
||||
module.SetOrientation(angle * 10) # KiCAD uses decidegrees
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Rotated component: {reference}",
|
||||
"component": {
|
||||
"reference": reference,
|
||||
"rotation": angle
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error rotating component: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to rotate component",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def delete_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Delete a component from the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
reference = params.get("reference")
|
||||
if not reference:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing reference",
|
||||
"errorDetails": "reference parameter is required"
|
||||
}
|
||||
|
||||
# Find the component
|
||||
module = self.board.FindFootprintByReference(reference)
|
||||
if not module:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Component not found",
|
||||
"errorDetails": f"Could not find component: {reference}"
|
||||
}
|
||||
|
||||
# Remove from board
|
||||
self.board.Remove(module)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Deleted component: {reference}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting component: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to delete component",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def edit_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Edit the properties of an existing component"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
reference = params.get("reference")
|
||||
new_reference = params.get("newReference")
|
||||
value = params.get("value")
|
||||
footprint = params.get("footprint")
|
||||
|
||||
if not reference:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing reference",
|
||||
"errorDetails": "reference parameter is required"
|
||||
}
|
||||
|
||||
# Find the component
|
||||
module = self.board.FindFootprintByReference(reference)
|
||||
if not module:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Component not found",
|
||||
"errorDetails": f"Could not find component: {reference}"
|
||||
}
|
||||
|
||||
# Update properties
|
||||
if new_reference:
|
||||
module.SetReference(new_reference)
|
||||
if value:
|
||||
module.SetValue(value)
|
||||
if footprint:
|
||||
module.SetFootprintName(footprint)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Updated component: {reference}",
|
||||
"component": {
|
||||
"reference": new_reference or reference,
|
||||
"value": value or module.GetValue(),
|
||||
"footprint": footprint or module.GetFootprintName()
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error editing component: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to edit component",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def get_component_properties(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get detailed properties of a component"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
reference = params.get("reference")
|
||||
if not reference:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing reference",
|
||||
"errorDetails": "reference parameter is required"
|
||||
}
|
||||
|
||||
# Find the component
|
||||
module = self.board.FindFootprintByReference(reference)
|
||||
if not module:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Component not found",
|
||||
"errorDetails": f"Could not find component: {reference}"
|
||||
}
|
||||
|
||||
# Get position in mm
|
||||
pos = module.GetPosition()
|
||||
x_mm = pos.x / 1000000
|
||||
y_mm = pos.y / 1000000
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"component": {
|
||||
"reference": module.GetReference(),
|
||||
"value": module.GetValue(),
|
||||
"footprint": module.GetFootprintName(),
|
||||
"position": {
|
||||
"x": x_mm,
|
||||
"y": y_mm,
|
||||
"unit": "mm"
|
||||
},
|
||||
"rotation": module.GetOrientation() / 10,
|
||||
"layer": self.board.GetLayerName(module.GetLayer()),
|
||||
"attributes": {
|
||||
"smd": module.GetAttributes() & pcbnew.FP_SMD,
|
||||
"through_hole": module.GetAttributes() & pcbnew.FP_THROUGH_HOLE,
|
||||
"virtual": module.GetAttributes() & pcbnew.FP_VIRTUAL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting component properties: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to get component properties",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def get_component_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get a list of all components on the board"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
components = []
|
||||
for module in self.board.GetFootprints():
|
||||
pos = module.GetPosition()
|
||||
x_mm = pos.x / 1000000
|
||||
y_mm = pos.y / 1000000
|
||||
|
||||
components.append({
|
||||
"reference": module.GetReference(),
|
||||
"value": module.GetValue(),
|
||||
"footprint": module.GetFootprintName(),
|
||||
"position": {
|
||||
"x": x_mm,
|
||||
"y": y_mm,
|
||||
"unit": "mm"
|
||||
},
|
||||
"rotation": module.GetOrientation() / 10,
|
||||
"layer": self.board.GetLayerName(module.GetLayer())
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"components": components
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting component list: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to get component list",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def place_component_array(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Place an array of components in a grid or circular pattern"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
component_id = params.get("componentId")
|
||||
pattern = params.get("pattern", "grid") # grid or circular
|
||||
count = params.get("count")
|
||||
reference_prefix = params.get("referencePrefix", "U")
|
||||
value = params.get("value")
|
||||
|
||||
if not component_id or not count:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "componentId and count are required"
|
||||
}
|
||||
|
||||
if pattern == "grid":
|
||||
start_position = params.get("startPosition")
|
||||
rows = params.get("rows")
|
||||
columns = params.get("columns")
|
||||
spacing_x = params.get("spacingX")
|
||||
spacing_y = params.get("spacingY")
|
||||
rotation = params.get("rotation", 0)
|
||||
layer = params.get("layer", "F.Cu")
|
||||
|
||||
if not start_position or not rows or not columns or not spacing_x or not spacing_y:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing grid parameters",
|
||||
"errorDetails": "For grid pattern, startPosition, rows, columns, spacingX, and spacingY are required"
|
||||
}
|
||||
|
||||
if rows * columns != count:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid grid parameters",
|
||||
"errorDetails": "rows * columns must equal count"
|
||||
}
|
||||
|
||||
placed_components = self._place_grid_array(
|
||||
component_id,
|
||||
start_position,
|
||||
rows,
|
||||
columns,
|
||||
spacing_x,
|
||||
spacing_y,
|
||||
reference_prefix,
|
||||
value,
|
||||
rotation,
|
||||
layer
|
||||
)
|
||||
|
||||
elif pattern == "circular":
|
||||
center = params.get("center")
|
||||
radius = params.get("radius")
|
||||
angle_start = params.get("angleStart", 0)
|
||||
angle_step = params.get("angleStep")
|
||||
rotation_offset = params.get("rotationOffset", 0)
|
||||
layer = params.get("layer", "F.Cu")
|
||||
|
||||
if not center or not radius or not angle_step:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing circular parameters",
|
||||
"errorDetails": "For circular pattern, center, radius, and angleStep are required"
|
||||
}
|
||||
|
||||
placed_components = self._place_circular_array(
|
||||
component_id,
|
||||
center,
|
||||
radius,
|
||||
count,
|
||||
angle_start,
|
||||
angle_step,
|
||||
reference_prefix,
|
||||
value,
|
||||
rotation_offset,
|
||||
layer
|
||||
)
|
||||
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid pattern",
|
||||
"errorDetails": "Pattern must be 'grid' or 'circular'"
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Placed {count} components in {pattern} pattern",
|
||||
"components": placed_components
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error placing component array: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to place component array",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def align_components(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Align multiple components along a line or distribute them evenly"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
references = params.get("references", [])
|
||||
alignment = params.get("alignment", "horizontal") # horizontal, vertical, or edge
|
||||
distribution = params.get("distribution", "none") # none, equal, or spacing
|
||||
spacing = params.get("spacing")
|
||||
|
||||
if not references or len(references) < 2:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing references",
|
||||
"errorDetails": "At least two component references are required"
|
||||
}
|
||||
|
||||
# Find all referenced components
|
||||
components = []
|
||||
for ref in references:
|
||||
module = self.board.FindFootprintByReference(ref)
|
||||
if not module:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Component not found",
|
||||
"errorDetails": f"Could not find component: {ref}"
|
||||
}
|
||||
components.append(module)
|
||||
|
||||
# Perform alignment based on selected option
|
||||
if alignment == "horizontal":
|
||||
self._align_components_horizontally(components, distribution, spacing)
|
||||
elif alignment == "vertical":
|
||||
self._align_components_vertically(components, distribution, spacing)
|
||||
elif alignment == "edge":
|
||||
edge = params.get("edge")
|
||||
if not edge:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing edge parameter",
|
||||
"errorDetails": "Edge parameter is required for edge alignment"
|
||||
}
|
||||
self._align_components_to_edge(components, edge)
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid alignment option",
|
||||
"errorDetails": "Alignment must be 'horizontal', 'vertical', or 'edge'"
|
||||
}
|
||||
|
||||
# Prepare result data
|
||||
aligned_components = []
|
||||
for module in components:
|
||||
pos = module.GetPosition()
|
||||
aligned_components.append({
|
||||
"reference": module.GetReference(),
|
||||
"position": {
|
||||
"x": pos.x / 1000000,
|
||||
"y": pos.y / 1000000,
|
||||
"unit": "mm"
|
||||
},
|
||||
"rotation": module.GetOrientation() / 10
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Aligned {len(components)} components",
|
||||
"alignment": alignment,
|
||||
"distribution": distribution,
|
||||
"components": aligned_components
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error aligning components: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to align components",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def duplicate_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Duplicate an existing component"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
reference = params.get("reference")
|
||||
new_reference = params.get("newReference")
|
||||
position = params.get("position")
|
||||
rotation = params.get("rotation")
|
||||
|
||||
if not reference or not new_reference:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "reference and newReference are required"
|
||||
}
|
||||
|
||||
# Find the source component
|
||||
source = self.board.FindFootprintByReference(reference)
|
||||
if not source:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Component not found",
|
||||
"errorDetails": f"Could not find component: {reference}"
|
||||
}
|
||||
|
||||
# Check if new reference already exists
|
||||
if self.board.FindFootprintByReference(new_reference):
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Reference already exists",
|
||||
"errorDetails": f"A component with reference {new_reference} already exists"
|
||||
}
|
||||
|
||||
# Create new footprint with the same properties
|
||||
new_module = pcbnew.FOOTPRINT(self.board)
|
||||
new_module.SetFootprintName(source.GetFootprintName())
|
||||
new_module.SetValue(source.GetValue())
|
||||
new_module.SetReference(new_reference)
|
||||
new_module.SetLayer(source.GetLayer())
|
||||
|
||||
# Copy pads and other items
|
||||
for pad in source.Pads():
|
||||
new_pad = pcbnew.PAD(new_module)
|
||||
new_pad.Copy(pad)
|
||||
new_module.Add(new_pad)
|
||||
|
||||
# Set position if provided, otherwise use offset from original
|
||||
if position:
|
||||
scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000
|
||||
x_nm = int(position["x"] * scale)
|
||||
y_nm = int(position["y"] * scale)
|
||||
new_module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
||||
else:
|
||||
# Offset by 5mm
|
||||
source_pos = source.GetPosition()
|
||||
new_module.SetPosition(pcbnew.VECTOR2I(source_pos.x + 5000000, source_pos.y))
|
||||
|
||||
# Set rotation if provided, otherwise use same as original
|
||||
if rotation is not None:
|
||||
new_module.SetOrientation(rotation * 10) # KiCAD uses decidegrees
|
||||
else:
|
||||
new_module.SetOrientation(source.GetOrientation())
|
||||
|
||||
# Add to board
|
||||
self.board.Add(new_module)
|
||||
|
||||
# Get final position in mm
|
||||
pos = new_module.GetPosition()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Duplicated component {reference} to {new_reference}",
|
||||
"component": {
|
||||
"reference": new_reference,
|
||||
"value": new_module.GetValue(),
|
||||
"footprint": new_module.GetFootprintName(),
|
||||
"position": {
|
||||
"x": pos.x / 1000000,
|
||||
"y": pos.y / 1000000,
|
||||
"unit": "mm"
|
||||
},
|
||||
"rotation": new_module.GetOrientation() / 10,
|
||||
"layer": self.board.GetLayerName(new_module.GetLayer())
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error duplicating component: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to duplicate component",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def _place_grid_array(self, component_id: str, start_position: Dict[str, Any],
|
||||
rows: int, columns: int, spacing_x: float, spacing_y: float,
|
||||
reference_prefix: str, value: str, rotation: float, layer: str) -> List[Dict[str, Any]]:
|
||||
"""Place components in a grid pattern and return the list of placed components"""
|
||||
placed = []
|
||||
|
||||
# Convert spacing to nm
|
||||
unit = start_position.get("unit", "mm")
|
||||
scale = 1000000 if unit == "mm" else 25400000 # mm or inch to nm
|
||||
spacing_x_nm = int(spacing_x * scale)
|
||||
spacing_y_nm = int(spacing_y * scale)
|
||||
|
||||
# Get layer ID
|
||||
layer_id = self.board.GetLayerID(layer)
|
||||
|
||||
for row in range(rows):
|
||||
for col in range(columns):
|
||||
# Calculate position
|
||||
x = start_position["x"] + (col * spacing_x)
|
||||
y = start_position["y"] + (row * spacing_y)
|
||||
|
||||
# Generate reference
|
||||
index = row * columns + col + 1
|
||||
component_reference = f"{reference_prefix}{index}"
|
||||
|
||||
# Place component
|
||||
result = self.place_component({
|
||||
"componentId": component_id,
|
||||
"position": {"x": x, "y": y, "unit": unit},
|
||||
"reference": component_reference,
|
||||
"value": value,
|
||||
"rotation": rotation,
|
||||
"layer": layer
|
||||
})
|
||||
|
||||
if result["success"]:
|
||||
placed.append(result["component"])
|
||||
|
||||
return placed
|
||||
|
||||
def _place_circular_array(self, component_id: str, center: Dict[str, Any],
|
||||
radius: float, count: int, angle_start: float,
|
||||
angle_step: float, reference_prefix: str,
|
||||
value: str, rotation_offset: float, layer: str) -> List[Dict[str, Any]]:
|
||||
"""Place components in a circular pattern and return the list of placed components"""
|
||||
placed = []
|
||||
|
||||
# Get unit
|
||||
unit = center.get("unit", "mm")
|
||||
|
||||
for i in range(count):
|
||||
# Calculate angle for this component
|
||||
angle = angle_start + (i * angle_step)
|
||||
angle_rad = math.radians(angle)
|
||||
|
||||
# Calculate position
|
||||
x = center["x"] + (radius * math.cos(angle_rad))
|
||||
y = center["y"] + (radius * math.sin(angle_rad))
|
||||
|
||||
# Generate reference
|
||||
component_reference = f"{reference_prefix}{i+1}"
|
||||
|
||||
# Calculate rotation (pointing outward from center)
|
||||
component_rotation = angle + rotation_offset
|
||||
|
||||
# Place component
|
||||
result = self.place_component({
|
||||
"componentId": component_id,
|
||||
"position": {"x": x, "y": y, "unit": unit},
|
||||
"reference": component_reference,
|
||||
"value": value,
|
||||
"rotation": component_rotation,
|
||||
"layer": layer
|
||||
})
|
||||
|
||||
if result["success"]:
|
||||
placed.append(result["component"])
|
||||
|
||||
return placed
|
||||
|
||||
def _align_components_horizontally(self, components: List[pcbnew.FOOTPRINT],
|
||||
distribution: str, spacing: Optional[float]) -> None:
|
||||
"""Align components horizontally and optionally distribute them"""
|
||||
if not components:
|
||||
return
|
||||
|
||||
# Find the average Y coordinate
|
||||
y_sum = sum(module.GetPosition().y for module in components)
|
||||
y_avg = y_sum // len(components)
|
||||
|
||||
# Sort components by X position
|
||||
components.sort(key=lambda m: m.GetPosition().x)
|
||||
|
||||
# Set Y coordinate for all components
|
||||
for module in components:
|
||||
pos = module.GetPosition()
|
||||
module.SetPosition(pcbnew.VECTOR2I(pos.x, y_avg))
|
||||
|
||||
# Handle distribution if requested
|
||||
if distribution == "equal" and len(components) > 1:
|
||||
# Get leftmost and rightmost X coordinates
|
||||
x_min = components[0].GetPosition().x
|
||||
x_max = components[-1].GetPosition().x
|
||||
|
||||
# Calculate equal spacing
|
||||
total_space = x_max - x_min
|
||||
spacing_nm = total_space // (len(components) - 1)
|
||||
|
||||
# Set X positions with equal spacing
|
||||
for i in range(1, len(components) - 1):
|
||||
pos = components[i].GetPosition()
|
||||
new_x = x_min + (i * spacing_nm)
|
||||
components[i].SetPosition(pcbnew.VECTOR2I(new_x, pos.y))
|
||||
|
||||
elif distribution == "spacing" and spacing is not None:
|
||||
# Convert spacing to nanometers
|
||||
spacing_nm = int(spacing * 1000000) # assuming mm
|
||||
|
||||
# Set X positions with the specified spacing
|
||||
x_current = components[0].GetPosition().x
|
||||
for i in range(1, len(components)):
|
||||
pos = components[i].GetPosition()
|
||||
x_current += spacing_nm
|
||||
components[i].SetPosition(pcbnew.VECTOR2I(x_current, pos.y))
|
||||
|
||||
def _align_components_vertically(self, components: List[pcbnew.FOOTPRINT],
|
||||
distribution: str, spacing: Optional[float]) -> None:
|
||||
"""Align components vertically and optionally distribute them"""
|
||||
if not components:
|
||||
return
|
||||
|
||||
# Find the average X coordinate
|
||||
x_sum = sum(module.GetPosition().x for module in components)
|
||||
x_avg = x_sum // len(components)
|
||||
|
||||
# Sort components by Y position
|
||||
components.sort(key=lambda m: m.GetPosition().y)
|
||||
|
||||
# Set X coordinate for all components
|
||||
for module in components:
|
||||
pos = module.GetPosition()
|
||||
module.SetPosition(pcbnew.VECTOR2I(x_avg, pos.y))
|
||||
|
||||
# Handle distribution if requested
|
||||
if distribution == "equal" and len(components) > 1:
|
||||
# Get topmost and bottommost Y coordinates
|
||||
y_min = components[0].GetPosition().y
|
||||
y_max = components[-1].GetPosition().y
|
||||
|
||||
# Calculate equal spacing
|
||||
total_space = y_max - y_min
|
||||
spacing_nm = total_space // (len(components) - 1)
|
||||
|
||||
# Set Y positions with equal spacing
|
||||
for i in range(1, len(components) - 1):
|
||||
pos = components[i].GetPosition()
|
||||
new_y = y_min + (i * spacing_nm)
|
||||
components[i].SetPosition(pcbnew.VECTOR2I(pos.x, new_y))
|
||||
|
||||
elif distribution == "spacing" and spacing is not None:
|
||||
# Convert spacing to nanometers
|
||||
spacing_nm = int(spacing * 1000000) # assuming mm
|
||||
|
||||
# Set Y positions with the specified spacing
|
||||
y_current = components[0].GetPosition().y
|
||||
for i in range(1, len(components)):
|
||||
pos = components[i].GetPosition()
|
||||
y_current += spacing_nm
|
||||
components[i].SetPosition(pcbnew.VECTOR2I(pos.x, y_current))
|
||||
|
||||
def _align_components_to_edge(self, components: List[pcbnew.FOOTPRINT], edge: str) -> None:
|
||||
"""Align components to the specified edge of the board"""
|
||||
if not components:
|
||||
return
|
||||
|
||||
# Get board bounds
|
||||
board_box = self.board.GetBoardEdgesBoundingBox()
|
||||
left = board_box.GetLeft()
|
||||
right = board_box.GetRight()
|
||||
top = board_box.GetTop()
|
||||
bottom = board_box.GetBottom()
|
||||
|
||||
# Align based on specified edge
|
||||
if edge == "left":
|
||||
for module in components:
|
||||
pos = module.GetPosition()
|
||||
module.SetPosition(pcbnew.VECTOR2I(left + 2000000, pos.y)) # 2mm offset from edge
|
||||
elif edge == "right":
|
||||
for module in components:
|
||||
pos = module.GetPosition()
|
||||
module.SetPosition(pcbnew.VECTOR2I(right - 2000000, pos.y)) # 2mm offset from edge
|
||||
elif edge == "top":
|
||||
for module in components:
|
||||
pos = module.GetPosition()
|
||||
module.SetPosition(pcbnew.VECTOR2I(pos.x, top + 2000000)) # 2mm offset from edge
|
||||
elif edge == "bottom":
|
||||
for module in components:
|
||||
pos = module.GetPosition()
|
||||
module.SetPosition(pcbnew.VECTOR2I(pos.x, bottom - 2000000)) # 2mm offset from edge
|
||||
else:
|
||||
logger.warning(f"Unknown edge alignment: {edge}")
|
||||
165
python/commands/component_schematic.py
Normal file
165
python/commands/component_schematic.py
Normal file
@@ -0,0 +1,165 @@
|
||||
from skip import Schematic
|
||||
# Symbol class might not be directly importable in the current version
|
||||
import os
|
||||
|
||||
class ComponentManager:
|
||||
"""Manage components in a schematic"""
|
||||
|
||||
@staticmethod
|
||||
def add_component(schematic: Schematic, component_def: dict):
|
||||
"""Add a component to the schematic"""
|
||||
try:
|
||||
# Create a new symbol
|
||||
symbol = schematic.add_symbol(
|
||||
lib=component_def.get('library', 'Device'),
|
||||
name=component_def.get('type', 'R'), # Default to Resistor symbol 'R'
|
||||
reference=component_def.get('reference', 'R?'),
|
||||
at=[component_def.get('x', 0), component_def.get('y', 0)],
|
||||
unit=component_def.get('unit', 1),
|
||||
rotation=component_def.get('rotation', 0)
|
||||
)
|
||||
|
||||
# Set properties
|
||||
if 'value' in component_def:
|
||||
symbol.property.Value.value = component_def['value']
|
||||
if 'footprint' in component_def:
|
||||
symbol.property.Footprint.value = component_def['footprint']
|
||||
if 'datasheet' in component_def:
|
||||
symbol.property.Datasheet.value = component_def['datasheet']
|
||||
|
||||
# Add additional properties
|
||||
for key, value in component_def.get('properties', {}).items():
|
||||
# Avoid overwriting standard properties unless explicitly intended
|
||||
if key not in ['Reference', 'Value', 'Footprint', 'Datasheet']:
|
||||
symbol.property.append(key, value)
|
||||
|
||||
print(f"Added component {symbol.reference} ({symbol.name}) to schematic.")
|
||||
return symbol
|
||||
except Exception as e:
|
||||
print(f"Error adding component: {e}")
|
||||
return None
|
||||
|
||||
@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")
|
||||
91
python/commands/connection_schematic.py
Normal file
91
python/commands/connection_schematic.py
Normal file
@@ -0,0 +1,91 @@
|
||||
from skip import Schematic
|
||||
# Wire and Net classes might not be directly importable in the current version
|
||||
import os
|
||||
|
||||
class ConnectionManager:
|
||||
"""Manage connections between components"""
|
||||
|
||||
@staticmethod
|
||||
def add_wire(schematic: Schematic, start_point: list, end_point: list, properties: dict = None):
|
||||
"""Add a wire between two points"""
|
||||
try:
|
||||
wire = schematic.add_wire(start=start_point, end=end_point)
|
||||
# kicad-skip wire properties are limited, but we can potentially
|
||||
# add graphical properties if needed in the future.
|
||||
print(f"Added wire from {start_point} to {end_point}.")
|
||||
return wire
|
||||
except Exception as e:
|
||||
print(f"Error adding wire: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def add_connection(schematic: Schematic, source_ref: str, source_pin: str, target_ref: str, target_pin: str):
|
||||
"""Add a connection between component pins"""
|
||||
# kicad-skip handles connections implicitly through wires and labels.
|
||||
# This method would typically involve adding wires and potentially net labels
|
||||
# to connect the specified pins.
|
||||
# A direct 'add_connection' between pins isn't a standard kicad-skip operation
|
||||
# in the way it is in some other schematic tools.
|
||||
# We will need to implement this logic by finding the component pins
|
||||
# and adding wires/labels between their locations. This is more complex
|
||||
# and might require pin location information which isn't directly
|
||||
# exposed in a simple way by default in kicad-skip Symbol objects.
|
||||
|
||||
# For now, this method will be a placeholder or require a more advanced
|
||||
# implementation based on how kicad-skip handles net connections.
|
||||
# A common approach is to add wires between graphical points and then
|
||||
# add net labels to define the net name.
|
||||
|
||||
print(f"Attempted to add connection between {source_ref}/{source_pin} and {target_ref}/{target_pin}. This requires advanced implementation.")
|
||||
return False # Indicate not fully implemented yet
|
||||
|
||||
@staticmethod
|
||||
def remove_connection(schematic: Schematic, connection_id: str):
|
||||
"""Remove a connection"""
|
||||
# Removing connections in kicad-skip typically means removing the wires
|
||||
# or net labels that form the connection.
|
||||
# This method would need to identify the relevant graphical elements
|
||||
# based on a connection identifier (which we would need to define).
|
||||
# This is also an advanced implementation task.
|
||||
print(f"Attempted to remove connection with ID {connection_id}. This requires advanced implementation.")
|
||||
return False # Indicate not fully implemented yet
|
||||
|
||||
@staticmethod
|
||||
def get_net_connections(schematic: Schematic, net_name: str):
|
||||
"""Get all connections in a named net"""
|
||||
# kicad-skip represents nets implicitly through connected wires and net labels.
|
||||
# To get connections for a net, we would need to iterate through wires
|
||||
# and net labels to build a list of connected pins/points.
|
||||
# This requires traversing the schematic's graphical elements and understanding
|
||||
# how they form nets. This is an advanced implementation task.
|
||||
print(f"Attempted to get connections for net '{net_name}'. This requires advanced implementation.")
|
||||
return [] # Return empty list for now
|
||||
|
||||
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("ConnectionTestSchematic")
|
||||
|
||||
# Add some wires
|
||||
wire1 = ConnectionManager.add_wire(test_sch, [100, 100], [200, 100])
|
||||
wire2 = ConnectionManager.add_wire(test_sch, [200, 100], [200, 200])
|
||||
|
||||
# Note: add_connection, remove_connection, get_net_connections are placeholders
|
||||
# and require more complex implementation based on kicad-skip's structure.
|
||||
|
||||
# Example of how you might add a net label (requires finding a point on a wire)
|
||||
# from skip import Label
|
||||
# if wire1:
|
||||
# net_label_pos = wire1.start # Or calculate a point on the wire
|
||||
# net_label = test_sch.add_label(text="Net_01", at=net_label_pos)
|
||||
# print(f"Added net label 'Net_01' at {net_label_pos}")
|
||||
|
||||
# Save the schematic (optional)
|
||||
# SchematicManager.save_schematic(test_sch, "connection_test.kicad_sch")
|
||||
|
||||
# Clean up (if saved)
|
||||
# if os.path.exists("connection_test.kicad_sch"):
|
||||
# os.remove("connection_test.kicad_sch")
|
||||
# print("Cleaned up connection_test.kicad_sch")
|
||||
239
python/commands/design_rules.py
Normal file
239
python/commands/design_rules.py
Normal file
@@ -0,0 +1,239 @@
|
||||
"""
|
||||
Design rules command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import os
|
||||
import pcbnew
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, List, Tuple
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
|
||||
class DesignRuleCommands:
|
||||
"""Handles design rule checking and configuration"""
|
||||
|
||||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||||
"""Initialize with optional board instance"""
|
||||
self.board = board
|
||||
|
||||
def set_design_rules(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Set design rules for the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
design_settings = self.board.GetDesignSettings()
|
||||
|
||||
# Convert mm to nanometers for KiCAD internal units
|
||||
scale = 1000000 # mm to nm
|
||||
|
||||
# Set clearance
|
||||
if "clearance" in params:
|
||||
design_settings.SetMinClearance(int(params["clearance"] * scale))
|
||||
|
||||
# Set track width
|
||||
if "trackWidth" in params:
|
||||
design_settings.SetCurrentTrackWidth(int(params["trackWidth"] * scale))
|
||||
|
||||
# Set via settings
|
||||
if "viaDiameter" in params:
|
||||
design_settings.SetCurrentViaSize(int(params["viaDiameter"] * scale))
|
||||
if "viaDrill" in params:
|
||||
design_settings.SetCurrentViaDrill(int(params["viaDrill"] * scale))
|
||||
|
||||
# Set micro via settings
|
||||
if "microViaDiameter" in params:
|
||||
design_settings.SetCurrentMicroViaSize(int(params["microViaDiameter"] * scale))
|
||||
if "microViaDrill" in params:
|
||||
design_settings.SetCurrentMicroViaDrill(int(params["microViaDrill"] * scale))
|
||||
|
||||
# Set minimum values
|
||||
if "minTrackWidth" in params:
|
||||
design_settings.m_TrackMinWidth = int(params["minTrackWidth"] * scale)
|
||||
if "minViaDiameter" in params:
|
||||
design_settings.m_ViasMinSize = int(params["minViaDiameter"] * scale)
|
||||
if "minViaDrill" in params:
|
||||
design_settings.m_ViasMinDrill = int(params["minViaDrill"] * scale)
|
||||
if "minMicroViaDiameter" in params:
|
||||
design_settings.m_MicroViasMinSize = int(params["minMicroViaDiameter"] * scale)
|
||||
if "minMicroViaDrill" in params:
|
||||
design_settings.m_MicroViasMinDrill = int(params["minMicroViaDrill"] * scale)
|
||||
|
||||
# Set hole diameter
|
||||
if "minHoleDiameter" in params:
|
||||
design_settings.m_MinHoleDiameter = int(params["minHoleDiameter"] * scale)
|
||||
|
||||
# Set courtyard settings
|
||||
if "requireCourtyard" in params:
|
||||
design_settings.m_RequireCourtyards = params["requireCourtyard"]
|
||||
if "courtyardClearance" in params:
|
||||
design_settings.m_CourtyardMinClearance = int(params["courtyardClearance"] * scale)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Updated design rules",
|
||||
"rules": {
|
||||
"clearance": design_settings.GetMinClearance() / scale,
|
||||
"trackWidth": design_settings.GetCurrentTrackWidth() / scale,
|
||||
"viaDiameter": design_settings.GetCurrentViaSize() / scale,
|
||||
"viaDrill": design_settings.GetCurrentViaDrill() / scale,
|
||||
"microViaDiameter": design_settings.GetCurrentMicroViaSize() / scale,
|
||||
"microViaDrill": design_settings.GetCurrentMicroViaDrill() / scale,
|
||||
"minTrackWidth": design_settings.m_TrackMinWidth / scale,
|
||||
"minViaDiameter": design_settings.m_ViasMinSize / scale,
|
||||
"minViaDrill": design_settings.m_ViasMinDrill / scale,
|
||||
"minMicroViaDiameter": design_settings.m_MicroViasMinSize / scale,
|
||||
"minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale,
|
||||
"minHoleDiameter": design_settings.m_MinHoleDiameter / scale,
|
||||
"requireCourtyard": design_settings.m_RequireCourtyards,
|
||||
"courtyardClearance": design_settings.m_CourtyardMinClearance / scale
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting design rules: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to set design rules",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def get_design_rules(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get current design rules"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
design_settings = self.board.GetDesignSettings()
|
||||
scale = 1000000 # nm to mm
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"rules": {
|
||||
"clearance": design_settings.GetMinClearance() / scale,
|
||||
"trackWidth": design_settings.GetCurrentTrackWidth() / scale,
|
||||
"viaDiameter": design_settings.GetCurrentViaSize() / scale,
|
||||
"viaDrill": design_settings.GetCurrentViaDrill() / scale,
|
||||
"microViaDiameter": design_settings.GetCurrentMicroViaSize() / scale,
|
||||
"microViaDrill": design_settings.GetCurrentMicroViaDrill() / scale,
|
||||
"minTrackWidth": design_settings.m_TrackMinWidth / scale,
|
||||
"minViaDiameter": design_settings.m_ViasMinSize / scale,
|
||||
"minViaDrill": design_settings.m_ViasMinDrill / scale,
|
||||
"minMicroViaDiameter": design_settings.m_MicroViasMinSize / scale,
|
||||
"minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale,
|
||||
"minHoleDiameter": design_settings.m_MinHoleDiameter / scale,
|
||||
"requireCourtyard": design_settings.m_RequireCourtyards,
|
||||
"courtyardClearance": design_settings.m_CourtyardMinClearance / scale
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting design rules: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to get design rules",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def run_drc(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Run Design Rule Check"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
report_path = params.get("reportPath")
|
||||
|
||||
# Create DRC runner
|
||||
drc = pcbnew.DRC(self.board)
|
||||
|
||||
# Run DRC
|
||||
drc.Run()
|
||||
|
||||
# Get violations
|
||||
violations = []
|
||||
for marker in drc.GetMarkers():
|
||||
violations.append({
|
||||
"type": marker.GetErrorCode(),
|
||||
"severity": "error",
|
||||
"message": marker.GetDescription(),
|
||||
"location": {
|
||||
"x": marker.GetPos().x / 1000000,
|
||||
"y": marker.GetPos().y / 1000000,
|
||||
"unit": "mm"
|
||||
}
|
||||
})
|
||||
|
||||
# Save report if path provided
|
||||
if report_path:
|
||||
report_path = os.path.abspath(os.path.expanduser(report_path))
|
||||
drc.WriteReport(report_path)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Found {len(violations)} DRC violations",
|
||||
"violations": violations,
|
||||
"reportPath": report_path if report_path else None
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error running DRC: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to run DRC",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def get_drc_violations(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get list of DRC violations"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
severity = params.get("severity", "all")
|
||||
|
||||
# Get DRC markers
|
||||
violations = []
|
||||
for marker in self.board.GetDRCMarkers():
|
||||
violation = {
|
||||
"type": marker.GetErrorCode(),
|
||||
"severity": "error", # KiCAD DRC markers are always errors
|
||||
"message": marker.GetDescription(),
|
||||
"location": {
|
||||
"x": marker.GetPos().x / 1000000,
|
||||
"y": marker.GetPos().y / 1000000,
|
||||
"unit": "mm"
|
||||
}
|
||||
}
|
||||
|
||||
# Filter by severity if specified
|
||||
if severity == "all" or severity == violation["severity"]:
|
||||
violations.append(violation)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"violations": violations
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting DRC violations: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to get DRC violations",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
475
python/commands/export.py
Normal file
475
python/commands/export.py
Normal file
@@ -0,0 +1,475 @@
|
||||
"""
|
||||
Export command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import os
|
||||
import pcbnew
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, List, Tuple
|
||||
import base64
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
|
||||
class ExportCommands:
|
||||
"""Handles export-related KiCAD operations"""
|
||||
|
||||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||||
"""Initialize with optional board instance"""
|
||||
self.board = board
|
||||
|
||||
def export_gerber(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Export Gerber files"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
output_dir = params.get("outputDir")
|
||||
layers = params.get("layers", [])
|
||||
use_protel_extensions = params.get("useProtelExtensions", False)
|
||||
generate_drill_files = params.get("generateDrillFiles", True)
|
||||
generate_map_file = params.get("generateMapFile", False)
|
||||
use_aux_origin = params.get("useAuxOrigin", False)
|
||||
|
||||
if not output_dir:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing output directory",
|
||||
"errorDetails": "outputDir parameter is required"
|
||||
}
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
output_dir = os.path.abspath(os.path.expanduser(output_dir))
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Create plot controller
|
||||
plotter = pcbnew.PLOT_CONTROLLER(self.board)
|
||||
|
||||
# Set up plot options
|
||||
plot_opts = plotter.GetPlotOptions()
|
||||
plot_opts.SetOutputDirectory(output_dir)
|
||||
plot_opts.SetFormat(pcbnew.PLOT_FORMAT_GERBER)
|
||||
plot_opts.SetUseGerberProtelExtensions(use_protel_extensions)
|
||||
plot_opts.SetUseAuxOrigin(use_aux_origin)
|
||||
plot_opts.SetCreateGerberJobFile(generate_map_file)
|
||||
plot_opts.SetSubtractMaskFromSilk(True)
|
||||
|
||||
# Plot specified layers or all copper layers
|
||||
plotted_layers = []
|
||||
if layers:
|
||||
for layer_name in layers:
|
||||
layer_id = self.board.GetLayerID(layer_name)
|
||||
if layer_id >= 0:
|
||||
plotter.PlotLayer(layer_id)
|
||||
plotted_layers.append(layer_name)
|
||||
else:
|
||||
for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
|
||||
if self.board.IsLayerEnabled(layer_id):
|
||||
layer_name = self.board.GetLayerName(layer_id)
|
||||
plotter.PlotLayer(layer_id)
|
||||
plotted_layers.append(layer_name)
|
||||
|
||||
# Generate drill files if requested
|
||||
drill_files = []
|
||||
if generate_drill_files:
|
||||
drill_writer = pcbnew.EXCELLON_WRITER(self.board)
|
||||
drill_writer.SetFormat(True)
|
||||
drill_writer.SetMapFileFormat(pcbnew.PLOT_FORMAT_GERBER)
|
||||
|
||||
merge_npth = False # Keep plated/non-plated holes separate
|
||||
drill_writer.SetOptions(merge_npth)
|
||||
|
||||
drill_writer.CreateDrillandMapFilesSet(output_dir, True, generate_map_file)
|
||||
|
||||
# Get list of generated drill files
|
||||
for file in os.listdir(output_dir):
|
||||
if file.endswith(".drl") or file.endswith(".cnc"):
|
||||
drill_files.append(file)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Exported Gerber files",
|
||||
"files": {
|
||||
"gerber": plotted_layers,
|
||||
"drill": drill_files,
|
||||
"map": ["job.gbrjob"] if generate_map_file else []
|
||||
},
|
||||
"outputDir": output_dir
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error exporting Gerber files: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to export Gerber files",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def export_pdf(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Export PDF files"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
output_path = params.get("outputPath")
|
||||
layers = params.get("layers", [])
|
||||
black_and_white = params.get("blackAndWhite", False)
|
||||
frame_reference = params.get("frameReference", True)
|
||||
page_size = params.get("pageSize", "A4")
|
||||
|
||||
if not output_path:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing output path",
|
||||
"errorDetails": "outputPath parameter is required"
|
||||
}
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
output_path = os.path.abspath(os.path.expanduser(output_path))
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
|
||||
# Create plot controller
|
||||
plotter = pcbnew.PLOT_CONTROLLER(self.board)
|
||||
|
||||
# Set up plot options
|
||||
plot_opts = plotter.GetPlotOptions()
|
||||
plot_opts.SetOutputDirectory(os.path.dirname(output_path))
|
||||
plot_opts.SetFormat(pcbnew.PLOT_FORMAT_PDF)
|
||||
plot_opts.SetPlotFrameRef(frame_reference)
|
||||
plot_opts.SetPlotValue(True)
|
||||
plot_opts.SetPlotReference(True)
|
||||
plot_opts.SetMonochrome(black_and_white)
|
||||
|
||||
# Set page size
|
||||
page_sizes = {
|
||||
"A4": (297, 210),
|
||||
"A3": (420, 297),
|
||||
"A2": (594, 420),
|
||||
"A1": (841, 594),
|
||||
"A0": (1189, 841),
|
||||
"Letter": (279.4, 215.9),
|
||||
"Legal": (355.6, 215.9),
|
||||
"Tabloid": (431.8, 279.4)
|
||||
}
|
||||
if page_size in page_sizes:
|
||||
height, width = page_sizes[page_size]
|
||||
plot_opts.SetPageSettings((width, height))
|
||||
|
||||
# Plot specified layers or all enabled layers
|
||||
plotted_layers = []
|
||||
if layers:
|
||||
for layer_name in layers:
|
||||
layer_id = self.board.GetLayerID(layer_name)
|
||||
if layer_id >= 0:
|
||||
plotter.PlotLayer(layer_id)
|
||||
plotted_layers.append(layer_name)
|
||||
else:
|
||||
for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
|
||||
if self.board.IsLayerEnabled(layer_id):
|
||||
layer_name = self.board.GetLayerName(layer_id)
|
||||
plotter.PlotLayer(layer_id)
|
||||
plotted_layers.append(layer_name)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Exported PDF file",
|
||||
"file": {
|
||||
"path": output_path,
|
||||
"layers": plotted_layers,
|
||||
"pageSize": page_size
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error exporting PDF file: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to export PDF file",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def export_svg(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Export SVG files"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
output_path = params.get("outputPath")
|
||||
layers = params.get("layers", [])
|
||||
black_and_white = params.get("blackAndWhite", False)
|
||||
include_components = params.get("includeComponents", True)
|
||||
|
||||
if not output_path:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing output path",
|
||||
"errorDetails": "outputPath parameter is required"
|
||||
}
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
output_path = os.path.abspath(os.path.expanduser(output_path))
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
|
||||
# Create plot controller
|
||||
plotter = pcbnew.PLOT_CONTROLLER(self.board)
|
||||
|
||||
# Set up plot options
|
||||
plot_opts = plotter.GetPlotOptions()
|
||||
plot_opts.SetOutputDirectory(os.path.dirname(output_path))
|
||||
plot_opts.SetFormat(pcbnew.PLOT_FORMAT_SVG)
|
||||
plot_opts.SetPlotValue(include_components)
|
||||
plot_opts.SetPlotReference(include_components)
|
||||
plot_opts.SetMonochrome(black_and_white)
|
||||
|
||||
# Plot specified layers or all enabled layers
|
||||
plotted_layers = []
|
||||
if layers:
|
||||
for layer_name in layers:
|
||||
layer_id = self.board.GetLayerID(layer_name)
|
||||
if layer_id >= 0:
|
||||
plotter.PlotLayer(layer_id)
|
||||
plotted_layers.append(layer_name)
|
||||
else:
|
||||
for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
|
||||
if self.board.IsLayerEnabled(layer_id):
|
||||
layer_name = self.board.GetLayerName(layer_id)
|
||||
plotter.PlotLayer(layer_id)
|
||||
plotted_layers.append(layer_name)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Exported SVG file",
|
||||
"file": {
|
||||
"path": output_path,
|
||||
"layers": plotted_layers
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error exporting SVG file: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to export SVG file",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def export_3d(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Export 3D model files"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
output_path = params.get("outputPath")
|
||||
format = params.get("format", "STEP")
|
||||
include_components = params.get("includeComponents", True)
|
||||
include_copper = params.get("includeCopper", True)
|
||||
include_solder_mask = params.get("includeSolderMask", True)
|
||||
include_silkscreen = params.get("includeSilkscreen", True)
|
||||
|
||||
if not output_path:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing output path",
|
||||
"errorDetails": "outputPath parameter is required"
|
||||
}
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
output_path = os.path.abspath(os.path.expanduser(output_path))
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
|
||||
# Get 3D viewer
|
||||
viewer = self.board.Get3DViewer()
|
||||
if not viewer:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "3D viewer not available",
|
||||
"errorDetails": "Could not initialize 3D viewer"
|
||||
}
|
||||
|
||||
# Set export options
|
||||
viewer.SetCopperLayersOn(include_copper)
|
||||
viewer.SetSolderMaskLayersOn(include_solder_mask)
|
||||
viewer.SetSilkScreenLayersOn(include_silkscreen)
|
||||
viewer.Set3DModelsOn(include_components)
|
||||
|
||||
# Export based on format
|
||||
if format == "STEP":
|
||||
viewer.ExportSTEPFile(output_path)
|
||||
elif format == "VRML":
|
||||
viewer.ExportVRMLFile(output_path)
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Unsupported format",
|
||||
"errorDetails": f"Format {format} is not supported"
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Exported {format} file",
|
||||
"file": {
|
||||
"path": output_path,
|
||||
"format": format
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error exporting 3D model: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to export 3D model",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def export_bom(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Export Bill of Materials"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
output_path = params.get("outputPath")
|
||||
format = params.get("format", "CSV")
|
||||
group_by_value = params.get("groupByValue", True)
|
||||
include_attributes = params.get("includeAttributes", [])
|
||||
|
||||
if not output_path:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing output path",
|
||||
"errorDetails": "outputPath parameter is required"
|
||||
}
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
output_path = os.path.abspath(os.path.expanduser(output_path))
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
|
||||
# Get all components
|
||||
components = []
|
||||
for module in self.board.GetFootprints():
|
||||
component = {
|
||||
"reference": module.GetReference(),
|
||||
"value": module.GetValue(),
|
||||
"footprint": module.GetFootprintName(),
|
||||
"layer": self.board.GetLayerName(module.GetLayer())
|
||||
}
|
||||
|
||||
# Add requested attributes
|
||||
for attr in include_attributes:
|
||||
if hasattr(module, f"Get{attr}"):
|
||||
component[attr] = getattr(module, f"Get{attr}")()
|
||||
|
||||
components.append(component)
|
||||
|
||||
# Group by value if requested
|
||||
if group_by_value:
|
||||
grouped = {}
|
||||
for comp in components:
|
||||
key = f"{comp['value']}_{comp['footprint']}"
|
||||
if key not in grouped:
|
||||
grouped[key] = {
|
||||
"value": comp["value"],
|
||||
"footprint": comp["footprint"],
|
||||
"quantity": 1,
|
||||
"references": [comp["reference"]]
|
||||
}
|
||||
else:
|
||||
grouped[key]["quantity"] += 1
|
||||
grouped[key]["references"].append(comp["reference"])
|
||||
components = list(grouped.values())
|
||||
|
||||
# Export based on format
|
||||
if format == "CSV":
|
||||
self._export_bom_csv(output_path, components)
|
||||
elif format == "XML":
|
||||
self._export_bom_xml(output_path, components)
|
||||
elif format == "HTML":
|
||||
self._export_bom_html(output_path, components)
|
||||
elif format == "JSON":
|
||||
self._export_bom_json(output_path, components)
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Unsupported format",
|
||||
"errorDetails": f"Format {format} is not supported"
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Exported BOM to {format}",
|
||||
"file": {
|
||||
"path": output_path,
|
||||
"format": format,
|
||||
"componentCount": len(components)
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error exporting BOM: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to export BOM",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def _export_bom_csv(self, path: str, components: List[Dict[str, Any]]) -> None:
|
||||
"""Export BOM to CSV format"""
|
||||
import csv
|
||||
with open(path, 'w', newline='') as f:
|
||||
writer = csv.DictWriter(f, fieldnames=components[0].keys())
|
||||
writer.writeheader()
|
||||
writer.writerows(components)
|
||||
|
||||
def _export_bom_xml(self, path: str, components: List[Dict[str, Any]]) -> None:
|
||||
"""Export BOM to XML format"""
|
||||
import xml.etree.ElementTree as ET
|
||||
root = ET.Element("bom")
|
||||
for comp in components:
|
||||
comp_elem = ET.SubElement(root, "component")
|
||||
for key, value in comp.items():
|
||||
elem = ET.SubElement(comp_elem, key)
|
||||
elem.text = str(value)
|
||||
tree = ET.ElementTree(root)
|
||||
tree.write(path, encoding='utf-8', xml_declaration=True)
|
||||
|
||||
def _export_bom_html(self, path: str, components: List[Dict[str, Any]]) -> None:
|
||||
"""Export BOM to HTML format"""
|
||||
html = ["<html><head><title>Bill of Materials</title></head><body>"]
|
||||
html.append("<table border='1'><tr>")
|
||||
# Headers
|
||||
for key in components[0].keys():
|
||||
html.append(f"<th>{key}</th>")
|
||||
html.append("</tr>")
|
||||
# Data
|
||||
for comp in components:
|
||||
html.append("<tr>")
|
||||
for value in comp.values():
|
||||
html.append(f"<td>{value}</td>")
|
||||
html.append("</tr>")
|
||||
html.append("</table></body></html>")
|
||||
with open(path, 'w') as f:
|
||||
f.write("\n".join(html))
|
||||
|
||||
def _export_bom_json(self, path: str, components: List[Dict[str, Any]]) -> None:
|
||||
"""Export BOM to JSON format"""
|
||||
import json
|
||||
with open(path, 'w') as f:
|
||||
json.dump({"components": components}, f, indent=2)
|
||||
141
python/commands/library_schematic.py
Normal file
141
python/commands/library_schematic.py
Normal file
@@ -0,0 +1,141 @@
|
||||
from skip import Schematic
|
||||
# Symbol class might not be directly importable in the current version
|
||||
import os
|
||||
import glob
|
||||
|
||||
class LibraryManager:
|
||||
"""Manage symbol libraries"""
|
||||
|
||||
@staticmethod
|
||||
def list_available_libraries(search_paths=None):
|
||||
"""List all available symbol libraries"""
|
||||
if search_paths is None:
|
||||
# Default library paths based on common KiCAD installations
|
||||
# This would need to be configured for the specific environment
|
||||
search_paths = [
|
||||
"C:/Program Files/KiCad/*/share/kicad/symbols/*.kicad_sym", # Windows path pattern
|
||||
"/usr/share/kicad/symbols/*.kicad_sym", # Linux path pattern
|
||||
"/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/*.kicad_sym", # macOS path pattern
|
||||
os.path.expanduser("~/Documents/KiCad/*/symbols/*.kicad_sym") # User libraries pattern
|
||||
]
|
||||
|
||||
libraries = []
|
||||
for path_pattern in search_paths:
|
||||
try:
|
||||
# Use glob to find all matching files
|
||||
matching_libs = glob.glob(path_pattern, recursive=True)
|
||||
libraries.extend(matching_libs)
|
||||
except Exception as e:
|
||||
print(f"Error searching for libraries at {path_pattern}: {e}")
|
||||
|
||||
# Extract library names from paths
|
||||
library_names = [os.path.splitext(os.path.basename(lib))[0] for lib in libraries]
|
||||
print(f"Found {len(library_names)} libraries: {', '.join(library_names[:10])}{'...' if len(library_names) > 10 else ''}")
|
||||
|
||||
# Return both full paths and library names
|
||||
return {"paths": libraries, "names": library_names}
|
||||
|
||||
@staticmethod
|
||||
def list_library_symbols(library_path):
|
||||
"""List all symbols in a library"""
|
||||
try:
|
||||
# kicad-skip doesn't provide a direct way to simply list symbols in a library
|
||||
# without loading each one. We might need to implement this using KiCAD's Python API
|
||||
# directly, or by using a different approach.
|
||||
# For now, this is a placeholder implementation.
|
||||
|
||||
# A potential approach would be to load the library file using KiCAD's Python API
|
||||
# or by parsing the library file format.
|
||||
# KiCAD symbol libraries are .kicad_sym files which are S-expression format
|
||||
print(f"Attempted to list symbols in library {library_path}. This requires advanced implementation.")
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"Error listing symbols in library {library_path}: {e}")
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_symbol_details(library_path, symbol_name):
|
||||
"""Get detailed information about a symbol"""
|
||||
try:
|
||||
# Similar to list_library_symbols, this might require a more direct approach
|
||||
# using KiCAD's Python API or by parsing the symbol library.
|
||||
print(f"Attempted to get details for symbol {symbol_name} in library {library_path}. This requires advanced implementation.")
|
||||
return {}
|
||||
except Exception as e:
|
||||
print(f"Error getting symbol details for {symbol_name} in {library_path}: {e}")
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def search_symbols(query, search_paths=None):
|
||||
"""Search for symbols matching criteria"""
|
||||
try:
|
||||
# This would typically involve:
|
||||
# 1. Getting a list of all libraries using list_available_libraries
|
||||
# 2. For each library, getting a list of all symbols
|
||||
# 3. Filtering symbols based on the query
|
||||
|
||||
# For now, this is a placeholder implementation
|
||||
libraries = LibraryManager.list_available_libraries(search_paths)
|
||||
|
||||
results = []
|
||||
print(f"Searched for symbols matching '{query}'. This requires advanced implementation.")
|
||||
return results
|
||||
except Exception as e:
|
||||
print(f"Error searching for symbols matching '{query}': {e}")
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_default_symbol_for_component_type(component_type, search_paths=None):
|
||||
"""Get a recommended default symbol for a given component type"""
|
||||
# This method provides a simplified way to get a symbol for common component types
|
||||
# It's useful when the user doesn't specify a particular library/symbol
|
||||
|
||||
# Define common mappings from component type to library/symbol
|
||||
common_mappings = {
|
||||
"resistor": {"library": "Device", "symbol": "R"},
|
||||
"capacitor": {"library": "Device", "symbol": "C"},
|
||||
"inductor": {"library": "Device", "symbol": "L"},
|
||||
"diode": {"library": "Device", "symbol": "D"},
|
||||
"led": {"library": "Device", "symbol": "LED"},
|
||||
"transistor_npn": {"library": "Device", "symbol": "Q_NPN_BCE"},
|
||||
"transistor_pnp": {"library": "Device", "symbol": "Q_PNP_BCE"},
|
||||
"opamp": {"library": "Amplifier_Operational", "symbol": "OpAmp_Dual_Generic"},
|
||||
"microcontroller": {"library": "MCU_Module", "symbol": "Arduino_UNO_R3"},
|
||||
# Add more common components as needed
|
||||
}
|
||||
|
||||
# Normalize input to lowercase
|
||||
component_type_lower = component_type.lower()
|
||||
|
||||
# Try direct match first
|
||||
if component_type_lower in common_mappings:
|
||||
return common_mappings[component_type_lower]
|
||||
|
||||
# Try partial matches
|
||||
for key, value in common_mappings.items():
|
||||
if component_type_lower in key or key in component_type_lower:
|
||||
return value
|
||||
|
||||
# Default fallback
|
||||
return {"library": "Device", "symbol": "R"}
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Example Usage (for testing)
|
||||
# List available libraries
|
||||
libraries = LibraryManager.list_available_libraries()
|
||||
if libraries["paths"]:
|
||||
first_lib = libraries["paths"][0]
|
||||
lib_name = libraries["names"][0]
|
||||
print(f"Testing with first library: {lib_name} ({first_lib})")
|
||||
|
||||
# List symbols in the first library
|
||||
symbols = LibraryManager.list_library_symbols(first_lib)
|
||||
# This will report that it requires advanced implementation
|
||||
|
||||
# Get default symbol for a component type
|
||||
resistor_sym = LibraryManager.get_default_symbol_for_component_type("resistor")
|
||||
print(f"Default symbol for resistor: {resistor_sym['library']}/{resistor_sym['symbol']}")
|
||||
|
||||
# Try a partial match
|
||||
cap_sym = LibraryManager.get_default_symbol_for_component_type("cap")
|
||||
print(f"Default symbol for 'cap': {cap_sym['library']}/{cap_sym['symbol']}")
|
||||
200
python/commands/project.py
Normal file
200
python/commands/project.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
Project-related command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import os
|
||||
import pcbnew # type: ignore
|
||||
import logging
|
||||
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:
|
||||
project_name = 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 project file
|
||||
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('}\n')
|
||||
|
||||
self.board = board
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Created project: {project_name}",
|
||||
"project": {
|
||||
"name": project_name,
|
||||
"path": project_path,
|
||||
"boardPath": board_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)
|
||||
}
|
||||
740
python/commands/routing.py
Normal file
740
python/commands/routing.py
Normal file
@@ -0,0 +1,740 @@
|
||||
"""
|
||||
Routing-related command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import os
|
||||
import pcbnew
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict, Any, Optional, List, Tuple
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
|
||||
class RoutingCommands:
|
||||
"""Handles routing-related KiCAD operations"""
|
||||
|
||||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||||
"""Initialize with optional board instance"""
|
||||
self.board = board
|
||||
|
||||
def add_net(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add a new net to the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
name = params.get("name")
|
||||
net_class = params.get("class")
|
||||
|
||||
if not name:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing net name",
|
||||
"errorDetails": "name parameter is required"
|
||||
}
|
||||
|
||||
# Create new net
|
||||
netinfo = self.board.GetNetInfo()
|
||||
net = netinfo.FindNet(name)
|
||||
if not net:
|
||||
net = netinfo.AddNet(name)
|
||||
|
||||
# Set net class if provided
|
||||
if net_class:
|
||||
net_classes = self.board.GetNetClasses()
|
||||
if net_classes.Find(net_class):
|
||||
net.SetClass(net_classes.Find(net_class))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Added net: {name}",
|
||||
"net": {
|
||||
"name": name,
|
||||
"class": net_class if net_class else "Default",
|
||||
"netcode": net.GetNetCode()
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding net: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to add net",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def route_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Route a trace between two points or pads"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
start = params.get("start")
|
||||
end = params.get("end")
|
||||
layer = params.get("layer", "F.Cu")
|
||||
width = params.get("width")
|
||||
net = params.get("net")
|
||||
via = params.get("via", False)
|
||||
|
||||
if not start or not end:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "start and end points are required"
|
||||
}
|
||||
|
||||
# Get layer ID
|
||||
layer_id = self.board.GetLayerID(layer)
|
||||
if layer_id < 0:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid layer",
|
||||
"errorDetails": f"Layer '{layer}' does not exist"
|
||||
}
|
||||
|
||||
# Get start point
|
||||
start_point = self._get_point(start)
|
||||
end_point = self._get_point(end)
|
||||
|
||||
# Create track segment
|
||||
track = pcbnew.PCB_TRACK(self.board)
|
||||
track.SetStart(start_point)
|
||||
track.SetEnd(end_point)
|
||||
track.SetLayer(layer_id)
|
||||
|
||||
# Set width (default to board's current track width)
|
||||
if width:
|
||||
track.SetWidth(int(width * 1000000)) # Convert mm to nm
|
||||
else:
|
||||
track.SetWidth(self.board.GetDesignSettings().GetCurrentTrackWidth())
|
||||
|
||||
# Set net if provided
|
||||
if net:
|
||||
netinfo = self.board.GetNetInfo()
|
||||
net_obj = netinfo.FindNet(net)
|
||||
if net_obj:
|
||||
track.SetNet(net_obj)
|
||||
|
||||
# Add track to board
|
||||
self.board.Add(track)
|
||||
|
||||
# Add via if requested and net is specified
|
||||
if via and net:
|
||||
via_point = end_point
|
||||
self.add_via({
|
||||
"position": {
|
||||
"x": via_point.x / 1000000,
|
||||
"y": via_point.y / 1000000,
|
||||
"unit": "mm"
|
||||
},
|
||||
"net": net
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Added trace",
|
||||
"trace": {
|
||||
"start": {
|
||||
"x": start_point.x / 1000000,
|
||||
"y": start_point.y / 1000000,
|
||||
"unit": "mm"
|
||||
},
|
||||
"end": {
|
||||
"x": end_point.x / 1000000,
|
||||
"y": end_point.y / 1000000,
|
||||
"unit": "mm"
|
||||
},
|
||||
"layer": layer,
|
||||
"width": track.GetWidth() / 1000000,
|
||||
"net": net
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error routing trace: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to route trace",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def add_via(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add a via at the specified location"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
position = params.get("position")
|
||||
size = params.get("size")
|
||||
drill = params.get("drill")
|
||||
net = params.get("net")
|
||||
from_layer = params.get("from_layer", "F.Cu")
|
||||
to_layer = params.get("to_layer", "B.Cu")
|
||||
|
||||
if not position:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing position",
|
||||
"errorDetails": "position parameter is required"
|
||||
}
|
||||
|
||||
# Create via
|
||||
via = pcbnew.PCB_VIA(self.board)
|
||||
|
||||
# Set position
|
||||
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
|
||||
x_nm = int(position["x"] * scale)
|
||||
y_nm = int(position["y"] * scale)
|
||||
via.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
||||
|
||||
# Set size and drill (default to board's current via settings)
|
||||
design_settings = self.board.GetDesignSettings()
|
||||
via.SetWidth(int(size * 1000000) if size else design_settings.GetCurrentViaSize())
|
||||
via.SetDrill(int(drill * 1000000) if drill else design_settings.GetCurrentViaDrill())
|
||||
|
||||
# Set layers
|
||||
from_id = self.board.GetLayerID(from_layer)
|
||||
to_id = self.board.GetLayerID(to_layer)
|
||||
if from_id < 0 or to_id < 0:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid layer",
|
||||
"errorDetails": "Specified layers do not exist"
|
||||
}
|
||||
via.SetLayerPair(from_id, to_id)
|
||||
|
||||
# Set net if provided
|
||||
if net:
|
||||
netinfo = self.board.GetNetInfo()
|
||||
net_obj = netinfo.FindNet(net)
|
||||
if net_obj:
|
||||
via.SetNet(net_obj)
|
||||
|
||||
# Add via to board
|
||||
self.board.Add(via)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Added via",
|
||||
"via": {
|
||||
"position": {
|
||||
"x": position["x"],
|
||||
"y": position["y"],
|
||||
"unit": position["unit"]
|
||||
},
|
||||
"size": via.GetWidth() / 1000000,
|
||||
"drill": via.GetDrill() / 1000000,
|
||||
"from_layer": from_layer,
|
||||
"to_layer": to_layer,
|
||||
"net": net
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding via: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to add via",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def delete_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Delete a trace from the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
trace_uuid = params.get("traceUuid")
|
||||
position = params.get("position")
|
||||
|
||||
if not trace_uuid and not position:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "Either traceUuid or position must be provided"
|
||||
}
|
||||
|
||||
# Find track by UUID
|
||||
if trace_uuid:
|
||||
track = None
|
||||
for item in self.board.Tracks():
|
||||
if str(item.m_Uuid) == trace_uuid:
|
||||
track = item
|
||||
break
|
||||
|
||||
if not track:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Track not found",
|
||||
"errorDetails": f"Could not find track with UUID: {trace_uuid}"
|
||||
}
|
||||
|
||||
self.board.Remove(track)
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Deleted track: {trace_uuid}"
|
||||
}
|
||||
|
||||
# Find track by position
|
||||
if position:
|
||||
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
|
||||
x_nm = int(position["x"] * scale)
|
||||
y_nm = int(position["y"] * scale)
|
||||
point = pcbnew.VECTOR2I(x_nm, y_nm)
|
||||
|
||||
# Find closest track
|
||||
closest_track = None
|
||||
min_distance = float('inf')
|
||||
for track in self.board.Tracks():
|
||||
dist = self._point_to_track_distance(point, track)
|
||||
if dist < min_distance:
|
||||
min_distance = dist
|
||||
closest_track = track
|
||||
|
||||
if closest_track and min_distance < 1000000: # Within 1mm
|
||||
self.board.Remove(closest_track)
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Deleted track at specified position"
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No track found",
|
||||
"errorDetails": "No track found near specified position"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting trace: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to delete trace",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def get_nets_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get a list of all nets in the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
nets = []
|
||||
netinfo = self.board.GetNetInfo()
|
||||
for net_code in range(netinfo.GetNetCount()):
|
||||
net = netinfo.GetNetItem(net_code)
|
||||
if net:
|
||||
nets.append({
|
||||
"name": net.GetNetname(),
|
||||
"code": net.GetNetCode(),
|
||||
"class": net.GetClassName()
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"nets": nets
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting nets list: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to get nets list",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def create_netclass(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Create a new net class with specified properties"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
name = params.get("name")
|
||||
clearance = params.get("clearance")
|
||||
track_width = params.get("trackWidth")
|
||||
via_diameter = params.get("viaDiameter")
|
||||
via_drill = params.get("viaDrill")
|
||||
uvia_diameter = params.get("uviaDiameter")
|
||||
uvia_drill = params.get("uviaDrill")
|
||||
diff_pair_width = params.get("diffPairWidth")
|
||||
diff_pair_gap = params.get("diffPairGap")
|
||||
nets = params.get("nets", [])
|
||||
|
||||
if not name:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing netclass name",
|
||||
"errorDetails": "name parameter is required"
|
||||
}
|
||||
|
||||
# Get net classes
|
||||
net_classes = self.board.GetNetClasses()
|
||||
|
||||
# Create new net class if it doesn't exist
|
||||
if not net_classes.Find(name):
|
||||
netclass = pcbnew.NETCLASS(name)
|
||||
net_classes.Add(netclass)
|
||||
else:
|
||||
netclass = net_classes.Find(name)
|
||||
|
||||
# Set properties
|
||||
scale = 1000000 # mm to nm
|
||||
if clearance is not None:
|
||||
netclass.SetClearance(int(clearance * scale))
|
||||
if track_width is not None:
|
||||
netclass.SetTrackWidth(int(track_width * scale))
|
||||
if via_diameter is not None:
|
||||
netclass.SetViaDiameter(int(via_diameter * scale))
|
||||
if via_drill is not None:
|
||||
netclass.SetViaDrill(int(via_drill * scale))
|
||||
if uvia_diameter is not None:
|
||||
netclass.SetMicroViaDiameter(int(uvia_diameter * scale))
|
||||
if uvia_drill is not None:
|
||||
netclass.SetMicroViaDrill(int(uvia_drill * scale))
|
||||
if diff_pair_width is not None:
|
||||
netclass.SetDiffPairWidth(int(diff_pair_width * scale))
|
||||
if diff_pair_gap is not None:
|
||||
netclass.SetDiffPairGap(int(diff_pair_gap * scale))
|
||||
|
||||
# Add nets to net class
|
||||
netinfo = self.board.GetNetInfo()
|
||||
for net_name in nets:
|
||||
net = netinfo.FindNet(net_name)
|
||||
if net:
|
||||
net.SetClass(netclass)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Created net class: {name}",
|
||||
"netClass": {
|
||||
"name": name,
|
||||
"clearance": netclass.GetClearance() / scale,
|
||||
"trackWidth": netclass.GetTrackWidth() / scale,
|
||||
"viaDiameter": netclass.GetViaDiameter() / scale,
|
||||
"viaDrill": netclass.GetViaDrill() / scale,
|
||||
"uviaDiameter": netclass.GetMicroViaDiameter() / scale,
|
||||
"uviaDrill": netclass.GetMicroViaDrill() / scale,
|
||||
"diffPairWidth": netclass.GetDiffPairWidth() / scale,
|
||||
"diffPairGap": netclass.GetDiffPairGap() / scale,
|
||||
"nets": nets
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating net class: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to create net class",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def add_copper_pour(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add a copper pour (zone) to the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
layer = params.get("layer", "F.Cu")
|
||||
net = params.get("net")
|
||||
clearance = params.get("clearance")
|
||||
min_width = params.get("minWidth", 0.2)
|
||||
points = params.get("points", [])
|
||||
priority = params.get("priority", 0)
|
||||
fill_type = params.get("fillType", "solid") # solid or hatched
|
||||
|
||||
if not points or len(points) < 3:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing points",
|
||||
"errorDetails": "At least 3 points are required for copper pour outline"
|
||||
}
|
||||
|
||||
# Get layer ID
|
||||
layer_id = self.board.GetLayerID(layer)
|
||||
if layer_id < 0:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid layer",
|
||||
"errorDetails": f"Layer '{layer}' does not exist"
|
||||
}
|
||||
|
||||
# Create zone
|
||||
zone = pcbnew.ZONE(self.board)
|
||||
zone.SetLayer(layer_id)
|
||||
|
||||
# Set net if provided
|
||||
if net:
|
||||
netinfo = self.board.GetNetInfo()
|
||||
net_obj = netinfo.FindNet(net)
|
||||
if net_obj:
|
||||
zone.SetNet(net_obj)
|
||||
|
||||
# Set zone properties
|
||||
scale = 1000000 # mm to nm
|
||||
zone.SetPriority(priority)
|
||||
|
||||
if clearance is not None:
|
||||
zone.SetLocalClearance(int(clearance * scale))
|
||||
|
||||
zone.SetMinThickness(int(min_width * scale))
|
||||
|
||||
# Set fill type
|
||||
if fill_type == "hatched":
|
||||
zone.SetFillMode(pcbnew.ZONE_FILL_MODE_HATCH_PATTERN)
|
||||
else:
|
||||
zone.SetFillMode(pcbnew.ZONE_FILL_MODE_POLYGON)
|
||||
|
||||
# Create outline
|
||||
outline = zone.Outline()
|
||||
|
||||
# Add points to outline
|
||||
for point in points:
|
||||
scale = 1000000 if point.get("unit", "mm") == "mm" else 25400000
|
||||
x_nm = int(point["x"] * scale)
|
||||
y_nm = int(point["y"] * scale)
|
||||
outline.Append(pcbnew.VECTOR2I(x_nm, y_nm))
|
||||
|
||||
# Add zone to board
|
||||
self.board.Add(zone)
|
||||
|
||||
# Fill zone
|
||||
filler = pcbnew.ZONE_FILLER(self.board)
|
||||
filler.Fill(self.board.Zones())
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Added copper pour",
|
||||
"pour": {
|
||||
"layer": layer,
|
||||
"net": net,
|
||||
"clearance": clearance,
|
||||
"minWidth": min_width,
|
||||
"priority": priority,
|
||||
"fillType": fill_type,
|
||||
"pointCount": len(points)
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding copper pour: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to add copper pour",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def route_differential_pair(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Route a differential pair between two sets of points or pads"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
start_pos = params.get("startPos")
|
||||
end_pos = params.get("endPos")
|
||||
net_pos = params.get("netPos")
|
||||
net_neg = params.get("netNeg")
|
||||
layer = params.get("layer", "F.Cu")
|
||||
width = params.get("width")
|
||||
gap = params.get("gap")
|
||||
|
||||
if not start_pos or not end_pos or not net_pos or not net_neg:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "startPos, endPos, netPos, and netNeg are required"
|
||||
}
|
||||
|
||||
# Get layer ID
|
||||
layer_id = self.board.GetLayerID(layer)
|
||||
if layer_id < 0:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid layer",
|
||||
"errorDetails": f"Layer '{layer}' does not exist"
|
||||
}
|
||||
|
||||
# Get nets
|
||||
netinfo = self.board.GetNetInfo()
|
||||
net_pos_obj = netinfo.FindNet(net_pos)
|
||||
net_neg_obj = netinfo.FindNet(net_neg)
|
||||
|
||||
if not net_pos_obj or not net_neg_obj:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Nets not found",
|
||||
"errorDetails": "One or both nets specified for the differential pair do not exist"
|
||||
}
|
||||
|
||||
# Get start and end points
|
||||
start_point = self._get_point(start_pos)
|
||||
end_point = self._get_point(end_pos)
|
||||
|
||||
# Calculate offset vectors for the two traces
|
||||
# First, get the direction vector from start to end
|
||||
dx = end_point.x - start_point.x
|
||||
dy = end_point.y - start_point.y
|
||||
length = math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
if length <= 0:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid points",
|
||||
"errorDetails": "Start and end points must be different"
|
||||
}
|
||||
|
||||
# Normalize direction vector
|
||||
dx /= length
|
||||
dy /= length
|
||||
|
||||
# Get perpendicular vector
|
||||
px = -dy
|
||||
py = dx
|
||||
|
||||
# Set default gap if not provided
|
||||
if gap is None:
|
||||
gap = 0.2 # mm
|
||||
|
||||
# Convert to nm
|
||||
gap_nm = int(gap * 1000000)
|
||||
|
||||
# Calculate offsets
|
||||
offset_x = int(px * gap_nm / 2)
|
||||
offset_y = int(py * gap_nm / 2)
|
||||
|
||||
# Create positive and negative trace points
|
||||
pos_start = pcbnew.VECTOR2I(int(start_point.x + offset_x), int(start_point.y + offset_y))
|
||||
pos_end = pcbnew.VECTOR2I(int(end_point.x + offset_x), int(end_point.y + offset_y))
|
||||
neg_start = pcbnew.VECTOR2I(int(start_point.x - offset_x), int(start_point.y - offset_y))
|
||||
neg_end = pcbnew.VECTOR2I(int(end_point.x - offset_x), int(end_point.y - offset_y))
|
||||
|
||||
# Create positive trace
|
||||
pos_track = pcbnew.PCB_TRACK(self.board)
|
||||
pos_track.SetStart(pos_start)
|
||||
pos_track.SetEnd(pos_end)
|
||||
pos_track.SetLayer(layer_id)
|
||||
pos_track.SetNet(net_pos_obj)
|
||||
|
||||
# Create negative trace
|
||||
neg_track = pcbnew.PCB_TRACK(self.board)
|
||||
neg_track.SetStart(neg_start)
|
||||
neg_track.SetEnd(neg_end)
|
||||
neg_track.SetLayer(layer_id)
|
||||
neg_track.SetNet(net_neg_obj)
|
||||
|
||||
# Set width
|
||||
if width:
|
||||
trace_width_nm = int(width * 1000000)
|
||||
pos_track.SetWidth(trace_width_nm)
|
||||
neg_track.SetWidth(trace_width_nm)
|
||||
else:
|
||||
# Get default width from design rules or net class
|
||||
trace_width = self.board.GetDesignSettings().GetCurrentTrackWidth()
|
||||
pos_track.SetWidth(trace_width)
|
||||
neg_track.SetWidth(trace_width)
|
||||
|
||||
# Add tracks to board
|
||||
self.board.Add(pos_track)
|
||||
self.board.Add(neg_track)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Added differential pair traces",
|
||||
"diffPair": {
|
||||
"posNet": net_pos,
|
||||
"negNet": net_neg,
|
||||
"layer": layer,
|
||||
"width": pos_track.GetWidth() / 1000000,
|
||||
"gap": gap,
|
||||
"length": length / 1000000
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error routing differential pair: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to route differential pair",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def _get_point(self, point_spec: Dict[str, Any]) -> pcbnew.VECTOR2I:
|
||||
"""Convert point specification to KiCAD point"""
|
||||
if "x" in point_spec and "y" in point_spec:
|
||||
scale = 1000000 if point_spec.get("unit", "mm") == "mm" else 25400000
|
||||
x_nm = int(point_spec["x"] * scale)
|
||||
y_nm = int(point_spec["y"] * scale)
|
||||
return pcbnew.VECTOR2I(x_nm, y_nm)
|
||||
elif "pad" in point_spec and "componentRef" in point_spec:
|
||||
module = self.board.FindFootprintByReference(point_spec["componentRef"])
|
||||
if module:
|
||||
pad = module.FindPadByName(point_spec["pad"])
|
||||
if pad:
|
||||
return pad.GetPosition()
|
||||
raise ValueError("Invalid point specification")
|
||||
|
||||
def _point_to_track_distance(self, point: pcbnew.VECTOR2I, track: pcbnew.PCB_TRACK) -> float:
|
||||
"""Calculate distance from point to track segment"""
|
||||
start = track.GetStart()
|
||||
end = track.GetEnd()
|
||||
|
||||
# Vector from start to end
|
||||
v = pcbnew.VECTOR2I(end.x - start.x, end.y - start.y)
|
||||
# Vector from start to point
|
||||
w = pcbnew.VECTOR2I(point.x - start.x, point.y - start.y)
|
||||
|
||||
# Length of track squared
|
||||
c1 = v.x * v.x + v.y * v.y
|
||||
if c1 == 0:
|
||||
return self._point_distance(point, start)
|
||||
|
||||
# Projection coefficient
|
||||
c2 = float(w.x * v.x + w.y * v.y) / c1
|
||||
|
||||
if c2 < 0:
|
||||
return self._point_distance(point, start)
|
||||
elif c2 > 1:
|
||||
return self._point_distance(point, end)
|
||||
|
||||
# Point on line
|
||||
proj = pcbnew.VECTOR2I(
|
||||
int(start.x + c2 * v.x),
|
||||
int(start.y + c2 * v.y)
|
||||
)
|
||||
return self._point_distance(point, proj)
|
||||
|
||||
def _point_distance(self, p1: pcbnew.VECTOR2I, p2: pcbnew.VECTOR2I) -> float:
|
||||
"""Calculate distance between two points"""
|
||||
dx = p1.x - p2.x
|
||||
dy = p1.y - p2.y
|
||||
return (dx * dx + dy * dy) ** 0.5
|
||||
96
python/commands/schematic.py
Normal file
96
python/commands/schematic.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from skip import Schematic
|
||||
import os
|
||||
|
||||
class SchematicManager:
|
||||
"""Core schematic operations using kicad-skip"""
|
||||
|
||||
@staticmethod
|
||||
def create_schematic(name, metadata=None):
|
||||
"""Create a new empty schematic"""
|
||||
# kicad-skip requires a filepath to create a schematic
|
||||
# We'll create a blank schematic file by loading an existing file
|
||||
# or we can create a template file first.
|
||||
|
||||
# Create an empty template file first
|
||||
temp_path = f"{name}_template.kicad_sch"
|
||||
with open(temp_path, 'w') as f:
|
||||
# Write minimal schematic file content
|
||||
f.write("(kicad_sch (version 20230121) (generator \"KiCAD-MCP-Server\"))\n")
|
||||
|
||||
# Now load it
|
||||
sch = Schematic(temp_path)
|
||||
sch.version = "20230121" # Set appropriate version
|
||||
sch.generator = "KiCAD-MCP-Server"
|
||||
|
||||
# Clean up the template
|
||||
os.remove(temp_path)
|
||||
# Add metadata if provided
|
||||
if metadata:
|
||||
for key, value in metadata.items():
|
||||
# kicad-skip doesn't have a direct metadata property on Schematic,
|
||||
# but we can add properties to the root sheet if needed, or
|
||||
# include it in the file path/name convention.
|
||||
# For now, we'll just create the schematic.
|
||||
pass # Placeholder for potential metadata handling
|
||||
|
||||
print(f"Created new schematic: {name}")
|
||||
return sch
|
||||
|
||||
@staticmethod
|
||||
def load_schematic(file_path):
|
||||
"""Load an existing schematic"""
|
||||
if not os.path.exists(file_path):
|
||||
print(f"Error: Schematic file not found at {file_path}")
|
||||
return None
|
||||
try:
|
||||
sch = Schematic(file_path)
|
||||
print(f"Loaded schematic from: {file_path}")
|
||||
return sch
|
||||
except Exception as e:
|
||||
print(f"Error loading schematic from {file_path}: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def save_schematic(schematic, file_path):
|
||||
"""Save a schematic to file"""
|
||||
try:
|
||||
# kicad-skip uses write method, not save
|
||||
schematic.write(file_path)
|
||||
print(f"Saved schematic to: {file_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error saving schematic to {file_path}: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_schematic_metadata(schematic):
|
||||
"""Extract metadata from schematic"""
|
||||
# kicad-skip doesn't expose a direct metadata object on Schematic.
|
||||
# We can return basic info like version and generator.
|
||||
metadata = {
|
||||
"version": schematic.version,
|
||||
"generator": schematic.generator,
|
||||
# Add other relevant properties if needed
|
||||
}
|
||||
print("Extracted schematic metadata")
|
||||
return metadata
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Example Usage (for testing)
|
||||
# Create a new schematic
|
||||
new_sch = SchematicManager.create_schematic("MyTestSchematic")
|
||||
|
||||
# Save the schematic
|
||||
test_file = "test_schematic.kicad_sch"
|
||||
SchematicManager.save_schematic(new_sch, test_file)
|
||||
|
||||
# Load the schematic
|
||||
loaded_sch = SchematicManager.load_schematic(test_file)
|
||||
if loaded_sch:
|
||||
metadata = SchematicManager.get_schematic_metadata(loaded_sch)
|
||||
print(f"Loaded schematic metadata: {metadata}")
|
||||
|
||||
# Clean up test file
|
||||
if os.path.exists(test_file):
|
||||
os.remove(test_file)
|
||||
print(f"Cleaned up {test_file}")
|
||||
27
python/kicad_api/__init__.py
Normal file
27
python/kicad_api/__init__.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
KiCAD API Abstraction Layer
|
||||
|
||||
This module provides a unified interface to KiCAD's Python APIs,
|
||||
supporting both the legacy SWIG bindings and the new IPC API.
|
||||
|
||||
Usage:
|
||||
from kicad_api import create_backend
|
||||
|
||||
# Auto-detect best available backend
|
||||
backend = create_backend()
|
||||
|
||||
# Or specify explicitly
|
||||
backend = create_backend('ipc') # Use IPC API
|
||||
backend = create_backend('swig') # Use legacy SWIG
|
||||
|
||||
# Connect and use
|
||||
if backend.connect():
|
||||
board = backend.get_board()
|
||||
board.set_size(100, 80)
|
||||
"""
|
||||
|
||||
from kicad_api.factory import create_backend
|
||||
from kicad_api.base import KiCADBackend
|
||||
|
||||
__all__ = ['create_backend', 'KiCADBackend']
|
||||
__version__ = '2.0.0-alpha.1'
|
||||
204
python/kicad_api/base.py
Normal file
204
python/kicad_api/base.py
Normal file
@@ -0,0 +1,204 @@
|
||||
"""
|
||||
Abstract base class for KiCAD API backends
|
||||
|
||||
Defines the interface that all KiCAD backends must implement.
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KiCADBackend(ABC):
|
||||
"""Abstract base class for KiCAD API backends"""
|
||||
|
||||
@abstractmethod
|
||||
def connect(self) -> bool:
|
||||
"""
|
||||
Connect to KiCAD
|
||||
|
||||
Returns:
|
||||
True if connection successful, False otherwise
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def disconnect(self) -> None:
|
||||
"""Disconnect from KiCAD and clean up resources"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_connected(self) -> bool:
|
||||
"""
|
||||
Check if currently connected to KiCAD
|
||||
|
||||
Returns:
|
||||
True if connected, False otherwise
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_version(self) -> str:
|
||||
"""
|
||||
Get KiCAD version
|
||||
|
||||
Returns:
|
||||
Version string (e.g., "9.0.0")
|
||||
"""
|
||||
pass
|
||||
|
||||
# Project Operations
|
||||
@abstractmethod
|
||||
def create_project(self, path: Path, name: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a new KiCAD project
|
||||
|
||||
Args:
|
||||
path: Directory path for the project
|
||||
name: Project name
|
||||
|
||||
Returns:
|
||||
Dictionary with project info
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def open_project(self, path: Path) -> Dict[str, Any]:
|
||||
"""
|
||||
Open an existing KiCAD project
|
||||
|
||||
Args:
|
||||
path: Path to .kicad_pro file
|
||||
|
||||
Returns:
|
||||
Dictionary with project info
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save_project(self, path: Optional[Path] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Save the current project
|
||||
|
||||
Args:
|
||||
path: Optional new path to save to
|
||||
|
||||
Returns:
|
||||
Dictionary with save status
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def close_project(self) -> None:
|
||||
"""Close the current project"""
|
||||
pass
|
||||
|
||||
# Board Operations
|
||||
@abstractmethod
|
||||
def get_board(self) -> 'BoardAPI':
|
||||
"""
|
||||
Get board API for current project
|
||||
|
||||
Returns:
|
||||
BoardAPI instance
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class BoardAPI(ABC):
|
||||
"""Abstract interface for board operations"""
|
||||
|
||||
@abstractmethod
|
||||
def set_size(self, width: float, height: float, unit: str = "mm") -> bool:
|
||||
"""
|
||||
Set board size
|
||||
|
||||
Args:
|
||||
width: Board width
|
||||
height: Board height
|
||||
unit: Unit of measurement ("mm" or "in")
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_size(self) -> Dict[str, float]:
|
||||
"""
|
||||
Get current board size
|
||||
|
||||
Returns:
|
||||
Dictionary with width, height, unit
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def add_layer(self, layer_name: str, layer_type: str) -> bool:
|
||||
"""
|
||||
Add a layer to the board
|
||||
|
||||
Args:
|
||||
layer_name: Name of the layer
|
||||
layer_type: Type ("copper", "technical", "user")
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def list_components(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
List all components on the board
|
||||
|
||||
Returns:
|
||||
List of component dictionaries
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def place_component(
|
||||
self,
|
||||
reference: str,
|
||||
footprint: str,
|
||||
x: float,
|
||||
y: float,
|
||||
rotation: float = 0,
|
||||
layer: str = "F.Cu"
|
||||
) -> bool:
|
||||
"""
|
||||
Place a component on the board
|
||||
|
||||
Args:
|
||||
reference: Component reference (e.g., "R1")
|
||||
footprint: Footprint library path
|
||||
x: X position (mm)
|
||||
y: Y position (mm)
|
||||
rotation: Rotation angle (degrees)
|
||||
layer: Layer name
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
"""
|
||||
pass
|
||||
|
||||
# Add more abstract methods for routing, DRC, export, etc.
|
||||
# These will be filled in during migration
|
||||
|
||||
|
||||
class BackendError(Exception):
|
||||
"""Base exception for backend errors"""
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionError(BackendError):
|
||||
"""Raised when connection to KiCAD fails"""
|
||||
pass
|
||||
|
||||
|
||||
class APINotAvailableError(BackendError):
|
||||
"""Raised when required API is not available"""
|
||||
pass
|
||||
198
python/kicad_api/factory.py
Normal file
198
python/kicad_api/factory.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""
|
||||
Backend factory for creating appropriate KiCAD API backend
|
||||
|
||||
Auto-detects available backends and provides fallback mechanism.
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
|
||||
from kicad_api.base import KiCADBackend, APINotAvailableError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_backend(backend_type: Optional[str] = None) -> KiCADBackend:
|
||||
"""
|
||||
Create appropriate KiCAD backend
|
||||
|
||||
Args:
|
||||
backend_type: Backend to use:
|
||||
- 'ipc': Use IPC API (recommended)
|
||||
- 'swig': Use legacy SWIG bindings
|
||||
- None or 'auto': Auto-detect (try IPC first, fall back to SWIG)
|
||||
|
||||
Returns:
|
||||
KiCADBackend instance
|
||||
|
||||
Raises:
|
||||
APINotAvailableError: If no backend is available
|
||||
|
||||
Environment Variables:
|
||||
KICAD_BACKEND: Override backend selection ('ipc', 'swig', or 'auto')
|
||||
"""
|
||||
# Check environment variable override
|
||||
if backend_type is None:
|
||||
backend_type = os.environ.get('KICAD_BACKEND', 'auto').lower()
|
||||
|
||||
logger.info(f"Requested backend: {backend_type}")
|
||||
|
||||
# Try specific backend if requested
|
||||
if backend_type == 'ipc':
|
||||
return _create_ipc_backend()
|
||||
elif backend_type == 'swig':
|
||||
return _create_swig_backend()
|
||||
elif backend_type == 'auto':
|
||||
return _auto_detect_backend()
|
||||
else:
|
||||
raise ValueError(f"Unknown backend type: {backend_type}")
|
||||
|
||||
|
||||
def _create_ipc_backend() -> KiCADBackend:
|
||||
"""
|
||||
Create IPC backend
|
||||
|
||||
Returns:
|
||||
IPCBackend instance
|
||||
|
||||
Raises:
|
||||
APINotAvailableError: If kicad-python not available
|
||||
"""
|
||||
try:
|
||||
from kicad_api.ipc_backend import IPCBackend
|
||||
logger.info("Creating IPC backend")
|
||||
return IPCBackend()
|
||||
except ImportError as e:
|
||||
logger.error(f"IPC backend not available: {e}")
|
||||
raise APINotAvailableError(
|
||||
"IPC backend requires 'kicad-python' package. "
|
||||
"Install with: pip install kicad-python"
|
||||
) from e
|
||||
|
||||
|
||||
def _create_swig_backend() -> KiCADBackend:
|
||||
"""
|
||||
Create SWIG backend
|
||||
|
||||
Returns:
|
||||
SWIGBackend instance
|
||||
|
||||
Raises:
|
||||
APINotAvailableError: If pcbnew not available
|
||||
"""
|
||||
try:
|
||||
from kicad_api.swig_backend import SWIGBackend
|
||||
logger.info("Creating SWIG backend")
|
||||
logger.warning(
|
||||
"SWIG backend is DEPRECATED and will be removed in KiCAD 10.0. "
|
||||
"Please migrate to IPC backend."
|
||||
)
|
||||
return SWIGBackend()
|
||||
except ImportError as e:
|
||||
logger.error(f"SWIG backend not available: {e}")
|
||||
raise APINotAvailableError(
|
||||
"SWIG backend requires 'pcbnew' module. "
|
||||
"Ensure KiCAD Python module is in PYTHONPATH."
|
||||
) from e
|
||||
|
||||
|
||||
def _auto_detect_backend() -> KiCADBackend:
|
||||
"""
|
||||
Auto-detect best available backend
|
||||
|
||||
Priority:
|
||||
1. IPC API (if kicad-python available and KiCAD running)
|
||||
2. SWIG API (if pcbnew available)
|
||||
|
||||
Returns:
|
||||
Best available KiCADBackend
|
||||
|
||||
Raises:
|
||||
APINotAvailableError: If no backend available
|
||||
"""
|
||||
logger.info("Auto-detecting available KiCAD backend...")
|
||||
|
||||
# Try IPC first (preferred)
|
||||
try:
|
||||
backend = _create_ipc_backend()
|
||||
# Test connection
|
||||
if backend.connect():
|
||||
logger.info("✓ IPC backend available and connected")
|
||||
return backend
|
||||
else:
|
||||
logger.warning("IPC backend available but connection failed")
|
||||
except (ImportError, APINotAvailableError) as e:
|
||||
logger.debug(f"IPC backend not available: {e}")
|
||||
|
||||
# Fall back to SWIG
|
||||
try:
|
||||
backend = _create_swig_backend()
|
||||
logger.warning(
|
||||
"Using deprecated SWIG backend. "
|
||||
"For best results, use IPC API with KiCAD running."
|
||||
)
|
||||
return backend
|
||||
except (ImportError, APINotAvailableError) as e:
|
||||
logger.error(f"SWIG backend not available: {e}")
|
||||
|
||||
# No backend available
|
||||
raise APINotAvailableError(
|
||||
"No KiCAD backend available. Please install either:\n"
|
||||
" - kicad-python (recommended): pip install kicad-python\n"
|
||||
" - Ensure KiCAD Python module (pcbnew) is in PYTHONPATH"
|
||||
)
|
||||
|
||||
|
||||
def get_available_backends() -> dict:
|
||||
"""
|
||||
Check which backends are available
|
||||
|
||||
Returns:
|
||||
Dictionary with backend availability:
|
||||
{
|
||||
'ipc': {'available': bool, 'version': str or None},
|
||||
'swig': {'available': bool, 'version': str or None}
|
||||
}
|
||||
"""
|
||||
results = {}
|
||||
|
||||
# Check IPC
|
||||
try:
|
||||
import kicad
|
||||
results['ipc'] = {
|
||||
'available': True,
|
||||
'version': getattr(kicad, '__version__', 'unknown')
|
||||
}
|
||||
except ImportError:
|
||||
results['ipc'] = {'available': False, 'version': None}
|
||||
|
||||
# Check SWIG
|
||||
try:
|
||||
import pcbnew
|
||||
results['swig'] = {
|
||||
'available': True,
|
||||
'version': pcbnew.GetBuildVersion()
|
||||
}
|
||||
except ImportError:
|
||||
results['swig'] = {'available': False, 'version': None}
|
||||
|
||||
return results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Quick diagnostic
|
||||
import json
|
||||
print("KiCAD Backend Availability:")
|
||||
print(json.dumps(get_available_backends(), indent=2))
|
||||
|
||||
print("\nAttempting to create backend...")
|
||||
try:
|
||||
backend = create_backend()
|
||||
print(f"✓ Created backend: {type(backend).__name__}")
|
||||
if backend.connect():
|
||||
print(f"✓ Connected to KiCAD: {backend.get_version()}")
|
||||
else:
|
||||
print("✗ Failed to connect to KiCAD")
|
||||
except Exception as e:
|
||||
print(f"✗ Error: {e}")
|
||||
195
python/kicad_api/ipc_backend.py
Normal file
195
python/kicad_api/ipc_backend.py
Normal file
@@ -0,0 +1,195 @@
|
||||
"""
|
||||
IPC API Backend (KiCAD 9.0+)
|
||||
|
||||
Uses the official kicad-python library for inter-process communication
|
||||
with a running KiCAD instance.
|
||||
|
||||
Note: Requires KiCAD to be running with IPC server enabled:
|
||||
Preferences > Plugins > Enable IPC API Server
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
from kicad_api.base import (
|
||||
KiCADBackend,
|
||||
BoardAPI,
|
||||
ConnectionError,
|
||||
APINotAvailableError
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IPCBackend(KiCADBackend):
|
||||
"""
|
||||
KiCAD IPC API backend
|
||||
|
||||
Communicates with KiCAD via Protocol Buffers over UNIX sockets.
|
||||
Requires KiCAD 9.0+ to be running with IPC enabled.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.kicad = None
|
||||
self._connected = False
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""
|
||||
Connect to running KiCAD instance via IPC
|
||||
|
||||
Returns:
|
||||
True if connection successful
|
||||
|
||||
Raises:
|
||||
ConnectionError: If connection fails
|
||||
"""
|
||||
try:
|
||||
# Import here to allow module to load even without kicad-python
|
||||
from kicad import KiCad
|
||||
|
||||
logger.info("Connecting to KiCAD via IPC...")
|
||||
self.kicad = KiCad()
|
||||
|
||||
# Verify connection with version check
|
||||
version = self.get_version()
|
||||
logger.info(f"✓ Connected to KiCAD {version} via IPC")
|
||||
self._connected = True
|
||||
return True
|
||||
|
||||
except ImportError as e:
|
||||
logger.error("kicad-python library not found")
|
||||
raise APINotAvailableError(
|
||||
"IPC backend requires kicad-python. "
|
||||
"Install with: pip install kicad-python"
|
||||
) from e
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect via IPC: {e}")
|
||||
logger.info(
|
||||
"Ensure KiCAD is running with IPC enabled: "
|
||||
"Preferences > Plugins > Enable IPC API Server"
|
||||
)
|
||||
raise ConnectionError(f"IPC connection failed: {e}") from e
|
||||
|
||||
def disconnect(self) -> None:
|
||||
"""Disconnect from KiCAD"""
|
||||
if self.kicad:
|
||||
# kicad-python handles cleanup automatically
|
||||
self.kicad = None
|
||||
self._connected = False
|
||||
logger.info("Disconnected from KiCAD IPC")
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
"""Check if connected"""
|
||||
return self._connected and self.kicad is not None
|
||||
|
||||
def get_version(self) -> str:
|
||||
"""Get KiCAD version"""
|
||||
if not self.kicad:
|
||||
raise ConnectionError("Not connected to KiCAD")
|
||||
|
||||
try:
|
||||
# Use kicad-python's version checking
|
||||
version_info = self.kicad.check_version()
|
||||
return str(version_info)
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get version: {e}")
|
||||
return "unknown"
|
||||
|
||||
# Project Operations
|
||||
def create_project(self, path: Path, name: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a new KiCAD project
|
||||
|
||||
TODO: Implement with IPC API
|
||||
"""
|
||||
if not self.is_connected():
|
||||
raise ConnectionError("Not connected to KiCAD")
|
||||
|
||||
logger.warning("create_project not yet implemented for IPC backend")
|
||||
raise NotImplementedError(
|
||||
"Project creation via IPC API is not yet implemented. "
|
||||
"This will be added in Week 2-3 migration."
|
||||
)
|
||||
|
||||
def open_project(self, path: Path) -> Dict[str, Any]:
|
||||
"""Open existing project"""
|
||||
if not self.is_connected():
|
||||
raise ConnectionError("Not connected to KiCAD")
|
||||
|
||||
logger.warning("open_project not yet implemented for IPC backend")
|
||||
raise NotImplementedError("Coming in Week 2-3 migration")
|
||||
|
||||
def save_project(self, path: Optional[Path] = None) -> Dict[str, Any]:
|
||||
"""Save current project"""
|
||||
if not self.is_connected():
|
||||
raise ConnectionError("Not connected to KiCAD")
|
||||
|
||||
logger.warning("save_project not yet implemented for IPC backend")
|
||||
raise NotImplementedError("Coming in Week 2-3 migration")
|
||||
|
||||
def close_project(self) -> None:
|
||||
"""Close current project"""
|
||||
if not self.is_connected():
|
||||
raise ConnectionError("Not connected to KiCAD")
|
||||
|
||||
logger.warning("close_project not yet implemented for IPC backend")
|
||||
raise NotImplementedError("Coming in Week 2-3 migration")
|
||||
|
||||
# Board Operations
|
||||
def get_board(self) -> BoardAPI:
|
||||
"""Get board API"""
|
||||
if not self.is_connected():
|
||||
raise ConnectionError("Not connected to KiCAD")
|
||||
|
||||
return IPCBoardAPI(self.kicad)
|
||||
|
||||
|
||||
class IPCBoardAPI(BoardAPI):
|
||||
"""Board API implementation for IPC backend"""
|
||||
|
||||
def __init__(self, kicad_instance):
|
||||
self.kicad = kicad_instance
|
||||
self._board = None
|
||||
|
||||
def _get_board(self):
|
||||
"""Lazy-load board instance"""
|
||||
if self._board is None:
|
||||
self._board = self.kicad.get_board()
|
||||
return self._board
|
||||
|
||||
def set_size(self, width: float, height: float, unit: str = "mm") -> bool:
|
||||
"""Set board size"""
|
||||
logger.warning("set_size not yet implemented for IPC backend")
|
||||
raise NotImplementedError("Coming in Week 2-3 migration")
|
||||
|
||||
def get_size(self) -> Dict[str, float]:
|
||||
"""Get board size"""
|
||||
logger.warning("get_size not yet implemented for IPC backend")
|
||||
raise NotImplementedError("Coming in Week 2-3 migration")
|
||||
|
||||
def add_layer(self, layer_name: str, layer_type: str) -> bool:
|
||||
"""Add layer"""
|
||||
logger.warning("add_layer not yet implemented for IPC backend")
|
||||
raise NotImplementedError("Coming in Week 2-3 migration")
|
||||
|
||||
def list_components(self) -> List[Dict[str, Any]]:
|
||||
"""List components"""
|
||||
logger.warning("list_components not yet implemented for IPC backend")
|
||||
raise NotImplementedError("Coming in Week 2-3 migration")
|
||||
|
||||
def place_component(
|
||||
self,
|
||||
reference: str,
|
||||
footprint: str,
|
||||
x: float,
|
||||
y: float,
|
||||
rotation: float = 0,
|
||||
layer: str = "F.Cu"
|
||||
) -> bool:
|
||||
"""Place component"""
|
||||
logger.warning("place_component not yet implemented for IPC backend")
|
||||
raise NotImplementedError("Coming in Week 2-3 migration")
|
||||
|
||||
|
||||
# Note: Full implementation will be completed during Week 2-3 migration
|
||||
# This is a skeleton to establish the pattern
|
||||
214
python/kicad_api/swig_backend.py
Normal file
214
python/kicad_api/swig_backend.py
Normal file
@@ -0,0 +1,214 @@
|
||||
"""
|
||||
SWIG Backend (Legacy - DEPRECATED)
|
||||
|
||||
Uses the legacy SWIG-based pcbnew Python bindings.
|
||||
This backend wraps the existing implementation for backward compatibility.
|
||||
|
||||
WARNING: SWIG bindings are deprecated as of KiCAD 9.0
|
||||
and will be removed in KiCAD 10.0.
|
||||
Please migrate to IPC backend.
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
from kicad_api.base import (
|
||||
KiCADBackend,
|
||||
BoardAPI,
|
||||
ConnectionError,
|
||||
APINotAvailableError
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SWIGBackend(KiCADBackend):
|
||||
"""
|
||||
Legacy SWIG-based backend
|
||||
|
||||
Wraps existing commands/project.py, commands/component.py, etc.
|
||||
for compatibility during migration period.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._connected = False
|
||||
self._pcbnew = None
|
||||
logger.warning(
|
||||
"⚠️ Using DEPRECATED SWIG backend. "
|
||||
"This will be removed in KiCAD 10.0. "
|
||||
"Please migrate to IPC API."
|
||||
)
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""
|
||||
'Connect' to SWIG API (just validates pcbnew import)
|
||||
|
||||
Returns:
|
||||
True if pcbnew module available
|
||||
"""
|
||||
try:
|
||||
import pcbnew
|
||||
self._pcbnew = pcbnew
|
||||
version = pcbnew.GetBuildVersion()
|
||||
logger.info(f"✓ Connected to pcbnew (SWIG): {version}")
|
||||
self._connected = True
|
||||
return True
|
||||
except ImportError as e:
|
||||
logger.error("pcbnew module not found")
|
||||
raise APINotAvailableError(
|
||||
"SWIG backend requires pcbnew module. "
|
||||
"Ensure KiCAD Python module is in PYTHONPATH."
|
||||
) from e
|
||||
|
||||
def disconnect(self) -> None:
|
||||
"""Disconnect from SWIG API (no-op)"""
|
||||
self._connected = False
|
||||
self._pcbnew = None
|
||||
logger.info("Disconnected from SWIG backend")
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
"""Check if connected"""
|
||||
return self._connected
|
||||
|
||||
def get_version(self) -> str:
|
||||
"""Get KiCAD version"""
|
||||
if not self.is_connected():
|
||||
raise ConnectionError("Not connected")
|
||||
|
||||
return self._pcbnew.GetBuildVersion()
|
||||
|
||||
# Project Operations
|
||||
def create_project(self, path: Path, name: str) -> Dict[str, Any]:
|
||||
"""Create project using existing SWIG implementation"""
|
||||
if not self.is_connected():
|
||||
raise ConnectionError("Not connected")
|
||||
|
||||
# Import existing implementation
|
||||
from commands.project import ProjectCommands
|
||||
|
||||
try:
|
||||
result = ProjectCommands.create_project(str(path), name)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create project: {e}")
|
||||
raise
|
||||
|
||||
def open_project(self, path: Path) -> Dict[str, Any]:
|
||||
"""Open project using existing SWIG implementation"""
|
||||
if not self.is_connected():
|
||||
raise ConnectionError("Not connected")
|
||||
|
||||
from commands.project import ProjectCommands
|
||||
|
||||
try:
|
||||
result = ProjectCommands.open_project(str(path))
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to open project: {e}")
|
||||
raise
|
||||
|
||||
def save_project(self, path: Optional[Path] = None) -> Dict[str, Any]:
|
||||
"""Save project using existing SWIG implementation"""
|
||||
if not self.is_connected():
|
||||
raise ConnectionError("Not connected")
|
||||
|
||||
from commands.project import ProjectCommands
|
||||
|
||||
try:
|
||||
path_str = str(path) if path else None
|
||||
result = ProjectCommands.save_project(path_str)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save project: {e}")
|
||||
raise
|
||||
|
||||
def close_project(self) -> None:
|
||||
"""Close project (SWIG doesn't have explicit close)"""
|
||||
logger.info("Closing project (SWIG backend)")
|
||||
# SWIG backend doesn't maintain project state,
|
||||
# so this is essentially a no-op
|
||||
|
||||
# Board Operations
|
||||
def get_board(self) -> BoardAPI:
|
||||
"""Get board API"""
|
||||
if not self.is_connected():
|
||||
raise ConnectionError("Not connected")
|
||||
|
||||
return SWIGBoardAPI(self._pcbnew)
|
||||
|
||||
|
||||
class SWIGBoardAPI(BoardAPI):
|
||||
"""Board API implementation wrapping SWIG/pcbnew"""
|
||||
|
||||
def __init__(self, pcbnew_module):
|
||||
self.pcbnew = pcbnew_module
|
||||
self._board = None
|
||||
|
||||
def set_size(self, width: float, height: float, unit: str = "mm") -> bool:
|
||||
"""Set board size using existing implementation"""
|
||||
from commands.board import BoardCommands
|
||||
|
||||
try:
|
||||
result = BoardCommands.set_board_size(width, height, unit)
|
||||
return result.get("success", False)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to set board size: {e}")
|
||||
return False
|
||||
|
||||
def get_size(self) -> Dict[str, float]:
|
||||
"""Get board size"""
|
||||
# TODO: Implement using existing SWIG code
|
||||
raise NotImplementedError("get_size not yet wrapped")
|
||||
|
||||
def add_layer(self, layer_name: str, layer_type: str) -> bool:
|
||||
"""Add layer using existing implementation"""
|
||||
from commands.board import BoardCommands
|
||||
|
||||
try:
|
||||
result = BoardCommands.add_layer(layer_name, layer_type)
|
||||
return result.get("success", False)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add layer: {e}")
|
||||
return False
|
||||
|
||||
def list_components(self) -> List[Dict[str, Any]]:
|
||||
"""List components using existing implementation"""
|
||||
from commands.component import ComponentCommands
|
||||
|
||||
try:
|
||||
result = ComponentCommands.get_component_list()
|
||||
if result.get("success"):
|
||||
return result.get("components", [])
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list components: {e}")
|
||||
return []
|
||||
|
||||
def place_component(
|
||||
self,
|
||||
reference: str,
|
||||
footprint: str,
|
||||
x: float,
|
||||
y: float,
|
||||
rotation: float = 0,
|
||||
layer: str = "F.Cu"
|
||||
) -> bool:
|
||||
"""Place component using existing implementation"""
|
||||
from commands.component import ComponentCommands
|
||||
|
||||
try:
|
||||
result = ComponentCommands.place_component(
|
||||
component_id=footprint,
|
||||
position={"x": x, "y": y, "unit": "mm"},
|
||||
reference=reference,
|
||||
rotation=rotation,
|
||||
layer=layer
|
||||
)
|
||||
return result.get("success", False)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to place component: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# This backend serves as a wrapper during the migration period.
|
||||
# Once IPC backend is fully implemented, this can be deprecated.
|
||||
423
python/kicad_interface.py
Normal file
423
python/kicad_interface.py
Normal file
@@ -0,0 +1,423 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
KiCAD Python Interface Script for Model Context Protocol
|
||||
|
||||
This script handles communication between the MCP TypeScript server
|
||||
and KiCAD's Python API (pcbnew). It receives commands via stdin as
|
||||
JSON and returns responses via stdout also as JSON.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import traceback
|
||||
import logging
|
||||
import os
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
# Configure logging
|
||||
log_dir = os.path.join(os.path.expanduser('~'), '.kicad-mcp', 'logs')
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
log_file = os.path.join(log_dir, 'kicad_interface.log')
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s [%(levelname)s] %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(log_file),
|
||||
logging.StreamHandler(sys.stderr)
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
|
||||
# Log Python environment details
|
||||
logger.info(f"Python version: {sys.version}")
|
||||
logger.info(f"Python executable: {sys.executable}")
|
||||
logger.info(f"Python path: {sys.path}")
|
||||
|
||||
# Add KiCAD Python paths
|
||||
kicad_paths = [
|
||||
os.path.join(os.path.dirname(sys.executable), 'Lib', 'site-packages'),
|
||||
os.path.dirname(sys.executable)
|
||||
]
|
||||
for path in kicad_paths:
|
||||
if path not in sys.path:
|
||||
logger.info(f"Adding KiCAD path: {path}")
|
||||
sys.path.append(path)
|
||||
|
||||
# Import KiCAD's Python API
|
||||
try:
|
||||
logger.info("Attempting to import pcbnew module...")
|
||||
import pcbnew # type: ignore
|
||||
logger.info(f"Successfully imported pcbnew module from: {pcbnew.__file__}")
|
||||
logger.info(f"pcbnew version: {pcbnew.GetBuildVersion()}")
|
||||
except ImportError as e:
|
||||
logger.error(f"Failed to import pcbnew module: {e}")
|
||||
logger.error(f"Current sys.path: {sys.path}")
|
||||
error_response = {
|
||||
"success": False,
|
||||
"message": "Failed to import pcbnew module",
|
||||
"errorDetails": f"Error: {str(e)}\nPython path: {sys.path}"
|
||||
}
|
||||
print(json.dumps(error_response))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error importing pcbnew: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
error_response = {
|
||||
"success": False,
|
||||
"message": "Error importing pcbnew module",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
print(json.dumps(error_response))
|
||||
sys.exit(1)
|
||||
|
||||
# Import command handlers
|
||||
try:
|
||||
logger.info("Importing command handlers...")
|
||||
from commands.project import ProjectCommands
|
||||
from commands.board import BoardCommands
|
||||
from commands.component import ComponentCommands
|
||||
from commands.routing import RoutingCommands
|
||||
from commands.design_rules import DesignRuleCommands
|
||||
from commands.export import ExportCommands
|
||||
from commands.schematic import SchematicManager
|
||||
from commands.component_schematic import ComponentManager
|
||||
from commands.connection_schematic import ConnectionManager
|
||||
from commands.library_schematic import LibraryManager
|
||||
logger.info("Successfully imported all command handlers")
|
||||
except ImportError as e:
|
||||
logger.error(f"Failed to import command handlers: {e}")
|
||||
error_response = {
|
||||
"success": False,
|
||||
"message": "Failed to import command handlers",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
print(json.dumps(error_response))
|
||||
sys.exit(1)
|
||||
|
||||
class KiCADInterface:
|
||||
"""Main interface class to handle KiCAD operations"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the interface and command handlers"""
|
||||
self.board = None
|
||||
self.project_filename = None
|
||||
|
||||
logger.info("Initializing command handlers...")
|
||||
|
||||
# Initialize command handlers
|
||||
self.project_commands = ProjectCommands(self.board)
|
||||
self.board_commands = BoardCommands(self.board)
|
||||
self.component_commands = ComponentCommands(self.board)
|
||||
self.routing_commands = RoutingCommands(self.board)
|
||||
self.design_rule_commands = DesignRuleCommands(self.board)
|
||||
self.export_commands = ExportCommands(self.board)
|
||||
|
||||
# Schematic-related classes don't need board reference
|
||||
# as they operate directly on schematic files
|
||||
|
||||
# Command routing dictionary
|
||||
self.command_routes = {
|
||||
# Project commands
|
||||
"create_project": self.project_commands.create_project,
|
||||
"open_project": self.project_commands.open_project,
|
||||
"save_project": self.project_commands.save_project,
|
||||
"get_project_info": self.project_commands.get_project_info,
|
||||
|
||||
# Board commands
|
||||
"set_board_size": self.board_commands.set_board_size,
|
||||
"add_layer": self.board_commands.add_layer,
|
||||
"set_active_layer": self.board_commands.set_active_layer,
|
||||
"get_board_info": self.board_commands.get_board_info,
|
||||
"get_layer_list": self.board_commands.get_layer_list,
|
||||
"get_board_2d_view": self.board_commands.get_board_2d_view,
|
||||
"add_board_outline": self.board_commands.add_board_outline,
|
||||
"add_mounting_hole": self.board_commands.add_mounting_hole,
|
||||
"add_text": self.board_commands.add_text,
|
||||
|
||||
# Component commands
|
||||
"place_component": self.component_commands.place_component,
|
||||
"move_component": self.component_commands.move_component,
|
||||
"rotate_component": self.component_commands.rotate_component,
|
||||
"delete_component": self.component_commands.delete_component,
|
||||
"edit_component": self.component_commands.edit_component,
|
||||
"get_component_properties": self.component_commands.get_component_properties,
|
||||
"get_component_list": self.component_commands.get_component_list,
|
||||
"place_component_array": self.component_commands.place_component_array,
|
||||
"align_components": self.component_commands.align_components,
|
||||
"duplicate_component": self.component_commands.duplicate_component,
|
||||
|
||||
# Routing commands
|
||||
"add_net": self.routing_commands.add_net,
|
||||
"route_trace": self.routing_commands.route_trace,
|
||||
"add_via": self.routing_commands.add_via,
|
||||
"delete_trace": self.routing_commands.delete_trace,
|
||||
"get_nets_list": self.routing_commands.get_nets_list,
|
||||
"create_netclass": self.routing_commands.create_netclass,
|
||||
"add_copper_pour": self.routing_commands.add_copper_pour,
|
||||
"route_differential_pair": self.routing_commands.route_differential_pair,
|
||||
|
||||
# Design rule commands
|
||||
"set_design_rules": self.design_rule_commands.set_design_rules,
|
||||
"get_design_rules": self.design_rule_commands.get_design_rules,
|
||||
"run_drc": self.design_rule_commands.run_drc,
|
||||
"get_drc_violations": self.design_rule_commands.get_drc_violations,
|
||||
|
||||
# Export commands
|
||||
"export_gerber": self.export_commands.export_gerber,
|
||||
"export_pdf": self.export_commands.export_pdf,
|
||||
"export_svg": self.export_commands.export_svg,
|
||||
"export_3d": self.export_commands.export_3d,
|
||||
"export_bom": self.export_commands.export_bom,
|
||||
|
||||
# Schematic commands
|
||||
"create_schematic": self._handle_create_schematic,
|
||||
"load_schematic": self._handle_load_schematic,
|
||||
"add_schematic_component": self._handle_add_schematic_component,
|
||||
"add_schematic_wire": self._handle_add_schematic_wire,
|
||||
"list_schematic_libraries": self._handle_list_schematic_libraries,
|
||||
"export_schematic_pdf": self._handle_export_schematic_pdf
|
||||
}
|
||||
|
||||
logger.info("KiCAD interface initialized")
|
||||
|
||||
def handle_command(self, command: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Route command to appropriate handler"""
|
||||
logger.info(f"Handling command: {command}")
|
||||
logger.debug(f"Command parameters: {params}")
|
||||
|
||||
try:
|
||||
# Get the handler for the command
|
||||
handler = self.command_routes.get(command)
|
||||
|
||||
if handler:
|
||||
# Execute the command
|
||||
result = handler(params)
|
||||
logger.debug(f"Command result: {result}")
|
||||
|
||||
# Update board reference if command was successful
|
||||
if result.get("success", False):
|
||||
if command == "create_project" or command == "open_project":
|
||||
logger.info("Updating board reference...")
|
||||
self.board = pcbnew.GetBoard()
|
||||
self._update_command_handlers()
|
||||
|
||||
return result
|
||||
else:
|
||||
logger.error(f"Unknown command: {command}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Unknown command: {command}",
|
||||
"errorDetails": "The specified command is not supported"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
# Get the full traceback
|
||||
traceback_str = traceback.format_exc()
|
||||
logger.error(f"Error handling command {command}: {str(e)}\n{traceback_str}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Error handling command: {command}",
|
||||
"errorDetails": f"{str(e)}\n{traceback_str}"
|
||||
}
|
||||
|
||||
def _update_command_handlers(self):
|
||||
"""Update board reference in all command handlers"""
|
||||
logger.debug("Updating board reference in command handlers")
|
||||
self.project_commands.board = self.board
|
||||
self.board_commands.board = self.board
|
||||
self.component_commands.board = self.board
|
||||
self.routing_commands.board = self.board
|
||||
self.design_rule_commands.board = self.board
|
||||
self.export_commands.board = self.board
|
||||
|
||||
# Schematic command handlers
|
||||
def _handle_create_schematic(self, params):
|
||||
"""Create a new schematic"""
|
||||
logger.info("Creating schematic")
|
||||
try:
|
||||
project_name = params.get("projectName")
|
||||
path = params.get("path", ".")
|
||||
metadata = params.get("metadata", {})
|
||||
|
||||
if not project_name:
|
||||
return {"success": False, "message": "Project name is required"}
|
||||
|
||||
schematic = SchematicManager.create_schematic(project_name, metadata)
|
||||
file_path = f"{path}/{project_name}.kicad_sch"
|
||||
success = SchematicManager.save_schematic(schematic, file_path)
|
||||
|
||||
return {"success": success, "file_path": file_path}
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating schematic: {str(e)}")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _handle_load_schematic(self, params):
|
||||
"""Load an existing schematic"""
|
||||
logger.info("Loading schematic")
|
||||
try:
|
||||
filename = params.get("filename")
|
||||
|
||||
if not filename:
|
||||
return {"success": False, "message": "Filename is required"}
|
||||
|
||||
schematic = SchematicManager.load_schematic(filename)
|
||||
success = schematic is not None
|
||||
|
||||
if success:
|
||||
metadata = SchematicManager.get_schematic_metadata(schematic)
|
||||
return {"success": success, "metadata": metadata}
|
||||
else:
|
||||
return {"success": False, "message": "Failed to load schematic"}
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading schematic: {str(e)}")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _handle_add_schematic_component(self, params):
|
||||
"""Add a component to a schematic"""
|
||||
logger.info("Adding component to schematic")
|
||||
try:
|
||||
schematic_path = params.get("schematicPath")
|
||||
component = params.get("component", {})
|
||||
|
||||
if not schematic_path:
|
||||
return {"success": False, "message": "Schematic path is required"}
|
||||
if not component:
|
||||
return {"success": False, "message": "Component definition is required"}
|
||||
|
||||
schematic = SchematicManager.load_schematic(schematic_path)
|
||||
if not schematic:
|
||||
return {"success": False, "message": "Failed to load schematic"}
|
||||
|
||||
component_obj = ComponentManager.add_component(schematic, component)
|
||||
success = component_obj is not None
|
||||
|
||||
if success:
|
||||
SchematicManager.save_schematic(schematic, schematic_path)
|
||||
return {"success": True}
|
||||
else:
|
||||
return {"success": False, "message": "Failed to add component"}
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding component to schematic: {str(e)}")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _handle_add_schematic_wire(self, params):
|
||||
"""Add a wire to a schematic"""
|
||||
logger.info("Adding wire to schematic")
|
||||
try:
|
||||
schematic_path = params.get("schematicPath")
|
||||
start_point = params.get("startPoint")
|
||||
end_point = params.get("endPoint")
|
||||
|
||||
if not schematic_path:
|
||||
return {"success": False, "message": "Schematic path is required"}
|
||||
if not start_point or not end_point:
|
||||
return {"success": False, "message": "Start and end points are required"}
|
||||
|
||||
schematic = SchematicManager.load_schematic(schematic_path)
|
||||
if not schematic:
|
||||
return {"success": False, "message": "Failed to load schematic"}
|
||||
|
||||
wire = ConnectionManager.add_wire(schematic, start_point, end_point)
|
||||
success = wire is not None
|
||||
|
||||
if success:
|
||||
SchematicManager.save_schematic(schematic, schematic_path)
|
||||
return {"success": True}
|
||||
else:
|
||||
return {"success": False, "message": "Failed to add wire"}
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding wire to schematic: {str(e)}")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _handle_list_schematic_libraries(self, params):
|
||||
"""List available symbol libraries"""
|
||||
logger.info("Listing schematic libraries")
|
||||
try:
|
||||
search_paths = params.get("searchPaths")
|
||||
|
||||
libraries = LibraryManager.list_available_libraries(search_paths)
|
||||
return {"success": True, "libraries": libraries}
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing schematic libraries: {str(e)}")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _handle_export_schematic_pdf(self, params):
|
||||
"""Export schematic to PDF"""
|
||||
logger.info("Exporting schematic to PDF")
|
||||
try:
|
||||
schematic_path = params.get("schematicPath")
|
||||
output_path = params.get("outputPath")
|
||||
|
||||
if not schematic_path:
|
||||
return {"success": False, "message": "Schematic path is required"}
|
||||
if not output_path:
|
||||
return {"success": False, "message": "Output path is required"}
|
||||
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
["kicad-cli", "sch", "export", "pdf", "--output", output_path, schematic_path],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
success = result.returncode == 0
|
||||
message = result.stderr if not success else ""
|
||||
|
||||
return {"success": success, "message": message}
|
||||
except Exception as e:
|
||||
logger.error(f"Error exporting schematic to PDF: {str(e)}")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
logger.info("Starting KiCAD interface...")
|
||||
interface = KiCADInterface()
|
||||
|
||||
try:
|
||||
logger.info("Processing commands from stdin...")
|
||||
# Process commands from stdin
|
||||
for line in sys.stdin:
|
||||
try:
|
||||
# Parse command
|
||||
logger.debug(f"Received input: {line.strip()}")
|
||||
command_data = json.loads(line)
|
||||
command = command_data.get("command")
|
||||
params = command_data.get("params", {})
|
||||
|
||||
if not command:
|
||||
logger.error("Missing command field")
|
||||
response = {
|
||||
"success": False,
|
||||
"message": "Missing command",
|
||||
"errorDetails": "The command field is required"
|
||||
}
|
||||
else:
|
||||
# Handle command
|
||||
response = interface.handle_command(command, params)
|
||||
|
||||
# Send response
|
||||
logger.debug(f"Sending response: {response}")
|
||||
print(json.dumps(response))
|
||||
sys.stdout.flush()
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Invalid JSON input: {str(e)}")
|
||||
response = {
|
||||
"success": False,
|
||||
"message": "Invalid JSON input",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
print(json.dumps(response))
|
||||
sys.stdout.flush()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("KiCAD interface stopped")
|
||||
sys.exit(0)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error: {str(e)}\n{traceback.format_exc()}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
13
python/requirements.txt
Normal file
13
python/requirements.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
# KiCAD MCP Python Interface Requirements
|
||||
|
||||
# Image processing
|
||||
Pillow>=9.0.0
|
||||
cairosvg>=2.7.0
|
||||
|
||||
# Type hints
|
||||
typing-extensions>=4.0.0
|
||||
|
||||
# Logging
|
||||
colorlog>=6.7.0
|
||||
|
||||
kicad-skip
|
||||
1
python/utils/__init__.py
Normal file
1
python/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Utility modules for KiCAD MCP Server"""
|
||||
271
python/utils/platform_helper.py
Normal file
271
python/utils/platform_helper.py
Normal file
@@ -0,0 +1,271 @@
|
||||
"""
|
||||
Platform detection and path utilities for cross-platform compatibility
|
||||
|
||||
This module provides helpers for detecting the current platform and
|
||||
getting appropriate paths for KiCAD, configuration, logs, etc.
|
||||
"""
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PlatformHelper:
|
||||
"""Platform detection and path resolution utilities"""
|
||||
|
||||
@staticmethod
|
||||
def is_windows() -> bool:
|
||||
"""Check if running on Windows"""
|
||||
return platform.system() == "Windows"
|
||||
|
||||
@staticmethod
|
||||
def is_linux() -> bool:
|
||||
"""Check if running on Linux"""
|
||||
return platform.system() == "Linux"
|
||||
|
||||
@staticmethod
|
||||
def is_macos() -> bool:
|
||||
"""Check if running on macOS"""
|
||||
return platform.system() == "Darwin"
|
||||
|
||||
@staticmethod
|
||||
def get_platform_name() -> str:
|
||||
"""Get human-readable platform name"""
|
||||
system = platform.system()
|
||||
if system == "Darwin":
|
||||
return "macOS"
|
||||
return system
|
||||
|
||||
@staticmethod
|
||||
def get_kicad_python_paths() -> List[Path]:
|
||||
"""
|
||||
Get potential KiCAD Python dist-packages paths for current platform
|
||||
|
||||
Returns:
|
||||
List of potential paths to check (in priority order)
|
||||
"""
|
||||
paths = []
|
||||
|
||||
if PlatformHelper.is_windows():
|
||||
# Windows: Check Program Files
|
||||
program_files = [
|
||||
Path("C:/Program Files/KiCad"),
|
||||
Path("C:/Program Files (x86)/KiCad"),
|
||||
]
|
||||
for pf in program_files:
|
||||
# Check multiple KiCAD versions
|
||||
for version in ["9.0", "9.1", "10.0", "8.0"]:
|
||||
path = pf / version / "lib" / "python3" / "dist-packages"
|
||||
if path.exists():
|
||||
paths.append(path)
|
||||
|
||||
elif PlatformHelper.is_linux():
|
||||
# Linux: Check common installation paths
|
||||
candidates = [
|
||||
Path("/usr/lib/kicad/lib/python3/dist-packages"),
|
||||
Path("/usr/share/kicad/scripting/plugins"),
|
||||
Path("/usr/local/lib/kicad/lib/python3/dist-packages"),
|
||||
Path.home() / ".local/lib/kicad/lib/python3/dist-packages",
|
||||
]
|
||||
|
||||
# Also check based on Python version
|
||||
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||
candidates.extend([
|
||||
Path(f"/usr/lib/python{py_version}/dist-packages/kicad"),
|
||||
Path(f"/usr/local/lib/python{py_version}/dist-packages/kicad"),
|
||||
])
|
||||
|
||||
paths = [p for p in candidates if p.exists()]
|
||||
|
||||
elif PlatformHelper.is_macos():
|
||||
# macOS: Check application bundle
|
||||
kicad_app = Path("/Applications/KiCad/KiCad.app")
|
||||
if kicad_app.exists():
|
||||
# Check Python framework path
|
||||
for version in ["3.9", "3.10", "3.11", "3.12"]:
|
||||
path = kicad_app / "Contents" / "Frameworks" / "Python.framework" / "Versions" / version / "lib" / f"python{version}" / "site-packages"
|
||||
if path.exists():
|
||||
paths.append(path)
|
||||
|
||||
if not paths:
|
||||
logger.warning(f"No KiCAD Python paths found for {PlatformHelper.get_platform_name()}")
|
||||
else:
|
||||
logger.info(f"Found {len(paths)} potential KiCAD Python paths")
|
||||
|
||||
return paths
|
||||
|
||||
@staticmethod
|
||||
def get_kicad_python_path() -> Optional[Path]:
|
||||
"""
|
||||
Get the first valid KiCAD Python path
|
||||
|
||||
Returns:
|
||||
Path to KiCAD Python dist-packages, or None if not found
|
||||
"""
|
||||
paths = PlatformHelper.get_kicad_python_paths()
|
||||
return paths[0] if paths else None
|
||||
|
||||
@staticmethod
|
||||
def get_kicad_library_search_paths() -> List[str]:
|
||||
"""
|
||||
Get platform-appropriate KiCAD symbol library search paths
|
||||
|
||||
Returns:
|
||||
List of glob patterns for finding .kicad_sym files
|
||||
"""
|
||||
patterns = []
|
||||
|
||||
if PlatformHelper.is_windows():
|
||||
patterns = [
|
||||
"C:/Program Files/KiCad/*/share/kicad/symbols/*.kicad_sym",
|
||||
"C:/Program Files (x86)/KiCad/*/share/kicad/symbols/*.kicad_sym",
|
||||
]
|
||||
elif PlatformHelper.is_linux():
|
||||
patterns = [
|
||||
"/usr/share/kicad/symbols/*.kicad_sym",
|
||||
"/usr/local/share/kicad/symbols/*.kicad_sym",
|
||||
str(Path.home() / ".local/share/kicad/symbols/*.kicad_sym"),
|
||||
]
|
||||
elif PlatformHelper.is_macos():
|
||||
patterns = [
|
||||
"/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/*.kicad_sym",
|
||||
]
|
||||
|
||||
# Add user library paths for all platforms
|
||||
patterns.append(str(Path.home() / "Documents" / "KiCad" / "*" / "symbols" / "*.kicad_sym"))
|
||||
|
||||
return patterns
|
||||
|
||||
@staticmethod
|
||||
def get_config_dir() -> Path:
|
||||
r"""
|
||||
Get appropriate configuration directory for current platform
|
||||
|
||||
Follows platform conventions:
|
||||
- Windows: %USERPROFILE%\.kicad-mcp
|
||||
- Linux: $XDG_CONFIG_HOME/kicad-mcp or ~/.config/kicad-mcp
|
||||
- macOS: ~/Library/Application Support/kicad-mcp
|
||||
|
||||
Returns:
|
||||
Path to configuration directory
|
||||
"""
|
||||
if PlatformHelper.is_windows():
|
||||
return Path.home() / ".kicad-mcp"
|
||||
elif PlatformHelper.is_linux():
|
||||
# Use XDG Base Directory specification
|
||||
xdg_config = os.environ.get("XDG_CONFIG_HOME")
|
||||
if xdg_config:
|
||||
return Path(xdg_config) / "kicad-mcp"
|
||||
return Path.home() / ".config" / "kicad-mcp"
|
||||
elif PlatformHelper.is_macos():
|
||||
return Path.home() / "Library" / "Application Support" / "kicad-mcp"
|
||||
else:
|
||||
# Fallback for unknown platforms
|
||||
return Path.home() / ".kicad-mcp"
|
||||
|
||||
@staticmethod
|
||||
def get_log_dir() -> Path:
|
||||
"""
|
||||
Get appropriate log directory for current platform
|
||||
|
||||
Returns:
|
||||
Path to log directory
|
||||
"""
|
||||
config_dir = PlatformHelper.get_config_dir()
|
||||
return config_dir / "logs"
|
||||
|
||||
@staticmethod
|
||||
def get_cache_dir() -> Path:
|
||||
r"""
|
||||
Get appropriate cache directory for current platform
|
||||
|
||||
Follows platform conventions:
|
||||
- Windows: %USERPROFILE%\.kicad-mcp\cache
|
||||
- Linux: $XDG_CACHE_HOME/kicad-mcp or ~/.cache/kicad-mcp
|
||||
- macOS: ~/Library/Caches/kicad-mcp
|
||||
|
||||
Returns:
|
||||
Path to cache directory
|
||||
"""
|
||||
if PlatformHelper.is_windows():
|
||||
return PlatformHelper.get_config_dir() / "cache"
|
||||
elif PlatformHelper.is_linux():
|
||||
xdg_cache = os.environ.get("XDG_CACHE_HOME")
|
||||
if xdg_cache:
|
||||
return Path(xdg_cache) / "kicad-mcp"
|
||||
return Path.home() / ".cache" / "kicad-mcp"
|
||||
elif PlatformHelper.is_macos():
|
||||
return Path.home() / "Library" / "Caches" / "kicad-mcp"
|
||||
else:
|
||||
return PlatformHelper.get_config_dir() / "cache"
|
||||
|
||||
@staticmethod
|
||||
def ensure_directories() -> None:
|
||||
"""Create all necessary directories if they don't exist"""
|
||||
dirs_to_create = [
|
||||
PlatformHelper.get_config_dir(),
|
||||
PlatformHelper.get_log_dir(),
|
||||
PlatformHelper.get_cache_dir(),
|
||||
]
|
||||
|
||||
for directory in dirs_to_create:
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
logger.debug(f"Ensured directory exists: {directory}")
|
||||
|
||||
@staticmethod
|
||||
def get_python_executable() -> Path:
|
||||
"""Get path to current Python executable"""
|
||||
return Path(sys.executable)
|
||||
|
||||
@staticmethod
|
||||
def add_kicad_to_python_path() -> bool:
|
||||
"""
|
||||
Add KiCAD Python paths to sys.path
|
||||
|
||||
Returns:
|
||||
True if at least one path was added, False otherwise
|
||||
"""
|
||||
paths_added = False
|
||||
|
||||
for path in PlatformHelper.get_kicad_python_paths():
|
||||
if str(path) not in sys.path:
|
||||
sys.path.insert(0, str(path))
|
||||
logger.info(f"Added to Python path: {path}")
|
||||
paths_added = True
|
||||
|
||||
return paths_added
|
||||
|
||||
|
||||
# Convenience function for quick platform detection
|
||||
def detect_platform() -> dict:
|
||||
"""
|
||||
Detect platform and return useful information
|
||||
|
||||
Returns:
|
||||
Dictionary with platform information
|
||||
"""
|
||||
return {
|
||||
"system": platform.system(),
|
||||
"platform": PlatformHelper.get_platform_name(),
|
||||
"is_windows": PlatformHelper.is_windows(),
|
||||
"is_linux": PlatformHelper.is_linux(),
|
||||
"is_macos": PlatformHelper.is_macos(),
|
||||
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
||||
"python_executable": str(PlatformHelper.get_python_executable()),
|
||||
"config_dir": str(PlatformHelper.get_config_dir()),
|
||||
"log_dir": str(PlatformHelper.get_log_dir()),
|
||||
"cache_dir": str(PlatformHelper.get_cache_dir()),
|
||||
"kicad_python_paths": [str(p) for p in PlatformHelper.get_kicad_python_paths()],
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Quick test/diagnostic
|
||||
import json
|
||||
info = detect_platform()
|
||||
print("Platform Information:")
|
||||
print(json.dumps(info, indent=2))
|
||||
Reference in New Issue
Block a user