+
+
+
+
Signal Strength (RSSI)
+
+
SNR: -- dB | Quality: --
+
+
+
+
+
+
+
Vital Signs
+
+ Breathing
+ -- BPM
+
+
+
+ Heart Rate
+ -- BPM
+
+
+
+
+
+
+
+
Subcarrier Amplitude Spectrum (56 channels)
+
+
+
+
+
Signal Features
+
Variance0
+
+
Motion Band0
+
+
Breathing Band0
+
+
Spectral Power0
+
+
Dominant Freq0 Hz
+
Change Points0
+
+
+
+
+
Subcarrier Waterfall (time × frequency)
+
+
+
+
+
Motion Timeline
+
+
+
+
+
+
Signal Field Heatmap (20×20 spatial grid)
+
+
+
+
+
+
+
+
diff --git a/ui/services/data-processor.js b/ui/services/data-processor.js
index aabf58ad..6e4dba1e 100644
--- a/ui/services/data-processor.js
+++ b/ui/services/data-processor.js
@@ -43,21 +43,22 @@ export class DataProcessor {
result.zoneOccupancy = this._extractZoneOccupancy(payload, message.zone_id);
result.signalData = this._extractSignalData(payload);
- result.metadata.isRealData = payload.metadata?.mock_data === false;
+ const meta = payload.metadata || {};
+ const source = meta.source || '';
+ result.metadata.isRealData = source !== 'mock' && source !== 'demo' && source !== '';
result.metadata.timestamp = message.timestamp;
- result.metadata.processingTime = payload.metadata?.processing_time_ms || 0;
- result.metadata.frameId = payload.metadata?.frame_id;
-
- // Determine sensing mode
- if (payload.metadata?.source === 'csi') {
- result.metadata.sensingMode = 'CSI';
- } else if (payload.metadata?.source === 'rssi') {
- result.metadata.sensingMode = 'RSSI';
- } else if (payload.metadata?.mock_data !== false) {
- result.metadata.sensingMode = 'Mock';
- } else {
- result.metadata.sensingMode = 'CSI';
- }
+ result.metadata.processingTime = meta.processing_time_ms || 0;
+ result.metadata.frameId = meta.frame_id;
+ result.metadata.poseSource = payload.pose_source || 'unknown';
+ result.metadata.signalStrength = meta.signal_strength;
+ result.metadata.motionBandPower = meta.motion_band_power;
+
+ // Map server source to UI sensing mode label
+ const sourceMap = {
+ 'esp32': 'CSI', 'csi': 'CSI', 'wifi': 'WiFi',
+ 'rssi': 'RSSI', 'simulated': 'Simulated', 'simulate': 'Simulated',
+ };
+ result.metadata.sensingMode = sourceMap[source] || (source || 'Unknown');
}
}
@@ -99,8 +100,7 @@ export class DataProcessor {
_normalizeKeypoints(keypoints) {
if (!keypoints || keypoints.length === 0) return [];
- return keypoints.map(kp => {
- // Handle various formats
+ const raw = keypoints.map(kp => {
if (Array.isArray(kp)) {
return { x: kp[0], y: kp[1], confidence: kp[2] || 0.5 };
}
@@ -110,6 +110,22 @@ export class DataProcessor {
confidence: kp.confidence !== undefined ? kp.confidence : (kp.score || 0.5)
};
});
+
+ // Auto-detect if values are in pixel coords (>1.0) and normalize to [0,1]
+ const maxX = Math.max(...raw.map(k => Math.abs(k.x)));
+ const maxY = Math.max(...raw.map(k => Math.abs(k.y)));
+
+ if (maxX > 1.5 || maxY > 1.5) {
+ const frameW = Math.max(maxX * 1.1, 640);
+ const frameH = Math.max(maxY * 1.1, 480);
+ return raw.map(kp => ({
+ x: Math.max(0, Math.min(1, kp.x / frameW)),
+ y: Math.max(0, Math.min(1, kp.y / frameH)),
+ confidence: kp.confidence
+ }));
+ }
+
+ return raw;
}
// Extract zone occupancy data
diff --git a/ui/services/websocket-client.js b/ui/services/websocket-client.js
index 93428b87..de7af724 100644
--- a/ui/services/websocket-client.js
+++ b/ui/services/websocket-client.js
@@ -3,7 +3,9 @@
export class WebSocketClient {
constructor(options = {}) {
- this.url = options.url || 'ws://localhost:8000/ws/pose';
+ const defaultProto = (typeof location !== 'undefined' && location.protocol === 'https:') ? 'wss:' : 'ws:';
+ const defaultHost = (typeof location !== 'undefined') ? location.host : 'localhost:8080';
+ this.url = options.url || `${defaultProto}//${defaultHost}/api/v1/stream/pose`;
this.ws = null;
this.state = 'disconnected'; // disconnected, connecting, connected, error
this.isRealData = false;
@@ -30,6 +32,9 @@ export class WebSocketClient {
bytesReceived: 0
};
+ // Data source tracking
+ this.dataSource = null; // actual source string from server (e.g. "simulated", "esp32", "wifi")
+
// Callbacks
this._onMessage = options.onMessage || (() => {});
this._onStateChange = options.onStateChange || (() => {});
@@ -87,6 +92,7 @@ export class WebSocketClient {
this._setState('disconnected');
this.isRealData = false;
+ this.dataSource = null;
console.log('[WS-VIZ] Disconnected');
}
@@ -141,11 +147,14 @@ export class WebSocketClient {
return;
}
- // Detect real vs mock data from metadata
- if (data.data && data.data.metadata) {
- this.isRealData = data.data.metadata.mock_data === false && data.data.metadata.source !== 'mock';
- } else if (data.metadata) {
- this.isRealData = data.metadata.mock_data === false;
+ // Detect data source from server metadata.
+ // The server sends: { type: "pose_data", payload: { metadata: { source: "..." } } }
+ // or sensing_update with top-level source field.
+ const meta = data?.payload?.metadata || data?.data?.metadata || data?.metadata;
+ const source = meta?.source || data?.source;
+ if (source) {
+ this.dataSource = source;
+ this.isRealData = source !== 'mock' && source !== 'demo';
}
// Calculate latency from message timestamp
@@ -240,6 +249,7 @@ export class WebSocketClient {
...this.metrics,
state: this.state,
isRealData: this.isRealData,
+ dataSource: this.dataSource,
reconnectAttempts: this.reconnectAttempts,
uptime: this.metrics.connectTime ? (Date.now() - this.metrics.connectTime) / 1000 : 0
};
diff --git a/ui/viz.html b/ui/viz.html
index 54fa0a5f..053120f8 100644
--- a/ui/viz.html
+++ b/ui/viz.html
@@ -84,15 +84,14 @@