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
v0.9.189: new kicad_plugin_diagnose - one read-only call that answers 'why is the reverse bridge not loading' with a verdict (deployed? imported by KiCad? what did the loader log? is the embedded stdlib complete?)
6 files changed
+167−7
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.188+0.9.189
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.188",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.188/adom-bridge-kicad-v0.9.188.zip",- "sha256": "551063bec787383debea7324f659ee9274195fd530f9aae840a452271815547f",- "size": 455691,+ "version": "0.9.189",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.189/adom-bridge-kicad-v0.9.189.zip",+ "sha256": "7f13596197384490e009ecea5f1a5cd8732d435a5f5b59e149be3a7ffbcaf451",+ "size": 458771, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-08-19T20:37:29Z",+ "released_at": "2026-08-19T20:51:11Z", "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png", "languages": [ "Python",
adom-bridge-kicad-v0.9.189.zipadded⋯ 1 unchanged line ⋯
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.188",+ "version": "0.9.189", "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/plugin_diagnose.pyadded+152@@ -0,0 +1,152 @@+"""plugin_diagnose.py — kicad_plugin_diagnose: why is the reverse bridge not loading?++Born out of wiki issue #34 (Colby Knox, 2026-08-19). The plugin binds on both of+this maintainer's boxes and not on his, and answering that took several rounds of+"please send me this file, now that one". Every artifact needed to tell the four+possible causes apart is readable from the box itself, so this verb collects them+in one call:++ 1. Was the loader DEPLOYED? -> the files, with sizes and ages+ 2. Did KiCad IMPORT it? -> a __pycache__ entry next to it is proof+ 3. Did the loader RUN and what did it say? -> its own log + the process breadcrumbs+ 4. Could the payload import at all? -> KiCad's embedded stdlib, which has been+ found incomplete on a real machine+ (a missing Lib/urllib made every import+ of the payload fail; no bridge code can+ work around that)++Read-only. No windows, no side effects.+"""+import json+import os+import time+from pathlib import Path+++def _stat(p: Path) -> dict:+ try:+ st = p.stat()+ return {"exists": True, "bytes": st.st_size,+ "ageMin": int((time.time() - st.st_mtime) / 60)}+ except Exception:+ return {"exists": False}+++def _dir_listing(d: Path, pattern: str = "*") -> list:+ out = []+ try:+ for p in sorted(d.glob(pattern)):+ e = _stat(p)+ e["name"] = p.name+ out.append(e)+ except Exception:+ pass+ return out+++def _tail(p: Path, lines: int = 25) -> list:+ try:+ return p.read_text(encoding="utf-8", errors="replace").splitlines()[-lines:]+ except Exception:+ return []+++def handle_plugin_diagnose(kicad_info: dict, args: dict) -> dict:+ """Collect every artifact that explains whether the reverse-bridge plugin+ loaded, and why not. Read-only; safe to run any time."""+ appdata = os.environ.get("APPDATA") or ""+ userprofile = os.environ.get("USERPROFILE") or str(Path.home())+ temp = Path(os.environ.get("TEMP") or "/tmp")+ version = str(kicad_info.get("version") or "10.0")++ report = {"success": True, "kicadVersion": version}++ # 1 + 2: the plugin-directory loader, and whether KiCad imported it+ pdirs = []+ base = Path(appdata) / "kicad" if appdata else None+ if base:+ for vdir in sorted([d for d in base.glob("*") if d.is_dir()]):+ pdir = vdir / "scripting" / "plugins"+ if not pdir.exists():+ continue+ loader = pdir / "adom_bridge_plugin.py"+ pyc = _dir_listing(pdir / "__pycache__", "adom_bridge_plugin*.pyc")+ pdirs.append({+ "dir": str(pdir),+ "loader": {**_stat(loader), "name": "adom_bridge_plugin.py"},+ "importedByKiCad": bool(pyc), # a .pyc is proof of import+ "pycache": pyc,+ "otherFiles": [f["name"] for f in _dir_listing(pdir, "*.py")],+ })+ report["pluginDirs"] = pdirs++ # the user-site payload (the other deploy target)+ site = Path(userprofile) / "Documents" / "KiCad" / version / "3rdparty" / "Python311" / "site-packages"+ report["userSite"] = {"path": str(site), "files": _dir_listing(site, "adom*")}+ std_site = Path(appdata) / "Python" / "Python311" / "site-packages" if appdata else None+ if std_site:+ report["stdUserSite"] = {"path": str(std_site), "files": _dir_listing(std_site, "adom*")}++ # 3: what the loader itself said, and which exes ever ran the bootstrap+ report["loaderLog"] = _tail(temp / "adom-kicad-plugin-loader.log")+ report["breadcrumbs"] = [+ {"name": p.name, **_stat(p)} for p in sorted(temp.glob("adom-kicad-process-*.json"))+ ]+ report["loaderErrors"] = {p.name: _tail(p, 12)+ for p in sorted(temp.glob("adom-kicad-loader-*.error.log"))}++ # 4: is KiCad's embedded stdlib complete? (a real machine was missing urllib,+ # which made every import of the payload fail with ModuleNotFoundError)+ stdlib = {}+ bin_dir = Path(kicad_info.get("kicad_exe") or "").parent if kicad_info.get("kicad_exe") else None+ if bin_dir and bin_dir.exists():+ lib = bin_dir / "Lib"+ stdlib["libDir"] = str(lib)+ for mod in ("urllib", "email", "http", "json", "encodings", "socket.py", "threading.py"):+ stdlib[mod] = (lib / mod).exists()+ stdlib["complete"] = all(v for k, v in stdlib.items()+ if isinstance(v, bool))+ report["embeddedStdlib"] = stdlib++ # live instances, for completeness+ try:+ from handlers.bridge_client import handle_bridge_status+ st = handle_bridge_status(kicad_info, {})+ report["live"] = st.get("summary") or {}+ report["instances"] = [{k: p.get(k) for k in ("alive", "exeName", "pid", "port")}+ for p in (st.get("plugins") or [])]+ except Exception as e:+ report["live"] = {"error": str(e)[:120]}++ # the verdict, so a human does not have to correlate five sections by hand+ deployed = any(d["loader"].get("exists") for d in pdirs)+ imported = any(d["importedByKiCad"] for d in pdirs)+ ran = bool(report["loaderLog"])+ alive = (report.get("live") or {}).get("alive") or 0+ if alive:+ verdict = "WORKING: the plugin is bound and answering."+ elif not deployed:+ verdict = ("NOT DEPLOYED: adom_bridge_plugin.py is missing from KiCad's "+ "scripting/plugins. Run kicad_install_plugin.")+ elif not imported and not ran:+ verdict = ("DEPLOYED BUT NEVER IMPORTED: no __pycache__ entry and no loader "+ "log, so KiCad has not scanned its plugin directory in this "+ "install. That scan happens when a BOARD EDITOR opens - if one "+ "has been opened and this still says never imported, this KiCad "+ "build does not scan that directory and the hook has to change.")+ elif ran and not alive:+ verdict = ("IMPORTED BUT DID NOT START: read loaderLog below - it names the "+ "exact failure. 'No module named X' means KiCad's embedded stdlib "+ "is incomplete (see embeddedStdlib); a repair install fixes it.")+ else:+ verdict = ("IMPORTED, no log yet: the loader arms a background thread and "+ "retries the payload import for ~60s. Re-run shortly.")+ report["verdict"] = verdict+ report["_hint"] = (+ "One-call answer to 'why is the reverse bridge not loading'. importedByKiCad "+ "is the key field: a __pycache__ entry beside the loader is proof KiCad "+ "imported it. If deployed but never imported, the plugin-directory scan is "+ "not happening in this build. If imported but not alive, loaderLog names the "+ "cause. Wiki issue #34."+ )+ return report
server.py+8@@ -69,6 +69,7 @@ from handlers.export_molecule import handle_export_molecule from handlers.demo import handle_demo from handlers.make_part_project import handle_make_part_project from handlers.export_part import handle_export_part+from handlers.plugin_diagnose import handle_plugin_diagnose from handlers.settings import handle_get_settings, handle_set_settings # v0.9.10+: check/upgrade KiCad via the OFFICIAL downloads.kicad.org installer # (NOT winget, which lags the real release). check_for_updates is read-only;@@ -350,6 +351,12 @@ _VERB_CATALOG = { "readiness": {"summary": "Read-only host-app detection — is KiCad installed/ready?", "long": False, "hint": "Zero side effects; returns {detected, ready, versionCount, defaultVersion}. Poll before driving.", "related": ["kicad_list_versions", "kicad_upgrade", "kicad_describe"], "pitfalls": ["there is NO prewarm — KiCad is a host app you install via kicad_upgrade, not a download Bridge warms"]},+ "plugin_diagnose": {"summary": "Why is the reverse-bridge plugin not loading? One call, every artifact.",+ "long": False,+ "hint": "Read-only. Reports whether the loader is deployed, whether KiCad IMPORTED it "+ "(a __pycache__ entry is proof), what the loader logged, and whether KiCad's "+ "embedded stdlib is complete - plus a plain-language verdict. Wiki issue #34.",+ "related": ["kicad_install_plugin", "kicad_bridge_status"], "pitfalls": []}, "describe": {"summary": "This catalog — every verb the bridge exposes.", "long": False, "hint": "Read-only full verb catalog; the completeness escape hatch. Per-call _hints carry you in practice.", "related": ["kicad_readiness"], "pitfalls": []},@@ -579,6 +586,7 @@ COMMAND_HANDLERS = { "state": handle_state, "uninstall": handle_uninstall, "install_plugin": plugin_install.handle_install_plugin,+ "plugin_diagnose": handle_plugin_diagnose, # v1.7.12+: headless lint pre-checks via kicad-cli "run_erc": handle_run_erc, "lint_board": handle_lint_board,