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:
ruv
2026-03-02 10:54:07 -05:00
parent fdc7142dfa
commit 8166d8d822
11 changed files with 1647 additions and 336 deletions
+44 -11
View File
@@ -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 --------------------------------------------------------