04db774a2b
Issue #32 - Unknown command errors: - Register get_board_extents in command_routes (was implemented but not registered) - Implement find_component command with pattern matching on reference/value/footprint - Add schemas for both commands Issue #30 - PCB routing replication (Phase 1): - Implement get_component_pads: returns all pads with positions, nets, shapes - Implement get_pad_position: returns specific pad coordinates and properties - Implement query_traces: query traces by net, layer, or bounding box - Add schemas for all new commands Issue #26 - Schematic workflow: - Add missing schemas for add_schematic_connection, add_schematic_net_label, connect_to_net, get_net_connections, and generate_netlist Issue #19 - macOS Python path detection: - Add Python 3.13 to version detection - Add alternative KiCAD installation paths (user Applications, capitalization variants) - Add Homebrew Python fallback paths for Apple Silicon and Intel Macs - Expand platform_helper.py with same improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
869 lines
32 KiB
Python
869 lines
32 KiB
Python
"""
|
|
Routing-related command implementations for KiCAD interface
|
|
"""
|
|
|
|
import os
|
|
import pcbnew
|
|
import logging
|
|
import math
|
|
from typing import Dict, Any, Optional, List, Tuple
|
|
|
|
logger = logging.getLogger('kicad_interface')
|
|
|
|
class RoutingCommands:
|
|
"""Handles routing-related KiCAD operations"""
|
|
|
|
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
|
"""Initialize with optional board instance"""
|
|
self.board = board
|
|
|
|
def add_net(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Add a new net to the PCB"""
|
|
try:
|
|
if not self.board:
|
|
return {
|
|
"success": False,
|
|
"message": "No board is loaded",
|
|
"errorDetails": "Load or create a board first"
|
|
}
|
|
|
|
name = params.get("name")
|
|
net_class = params.get("class")
|
|
|
|
if not name:
|
|
return {
|
|
"success": False,
|
|
"message": "Missing net name",
|
|
"errorDetails": "name parameter is required"
|
|
}
|
|
|
|
# Create new net
|
|
netinfo = self.board.GetNetInfo()
|
|
nets_map = netinfo.NetsByName()
|
|
if nets_map.has_key(name):
|
|
net = nets_map[name]
|
|
else:
|
|
net = pcbnew.NETINFO_ITEM(self.board, name)
|
|
self.board.Add(net)
|
|
|
|
# Set net class if provided
|
|
if net_class:
|
|
net_classes = self.board.GetNetClasses()
|
|
if net_classes.Find(net_class):
|
|
net.SetClass(net_classes.Find(net_class))
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Added net: {name}",
|
|
"net": {
|
|
"name": name,
|
|
"class": net_class if net_class else "Default",
|
|
"netcode": net.GetNetCode()
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error adding net: {str(e)}")
|
|
return {
|
|
"success": False,
|
|
"message": "Failed to add net",
|
|
"errorDetails": str(e)
|
|
}
|
|
|
|
def route_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Route a trace between two points or pads"""
|
|
try:
|
|
if not self.board:
|
|
return {
|
|
"success": False,
|
|
"message": "No board is loaded",
|
|
"errorDetails": "Load or create a board first"
|
|
}
|
|
|
|
start = params.get("start")
|
|
end = params.get("end")
|
|
layer = params.get("layer", "F.Cu")
|
|
width = params.get("width")
|
|
net = params.get("net")
|
|
via = params.get("via", False)
|
|
|
|
if not start or not end:
|
|
return {
|
|
"success": False,
|
|
"message": "Missing parameters",
|
|
"errorDetails": "start and end points are required"
|
|
}
|
|
|
|
# Get layer ID
|
|
layer_id = self.board.GetLayerID(layer)
|
|
if layer_id < 0:
|
|
return {
|
|
"success": False,
|
|
"message": "Invalid layer",
|
|
"errorDetails": f"Layer '{layer}' does not exist"
|
|
}
|
|
|
|
# Get start point
|
|
start_point = self._get_point(start)
|
|
end_point = self._get_point(end)
|
|
|
|
# Create track segment
|
|
track = pcbnew.PCB_TRACK(self.board)
|
|
track.SetStart(start_point)
|
|
track.SetEnd(end_point)
|
|
track.SetLayer(layer_id)
|
|
|
|
# Set width (default to board's current track width)
|
|
if width:
|
|
track.SetWidth(int(width * 1000000)) # Convert mm to nm
|
|
else:
|
|
track.SetWidth(self.board.GetDesignSettings().GetCurrentTrackWidth())
|
|
|
|
# Set net if provided
|
|
if net:
|
|
netinfo = self.board.GetNetInfo()
|
|
nets_map = netinfo.NetsByName()
|
|
if nets_map.has_key(net):
|
|
net_obj = nets_map[net]
|
|
track.SetNet(net_obj)
|
|
|
|
# Add track to board
|
|
self.board.Add(track)
|
|
|
|
# Add via if requested and net is specified
|
|
if via and net:
|
|
via_point = end_point
|
|
self.add_via({
|
|
"position": {
|
|
"x": via_point.x / 1000000,
|
|
"y": via_point.y / 1000000,
|
|
"unit": "mm"
|
|
},
|
|
"net": net
|
|
})
|
|
|
|
return {
|
|
"success": True,
|
|
"message": "Added trace",
|
|
"trace": {
|
|
"start": {
|
|
"x": start_point.x / 1000000,
|
|
"y": start_point.y / 1000000,
|
|
"unit": "mm"
|
|
},
|
|
"end": {
|
|
"x": end_point.x / 1000000,
|
|
"y": end_point.y / 1000000,
|
|
"unit": "mm"
|
|
},
|
|
"layer": layer,
|
|
"width": track.GetWidth() / 1000000,
|
|
"net": net
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error routing trace: {str(e)}")
|
|
return {
|
|
"success": False,
|
|
"message": "Failed to route trace",
|
|
"errorDetails": str(e)
|
|
}
|
|
|
|
def add_via(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Add a via at the specified location"""
|
|
try:
|
|
if not self.board:
|
|
return {
|
|
"success": False,
|
|
"message": "No board is loaded",
|
|
"errorDetails": "Load or create a board first"
|
|
}
|
|
|
|
position = params.get("position")
|
|
size = params.get("size")
|
|
drill = params.get("drill")
|
|
net = params.get("net")
|
|
from_layer = params.get("from_layer", "F.Cu")
|
|
to_layer = params.get("to_layer", "B.Cu")
|
|
|
|
if not position:
|
|
return {
|
|
"success": False,
|
|
"message": "Missing position",
|
|
"errorDetails": "position parameter is required"
|
|
}
|
|
|
|
# Create via
|
|
via = pcbnew.PCB_VIA(self.board)
|
|
|
|
# Set position
|
|
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
|
|
x_nm = int(position["x"] * scale)
|
|
y_nm = int(position["y"] * scale)
|
|
via.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
|
|
|
# Set size and drill (default to board's current via settings)
|
|
design_settings = self.board.GetDesignSettings()
|
|
via.SetWidth(int(size * 1000000) if size else design_settings.GetCurrentViaSize())
|
|
via.SetDrill(int(drill * 1000000) if drill else design_settings.GetCurrentViaDrill())
|
|
|
|
# Set layers
|
|
from_id = self.board.GetLayerID(from_layer)
|
|
to_id = self.board.GetLayerID(to_layer)
|
|
if from_id < 0 or to_id < 0:
|
|
return {
|
|
"success": False,
|
|
"message": "Invalid layer",
|
|
"errorDetails": "Specified layers do not exist"
|
|
}
|
|
via.SetLayerPair(from_id, to_id)
|
|
|
|
# Set net if provided
|
|
if net:
|
|
netinfo = self.board.GetNetInfo()
|
|
nets_map = netinfo.NetsByName()
|
|
if nets_map.has_key(net):
|
|
net_obj = nets_map[net]
|
|
via.SetNet(net_obj)
|
|
|
|
# Add via to board
|
|
self.board.Add(via)
|
|
|
|
return {
|
|
"success": True,
|
|
"message": "Added via",
|
|
"via": {
|
|
"position": {
|
|
"x": position["x"],
|
|
"y": position["y"],
|
|
"unit": position["unit"]
|
|
},
|
|
"size": via.GetWidth() / 1000000,
|
|
"drill": via.GetDrill() / 1000000,
|
|
"from_layer": from_layer,
|
|
"to_layer": to_layer,
|
|
"net": net
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error adding via: {str(e)}")
|
|
return {
|
|
"success": False,
|
|
"message": "Failed to add via",
|
|
"errorDetails": str(e)
|
|
}
|
|
|
|
def delete_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Delete a trace from the PCB"""
|
|
try:
|
|
if not self.board:
|
|
return {
|
|
"success": False,
|
|
"message": "No board is loaded",
|
|
"errorDetails": "Load or create a board first"
|
|
}
|
|
|
|
trace_uuid = params.get("traceUuid")
|
|
position = params.get("position")
|
|
|
|
if not trace_uuid and not position:
|
|
return {
|
|
"success": False,
|
|
"message": "Missing parameters",
|
|
"errorDetails": "Either traceUuid or position must be provided"
|
|
}
|
|
|
|
# Find track by UUID
|
|
if trace_uuid:
|
|
track = None
|
|
for item in self.board.Tracks():
|
|
if str(item.m_Uuid) == trace_uuid:
|
|
track = item
|
|
break
|
|
|
|
if not track:
|
|
return {
|
|
"success": False,
|
|
"message": "Track not found",
|
|
"errorDetails": f"Could not find track with UUID: {trace_uuid}"
|
|
}
|
|
|
|
self.board.Remove(track)
|
|
return {
|
|
"success": True,
|
|
"message": f"Deleted track: {trace_uuid}"
|
|
}
|
|
|
|
# Find track by position
|
|
if position:
|
|
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
|
|
x_nm = int(position["x"] * scale)
|
|
y_nm = int(position["y"] * scale)
|
|
point = pcbnew.VECTOR2I(x_nm, y_nm)
|
|
|
|
# Find closest track
|
|
closest_track = None
|
|
min_distance = float('inf')
|
|
for track in self.board.Tracks():
|
|
dist = self._point_to_track_distance(point, track)
|
|
if dist < min_distance:
|
|
min_distance = dist
|
|
closest_track = track
|
|
|
|
if closest_track and min_distance < 1000000: # Within 1mm
|
|
self.board.Remove(closest_track)
|
|
return {
|
|
"success": True,
|
|
"message": "Deleted track at specified position"
|
|
}
|
|
else:
|
|
return {
|
|
"success": False,
|
|
"message": "No track found",
|
|
"errorDetails": "No track found near specified position"
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error deleting trace: {str(e)}")
|
|
return {
|
|
"success": False,
|
|
"message": "Failed to delete trace",
|
|
"errorDetails": str(e)
|
|
}
|
|
|
|
def get_nets_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Get a list of all nets in the PCB"""
|
|
try:
|
|
if not self.board:
|
|
return {
|
|
"success": False,
|
|
"message": "No board is loaded",
|
|
"errorDetails": "Load or create a board first"
|
|
}
|
|
|
|
nets = []
|
|
netinfo = self.board.GetNetInfo()
|
|
for net_code in range(netinfo.GetNetCount()):
|
|
net = netinfo.GetNetItem(net_code)
|
|
if net:
|
|
nets.append({
|
|
"name": net.GetNetname(),
|
|
"code": net.GetNetCode(),
|
|
"class": net.GetClassName()
|
|
})
|
|
|
|
return {
|
|
"success": True,
|
|
"nets": nets
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting nets list: {str(e)}")
|
|
return {
|
|
"success": False,
|
|
"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:
|
|
if not self.board:
|
|
return {
|
|
"success": False,
|
|
"message": "No board is loaded",
|
|
"errorDetails": "Load or create a board first"
|
|
}
|
|
|
|
name = params.get("name")
|
|
clearance = params.get("clearance")
|
|
track_width = params.get("trackWidth")
|
|
via_diameter = params.get("viaDiameter")
|
|
via_drill = params.get("viaDrill")
|
|
uvia_diameter = params.get("uviaDiameter")
|
|
uvia_drill = params.get("uviaDrill")
|
|
diff_pair_width = params.get("diffPairWidth")
|
|
diff_pair_gap = params.get("diffPairGap")
|
|
nets = params.get("nets", [])
|
|
|
|
if not name:
|
|
return {
|
|
"success": False,
|
|
"message": "Missing netclass name",
|
|
"errorDetails": "name parameter is required"
|
|
}
|
|
|
|
# Get net classes
|
|
net_classes = self.board.GetNetClasses()
|
|
|
|
# Create new net class if it doesn't exist
|
|
if not net_classes.Find(name):
|
|
netclass = pcbnew.NETCLASS(name)
|
|
net_classes.Add(netclass)
|
|
else:
|
|
netclass = net_classes.Find(name)
|
|
|
|
# Set properties
|
|
scale = 1000000 # mm to nm
|
|
if clearance is not None:
|
|
netclass.SetClearance(int(clearance * scale))
|
|
if track_width is not None:
|
|
netclass.SetTrackWidth(int(track_width * scale))
|
|
if via_diameter is not None:
|
|
netclass.SetViaDiameter(int(via_diameter * scale))
|
|
if via_drill is not None:
|
|
netclass.SetViaDrill(int(via_drill * scale))
|
|
if uvia_diameter is not None:
|
|
netclass.SetMicroViaDiameter(int(uvia_diameter * scale))
|
|
if uvia_drill is not None:
|
|
netclass.SetMicroViaDrill(int(uvia_drill * scale))
|
|
if diff_pair_width is not None:
|
|
netclass.SetDiffPairWidth(int(diff_pair_width * scale))
|
|
if diff_pair_gap is not None:
|
|
netclass.SetDiffPairGap(int(diff_pair_gap * scale))
|
|
|
|
# Add nets to net class
|
|
netinfo = self.board.GetNetInfo()
|
|
nets_map = netinfo.NetsByName()
|
|
for net_name in nets:
|
|
if nets_map.has_key(net_name):
|
|
net = nets_map[net_name]
|
|
net.SetClass(netclass)
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Created net class: {name}",
|
|
"netClass": {
|
|
"name": name,
|
|
"clearance": netclass.GetClearance() / scale,
|
|
"trackWidth": netclass.GetTrackWidth() / scale,
|
|
"viaDiameter": netclass.GetViaDiameter() / scale,
|
|
"viaDrill": netclass.GetViaDrill() / scale,
|
|
"uviaDiameter": netclass.GetMicroViaDiameter() / scale,
|
|
"uviaDrill": netclass.GetMicroViaDrill() / scale,
|
|
"diffPairWidth": netclass.GetDiffPairWidth() / scale,
|
|
"diffPairGap": netclass.GetDiffPairGap() / scale,
|
|
"nets": nets
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error creating net class: {str(e)}")
|
|
return {
|
|
"success": False,
|
|
"message": "Failed to create net class",
|
|
"errorDetails": str(e)
|
|
}
|
|
|
|
def add_copper_pour(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Add a copper pour (zone) to the PCB"""
|
|
try:
|
|
if not self.board:
|
|
return {
|
|
"success": False,
|
|
"message": "No board is loaded",
|
|
"errorDetails": "Load or create a board first"
|
|
}
|
|
|
|
layer = params.get("layer", "F.Cu")
|
|
net = params.get("net")
|
|
clearance = params.get("clearance")
|
|
min_width = params.get("minWidth", 0.2)
|
|
points = params.get("points", [])
|
|
priority = params.get("priority", 0)
|
|
fill_type = params.get("fillType", "solid") # solid or hatched
|
|
|
|
if not points or len(points) < 3:
|
|
return {
|
|
"success": False,
|
|
"message": "Missing points",
|
|
"errorDetails": "At least 3 points are required for copper pour outline"
|
|
}
|
|
|
|
# Get layer ID
|
|
layer_id = self.board.GetLayerID(layer)
|
|
if layer_id < 0:
|
|
return {
|
|
"success": False,
|
|
"message": "Invalid layer",
|
|
"errorDetails": f"Layer '{layer}' does not exist"
|
|
}
|
|
|
|
# Create zone
|
|
zone = pcbnew.ZONE(self.board)
|
|
zone.SetLayer(layer_id)
|
|
|
|
# Set net if provided
|
|
if net:
|
|
netinfo = self.board.GetNetInfo()
|
|
nets_map = netinfo.NetsByName()
|
|
if nets_map.has_key(net):
|
|
net_obj = nets_map[net]
|
|
zone.SetNet(net_obj)
|
|
|
|
# Set zone properties
|
|
scale = 1000000 # mm to nm
|
|
zone.SetAssignedPriority(priority)
|
|
|
|
if clearance is not None:
|
|
zone.SetLocalClearance(int(clearance * scale))
|
|
|
|
zone.SetMinThickness(int(min_width * scale))
|
|
|
|
# Set fill type
|
|
if fill_type == "hatched":
|
|
zone.SetFillMode(pcbnew.ZONE_FILL_MODE_HATCH_PATTERN)
|
|
else:
|
|
zone.SetFillMode(pcbnew.ZONE_FILL_MODE_POLYGONS)
|
|
|
|
# Create outline
|
|
outline = zone.Outline()
|
|
outline.NewOutline() # Create a new outline contour first
|
|
|
|
# Add points to outline
|
|
for point in points:
|
|
scale = 1000000 if point.get("unit", "mm") == "mm" else 25400000
|
|
x_nm = int(point["x"] * scale)
|
|
y_nm = int(point["y"] * scale)
|
|
outline.Append(pcbnew.VECTOR2I(x_nm, y_nm)) # Add point to outline
|
|
|
|
# Add zone to board
|
|
self.board.Add(zone)
|
|
|
|
# Fill zone
|
|
# Note: Zone filling can cause issues with SWIG API
|
|
# Comment out for now - zones will be filled when board is saved/opened in KiCAD
|
|
# filler = pcbnew.ZONE_FILLER(self.board)
|
|
# filler.Fill(self.board.Zones())
|
|
|
|
return {
|
|
"success": True,
|
|
"message": "Added copper pour",
|
|
"pour": {
|
|
"layer": layer,
|
|
"net": net,
|
|
"clearance": clearance,
|
|
"minWidth": min_width,
|
|
"priority": priority,
|
|
"fillType": fill_type,
|
|
"pointCount": len(points)
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error adding copper pour: {str(e)}")
|
|
return {
|
|
"success": False,
|
|
"message": "Failed to add copper pour",
|
|
"errorDetails": str(e)
|
|
}
|
|
|
|
def route_differential_pair(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Route a differential pair between two sets of points or pads"""
|
|
try:
|
|
if not self.board:
|
|
return {
|
|
"success": False,
|
|
"message": "No board is loaded",
|
|
"errorDetails": "Load or create a board first"
|
|
}
|
|
|
|
start_pos = params.get("startPos")
|
|
end_pos = params.get("endPos")
|
|
net_pos = params.get("netPos")
|
|
net_neg = params.get("netNeg")
|
|
layer = params.get("layer", "F.Cu")
|
|
width = params.get("width")
|
|
gap = params.get("gap")
|
|
|
|
if not start_pos or not end_pos or not net_pos or not net_neg:
|
|
return {
|
|
"success": False,
|
|
"message": "Missing parameters",
|
|
"errorDetails": "startPos, endPos, netPos, and netNeg are required"
|
|
}
|
|
|
|
# Get layer ID
|
|
layer_id = self.board.GetLayerID(layer)
|
|
if layer_id < 0:
|
|
return {
|
|
"success": False,
|
|
"message": "Invalid layer",
|
|
"errorDetails": f"Layer '{layer}' does not exist"
|
|
}
|
|
|
|
# Get nets
|
|
netinfo = self.board.GetNetInfo()
|
|
nets_map = netinfo.NetsByName()
|
|
|
|
net_pos_obj = nets_map[net_pos] if nets_map.has_key(net_pos) else None
|
|
net_neg_obj = nets_map[net_neg] if nets_map.has_key(net_neg) else None
|
|
|
|
if not net_pos_obj or not net_neg_obj:
|
|
return {
|
|
"success": False,
|
|
"message": "Nets not found",
|
|
"errorDetails": "One or both nets specified for the differential pair do not exist"
|
|
}
|
|
|
|
# Get start and end points
|
|
start_point = self._get_point(start_pos)
|
|
end_point = self._get_point(end_pos)
|
|
|
|
# Calculate offset vectors for the two traces
|
|
# First, get the direction vector from start to end
|
|
dx = end_point.x - start_point.x
|
|
dy = end_point.y - start_point.y
|
|
length = math.sqrt(dx * dx + dy * dy)
|
|
|
|
if length <= 0:
|
|
return {
|
|
"success": False,
|
|
"message": "Invalid points",
|
|
"errorDetails": "Start and end points must be different"
|
|
}
|
|
|
|
# Normalize direction vector
|
|
dx /= length
|
|
dy /= length
|
|
|
|
# Get perpendicular vector
|
|
px = -dy
|
|
py = dx
|
|
|
|
# Set default gap if not provided
|
|
if gap is None:
|
|
gap = 0.2 # mm
|
|
|
|
# Convert to nm
|
|
gap_nm = int(gap * 1000000)
|
|
|
|
# Calculate offsets
|
|
offset_x = int(px * gap_nm / 2)
|
|
offset_y = int(py * gap_nm / 2)
|
|
|
|
# Create positive and negative trace points
|
|
pos_start = pcbnew.VECTOR2I(int(start_point.x + offset_x), int(start_point.y + offset_y))
|
|
pos_end = pcbnew.VECTOR2I(int(end_point.x + offset_x), int(end_point.y + offset_y))
|
|
neg_start = pcbnew.VECTOR2I(int(start_point.x - offset_x), int(start_point.y - offset_y))
|
|
neg_end = pcbnew.VECTOR2I(int(end_point.x - offset_x), int(end_point.y - offset_y))
|
|
|
|
# Create positive trace
|
|
pos_track = pcbnew.PCB_TRACK(self.board)
|
|
pos_track.SetStart(pos_start)
|
|
pos_track.SetEnd(pos_end)
|
|
pos_track.SetLayer(layer_id)
|
|
pos_track.SetNet(net_pos_obj)
|
|
|
|
# Create negative trace
|
|
neg_track = pcbnew.PCB_TRACK(self.board)
|
|
neg_track.SetStart(neg_start)
|
|
neg_track.SetEnd(neg_end)
|
|
neg_track.SetLayer(layer_id)
|
|
neg_track.SetNet(net_neg_obj)
|
|
|
|
# Set width
|
|
if width:
|
|
trace_width_nm = int(width * 1000000)
|
|
pos_track.SetWidth(trace_width_nm)
|
|
neg_track.SetWidth(trace_width_nm)
|
|
else:
|
|
# Get default width from design rules or net class
|
|
trace_width = self.board.GetDesignSettings().GetCurrentTrackWidth()
|
|
pos_track.SetWidth(trace_width)
|
|
neg_track.SetWidth(trace_width)
|
|
|
|
# Add tracks to board
|
|
self.board.Add(pos_track)
|
|
self.board.Add(neg_track)
|
|
|
|
return {
|
|
"success": True,
|
|
"message": "Added differential pair traces",
|
|
"diffPair": {
|
|
"posNet": net_pos,
|
|
"negNet": net_neg,
|
|
"layer": layer,
|
|
"width": pos_track.GetWidth() / 1000000,
|
|
"gap": gap,
|
|
"length": length / 1000000
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error routing differential pair: {str(e)}")
|
|
return {
|
|
"success": False,
|
|
"message": "Failed to route differential pair",
|
|
"errorDetails": str(e)
|
|
}
|
|
|
|
def _get_point(self, point_spec: Dict[str, Any]) -> pcbnew.VECTOR2I:
|
|
"""Convert point specification to KiCAD point"""
|
|
if "x" in point_spec and "y" in point_spec:
|
|
scale = 1000000 if point_spec.get("unit", "mm") == "mm" else 25400000
|
|
x_nm = int(point_spec["x"] * scale)
|
|
y_nm = int(point_spec["y"] * scale)
|
|
return pcbnew.VECTOR2I(x_nm, y_nm)
|
|
elif "pad" in point_spec and "componentRef" in point_spec:
|
|
module = self.board.FindFootprintByReference(point_spec["componentRef"])
|
|
if module:
|
|
pad = module.FindPadByName(point_spec["pad"])
|
|
if pad:
|
|
return pad.GetPosition()
|
|
raise ValueError("Invalid point specification")
|
|
|
|
def _point_to_track_distance(self, point: pcbnew.VECTOR2I, track: pcbnew.PCB_TRACK) -> float:
|
|
"""Calculate distance from point to track segment"""
|
|
start = track.GetStart()
|
|
end = track.GetEnd()
|
|
|
|
# Vector from start to end
|
|
v = pcbnew.VECTOR2I(end.x - start.x, end.y - start.y)
|
|
# Vector from start to point
|
|
w = pcbnew.VECTOR2I(point.x - start.x, point.y - start.y)
|
|
|
|
# Length of track squared
|
|
c1 = v.x * v.x + v.y * v.y
|
|
if c1 == 0:
|
|
return self._point_distance(point, start)
|
|
|
|
# Projection coefficient
|
|
c2 = float(w.x * v.x + w.y * v.y) / c1
|
|
|
|
if c2 < 0:
|
|
return self._point_distance(point, start)
|
|
elif c2 > 1:
|
|
return self._point_distance(point, end)
|
|
|
|
# Point on line
|
|
proj = pcbnew.VECTOR2I(
|
|
int(start.x + c2 * v.x),
|
|
int(start.y + c2 * v.y)
|
|
)
|
|
return self._point_distance(point, proj)
|
|
|
|
def _point_distance(self, p1: pcbnew.VECTOR2I, p2: pcbnew.VECTOR2I) -> float:
|
|
"""Calculate distance between two points"""
|
|
dx = p1.x - p2.x
|
|
dy = p1.y - p2.y
|
|
return (dx * dx + dy * dy) ** 0.5
|