---
name: kicad-bridge-dev
description: DEVELOPER skill for building/debugging the Adom Desktop KiCad bridge. NOT shipped to users (source-only). Read before editing bridge code — architecture, the 4 control surfaces, how to add a verb, and every fresh-machine gotcha this bridge has hit. Trigger words — kicad bridge dev, edit kicad bridge, add kicad verb, kicad bridge architecture, debug kicad bridge, kicad control surfaces, kicad fresh machine.
---

# kicad-bridge-dev

Developer notes for the `adom-desktop-kicad-bridge`. Source-only (belongs in the repo, never shipped to a user install). Read `adom-desktop-bridge-sdk` first for the AD-core-vs-author boundary.

## What it is

A reverse bridge AD spawns on the user's laptop (`spawn.kind: python`, `entrypoint: server.py`, `port: 0`). It adds the `kicad_*` verb namespace. AD provisions Python (Runtime contract v1.9.63+) and passes `ADOM_BIND_HOST` — bind that, never `0.0.0.0`.

## The 4 control surfaces (how a verb reaches KiCad)

1. **kicad-cli** (`subprocess` → `kicad-cli.exe`) — headless: `pcb drc`, `sch erc`, `pcb/sch export` (gerber/pdf/svg/step/bom), `pcb/sch upgrade`. No GUI. Handlers: `run_drc`, `run_erc`, `lint_*`, `export.py`, `format_upgrade`, `bom.py`, `netlist.py`.
2. **KiCad IPC API (kipy)** — KiCad 9+. Board/schematic introspection + automation (e.g. `place_footprint` confirms via `kipy get_footprints()`). Expands with KiCad 11.
3. **Embedded-Python reverse bridge** (`plugin_payload/adom_bridge.py`) — `usercustomize.py` auto-injects at KiCad's USER_SITE → runs an HTTP server INSIDE each KiCad process, writes a `%TEMP%` discovery file; container-side `handlers/bridge_client.py` POSTs JSON-RPC to `/rpc`, dispatched onto KiCad's wx UI thread via `wx.CallAfter`. Methods: `ping`, `list_frames`, `get_menu_ids`, `wm_command`. Verbs: `bridge_status`, `bridge_call`.
4. **Win32 / UIA / SendKeys** (`handlers/kicad_ui.py`, ctypes/user32) — the last resort: `WM_COMMAND` (open editors), `SendInput`/`keybd_event` (`alt+3`, `ctrl+s`), `PrintWindow`+`EnumWindows` (screenshots), `GetWindowRect`. For opening the 3D viewer, screenshots, clicks, keystrokes, window management.

## Adding a verb (3 places, keep in sync)

1. Write `handle_<verb>` in a handler; return a dict with `success` + a rich **`_hint`** (mandatory — the AI reads verb OUTPUT, not this skill).
2. Register in `server.py` `COMMAND_HANDLERS`.
3. Add `kicad_<verb>` to `bridge.json` `verbs[]` AND an entry (summary/hint/related/pitfalls) to `_VERB_CATALOG` in `server.py` (the `kicad_describe` catalog). A test checks these three stay aligned.

**Dispatcher special cases** (`dispatch_command`): `readiness`, `describe`, `check_for_updates` return BEFORE the plugin auto-install + the KiCad-not-installed pre-check (they must work with KiCad absent). `upgrade` also bypasses that pre-check (it INSTALLS KiCad — do not gate the installer behind "KiCad not installed") and refreshes the detection cache on success.

## Fresh-machine gotchas (all hard-won, all fixed — don't regress them)

- **Install scope is mandatory.** KiCad 10's NsisMultiUser installer: bare `/S` = `rc=666660` (invalid params). Pass `/allusers /S` (elevated) or `/currentuser /S` (per-user, `%LOCALAPPDATA%\Programs\KiCad`, **no UAC**). See `handlers/upgrade.py`.
- **TLS on the portable Python.** AD's provisioned Python has no CA bundle → `CERTIFICATE_VERIFY_FAILED`. We bundle `certs/cacert.pem` (certifi) and load it in `_ssl_context()`. Any new https fetch must use that context.
- **Truncated downloads** read as EOF and pass MZ/size checks, then NSIS fails at install. `upgrade.py` HEADs `Content-Length` and refuses/deletes a size-mismatched file.
- **Config doesn't exist pre-first-launch.** `%APPDATA%/kicad/<ver>/` (lib tables) is created on KiCad's first GUI run. `kicad_detect.ensure_win_user_config()` bootstraps it (seeds tables from the install template) so `install_library`/`_footprint` work on a never-launched KiCad.
- **Detection cache is built once at startup.** After `kicad_upgrade` installs KiCad, `dispatch_command` re-runs `detect_all_kicad_versions()` so subsequent verbs see it without a bridge restart. Detect paths include `%LocalAppData%\Programs\KiCad` (per-user installs).
- **Blocking dialogs stall everything.** `handlers/close_windows.py` scans EVERY window owned by a running KiCad process (by PID — `_win_scan_kicad_dialogs`, reliable; title/owner matching is NOT), auto-expires benign ones (OpenGL notice), screenshots them, and returns hints. `window_info` self-heals by default; `kicad_dismiss_dialogs {all|forceSoftwareCanvas}`.
- **GPU-less hosts.** No OpenGL → the notice pops and pcbnew/3D may not render. On expiring that notice we persist `graphics.canvas_type=2` (Cairo) to `kicad_common.json`. NOTE: a truly minimal VM (Hyper-V, no GPU) may still not render the heavy editors even in Cairo — that's the hardware. Validate GUI/screenshot work on a real-GPU machine.

## Diagnostics you can call

- `kicad_upgrade {"diagnoseOnly":true}` — token elevation type + UAC `EnableLUA` + cached-installer/truncation facts (no install).
- `kicad_dismiss_dialogs {"debug":true}` — raw process-based dialog scan (PIDs + every KiCad window + class + isDialog).

## Testing on a fresh AD / VM

`refresh_bridges {name:kicad}` pulls the new cache; the RUNNING instance keeps its old code until it dies — `bridge_kill {name:kicad}` forces a fresh spawn that loads the new version and re-detects. The first verb after a kill returns a `bridge_starting` envelope — retry. Confirm the running version via `bridge_log_read` ("starting v0.9.xx").
