diff --git a/python/commands/connection_schematic.py b/python/commands/connection_schematic.py index b4e030f..88b3d8e 100644 --- a/python/commands/connection_schematic.py +++ b/python/commands/connection_schematic.py @@ -1,65 +1,348 @@ from skip import Schematic -# Wire and Net classes might not be directly importable in the current version import os +import logging + +logger = logging.getLogger(__name__) class ConnectionManager: - """Manage connections between components""" + """Manage connections between components in schematics""" @staticmethod def add_wire(schematic: Schematic, start_point: list, end_point: list, properties: dict = None): - """Add a wire between two points""" + """ + Add a wire between two points + + Args: + schematic: Schematic object + start_point: [x, y] coordinates for wire start + end_point: [x, y] coordinates for wire end + properties: Optional wire properties (currently unused) + + Returns: + Wire object or None on error + """ 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}.") + # Check if wire collection exists + if not hasattr(schematic, 'wire'): + logger.error("Schematic does not have wire collection") + return None + + wire = schematic.wire.append( + start={'x': start_point[0], 'y': start_point[1]}, + end={'x': end_point[0], 'y': end_point[1]} + ) + logger.info(f"Added wire from {start_point} to {end_point}") return wire except Exception as e: - print(f"Error adding wire: {e}") + logger.error(f"Error adding wire: {e}") + return None + + @staticmethod + def get_pin_location(symbol, pin_name: str): + """ + Get the absolute location of a pin on a symbol + + Args: + symbol: Symbol object + pin_name: Name or number of the pin (e.g., "1", "GND", "VCC") + + Returns: + [x, y] coordinates or None if pin not found + """ + try: + if not hasattr(symbol, 'pin'): + logger.warning(f"Symbol {symbol.property.Reference.value} has no pins") + return None + + # Find the pin by name + target_pin = None + for pin in symbol.pin: + if pin.name == pin_name: + target_pin = pin + break + + if not target_pin: + logger.warning(f"Pin '{pin_name}' not found on {symbol.property.Reference.value}") + return None + + # Get pin location relative to symbol + pin_loc = target_pin.location + # Get symbol location + symbol_at = symbol.at.value + + # Calculate absolute position + # pin_loc is relative to symbol origin, need to add symbol position + abs_x = symbol_at[0] + pin_loc[0] + abs_y = symbol_at[1] + pin_loc[1] + + return [abs_x, abs_y] + except Exception as e: + logger.error(f"Error getting pin location: {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. + """ + Add a wire connection between two component pins - # 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. + Args: + schematic: Schematic object + source_ref: Reference designator of source component (e.g., "R1") + source_pin: Pin name/number on source component + target_ref: Reference designator of target component (e.g., "C1") + target_pin: Pin name/number on target component - 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 + Returns: + True if connection was successful, False otherwise + """ + try: + # Find source and target symbols + source_symbol = None + target_symbol = None + + if not hasattr(schematic, 'symbol'): + logger.error("Schematic has no symbols") + return False + + for symbol in schematic.symbol: + ref = symbol.property.Reference.value + if ref == source_ref: + source_symbol = symbol + if ref == target_ref: + target_symbol = symbol + + if not source_symbol: + logger.error(f"Source component '{source_ref}' not found") + return False + + if not target_symbol: + logger.error(f"Target component '{target_ref}' not found") + return False + + # Get pin locations + source_loc = ConnectionManager.get_pin_location(source_symbol, source_pin) + target_loc = ConnectionManager.get_pin_location(target_symbol, target_pin) + + if not source_loc or not target_loc: + logger.error("Could not determine pin locations") + return False + + # Add wire between pins + wire = ConnectionManager.add_wire(schematic, source_loc, target_loc) + + if wire: + logger.info(f"Connected {source_ref}/{source_pin} to {target_ref}/{target_pin}") + return True + else: + return False + + except Exception as e: + logger.error(f"Error adding connection: {e}") + return False @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 + def add_net_label(schematic: Schematic, net_name: str, position: list): + """ + Add a net label to the schematic + + Args: + schematic: Schematic object + net_name: Name of the net (e.g., "VCC", "GND", "SIGNAL_1") + position: [x, y] coordinates for the label + + Returns: + Label object or None on error + """ + try: + if not hasattr(schematic, 'label'): + logger.error("Schematic does not have label collection") + return None + + label = schematic.label.append( + text=net_name, + at={'x': position[0], 'y': position[1]} + ) + logger.info(f"Added net label '{net_name}' at {position}") + return label + except Exception as e: + logger.error(f"Error adding net label: {e}") + return None + + @staticmethod + def connect_to_net(schematic: Schematic, component_ref: str, pin_name: str, net_name: str): + """ + Connect a component pin to a named net using a label + + Args: + schematic: Schematic object + component_ref: Reference designator (e.g., "U1") + pin_name: Pin name/number + net_name: Name of the net to connect to + + Returns: + True if successful, False otherwise + """ + try: + # Find the component + symbol = None + if hasattr(schematic, 'symbol'): + for s in schematic.symbol: + if s.property.Reference.value == component_ref: + symbol = s + break + + if not symbol: + logger.error(f"Component '{component_ref}' not found") + return False + + # Get pin location + pin_loc = ConnectionManager.get_pin_location(symbol, pin_name) + if not pin_loc: + return False + + # Add a small wire stub from the pin (so label has something to attach to) + stub_end = [pin_loc[0] + 2.54, pin_loc[1]] # 2.54mm = 0.1 inch grid + wire = ConnectionManager.add_wire(schematic, pin_loc, stub_end) + + if not wire: + return False + + # Add label at the end of the stub + label = ConnectionManager.add_net_label(schematic, net_name, stub_end) + + if label: + logger.info(f"Connected {component_ref}/{pin_name} to net '{net_name}'") + return True + else: + return False + + except Exception as e: + logger.error(f"Error connecting to net: {e}") + return False @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 + """ + Get all connections for a named net + + Args: + schematic: Schematic object + net_name: Name of the net to query + + Returns: + List of connections: [{"component": ref, "pin": pin_name}, ...] + """ + try: + connections = [] + + if not hasattr(schematic, 'label'): + logger.warning("Schematic has no labels") + return connections + + # Find all labels with this net name + net_labels = [] + for label in schematic.label: + if hasattr(label, 'value') and label.value == net_name: + net_labels.append(label) + + if not net_labels: + logger.info(f"No labels found for net '{net_name}'") + return connections + + # For each label, find connected symbols + for label in net_labels: + # Find wires connected to this label position + label_pos = label.at.value if hasattr(label, 'at') else None + if not label_pos: + continue + + # Search for symbols near this label + if hasattr(schematic, 'symbol'): + for symbol in schematic.symbol: + # Check if symbol has wires attached + if hasattr(symbol, 'attached_labels'): + for attached_label in symbol.attached_labels: + if attached_label.value == net_name: + # Find which pin is connected + if hasattr(symbol, 'pin'): + for pin in symbol.pin: + pin_loc = ConnectionManager.get_pin_location(symbol, pin.name) + if pin_loc: + # Check if pin is connected to any wire attached to this label + connections.append({ + "component": symbol.property.Reference.value, + "pin": pin.name + }) + + logger.info(f"Found {len(connections)} connections for net '{net_name}'") + return connections + + except Exception as e: + logger.error(f"Error getting net connections: {e}") + return [] + + @staticmethod + def generate_netlist(schematic: Schematic): + """ + Generate a netlist from the schematic + + Returns: + Dictionary with net information: + { + "nets": [ + { + "name": "VCC", + "connections": [ + {"component": "R1", "pin": "1"}, + {"component": "C1", "pin": "1"} + ] + }, + ... + ], + "components": [ + {"reference": "R1", "value": "10k", "footprint": "..."}, + ... + ] + } + """ + try: + netlist = { + "nets": [], + "components": [] + } + + # Gather all components + if hasattr(schematic, 'symbol'): + for symbol in schematic.symbol: + component_info = { + "reference": symbol.property.Reference.value, + "value": symbol.property.Value.value if hasattr(symbol.property, 'Value') else "", + "footprint": symbol.property.Footprint.value if hasattr(symbol.property, 'Footprint') else "" + } + netlist["components"].append(component_info) + + # Gather all nets from labels + if hasattr(schematic, 'label'): + net_names = set() + for label in schematic.label: + if hasattr(label, 'value'): + net_names.add(label.value) + + # For each net, get connections + for net_name in net_names: + connections = ConnectionManager.get_net_connections(schematic, net_name) + if connections: + netlist["nets"].append({ + "name": net_name, + "connections": connections + }) + + logger.info(f"Generated netlist with {len(netlist['nets'])} nets and {len(netlist['components'])} components") + return netlist + + except Exception as e: + logger.error(f"Error generating netlist: {e}") + return {"nets": [], "components": []} if __name__ == '__main__': # Example Usage (for testing) diff --git a/python/kicad_interface.py b/python/kicad_interface.py index 2a6bf97..8df089a 100644 --- a/python/kicad_interface.py +++ b/python/kicad_interface.py @@ -272,6 +272,11 @@ class KiCADInterface: "load_schematic": self._handle_load_schematic, "add_schematic_component": self._handle_add_schematic_component, "add_schematic_wire": self._handle_add_schematic_wire, + "add_schematic_connection": self._handle_add_schematic_connection, + "add_schematic_net_label": self._handle_add_schematic_net_label, + "connect_to_net": self._handle_connect_to_net, + "get_net_connections": self._handle_get_net_connections, + "generate_netlist": self._handle_generate_netlist, "list_schematic_libraries": self._handle_list_schematic_libraries, "export_schematic_pdf": self._handle_export_schematic_pdf, @@ -471,6 +476,126 @@ class KiCADInterface: logger.error(f"Error exporting schematic to PDF: {str(e)}") return {"success": False, "message": str(e)} + def _handle_add_schematic_connection(self, params): + """Add a pin-to-pin connection in schematic""" + logger.info("Adding pin-to-pin connection in schematic") + try: + schematic_path = params.get("schematicPath") + source_ref = params.get("sourceRef") + source_pin = params.get("sourcePin") + target_ref = params.get("targetRef") + target_pin = params.get("targetPin") + + if not all([schematic_path, source_ref, source_pin, target_ref, target_pin]): + return {"success": False, "message": "Missing required parameters"} + + schematic = SchematicManager.load_schematic(schematic_path) + if not schematic: + return {"success": False, "message": "Failed to load schematic"} + + success = ConnectionManager.add_connection(schematic, source_ref, source_pin, target_ref, target_pin) + + if success: + SchematicManager.save_schematic(schematic, schematic_path) + return {"success": True} + else: + return {"success": False, "message": "Failed to add connection"} + except Exception as e: + logger.error(f"Error adding schematic connection: {str(e)}") + return {"success": False, "message": str(e)} + + def _handle_add_schematic_net_label(self, params): + """Add a net label to schematic""" + logger.info("Adding net label to schematic") + try: + schematic_path = params.get("schematicPath") + net_name = params.get("netName") + position = params.get("position") + + if not all([schematic_path, net_name, position]): + return {"success": False, "message": "Missing required parameters"} + + schematic = SchematicManager.load_schematic(schematic_path) + if not schematic: + return {"success": False, "message": "Failed to load schematic"} + + label = ConnectionManager.add_net_label(schematic, net_name, position) + + if label: + SchematicManager.save_schematic(schematic, schematic_path) + return {"success": True} + else: + return {"success": False, "message": "Failed to add net label"} + except Exception as e: + logger.error(f"Error adding net label: {str(e)}") + return {"success": False, "message": str(e)} + + def _handle_connect_to_net(self, params): + """Connect a component pin to a named net""" + logger.info("Connecting component pin to net") + try: + schematic_path = params.get("schematicPath") + component_ref = params.get("componentRef") + pin_name = params.get("pinName") + net_name = params.get("netName") + + if not all([schematic_path, component_ref, pin_name, net_name]): + return {"success": False, "message": "Missing required parameters"} + + schematic = SchematicManager.load_schematic(schematic_path) + if not schematic: + return {"success": False, "message": "Failed to load schematic"} + + success = ConnectionManager.connect_to_net(schematic, component_ref, pin_name, net_name) + + if success: + SchematicManager.save_schematic(schematic, schematic_path) + return {"success": True} + else: + return {"success": False, "message": "Failed to connect to net"} + except Exception as e: + logger.error(f"Error connecting to net: {str(e)}") + return {"success": False, "message": str(e)} + + def _handle_get_net_connections(self, params): + """Get all connections for a named net""" + logger.info("Getting net connections") + try: + schematic_path = params.get("schematicPath") + net_name = params.get("netName") + + if not all([schematic_path, net_name]): + return {"success": False, "message": "Missing required parameters"} + + schematic = SchematicManager.load_schematic(schematic_path) + if not schematic: + return {"success": False, "message": "Failed to load schematic"} + + connections = ConnectionManager.get_net_connections(schematic, net_name) + return {"success": True, "connections": connections} + except Exception as e: + logger.error(f"Error getting net connections: {str(e)}") + return {"success": False, "message": str(e)} + + def _handle_generate_netlist(self, params): + """Generate netlist from schematic""" + logger.info("Generating netlist from schematic") + try: + schematic_path = params.get("schematicPath") + + if not schematic_path: + return {"success": False, "message": "Schematic path is required"} + + schematic = SchematicManager.load_schematic(schematic_path) + if not schematic: + return {"success": False, "message": "Failed to load schematic"} + + netlist = ConnectionManager.generate_netlist(schematic) + return {"success": True, "netlist": netlist} + except Exception as e: + logger.error(f"Error generating netlist: {str(e)}") + return {"success": False, "message": str(e)} + def _handle_check_kicad_ui(self, params): """Check if KiCAD UI is running""" logger.info("Checking if KiCAD UI is running") diff --git a/src/tools/schematic.ts b/src/tools/schematic.ts index b6b9c03..5afce94 100644 --- a/src/tools/schematic.ts +++ b/src/tools/schematic.ts @@ -73,4 +73,168 @@ export function registerSchematicTools(server: McpServer, callKicadScript: Funct }; } ); + + // Add pin-to-pin connection + server.tool( + "add_schematic_connection", + "Connect two component pins with a wire", + { + schematicPath: z.string().describe("Path to the schematic file"), + sourceRef: z.string().describe("Source component reference (e.g., R1)"), + sourcePin: z.string().describe("Source pin name/number (e.g., 1, 2, GND)"), + targetRef: z.string().describe("Target component reference (e.g., C1)"), + targetPin: z.string().describe("Target pin name/number (e.g., 1, 2, VCC)") + }, + async (args: { schematicPath: string; sourceRef: string; sourcePin: string; targetRef: string; targetPin: string }) => { + const result = await callKicadScript("add_schematic_connection", args); + if (result.success) { + return { + content: [{ + type: "text", + text: `Successfully connected ${args.sourceRef}/${args.sourcePin} to ${args.targetRef}/${args.targetPin}` + }] + }; + } else { + return { + content: [{ + type: "text", + text: `Failed to add connection: ${result.message || 'Unknown error'}` + }] + }; + } + } + ); + + // Add net label + server.tool( + "add_schematic_net_label", + "Add a net label to the schematic", + { + schematicPath: z.string().describe("Path to the schematic file"), + netName: z.string().describe("Name of the net (e.g., VCC, GND, SIGNAL_1)"), + position: z.array(z.number()).length(2).describe("Position [x, y] for the label") + }, + async (args: { schematicPath: string; netName: string; position: number[] }) => { + const result = await callKicadScript("add_schematic_net_label", args); + if (result.success) { + return { + content: [{ + type: "text", + text: `Successfully added net label '${args.netName}' at position [${args.position}]` + }] + }; + } else { + return { + content: [{ + type: "text", + text: `Failed to add net label: ${result.message || 'Unknown error'}` + }] + }; + } + } + ); + + // Connect pin to net + server.tool( + "connect_to_net", + "Connect a component pin to a named net", + { + schematicPath: z.string().describe("Path to the schematic file"), + componentRef: z.string().describe("Component reference (e.g., U1, R1)"), + pinName: z.string().describe("Pin name/number to connect"), + netName: z.string().describe("Name of the net to connect to") + }, + async (args: { schematicPath: string; componentRef: string; pinName: string; netName: string }) => { + const result = await callKicadScript("connect_to_net", args); + if (result.success) { + return { + content: [{ + type: "text", + text: `Successfully connected ${args.componentRef}/${args.pinName} to net '${args.netName}'` + }] + }; + } else { + return { + content: [{ + type: "text", + text: `Failed to connect to net: ${result.message || 'Unknown error'}` + }] + }; + } + } + ); + + // Get net connections + server.tool( + "get_net_connections", + "Get all connections for a named net", + { + schematicPath: z.string().describe("Path to the schematic file"), + netName: z.string().describe("Name of the net to query") + }, + async (args: { schematicPath: string; netName: string }) => { + const result = await callKicadScript("get_net_connections", args); + if (result.success && result.connections) { + const connectionList = result.connections.map((conn: any) => + ` - ${conn.component}/${conn.pin}` + ).join('\n'); + return { + content: [{ + type: "text", + text: `Net '${args.netName}' connections:\n${connectionList}` + }] + }; + } else { + return { + content: [{ + type: "text", + text: `Failed to get net connections: ${result.message || 'Unknown error'}` + }] + }; + } + } + ); + + // Generate netlist + server.tool( + "generate_netlist", + "Generate a netlist from the schematic", + { + schematicPath: z.string().describe("Path to the schematic file") + }, + async (args: { schematicPath: string }) => { + const result = await callKicadScript("generate_netlist", args); + if (result.success && result.netlist) { + const netlist = result.netlist; + const output = [ + `=== Netlist for ${args.schematicPath} ===`, + `\nComponents (${netlist.components.length}):`, + ...netlist.components.map((comp: any) => + ` ${comp.reference}: ${comp.value} (${comp.footprint || 'No footprint'})` + ), + `\nNets (${netlist.nets.length}):`, + ...netlist.nets.map((net: any) => { + const connections = net.connections.map((conn: any) => + `${conn.component}/${conn.pin}` + ).join(', '); + return ` ${net.name}: ${connections}`; + }) + ].join('\n'); + + return { + content: [{ + type: "text", + text: output + }] + }; + } else { + return { + content: [{ + type: "text", + text: `Failed to generate netlist: ${result.message || 'Unknown error'}` + }] + }; + } + } + ); }