app
adom-desktop-altium-bridge
Public Unreviewedby barrett-land
Adom Desktop bridge for Altium Designer — add LCSC/JLCPCB-tagged parts to file-based libraries (symbols, footprints with 3D, value-only display) by driving Altium via DelphiScript.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
#!/usr/bin/env python3
"""Adom Altium Bridge — localhost HTTP server (runs on the user's Windows desktop).
Spawned on demand by Adom Desktop (v1.8.0+ dynamic bridge). Implements the standard
dynamic-bridge contract:
GET /status -> {"ok": true, "version": "...", ...}
POST /command body {"command"|"verb": "altium_*", "args": {...}}
-> {"success": true, "output": "<json-string>"} | {"success": false, "error", "_hint"}
Phase 0 verbs: altium_ping, altium_detect. Later phases add the IntLib/DbLib/365
component-add verbs and BOM/CPL export (those route to the in-Altium add-in on :8775).
Usage: python server.py [--port 8801]
"""
import json
import sys
import traceback
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from altium_detect import detect_altium
from addin_client import addin_call
DEFAULT_PORT = 8801
VERSION = (Path(__file__).parent / "BRIDGE_VERSION").read_text().strip() \
if (Path(__file__).parent / "BRIDGE_VERSION").exists() else "0.0.0"
# Address of the in-Altium add-in (Phase 2+); not used by Phase 0 verbs.
ADDIN_URL = "http://127.0.0.1:8775"
def handle_ping(args: dict) -> dict:
"""Liveness + a quick Altium-presence summary."""
info = detect_altium()
return {
"ok": True,
"bridge": "altium",
"version": VERSION,
"altiumInstalled": info["installed"],
"altiumVersion": info.get("version"),
"altiumExe": info.get("exe"),
"addinUrl": ADDIN_URL,
}
def handle_detect(args: dict) -> dict:
"""Full Altium install + library-location snapshot for this machine."""
info = detect_altium()
info["ok"] = True
if not info["installed"]:
info["_hint"] = (
"Altium Designer not found. Expected C:\\Program Files\\Altium\\AD<NN>\\X2.exe "
"or an 'Altium Designer' Uninstall registry entry."
)
return info
def handle_addin_ping(args: dict) -> dict:
"""Round-trip the in-Altium dispatcher (proves request/response works live)."""
return addin_call("ping", {}, timeout=args.get("timeout", 60))
def handle_add_component(args: dict) -> dict:
"""Add/refresh a component's identity params (incl. 'LCSC Part #') in a SchLib."""
if not args.get("schlib"):
return {"ok": False, "error": "missing 'schlib' (path to the .SchLib on the desktop)"}
keys = ("schlib", "component", "lcsc", "mpn", "manufacturer",
"value", "description", "package", "datasheet")
return addin_call("add_lcsc_param", {k: args.get(k) for k in keys},
timeout=args.get("timeout", 90))
def handle_read_params(args: dict) -> dict:
"""Read back a component's parameters (verification)."""
return addin_call("read_params",
{"schlib": args.get("schlib"), "component": args.get("component")},
timeout=args.get("timeout", 60))
def handle_create_and_attach(args: dict) -> dict:
"""Create a fresh SchLib + component, attach LCSC/identity params, read back, save.
No external symbol source needed — proves the keystone end to end."""
keys = ("component", "schlib", "lcsc", "mpn", "manufacturer", "value", "description")
return addin_call("create_and_attach", {k: args.get(k) for k in keys},
timeout=args.get("timeout", 90))
def handle_compile_intlib(args: dict) -> dict:
"""Generate a .LibPkg wrapping the .SchLib, then compile it to .IntLib."""
import os
schlib = args.get("schlib") or r"C:\Users\Public\AdomAltium\Adom.SchLib"
d = os.path.dirname(schlib)
libname = os.path.splitext(os.path.basename(schlib))[0]
libpkg = os.path.join(d, libname + ".LibPkg")
# CreateIntegratedLibrary wants the full .IntLib FILE path, not a folder.
output = args.get("output_file") or os.path.join(d, libname + ".IntLib")
# .LibPkg is an INI project file; reference the SchLib by relative name (same dir).
content = (
"[Design]\r\n"
"Version=1.0\r\n"
"HierarchyMode=1\r\n"
"\r\n"
"[Document1]\r\n"
"DocumentPath=" + os.path.basename(schlib) + "\r\n"
)
try:
with open(libpkg, "w", encoding="utf-8") as fh:
fh.write(content)
except OSError as e:
return {"ok": False, "error": "could not write LibPkg: {}".format(e)}
return addin_call("compile_intlib", {"libpkg": libpkg, "output": output},
timeout=args.get("timeout", 180))
def handle_extract_intlib(args: dict) -> dict:
"""Decompile a compiled .IntLib into editable source libs (.SchLib + .PcbLib);
returns the extracted document paths so the caller can edit them."""
if not args.get("intlib"):
return {"ok": False, "error": "missing 'intlib' (path to the .IntLib on the desktop)"}
return addin_call("extract_intlib", {"intlib": args.get("intlib")},
timeout=args.get("timeout", 150))
def handle_create_resistor(args: dict) -> dict:
"""Create a 2-pin resistor symbol with params + footprint link, save to a Resistors lib."""
import os
schlib = args.get("schlib") or r"C:\Users\Public\AdomLibs\Resistors.SchLib"
try:
os.makedirs(os.path.dirname(schlib), exist_ok=True)
except OSError:
pass
payload = {
"schlib": schlib,
"component": args.get("component") or "R",
"lcsc": args.get("lcsc"),
"mpn": args.get("mpn"),
"manufacturer": args.get("manufacturer"),
"value": args.get("value"),
"description": args.get("description"),
"footprint": args.get("footprint") or "R0603",
"footprint_lib": args.get("footprint_lib") or "Resistors.PcbLib",
}
return addin_call("create_resistor", payload, timeout=args.get("timeout", 120))
def handle_clone_symbol(args: dict) -> dict:
keys = ("schlib", "template", "component", "footprint", "designator")
return addin_call("clone_symbol", {k: args.get(k) for k in keys}, timeout=args.get("timeout", 120))
def handle_rename_component(args: dict) -> dict:
"""Rename a SchLib component in place; optionally update Description, footprint
link, and designator. Preserves the exact symbol graphics/pins/params."""
keys = ("schlib", "component", "newname", "footprint", "designator", "description")
return addin_call("rename_component", {k: args.get(k) for k in keys},
timeout=args.get("timeout", 120))
def handle_rename_footprint(args: dict) -> dict:
"""Rename a footprint (IPCB_LibComponent) inside a PcbLib in place."""
keys = ("pcblib", "footprint", "newname")
return addin_call("rename_footprint", {k: args.get(k) for k in keys},
timeout=args.get("timeout", 120))
def handle_set_visibility(args: dict) -> dict:
"""Set which parameters are visible on a component (others hidden); controls
what shows when the symbol is placed on a schematic."""
keys = ("schlib", "component", "visible")
return addin_call("set_visibility", {k: args.get(k) for k in keys},
timeout=args.get("timeout", 90))
def handle_add_body(args: dict) -> dict:
"""Attach a simple extruded 3D body (box, sized in mils) to a footprint."""
keys = ("pcblib", "footprint", "bw_mil", "bh_mil", "h_mil", "so_mil")
return addin_call("add_body", {k: args.get(k) for k in keys},
timeout=args.get("timeout", 120))
def handle_import_part(args: dict) -> dict:
"""Batched single-round-trip import: from already-extracted src SchLib+PcbLib,
rename footprint + save-as into the target PcbLib; attach LCSC/params + rename
symbol + link + save-as into the target SchLib; install both; read back params."""
keys = ("src_sch", "src_pcb", "tgt_sch", "tgt_pcb", "name", "footprint",
"designator", "lcsc", "mpn", "manufacturer", "value", "description", "package")
return addin_call("import_part", {k: args.get(k) for k in keys},
timeout=args.get("timeout", 180))
def handle_list_symbols(args: dict) -> dict:
return addin_call("list_symbols", {"schlib": args.get("schlib")}, timeout=args.get("timeout", 60))
def handle_read_symbol(args: dict) -> dict:
return addin_call("read_symbol",
{"schlib": args.get("schlib"), "component": args.get("component")},
timeout=args.get("timeout", 60))
def handle_install_library(args: dict) -> dict:
"""Register a file-based library (already on the desktop) into Available Libraries."""
return addin_call("install_library", {"path": args.get("path")}, timeout=args.get("timeout", 60))
def handle_build_symbol(args: dict) -> dict:
"""Build/append a symbol (N pins + footprint-implementation link) into a SchLib."""
return addin_call("build_symbol", args, timeout=args.get("timeout", 120))
def handle_build_footprint(args: dict) -> dict:
"""Build an Altium footprint from a pad spec + optional STEP body. The STEP must
already be on the desktop (the orchestrator send_files it first). Pads come as
pad0..padN keys (CommaText: smd,x,y,w,h,drill,name in mils)."""
return addin_call("build_footprint", args, timeout=args.get("timeout", 150))
def handle_list_docs(args: dict) -> dict:
return addin_call("list_docs", {}, timeout=args.get("timeout", 60))
def handle_remove_component(args: dict) -> dict:
return addin_call("remove_component",
{"schlib": args.get("schlib"), "component": args.get("component")},
timeout=args.get("timeout", 90))
def handle_close_doc(args: dict) -> dict:
return addin_call("close_doc", {"path": args.get("path")}, timeout=args.get("timeout", 60))
def handle_cleanup_libs(args: dict) -> dict:
"""Uninstall available libraries whose path matches any comma-separated needle."""
match = args.get("match") or "adomaltium,schlib1,adom.intlib,adom.schlib"
return addin_call("cleanup_libs", {"match": match}, timeout=args.get("timeout", 60))
COMMAND_HANDLERS = {
"altium_ping": handle_ping,
"altium_detect": handle_detect,
"altium_compile_intlib": handle_compile_intlib,
"altium_extract_intlib": handle_extract_intlib,
"altium_cleanup_libs": handle_cleanup_libs,
"altium_addin_ping": handle_addin_ping,
"altium_add_component": handle_add_component,
"altium_read_params": handle_read_params,
"altium_create_and_attach": handle_create_and_attach,
"altium_create_resistor": handle_create_resistor,
"altium_list_docs": handle_list_docs,
"altium_remove_component": handle_remove_component,
"altium_close_doc": handle_close_doc,
"altium_build_footprint": handle_build_footprint,
"altium_build_symbol": handle_build_symbol,
"altium_install_library": handle_install_library,
"altium_list_symbols": handle_list_symbols,
"altium_read_symbol": handle_read_symbol,
"altium_clone_symbol": handle_clone_symbol,
"altium_rename_component": handle_rename_component,
"altium_rename_footprint": handle_rename_footprint,
"altium_import_part": handle_import_part,
"altium_add_body": handle_add_body,
"altium_set_visibility": handle_set_visibility,
}
def dispatch(verb: str, args: dict) -> dict:
handler = COMMAND_HANDLERS.get(verb)
if handler is None:
return {
"success": False,
"error": f"Unknown verb: {verb}",
"_hint": f"Valid altium verbs: {', '.join(sorted(COMMAND_HANDLERS))}.",
}
payload = handler(args or {})
# Dynamic-bridge contract: success envelope with output as a JSON string.
return {"success": True, "output": json.dumps(payload)}
class AltiumBridgeHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/status":
self._respond(200, {"ok": True, "bridge": "altium", "version": VERSION})
else:
self._respond(404, {"ok": False, "error": "Not found"})
def do_POST(self):
if self.path != "/command":
self._respond(404, {"success": False, "error": "Not found"})
return
length = int(self.headers.get("Content-Length", 0))
try:
request = json.loads(self.rfile.read(length) or b"{}")
except json.JSONDecodeError as e:
self._respond(400, {"success": False, "error": f"Invalid JSON: {e}"})
return
verb = request.get("command") or request.get("verb") or ""
args = request.get("args", {})
print(f"[Altium Bridge] {verb} | {json.dumps(args)}")
try:
self._respond(200, dispatch(verb, args))
except Exception as e:
traceback.print_exc()
self._respond(500, {"success": False, "error": f"Internal error: {e}"})
def _respond(self, status: int, data: dict):
body = json.dumps(data).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, fmt, *args):
print(f"[Altium Bridge] {args[0]} {args[1]} {args[2]}")
def main():
port = DEFAULT_PORT
if "--port" in sys.argv:
idx = sys.argv.index("--port")
if idx + 1 < len(sys.argv):
port = int(sys.argv[idx + 1])
info = detect_altium()
print(f"[Altium Bridge] v{VERSION} | Altium installed: {info['installed']} "
f"({info.get('version')}) exe={info.get('exe')}")
server = ThreadingHTTPServer(("127.0.0.1", port), AltiumBridgeHandler)
print(f"[Altium Bridge] Listening on http://127.0.0.1:{port} (status: /status)")
try:
server.serve_forever()
except KeyboardInterrupt:
server.server_close()
if __name__ == "__main__":
main()