Skip to main content
Glama
component.py38.3 kB
""" 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 from commands.library import LibraryManager 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 if footprint: module.SetFootprintName(footprint) # 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: 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.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 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) new_module.SetFootprintName(source.GetFPIDAsString()) 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 _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}")

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mixelpixx/KiCAD-MCP-Server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server