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 5 file(s)
5 files changed
+100−10
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.198+0.9.199
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.198",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.198/adom-bridge-kicad-v0.9.198.zip",- "sha256": "f29948616354a5a2caa53a9a7ad2e3df277e6cfea33ff2cd075137f08a39363d",- "size": 468068,+ "version": "0.9.199",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.199/adom-bridge-kicad-v0.9.199.zip",+ "sha256": "a9af58611683d9f4ed4af9ff92cd851f26eeeae7914a23d9c15938585ff76f78",+ "size": 469492, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-08-20T01:05:00Z",+ "released_at": "2026-08-20T01: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.198",+ "version": "0.9.199", "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+26−3@@ -604,8 +604,29 @@ def handle_demo(kicad_info: dict, args: dict) -> dict: return {"success": False, "error": "no demo job has been started", "_hint": "Start one with kicad_demo {\"all\":true,\"background\":true}."} out = {"success": True, **{k: snap.get(k) for k in- ("active", "done", "step", "stepLabel", "beatIndex", "beatCount",+ ("done", "step", "stepLabel", "beatIndex", "beatCount", "percent", "elapsedSec", "estimatedSec", "error")}}+ # 0.9.199 (wiki issue #39, John): `active` used to be a BOOLEAN here and+ # an ARRAY in kicad_progress - the same key, two types, from one bridge.+ # A renderer written as `if (resp.active)` then treats a finished job as+ # running (a non-empty array and `true` are both truthy) and cannot tell+ # the two idle forms apart. So `active` is now the canonical ARRAY of+ # progress blocks everywhere, and the boolean moved to `running`.+ _running = bool(snap.get("active"))+ out["running"] = _running+ out["active"] = ([{+ "phase": "kicad.demo",+ "stepLabel": snap.get("stepLabel") or snap.get("step") or "running",+ "percent": snap.get("percent"),+ "elapsedSec": snap.get("elapsedSec"),+ "estimatedSec": snap.get("estimatedSec"),+ "etaSec": (None if snap.get("estimatedSec") is None or snap.get("elapsedSec") is None+ else max(0.0, round(snap["estimatedSec"] - snap["elapsedSec"], 1))),+ "confidence": "typical",+ "startKind": "cold",+ "beatIndex": snap.get("beatIndex"),+ "beatCount": snap.get("beatCount"),+ }] if _running else []) # Per-step evidence (#25): one completed-step screenshot rides the FIRST # progress frame after that step lands — {step, title, image<=640px}. with _JOB_LOCK:@@ -618,8 +639,10 @@ def handle_demo(kicad_info: dict, args: dict) -> dict: "PNG (render them all); speak result.narrate. Tear down with " "kicad_close {\"force\":true} when the user is done looking.") else:- out["_hint"] = ("Long-running job — render a real progress bar from percent/"- "elapsedSec/estimatedSec and show stepLabel. Poll every 2-3s. "+ out["_hint"] = ("Long-running job — render a real progress bar from active[0] "+ "(percent/elapsedSec/estimatedSec/stepLabel). Poll every 2-3s. "+ "`active` is an ARRAY (canonical contract); the boolean "+ "still-running flag is `running`. " "stepShot (when present) is the completed step's own window as " "base64 PNG — pin it as that step's evidence.") return out
server.py+67@@ -109,6 +109,59 @@ BRIDGE_VERSION = _read_bridge_version() _DIALOG_BLOCK_AFTER_SEC = 3.0 _DIALOG_FIRST_SEEN: dict = {} +# stepShot cache: {phase: (stepLabel, {step,title,image})}. Keyed on the STEP+# LABEL so a shot is taken once per step, not once per poll - a 2.5s poll of a+# 20s step would otherwise capture eight identical images and push megabytes+# through the relay for one frame of information.+_STEPSHOT: dict = {}+_STEPSHOT_MAX_BYTES = 1_200_000 # keep a frame comfortably under the inline cap++# Which window IS this phase's surface? Without this a stepShot could pin the+# project manager as evidence for a footprint step - the exact mislabel that+# made evidence untrustworthy in 0.9.172.+_PHASE_SURFACE = {+ "show_symbol": "symbol editor",+ "show_library": "symbol editor",+ "show_footprint": "footprint editor",+ "show_3d_chip": "3d viewer",+ "show_3d_board": "3d viewer",+ "show_2d_board": "pcb editor",+ "show_schematic": "schematic editor",+}+++def _stepshot_for(phase: str, step_label: str):+ """A small base64 PNG of THIS phase's surface, at most one per step label."""+ cached = _STEPSHOT.get(phase)+ if cached and cached[0] == step_label:+ return cached[1]+ needle = _PHASE_SURFACE.get(phase)+ if not needle:+ return None+ try:+ import base64 as _b64+ from handlers.kicad_ui import _find_all_kicad_windows, _screenshot_hwnd+ win = None+ for w in _find_all_kicad_windows() or []:+ if needle in (w.get("title") or "").lower():+ win = w+ break+ if not win or not win.get("hwnd"):+ return None+ shot = _screenshot_hwnd(win["hwnd"], label=f"step-{phase}", max_dim=640)+ path = (shot or {}).get("fullPath") or (shot or {}).get("savedTo")+ if not path:+ return None+ raw = open(path, "rb").read()+ if len(raw) > _STEPSHOT_MAX_BYTES:+ return None+ out = {"step": step_label, "title": win.get("title"),+ "image": "data:image/png;base64," + _b64.b64encode(raw).decode()}+ _STEPSHOT[phase] = (step_label, out)+ return out+ except Exception:+ return None+ _PHASES = { "launch": "cold_start", "show_symbol": "show_symbol", "show_footprint": "show_footprint", "show_3d_chip": "show_3d_chip",@@ -597,6 +650,20 @@ def _handle_progress(kicad_info: dict, args: dict) -> dict: f["stepLabel"] = f"waiting for you: {blocker}" except Exception: pass++ # stepShot: the step's own evidence thumbnail, rendered verbatim by the+ # web surface. Skipped while blocked - the surface behind a modal is not+ # what the user needs to look at, the modal is.+ for f in frames:+ try:+ if f.get("blockedBy"):+ continue+ ph = (f.get("phase") or "").split(".", 1)[-1]+ ss = _stepshot_for(ph, f.get("stepLabel") or "")+ if ss:+ f["stepShot"] = ss+ except Exception:+ pass return { "success": True, "busy": bool(frames),