/** * ESP32 WiFi Provisioning Service * * Handles BLE communication with WellNuo ESP32 sensors (WP_xxx_xxxxxx) * for WiFi configuration and provisioning. * * Uses @orbital-systems/react-native-esp-idf-provisioning which wraps * Espressif's official provisioning libraries. * * NOTE: This module requires a development build. In Expo Go, a mock * implementation is used that returns empty results. */ import { Platform, PermissionsAndroid, Alert } from 'react-native'; import Constants from 'expo-constants'; // Check if we're running in Expo Go (no native modules available) const isExpoGo = Constants.appOwnership === 'expo'; // WellNuo device prefix (matches WP_xxx_xxxxxx pattern) const WELLNUO_DEVICE_PREFIX = 'WP_'; export interface WellNuoDevice { name: string; device: any; // ESPDevice when native module available wellId?: string; // Extracted from name: WP__ macPart?: string; // Last part of MAC address } export interface WifiNetwork { ssid: string; rssi: number; auth: string; } // Dynamic import types - will be null in Expo Go let ESPProvisionManager: any = null; let ESPTransport: any = null; let ESPSecurity: any = null; // Try to load native module only in development builds if (!isExpoGo) { try { const espModule = require('@orbital-systems/react-native-esp-idf-provisioning'); ESPProvisionManager = espModule.ESPProvisionManager; ESPTransport = espModule.ESPTransport; ESPSecurity = espModule.ESPSecurity; } catch (e) { // Native provisioning module not available } } class ESPProvisioningService { private connectedDevice: any | null = null; private isScanning = false; /** * Check if ESP provisioning is available (development build only) */ isAvailable(): boolean { return ESPProvisionManager !== null; } /** * Request necessary permissions for BLE on Android */ async requestPermissions(): Promise { if (!this.isAvailable()) { return false; } if (Platform.OS !== 'android') { return true; } try { const granted = await PermissionsAndroid.requestMultiple([ PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, ]); const allGranted = Object.values(granted).every( (status) => status === PermissionsAndroid.RESULTS.GRANTED ); return allGranted; } catch (error) { return false; } } /** * Scan for WellNuo ESP32 devices * Returns list of devices matching WP_xxx_xxxxxx pattern */ async scanForDevices(timeoutMs = 10000): Promise { if (!this.isAvailable()) { Alert.alert( 'Development Build Required', 'WiFi provisioning requires a development build. This feature is not available in Expo Go.', [{ text: 'OK' }] ); return []; } if (this.isScanning) { return []; } const hasPermissions = await this.requestPermissions(); if (!hasPermissions) { throw new Error('Bluetooth permissions not granted'); } this.isScanning = true; try { const devices = await ESPProvisionManager.searchESPDevices( WELLNUO_DEVICE_PREFIX, ESPTransport.ble, ESPSecurity.unsecure ); return devices.map((device: any) => { // Parse device name: WP__ const parts = device.name?.split('_') || []; return { name: device.name || 'Unknown', device, wellId: parts[1], macPart: parts[2], }; }); } catch (error) { throw error; } finally { this.isScanning = false; } } /** * Connect to a WellNuo device * @param device - The device to connect to * @param proofOfPossession - Optional PoP for secure devices */ async connect( device: any, proofOfPossession?: string ): Promise { if (!this.isAvailable()) { return false; } if (this.connectedDevice) { await this.disconnect(); } try { // Try without PoP first (unsecure mode) await device.connect(proofOfPossession || null); this.connectedDevice = device; return true; } catch (error) { throw error; } } /** * Scan for available WiFi networks through connected device */ async scanWifiNetworks(): Promise { if (!this.isAvailable()) { return []; } if (!this.connectedDevice) { throw new Error('Not connected to any device'); } try { const wifiList = await this.connectedDevice.scanWifiList(); return wifiList.map((wifi: any) => ({ ssid: wifi.ssid, rssi: wifi.rssi, auth: this.getAuthModeName(wifi.auth), })); } catch (error) { throw error; } } /** * Provision device with WiFi credentials * @param ssid - WiFi network name * @param password - WiFi password */ async provisionWifi(ssid: string, password: string): Promise { if (!this.isAvailable()) { return false; } if (!this.connectedDevice) { throw new Error('Not connected to any device'); } try { await this.connectedDevice.provision(ssid, password); return true; } catch (error) { throw error; } } /** * Disconnect from current device */ async disconnect(): Promise { if (!this.connectedDevice) { return; } try { this.connectedDevice.disconnect(); this.connectedDevice = null; } catch (error) { this.connectedDevice = null; } } /** * Check if currently connected to a device */ isConnected(): boolean { return this.connectedDevice !== null; } /** * Get currently connected device name */ getConnectedDeviceName(): string | null { return this.connectedDevice?.name || null; } /** * Convert auth mode number to human-readable string */ private getAuthModeName(authMode: number): string { const modes: Record = { 0: 'Open', 1: 'WEP', 2: 'WPA2 Enterprise', 3: 'WPA2 PSK', 4: 'WPA PSK', 5: 'WPA/WPA2 PSK', }; return modes[authMode] || `Unknown (${authMode})`; } } // Export singleton instance export const espProvisioning = new ESPProvisioningService(); // Export types for components export type ESPDevice = any;