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
Update 5 file(s)
5 files changed
+90−15
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.193+0.9.194
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.193",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.193/adom-bridge-kicad-v0.9.193.zip",- "sha256": "d22381f4e66f2699baa2d1d0d19102ed64e4fc2b7099859bb29403e8b7685775",- "size": 462071,+ "version": "0.9.194",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.194/adom-bridge-kicad-v0.9.194.zip",+ "sha256": "eac65abb0e01fa6fca01404fed453ed9d0e1c330e39351dda33de587c666e098",+ "size": 463369, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-08-19T21:55:00Z",+ "released_at": "2026-08-19T23:10:00Z", "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png", "languages": [ "Python",
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.193",+ "version": "0.9.194", "description": "Reverse bridge for KiCad \u2014 board/schematic introspection, lint via kicad-cli, plugin install, multi-instance probe, in-process DRC.", "homepage": "https://wiki.adom.inc/adom/adom-bridge", "author": "Adom Inc.",
handlers/open_symbol_editor.py+54−6@@ -38,6 +38,51 @@ SW_RESTORE = 9 GW_OWNER = 4 +def _parse_editor_title(title: str) -> tuple[str, str]:+ """Split a Symbol Editor title into (library, symbol).++ KiCad titles the frame "<library>:<symbol> — Symbol Editor", with a leading+ '*' when there are unsaved changes. Returns ("", "") when the title is not+ in that shape (no part loaded yet)."""+ t = (title or "").strip()+ for suffix in ("— Symbol Editor", "- Symbol Editor", "Symbol Editor"):+ if t.endswith(suffix):+ t = t[: -len(suffix)].strip()+ break+ t = t.lstrip("*").strip()+ if ":" not in t:+ return ("", "")+ lib, _, sym = t.partition(":")+ return (lib.strip(), sym.strip())+++def _title_confirms(title: str, symbol: str, library: str = "") -> bool:+ """Does this title prove THE REQUESTED part is loaded?++ 0.9.194: this used to be `requested.lower() in title.lower()`, and a+ substring test is not proof. Reproduced live on ConfRoomROG: asking for+ symbol RP2040 in library Adom filtered the tree to "Adom RP2040", KiCad+ selected the first symbol of a DIFFERENT library that happened to be called+ AdomRP2040, and the title "AdomRP2040:ADOM_MECHANICAL_PIN — Symbol Editor"+ contains "rp2040" - so the bridge reported success for a part that does not+ exist in the requested library, with an unrelated symbol on screen. A caller+ then screenshots that as evidence. Compare the parsed library:symbol+ exactly instead."""+ lib, sym = _parse_editor_title(title)+ if not sym:+ return False+ if "no symbol loaded" in (title or "").lower():+ return False+ if sym.lower() != (symbol or "").lower():+ return False+ # The library is only checked when the caller named one. KiCad reports the+ # library the part was actually loaded from, so a mismatch means we landed+ # in a different library with a same-named part - still not what was asked.+ if library and lib.lower() != library.lower():+ return False+ return True++ def _symbol_on_disk_but_not_in_editor(kicad_info: dict, name: str) -> bool: """True when `name` IS in the Adom library file but a Symbol Editor that opened earlier cannot see it.@@ -235,7 +280,8 @@ def _plugin_background_load(symbol_name: str, library_name: str = "", return {"loaded": False, "_error": f"{type(e).__name__}: {e}"} -def _background_navigate_win32(sym_hwnd: int, filter_text: str, verify: str) -> bool:+def _background_navigate_win32(sym_hwnd: int, filter_text: str, verify: str,+ verify_lib: str = "") -> bool: """v0.9.83 (#20/#21): plugin-free BACKGROUND symbol load. WM_SETTEXT into the LIB_TREE's filter Edit (fires EN_CHANGE -> KiCad filters and auto-selects the best match on its debounce timer), then Enter posted to the Edit (KiCad's@@ -283,8 +329,10 @@ def _background_navigate_win32(sym_hwnd: int, filter_text: str, verify: str) -> return cands def _title_ok():- t = (_get_symbol_editor_title() or "").lower()- return verify in t and "no symbol loaded" not in t+ # The sentinel means "no title check applies" (library-browse mode).+ if verify == "\u0000never":+ return False+ return _title_confirms(_get_symbol_editor_title() or "", verify, verify_lib) # Round 1: Enter posted straight to each tree candidate (the payload's own # in-process technique, aimed from outside).@@ -674,7 +722,7 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict: if str(_pl.get("loaded")) in ("True", "unverified"): time.sleep(0.6) _title = _get_symbol_editor_title() if "_get_symbol_editor_title" in globals() else ""- if _pl.get("loaded") is True or (_verify in (_title or "").lower()):+ if _pl.get("loaded") is True or _title_confirms(_title or "", _verify, library_name): return { "success": True, "output": f"Symbol '{search_text}' loaded (background plugin path): {_title or _pl.get('frameTitle','')}",@@ -685,7 +733,7 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict: # v0.9.83: plugin-free win32 tier — filter via WM_SETTEXT + Enter, background. # This is the workhorse while eeschema's Python is cold (issue #21). _filter = f"{library_name} {symbol_name}".strip() if library_name else search_text- if _background_navigate_win32(sym_hwnd, _filter, _verify):+ if _background_navigate_win32(sym_hwnd, _filter, _verify, library_name): return { "success": True, "output": f"Symbol '{search_text}' loaded (background win32 path): {_get_symbol_editor_title()}",@@ -705,7 +753,7 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict: # own retry loop out through the API. Do it here, once, before failing. # The first attempt is what warms the tree, so this is a fresh-tree retry, # not a duplicate of a hopeless call.- if _background_navigate_win32(sym_hwnd, _filter, _verify):+ if _background_navigate_win32(sym_hwnd, _filter, _verify, library_name): return { "success": True, "output": f"Symbol '{search_text}' loaded (background win32 path, 2nd attempt): "
handlers/plugin_diagnose.py+29−2@@ -118,13 +118,40 @@ def handle_plugin_diagnose(kicad_info: dict, args: dict) -> dict: except Exception as e: report["live"] = {"error": str(e)[:120]} + # 0.9.194: the loader log is written by ANY process that runs the bootstrap,+ # including the bridge's own short-lived `python.exe -c ...` probes. Judging+ # "did the plugin run" from those lines is how this verb told a live box+ # "IMPORTED BUT DID NOT START" while no KiCad was running at all - a+ # confident answer to a question nobody could answer yet. Split the log by+ # process and only count GUI lines as evidence about KiCad.+ gui_names = ("kicad", "eeschema", "pcbnew", "gerbview", "pl_editor", "bitmap2component")+ gui_log, probe_log = [], []+ for line in report["loaderLog"]:+ low = line.lower()+ (gui_log if any(f"in {n}.exe" in low for n in gui_names) else probe_log).append(line)+ report["loaderLogGui"] = gui_log+ report["loaderLogProbes"] = probe_log++ try:+ from handlers.kicad_ui import _find_all_kicad_windows+ kicad_running = bool(_find_all_kicad_windows())+ except Exception:+ kicad_running = bool(report.get("instances"))+ report["kicadRunning"] = kicad_running+ # the verdict, so a human does not have to correlate five sections by hand deployed = any(d["loader"].get("exists") for d in pdirs) imported = any(d["importedByKiCad"] for d in pdirs)- ran = bool(report["loaderLog"])+ ran = bool(gui_log) # GUI evidence only, never a probe process alive = (report.get("live") or {}).get("alive") or 0 if alive: verdict = "WORKING: the plugin is bound and answering."+ elif not kicad_running:+ verdict = ("KICAD NOT RUNNING: there is nothing for the plugin to load into "+ "yet, so this says nothing about whether it works. Launch KiCad "+ "and open a board editor, then re-run. "+ + ("The loader IS deployed." if deployed else+ "The loader is NOT deployed - run kicad_install_plugin.")) elif not deployed: verdict = ("NOT DEPLOYED: adom_bridge_plugin.py is missing from KiCad's " "scripting/plugins. Run kicad_install_plugin.")@@ -135,7 +162,7 @@ def handle_plugin_diagnose(kicad_info: dict, args: dict) -> dict: "has been opened and this still says never imported, this KiCad " "build does not scan that directory and the hook has to change.") elif ran and not alive:- verdict = ("IMPORTED BUT DID NOT START: read loaderLog below - it names the "+ verdict = ("IMPORTED BUT DID NOT START: read loaderLogGui below - it names the " "exact failure. 'No module named X' means KiCad's embedded stdlib " "is incomplete (see embeddedStdlib); a repair install fixes it.") else: