Files
KiCAD-MCP-Server/python/commands/component.py
T
2026-03-08 03:17:21 +01:00

1714 lines
67 KiB
Python

"""
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
import io
from commands.library import LibraryManager
from PIL import Image, ImageDraw
logger = logging.getLogger('kicad_interface')
class ComponentCommands:
"""Handles component-related KiCAD operations"""
def __init__(self, board: Optional[pcbnew.BOARD] = None, library_manager: Optional[LibraryManager] = None):
"""Initialize with optional board instance and library manager"""
self.board = board
self.library_manager = library_manager or LibraryManager()
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"
}
# Find footprint using library manager
# component_id can be "Library:Footprint" or just "Footprint"
footprint_result = self.library_manager.find_footprint(component_id)
if not footprint_result:
# Try to suggest similar footprints
suggestions = self.library_manager.search_footprints(f"*{component_id}*", limit=5)
suggestion_text = ""
if suggestions:
suggestion_text = "\n\nDid you mean one of these?\n" + \
"\n".join([f" - {s['full_name']}" for s in suggestions])
return {
"success": False,
"message": "Footprint not found",
"errorDetails": f"Could not find footprint: {component_id}{suggestion_text}"
}
library_path, footprint_name = footprint_result
# Load footprint from library
# Extract library nickname from path
library_nickname = None
for nick, path in self.library_manager.libraries.items():
if path == library_path:
library_nickname = nick
break
if not library_nickname:
return {
"success": False,
"message": "Internal error",
"errorDetails": "Could not determine library nickname"
}
# Load the footprint
module = pcbnew.FootprintLoad(library_path, footprint_name)
if not module:
return {
"success": False,
"message": "Failed to load footprint",
"errorDetails": f"Could not load footprint from {library_path}/{footprint_name}"
}
# 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 (use existing library_nickname and footprint_name)
# For KiCAD 9.x compatibility, use SetFPID instead of SetFootprintName
if footprint:
# Parse footprint string if it's in "Library:Footprint" format
if ":" in footprint:
lib_name, fp_name = footprint.split(":", 1)
else:
# Use the library_nickname we already have from loading
lib_name = library_nickname
fp_name = footprint
fpid = pcbnew.LIB_ID(lib_name, fp_name)
module.SetFPID(fpid)
else:
# Use the footprint we just loaded
fpid = pcbnew.LIB_ID(library_nickname, footprint_name)
module.SetFPID(fpid)
# Set rotation (KiCAD 9.0 uses EDA_ANGLE)
angle = pcbnew.EDA_ANGLE(rotation, pcbnew.DEGREES_T)
module.SetOrientation(angle)
# 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:
angle = pcbnew.EDA_ANGLE(rotation, pcbnew.DEGREES_T)
module.SetOrientation(angle)
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().AsDegrees()
}
}
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
rotation_angle = pcbnew.EDA_ANGLE(angle, pcbnew.DEGREES_T)
module.SetOrientation(rotation_angle)
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:
# For KiCAD 9.x compatibility, use SetFPID instead of SetFootprintName
# Parse footprint string (format: "Library:Footprint")
if ":" in footprint:
lib_name, fp_name = footprint.split(":", 1)
fpid = pcbnew.LIB_ID(lib_name, fp_name)
module.SetFPID(fpid)
else:
# If no library specified, keep existing library
current_fpid = module.GetFPID()
lib_name = current_fpid.GetLibNickname().GetUTF8()
fpid = pcbnew.LIB_ID(lib_name, footprint)
module.SetFPID(fpid)
return {
"success": True,
"message": f"Updated component: {reference}",
"component": {
"reference": new_reference or reference,
"value": value or module.GetValue(),
"footprint": footprint or module.GetFPIDAsString()
}
}
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.GetFPIDAsString(),
"position": {
"x": x_mm,
"y": y_mm,
"unit": "mm"
},
"rotation": module.GetOrientation().AsDegrees(),
"layer": self.board.GetLayerName(module.GetLayer()),
"attributes": {
"smd": module.GetAttributes() & pcbnew.FP_SMD,
"through_hole": module.GetAttributes() & pcbnew.FP_THROUGH_HOLE,
"board_only": module.GetAttributes() & pcbnew.FP_BOARD_ONLY
}
}
}
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.GetFPIDAsString(),
"position": {
"x": x_mm,
"y": y_mm,
"unit": "mm"
},
"rotation": module.GetOrientation().AsDegrees(),
"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 find_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Find components matching search criteria (reference, value, or footprint pattern)"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
# Get search parameters
reference_pattern = params.get("reference", "").lower()
value_pattern = params.get("value", "").lower()
footprint_pattern = params.get("footprint", "").lower()
if not reference_pattern and not value_pattern and not footprint_pattern:
return {
"success": False,
"message": "Missing search criteria",
"errorDetails": "At least one of reference, value, or footprint pattern is required"
}
matches = []
for module in self.board.GetFootprints():
ref = module.GetReference().lower()
val = module.GetValue().lower()
fp = module.GetFPIDAsString().lower()
# Check if component matches all provided patterns
match = True
if reference_pattern and reference_pattern not in ref:
match = False
if value_pattern and value_pattern not in val:
match = False
if footprint_pattern and footprint_pattern not in fp:
match = False
if match:
pos = module.GetPosition()
matches.append({
"reference": module.GetReference(),
"value": module.GetValue(),
"footprint": module.GetFPIDAsString(),
"position": {
"x": pos.x / 1000000,
"y": pos.y / 1000000,
"unit": "mm"
},
"rotation": module.GetOrientation().AsDegrees(),
"layer": self.board.GetLayerName(module.GetLayer())
})
return {
"success": True,
"matchCount": len(matches),
"components": matches
}
except Exception as e:
logger.error(f"Error finding components: {str(e)}")
return {
"success": False,
"message": "Failed to find components",
"errorDetails": str(e)
}
def get_component_pads(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get all pads for a component with their positions and net connections"""
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}"
}
pads = []
for pad in module.Pads():
pos = pad.GetPosition()
size = pad.GetSize()
# Get pad shape as string
shape_map = {
pcbnew.PAD_SHAPE_CIRCLE: "circle",
pcbnew.PAD_SHAPE_RECT: "rect",
pcbnew.PAD_SHAPE_OVAL: "oval",
pcbnew.PAD_SHAPE_TRAPEZOID: "trapezoid",
pcbnew.PAD_SHAPE_ROUNDRECT: "roundrect",
pcbnew.PAD_SHAPE_CHAMFERED_RECT: "chamfered_rect",
pcbnew.PAD_SHAPE_CUSTOM: "custom"
}
shape = shape_map.get(pad.GetShape(), "unknown")
# Get pad type
type_map = {
pcbnew.PAD_ATTRIB_PTH: "through_hole",
pcbnew.PAD_ATTRIB_SMD: "smd",
pcbnew.PAD_ATTRIB_CONN: "connector",
pcbnew.PAD_ATTRIB_NPTH: "npth"
}
pad_type = type_map.get(pad.GetAttribute(), "unknown")
pads.append({
"name": pad.GetName(),
"number": pad.GetNumber(),
"position": {
"x": pos.x / 1000000,
"y": pos.y / 1000000,
"unit": "mm"
},
"net": pad.GetNetname(),
"netCode": pad.GetNetCode(),
"shape": shape,
"type": pad_type,
"size": {
"x": size.x / 1000000,
"y": size.y / 1000000,
"unit": "mm"
},
"drillSize": pad.GetDrillSize().x / 1000000 if pad.GetDrillSize().x > 0 else None
})
# Get component position for reference
comp_pos = module.GetPosition()
return {
"success": True,
"reference": reference,
"componentPosition": {
"x": comp_pos.x / 1000000,
"y": comp_pos.y / 1000000,
"unit": "mm"
},
"padCount": len(pads),
"pads": pads
}
except Exception as e:
logger.error(f"Error getting component pads: {str(e)}")
return {
"success": False,
"message": "Failed to get component pads",
"errorDetails": str(e)
}
def get_pad_position(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get the position of a specific pad on 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")
pad_name = params.get("padName") or params.get("padNumber")
if not reference:
return {
"success": False,
"message": "Missing reference",
"errorDetails": "reference parameter is required"
}
if not pad_name:
return {
"success": False,
"message": "Missing pad identifier",
"errorDetails": "padName or padNumber 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}"
}
# Find the specific pad
pad = module.FindPadByNumber(str(pad_name))
if not pad:
# List available pads in error message
available_pads = [p.GetNumber() for p in module.Pads()]
return {
"success": False,
"message": "Pad not found",
"errorDetails": f"Pad '{pad_name}' not found on {reference}. Available pads: {', '.join(available_pads)}"
}
pos = pad.GetPosition()
size = pad.GetSize()
return {
"success": True,
"reference": reference,
"padName": pad.GetNumber(),
"position": {
"x": pos.x / 1000000,
"y": pos.y / 1000000,
"unit": "mm"
},
"net": pad.GetNetname(),
"netCode": pad.GetNetCode(),
"size": {
"x": size.x / 1000000,
"y": size.y / 1000000,
"unit": "mm"
}
}
except Exception as e:
logger.error(f"Error getting pad position: {str(e)}")
return {
"success": False,
"message": "Failed to get pad position",
"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().AsDegrees()
})
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)
# For KiCAD 9.x compatibility, use SetFPID instead of SetFootprintName
new_module.SetFPID(source.GetFPID())
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:
rotation_angle = pcbnew.EDA_ANGLE(rotation, pcbnew.DEGREES_T)
new_module.SetOrientation(rotation_angle)
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.GetFPIDAsString(),
"position": {
"x": pos.x / 1000000,
"y": pos.y / 1000000,
"unit": "mm"
},
"rotation": new_module.GetOrientation().AsDegrees(),
"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 add_component_annotation(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Add a text annotation close to a component footprint."""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first",
}
reference = params.get("reference")
annotation = params.get("annotation")
visible = params.get("visible", True)
if not reference or not annotation:
return {
"success": False,
"message": "Missing parameters",
"errorDetails": "reference and annotation are required",
}
module = self.board.FindFootprintByReference(reference)
if not module:
return {
"success": False,
"message": "Component not found",
"errorDetails": f"Could not find component: {reference}",
}
bbox = module.GetBoundingBox()
layer_name = "B.SilkS" if self.board.GetLayerName(module.GetLayer()).startswith("B.") else "F.SilkS"
layer_id = self.board.GetLayerID(layer_name)
text_item = pcbnew.PCB_TEXT(self.board)
text_item.SetText(annotation)
text_item.SetLayer(layer_id)
text_item.SetPosition(
pcbnew.VECTOR2I(
bbox.GetRight() + 800000,
bbox.GetTop() - 800000,
)
)
text_item.SetTextSize(pcbnew.VECTOR2I(1000000, 1000000))
text_item.SetTextThickness(150000)
if hasattr(text_item, "SetVisible"):
text_item.SetVisible(bool(visible))
self.board.Add(text_item)
return {
"success": True,
"message": f"Added annotation to {reference}",
"annotation": {
"reference": reference,
"text": annotation,
"layer": layer_name,
"visible": bool(visible),
},
}
except Exception as e:
logger.error(f"Error adding component annotation: {str(e)}")
return {
"success": False,
"message": "Failed to add component annotation",
"errorDetails": str(e),
}
def group_components(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Create or extend a real KiCad PCB group with the selected components."""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first",
}
references = params.get("references", [])
group_name = params.get("groupName")
if not references or not group_name:
return {
"success": False,
"message": "Missing parameters",
"errorDetails": "references and groupName are required",
}
modules = []
missing = []
for reference in references:
module = self.board.FindFootprintByReference(reference)
if not module:
missing.append(reference)
continue
modules.append(module)
if not modules:
return {
"success": False,
"message": "No valid components found",
"errorDetails": f"Missing references: {', '.join(missing)}",
}
group = None
if hasattr(self.board, "Groups"):
for existing in self.board.Groups():
name = existing.GetName() if hasattr(existing, "GetName") else ""
if name == group_name:
group = existing
break
if group is None:
group = pcbnew.PCB_GROUP(self.board)
if hasattr(group, "SetName"):
group.SetName(group_name)
self.board.Add(group)
for module in modules:
group.AddItem(module)
if hasattr(module, "SetParentGroup"):
module.SetParentGroup(group)
return {
"success": True,
"message": f"Grouped {len(modules)} component(s) into {group_name}",
"group": {
"name": group_name,
"references": [module.GetReference() for module in modules],
"missing": missing,
},
}
except Exception as e:
logger.error(f"Error grouping components: {str(e)}")
return {
"success": False,
"message": "Failed to group components",
"errorDetails": str(e),
}
def replace_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Replace a component footprint while preserving placement and reference."""
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_component_id = params.get("newComponentId")
new_footprint = params.get("newFootprint")
new_value = params.get("newValue")
if not reference or not new_component_id:
return {
"success": False,
"message": "Missing parameters",
"errorDetails": "reference and newComponentId are required",
}
module = self.board.FindFootprintByReference(reference)
if not module:
return {
"success": False,
"message": "Component not found",
"errorDetails": f"Could not find component: {reference}",
}
pos = module.GetPosition()
layer = self.board.GetLayerName(module.GetLayer())
rotation = module.GetOrientation().AsDegrees()
value = new_value or module.GetValue()
temp_reference = f"{reference}_TMP"
replacement_spec = new_footprint or new_component_id
place_result = self.place_component(
{
"componentId": new_component_id,
"position": {
"x": pos.x / 1000000,
"y": pos.y / 1000000,
"unit": "mm",
},
"reference": temp_reference,
"value": value,
"footprint": replacement_spec,
"rotation": rotation,
"layer": layer,
}
)
if not place_result.get("success"):
return place_result
delete_result = self.delete_component({"reference": reference})
if not delete_result.get("success"):
return delete_result
rename_result = self.edit_component(
{
"reference": temp_reference,
"newReference": reference,
"value": value,
"footprint": replacement_spec,
}
)
if not rename_result.get("success"):
return rename_result
return {
"success": True,
"message": f"Replaced component {reference}",
"component": {
"reference": reference,
"newComponentId": new_component_id,
"footprint": replacement_spec,
"value": value,
},
}
except Exception as e:
logger.error(f"Error replacing component: {str(e)}")
return {
"success": False,
"message": "Failed to replace component",
"errorDetails": str(e),
}
def get_component_connections(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get pad and net connectivity for 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",
}
module = self.board.FindFootprintByReference(reference)
if not module:
return {
"success": False,
"message": "Component not found",
"errorDetails": f"Could not find component: {reference}",
}
nets = {}
pads = []
for pad in module.Pads():
net_name = pad.GetNetname() or ""
pad_entry = {
"pad": pad.GetNumber(),
"net": net_name,
"position": {
"x": pad.GetPosition().x / 1000000,
"y": pad.GetPosition().y / 1000000,
"unit": "mm",
},
}
pads.append(pad_entry)
if net_name:
nets.setdefault(net_name, []).append(pad.GetNumber())
related_components = {}
for other in self.board.GetFootprints():
other_ref = other.GetReference()
if other_ref == reference:
continue
for pad in other.Pads():
net_name = pad.GetNetname() or ""
if net_name and net_name in nets:
related_components.setdefault(net_name, set()).add(other_ref)
return {
"success": True,
"reference": reference,
"pads": pads,
"connections": [
{
"net": net_name,
"pads": pad_numbers,
"connectedReferences": sorted(related_components.get(net_name, set())),
}
for net_name, pad_numbers in sorted(nets.items())
],
}
except Exception as e:
logger.error(f"Error getting component connections: {str(e)}")
return {
"success": False,
"message": "Failed to get component connections",
"errorDetails": str(e),
}
def get_component_placement(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get placement data for 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",
}
placements = []
for module in self.board.GetFootprints():
bbox = module.GetBoundingBox()
placements.append(
{
"reference": module.GetReference(),
"value": module.GetValue(),
"footprint": module.GetFPIDAsString(),
"layer": self.board.GetLayerName(module.GetLayer()),
"position": {
"x": module.GetPosition().x / 1000000,
"y": module.GetPosition().y / 1000000,
"unit": "mm",
},
"rotation": module.GetOrientation().AsDegrees(),
"bounds": {
"left": bbox.GetLeft() / 1000000,
"top": bbox.GetTop() / 1000000,
"right": bbox.GetRight() / 1000000,
"bottom": bbox.GetBottom() / 1000000,
"unit": "mm",
},
}
)
return {
"success": True,
"placements": placements,
"count": len(placements),
}
except Exception as e:
logger.error(f"Error getting component placement: {str(e)}")
return {
"success": False,
"message": "Failed to get component placement",
"errorDetails": str(e),
}
def get_component_groups(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get explicit KiCad groups, or derived prefix groups when none exist."""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first",
}
grouped = {}
derived = {}
for module in self.board.GetFootprints():
reference = module.GetReference()
parent_group = module.GetParentGroup() if hasattr(module, "GetParentGroup") else None
if parent_group:
name = ""
if hasattr(parent_group, "GetName"):
name = parent_group.GetName()
if not name and hasattr(parent_group, "GetFriendlyName"):
name = parent_group.GetFriendlyName()
name = name or "unnamed-group"
grouped.setdefault(name, {"name": name, "source": "board", "references": []})
grouped[name]["references"].append(reference)
else:
prefix = "".join(ch for ch in reference if ch.isalpha()) or "misc"
derived.setdefault(prefix, {"name": prefix, "source": "derived-prefix", "references": []})
derived[prefix]["references"].append(reference)
groups = list(grouped.values()) if grouped else list(derived.values())
for group in groups:
group["references"].sort()
group["count"] = len(group["references"])
return {
"success": True,
"groups": sorted(groups, key=lambda group: group["name"]),
"count": len(groups),
}
except Exception as e:
logger.error(f"Error getting component groups: {str(e)}")
return {
"success": False,
"message": "Failed to get component groups",
"errorDetails": str(e),
}
def get_component_visualization(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Render a lightweight top-down PNG preview for a component footprint."""
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",
}
module = self.board.FindFootprintByReference(reference)
if not module:
return {
"success": False,
"message": "Component not found",
"errorDetails": f"Could not find component: {reference}",
}
bbox = module.GetBoundingBox()
margin_nm = 500000
min_x = bbox.GetLeft() - margin_nm
min_y = bbox.GetTop() - margin_nm
max_x = bbox.GetRight() + margin_nm
max_y = bbox.GetBottom() + margin_nm
width_nm = max(max_x - min_x, 1000000)
height_nm = max(max_y - min_y, 1000000)
canvas_size = 320
padding = 20
scale = min(
(canvas_size - 2 * padding) / width_nm,
(canvas_size - 2 * padding) / height_nm,
)
def to_px(x_nm: int, y_nm: int) -> Tuple[int, int]:
x = padding + int((x_nm - min_x) * scale)
y = padding + int((y_nm - min_y) * scale)
return x, y
image = Image.new("RGBA", (canvas_size, canvas_size), (255, 255, 255, 0))
draw = ImageDraw.Draw(image)
body_left, body_top = to_px(bbox.GetLeft(), bbox.GetTop())
body_right, body_bottom = to_px(bbox.GetRight(), bbox.GetBottom())
draw.rounded_rectangle(
[body_left, body_top, body_right, body_bottom],
radius=10,
outline=(36, 74, 124, 255),
width=3,
fill=(230, 238, 250, 180),
)
for pad in module.Pads():
pad_box = pad.GetBoundingBox()
x0, y0 = to_px(pad_box.GetLeft(), pad_box.GetTop())
x1, y1 = to_px(pad_box.GetRight(), pad_box.GetBottom())
shape = pad.GetShape()
circle_shapes = {
getattr(pcbnew, "PAD_SHAPE_CIRCLE", object()),
getattr(pcbnew, "PAD_SHAPE_OVAL", object()),
}
if shape in circle_shapes:
draw.ellipse([x0, y0, x1, y1], fill=(201, 96, 74, 255), outline=(122, 41, 27, 255))
else:
draw.rounded_rectangle(
[x0, y0, x1, y1],
radius=4,
fill=(201, 96, 74, 255),
outline=(122, 41, 27, 255),
)
draw.text((16, 12), reference, fill=(22, 22, 22, 255))
buffer = io.BytesIO()
image.save(buffer, format="PNG")
return {
"success": True,
"imageData": base64.b64encode(buffer.getvalue()).decode("utf-8"),
"format": "png",
"reference": reference,
}
except Exception as e:
logger.error(f"Error getting component visualization: {str(e)}")
return {
"success": False,
"message": "Failed to get component visualization",
"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}")