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
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user