Files
wifi-densepose/mobile/src/services/simulation.service.ts
Yossi Elkrief 779bf8ff43 feat: Phase 3 — services, stores, navigation, design system
Services: ws.service, api.service, simulation.service, rssi.service (android+ios)
Stores: poseStore, settingsStore, matStore (Zustand)
Types: sensing, mat, api, navigation
Hooks: usePoseStream, useRssiScanner, useServerReachability
Theme: colors, typography, spacing, ThemeContext
Navigation: MainTabs (5 tabs), RootNavigator, types
Components: GaugeArc, SparklineChart, OccupancyGrid, StatusDot, ConnectionBanner, SignalBar, +more
Utils: ringBuffer, colorMap, formatters, urlValidator

Verified: tsc 0 errors, jest passes
2026-03-02 12:53:45 +02:00

108 lines
3.5 KiB
TypeScript

import {
BREATHING_BAND_AMPLITUDE,
BREATHING_BAND_MIN,
BREATHING_BPM_MAX,
BREATHING_BPM_MIN,
HEART_BPM_MAX,
HEART_BPM_MIN,
MOTION_BAND_AMPLITUDE,
MOTION_BAND_MIN,
RSSI_AMPLITUDE_DBM,
RSSI_BASE_DBM,
SIMULATION_GRID_SIZE,
SIMULATION_TICK_INTERVAL_MS,
SIGNAL_FIELD_PRESENCE_LEVEL,
VARIANCE_AMPLITUDE,
VARIANCE_BASE,
} from '@/constants/simulation';
import type { SensingFrame } from '@/types/sensing';
function gaussian(x: number, y: number, cx: number, cy: number, sigma: number): number {
const dx = x - cx;
const dy = y - cy;
return Math.exp(-(dx * dx + dy * dy) / (2 * sigma * sigma));
}
function clamp(v: number, min: number, max: number): number {
return Math.max(min, Math.min(max, v));
}
export function generateSimulatedData(timeMs = Date.now()): SensingFrame {
const t = timeMs / 1000;
const baseRssi = RSSI_BASE_DBM + Math.sin(t * 0.5) * RSSI_AMPLITUDE_DBM;
const variance = VARIANCE_BASE + Math.sin(t * 0.1) * VARIANCE_AMPLITUDE;
const motionBand = MOTION_BAND_MIN + Math.abs(Math.sin(t * 0.3)) * MOTION_BAND_AMPLITUDE;
const breathingBand = BREATHING_BAND_MIN + Math.abs(Math.sin(t * 0.05)) * BREATHING_BAND_AMPLITUDE;
const isPresent = variance > SIGNAL_FIELD_PRESENCE_LEVEL;
const isActive = motionBand > 0.12;
const grid = SIMULATION_GRID_SIZE;
const cx = grid / 2;
const cy = grid / 2;
const bodyX = cx + 3 * Math.sin(t * 0.2);
const bodyY = cy + 2 * Math.cos(t * 0.15);
const breathX = cx + 4 * Math.sin(t * 0.04);
const breathY = cy + 4 * Math.cos(t * 0.04);
const values: number[] = [];
for (let z = 0; z < grid; z += 1) {
for (let x = 0; x < grid; x += 1) {
let value = Math.max(0, 1 - Math.sqrt((x - cx) ** 2 + (z - cy) ** 2) / (grid * 0.7)) * 0.3;
value += gaussian(x, z, bodyX, bodyY, 3.4) * (0.3 + motionBand * 3);
value += gaussian(x, z, breathX, breathY, 6) * (0.15 + breathingBand * 2);
if (!isPresent) {
value *= 0.7;
}
values.push(clamp(value, 0, 1));
}
}
const dominantFreqHz = 0.3 + Math.sin(t * 0.02) * 0.1;
const breathingBpm = BREATHING_BPM_MIN + ((Math.sin(t * 0.07) + 1) * 0.5) * (BREATHING_BPM_MAX - BREATHING_BPM_MIN);
const hrProxy = HEART_BPM_MIN + ((Math.sin(t * 0.09) + 1) * 0.5) * (HEART_BPM_MAX - HEART_BPM_MIN);
const confidence = 0.6 + Math.abs(Math.sin(t * 0.03)) * 0.4;
return {
type: 'sensing_update',
timestamp: timeMs,
source: 'simulated',
tick: Math.floor(t / (SIMULATION_TICK_INTERVAL_MS / 1000)),
nodes: [
{
node_id: 1,
rssi_dbm: baseRssi,
position: [2, 0, 1.5],
amplitude: [baseRssi],
subcarrier_count: 1,
},
],
features: {
mean_rssi: baseRssi,
variance,
motion_band_power: motionBand,
breathing_band_power: breathingBand,
spectral_entropy: 1 - clamp(Math.abs(dominantFreqHz - 0.3), 0, 1),
std: Math.sqrt(Math.abs(variance)),
dominant_freq_hz: dominantFreqHz,
change_points: Math.max(0, Math.floor(variance * 2)),
spectral_power: motionBand + breathingBand,
},
classification: {
motion_level: isActive ? 'active' : isPresent ? 'present_still' : 'absent',
presence: isPresent,
confidence: isPresent ? 0.75 + Math.abs(Math.sin(t * 0.03)) * 0.2 : 0.5 + Math.abs(Math.cos(t * 0.03)) * 0.3,
},
signal_field: {
grid_size: [grid, 1, grid],
values,
},
vital_signs: {
breathing_bpm: breathingBpm,
hr_proxy_bpm: hrProxy,
confidence,
},
};
}