/** * 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. */ import { ESPProvisionManager, ESPDevice, ESPTransport, ESPSecurity, type ESPWifi, } from '@orbital-systems/react-native-esp-idf-provisioning'; import { Platform, PermissionsAndroid, Alert } from 'react-native'; // WellNuo device prefix (matches WP_xxx_xxxxxx pattern) const WELLNUO_DEVICE_PREFIX = 'WP_'; // Security mode - most ESP32 devices use secure or secure2 // Try unsecure first if device doesn't have proof-of-possession const DEFAULT_SECURITY = ESPSecurity.unsecure; export interface WellNuoDevice { name: string; device: ESPDevice; wellId?: string; // Extracted from name: WP__ macPart?: string; // Last part of MAC address } export interface WifiNetwork { ssid: string; rssi: number; auth: string; } class ESPProvisioningService { private connectedDevice: ESPDevice | null = null; private isScanning = false; /** * Request necessary permissions for BLE on Android */ async requestPermissions(): Promise { 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 ); if (!allGranted) { console.warn('[ESP] Some permissions were not granted:', granted); } return allGranted; } catch (error) { console.error('[ESP] Permission request error:', error); return false; } } /** * Scan for WellNuo ESP32 devices * Returns list of devices matching WP_xxx_xxxxxx pattern */ async scanForDevices(timeoutMs = 10000): Promise { if (this.isScanning) { console.warn('[ESP] Already scanning, please wait'); return []; } const hasPermissions = await this.requestPermissions(); if (!hasPermissions) { throw new Error('Bluetooth permissions not granted'); } this.isScanning = true; console.log('[ESP] Starting BLE scan for WellNuo devices...'); try { const devices = await ESPProvisionManager.searchESPDevices( WELLNUO_DEVICE_PREFIX, ESPTransport.ble, DEFAULT_SECURITY ); console.log(`[ESP] Found ${devices.length} WellNuo device(s)`); return devices.map((device: ESPDevice) => { // Parse device name: WP__ const parts = device.name?.split('_') || []; return { name: device.name || 'Unknown', device, wellId: parts[1], macPart: parts[2], }; }); } catch (error) { console.error('[ESP] Scan error:', 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: ESPDevice, proofOfPossession?: string ): Promise { if (this.connectedDevice) { console.warn('[ESP] Already connected, disconnecting first...'); await this.disconnect(); } console.log(`[ESP] Connecting to ${device.name}...`); try { // Try without PoP first (unsecure mode) await device.connect(proofOfPossession || null); this.connectedDevice = device; console.log(`[ESP] Connected to ${device.name}`); return true; } catch (error) { console.error('[ESP] Connection error:', error); throw error; } } /** * Scan for available WiFi networks through connected device */ async scanWifiNetworks(): Promise { if (!this.connectedDevice) { throw new Error('Not connected to any device'); } console.log('[ESP] Scanning for WiFi networks...'); try { const wifiList = await this.connectedDevice.scanWifiList(); console.log(`[ESP] Found ${wifiList.length} WiFi network(s)`); return wifiList.map((wifi: ESPWifi) => ({ ssid: wifi.ssid, rssi: wifi.rssi, auth: this.getAuthModeName(wifi.auth), })); } catch (error) { console.error('[ESP] WiFi scan error:', 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.connectedDevice) { throw new Error('Not connected to any device'); } console.log(`[ESP] Provisioning WiFi: ${ssid}`); try { await this.connectedDevice.provision(ssid, password); console.log('[ESP] WiFi provisioning successful!'); return true; } catch (error) { console.error('[ESP] Provisioning error:', error); throw error; } } /** * Disconnect from current device */ async disconnect(): Promise { if (!this.connectedDevice) { return; } console.log(`[ESP] Disconnecting from ${this.connectedDevice.name}...`); try { this.connectedDevice.disconnect(); this.connectedDevice = null; console.log('[ESP] Disconnected'); } catch (error) { console.error('[ESP] Disconnect error:', 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 };