From 13760f2328b00eb3b75063c5eef24668959d4812 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 30 Apr 2026 03:03:25 +0000 Subject: [PATCH] deploy(pointcloud): 9a078e4ac85253ea63bb6b3874390e230c928609 9a078e4ac85253ea63bb6b3874390e230c928609 --- pointcloud/index.html | 76 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 9 deletions(-) diff --git a/pointcloud/index.html b/pointcloud/index.html index 2aea08c1..865bcb17 100644 --- a/pointcloud/index.html +++ b/pointcloud/index.html @@ -477,15 +477,31 @@ // Once auto mode confirms there is no /api/splats backend on this origin, // set this flag so we stop hammering the network with 404 fetches every - // tick. Remote (?backend=) and live (?live=1) modes keep retrying so - // a transient outage doesn't permanently downgrade them. + // tick. Console stays clean; demo renders locally. var networkDisabled = false; + // Exponential backoff state for explicit ?backend=. The user's + // local server may be down (ERR_CONNECTION_REFUSED) and we shouldn't + // hammer it 10 Hz indefinitely. After each failure we lengthen the + // delay; on success we snap back to the normal cadence. + var BASE_INTERVAL_MS = 250; + var MAX_INTERVAL_MS = 30000; + var currentIntervalMs = BASE_INTERVAL_MS; + var consecutiveFailures = 0; + var fetchTimer = null; + var lastBackendError = null; + + function scheduleNextFetch(delayMs) { + if (fetchTimer) clearTimeout(fetchTimer); + fetchTimer = setTimeout(fetchCloud, delayMs); + } + async function fetchCloud() { - // Demo-only mode: never hit the network. + // Demo-only mode: never hit the network. Use the normal cadence. if (backendArg === "demo" || networkDisabled) { transportMode = "demo"; handleData(pickDemoFrame()); + scheduleNextFetch(BASE_INTERVAL_MS); return; } try { @@ -493,18 +509,44 @@ if (!resp.ok) throw new Error("HTTP " + resp.status); var data = await resp.json(); transportMode = (backendArg === "auto") ? "live" : "remote"; + consecutiveFailures = 0; + currentIntervalMs = BASE_INTERVAL_MS; + lastBackendError = null; handleData(data); + scheduleNextFetch(BASE_INTERVAL_MS); } catch (err) { + consecutiveFailures += 1; + lastBackendError = err && err.message ? err.message : String(err); if (requireLive) { document.getElementById("stats").innerHTML = - '● OFFLINE
Live backend required (?live=1) but unreachable.
' + (err && err.message ? err.message : err) + ''; + '● OFFLINE
Live backend required (?live=1) but unreachable.
' + lastBackendError + ''; + // Even strict-live: back off so we don't spam. + currentIntervalMs = Math.min(currentIntervalMs * 2, MAX_INTERVAL_MS); + scheduleNextFetch(currentIntervalMs); return; } - // Auto mode + first failure → assume this is a static host (Pages) - // and stop polling. Console stays clean; demo renders locally. - if (backendArg === "auto") networkDisabled = true; + // Auto mode + first failure → assume static host (Pages), disable + // network entirely so the console stays clean. + if (backendArg === "auto") { + networkDisabled = true; + transportMode = "demo"; + handleData(pickDemoFrame()); + scheduleNextFetch(BASE_INTERVAL_MS); + return; + } + // Explicit backend (?backend=) — keep trying with + // exponential backoff: 250 ms → 500 ms → 1 s → 2 s … up to 30 s. + // Render the demo while we wait so the scene stays alive, and + // surface the failure so the user knows the server is down. + currentIntervalMs = Math.min(Math.max(BASE_INTERVAL_MS * Math.pow(2, consecutiveFailures - 1), 1000), MAX_INTERVAL_MS); transportMode = "demo"; - handleData(pickDemoFrame()); + var demoFrame = pickDemoFrame(); + demoFrame._backendUnreachable = true; + demoFrame._backendUrl = backendArg; + demoFrame._backendError = lastBackendError; + demoFrame._retryInMs = currentIntervalMs; + handleData(demoFrame); + scheduleNextFetch(currentIntervalMs); } } @@ -564,6 +606,21 @@ + "Splats: " + splatCount + "
" + "Frame: " + data.frame; + // Unreachable backend banner — explicit ?backend= failed + // to connect. Show actionable guidance instead of leaving the + // user staring at a "demo" badge wondering why their ESP32 + // feed isn't visible. + if (data._backendUnreachable) { + var nextSec = Math.round((data._retryInMs || 1000) / 1000); + html += '
' + + '● ' + data._backendUrl + ' unreachable' + + '
' + (data._backendError || "connection failed") + '' + + '
retry in ' + nextSec + 's' + + '

start the server:' + + '
cargo run -p wifi-densepose-pointcloud --release \\
  -- serve --bind 127.0.0.1:9880
' + + '
'; + } + // CSI frame rate html += '
' + 'CSI Rate: ' @@ -676,8 +733,9 @@ }); })(); + // fetchCloud self-schedules via setTimeout — no setInterval to avoid + // overlapping calls on slow networks and to support exponential backoff. fetchCloud(); - setInterval(fetchCloud, 100); // 10 Hz — denser updates so face mesh feels live and the spiral animates smoothly function updateSplats(splats) { if (pointsMesh) scene.remove(pointsMesh);