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:
@@ -7,7 +7,8 @@ import pcbnew
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, List, Tuple
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
logger = logging.getLogger("kicad_interface")
|
||||
|
||||
|
||||
class DesignRuleCommands:
|
||||
"""Handles design rule checking and configuration"""
|
||||
@@ -23,7 +24,7 @@ class DesignRuleCommands:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
"errorDetails": "Load or create a board first",
|
||||
}
|
||||
|
||||
design_settings = self.board.GetDesignSettings()
|
||||
@@ -57,9 +58,13 @@ class DesignRuleCommands:
|
||||
|
||||
# Set micro via settings (use properties - methods removed in KiCAD 9.0)
|
||||
if "microViaDiameter" in params:
|
||||
design_settings.m_MicroViasMinSize = int(params["microViaDiameter"] * scale)
|
||||
design_settings.m_MicroViasMinSize = int(
|
||||
params["microViaDiameter"] * scale
|
||||
)
|
||||
if "microViaDrill" in params:
|
||||
design_settings.m_MicroViasMinDrill = int(params["microViaDrill"] * scale)
|
||||
design_settings.m_MicroViasMinDrill = int(
|
||||
params["microViaDrill"] * scale
|
||||
)
|
||||
|
||||
# Set minimum values
|
||||
if "minTrackWidth" in params:
|
||||
@@ -72,13 +77,19 @@ class DesignRuleCommands:
|
||||
design_settings.m_MinThroughDrill = int(params["minViaDrill"] * scale)
|
||||
|
||||
if "minMicroViaDiameter" in params:
|
||||
design_settings.m_MicroViasMinSize = int(params["minMicroViaDiameter"] * scale)
|
||||
design_settings.m_MicroViasMinSize = int(
|
||||
params["minMicroViaDiameter"] * scale
|
||||
)
|
||||
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
|
||||
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
|
||||
if "holeClearance" in params:
|
||||
@@ -102,13 +113,13 @@ class DesignRuleCommands:
|
||||
"minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale,
|
||||
"holeClearance": design_settings.m_HoleClearance / scale,
|
||||
"holeToHoleMin": design_settings.m_HoleToHoleMin / scale,
|
||||
"viasMinAnnularWidth": design_settings.m_ViasMinAnnularWidth / scale
|
||||
"viasMinAnnularWidth": design_settings.m_ViasMinAnnularWidth / scale,
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Updated design rules",
|
||||
"rules": response_rules
|
||||
"rules": response_rules,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@@ -116,7 +127,7 @@ class DesignRuleCommands:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to set design rules",
|
||||
"errorDetails": str(e)
|
||||
"errorDetails": str(e),
|
||||
}
|
||||
|
||||
def get_design_rules(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
@@ -126,7 +137,7 @@ class DesignRuleCommands:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
"errorDetails": "Load or create a board first",
|
||||
}
|
||||
|
||||
design_settings = self.board.GetDesignSettings()
|
||||
@@ -138,42 +149,34 @@ class DesignRuleCommands:
|
||||
"clearance": design_settings.m_MinClearance / scale,
|
||||
"trackWidth": design_settings.GetCurrentTrackWidth() / scale,
|
||||
"minTrackWidth": design_settings.m_TrackMinWidth / scale,
|
||||
|
||||
# Via settings (current values from methods)
|
||||
"viaDiameter": design_settings.GetCurrentViaSize() / scale,
|
||||
"viaDrill": design_settings.GetCurrentViaDrill() / scale,
|
||||
|
||||
# Via minimum values
|
||||
"minViaDiameter": design_settings.m_ViasMinSize / scale,
|
||||
"viasMinAnnularWidth": design_settings.m_ViasMinAnnularWidth / scale,
|
||||
|
||||
# Micro via settings
|
||||
"microViaDiameter": design_settings.m_MicroViasMinSize / scale,
|
||||
"microViaDrill": design_settings.m_MicroViasMinDrill / scale,
|
||||
"minMicroViaDiameter": design_settings.m_MicroViasMinSize / scale,
|
||||
"minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale,
|
||||
|
||||
# KiCAD 9.0: Hole and drill settings (replaces removed m_ViasMinDrill and m_MinHoleDiameter)
|
||||
"minThroughDrill": design_settings.m_MinThroughDrill / scale,
|
||||
"holeClearance": design_settings.m_HoleClearance / scale,
|
||||
"holeToHoleMin": design_settings.m_HoleToHoleMin / scale,
|
||||
|
||||
# Other constraints
|
||||
"copperEdgeClearance": design_settings.m_CopperEdgeClearance / scale,
|
||||
"silkClearance": design_settings.m_SilkClearance / scale,
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"rules": rules
|
||||
}
|
||||
return {"success": True, "rules": rules}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting design rules: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to get design rules",
|
||||
"errorDetails": str(e)
|
||||
"errorDetails": str(e),
|
||||
}
|
||||
|
||||
def run_drc(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
@@ -189,7 +192,7 @@ class DesignRuleCommands:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
"errorDetails": "Load or create a board first",
|
||||
}
|
||||
|
||||
report_path = params.get("reportPath")
|
||||
@@ -200,7 +203,7 @@ class DesignRuleCommands:
|
||||
return {
|
||||
"success": False,
|
||||
"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
|
||||
@@ -209,23 +212,28 @@ class DesignRuleCommands:
|
||||
return {
|
||||
"success": False,
|
||||
"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
|
||||
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
|
||||
|
||||
try:
|
||||
# Build command
|
||||
cmd = [
|
||||
kicad_cli,
|
||||
'pcb',
|
||||
'drc',
|
||||
'--format', 'json',
|
||||
'--output', json_output,
|
||||
'--units', 'mm',
|
||||
board_file
|
||||
"pcb",
|
||||
"drc",
|
||||
"--format",
|
||||
"json",
|
||||
"--output",
|
||||
json_output,
|
||||
"--units",
|
||||
"mm",
|
||||
board_file,
|
||||
]
|
||||
|
||||
logger.info(f"Running DRC command: {' '.join(cmd)}")
|
||||
@@ -235,7 +243,7 @@ class DesignRuleCommands:
|
||||
cmd,
|
||||
capture_output=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:
|
||||
@@ -243,32 +251,34 @@ class DesignRuleCommands:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "DRC command failed",
|
||||
"errorDetails": result.stderr
|
||||
"errorDetails": result.stderr,
|
||||
}
|
||||
|
||||
# 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)
|
||||
|
||||
# Parse violations from kicad-cli output
|
||||
violations = []
|
||||
violation_counts = {}
|
||||
violation_counts: dict[str, int] = {}
|
||||
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")
|
||||
vseverity = violation.get("severity", "error")
|
||||
|
||||
violations.append({
|
||||
"type": vtype,
|
||||
"severity": vseverity,
|
||||
"message": violation.get("description", ""),
|
||||
"location": {
|
||||
"x": violation.get("x", 0),
|
||||
"y": violation.get("y", 0),
|
||||
"unit": "mm"
|
||||
violations.append(
|
||||
{
|
||||
"type": vtype,
|
||||
"severity": vseverity,
|
||||
"message": violation.get("description", ""),
|
||||
"location": {
|
||||
"x": violation.get("x", 0),
|
||||
"y": violation.get("y", 0),
|
||||
"unit": "mm",
|
||||
},
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
# Count violations by type
|
||||
violation_counts[vtype] = violation_counts.get(vtype, 0) + 1
|
||||
@@ -280,30 +290,39 @@ class DesignRuleCommands:
|
||||
# Determine where to save the violations file
|
||||
board_dir = os.path.dirname(board_file)
|
||||
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)
|
||||
with open(violations_file, 'w', encoding='utf-8') as f:
|
||||
json.dump({
|
||||
"board": board_file,
|
||||
"timestamp": drc_data.get("date", "unknown"),
|
||||
"total_violations": len(violations),
|
||||
"violation_counts": violation_counts,
|
||||
"severity_counts": severity_counts,
|
||||
"violations": violations
|
||||
}, f, indent=2)
|
||||
with open(violations_file, "w", encoding="utf-8") as f:
|
||||
json.dump(
|
||||
{
|
||||
"board": board_file,
|
||||
"timestamp": drc_data.get("date", "unknown"),
|
||||
"total_violations": len(violations),
|
||||
"violation_counts": violation_counts,
|
||||
"severity_counts": severity_counts,
|
||||
"violations": violations,
|
||||
},
|
||||
f,
|
||||
indent=2,
|
||||
)
|
||||
|
||||
# Save text report if requested
|
||||
if report_path:
|
||||
report_path = os.path.abspath(os.path.expanduser(report_path))
|
||||
cmd_report = [
|
||||
kicad_cli,
|
||||
'pcb',
|
||||
'drc',
|
||||
'--format', 'report',
|
||||
'--output', report_path,
|
||||
'--units', 'mm',
|
||||
board_file
|
||||
"pcb",
|
||||
"drc",
|
||||
"--format",
|
||||
"report",
|
||||
"--output",
|
||||
report_path,
|
||||
"--units",
|
||||
"mm",
|
||||
board_file,
|
||||
]
|
||||
subprocess.run(cmd_report, capture_output=True, timeout=600)
|
||||
|
||||
@@ -314,10 +333,10 @@ class DesignRuleCommands:
|
||||
"summary": {
|
||||
"total": len(violations),
|
||||
"by_severity": severity_counts,
|
||||
"by_type": violation_counts
|
||||
"by_type": violation_counts,
|
||||
},
|
||||
"violationsFile": violations_file,
|
||||
"reportPath": report_path if report_path else None
|
||||
"reportPath": report_path if report_path else None,
|
||||
}
|
||||
|
||||
finally:
|
||||
@@ -330,14 +349,14 @@ class DesignRuleCommands:
|
||||
return {
|
||||
"success": False,
|
||||
"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:
|
||||
logger.error(f"Error running DRC: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to run DRC",
|
||||
"errorDetails": str(e)
|
||||
"errorDetails": str(e),
|
||||
}
|
||||
|
||||
def _find_kicad_cli(self) -> Optional[str]:
|
||||
@@ -399,7 +418,7 @@ class DesignRuleCommands:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
"errorDetails": "Load or create a board first",
|
||||
}
|
||||
|
||||
severity = params.get("severity", "all")
|
||||
@@ -416,25 +435,27 @@ class DesignRuleCommands:
|
||||
return {
|
||||
"success": False,
|
||||
"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
|
||||
with open(violations_file, 'r', encoding='utf-8') as f:
|
||||
with open(violations_file, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
all_violations = data.get("violations", [])
|
||||
|
||||
# Filter by severity if specified
|
||||
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:
|
||||
filtered_violations = all_violations
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"violations": filtered_violations,
|
||||
"violationsFile": violations_file # Include file path for reference
|
||||
"violationsFile": violations_file, # Include file path for reference
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@@ -442,5 +463,5 @@ class DesignRuleCommands:
|
||||
return {
|
||||
"success": False,
|
||||
"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
|
||||
import glob
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
logger = logging.getLogger("kicad_interface")
|
||||
|
||||
|
||||
class LibraryManager:
|
||||
@@ -85,7 +85,7 @@ class LibraryManager:
|
||||
)
|
||||
"""
|
||||
try:
|
||||
with open(table_path, 'r') as f:
|
||||
with open(table_path, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
# Simple regex-based parser for lib entries
|
||||
@@ -103,7 +103,9 @@ class LibraryManager:
|
||||
self.libraries[nickname] = resolved_uri
|
||||
logger.debug(f" Found library: {nickname} -> {resolved_uri}")
|
||||
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:
|
||||
logger.error(f"Error parsing fp-lib-table at {table_path}: {e}")
|
||||
@@ -124,23 +126,23 @@ class LibraryManager:
|
||||
|
||||
# Common KiCAD environment variables
|
||||
env_vars = {
|
||||
'KICAD9_FOOTPRINT_DIR': self._find_kicad_footprint_dir(),
|
||||
'KICAD8_FOOTPRINT_DIR': self._find_kicad_footprint_dir(),
|
||||
'KICAD_FOOTPRINT_DIR': self._find_kicad_footprint_dir(),
|
||||
'KISYSMOD': self._find_kicad_footprint_dir(),
|
||||
'KICAD9_3RD_PARTY': self._find_kicad_3rdparty_dir(),
|
||||
'KICAD8_3RD_PARTY': self._find_kicad_3rdparty_dir(),
|
||||
"KICAD9_FOOTPRINT_DIR": self._find_kicad_footprint_dir(),
|
||||
"KICAD8_FOOTPRINT_DIR": self._find_kicad_footprint_dir(),
|
||||
"KICAD_FOOTPRINT_DIR": self._find_kicad_footprint_dir(),
|
||||
"KISYSMOD": self._find_kicad_footprint_dir(),
|
||||
"KICAD9_3RD_PARTY": self._find_kicad_3rdparty_dir(),
|
||||
"KICAD8_3RD_PARTY": self._find_kicad_3rdparty_dir(),
|
||||
}
|
||||
|
||||
# Project directory
|
||||
if self.project_path:
|
||||
env_vars['KIPRJMOD'] = str(self.project_path)
|
||||
env_vars["KIPRJMOD"] = str(self.project_path)
|
||||
|
||||
# Replace environment variables
|
||||
for var, value in env_vars.items():
|
||||
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
|
||||
resolved = os.path.expanduser(resolved)
|
||||
@@ -167,10 +169,10 @@ class LibraryManager:
|
||||
]
|
||||
|
||||
# Also check environment variable
|
||||
if 'KICAD9_FOOTPRINT_DIR' in os.environ:
|
||||
possible_paths.insert(0, os.environ['KICAD9_FOOTPRINT_DIR'])
|
||||
if 'KICAD8_FOOTPRINT_DIR' in os.environ:
|
||||
possible_paths.insert(0, os.environ['KICAD8_FOOTPRINT_DIR'])
|
||||
if "KICAD9_FOOTPRINT_DIR" in os.environ:
|
||||
possible_paths.insert(0, os.environ["KICAD9_FOOTPRINT_DIR"])
|
||||
if "KICAD8_FOOTPRINT_DIR" in os.environ:
|
||||
possible_paths.insert(0, os.environ["KICAD8_FOOTPRINT_DIR"])
|
||||
|
||||
for path in possible_paths:
|
||||
if os.path.isdir(path):
|
||||
@@ -190,26 +192,36 @@ class LibraryManager:
|
||||
import json
|
||||
|
||||
# 1. Check shell environment variable first
|
||||
if 'KICAD9_3RD_PARTY' in os.environ:
|
||||
path = os.environ['KICAD9_3RD_PARTY']
|
||||
if "KICAD9_3RD_PARTY" in os.environ:
|
||||
path = os.environ["KICAD9_3RD_PARTY"]
|
||||
if os.path.isdir(path):
|
||||
return path
|
||||
|
||||
# 2. Check kicad_common.json for user-defined variables
|
||||
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() / "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:
|
||||
if config_path.exists():
|
||||
try:
|
||||
with open(config_path, 'r') as f:
|
||||
with open(config_path, "r") as f:
|
||||
config = json.load(f)
|
||||
env_vars = config.get('environment', {}).get('vars', {})
|
||||
if env_vars and 'KICAD9_3RD_PARTY' in env_vars:
|
||||
path = env_vars['KICAD9_3RD_PARTY']
|
||||
env_vars = config.get("environment", {}).get("vars", {})
|
||||
if env_vars and "KICAD9_3RD_PARTY" in env_vars:
|
||||
path = env_vars["KICAD9_3RD_PARTY"]
|
||||
if os.path.isdir(path):
|
||||
return path
|
||||
except (json.JSONDecodeError, KeyError, TypeError):
|
||||
@@ -231,10 +243,10 @@ class LibraryManager:
|
||||
Path.home() / "Documents" / "KiCad" / version / "3rdparty",
|
||||
]
|
||||
|
||||
for path in possible_paths:
|
||||
if path.exists():
|
||||
logger.info(f"Found KiCad 3rd party directory: {path}")
|
||||
return str(path)
|
||||
for candidate in possible_paths:
|
||||
if candidate.exists():
|
||||
logger.info(f"Found KiCad 3rd party directory: {candidate}")
|
||||
return str(candidate)
|
||||
|
||||
logger.warning("Could not find KiCad 3rd party directory")
|
||||
return None
|
||||
@@ -325,7 +337,9 @@ class LibraryManager:
|
||||
for library_nickname, library_path in self.libraries.items():
|
||||
fp_file = Path(library_path) / f"{footprint_name}.kicad_mod"
|
||||
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)
|
||||
|
||||
logger.warning(f"Footprint not found in any library: {footprint_name}")
|
||||
@@ -354,18 +368,22 @@ class LibraryManager:
|
||||
|
||||
for footprint in footprints:
|
||||
if regex.search(footprint.lower()):
|
||||
results.append({
|
||||
'library': library_nickname,
|
||||
'footprint': footprint,
|
||||
'full_name': f"{library_nickname}:{footprint}"
|
||||
})
|
||||
results.append(
|
||||
{
|
||||
"library": library_nickname,
|
||||
"footprint": footprint,
|
||||
"full_name": f"{library_nickname}:{footprint}",
|
||||
}
|
||||
)
|
||||
|
||||
if len(results) >= limit:
|
||||
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
|
||||
|
||||
@@ -385,11 +403,11 @@ class LibraryManager:
|
||||
return None
|
||||
|
||||
return {
|
||||
'library': library_nickname,
|
||||
'footprint': footprint_name,
|
||||
'full_name': f"{library_nickname}:{footprint_name}",
|
||||
'path': str(fp_file),
|
||||
'library_path': library_path
|
||||
"library": library_nickname,
|
||||
"footprint": footprint_name,
|
||||
"full_name": f"{library_nickname}:{footprint_name}",
|
||||
"path": str(fp_file),
|
||||
"library_path": library_path,
|
||||
}
|
||||
|
||||
|
||||
@@ -404,39 +422,48 @@ class LibraryCommands:
|
||||
"""List all available footprint libraries"""
|
||||
try:
|
||||
libraries = self.library_manager.list_libraries()
|
||||
return {
|
||||
"success": True,
|
||||
"libraries": libraries,
|
||||
"count": len(libraries)
|
||||
}
|
||||
return {"success": True, "libraries": libraries, "count": len(libraries)}
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing libraries: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to list libraries",
|
||||
"errorDetails": str(e)
|
||||
"errorDetails": str(e),
|
||||
}
|
||||
|
||||
def search_footprints(self, params: Dict) -> Dict:
|
||||
"""Search for footprints by pattern"""
|
||||
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)
|
||||
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 {
|
||||
"success": True,
|
||||
"footprints": results,
|
||||
"count": len(results),
|
||||
"pattern": pattern
|
||||
"pattern": pattern,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching footprints: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to search footprints",
|
||||
"errorDetails": str(e)
|
||||
"errorDetails": str(e),
|
||||
}
|
||||
|
||||
def list_library_footprints(self, params: Dict) -> Dict:
|
||||
@@ -444,10 +471,7 @@ class LibraryCommands:
|
||||
try:
|
||||
library = params.get("library")
|
||||
if not library:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing library parameter"
|
||||
}
|
||||
return {"success": False, "message": "Missing library parameter"}
|
||||
|
||||
footprints = self.library_manager.list_footprints(library)
|
||||
|
||||
@@ -455,14 +479,14 @@ class LibraryCommands:
|
||||
"success": True,
|
||||
"library": library,
|
||||
"footprints": footprints,
|
||||
"count": len(footprints)
|
||||
"count": len(footprints),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing library footprints: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to list library footprints",
|
||||
"errorDetails": str(e)
|
||||
"errorDetails": str(e),
|
||||
}
|
||||
|
||||
def get_footprint_info(self, params: Dict) -> Dict:
|
||||
@@ -470,10 +494,7 @@ class LibraryCommands:
|
||||
try:
|
||||
footprint_spec = params.get("footprint")
|
||||
if not footprint_spec:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing footprint parameter"
|
||||
}
|
||||
return {"success": False, "message": "Missing footprint parameter"}
|
||||
|
||||
# Try to find the footprint
|
||||
result = self.library_manager.find_footprint(footprint_spec)
|
||||
@@ -491,17 +512,14 @@ class LibraryCommands:
|
||||
"library": library_nickname,
|
||||
"footprint": footprint_name,
|
||||
"full_name": f"{library_nickname}:{footprint_name}",
|
||||
"library_path": library_path
|
||||
"library_path": library_path,
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"footprint_info": info
|
||||
}
|
||||
return {"success": True, "footprint_info": info}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Footprint not found: {footprint_spec}"
|
||||
"message": f"Footprint not found: {footprint_spec}",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@@ -509,5 +527,5 @@ class LibraryCommands:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to get footprint info",
|
||||
"errorDetails": str(e)
|
||||
"errorDetails": str(e),
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user