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.307: Tour verification (John: 'tool your bridge code to definitively know that the correct thing is showing'): every beat now MEASURES, after its raise, that its own window is the top of AD's z-order, that the live title carries the expected document, that the GL canvas is rendered (kicad_ui canvas probe) and that two captures 0.8 s apart are identical (settled). The result rides on the step and on progress as ; motion runs only on a verified window, otherwise the step says exactly which fact failed and which window was actually on top. Zoom discipline: 2D beats start and end on extents with two wheel clicks in and back out; 3D beats zoom one menu step in and out and end on Zoom to Fit while staying isometric.
3 files changed
+83−12
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.306+0.9.307
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.306",+ "version": "0.9.307", "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+81−10@@ -628,6 +628,68 @@ def _job_update(**kw): _JOB.update(kw) +def _verify_beat(hwnd: int | None, expected_title: str, tries: int = 2) -> dict:+ """MEASURE, never assume (John, 2026-09-03: "you have to tool your bridge+ code to definitively know that the correct thing is showing"). Four facts+ per beat, each read from the desktop:+ front : AD's z-order says our hwnd is the top window+ title : the window's live title carries the document we expect+ painted : the GL canvas is not a uniform blank (kicad_ui canvas probe)+ stable : two captures 0.8 s apart are byte-identical (rendering settled)+ One extra raise attempt if it is not in front. Motion and the frame are+ gated on the result; the result rides on the step so nobody has to guess."""+ import time as _t, hashlib as _h+ v = {"hwnd": hwnd, "expected": expected_title, "front": False, "title": False,+ "painted": None, "stable": None, "frontTitle": None, "attempts": 0}+ if not hwnd:+ v["why"] = "no window handle for this beat"+ return v+ try:+ from handlers import kicad_windows, win_focus, ad_client+ from handlers.kicad_ui import _canvas_probe, _screenshot_hwnd+ except Exception as e: # pylint: disable=broad-except+ v["why"] = f"helpers unavailable: {e}"+ return v+ for attempt in range(tries):+ v["attempts"] = attempt + 1+ rows = [r for r in (kicad_windows.find(all_windows=True, fresh=True) or []) if r.get("z") is not None]+ top = sorted(rows, key=lambda r: r["z"])[0] if rows else {}+ v["frontTitle"] = top.get("title")+ v["front"] = int(top.get("hwnd") or 0) == int(hwnd)+ mine = next((r for r in rows if int(r.get("hwnd") or 0) == int(hwnd)), None)+ live_title = (mine or {}).get("title") or ""+ v["liveTitle"] = live_title+ v["title"] = expected_title.lower() in live_title.lower() if expected_title else bool(live_title)+ if v["front"]:+ break+ try: # one more ask, through AD, then measure again+ win_focus.allow_activation(int(hwnd))+ ad_client.call("desktop_bring_to_front", {"hwnd": int(hwnd),+ "reason": f"KiCad tour: bring the {expected_title or 'beat'} window forward, once"}, timeout=8)+ except Exception: # pylint: disable=broad-except+ pass+ _t.sleep(0.6)+ try:+ probe = _canvas_probe(int(hwnd)) or {}+ v["painted"] = bool(probe.get("rendered")) if probe.get("checked") else None+ v["canvas"] = {k: probe.get(k) for k in ("checked", "rendered", "distinctColors", "region") if k in probe}+ except Exception: # pylint: disable=broad-except+ v["painted"] = None+ try:+ def _digest():+ r = _screenshot_hwnd(int(hwnd), label="verify", max_dim=480)+ pth = r.get("fullPath") or r.get("savedTo")+ return _h.sha1(open(pth, "rb").read()).hexdigest() if pth else None+ a = _digest(); _t.sleep(0.8); b = _digest()+ v["stable"] = bool(a and b and a == b)+ if not v["stable"]:+ _t.sleep(1.2); c = _digest(); v["stable"] = bool(b and c and b == c)+ except Exception: # pylint: disable=broad-except+ v["stable"] = None+ v["ok"] = bool(v["front"] and v["title"] and v["painted"] is not False)+ return v++ def _foreground_once(window_title: str, hwnd: int | None = None) -> bool: """Bring the window this beat opened to the front ONCE, then hands off. @@ -1321,15 +1383,23 @@ def _run_demo_background(kicad_info: dict) -> None: if r.get("ok"): 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}"}+ # MEASURE before moving: the right window, in front, painted, settled.+ target = r.get("hwnd") or _hwnd_for_title(r.get("window") or "")+ r["verified"] = _verify_beat(target, r.get("window") or "")+ _job_update(verified=r["verified"])+ if not r["verified"].get("ok"):+ r["motion"] = {"skipped": f"not verified: front={r['verified'].get('front')} "+ f"title={r['verified'].get('title')} painted={r['verified'].get('painted')} "+ f"(top window: {r['verified'].get('frontTitle')})"}+ else:+ # Motion for the recording (John): only on a verified window, 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"+ 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@@ -1393,6 +1463,7 @@ def _run_demo_background(kicad_info: dict) -> None: "foregrounded": r.get("foregrounded"), "motion": r.get("motion"), "hwnd": r.get("hwnd"), "window": r.get("window"),+ "verified": r.get("verified"), "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],@@ -1442,7 +1513,7 @@ def handle_demo(kicad_info: dict, args: dict) -> dict: out = {"success": True, **{k: snap.get(k) for k in ("done", "step", "stepLabel", "beatIndex", "beatCount", "percent", "elapsedSec", "estimatedSec", "error",- "panel", "muted", "muteSource", "lastPress", "paused", "stopping")}}+ "panel", "muted", "muteSource", "lastPress", "paused", "stopping", "verified")}} # 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