WellNuo/services/espProvisioning.ts
Sergei d453126c89 feat: Room location picker + robster credentials
- Backend: Update Legacy API credentials to robster/rob2
- Frontend: ROOM_LOCATIONS with icons and legacyCode mapping
- Device Settings: Modal picker for room selection
- api.ts: Bidirectional conversion (code ↔ name)
- Various UI/UX improvements across screens

PRD-DEPLOYMENT.md completed (Score: 9/10)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-24 15:22:40 -08:00

267 lines
6.4 KiB
TypeScript

/**
* 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_<wellId>_<mac>
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<boolean> {
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<WellNuoDevice[]> {
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_<wellId>_<macPart>
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<boolean> {
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<WifiNetwork[]> {
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<boolean> {
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<void> {
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<number, string> = {
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;