mirror of
https://github.com/ruvnet/RuView
synced 2026-07-31 18:51:42 +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:
@@ -4,8 +4,9 @@
|
||||
* Manages the connection to the Python sensing WebSocket server
|
||||
* (ws://localhost:8765) and provides a callback-based API for the UI.
|
||||
*
|
||||
* Falls back to simulated data if the server is unreachable so the UI
|
||||
* always shows something.
|
||||
* Falls back to simulated data only after MAX_RECONNECT_ATTEMPTS exhausted.
|
||||
* While reconnecting the service stays in "reconnecting" state and does NOT
|
||||
* emit simulated frames so the UI can clearly distinguish live vs. fallback data.
|
||||
*/
|
||||
|
||||
// Derive WebSocket URL from the page origin so it works on any port
|
||||
@@ -14,7 +15,10 @@ const _wsProto = (typeof window !== 'undefined' && window.location.protocol ===
|
||||
const _wsHost = (typeof window !== 'undefined' && window.location.host) ? window.location.host : 'localhost:3000';
|
||||
const SENSING_WS_URL = `${_wsProto}//${_wsHost}/ws/sensing`;
|
||||
const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 16000];
|
||||
const MAX_RECONNECT_ATTEMPTS = 10;
|
||||
const MAX_RECONNECT_ATTEMPTS = 20;
|
||||
// Number of failed attempts that must occur before simulation starts.
|
||||
// This prevents the UI from flashing "SIMULATED" on a brief hiccup.
|
||||
const SIM_FALLBACK_AFTER_ATTEMPTS = 5;
|
||||
const SIMULATION_INTERVAL = 500; // ms
|
||||
|
||||
class SensingService {
|
||||
@@ -26,7 +30,10 @@ class SensingService {
|
||||
this._reconnectAttempt = 0;
|
||||
this._reconnectTimer = null;
|
||||
this._simTimer = null;
|
||||
this._state = 'disconnected'; // disconnected | connecting | connected | simulated
|
||||
// Connection state: disconnected | connecting | connected | reconnecting | simulated
|
||||
this._state = 'disconnected';
|
||||
// Data-source label exposed to the UI: "live" | "reconnecting" | "simulated"
|
||||
this._dataSource = 'reconnecting';
|
||||
this._lastMessage = null;
|
||||
|
||||
// Ring buffer of recent RSSI values for sparkline
|
||||
@@ -76,6 +83,16 @@ class SensingService {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current data source label.
|
||||
* "live" — frames are arriving from the real ESP32 over WebSocket
|
||||
* "reconnecting" — WebSocket disconnected; actively retrying, no frames emitted
|
||||
* "simulated" — max reconnect attempts exhausted; emitting synthetic frames
|
||||
*/
|
||||
get dataSource() {
|
||||
return this._dataSource;
|
||||
}
|
||||
|
||||
// ---- Connection --------------------------------------------------------
|
||||
|
||||
_connect() {
|
||||
@@ -96,6 +113,7 @@ class SensingService {
|
||||
this._reconnectAttempt = 0;
|
||||
this._stopSimulation();
|
||||
this._setState('connected');
|
||||
this._setDataSource('live');
|
||||
};
|
||||
|
||||
this._ws.onmessage = (evt) => {
|
||||
@@ -118,28 +136,33 @@ class SensingService {
|
||||
this._scheduleReconnect();
|
||||
} else {
|
||||
this._setState('disconnected');
|
||||
this._setDataSource('reconnecting');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_scheduleReconnect() {
|
||||
if (this._reconnectAttempt >= MAX_RECONNECT_ATTEMPTS) {
|
||||
console.warn('[Sensing] Max reconnect attempts reached, switching to simulation');
|
||||
console.warn('[Sensing] Max reconnect attempts (%d) reached, switching to simulation', MAX_RECONNECT_ATTEMPTS);
|
||||
this._fallbackToSimulation();
|
||||
return;
|
||||
}
|
||||
|
||||
const delay = RECONNECT_DELAYS[Math.min(this._reconnectAttempt, RECONNECT_DELAYS.length - 1)];
|
||||
this._reconnectAttempt++;
|
||||
console.info('[Sensing] Reconnecting in %dms (attempt %d)', delay, this._reconnectAttempt);
|
||||
console.info('[Sensing] Reconnecting in %dms (attempt %d/%d)', delay, this._reconnectAttempt, MAX_RECONNECT_ATTEMPTS);
|
||||
|
||||
this._setState('reconnecting');
|
||||
this._setDataSource('reconnecting');
|
||||
|
||||
this._reconnectTimer = setTimeout(() => {
|
||||
this._reconnectTimer = null;
|
||||
this._connect();
|
||||
}, delay);
|
||||
|
||||
// Start simulation while waiting
|
||||
if (this._state !== 'simulated') {
|
||||
// Only start simulation after several failed attempts so a brief hiccup
|
||||
// does not immediately switch the UI to "SIMULATED DATA".
|
||||
if (this._reconnectAttempt >= SIM_FALLBACK_AFTER_ATTEMPTS && this._state !== 'simulated') {
|
||||
this._fallbackToSimulation();
|
||||
}
|
||||
}
|
||||
@@ -148,6 +171,7 @@ class SensingService {
|
||||
|
||||
_fallbackToSimulation() {
|
||||
this._setState('simulated');
|
||||
this._setDataSource('simulated');
|
||||
if (this._simTimer) return; // already running
|
||||
console.info('[Sensing] Running in simulation mode');
|
||||
|
||||
@@ -196,6 +220,9 @@ class SensingService {
|
||||
type: 'sensing_update',
|
||||
timestamp: t,
|
||||
source: 'simulated',
|
||||
// Explicit machine-readable marker so the UI can always detect simulated
|
||||
// frames regardless of which code path produced them.
|
||||
_simulated: true,
|
||||
nodes: [{
|
||||
node_id: 1,
|
||||
rssi_dbm: baseRssi + Math.sin(t * 0.5) * 3,
|
||||
@@ -262,6 +289,21 @@ class SensingService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the dataSource label and notify state listeners so the UI can
|
||||
* react without needing a separate subscription.
|
||||
* @param {'live'|'reconnecting'|'simulated'} source
|
||||
*/
|
||||
_setDataSource(source) {
|
||||
if (source === this._dataSource) return;
|
||||
this._dataSource = source;
|
||||
// Re-use the same state-listener channel — listeners receive the
|
||||
// connection state but can read dataSource via service.dataSource.
|
||||
for (const cb of this._stateListeners) {
|
||||
try { cb(this._state); } catch (e) { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
_clearTimers() {
|
||||
this._stopSimulation();
|
||||
if (this._reconnectTimer) {
|
||||
|
||||
Reference in New Issue
Block a user