Merge pull request #1 from Kletternaut/fix/template-kicad9-format
Bug fixes + new tool, discovered via live testing on a real JLCPCB/KiCAD 9 project. ### Changes 1. **Format version bump**: KiCAD 7 → KiCAD 9 (`20230121` → `20250114`) MCP server targets KiCAD 9 exclusively. Every newly created schematic showed spurious "older version" warning. 2. **Remove corrupt `_TEMPLATE_*` placed-symbol blocks** (`lib_id -100` crash) 13 blocks with `(lib_id -100)` caused KiCAD null-pointer crash on box-select. Same sexpdata serializer root cause as PR #40. 3. **Fix footprint param silently ignored** in `add_schematic_component` The `footprint` field was never passed through the call chain to the f-string in `DynamicSymbolLoader`. 4. **Fix `delete_schematic_component` only deleting first duplicate** `break` after first match replaced with full scan + back-to-front deletion. Response now includes `deleted_count`. 5. **New tool: `edit_schematic_component`** Update `footprint`, `value`, and/or `reference` in-place on an existing placed symbol.
This commit is contained in:
47
CHANGELOG.md
47
CHANGELOG.md
@@ -2,6 +2,47 @@
|
|||||||
|
|
||||||
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.1-alpha] - 2026-02-28
|
||||||
|
|
||||||
|
### New MCP Tools
|
||||||
|
|
||||||
|
- `edit_schematic_component` – Update properties of a placed symbol in-place (footprint,
|
||||||
|
value, reference rename). More efficient than delete + re-add: preserves position and UUID.
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- `add_schematic_component`: `footprint` parameter was accepted but silently ignored – the
|
||||||
|
value was never passed through to `DynamicSymbolLoader.add_component()` /
|
||||||
|
`create_component_instance()`. All newly placed symbols always had an empty Footprint
|
||||||
|
field. Fix: added `footprint: str = ""` to both functions and threaded it through every
|
||||||
|
call site including the TypeScript tool schema.
|
||||||
|
|
||||||
|
- `delete_schematic_component`: only deleted the first matching instance when duplicate
|
||||||
|
references existed (e.g. after an aborted add attempt). Root cause: loop used `break`
|
||||||
|
after the first match. Fix: collect all matching blocks first, then delete them all back-
|
||||||
|
to-front (to preserve line indices). Response now includes `deleted_count`.
|
||||||
|
|
||||||
|
- `templates/*.kicad_sch`, `project.py`, `schematic.py`: Update KiCAD schematic format
|
||||||
|
version from `20230121` (KiCAD 7) to `20250114` (KiCAD 9). The MCP server targets
|
||||||
|
KiCAD 9 exclusively (`pcbnew.pyd` compiled for KiCAD 9.0, Python 3.11.5) – generating
|
||||||
|
files in an outdated format caused a spurious "This file was created with an older
|
||||||
|
KiCAD version" warning on every newly created schematic.
|
||||||
|
|
||||||
|
- `template_with_symbols_expanded.kicad_sch`: Remove 13 corrupt `_TEMPLATE_*` placed-symbol
|
||||||
|
blocks with `(lib_id -100)` – an integer caused by old sexpdata serializer (same bug
|
||||||
|
PR #40 fixed for the add path). KiCAD crashed with a null-pointer when selecting these
|
||||||
|
symbols. They appeared as grey `_TEMPLATE_R?`, `_TEMPLATE_U_REG?` etc. labels far
|
||||||
|
outside the sheet boundary (~5000mm off-sheet).
|
||||||
|
|
||||||
|
**Discovered via:** live testing on a real JLCPCB/KiCAD 9 project.
|
||||||
|
**Affected users:** schematics created from this template before this fix contain the
|
||||||
|
same corrupt blocks – remove all `(symbol (lib_id -100) ...)` blocks whose Reference
|
||||||
|
starts with `_TEMPLATE_`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [2.2.0-alpha] - 2026-02-27
|
## [2.2.0-alpha] - 2026-02-27
|
||||||
|
|
||||||
### New MCP Tools (TypeScript layer – previously Python-only)
|
### New MCP Tools (TypeScript layer – previously Python-only)
|
||||||
@@ -36,7 +77,7 @@ All notable changes to the KiCAD MCP Server project are documented here.
|
|||||||
- `library.py`: Fix loop variable shadowing `Path` object (mypy)
|
- `library.py`: Fix loop variable shadowing `Path` object (mypy)
|
||||||
- `design_rules.py`: Add type annotation for `violation_counts` (mypy)
|
- `design_rules.py`: Add type annotation for `violation_counts` (mypy)
|
||||||
|
|
||||||
### Pending additions (not yet committed)
|
### New MCP Tools (cont.)
|
||||||
|
|
||||||
**Datasheet tools:**
|
**Datasheet tools:**
|
||||||
- `get_datasheet_url` - Return LCSC datasheet PDF URL and product page URL for a given
|
- `get_datasheet_url` - Return LCSC datasheet PDF URL and product page URL for a given
|
||||||
@@ -53,7 +94,7 @@ All notable changes to the KiCAD MCP Server project are documented here.
|
|||||||
- `delete_schematic_component` - Remove a placed symbol from a `.kicad_sch` file by
|
- `delete_schematic_component` - Remove a placed symbol from a `.kicad_sch` file by
|
||||||
reference designator (e.g. `R1`, `U3`).
|
reference designator (e.g. `R1`, `U3`).
|
||||||
|
|
||||||
### Bug Fixes (pending)
|
### Bug Fixes (cont.)
|
||||||
|
|
||||||
- `schematic.ts` / `kicad_interface.py`: Fix missing `delete_schematic_component` MCP tool.
|
- `schematic.ts` / `kicad_interface.py`: Fix missing `delete_schematic_component` MCP tool.
|
||||||
|
|
||||||
@@ -75,7 +116,7 @@ All notable changes to the KiCAD MCP Server project are documented here.
|
|||||||
- Error message explicitly guides the user when the wrong tool is used:
|
- Error message explicitly guides the user when the wrong tool is used:
|
||||||
*"note: this tool removes schematic symbols, use delete_component for PCB footprints"*
|
*"note: this tool removes schematic symbols, use delete_component for PCB footprints"*
|
||||||
|
|
||||||
### Pending fixes (not yet committed)
|
### Additional Bug Fixes
|
||||||
|
|
||||||
- `connection_schematic.py` / `kicad_interface.py`: Fix `generate_netlist` missing
|
- `connection_schematic.py` / `kicad_interface.py`: Fix `generate_netlist` missing
|
||||||
`schematic_path` parameter – without it `get_net_connections` always fell back to
|
`schematic_path` parameter – without it `get_net_connections` always fell back to
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import logging
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
logger = logging.getLogger('kicad_interface')
|
logger = logging.getLogger("kicad_interface")
|
||||||
|
|
||||||
|
|
||||||
class DynamicSymbolLoader:
|
class DynamicSymbolLoader:
|
||||||
@@ -43,7 +43,7 @@ class DynamicSymbolLoader:
|
|||||||
Path.home() / ".local" / "share" / "kicad" / "9.0" / "symbols",
|
Path.home() / ".local" / "share" / "kicad" / "9.0" / "symbols",
|
||||||
Path.home() / "Documents" / "KiCad" / "9.0" / "3rdparty" / "symbols",
|
Path.home() / "Documents" / "KiCad" / "9.0" / "3rdparty" / "symbols",
|
||||||
]
|
]
|
||||||
for env_var in ['KICAD9_SYMBOL_DIR', 'KICAD8_SYMBOL_DIR', 'KICAD_SYMBOL_DIR']:
|
for env_var in ["KICAD9_SYMBOL_DIR", "KICAD8_SYMBOL_DIR", "KICAD_SYMBOL_DIR"]:
|
||||||
if env_var in os.environ:
|
if env_var in os.environ:
|
||||||
possible_paths.insert(0, Path(os.environ[env_var]))
|
possible_paths.insert(0, Path(os.environ[env_var]))
|
||||||
|
|
||||||
@@ -63,13 +63,14 @@ class DynamicSymbolLoader:
|
|||||||
Extract a complete symbol block from a library or schematic file by matching
|
Extract a complete symbol block from a library or schematic file by matching
|
||||||
parentheses depth. Returns the raw text of the symbol definition.
|
parentheses depth. Returns the raw text of the symbol definition.
|
||||||
"""
|
"""
|
||||||
lines = text.split('\n')
|
lines = text.split("\n")
|
||||||
start = None
|
start = None
|
||||||
for i, line in enumerate(lines):
|
for i, line in enumerate(lines):
|
||||||
stripped = line.strip()
|
stripped = line.strip()
|
||||||
# Match exact symbol name (not sub-symbols like Name_0_1)
|
# Match exact symbol name (not sub-symbols like Name_0_1)
|
||||||
if stripped.startswith(f'(symbol "{symbol_name}"') and \
|
if stripped.startswith(f'(symbol "{symbol_name}"') and not re.match(
|
||||||
not re.match(r'.*_\d+_\d+"', stripped):
|
r'.*_\d+_\d+"', stripped
|
||||||
|
):
|
||||||
start = i
|
start = i
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -80,9 +81,9 @@ class DynamicSymbolLoader:
|
|||||||
end = None
|
end = None
|
||||||
for i in range(start, len(lines)):
|
for i in range(start, len(lines)):
|
||||||
for ch in lines[i]:
|
for ch in lines[i]:
|
||||||
if ch == '(':
|
if ch == "(":
|
||||||
depth += 1
|
depth += 1
|
||||||
elif ch == ')':
|
elif ch == ")":
|
||||||
depth -= 1
|
depth -= 1
|
||||||
if depth == 0:
|
if depth == 0:
|
||||||
end = i
|
end = i
|
||||||
@@ -93,9 +94,11 @@ class DynamicSymbolLoader:
|
|||||||
if end is None:
|
if end is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return '\n'.join(lines[start:end + 1])
|
return "\n".join(lines[start : end + 1])
|
||||||
|
|
||||||
def extract_symbol_from_library(self, library_name: str, symbol_name: str) -> Optional[str]:
|
def extract_symbol_from_library(
|
||||||
|
self, library_name: str, symbol_name: str
|
||||||
|
) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Extract a symbol definition from a KiCad .kicad_sym library file.
|
Extract a symbol definition from a KiCad .kicad_sym library file.
|
||||||
Returns the raw text block, ready to be injected into a schematic.
|
Returns the raw text block, ready to be injected into a schematic.
|
||||||
@@ -112,12 +115,14 @@ class DynamicSymbolLoader:
|
|||||||
if not lib_path:
|
if not lib_path:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
with open(lib_path, 'r', encoding='utf-8') as f:
|
with open(lib_path, "r", encoding="utf-8") as f:
|
||||||
lib_content = f.read()
|
lib_content = f.read()
|
||||||
|
|
||||||
block = self._extract_symbol_block(lib_content, symbol_name)
|
block = self._extract_symbol_block(lib_content, symbol_name)
|
||||||
if block is None:
|
if block is None:
|
||||||
logger.warning(f"Symbol '{symbol_name}' not found in {library_name}.kicad_sym")
|
logger.warning(
|
||||||
|
f"Symbol '{symbol_name}' not found in {library_name}.kicad_sym"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Check if this symbol uses (extends "ParentName")
|
# Check if this symbol uses (extends "ParentName")
|
||||||
@@ -125,14 +130,16 @@ class DynamicSymbolLoader:
|
|||||||
parent_block = None
|
parent_block = None
|
||||||
if extends_match:
|
if extends_match:
|
||||||
parent_name = extends_match.group(1)
|
parent_name = extends_match.group(1)
|
||||||
logger.info(f"Symbol {symbol_name} extends {parent_name}, extracting parent too")
|
logger.info(
|
||||||
|
f"Symbol {symbol_name} extends {parent_name}, extracting parent too"
|
||||||
|
)
|
||||||
parent_block = self._extract_symbol_block(lib_content, parent_name)
|
parent_block = self._extract_symbol_block(lib_content, parent_name)
|
||||||
if parent_block:
|
if parent_block:
|
||||||
# Prefix parent top-level name with library
|
# Prefix parent top-level name with library
|
||||||
parent_block = parent_block.replace(
|
parent_block = parent_block.replace(
|
||||||
f'(symbol "{parent_name}"',
|
f'(symbol "{parent_name}"',
|
||||||
f'(symbol "{library_name}:{parent_name}"',
|
f'(symbol "{library_name}:{parent_name}"',
|
||||||
1 # Only first occurrence (top-level)
|
1, # Only first occurrence (top-level)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Prefix top-level symbol name with library
|
# Prefix top-level symbol name with library
|
||||||
@@ -140,13 +147,13 @@ class DynamicSymbolLoader:
|
|||||||
block = block.replace(
|
block = block.replace(
|
||||||
f'(symbol "{symbol_name}"',
|
f'(symbol "{symbol_name}"',
|
||||||
f'(symbol "{full_name}"',
|
f'(symbol "{full_name}"',
|
||||||
1 # Only first occurrence (top-level)
|
1, # Only first occurrence (top-level)
|
||||||
)
|
)
|
||||||
# Sub-symbols like "Name_0_1" keep their short names (already correct from library)
|
# Sub-symbols like "Name_0_1" keep their short names (already correct from library)
|
||||||
|
|
||||||
# Combine parent + child if extends is used
|
# Combine parent + child if extends is used
|
||||||
if parent_block:
|
if parent_block:
|
||||||
result = parent_block + '\n' + block
|
result = parent_block + "\n" + block
|
||||||
else:
|
else:
|
||||||
result = block
|
result = block
|
||||||
|
|
||||||
@@ -154,14 +161,16 @@ class DynamicSymbolLoader:
|
|||||||
logger.info(f"Extracted symbol {full_name} ({len(result)} chars)")
|
logger.info(f"Extracted symbol {full_name} ({len(result)} chars)")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def inject_symbol_into_schematic(self, schematic_path: Path, library_name: str, symbol_name: str) -> bool:
|
def inject_symbol_into_schematic(
|
||||||
|
self, schematic_path: Path, library_name: str, symbol_name: str
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Inject a symbol definition into a schematic's lib_symbols section.
|
Inject a symbol definition into a schematic's lib_symbols section.
|
||||||
Uses text manipulation to preserve file formatting.
|
Uses text manipulation to preserve file formatting.
|
||||||
"""
|
"""
|
||||||
full_name = f"{library_name}:{symbol_name}"
|
full_name = f"{library_name}:{symbol_name}"
|
||||||
|
|
||||||
with open(schematic_path, 'r', encoding='utf-8') as f:
|
with open(schematic_path, "r", encoding="utf-8") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
# Check if symbol already exists
|
# Check if symbol already exists
|
||||||
@@ -172,36 +181,38 @@ class DynamicSymbolLoader:
|
|||||||
# Extract symbol from library
|
# Extract symbol from library
|
||||||
symbol_block = self.extract_symbol_from_library(library_name, symbol_name)
|
symbol_block = self.extract_symbol_from_library(library_name, symbol_name)
|
||||||
if not symbol_block:
|
if not symbol_block:
|
||||||
raise ValueError(f"Symbol '{symbol_name}' not found in library '{library_name}'")
|
raise ValueError(
|
||||||
|
f"Symbol '{symbol_name}' not found in library '{library_name}'"
|
||||||
|
)
|
||||||
|
|
||||||
# Indent the block to match lib_symbols indentation (4 spaces for top-level)
|
# Indent the block to match lib_symbols indentation (4 spaces for top-level)
|
||||||
indented_lines = []
|
indented_lines = []
|
||||||
for line in symbol_block.split('\n'):
|
for line in symbol_block.split("\n"):
|
||||||
# Add 4-space indent for the content inside lib_symbols
|
# Add 4-space indent for the content inside lib_symbols
|
||||||
indented_lines.append(' ' + line if line.strip() else line)
|
indented_lines.append(" " + line if line.strip() else line)
|
||||||
indented_block = '\n'.join(indented_lines)
|
indented_block = "\n".join(indented_lines)
|
||||||
|
|
||||||
# Find the end of lib_symbols section to insert before closing )
|
# Find the end of lib_symbols section to insert before closing )
|
||||||
lines = content.split('\n')
|
lines = content.split("\n")
|
||||||
lib_sym_start = None
|
lib_sym_start = None
|
||||||
lib_sym_end = None
|
lib_sym_end = None
|
||||||
depth = 0
|
depth = 0
|
||||||
|
|
||||||
for i, line in enumerate(lines):
|
for i, line in enumerate(lines):
|
||||||
if '(lib_symbols' in line and lib_sym_start is None:
|
if "(lib_symbols" in line and lib_sym_start is None:
|
||||||
lib_sym_start = i
|
lib_sym_start = i
|
||||||
depth = 0
|
depth = 0
|
||||||
for ch in line:
|
for ch in line:
|
||||||
if ch == '(':
|
if ch == "(":
|
||||||
depth += 1
|
depth += 1
|
||||||
elif ch == ')':
|
elif ch == ")":
|
||||||
depth -= 1
|
depth -= 1
|
||||||
continue
|
continue
|
||||||
if lib_sym_start is not None and lib_sym_end is None:
|
if lib_sym_start is not None and lib_sym_end is None:
|
||||||
for ch in line:
|
for ch in line:
|
||||||
if ch == '(':
|
if ch == "(":
|
||||||
depth += 1
|
depth += 1
|
||||||
elif ch == ')':
|
elif ch == ")":
|
||||||
depth -= 1
|
depth -= 1
|
||||||
if depth == 0:
|
if depth == 0:
|
||||||
lib_sym_end = i
|
lib_sym_end = i
|
||||||
@@ -215,15 +226,29 @@ class DynamicSymbolLoader:
|
|||||||
# Insert the symbol block just before the closing ) of lib_symbols
|
# Insert the symbol block just before the closing ) of lib_symbols
|
||||||
lines.insert(lib_sym_end, indented_block)
|
lines.insert(lib_sym_end, indented_block)
|
||||||
|
|
||||||
with open(schematic_path, 'w', encoding='utf-8') as f:
|
with open(schematic_path, "w", encoding="utf-8") as f:
|
||||||
f.write('\n'.join(lines))
|
f.write("\n".join(lines))
|
||||||
|
|
||||||
logger.info(f"Injected symbol {full_name} into {schematic_path.name}")
|
# Handle both Path objects and strings
|
||||||
|
sch_name = (
|
||||||
|
schematic_path.name
|
||||||
|
if hasattr(schematic_path, "name")
|
||||||
|
else str(schematic_path)
|
||||||
|
)
|
||||||
|
logger.info(f"Injected symbol {full_name} into {sch_name}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def create_component_instance(self, schematic_path: Path, library_name: str,
|
def create_component_instance(
|
||||||
symbol_name: str, reference: str,
|
self,
|
||||||
value: str = "", x: float = 0, y: float = 0) -> bool:
|
schematic_path: Path,
|
||||||
|
library_name: str,
|
||||||
|
symbol_name: str,
|
||||||
|
reference: str,
|
||||||
|
value: str = "",
|
||||||
|
footprint: str = "",
|
||||||
|
x: float = 0,
|
||||||
|
y: float = 0,
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Add a component instance to the schematic.
|
Add a component instance to the schematic.
|
||||||
This creates the (symbol ...) block with lib_id reference.
|
This creates the (symbol ...) block with lib_id reference.
|
||||||
@@ -231,7 +256,7 @@ class DynamicSymbolLoader:
|
|||||||
full_lib_id = f"{library_name}:{symbol_name}"
|
full_lib_id = f"{library_name}:{symbol_name}"
|
||||||
new_uuid = str(uuid.uuid4())
|
new_uuid = str(uuid.uuid4())
|
||||||
|
|
||||||
instance_block = f''' (symbol (lib_id "{full_lib_id}") (at {x} {y} 0) (unit 1)
|
instance_block = f""" (symbol (lib_id "{full_lib_id}") (at {x} {y} 0) (unit 1)
|
||||||
(in_bom yes) (on_board yes) (dnp no)
|
(in_bom yes) (on_board yes) (dnp no)
|
||||||
(uuid "{new_uuid}")
|
(uuid "{new_uuid}")
|
||||||
(property "Reference" "{reference}" (at {x} {y - 2.54} 0)
|
(property "Reference" "{reference}" (at {x} {y - 2.54} 0)
|
||||||
@@ -240,30 +265,30 @@ class DynamicSymbolLoader:
|
|||||||
(property "Value" "{value or symbol_name}" (at {x} {y + 2.54} 0)
|
(property "Value" "{value or symbol_name}" (at {x} {y + 2.54} 0)
|
||||||
(effects (font (size 1.27 1.27)))
|
(effects (font (size 1.27 1.27)))
|
||||||
)
|
)
|
||||||
(property "Footprint" "" (at {x} {y} 0)
|
(property "Footprint" "{footprint}" (at {x} {y} 0)
|
||||||
(effects (font (size 1.27 1.27)) (hide yes))
|
(effects (font (size 1.27 1.27)) (hide yes))
|
||||||
)
|
)
|
||||||
(property "Datasheet" "~" (at {x} {y} 0)
|
(property "Datasheet" "~" (at {x} {y} 0)
|
||||||
(effects (font (size 1.27 1.27)) (hide yes))
|
(effects (font (size 1.27 1.27)) (hide yes))
|
||||||
)
|
)
|
||||||
)'''
|
)"""
|
||||||
|
|
||||||
with open(schematic_path, 'r', encoding='utf-8') as f:
|
with open(schematic_path, "r", encoding="utf-8") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
# Insert before (sheet_instances or at end before final )
|
# Insert before (sheet_instances or at end before final )
|
||||||
lines = content.split('\n')
|
lines = content.split("\n")
|
||||||
insert_pos = None
|
insert_pos = None
|
||||||
|
|
||||||
for i, line in enumerate(lines):
|
for i, line in enumerate(lines):
|
||||||
if '(sheet_instances' in line:
|
if "(sheet_instances" in line:
|
||||||
insert_pos = i
|
insert_pos = i
|
||||||
break
|
break
|
||||||
|
|
||||||
if insert_pos is None:
|
if insert_pos is None:
|
||||||
# Insert before the last closing parenthesis
|
# Insert before the last closing parenthesis
|
||||||
for i in range(len(lines) - 1, -1, -1):
|
for i in range(len(lines) - 1, -1, -1):
|
||||||
if lines[i].strip() == ')':
|
if lines[i].strip() == ")":
|
||||||
insert_pos = i
|
insert_pos = i
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -272,13 +297,17 @@ class DynamicSymbolLoader:
|
|||||||
|
|
||||||
lines.insert(insert_pos, instance_block)
|
lines.insert(insert_pos, instance_block)
|
||||||
|
|
||||||
with open(schematic_path, 'w', encoding='utf-8') as f:
|
with open(schematic_path, "w", encoding="utf-8") as f:
|
||||||
f.write('\n'.join(lines))
|
f.write("\n".join(lines))
|
||||||
|
|
||||||
logger.info(f"Added component instance {reference} ({full_lib_id}) at ({x}, {y})")
|
logger.info(
|
||||||
|
f"Added component instance {reference} ({full_lib_id}) at ({x}, {y})"
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def load_symbol_dynamically(self, schematic_path: Path, library_name: str, symbol_name: str) -> str:
|
def load_symbol_dynamically(
|
||||||
|
self, schematic_path: Path, library_name: str, symbol_name: str
|
||||||
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Complete workflow: inject symbol definition and create a template instance.
|
Complete workflow: inject symbol definition and create a template instance.
|
||||||
Returns a template reference name.
|
Returns a template reference name.
|
||||||
@@ -289,21 +318,34 @@ class DynamicSymbolLoader:
|
|||||||
self.inject_symbol_into_schematic(schematic_path, library_name, symbol_name)
|
self.inject_symbol_into_schematic(schematic_path, library_name, symbol_name)
|
||||||
|
|
||||||
# Step 2: Create an offscreen template instance
|
# Step 2: Create an offscreen template instance
|
||||||
lib_clean = library_name.replace('-', '_').replace('.', '_')
|
lib_clean = library_name.replace("-", "_").replace(".", "_")
|
||||||
sym_clean = symbol_name.replace('-', '_').replace('.', '_')
|
sym_clean = symbol_name.replace("-", "_").replace(".", "_")
|
||||||
template_ref = f"_TEMPLATE_{lib_clean}_{sym_clean}"
|
template_ref = f"_TEMPLATE_{lib_clean}_{sym_clean}"
|
||||||
|
|
||||||
self.create_component_instance(
|
self.create_component_instance(
|
||||||
schematic_path, library_name, symbol_name,
|
schematic_path,
|
||||||
reference=template_ref, value=symbol_name,
|
library_name,
|
||||||
x=-200, y=-200
|
symbol_name,
|
||||||
|
reference=template_ref,
|
||||||
|
value=symbol_name,
|
||||||
|
x=-200,
|
||||||
|
y=-200,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Symbol loaded. Template reference: {template_ref}")
|
logger.info(f"Symbol loaded. Template reference: {template_ref}")
|
||||||
return template_ref
|
return template_ref
|
||||||
|
|
||||||
def add_component(self, schematic_path: Path, library_name: str, symbol_name: str,
|
def add_component(
|
||||||
reference: str, value: str = "", x: float = 0, y: float = 0) -> bool:
|
self,
|
||||||
|
schematic_path: Path,
|
||||||
|
library_name: str,
|
||||||
|
symbol_name: str,
|
||||||
|
reference: str,
|
||||||
|
value: str = "",
|
||||||
|
footprint: str = "",
|
||||||
|
x: float = 0,
|
||||||
|
y: float = 0,
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
High-level: ensure symbol definition exists in schematic, then add an instance.
|
High-level: ensure symbol definition exists in schematic, then add an instance.
|
||||||
This is the main entry point for adding components.
|
This is the main entry point for adding components.
|
||||||
@@ -313,12 +355,18 @@ class DynamicSymbolLoader:
|
|||||||
|
|
||||||
# Add the component instance
|
# Add the component instance
|
||||||
return self.create_component_instance(
|
return self.create_component_instance(
|
||||||
schematic_path, library_name, symbol_name,
|
schematic_path,
|
||||||
reference=reference, value=value, x=x, y=y
|
library_name,
|
||||||
|
symbol_name,
|
||||||
|
reference=reference,
|
||||||
|
value=value,
|
||||||
|
footprint=footprint,
|
||||||
|
x=x,
|
||||||
|
y=y,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
loader = DynamicSymbolLoader()
|
loader = DynamicSymbolLoader()
|
||||||
|
|
||||||
@@ -329,7 +377,12 @@ if __name__ == '__main__':
|
|||||||
print(f" Found {len(lib_dirs)} directories")
|
print(f" Found {len(lib_dirs)} directories")
|
||||||
|
|
||||||
print("\n2. Extracting symbols...")
|
print("\n2. Extracting symbols...")
|
||||||
for lib, sym in [('Device', 'R'), ('Device', 'C'), ('Device', 'LED'), ('Device', 'Q_NMOS')]:
|
for lib, sym in [
|
||||||
|
("Device", "R"),
|
||||||
|
("Device", "C"),
|
||||||
|
("Device", "LED"),
|
||||||
|
("Device", "Q_NMOS"),
|
||||||
|
]:
|
||||||
block = loader.extract_symbol_from_library(lib, sym)
|
block = loader.extract_symbol_from_library(lib, sym)
|
||||||
if block:
|
if block:
|
||||||
print(f" OK: {lib}:{sym} ({len(block)} chars)")
|
print(f" OK: {lib}:{sym} ({len(block)} chars)")
|
||||||
@@ -337,8 +390,8 @@ if __name__ == '__main__':
|
|||||||
print(f" FAIL: {lib}:{sym}")
|
print(f" FAIL: {lib}:{sym}")
|
||||||
|
|
||||||
print("\n3. Testing extends resolution...")
|
print("\n3. Testing extends resolution...")
|
||||||
block = loader.extract_symbol_from_library('Regulator_Switching', 'LM2596S-5')
|
block = loader.extract_symbol_from_library("Regulator_Switching", "LM2596S-5")
|
||||||
if block and 'LM2596S-12' in block:
|
if block and "LM2596S-12" in block:
|
||||||
print(f" OK: LM2596S-5 includes parent LM2596S-12 ({len(block)} chars)")
|
print(f" OK: LM2596S-5 includes parent LM2596S-12 ({len(block)} chars)")
|
||||||
else:
|
else:
|
||||||
print(f" FAIL: extends not resolved")
|
print(f" FAIL: extends not resolved")
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import logging
|
|||||||
import shutil
|
import shutil
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
logger = logging.getLogger('kicad_interface')
|
logger = logging.getLogger("kicad_interface")
|
||||||
|
|
||||||
|
|
||||||
class ProjectCommands:
|
class ProjectCommands:
|
||||||
"""Handles project-related KiCAD operations"""
|
"""Handles project-related KiCAD operations"""
|
||||||
@@ -21,7 +22,9 @@ class ProjectCommands:
|
|||||||
"""Create a new KiCAD project"""
|
"""Create a new KiCAD project"""
|
||||||
try:
|
try:
|
||||||
# Accept both 'name' (from MCP tool) and 'projectName' (legacy)
|
# Accept both 'name' (from MCP tool) and 'projectName' (legacy)
|
||||||
project_name = params.get("name") or params.get("projectName", "New_Project")
|
project_name = params.get("name") or params.get(
|
||||||
|
"projectName", "New_Project"
|
||||||
|
)
|
||||||
path = params.get("path", os.getcwd())
|
path = params.get("path", os.getcwd())
|
||||||
template = params.get("template")
|
template = params.get("template")
|
||||||
|
|
||||||
@@ -35,12 +38,13 @@ class ProjectCommands:
|
|||||||
|
|
||||||
# Create a new board
|
# Create a new board
|
||||||
board = pcbnew.BOARD()
|
board = pcbnew.BOARD()
|
||||||
|
|
||||||
# Set project properties
|
# Set project properties
|
||||||
board.GetTitleBlock().SetTitle(project_name)
|
board.GetTitleBlock().SetTitle(project_name)
|
||||||
|
|
||||||
# Set current date with proper parameter
|
# Set current date with proper parameter
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
current_date = datetime.now().strftime("%Y-%m-%d")
|
current_date = datetime.now().strftime("%Y-%m-%d")
|
||||||
board.GetTitleBlock().SetDate(current_date)
|
board.GetTitleBlock().SetDate(current_date)
|
||||||
|
|
||||||
@@ -58,38 +62,58 @@ class ProjectCommands:
|
|||||||
board.SetFileName(board_path)
|
board.SetFileName(board_path)
|
||||||
pcbnew.SaveBoard(board_path, board)
|
pcbnew.SaveBoard(board_path, board)
|
||||||
|
|
||||||
# Create schematic from template (use expanded template with many component types)
|
# Create schematic from template (use expanded template with symbol definitions)
|
||||||
schematic_path = project_path.replace(".kicad_pro", ".kicad_sch")
|
schematic_path = project_path.replace(".kicad_pro", ".kicad_sch")
|
||||||
template_sch_path = os.path.join(
|
template_sch_path = os.path.join(
|
||||||
os.path.dirname(os.path.abspath(__file__)),
|
os.path.dirname(os.path.abspath(__file__)),
|
||||||
'..', 'templates', 'template_with_symbols_expanded.kicad_sch'
|
"..",
|
||||||
|
"templates",
|
||||||
|
"template_with_symbols_expanded.kicad_sch",
|
||||||
)
|
)
|
||||||
|
|
||||||
if os.path.exists(template_sch_path):
|
if os.path.exists(template_sch_path):
|
||||||
# Copy template schematic
|
# Copy template schematic
|
||||||
shutil.copy(template_sch_path, schematic_path)
|
shutil.copy(template_sch_path, schematic_path)
|
||||||
|
|
||||||
|
# Replace placeholder UUID with a real one
|
||||||
|
import uuid as uuid_module
|
||||||
|
|
||||||
|
with open(schematic_path, "r", encoding="utf-8") as f:
|
||||||
|
sch_content = f.read()
|
||||||
|
sch_content = sch_content.replace(
|
||||||
|
"00000000-0000-0000-0000-000000000000", str(uuid_module.uuid4())
|
||||||
|
)
|
||||||
|
with open(schematic_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(sch_content)
|
||||||
|
|
||||||
logger.info(f"Created schematic from template: {schematic_path}")
|
logger.info(f"Created schematic from template: {schematic_path}")
|
||||||
else:
|
else:
|
||||||
# Fallback: create minimal schematic
|
# Fallback: create minimal schematic
|
||||||
logger.warning(f"Template not found at {template_sch_path}, creating minimal schematic")
|
logger.warning(
|
||||||
with open(schematic_path, 'w') as f:
|
f"Template not found at {template_sch_path}, creating minimal schematic"
|
||||||
f.write(f'(kicad_sch (version 20230121) (generator "KiCAD-MCP-Server")\n\n')
|
)
|
||||||
f.write(f' (uuid 00000000-0000-0000-0000-000000000000)\n\n')
|
import uuid as uuid_module
|
||||||
|
|
||||||
|
with open(schematic_path, "w") as f:
|
||||||
|
f.write(
|
||||||
|
f'(kicad_sch (version 20250114) (generator "KiCAD-MCP-Server")\n\n'
|
||||||
|
)
|
||||||
|
f.write(f" (uuid {str(uuid_module.uuid4())})\n\n")
|
||||||
f.write(f' (paper "A4")\n\n')
|
f.write(f' (paper "A4")\n\n')
|
||||||
f.write(f' (lib_symbols\n )\n\n')
|
f.write(f" (lib_symbols\n )\n\n")
|
||||||
f.write(f' (sheet_instances\n (path "/" (page "1"))\n )\n')
|
f.write(f' (sheet_instances\n (path "/" (page "1"))\n )\n')
|
||||||
f.write(f')\n')
|
f.write(f")\n")
|
||||||
|
|
||||||
# Create project file with schematic reference
|
# Create project file with schematic reference
|
||||||
with open(project_path, 'w') as f:
|
with open(project_path, "w") as f:
|
||||||
f.write('{\n')
|
f.write("{\n")
|
||||||
f.write(' "board": {\n')
|
f.write(' "board": {\n')
|
||||||
f.write(f' "filename": "{os.path.basename(board_path)}"\n')
|
f.write(f' "filename": "{os.path.basename(board_path)}"\n')
|
||||||
f.write(' },\n')
|
f.write(" },\n")
|
||||||
f.write(' "sheets": [\n')
|
f.write(' "sheets": [\n')
|
||||||
f.write(f' ["root", "{os.path.basename(schematic_path)}"]\n')
|
f.write(f' ["root", "{os.path.basename(schematic_path)}"]\n')
|
||||||
f.write(' ]\n')
|
f.write(" ]\n")
|
||||||
f.write('}\n')
|
f.write("}\n")
|
||||||
|
|
||||||
self.board = board
|
self.board = board
|
||||||
|
|
||||||
@@ -100,8 +124,8 @@ class ProjectCommands:
|
|||||||
"name": project_name,
|
"name": project_name,
|
||||||
"path": project_path,
|
"path": project_path,
|
||||||
"boardPath": board_path,
|
"boardPath": board_path,
|
||||||
"schematicPath": schematic_path
|
"schematicPath": schematic_path,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -109,7 +133,7 @@ class ProjectCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to create project",
|
"message": "Failed to create project",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def open_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def open_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -120,7 +144,7 @@ class ProjectCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No filename provided",
|
"message": "No filename provided",
|
||||||
"errorDetails": "The filename parameter is required"
|
"errorDetails": "The filename parameter is required",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Expand user path and make absolute
|
# Expand user path and make absolute
|
||||||
@@ -142,8 +166,8 @@ class ProjectCommands:
|
|||||||
"project": {
|
"project": {
|
||||||
"name": os.path.splitext(os.path.basename(board_path))[0],
|
"name": os.path.splitext(os.path.basename(board_path))[0],
|
||||||
"path": filename,
|
"path": filename,
|
||||||
"boardPath": board_path
|
"boardPath": board_path,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -151,7 +175,7 @@ class ProjectCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to open project",
|
"message": "Failed to open project",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def save_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def save_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -161,7 +185,7 @@ class ProjectCommands:
|
|||||||
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",
|
||||||
}
|
}
|
||||||
|
|
||||||
filename = params.get("filename")
|
filename = params.get("filename")
|
||||||
@@ -177,9 +201,11 @@ class ProjectCommands:
|
|||||||
"success": True,
|
"success": True,
|
||||||
"message": f"Saved project to: {self.board.GetFileName()}",
|
"message": f"Saved project to: {self.board.GetFileName()}",
|
||||||
"project": {
|
"project": {
|
||||||
"name": os.path.splitext(os.path.basename(self.board.GetFileName()))[0],
|
"name": os.path.splitext(
|
||||||
"path": self.board.GetFileName()
|
os.path.basename(self.board.GetFileName())
|
||||||
}
|
)[0],
|
||||||
|
"path": self.board.GetFileName(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -187,7 +213,7 @@ class ProjectCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to save project",
|
"message": "Failed to save project",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_project_info(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def get_project_info(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -197,12 +223,12 @@ class ProjectCommands:
|
|||||||
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",
|
||||||
}
|
}
|
||||||
|
|
||||||
title_block = self.board.GetTitleBlock()
|
title_block = self.board.GetTitleBlock()
|
||||||
filename = self.board.GetFileName()
|
filename = self.board.GetFileName()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"project": {
|
"project": {
|
||||||
@@ -215,8 +241,8 @@ class ProjectCommands:
|
|||||||
"comment1": title_block.GetComment(0),
|
"comment1": title_block.GetComment(0),
|
||||||
"comment2": title_block.GetComment(1),
|
"comment2": title_block.GetComment(1),
|
||||||
"comment3": title_block.GetComment(2),
|
"comment3": title_block.GetComment(2),
|
||||||
"comment4": title_block.GetComment(3)
|
"comment4": title_block.GetComment(3),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -224,5 +250,5 @@ class ProjectCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to get project information",
|
"message": "Failed to get project information",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import shutil
|
|||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
logger = logging.getLogger('kicad_interface')
|
logger = logging.getLogger("kicad_interface")
|
||||||
|
|
||||||
|
|
||||||
class SchematicManager:
|
class SchematicManager:
|
||||||
"""Core schematic operations using kicad-skip"""
|
"""Core schematic operations using kicad-skip"""
|
||||||
@@ -16,11 +17,13 @@ class SchematicManager:
|
|||||||
# Determine template path (use template_with_symbols for component cloning support)
|
# Determine template path (use template_with_symbols for component cloning support)
|
||||||
template_path = os.path.join(
|
template_path = os.path.join(
|
||||||
os.path.dirname(os.path.abspath(__file__)),
|
os.path.dirname(os.path.abspath(__file__)),
|
||||||
'..', 'templates', 'template_with_symbols.kicad_sch'
|
"..",
|
||||||
|
"templates",
|
||||||
|
"template_with_symbols.kicad_sch",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Determine output path
|
# Determine output path
|
||||||
output_path = name if name.endswith('.kicad_sch') else f"{name}.kicad_sch"
|
output_path = name if name.endswith(".kicad_sch") else f"{name}.kicad_sch"
|
||||||
|
|
||||||
if os.path.exists(template_path):
|
if os.path.exists(template_path):
|
||||||
# Copy template to target location
|
# Copy template to target location
|
||||||
@@ -28,17 +31,21 @@ class SchematicManager:
|
|||||||
logger.info(f"Created schematic from template: {output_path}")
|
logger.info(f"Created schematic from template: {output_path}")
|
||||||
else:
|
else:
|
||||||
# Fallback: create minimal schematic
|
# Fallback: create minimal schematic
|
||||||
logger.warning(f"Template not found at {template_path}, creating minimal schematic")
|
logger.warning(
|
||||||
|
f"Template not found at {template_path}, creating minimal schematic"
|
||||||
|
)
|
||||||
# Generate unique UUID for this schematic
|
# Generate unique UUID for this schematic
|
||||||
schematic_uuid = str(uuid.uuid4())
|
schematic_uuid = str(uuid.uuid4())
|
||||||
# Write with explicit UTF-8 encoding and Unix line endings for cross-platform compatibility
|
# Write with explicit UTF-8 encoding and Unix line endings for cross-platform compatibility
|
||||||
with open(output_path, 'w', encoding='utf-8', newline='\n') as f:
|
with open(output_path, "w", encoding="utf-8", newline="\n") as f:
|
||||||
f.write('(kicad_sch (version 20230121) (generator "KiCAD-MCP-Server")\n\n')
|
f.write(
|
||||||
f.write(f' (uuid {schematic_uuid})\n\n')
|
'(kicad_sch (version 20250114) (generator "KiCAD-MCP-Server")\n\n'
|
||||||
|
)
|
||||||
|
f.write(f" (uuid {schematic_uuid})\n\n")
|
||||||
f.write(' (paper "A4")\n\n')
|
f.write(' (paper "A4")\n\n')
|
||||||
f.write(' (lib_symbols\n )\n\n')
|
f.write(" (lib_symbols\n )\n\n")
|
||||||
f.write(' (sheet_instances\n (path "/" (page "1"))\n )\n')
|
f.write(' (sheet_instances\n (path "/" (page "1"))\n )\n')
|
||||||
f.write(')\n')
|
f.write(")\n")
|
||||||
|
|
||||||
# Load the schematic
|
# Load the schematic
|
||||||
sch = Schematic(output_path)
|
sch = Schematic(output_path)
|
||||||
@@ -88,7 +95,8 @@ class SchematicManager:
|
|||||||
logger.debug("Extracted schematic metadata")
|
logger.debug("Extracted schematic metadata")
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
if __name__ == "__main__":
|
||||||
# Example Usage (for testing)
|
# Example Usage (for testing)
|
||||||
# Create a new schematic
|
# Create a new schematic
|
||||||
new_sch = SchematicManager.create_schematic("MyTestSchematic")
|
new_sch = SchematicManager.create_schematic("MyTestSchematic")
|
||||||
|
|||||||
@@ -366,6 +366,7 @@ class KiCADInterface:
|
|||||||
"load_schematic": self._handle_load_schematic,
|
"load_schematic": self._handle_load_schematic,
|
||||||
"add_schematic_component": self._handle_add_schematic_component,
|
"add_schematic_component": self._handle_add_schematic_component,
|
||||||
"delete_schematic_component": self._handle_delete_schematic_component,
|
"delete_schematic_component": self._handle_delete_schematic_component,
|
||||||
|
"edit_schematic_component": self._handle_edit_schematic_component,
|
||||||
"add_schematic_wire": self._handle_add_schematic_wire,
|
"add_schematic_wire": self._handle_add_schematic_wire,
|
||||||
"add_schematic_connection": self._handle_add_schematic_connection,
|
"add_schematic_connection": self._handle_add_schematic_connection,
|
||||||
"add_schematic_net_label": self._handle_add_schematic_net_label,
|
"add_schematic_net_label": self._handle_add_schematic_net_label,
|
||||||
@@ -585,6 +586,7 @@ class KiCADInterface:
|
|||||||
library = component.get("library", "Device")
|
library = component.get("library", "Device")
|
||||||
reference = component.get("reference", "X?")
|
reference = component.get("reference", "X?")
|
||||||
value = component.get("value", comp_type)
|
value = component.get("value", comp_type)
|
||||||
|
footprint = component.get("footprint", "")
|
||||||
x = component.get("x", 0)
|
x = component.get("x", 0)
|
||||||
y = component.get("y", 0)
|
y = component.get("y", 0)
|
||||||
|
|
||||||
@@ -595,6 +597,7 @@ class KiCADInterface:
|
|||||||
comp_type,
|
comp_type,
|
||||||
reference=reference,
|
reference=reference,
|
||||||
value=value,
|
value=value,
|
||||||
|
footprint=footprint,
|
||||||
x=x,
|
x=x,
|
||||||
y=y,
|
y=y,
|
||||||
)
|
)
|
||||||
@@ -646,9 +649,8 @@ class KiCADInterface:
|
|||||||
lib_sym_end = i
|
lib_sym_end = i
|
||||||
break
|
break
|
||||||
|
|
||||||
# Find the placed symbol block matching the reference
|
# Find ALL placed symbol blocks matching the reference (handles duplicates)
|
||||||
block_start = None
|
blocks_to_delete = []
|
||||||
block_end = None
|
|
||||||
i = 0
|
i = 0
|
||||||
while i < len(lines):
|
while i < len(lines):
|
||||||
# Skip lib_symbols
|
# Skip lib_symbols
|
||||||
@@ -666,35 +668,33 @@ class KiCADInterface:
|
|||||||
j += 1
|
j += 1
|
||||||
b_end = j - 1
|
b_end = j - 1
|
||||||
|
|
||||||
# Check if this block contains the target reference
|
|
||||||
block_text = "\n".join(lines[b_start:b_end + 1])
|
block_text = "\n".join(lines[b_start:b_end + 1])
|
||||||
# Match: (property "Reference" "R1" ...
|
|
||||||
if re.search(r'\(property\s+"Reference"\s+"' + re.escape(reference) + r'"', block_text):
|
if re.search(r'\(property\s+"Reference"\s+"' + re.escape(reference) + r'"', block_text):
|
||||||
block_start = b_start
|
blocks_to_delete.append((b_start, b_end))
|
||||||
block_end = b_end
|
|
||||||
break
|
|
||||||
|
|
||||||
i = b_end + 1
|
i = b_end + 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
if block_start is None:
|
if not blocks_to_delete:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": f"Component '{reference}' not found in schematic (note: this tool removes schematic symbols, use delete_component for PCB footprints)",
|
"message": f"Component '{reference}' not found in schematic (note: this tool removes schematic symbols, use delete_component for PCB footprints)",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Remove the block (and any trailing blank line)
|
# Delete from back to front to preserve line indices
|
||||||
del lines[block_start:block_end + 1]
|
for b_start, b_end in sorted(blocks_to_delete, reverse=True):
|
||||||
if block_start < len(lines) and lines[block_start].strip() == "":
|
del lines[b_start:b_end + 1]
|
||||||
del lines[block_start]
|
if b_start < len(lines) and lines[b_start].strip() == "":
|
||||||
|
del lines[b_start]
|
||||||
|
|
||||||
with open(sch_file, "w", encoding="utf-8") as f:
|
with open(sch_file, "w", encoding="utf-8") as f:
|
||||||
f.write("\n".join(lines))
|
f.write("\n".join(lines))
|
||||||
|
|
||||||
logger.info(f"Deleted schematic component {reference} from {sch_file.name}")
|
deleted_count = len(blocks_to_delete)
|
||||||
return {"success": True, "reference": reference, "schematic": str(sch_file)}
|
logger.info(f"Deleted {deleted_count} instance(s) of {reference} from {sch_file.name}")
|
||||||
|
return {"success": True, "reference": reference, "deleted_count": deleted_count, "schematic": str(sch_file)}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error deleting schematic component: {e}")
|
logger.error(f"Error deleting schematic component: {e}")
|
||||||
@@ -702,6 +702,98 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
|
def _handle_edit_schematic_component(self, params):
|
||||||
|
"""Update properties of a placed symbol in a schematic (footprint, value, reference).
|
||||||
|
Uses text-based in-place editing – preserves position, UUID and all other fields."""
|
||||||
|
logger.info("Editing schematic component")
|
||||||
|
try:
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
schematic_path = params.get("schematicPath")
|
||||||
|
reference = params.get("reference")
|
||||||
|
new_footprint = params.get("footprint")
|
||||||
|
new_value = params.get("value")
|
||||||
|
new_reference = params.get("newReference")
|
||||||
|
|
||||||
|
if not schematic_path:
|
||||||
|
return {"success": False, "message": "schematicPath is required"}
|
||||||
|
if not reference:
|
||||||
|
return {"success": False, "message": "reference is required"}
|
||||||
|
if not any([new_footprint is not None, new_value is not None, new_reference is not None]):
|
||||||
|
return {"success": False, "message": "At least one of footprint, value, or newReference must be provided"}
|
||||||
|
|
||||||
|
sch_file = Path(schematic_path)
|
||||||
|
if not sch_file.exists():
|
||||||
|
return {"success": False, "message": f"Schematic not found: {schematic_path}"}
|
||||||
|
|
||||||
|
with open(sch_file, "r", encoding="utf-8") as f:
|
||||||
|
lines = f.read().split("\n")
|
||||||
|
|
||||||
|
# Find lib_symbols range to skip
|
||||||
|
lib_sym_start, lib_sym_end = None, None
|
||||||
|
depth = 0
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if "(lib_symbols" in line and lib_sym_start is None:
|
||||||
|
lib_sym_start = i
|
||||||
|
depth = sum(1 for c in line if c == "(") - sum(1 for c in line if c == ")")
|
||||||
|
elif lib_sym_start is not None and lib_sym_end is None:
|
||||||
|
depth += sum(1 for c in line if c == "(") - sum(1 for c in line if c == ")")
|
||||||
|
if depth == 0:
|
||||||
|
lib_sym_end = i
|
||||||
|
break
|
||||||
|
|
||||||
|
# Find the placed symbol block
|
||||||
|
block_start = block_end = None
|
||||||
|
i = 0
|
||||||
|
while i < len(lines):
|
||||||
|
if lib_sym_start is not None and lib_sym_end is not None:
|
||||||
|
if lib_sym_start <= i <= lib_sym_end:
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if re.match(r"\s*\(symbol\s+\(lib_id\s+\"", lines[i]):
|
||||||
|
b_start = i
|
||||||
|
b_depth = sum(1 for c in lines[i] if c == "(") - sum(1 for c in lines[i] if c == ")")
|
||||||
|
j = i + 1
|
||||||
|
while j < len(lines) and b_depth > 0:
|
||||||
|
b_depth += sum(1 for c in lines[j] if c == "(") - sum(1 for c in lines[j] if c == ")")
|
||||||
|
j += 1
|
||||||
|
b_end = j - 1
|
||||||
|
block_text = "\n".join(lines[b_start:b_end + 1])
|
||||||
|
if re.search(r'\(property\s+"Reference"\s+"' + re.escape(reference) + r'"', block_text):
|
||||||
|
block_start, block_end = b_start, b_end
|
||||||
|
break
|
||||||
|
i = b_end + 1
|
||||||
|
continue
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if block_start is None:
|
||||||
|
return {"success": False, "message": f"Component '{reference}' not found in schematic"}
|
||||||
|
|
||||||
|
# Apply in-place property updates within the block
|
||||||
|
for k in range(block_start, block_end + 1):
|
||||||
|
line = lines[k]
|
||||||
|
if new_footprint is not None and re.match(r'\s*\(property\s+"Footprint"\s+"', line):
|
||||||
|
line = re.sub(r'(\(property\s+"Footprint"\s+)"[^"]*"', rf'\1"{new_footprint}"', line)
|
||||||
|
if new_value is not None and re.match(r'\s*\(property\s+"Value"\s+"', line):
|
||||||
|
line = re.sub(r'(\(property\s+"Value"\s+)"[^"]*"', rf'\1"{new_value}"', line)
|
||||||
|
if new_reference is not None and re.match(r'\s*\(property\s+"Reference"\s+"', line):
|
||||||
|
line = re.sub(r'(\(property\s+"Reference"\s+)"[^"]*"', rf'\1"{new_reference}"', line)
|
||||||
|
lines[k] = line
|
||||||
|
|
||||||
|
with open(sch_file, "w", encoding="utf-8") as f:
|
||||||
|
f.write("\n".join(lines))
|
||||||
|
|
||||||
|
changes = {k: v for k, v in {"footprint": new_footprint, "value": new_value, "reference": new_reference}.items() if v is not None}
|
||||||
|
logger.info(f"Edited schematic component {reference}: {changes}")
|
||||||
|
return {"success": True, "reference": reference, "updated": changes}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error editing schematic component: {e}")
|
||||||
|
import traceback
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_add_schematic_wire(self, params):
|
def _handle_add_schematic_wire(self, params):
|
||||||
"""Add a wire to a schematic using WireManager"""
|
"""Add a wire to a schematic using WireManager"""
|
||||||
logger.info("Adding wire to schematic")
|
logger.info("Adding wire to schematic")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
(kicad_sch (version 20230121) (generator "KiCAD-MCP-Server")
|
(kicad_sch (version 20250114) (generator "KiCAD-MCP-Server")
|
||||||
|
|
||||||
(uuid 00000000-0000-0000-0000-000000000000)
|
(uuid 00000000-0000-0000-0000-000000000000)
|
||||||
|
|
||||||
|
|||||||
13
python/templates/minimal.kicad_sch
Normal file
13
python/templates/minimal.kicad_sch
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
(kicad_sch (version 20250114) (generator "KiCAD-MCP-Server")
|
||||||
|
|
||||||
|
(uuid 00000000-0000-0000-0000-000000000000)
|
||||||
|
|
||||||
|
(paper "A4")
|
||||||
|
|
||||||
|
(lib_symbols
|
||||||
|
)
|
||||||
|
|
||||||
|
(sheet_instances
|
||||||
|
(path "/" (page "1"))
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
(kicad_sch (version 20230121) (generator "KiCAD-MCP-Server")
|
(kicad_sch (version 20250114) (generator "KiCAD-MCP-Server")
|
||||||
|
|
||||||
(uuid a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d)
|
(uuid a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d)
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
(symbol (lib_id "Device:R") (at -100 -100 0) (unit 1)
|
(symbol (lib_id "Device:R") (at -10000 -10000 0) (unit 1)
|
||||||
(in_bom no) (on_board no) (dnp yes)
|
(in_bom no) (on_board no) (dnp yes)
|
||||||
(uuid 00000000-0000-0000-0000-000000000001)
|
(uuid 00000000-0000-0000-0000-000000000001)
|
||||||
(property "Reference" "_TEMPLATE_R" (at -100 -102.54 0)
|
(property "Reference" "_TEMPLATE_R" (at -100 -102.54 0)
|
||||||
@@ -143,14 +143,14 @@
|
|||||||
(property "Footprint" "" (at -100 -100 90)
|
(property "Footprint" "" (at -100 -100 90)
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
(effects (font (size 1.27 1.27)) hide)
|
||||||
)
|
)
|
||||||
(property "Datasheet" "~" (at -100 -100 0)
|
(property "Datasheet" "~" (at -10000 -10000 0)
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
(effects (font (size 1.27 1.27)) hide)
|
||||||
)
|
)
|
||||||
(pin "1" (uuid 00000000-0000-0000-0000-000000000010))
|
(pin "1" (uuid 00000000-0000-0000-0000-000000000010))
|
||||||
(pin "2" (uuid 00000000-0000-0000-0000-000000000011))
|
(pin "2" (uuid 00000000-0000-0000-0000-000000000011))
|
||||||
)
|
)
|
||||||
|
|
||||||
(symbol (lib_id "Device:C") (at -100 -110 0) (unit 1)
|
(symbol (lib_id "Device:C") (at -10000 -10000 0) (unit 1)
|
||||||
(in_bom no) (on_board no) (dnp yes)
|
(in_bom no) (on_board no) (dnp yes)
|
||||||
(uuid 00000000-0000-0000-0000-000000000002)
|
(uuid 00000000-0000-0000-0000-000000000002)
|
||||||
(property "Reference" "_TEMPLATE_C" (at -100 -107.46 0)
|
(property "Reference" "_TEMPLATE_C" (at -100 -107.46 0)
|
||||||
@@ -159,17 +159,17 @@
|
|||||||
(property "Value" "C_TEMPLATE" (at -100 -112.54 0)
|
(property "Value" "C_TEMPLATE" (at -100 -112.54 0)
|
||||||
(effects (font (size 1.27 1.27)))
|
(effects (font (size 1.27 1.27)))
|
||||||
)
|
)
|
||||||
(property "Footprint" "" (at -100 -110 0)
|
(property "Footprint" "" (at -10000 -10000 0)
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
(effects (font (size 1.27 1.27)) hide)
|
||||||
)
|
)
|
||||||
(property "Datasheet" "~" (at -100 -110 0)
|
(property "Datasheet" "~" (at -10000 -10000 0)
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
(effects (font (size 1.27 1.27)) hide)
|
||||||
)
|
)
|
||||||
(pin "1" (uuid 00000000-0000-0000-0000-000000000020))
|
(pin "1" (uuid 00000000-0000-0000-0000-000000000020))
|
||||||
(pin "2" (uuid 00000000-0000-0000-0000-000000000021))
|
(pin "2" (uuid 00000000-0000-0000-0000-000000000021))
|
||||||
)
|
)
|
||||||
|
|
||||||
(symbol (lib_id "Device:LED") (at -100 -120 0) (unit 1)
|
(symbol (lib_id "Device:LED") (at -10000 -10000 0) (unit 1)
|
||||||
(in_bom no) (on_board no) (dnp yes)
|
(in_bom no) (on_board no) (dnp yes)
|
||||||
(uuid 00000000-0000-0000-0000-000000000003)
|
(uuid 00000000-0000-0000-0000-000000000003)
|
||||||
(property "Reference" "_TEMPLATE_D" (at -100 -117.46 0)
|
(property "Reference" "_TEMPLATE_D" (at -100 -117.46 0)
|
||||||
@@ -178,10 +178,10 @@
|
|||||||
(property "Value" "LED_TEMPLATE" (at -100 -122.54 0)
|
(property "Value" "LED_TEMPLATE" (at -100 -122.54 0)
|
||||||
(effects (font (size 1.27 1.27)))
|
(effects (font (size 1.27 1.27)))
|
||||||
)
|
)
|
||||||
(property "Footprint" "" (at -100 -120 0)
|
(property "Footprint" "" (at -10000 -10000 0)
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
(effects (font (size 1.27 1.27)) hide)
|
||||||
)
|
)
|
||||||
(property "Datasheet" "~" (at -100 -120 0)
|
(property "Datasheet" "~" (at -10000 -10000 0)
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
(effects (font (size 1.27 1.27)) hide)
|
||||||
)
|
)
|
||||||
(pin "1" (uuid 00000000-0000-0000-0000-000000000030))
|
(pin "1" (uuid 00000000-0000-0000-0000-000000000030))
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
(kicad_sch (version 20230121) (generator "KiCAD-MCP-Server")
|
(kicad_sch (version 20250114) (generator "KiCAD-MCP-Server")
|
||||||
|
|
||||||
(uuid 00000000-0000-0000-0000-000000000000)
|
(uuid 00000000-0000-0000-0000-000000000000)
|
||||||
|
|
||||||
(paper "A4")
|
(paper "A4")
|
||||||
|
|
||||||
(lib_symbols
|
(lib_symbols
|
||||||
;; PASSIVES
|
|
||||||
(symbol "Device:R" (pin_numbers hide) (pin_names (offset 0)) (in_bom yes) (on_board yes)
|
(symbol "Device:R" (pin_numbers hide) (pin_names (offset 0)) (in_bom yes) (on_board yes)
|
||||||
(property "Reference" "R" (at 2.032 0 90)
|
(property "Reference" "R" (at 2.032 0 90)
|
||||||
(effects (font (size 1.27 1.27)))
|
(effects (font (size 1.27 1.27)))
|
||||||
@@ -182,7 +182,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
;; SEMICONDUCTORS
|
|
||||||
(symbol "Device:D" (pin_numbers hide) (pin_names (offset 1.016) hide) (in_bom yes) (on_board yes)
|
(symbol "Device:D" (pin_numbers hide) (pin_names (offset 1.016) hide) (in_bom yes) (on_board yes)
|
||||||
(property "Reference" "D" (at 0 2.54 0)
|
(property "Reference" "D" (at 0 2.54 0)
|
||||||
(effects (font (size 1.27 1.27)))
|
(effects (font (size 1.27 1.27)))
|
||||||
@@ -497,7 +497,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
;; INTEGRATED CIRCUITS
|
|
||||||
(symbol "Amplifier_Operational:LM358" (pin_names (offset 0.127)) (in_bom yes) (on_board yes)
|
(symbol "Amplifier_Operational:LM358" (pin_names (offset 0.127)) (in_bom yes) (on_board yes)
|
||||||
(property "Reference" "U" (at 0 5.08 0)
|
(property "Reference" "U" (at 0 5.08 0)
|
||||||
(effects (font (size 1.27 1.27)) (justify left))
|
(effects (font (size 1.27 1.27)) (justify left))
|
||||||
@@ -546,7 +546,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
;; CONNECTORS
|
|
||||||
(symbol "Connector_Generic:Conn_01x02" (pin_names (offset 1.016) hide) (in_bom yes) (on_board yes)
|
(symbol "Connector_Generic:Conn_01x02" (pin_names (offset 1.016) hide) (in_bom yes) (on_board yes)
|
||||||
(property "Reference" "J" (at 0 2.54 0)
|
(property "Reference" "J" (at 0 2.54 0)
|
||||||
(effects (font (size 1.27 1.27)))
|
(effects (font (size 1.27 1.27)))
|
||||||
@@ -635,7 +635,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
;; POWER/REGULATORS
|
|
||||||
(symbol "Regulator_Linear:AMS1117-3.3" (pin_names (offset 0.254)) (in_bom yes) (on_board yes)
|
(symbol "Regulator_Linear:AMS1117-3.3" (pin_names (offset 0.254)) (in_bom yes) (on_board yes)
|
||||||
(property "Reference" "U" (at -3.81 3.175 0)
|
(property "Reference" "U" (at -3.81 3.175 0)
|
||||||
(effects (font (size 1.27 1.27)))
|
(effects (font (size 1.27 1.27)))
|
||||||
@@ -670,7 +670,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
;; MISC
|
|
||||||
(symbol "Switch:SW_Push" (pin_numbers hide) (pin_names (offset 1.016) hide) (in_bom yes) (on_board yes)
|
(symbol "Switch:SW_Push" (pin_numbers hide) (pin_names (offset 1.016) hide) (in_bom yes) (on_board yes)
|
||||||
(property "Reference" "SW" (at 1.27 2.54 0)
|
(property "Reference" "SW" (at 1.27 2.54 0)
|
||||||
(effects (font (size 1.27 1.27)) (justify left))
|
(effects (font (size 1.27 1.27)) (justify left))
|
||||||
@@ -721,261 +721,19 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
;; TEMPLATE INSTANCES (placed offscreen at -100, -110, etc.)
|
|
||||||
(symbol (lib_id "Device:R") (at -100 -100 0) (unit 1)
|
|
||||||
(in_bom no) (on_board no) (dnp yes)
|
|
||||||
(uuid 00000000-0000-0000-0000-000000000001)
|
|
||||||
(property "Reference" "_TEMPLATE_R" (at -100 -102.54 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Value" "R" (at -100 -100 90)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Footprint" "" (at -100 -100 90)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(property "Datasheet" "~" (at -100 -100 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(pin "1" (uuid 10000000-0000-0000-0000-000000000001))
|
|
||||||
(pin "2" (uuid 10000000-0000-0000-0000-000000000002))
|
|
||||||
)
|
|
||||||
|
|
||||||
(symbol (lib_id "Device:C") (at -100 -110 0) (unit 1)
|
|
||||||
(in_bom no) (on_board no) (dnp yes)
|
|
||||||
(uuid 00000000-0000-0000-0000-000000000002)
|
|
||||||
(property "Reference" "_TEMPLATE_C" (at -100 -107.46 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Value" "C" (at -100 -112.54 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Footprint" "" (at -100 -110 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(property "Datasheet" "~" (at -100 -110 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(pin "1" (uuid 20000000-0000-0000-0000-000000000001))
|
|
||||||
(pin "2" (uuid 20000000-0000-0000-0000-000000000002))
|
|
||||||
)
|
|
||||||
|
|
||||||
(symbol (lib_id "Device:L") (at -100 -120 0) (unit 1)
|
|
||||||
(in_bom no) (on_board no) (dnp yes)
|
|
||||||
(uuid 00000000-0000-0000-0000-000000000003)
|
|
||||||
(property "Reference" "_TEMPLATE_L" (at -100 -117.46 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Value" "L" (at -100 -122.54 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Footprint" "" (at -100 -120 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(property "Datasheet" "~" (at -100 -120 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(pin "1" (uuid 30000000-0000-0000-0000-000000000001))
|
|
||||||
(pin "2" (uuid 30000000-0000-0000-0000-000000000002))
|
|
||||||
)
|
|
||||||
|
|
||||||
(symbol (lib_id "Device:Crystal") (at -100 -130 0) (unit 1)
|
|
||||||
(in_bom no) (on_board no) (dnp yes)
|
|
||||||
(uuid 00000000-0000-0000-0000-000000000004)
|
|
||||||
(property "Reference" "_TEMPLATE_Y" (at -100 -127.46 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Value" "Crystal" (at -100 -132.54 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Footprint" "" (at -100 -130 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(property "Datasheet" "~" (at -100 -130 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(pin "1" (uuid 40000000-0000-0000-0000-000000000001))
|
|
||||||
(pin "2" (uuid 40000000-0000-0000-0000-000000000002))
|
|
||||||
)
|
|
||||||
|
|
||||||
(symbol (lib_id "Device:D") (at -100 -140 0) (unit 1)
|
|
||||||
(in_bom no) (on_board no) (dnp yes)
|
|
||||||
(uuid 00000000-0000-0000-0000-000000000005)
|
|
||||||
(property "Reference" "_TEMPLATE_D" (at -100 -137.46 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Value" "D" (at -100 -142.54 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Footprint" "" (at -100 -140 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(property "Datasheet" "~" (at -100 -140 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(pin "1" (uuid 50000000-0000-0000-0000-000000000001))
|
|
||||||
(pin "2" (uuid 50000000-0000-0000-0000-000000000002))
|
|
||||||
)
|
|
||||||
|
|
||||||
(symbol (lib_id "Device:LED") (at -100 -150 0) (unit 1)
|
|
||||||
(in_bom no) (on_board no) (dnp yes)
|
|
||||||
(uuid 00000000-0000-0000-0000-000000000006)
|
|
||||||
(property "Reference" "_TEMPLATE_LED" (at -100 -147.46 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Value" "LED" (at -100 -152.54 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Footprint" "" (at -100 -150 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(property "Datasheet" "~" (at -100 -150 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(pin "1" (uuid 60000000-0000-0000-0000-000000000001))
|
|
||||||
(pin "2" (uuid 60000000-0000-0000-0000-000000000002))
|
|
||||||
)
|
|
||||||
|
|
||||||
(symbol (lib_id "Device:Q_NPN_BCE") (at -100 -160 0) (unit 1)
|
|
||||||
(in_bom no) (on_board no) (dnp yes)
|
|
||||||
(uuid 00000000-0000-0000-0000-000000000007)
|
|
||||||
(property "Reference" "_TEMPLATE_Q_NPN" (at -100 -157.46 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Value" "Q_NPN" (at -100 -162.54 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Footprint" "" (at -100 -160 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(property "Datasheet" "~" (at -100 -160 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(pin "1" (uuid 70000000-0000-0000-0000-000000000001))
|
|
||||||
(pin "2" (uuid 70000000-0000-0000-0000-000000000002))
|
|
||||||
(pin "3" (uuid 70000000-0000-0000-0000-000000000003))
|
|
||||||
)
|
|
||||||
|
|
||||||
(symbol (lib_id "Device:Q_NMOS_GSD") (at -100 -170 0) (unit 1)
|
|
||||||
(in_bom no) (on_board no) (dnp yes)
|
|
||||||
(uuid 00000000-0000-0000-0000-000000000008)
|
|
||||||
(property "Reference" "_TEMPLATE_Q_NMOS" (at -100 -167.46 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Value" "Q_NMOS" (at -100 -172.54 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Footprint" "" (at -100 -170 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(property "Datasheet" "~" (at -100 -170 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(pin "1" (uuid 80000000-0000-0000-0000-000000000001))
|
|
||||||
(pin "2" (uuid 80000000-0000-0000-0000-000000000002))
|
|
||||||
(pin "3" (uuid 80000000-0000-0000-0000-000000000003))
|
|
||||||
)
|
|
||||||
|
|
||||||
(symbol (lib_id "Amplifier_Operational:LM358") (at -100 -180 0) (unit 1)
|
|
||||||
(in_bom no) (on_board no) (dnp yes)
|
|
||||||
(uuid 00000000-0000-0000-0000-000000000009)
|
|
||||||
(property "Reference" "_TEMPLATE_U_OPAMP" (at -100 -177.46 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Value" "OpAmp" (at -100 -182.54 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Footprint" "" (at -100 -180 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(property "Datasheet" "" (at -100 -180 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(pin "1" (uuid 90000000-0000-0000-0000-000000000001))
|
|
||||||
(pin "2" (uuid 90000000-0000-0000-0000-000000000002))
|
|
||||||
(pin "3" (uuid 90000000-0000-0000-0000-000000000003))
|
|
||||||
(pin "4" (uuid 90000000-0000-0000-0000-000000000004))
|
|
||||||
(pin "8" (uuid 90000000-0000-0000-0000-000000000005))
|
|
||||||
)
|
|
||||||
|
|
||||||
(symbol (lib_id "Connector_Generic:Conn_01x02") (at -100 -190 0) (unit 1)
|
|
||||||
(in_bom no) (on_board no) (dnp yes)
|
|
||||||
(uuid 00000000-0000-0000-0000-00000000000A)
|
|
||||||
(property "Reference" "_TEMPLATE_J2" (at -100 -187.46 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Value" "Conn_2" (at -100 -192.54 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Footprint" "" (at -100 -190 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(property "Datasheet" "~" (at -100 -190 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(pin "1" (uuid A0000000-0000-0000-0000-000000000001))
|
|
||||||
(pin "2" (uuid A0000000-0000-0000-0000-000000000002))
|
|
||||||
)
|
|
||||||
|
|
||||||
(symbol (lib_id "Connector_Generic:Conn_01x04") (at -100 -200 0) (unit 1)
|
|
||||||
(in_bom no) (on_board no) (dnp yes)
|
|
||||||
(uuid 00000000-0000-0000-0000-00000000000B)
|
|
||||||
(property "Reference" "_TEMPLATE_J4" (at -100 -197.46 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Value" "Conn_4" (at -100 -202.54 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Footprint" "" (at -100 -200 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(property "Datasheet" "~" (at -100 -200 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(pin "1" (uuid B0000000-0000-0000-0000-000000000001))
|
|
||||||
(pin "2" (uuid B0000000-0000-0000-0000-000000000002))
|
|
||||||
(pin "3" (uuid B0000000-0000-0000-0000-000000000003))
|
|
||||||
(pin "4" (uuid B0000000-0000-0000-0000-000000000004))
|
|
||||||
)
|
|
||||||
|
|
||||||
(symbol (lib_id "Regulator_Linear:AMS1117-3.3") (at -100 -210 0) (unit 1)
|
|
||||||
(in_bom no) (on_board no) (dnp yes)
|
|
||||||
(uuid 00000000-0000-0000-0000-00000000000C)
|
|
||||||
(property "Reference" "_TEMPLATE_U_REG" (at -100 -207.46 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Value" "Regulator" (at -100 -212.54 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Footprint" "" (at -100 -210 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(property "Datasheet" "" (at -100 -210 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(pin "1" (uuid C0000000-0000-0000-0000-000000000001))
|
|
||||||
(pin "2" (uuid C0000000-0000-0000-0000-000000000002))
|
|
||||||
(pin "3" (uuid C0000000-0000-0000-0000-000000000003))
|
|
||||||
)
|
|
||||||
|
|
||||||
(symbol (lib_id "Switch:SW_Push") (at -100 -220 0) (unit 1)
|
|
||||||
(in_bom no) (on_board no) (dnp yes)
|
|
||||||
(uuid 00000000-0000-0000-0000-00000000000D)
|
|
||||||
(property "Reference" "_TEMPLATE_SW" (at -100 -217.46 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Value" "Switch" (at -100 -222.54 0)
|
|
||||||
(effects (font (size 1.27 1.27)))
|
|
||||||
)
|
|
||||||
(property "Footprint" "" (at -100 -220 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(property "Datasheet" "~" (at -100 -220 0)
|
|
||||||
(effects (font (size 1.27 1.27)) hide)
|
|
||||||
)
|
|
||||||
(pin "1" (uuid D0000000-0000-0000-0000-000000000001))
|
|
||||||
(pin "2" (uuid D0000000-0000-0000-0000-000000000002))
|
|
||||||
)
|
|
||||||
|
|
||||||
(sheet_instances
|
(sheet_instances
|
||||||
(path "/" (page "1"))
|
(path "/" (page "1"))
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export function registerSchematicTools(
|
|||||||
),
|
),
|
||||||
reference: z.string().describe("Component reference (e.g., R1, U1)"),
|
reference: z.string().describe("Component reference (e.g., R1, U1)"),
|
||||||
value: z.string().optional().describe("Component value"),
|
value: z.string().optional().describe("Component value"),
|
||||||
|
footprint: z.string().optional().describe("KiCAD footprint (e.g. Resistor_SMD:R_0603_1608Metric)"),
|
||||||
position: z
|
position: z
|
||||||
.object({
|
.object({
|
||||||
x: z.number(),
|
x: z.number(),
|
||||||
@@ -56,6 +57,7 @@ export function registerSchematicTools(
|
|||||||
symbol: string;
|
symbol: string;
|
||||||
reference: string;
|
reference: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
|
footprint?: string;
|
||||||
position?: { x: number; y: number };
|
position?: { x: number; y: number };
|
||||||
}) => {
|
}) => {
|
||||||
// Transform to what Python backend expects
|
// Transform to what Python backend expects
|
||||||
@@ -70,6 +72,7 @@ export function registerSchematicTools(
|
|||||||
type: symbolName,
|
type: symbolName,
|
||||||
reference: args.reference,
|
reference: args.reference,
|
||||||
value: args.value,
|
value: args.value,
|
||||||
|
footprint: args.footprint ?? "",
|
||||||
// Python expects flat x, y not nested position
|
// Python expects flat x, y not nested position
|
||||||
x: args.position?.x ?? 0,
|
x: args.position?.x ?? 0,
|
||||||
y: args.position?.y ?? 0,
|
y: args.position?.y ?? 0,
|
||||||
@@ -141,6 +144,55 @@ To remove a footprint from a PCB, use delete_component instead.`,
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Edit component properties in schematic (footprint, value, reference)
|
||||||
|
server.tool(
|
||||||
|
"edit_schematic_component",
|
||||||
|
`Update properties of a placed symbol in a KiCAD schematic (.kicad_sch) in-place.
|
||||||
|
|
||||||
|
Use this tool to assign or update a footprint, change the value, or rename the reference
|
||||||
|
of an already-placed component. This is more efficient than delete + re-add because it
|
||||||
|
preserves the component's position and UUID.
|
||||||
|
|
||||||
|
Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_component.`,
|
||||||
|
{
|
||||||
|
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||||
|
reference: z.string().describe("Current reference designator of the component (e.g. R1, U3)"),
|
||||||
|
footprint: z.string().optional().describe("New KiCAD footprint string (e.g. Resistor_SMD:R_0603_1608Metric)"),
|
||||||
|
value: z.string().optional().describe("New value string (e.g. 10k, 100nF)"),
|
||||||
|
newReference: z.string().optional().describe("Rename the reference designator (e.g. R1 → R10)"),
|
||||||
|
},
|
||||||
|
async (args: {
|
||||||
|
schematicPath: string;
|
||||||
|
reference: string;
|
||||||
|
footprint?: string;
|
||||||
|
value?: string;
|
||||||
|
newReference?: string;
|
||||||
|
}) => {
|
||||||
|
const result = await callKicadScript("edit_schematic_component", args);
|
||||||
|
if (result.success) {
|
||||||
|
const changes = Object.entries(result.updated ?? {})
|
||||||
|
.map(([k, v]) => `${k}=${v}`)
|
||||||
|
.join(", ");
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text" as const,
|
||||||
|
text: `Successfully updated ${args.reference}: ${changes}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text" as const,
|
||||||
|
text: `Failed to edit component: ${result.message || "Unknown error"}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Connect components with wire
|
// Connect components with wire
|
||||||
server.tool(
|
server.tool(
|
||||||
"add_wire",
|
"add_wire",
|
||||||
|
|||||||
Reference in New Issue
Block a user