Files
kicad-mcp-server/python/commands/connection_schematic.py

456 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import logging
import os
from pathlib import Path
from typing import Any, Dict, List, Optional
from skip import Schematic
logger = logging.getLogger(__name__)
# Import new wire and pin managers
try:
from commands.pin_locator import PinLocator
from commands.wire_manager import WireManager
WIRE_MANAGER_AVAILABLE = True
except ImportError:
logger.warning("WireManager/PinLocator not available")
WIRE_MANAGER_AVAILABLE = False
class ConnectionManager:
"""Manage connections between components in schematics"""
# Initialize pin locator (class variable, shared across instances)
_pin_locator = None
@classmethod
def get_pin_locator(cls) -> Any:
"""Get or create pin locator instance"""
if cls._pin_locator is None and WIRE_MANAGER_AVAILABLE:
cls._pin_locator = PinLocator()
return cls._pin_locator
@staticmethod
def add_net_label(schematic: Schematic, net_name: str, position: list) -> Any:
"""
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_path: Path, component_ref: str, pin_name: str, net_name: str
) -> Dict[str, Any]:
"""
Connect a component pin to a named net using a wire stub and label.
Args:
schematic_path: Path to .kicad_sch file
component_ref: Reference designator (e.g., "U1", "U1_")
pin_name: Pin name/number
net_name: Name of the net to connect to (e.g., "VCC", "GND", "SIGNAL_1")
Returns:
Dict with keys:
success bool
pin_location [x, y] exact pin endpoint used (present on success)
label_location [x, y] where the net label was placed (present on success)
wire_stub [[x1,y1],[x2,y2]] the wire segment added (present on success)
message human-readable status
"""
try:
if not WIRE_MANAGER_AVAILABLE:
logger.error("WireManager/PinLocator not available")
return {"success": False, "message": "WireManager/PinLocator not available"}
locator = ConnectionManager.get_pin_locator()
if not locator:
logger.error("Pin locator unavailable")
return {"success": False, "message": "Pin locator unavailable"}
# Get pin location using PinLocator
pin_loc = locator.get_pin_location(schematic_path, component_ref, pin_name)
if not pin_loc:
msg = f"Could not locate pin {component_ref}/{pin_name}"
logger.error(msg)
return {"success": False, "message": msg}
# Add a small wire stub from the pin (2.54mm = 0.1 inch, standard grid spacing)
# Stub direction follows the pin's outward angle from the PinLocator
try:
pin_angle_deg = locator.get_pin_angle(schematic_path, component_ref, pin_name) or 0
except Exception as e:
logger.warning(
f"Could not get pin angle for {component_ref}/{pin_name}, defaulting to 0: {e}"
)
pin_angle_deg = 0
import math as _math
angle_rad = _math.radians(pin_angle_deg)
stub_end = [
round(pin_loc[0] + 2.54 * _math.cos(angle_rad), 4),
round(pin_loc[1] - 2.54 * _math.sin(angle_rad), 4),
]
# Create wire stub using WireManager
wire_success = WireManager.add_wire(schematic_path, pin_loc, stub_end)
if not wire_success:
msg = "Failed to create wire stub for net connection"
logger.error(msg)
return {"success": False, "message": msg}
# Add label at the end of the stub using WireManager
label_success = WireManager.add_label(
schematic_path, net_name, stub_end, label_type="label"
)
if not label_success:
msg = f"Failed to add net label '{net_name}'"
logger.error(msg)
return {"success": False, "message": msg}
logger.info(f"Connected {component_ref}/{pin_name} to net '{net_name}'")
return {
"success": True,
"message": f"Connected {component_ref}/{pin_name} to net '{net_name}'",
"pin_location": pin_loc,
"label_location": stub_end,
"wire_stub": [pin_loc, stub_end],
}
except Exception as e:
logger.error(f"Error connecting to net: {e}")
import traceback
logger.error(traceback.format_exc())
return {"success": False, "message": str(e)}
@staticmethod
def connect_passthrough(
schematic_path: Path,
source_ref: str,
target_ref: str,
net_prefix: str = "PIN",
pin_offset: int = 0,
) -> Dict[str, List[str]]:
"""
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}"
)
res_src = ConnectionManager.connect_to_net(
schematic_path, source_ref, pin_num, net_name
)
if not res_src.get("success"):
failed.append(f"{source_ref}/{pin_num}")
continue
if pin_num in tgt_pins:
res_tgt = ConnectionManager.connect_to_net(
schematic_path, target_ref, pin_num, net_name
)
if not res_tgt.get("success"):
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
) -> List[Dict]:
"""
Get all connections for a named net using wire graph analysis
Args:
schematic: Schematic object
net_name: Name of the net to query
schematic_path: Optional path to schematic file (enables accurate pin matching)
Returns:
List of connections: [{"component": ref, "pin": pin_name}, ...]
"""
try:
from commands.pin_locator import PinLocator
connections = []
tolerance = 0.5 # 0.5mm tolerance for point coincidence (grid spacing consideration)
def points_coincide(p1: Any, p2: Any) -> bool:
"""Check if two points are the same (within tolerance)"""
if not p1 or not p2:
return False
dx = abs(p1[0] - p2[0])
dy = abs(p1[1] - p2[1])
return dx < tolerance and dy < tolerance
# 1. Find all labels with this net name
if not hasattr(schematic, "label"):
logger.warning("Schematic has no labels")
return connections
net_label_positions = []
for label in schematic.label:
if hasattr(label, "value") and label.value == net_name:
if hasattr(label, "at") and hasattr(label.at, "value"):
pos = label.at.value
net_label_positions.append([float(pos[0]), float(pos[1])])
if not net_label_positions:
logger.info(f"No labels found for net '{net_name}'")
return connections
logger.debug(f"Found {len(net_label_positions)} labels for net '{net_name}'")
# 2. Find all wires connected to these label positions
if not hasattr(schematic, "wire"):
logger.warning("Schematic has no wires")
return connections
connected_wire_points = set()
for wire in schematic.wire:
if hasattr(wire, "pts") and hasattr(wire.pts, "xy"):
# Get all points in this wire (polyline)
wire_points = []
for point in wire.pts.xy:
if hasattr(point, "value"):
wire_points.append([float(point.value[0]), float(point.value[1])])
# Check if any wire point touches a label
wire_connected = False
for wire_pt in wire_points:
for label_pt in net_label_positions:
if points_coincide(wire_pt, label_pt):
wire_connected = True
break
if wire_connected:
break
# If this wire is connected to the net, add all its points
if wire_connected:
for pt in wire_points:
connected_wire_points.add((pt[0], pt[1]))
if not connected_wire_points:
logger.debug(f"No wires connected to net '{net_name}' labels")
return connections
logger.debug(
f"Found {len(connected_wire_points)} wire connection points for net '{net_name}'"
)
# 3. Find component pins at wire endpoints
if not hasattr(schematic, "symbol"):
logger.warning("Schematic has no symbols")
return connections
# Create pin locator for accurate pin matching (if schematic_path available)
locator = None
if schematic_path and WIRE_MANAGER_AVAILABLE:
locator = PinLocator()
for symbol in schematic.symbol:
# Skip template symbols
if not hasattr(symbol.property, "Reference"):
continue
ref = symbol.property.Reference.value
if ref.startswith("_TEMPLATE"):
continue
# Get lib_id for pin location lookup
lib_id = symbol.lib_id.value if hasattr(symbol, "lib_id") else None
if not lib_id:
continue
# If we have PinLocator and schematic_path, do accurate pin matching
if locator and schematic_path:
try:
# Get all pins for this symbol
pins = locator.get_symbol_pins(schematic_path, lib_id)
if not pins:
continue
# Check each pin
for pin_num, pin_data in pins.items():
# Get pin location
pin_loc = locator.get_pin_location(schematic_path, ref, pin_num)
if not pin_loc:
continue
# Check if pin coincides with any wire point
for wire_pt_tup in connected_wire_points:
if points_coincide(pin_loc, list(wire_pt_tup)):
connections.append({"component": ref, "pin": pin_num})
break # Pin found, no need to check more wire points
except Exception as e:
logger.warning(f"Error matching pins for {ref}: {e}")
# Fall back to proximity matching
pass
# Fallback: proximity-based matching if no PinLocator
if not locator or not schematic_path:
symbol_pos = symbol.at.value if hasattr(symbol, "at") else None
if not symbol_pos:
continue
symbol_x = float(symbol_pos[0])
symbol_y = float(symbol_pos[1])
# Check if symbol is near any wire point (within 10mm)
for wire_pt_tup in connected_wire_points:
dist = (
(symbol_x - wire_pt_tup[0]) ** 2 + (symbol_y - wire_pt_tup[1]) ** 2
) ** 0.5
if dist < 10.0: # 10mm proximity threshold
connections.append({"component": ref, "pin": "unknown"})
break # Only add once per component
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}")
import traceback
logger.error(traceback.format_exc())
return []
@staticmethod
def generate_netlist(
schematic: Schematic, schematic_path: Optional[Path] = None
) -> Dict[str, Any]:
"""
Generate a netlist from the schematic
Args:
schematic: Schematic object
schematic_path: Optional path to schematic file (enables accurate pin matching
via PinLocator; without it, only one connection per component is found)
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, schematic_path
)
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": []}