From 8f3edd9351f98ee47123f700e399f3c786aa16ce Mon Sep 17 00:00:00 2001 From: ruv Date: Thu, 12 Mar 2026 21:18:20 -0400 Subject: [PATCH] deploy: fix toFixed crash and Blob WebSocket handling in CSI simulator Co-Authored-By: claude-flow --- pose-fusion.html | 4 +- pose-fusion/js/csi-simulator.js | 96 ++++++++++++++++++++++++++++++ pose-fusion/js/main.js | 2 +- ui/pose-fusion.html | 4 +- ui/pose-fusion/js/csi-simulator.js | 96 ++++++++++++++++++++++++++++++ ui/pose-fusion/js/main.js | 2 +- 6 files changed, 198 insertions(+), 6 deletions(-) diff --git a/pose-fusion.html b/pose-fusion.html index 326da3ce..08ff952a 100644 --- a/pose-fusion.html +++ b/pose-fusion.html @@ -4,7 +4,7 @@ WiFi-DensePose — Dual-Modal Pose Estimation - + @@ -196,6 +196,6 @@ - + diff --git a/pose-fusion/js/csi-simulator.js b/pose-fusion/js/csi-simulator.js index 62540995..f234d593 100644 --- a/pose-fusion/js/csi-simulator.js +++ b/pose-fusion/js/csi-simulator.js @@ -9,6 +9,8 @@ */ export class CsiSimulator { + static VERSION = 'v4-drift'; // Cache-bust verification + constructor(opts = {}) { this.subcarriers = opts.subcarriers || 52; // 802.11n HT20 this.timeWindow = opts.timeWindow || 56; // frames in sliding window @@ -32,6 +34,10 @@ export class CsiSimulator { this._basePhase[i] = (i / this.subcarriers) * Math.PI * 2; } + // RSSI tracking + this.rssiDbm = -70; // default mid-range + this._rssiTarget = -70; + // Person influence (updated from video motion) this.personPresence = 0; this.personX = 0.5; @@ -73,6 +79,9 @@ export class CsiSimulator { * (simulating through-wall sensing capability). */ updatePersonState(presence, x, y, motion) { + // Don't override real CSI sensing with synthetic video-derived state + if (this.mode === 'live') return; + if (presence > 0.1) { // Person detected in video — update CSI state directly this.personPresence = presence; @@ -126,6 +135,13 @@ export class CsiSimulator { this.phaseBuffer.shift(); } + // RSSI: smooth toward target (demo mode generates synthetic RSSI) + if (this.mode === 'demo') { + // Simulate RSSI based on person presence and slow drift + this._rssiTarget = -55 - 25 * (1 - this.personPresence) + Math.sin(elapsed * 0.3) * 3; + } + this.rssiDbm += (this._rssiTarget - this.rssiDbm) * 0.1; + // SNR estimate let signalPower = 0, noisePower = 0; for (let i = 0; i < this.subcarriers; i++) { @@ -215,6 +231,11 @@ export class CsiSimulator { this._noiseState[i] = 0.95 * this._noiseState[i] + 0.05 * (rng() * 2 - 1) * 0.03; a += this._noiseState[i]; + // Ambient temporal drift (multipath fading even in empty room) + a += 0.06 * Math.sin(elapsed * 0.7 + i * 0.25) + + 0.04 * Math.sin(elapsed * 1.3 - i * 0.18) + + 0.03 * Math.cos(elapsed * 2.1 + i * 0.4); + // Person-induced CSI perturbation if (presence > 0.1) { // Subcarrier-dependent body reflection (Fresnel zone model) @@ -237,6 +258,23 @@ export class CsiSimulator { } _handleLiveFrame(data) { + // Handle JSON text frames from the sensing server + if (typeof data === 'string') { + try { + const msg = JSON.parse(data); + this._handleJsonFrame(msg); + } catch (_) { /* ignore malformed JSON */ } + return; + } + + // Handle Blob data (convert to ArrayBuffer and re-process) + if (data instanceof Blob) { + data.arrayBuffer().then(ab => this._handleLiveFrame(ab)).catch(() => {}); + return; + } + + // Handle binary ArrayBuffer frames (ADR-018 format) + if (!(data instanceof ArrayBuffer)) return; const view = new DataView(data); // Check ADR-018 magic: 0xC5110001 if (data.byteLength < 20) return; @@ -256,6 +294,64 @@ export class CsiSimulator { } } + _handleJsonFrame(msg) { + // Sensing server sends: { type: "sensing_update", nodes: [{ amplitude: [...], subcarrier_count }], classification, features } + this._liveAmplitude = new Float32Array(this.subcarriers); + this._livePhase = new Float32Array(this.subcarriers); + + // Extract amplitude from sensing_update node data + const node = (msg.nodes && msg.nodes[0]) || msg; + const ampArr = node.amplitude || msg.amplitude; + if (ampArr && Array.isArray(ampArr)) { + const n = Math.min(ampArr.length, this.subcarriers); + // Server sends raw amplitude (already magnitude), normalize to 0-1 + let maxAmp = 0; + for (let i = 0; i < n; i++) maxAmp = Math.max(maxAmp, Math.abs(ampArr[i])); + const scale = maxAmp > 0 ? 1.0 / maxAmp : 1.0; + for (let i = 0; i < n; i++) { + this._liveAmplitude[i] = Math.abs(ampArr[i]) * scale; + } + } + + // Phase from node (if available) + const phaseArr = node.phase || msg.phase; + if (phaseArr && Array.isArray(phaseArr)) { + const n = Math.min(phaseArr.length, this.subcarriers); + for (let i = 0; i < n; i++) this._livePhase[i] = phaseArr[i]; + } else if (ampArr) { + // Synthesize phase from amplitude variation (Hilbert-like estimate) + for (let i = 1; i < this.subcarriers; i++) { + this._livePhase[i] = this._livePhase[i - 1] + (this._liveAmplitude[i] - this._liveAmplitude[i - 1]) * Math.PI; + } + } + + // Handle raw I/Q pairs + const iq = node.iq || msg.iq; + if (iq && Array.isArray(iq)) { + const n = Math.min(iq.length / 2, this.subcarriers); + for (let i = 0; i < n; i++) { + const real = iq[i * 2], imag = iq[i * 2 + 1]; + this._liveAmplitude[i] = Math.sqrt(real * real + imag * imag) / 2048; + this._livePhase[i] = Math.atan2(imag, real); + } + } + + // Extract RSSI from node data + if (typeof node.rssi_dbm === 'number') { + this._rssiTarget = node.rssi_dbm; + } else if (msg.features && typeof msg.features.mean_rssi === 'number') { + this._rssiTarget = msg.features.mean_rssi; + } + + // Update presence from server classification + const cls = msg.classification; + if (cls) { + if (typeof cls.confidence === 'number') { + this.personPresence = cls.presence ? cls.confidence : 0; + } + } + } + _mulberry32(seed) { return function() { let t = (seed += 0x6D2B79F5); diff --git a/pose-fusion/js/main.js b/pose-fusion/js/main.js index 1001d636..88608930 100644 --- a/pose-fusion/js/main.js +++ b/pose-fusion/js/main.js @@ -361,7 +361,7 @@ function mainLoop(timestamp) { // One-time diagnostic if (!_diagDone) { _diagDone = true; - console.log(`[PoseFusion] frame 1 OK — mode=${mode}, csi.bufLen=${csiSimulator.amplitudeBuffer.length}, embPts=${embPoints.fused.length}, rssi=${csiSimulator.rssiDbm.toFixed(1)}`); + console.log(`[PoseFusion] frame 1 OK — mode=${mode}, csi.bufLen=${csiSimulator.amplitudeBuffer.length}, embPts=${embPoints?.fused?.length ?? 0}, rssi=${(csiSimulator.rssiDbm ?? -99).toFixed(1)}`); } } catch (err) { diff --git a/ui/pose-fusion.html b/ui/pose-fusion.html index 326da3ce..08ff952a 100644 --- a/ui/pose-fusion.html +++ b/ui/pose-fusion.html @@ -4,7 +4,7 @@ WiFi-DensePose — Dual-Modal Pose Estimation - + @@ -196,6 +196,6 @@ - + diff --git a/ui/pose-fusion/js/csi-simulator.js b/ui/pose-fusion/js/csi-simulator.js index 62540995..f234d593 100644 --- a/ui/pose-fusion/js/csi-simulator.js +++ b/ui/pose-fusion/js/csi-simulator.js @@ -9,6 +9,8 @@ */ export class CsiSimulator { + static VERSION = 'v4-drift'; // Cache-bust verification + constructor(opts = {}) { this.subcarriers = opts.subcarriers || 52; // 802.11n HT20 this.timeWindow = opts.timeWindow || 56; // frames in sliding window @@ -32,6 +34,10 @@ export class CsiSimulator { this._basePhase[i] = (i / this.subcarriers) * Math.PI * 2; } + // RSSI tracking + this.rssiDbm = -70; // default mid-range + this._rssiTarget = -70; + // Person influence (updated from video motion) this.personPresence = 0; this.personX = 0.5; @@ -73,6 +79,9 @@ export class CsiSimulator { * (simulating through-wall sensing capability). */ updatePersonState(presence, x, y, motion) { + // Don't override real CSI sensing with synthetic video-derived state + if (this.mode === 'live') return; + if (presence > 0.1) { // Person detected in video — update CSI state directly this.personPresence = presence; @@ -126,6 +135,13 @@ export class CsiSimulator { this.phaseBuffer.shift(); } + // RSSI: smooth toward target (demo mode generates synthetic RSSI) + if (this.mode === 'demo') { + // Simulate RSSI based on person presence and slow drift + this._rssiTarget = -55 - 25 * (1 - this.personPresence) + Math.sin(elapsed * 0.3) * 3; + } + this.rssiDbm += (this._rssiTarget - this.rssiDbm) * 0.1; + // SNR estimate let signalPower = 0, noisePower = 0; for (let i = 0; i < this.subcarriers; i++) { @@ -215,6 +231,11 @@ export class CsiSimulator { this._noiseState[i] = 0.95 * this._noiseState[i] + 0.05 * (rng() * 2 - 1) * 0.03; a += this._noiseState[i]; + // Ambient temporal drift (multipath fading even in empty room) + a += 0.06 * Math.sin(elapsed * 0.7 + i * 0.25) + + 0.04 * Math.sin(elapsed * 1.3 - i * 0.18) + + 0.03 * Math.cos(elapsed * 2.1 + i * 0.4); + // Person-induced CSI perturbation if (presence > 0.1) { // Subcarrier-dependent body reflection (Fresnel zone model) @@ -237,6 +258,23 @@ export class CsiSimulator { } _handleLiveFrame(data) { + // Handle JSON text frames from the sensing server + if (typeof data === 'string') { + try { + const msg = JSON.parse(data); + this._handleJsonFrame(msg); + } catch (_) { /* ignore malformed JSON */ } + return; + } + + // Handle Blob data (convert to ArrayBuffer and re-process) + if (data instanceof Blob) { + data.arrayBuffer().then(ab => this._handleLiveFrame(ab)).catch(() => {}); + return; + } + + // Handle binary ArrayBuffer frames (ADR-018 format) + if (!(data instanceof ArrayBuffer)) return; const view = new DataView(data); // Check ADR-018 magic: 0xC5110001 if (data.byteLength < 20) return; @@ -256,6 +294,64 @@ export class CsiSimulator { } } + _handleJsonFrame(msg) { + // Sensing server sends: { type: "sensing_update", nodes: [{ amplitude: [...], subcarrier_count }], classification, features } + this._liveAmplitude = new Float32Array(this.subcarriers); + this._livePhase = new Float32Array(this.subcarriers); + + // Extract amplitude from sensing_update node data + const node = (msg.nodes && msg.nodes[0]) || msg; + const ampArr = node.amplitude || msg.amplitude; + if (ampArr && Array.isArray(ampArr)) { + const n = Math.min(ampArr.length, this.subcarriers); + // Server sends raw amplitude (already magnitude), normalize to 0-1 + let maxAmp = 0; + for (let i = 0; i < n; i++) maxAmp = Math.max(maxAmp, Math.abs(ampArr[i])); + const scale = maxAmp > 0 ? 1.0 / maxAmp : 1.0; + for (let i = 0; i < n; i++) { + this._liveAmplitude[i] = Math.abs(ampArr[i]) * scale; + } + } + + // Phase from node (if available) + const phaseArr = node.phase || msg.phase; + if (phaseArr && Array.isArray(phaseArr)) { + const n = Math.min(phaseArr.length, this.subcarriers); + for (let i = 0; i < n; i++) this._livePhase[i] = phaseArr[i]; + } else if (ampArr) { + // Synthesize phase from amplitude variation (Hilbert-like estimate) + for (let i = 1; i < this.subcarriers; i++) { + this._livePhase[i] = this._livePhase[i - 1] + (this._liveAmplitude[i] - this._liveAmplitude[i - 1]) * Math.PI; + } + } + + // Handle raw I/Q pairs + const iq = node.iq || msg.iq; + if (iq && Array.isArray(iq)) { + const n = Math.min(iq.length / 2, this.subcarriers); + for (let i = 0; i < n; i++) { + const real = iq[i * 2], imag = iq[i * 2 + 1]; + this._liveAmplitude[i] = Math.sqrt(real * real + imag * imag) / 2048; + this._livePhase[i] = Math.atan2(imag, real); + } + } + + // Extract RSSI from node data + if (typeof node.rssi_dbm === 'number') { + this._rssiTarget = node.rssi_dbm; + } else if (msg.features && typeof msg.features.mean_rssi === 'number') { + this._rssiTarget = msg.features.mean_rssi; + } + + // Update presence from server classification + const cls = msg.classification; + if (cls) { + if (typeof cls.confidence === 'number') { + this.personPresence = cls.presence ? cls.confidence : 0; + } + } + } + _mulberry32(seed) { return function() { let t = (seed += 0x6D2B79F5); diff --git a/ui/pose-fusion/js/main.js b/ui/pose-fusion/js/main.js index 1001d636..88608930 100644 --- a/ui/pose-fusion/js/main.js +++ b/ui/pose-fusion/js/main.js @@ -361,7 +361,7 @@ function mainLoop(timestamp) { // One-time diagnostic if (!_diagDone) { _diagDone = true; - console.log(`[PoseFusion] frame 1 OK — mode=${mode}, csi.bufLen=${csiSimulator.amplitudeBuffer.length}, embPts=${embPoints.fused.length}, rssi=${csiSimulator.rssiDbm.toFixed(1)}`); + console.log(`[PoseFusion] frame 1 OK — mode=${mode}, csi.bufLen=${csiSimulator.amplitudeBuffer.length}, embPts=${embPoints?.fused?.length ?? 0}, rssi=${(csiSimulator.rssiDbm ?? -99).toFixed(1)}`); } } catch (err) {