app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
← Commit history
v1.6.64: ad_client DISCOVERS AD's loopback direct-API when ADOM_DIRECT_API_URL is not injected (AD 1.9.103 VM had the API up on 127.0.0.1 but no env var, so available() short-circuited False and everything fell back to slow CLI relays). Fixes seat-resolve + notify + ownedPopupCount at the ROOT; honest seat click-counting.
5 files changed
+78−10
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.6.63+1.6.64
ad_client.py+70−2@@ -33,9 +33,77 @@ _caps_cache = None _self_name_cache = None +_discovered_base = None # None = not tried, "" = tried+failed, str = discovered base URL+++def _raw_get(url, timeout=4):+ """Low-level GET that does NOT go through _base_url (used by discovery so it can't recurse).+ Returns parsed JSON, or None."""+ try:+ req = urllib.request.Request(url, headers={"Accept": "application/json"}, method="GET")+ with urllib.request.urlopen(req, timeout=timeout) as resp:+ return json.loads(resp.read().decode("utf-8", "replace"))+ except Exception:+ return None+++def _looks_like_ad_api(data) -> bool:+ """Does a /commands response look like AD's verb catalog (so we don't bind a random service)?"""+ if isinstance(data, dict):+ if isinstance(data.get("commands"), list):+ return True+ # AD's /commands also carries a _hint mentioning POST /command+ if "command" in (data.get("_hint") or "").lower():+ return True+ return isinstance(data, list)+++def _discover_direct_api():+ """Find AD's loopback direct-API base by port-probing, for the case where AD did NOT inject+ ADOM_DIRECT_API_URL into our spawn env (seen live on an AD 1.9.103 Hyper-V VM: the direct API+ WAS up on 127.0.0.1 but the env var was absent, so available() short-circuited to False and the+ bridge fell back to slow CLI relays - John caught it 2026-07-14). We enumerate the loopback TCP+ ports adom-desktop.exe is LISTENING on and probe each `/commands`; the one that returns AD's verb+ catalog is the direct API. Cached for the process lifetime. Windows-only + best-effort; never raises."""+ global _discovered_base+ if _discovered_base is not None:+ return _discovered_base or None+ _discovered_base = "" # default: tried and failed (so we don't re-probe every call)+ try:+ import subprocess as _sp, re as _re+ cnw = getattr(_sp, "CREATE_NO_WINDOW", 0)+ tl = _sp.run(["tasklist", "/FI", "IMAGENAME eq adom-desktop.exe", "/FO", "CSV", "/NH"],+ capture_output=True, text=True, timeout=8, creationflags=cnw).stdout+ pids = set(_re.findall(r'"adom-desktop\.exe","(\d+)"', tl))+ if not pids:+ return None+ ns = _sp.run(["netstat", "-ano", "-p", "TCP"], capture_output=True, text=True,+ timeout=8, creationflags=cnw).stdout+ seen = []+ for line in ns.splitlines():+ m = _re.search(r'\s(?:127\.0\.0\.1|\[::1\]):(\d+)\s+\S+\s+LISTENING\s+(\d+)', line)+ if m and m.group(2) in pids:+ p = int(m.group(1))+ if p not in seen:+ seen.append(p)+ for p in seen:+ base = f"http://127.0.0.1:{p}"+ if _looks_like_ad_api(_raw_get(base + "/commands")):+ _discovered_base = base+ return base+ except Exception:+ pass+ return None++ def _base_url():- """The loopback direct-API base AD injects into our spawn env, else None."""- return (os.environ.get("ADOM_DIRECT_API_URL") or "").rstrip("/") or None+ """The loopback direct-API base: the URL AD injects into our spawn env, else one we DISCOVER by+ probing adom-desktop's loopback ports (AD sometimes fails to inject the env var even though the+ API is up - see _discover_direct_api). None only if neither yields a reachable API."""+ env = (os.environ.get("ADOM_DIRECT_API_URL") or "").rstrip("/")+ if env:+ return env+ return _discover_direct_api() def _token():
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.6.63",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.63/adom-bridge-fusion-v1.6.63.zip",- "sha256": "8db07d4fb1be353eb055cc37681e14700b4c4da3e47cd2eecbb46c14cad91a17",- "size": 357907,+ "version": "1.6.64",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.64/adom-bridge-fusion-v1.6.64.zip",+ "sha256": "98761caabf5b6a40e68b9cfd11b61ab08ed741a22d148505b45c07f4a711c539",+ "size": 359076, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.6.63",+ "version": "1.6.64", "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.", "homepage": "https://wiki.adom.inc/adom/adom-desktop-fusion-bridge", "author": "Adom Inc.",
server.py+2−2@@ -185,8 +185,8 @@ def _resolve_seat_dialog(max_clicks: int = 8) -> dict: break saw = True parent = dlg.get("owner")- _resolve_seat_via_uia(dlg)- clicks += 1+ if _resolve_seat_via_uia(dlg): # count only clicks that actually landed, so+ clicks += 1 # seatDialogAutoResolved isn't a lie when ad_client is down _t.sleep(1.5) # Ground-truth confirm via parent/child screenshot (John's insisted-on check). cnt = _owned_popup_count(parent)