feat: Add missing routing/component tools and fix SWIG/UUID bugs
New MCP tools added (TypeScript layer): - Routing: delete_trace, query_traces, get_nets_list, modify_trace, create_netclass, route_differential_pair, refill_zones (with SWIG warning) - Component: get_component_pads, get_component_list, get_pad_position, place_component_array, align_components, duplicate_component Bug fixes: - routing.py: Fix SwigPyObject UUID comparison (str() -> m_Uuid.AsString()) - routing.py: Fix SWIG iterator invalidation after board.Remove() by converting board.Tracks() to list() before iteration - routing.py: Add board.SetModified() and clear Python refs after Remove() to prevent dangling SWIG pointers crashing subsequent calls - routing.py: Wrap each track access in try/except in query_traces() to gracefully skip invalid track objects after bulk delete - routing.py: Add missing return statement (mypy fix) - library.py: Fix search_footprints param mapping search_term->pattern - library.py: Fix fp.name -> fp.full_name field access - library.py: Accept both 'pattern' and 'search_term' parameter names - library.py: Add library filter support in search_footprints - library.py: Fix loop variable shadowing Path object (mypy fix) - design_rules.py: Add type annotation for violation_counts (mypy fix) Contribution guidelines followed (CONTRIBUTING.md): - black python/ applied (3 touched files pass without changes) - mypy python/ passes with 0 errors on all changed files - npx prettier --write applied to changed TypeScript files - npm run build passes (tsc --noEmit: 0 errors) - Commit messages follow feat:/fix: convention Note on diff size: The large insertion/deletion count in the changed files is due to black and prettier reformatting previously unformatted code (missing trailing commas, quote style, line length). The actual logic changes are limited to the 6 files listed above. Note: refill_zones has known SWIG segfault risk (see KNOWN_ISSUES.md). Prefer IPC backend (KiCAD open) or zone fill via KiCAD UI.
This commit is contained in:
36
CHANGELOG.md
36
CHANGELOG.md
@@ -2,6 +2,42 @@
|
|||||||
|
|
||||||
All notable changes to the KiCAD MCP Server project are documented here.
|
All notable changes to the KiCAD MCP Server project are documented here.
|
||||||
|
|
||||||
|
## [2.2.0-alpha] - 2026-02-27
|
||||||
|
|
||||||
|
### New MCP Tools (TypeScript layer – previously Python-only)
|
||||||
|
|
||||||
|
**Routing tools:**
|
||||||
|
- `delete_trace` - Delete traces by UUID, position or net name
|
||||||
|
- `query_traces` - Query/filter traces on the board
|
||||||
|
- `get_nets_list` - List all nets with net code and class
|
||||||
|
- `modify_trace` - Modify trace width or layer
|
||||||
|
- `create_netclass` - Create or update a net class
|
||||||
|
- `route_differential_pair` - Route a differential pair between two points
|
||||||
|
- `refill_zones` - Refill all copper zones ⚠️ SWIG segfault risk, prefer IPC/UI
|
||||||
|
|
||||||
|
**Component tools:**
|
||||||
|
- `get_component_pads` - Get all pad data for a component
|
||||||
|
- `get_component_list` - List all components on the board
|
||||||
|
- `get_pad_position` - Get absolute position of a specific pad
|
||||||
|
- `place_component_array` - Place components in a grid array
|
||||||
|
- `align_components` - Align components along an axis
|
||||||
|
- `duplicate_component` - Duplicate a component with offset
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- `routing.py`: Fix SwigPyObject UUID comparison (`str()` → `m_Uuid.AsString()`)
|
||||||
|
- `routing.py`: Fix SWIG iterator invalidation after `board.Remove()` by snapshotting `list(board.Tracks())`
|
||||||
|
- `routing.py`: Add `board.SetModified()` + `track = None` after `Remove()` to prevent dangling SWIG pointer crashes
|
||||||
|
- `routing.py`: Per-track `try/except` in `query_traces()` to skip invalid objects after bulk delete
|
||||||
|
- `routing.py`: Add missing return statement (mypy)
|
||||||
|
- `library.py`: Fix `search_footprints` parameter mapping (`search_term` → `pattern`)
|
||||||
|
- `library.py`: Fix field access (`fp.name` → `fp.full_name`)
|
||||||
|
- `library.py`: Accept both `pattern` and `search_term` parameter names
|
||||||
|
- `library.py`: Fix loop variable shadowing `Path` object (mypy)
|
||||||
|
- `design_rules.py`: Add type annotation for `violation_counts` (mypy)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [2.1.0-alpha] - 2026-01-10
|
## [2.1.0-alpha] - 2026-01-10
|
||||||
|
|
||||||
### Phase 1: Intelligent Schematic Wiring System - Core Infrastructure
|
### Phase 1: Intelligent Schematic Wiring System - Core Infrastructure
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import pcbnew
|
|||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any, Optional, List, Tuple
|
from typing import Dict, Any, Optional, List, Tuple
|
||||||
|
|
||||||
logger = logging.getLogger('kicad_interface')
|
logger = logging.getLogger("kicad_interface")
|
||||||
|
|
||||||
|
|
||||||
class DesignRuleCommands:
|
class DesignRuleCommands:
|
||||||
"""Handles design rule checking and configuration"""
|
"""Handles design rule checking and configuration"""
|
||||||
@@ -23,7 +24,7 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
design_settings = self.board.GetDesignSettings()
|
design_settings = self.board.GetDesignSettings()
|
||||||
@@ -57,9 +58,13 @@ class DesignRuleCommands:
|
|||||||
|
|
||||||
# Set micro via settings (use properties - methods removed in KiCAD 9.0)
|
# Set micro via settings (use properties - methods removed in KiCAD 9.0)
|
||||||
if "microViaDiameter" in params:
|
if "microViaDiameter" in params:
|
||||||
design_settings.m_MicroViasMinSize = int(params["microViaDiameter"] * scale)
|
design_settings.m_MicroViasMinSize = int(
|
||||||
|
params["microViaDiameter"] * scale
|
||||||
|
)
|
||||||
if "microViaDrill" in params:
|
if "microViaDrill" in params:
|
||||||
design_settings.m_MicroViasMinDrill = int(params["microViaDrill"] * scale)
|
design_settings.m_MicroViasMinDrill = int(
|
||||||
|
params["microViaDrill"] * scale
|
||||||
|
)
|
||||||
|
|
||||||
# Set minimum values
|
# Set minimum values
|
||||||
if "minTrackWidth" in params:
|
if "minTrackWidth" in params:
|
||||||
@@ -72,13 +77,19 @@ class DesignRuleCommands:
|
|||||||
design_settings.m_MinThroughDrill = int(params["minViaDrill"] * scale)
|
design_settings.m_MinThroughDrill = int(params["minViaDrill"] * scale)
|
||||||
|
|
||||||
if "minMicroViaDiameter" in params:
|
if "minMicroViaDiameter" in params:
|
||||||
design_settings.m_MicroViasMinSize = int(params["minMicroViaDiameter"] * scale)
|
design_settings.m_MicroViasMinSize = int(
|
||||||
|
params["minMicroViaDiameter"] * scale
|
||||||
|
)
|
||||||
if "minMicroViaDrill" in params:
|
if "minMicroViaDrill" in params:
|
||||||
design_settings.m_MicroViasMinDrill = int(params["minMicroViaDrill"] * scale)
|
design_settings.m_MicroViasMinDrill = int(
|
||||||
|
params["minMicroViaDrill"] * scale
|
||||||
|
)
|
||||||
|
|
||||||
# KiCAD 9.0: m_MinHoleDiameter removed - use m_MinThroughDrill
|
# KiCAD 9.0: m_MinHoleDiameter removed - use m_MinThroughDrill
|
||||||
if "minHoleDiameter" in params:
|
if "minHoleDiameter" in params:
|
||||||
design_settings.m_MinThroughDrill = int(params["minHoleDiameter"] * scale)
|
design_settings.m_MinThroughDrill = int(
|
||||||
|
params["minHoleDiameter"] * scale
|
||||||
|
)
|
||||||
|
|
||||||
# KiCAD 9.0: Added hole clearance settings
|
# KiCAD 9.0: Added hole clearance settings
|
||||||
if "holeClearance" in params:
|
if "holeClearance" in params:
|
||||||
@@ -102,13 +113,13 @@ class DesignRuleCommands:
|
|||||||
"minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale,
|
"minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale,
|
||||||
"holeClearance": design_settings.m_HoleClearance / scale,
|
"holeClearance": design_settings.m_HoleClearance / scale,
|
||||||
"holeToHoleMin": design_settings.m_HoleToHoleMin / scale,
|
"holeToHoleMin": design_settings.m_HoleToHoleMin / scale,
|
||||||
"viasMinAnnularWidth": design_settings.m_ViasMinAnnularWidth / scale
|
"viasMinAnnularWidth": design_settings.m_ViasMinAnnularWidth / scale,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": "Updated design rules",
|
"message": "Updated design rules",
|
||||||
"rules": response_rules
|
"rules": response_rules,
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -116,7 +127,7 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to set design rules",
|
"message": "Failed to set design rules",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_design_rules(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def get_design_rules(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -126,7 +137,7 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
design_settings = self.board.GetDesignSettings()
|
design_settings = self.board.GetDesignSettings()
|
||||||
@@ -138,42 +149,34 @@ class DesignRuleCommands:
|
|||||||
"clearance": design_settings.m_MinClearance / scale,
|
"clearance": design_settings.m_MinClearance / scale,
|
||||||
"trackWidth": design_settings.GetCurrentTrackWidth() / scale,
|
"trackWidth": design_settings.GetCurrentTrackWidth() / scale,
|
||||||
"minTrackWidth": design_settings.m_TrackMinWidth / scale,
|
"minTrackWidth": design_settings.m_TrackMinWidth / scale,
|
||||||
|
|
||||||
# Via settings (current values from methods)
|
# Via settings (current values from methods)
|
||||||
"viaDiameter": design_settings.GetCurrentViaSize() / scale,
|
"viaDiameter": design_settings.GetCurrentViaSize() / scale,
|
||||||
"viaDrill": design_settings.GetCurrentViaDrill() / scale,
|
"viaDrill": design_settings.GetCurrentViaDrill() / scale,
|
||||||
|
|
||||||
# Via minimum values
|
# Via minimum values
|
||||||
"minViaDiameter": design_settings.m_ViasMinSize / scale,
|
"minViaDiameter": design_settings.m_ViasMinSize / scale,
|
||||||
"viasMinAnnularWidth": design_settings.m_ViasMinAnnularWidth / scale,
|
"viasMinAnnularWidth": design_settings.m_ViasMinAnnularWidth / scale,
|
||||||
|
|
||||||
# Micro via settings
|
# Micro via settings
|
||||||
"microViaDiameter": design_settings.m_MicroViasMinSize / scale,
|
"microViaDiameter": design_settings.m_MicroViasMinSize / scale,
|
||||||
"microViaDrill": design_settings.m_MicroViasMinDrill / scale,
|
"microViaDrill": design_settings.m_MicroViasMinDrill / scale,
|
||||||
"minMicroViaDiameter": design_settings.m_MicroViasMinSize / scale,
|
"minMicroViaDiameter": design_settings.m_MicroViasMinSize / scale,
|
||||||
"minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale,
|
"minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale,
|
||||||
|
|
||||||
# KiCAD 9.0: Hole and drill settings (replaces removed m_ViasMinDrill and m_MinHoleDiameter)
|
# KiCAD 9.0: Hole and drill settings (replaces removed m_ViasMinDrill and m_MinHoleDiameter)
|
||||||
"minThroughDrill": design_settings.m_MinThroughDrill / scale,
|
"minThroughDrill": design_settings.m_MinThroughDrill / scale,
|
||||||
"holeClearance": design_settings.m_HoleClearance / scale,
|
"holeClearance": design_settings.m_HoleClearance / scale,
|
||||||
"holeToHoleMin": design_settings.m_HoleToHoleMin / scale,
|
"holeToHoleMin": design_settings.m_HoleToHoleMin / scale,
|
||||||
|
|
||||||
# Other constraints
|
# Other constraints
|
||||||
"copperEdgeClearance": design_settings.m_CopperEdgeClearance / scale,
|
"copperEdgeClearance": design_settings.m_CopperEdgeClearance / scale,
|
||||||
"silkClearance": design_settings.m_SilkClearance / scale,
|
"silkClearance": design_settings.m_SilkClearance / scale,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {"success": True, "rules": rules}
|
||||||
"success": True,
|
|
||||||
"rules": rules
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting design rules: {str(e)}")
|
logger.error(f"Error getting design rules: {str(e)}")
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to get design rules",
|
"message": "Failed to get design rules",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def run_drc(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def run_drc(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -189,7 +192,7 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
report_path = params.get("reportPath")
|
report_path = params.get("reportPath")
|
||||||
@@ -200,7 +203,7 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Board file not found",
|
"message": "Board file not found",
|
||||||
"errorDetails": "Cannot run DRC without a saved board file"
|
"errorDetails": "Cannot run DRC without a saved board file",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Find kicad-cli executable
|
# Find kicad-cli executable
|
||||||
@@ -209,23 +212,28 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "kicad-cli not found",
|
"message": "kicad-cli not found",
|
||||||
"errorDetails": "KiCAD CLI tool not found in system. Install KiCAD 8.0+ or set PATH."
|
"errorDetails": "KiCAD CLI tool not found in system. Install KiCAD 8.0+ or set PATH.",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create temporary JSON output file
|
# Create temporary JSON output file
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp:
|
with tempfile.NamedTemporaryFile(
|
||||||
|
mode="w", suffix=".json", delete=False
|
||||||
|
) as tmp:
|
||||||
json_output = tmp.name
|
json_output = tmp.name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Build command
|
# Build command
|
||||||
cmd = [
|
cmd = [
|
||||||
kicad_cli,
|
kicad_cli,
|
||||||
'pcb',
|
"pcb",
|
||||||
'drc',
|
"drc",
|
||||||
'--format', 'json',
|
"--format",
|
||||||
'--output', json_output,
|
"json",
|
||||||
'--units', 'mm',
|
"--output",
|
||||||
board_file
|
json_output,
|
||||||
|
"--units",
|
||||||
|
"mm",
|
||||||
|
board_file,
|
||||||
]
|
]
|
||||||
|
|
||||||
logger.info(f"Running DRC command: {' '.join(cmd)}")
|
logger.info(f"Running DRC command: {' '.join(cmd)}")
|
||||||
@@ -235,7 +243,7 @@ class DesignRuleCommands:
|
|||||||
cmd,
|
cmd,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=600 # 10 minute timeout for large boards (21MB PCB needs time)
|
timeout=600, # 10 minute timeout for large boards (21MB PCB needs time)
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
@@ -243,32 +251,34 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "DRC command failed",
|
"message": "DRC command failed",
|
||||||
"errorDetails": result.stderr
|
"errorDetails": result.stderr,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Read JSON output
|
# Read JSON output
|
||||||
with open(json_output, 'r', encoding='utf-8') as f:
|
with open(json_output, "r", encoding="utf-8") as f:
|
||||||
drc_data = json.load(f)
|
drc_data = json.load(f)
|
||||||
|
|
||||||
# Parse violations from kicad-cli output
|
# Parse violations from kicad-cli output
|
||||||
violations = []
|
violations = []
|
||||||
violation_counts = {}
|
violation_counts: dict[str, int] = {}
|
||||||
severity_counts = {"error": 0, "warning": 0, "info": 0}
|
severity_counts = {"error": 0, "warning": 0, "info": 0}
|
||||||
|
|
||||||
for violation in drc_data.get('violations', []):
|
for violation in drc_data.get("violations", []):
|
||||||
vtype = violation.get("type", "unknown")
|
vtype = violation.get("type", "unknown")
|
||||||
vseverity = violation.get("severity", "error")
|
vseverity = violation.get("severity", "error")
|
||||||
|
|
||||||
violations.append({
|
violations.append(
|
||||||
"type": vtype,
|
{
|
||||||
"severity": vseverity,
|
"type": vtype,
|
||||||
"message": violation.get("description", ""),
|
"severity": vseverity,
|
||||||
"location": {
|
"message": violation.get("description", ""),
|
||||||
"x": violation.get("x", 0),
|
"location": {
|
||||||
"y": violation.get("y", 0),
|
"x": violation.get("x", 0),
|
||||||
"unit": "mm"
|
"y": violation.get("y", 0),
|
||||||
|
"unit": "mm",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
# Count violations by type
|
# Count violations by type
|
||||||
violation_counts[vtype] = violation_counts.get(vtype, 0) + 1
|
violation_counts[vtype] = violation_counts.get(vtype, 0) + 1
|
||||||
@@ -280,30 +290,39 @@ class DesignRuleCommands:
|
|||||||
# Determine where to save the violations file
|
# Determine where to save the violations file
|
||||||
board_dir = os.path.dirname(board_file)
|
board_dir = os.path.dirname(board_file)
|
||||||
board_name = os.path.splitext(os.path.basename(board_file))[0]
|
board_name = os.path.splitext(os.path.basename(board_file))[0]
|
||||||
violations_file = os.path.join(board_dir, f"{board_name}_drc_violations.json")
|
violations_file = os.path.join(
|
||||||
|
board_dir, f"{board_name}_drc_violations.json"
|
||||||
|
)
|
||||||
|
|
||||||
# Always save violations to JSON file (for large result sets)
|
# Always save violations to JSON file (for large result sets)
|
||||||
with open(violations_file, 'w', encoding='utf-8') as f:
|
with open(violations_file, "w", encoding="utf-8") as f:
|
||||||
json.dump({
|
json.dump(
|
||||||
"board": board_file,
|
{
|
||||||
"timestamp": drc_data.get("date", "unknown"),
|
"board": board_file,
|
||||||
"total_violations": len(violations),
|
"timestamp": drc_data.get("date", "unknown"),
|
||||||
"violation_counts": violation_counts,
|
"total_violations": len(violations),
|
||||||
"severity_counts": severity_counts,
|
"violation_counts": violation_counts,
|
||||||
"violations": violations
|
"severity_counts": severity_counts,
|
||||||
}, f, indent=2)
|
"violations": violations,
|
||||||
|
},
|
||||||
|
f,
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
|
||||||
# Save text report if requested
|
# Save text report if requested
|
||||||
if report_path:
|
if report_path:
|
||||||
report_path = os.path.abspath(os.path.expanduser(report_path))
|
report_path = os.path.abspath(os.path.expanduser(report_path))
|
||||||
cmd_report = [
|
cmd_report = [
|
||||||
kicad_cli,
|
kicad_cli,
|
||||||
'pcb',
|
"pcb",
|
||||||
'drc',
|
"drc",
|
||||||
'--format', 'report',
|
"--format",
|
||||||
'--output', report_path,
|
"report",
|
||||||
'--units', 'mm',
|
"--output",
|
||||||
board_file
|
report_path,
|
||||||
|
"--units",
|
||||||
|
"mm",
|
||||||
|
board_file,
|
||||||
]
|
]
|
||||||
subprocess.run(cmd_report, capture_output=True, timeout=600)
|
subprocess.run(cmd_report, capture_output=True, timeout=600)
|
||||||
|
|
||||||
@@ -314,10 +333,10 @@ class DesignRuleCommands:
|
|||||||
"summary": {
|
"summary": {
|
||||||
"total": len(violations),
|
"total": len(violations),
|
||||||
"by_severity": severity_counts,
|
"by_severity": severity_counts,
|
||||||
"by_type": violation_counts
|
"by_type": violation_counts,
|
||||||
},
|
},
|
||||||
"violationsFile": violations_file,
|
"violationsFile": violations_file,
|
||||||
"reportPath": report_path if report_path else None
|
"reportPath": report_path if report_path else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
@@ -330,14 +349,14 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "DRC command timed out",
|
"message": "DRC command timed out",
|
||||||
"errorDetails": "Command took longer than 600 seconds (10 minutes)"
|
"errorDetails": "Command took longer than 600 seconds (10 minutes)",
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error running DRC: {str(e)}")
|
logger.error(f"Error running DRC: {str(e)}")
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to run DRC",
|
"message": "Failed to run DRC",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _find_kicad_cli(self) -> Optional[str]:
|
def _find_kicad_cli(self) -> Optional[str]:
|
||||||
@@ -399,7 +418,7 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
severity = params.get("severity", "all")
|
severity = params.get("severity", "all")
|
||||||
@@ -416,25 +435,27 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Violations file not found",
|
"message": "Violations file not found",
|
||||||
"errorDetails": "run_drc did not create violations file"
|
"errorDetails": "run_drc did not create violations file",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Load violations from file
|
# Load violations from file
|
||||||
with open(violations_file, 'r', encoding='utf-8') as f:
|
with open(violations_file, "r", encoding="utf-8") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
all_violations = data.get("violations", [])
|
all_violations = data.get("violations", [])
|
||||||
|
|
||||||
# Filter by severity if specified
|
# Filter by severity if specified
|
||||||
if severity != "all":
|
if severity != "all":
|
||||||
filtered_violations = [v for v in all_violations if v.get("severity") == severity]
|
filtered_violations = [
|
||||||
|
v for v in all_violations if v.get("severity") == severity
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
filtered_violations = all_violations
|
filtered_violations = all_violations
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"violations": filtered_violations,
|
"violations": filtered_violations,
|
||||||
"violationsFile": violations_file # Include file path for reference
|
"violationsFile": violations_file, # Include file path for reference
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -442,5 +463,5 @@ class DesignRuleCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to get DRC violations",
|
"message": "Failed to get DRC violations",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from pathlib import Path
|
|||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
import glob
|
import glob
|
||||||
|
|
||||||
logger = logging.getLogger('kicad_interface')
|
logger = logging.getLogger("kicad_interface")
|
||||||
|
|
||||||
|
|
||||||
class LibraryManager:
|
class LibraryManager:
|
||||||
@@ -85,7 +85,7 @@ class LibraryManager:
|
|||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with open(table_path, 'r') as f:
|
with open(table_path, "r") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
# Simple regex-based parser for lib entries
|
# Simple regex-based parser for lib entries
|
||||||
@@ -103,7 +103,9 @@ class LibraryManager:
|
|||||||
self.libraries[nickname] = resolved_uri
|
self.libraries[nickname] = resolved_uri
|
||||||
logger.debug(f" Found library: {nickname} -> {resolved_uri}")
|
logger.debug(f" Found library: {nickname} -> {resolved_uri}")
|
||||||
else:
|
else:
|
||||||
logger.warning(f" Could not resolve URI for library {nickname}: {uri}")
|
logger.warning(
|
||||||
|
f" Could not resolve URI for library {nickname}: {uri}"
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error parsing fp-lib-table at {table_path}: {e}")
|
logger.error(f"Error parsing fp-lib-table at {table_path}: {e}")
|
||||||
@@ -124,23 +126,23 @@ class LibraryManager:
|
|||||||
|
|
||||||
# Common KiCAD environment variables
|
# Common KiCAD environment variables
|
||||||
env_vars = {
|
env_vars = {
|
||||||
'KICAD9_FOOTPRINT_DIR': self._find_kicad_footprint_dir(),
|
"KICAD9_FOOTPRINT_DIR": self._find_kicad_footprint_dir(),
|
||||||
'KICAD8_FOOTPRINT_DIR': self._find_kicad_footprint_dir(),
|
"KICAD8_FOOTPRINT_DIR": self._find_kicad_footprint_dir(),
|
||||||
'KICAD_FOOTPRINT_DIR': self._find_kicad_footprint_dir(),
|
"KICAD_FOOTPRINT_DIR": self._find_kicad_footprint_dir(),
|
||||||
'KISYSMOD': self._find_kicad_footprint_dir(),
|
"KISYSMOD": self._find_kicad_footprint_dir(),
|
||||||
'KICAD9_3RD_PARTY': self._find_kicad_3rdparty_dir(),
|
"KICAD9_3RD_PARTY": self._find_kicad_3rdparty_dir(),
|
||||||
'KICAD8_3RD_PARTY': self._find_kicad_3rdparty_dir(),
|
"KICAD8_3RD_PARTY": self._find_kicad_3rdparty_dir(),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Project directory
|
# Project directory
|
||||||
if self.project_path:
|
if self.project_path:
|
||||||
env_vars['KIPRJMOD'] = str(self.project_path)
|
env_vars["KIPRJMOD"] = str(self.project_path)
|
||||||
|
|
||||||
# Replace environment variables
|
# Replace environment variables
|
||||||
for var, value in env_vars.items():
|
for var, value in env_vars.items():
|
||||||
if value:
|
if value:
|
||||||
resolved = resolved.replace(f'${{{var}}}', value)
|
resolved = resolved.replace(f"${{{var}}}", value)
|
||||||
resolved = resolved.replace(f'${var}', value)
|
resolved = resolved.replace(f"${var}", value)
|
||||||
|
|
||||||
# Expand ~ to home directory
|
# Expand ~ to home directory
|
||||||
resolved = os.path.expanduser(resolved)
|
resolved = os.path.expanduser(resolved)
|
||||||
@@ -167,10 +169,10 @@ class LibraryManager:
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Also check environment variable
|
# Also check environment variable
|
||||||
if 'KICAD9_FOOTPRINT_DIR' in os.environ:
|
if "KICAD9_FOOTPRINT_DIR" in os.environ:
|
||||||
possible_paths.insert(0, os.environ['KICAD9_FOOTPRINT_DIR'])
|
possible_paths.insert(0, os.environ["KICAD9_FOOTPRINT_DIR"])
|
||||||
if 'KICAD8_FOOTPRINT_DIR' in os.environ:
|
if "KICAD8_FOOTPRINT_DIR" in os.environ:
|
||||||
possible_paths.insert(0, os.environ['KICAD8_FOOTPRINT_DIR'])
|
possible_paths.insert(0, os.environ["KICAD8_FOOTPRINT_DIR"])
|
||||||
|
|
||||||
for path in possible_paths:
|
for path in possible_paths:
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
@@ -190,26 +192,36 @@ class LibraryManager:
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
# 1. Check shell environment variable first
|
# 1. Check shell environment variable first
|
||||||
if 'KICAD9_3RD_PARTY' in os.environ:
|
if "KICAD9_3RD_PARTY" in os.environ:
|
||||||
path = os.environ['KICAD9_3RD_PARTY']
|
path = os.environ["KICAD9_3RD_PARTY"]
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
return path
|
return path
|
||||||
|
|
||||||
# 2. Check kicad_common.json for user-defined variables
|
# 2. Check kicad_common.json for user-defined variables
|
||||||
kicad_common_paths = [
|
kicad_common_paths = [
|
||||||
Path.home() / "Library" / "Preferences" / "kicad" / "9.0" / "kicad_common.json", # macOS
|
Path.home()
|
||||||
|
/ "Library"
|
||||||
|
/ "Preferences"
|
||||||
|
/ "kicad"
|
||||||
|
/ "9.0"
|
||||||
|
/ "kicad_common.json", # macOS
|
||||||
Path.home() / ".config" / "kicad" / "9.0" / "kicad_common.json", # Linux
|
Path.home() / ".config" / "kicad" / "9.0" / "kicad_common.json", # Linux
|
||||||
Path.home() / "AppData" / "Roaming" / "kicad" / "9.0" / "kicad_common.json", # Windows
|
Path.home()
|
||||||
|
/ "AppData"
|
||||||
|
/ "Roaming"
|
||||||
|
/ "kicad"
|
||||||
|
/ "9.0"
|
||||||
|
/ "kicad_common.json", # Windows
|
||||||
]
|
]
|
||||||
|
|
||||||
for config_path in kicad_common_paths:
|
for config_path in kicad_common_paths:
|
||||||
if config_path.exists():
|
if config_path.exists():
|
||||||
try:
|
try:
|
||||||
with open(config_path, 'r') as f:
|
with open(config_path, "r") as f:
|
||||||
config = json.load(f)
|
config = json.load(f)
|
||||||
env_vars = config.get('environment', {}).get('vars', {})
|
env_vars = config.get("environment", {}).get("vars", {})
|
||||||
if env_vars and 'KICAD9_3RD_PARTY' in env_vars:
|
if env_vars and "KICAD9_3RD_PARTY" in env_vars:
|
||||||
path = env_vars['KICAD9_3RD_PARTY']
|
path = env_vars["KICAD9_3RD_PARTY"]
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
return path
|
return path
|
||||||
except (json.JSONDecodeError, KeyError, TypeError):
|
except (json.JSONDecodeError, KeyError, TypeError):
|
||||||
@@ -231,10 +243,10 @@ class LibraryManager:
|
|||||||
Path.home() / "Documents" / "KiCad" / version / "3rdparty",
|
Path.home() / "Documents" / "KiCad" / version / "3rdparty",
|
||||||
]
|
]
|
||||||
|
|
||||||
for path in possible_paths:
|
for candidate in possible_paths:
|
||||||
if path.exists():
|
if candidate.exists():
|
||||||
logger.info(f"Found KiCad 3rd party directory: {path}")
|
logger.info(f"Found KiCad 3rd party directory: {candidate}")
|
||||||
return str(path)
|
return str(candidate)
|
||||||
|
|
||||||
logger.warning("Could not find KiCad 3rd party directory")
|
logger.warning("Could not find KiCad 3rd party directory")
|
||||||
return None
|
return None
|
||||||
@@ -325,7 +337,9 @@ class LibraryManager:
|
|||||||
for library_nickname, library_path in self.libraries.items():
|
for library_nickname, library_path in self.libraries.items():
|
||||||
fp_file = Path(library_path) / f"{footprint_name}.kicad_mod"
|
fp_file = Path(library_path) / f"{footprint_name}.kicad_mod"
|
||||||
if fp_file.exists():
|
if fp_file.exists():
|
||||||
logger.info(f"Found footprint {footprint_name} in library {library_nickname}")
|
logger.info(
|
||||||
|
f"Found footprint {footprint_name} in library {library_nickname}"
|
||||||
|
)
|
||||||
return (library_path, footprint_name)
|
return (library_path, footprint_name)
|
||||||
|
|
||||||
logger.warning(f"Footprint not found in any library: {footprint_name}")
|
logger.warning(f"Footprint not found in any library: {footprint_name}")
|
||||||
@@ -354,18 +368,22 @@ class LibraryManager:
|
|||||||
|
|
||||||
for footprint in footprints:
|
for footprint in footprints:
|
||||||
if regex.search(footprint.lower()):
|
if regex.search(footprint.lower()):
|
||||||
results.append({
|
results.append(
|
||||||
'library': library_nickname,
|
{
|
||||||
'footprint': footprint,
|
"library": library_nickname,
|
||||||
'full_name': f"{library_nickname}:{footprint}"
|
"footprint": footprint,
|
||||||
})
|
"full_name": f"{library_nickname}:{footprint}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if len(results) >= limit:
|
if len(results) >= limit:
|
||||||
return results
|
return results
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def get_footprint_info(self, library_nickname: str, footprint_name: str) -> Optional[Dict[str, str]]:
|
def get_footprint_info(
|
||||||
|
self, library_nickname: str, footprint_name: str
|
||||||
|
) -> Optional[Dict[str, str]]:
|
||||||
"""
|
"""
|
||||||
Get information about a specific footprint
|
Get information about a specific footprint
|
||||||
|
|
||||||
@@ -385,11 +403,11 @@ class LibraryManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'library': library_nickname,
|
"library": library_nickname,
|
||||||
'footprint': footprint_name,
|
"footprint": footprint_name,
|
||||||
'full_name': f"{library_nickname}:{footprint_name}",
|
"full_name": f"{library_nickname}:{footprint_name}",
|
||||||
'path': str(fp_file),
|
"path": str(fp_file),
|
||||||
'library_path': library_path
|
"library_path": library_path,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -404,39 +422,48 @@ class LibraryCommands:
|
|||||||
"""List all available footprint libraries"""
|
"""List all available footprint libraries"""
|
||||||
try:
|
try:
|
||||||
libraries = self.library_manager.list_libraries()
|
libraries = self.library_manager.list_libraries()
|
||||||
return {
|
return {"success": True, "libraries": libraries, "count": len(libraries)}
|
||||||
"success": True,
|
|
||||||
"libraries": libraries,
|
|
||||||
"count": len(libraries)
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error listing libraries: {e}")
|
logger.error(f"Error listing libraries: {e}")
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to list libraries",
|
"message": "Failed to list libraries",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def search_footprints(self, params: Dict) -> Dict:
|
def search_footprints(self, params: Dict) -> Dict:
|
||||||
"""Search for footprints by pattern"""
|
"""Search for footprints by pattern"""
|
||||||
try:
|
try:
|
||||||
pattern = params.get("pattern", "*")
|
# Support both 'pattern' and 'search_term' parameter names
|
||||||
|
pattern = params.get("pattern") or params.get("search_term", "*")
|
||||||
limit = params.get("limit", 20)
|
limit = params.get("limit", 20)
|
||||||
|
library_filter = params.get("library")
|
||||||
|
|
||||||
results = self.library_manager.search_footprints(pattern, limit)
|
results = self.library_manager.search_footprints(
|
||||||
|
pattern, limit * 10 if library_filter else limit
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filter by library if specified
|
||||||
|
if library_filter:
|
||||||
|
results = [
|
||||||
|
r
|
||||||
|
for r in results
|
||||||
|
if r.get("library", "").lower() == library_filter.lower()
|
||||||
|
]
|
||||||
|
results = results[:limit]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"footprints": results,
|
"footprints": results,
|
||||||
"count": len(results),
|
"count": len(results),
|
||||||
"pattern": pattern
|
"pattern": pattern,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error searching footprints: {e}")
|
logger.error(f"Error searching footprints: {e}")
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to search footprints",
|
"message": "Failed to search footprints",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def list_library_footprints(self, params: Dict) -> Dict:
|
def list_library_footprints(self, params: Dict) -> Dict:
|
||||||
@@ -444,10 +471,7 @@ class LibraryCommands:
|
|||||||
try:
|
try:
|
||||||
library = params.get("library")
|
library = params.get("library")
|
||||||
if not library:
|
if not library:
|
||||||
return {
|
return {"success": False, "message": "Missing library parameter"}
|
||||||
"success": False,
|
|
||||||
"message": "Missing library parameter"
|
|
||||||
}
|
|
||||||
|
|
||||||
footprints = self.library_manager.list_footprints(library)
|
footprints = self.library_manager.list_footprints(library)
|
||||||
|
|
||||||
@@ -455,14 +479,14 @@ class LibraryCommands:
|
|||||||
"success": True,
|
"success": True,
|
||||||
"library": library,
|
"library": library,
|
||||||
"footprints": footprints,
|
"footprints": footprints,
|
||||||
"count": len(footprints)
|
"count": len(footprints),
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error listing library footprints: {e}")
|
logger.error(f"Error listing library footprints: {e}")
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to list library footprints",
|
"message": "Failed to list library footprints",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_footprint_info(self, params: Dict) -> Dict:
|
def get_footprint_info(self, params: Dict) -> Dict:
|
||||||
@@ -470,10 +494,7 @@ class LibraryCommands:
|
|||||||
try:
|
try:
|
||||||
footprint_spec = params.get("footprint")
|
footprint_spec = params.get("footprint")
|
||||||
if not footprint_spec:
|
if not footprint_spec:
|
||||||
return {
|
return {"success": False, "message": "Missing footprint parameter"}
|
||||||
"success": False,
|
|
||||||
"message": "Missing footprint parameter"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Try to find the footprint
|
# Try to find the footprint
|
||||||
result = self.library_manager.find_footprint(footprint_spec)
|
result = self.library_manager.find_footprint(footprint_spec)
|
||||||
@@ -491,17 +512,14 @@ class LibraryCommands:
|
|||||||
"library": library_nickname,
|
"library": library_nickname,
|
||||||
"footprint": footprint_name,
|
"footprint": footprint_name,
|
||||||
"full_name": f"{library_nickname}:{footprint_name}",
|
"full_name": f"{library_nickname}:{footprint_name}",
|
||||||
"library_path": library_path
|
"library_path": library_path,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {"success": True, "footprint_info": info}
|
||||||
"success": True,
|
|
||||||
"footprint_info": info
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": f"Footprint not found: {footprint_spec}"
|
"message": f"Footprint not found: {footprint_spec}",
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -509,5 +527,5 @@ class LibraryCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to get footprint info",
|
"message": "Failed to get footprint info",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import logging
|
|||||||
import math
|
import math
|
||||||
from typing import Dict, Any, Optional, List, Tuple
|
from typing import Dict, Any, Optional, List, Tuple
|
||||||
|
|
||||||
logger = logging.getLogger('kicad_interface')
|
logger = logging.getLogger("kicad_interface")
|
||||||
|
|
||||||
|
|
||||||
class RoutingCommands:
|
class RoutingCommands:
|
||||||
"""Handles routing-related KiCAD operations"""
|
"""Handles routing-related KiCAD operations"""
|
||||||
@@ -24,7 +25,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
name = params.get("name")
|
name = params.get("name")
|
||||||
@@ -34,7 +35,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Missing net name",
|
"message": "Missing net name",
|
||||||
"errorDetails": "name parameter is required"
|
"errorDetails": "name parameter is required",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create new net
|
# Create new net
|
||||||
@@ -58,8 +59,8 @@ class RoutingCommands:
|
|||||||
"net": {
|
"net": {
|
||||||
"name": name,
|
"name": name,
|
||||||
"class": net_class if net_class else "Default",
|
"class": net_class if net_class else "Default",
|
||||||
"netcode": net.GetNetCode()
|
"netcode": net.GetNetCode(),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -67,7 +68,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to add net",
|
"message": "Failed to add net",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def route_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def route_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -77,7 +78,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
start = params.get("start")
|
start = params.get("start")
|
||||||
@@ -91,7 +92,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Missing parameters",
|
"message": "Missing parameters",
|
||||||
"errorDetails": "start and end points are required"
|
"errorDetails": "start and end points are required",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get layer ID
|
# Get layer ID
|
||||||
@@ -100,7 +101,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Invalid layer",
|
"message": "Invalid layer",
|
||||||
"errorDetails": f"Layer '{layer}' does not exist"
|
"errorDetails": f"Layer '{layer}' does not exist",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get start point
|
# Get start point
|
||||||
@@ -133,14 +134,16 @@ class RoutingCommands:
|
|||||||
# Add via if requested and net is specified
|
# Add via if requested and net is specified
|
||||||
if via and net:
|
if via and net:
|
||||||
via_point = end_point
|
via_point = end_point
|
||||||
self.add_via({
|
self.add_via(
|
||||||
"position": {
|
{
|
||||||
"x": via_point.x / 1000000,
|
"position": {
|
||||||
"y": via_point.y / 1000000,
|
"x": via_point.x / 1000000,
|
||||||
"unit": "mm"
|
"y": via_point.y / 1000000,
|
||||||
},
|
"unit": "mm",
|
||||||
"net": net
|
},
|
||||||
})
|
"net": net,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -149,17 +152,17 @@ class RoutingCommands:
|
|||||||
"start": {
|
"start": {
|
||||||
"x": start_point.x / 1000000,
|
"x": start_point.x / 1000000,
|
||||||
"y": start_point.y / 1000000,
|
"y": start_point.y / 1000000,
|
||||||
"unit": "mm"
|
"unit": "mm",
|
||||||
},
|
},
|
||||||
"end": {
|
"end": {
|
||||||
"x": end_point.x / 1000000,
|
"x": end_point.x / 1000000,
|
||||||
"y": end_point.y / 1000000,
|
"y": end_point.y / 1000000,
|
||||||
"unit": "mm"
|
"unit": "mm",
|
||||||
},
|
},
|
||||||
"layer": layer,
|
"layer": layer,
|
||||||
"width": track.GetWidth() / 1000000,
|
"width": track.GetWidth() / 1000000,
|
||||||
"net": net
|
"net": net,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -167,7 +170,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to route trace",
|
"message": "Failed to route trace",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def add_via(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def add_via(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -177,7 +180,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
position = params.get("position")
|
position = params.get("position")
|
||||||
@@ -191,22 +194,28 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Missing position",
|
"message": "Missing position",
|
||||||
"errorDetails": "position parameter is required"
|
"errorDetails": "position parameter is required",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create via
|
# Create via
|
||||||
via = pcbnew.PCB_VIA(self.board)
|
via = pcbnew.PCB_VIA(self.board)
|
||||||
|
|
||||||
# Set position
|
# Set position
|
||||||
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
|
scale = (
|
||||||
|
1000000 if position["unit"] == "mm" else 25400000
|
||||||
|
) # mm or inch to nm
|
||||||
x_nm = int(position["x"] * scale)
|
x_nm = int(position["x"] * scale)
|
||||||
y_nm = int(position["y"] * scale)
|
y_nm = int(position["y"] * scale)
|
||||||
via.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
via.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
||||||
|
|
||||||
# Set size and drill (default to board's current via settings)
|
# Set size and drill (default to board's current via settings)
|
||||||
design_settings = self.board.GetDesignSettings()
|
design_settings = self.board.GetDesignSettings()
|
||||||
via.SetWidth(int(size * 1000000) if size else design_settings.GetCurrentViaSize())
|
via.SetWidth(
|
||||||
via.SetDrill(int(drill * 1000000) if drill else design_settings.GetCurrentViaDrill())
|
int(size * 1000000) if size else design_settings.GetCurrentViaSize()
|
||||||
|
)
|
||||||
|
via.SetDrill(
|
||||||
|
int(drill * 1000000) if drill else design_settings.GetCurrentViaDrill()
|
||||||
|
)
|
||||||
|
|
||||||
# Set layers
|
# Set layers
|
||||||
from_id = self.board.GetLayerID(from_layer)
|
from_id = self.board.GetLayerID(from_layer)
|
||||||
@@ -215,7 +224,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Invalid layer",
|
"message": "Invalid layer",
|
||||||
"errorDetails": "Specified layers do not exist"
|
"errorDetails": "Specified layers do not exist",
|
||||||
}
|
}
|
||||||
via.SetLayerPair(from_id, to_id)
|
via.SetLayerPair(from_id, to_id)
|
||||||
|
|
||||||
@@ -237,14 +246,14 @@ class RoutingCommands:
|
|||||||
"position": {
|
"position": {
|
||||||
"x": position["x"],
|
"x": position["x"],
|
||||||
"y": position["y"],
|
"y": position["y"],
|
||||||
"unit": position["unit"]
|
"unit": position["unit"],
|
||||||
},
|
},
|
||||||
"size": via.GetWidth() / 1000000,
|
"size": via.GetWidth() / 1000000,
|
||||||
"drill": via.GetDrill() / 1000000,
|
"drill": via.GetDrill() / 1000000,
|
||||||
"from_layer": from_layer,
|
"from_layer": from_layer,
|
||||||
"to_layer": to_layer,
|
"to_layer": to_layer,
|
||||||
"net": net
|
"net": net,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -252,7 +261,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to add via",
|
"message": "Failed to add via",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def delete_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def delete_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -262,7 +271,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
trace_uuid = params.get("traceUuid")
|
trace_uuid = params.get("traceUuid")
|
||||||
@@ -275,13 +284,13 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Missing parameters",
|
"message": "Missing parameters",
|
||||||
"errorDetails": "One of traceUuid, position, or net must be provided"
|
"errorDetails": "One of traceUuid, position, or net must be provided",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Delete by net name (bulk delete)
|
# Delete by net name (bulk delete)
|
||||||
if net_name:
|
if net_name:
|
||||||
tracks_to_remove = []
|
tracks_to_remove = []
|
||||||
for track in self.board.Tracks():
|
for track in list(self.board.Tracks()):
|
||||||
if track.GetNetname() != net_name:
|
if track.GetNetname() != net_name:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -298,20 +307,23 @@ class RoutingCommands:
|
|||||||
|
|
||||||
tracks_to_remove.append(track)
|
tracks_to_remove.append(track)
|
||||||
|
|
||||||
|
deleted_count = len(tracks_to_remove)
|
||||||
for track in tracks_to_remove:
|
for track in tracks_to_remove:
|
||||||
self.board.Remove(track)
|
self.board.Remove(track)
|
||||||
|
tracks_to_remove.clear()
|
||||||
|
self.board.SetModified()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": f"Deleted {len(tracks_to_remove)} traces on net '{net_name}'",
|
"message": f"Deleted {deleted_count} traces on net '{net_name}'",
|
||||||
"deletedCount": len(tracks_to_remove)
|
"deletedCount": deleted_count,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Find track by UUID
|
# Find track by UUID
|
||||||
if trace_uuid:
|
if trace_uuid:
|
||||||
track = None
|
track = None
|
||||||
for item in self.board.Tracks():
|
for item in list(self.board.Tracks()):
|
||||||
if str(item.m_Uuid) == trace_uuid:
|
if item.m_Uuid.AsString() == trace_uuid:
|
||||||
track = item
|
track = item
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -319,26 +331,35 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Track not found",
|
"message": "Track not found",
|
||||||
"errorDetails": f"Could not find track with UUID: {trace_uuid}"
|
"errorDetails": f"Could not find track with UUID: {trace_uuid}",
|
||||||
}
|
}
|
||||||
|
|
||||||
self.board.Remove(track)
|
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 {
|
return {
|
||||||
"success": True,
|
"success": False,
|
||||||
"message": f"Deleted track: {trace_uuid}"
|
"message": "No valid search parameter provided",
|
||||||
|
"errorDetails": "Provide traceUuid, position, or net parameter",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Find track by position
|
# Find track by position
|
||||||
if position:
|
if position:
|
||||||
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
|
scale = (
|
||||||
|
1000000 if position["unit"] == "mm" else 25400000
|
||||||
|
) # mm or inch to nm
|
||||||
x_nm = int(position["x"] * scale)
|
x_nm = int(position["x"] * scale)
|
||||||
y_nm = int(position["y"] * scale)
|
y_nm = int(position["y"] * scale)
|
||||||
point = pcbnew.VECTOR2I(x_nm, y_nm)
|
point = pcbnew.VECTOR2I(x_nm, y_nm)
|
||||||
|
|
||||||
# Find closest track
|
# Find closest track
|
||||||
closest_track = None
|
closest_track = None
|
||||||
min_distance = float('inf')
|
min_distance = float("inf")
|
||||||
for track in self.board.Tracks():
|
for track in list(self.board.Tracks()):
|
||||||
dist = self._point_to_track_distance(point, track)
|
dist = self._point_to_track_distance(point, track)
|
||||||
if dist < min_distance:
|
if dist < min_distance:
|
||||||
min_distance = dist
|
min_distance = dist
|
||||||
@@ -346,15 +367,17 @@ class RoutingCommands:
|
|||||||
|
|
||||||
if closest_track and min_distance < 1000000: # Within 1mm
|
if closest_track and min_distance < 1000000: # Within 1mm
|
||||||
self.board.Remove(closest_track)
|
self.board.Remove(closest_track)
|
||||||
|
closest_track = None
|
||||||
|
self.board.SetModified()
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": "Deleted track at specified position"
|
"message": "Deleted track at specified position",
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No track found",
|
"message": "No track found",
|
||||||
"errorDetails": "No track found near specified position"
|
"errorDetails": "No track found near specified position",
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -362,8 +385,13 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to delete trace",
|
"message": "Failed to delete trace",
|
||||||
"errorDetails": str(e)
|
"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]:
|
def get_nets_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Get a list of all nets in the PCB"""
|
"""Get a list of all nets in the PCB"""
|
||||||
@@ -372,7 +400,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
nets = []
|
nets = []
|
||||||
@@ -380,23 +408,22 @@ class RoutingCommands:
|
|||||||
for net_code in range(netinfo.GetNetCount()):
|
for net_code in range(netinfo.GetNetCount()):
|
||||||
net = netinfo.GetNetItem(net_code)
|
net = netinfo.GetNetItem(net_code)
|
||||||
if net:
|
if net:
|
||||||
nets.append({
|
nets.append(
|
||||||
"name": net.GetNetname(),
|
{
|
||||||
"code": net.GetNetCode(),
|
"name": net.GetNetname(),
|
||||||
"class": net.GetClassName()
|
"code": net.GetNetCode(),
|
||||||
})
|
"class": net.GetClassName(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {"success": True, "nets": nets}
|
||||||
"success": True,
|
|
||||||
"nets": nets
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting nets list: {str(e)}")
|
logger.error(f"Error getting nets list: {str(e)}")
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to get nets list",
|
"message": "Failed to get nets list",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def query_traces(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def query_traces(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -406,7 +433,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get filter parameters
|
# Get filter parameters
|
||||||
@@ -420,86 +447,90 @@ class RoutingCommands:
|
|||||||
vias = []
|
vias = []
|
||||||
|
|
||||||
# Process tracks
|
# Process tracks
|
||||||
for track in self.board.Tracks():
|
for track in list(self.board.Tracks()):
|
||||||
# Check if it's a via
|
try:
|
||||||
is_via = track.Type() == pcbnew.PCB_VIA_T
|
# Check if it's a via
|
||||||
|
is_via = track.Type() == pcbnew.PCB_VIA_T
|
||||||
|
|
||||||
if is_via and not include_vias:
|
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
|
continue
|
||||||
|
|
||||||
# Filter by bounding box
|
# Filter by net
|
||||||
if bbox:
|
if net_name and track.GetNetname() != net_name:
|
||||||
bbox_unit = bbox.get("unit", "mm")
|
continue
|
||||||
bbox_scale = scale if bbox_unit == "mm" else 25400000
|
|
||||||
x1 = int(bbox.get("x1", 0) * bbox_scale)
|
# Filter by layer (only for tracks, not vias)
|
||||||
y1 = int(bbox.get("y1", 0) * bbox_scale)
|
if layer and not is_via:
|
||||||
x2 = int(bbox.get("x2", 0) * bbox_scale)
|
layer_id = self.board.GetLayerID(layer)
|
||||||
y2 = int(bbox.get("y2", 0) * bbox_scale)
|
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:
|
if is_via:
|
||||||
pos = track.GetPosition()
|
pos = track.GetPosition()
|
||||||
if not (x1 <= pos.x <= x2 and y1 <= pos.y <= y2):
|
vias.append(
|
||||||
continue
|
{
|
||||||
|
"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:
|
else:
|
||||||
start = track.GetStart()
|
start = track.GetStart()
|
||||||
end = track.GetEnd()
|
end = track.GetEnd()
|
||||||
# Check if either endpoint is within bbox
|
traces.append(
|
||||||
start_in = x1 <= start.x <= x2 and y1 <= start.y <= y2
|
{
|
||||||
end_in = x1 <= end.x <= x2 and y1 <= end.y <= y2
|
"uuid": track.m_Uuid.AsString(),
|
||||||
if not (start_in or end_in):
|
"net": track.GetNetname(),
|
||||||
continue
|
"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
|
||||||
|
|
||||||
if is_via:
|
result = {"success": True, "traceCount": len(traces), "traces": traces}
|
||||||
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:
|
if include_vias:
|
||||||
result["viaCount"] = len(vias)
|
result["viaCount"] = len(vias)
|
||||||
@@ -512,7 +543,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to query traces",
|
"message": "Failed to query traces",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def modify_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def modify_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -526,7 +557,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Identification parameters
|
# Identification parameters
|
||||||
@@ -542,7 +573,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Missing trace identifier",
|
"message": "Missing trace identifier",
|
||||||
"errorDetails": "Provide either 'uuid' or 'position' to identify the trace"
|
"errorDetails": "Provide either 'uuid' or 'position' to identify the trace",
|
||||||
}
|
}
|
||||||
|
|
||||||
scale = 1000000 # nm to mm conversion
|
scale = 1000000 # nm to mm conversion
|
||||||
@@ -551,8 +582,8 @@ class RoutingCommands:
|
|||||||
track = None
|
track = None
|
||||||
|
|
||||||
if trace_uuid:
|
if trace_uuid:
|
||||||
for item in self.board.Tracks():
|
for item in list(self.board.Tracks()):
|
||||||
if str(item.m_Uuid) == trace_uuid:
|
if item.m_Uuid.AsString() == trace_uuid:
|
||||||
track = item
|
track = item
|
||||||
break
|
break
|
||||||
elif position:
|
elif position:
|
||||||
@@ -563,8 +594,8 @@ class RoutingCommands:
|
|||||||
point = pcbnew.VECTOR2I(x_nm, y_nm)
|
point = pcbnew.VECTOR2I(x_nm, y_nm)
|
||||||
|
|
||||||
# Find closest track
|
# Find closest track
|
||||||
min_distance = float('inf')
|
min_distance = float("inf")
|
||||||
for item in self.board.Tracks():
|
for item in list(self.board.Tracks()):
|
||||||
dist = self._point_to_track_distance(point, item)
|
dist = self._point_to_track_distance(point, item)
|
||||||
if dist < min_distance:
|
if dist < min_distance:
|
||||||
min_distance = dist
|
min_distance = dist
|
||||||
@@ -578,7 +609,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Track not found",
|
"message": "Track not found",
|
||||||
"errorDetails": "Could not find track with specified identifier"
|
"errorDetails": "Could not find track with specified identifier",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if it's a via (some modifications don't apply)
|
# Check if it's a via (some modifications don't apply)
|
||||||
@@ -597,7 +628,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Invalid layer",
|
"message": "Invalid layer",
|
||||||
"errorDetails": f"Layer '{new_layer}' not found"
|
"errorDetails": f"Layer '{new_layer}' not found",
|
||||||
}
|
}
|
||||||
track.SetLayer(layer_id)
|
track.SetLayer(layer_id)
|
||||||
modifications.append(f"layer={new_layer}")
|
modifications.append(f"layer={new_layer}")
|
||||||
@@ -609,7 +640,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Invalid net",
|
"message": "Invalid net",
|
||||||
"errorDetails": f"Net '{new_net}' not found"
|
"errorDetails": f"Net '{new_net}' not found",
|
||||||
}
|
}
|
||||||
track.SetNet(net)
|
track.SetNet(net)
|
||||||
modifications.append(f"net={new_net}")
|
modifications.append(f"net={new_net}")
|
||||||
@@ -618,14 +649,14 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No modifications specified",
|
"message": "No modifications specified",
|
||||||
"errorDetails": "Provide at least one of: width, layer, net"
|
"errorDetails": "Provide at least one of: width, layer, net",
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": f"Modified trace: {', '.join(modifications)}",
|
"message": f"Modified trace: {', '.join(modifications)}",
|
||||||
"uuid": str(track.m_Uuid),
|
"uuid": track.m_Uuid.AsString(),
|
||||||
"modifications": modifications
|
"modifications": modifications,
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -633,7 +664,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to modify trace",
|
"message": "Failed to modify trace",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def copy_routing_pattern(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def copy_routing_pattern(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -648,7 +679,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
source_refs = params.get("sourceRefs", []) # e.g., ["U1", "U2", "U3"]
|
source_refs = params.get("sourceRefs", []) # e.g., ["U1", "U2", "U3"]
|
||||||
@@ -660,14 +691,14 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Missing component references",
|
"message": "Missing component references",
|
||||||
"errorDetails": "Provide both 'sourceRefs' and 'targetRefs' arrays"
|
"errorDetails": "Provide both 'sourceRefs' and 'targetRefs' arrays",
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(source_refs) != len(target_refs):
|
if len(source_refs) != len(target_refs):
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Mismatched component counts",
|
"message": "Mismatched component counts",
|
||||||
"errorDetails": f"sourceRefs has {len(source_refs)} items, targetRefs has {len(target_refs)}"
|
"errorDetails": f"sourceRefs has {len(source_refs)} items, targetRefs has {len(target_refs)}",
|
||||||
}
|
}
|
||||||
|
|
||||||
scale = 1000000 # nm to mm conversion
|
scale = 1000000 # nm to mm conversion
|
||||||
@@ -681,7 +712,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Component not found",
|
"message": "Component not found",
|
||||||
"errorDetails": f"Component '{ref}' not found on board"
|
"errorDetails": f"Component '{ref}' not found on board",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Calculate offset from first source to first target component
|
# Calculate offset from first source to first target component
|
||||||
@@ -709,7 +740,7 @@ class RoutingCommands:
|
|||||||
traces_to_copy = []
|
traces_to_copy = []
|
||||||
vias_to_copy = []
|
vias_to_copy = []
|
||||||
|
|
||||||
for track in self.board.Tracks():
|
for track in list(self.board.Tracks()):
|
||||||
if track.GetNetname() not in source_nets:
|
if track.GetNetname() not in source_nets:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -731,7 +762,9 @@ class RoutingCommands:
|
|||||||
|
|
||||||
# Create new track
|
# Create new track
|
||||||
new_track = pcbnew.PCB_TRACK(self.board)
|
new_track = pcbnew.PCB_TRACK(self.board)
|
||||||
new_track.SetStart(pcbnew.VECTOR2I(start.x + offset_x, start.y + offset_y))
|
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.SetEnd(pcbnew.VECTOR2I(end.x + offset_x, end.y + offset_y))
|
||||||
new_track.SetLayer(track.GetLayer())
|
new_track.SetLayer(track.GetLayer())
|
||||||
|
|
||||||
@@ -763,15 +796,11 @@ class RoutingCommands:
|
|||||||
result = {
|
result = {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": f"Copied routing pattern: {created_traces} traces, {created_vias} vias",
|
"message": f"Copied routing pattern: {created_traces} traces, {created_vias} vias",
|
||||||
"offset": {
|
"offset": {"x": offset_x / scale, "y": offset_y / scale, "unit": "mm"},
|
||||||
"x": offset_x / scale,
|
|
||||||
"y": offset_y / scale,
|
|
||||||
"unit": "mm"
|
|
||||||
},
|
|
||||||
"createdTraces": created_traces,
|
"createdTraces": created_traces,
|
||||||
"createdVias": created_vias,
|
"createdVias": created_vias,
|
||||||
"sourceComponents": source_refs,
|
"sourceComponents": source_refs,
|
||||||
"targetComponents": target_refs
|
"targetComponents": target_refs,
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -781,7 +810,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to copy routing pattern",
|
"message": "Failed to copy routing pattern",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def create_netclass(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def create_netclass(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -791,7 +820,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
name = params.get("name")
|
name = params.get("name")
|
||||||
@@ -809,7 +838,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Missing netclass name",
|
"message": "Missing netclass name",
|
||||||
"errorDetails": "name parameter is required"
|
"errorDetails": "name parameter is required",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get net classes
|
# Get net classes
|
||||||
@@ -862,8 +891,8 @@ class RoutingCommands:
|
|||||||
"uviaDrill": netclass.GetMicroViaDrill() / scale,
|
"uviaDrill": netclass.GetMicroViaDrill() / scale,
|
||||||
"diffPairWidth": netclass.GetDiffPairWidth() / scale,
|
"diffPairWidth": netclass.GetDiffPairWidth() / scale,
|
||||||
"diffPairGap": netclass.GetDiffPairGap() / scale,
|
"diffPairGap": netclass.GetDiffPairGap() / scale,
|
||||||
"nets": nets
|
"nets": nets,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -871,7 +900,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to create net class",
|
"message": "Failed to create net class",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def add_copper_pour(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def add_copper_pour(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -881,7 +910,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
layer = params.get("layer", "F.Cu")
|
layer = params.get("layer", "F.Cu")
|
||||||
@@ -896,7 +925,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Missing points",
|
"message": "Missing points",
|
||||||
"errorDetails": "At least 3 points are required for copper pour outline"
|
"errorDetails": "At least 3 points are required for copper pour outline",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get layer ID
|
# Get layer ID
|
||||||
@@ -905,7 +934,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Invalid layer",
|
"message": "Invalid layer",
|
||||||
"errorDetails": f"Layer '{layer}' does not exist"
|
"errorDetails": f"Layer '{layer}' does not exist",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create zone
|
# Create zone
|
||||||
@@ -965,8 +994,8 @@ class RoutingCommands:
|
|||||||
"minWidth": min_width,
|
"minWidth": min_width,
|
||||||
"priority": priority,
|
"priority": priority,
|
||||||
"fillType": fill_type,
|
"fillType": fill_type,
|
||||||
"pointCount": len(points)
|
"pointCount": len(points),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -974,7 +1003,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to add copper pour",
|
"message": "Failed to add copper pour",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def route_differential_pair(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def route_differential_pair(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -984,7 +1013,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
start_pos = params.get("startPos")
|
start_pos = params.get("startPos")
|
||||||
@@ -999,7 +1028,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Missing parameters",
|
"message": "Missing parameters",
|
||||||
"errorDetails": "startPos, endPos, netPos, and netNeg are required"
|
"errorDetails": "startPos, endPos, netPos, and netNeg are required",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get layer ID
|
# Get layer ID
|
||||||
@@ -1008,7 +1037,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Invalid layer",
|
"message": "Invalid layer",
|
||||||
"errorDetails": f"Layer '{layer}' does not exist"
|
"errorDetails": f"Layer '{layer}' does not exist",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get nets
|
# Get nets
|
||||||
@@ -1022,7 +1051,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Nets not found",
|
"message": "Nets not found",
|
||||||
"errorDetails": "One or both nets specified for the differential pair do not exist"
|
"errorDetails": "One or both nets specified for the differential pair do not exist",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get start and end points
|
# Get start and end points
|
||||||
@@ -1039,7 +1068,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Invalid points",
|
"message": "Invalid points",
|
||||||
"errorDetails": "Start and end points must be different"
|
"errorDetails": "Start and end points must be different",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Normalize direction vector
|
# Normalize direction vector
|
||||||
@@ -1062,10 +1091,18 @@ class RoutingCommands:
|
|||||||
offset_y = int(py * gap_nm / 2)
|
offset_y = int(py * gap_nm / 2)
|
||||||
|
|
||||||
# Create positive and negative trace points
|
# Create positive and negative trace points
|
||||||
pos_start = pcbnew.VECTOR2I(int(start_point.x + offset_x), int(start_point.y + offset_y))
|
pos_start = pcbnew.VECTOR2I(
|
||||||
pos_end = pcbnew.VECTOR2I(int(end_point.x + offset_x), int(end_point.y + offset_y))
|
int(start_point.x + offset_x), int(start_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))
|
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
|
# Create positive trace
|
||||||
pos_track = pcbnew.PCB_TRACK(self.board)
|
pos_track = pcbnew.PCB_TRACK(self.board)
|
||||||
@@ -1105,8 +1142,8 @@ class RoutingCommands:
|
|||||||
"layer": layer,
|
"layer": layer,
|
||||||
"width": pos_track.GetWidth() / 1000000,
|
"width": pos_track.GetWidth() / 1000000,
|
||||||
"gap": gap,
|
"gap": gap,
|
||||||
"length": length / 1000000
|
"length": length / 1000000,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1114,7 +1151,7 @@ class RoutingCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to route differential pair",
|
"message": "Failed to route differential pair",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_point(self, point_spec: Dict[str, Any]) -> pcbnew.VECTOR2I:
|
def _get_point(self, point_spec: Dict[str, Any]) -> pcbnew.VECTOR2I:
|
||||||
@@ -1132,7 +1169,9 @@ class RoutingCommands:
|
|||||||
return pad.GetPosition()
|
return pad.GetPosition()
|
||||||
raise ValueError("Invalid point specification")
|
raise ValueError("Invalid point specification")
|
||||||
|
|
||||||
def _point_to_track_distance(self, point: pcbnew.VECTOR2I, track: pcbnew.PCB_TRACK) -> float:
|
def _point_to_track_distance(
|
||||||
|
self, point: pcbnew.VECTOR2I, track: pcbnew.PCB_TRACK
|
||||||
|
) -> float:
|
||||||
"""Calculate distance from point to track segment"""
|
"""Calculate distance from point to track segment"""
|
||||||
start = track.GetStart()
|
start = track.GetStart()
|
||||||
end = track.GetEnd()
|
end = track.GetEnd()
|
||||||
@@ -1156,10 +1195,7 @@ class RoutingCommands:
|
|||||||
return self._point_distance(point, end)
|
return self._point_distance(point, end)
|
||||||
|
|
||||||
# Point on line
|
# Point on line
|
||||||
proj = pcbnew.VECTOR2I(
|
proj = pcbnew.VECTOR2I(int(start.x + c2 * v.x), int(start.y + c2 * v.y))
|
||||||
int(start.x + c2 * v.x),
|
|
||||||
int(start.y + c2 * v.y)
|
|
||||||
)
|
|
||||||
return self._point_distance(point, proj)
|
return self._point_distance(point, proj)
|
||||||
|
|
||||||
def _point_distance(self, p1: pcbnew.VECTOR2I, p2: pcbnew.VECTOR2I) -> float:
|
def _point_distance(self, p1: pcbnew.VECTOR2I, p2: pcbnew.VECTOR2I) -> float:
|
||||||
|
|||||||
@@ -2,12 +2,15 @@
|
|||||||
* Component management tools for KiCAD MCP server
|
* Component management tools for KiCAD MCP server
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from 'zod';
|
import { z } from "zod";
|
||||||
import { logger } from '../logger.js';
|
import { logger } from "../logger.js";
|
||||||
|
|
||||||
// Command function type for KiCAD script calls
|
// Command function type for KiCAD script calls
|
||||||
type CommandFunction = (command: string, params: Record<string, unknown>) => Promise<any>;
|
type CommandFunction = (
|
||||||
|
command: string,
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
) => Promise<any>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register component management tools with the MCP server
|
* Register component management tools with the MCP server
|
||||||
@@ -15,8 +18,11 @@ type CommandFunction = (command: string, params: Record<string, unknown>) => Pro
|
|||||||
* @param server MCP server instance
|
* @param server MCP server instance
|
||||||
* @param callKicadScript Function to call KiCAD script commands
|
* @param callKicadScript Function to call KiCAD script commands
|
||||||
*/
|
*/
|
||||||
export function registerComponentTools(server: McpServer, callKicadScript: CommandFunction): void {
|
export function registerComponentTools(
|
||||||
logger.info('Registering component management tools');
|
server: McpServer,
|
||||||
|
callKicadScript: CommandFunction,
|
||||||
|
): void {
|
||||||
|
logger.info("Registering component management tools");
|
||||||
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
// Place Component Tool
|
// Place Component Tool
|
||||||
@@ -24,20 +30,46 @@ export function registerComponentTools(server: McpServer, callKicadScript: Comma
|
|||||||
server.tool(
|
server.tool(
|
||||||
"place_component",
|
"place_component",
|
||||||
{
|
{
|
||||||
componentId: z.string().describe("Identifier for the component to place (e.g., 'R_0603_10k')"),
|
componentId: z
|
||||||
position: z.object({
|
.string()
|
||||||
x: z.number().describe("X coordinate"),
|
.describe("Identifier for the component to place (e.g., 'R_0603_10k')"),
|
||||||
y: z.number().describe("Y coordinate"),
|
position: z
|
||||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement")
|
.object({
|
||||||
}).describe("Position coordinates and unit"),
|
x: z.number().describe("X coordinate"),
|
||||||
reference: z.string().optional().describe("Optional desired reference (e.g., 'R5')"),
|
y: z.number().describe("Y coordinate"),
|
||||||
value: z.string().optional().describe("Optional component value (e.g., '10k')"),
|
unit: z.enum(["mm", "inch"]).describe("Unit of measurement"),
|
||||||
footprint: z.string().optional().describe("Optional specific footprint name"),
|
})
|
||||||
|
.describe("Position coordinates and unit"),
|
||||||
|
reference: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Optional desired reference (e.g., 'R5')"),
|
||||||
|
value: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Optional component value (e.g., '10k')"),
|
||||||
|
footprint: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Optional specific footprint name"),
|
||||||
rotation: z.number().optional().describe("Optional rotation in degrees"),
|
rotation: z.number().optional().describe("Optional rotation in degrees"),
|
||||||
layer: z.string().optional().describe("Optional layer (e.g., 'F.Cu', 'B.SilkS')")
|
layer: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Optional layer (e.g., 'F.Cu', 'B.SilkS')"),
|
||||||
},
|
},
|
||||||
async ({ componentId, position, reference, value, footprint, rotation, layer }) => {
|
async ({
|
||||||
logger.debug(`Placing component: ${componentId} at ${position.x},${position.y} ${position.unit}`);
|
componentId,
|
||||||
|
position,
|
||||||
|
reference,
|
||||||
|
value,
|
||||||
|
footprint,
|
||||||
|
rotation,
|
||||||
|
layer,
|
||||||
|
}) => {
|
||||||
|
logger.debug(
|
||||||
|
`Placing component: ${componentId} at ${position.x},${position.y} ${position.unit}`,
|
||||||
|
);
|
||||||
const result = await callKicadScript("place_component", {
|
const result = await callKicadScript("place_component", {
|
||||||
componentId,
|
componentId,
|
||||||
position,
|
position,
|
||||||
@@ -45,16 +77,18 @@ export function registerComponentTools(server: McpServer, callKicadScript: Comma
|
|||||||
value,
|
value,
|
||||||
footprint,
|
footprint,
|
||||||
rotation,
|
rotation,
|
||||||
layer
|
layer,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: JSON.stringify(result)
|
type: "text",
|
||||||
}]
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
@@ -63,29 +97,40 @@ export function registerComponentTools(server: McpServer, callKicadScript: Comma
|
|||||||
server.tool(
|
server.tool(
|
||||||
"move_component",
|
"move_component",
|
||||||
{
|
{
|
||||||
reference: z.string().describe("Reference designator of the component (e.g., 'R5')"),
|
reference: z
|
||||||
position: z.object({
|
.string()
|
||||||
x: z.number().describe("X coordinate"),
|
.describe("Reference designator of the component (e.g., 'R5')"),
|
||||||
y: z.number().describe("Y coordinate"),
|
position: z
|
||||||
unit: z.enum(["mm", "inch"]).describe("Unit of measurement")
|
.object({
|
||||||
}).describe("New position coordinates and unit"),
|
x: z.number().describe("X coordinate"),
|
||||||
rotation: z.number().optional().describe("Optional new rotation in degrees")
|
y: z.number().describe("Y coordinate"),
|
||||||
|
unit: z.enum(["mm", "inch"]).describe("Unit of measurement"),
|
||||||
|
})
|
||||||
|
.describe("New position coordinates and unit"),
|
||||||
|
rotation: z
|
||||||
|
.number()
|
||||||
|
.optional()
|
||||||
|
.describe("Optional new rotation in degrees"),
|
||||||
},
|
},
|
||||||
async ({ reference, position, rotation }) => {
|
async ({ reference, position, rotation }) => {
|
||||||
logger.debug(`Moving component: ${reference} to ${position.x},${position.y} ${position.unit}`);
|
logger.debug(
|
||||||
|
`Moving component: ${reference} to ${position.x},${position.y} ${position.unit}`,
|
||||||
|
);
|
||||||
const result = await callKicadScript("move_component", {
|
const result = await callKicadScript("move_component", {
|
||||||
reference,
|
reference,
|
||||||
position,
|
position,
|
||||||
rotation
|
rotation,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: JSON.stringify(result)
|
type: "text",
|
||||||
}]
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
@@ -94,23 +139,29 @@ export function registerComponentTools(server: McpServer, callKicadScript: Comma
|
|||||||
server.tool(
|
server.tool(
|
||||||
"rotate_component",
|
"rotate_component",
|
||||||
{
|
{
|
||||||
reference: z.string().describe("Reference designator of the component (e.g., 'R5')"),
|
reference: z
|
||||||
angle: z.number().describe("Rotation angle in degrees (absolute, not relative)")
|
.string()
|
||||||
|
.describe("Reference designator of the component (e.g., 'R5')"),
|
||||||
|
angle: z
|
||||||
|
.number()
|
||||||
|
.describe("Rotation angle in degrees (absolute, not relative)"),
|
||||||
},
|
},
|
||||||
async ({ reference, angle }) => {
|
async ({ reference, angle }) => {
|
||||||
logger.debug(`Rotating component: ${reference} to ${angle} degrees`);
|
logger.debug(`Rotating component: ${reference} to ${angle} degrees`);
|
||||||
const result = await callKicadScript("rotate_component", {
|
const result = await callKicadScript("rotate_component", {
|
||||||
reference,
|
reference,
|
||||||
angle
|
angle,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: JSON.stringify(result)
|
type: "text",
|
||||||
}]
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
@@ -119,19 +170,25 @@ export function registerComponentTools(server: McpServer, callKicadScript: Comma
|
|||||||
server.tool(
|
server.tool(
|
||||||
"delete_component",
|
"delete_component",
|
||||||
{
|
{
|
||||||
reference: z.string().describe("Reference designator of the component to delete (e.g., 'R5')")
|
reference: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
"Reference designator of the component to delete (e.g., 'R5')",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
async ({ reference }) => {
|
async ({ reference }) => {
|
||||||
logger.debug(`Deleting component: ${reference}`);
|
logger.debug(`Deleting component: ${reference}`);
|
||||||
const result = await callKicadScript("delete_component", { reference });
|
const result = await callKicadScript("delete_component", { reference });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: JSON.stringify(result)
|
type: "text",
|
||||||
}]
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
@@ -140,10 +197,15 @@ export function registerComponentTools(server: McpServer, callKicadScript: Comma
|
|||||||
server.tool(
|
server.tool(
|
||||||
"edit_component",
|
"edit_component",
|
||||||
{
|
{
|
||||||
reference: z.string().describe("Reference designator of the component (e.g., 'R5')"),
|
reference: z
|
||||||
newReference: z.string().optional().describe("Optional new reference designator"),
|
.string()
|
||||||
|
.describe("Reference designator of the component (e.g., 'R5')"),
|
||||||
|
newReference: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Optional new reference designator"),
|
||||||
value: z.string().optional().describe("Optional new component value"),
|
value: z.string().optional().describe("Optional new component value"),
|
||||||
footprint: z.string().optional().describe("Optional new footprint")
|
footprint: z.string().optional().describe("Optional new footprint"),
|
||||||
},
|
},
|
||||||
async ({ reference, newReference, value, footprint }) => {
|
async ({ reference, newReference, value, footprint }) => {
|
||||||
logger.debug(`Editing component: ${reference}`);
|
logger.debug(`Editing component: ${reference}`);
|
||||||
@@ -151,16 +213,18 @@ export function registerComponentTools(server: McpServer, callKicadScript: Comma
|
|||||||
reference,
|
reference,
|
||||||
newReference,
|
newReference,
|
||||||
value,
|
value,
|
||||||
footprint
|
footprint,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: JSON.stringify(result)
|
type: "text",
|
||||||
}]
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
@@ -169,20 +233,30 @@ export function registerComponentTools(server: McpServer, callKicadScript: Comma
|
|||||||
server.tool(
|
server.tool(
|
||||||
"find_component",
|
"find_component",
|
||||||
{
|
{
|
||||||
reference: z.string().optional().describe("Reference designator to search for"),
|
reference: z
|
||||||
value: z.string().optional().describe("Component value to search for")
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Reference designator to search for"),
|
||||||
|
value: z.string().optional().describe("Component value to search for"),
|
||||||
},
|
},
|
||||||
async ({ reference, value }) => {
|
async ({ reference, value }) => {
|
||||||
logger.debug(`Finding component with ${reference ? `reference: ${reference}` : `value: ${value}`}`);
|
logger.debug(
|
||||||
const result = await callKicadScript("find_component", { reference, value });
|
`Finding component with ${reference ? `reference: ${reference}` : `value: ${value}`}`,
|
||||||
|
);
|
||||||
|
const result = await callKicadScript("find_component", {
|
||||||
|
reference,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: JSON.stringify(result)
|
type: "text",
|
||||||
}]
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
@@ -191,19 +265,25 @@ export function registerComponentTools(server: McpServer, callKicadScript: Comma
|
|||||||
server.tool(
|
server.tool(
|
||||||
"get_component_properties",
|
"get_component_properties",
|
||||||
{
|
{
|
||||||
reference: z.string().describe("Reference designator of the component (e.g., 'R5')")
|
reference: z
|
||||||
|
.string()
|
||||||
|
.describe("Reference designator of the component (e.g., 'R5')"),
|
||||||
},
|
},
|
||||||
async ({ reference }) => {
|
async ({ reference }) => {
|
||||||
logger.debug(`Getting properties for component: ${reference}`);
|
logger.debug(`Getting properties for component: ${reference}`);
|
||||||
const result = await callKicadScript("get_component_properties", { reference });
|
const result = await callKicadScript("get_component_properties", {
|
||||||
|
reference,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: JSON.stringify(result)
|
type: "text",
|
||||||
}]
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
@@ -212,25 +292,32 @@ export function registerComponentTools(server: McpServer, callKicadScript: Comma
|
|||||||
server.tool(
|
server.tool(
|
||||||
"add_component_annotation",
|
"add_component_annotation",
|
||||||
{
|
{
|
||||||
reference: z.string().describe("Reference designator of the component (e.g., 'R5')"),
|
reference: z
|
||||||
|
.string()
|
||||||
|
.describe("Reference designator of the component (e.g., 'R5')"),
|
||||||
annotation: z.string().describe("Annotation or comment text to add"),
|
annotation: z.string().describe("Annotation or comment text to add"),
|
||||||
visible: z.boolean().optional().describe("Whether the annotation should be visible on the PCB")
|
visible: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe("Whether the annotation should be visible on the PCB"),
|
||||||
},
|
},
|
||||||
async ({ reference, annotation, visible }) => {
|
async ({ reference, annotation, visible }) => {
|
||||||
logger.debug(`Adding annotation to component: ${reference}`);
|
logger.debug(`Adding annotation to component: ${reference}`);
|
||||||
const result = await callKicadScript("add_component_annotation", {
|
const result = await callKicadScript("add_component_annotation", {
|
||||||
reference,
|
reference,
|
||||||
annotation,
|
annotation,
|
||||||
visible
|
visible,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: JSON.stringify(result)
|
type: "text",
|
||||||
}]
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
@@ -239,23 +326,29 @@ export function registerComponentTools(server: McpServer, callKicadScript: Comma
|
|||||||
server.tool(
|
server.tool(
|
||||||
"group_components",
|
"group_components",
|
||||||
{
|
{
|
||||||
references: z.array(z.string()).describe("Reference designators of components to group"),
|
references: z
|
||||||
groupName: z.string().describe("Name for the component group")
|
.array(z.string())
|
||||||
|
.describe("Reference designators of components to group"),
|
||||||
|
groupName: z.string().describe("Name for the component group"),
|
||||||
},
|
},
|
||||||
async ({ references, groupName }) => {
|
async ({ references, groupName }) => {
|
||||||
logger.debug(`Grouping components: ${references.join(', ')} as ${groupName}`);
|
logger.debug(
|
||||||
|
`Grouping components: ${references.join(", ")} as ${groupName}`,
|
||||||
|
);
|
||||||
const result = await callKicadScript("group_components", {
|
const result = await callKicadScript("group_components", {
|
||||||
references,
|
references,
|
||||||
groupName
|
groupName,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: JSON.stringify(result)
|
type: "text",
|
||||||
}]
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
@@ -264,10 +357,12 @@ export function registerComponentTools(server: McpServer, callKicadScript: Comma
|
|||||||
server.tool(
|
server.tool(
|
||||||
"replace_component",
|
"replace_component",
|
||||||
{
|
{
|
||||||
reference: z.string().describe("Reference designator of the component to replace"),
|
reference: z
|
||||||
|
.string()
|
||||||
|
.describe("Reference designator of the component to replace"),
|
||||||
newComponentId: z.string().describe("ID of the new component to use"),
|
newComponentId: z.string().describe("ID of the new component to use"),
|
||||||
newFootprint: z.string().optional().describe("Optional new footprint"),
|
newFootprint: z.string().optional().describe("Optional new footprint"),
|
||||||
newValue: z.string().optional().describe("Optional new component value")
|
newValue: z.string().optional().describe("Optional new component value"),
|
||||||
},
|
},
|
||||||
async ({ reference, newComponentId, newFootprint, newValue }) => {
|
async ({ reference, newComponentId, newFootprint, newValue }) => {
|
||||||
logger.debug(`Replacing component: ${reference} with ${newComponentId}`);
|
logger.debug(`Replacing component: ${reference} with ${newComponentId}`);
|
||||||
@@ -275,17 +370,275 @@ export function registerComponentTools(server: McpServer, callKicadScript: Comma
|
|||||||
reference,
|
reference,
|
||||||
newComponentId,
|
newComponentId,
|
||||||
newFootprint,
|
newFootprint,
|
||||||
newValue
|
newValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: JSON.stringify(result)
|
type: "text",
|
||||||
}]
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info('Component management tools registered');
|
// ------------------------------------------------------
|
||||||
|
// Get Component Pads Tool
|
||||||
|
// ------------------------------------------------------
|
||||||
|
server.tool(
|
||||||
|
"get_component_pads",
|
||||||
|
{
|
||||||
|
reference: z
|
||||||
|
.string()
|
||||||
|
.describe("Reference designator of the component (e.g., 'U1')"),
|
||||||
|
unit: z
|
||||||
|
.enum(["mm", "inch"])
|
||||||
|
.optional()
|
||||||
|
.describe("Unit for coordinates (default: mm)"),
|
||||||
|
},
|
||||||
|
async ({ reference, unit }) => {
|
||||||
|
logger.debug(`Getting pads for component: ${reference}`);
|
||||||
|
const result = await callKicadScript("get_component_pads", {
|
||||||
|
reference,
|
||||||
|
unit: unit || "mm",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// Get Component List Tool
|
||||||
|
// ------------------------------------------------------
|
||||||
|
server.tool(
|
||||||
|
"get_component_list",
|
||||||
|
{
|
||||||
|
layer: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Filter by layer (e.g., 'F.Cu', 'B.Cu')"),
|
||||||
|
boundingBox: z
|
||||||
|
.object({
|
||||||
|
x1: z.number(),
|
||||||
|
y1: z.number(),
|
||||||
|
x2: z.number(),
|
||||||
|
y2: z.number(),
|
||||||
|
unit: z.enum(["mm", "inch"]).optional(),
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.describe("Filter by bounding box region"),
|
||||||
|
unit: z
|
||||||
|
.enum(["mm", "inch"])
|
||||||
|
.optional()
|
||||||
|
.describe("Unit for coordinates (default: mm)"),
|
||||||
|
},
|
||||||
|
async ({ layer, boundingBox, unit }) => {
|
||||||
|
logger.debug("Getting component list");
|
||||||
|
const result = await callKicadScript("get_component_list", {
|
||||||
|
layer,
|
||||||
|
boundingBox,
|
||||||
|
unit: unit || "mm",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// Get Pad Position Tool
|
||||||
|
// ------------------------------------------------------
|
||||||
|
server.tool(
|
||||||
|
"get_pad_position",
|
||||||
|
{
|
||||||
|
reference: z
|
||||||
|
.string()
|
||||||
|
.describe("Component reference designator (e.g., 'U1')"),
|
||||||
|
pad: z.string().describe("Pad number or name (e.g., '1', 'A1')"),
|
||||||
|
unit: z
|
||||||
|
.enum(["mm", "inch"])
|
||||||
|
.optional()
|
||||||
|
.describe("Unit for coordinates (default: mm)"),
|
||||||
|
},
|
||||||
|
async ({ reference, pad, unit }) => {
|
||||||
|
logger.debug(`Getting pad position for ${reference} pad ${pad}`);
|
||||||
|
const result = await callKicadScript("get_pad_position", {
|
||||||
|
reference,
|
||||||
|
pad,
|
||||||
|
unit: unit || "mm",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// Place Component Array Tool
|
||||||
|
// ------------------------------------------------------
|
||||||
|
server.tool(
|
||||||
|
"place_component_array",
|
||||||
|
{
|
||||||
|
componentId: z.string().describe("Component identifier"),
|
||||||
|
startPosition: z
|
||||||
|
.object({
|
||||||
|
x: z.number(),
|
||||||
|
y: z.number(),
|
||||||
|
unit: z.enum(["mm", "inch"]),
|
||||||
|
})
|
||||||
|
.describe("Starting position"),
|
||||||
|
rows: z.number().describe("Number of rows"),
|
||||||
|
columns: z.number().describe("Number of columns"),
|
||||||
|
rowSpacing: z.number().describe("Spacing between rows"),
|
||||||
|
columnSpacing: z.number().describe("Spacing between columns"),
|
||||||
|
startReference: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Starting reference (e.g., 'R1')"),
|
||||||
|
footprint: z.string().optional().describe("Footprint name"),
|
||||||
|
value: z.string().optional().describe("Component value"),
|
||||||
|
rotation: z.number().optional().describe("Rotation in degrees"),
|
||||||
|
},
|
||||||
|
async ({
|
||||||
|
componentId,
|
||||||
|
startPosition,
|
||||||
|
rows,
|
||||||
|
columns,
|
||||||
|
rowSpacing,
|
||||||
|
columnSpacing,
|
||||||
|
startReference,
|
||||||
|
footprint,
|
||||||
|
value,
|
||||||
|
rotation,
|
||||||
|
}) => {
|
||||||
|
logger.debug(
|
||||||
|
`Placing component array: ${rows}x${columns} of ${componentId}`,
|
||||||
|
);
|
||||||
|
const result = await callKicadScript("place_component_array", {
|
||||||
|
componentId,
|
||||||
|
startPosition,
|
||||||
|
rows,
|
||||||
|
columns,
|
||||||
|
rowSpacing,
|
||||||
|
columnSpacing,
|
||||||
|
startReference,
|
||||||
|
footprint,
|
||||||
|
value,
|
||||||
|
rotation,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// Align Components Tool
|
||||||
|
// ------------------------------------------------------
|
||||||
|
server.tool(
|
||||||
|
"align_components",
|
||||||
|
{
|
||||||
|
references: z
|
||||||
|
.array(z.string())
|
||||||
|
.describe("Array of component references to align"),
|
||||||
|
alignmentType: z
|
||||||
|
.enum(["horizontal", "vertical", "grid"])
|
||||||
|
.describe("Type of alignment"),
|
||||||
|
spacing: z
|
||||||
|
.number()
|
||||||
|
.optional()
|
||||||
|
.describe("Spacing between components in mm"),
|
||||||
|
referenceComponent: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Reference component for alignment"),
|
||||||
|
},
|
||||||
|
async ({ references, alignmentType, spacing, referenceComponent }) => {
|
||||||
|
logger.debug(`Aligning components: ${references.join(", ")}`);
|
||||||
|
const result = await callKicadScript("align_components", {
|
||||||
|
references,
|
||||||
|
alignmentType,
|
||||||
|
spacing,
|
||||||
|
referenceComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// Duplicate Component Tool
|
||||||
|
// ------------------------------------------------------
|
||||||
|
server.tool(
|
||||||
|
"duplicate_component",
|
||||||
|
{
|
||||||
|
reference: z.string().describe("Reference of component to duplicate"),
|
||||||
|
offset: z
|
||||||
|
.object({
|
||||||
|
x: z.number(),
|
||||||
|
y: z.number(),
|
||||||
|
unit: z.enum(["mm", "inch"]).optional(),
|
||||||
|
})
|
||||||
|
.describe("Offset from original position"),
|
||||||
|
newReference: z.string().optional().describe("New reference designator"),
|
||||||
|
count: z
|
||||||
|
.number()
|
||||||
|
.optional()
|
||||||
|
.describe("Number of duplicates (default: 1)"),
|
||||||
|
},
|
||||||
|
async ({ reference, offset, newReference, count }) => {
|
||||||
|
logger.debug(`Duplicating component: ${reference}`);
|
||||||
|
const result = await callKicadScript("duplicate_component", {
|
||||||
|
reference,
|
||||||
|
offset,
|
||||||
|
newReference,
|
||||||
|
count,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info("Component management tools registered");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,22 @@
|
|||||||
* Provides access to KiCAD footprint libraries and symbols
|
* Provides access to KiCAD footprint libraries and symbols
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from 'zod';
|
import { z } from "zod";
|
||||||
|
|
||||||
export function registerLibraryTools(server: McpServer, callKicadScript: Function) {
|
export function registerLibraryTools(
|
||||||
|
server: McpServer,
|
||||||
|
callKicadScript: Function,
|
||||||
|
) {
|
||||||
// List available footprint libraries
|
// List available footprint libraries
|
||||||
server.tool(
|
server.tool(
|
||||||
"list_libraries",
|
"list_libraries",
|
||||||
"List all available KiCAD footprint libraries",
|
"List all available KiCAD footprint libraries",
|
||||||
{
|
{
|
||||||
search_paths: z.array(z.string()).optional()
|
search_paths: z
|
||||||
.describe("Optional additional search paths for libraries")
|
.array(z.string())
|
||||||
|
.optional()
|
||||||
|
.describe("Optional additional search paths for libraries"),
|
||||||
},
|
},
|
||||||
async (args: { search_paths?: string[] }) => {
|
async (args: { search_paths?: string[] }) => {
|
||||||
const result = await callKicadScript("list_libraries", args);
|
const result = await callKicadScript("list_libraries", args);
|
||||||
@@ -22,20 +27,20 @@ export function registerLibraryTools(server: McpServer, callKicadScript: Functio
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Found ${result.libraries.length} footprint libraries:\n${result.libraries.join('\n')}`
|
text: `Found ${result.libraries.length} footprint libraries:\n${result.libraries.join("\n")}`,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Failed to list libraries: ${result.message || 'Unknown error'}`
|
text: `Failed to list libraries: ${result.message || "Unknown error"}`,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Search for footprints across all libraries
|
// Search for footprints across all libraries
|
||||||
@@ -43,37 +48,50 @@ export function registerLibraryTools(server: McpServer, callKicadScript: Functio
|
|||||||
"search_footprints",
|
"search_footprints",
|
||||||
"Search for footprints matching a pattern across all libraries",
|
"Search for footprints matching a pattern across all libraries",
|
||||||
{
|
{
|
||||||
search_term: z.string()
|
search_term: z
|
||||||
|
.string()
|
||||||
.describe("Search term or pattern to match footprint names"),
|
.describe("Search term or pattern to match footprint names"),
|
||||||
library: z.string().optional()
|
library: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
.describe("Optional specific library to search in"),
|
.describe("Optional specific library to search in"),
|
||||||
limit: z.number().optional().default(50)
|
limit: z
|
||||||
.describe("Maximum number of results to return")
|
.number()
|
||||||
|
.optional()
|
||||||
|
.default(50)
|
||||||
|
.describe("Maximum number of results to return"),
|
||||||
},
|
},
|
||||||
async (args: { search_term: string; library?: string; limit?: number }) => {
|
async (args: { search_term: string; library?: string; limit?: number }) => {
|
||||||
const result = await callKicadScript("search_footprints", args);
|
const result = await callKicadScript("search_footprints", {
|
||||||
|
pattern: args.search_term,
|
||||||
|
library: args.library,
|
||||||
|
limit: args.limit,
|
||||||
|
});
|
||||||
if (result.success && result.footprints) {
|
if (result.success && result.footprints) {
|
||||||
const footprintList = result.footprints.map((fp: any) =>
|
const footprintList = result.footprints
|
||||||
`${fp.library}:${fp.name}${fp.description ? ' - ' + fp.description : ''}`
|
.map(
|
||||||
).join('\n');
|
(fp: any) =>
|
||||||
|
`${fp.full_name || fp.library + ":" + fp.footprint}${fp.description ? " - " + fp.description : ""}`,
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Found ${result.footprints.length} matching footprints:\n${footprintList}`
|
text: `Found ${result.footprints.length} matching footprints:\n${footprintList}`,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Failed to search footprints: ${result.message || 'Unknown error'}`
|
text: `Failed to search footprints: ${result.message || "Unknown error"}`,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// List footprints in a specific library
|
// List footprints in a specific library
|
||||||
@@ -81,35 +99,43 @@ export function registerLibraryTools(server: McpServer, callKicadScript: Functio
|
|||||||
"list_library_footprints",
|
"list_library_footprints",
|
||||||
"List all footprints in a specific KiCAD library",
|
"List all footprints in a specific KiCAD library",
|
||||||
{
|
{
|
||||||
library_name: z.string()
|
library_name: z
|
||||||
|
.string()
|
||||||
.describe("Name of the library to list footprints from"),
|
.describe("Name of the library to list footprints from"),
|
||||||
filter: z.string().optional()
|
filter: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
.describe("Optional filter pattern for footprint names"),
|
.describe("Optional filter pattern for footprint names"),
|
||||||
limit: z.number().optional().default(100)
|
limit: z
|
||||||
.describe("Maximum number of footprints to list")
|
.number()
|
||||||
|
.optional()
|
||||||
|
.default(100)
|
||||||
|
.describe("Maximum number of footprints to list"),
|
||||||
},
|
},
|
||||||
async (args: { library_name: string; filter?: string; limit?: number }) => {
|
async (args: { library_name: string; filter?: string; limit?: number }) => {
|
||||||
const result = await callKicadScript("list_library_footprints", args);
|
const result = await callKicadScript("list_library_footprints", args);
|
||||||
if (result.success && result.footprints) {
|
if (result.success && result.footprints) {
|
||||||
const footprintList = result.footprints.map((fp: string) => ` - ${fp}`).join('\n');
|
const footprintList = result.footprints
|
||||||
|
.map((fp: string) => ` - ${fp}`)
|
||||||
|
.join("\n");
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Library ${args.library_name} contains ${result.footprints.length} footprints:\n${footprintList}`
|
text: `Library ${args.library_name} contains ${result.footprints.length} footprints:\n${footprintList}`,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Failed to list footprints in library ${args.library_name}: ${result.message || 'Unknown error'}`
|
text: `Failed to list footprints in library ${args.library_name}: ${result.message || "Unknown error"}`,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get detailed information about a specific footprint
|
// Get detailed information about a specific footprint
|
||||||
@@ -117,10 +143,12 @@ export function registerLibraryTools(server: McpServer, callKicadScript: Functio
|
|||||||
"get_footprint_info",
|
"get_footprint_info",
|
||||||
"Get detailed information about a specific footprint",
|
"Get detailed information about a specific footprint",
|
||||||
{
|
{
|
||||||
library_name: z.string()
|
library_name: z
|
||||||
|
.string()
|
||||||
.describe("Name of the library containing the footprint"),
|
.describe("Name of the library containing the footprint"),
|
||||||
footprint_name: z.string()
|
footprint_name: z
|
||||||
.describe("Name of the footprint to get information about")
|
.string()
|
||||||
|
.describe("Name of the footprint to get information about"),
|
||||||
},
|
},
|
||||||
async (args: { library_name: string; footprint_name: string }) => {
|
async (args: { library_name: string; footprint_name: string }) => {
|
||||||
const result = await callKicadScript("get_footprint_info", args);
|
const result = await callKicadScript("get_footprint_info", args);
|
||||||
@@ -129,31 +157,37 @@ export function registerLibraryTools(server: McpServer, callKicadScript: Functio
|
|||||||
const details = [
|
const details = [
|
||||||
`Footprint: ${info.name}`,
|
`Footprint: ${info.name}`,
|
||||||
`Library: ${info.library}`,
|
`Library: ${info.library}`,
|
||||||
info.description ? `Description: ${info.description}` : '',
|
info.description ? `Description: ${info.description}` : "",
|
||||||
info.keywords ? `Keywords: ${info.keywords}` : '',
|
info.keywords ? `Keywords: ${info.keywords}` : "",
|
||||||
info.pads ? `Number of pads: ${info.pads}` : '',
|
info.pads ? `Number of pads: ${info.pads}` : "",
|
||||||
info.layers ? `Layers used: ${info.layers.join(', ')}` : '',
|
info.layers ? `Layers used: ${info.layers.join(", ")}` : "",
|
||||||
info.courtyard ? `Courtyard size: ${info.courtyard.width}mm x ${info.courtyard.height}mm` : '',
|
info.courtyard
|
||||||
info.attributes ? `Attributes: ${JSON.stringify(info.attributes)}` : ''
|
? `Courtyard size: ${info.courtyard.width}mm x ${info.courtyard.height}mm`
|
||||||
].filter(line => line).join('\n');
|
: "",
|
||||||
|
info.attributes
|
||||||
|
? `Attributes: ${JSON.stringify(info.attributes)}`
|
||||||
|
: "",
|
||||||
|
]
|
||||||
|
.filter((line) => line)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: details
|
text: details,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Failed to get footprint info: ${result.message || 'Unknown error'}`
|
text: `Failed to get footprint info: ${result.message || "Unknown error"}`,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,13 @@
|
|||||||
* Routing tools for KiCAD MCP server
|
* Routing tools for KiCAD MCP server
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from 'zod';
|
import { z } from "zod";
|
||||||
|
|
||||||
export function registerRoutingTools(server: McpServer, callKicadScript: Function) {
|
export function registerRoutingTools(
|
||||||
|
server: McpServer,
|
||||||
|
callKicadScript: Function,
|
||||||
|
) {
|
||||||
// Add net tool
|
// Add net tool
|
||||||
server.tool(
|
server.tool(
|
||||||
"add_net",
|
"add_net",
|
||||||
@@ -17,12 +20,14 @@ export function registerRoutingTools(server: McpServer, callKicadScript: Functio
|
|||||||
async (args: { name: string; netClass?: string }) => {
|
async (args: { name: string; netClass?: string }) => {
|
||||||
const result = await callKicadScript("add_net", args);
|
const result = await callKicadScript("add_net", args);
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: JSON.stringify(result, null, 2)
|
type: "text",
|
||||||
}]
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Route trace tool
|
// Route trace tool
|
||||||
@@ -30,16 +35,20 @@ export function registerRoutingTools(server: McpServer, callKicadScript: Functio
|
|||||||
"route_trace",
|
"route_trace",
|
||||||
"Route a trace between two points",
|
"Route a trace between two points",
|
||||||
{
|
{
|
||||||
start: z.object({
|
start: z
|
||||||
x: z.number(),
|
.object({
|
||||||
y: z.number(),
|
x: z.number(),
|
||||||
unit: z.string().optional()
|
y: z.number(),
|
||||||
}).describe("Start position"),
|
unit: z.string().optional(),
|
||||||
end: z.object({
|
})
|
||||||
x: z.number(),
|
.describe("Start position"),
|
||||||
y: z.number(),
|
end: z
|
||||||
unit: z.string().optional()
|
.object({
|
||||||
}).describe("End position"),
|
x: z.number(),
|
||||||
|
y: z.number(),
|
||||||
|
unit: z.string().optional(),
|
||||||
|
})
|
||||||
|
.describe("End position"),
|
||||||
layer: z.string().describe("PCB layer"),
|
layer: z.string().describe("PCB layer"),
|
||||||
width: z.number().describe("Trace width in mm"),
|
width: z.number().describe("Trace width in mm"),
|
||||||
net: z.string().describe("Net name"),
|
net: z.string().describe("Net name"),
|
||||||
@@ -47,12 +56,14 @@ export function registerRoutingTools(server: McpServer, callKicadScript: Functio
|
|||||||
async (args: any) => {
|
async (args: any) => {
|
||||||
const result = await callKicadScript("route_trace", args);
|
const result = await callKicadScript("route_trace", args);
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: JSON.stringify(result, null, 2)
|
type: "text",
|
||||||
}]
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add via tool
|
// Add via tool
|
||||||
@@ -60,23 +71,30 @@ export function registerRoutingTools(server: McpServer, callKicadScript: Functio
|
|||||||
"add_via",
|
"add_via",
|
||||||
"Add a via to the PCB",
|
"Add a via to the PCB",
|
||||||
{
|
{
|
||||||
position: z.object({
|
position: z
|
||||||
x: z.number(),
|
.object({
|
||||||
y: z.number(),
|
x: z.number(),
|
||||||
unit: z.string().optional()
|
y: z.number(),
|
||||||
}).describe("Via position"),
|
unit: z.string().optional(),
|
||||||
|
})
|
||||||
|
.describe("Via position"),
|
||||||
net: z.string().describe("Net name"),
|
net: z.string().describe("Net name"),
|
||||||
viaType: z.string().optional().describe("Via type (through, blind, buried)"),
|
viaType: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Via type (through, blind, buried)"),
|
||||||
},
|
},
|
||||||
async (args: any) => {
|
async (args: any) => {
|
||||||
const result = await callKicadScript("add_via", args);
|
const result = await callKicadScript("add_via", args);
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: JSON.stringify(result, null, 2)
|
type: "text",
|
||||||
}]
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add copper pour tool
|
// Add copper pour tool
|
||||||
@@ -91,11 +109,216 @@ export function registerRoutingTools(server: McpServer, callKicadScript: Functio
|
|||||||
async (args: any) => {
|
async (args: any) => {
|
||||||
const result = await callKicadScript("add_copper_pour", args);
|
const result = await callKicadScript("add_copper_pour", args);
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: JSON.stringify(result, null, 2)
|
type: "text",
|
||||||
}]
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete trace tool
|
||||||
|
server.tool(
|
||||||
|
"delete_trace",
|
||||||
|
"Delete traces from the PCB. Can delete by UUID, position, or bulk-delete all traces on a net.",
|
||||||
|
{
|
||||||
|
traceUuid: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("UUID of a specific trace to delete"),
|
||||||
|
position: z
|
||||||
|
.object({
|
||||||
|
x: z.number(),
|
||||||
|
y: z.number(),
|
||||||
|
unit: z.enum(["mm", "inch"]).optional(),
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.describe("Delete trace nearest to this position"),
|
||||||
|
net: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Delete all traces on this net (bulk delete)"),
|
||||||
|
layer: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Filter by layer when using net-based deletion"),
|
||||||
|
includeVias: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe("Include vias in net-based deletion"),
|
||||||
|
},
|
||||||
|
async (args: any) => {
|
||||||
|
const result = await callKicadScript("delete_trace", args);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Query traces tool
|
||||||
|
server.tool(
|
||||||
|
"query_traces",
|
||||||
|
"Query traces on the board with optional filters by net, layer, or bounding box.",
|
||||||
|
{
|
||||||
|
net: z.string().optional().describe("Filter by net name"),
|
||||||
|
layer: z.string().optional().describe("Filter by layer name"),
|
||||||
|
boundingBox: z
|
||||||
|
.object({
|
||||||
|
x1: z.number(),
|
||||||
|
y1: z.number(),
|
||||||
|
x2: z.number(),
|
||||||
|
y2: z.number(),
|
||||||
|
unit: z.enum(["mm", "inch"]).optional(),
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.describe("Filter by bounding box region"),
|
||||||
|
unit: z.enum(["mm", "inch"]).optional().describe("Unit for coordinates"),
|
||||||
|
},
|
||||||
|
async (args: any) => {
|
||||||
|
const result = await callKicadScript("query_traces", args);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get nets list tool
|
||||||
|
server.tool(
|
||||||
|
"get_nets_list",
|
||||||
|
"Get a list of all nets in the PCB with optional statistics.",
|
||||||
|
{
|
||||||
|
includeStats: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe("Include statistics (track count, total length, etc.)"),
|
||||||
|
unit: z
|
||||||
|
.enum(["mm", "inch"])
|
||||||
|
.optional()
|
||||||
|
.describe("Unit for length measurements"),
|
||||||
|
},
|
||||||
|
async (args: any) => {
|
||||||
|
const result = await callKicadScript("get_nets_list", args);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Modify trace tool
|
||||||
|
server.tool(
|
||||||
|
"modify_trace",
|
||||||
|
"Modify an existing trace (change width, layer, or net).",
|
||||||
|
{
|
||||||
|
traceUuid: z.string().describe("UUID of the trace to modify"),
|
||||||
|
width: z.number().optional().describe("New trace width in mm"),
|
||||||
|
layer: z.string().optional().describe("New layer name"),
|
||||||
|
net: z.string().optional().describe("New net name"),
|
||||||
|
},
|
||||||
|
async (args: any) => {
|
||||||
|
const result = await callKicadScript("modify_trace", args);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create netclass tool
|
||||||
|
server.tool(
|
||||||
|
"create_netclass",
|
||||||
|
"Create a new net class with custom design rules.",
|
||||||
|
{
|
||||||
|
name: z.string().describe("Net class name"),
|
||||||
|
traceWidth: z.number().optional().describe("Default trace width in mm"),
|
||||||
|
clearance: z.number().optional().describe("Clearance in mm"),
|
||||||
|
viaDiameter: z.number().optional().describe("Via diameter in mm"),
|
||||||
|
viaDrill: z.number().optional().describe("Via drill size in mm"),
|
||||||
|
},
|
||||||
|
async (args: any) => {
|
||||||
|
const result = await callKicadScript("create_netclass", args);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Route differential pair tool
|
||||||
|
server.tool(
|
||||||
|
"route_differential_pair",
|
||||||
|
"Route a differential pair between two sets of points.",
|
||||||
|
{
|
||||||
|
positivePad: z
|
||||||
|
.object({
|
||||||
|
reference: z.string(),
|
||||||
|
pad: z.string(),
|
||||||
|
})
|
||||||
|
.describe("Positive pad (component and pad number)"),
|
||||||
|
negativePad: z
|
||||||
|
.object({
|
||||||
|
reference: z.string(),
|
||||||
|
pad: z.string(),
|
||||||
|
})
|
||||||
|
.describe("Negative pad (component and pad number)"),
|
||||||
|
layer: z.string().describe("PCB layer"),
|
||||||
|
width: z.number().describe("Trace width in mm"),
|
||||||
|
gap: z.number().describe("Gap between traces in mm"),
|
||||||
|
positiveNet: z.string().describe("Positive net name"),
|
||||||
|
negativeNet: z.string().describe("Negative net name"),
|
||||||
|
},
|
||||||
|
async (args: any) => {
|
||||||
|
const result = await callKicadScript("route_differential_pair", args);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Refill zones tool
|
||||||
|
server.tool(
|
||||||
|
"refill_zones",
|
||||||
|
"Refill all copper zones on the board. WARNING: SWIG path has known segfault risk (see KNOWN_ISSUES.md). Prefer using IPC backend (KiCAD open) or triggering zone fill via KiCAD UI instead.",
|
||||||
|
{},
|
||||||
|
async (args: any) => {
|
||||||
|
const result = await callKicadScript("refill_zones", args);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user