diff --git a/web/app.js b/web/app.js index 5be4743..a2ebb7b 100644 --- a/web/app.js +++ b/web/app.js @@ -99,11 +99,8 @@ async function discoverServices() { // Initial read so values show immediately parseTelemetry(await chars.telemetry.readValue()); - // IMU stream notify (~100 Hz) + // IMU stream — subscribed on demand via play button chars.imuStream.addEventListener('characteristicvaluechanged', e => parseImuStream(e.target.value)); - await chars.imuStream.startNotifications(); - document.getElementById('vizLive').classList.add('on'); - log('IMU stream subscribed','ok'); log('Config service ready (4 chars)','ok'); } catch(e) { @@ -396,7 +393,7 @@ function onDisconnected() { document.getElementById('battBar').style.display='none'; document.getElementById('badgeCharging').classList.remove('show'); document.getElementById('badgeFull').classList.remove('show'); - document.getElementById('vizLive').classList.remove('on'); + imuSubscribed = false; vizPaused = true; vizUpdateIndicator(); clearTelemetry(); if (!userDisconnected && document.getElementById('autoReconnect').checked && savedDevice) { log('Auto-reconnecting…','info'); @@ -426,8 +423,43 @@ const canvas = document.getElementById('vizCanvas'); const ctx = canvas.getContext('2d'); const TRAIL_LEN = 120; let cursorX = canvas.width/2, cursorY = canvas.height/2, trail = []; +let vizPaused = true; +let imuSubscribed = false; + +function vizUpdateIndicator() { + const el = document.getElementById('vizLive'); + if (!imuSubscribed || vizPaused) { + el.classList.remove('on'); + el.classList.add('paused'); + el.textContent = '⏸ PAUSED'; + } else { + el.classList.add('on'); + el.classList.remove('paused'); + el.textContent = '● LIVE'; + } + document.getElementById('vizPauseBtn').style.display = (!vizPaused && imuSubscribed) ? '' : 'none'; + document.getElementById('vizPlayBtn').style.display = (vizPaused || !imuSubscribed) ? '' : 'none'; +} + +async function vizSetPaused(paused) { + vizPaused = paused; + if (!paused && chars.imuStream && !imuSubscribed) { + try { + await chars.imuStream.startNotifications(); + imuSubscribed = true; + log('IMU stream subscribed','ok'); + } catch(e) { log(`IMU stream start failed: ${e.message}`,'err'); vizPaused = true; } + } else if (paused && imuSubscribed) { + try { + await chars.imuStream.stopNotifications(); + imuSubscribed = false; + } catch(e) { log(`IMU stream stop failed: ${e.message}`,'err'); } + } + vizUpdateIndicator(); +} function parseImuStream(dv) { + if (vizPaused) return; let view; try { view = dv instanceof DataView ? new DataView(dv.buffer, dv.byteOffset, dv.byteLength) : new DataView(dv); diff --git a/web/index.html b/web/index.html index 336e9e6..b6d7d7e 100644 --- a/web/index.html +++ b/web/index.html @@ -158,7 +158,11 @@
IMU Stream
-
● LIVE
+
+
⏸ PAUSED
+ + +
diff --git a/web/style.css b/web/style.css index c9876b9..b091d1a 100644 --- a/web/style.css +++ b/web/style.css @@ -222,8 +222,11 @@ .viz-panel { background:var(--panel2); border:1px solid var(--border); padding:16px; } .viz-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; } .viz-title { font-family:var(--sans); font-size:11px; font-weight:600; letter-spacing:0.25em; text-transform:uppercase; color:var(--label); } - .viz-live { font-size:9px; letter-spacing:0.2em; color:var(--accent2); display:none; } - .viz-live.on { display:block; animation:pulse 1.5s infinite; } + .viz-ctrl-btn { background:none; border:1px solid var(--border); color:var(--label); font-size:11px; line-height:1; padding:3px 8px; cursor:pointer; letter-spacing:0.05em; } + .viz-ctrl-btn:hover { border-color:var(--accent); color:var(--accent); } + .viz-live { font-size:9px; letter-spacing:0.2em; display:block; } + .viz-live.on { color:var(--accent2); animation:pulse 1.5s infinite; } + .viz-live.paused { color:var(--label); } #vizCanvas { display:block; width:100%; background:var(--panel2); border:1px solid var(--border); cursor:crosshair; image-rendering:pixelated; } .viz-axes { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:10px; } .axis-bar-wrap { display:flex; flex-direction:column; gap:3px; }