app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
"""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]}
# 0.9.194: the loader log is written by ANY process that runs the bootstrap,
# including the bridge's own short-lived `python.exe -c ...` probes. Judging
# "did the plugin run" from those lines is how this verb told a live box
# "IMPORTED BUT DID NOT START" while no KiCad was running at all - a
# confident answer to a question nobody could answer yet. Split the log by
# process and only count GUI lines as evidence about KiCad.
gui_names = ("kicad", "eeschema", "pcbnew", "gerbview", "pl_editor", "bitmap2component")
gui_log, probe_log = [], []
for line in report["loaderLog"]:
low = line.lower()
(gui_log if any(f"in {n}.exe" in low for n in gui_names) else probe_log).append(line)
report["loaderLogGui"] = gui_log
report["loaderLogProbes"] = probe_log
try:
from handlers.kicad_ui import _find_all_kicad_windows
kicad_running = bool(_find_all_kicad_windows())
except Exception:
kicad_running = bool(report.get("instances"))
report["kicadRunning"] = kicad_running
# 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(gui_log) # GUI evidence only, never a probe process
alive = (report.get("live") or {}).get("alive") or 0
if alive:
verdict = "WORKING: the plugin is bound and answering."
elif not kicad_running:
verdict = ("KICAD NOT RUNNING: there is nothing for the plugin to load into "
"yet, so this says nothing about whether it works. Launch KiCad "
"and open a board editor, then re-run. "
+ ("The loader IS deployed." if deployed else
"The loader is NOT deployed - run kicad_install_plugin."))
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 loaderLogGui 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