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
@@ -0,0 +1,36 @@
import { Platform, Switch, View } from 'react-native';
import { ThemedText } from '@/components/ThemedText';
import { colors } from '@/theme/colors';
import { spacing } from '@/theme/spacing';
type RssiToggleProps = {
enabled: boolean;
onChange: (value: boolean) => void;
};
export const RssiToggle = ({ enabled, onChange }: RssiToggleProps) => {
return (
<View>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<View style={{ flex: 1 }}>
<ThemedText preset="bodyMd">RSSI Scan</ThemedText>
<ThemedText preset="bodySm" style={{ color: colors.textSecondary }}>
Scan for nearby Wi-Fi signals from Android devices
</ThemedText>
</View>
<Switch
value={enabled}
onValueChange={onChange}
trackColor={{ true: colors.accent, false: colors.surfaceAlt }}
thumbColor={colors.textPrimary}
/>
</View>
{Platform.OS === 'ios' && (
<ThemedText preset="bodySm" style={{ color: colors.textSecondary, marginTop: spacing.xs }}>
iOS: RSSI scan is currently limited using stub data.
</ThemedText>
)}
</View>
);
};
@@ -0,0 +1,102 @@
import { useState } from 'react';
import { Pressable, TextInput, View } from 'react-native';
import { validateServerUrl } from '@/utils/urlValidator';
import { apiService } from '@/services/api.service';
import { ThemedText } from '@/components/ThemedText';
import { colors } from '@/theme/colors';
import { spacing } from '@/theme/spacing';
type ServerUrlInputProps = {
value: string;
onChange: (value: string) => void;
onSave: () => void;
};
export const ServerUrlInput = ({ value, onChange, onSave }: ServerUrlInputProps) => {
const [testResult, setTestResult] = useState('');
const validation = validateServerUrl(value);
const handleTest = async () => {
if (!validation.valid) {
setTestResult('✗ Invalid URL');
return;
}
const start = Date.now();
try {
await apiService.getStatus();
setTestResult(`${Date.now() - start}ms`);
} catch {
setTestResult('✗ Failed');
}
};
return (
<View>
<ThemedText preset="labelMd" style={{ marginBottom: spacing.sm }}>
Server URL
</ThemedText>
<TextInput
value={value}
onChangeText={onChange}
autoCapitalize="none"
autoCorrect={false}
placeholder="http://192.168.1.100:8080"
keyboardType="url"
placeholderTextColor={colors.textSecondary}
style={{
borderWidth: 1,
borderColor: validation.valid ? colors.border : colors.danger,
borderRadius: 10,
backgroundColor: colors.surface,
color: colors.textPrimary,
padding: spacing.sm,
marginBottom: spacing.sm,
}}
/>
{!validation.valid && (
<ThemedText preset="bodySm" style={{ color: colors.danger, marginBottom: spacing.sm }}>
{validation.error}
</ThemedText>
)}
<ThemedText preset="bodySm" style={{ color: colors.textSecondary, marginBottom: spacing.sm }}>
{testResult || 'Ready to test connection'}
</ThemedText>
<View style={{ flexDirection: 'row', gap: spacing.sm }}>
<Pressable
onPress={handleTest}
disabled={!validation.valid}
style={{
flex: 1,
paddingVertical: 10,
borderRadius: 8,
backgroundColor: validation.valid ? colors.accentDim : colors.surfaceAlt,
alignItems: 'center',
}}
>
<ThemedText preset="labelMd" style={{ color: colors.textPrimary }}>
Test Connection
</ThemedText>
</Pressable>
<Pressable
onPress={onSave}
disabled={!validation.valid}
style={{
flex: 1,
paddingVertical: 10,
borderRadius: 8,
backgroundColor: validation.valid ? colors.success : colors.surfaceAlt,
alignItems: 'center',
}}
>
<ThemedText preset="labelMd" style={{ color: colors.textPrimary }}>
Save
</ThemedText>
</Pressable>
</View>
</View>
);
};
@@ -0,0 +1,47 @@
import { Pressable, View } from 'react-native';
import { ThemeMode } from '@/theme/ThemeContext';
import { ThemedText } from '@/components/ThemedText';
import { colors } from '@/theme/colors';
import { spacing } from '@/theme/spacing';
type ThemePickerProps = {
value: ThemeMode;
onChange: (value: ThemeMode) => void;
};
const OPTIONS: ThemeMode[] = ['light', 'dark', 'system'];
export const ThemePicker = ({ value, onChange }: ThemePickerProps) => {
return (
<View
style={{
flexDirection: 'row',
gap: spacing.sm,
marginTop: spacing.sm,
}}
>
{OPTIONS.map((option) => {
const isActive = option === value;
return (
<Pressable
key={option}
onPress={() => onChange(option)}
style={{
flex: 1,
borderRadius: 8,
borderWidth: 1,
borderColor: isActive ? colors.accent : colors.border,
backgroundColor: isActive ? `${colors.accent}22` : '#0D1117',
paddingVertical: 10,
alignItems: 'center',
}}
>
<ThemedText preset="labelMd" style={{ color: isActive ? colors.accent : colors.textSecondary }}>
{option.toUpperCase()}
</ThemedText>
</Pressable>
);
})}
</View>
);
};
@@ -0,0 +1,170 @@
import { useEffect, useMemo, useState } from 'react';
import { Linking, ScrollView, View } from 'react-native';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { colors } from '@/theme/colors';
import { spacing } from '@/theme/spacing';
import { WS_PATH } from '@/constants/websocket';
import { apiService } from '@/services/api.service';
import { wsService } from '@/services/ws.service';
import { useSettingsStore } from '@/stores/settingsStore';
import { Alert, Pressable, Platform } from 'react-native';
import { ThemePicker } from './ThemePicker';
import { RssiToggle } from './RssiToggle';
import { ServerUrlInput } from './ServerUrlInput';
type GlowCardProps = {
title: string;
children: React.ReactNode;
};
const GlowCard = ({ title, children }: GlowCardProps) => {
return (
<View
style={{
backgroundColor: '#0F141E',
borderRadius: 14,
borderWidth: 1,
borderColor: `${colors.accent}35`,
padding: spacing.md,
marginBottom: spacing.md,
}}
>
<ThemedText preset="labelMd" style={{ marginBottom: spacing.sm, color: colors.textPrimary }}>
{title}
</ThemedText>
{children}
</View>
);
};
const ScanIntervalPicker = ({
value,
onChange,
}: {
value: number;
onChange: (value: number) => void;
}) => {
const options = [1, 2, 5];
return (
<View style={{ flexDirection: 'row', gap: spacing.sm, marginTop: spacing.sm }}>
{options.map((interval) => {
const isActive = interval === value;
return (
<Pressable
key={interval}
onPress={() => onChange(interval)}
style={{
flex: 1,
borderWidth: 1,
borderColor: isActive ? colors.accent : colors.border,
borderRadius: 8,
backgroundColor: isActive ? `${colors.accent}20` : colors.surface,
alignItems: 'center',
}}
>
<ThemedText
preset="bodySm"
style={{
color: isActive ? colors.accent : colors.textSecondary,
paddingVertical: 8,
}}
>
{interval}s
</ThemedText>
</Pressable>
);
})}
</View>
);
};
export const SettingsScreen = () => {
const serverUrl = useSettingsStore((state) => state.serverUrl);
const rssiScanEnabled = useSettingsStore((state) => state.rssiScanEnabled);
const theme = useSettingsStore((state) => state.theme);
const setServerUrl = useSettingsStore((state) => state.setServerUrl);
const setRssiScanEnabled = useSettingsStore((state) => state.setRssiScanEnabled);
const setTheme = useSettingsStore((state) => state.setTheme);
const [draftUrl, setDraftUrl] = useState(serverUrl);
const [scanInterval, setScanInterval] = useState(2);
useEffect(() => {
setDraftUrl(serverUrl);
}, [serverUrl]);
const intervalSummary = useMemo(() => `${scanInterval}s`, [scanInterval]);
const handleSaveUrl = () => {
const newUrl = draftUrl.trim();
setServerUrl(newUrl);
wsService.disconnect();
wsService.connect(newUrl);
apiService.setBaseUrl(newUrl);
};
const handleOpenGitHub = async () => {
const handled = await Linking.canOpenURL('https://github.com');
if (!handled) {
Alert.alert('Unable to open link', 'Please open https://github.com manually in your browser.');
return;
}
await Linking.openURL('https://github.com');
};
return (
<ThemedView style={{ flex: 1, backgroundColor: colors.bg, padding: spacing.md }}>
<ScrollView
contentContainerStyle={{
paddingBottom: spacing.xl,
}}
>
<GlowCard title="SERVER">
<ServerUrlInput value={draftUrl} onChange={setDraftUrl} onSave={handleSaveUrl} />
</GlowCard>
<GlowCard title="SENSING">
<RssiToggle enabled={rssiScanEnabled} onChange={setRssiScanEnabled} />
<ThemedText preset="bodyMd" style={{ marginTop: spacing.md }}>
Scan interval
</ThemedText>
<ScanIntervalPicker value={scanInterval} onChange={setScanInterval} />
<ThemedText preset="bodySm" style={{ color: colors.textSecondary, marginTop: spacing.sm }}>
Active interval: {intervalSummary}
</ThemedText>
{Platform.OS === 'ios' && (
<ThemedText preset="bodySm" style={{ color: colors.textSecondary, marginTop: spacing.sm }}>
iOS: RSSI scanning uses stubbed telemetry in this build.
</ThemedText>
)}
</GlowCard>
<GlowCard title="APPEARANCE">
<ThemePicker value={theme} onChange={setTheme} />
</GlowCard>
<GlowCard title="ABOUT">
<ThemedText preset="bodyMd" style={{ marginBottom: spacing.xs }}>
WiFi-DensePose Mobile v1.0.0
</ThemedText>
<ThemedText
preset="bodySm"
style={{ color: colors.accent, marginBottom: spacing.sm }}
onPress={handleOpenGitHub}
>
View on GitHub
</ThemedText>
<ThemedText preset="bodySm">WebSocket: {WS_PATH}</ThemedText>
<ThemedText preset="bodySm" style={{ color: colors.textSecondary }}>
Triage priority mapping: Immediate/Delayed/Minor/Deceased/Unknown
</ThemedText>
</GlowCard>
</ScrollView>
</ThemedView>
);
};
export default SettingsScreen;