diff --git a/python/commands/component.py b/python/commands/component.py index 40fa9e7..9489060 100644 --- a/python/commands/component.py +++ b/python/commands/component.py @@ -483,7 +483,244 @@ class ComponentCommands: "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: diff --git a/python/commands/routing.py b/python/commands/routing.py index 587d8ac..19de30f 100644 --- a/python/commands/routing.py +++ b/python/commands/routing.py @@ -366,7 +366,123 @@ class RoutingCommands: "message": "Failed to get nets list", "errorDetails": str(e) } - + + def query_traces(self, params: Dict[str, Any]) -> Dict[str, Any]: + """Query traces by net, layer, or bounding box""" + try: + if not self.board: + return { + "success": False, + "message": "No board is loaded", + "errorDetails": "Load or create a board first" + } + + # Get filter parameters + net_name = params.get("net") + layer = params.get("layer") + bbox = params.get("boundingBox") # {x1, y1, x2, y2, unit} + include_vias = params.get("includeVias", False) + + scale = 1000000 # nm to mm conversion factor + traces = [] + vias = [] + + # Process tracks + for track in self.board.Tracks(): + # Check if it's a via + is_via = track.Type() == pcbnew.PCB_VIA_T + + if is_via and not include_vias: + continue + + # Filter by net + if net_name and track.GetNetname() != net_name: + continue + + # Filter by layer (only for tracks, not vias) + if layer and not is_via: + layer_id = self.board.GetLayerID(layer) + if track.GetLayer() != layer_id: + continue + + # Filter by bounding box + if bbox: + bbox_unit = bbox.get("unit", "mm") + bbox_scale = scale if bbox_unit == "mm" else 25400000 + x1 = int(bbox.get("x1", 0) * bbox_scale) + y1 = int(bbox.get("y1", 0) * bbox_scale) + x2 = int(bbox.get("x2", 0) * bbox_scale) + y2 = int(bbox.get("y2", 0) * bbox_scale) + + if is_via: + pos = track.GetPosition() + if not (x1 <= pos.x <= x2 and y1 <= pos.y <= y2): + continue + else: + start = track.GetStart() + end = track.GetEnd() + # Check if either endpoint is within bbox + start_in = x1 <= start.x <= x2 and y1 <= start.y <= y2 + end_in = x1 <= end.x <= x2 and y1 <= end.y <= y2 + if not (start_in or end_in): + continue + + if is_via: + pos = track.GetPosition() + vias.append({ + "uuid": str(track.m_Uuid), + "position": { + "x": pos.x / scale, + "y": pos.y / scale, + "unit": "mm" + }, + "net": track.GetNetname(), + "netCode": track.GetNetCode(), + "diameter": track.GetWidth() / scale, + "drill": track.GetDrillValue() / scale + }) + else: + start = track.GetStart() + end = track.GetEnd() + traces.append({ + "uuid": str(track.m_Uuid), + "net": track.GetNetname(), + "netCode": track.GetNetCode(), + "layer": self.board.GetLayerName(track.GetLayer()), + "width": track.GetWidth() / scale, + "start": { + "x": start.x / scale, + "y": start.y / scale, + "unit": "mm" + }, + "end": { + "x": end.x / scale, + "y": end.y / scale, + "unit": "mm" + }, + "length": track.GetLength() / scale + }) + + result = { + "success": True, + "traceCount": len(traces), + "traces": traces + } + + if include_vias: + result["viaCount"] = len(vias) + result["vias"] = vias + + return result + + except Exception as e: + logger.error(f"Error querying traces: {str(e)}") + return { + "success": False, + "message": "Failed to query traces", + "errorDetails": str(e) + } + def create_netclass(self, params: Dict[str, Any]) -> Dict[str, Any]: """Create a new net class with specified properties""" try: diff --git a/python/kicad_interface.py b/python/kicad_interface.py index 10ac0cb..fcc9e6c 100644 --- a/python/kicad_interface.py +++ b/python/kicad_interface.py @@ -288,6 +288,7 @@ class KiCADInterface: "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, + "get_board_extents": self.board_commands.get_board_extents, "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, @@ -301,6 +302,9 @@ class KiCADInterface: "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, + "find_component": self.component_commands.find_component, + "get_component_pads": self.component_commands.get_component_pads, + "get_pad_position": self.component_commands.get_pad_position, "place_component_array": self.component_commands.place_component_array, "align_components": self.component_commands.align_components, "duplicate_component": self.component_commands.duplicate_component, @@ -310,6 +314,7 @@ class KiCADInterface: "route_trace": self.routing_commands.route_trace, "add_via": self.routing_commands.add_via, "delete_trace": self.routing_commands.delete_trace, + "query_traces": self.routing_commands.query_traces, "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, diff --git a/python/schemas/tool_schemas.py b/python/schemas/tool_schemas.py index da59536..4b7b195 100644 --- a/python/schemas/tool_schemas.py +++ b/python/schemas/tool_schemas.py @@ -224,6 +224,22 @@ BOARD_TOOLS = [ } } }, + { + "name": "get_board_extents", + "title": "Get Board Bounding Box", + "description": "Returns the bounding box extents of the PCB board including all edge cuts, components, and traces.", + "inputSchema": { + "type": "object", + "properties": { + "unit": { + "type": "string", + "enum": ["mm", "inch"], + "description": "Unit for returned coordinates (default: mm)", + "default": "mm" + } + } + } + }, { "name": "add_mounting_hole", "title": "Add Mounting Hole", @@ -440,6 +456,66 @@ COMPONENT_TOOLS = [ "properties": {} } }, + { + "name": "find_component", + "title": "Find Components", + "description": "Searches for components matching specified criteria. Supports partial matching on reference, value, or footprint patterns.", + "inputSchema": { + "type": "object", + "properties": { + "reference": { + "type": "string", + "description": "Reference designator pattern to match (e.g., 'R1', 'U', 'C2')" + }, + "value": { + "type": "string", + "description": "Value pattern to match (e.g., '10k', '100nF')" + }, + "footprint": { + "type": "string", + "description": "Footprint pattern to match (e.g., '0805', 'SOIC')" + } + } + } + }, + { + "name": "get_component_pads", + "title": "Get Component Pads", + "description": "Returns all pads for a component with their positions, net connections, sizes, and shapes.", + "inputSchema": { + "type": "object", + "properties": { + "reference": { + "type": "string", + "description": "Component reference designator (e.g., U1, R5)" + } + }, + "required": ["reference"] + } + }, + { + "name": "get_pad_position", + "title": "Get Pad Position", + "description": "Returns the position and properties of a specific pad on a component.", + "inputSchema": { + "type": "object", + "properties": { + "reference": { + "type": "string", + "description": "Component reference designator" + }, + "padName": { + "type": "string", + "description": "Pad name or number (e.g., '1', '2', 'A1')" + }, + "padNumber": { + "type": "string", + "description": "Alternative to padName - pad number" + } + }, + "required": ["reference"] + } + }, { "name": "place_component_array", "title": "Place Component Array", @@ -671,6 +747,40 @@ ROUTING_TOOLS = [ "required": ["traceId"] } }, + { + "name": "query_traces", + "title": "Query Traces", + "description": "Queries traces on the board with optional filters by net, layer, or bounding box. Returns trace details including UUID, positions, width, and length.", + "inputSchema": { + "type": "object", + "properties": { + "net": { + "type": "string", + "description": "Filter by net name (e.g., 'GND', 'VCC')" + }, + "layer": { + "type": "string", + "description": "Filter by layer name (e.g., 'F.Cu', 'B.Cu')" + }, + "boundingBox": { + "type": "object", + "description": "Filter by bounding box region", + "properties": { + "x1": {"type": "number", "description": "Left X coordinate"}, + "y1": {"type": "number", "description": "Top Y coordinate"}, + "x2": {"type": "number", "description": "Right X coordinate"}, + "y2": {"type": "number", "description": "Bottom Y coordinate"}, + "unit": {"type": "string", "enum": ["mm", "inch"], "default": "mm"} + } + }, + "includeVias": { + "type": "boolean", + "description": "Include vias in the result", + "default": False + } + } + } + }, { "name": "get_nets_list", "title": "List All Nets", @@ -1164,6 +1274,132 @@ SCHEMATIC_TOOLS = [ "required": ["points"] } }, + { + "name": "add_schematic_connection", + "title": "Add Junction/Connection Point", + "description": "Adds a junction (connection point) at the specified location on the schematic where wires cross and should connect.", + "inputSchema": { + "type": "object", + "properties": { + "schematicPath": { + "type": "string", + "description": "Path to schematic file" + }, + "x": { + "type": "number", + "description": "X coordinate on schematic" + }, + "y": { + "type": "number", + "description": "Y coordinate on schematic" + } + }, + "required": ["schematicPath", "x", "y"] + } + }, + { + "name": "add_schematic_net_label", + "title": "Add Net Label", + "description": "Adds a net label to assign a name to a wire/net on the schematic.", + "inputSchema": { + "type": "object", + "properties": { + "schematicPath": { + "type": "string", + "description": "Path to schematic file" + }, + "netName": { + "type": "string", + "description": "Name of the net (e.g., VCC, GND, SDA)" + }, + "x": { + "type": "number", + "description": "X coordinate on schematic" + }, + "y": { + "type": "number", + "description": "Y coordinate on schematic" + }, + "rotation": { + "type": "number", + "description": "Rotation angle in degrees (0, 90, 180, 270)", + "default": 0 + } + }, + "required": ["schematicPath", "netName", "x", "y"] + } + }, + { + "name": "connect_to_net", + "title": "Connect Pin to Net", + "description": "Intelligently connects a component pin to a named net, automatically routing wires as needed.", + "inputSchema": { + "type": "object", + "properties": { + "schematicPath": { + "type": "string", + "description": "Path to schematic file" + }, + "reference": { + "type": "string", + "description": "Component reference designator (e.g., R1, U3)" + }, + "pinNumber": { + "type": "string", + "description": "Pin number or name on the component" + }, + "netName": { + "type": "string", + "description": "Name of the net to connect to" + } + }, + "required": ["schematicPath", "reference", "pinNumber", "netName"] + } + }, + { + "name": "get_net_connections", + "title": "Get Net Connections", + "description": "Returns all components and pins connected to a specified net.", + "inputSchema": { + "type": "object", + "properties": { + "schematicPath": { + "type": "string", + "description": "Path to schematic file" + }, + "netName": { + "type": "string", + "description": "Name of the net to query" + } + }, + "required": ["schematicPath", "netName"] + } + }, + { + "name": "generate_netlist", + "title": "Generate Netlist", + "description": "Generates a netlist from the schematic showing all components and their net connections.", + "inputSchema": { + "type": "object", + "properties": { + "schematicPath": { + "type": "string", + "description": "Path to schematic file" + }, + "outputPath": { + "type": "string", + "description": "Optional path to save netlist file" + }, + "format": { + "type": "string", + "enum": ["kicad", "json", "spice"], + "description": "Netlist output format", + "default": "json" + } + }, + "required": ["schematicPath"] + } + }, { "name": "list_schematic_libraries", "title": "List Symbol Libraries", diff --git a/python/utils/platform_helper.py b/python/utils/platform_helper.py index 525468d..758735b 100644 --- a/python/utils/platform_helper.py +++ b/python/utils/platform_helper.py @@ -91,14 +91,32 @@ class PlatformHelper: 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) + # macOS: Check multiple KiCAD application bundle locations + kicad_app_paths = [ + Path("/Applications/KiCad/KiCad.app"), + Path("/Applications/KiCAD/KiCad.app"), # Alternative capitalization + Path.home() / "Applications" / "KiCad" / "KiCad.app", # User Applications + ] + + # Check Python framework paths in each KiCAD installation + for kicad_app in kicad_app_paths: + if kicad_app.exists(): + for version in ["3.9", "3.10", "3.11", "3.12", "3.13"]: + path = kicad_app / "Contents" / "Frameworks" / "Python.framework" / "Versions" / version / "lib" / f"python{version}" / "site-packages" + if path.exists(): + paths.append(path) + + # Also check Homebrew Python site-packages (if pcbnew installed via pip) + homebrew_paths = [ + Path("/opt/homebrew/lib/python3.12/site-packages"), # Apple Silicon + Path("/opt/homebrew/lib/python3.11/site-packages"), + Path("/usr/local/lib/python3.12/site-packages"), # Intel Mac + Path("/usr/local/lib/python3.11/site-packages"), + ] + for hp in homebrew_paths: + pcbnew_path = hp / "pcbnew.py" + if pcbnew_path.exists(): + paths.append(hp) if not paths: logger.warning(f"No KiCAD Python paths found for {PlatformHelper.get_platform_name()}") @@ -142,6 +160,8 @@ class PlatformHelper: elif PlatformHelper.is_macos(): patterns = [ "/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/*.kicad_sym", + "/Applications/KiCAD/KiCad.app/Contents/SharedSupport/symbols/*.kicad_sym", + str(Path.home() / "Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/*.kicad_sym"), ] # Add user library paths for all platforms diff --git a/src/server.ts b/src/server.ts index 6fe9904..af1db1b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -75,13 +75,39 @@ function findPythonExecutable(scriptPath: string): string { return kicadPython; } } else if (isMac) { - // macOS: Try KiCAD's bundled Python (check multiple versions) - const kicadPythonVersions = ['3.9', '3.10', '3.11', '3.12']; - for (const version of kicadPythonVersions) { - const kicadPython = `/Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/${version}/bin/python3`; - if (existsSync(kicadPython)) { - logger.info(`Found KiCAD bundled Python at: ${kicadPython}`); - return kicadPython; + // macOS: Try KiCAD's bundled Python (check multiple versions and locations) + const kicadPythonVersions = ['3.9', '3.10', '3.11', '3.12', '3.13']; + + // Standard KiCAD installation paths + const kicadAppPaths = [ + '/Applications/KiCad/KiCad.app', + '/Applications/KiCAD/KiCad.app', // Alternative capitalization + `${process.env.HOME}/Applications/KiCad/KiCad.app`, // User Applications folder + ]; + + // Check all KiCAD app locations with all Python versions + for (const appPath of kicadAppPaths) { + for (const version of kicadPythonVersions) { + const kicadPython = `${appPath}/Contents/Frameworks/Python.framework/Versions/${version}/bin/python3`; + if (existsSync(kicadPython)) { + logger.info(`Found KiCAD bundled Python at: ${kicadPython}`); + return kicadPython; + } + } + } + + // Fallback to Homebrew Python (if pcbnew is installed via pip) + const homebrewPaths = [ + '/opt/homebrew/bin/python3', // Apple Silicon + '/usr/local/bin/python3', // Intel Mac + '/opt/homebrew/bin/python3.12', + '/opt/homebrew/bin/python3.11', + ]; + + for (const path of homebrewPaths) { + if (existsSync(path)) { + logger.info(`Found Homebrew Python at: ${path} (ensure pcbnew is importable)`); + return path; } } } else if (isLinux) {