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
Update 7 file(s)
7 files changed
+118−28
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.134"+version = "1.9.138" edition = "2021" [dependencies]
dashboard/src/main.rs+98−16@@ -125,7 +125,17 @@ fn gather_status_ex(ai_thread: &str, target: &str, launching: bool) -> serde_jso // and no running flag - that is UNKNOWN, not "everything off". let answered = rd.get("bridgeVersion").map(|v| !v.is_null()).unwrap_or(false) || rd.get("running").is_some() || rd.get("installed").is_some();+ // TOP-LEVEL ROLLUP (dock_manifest 1.2.0, issue #37 checklist item 2): our own verdict+ // of which LEDs deserve to change the card, so the dock never falls back to its blunt+ // worst-facet rule. Fusion being CLOSED is a normal state for a control panel - the+ // card stays green. Only two things are worth a yellow: the bridge not answering+ // (transient - busy or relay hiccup), and Fusion up with the add-in neither serving+ // nor busy (self-healable, so warn, not fail). update/aps/mcp never move the card.+ let rollup = if !answered { "warn" }+ else if running && !ready && last_ready.is_empty() { "warn" }+ else { "ok" }; serde_json::json!({+ "state": rollup, "target": target, "answered": answered, "busyReason": if answered { serde_json::Value::Null }@@ -998,6 +1008,13 @@ fn handle_conn(app: &Arc<App>, mut stream: TcpStream) { respond_json(&mut stream, &serde_json::json!({"targets": rows, "_hint": "One Autodesk seat: start Fusion on whichever box suits the moment - the seat follows automatically (the other box's Fusion suspends resumably). Pass ?target=<name> on /api/status and target in /api/action bodies."})); }+ ("GET", "/api/ping") => {+ // INSTANT identity probe - /api/status does a live bridge round trip (seconds),+ // which made the self-background launch probe time out and the dock step hang+ // (issue #37). This answers from memory, always, in microseconds.+ respond_json(&mut stream, &serde_json::json!({+ "app": "fusion-dashboard", "dashboardVersion": VERSION}));+ } ("GET", "/api/status") | ("GET", "/state") => { let t = qs_target.clone().unwrap_or_else(|| app.tgt()); app.log(format!("status target={}", t));@@ -1141,7 +1158,88 @@ fn handle_conn(app: &Arc<App>, mut stream: TcpStream) { } } +/// The URL a dock webview can ACTUALLY REACH for a given port (issue #37: a webview+/// renders OUTSIDE this container, so loopback is only right for a genuinely local run).+fn url_for(port: u16) -> String {+ std::env::var("VSCODE_PROXY_URI")+ .ok()+ .filter(|u| u.contains("{{port}}"))+ .map(|u| u.replace("{{port}}", &port.to_string()))+ .unwrap_or_else(|| format!("http://127.0.0.1:{}/", port))+}++/// Probe 8930-8940 for an already-serving FUSION dashboard. "dashboardVersion" alone is+/// not enough: the KiCad dashboard also serves it (found live on 8931 - the first probe+/// draft would have handed the Fusion dock card KiCad's URL). Require a facet only OUR+/// payload carries.+fn find_running_instance() -> Option<u16> {+ for p in 8930..=8940u16 {+ let out = Command::new("curl")+ .args(["-s", "-m", "1", &format!("http://127.0.0.1:{}/api/ping", p)])+ .output();+ if let Ok(o) = out {+ if String::from_utf8_lossy(&o.stdout).contains("\"app\":\"fusion-dashboard\"") {+ return Some(p);+ }+ }+ }+ None+}+ fn serve(port: u16, target: String, ai_thread: String, print_url: bool) {+ if print_url {+ // THE DOCK LAUNCH STEP (issue #37, the blocker verified on v1.9.126 and v1.9.133):+ // Hydrogen runs this step SYNCHRONOUSLY, so a serve that stays in the foreground+ // blocks the exec for the whole step budget, gets its process TREE killed on+ // timeout, and the URL we printed is dead before Hydrogen polls it. The contract+ // is: print the URL, put the server in the BACKGROUND, exit 0 within seconds.+ //+ // Warm path first (SDK rule: "detect a healthy instance already on the port,+ // print ITS url, exit 0") - a relaunch then costs nothing and never kills the+ // running instance.+ if let Some(p) = find_running_instance() {+ println!("{}", url_for(p));+ ok(&format!("fusion-dashboard already serving on port {} - reusing it", p));+ return;+ }+ // Cold path: re-exec ourselves detached (log to ~/.adom/fusion-bridge/), wait+ // until the child answers, print its URL, and let THIS process exit 0.+ let exe = std::env::current_exe().unwrap_or_else(|_| "fusion-dashboard".into());+ let log_dir = std::env::var("HOME").map(|h| format!("{}/.adom/fusion-bridge", h))+ .unwrap_or_else(|_| "/tmp".into());+ let _ = std::fs::create_dir_all(&log_dir);+ let log = std::fs::OpenOptions::new().create(true).append(true)+ .open(format!("{}/dashboard.log", log_dir));+ let mut cmd = Command::new(exe);+ cmd.arg("serve").args(["--port", &port.to_string()]);+ if !target.is_empty() { cmd.args(["--target", &target]); }+ cmd.args(["--ai-thread", &ai_thread]);+ cmd.stdin(std::process::Stdio::null());+ match log {+ Ok(f) => {+ let f2 = f.try_clone().ok();+ cmd.stdout(f);+ if let Some(f2) = f2 { cmd.stderr(f2); } else { cmd.stderr(std::process::Stdio::null()); }+ }+ Err(_) => { cmd.stdout(std::process::Stdio::null()); cmd.stderr(std::process::Stdio::null()); }+ }+ if cmd.spawn().is_err() {+ err_hint("could not spawn the background server",+ "Run `fusion-dashboard serve` by hand and read its output.");+ std::process::exit(1);+ }+ for _ in 0..40 {+ std::thread::sleep(Duration::from_millis(250));+ if let Some(p) = find_running_instance() {+ println!("{}", url_for(p));+ ok(&format!("fusion-dashboard serving on port {} (backgrounded; log: {}/dashboard.log)", p, log_dir));+ return;+ }+ }+ err_hint("background server did not answer within 10s",+ &format!("Read {}/dashboard.log for its output.", log_dir));+ std::process::exit(1);+ } let mut port = port; let listener = loop { match TcpListener::bind(("0.0.0.0", port)) {@@ -1171,22 +1269,6 @@ fn serve(port: u16, target: String, ai_thread: String, print_url: bool) { launching: Mutex::new(std::collections::HashMap::new()), last_launch_note: Mutex::new(std::collections::HashMap::new()), });- if print_url {- // Hydrogen dock await:url contract - the URL a webview can ACTUALLY REACH.- //- // The comment here used to say "the proxied URL" while printing loopback (page- // issue #37). A dock webview renders OUTSIDE this container, so 127.0.0.1 is not- // its loopback - it resolves to the viewer's own machine and the dock navigates to- // nothing. code-server publishes the mapping we need as VSCODE_PROXY_URI, e.g.- // https://<workspace>.adom.cloud/proxy/{{port}}/ - substitute the port and print- // that. Loopback stays as the fallback for a genuinely local run, where it is right.- let url = std::env::var("VSCODE_PROXY_URI")- .ok()- .filter(|u| u.contains("{{port}}"))- .map(|u| u.replace("{{port}}", &port.to_string()))- .unwrap_or_else(|| format!("http://127.0.0.1:{}/", port));- println!("{}", url);- } ok(&format!("fusion-dashboard v{} serving on port {} (target {})", VERSION, port, if app.target.is_empty() { "auto" } else { &app.target }));
dock.json+7−2@@ -1,8 +1,8 @@ {- "dock_manifest": "1.1.0",+ "dock_manifest": "1.2.0", "webview": { "title": "Fusion Dashboard",- "favicon": "docs/img/dock-fusion.svg"+ "favicon": "docs/img/dock-fusion.png" }, "launch": { "mode": "container_command",@@ -29,5 +29,10 @@ "submit": "staged", "typed": false }+ },+ "health": {+ "url": "/api/status",+ "poll_seconds": 15,+ "timeout_ms": 2000 } }
docs/img/dock-fusion.pngadded⋯ 1 unchanged line ⋯
install.sh+1−1@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.9.134"+DASH_VERSION="1.9.138" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.137",+ "version": "1.9.138", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {
page.json+10−7@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.137",+ "version": "1.9.138", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {@@ -11,6 +11,10 @@ "uninstall": "./uninstall.sh" }, "tags": [+ "ab",+ "bridge",+ "fusion",+ "desktop-app", "fusion360", "autodesk", "cad",@@ -70,7 +74,10 @@ "build a basic parts library", "add a 3d model to a component", "make a fusion 360 library with 3d",- "set up the fusion bridge"+ "set up the fusion bridge",+ "fusion dashboard",+ "fusion status page",+ "open the fusion launch page" ], "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).", "brief": "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.",@@ -99,9 +106,5 @@ "label": "Part volume", "prompt": "How much material does each printed part in this assembly use?" }- ],- "author": {- "name": "John Lauer",- "email": "[email protected]"- }+ ] }