app
Adom Desktop - KiCad Bridge
Public Made by Adomby adom
Reference implementation of the KiCad bridge — multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
"""Handler for browsing and searching KiCad symbol libraries."""
import os
import re
import sys
from pathlib import Path
def handle_list_symbol_libraries(kicad_info: dict, args: dict) -> dict:
"""List all symbol libraries registered in KiCad's sym-lib-table."""
sys.path.insert(0, str(Path(__file__).parent.parent))
from lib_table import LibTable
sym_lib_table = kicad_info.get("sym_lib_table")
if not sym_lib_table:
return {"success": False, "error": "KiCad sym-lib-table path not found"}
try:
table = LibTable.parse_file(sym_lib_table)
except Exception as e:
return {"success": False, "error": f"Failed to parse sym-lib-table: {e}"}
libraries = []
for entry in table.entries:
# Resolve ${KICAD9_SYMBOL_DIR} and similar env vars
uri = _resolve_kicad_vars(entry.uri, kicad_info)
exists = Path(uri).exists() if uri else False
libraries.append({
"name": entry.name,
"type": entry.lib_type,
"uri": entry.uri,
"resolved_path": uri,
"exists": exists,
"description": entry.descr,
})
return {
"success": True,
"output": f"Found {len(libraries)} symbol libraries",
"data": {"libraries": libraries},
}
def handle_search_symbols(kicad_info: dict, args: dict) -> dict:
"""Search across all symbol libraries by name/keyword.
Args (in args dict):
query: Search term (regex supported)
limit: Max results (default 50)
"""
sys.path.insert(0, str(Path(__file__).parent.parent))
from lib_table import LibTable
from parsers.sexpr import parse_file, find_nodes, find_node, node_value
query = args.get("query", "")
if not query:
return {"success": False, "error": "No query specified"}
limit = int(args.get("limit", 50))
sym_lib_table = kicad_info.get("sym_lib_table")
if not sym_lib_table:
return {"success": False, "error": "KiCad sym-lib-table path not found"}
try:
table = LibTable.parse_file(sym_lib_table)
except Exception as e:
return {"success": False, "error": f"Failed to parse sym-lib-table: {e}"}
try:
pattern = re.compile(query, re.IGNORECASE)
except re.error as e:
return {"success": False, "error": f"Invalid regex pattern: {e}"}
results = []
searched = 0
for entry in table.entries:
uri = _resolve_kicad_vars(entry.uri, kicad_info)
if not uri or not Path(uri).exists():
continue
searched += 1
try:
tree = parse_file(uri)
except Exception:
continue
for sym in find_nodes(tree, "symbol"):
if len(sym) < 2 or not isinstance(sym[1], str):
continue
sym_name = sym[1]
# Skip internal sub-symbols (e.g., "OpAmp_0_1", "OpAmp_1_1")
if re.match(r'.*_\d+_\d+$', sym_name):
continue
# Check name match
if not pattern.search(sym_name):
# Also check Description and keywords properties
desc_prop = find_node(sym, "property")
match_found = False
for child in sym:
if isinstance(child, list) and child and child[0] == "property":
prop_name = child[1] if len(child) > 1 else ""
prop_val = child[2] if len(child) > 2 else ""
if prop_name in ("Description", "ki_keywords") and isinstance(prop_val, str):
if pattern.search(prop_val):
match_found = True
break
if not match_found:
continue
# Extract symbol info
props = {}
for child in sym:
if isinstance(child, list) and child and child[0] == "property" and len(child) >= 3:
props[child[1]] = child[2]
# Count pins (follow extends chain if needed)
pin_count = _count_pins(sym, tree)
results.append({
"library": entry.name,
"name": sym_name,
"description": props.get("Description", ""),
"footprint": props.get("Footprint", ""),
"datasheet": props.get("Datasheet", ""),
"keywords": props.get("ki_keywords", ""),
"pin_count": pin_count,
})
if len(results) >= limit:
break
if len(results) >= limit:
break
return {
"success": True,
"output": f"Found {len(results)} symbol(s) matching '{query}' (searched {searched} libraries)",
"data": {
"results": results,
"query": query,
"libraries_searched": searched,
"truncated": len(results) >= limit,
},
}
def handle_get_symbol_info(kicad_info: dict, args: dict) -> dict:
"""Get detailed info about a specific symbol.
Args:
library: Library name
symbol: Symbol name
"""
sys.path.insert(0, str(Path(__file__).parent.parent))
from lib_table import LibTable
from parsers.sexpr import parse_file, find_node, find_nodes, node_value
library = args.get("library", "")
symbol = args.get("symbol", "")
if not library:
return {"success": False, "error": "No library specified"}
if not symbol:
return {"success": False, "error": "No symbol specified"}
sym_lib_table = kicad_info.get("sym_lib_table")
if not sym_lib_table:
return {"success": False, "error": "KiCad sym-lib-table path not found"}
try:
table = LibTable.parse_file(sym_lib_table)
except Exception as e:
return {"success": False, "error": f"Failed to parse sym-lib-table: {e}"}
# Find the library entry
lib_entry = None
for entry in table.entries:
if entry.name == library:
lib_entry = entry
break
if not lib_entry:
return {
"success": False,
"error": f"Library '{library}' not found",
"_hint": "Call list_symbol_libraries to see valid library names.",
}
uri = _resolve_kicad_vars(lib_entry.uri, kicad_info)
if not uri or not Path(uri).exists():
return {"success": False, "error": f"Library file not found: {lib_entry.uri}"}
try:
tree = parse_file(uri)
except Exception as e:
return {"success": False, "error": f"Failed to parse library: {e}"}
# Find the symbol
sym_node = None
for sym in find_nodes(tree, "symbol"):
if len(sym) >= 2 and sym[1] == symbol:
sym_node = sym
break
if not sym_node:
return {
"success": False,
"error": f"Symbol '{symbol}' not found in library '{library}'",
"_hint": "Call search_symbols to find the correct symbol name, or list_library_symbols to enumerate symbols in this library.",
}
# Extract properties
props = {}
for child in sym_node:
if isinstance(child, list) and child and child[0] == "property" and len(child) >= 3:
props[child[1]] = child[2]
# Extract pins from sub-symbols (follow extends chain if needed)
pins = _extract_pins(sym_node, tree)
# Check flags
is_power = any(
(isinstance(c, list) and c == ["power"]) or c == "power"
for c in sym_node
)
return {
"success": True,
"output": f"Symbol '{symbol}' from '{library}': {len(pins)} pin(s)",
"data": {
"library": library,
"name": symbol,
"properties": props,
"pins": pins,
"pin_count": len(pins),
"is_power": is_power,
"description": props.get("Description", ""),
"footprint": props.get("Footprint", ""),
"datasheet": props.get("Datasheet", ""),
},
}
def handle_search_footprints(kicad_info: dict, args: dict) -> dict:
"""Search across all footprint libraries by name/keyword.
Args (in args dict):
query: Search term (regex supported)
limit: Max results (default 50)
"""
sys.path.insert(0, str(Path(__file__).parent.parent))
from lib_table import LibTable
from parsers.sexpr import parse_file, find_node, find_nodes, node_value
query = args.get("query", "")
if not query:
return {"success": False, "error": "No query specified"}
limit = int(args.get("limit", 50))
fp_lib_table = kicad_info.get("fp_lib_table")
if not fp_lib_table:
return {"success": False, "error": "KiCad fp-lib-table path not found"}
try:
table = LibTable.parse_file(fp_lib_table)
except Exception as e:
return {"success": False, "error": f"Failed to parse fp-lib-table: {e}"}
try:
pattern = re.compile(query, re.IGNORECASE)
except re.error as e:
return {"success": False, "error": f"Invalid regex pattern: {e}"}
results = []
searched = 0
# Build list of (library_name, pretty_dir_path) to search.
# Includes both fp-lib-table entries AND system footprint libraries.
lib_dirs = []
for entry in table.entries:
uri = _resolve_kicad_vars(entry.uri, kicad_info)
if uri:
lib_dirs.append((entry.name, Path(uri)))
# Also include system footprint libraries (not always in user fp-lib-table)
base_dir = kicad_info.get("base_dir", "")
if base_dir:
sys_fp_dir = Path(base_dir) / "share" / "kicad" / "footprints"
if sys_fp_dir.is_dir():
seen = {d.name for _, d in lib_dirs}
for pretty in sorted(sys_fp_dir.iterdir()):
if pretty.suffix == ".pretty" and pretty.name not in seen:
lib_name = pretty.stem # e.g. "Capacitor_SMD"
lib_dirs.append((lib_name, pretty))
for lib_name, pretty_dir in lib_dirs:
if not pretty_dir.is_dir():
continue
searched += 1
# Check if the library name itself matches (all footprints qualify)
lib_match = pattern.search(lib_name)
for mod_file in sorted(pretty_dir.glob("*.kicad_mod")):
fp_name = mod_file.stem # filename without .kicad_mod
# Fast match: filename or library name only (no file parse)
if not (pattern.search(fp_name) or lib_match):
continue
# Only parse the file for metadata on matches (fast path)
descr = ""
tags = ""
pad_count = 0
fp_type = ""
try:
tree = parse_file(str(mod_file))
descr_node = find_node(tree, "descr")
tags_node = find_node(tree, "tags")
descr = node_value(descr_node) or "" if descr_node else ""
tags = node_value(tags_node) or "" if tags_node else ""
pad_count = len(find_nodes(tree, "pad"))
attr_node = find_node(tree, "attr")
if attr_node:
fp_type = node_value(attr_node) or ""
except Exception:
pass
results.append({
"library": lib_name,
"name": fp_name,
"description": descr,
"tags": tags,
"pad_count": pad_count,
"type": fp_type,
})
if len(results) >= limit:
break
if len(results) >= limit:
break
return {
"success": True,
"output": f"Found {len(results)} footprint(s) matching '{query}' (searched {searched} libraries)",
"data": {
"results": results,
"query": query,
"libraries_searched": searched,
"truncated": len(results) >= limit,
},
}
def _count_pins(sym_node: list, tree: list) -> int:
"""Count pins in a symbol, following extends chains."""
from parsers.sexpr import find_node, find_nodes, node_value
# Check for extends
extends_node = find_node(sym_node, "extends")
if extends_node:
parent_name = node_value(extends_node)
if parent_name:
for sym in find_nodes(tree, "symbol"):
if len(sym) >= 2 and sym[1] == parent_name:
return _count_pins(sym, tree)
return 0
count = 0
for child in sym_node:
if isinstance(child, list) and child and child[0] == "symbol":
count += len(find_nodes(child, "pin"))
return count
def _extract_pins(sym_node: list, tree: list) -> list[dict]:
"""Extract pins from a symbol, following extends chains."""
from parsers.sexpr import find_node, find_nodes, node_value
# Check for extends
extends_node = find_node(sym_node, "extends")
if extends_node:
parent_name = node_value(extends_node)
if parent_name:
for sym in find_nodes(tree, "symbol"):
if len(sym) >= 2 and sym[1] == parent_name:
return _extract_pins(sym, tree)
return []
pins = []
for child in sym_node:
if isinstance(child, list) and child and child[0] == "symbol":
for pin in find_nodes(child, "pin"):
if len(pin) >= 3:
pin_type = pin[1] if isinstance(pin[1], str) else ""
pin_style = pin[2] if len(pin) > 2 and isinstance(pin[2], str) else ""
name_node = find_node(pin, "name")
number_node = find_node(pin, "number")
pins.append({
"type": pin_type,
"style": pin_style,
"name": node_value(name_node) or "",
"number": node_value(number_node) or "",
})
return pins
def _resolve_kicad_vars(uri: str, kicad_info: dict) -> str:
"""Resolve KiCad environment variables in URIs.
e.g., ${KICAD9_SYMBOL_DIR}/Device.kicad_sym
"""
if "${" not in uri:
return uri
base_dir = kicad_info.get("base_dir", "")
# Common KiCad environment variables
replacements = {
"KICAD9_SYMBOL_DIR": os.path.join(base_dir, "share", "kicad", "symbols") if base_dir else "",
"KICAD8_SYMBOL_DIR": os.path.join(base_dir, "share", "kicad", "symbols") if base_dir else "",
"KICAD7_SYMBOL_DIR": os.path.join(base_dir, "share", "kicad", "symbols") if base_dir else "",
"KICAD_SYMBOL_DIR": os.path.join(base_dir, "share", "kicad", "symbols") if base_dir else "",
"KICAD9_FOOTPRINT_DIR": os.path.join(base_dir, "share", "kicad", "footprints") if base_dir else "",
"KICAD8_FOOTPRINT_DIR": os.path.join(base_dir, "share", "kicad", "footprints") if base_dir else "",
"KICAD7_FOOTPRINT_DIR": os.path.join(base_dir, "share", "kicad", "footprints") if base_dir else "",
"KICAD_FOOTPRINT_DIR": os.path.join(base_dir, "share", "kicad", "footprints") if base_dir else "",
"KICAD9_3DMODEL_DIR": os.path.join(base_dir, "share", "kicad", "3dmodels") if base_dir else "",
"KICAD8_3DMODEL_DIR": os.path.join(base_dir, "share", "kicad", "3dmodels") if base_dir else "",
"KICAD7_3DMODEL_DIR": os.path.join(base_dir, "share", "kicad", "3dmodels") if base_dir else "",
}
# Also check OS environment variables
for var_name in re.findall(r'\$\{(\w+)\}', uri):
if var_name not in replacements:
env_val = os.environ.get(var_name, "")
if env_val:
replacements[var_name] = env_val
result = uri
for var_name, var_value in replacements.items():
result = result.replace(f"${{{var_name}}}", var_value)
return result