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 4 file(s)
4 files changed
+94−12
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.216+0.9.217
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.216",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.216/adom-bridge-kicad-v0.9.216.zip",- "sha256": "dbb8dd91e509f0f7c5f80fa24ec1190dc8bc7331777f64aad304f27ed560273e",- "size": 483198,+ "version": "0.9.217",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.217/adom-bridge-kicad-v0.9.217.zip",+ "sha256": "3430f247cb8244f9cd8897bc993b07c78225ee3511766a360e910d94f72be932",+ "size": 484313, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-08-20T18:20:00Z",+ "released_at": "2026-08-20T18:45: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.216",+ "version": "0.9.217", "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/show.py+87−5@@ -77,7 +77,10 @@ def handle_show_symbol(kicad_info: dict, args: dict) -> dict: a.get("libraryName") or a.get("library") or "") if already: _ev = _capture_evidence(already, "symbol-editor-reused")+ _sh = _build_shot(already, "symbol-editor-reused",+ a.get("captureMaxWidth", _SHOT_DEFAULT_MAX)) if a.get("capture") else None return {"success": True, "surface": "symbol", "reused": True,+ **({"shot": _sh} if _sh else {}), "window": already.get("title"), **({"evidence": _ev} if _ev else {}), "output": f"Symbol Editor is already showing {already.get('title')} - "@@ -87,7 +90,7 @@ def handle_show_symbol(kicad_info: dict, args: dict) -> dict: "opens, so reuse is the difference between about a second and " "about a minute (wiki issue #35)."} r = handle_open_symbol_editor(kicad_info, a)- r = _verify_window(r, _find_symbol_editor, "Symbol Editor")+ r = _verify_window(r, _find_symbol_editor, "Symbol Editor", args=a) r.setdefault("surface", "symbol") return r @@ -202,6 +205,81 @@ def _capture_evidence(win, label: str) -> dict | None: return None ++_SHOT_DEFAULT_MAX = 640+_SHOT_MIN_MAX, _SHOT_MAX_MAX = 120, 1600+++def _build_shot(win, label: str, max_width: int) -> dict | None:+ """The ab gallery-item `shot` contract (wiki #45, settled in adom-bridge#91).++ Why this exists next to `evidence` rather than inside it: the wiki Quick+ Actions caller is a BROWSER. It has no container for ab to auto-pull+ `fullPath` into, and the /command response carries no image bytes, so a page+ cannot render the thumbnail beside "opened in KiCad". Colby's split is the+ right one and is kept here deliberately:++ evidence -> render PROOF (canvasRendered, hwnd, window)+ shot -> the FRAME (bytes a browser can paint)++ Opt-in via capture:true, so no existing caller pays for bytes it cannot use.+ """+ try:+ import base64+ if isinstance(win, dict):+ hwnd, title = win.get("hwnd"), win.get("title")+ else:+ hwnd, title = win, _window_title(win)+ if not hwnd:+ return None+ try:+ mw = int(max_width)+ except Exception:+ mw = _SHOT_DEFAULT_MAX+ mw = max(_SHOT_MIN_MAX, min(_SHOT_MAX_MAX, mw))++ from handlers.kicad_ui import _screenshot_hwnd+ shot = _screenshot_hwnd(hwnd, label=f"shot-{label}", max_dim=mw)+ path = (shot or {}).get("fullPath") or (shot or {}).get("savedTo")+ if not path:+ return None+ raw = open(path, "rb").read()+ b64 = base64.b64encode(raw).decode()+ # The MIME belongs to whoever encodes the bytes. Colby drops+ # thumbnailDataUrl straight into <img src> and does NOT rebuild it from+ # `image`, so a mislabelled prefix is our bug, not his. Derive it from+ # what we actually wrote rather than assuming PNG.+ ext = str(path).lower().rsplit(".", 1)[-1]+ mime = {"png": "image/png", "bmp": "image/bmp", "webp": "image/webp"}.get(ext, "image/png")+ return {+ "title": title,+ "image": b64,+ "thumbnailDataUrl": f"data:{mime};base64,{b64}",+ "maxWidth": mw,+ "path": str(path),+ "_hint": ("thumbnailDataUrl drops straight into an <img src>. `image` is the same "+ "bytes raw for container callers, and `path` is the on-box file ab "+ "auto-pulls. Request it with capture:true; captureMaxWidth defaults to "+ f"{_SHOT_DEFAULT_MAX} and clamps to {_SHOT_MIN_MAX}-{_SHOT_MAX_MAX}."),+ }+ except Exception:+ return None+++def _attach_shot(result: dict, win, label: str, args: dict) -> dict:+ """Attach `shot` when the caller asked for it. Never replaces `evidence`."""+ try:+ if not isinstance(result, dict) or not (args or {}).get("capture"):+ return result+ sh = _build_shot(win, label, (args or {}).get("captureMaxWidth", _SHOT_DEFAULT_MAX))+ if sh:+ result["shot"] = sh+ else:+ result["shotUnavailable"] = "capture was requested but the frame could not be grabbed"+ except Exception:+ pass+ return result+ def _find_symbol_editor(): return _find_by_title("symbol editor") @@ -210,7 +288,8 @@ def _find_3d_viewer(): return _find_by_title("3d viewer") -def _verify_window(result: dict, finder, label: str, waits: int = 12) -> dict:+def _verify_window(result: dict, finder, label: str, waits: int = 12,+ args: dict = None) -> dict: """A show_* verb has ONE job: put THAT surface on screen. If its window is not there, the verb FAILS - even when every internal step reported success. @@ -231,7 +310,7 @@ def _verify_window(result: dict, finder, label: str, waits: int = 12) -> dict: result["evidence"] = ev else: result["evidenceUnavailable"] = "window verified but the capture failed"- return result+ return _attach_shot(result, win, label, args or {}) time.sleep(0.5) return {**result, "success": False, "error": f"{label} reported success but its window never appeared",@@ -318,7 +397,10 @@ def handle_show_footprint(kicad_info: dict, args: dict) -> dict: a2.get("library") or a2.get("libraryName") or "") if already_fp: _ev = _capture_evidence(already_fp, "footprint-editor-reused")+ _sh = _build_shot(already_fp, "footprint-editor-reused",+ a2.get("captureMaxWidth", _SHOT_DEFAULT_MAX)) if a2.get("capture") else None return {"success": True, "surface": "footprint", "reused": True,+ **({"shot": _sh} if _sh else {}), "window": already_fp.get("title"), **({"evidence": _ev} if _ev else {}), "output": f"Footprint Editor is already showing {already_fp.get('title')} - "@@ -327,7 +409,7 @@ def handle_show_footprint(kicad_info: dict, args: dict) -> dict: "(wiki issue #35)."} ensure_plugin_ready(kicad_info) r = handle_open_footprint_editor(kicad_info, a2)- r = _verify_window(r, _find_footprint_editor, "Footprint Editor")+ r = _verify_window(r, _find_footprint_editor, "Footprint Editor", args=a2) r.setdefault("surface", "footprint") return r @@ -347,7 +429,7 @@ def handle_show_3d_chip(kicad_info: dict, args: dict) -> dict: break time.sleep(0.5) r = handle_open_3d_viewer(kicad_info, {**a, "editor": "fp"})- r = _verify_window(r, _find_3d_viewer, "3D Viewer")+ r = _verify_window(r, _find_3d_viewer, "3D Viewer", args=a) r.setdefault("surface", "3d_chip") # Wiki issue #42 item 4 (John): "the 3D Viewer window is titled just