feat: Implement RSSI service for iOS and Web platforms

- Added IosRssiService to handle synthetic RSSI data for iOS.
- Created WebRssiService to simulate RSSI scanning on the web.
- Defined shared types for WifiNetwork and RssiService in rssi.service.ts.
- Introduced simulation service to generate synthetic sensing data.
- Implemented WebSocket service for real-time data handling with reconnection logic.
- Established Zustand stores for managing application state related to MAT and pose data.
- Developed theme context and utility functions for consistent styling and formatting.
- Added type definitions for various application entities including API responses and sensing data.
- Created utility functions for color mapping and URL validation.
- Configured TypeScript settings for the mobile application.
This commit is contained in:
ruv
2026-03-02 10:30:33 -05:00
parent 02192b0232
commit fdc7142dfa
131 changed files with 24090 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
export function valueToColor(v: number): [number, number, number] {
const clamped = Math.max(0, Math.min(1, v));
let r: number;
let g: number;
let b: number;
if (clamped < 0.5) {
const t = clamped * 2;
r = 0;
g = t;
b = 1 - t;
} else {
const t = (clamped - 0.5) * 2;
r = t;
g = 1 - t;
b = 0;
}
return [r, g, b];
}
+34
View File
@@ -0,0 +1,34 @@
export function formatRssi(v: number | null | undefined): string {
if (typeof v !== 'number' || !Number.isFinite(v)) {
return '-- dBm';
}
return `${Math.round(v)} dBm`;
}
export function formatBpm(v: number | null | undefined): string {
if (typeof v !== 'number' || !Number.isFinite(v)) {
return '--';
}
return `${Math.round(v)} BPM`;
}
export function formatConfidence(v: number | null | undefined): string {
if (typeof v !== 'number' || !Number.isFinite(v)) {
return '--';
}
const normalized = v > 1 ? v / 100 : v;
return `${Math.round(Math.max(0, Math.min(1, normalized)) * 100)}%`;
}
export function formatUptime(ms: number | null | undefined): string {
if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0) {
return '--:--:--';
}
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}
+48
View File
@@ -0,0 +1,48 @@
export class RingBuffer<T> {
private readonly capacity: number;
private readonly compare?: (a: T, b: T) => number;
private readonly values: T[] = [];
constructor(capacity: number, compare?: (a: T, b: T) => number) {
if (!Number.isFinite(capacity) || capacity <= 0) {
throw new Error('RingBuffer capacity must be greater than 0');
}
this.capacity = Math.floor(capacity);
this.compare = compare;
}
push(v: T): void {
this.values.push(v);
if (this.values.length > this.capacity) {
this.values.shift();
}
}
toArray(): T[] {
return [...this.values];
}
clear(): void {
this.values.length = 0;
}
get max(): T | null {
if (this.values.length === 0) {
return null;
}
if (!this.compare) {
throw new Error('Comparator required for max()');
}
return this.values.reduce((acc, value) => (this.compare!(value, acc) > 0 ? value : acc), this.values[0]);
}
get min(): T | null {
if (this.values.length === 0) {
return null;
}
if (!this.compare) {
throw new Error('Comparator required for min()');
}
return this.values.reduce((acc, value) => (this.compare!(value, acc) < 0 ? value : acc), this.values[0]);
}
}
+25
View File
@@ -0,0 +1,25 @@
const ALLOWED_PROTOCOLS = new Set(['http:', 'https:', 'ws:', 'wss:']);
export interface UrlValidationResult {
valid: boolean;
error?: string;
}
export function validateServerUrl(url: string): UrlValidationResult {
if (typeof url !== 'string' || !url.trim()) {
return { valid: false, error: 'URL must be a non-empty string.' };
}
try {
const parsed = new URL(url);
if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) {
return { valid: false, error: 'URL must use http, https, ws, or wss.' };
}
if (!parsed.host) {
return { valid: false, error: 'URL must include a host.' };
}
return { valid: true };
} catch {
return { valid: false, error: 'Invalid URL format.' };
}
}