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 40 file(s)
4 files changed
+104−7
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.266+0.9.267
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.266",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.266/adom-bridge-kicad-v0.9.266.zip",- "sha256": "53b43dbac7db105ecc771b4c31e56eb982d37680bc52907f21d2065ad7ac8a37",- "size": 1037305,+ "version": "0.9.267",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.267/adom-bridge-kicad-v0.9.267.zip",+ "sha256": "97384b29d0c8789dd3c0dbf7bf6a5cfde124066fece969a67b6108a696692c96",+ "size": 1039132, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-09-01T02:28:40Z",+ "released_at": "2026-09-01T02:31:50Z", "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.266",+ "version": "0.9.267", "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/kicad_log.py+97@@ -140,3 +140,100 @@ def handle_log_tail(kicad_info: dict, args: dict) -> dict: "_hint": ("Carry nextOffset into the next call and you get ONLY what happened " "since — tail, act, tail again, and the second read is your action's " "consequences. errorsOnly:true filters to error/warning.")}+++# ---------------------------------------------------------------------------+# THE ERROR CHANNEL THAT ACTUALLY WORKS: KiCad's own dialogs.+#+# Measured on ConfRoomROG, KiCad 10.0, 2026-09-01. A board that fails to parse+# produces a "KiCad PCB Editor Error" window carrying the exact reason:+#+# Error loading PCB '...demo-project.kicad_pcb'.+# Expecting thru_hole, smd, connect, or np_thru_hole+#+# ...and the captured stderr log for that same launch was ZERO BYTES. That is+# not a bug in the capture: wxWidgets in a GUI app installs wxLogGui as the+# active log target, so wxLogError becomes a MESSAGE BOX, not a line on stderr.+# KICAD_ENABLE_WXTRACE turns logging on but does not redirect it to a console+# that a windowed process does not have.+#+# So: the dialogs ARE the log. This function reads them without screenshots,+# without OCR, and without touching the foreground - it is pure win32 text+# harvesting (EnumWindows + WM_GETTEXT), which is read-only by construction.+#+# stderr capture stays (kicad_log_tail) because it still catches the things that+# BYPASS wxLog: crashes, asserts, and anything printed with stdio. It is the+# second channel, not the first.++_ERR_TITLE = re.compile(r"\b(error|warning|problem|failed|cannot)\b", re.I)+++def _enum_dialog_text(hwnd: int) -> list[str]:+ """Every static/text child of a window, in z-order. wx dialogs put the+ message in one or more Static controls."""+ import ctypes+ from ctypes import wintypes+ user32 = ctypes.windll.user32+ out: list[str] = []++ @ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)+ def cb(child, _lp):+ buf = ctypes.create_unicode_buffer(2048)+ user32.GetWindowTextW(child, buf, 2048)+ txt = (buf.value or "").strip()+ cls = ctypes.create_unicode_buffer(256)+ user32.GetClassNameW(child, cls, 256)+ if txt and cls.value.lower() in ("static", "wxwindownr", "wxstatictext"):+ out.append(txt)+ return True++ user32.EnumChildWindows(hwnd, cb, 0)+ return out+++def handle_errors(kicad_info: dict, args: dict) -> dict:+ """Every KiCad error/warning dialog currently on screen, with its text.++ Read-only and background: enumerates windows and reads their text. Never+ raises, never clicks, never screenshots. Args: includeAll:true to return+ every KiCad dialog rather than only ones whose title looks like an error."""+ import ctypes+ from ctypes import wintypes+ user32 = ctypes.windll.user32+ wins: list[dict] = []++ @ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)+ def cb(hwnd, _lp):+ if not user32.IsWindowVisible(hwnd):+ return True+ buf = ctypes.create_unicode_buffer(512)+ user32.GetWindowTextW(hwnd, buf, 512)+ title = (buf.value or "").strip()+ if not title:+ return True+ low = title.lower()+ looks_kicad = any(k in low for k in ("kicad", "pcb editor", "schematic editor",+ "symbol editor", "footprint editor", "3d viewer"))+ if not looks_kicad:+ return True+ if not args.get("includeAll") and not _ERR_TITLE.search(title):+ return True+ texts = _enum_dialog_text(hwnd)+ # the message is the longest static; buttons are short ("OK", "Cancel")+ msg = max(texts, key=len) if texts else ""+ wins.append({"hwnd": hwnd, "title": title, "message": msg,+ "allText": texts[:12],+ "level": "error" if _ERR_TITLE.search(title) else "info"})+ return True++ try:+ user32.EnumWindows(cb, 0)+ except Exception as e: # pylint: disable=broad-except+ return {"success": False, "error": f"window enumeration failed: {type(e).__name__}: {e}"}++ return {"success": True, "count": len(wins), "dialogs": wins,+ "_hint": ("KiCad has NO log file and in GUI mode its errors go to DIALOGS, not "+ "stderr — so this, not kicad_log_tail, is where a failure reason lives. "+ "Read-only: nothing is clicked or raised. Dismiss with kicad_dismiss_dialogs "+ "(which DOES foreground). kicad_log_tail is the second channel and catches "+ "only what bypasses wxLog: crashes, asserts, stdio.")}