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
v0.9.134: menu ids resolved LIVE via the plugin's get_menu_ids (10.0.5 renumbered eeschema 20390->20391); demo warm launches the PM so the part beats' in-process route has a bound target
8 files changed
+63−11
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.133+0.9.134
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.133",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.133/adom-bridge-kicad-v0.9.133.zip",- "sha256": "b89504a941f9c10bd884af64e9e66819290ed3751cca4f5b8750f156e1b3aadf",- "size": 414617,+ "version": "0.9.134",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.134/adom-bridge-kicad-v0.9.134.zip",+ "sha256": "2e2586a98ab1fc48e47ac01cf507dd0a28de2e76777df1eca02d9e15cb216b23",+ "size": 415193, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-08-16T20:19:46Z",+ "released_at": "2026-08-16T20:27:28Z", "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png", "languages": [ "Python",
adom-bridge-kicad-v0.9.134.zipadded⋯ 1 unchanged line ⋯
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.133",+ "version": "0.9.134", "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/bridge_client.py+26@@ -444,6 +444,32 @@ def handle_open_editors(kicad_info: dict, args: dict) -> dict: # ── Phase 3 — three-tier fallback for menu-driven actions ────────────── +def find_menu_id_via_plugin(exe_name: str, *label_contains: str) -> int | None:+ """Resolve a menu command id from the LIVE process's own menu bar via the+ plugin's get_menu_ids RPC — never trust hardcoded ids across KiCad versions+ (10.0.5 renumbered eeschema's Symbol Editor 20390 -> 20391, found live+ 2026-08-16). Returns the id or None when no plugin/match."""+ try:+ meta, err = _resolve_plugin(exe_name)+ if err or not meta:+ return None+ rpc = call(meta["port"], "get_menu_ids", {}, timeout=8.0)+ result = rpc.get("result")+ frames = result if isinstance(result, list) else (result or {}).get("frames") or []+ wants = [w.lower() for w in label_contains]+ for frame in frames:+ for it in (frame or {}).get("menus") or []:+ label = str(it.get("label") or "").lower()+ if all(w in label for w in wants):+ try:+ return int(it.get("id"))+ except (TypeError, ValueError):+ continue+ except Exception:+ pass+ return None++ def try_wm_command_via_bridge(exe_name: str, menu_id: int, frame_index: int = 0, pid: int | None = None, timeout: float = 10.0) -> dict: """Tier 1 of Phase 3's 3-tier fallback chain.
handlers/demo.py+9@@ -299,6 +299,7 @@ def _prepare(kicad_info: dict) -> dict: (d / "adom-demo.kicad_pcb").write_text(_board(kicad_info), encoding="utf-8") (d / "adom-demo.kicad_pro").write_text('{"board":{},"meta":{"filename":"adom-demo.kicad_pro","version":1},"schematic":{},"sheets":[]}', encoding="utf-8") out["projectDir"] = str(d)+ out["project"] = str(d / "adom-demo.kicad_pro") out["schematic"] = str(d / "adom-demo.kicad_sch") out["board"] = str(d / "adom-demo.kicad_pcb") return out@@ -331,6 +332,14 @@ def _warm_kicad(kicad_info: dict, prep: dict) -> dict: # "Could not open the Footprint Editor via the background (plugin) path". # So open BOTH documents up front; every later beat then takes the fast # in-process plugin pathway.+ # Launch the PROJECT MANAGER first: the part beats' most reliable route is+ # the PM's in-process plugin post, so kicad.exe must exist and its plugin+ # must have bind time — the design beats provide that time for free.+ try:+ from handlers.launch import handle_launch+ handle_launch(kicad_info, {"project": prep.get("project") or "", "waitSeconds": 15})+ except Exception:+ pass sch = handle_open_schematic(kicad_info, {"filePath": prep["schematic"]}) time.sleep(6) brd = handle_open_board(kicad_info, {"filePath": prep["board"]})
handlers/open_footprint_editor.py+12@@ -484,6 +484,13 @@ def handle_open_footprint_editor(kicad_info: dict, args: dict) -> dict: # running, and whose first-run "KiCad Setup" wizard otherwise blocks the icon # click). Mirrors how open_symbol_editor drives a live eeschema. (ADOMBASELINE 2026-07.) PCBNEW_FOOTPRINT_EDITOR_MENU_ID = 20572 # noqa: N806+ try:+ from handlers.bridge_client import find_menu_id_via_plugin+ _live = find_menu_id_via_plugin("pcbnew", "footprint editor")+ if _live:+ PCBNEW_FOOTPRINT_EDITOR_MENU_ID = _live # noqa: N806+ except Exception:+ pass if not fp_hwnd: try: from handlers.bridge_client import try_wm_command_via_bridge@@ -534,6 +541,11 @@ def handle_open_footprint_editor(kicad_info: dict, args: dict) -> dict: if not fp_hwnd: try: from handlers.bridge_client import try_wm_command_via_bridge+ try:+ from handlers.bridge_client import find_menu_id_via_plugin+ _pm_fp_id = find_menu_id_via_plugin("kicad", "footprint editor") or _pm_fp_id+ except Exception:+ pass tier1 = try_wm_command_via_bridge( exe_name="kicad", menu_id=_pm_fp_id or KICAD_FOOTPRINT_EDITOR_CMD_ID, frame_index=0, )
handlers/open_symbol_editor.py+9−4@@ -434,9 +434,12 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict: return h, t return None, t - # 1a: eeschema's own menu, if eeschema is alive.- sym_hwnd, tier1 = _post_and_wait("eeschema", 20390, "eeschema-20390", 12)- tier1_source = "eeschema-20390"+ # 1a: eeschema's own menu, if eeschema is alive — id resolved from+ # the LIVE menu bar (10.0.5 renumbered 20390 -> 20391).+ from handlers.bridge_client import find_menu_id_via_plugin+ _ee_id = find_menu_id_via_plugin("eeschema", "symbol editor") or 20390+ sym_hwnd, tier1 = _post_and_wait("eeschema", _ee_id, f"eeschema-{_ee_id}", 12)+ tier1_source = f"eeschema-{_ee_id}" if not sym_hwnd: # 1b: the kicad project manager, IN-PROCESS. On 10.0.5 this is # the ONLY reliable route from a running PM: the external@@ -449,9 +452,11 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict: # (measured live on 10.0.5, 2026-08-16; 15s of retries all hit # plugin_not_running while a minutes-old PM answered # instantly). Retry for up to ~45s.+ _pm_id = None for _attempt in range(15):+ _pm_id = _pm_id or find_menu_id_via_plugin("kicad", "symbol editor") sym_hwnd, tier1 = _post_and_wait(- "kicad", KICAD_SYMBOL_EDITOR_CMD_ID, "kicad-20011", 24)+ "kicad", _pm_id or KICAD_SYMBOL_EDITOR_CMD_ID, "kicad-pm", 24) if sym_hwnd: tier1_source = "kicad-20011" break