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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
"""Handler for place_footprint command.
DETERMINISTIC footprint placement onto a PCB — no AI, no fragile protobuf.
Approach: take the footprint's s-expression (a .kicad_mod is already a valid
`(footprint ...)` block), give it a position + uuid, splice it into a known-good
blank KiCad board template, and open the result in pcbnew. Proven 2026-06-25
(kipy get_footprints() confirmed the placed footprint + pad count).
This is a PREVIEW placement (a fresh board with just this footprint) — it never
touches the user's existing board, so it can't clobber their work. The footprint
is also already in the Adom library (via install_footprint), so the user can drag
it into their own board from there.
Args:
fileName (required): e.g. "BMI423.kicad_mod"
fileContent (required): base64-encoded .kicad_mod content
footprintName (opt): parsed from the content if omitted
x, y (opt): placement in mm (default board-center-ish 148,105)
open (opt): open the board in pcbnew (default True)
Returns: {success, boardPath, footprintName, placedAt, opened, output}
"""
import base64
import re
import subprocess
import sys
import uuid
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from proc import run as _safe_run # noqa: E402
_TEMPLATE = Path(__file__).resolve().parent.parent / "templates" / "blank-board.kicad_pcb"
def handle_place_footprint(kicad_info: dict, args: dict) -> dict:
if not kicad_info.get("installed"):
return {"success": False, "error": "KiCad not installed",
"_hint": "Install KiCad from https://www.kicad.org/download/ then retry."}
file_name = args.get("fileName", "")
file_content = args.get("fileContent", "")
if not file_content:
return {"success": False, "error": "No fileContent (base64 .kicad_mod) specified"}
try:
fp_text = base64.b64decode(file_content).decode("utf-8", "replace").strip()
except Exception as e:
return {"success": False, "error": f"could not decode fileContent: {e}"}
# Accept legacy `(module ...)` footprints (KiCad ≤5 / EAGLE-imported / many
# chip-fetcher parts still ship these). KiCad auto-migrates them on load, but the
# board *file* parser wants the modern `(footprint ...)` keyword, so normalize:
# rename the wrapper and quote the top-level (layer X) token the splice relies on.
if fp_text.startswith("(module"):
fp_text = re.sub(r'^\(module\b', '(footprint', fp_text, count=1)
# quote an unquoted top-level layer name: (layer F.Cu) -> (layer "F.Cu")
fp_text = re.sub(r'\(layer\s+([A-Za-z][\w.]*)\s*\)', r'(layer "\1")', fp_text, count=1)
if not fp_text.startswith("(footprint"):
return {"success": False, "error": "fileContent is not a .kicad_mod (must start with '(footprint' or '(module')"}
fp_name = args.get("footprintName") or ""
if not fp_name:
m = re.search(r'\(footprint\s+"([^"]+)"', fp_text)
fp_name = m.group(1) if m else Path(file_name).stem or "footprint"
x = args.get("x", 148.0)
y = args.get("y", 105.0)
if not _TEMPLATE.exists():
return {"success": False, "error": f"board template missing at {_TEMPLATE}"}
skel = _TEMPLATE.read_text(encoding="utf-8")
# Make the footprint a board item: inject a uuid + position right after its
# first (layer "...") token, then splice before the board's final paren.
uid = str(uuid.uuid4())
fp_board = re.sub(
r'(\(layer\s+"[^"]+"\))',
r'\1\n\t\t(uuid "%s")\n\t\t(at %s %s)' % (uid, x, y),
fp_text, count=1,
)
idx = skel.rstrip().rfind(")")
combined = skel[:idx].rstrip() + "\n" + fp_board + "\n)\n"
user_dir = kicad_info.get("user_dir") or str(Path.home())
out_path = Path(user_dir) / f"adom-place-{fp_name}.kicad_pcb"
try:
out_path.write_text(combined, encoding="utf-8")
except OSError as e:
return {"success": False, "error": f"could not write board: {e}"}
opened = False
open_it = args.get("open", True)
if open_it:
pcbnew_exe = kicad_info.get("pcbnew_exe")
if pcbnew_exe and Path(pcbnew_exe).exists():
try:
# DETACHED launch — a GUI must NOT go through subprocess.run(timeout=…),
# which KILLS the child on timeout. Popen fire-and-forget, DETACHED_PROCESS
# so pcbnew outlives the bridge and inherits no console handle.
flags = 0x00000008 if sys.platform == "win32" else 0 # DETACHED_PROCESS
_si = None
try:
from handlers import win_focus
_si = win_focus.background_startupinfo() # SW_SHOWNOACTIVATE — open behind the user
except Exception:
_si = None
subprocess.Popen(
[pcbnew_exe, str(out_path)],
creationflags=flags,
startupinfo=_si,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
close_fds=True,
)
opened = True
except OSError as e:
return {"success": True, "boardPath": str(out_path), "footprintName": fp_name,
"placedAt": [x, y], "opened": False,
"output": f"Board written but pcbnew launch failed: {e}",
"_hint": f"Open it manually: {out_path}"}
return {
"success": True,
"boardPath": str(out_path),
"footprintName": fp_name,
"placedAt": [x, y],
"opened": opened,
"output": f"Placed footprint '{fp_name}' at ({x}, {y}) mm on a preview board"
+ (" and opened it in pcbnew" if opened else ""),
}