WellNuo/services/espProvisioning.ts
Sergei 5e0b38748b Update Stripe integration, API services, and purchase screens
- Update purchase screens (auth and beneficiary)
- Update Stripe configuration and setup scripts
- Update api.ts services
- Update espProvisioning and sherpaTTS services
- Update verify-otp flow
- Package updates
2026-01-12 21:44:57 -08:00

301 lines
7.9 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;
console.log('[ESP] Native provisioning module loaded');
} catch (e) {
console.warn('[ESP] Native provisioning module not available:', e);
}
}
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()) {
console.warn('[ESP] Provisioning not available in Expo Go');
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
);
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.isAvailable()) {
console.warn('[ESP] Scan not available - running in Expo Go');
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) {
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,
ESPSecurity.unsecure
);
console.log(`[ESP] Found ${devices.length} WellNuo device(s)`);
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) {
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: any,
proofOfPossession?: string
): Promise<boolean> {
if (!this.isAvailable()) {
console.warn('[ESP] Connect not available - running in Expo Go');
return false;
}
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.isAvailable()) {
console.warn('[ESP] WiFi scan not available - running in Expo Go');
return [];
}
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: any) => ({
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.isAvailable()) {
console.warn('[ESP] Provisioning not available - running in Expo Go');
return false;
}
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 = any;