456 lines
18 KiB
Python
456 lines
18 KiB
Python
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": []}
|