app
Adom Desktop - 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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
"""KiCad automation-capability reporting (version-awareness).
The bridge should be able to tell the user, in plain terms:
- what automation THEIR installed KiCad supports,
- what's gated behind enabling the API server,
- and when upgrading KiCad would unlock more deterministic automation.
This is deliberately separate from kicad_detect.py: detection answers
"what's installed and where"; this answers "what can we actually drive,
and what should the user do to get more."
Ground truth (measured against KiCad 10.0.1 on 2026-06-25, and the
public KiCad IPC-API roadmap):
* KiCad < 9.0 → no IPC API at all. Only kicad-cli (headless convert/
lint/erc/drc) + GUI launch. Symbol/footprint *delivery* works (file +
lib-table), but nothing can be driven on the canvas.
* KiCad 9.0 / 10.x → IPC API present and **board automation WORKS**
(get_board() → place via create_items()+push_commit()). CRITICAL gotcha
(measured 10.0.1, 2026-06-25): KiCad exposes ONE API socket, owned by
whichever instance bound it. If the PROJECT MANAGER (kicad.exe) owns it,
board commands return "no handler available". When **pcbnew is the
API-owning instance**, get_board() works. So deterministic footprint
placement requires routing IPC to a pcbnew that owns the socket.
The schematic/symbol editor has NO IPC handler yet, so opening a
*symbol* on the canvas stays assisted (Win32) automation.
* KiCad 11 (when it ships) → SWIG bindings removed; IPC is the only
Python path and broadens to the schematic editor. Confirmed against
kipy (kicad-python): `KiCad.get_schematic()` is tagged "Added in …
(KiCad 11)" — the schematic object is NOT in KiCad 10.x, so on 10.x
symbol/schematic work stays assisted (Win32) automation. When 11 lands,
symbol delivery flips from "assisted" to "ipc" with no caller change —
they just read `automation.symbol`. (Verified 2026-07: get_board() is
the only document object exposed by kipy on 10.0.x.)
The IPC API also requires the user to ENABLE the API server
(Preferences > Plugins > "Enable KiCad API server"); we detect that from
kicad_common.json so we can tell them (or fix it) instead of timing out.
"""
import json
import re
from pathlib import Path
# Latest stable KiCad major we know about, for "is an upgrade available?"
# messaging. Bump when a new stable major ships. (KiCad 10 is current as of
# 2026-06; 11 removes SWIG and is expected to broaden IPC.)
LATEST_KNOWN_STABLE_MAJOR = 10
def _version_major_minor(version: str) -> tuple[int, int]:
m = re.match(r"^(\d+)(?:\.(\d+))?", str(version or ""))
if not m:
return (0, 0)
return (int(m.group(1)), int(m.group(2) or 0))
def _api_server_enabled(config_dir: str | None) -> bool | None:
"""Read api.enable_server from <config_dir>/kicad_common.json.
Returns True/False if we can read it, or None if the file/field is
missing (older KiCad without the setting, or unreadable). The bridge
needs this on for any IPC call to connect — when it's off, every IPC
verb otherwise times out silently.
"""
if not config_dir:
return None
common = Path(config_dir) / "kicad_common.json"
if not common.exists():
return None
try:
data = json.loads(common.read_text(encoding="utf-8", errors="replace"))
except (OSError, ValueError):
return None
api = data.get("api")
if not isinstance(api, dict) or "enable_server" not in api:
return None
return bool(api.get("enable_server"))
def capability_for_version(info: dict) -> dict:
"""Build the capability report for a single installed KiCad version."""
version = info.get("version") or "0.0"
major, _minor = _version_major_minor(version)
ipc_supported = major >= 9 # KiCad 9.0+ ships the IPC API server
api_enabled = _api_server_enabled(info.get("config_dir"))
# What can each artifact kind be driven by, on THIS version?
# "ipc" → deterministic via the IPC API (best path; no AI). Verified
# working on 10.0.1 — get_board()+create_items()+push_commit,
# PROVIDED pcbnew (not the project manager) owns the API socket.
# "assisted" → IPC can't do it; needs assisted/Win32 automation
# "cli" → only file delivery + headless kicad-cli; no canvas drive
fp_board = "ipc" if ipc_supported else "cli"
automation = {
"footprint": fp_board,
"board": fp_board,
"symbol": "assisted", # no schematic/symbol IPC handler yet
"schematic": "assisted",
}
# Upgrade guidance — the "hey, your KiCad doesn't have the automation
# APIs, let's upgrade you" message the user asked for.
upgrade = None
if not ipc_supported:
upgrade = {
"recommended": True,
"reason": (
f"KiCad {version} predates the IPC automation API (added in 9.0). "
f"Upgrading unlocks deterministic, no-AI footprint/board automation "
f"and live canvas control."
),
"unlocks": ["ipc_api", "deterministic_footprint", "deterministic_board"],
}
elif major < LATEST_KNOWN_STABLE_MAJOR:
upgrade = {
"recommended": False,
"reason": (
f"KiCad {version} has the IPC API but a newer stable "
f"(KiCad {LATEST_KNOWN_STABLE_MAJOR}+) may broaden automation."
),
"unlocks": [],
}
notes = []
if ipc_supported and api_enabled is False:
notes.append(
"IPC API server is DISABLED (Preferences > Plugins > 'Enable KiCad "
"API server'). Enable it (the bridge can do this for you) or every "
"IPC automation call will hang."
)
if ipc_supported and automation["symbol"] == "assisted":
notes.append(
"Symbol-editor IPC is not exposed in this KiCad yet, so opening a "
"symbol on the canvas uses assisted automation. It will become "
"deterministic automatically when KiCad ships the schematic IPC API."
)
return {
"version": version,
"ipcApiSupported": ipc_supported, # does this KiCad have the API at all
"ipcApiServerEnabled": api_enabled, # True/False/None (None = unknown/older)
"ipcClientInstalled": bool(info.get("ipcApiAvailable")), # kipy importable
"automation": automation,
"upgrade": upgrade,
"notes": notes,
}
def build_capability_report(all_kicad_versions: list[dict]) -> dict:
"""Top-level capability map across all installed KiCad versions.
Shape:
{
"kicadInstalled": bool,
"latestInstalled": "10.0" | None,
"perVersion": [ capability_for_version(...) , ... ],
"summary": "<one-line human summary for the newest install>",
}
"""
if not all_kicad_versions:
return {
"kicadInstalled": False,
"latestInstalled": None,
"perVersion": [],
"summary": (
"KiCad is not installed. The bridge can install it for you "
"(winget) — ask to 'install KiCad'."
),
}
per_version = [capability_for_version(v) for v in all_kicad_versions]
newest = per_version[0]
nv = newest["version"]
if not newest["ipcApiSupported"]:
summary = (
f"KiCad {nv} is installed but too old for the automation API. "
f"Upgrade to KiCad {LATEST_KNOWN_STABLE_MAJOR}+ to unlock deterministic automation."
)
elif newest["ipcApiServerEnabled"] is False:
summary = (
f"KiCad {nv} supports IPC automation, but the API server is OFF. "
f"Enable it to drive KiCad (the bridge can do this for you)."
)
else:
summary = (
f"KiCad {nv}: IPC automation ready. Footprints/boards are deterministic; "
f"symbols use assisted automation until KiCad exposes the schematic IPC API."
)
return {
"kicadInstalled": True,
"latestInstalled": nv,
"perVersion": per_version,
"summary": summary,
}