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>
250 lines
6.4 KiB
TypeScript
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 };
|