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.290: kicad_demo drives ab's desktop_demo_panel (Adom Bridge 2.1.0+, wiki adom/adom-bridge#116): the floaty play/pause/next/back/stop remote is shown at stage 0 with the seven stops, polled between beats, stage and state updated per beat, hidden on done/exit/error; the toast transport remains only as the fallback for an older ab. Every beat paints one short ab caption (KiCad tour n/7: ...) for about the beat's length instead of a paragraph for a minute (John, 2026-09-03).
3 files changed
+120−35
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.289+0.9.290
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.289",+ "version": "0.9.290", "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+118−33@@ -510,32 +510,20 @@ def _beat_shot(kicad_info: dict, step_label: str, window_title: str) -> dict | N # names the MECHANISM, not just the step - the layers are the demo. _CAPTION_ID = "kicad-demo" +# John, 2026-09-03: one short line per beat, on screen only for the beat. The+# long "mechanism" paragraphs of 2026-08-24 were the "large, ugly, far too long"+# caption he called out; the dashboard's step log carries the mechanism now. _BEAT_CAPTIONS = {- "prepare": ("KiCad demo, step 1 of 7 - generating a real project. The bridge is writing "- "a symbol, footprint, schematic and board on this machine, then installing "- "them into your actual KiCad libraries (quote-aware merge, balance-checked "- "before every write)."),- "schematic": ("Step 2 of 7 - the schematic. eeschema opens IN THE BACKGROUND "- "(SW_SHOWNOACTIVATE plus a focus guardian that keeps handing your window "- "back), and the screenshot you are about to see is captured from that "- "buried window by hwnd - your foreground is never taken."),- "board": ("Step 3 of 7 - the 2D board. Same background contract for pcbnew; the shot "- "streams to the dashboard live over its SSE channel the moment the beat "- "finishes."),- "board3d": ("Step 4 of 7 - the whole board in 3D. The 3D viewer re-raises itself while "- "it renders, so a longer focus guardian absorbs every self-raise; the "- "capture also waits for the GL canvas to actually paint before trusting "- "the frame."),- "symbol": ("Step 5 of 7 - the symbol just installed. A reverse-bridge Python server "- "living INSIDE eeschema navigates the library tree for us - window-targeted "- "messages only, never your keyboard or mouse."),- "footprint": ("Step 6 of 7 - that part's land pattern. Menu commands are resolved from "- "KiCad's LIVE menu bar (ids change between versions), and the library "- "tree was refreshed in place rather than restarting KiCad."),- "footprint3d": ("Step 7 of 7 - the part in 3D, real SOIC-8 body on its pads. Every "- "screenshot in this demo was taken from a background window and "- "verified rendered before it was shown."),+ "prepare": "KiCad tour 1/7: writing a real symbol, footprint, schematic and board into your KiCad",+ "schematic": "KiCad tour 2/7: the schematic, opened in the background",+ "board": "KiCad tour 3/7: the 2D board",+ "board3d": "KiCad tour 4/7: the whole board in 3D",+ "symbol": "KiCad tour 5/7: the symbol that was just installed",+ "footprint": "KiCad tour 6/7: that part's footprint",+ "footprint3d": "KiCad tour 7/7: the part in 3D on its pads", }+_BEAT_CAPTION_MS = {"prepare": 35000, "schematic": 25000, "board": 25000, "board3d": 40000,+ "symbol": 30000, "footprint": 30000, "footprint3d": 35000} # ── Narration: PRE-RENDERED mp3s, never on-the-fly TTS (John, 2026-08-24) ───@@ -604,6 +592,8 @@ def set_demo_control(cmd: str) -> None: def _post_transport(body: str, paused: bool = False) -> None:+ if _PANEL["ok"]:+ return # ab's demo panel is on screen; no toast on top of it try: from handlers import ad_client ad_client.call("notify_user", {@@ -618,6 +608,90 @@ def _post_transport(body: str, paused: bool = False) -> None: pass +# ── ab's demo transport panel (Adom Bridge 2.1.0+, wiki adom/adom-bridge#116) ──+# John, 2026-09-03: "ab has a new demo control, make your demo use it." One+# floaty glass PLAY/PAUSE/NEXT/BACK/STOP panel shared by every bridge's tour.+# Buttons only queue events; the job thread drains them with poll BETWEEN+# beats (a beat cannot be interrupted mid-verb). The toast transport stays as+# the fallback for an ab older than 2.1.0 (the verb answers "unknown verb").+_PANEL_ID = "kicad-demo"+_PANEL = {"ok": None} # None = untried, True = panel shown, False = fall back to the toast+_PANEL_STOPS = ["Preparing the project", "The schematic", "The 2D board", "The board in 3D",+ "The symbol", "The footprint", "The part in 3D"]+++def _panel_show(stage: int, state: str = "playing") -> bool:+ if _PANEL["ok"] is False:+ return False+ try:+ from handlers import ad_client+ r = ad_client.call("desktop_demo_panel", {+ "action": "show", "id": _PANEL_ID, "title": "KiCad bridge tour",+ "stops": _PANEL_STOPS, "stage": int(stage), "state": state,+ "position": "bottom-right",+ "reason": "Transport for the KiCad tour the user started: play, pause, next, back, stop"},+ timeout=8) or {}+ err = str(r.get("error") or "")+ if "unknown verb" in err.lower() or "no installed bridge" in err.lower():+ _PANEL["ok"] = False+ return False+ out = r.get("output") if isinstance(r, dict) else None+ if isinstance(out, str):+ try:+ import json as _j+ out = _j.loads(out)+ except Exception: # pylint: disable=broad-except+ out = {}+ data = (out or {}).get("data") if isinstance(out, dict) else None+ shown = bool((data or out or {}).get("shown")) or (r.get("success") is True and not err)+ _PANEL["ok"] = True if shown else (_PANEL["ok"] or None)+ return shown+ except Exception: # pylint: disable=broad-except+ return False+++def _panel_poll() -> str | None:+ """Drain the panel's button presses; newest press wins. Maps the panel's+ vocabulary (back/play/pause/next/stop) onto the demo's (prev/play/pause/next/exit)."""+ if not _PANEL["ok"]:+ return None+ try:+ from handlers import ad_client+ r = ad_client.call("desktop_demo_panel", {"action": "poll", "id": _PANEL_ID}, timeout=6) or {}+ out = r.get("output") if isinstance(r, dict) else None+ if isinstance(out, str):+ try:+ import json as _j+ out = _j.loads(out)+ except Exception: # pylint: disable=broad-except+ out = {}+ data = (out or {}).get("data") if isinstance(out, dict) else None+ d = data or out or {}+ events = d.get("events") or []+ mapping = {"back": "prev", "play": "play", "pause": "pause", "next": "next", "stop": "exit"}+ for ev in reversed(events):+ b = str((ev or {}).get("button") or "").lower()+ if b in mapping:+ return mapping[b]+ # No press since last poll: honor the panel's own state (the user may+ # have toggled pause through a press we already consumed).+ if d.get("state") == "paused":+ return "pause-held"+ except Exception: # pylint: disable=broad-except+ pass+ return None+++def _panel_hide() -> None:+ if not _PANEL["ok"]:+ return+ try:+ from handlers import ad_client+ ad_client.call("desktop_demo_panel", {"action": "hide", "id": _PANEL_ID}, timeout=6)+ except Exception: # pylint: disable=broad-except+ pass++ def _drain_ctrl() -> str | None: """One pending control command, from the verb channel or a toast click.""" with _CTRL_LOCK:@@ -625,6 +699,13 @@ def _drain_ctrl() -> str | None: _CTRL["cmd"] = None if cmd: return cmd+ pc = _panel_poll()+ if pc == "pause-held":+ return None if _JOB.get("paused") else "pause"+ if pc:+ return pc+ if _PANEL["ok"]:+ return None # the panel is the transport; the toast is not shown try: from handlers import ad_client r = ad_client.call("notify_events", {"sinceSeq": _CTRL["seq"]}, timeout=6) or {}@@ -679,7 +760,8 @@ def _run_demo_background(kicad_info: dict) -> None: pass done_weight = 0.0 try:- _caption(_BEAT_CAPTIONS["prepare"])+ _panel_show(0, "playing")+ _caption(_BEAT_CAPTIONS["prepare"], duration_ms=_BEAT_CAPTION_MS["prepare"]) _play_narration("prepare") prep = _prepare(kicad_info) _job_update(step="prepare", stepLabel="Preparing the sample project",@@ -702,8 +784,10 @@ def _run_demo_background(kicad_info: dict) -> None: cmd = _drain_ctrl() if cmd == "pause": _stop_narration()- _caption("Demo paused. Use the toast buttons (Play / Prev / Next / "- "Exit) or the dashboard to continue.", duration_ms=300000)+ _panel_show(i + 1, "paused")+ _caption("KiCad tour paused" + ("" if _PANEL["ok"] else+ " - use the toast buttons or the dashboard to continue"),+ duration_ms=300000) _post_transport("Paused before beat %d of %d." % (i + 1, len(STEPS)), paused=True) _job_update(stepLabel="Paused", paused=True)@@ -728,8 +812,9 @@ def _run_demo_background(kicad_info: dict) -> None: beatIndex=i + 1, beatCount=len(STEPS), percent=min(95, int(100 * done_weight / total_est)), elapsedSec=round(_t.monotonic() - t0))+ _panel_show(i + 1, "playing") if s_ in _BEAT_CAPTIONS:- _caption(_BEAT_CAPTIONS[s_])+ _caption(_BEAT_CAPTIONS[s_], duration_ms=_BEAT_CAPTION_MS.get(s_, 30000)) _play_narration(s_) r = _run_step(kicad_info, s_, prep) if not r.get("ok"):@@ -787,13 +872,14 @@ def _run_demo_background(kicad_info: dict) -> None: state = {"success": False, "error": str(e)} ok_ct = len([r for r in results if r.get("ok")]) _stop_narration()+ _panel_hide() try: from handlers import ad_client ad_client.call("notify_dismiss", {"id": _CTL_TOAST_ID}, timeout=4) except Exception: pass if exited:- _caption("Demo stopped.", duration_ms=5000)+ _caption("KiCad tour stopped", duration_ms=4000) _job_update(active=False, done=True, exited=True, percent=100, elapsedSec=round(_t.monotonic() - t0), result={"openedBeats": ok_ct, "totalBeats": len(STEPS),@@ -801,10 +887,8 @@ def _run_demo_background(kicad_info: dict) -> None: "failedSteps": [r["step"] for r in results if not r.get("ok")]}) return _play_narration("done")- _caption(f"Demo complete: {ok_ct} of {len(STEPS)} views opened, every one in the "- "background - your foreground was never taken. The shots you saw streamed "- "live from buried windows. This caption clears itself in a few seconds.",- duration_ms=12000)+ _caption(f"KiCad tour done: {ok_ct} of {len(STEPS)} views opened in the background",+ duration_ms=6000) _job_update(active=False, done=True, percent=100, elapsedSec=round(_t.monotonic() - t0), result={@@ -822,6 +906,7 @@ def _run_demo_background(kicad_info: dict) -> None: "state": state, }) except Exception as e: # pylint: disable=broad-except+ _panel_hide() _job_update(active=False, done=True, error=f"{type(e).__name__}: {e}", elapsedSec=round(_t.monotonic() - t0))