Files
wifi-densepose/mobile/src/services/ws.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

165 lines
4.4 KiB
TypeScript

import { SIMULATION_TICK_INTERVAL_MS } from '@/constants/simulation';
import { MAX_RECONNECT_ATTEMPTS, RECONNECT_DELAYS, WS_PATH } from '@/constants/websocket';
import { usePoseStore } from '@/stores/poseStore';
import { generateSimulatedData } from '@/services/simulation.service';
import type { ConnectionStatus, SensingFrame } from '@/types/sensing';
type FrameListener = (frame: SensingFrame) => void;
class WsService {
private ws: WebSocket | null = null;
private listeners = new Set<FrameListener>();
private reconnectAttempt = 0;
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
private simulationTimer: ReturnType<typeof setInterval> | null = null;
private targetUrl = '';
private active = false;
private status: ConnectionStatus = 'disconnected';
connect(url: string): void {
this.targetUrl = url;
this.active = true;
this.reconnectAttempt = 0;
if (!url) {
this.handleStatusChange('simulated');
this.startSimulation();
return;
}
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
return;
}
this.handleStatusChange('connecting');
try {
const endpoint = this.buildWsUrl(url);
const socket = new WebSocket(endpoint);
this.ws = socket;
socket.onopen = () => {
this.reconnectAttempt = 0;
this.stopSimulation();
this.handleStatusChange('connected');
};
socket.onmessage = (evt) => {
try {
const raw = typeof evt.data === 'string' ? evt.data : JSON.stringify(evt.data);
const frame = JSON.parse(raw) as SensingFrame;
this.listeners.forEach((listener) => listener(frame));
} catch {
// ignore malformed frames
}
};
socket.onerror = () => {
// handled by onclose
};
socket.onclose = (evt) => {
this.ws = null;
if (!this.active) {
this.handleStatusChange('disconnected');
return;
}
if (evt.code === 1000) {
this.handleStatusChange('disconnected');
return;
}
this.scheduleReconnect();
};
} catch {
this.scheduleReconnect();
}
}
disconnect(): void {
this.active = false;
this.clearReconnectTimer();
this.stopSimulation();
if (this.ws) {
this.ws.close(1000, 'client disconnect');
this.ws = null;
}
this.handleStatusChange('disconnected');
}
subscribe(listener: FrameListener): () => void {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
getStatus(): ConnectionStatus {
return this.status;
}
private buildWsUrl(rawUrl: string): string {
const parsed = new URL(rawUrl);
const proto = parsed.protocol === 'https:' || parsed.protocol === 'wss:' ? 'wss:' : 'ws:';
return `${proto}//${parsed.host}${WS_PATH}`;
}
private handleStatusChange(status: ConnectionStatus): void {
if (status === this.status) {
return;
}
this.status = status;
usePoseStore.getState().setConnectionStatus(status);
}
private scheduleReconnect(): void {
if (!this.active) {
this.handleStatusChange('disconnected');
return;
}
if (this.reconnectAttempt >= MAX_RECONNECT_ATTEMPTS) {
this.handleStatusChange('simulated');
this.startSimulation();
return;
}
const delay = RECONNECT_DELAYS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS.length - 1)];
this.reconnectAttempt += 1;
this.clearReconnectTimer();
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.connect(this.targetUrl);
}, delay);
this.startSimulation();
}
private startSimulation(): void {
if (this.simulationTimer) {
return;
}
this.simulationTimer = setInterval(() => {
this.handleStatusChange('simulated');
const frame = generateSimulatedData();
this.listeners.forEach((listener) => {
listener(frame);
});
}, SIMULATION_TICK_INTERVAL_MS);
}
private stopSimulation(): void {
if (this.simulationTimer) {
clearInterval(this.simulationTimer);
this.simulationTimer = null;
}
}
private clearReconnectTimer(): void {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
}
}
export const wsService = new WsService();