From 11d7e49e74db3a636ae59e5864e75d026112ed12 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 6 Mar 2026 12:25:25 +0100 Subject: [PATCH] feat: add connect_passthrough tool for FFC/ribbon cable pin-to-pin wiring --- python/commands/connection_schematic.py | 71 +++++++++++++++++++++++++ python/kicad_interface.py | 34 ++++++++++++ python/schemas/tool_schemas.py | 33 +++++++++++- src/tools/schematic.ts | 28 ++++++++++ 4 files changed, 165 insertions(+), 1 deletion(-) diff --git a/python/commands/connection_schematic.py b/python/commands/connection_schematic.py index d18e298..45542d1 100644 --- a/python/commands/connection_schematic.py +++ b/python/commands/connection_schematic.py @@ -291,6 +291,77 @@ class ConnectionManager: logger.error(traceback.format_exc()) return False + @staticmethod + def connect_passthrough( + schematic_path: Path, + source_ref: str, + target_ref: str, + net_prefix: str = "PIN", + pin_offset: int = 0, + ): + """ + Connect all pins of source_ref to matching pins of target_ref via shared net labels. + Useful for passthrough adapters: J1 pin N <-> J2 pin N on net {net_prefix}_{N}. + + Args: + schematic_path: Path to .kicad_sch file + source_ref: Reference of the first connector (e.g., "J1") + target_ref: Reference of the second connector (e.g., "J2") + net_prefix: Prefix for generated net names (default: "PIN" -> PIN_1, PIN_2, ...) + pin_offset: Add this value to the pin number when building the net name (default 0) + + Returns: + dict with 'connected' list and 'failed' list + """ + if not WIRE_MANAGER_AVAILABLE: + logger.error("WireManager/PinLocator not available") + return {"connected": [], "failed": ["WireManager unavailable"]} + + locator = ConnectionManager.get_pin_locator() + if not locator: + return {"connected": [], "failed": ["PinLocator unavailable"]} + + # Get all pins of source and target + src_pins = locator.get_all_symbol_pins(schematic_path, source_ref) or {} + tgt_pins = locator.get_all_symbol_pins(schematic_path, target_ref) or {} + + if not src_pins: + return {"connected": [], "failed": [f"No pins found on {source_ref}"]} + if not tgt_pins: + return {"connected": [], "failed": [f"No pins found on {target_ref}"]} + + connected = [] + failed = [] + + for pin_num in sorted(src_pins.keys(), key=lambda x: int(x) if x.isdigit() else 0): + try: + net_name = f"{net_prefix}_{int(pin_num) + pin_offset}" if pin_num.isdigit() else f"{net_prefix}_{pin_num}" + + ok_src = ConnectionManager.connect_to_net( + schematic_path, source_ref, pin_num, net_name + ) + if not ok_src: + failed.append(f"{source_ref}/{pin_num}") + continue + + if pin_num in tgt_pins: + ok_tgt = ConnectionManager.connect_to_net( + schematic_path, target_ref, pin_num, net_name + ) + if not ok_tgt: + failed.append(f"{target_ref}/{pin_num}") + continue + else: + failed.append(f"{target_ref}/{pin_num} (pin not found)") + continue + + connected.append(f"{source_ref}/{pin_num} <-> {target_ref}/{pin_num} [{net_name}]") + except Exception as e: + failed.append(f"{source_ref}/{pin_num}: {e}") + + logger.info(f"connect_passthrough: {len(connected)} connected, {len(failed)} failed") + return {"connected": connected, "failed": failed} + @staticmethod def get_net_connections( schematic: Schematic, net_name: str, schematic_path: Optional[Path] = None diff --git a/python/kicad_interface.py b/python/kicad_interface.py index 1f58207..0947d8f 100644 --- a/python/kicad_interface.py +++ b/python/kicad_interface.py @@ -375,6 +375,7 @@ class KiCADInterface: "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, + "connect_passthrough": self._handle_connect_passthrough, "get_schematic_pin_locations": self._handle_get_schematic_pin_locations, "get_net_connections": self._handle_get_net_connections, "generate_netlist": self._handle_generate_netlist, @@ -1272,6 +1273,39 @@ class KiCADInterface: "errorDetails": traceback.format_exc(), } + def _handle_connect_passthrough(self, params): + """Connect all pins of source connector to matching pins of target connector""" + logger.info("Connecting passthrough between two connectors") + try: + from pathlib import Path + + schematic_path = params.get("schematicPath") + source_ref = params.get("sourceRef") + target_ref = params.get("targetRef") + net_prefix = params.get("netPrefix", "PIN") + pin_offset = int(params.get("pinOffset", 0)) + + if not all([schematic_path, source_ref, target_ref]): + return {"success": False, "message": "Missing required parameters: schematicPath, sourceRef, targetRef"} + + result = ConnectionManager.connect_passthrough( + Path(schematic_path), source_ref, target_ref, net_prefix, pin_offset + ) + + n_ok = len(result["connected"]) + n_fail = len(result["failed"]) + return { + "success": n_fail == 0, + "message": f"Passthrough complete: {n_ok} connected, {n_fail} failed", + "connected": result["connected"], + "failed": result["failed"], + } + except Exception as e: + logger.error(f"Error in connect_passthrough: {str(e)}") + import traceback + logger.error(traceback.format_exc()) + return {"success": False, "message": str(e)} + def _handle_get_schematic_pin_locations(self, params): """Return exact pin endpoint coordinates for a schematic component""" logger.info("Getting schematic pin locations") diff --git a/python/schemas/tool_schemas.py b/python/schemas/tool_schemas.py index 223b8fb..660e31b 100644 --- a/python/schemas/tool_schemas.py +++ b/python/schemas/tool_schemas.py @@ -1483,7 +1483,38 @@ SCHEMATIC_TOOLS = [ } }, { - "name": "generate_netlist", + "name": "connect_passthrough", + "title": "Connect Passthrough (Pin-to-Pin)", + "description": "Connects all pins of a source connector to the matching pins of a target connector using shared net labels. Ideal for passthrough adapters where J1 pin N connects directly to J2 pin N. Each pair gets a net label '{netPrefix}_{pinNumber}'. Use this instead of calling connect_to_net 15 times for FFC/ribbon cable passthroughs.", + "inputSchema": { + "type": "object", + "properties": { + "schematicPath": { + "type": "string", + "description": "Path to the schematic file" + }, + "sourceRef": { + "type": "string", + "description": "Reference of the source connector (e.g., J1)" + }, + "targetRef": { + "type": "string", + "description": "Reference of the target connector (e.g., J2)" + }, + "netPrefix": { + "type": "string", + "description": "Prefix for generated net names, e.g. 'CSI' produces CSI_1, CSI_2, ... (default: PIN)" + }, + "pinOffset": { + "type": "integer", + "description": "Add this value to the pin number when building net names (default: 0)" + } + }, + "required": ["schematicPath", "sourceRef", "targetRef"] + } + }, + { + "name": "generate_netlist",, "title": "Generate Netlist", "description": "Generates a netlist from the schematic showing all components and their net connections.", "inputSchema": { diff --git a/src/tools/schematic.ts b/src/tools/schematic.ts index 9407b93..65a6822 100644 --- a/src/tools/schematic.ts +++ b/src/tools/schematic.ts @@ -417,6 +417,34 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp }, ); + // Connect all pins of source connector to matching pins of target connector (passthrough) + server.tool( + "connect_passthrough", + "Connects all pins of a source connector (e.g. J1) to matching pins of a target connector (e.g. J2) via shared net labels — pin N gets net '{netPrefix}_{N}'. Use this for FFC/ribbon cable passthrough adapters instead of calling connect_to_net for every pin.", + { + schematicPath: z.string().describe("Path to the schematic file"), + sourceRef: z.string().describe("Source connector reference (e.g. J1)"), + targetRef: z.string().describe("Target connector reference (e.g. J2)"), + netPrefix: z.string().optional().describe("Net name prefix, e.g. 'CSI' → CSI_1, CSI_2 (default: PIN)"), + pinOffset: z.number().optional().describe("Add to pin number when building net name (default: 0)"), + }, + async (args: { schematicPath: string; sourceRef: string; targetRef: string; netPrefix?: string; pinOffset?: number }) => { + const result = await callKicadScript("connect_passthrough", args); + if (result.success !== false || (result.connected && result.connected.length > 0)) { + const lines: string[] = []; + if (result.connected?.length) lines.push(`Connected (${result.connected.length}): ${result.connected.slice(0, 5).join(", ")}${result.connected.length > 5 ? " ..." : ""}`); + if (result.failed?.length) lines.push(`Failed (${result.failed.length}): ${result.failed.join(", ")}`); + return { + content: [{ type: "text", text: result.message + "\n" + lines.join("\n") }], + }; + } else { + return { + content: [{ type: "text", text: `Passthrough failed: ${result.message || "Unknown error"}` }], + }; + } + }, + ); + // Generate netlist server.tool( "generate_netlist",