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 7 file(s)
7 files changed
+196−35
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.206+0.9.207
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.206",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.206/adom-bridge-kicad-v0.9.206.zip",- "sha256": "5fe9ac7c74cff82499faac9138ef9a36ced7ab012f040af0eb386cc585191054",- "size": 474127,+ "version": "0.9.207",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.207/adom-bridge-kicad-v0.9.207.zip",+ "sha256": "060fb8c7fb95348be3b82e9bef760019c98da26efb9f6d0fb7171236a1455fda",+ "size": 476698, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-08-20T12:40:00Z",+ "released_at": "2026-08-20T13:05: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.206",+ "version": "0.9.207", "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/install_footprint.py+57@@ -222,3 +222,60 @@ def handle_install_footprint(kicad_info: dict, args: dict) -> dict: "editorOpened": editor_opened, "model3d": model_installed or None, }+++def handle_list_footprints(kicad_info: dict, args: dict) -> dict:+ """kicad_list_footprints — what footprints actually exist, per library.++ Wiki issue #42 item 3 (John): a failure hint told callers to run+ kicad_list_footprints, and no such verb existed. Calling it returned the+ bridge's "valid commands" list, so a caller chasing a real failure lost a+ round trip and had every reason to think the bridge was broken. Rather than+ delete the hint, make it true - "does this footprint exist?" is the first+ question anyone asks when a load fails, and nothing else answered it.++ Read-only. Args: library (optional, defaults to every registered .pretty).+ """+ from pathlib import Path as _P+ import os as _os++ want = (args.get("library") or args.get("libraryName") or "").strip()+ user_dir = kicad_info.get("user_dir")+ if not user_dir:+ try:+ import kicad_detect+ _c, _u = kicad_detect.ensure_win_user_config(+ kicad_info.get("version") or "", kicad_info.get("base_dir"))+ user_dir = str(_u) if _u else None+ except Exception:+ user_dir = None+ if not user_dir:+ return {"success": False, "error": "KiCad user directory not found",+ "_hint": "Launch KiCad once so it creates Documents/KiCad/<ver>/."}++ roots = [_P(user_dir) / "footprints"]+ libraries, total = {}, 0+ for root in roots:+ if not root.is_dir():+ continue+ for pretty in sorted(root.glob("*.pretty")):+ name = pretty.stem+ if want and name.lower() != want.lower():+ continue+ names = sorted(p.stem for p in pretty.glob("*.kicad_mod"))+ libraries[name] = {"path": str(pretty).replace("\\", "/"),+ "count": len(names), "footprints": names}+ total += len(names)++ out = {"success": True, "libraryCount": len(libraries), "footprintCount": total,+ "libraries": libraries}+ if want and not libraries:+ out["_hint"] = (f"No library named '{want}' on this machine. Libraries present: "+ f"{', '.join(sorted(libraries)) or 'none'}. Install one with "+ f"kicad_install_footprint, which creates Adom.pretty on first use.")+ else:+ out["_hint"] = ("The footprint NAME is the .kicad_mod file stem, which is what "+ "kicad_show_footprint expects - it is often NOT the name declared "+ "inside the file. If a show_footprint failed, check the name here "+ "before assuming the editor is at fault.")+ return out
handlers/open_footprint_editor.py+93−26@@ -301,7 +301,54 @@ def _get_footprint_editor_title() -> str: return "" -def _background_navigate_win32_fp(fp_hwnd: int, filter_text: str, verify: str) -> bool:++def _parse_fp_title(title: str) -> tuple:+ """Split a Footprint Editor title into (library, footprint).++ KiCad titles the frame "<library>:<footprint> - Footprint Editor", with a+ leading '*' for unsaved changes."""+ t = (title or "").strip()+ for suffix in ("\u2014 Footprint Editor", "- Footprint Editor", "Footprint Editor"):+ if t.endswith(suffix):+ t = t[: -len(suffix)].strip()+ break+ t = t.lstrip("*").strip()+ if ":" not in t:+ return ("", "")+ lib, _, fp = t.partition(":")+ return (lib.strip(), fp.strip())+++def _fp_title_confirms(title: str, footprint: str, library: str = "") -> bool:+ """Does this title PROVE the requested footprint is loaded?++ 0.9.207, wiki issue #42 (John, P0). Two separate holes, both here:++ * the background tier used `verify in title` - a substring test, the same+ one that certified a wrong part for symbols in 0.9.194 and was fixed+ there but never here;+ * the allowFocusSteal keystroke tier confirmed NOTHING at all. It returned+ success on the basis of having typed, so two different requests both+ reported success while `Adom:s10b-ph-sm4-tb` stayed on screen.++ A caller that trusts success:true renders whatever window is up as evidence+ for the part it asked for, which is how a user got a green "Done" over+ screenshots of a different component. Compare the parsed library:footprint+ exactly instead."""+ lib, fp = _parse_fp_title(title)+ if not fp:+ return False+ low = (title or "").lower()+ if "no footprint loaded" in low:+ return False+ if fp.lower() != (footprint or "").lower():+ return False+ if library and lib.lower() != library.lower():+ return False+ return True++def _background_navigate_win32_fp(fp_hwnd: int, filter_text: str, verify: str,+ verify_lib: str = "") -> bool: """v0.9.89 (#21): plugin-free BACKGROUND footprint load — the same tier that fixed the Symbol Editor (0.9.86). WM_SETTEXT into the LIB_TREE filter, Down through the filter box to select the match, Enter posted to the tree@@ -340,8 +387,7 @@ def _background_navigate_win32_fp(fp_hwnd: int, filter_text: str, verify: str) - return cands def _title_ok():- t = (_get_footprint_editor_title() or "").lower()- return verify in t and "no footprint loaded" not in t+ return _fp_title_confirms(_get_footprint_editor_title() or "", verify, verify_lib) for hwnd, _r in _tree_candidates(): user32.PostMessageW(hwnd, WM_KEYDOWN, VK_RETURN, 0)@@ -783,7 +829,7 @@ def handle_open_footprint_editor(kicad_info: dict, args: dict) -> dict: # v0.9.89: plugin-free background load first (the 0.9.86 symbol-editor tier). _filter = f"{library_name} {footprint_name}".strip() if library_name else search_text _step(f"navigating the library tree to '{search_text}'", 80)- if _background_navigate_win32_fp(fp_hwnd, _filter, _verify):+ if _background_navigate_win32_fp(fp_hwnd, _filter, _verify, library_name): return { "success": True, "output": f"Footprint '{search_text}' loaded (background win32 path): {_get_footprint_editor_title()}",@@ -797,7 +843,7 @@ def handle_open_footprint_editor(kicad_info: dict, args: dict) -> dict: # what warms the library tree, so a second one is a genuinely different shot, # not a repeat of a hopeless call. Same change as open_symbol_editor. _step("first pass did not confirm; retrying now the tree is warm", 90)- if _background_navigate_win32_fp(fp_hwnd, _filter, _verify):+ if _background_navigate_win32_fp(fp_hwnd, _filter, _verify, library_name): return { "success": True, "output": f"Footprint '{search_text}' loaded (background win32 path, 2nd attempt): "@@ -819,31 +865,52 @@ def handle_open_footprint_editor(kicad_info: dict, args: dict) -> dict: "foreground:true to bring the editor to you."), } - nav_result = _navigate_to_footprint(fp_hwnd, search_text)-- if nav_result["status"] == "ok":- return {- "success": True,- "output": f"Footprint '{search_text}' loaded in the Footprint Editor: {nav_result['title']}",- }- elif nav_result["status"] == "partial":+ # P0 (#42): the keystroke tier used to report success from its OWN verdict -+ # "I typed, and the editor says it loaded something" - without ever checking+ # that the something was what the caller asked for. Two different requests+ # both returned success:true with `Adom:s10b-ph-sm4-tb` on screen. The+ # post-condition below is the same one the background tier already applies:+ # the TITLE must name the requested part, or this is a failure.+ def _steal_result(nav, attempt: int):+ title = _get_footprint_editor_title() or nav.get("title") or ""+ if _fp_title_confirms(title, footprint_name or search_text, library_name):+ return {+ "success": True,+ "output": f"Footprint '{search_text}' loaded in the Footprint Editor: {title}",+ "via": "keystroke-focus-steal",+ "attempts": attempt,+ } return {- "success": True,- "output": f"Footprint Editor loaded: {nav_result['title']}",+ "success": False,+ "editorOpened": True,+ "footprintLoaded": False,+ "errorCode": "wrong_footprint_on_screen",+ "attempts": attempt,+ "error": (f"The keystroke path ran but the Footprint Editor is not showing "+ f"'{search_text}' (title: {title or 'unknown'})."),+ "_hint": ("Reported as a FAILURE deliberately: the editor is displaying a "+ "different part, and a caller that trusted success here would "+ "screenshot the wrong footprint as evidence. Retry, or confirm the "+ "library:footprint pair exists with kicad_list_footprints."), }- elif nav_result["status"] == "noload":++ nav_result = _navigate_to_footprint(fp_hwnd, search_text)++ if nav_result["status"] in ("ok", "partial"):+ r = _steal_result(nav_result, 1)+ if r["success"]:+ return r+ # fall through to the retry below rather than failing on the first pass+ nav_result = {"status": "noload"}++ if nav_result["status"] == "noload": time.sleep(0.5) nav_result2 = _navigate_to_footprint(fp_hwnd, search_text)- if nav_result2["status"] == "ok":- return {- "success": True,- "output": f"Footprint '{search_text}' loaded in the Footprint Editor: {nav_result2['title']}",- }- elif nav_result2["status"] == "partial":- return {- "success": True,- "output": f"Footprint Editor loaded: {nav_result2['title']}",- }+ if nav_result2["status"] in ("ok", "partial"):+ r2 = _steal_result(nav_result2, 2)+ if r2["success"]:+ return r2+ return r2 else: return { "success": False,
handlers/show.py+36@@ -349,6 +349,42 @@ def handle_show_3d_chip(kicad_info: dict, args: dict) -> dict: r = handle_open_3d_viewer(kicad_info, {**a, "editor": "fp"}) r = _verify_window(r, _find_3d_viewer, "3D Viewer") r.setdefault("surface", "3d_chip")++ # Wiki issue #42 item 4 (John): "the 3D Viewer window is titled just+ # '3D Viewer', with no part identity anywhere, so nothing downstream can+ # confirm which part is on screen - we open it and simply cannot check."+ #+ # KiCad owns that title and we are not going to rewrite it, but the 3D chip+ # view is rendered FROM the Footprint Editor, and that window's title does+ # name the part. So echo the part this viewer was opened from, and say where+ # the claim comes from rather than asserting it out of thin air.+ want = (a.get("footprintName") or a.get("footprint") or "").strip()+ if isinstance(r, dict) and r.get("success"):+ src_title = ""+ try:+ h = _find_footprint_editor()+ if h:+ src_title = _window_title(h)+ except Exception:+ pass+ from handlers.open_footprint_editor import _parse_fp_title+ lib, fp = _parse_fp_title(src_title)+ r["shows"] = {+ "requested": want or None,+ "footprint": fp or None,+ "library": lib or None,+ "sourceWindow": src_title or None,+ "verified": bool(want and fp and fp.lower() == want.lower()),+ "_hint": ("The 3D Viewer's own title carries no part identity, so this is read "+ "from the Footprint Editor the view was rendered from. verified:true "+ "means that editor is on the part you asked for; false means it is "+ "not, and the 3D view is therefore of something else."),+ }+ if want and not r["shows"]["verified"]:+ r["success"] = False+ r["errorCode"] = "wrong_part_in_3d_view"+ r["error"] = (f"3D Viewer opened, but it was rendered from '{fp or 'unknown'}' "+ f"rather than the requested '{want}'.") return r
server.py+3−2@@ -41,7 +41,7 @@ from handlers.close_windows import ( ) from handlers.install_library import handle_install_library from handlers.install_symbol import handle_install_symbol-from handlers.install_footprint import handle_install_footprint+from handlers.install_footprint import handle_install_footprint, handle_list_footprints from handlers.place_footprint import handle_place_footprint from handlers.run_drc import handle_run_drc from handlers.fix_keyboard import handle_fix_keyboard@@ -727,6 +727,7 @@ COMMAND_HANDLERS = { "uninstall": handle_uninstall, "install_plugin": plugin_install.handle_install_plugin, "plugin_diagnose": handle_plugin_diagnose,+ "list_footprints": handle_list_footprints, "progress": _handle_progress, # v1.7.12+: headless lint pre-checks via kicad-cli "run_erc": handle_run_erc,@@ -917,7 +918,7 @@ def dispatch_command(command: str, args: dict) -> dict: _NO_GUI = {"run_drc", "run_erc", "lint_board", "lint_schematic", "lint_library", "format_upgrade", "export_gerber", "export_gerbers", "export_pdf", "export_svg", "export_step", "export_bom_csv", "adom_library_status",- "list_versions", "install_library", "progress"}+ "list_versions", "install_library", "progress", "list_footprints"} # Only verbs that actually SPAWN a window get the ~3s settle-poll (they can # trigger an async dialog). Read-only reporters (open_editors, screenshot_all, # window_info) don't spawn windows → one cheap scan, no needless 3s poll.