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
0.9.295: The tour now walks a real Adom board, adom/esc-g431 (149 footprints, 227 symbols): its STM32G431 symbol and LQFP-48 footprint are lifted from the wiki files at tour time and installed into the Adom library, then symbol, footprint, chip in 3D, schematic, board 2D, board 3D in that order. Captions were never painting because ab requires reason on desktop_caption; fixed, medium size, one line per beat. Narration re-rendered to match. Wording no longer claims the background: each beat's window is brought to the user once. Dead local 3D model paths in the board are dropped.
3 files changed
+203−89
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.294+0.9.295
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.294",+ "version": "0.9.295", "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/demo.py+201−87@@ -37,8 +37,25 @@ from .kicad_ui import handle_screenshot_all from kicad_detect import detect_kicad DEMO_LIB = "Adom"-IC_SYM, IC_FP = "AdomDemo_IC", "AdomDemo_SOIC8"-R_SYM, R_FP = "AdomDemo_R", "AdomDemo_R0805"+# John, 2026-09-03: "make it a nice sexy cool board, not a lame board". The+# tour now walks a REAL Adom board from the wiki, adom/esc-g431 (a brushless+# motor controller: 149 footprints, 227 schematic symbols, an STM32G431 MCU+# and six MOSFETs), and the chip beats show that board's own MCU: the symbol+# is lifted from the schematic's embedded lib_symbols and the footprint from+# the board's own U5, complete with KiCad's standard LQFP-48 3D body.+DEMO_BOARD = {+ "page": "adom/esc-g431",+ "name": "Adom ESC G431",+ "blurb": "a brushless motor controller",+ "files": {"schematic": "esc-g431.kicad_sch", "board": "esc-g431.kicad_pcb",+ "project": "src/esc-g431.kicad_pro"},+ "chipLibId": "MCU_ST_STM32G4:STM32G431C_6-8-B_Tx",+ "chipRef": "U5",+ "chipFootprint": "Package_QFP:LQFP-48_7x7mm_P0.5mm",+}+IC_SYM, IC_FP = "STM32G431C_6-8-B_Tx", "LQFP-48_STM32G431"+R_SYM, R_FP = "AdomDemo_R", "AdomDemo_R0805" # legacy generator names (unused by the tour)+_WIKI_FILES = "https://wiki.adom.inc/api/pages/{page}/files/{path}" # Beat ORDER is a reliability decision, not just a story decision (2026-08-16): # the design beats (schematic/board/board3d) are plain file-opens that need no@@ -47,7 +64,7 @@ R_SYM, R_FP = "AdomDemo_R", "AdomDemo_R0805" # so on 10.0.5 the plugin takes 1-2 minutes to bind after spawn. Running the # design beats first gives the plugin that time for free; part beats then take # the fast in-process path instead of racing a cold interpreter.-STEPS = ["schematic", "board", "board3d", "symbol", "footprint", "footprint3d"]+STEPS = ["symbol", "footprint", "footprint3d", "schematic", "board", "board3d"] # Screenshot labels. `schematic` / `board_2d` / `board_3d` deliberately match the # labels fusion_demo returns, so an AI driving BOTH bridges (Hydrogen's installer does)@@ -263,50 +280,142 @@ def _b64(s: str) -> str: return base64.b64encode(s.encode("utf-8")).decode() -def _prepare(kicad_info: dict) -> dict:- """Install the two demo parts into the Adom library + write the project."""- out = {"installed": [], "warnings": []}- for fname, sym, content in ((f"{IC_SYM}.kicad_sym", IC_SYM, _sym_lib(IC_SYM)),- (f"{R_SYM}.kicad_sym", R_SYM, _sym_lib(R_SYM))):- r = handle_install_symbol(kicad_info, {"fileName": fname, "symbolName": sym,- "fileContent": _b64(content), "quietInstall": True})- (out["installed"] if r.get("success") else out["warnings"]).append(- sym if r.get("success") else f"{sym}: {r.get('error')}")+def _sexpr_block(text: str, start: int) -> str:+ """The balanced s-expression starting at text[start] == '('. Quote-aware."""+ depth = 0; k = start; inq = False+ while k < len(text):+ c = text[k]+ if c == '"' and text[k - 1] != "\\":+ inq = not inq+ elif not inq:+ if c == "(":+ depth += 1+ elif c == ")":+ depth -= 1+ if depth == 0:+ return text[start:k + 1]+ k += 1+ return text[start:]+++def _fetch_board(demo_dir: Path) -> dict:+ """Download the wiki board's schematic, board and project into the demo+ dir (once a day; the page is public, no token needed on the desktop)."""+ import urllib.request, time as _t+ out = {}+ for key, rel in DEMO_BOARD["files"].items():+ dst = demo_dir / Path(rel).name+ fresh = dst.exists() and (_t.time() - dst.stat().st_mtime) < 86400 and dst.stat().st_size > 1000+ if not fresh:+ url = _WIKI_FILES.format(page=DEMO_BOARD["page"], path=rel)+ req = urllib.request.Request(url, headers={"User-Agent": "adom-bridge-kicad demo"})+ with urllib.request.urlopen(req, timeout=60) as r:+ data = r.read()+ if key == "board":+ data = _patch_models(data.decode("utf-8", errors="replace")).encode("utf-8")+ dst.write_bytes(data)+ out[key] = str(dst)+ return out+++def _patch_models(pcb: str) -> str:+ """The board's author referenced a few 3D bodies from his own disk+ (C:/Users/caleb/...). Those paths are dead everywhere else, so drop them;+ KiCad then simply draws the pads. Standard ${KICAD10_3DMODEL_DIR} bodies+ (resistors, capacitors, the LQFP-48 MCU, SOT-23, JST) stay as they are."""+ import re as _re+ return _re.sub(r'\n\s*\(model "(?:[A-Za-z]:/|\.\./)[^"]*"(?:(?!\n\t\t\(model)[\s\S])*?\n\t\t\)', "", pcb)+++def _extract_symbol(sch_text: str, lib_id: str) -> str:+ """One embedded library symbol (lib_symbols block) as a standalone .kicad_sym."""+ i = sch_text.find("(lib_symbols")+ lib = _sexpr_block(sch_text, i) if i >= 0 else ""+ j = lib.find(f'(symbol "{lib_id}"')+ if j < 0:+ raise KeyError(f"{lib_id} not in lib_symbols")+ block = _sexpr_block(lib, j)+ bare = lib_id.split(":", 1)[1] if ":" in lib_id else lib_id+ block = block.replace(f'(symbol "{lib_id}"', f'(symbol "{bare}"', 1)+ return f'(kicad_symbol_lib (version 20231120) (generator "adom-demo")\n{block}\n)\n'+++def _extract_footprint(pcb_text: str, fp_id: str, ref: str, new_name: str) -> str:+ """The board's placed footprint `ref` as a library .kicad_mod (placement,+ uuids, net assignments and sheet paths stripped; pads, silk, courtyard and+ the 3D model kept)."""+ import re as _re+ pos = 0+ while True:+ j = pcb_text.find(f'(footprint "{fp_id}"', pos)+ if j < 0:+ raise KeyError(f"{fp_id} {ref} not on the board")+ block = _sexpr_block(pcb_text, j)+ if f'(property "Reference" "{ref}"' in block:+ break+ pos = j + 10+ block = block.replace(f'(footprint "{fp_id}"', f'(footprint "{new_name}"', 1)+ block = _re.sub(r'\n\t\t\(at [^)]*\)', "", block, count=1) # placement+ block = _re.sub(r'\n\t\t\(uuid "[^"]*"\)', "", block)+ block = _re.sub(r'\n\t\t\(path "[^"]*"\)', "", block)+ block = _re.sub(r'\n\t\t\((?:sheetname|sheetfile) "[^"]*"\)', "", block)+ block = _re.sub(r'\n\t*\(net (?:\d+ )?"[^"]*"\)', "", block) # pads carry nets on a board (KiCad 10: no number)+ block = _re.sub(r'\n\t*\(pinfunction "[^"]*"\)', "", block)+ block = _re.sub(r'\n\t*\(pintype "[^"]*"\)', "", block)+ block = _re.sub(r'\(property "Reference" "[^"]*"', '(property "Reference" "REF**"', block, count=1)+ block = _re.sub(r'\(property "Value" "[^"]*"', f'(property "Value" "{new_name}"', block, count=1)+ block = _re.sub(r'\n\t\t\(uuid "[^"]*"\)', "", block)+ header = f'(footprint "{new_name}"\n\t(version 20240108)\n\t(generator "adom-demo")'+ block = block.replace(f'(footprint "{new_name}"', header, 1)+ return block + "\n" - soic = _model_path(kicad_info, "Package_SO.3dshapes", "SOIC-8_3.9x4.9mm_P1.27mm")- r08 = _model_path(kicad_info, "Resistor_SMD.3dshapes", "R_0805_2012Metric")- out["models3d"] = {"soic8": soic, "r0805": r08}- if not (soic and r08):- out["warnings"].append(- "KiCad's bundled 3D models were not found — pads/board still render, "- "component bodies will be missing in the 3D views.")- for fname, fp, content in ((f"{IC_FP}.kicad_mod", IC_FP, _fp_soic8(soic)),- (f"{R_FP}.kicad_mod", R_FP, _fp_r0805(r08))):- r = handle_install_footprint(kicad_info, {"fileName": fname, "footprintName": fp,- "fileContent": _b64(content), "quietInstall": True})- (out["installed"] if r.get("success") else out["warnings"]).append(- fp if r.get("success") else f"{fp}: {r.get('error')}") +def _prepare(kicad_info: dict) -> dict:+ """Fetch the wiki board, lift its MCU symbol + footprint into the Adom+ library, and hand back the paths every beat opens."""+ out = {"installed": [], "warnings": [], "board": None, "schematic": None, "project": None,+ "boardName": DEMO_BOARD["name"], "boardPage": DEMO_BOARD["page"]} d = _demo_dir()- # A force-killed KiCad leaves `~<file>.lck` lockfiles behind; the next open- # then pops a modal "File Open Warning" and the sheet stays untitled- # (observed live 2026-08-09: five blocked editor pairs). These are OUR- # generated files, so clearing stale locks is always safe. for lck in d.glob("*.lck"): try: lck.unlink() except Exception: pass- (d / "adom-demo.kicad_sch").write_text(_schematic(), encoding="utf-8")- (d / "adom-demo.kicad_pcb").write_text(_board(kicad_info), encoding="utf-8")- (d / "adom-demo.kicad_pro").write_text('{"board":{},"meta":{"filename":"adom-demo.kicad_pro","version":1},"schematic":{},"sheets":[]}', encoding="utf-8")- out["projectDir"] = str(d)- out["project"] = str(d / "adom-demo.kicad_pro")+ try:+ files = _fetch_board(d)+ except Exception as e: # pylint: disable=broad-except+ out["warnings"].append(f"could not fetch {DEMO_BOARD['page']} from the wiki: {e}")+ return out+ out.update({"schematic": files.get("schematic"), "board": files.get("board"),+ "project": files.get("project")})+ sch_text = Path(files["schematic"]).read_text(encoding="utf-8", errors="replace")+ pcb_text = Path(files["board"]).read_text(encoding="utf-8", errors="replace")+ try:+ sym = _extract_symbol(sch_text, DEMO_BOARD["chipLibId"])+ r = handle_install_symbol(kicad_info, {"fileName": f"{IC_SYM}.kicad_sym", "symbolName": IC_SYM,+ "fileContent": _b64(sym), "quietInstall": True})+ (out["installed"] if r.get("success") else out["warnings"]).append(+ IC_SYM if r.get("success") else f"{IC_SYM}: {r.get('error')}")+ except Exception as e: # pylint: disable=broad-except+ out["warnings"].append(f"{IC_SYM}: {e}")+ try:+ fp = _extract_footprint(pcb_text, DEMO_BOARD["chipFootprint"], DEMO_BOARD["chipRef"], IC_FP)+ r = handle_install_footprint(kicad_info, {"fileName": f"{IC_FP}.kicad_mod", "footprintName": IC_FP,+ "fileContent": _b64(fp), "quietInstall": True})+ (out["installed"] if r.get("success") else out["warnings"]).append(+ IC_FP if r.get("success") else f"{IC_FP}: {r.get('error')}")+ except Exception as e: # pylint: disable=broad-except+ out["warnings"].append(f"{IC_FP}: {e}")+ lqfp = _model_path(kicad_info, "Package_QFP.3dshapes", "LQFP-48_7x7mm_P0.5mm")+ out["models3d"] = {"lqfp48": lqfp, "soic8": lqfp} # soic8 key kept for has3d readers+ if not lqfp:+ out["warnings"].append("KiCad's bundled 3D model library is not installed on this machine: "+ "the 3D views show pads and board, not component bodies.") try: from handlers.win_focus import register_owned_project- register_owned_project(out["project"])+ if out["project"]:+ register_owned_project(out["project"]) except Exception: pass- out["schematic"] = str(d / "adom-demo.kicad_sch")- out["board"] = str(d / "adom-demo.kicad_pcb")+ out["projectDir"] = str(d) return out @@ -377,52 +486,51 @@ def _run_step(kicad_info: dict, step: str, prep: dict) -> dict: except Exception: loaded = True # can't verify — don't fail the beat on the checker return {"ok": bool(r.get("success")) and loaded, "window": "Symbol Editor",- "title": "4/6 · Schematic symbol",- "say": (f"This is the {IC_SYM} symbol, in the user's own KiCad symbol editor. "- "The bridge installed it into their Adom library a second ago — no file "- "copying, no dialogs."),- "pointOut": ["8 named pins (VDD, IN1/IN2, GND, OUT1/OUT2, EN, NC)",- "the body rectangle and pin numbering",- f"it is registered in the '{DEMO_LIB}' library, not a scratch file"],+ "title": "1/6 · The MCU symbol",+ "say": ("This is the STM32G431, the microcontroller on the board you are about to see, "+ "in your own KiCad Symbol Editor. The bridge lifted it from the board's schematic "+ "and installed it into your Adom library a moment ago."),+ "pointOut": ["48 pins across the MCU's units", "the body and pin numbering",+ f"registered in your '{DEMO_LIB}' library, not a scratch file"], "raw": r} if step == "footprint": r = handle_open_footprint_editor(kicad_info, {"libraryName": DEMO_LIB, "footprintName": IC_FP}) return {"ok": bool(r.get("success")), "window": "Footprint Editor",- "title": "5/6 · The footprint for that symbol",- "say": (f"Same part, now its land pattern: {IC_FP}. This is what actually gets "- "soldered — the symbol is the idea, the footprint is the copper."),- "pointOut": ["8 SMD pads on 1.27 mm pitch", "silkscreen outline + the pin-1 dot",- "the courtyard rectangle (keep-out for the assembler)"],+ "title": "2/6 · Its footprint",+ "say": ("Same chip, now its land pattern: a 48 pin LQFP on half millimetre pitch, "+ "taken from the board itself. The symbol is the idea, the footprint is the copper."),+ "pointOut": ["48 pads on 0.5 mm pitch", "silkscreen outline and the pin-1 mark",+ "the courtyard (the assembler's keep-out)"], "raw": r} if step == "footprint3d": import time; time.sleep(2) r = handle_open_3d_viewer(kicad_info, {"editor": "footprint"}) return {"ok": bool(r.get("success")), "window": "3D Viewer",- "title": "6/6 · That part in 3D",- "say": ("And here is the same footprint in three dimensions" +- (", with the real SOIC-8 body from KiCad's own 3D model library."- if has3d else " — pads only; KiCad's bundled 3D models weren't found.")),- "pointOut": (["the SOIC-8 package body sitting on its pads", "gold pad plating"]- if has3d else ["the bare pads (no bundled 3D model on this machine)"]),+ "title": "3/6 · The chip in 3D",+ "say": ("And the same chip in three dimensions" ++ (", the LQFP-48 body from KiCad's own 3D library sitting on its pads."+ if has3d else ". Pads only here: KiCad's 3D model library is not installed on this machine.")),+ "pointOut": (["the LQFP-48 body on its 48 pads", "gold pad plating"]+ if has3d else ["the bare pads (KiCad's 3D library is not installed here)"]), "raw": r} if step == "schematic": r = handle_open_schematic(kicad_info, {"filePath": prep["schematic"]}) return {"ok": bool(r.get("success")), "window": "Schematic Editor",- "title": "1/6 · A schematic using it",- "say": ("Now a real schematic sheet: the demo IC as U1 with two 10k resistors, "- "R1 and R2. The bridge generated and opened this project itself."),- "pointOut": ["U1 plus R1/R2, each properly annotated",- "the title block: 'Adom KiCad Bridge — Demo'"],+ "title": "4/6 · The ESC schematic",+ "say": ("Now the real schematic: the Adom ESC G431, a brushless motor controller, "+ "straight from its wiki page. Over two hundred symbols: the STM32 you just saw, "+ "six power MOSFETs, gate drivers, sensing and the connectors."),+ "pointOut": ["U5, the STM32G431 from the first beats", "the six MOSFET half-bridges",+ "the whole sheet came from wiki.adom.inc/adom/esc-g431"], "raw": r} if step == "board": r = handle_open_board(kicad_info, {"filePath": prep["board"]}) return {"ok": bool(r.get("success")), "window": "PCB Editor",- "title": "2/6 · The 2D board layout",- "say": ("The PCB side: the same three parts placed on a 70 by 40 millimetre "- "board outline, copper, silkscreen and courtyards all real."),- "pointOut": ["the SOIC-8 land pattern and two 0805 resistors",- "the Edge.Cuts board outline",- "'Pads 12' in the status bar — proof the footprints loaded"],+ "title": "5/6 · The board in 2D",+ "say": ("The layout of that board: one hundred and forty nine footprints, the MOSFETs "+ "around the edge, the MCU in the middle, copper, silkscreen and courtyards all real."),+ "pointOut": ["the six TDSON MOSFET land patterns", "the LQFP-48 in the middle",+ "the pad count in the status bar"], "raw": r} if step == "board3d": # Wait for the PCB editor to actually be findable — the board beat can@@ -441,13 +549,14 @@ def _run_step(kicad_info: dict, step: str, prep: dict) -> dict: time.sleep(2) r = handle_open_3d_viewer(kicad_info, {"editor": "pcb"}) return {"ok": bool(r.get("success")), "window": "3D Viewer",- "title": "3/6 · The whole board in 3D",- "say": ("And the finished board in 3D — this is the payoff: from a symbol to a "- "rendered assembly, every step driven from the cloud on the user's own "- "machine."),- "pointOut": (["all three component bodies on the green board"] if has3d- else ["the board and copper (component bodies need KiCad's 3D models)"])- + ["you can orbit it with the mouse — it is their real KiCad"],+ "title": "6/6 · The board in 3D",+ "say": ("And the finished ESC in 3D. This is the payoff: from one symbol to a rendered "+ "assembly, every step driven on your own machine, and each window brought to "+ "you once, then left alone."),+ "pointOut": (["the STM32, capacitors, resistors and connectors with real bodies",+ "the MOSFET pads (their vendor bodies are not in KiCad's library)"] if has3d+ else ["the board and copper (component bodies need KiCad's 3D model library)"])+ + ["orbit it with the mouse: it is your real KiCad"], "raw": r} return {"ok": False, "error": f"unknown step '{step}'", "validSteps": STEPS} @@ -458,8 +567,8 @@ def _run_step(kicad_info: dict, step: str, prep: dict) -> dict: import sys as _sys import threading as _threading -_BEAT_EST = {"prepare": 18, "symbol": 12, "footprint": 12, "part_3d": 16,- "schematic": 10, "board_2d": 10, "board_3d": 22}+_BEAT_EST = {"prepare": 25, "symbol": 12, "footprint": 12, "part_3d": 16,+ "schematic": 15, "board_2d": 15, "board_3d": 30} # the ESC is a real board: bigger files _JOB: dict = {"active": False} _JOB_LOCK = _threading.Lock()@@ -538,13 +647,13 @@ _CAPTION_ID = "kicad-demo" # long "mechanism" paragraphs of 2026-08-24 were the "large, ugly, far too long" # caption he called out; the dashboard's step log carries the mechanism now. _BEAT_CAPTIONS = {- "prepare": "KiCad tour 1/7: writing a real symbol, footprint, schematic and board into your KiCad",- "schematic": "KiCad tour 2/7: the schematic, opened in the background",- "board": "KiCad tour 3/7: the 2D board",- "board3d": "KiCad tour 4/7: the whole board in 3D",- "symbol": "KiCad tour 5/7: the symbol that was just installed",- "footprint": "KiCad tour 6/7: that part's footprint",- "footprint3d": "KiCad tour 7/7: the part in 3D on its pads",+ "prepare": "KiCad tour 1/7: fetching the Adom ESC G431 board and installing its STM32 into your library",+ "symbol": "KiCad tour 2/7: the STM32G431 symbol, lifted from the board's schematic",+ "footprint": "KiCad tour 3/7: its LQFP-48 footprint",+ "footprint3d": "KiCad tour 4/7: the chip in 3D",+ "schematic": "KiCad tour 5/7: the ESC schematic, 227 symbols",+ "board": "KiCad tour 6/7: the board in 2D, 149 footprints",+ "board3d": "KiCad tour 7/7: the board in 3D", } _BEAT_CAPTION_MS = {"prepare": 35000, "schematic": 25000, "board": 25000, "board3d": 40000, "symbol": 30000, "footprint": 30000, "footprint3d": 35000}@@ -641,8 +750,8 @@ def _post_transport(body: str, paused: bool = False) -> None: # the fallback for an ab older than 2.1.0 (the verb answers "unknown verb"). _PANEL_ID = "kicad-demo" _PANEL = {"ok": None} # None = untried, True = panel shown, False = fall back to the toast-_PANEL_STOPS = ["Preparing the project", "The schematic", "The 2D board", "The board in 3D",- "The symbol", "The footprint", "The part in 3D"]+_PANEL_STOPS = ["Fetching the ESC board", "The MCU symbol", "Its footprint", "The chip in 3D",+ "The ESC schematic", "The board in 2D", "The board in 3D"] _PANEL_SEEN: set = set() # `at` stamps already honored via the instant callback@@ -857,9 +966,14 @@ def _caption(text: str, duration_ms: int = 60000) -> None: # Fleet ruling (dash-demo, 2026-08-24): ab captions render at the # bridge's DEFAULT size and position, never custom - seven apps, one # caption look. Only id (replacement), text and duration are passed.+ # reason is REQUIRED (ab refuses a caption without one; that is why no+ # caption painted during the 2026-09-03 tours) and John asked for the+ # MEDIUM size explicitly. ad_client.call("desktop_caption",- {"id": _CAPTION_ID, "text": text,- "duration": duration_ms}, timeout=6)+ {"id": _CAPTION_ID, "text": text, "duration": duration_ms,+ "size": "medium", "position": "bottom",+ "reason": "Narrate the KiCad tour the user started, one line per beat"},+ timeout=6) except Exception: pass @@ -1012,7 +1126,7 @@ def _run_demo_background(kicad_info: dict) -> None: "failedSteps": [r["step"] for r in results if not r.get("ok")]}) return _play_narration("done")- _caption(f"KiCad tour done: {ok_ct} of {len(STEPS)} views opened in the background",+ _caption(f"KiCad tour done: {ok_ct} of {len(STEPS)} views of the Adom ESC G431, each brought to you once", duration_ms=6000) _job_update(active=False, done=True, percent=100, elapsedSec=round(_t.monotonic() - t0),