mirror of
https://github.com/ruvnet/RuView
synced 2026-06-09 10:13:17 +00:00
fix: live demo static pose & inaccurate sensing data (issue #86)
- Docker default changed from --source simulated to --source auto (auto-detects ESP32 on UDP 5005, falls back to simulation) - Pose derivation now driven by real sensing features: motion_band_power, breathing_band_power, variance, dominant_freq_hz, change_points - Temporal feature extraction: 100-frame circular buffer, Goertzel breathing rate estimation (0.1-0.5 Hz), frame-to-frame L2 motion detection, SNR-based signal quality metric - Signal field driven by subcarrier variance spatial mapping instead of fixed animation circle - UI data source indicators: LIVE/RECONNECTING/SIMULATED banner on sensing tab, estimation mode badge on live demo tab - Setup guide panel explaining ESP32 count requirements for each capability level (1x: presence, 3x: localization, 4x+: full pose) - Tick rate improved from 500ms to 100ms (2fps to 10fps) - Fixed Option<f64> division bug from PR #83 - ADR-035 documents all decisions Closes #86 Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -14,7 +14,9 @@ export class LiveDemoTab {
|
||||
currentZone: 'zone_1',
|
||||
debugMode: false,
|
||||
autoReconnect: true,
|
||||
renderMode: 'skeleton'
|
||||
renderMode: 'skeleton',
|
||||
// 'unknown' | 'signal_derived' | 'model_inference'
|
||||
poseSource: 'unknown'
|
||||
};
|
||||
|
||||
this.components = {
|
||||
@@ -136,6 +138,48 @@ export class LiveDemoTab {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pose-source-panel">
|
||||
<h4>Estimation Mode</h4>
|
||||
<div class="pose-source-indicator" id="pose-source-indicator">
|
||||
<span class="pose-source-badge pose-source-unknown" id="pose-source-badge">Unknown</span>
|
||||
<p class="pose-source-description" id="pose-source-description">
|
||||
Waiting for first frame...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setup-guide-panel">
|
||||
<h4>Setup Guide</h4>
|
||||
<div class="setup-levels">
|
||||
<div class="setup-level">
|
||||
<span class="setup-level-icon">1x</span>
|
||||
<div class="setup-level-info">
|
||||
<strong>1 ESP32 + 1 AP</strong>
|
||||
<p>Presence, breathing, gross motion</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setup-level">
|
||||
<span class="setup-level-icon">3x</span>
|
||||
<div class="setup-level-info">
|
||||
<strong>2-3 ESP32s</strong>
|
||||
<p>Body localization, motion direction</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setup-level">
|
||||
<span class="setup-level-icon">4x+</span>
|
||||
<div class="setup-level-info">
|
||||
<strong>4+ ESP32s + trained model</strong>
|
||||
<p>Individual limb tracking, full pose</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="setup-note">
|
||||
Signal-Derived mode uses aggregate CSI features.
|
||||
For per-limb tracking, load a trained <code>.rvf</code> model
|
||||
with <code>--model path.rvf</code> and use 4+ sensors.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="health-panel">
|
||||
<h4>System Health</h4>
|
||||
<div class="health-check">
|
||||
@@ -432,6 +476,133 @@ export class LiveDemoTab {
|
||||
.health-good { color: #28a745; }
|
||||
.health-poor { color: #ffc107; }
|
||||
.health-bad { color: #dc3545; }
|
||||
|
||||
/* Pose estimation mode indicator */
|
||||
.pose-source-panel {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.pose-source-panel h4 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pose-source-indicator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pose-source-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.pose-source-unknown {
|
||||
background: #f0f0f0;
|
||||
color: #6c757d;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.pose-source-signal {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
border: 1px solid #a5d6a7;
|
||||
}
|
||||
|
||||
.pose-source-model {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
border: 1px solid #90caf9;
|
||||
}
|
||||
|
||||
.pose-source-description {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.setup-guide-panel {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.setup-guide-panel h4 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.setup-levels {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.setup-level {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.setup-level-icon {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.setup-level-info strong {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.setup-level-info p {
|
||||
margin: 2px 0 0;
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.setup-note {
|
||||
margin: 10px 0 0;
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.setup-note code {
|
||||
background: #f0f0f0;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
if (!document.querySelector('#live-demo-enhanced-styles')) {
|
||||
@@ -545,7 +716,11 @@ export class LiveDemoTab {
|
||||
handlePoseUpdate(data) {
|
||||
this.metrics.frameCount++;
|
||||
this.metrics.lastUpdate = Date.now();
|
||||
this.updateDebugOutput(`Pose update: ${data.persons?.length || 0} persons detected`);
|
||||
// Update pose source indicator if the backend supplies it
|
||||
if (data.pose_source && data.pose_source !== this.state.poseSource) {
|
||||
this.setState({ poseSource: data.pose_source });
|
||||
}
|
||||
this.updateDebugOutput(`Pose update: ${data.persons?.length || 0} persons detected (${data.pose_source || 'unknown'})`);
|
||||
}
|
||||
|
||||
handleCanvasError(error) {
|
||||
@@ -706,6 +881,7 @@ export class LiveDemoTab {
|
||||
this.updateStatusIndicator();
|
||||
this.updateControls();
|
||||
this.updateMetricsDisplay();
|
||||
this.updatePoseSourceIndicator();
|
||||
}
|
||||
|
||||
updateStatusIndicator() {
|
||||
@@ -789,6 +965,33 @@ export class LiveDemoTab {
|
||||
}
|
||||
}
|
||||
|
||||
updatePoseSourceIndicator() {
|
||||
const badge = this.container.querySelector('#pose-source-badge');
|
||||
const description = this.container.querySelector('#pose-source-description');
|
||||
|
||||
if (!badge || !description) return;
|
||||
|
||||
const source = this.state.poseSource;
|
||||
|
||||
if (source === 'model_inference') {
|
||||
badge.className = 'pose-source-badge pose-source-model';
|
||||
badge.textContent = 'Model Inference';
|
||||
description.textContent =
|
||||
'Pose is estimated by a trained neural network ' +
|
||||
'loaded from an RVF container.';
|
||||
} else if (source === 'signal_derived') {
|
||||
badge.className = 'pose-source-badge pose-source-signal';
|
||||
badge.textContent = 'Signal-Derived';
|
||||
description.textContent =
|
||||
'Keypoints are derived from live CSI signal features ' +
|
||||
'(motion power, breathing rate, variance).';
|
||||
} else {
|
||||
badge.className = 'pose-source-badge pose-source-unknown';
|
||||
badge.textContent = 'Unknown';
|
||||
description.textContent = 'Waiting for first frame...';
|
||||
}
|
||||
}
|
||||
|
||||
getHealthClass(status) {
|
||||
switch (status) {
|
||||
case 'connected': return 'good';
|
||||
|
||||
+44
-11
@@ -33,6 +33,13 @@ export class SensingTab {
|
||||
_buildDOM() {
|
||||
this.container.innerHTML = `
|
||||
<h2>Live WiFi Sensing</h2>
|
||||
|
||||
<!-- Data-source status banner — updated by _onStateChange -->
|
||||
<div id="sensingSourceBanner" class="sensing-source-banner sensing-source-reconnecting"
|
||||
role="status" aria-live="polite">
|
||||
RECONNECTING...
|
||||
</div>
|
||||
|
||||
<div class="sensing-layout">
|
||||
<!-- 3D viewport -->
|
||||
<div class="sensing-viewport" id="sensingViewport">
|
||||
@@ -98,6 +105,17 @@ export class SensingTab {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Setup info -->
|
||||
<div class="sensing-card">
|
||||
<div class="sensing-card-title">About This Data</div>
|
||||
<p class="sensing-about-text">
|
||||
Metrics are computed from WiFi Channel State Information (CSI).
|
||||
With <strong>1 ESP32</strong> you get presence detection, breathing
|
||||
estimation, and gross motion. Add <strong>3-4+ ESP32 nodes</strong>
|
||||
around the room for spatial resolution and limb-level tracking.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Extra info -->
|
||||
<div class="sensing-card">
|
||||
<div class="sensing-card-title">Details</div>
|
||||
@@ -178,19 +196,34 @@ export class SensingTab {
|
||||
}
|
||||
|
||||
_onStateChange(state) {
|
||||
const dot = this.container.querySelector('#sensingDot');
|
||||
const text = this.container.querySelector('#sensingState');
|
||||
if (!dot || !text) return;
|
||||
const dot = this.container.querySelector('#sensingDot');
|
||||
const text = this.container.querySelector('#sensingState');
|
||||
const banner = this.container.querySelector('#sensingSourceBanner');
|
||||
|
||||
const labels = {
|
||||
disconnected: 'Disconnected',
|
||||
connecting: 'Connecting...',
|
||||
connected: 'Connected',
|
||||
simulated: 'Simulated',
|
||||
};
|
||||
if (dot && text) {
|
||||
const stateLabels = {
|
||||
disconnected: 'Disconnected',
|
||||
connecting: 'Connecting...',
|
||||
connected: 'Connected',
|
||||
reconnecting: 'Reconnecting...',
|
||||
simulated: 'Simulated',
|
||||
};
|
||||
dot.className = 'sensing-dot ' + state;
|
||||
text.textContent = stateLabels[state] || state;
|
||||
}
|
||||
|
||||
dot.className = 'sensing-dot ' + state;
|
||||
text.textContent = labels[state] || state;
|
||||
if (banner) {
|
||||
// Map the service's dataSource to banner text and CSS modifier class.
|
||||
const dataSource = sensingService.dataSource;
|
||||
const bannerConfig = {
|
||||
live: { text: 'LIVE - ESP32', cls: 'sensing-source-live' },
|
||||
reconnecting: { text: 'RECONNECTING...', cls: 'sensing-source-reconnecting' },
|
||||
simulated: { text: 'SIMULATED DATA', cls: 'sensing-source-simulated' },
|
||||
};
|
||||
const cfg = bannerConfig[dataSource] || bannerConfig.reconnecting;
|
||||
banner.textContent = cfg.text;
|
||||
banner.className = 'sensing-source-banner ' + cfg.cls;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- HUD update --------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user