WellNuo/services/espProvisioning.ts
Sergei 7105bb72f7 Stable Light version - App Store submission
WellNuo Lite architecture:
- Simplified navigation flow with NavigationController
- Profile editing with API sync (/auth/profile endpoint)
- OTP verification improvements
- ESP WiFi provisioning setup (espProvisioning.ts)
- E2E testing infrastructure (Playwright)
- Speech recognition hooks (web/native)
- Backend auth enhancements

This is the stable version submitted to App Store.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-12 20:28:18 -08:00

250 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.
*/
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_<wellId>_<mac>
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<boolean> {
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<WellNuoDevice[]> {
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_<wellId>_<macPart>
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<boolean> {
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<WifiNetwork[]> {
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<boolean> {
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<void> {
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<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 };