← 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).

John Lauer ·e539a12add ·1mo ago ·parent 4085dcb
3 files changed +120−35
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.289\ No newline at end of file+0.9.290\ No newline at end of file
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))