1386 lines
52 KiB
Python
1386 lines
52 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_pad_to_pad(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""Route a trace directly from one component pad to another.
|
||
|
||
Looks up pad positions automatically, then creates a trace.
|
||
Convenience wrapper around route_trace that eliminates the need
|
||
for separate get_pad_position calls.
|
||
"""
|
||
try:
|
||
if not self.board:
|
||
return {
|
||
"success": False,
|
||
"message": "No board is loaded",
|
||
"errorDetails": "Load or create a board first",
|
||
}
|
||
|
||
from_ref = params.get("fromRef")
|
||
from_pad = str(params.get("fromPad", ""))
|
||
to_ref = params.get("toRef")
|
||
to_pad = str(params.get("toPad", ""))
|
||
layer = params.get("layer", "F.Cu")
|
||
width = params.get("width")
|
||
net = params.get("net") # optional override
|
||
|
||
if not from_ref or not from_pad or not to_ref or not to_pad:
|
||
return {
|
||
"success": False,
|
||
"message": "Missing parameters",
|
||
"errorDetails": "fromRef, fromPad, toRef, toPad are all required",
|
||
}
|
||
|
||
scale = 1000000 # nm to mm
|
||
|
||
# Find pads
|
||
footprints = {fp.GetReference(): fp for fp in self.board.GetFootprints()}
|
||
|
||
for ref in [from_ref, to_ref]:
|
||
if ref not in footprints:
|
||
return {
|
||
"success": False,
|
||
"message": f"Component not found: {ref}",
|
||
"errorDetails": f"'{ref}' does not exist on the board",
|
||
}
|
||
|
||
def find_pad(ref: str, pad_num: str):
|
||
fp = footprints[ref]
|
||
for pad in fp.Pads():
|
||
if pad.GetNumber() == pad_num:
|
||
return pad
|
||
return None
|
||
|
||
start_pad = find_pad(from_ref, from_pad)
|
||
end_pad = find_pad(to_ref, to_pad)
|
||
|
||
if not start_pad:
|
||
return {
|
||
"success": False,
|
||
"message": f"Pad not found: {from_ref} pad {from_pad}",
|
||
"errorDetails": f"Check pad number for {from_ref}",
|
||
}
|
||
if not end_pad:
|
||
return {
|
||
"success": False,
|
||
"message": f"Pad not found: {to_ref} pad {to_pad}",
|
||
"errorDetails": f"Check pad number for {to_ref}",
|
||
}
|
||
|
||
start_pos = start_pad.GetPosition()
|
||
end_pos = end_pad.GetPosition()
|
||
|
||
# Use net from start pad if not overridden
|
||
if not net:
|
||
net = start_pad.GetNetname() or end_pad.GetNetname() or ""
|
||
|
||
# Delegate to route_trace
|
||
result = self.route_trace(
|
||
{
|
||
"start": {
|
||
"x": start_pos.x / scale,
|
||
"y": start_pos.y / scale,
|
||
"unit": "mm",
|
||
},
|
||
"end": {
|
||
"x": end_pos.x / scale,
|
||
"y": end_pos.y / scale,
|
||
"unit": "mm",
|
||
},
|
||
"layer": layer,
|
||
"width": width,
|
||
"net": net,
|
||
}
|
||
)
|
||
|
||
if result.get("success"):
|
||
result["message"] = (
|
||
f"Routed {from_ref}.{from_pad} → {to_ref}.{to_pad} (net: {net or 'none'})"
|
||
)
|
||
result["fromPad"] = {
|
||
"ref": from_ref,
|
||
"pad": from_pad,
|
||
"x": start_pos.x / scale,
|
||
"y": start_pos.y / scale,
|
||
}
|
||
result["toPad"] = {
|
||
"ref": to_ref,
|
||
"pad": to_pad,
|
||
"x": end_pos.x / scale,
|
||
"y": end_pos.y / scale,
|
||
}
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in route_pad_to_pad: {str(e)}")
|
||
return {
|
||
"success": False,
|
||
"message": "Failed to route pad to pad",
|
||
"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")
|
||
net_name = params.get("net")
|
||
layer = params.get("layer")
|
||
include_vias = params.get("includeVias", False)
|
||
|
||
if not trace_uuid and not position and not net_name:
|
||
return {
|
||
"success": False,
|
||
"message": "Missing parameters",
|
||
"errorDetails": "One of traceUuid, position, or net must be provided",
|
||
}
|
||
|
||
# Delete by net name (bulk delete)
|
||
if net_name:
|
||
tracks_to_remove = []
|
||
for track in list(self.board.Tracks()):
|
||
if track.GetNetname() != net_name:
|
||
continue
|
||
|
||
# Skip vias if not requested
|
||
is_via = track.Type() == pcbnew.PCB_VIA_T
|
||
if is_via and not include_vias:
|
||
continue
|
||
|
||
# Filter by layer if specified (only for non-vias)
|
||
if layer and not is_via:
|
||
layer_id = self.board.GetLayerID(layer)
|
||
if track.GetLayer() != layer_id:
|
||
continue
|
||
|
||
tracks_to_remove.append(track)
|
||
|
||
deleted_count = len(tracks_to_remove)
|
||
for track in tracks_to_remove:
|
||
self.board.Remove(track)
|
||
tracks_to_remove.clear()
|
||
self.board.SetModified()
|
||
|
||
return {
|
||
"success": True,
|
||
"message": f"Deleted {deleted_count} traces on net '{net_name}'",
|
||
"deletedCount": deleted_count,
|
||
}
|
||
|
||
# Find track by UUID
|
||
if trace_uuid:
|
||
track = None
|
||
for item in list(self.board.Tracks()):
|
||
if item.m_Uuid.AsString() == 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)
|
||
track = None
|
||
self.board.SetModified()
|
||
return {"success": True, "message": f"Deleted track: {trace_uuid}"}
|
||
|
||
# No valid parameters provided
|
||
if not position:
|
||
return {
|
||
"success": False,
|
||
"message": "No valid search parameter provided",
|
||
"errorDetails": "Provide traceUuid, position, or net parameter",
|
||
}
|
||
|
||
# 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 list(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)
|
||
closest_track = None
|
||
self.board.SetModified()
|
||
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),
|
||
}
|
||
return {
|
||
"success": False,
|
||
"message": "No action taken",
|
||
"errorDetails": "No matching trace found for given parameters",
|
||
}
|
||
|
||
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.GetNetClassName(),
|
||
}
|
||
)
|
||
|
||
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 list(self.board.Tracks()):
|
||
try:
|
||
# 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": track.m_Uuid.AsString(),
|
||
"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": track.m_Uuid.AsString(),
|
||
"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,
|
||
}
|
||
)
|
||
except Exception as track_err:
|
||
logger.warning(f"Skipping invalid track object: {track_err}")
|
||
continue
|
||
|
||
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 modify_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""Modify properties of an existing trace
|
||
|
||
Allows changing trace width, layer, and net assignment.
|
||
Find trace by UUID or position.
|
||
"""
|
||
try:
|
||
if not self.board:
|
||
return {
|
||
"success": False,
|
||
"message": "No board is loaded",
|
||
"errorDetails": "Load or create a board first",
|
||
}
|
||
|
||
# Identification parameters
|
||
trace_uuid = params.get("uuid")
|
||
position = params.get("position") # {x, y, unit}
|
||
|
||
# Modification parameters
|
||
new_width = params.get("width") # in mm
|
||
new_layer = params.get("layer")
|
||
new_net = params.get("net")
|
||
|
||
if not trace_uuid and not position:
|
||
return {
|
||
"success": False,
|
||
"message": "Missing trace identifier",
|
||
"errorDetails": "Provide either 'uuid' or 'position' to identify the trace",
|
||
}
|
||
|
||
scale = 1000000 # nm to mm conversion
|
||
|
||
# Find the track
|
||
track = None
|
||
|
||
if trace_uuid:
|
||
for item in list(self.board.Tracks()):
|
||
if item.m_Uuid.AsString() == trace_uuid:
|
||
track = item
|
||
break
|
||
elif position:
|
||
pos_unit = position.get("unit", "mm")
|
||
pos_scale = scale if pos_unit == "mm" else 25400000
|
||
x_nm = int(position["x"] * pos_scale)
|
||
y_nm = int(position["y"] * pos_scale)
|
||
point = pcbnew.VECTOR2I(x_nm, y_nm)
|
||
|
||
# Find closest track
|
||
min_distance = float("inf")
|
||
for item in list(self.board.Tracks()):
|
||
dist = self._point_to_track_distance(point, item)
|
||
if dist < min_distance:
|
||
min_distance = dist
|
||
track = item
|
||
|
||
# Only accept if within 1mm
|
||
if min_distance >= 1000000:
|
||
track = None
|
||
|
||
if not track:
|
||
return {
|
||
"success": False,
|
||
"message": "Track not found",
|
||
"errorDetails": "Could not find track with specified identifier",
|
||
}
|
||
|
||
# Check if it's a via (some modifications don't apply)
|
||
is_via = track.Type() == pcbnew.PCB_VIA_T
|
||
modifications = []
|
||
|
||
# Apply modifications
|
||
if new_width is not None:
|
||
width_nm = int(new_width * scale)
|
||
track.SetWidth(width_nm)
|
||
modifications.append(f"width={new_width}mm")
|
||
|
||
if new_layer and not is_via:
|
||
layer_id = self.board.GetLayerID(new_layer)
|
||
if layer_id < 0:
|
||
return {
|
||
"success": False,
|
||
"message": "Invalid layer",
|
||
"errorDetails": f"Layer '{new_layer}' not found",
|
||
}
|
||
track.SetLayer(layer_id)
|
||
modifications.append(f"layer={new_layer}")
|
||
|
||
if new_net:
|
||
netinfo = self.board.GetNetInfo()
|
||
net = netinfo.GetNetItem(new_net)
|
||
if not net:
|
||
return {
|
||
"success": False,
|
||
"message": "Invalid net",
|
||
"errorDetails": f"Net '{new_net}' not found",
|
||
}
|
||
track.SetNet(net)
|
||
modifications.append(f"net={new_net}")
|
||
|
||
if not modifications:
|
||
return {
|
||
"success": False,
|
||
"message": "No modifications specified",
|
||
"errorDetails": "Provide at least one of: width, layer, net",
|
||
}
|
||
|
||
return {
|
||
"success": True,
|
||
"message": f"Modified trace: {', '.join(modifications)}",
|
||
"uuid": track.m_Uuid.AsString(),
|
||
"modifications": modifications,
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error modifying trace: {str(e)}")
|
||
return {
|
||
"success": False,
|
||
"message": "Failed to modify trace",
|
||
"errorDetails": str(e),
|
||
}
|
||
|
||
def copy_routing_pattern(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""Copy routing pattern from source components to target components
|
||
|
||
This enables routing replication between identical component groups.
|
||
The pattern is copied with a translation offset calculated from
|
||
the position difference between source and target components.
|
||
"""
|
||
try:
|
||
if not self.board:
|
||
return {
|
||
"success": False,
|
||
"message": "No board is loaded",
|
||
"errorDetails": "Load or create a board first",
|
||
}
|
||
|
||
source_refs = params.get("sourceRefs", []) # e.g., ["U1", "U2", "U3"]
|
||
target_refs = params.get("targetRefs", []) # e.g., ["U4", "U5", "U6"]
|
||
include_vias = params.get("includeVias", True)
|
||
trace_width = params.get("traceWidth") # Optional override
|
||
|
||
if not source_refs or not target_refs:
|
||
return {
|
||
"success": False,
|
||
"message": "Missing component references",
|
||
"errorDetails": "Provide both 'sourceRefs' and 'targetRefs' arrays",
|
||
}
|
||
|
||
if len(source_refs) != len(target_refs):
|
||
return {
|
||
"success": False,
|
||
"message": "Mismatched component counts",
|
||
"errorDetails": f"sourceRefs has {len(source_refs)} items, targetRefs has {len(target_refs)}",
|
||
}
|
||
|
||
scale = 1000000 # nm to mm conversion
|
||
|
||
# Get footprints
|
||
footprints = {fp.GetReference(): fp for fp in self.board.GetFootprints()}
|
||
|
||
# Validate all references exist
|
||
for ref in source_refs + target_refs:
|
||
if ref not in footprints:
|
||
return {
|
||
"success": False,
|
||
"message": "Component not found",
|
||
"errorDetails": f"Component '{ref}' not found on board",
|
||
}
|
||
|
||
# Calculate offset from first source to first target component
|
||
source_fp = footprints[source_refs[0]]
|
||
target_fp = footprints[target_refs[0]]
|
||
source_pos = source_fp.GetPosition()
|
||
target_pos = target_fp.GetPosition()
|
||
|
||
offset_x = target_pos.x - source_pos.x
|
||
offset_y = target_pos.y - source_pos.y
|
||
|
||
# Build mapping from source refs to target refs
|
||
ref_mapping = dict(zip(source_refs, target_refs))
|
||
|
||
# Collect all nets connected to source components
|
||
source_nets = set()
|
||
source_pad_positions = [] # (x, y) in nm for geometric fallback
|
||
for ref in source_refs:
|
||
fp = footprints[ref]
|
||
for pad in fp.Pads():
|
||
net_name = pad.GetNetname()
|
||
if net_name and net_name != "":
|
||
source_nets.add(net_name)
|
||
pos = pad.GetPosition()
|
||
source_pad_positions.append((pos.x, pos.y))
|
||
|
||
# Build bounding box around source pads (with 5mm tolerance in nm)
|
||
TOLERANCE_NM = int(5 * scale)
|
||
if source_pad_positions:
|
||
xs = [p[0] for p in source_pad_positions]
|
||
ys = [p[1] for p in source_pad_positions]
|
||
bbox_x1 = min(xs) - TOLERANCE_NM
|
||
bbox_x2 = max(xs) + TOLERANCE_NM
|
||
bbox_y1 = min(ys) - TOLERANCE_NM
|
||
bbox_y2 = max(ys) + TOLERANCE_NM
|
||
else:
|
||
# Fall back to component position ± 25mm
|
||
sp = source_fp.GetPosition()
|
||
bbox_x1 = sp.x - int(25 * scale)
|
||
bbox_x2 = sp.x + int(25 * scale)
|
||
bbox_y1 = sp.y - int(25 * scale)
|
||
bbox_y2 = sp.y + int(25 * scale)
|
||
|
||
def point_in_bbox(px: int, py: int) -> bool:
|
||
return bbox_x1 <= px <= bbox_x2 and bbox_y1 <= py <= bbox_y2
|
||
|
||
# Collect traces: by net name (if available) OR by geometric proximity
|
||
use_net_filter = len(source_nets) > 0
|
||
traces_to_copy = []
|
||
vias_to_copy = []
|
||
|
||
for track in list(self.board.Tracks()):
|
||
is_via = track.Type() == pcbnew.PCB_VIA_T
|
||
|
||
if use_net_filter:
|
||
# Primary: net-based filter
|
||
if track.GetNetname() not in source_nets:
|
||
continue
|
||
else:
|
||
# Fallback: geometric filter – trace start OR end inside source bbox
|
||
if is_via:
|
||
pos = track.GetPosition()
|
||
if not point_in_bbox(pos.x, pos.y):
|
||
continue
|
||
else:
|
||
s = track.GetStart()
|
||
e = track.GetEnd()
|
||
if not (point_in_bbox(s.x, s.y) or point_in_bbox(e.x, e.y)):
|
||
continue
|
||
|
||
if is_via:
|
||
if include_vias:
|
||
vias_to_copy.append(track)
|
||
else:
|
||
traces_to_copy.append(track)
|
||
|
||
filter_method = (
|
||
"net-based" if use_net_filter else "geometric (pads have no nets)"
|
||
)
|
||
logger.info(
|
||
f"copy_routing_pattern: {len(traces_to_copy)} traces, "
|
||
f"{len(vias_to_copy)} vias selected via {filter_method}"
|
||
)
|
||
|
||
# Create new traces with offset
|
||
created_traces = 0
|
||
created_vias = 0
|
||
|
||
for track in traces_to_copy:
|
||
start = track.GetStart()
|
||
end = track.GetEnd()
|
||
|
||
# Create new track
|
||
new_track = pcbnew.PCB_TRACK(self.board)
|
||
new_track.SetStart(
|
||
pcbnew.VECTOR2I(start.x + offset_x, start.y + offset_y)
|
||
)
|
||
new_track.SetEnd(pcbnew.VECTOR2I(end.x + offset_x, end.y + offset_y))
|
||
new_track.SetLayer(track.GetLayer())
|
||
|
||
# Set width (use override or original)
|
||
if trace_width:
|
||
new_track.SetWidth(int(trace_width * scale))
|
||
else:
|
||
new_track.SetWidth(track.GetWidth())
|
||
|
||
# Try to find corresponding target net
|
||
# This is a simplification - more sophisticated mapping would be needed
|
||
# for complex designs
|
||
self.board.Add(new_track)
|
||
created_traces += 1
|
||
|
||
for via in vias_to_copy:
|
||
pos = via.GetPosition()
|
||
|
||
# Create new via
|
||
new_via = pcbnew.PCB_VIA(self.board)
|
||
new_via.SetPosition(pcbnew.VECTOR2I(pos.x + offset_x, pos.y + offset_y))
|
||
new_via.SetWidth(via.GetWidth())
|
||
new_via.SetDrill(via.GetDrillValue())
|
||
new_via.SetViaType(via.GetViaType())
|
||
|
||
self.board.Add(new_via)
|
||
created_vias += 1
|
||
|
||
result = {
|
||
"success": True,
|
||
"message": f"Copied routing pattern: {created_traces} traces, {created_vias} vias",
|
||
"filterMethod": filter_method,
|
||
"offset": {"x": offset_x / scale, "y": offset_y / scale, "unit": "mm"},
|
||
"createdTraces": created_traces,
|
||
"createdVias": created_vias,
|
||
"sourceComponents": source_refs,
|
||
"targetComponents": target_refs,
|
||
}
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error copying routing pattern: {str(e)}")
|
||
return {
|
||
"success": False,
|
||
"message": "Failed to copy routing pattern",
|
||
"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("outline", params.get("points", []))
|
||
priority = params.get("priority", 0)
|
||
fill_type = params.get("fillType", "solid") # solid or hatched
|
||
|
||
# If no outline provided, use board outline
|
||
if not points or len(points) < 3:
|
||
board_box = self.board.GetBoardEdgesBoundingBox()
|
||
if board_box.GetWidth() > 0 and board_box.GetHeight() > 0:
|
||
scale = 1000000 # nm to mm
|
||
x1 = board_box.GetX() / scale
|
||
y1 = board_box.GetY() / scale
|
||
x2 = (board_box.GetX() + board_box.GetWidth()) / scale
|
||
y2 = (board_box.GetY() + board_box.GetHeight()) / scale
|
||
points = [
|
||
{"x": x1, "y": y1},
|
||
{"x": x2, "y": y1},
|
||
{"x": x2, "y": y2},
|
||
{"x": x1, "y": y2},
|
||
]
|
||
else:
|
||
return {
|
||
"success": False,
|
||
"message": "Missing outline",
|
||
"errorDetails": "Provide an outline array or add a board outline first",
|
||
}
|
||
|
||
# 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
|