feat: Address multiple open issues (#32, #30, #26, #19)

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>
This commit is contained in:
KiCAD MCP Bot
2026-01-19 19:17:35 -05:00
parent d2723bc292
commit 04db774a2b
6 changed files with 657 additions and 17 deletions

View File

@@ -483,7 +483,244 @@ class ComponentCommands:
"message": "Failed to get component list",
"errorDetails": str(e)
}
def find_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Find components matching search criteria (reference, value, or footprint pattern)"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
# Get search parameters
reference_pattern = params.get("reference", "").lower()
value_pattern = params.get("value", "").lower()
footprint_pattern = params.get("footprint", "").lower()
if not reference_pattern and not value_pattern and not footprint_pattern:
return {
"success": False,
"message": "Missing search criteria",
"errorDetails": "At least one of reference, value, or footprint pattern is required"
}
matches = []
for module in self.board.GetFootprints():
ref = module.GetReference().lower()
val = module.GetValue().lower()
fp = module.GetFPIDAsString().lower()
# Check if component matches all provided patterns
match = True
if reference_pattern and reference_pattern not in ref:
match = False
if value_pattern and value_pattern not in val:
match = False
if footprint_pattern and footprint_pattern not in fp:
match = False
if match:
pos = module.GetPosition()
matches.append({
"reference": module.GetReference(),
"value": module.GetValue(),
"footprint": module.GetFPIDAsString(),
"position": {
"x": pos.x / 1000000,
"y": pos.y / 1000000,
"unit": "mm"
},
"rotation": module.GetOrientation().AsDegrees(),
"layer": self.board.GetLayerName(module.GetLayer())
})
return {
"success": True,
"matchCount": len(matches),
"components": matches
}
except Exception as e:
logger.error(f"Error finding components: {str(e)}")
return {
"success": False,
"message": "Failed to find components",
"errorDetails": str(e)
}
def get_component_pads(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get all pads for a component with their positions and net connections"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
reference = params.get("reference")
if not reference:
return {
"success": False,
"message": "Missing reference",
"errorDetails": "reference parameter is required"
}
# Find the component
module = self.board.FindFootprintByReference(reference)
if not module:
return {
"success": False,
"message": "Component not found",
"errorDetails": f"Could not find component: {reference}"
}
pads = []
for pad in module.Pads():
pos = pad.GetPosition()
size = pad.GetSize()
# Get pad shape as string
shape_map = {
pcbnew.PAD_SHAPE_CIRCLE: "circle",
pcbnew.PAD_SHAPE_RECT: "rect",
pcbnew.PAD_SHAPE_OVAL: "oval",
pcbnew.PAD_SHAPE_TRAPEZOID: "trapezoid",
pcbnew.PAD_SHAPE_ROUNDRECT: "roundrect",
pcbnew.PAD_SHAPE_CHAMFERED_RECT: "chamfered_rect",
pcbnew.PAD_SHAPE_CUSTOM: "custom"
}
shape = shape_map.get(pad.GetShape(), "unknown")
# Get pad type
type_map = {
pcbnew.PAD_ATTRIB_PTH: "through_hole",
pcbnew.PAD_ATTRIB_SMD: "smd",
pcbnew.PAD_ATTRIB_CONN: "connector",
pcbnew.PAD_ATTRIB_NPTH: "npth"
}
pad_type = type_map.get(pad.GetAttribute(), "unknown")
pads.append({
"name": pad.GetName(),
"number": pad.GetNumber(),
"position": {
"x": pos.x / 1000000,
"y": pos.y / 1000000,
"unit": "mm"
},
"net": pad.GetNetname(),
"netCode": pad.GetNetCode(),
"shape": shape,
"type": pad_type,
"size": {
"x": size.x / 1000000,
"y": size.y / 1000000,
"unit": "mm"
},
"drillSize": pad.GetDrillSize().x / 1000000 if pad.GetDrillSize().x > 0 else None
})
# Get component position for reference
comp_pos = module.GetPosition()
return {
"success": True,
"reference": reference,
"componentPosition": {
"x": comp_pos.x / 1000000,
"y": comp_pos.y / 1000000,
"unit": "mm"
},
"padCount": len(pads),
"pads": pads
}
except Exception as e:
logger.error(f"Error getting component pads: {str(e)}")
return {
"success": False,
"message": "Failed to get component pads",
"errorDetails": str(e)
}
def get_pad_position(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Get the position of a specific pad on a component"""
try:
if not self.board:
return {
"success": False,
"message": "No board is loaded",
"errorDetails": "Load or create a board first"
}
reference = params.get("reference")
pad_name = params.get("padName") or params.get("padNumber")
if not reference:
return {
"success": False,
"message": "Missing reference",
"errorDetails": "reference parameter is required"
}
if not pad_name:
return {
"success": False,
"message": "Missing pad identifier",
"errorDetails": "padName or padNumber parameter is required"
}
# Find the component
module = self.board.FindFootprintByReference(reference)
if not module:
return {
"success": False,
"message": "Component not found",
"errorDetails": f"Could not find component: {reference}"
}
# Find the specific pad
pad = module.FindPadByNumber(str(pad_name))
if not pad:
# List available pads in error message
available_pads = [p.GetNumber() for p in module.Pads()]
return {
"success": False,
"message": "Pad not found",
"errorDetails": f"Pad '{pad_name}' not found on {reference}. Available pads: {', '.join(available_pads)}"
}
pos = pad.GetPosition()
size = pad.GetSize()
return {
"success": True,
"reference": reference,
"padName": pad.GetNumber(),
"position": {
"x": pos.x / 1000000,
"y": pos.y / 1000000,
"unit": "mm"
},
"net": pad.GetNetname(),
"netCode": pad.GetNetCode(),
"size": {
"x": size.x / 1000000,
"y": size.y / 1000000,
"unit": "mm"
}
}
except Exception as e:
logger.error(f"Error getting pad position: {str(e)}")
return {
"success": False,
"message": "Failed to get pad position",
"errorDetails": str(e)
}
def place_component_array(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Place an array of components in a grid or circular pattern"""
try:

View File

@@ -366,7 +366,123 @@ class RoutingCommands:
"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: