app
KiCad - the 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.
← Commit history
0.9.283: Wiki #73: kicad_list_design_rules and kicad_set_design_rules consume adom/pcb-design-rules at call time (fab, layers, process; layers defaults to the board's copper count; stages <stem>.kicad_dru with backup; runs DRC with it). Wiki #62: kicad_list_symbols (bridge inventory + kicadParser), and kicad_show_library resolves an MPN/file name to the real symbol name (exact, case-insensitive, unique prefix) reporting aliasedFrom/aliasHow. Navigation trace no longer implies the filter was not applied when wx answers WM_GETTEXT with ''. kicad-bridge-dev skill records the #71 lessons.
2 files changed
+78−1
handlers/open_symbol_editor.py+64−1@@ -513,7 +513,10 @@ def _background_navigate_win32(sym_hwnd: int, filter_text: str, verify: str, return False time.sleep(1.4) # LIB_TREE search debounce runs on the UI thread (~200-500ms) + tree rebuild trace["editTextAfterSet"] = _edit_text(edit)- trace["steps"].append(f"WM_SETTEXT -> edit reads {trace['editTextAfterSet']!r}")+ # The read-back is unreliable: wx answers WM_GETTEXT for its own windows+ # with '' even while the box visibly holds the filter (screenshot-verified+ # 2026-09-03). Record it, but do not let '' read as "filter not applied".+ trace["steps"].append(f"WM_SETTEXT sent (read-back {trace['editTextAfterSet']!r}; wx often answers '' even when the box shows the text)") VK_DOWN = 0x28 # Down THROUGH the filter box: KiCad forwards nav keys to the tree, which # selects the first filtered match (verified live: Device:R row highlights).@@ -699,7 +702,67 @@ def _mac_open_symbol_editor(kicad_info: dict, args: dict) -> dict: return {"success": True, "output": "Symbol Editor opened."} ++def _resolve_symbol_alias(kicad_info: dict, requested: str) -> tuple[str | None, str]:+ """Map a caller's name onto a real symbol name in the Adom library.++ Exact match wins. Then case-insensitive. Then a UNIQUE prefix match in+ either direction (MPN longer than the symbol name, or the reverse). Returns+ (name, how); (None, reason) when nothing matches or the match is ambiguous.+ """+ from adom_library import _get_paths, list_symbols # local: avoids an import cycle at load+ _table_path, lib_path = _get_paths(kicad_info) # (sym-lib-table, Adom.kicad_sym)+ if not lib_path or not Path(lib_path).exists():+ return None, "library file not found"+ names = list_symbols(Path(lib_path)) or []+ if requested in names:+ return requested, "exact"+ low = {n.lower(): n for n in names}+ if requested.lower() in low:+ return low[requested.lower()], "case-insensitive"+ r = requested.lower()+ pref = [n for n in names if n.lower().startswith(r) or r.startswith(n.lower())]+ if len(pref) == 1:+ return pref[0], "unique prefix match"+ if len(pref) > 1:+ return None, f"ambiguous: {pref[:5]}"+ return None, "no match"++ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict:+ """show_library / open_symbol_editor entry point.++ Wiki #62 finding 2: the file is named for the MPN but the symbol inside is+ not (CL21A226MPQNNNE.kicad_sym declares "CL21A226MPQNNN"). Resolve the+ caller's name against the library's real names BEFORE navigating and say+ so (aliasedFrom / aliasHow) on whatever the inner handler returns, instead+ of hunting for a name that does not exist and then blaming the editor.+ """+ note: dict = {}+ symbol_name = args.get("symbolName", "") or ""+ library_name = args.get("libraryName", "") or args.get("library", "") or ""+ if symbol_name and (not library_name or library_name.lower() == "adom"):+ try:+ resolved, how = _resolve_symbol_alias(kicad_info, symbol_name)+ if resolved and resolved != symbol_name:+ note = {"aliasedFrom": symbol_name, "aliasHow": how}+ args = {**args, "symbolName": resolved}+ elif not resolved and how.startswith("ambiguous"):+ note = {"aliasNote": f"'{symbol_name}' is not in the library and the prefix match is {how}"}+ elif not resolved and how == "no match":+ note = {"aliasNote": f"'{symbol_name}' is not a symbol name in the Adom library (no exact, case-insensitive or prefix match)"}+ except Exception: # pylint: disable=broad-except+ pass+ out = _handle_open_symbol_editor_inner(kicad_info, args)+ if isinstance(out, dict) and note:+ out = {**out, **note}+ if note.get("aliasedFrom"):+ out["_hint"] = (f"Resolved '{note['aliasedFrom']}' to symbol '{args['symbolName']}' ({note['aliasHow']}). "+ + str(out.get("_hint") or ""))+ return out+++def _handle_open_symbol_editor_inner(kicad_info: dict, args: dict) -> dict: """Open the KiCad Symbol Editor and optionally load a specific symbol. Args:
server.py+14@@ -63,6 +63,8 @@ from handlers.kicad_cli_lint import ( handle_run_erc, handle_lint_board, handle_lint_schematic, handle_lint_library, handle_format_upgrade, )+from handlers.design_rules import handle_list_design_rules, handle_set_design_rules # wiki #73+from handlers.list_symbols import handle_list_symbols # wiki #62 # v0.7.1+: export handlers (Gerber/PDF/SVG/STEP/BOM_CSV) via kicad-cli. The # functions have existed in handlers/export.py for a while but were never wired # into COMMAND_HANDLERS — kicad_export_gerbers/pdf/svg/step/bom_csv all returned@@ -440,6 +442,15 @@ _VERB_CATALOG = { "place_footprint": {"summary": "Deterministically place a footprint into a board preview.", "long": False, "hint": "Splices a footprint into a board via s-expr (no GUI); good for previews.", "related": ["kicad_open_board", "kicad_install_footprint"], "pitfalls": ["operates on the file — close the board in KiCad first or the edit races the GUI"]},+ "list_design_rules": {"summary": "List the fabs / processes / layer counts on adom/pcb-design-rules.", "long": False,+ "hint": "kicad_list_design_rules {\"fab\":\"jlcpcb\"} (fab optional). Rows come from the wiki page at call time; InstaPCB is the house baseline.",+ "related": ["kicad_set_design_rules", "kicad_lint_board"], "pitfalls": ["needs wiki.adom.inc reachable from the desktop"]},+ "set_design_rules": {"summary": "Stage a fab's .kicad_dru next to a board and run DRC with it.", "long": False,+ "hint": "kicad_set_design_rules {\"fab\":\"adom-instapcb\",\"layers\":2,\"boardPath\":\"C:/.../x.kicad_pcb\"}. layers defaults to the board's copper count; druPath overrides the wiki file; runDrc:false to only stage.",+ "related": ["kicad_list_design_rules", "kicad_lint_board", "kicad_run_drc"], "pitfalls": ["replaces an existing <stem>.kicad_dru (backed up next to it); pass keepExisting:true to refuse instead", "an open PCB editor applies the file on its next DRC run"]},+ "list_symbols": {"summary": "Names in a registered symbol library, with KiCad's own inventory.", "long": False,+ "hint": "kicad_list_symbols {\"libraryName\":\"Adom\"} (default Adom; or libraryPath). Use a returned name as symbolName; the file name is often an MPN, not the symbol name.",+ "related": ["kicad_show_library", "kicad_adom_library_status", "kicad_lint_library"], "pitfalls": ["kicadParser.symbols is what the editor can show; symbols is the bridge's reader"]}, "run_drc": {"summary": "Run DRC on a board via kicad-cli (read-only).", "long": False, "hint": "Absolute board path; runs kicad-cli DRC and returns violations. Doesn't need KiCad open.", "related": ["kicad_lint_board", "kicad_open_board", "kicad_run_erc"], "pitfalls": ["needs KiCad >= 7 (kicad-cli); older installs lack it"]},@@ -931,6 +942,9 @@ COMMAND_HANDLERS = { "find_connections": handle_find_connections, "analyze_connections": handle_analyze_connections, "lint_board": handle_lint_board,+ "list_design_rules": handle_list_design_rules,+ "set_design_rules": handle_set_design_rules,+ "list_symbols": handle_list_symbols, "lint_schematic": handle_lint_schematic, # v1.7.13+: library lint + file-format upgrade "lint_library": handle_lint_library,