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
+74−21
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.217+0.9.218
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.217",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.217/adom-bridge-kicad-v0.9.217.zip",- "sha256": "3430f247cb8244f9cd8897bc993b07c78225ee3511766a360e910d94f72be932",- "size": 484313,+ "version": "0.9.218",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.218/adom-bridge-kicad-v0.9.218.zip",+ "sha256": "b563d16f60a361cc777db1353b96df208297b7b0541fabd92a8d5569e0dc5375",+ "size": 485454, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-08-20T18:45:00Z",+ "released_at": "2026-08-20T19:10: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.217",+ "version": "0.9.218", "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/progress.py+57−12@@ -206,14 +206,41 @@ _live: dict = {} _live_seq = 0 -def begin(phase: str, start_kind: str = "warm", step_label: str = "starting") -> None:- """Mark a phase as in flight. Safe to call for phases nobody watches."""+def _caller() -> str:+ """Who is asking. Used to keep concurrent callers out of each other's steps."""+ try:+ import caller_identity+ return str(caller_identity.thread_name() or "unknown")+ except Exception:+ return "unknown"+++def begin(phase: str, start_kind: str = "warm", step_label: str = "starting",+ caller: str = None) -> None:+ """Mark a phase as in flight, SCOPED TO THE CALLER.++ Wiki issue #46 (John): a second caller's steps appeared inside a first+ caller's response - "locating 'OPA2134'" in a response about nRF54L15,+ because someone used a wiki Quick Action on one KiCad while a matrix run+ drove the same box. Two independent web callers, one global step stream.++ Two distinct bugs, both from keying this registry on the phase alone:+ * a reader saw steps that were never theirs, which reads as a wrong-part+ failure to anyone who does not go and check the pixels;+ * worse and quieter, the second caller OVERWROTE the first's entry, so a+ caller could lose its own progress to someone else's call.++ Keying on (caller, phase) fixes both. It degrades honestly rather than+ perfectly: two browser tabs from one origin still share an identity until ab+ ships a per-page discriminator, but every other pair of callers separates+ correctly today, and every frame now says who produced it.""" global _live_seq+ who = caller or _caller() with _lock: _live_seq += 1- _live[phase] = {"startedAt": time.time(), "startKind": start_kind,- "stepLabel": step_label, "percent": None,- "blockedBy": None, "seq": _live_seq}+ _live[(who, phase)] = {"startedAt": time.time(), "startKind": start_kind,+ "stepLabel": step_label, "percent": None,+ "blockedBy": None, "seq": _live_seq, "caller": who} def step(phase: str, step_label: str, percent=None, blocked_by: str = None) -> None:@@ -222,10 +249,18 @@ def step(phase: str, step_label: str, percent=None, blocked_by: str = None) -> N This is what turns one opaque 24-second bar into "launching KiCad" -> "indexing symbol libraries" -> "navigating to IN-S42ATR", which is the sub-phase breakdown Colby asked for on wiki issue #35."""+ who = _caller() with _lock:- cur = _live.get(phase)- if not cur:- return+ cur = _live.get((who, phase))+ if cur is None:+ # A handler thread may not carry the caller context; fall back to the+ # only in-flight entry for this phase, but never guess when several+ # callers are running the same phase - a wrong attribution is the+ # bug this issue is about.+ matches = [k for k in _live if k[1] == phase]+ if len(matches) != 1:+ return+ cur = _live[matches[0]] cur["stepLabel"] = step_label cur["blockedBy"] = blocked_by if percent is not None:@@ -233,21 +268,30 @@ def step(phase: str, step_label: str, percent=None, blocked_by: str = None) -> N cur["percent"] = max(percent, cur.get("percent") or 0) -def end(phase: str) -> None:+def end(phase: str, caller: str = None) -> None:+ who = caller or _caller() with _lock:- _live.pop(phase, None)+ if _live.pop((who, phase), None) is None:+ for k in [k for k in _live if k[1] == phase and k[0] == who]:+ _live.pop(k, None) -def live() -> list:+def live(mine_only: bool = True, asker: str = None) -> list: """Every phase currently in flight, as full progress blocks. + By default a caller sees ONLY ITS OWN phases (#46). Pass mine_only=False for+ an operator view of everything in flight; each frame carries `caller`.+ Percent/ETA are recomputed at READ time from this machine's own history, so a poller gets a bar that keeps moving even if the verb has not reported a new sub-step since its last one."""+ asker = asker or _caller() out = [] with _lock: snapshot = list(_live.items())- for phase, cur in snapshot:+ for (who, phase), cur in snapshot:+ if mine_only and who != asker:+ continue try: # A step's explicit percent is a FLOOR, not a freeze. The first live # test pinned the bar at 10% for 47s while the ETA counted to zero,@@ -264,6 +308,7 @@ def live() -> list: percent=_pct_now, start_kind=cur.get("startKind", "warm"), blocked_by=cur.get("blockedBy")) b["seq"] = cur.get("seq")+ b["caller"] = who out.append(b) except Exception: pass
server.py+10−2@@ -610,7 +610,10 @@ def _handle_progress(kicad_info: dict, args: dict) -> dict: is still happening. Read-only, cheap, and safe to poll every second.""" try: from handlers import progress as _pg- frames = _pg.live()+ # #46: a caller sees ONLY its own steps by default. `all:true` is the+ # operator view, and every frame carries `caller` either way so a reader+ # can always tell whose step it is looking at.+ frames = _pg.live(mine_only=not bool((args or {}).get("all"))) except Exception as e: return {"success": False, "error": f"progress unavailable: {e}"[:140]} @@ -669,7 +672,12 @@ def _handle_progress(kicad_info: dict, args: dict) -> dict: "busy": bool(frames), "active": frames, "history": _pg.history(),- "_hint": ("Poll this while a long verb runs (1s is fine). `active` holds one "+ "callerScoped": True,+ "_hint": ("Scoped to YOU: `active` holds only the phases this caller started "+ "(wiki #46 - another caller's steps used to appear in your response). "+ "Pass all:true for every in-flight phase on the box; each frame carries "+ "`caller` regardless. "+ "Poll this while a long verb runs (1s is fine). `active` holds one " "progress block per in-flight phase: stepLabel is the human line for " "a status bar, percent/etaSec drive the bar, and confidence says " "whether the estimate is measured on THIS machine or still a default. "