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.297: Tour: motion on every beat for the recording (fit, wheel zoom in/out, middle-drag pans in the 2D editors; Home, an isometric left-drag and three orbits with zoom in the 3D viewers), sent only while the beat's window is confirmed in front and aborted at the next gesture on STOP or when the user switches away. The board-in-3D beat now closes the chip's 3D viewer first and picks the viewer owned by the PCB Editor (the last beat was showing the chip's viewer by title match). STOP from the remote's X now clears the caption and ends the tour within the beat, audio already off. Per-beat motion reports ride on the result.
3 files changed
+95−13
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.296+0.9.297
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.296",+ "version": "0.9.297", "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/demo.py+93−11@@ -505,7 +505,9 @@ def _run_step(kicad_info: dict, step: str, prep: dict) -> dict: if step == "footprint3d": import time; time.sleep(2) r = handle_open_3d_viewer(kicad_info, {"editor": "footprint"})+ time.sleep(2) return {"ok": bool(r.get("success")), "window": "3D Viewer",+ "hwnd": _viewer_3d_owned_by("Footprint Editor"), "title": "3/6 · The chip in 3D", "say": ("And the same chip in three dimensions" + (", the LQFP-48 body from KiCad's own 3D library sitting on its pads."@@ -547,8 +549,12 @@ def _run_step(kicad_info: dict, step: str, prep: dict) -> dict: except Exception: pass time.sleep(2)+ _close_hwnd(_viewer_3d_owned_by("Footprint Editor")) # the chip's viewer must not win the title match+ time.sleep(1) r = handle_open_3d_viewer(kicad_info, {"editor": "pcb"})- return {"ok": bool(r.get("success")), "window": "3D Viewer",+ time.sleep(3)+ v = _viewer_3d_owned_by("PCB Editor")+ return {"ok": bool(r.get("success")) and bool(v), "window": "3D Viewer", "hwnd": v, "title": "6/6 · The board in 3D", "say": ("And the finished ESC in 3D. This is the payoff: from one symbol to a rendered " "assembly, every step driven on your own machine, and each window brought to "@@ -585,7 +591,7 @@ def _job_update(**kw): _JOB.update(kw) -def _foreground_once(window_title: str) -> bool:+def _foreground_once(window_title: str, hwnd: int | None = None) -> bool: """Bring the window this beat opened to the front ONCE, then hands off. John, 2026-09-03: the first version called SetForegroundWindow from the@@ -602,10 +608,11 @@ def _foreground_once(window_title: str) -> bool: try: from handlers.kicad_ui import _find_all_kicad_windows from handlers import win_focus, ad_client, kicad_windows- target = None- for w in _find_all_kicad_windows():- if window_title.lower() in (w.get("title") or "").lower():- target = int(w["hwnd"]); break+ target = int(hwnd) if hwnd else None+ if not target:+ for w in _find_all_kicad_windows():+ if window_title.lower() in (w.get("title") or "").lower():+ target = int(w["hwnd"]); break if not target: return False try:@@ -638,7 +645,46 @@ def _foreground_once(window_title: str) -> bool: -def _beat_shot(kicad_info: dict, step_label: str, window_title: str) -> dict | None:+def _hwnd_for_title(window_title: str) -> int | None:+ try:+ from handlers.kicad_ui import _find_all_kicad_windows+ for w in _find_all_kicad_windows():+ if window_title.lower() in (w.get("title") or "").lower():+ return int(w["hwnd"])+ except Exception: # pylint: disable=broad-except+ pass+ return None+++def _viewer_3d_owned_by(editor_title: str) -> int | None:+ """The '3D Viewer' whose OWNER is the given editor frame (Footprint+ Editor vs PCB Editor). John, 2026-09-03: the last beat showed the chip's+ viewer instead of the board's because both are titled '3D Viewer' and the+ first title match won."""+ try:+ from handlers.kicad_ui import _find_all_kicad_windows+ rows = _find_all_kicad_windows()+ owners = [int(w["hwnd"]) for w in rows if editor_title.lower() in (w.get("title") or "").lower()]+ for w in rows:+ if "3d viewer" in (w.get("title") or "").lower() and int(w.get("ownerHwnd") or 0) in owners:+ return int(w["hwnd"])+ except Exception: # pylint: disable=broad-except+ pass+ return None+++def _close_hwnd(hwnd: int | None) -> None:+ if not hwnd:+ return+ try:+ from handlers import ad_client+ ad_client.call("desktop_close_window", {"hwnd": int(hwnd),+ "reason": "KiCad tour: close the chip's 3D viewer before showing the board's"}, timeout=10)+ except Exception: # pylint: disable=broad-except+ pass+++def _beat_shot(kicad_info: dict, step_label: str, window_title: str, hwnd: int | None = None) -> dict | None: """The per-step EVIDENCE frame for the web-control page (#25, John 2026-08-16: the page's own guess-snaps race the tour and shoot the wrong window — only the bridge knows the moment a step completes and which window is its@@ -649,7 +695,7 @@ def _beat_shot(kicad_info: dict, step_label: str, window_title: str) -> dict | N import base64 as _b64 from handlers.kicad_ui import _find_all_kicad_windows, _screenshot_hwnd for w in _find_all_kicad_windows():- if window_title.lower() in (w.get("title") or "").lower():+ if (hwnd and int(w["hwnd"]) == int(hwnd)) or (not hwnd and window_title.lower() in (w.get("title") or "").lower()): shot = _screenshot_hwnd(w["hwnd"], label="stepshot", max_dim=640) p = shot.get("fullPath") or shot.get("savedTo") if shot.get("success") and p and p.lower().endswith(".png"):@@ -898,6 +944,13 @@ def on_panel_event(ev: dict) -> dict: return {"ignored": b} if cmd in ("pause", "exit"): _stop_narration() # the user's hand: silence within the beat+ if cmd == "exit":+ # John, 2026-09-03: on the panel's X "exit immediately, remove the+ # captions and stop the audio". Audio is already dead; clear the+ # caption now; the beat loop, the narration wait and the motion all+ # poll _EXIT and bail at their next gesture.+ _EXIT["now"] = True+ _caption_clear() set_demo_control(cmd) _job_update(lastPress={"button": b, "at": at, "via": "callback"}) return {"queued": cmd}@@ -987,7 +1040,18 @@ def _drain_ctrl() -> str | None: return None +def _caption_clear() -> None:+ try:+ from handlers import ad_client+ ad_client.call("desktop_caption", {"action": "hide", "id": _CAPTION_ID,+ "reason": "The user stopped the KiCad tour; clear its caption"}, timeout=6)+ except Exception: # pylint: disable=broad-except+ pass++ def _caption(text: str, duration_ms: int = 60000) -> None:+ if _EXIT["now"]:+ return """Paint/replace the demo caption on the user's screen. Best-effort: a caption must never be able to fail a beat.""" try:@@ -1025,6 +1089,7 @@ def _run_demo_background(kicad_info: dict) -> None: # (it would fight that single raise), and no push-to-back of the demo's # windows. Every other verb keeps the background contract. done_weight = 0.0+ _EXIT["now"] = False try: _panel_show(0, "playing") _caption(_BEAT_CAPTIONS["prepare"], duration_ms=_BEAT_CAPTION_MS["prepare"])@@ -1105,10 +1170,26 @@ def _run_demo_background(kicad_info: dict) -> None: file=_sys.stderr) r["step"] = s_ results.append(r)+ if _EXIT["now"]:+ exited = True+ break if r.get("ok"):- r["foregrounded"] = _foreground_once(r.get("window") or r.get("windowTitle") or "")+ r["foregrounded"] = _foreground_once(r.get("window") or r.get("windowTitle") or "",+ hwnd=r.get("hwnd"))+ # Motion for the recording (John): only while OUR window is in+ # front, and it stops at the next gesture on STOP or a switch away.+ try:+ from handlers import demo_motion+ kind = "3d" if s_ in ("footprint3d", "board3d") else "2d"+ target = r.get("hwnd") or _hwnd_for_title(r.get("window") or "")+ r["motion"] = demo_motion.choreograph(target, kind, abort=lambda: _EXIT["now"])+ except Exception as e: # pylint: disable=broad-except+ r["motion"] = {"error": f"{type(e).__name__}: {e}"}+ if _EXIT["now"]:+ exited = True+ break shot = _beat_shot(kicad_info, f"Opening {s_.replace('_', ' ')}",- r.get("window") or r.get("windowTitle") or "")+ r.get("window") or r.get("windowTitle") or "", hwnd=r.get("hwnd")) if shot: with _JOB_LOCK: _JOB.setdefault("pendingShots", []).append(shot)@@ -1120,7 +1201,7 @@ def _run_demo_background(kicad_info: dict) -> None: if not pr or pr.poll() is not None: break cmd = _drain_ctrl()- if cmd == "exit":+ if cmd == "exit" or _EXIT["now"]: exited = True break if cmd:@@ -1165,6 +1246,7 @@ def _run_demo_background(kicad_info: dict) -> None: "steps": [{"step": r["step"], "title": r.get("title"), "ok": r.get("ok"), "pointOut": r.get("pointOut"), "foregrounded": r.get("foregrounded"),+ "motion": r.get("motion"), "failReason": (None if r.get("ok") else { "handlerSuccess": bool((r.get("raw") or {}).get("success")), "error": str((r.get("raw") or {}).get("error") or "")[:200],