import React, { useState, useEffect, useCallback, useRef } from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Alert, ActivityIndicator, TextInput, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { SafeAreaView } from 'react-native-safe-area-context'; import { router, useLocalSearchParams } from 'expo-router'; import * as Device from 'expo-device'; import { useBLE } from '@/contexts/BLEContext'; import { api } from '@/services/api'; import type { WiFiNetwork } from '@/services/ble'; import type { SensorSetupState, SensorSetupStep, SensorSetupStatus, } from '@/types'; import BatchSetupProgress from '@/components/BatchSetupProgress'; import SetupResultsScreen from '@/components/SetupResultsScreen'; import { AppColors, BorderRadius, FontSizes, FontWeights, Spacing, Shadows, } from '@/constants/theme'; // Type for device passed via navigation params interface DeviceParam { id: string; name: string; mac: string; wellId?: number; } type SetupPhase = 'wifi_selection' | 'batch_setup' | 'results'; // Initialize steps for a sensor function createInitialSteps(): SensorSetupStep[] { return [ { name: 'connect', status: 'pending' }, { name: 'unlock', status: 'pending' }, { name: 'wifi', status: 'pending' }, { name: 'attach', status: 'pending' }, { name: 'reboot', status: 'pending' }, ]; } // Initialize sensor state function createSensorState(device: DeviceParam): SensorSetupState { return { deviceId: device.id, deviceName: device.name, wellId: device.wellId, mac: device.mac, status: 'pending', steps: createInitialSteps(), }; } export default function SetupWiFiScreen() { const { id, devices: devicesParam } = useLocalSearchParams<{ id: string; devices: string; // JSON string of DeviceParam[] }>(); const { getWiFiList, setWiFi, connectDevice, disconnectDevice, rebootDevice, } = useBLE(); // Parse devices from navigation params const selectedDevices: DeviceParam[] = React.useMemo(() => { if (!devicesParam) return []; try { return JSON.parse(devicesParam); } catch (e) { console.error('[SetupWiFi] Failed to parse devices param:', e); return []; } }, [devicesParam]); // Use first device for WiFi scanning const firstDevice = selectedDevices[0]; const deviceId = firstDevice?.id; // UI Phase const [phase, setPhase] = useState('wifi_selection'); // WiFi selection state const [networks, setNetworks] = useState([]); const [isLoadingNetworks, setIsLoadingNetworks] = useState(false); const [selectedNetwork, setSelectedNetwork] = useState(null); const [password, setPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); // Batch setup state const [sensors, setSensors] = useState([]); const [currentIndex, setCurrentIndex] = useState(0); const [isPaused, setIsPaused] = useState(false); const setupInProgressRef = useRef(false); const shouldCancelRef = useRef(false); useEffect(() => { loadWiFiNetworks(); }, []); const loadWiFiNetworks = async () => { if (!deviceId) return; setIsLoadingNetworks(true); try { const wifiList = await getWiFiList(deviceId); setNetworks(wifiList); } catch (error: any) { console.error('[SetupWiFi] Failed to get WiFi list:', error); Alert.alert('Error', error.message || 'Failed to get WiFi networks. Please try again.'); } finally { setIsLoadingNetworks(false); } }; const handleSelectNetwork = (network: WiFiNetwork) => { setSelectedNetwork(network); setPassword(''); }; // Update a specific step for a sensor const updateSensorStep = useCallback(( deviceId: string, stepName: SensorSetupStep['name'], stepStatus: SensorSetupStep['status'], error?: string ) => { setSensors(prev => prev.map(sensor => { if (sensor.deviceId !== deviceId) return sensor; return { ...sensor, steps: sensor.steps.map(step => step.name === stepName ? { ...step, status: stepStatus, error } : step ), }; })); }, []); // Update sensor status const updateSensorStatus = useCallback(( deviceId: string, status: SensorSetupStatus, error?: string ) => { setSensors(prev => prev.map(sensor => sensor.deviceId === deviceId ? { ...sensor, status, error, endTime: Date.now() } : sensor )); }, []); // Process a single sensor const processSensor = useCallback(async ( sensor: SensorSetupState, ssid: string, pwd: string ): Promise => { const { deviceId, wellId, deviceName } = sensor; const isSimulator = !Device.isDevice; console.log(`[SetupWiFi] [${deviceName}] Starting setup...`); // Set start time setSensors(prev => prev.map(s => s.deviceId === deviceId ? { ...s, startTime: Date.now() } : s )); try { // Step 1: Connect updateSensorStep(deviceId, 'connect', 'in_progress'); updateSensorStatus(deviceId, 'connecting'); const connected = await connectDevice(deviceId); if (!connected) throw new Error('Could not connect to sensor'); updateSensorStep(deviceId, 'connect', 'completed'); if (shouldCancelRef.current) return false; // Step 2: Unlock (PIN is handled by connectDevice in BLE manager) updateSensorStep(deviceId, 'unlock', 'in_progress'); updateSensorStatus(deviceId, 'unlocking'); // PIN unlock is automatic in connectDevice, mark as completed updateSensorStep(deviceId, 'unlock', 'completed'); if (shouldCancelRef.current) return false; // Step 3: Set WiFi updateSensorStep(deviceId, 'wifi', 'in_progress'); updateSensorStatus(deviceId, 'setting_wifi'); const wifiSuccess = await setWiFi(deviceId, ssid, pwd); if (!wifiSuccess) throw new Error('Failed to configure WiFi'); updateSensorStep(deviceId, 'wifi', 'completed'); if (shouldCancelRef.current) return false; // Step 4: Attach to deployment via API updateSensorStep(deviceId, 'attach', 'in_progress'); updateSensorStatus(deviceId, 'attaching'); if (!isSimulator && wellId) { const attachResponse = await api.attachDeviceToBeneficiary( id!, wellId, ssid, pwd ); if (!attachResponse.ok) { throw new Error('Failed to register sensor'); } } else { console.log(`[SetupWiFi] [${deviceName}] Simulator mode - skipping API attach`); } updateSensorStep(deviceId, 'attach', 'completed'); if (shouldCancelRef.current) return false; // Step 5: Reboot updateSensorStep(deviceId, 'reboot', 'in_progress'); updateSensorStatus(deviceId, 'rebooting'); await rebootDevice(deviceId); updateSensorStep(deviceId, 'reboot', 'completed'); // Success! updateSensorStatus(deviceId, 'success'); console.log(`[SetupWiFi] [${deviceName}] Setup completed successfully`); return true; } catch (error: any) { console.error(`[SetupWiFi] [${deviceName}] Setup failed:`, error); const errorMsg = error.message || 'Unknown error'; // Find current step and mark as failed setSensors(prev => prev.map(s => { if (s.deviceId !== deviceId) return s; const currentStep = s.steps.find(step => step.status === 'in_progress'); return { ...s, status: 'error' as SensorSetupStatus, error: errorMsg, steps: s.steps.map(step => step.status === 'in_progress' ? { ...step, status: 'failed' as const, error: errorMsg } : step ), }; })); // Disconnect on error try { await disconnectDevice(deviceId); } catch (e) { // Ignore disconnect errors } return false; } }, [ id, connectDevice, disconnectDevice, setWiFi, rebootDevice, updateSensorStep, updateSensorStatus ]); // Run batch setup sequentially const runBatchSetup = useCallback(async () => { if (setupInProgressRef.current) return; setupInProgressRef.current = true; shouldCancelRef.current = false; const ssid = selectedNetwork!.ssid; const pwd = password; for (let i = currentIndex; i < sensors.length; i++) { if (shouldCancelRef.current) { console.log('[SetupWiFi] Batch setup cancelled'); break; } setCurrentIndex(i); const sensor = sensors[i]; // Skip already processed sensors if (sensor.status === 'success' || sensor.status === 'skipped') { continue; } // If sensor has error and we're not retrying, pause if (sensor.status === 'error' && isPaused) { break; } // Reset sensor state if retrying if (sensor.status === 'error') { setSensors(prev => prev.map(s => s.deviceId === sensor.deviceId ? { ...s, status: 'pending' as SensorSetupStatus, error: undefined, steps: createInitialSteps() } : s )); } const success = await processSensor( sensors[i], ssid, pwd ); // Check for cancellation after each sensor if (shouldCancelRef.current) break; // If failed, pause for user input if (!success) { setIsPaused(true); setupInProgressRef.current = false; return; } } // All done setupInProgressRef.current = false; // Check if we should show results const finalSensors = sensors; const allProcessed = finalSensors.every( s => s.status === 'success' || s.status === 'error' || s.status === 'skipped' ); if (allProcessed || shouldCancelRef.current) { setPhase('results'); } }, [sensors, currentIndex, selectedNetwork, password, isPaused, processSensor]); // Start batch setup const handleStartBatchSetup = () => { if (!selectedNetwork) { Alert.alert('Error', 'Please select a WiFi network'); return; } if (!password) { Alert.alert('Error', 'Please enter WiFi password'); return; } // Initialize sensor states const initialStates = selectedDevices.map(createSensorState); setSensors(initialStates); setCurrentIndex(0); setIsPaused(false); setPhase('batch_setup'); }; // Start processing after phase change useEffect(() => { if (phase === 'batch_setup' && sensors.length > 0 && !setupInProgressRef.current) { runBatchSetup(); } }, [phase, sensors.length, runBatchSetup]); // Retry failed sensor const handleRetry = (deviceId: string) => { const index = sensors.findIndex(s => s.deviceId === deviceId); if (index >= 0) { setSensors(prev => prev.map(s => s.deviceId === deviceId ? { ...s, status: 'pending' as SensorSetupStatus, error: undefined, steps: createInitialSteps() } : s )); setCurrentIndex(index); setIsPaused(false); runBatchSetup(); } }; // Skip failed sensor const handleSkip = (deviceId: string) => { setSensors(prev => prev.map(s => s.deviceId === deviceId ? { ...s, status: 'skipped' as SensorSetupStatus } : s )); setIsPaused(false); // Move to next sensor const nextIndex = currentIndex + 1; if (nextIndex < sensors.length) { setCurrentIndex(nextIndex); runBatchSetup(); } else { setPhase('results'); } }; // Cancel all const handleCancelAll = () => { Alert.alert( 'Cancel Setup', 'Are you sure you want to cancel? Progress will be lost.', [ { text: 'Continue Setup', style: 'cancel' }, { text: 'Cancel', style: 'destructive', onPress: () => { shouldCancelRef.current = true; setupInProgressRef.current = false; // Disconnect all devices selectedDevices.forEach(d => disconnectDevice(d.id)); router.back(); }, }, ] ); }; // Done - navigate back const handleDone = () => { router.replace(`/(tabs)/beneficiaries/${id}/equipment` as any); }; // Retry a single sensor from results screen const handleRetryFromResults = (deviceId: string) => { const index = sensors.findIndex(s => s.deviceId === deviceId); if (index >= 0) { // Reset the sensor state setSensors(prev => prev.map(s => s.deviceId === deviceId ? { ...s, status: 'pending' as SensorSetupStatus, error: undefined, steps: createInitialSteps() } : s )); setCurrentIndex(index); setIsPaused(false); // Go back to batch setup phase setPhase('batch_setup'); } }; const getSignalStrength = (rssi: number): string => { if (rssi >= -50) return 'Excellent'; if (rssi >= -60) return 'Good'; if (rssi >= -70) return 'Fair'; return 'Weak'; }; const getSignalColor = (rssi: number) => { if (rssi >= -50) return AppColors.success; if (rssi >= -60) return AppColors.info; if (rssi >= -70) return AppColors.warning; return AppColors.error; }; const getSignalIcon = (rssi: number) => { if (rssi >= -50) return 'wifi'; if (rssi >= -60) return 'wifi'; if (rssi >= -70) return 'wifi-outline'; return 'wifi-outline'; }; // Results screen if (phase === 'results') { return ( ); } // Batch setup progress screen if (phase === 'batch_setup') { return ( Setting Up Sensors ); } // WiFi selection screen (default) return ( {/* Header */} { selectedDevices.forEach(d => disconnectDevice(d.id)); router.back(); }} > Setup WiFi {/* Device Info Card */} {selectedDevices.length === 1 ? ( <> {firstDevice?.name} Well ID: {firstDevice?.wellId} ) : ( <> {selectedDevices.length} Sensors Selected {selectedDevices.map(d => d.name).join(', ')} )} {/* Instructions */} {selectedDevices.length === 1 ? 'Select the WiFi network your sensor should connect to. Make sure the network has internet access.' : `Select the WiFi network for all ${selectedDevices.length} sensors. They will all be configured with the same WiFi credentials.`} {/* WiFi Networks List */} {isLoadingNetworks ? ( Scanning for WiFi networks... ) : ( <> Available Networks ({networks.length}) {networks.length === 0 ? ( No WiFi networks found Try Again ) : ( {networks.map((network, index) => { const isSelected = selectedNetwork?.ssid === network.ssid; return ( handleSelectNetwork(network)} activeOpacity={0.7} > {network.ssid} {getSignalStrength(network.rssi)} ({network.rssi} dBm) {isSelected && ( )} ); })} )} )} {/* Password Input (shown when network selected) */} {selectedNetwork && ( WiFi Password setShowPassword(!showPassword)} > {/* Connect Button */} {selectedDevices.length === 1 ? 'Connect & Complete Setup' : `Connect All ${selectedDevices.length} Sensors`} )} {/* Help Card */} Important • The sensor will reboot after WiFi is configured{'\n'} • It may take up to 1 minute for the sensor to connect{'\n'} • Make sure the WiFi password is correct{'\n'} • The network must have internet access ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: AppColors.background, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: Spacing.md, paddingVertical: Spacing.sm, borderBottomWidth: 1, borderBottomColor: AppColors.border, }, backButton: { padding: Spacing.xs, }, headerTitle: { fontSize: FontSizes.lg, fontWeight: FontWeights.semibold, color: AppColors.textPrimary, }, placeholder: { width: 32, }, content: { flex: 1, }, scrollContent: { padding: Spacing.lg, paddingBottom: Spacing.xxl, }, batchContent: { flex: 1, padding: Spacing.lg, }, // Device Card deviceCard: { flexDirection: 'row', alignItems: 'center', backgroundColor: AppColors.surface, borderRadius: BorderRadius.xl, padding: Spacing.lg, marginBottom: Spacing.lg, ...Shadows.sm, }, deviceIcon: { width: 60, height: 60, borderRadius: BorderRadius.lg, backgroundColor: AppColors.primaryLighter, justifyContent: 'center', alignItems: 'center', marginRight: Spacing.md, }, deviceInfo: { flex: 1, }, deviceName: { fontSize: FontSizes.lg, fontWeight: FontWeights.semibold, color: AppColors.textPrimary, marginBottom: 2, }, deviceMeta: { fontSize: FontSizes.sm, color: AppColors.textMuted, }, // Instructions instructionsCard: { backgroundColor: AppColors.infoLight, borderRadius: BorderRadius.lg, padding: Spacing.md, marginBottom: Spacing.lg, }, instructionsText: { fontSize: FontSizes.sm, color: AppColors.info, lineHeight: 20, }, // Loading loadingContainer: { alignItems: 'center', padding: Spacing.xl, backgroundColor: AppColors.surface, borderRadius: BorderRadius.xl, ...Shadows.sm, }, loadingText: { fontSize: FontSizes.base, color: AppColors.textSecondary, marginTop: Spacing.md, }, // Section Header sectionHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: Spacing.md, }, sectionTitle: { fontSize: FontSizes.sm, fontWeight: FontWeights.semibold, color: AppColors.textSecondary, textTransform: 'uppercase', letterSpacing: 0.5, }, refreshButton: { padding: Spacing.xs, }, // Empty State emptyState: { alignItems: 'center', padding: Spacing.xl, backgroundColor: AppColors.surface, borderRadius: BorderRadius.xl, ...Shadows.sm, }, emptyText: { fontSize: FontSizes.base, color: AppColors.textMuted, marginTop: Spacing.md, marginBottom: Spacing.md, }, retryButton: { paddingVertical: Spacing.sm, paddingHorizontal: Spacing.lg, }, retryText: { fontSize: FontSizes.base, fontWeight: FontWeights.semibold, color: AppColors.primary, }, // Networks List networksList: { gap: Spacing.sm, marginBottom: Spacing.lg, }, networkCard: { backgroundColor: AppColors.surface, borderRadius: BorderRadius.lg, padding: Spacing.md, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', ...Shadows.xs, }, networkCardSelected: { borderWidth: 2, borderColor: AppColors.primary, }, networkInfo: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: Spacing.md, }, networkDetails: { flex: 1, }, networkName: { fontSize: FontSizes.base, fontWeight: FontWeights.semibold, color: AppColors.textPrimary, marginBottom: 2, }, signalText: { fontSize: FontSizes.xs, fontWeight: FontWeights.medium, }, // Password Input passwordCard: { backgroundColor: AppColors.surface, borderRadius: BorderRadius.xl, padding: Spacing.lg, marginBottom: Spacing.lg, ...Shadows.sm, }, passwordLabel: { fontSize: FontSizes.sm, fontWeight: FontWeights.semibold, color: AppColors.textSecondary, marginBottom: Spacing.sm, }, passwordInputContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: AppColors.background, borderRadius: BorderRadius.md, borderWidth: 1, borderColor: AppColors.border, marginBottom: Spacing.md, }, passwordInput: { flex: 1, paddingVertical: Spacing.sm, paddingHorizontal: Spacing.md, fontSize: FontSizes.base, color: AppColors.textPrimary, }, togglePasswordButton: { padding: Spacing.md, }, // Connect Button connectButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: AppColors.primary, paddingVertical: Spacing.md, borderRadius: BorderRadius.lg, gap: Spacing.sm, ...Shadows.md, }, connectButtonDisabled: { opacity: 0.5, }, connectButtonText: { fontSize: FontSizes.base, fontWeight: FontWeights.semibold, color: AppColors.white, }, // Help Card helpCard: { backgroundColor: AppColors.infoLight, borderRadius: BorderRadius.lg, padding: Spacing.md, }, helpHeader: { flexDirection: 'row', alignItems: 'center', gap: Spacing.sm, marginBottom: Spacing.xs, }, helpTitle: { fontSize: FontSizes.sm, fontWeight: FontWeights.semibold, color: AppColors.info, }, helpText: { fontSize: FontSizes.sm, color: AppColors.info, lineHeight: 20, }, });