// Web Bluetooth BLE Manager for browser-based sensor configuration // Supports Chrome, Edge, Opera (NOT Safari/Firefox) import { IBLEManager, WPDevice, WiFiNetwork, WiFiStatus, BLE_CONFIG, BLE_COMMANDS, BLEConnectionState, BLEDeviceConnection, BLEEventListener, BLEConnectionEvent, SensorHealthMetrics, SensorHealthStatus, WiFiSignalQuality, CommunicationHealth, BulkOperationResult, BulkWiFiResult, ReconnectConfig, ReconnectState, DEFAULT_RECONNECT_CONFIG, } from './types'; import { BLEError, BLEErrorCode, BLELogger, isBLEError, parseBLEError, } from './errors'; import { checkWebBluetoothSupport, getUnsupportedBrowserMessage, } from './webBluetooth'; // Web Bluetooth API types // These types are available when running in a browser that supports Web Bluetooth // We use `any` here to avoid conflicts with different type definitions type WebBluetoothDevice = { id: string; name?: string; gatt?: WebBluetoothGATTServer; addEventListener(type: string, listener: EventListener): void; removeEventListener(type: string, listener: EventListener): void; }; type WebBluetoothGATTServer = { device: WebBluetoothDevice; connected: boolean; connect(): Promise; disconnect(): void; getPrimaryService(service: string): Promise; }; type WebBluetoothGATTService = { device: WebBluetoothDevice; uuid: string; getCharacteristic(characteristic: string): Promise; }; type WebBluetoothGATTCharacteristic = { service: WebBluetoothGATTService; uuid: string; value?: DataView; startNotifications(): Promise; stopNotifications(): Promise; readValue(): Promise; writeValue(value: BufferSource): Promise; writeValueWithResponse(value: BufferSource): Promise; addEventListener(type: string, listener: EventListener): void; removeEventListener(type: string, listener: EventListener): void; }; /** * Web Bluetooth implementation of BLE Manager * Works in Chrome, Edge, Opera on desktop and Android */ export class WebBLEManager implements IBLEManager { private connectedDevices = new Map(); private gattServers = new Map(); private characteristics = new Map(); private connectionStates = new Map(); private eventListeners: BLEEventListener[] = []; private connectingDevices = new Set(); // Health monitoring state private sensorHealthMetrics = new Map(); private communicationStats = new Map(); // Reconnect state private reconnectConfig: ReconnectConfig = { ...DEFAULT_RECONNECT_CONFIG }; private reconnectStates = new Map(); private reconnectTimers = new Map>(); constructor() { // Check browser support on initialization const support = checkWebBluetoothSupport(); if (!support.supported) { const msg = getUnsupportedBrowserMessage(support); BLELogger.warn(`Web Bluetooth not supported: ${msg.message}`); } } /** * Check if Web Bluetooth is available before any operation */ private checkBluetoothAvailable(): void { const support = checkWebBluetoothSupport(); if (!support.supported) { const msg = getUnsupportedBrowserMessage(support); throw new BLEError(BLEErrorCode.BLUETOOTH_DISABLED, { message: `${msg.title}: ${msg.message} ${msg.suggestion}`, }); } } /** * Update connection state and notify listeners */ private updateConnectionState( deviceId: string, state: BLEConnectionState, deviceName?: string, error?: string ): void { const existing = this.connectionStates.get(deviceId); const now = Date.now(); const connection: BLEDeviceConnection = { deviceId, deviceName: deviceName || existing?.deviceName || deviceId, state, error, connectedAt: state === BLEConnectionState.CONNECTED ? now : existing?.connectedAt, lastActivity: now, }; this.connectionStates.set(deviceId, connection); this.emitEvent(deviceId, 'state_changed', { state, error }); } /** * Emit event to all registered listeners */ private emitEvent(deviceId: string, event: BLEConnectionEvent, data?: any): void { this.eventListeners.forEach((listener) => { try { listener(deviceId, event, data); } catch { // Listener error should not crash the app } }); } /** * Get current connection state for a device */ getConnectionState(deviceId: string): BLEConnectionState { const connection = this.connectionStates.get(deviceId); return connection?.state || BLEConnectionState.DISCONNECTED; } /** * Get all active connections */ getAllConnections(): Map { return new Map(this.connectionStates); } /** * Add event listener */ addEventListener(listener: BLEEventListener): void { if (!this.eventListeners.includes(listener)) { this.eventListeners.push(listener); } } /** * Remove event listener */ removeEventListener(listener: BLEEventListener): void { const index = this.eventListeners.indexOf(listener); if (index > -1) { this.eventListeners.splice(index, 1); } } /** * Scan for WellNuo sensor devices * In Web Bluetooth, this opens a browser picker dialog */ async scanDevices(): Promise { this.checkBluetoothAvailable(); BLELogger.log('[Web] Starting device scan (browser picker)...'); try { // Request device with name prefix filter const device = await navigator.bluetooth!.requestDevice({ filters: [{ namePrefix: BLE_CONFIG.DEVICE_NAME_PREFIX }], optionalServices: [BLE_CONFIG.SERVICE_UUID], }); if (!device || !device.name) { BLELogger.log('[Web] No device selected'); return []; } // Parse device info from name (WP_497_81a14c) const wellIdMatch = device.name.match(/WP_(\d+)_/); const wellId = wellIdMatch ? parseInt(wellIdMatch[1], 10) : undefined; // Extract partial MAC from name const macMatch = device.name.match(/_([a-fA-F0-9]{6})$/); const mac = macMatch ? macMatch[1].toUpperCase() : ''; const wpDevice: WPDevice = { id: device.id, name: device.name, mac, rssi: -60, // Web Bluetooth doesn't provide RSSI during pairing wellId, }; BLELogger.log(`[Web] Device selected: ${device.name}`); // Store device reference for later connection this.connectedDevices.set(device.id, device); return [wpDevice]; } catch (error: any) { // User cancelled the picker if (error.name === 'NotFoundError' || error.message?.includes('cancelled')) { BLELogger.log('[Web] Device selection cancelled by user'); return []; } // Permission denied if (error.name === 'SecurityError' || error.name === 'NotAllowedError') { throw new BLEError(BLEErrorCode.PERMISSION_DENIED, { message: 'Bluetooth permission denied. Please allow access in your browser settings.', originalError: error, }); } throw parseBLEError(error, { operation: 'scan' }); } } /** * Stop scan - no-op in Web Bluetooth (picker handles this) */ stopScan(): void { // Web Bluetooth doesn't have a continuous scan to stop } /** * Connect to a device by ID */ async connectDevice(deviceId: string): Promise { this.checkBluetoothAvailable(); const startTime = Date.now(); BLELogger.log(`[Web] Connecting to device: ${deviceId}`); try { // Check if connection is already in progress if (this.connectingDevices.has(deviceId)) { throw new BLEError(BLEErrorCode.CONNECTION_IN_PROGRESS, { deviceId }); } // Check if already connected const existingServer = this.gattServers.get(deviceId); if (existingServer?.connected) { BLELogger.log(`[Web] Device already connected: ${deviceId}`); this.updateConnectionState(deviceId, BLEConnectionState.READY); this.emitEvent(deviceId, 'ready'); return true; } // Get device reference const device = this.connectedDevices.get(deviceId); if (!device) { throw new BLEError(BLEErrorCode.DEVICE_NOT_FOUND, { deviceId, message: 'Device not found. Please scan for devices first.', }); } // Mark as connecting this.connectingDevices.add(deviceId); this.updateConnectionState(deviceId, BLEConnectionState.CONNECTING, device.name || undefined); // Set up disconnection handler device.addEventListener('gattserverdisconnected', () => { this.handleDisconnection(deviceId, device.name); }); // Connect to GATT server if (!device.gatt) { throw new BLEError(BLEErrorCode.CONNECTION_FAILED, { deviceId, message: 'Device does not support GATT', }); } const server = await device.gatt.connect(); this.gattServers.set(deviceId, server); this.updateConnectionState(deviceId, BLEConnectionState.CONNECTED, device.name || undefined); // Discover services this.updateConnectionState(deviceId, BLEConnectionState.DISCOVERING, device.name || undefined); const service = await server.getPrimaryService(BLE_CONFIG.SERVICE_UUID); const characteristic = await service.getCharacteristic(BLE_CONFIG.CHAR_UUID); this.characteristics.set(deviceId, characteristic); // Enable notifications await characteristic.startNotifications(); // Ready this.updateConnectionState(deviceId, BLEConnectionState.READY, device.name || undefined); this.emitEvent(deviceId, 'ready'); const duration = Date.now() - startTime; BLELogger.log(`[Web] Device ready: ${device.name || deviceId} (${(duration / 1000).toFixed(1)}s)`); return true; } catch (error: any) { const bleError = isBLEError(error) ? error : parseBLEError(error, { deviceId }); const errorMessage = bleError.userMessage.message; this.updateConnectionState(deviceId, BLEConnectionState.ERROR, undefined, errorMessage); this.emitEvent(deviceId, 'connection_failed', { error: errorMessage, code: bleError.code }); BLELogger.error(`[Web] Connection failed for ${deviceId}`, bleError); return false; } finally { this.connectingDevices.delete(deviceId); } } /** * Handle device disconnection */ private handleDisconnection(deviceId: string, deviceName?: string): void { BLELogger.log(`[Web] Device disconnected: ${deviceName || deviceId}`); // Clean up this.gattServers.delete(deviceId); this.characteristics.delete(deviceId); this.updateConnectionState(deviceId, BLEConnectionState.DISCONNECTED, deviceName); this.emitEvent(deviceId, 'disconnected', { unexpected: true }); // Handle auto-reconnect if enabled if (this.reconnectConfig.enabled) { const state = this.reconnectStates.get(deviceId); if (state && state.attempts < this.reconnectConfig.maxAttempts) { this.scheduleReconnect(deviceId, deviceName || deviceId); } } } /** * Disconnect from a device */ async disconnectDevice(deviceId: string): Promise { BLELogger.log(`[Web] Disconnecting device: ${deviceId}`); this.updateConnectionState(deviceId, BLEConnectionState.DISCONNECTING); // Stop notifications const characteristic = this.characteristics.get(deviceId); if (characteristic) { try { await characteristic.stopNotifications(); } catch { // Ignore errors during cleanup } } // Disconnect GATT const server = this.gattServers.get(deviceId); if (server?.connected) { server.disconnect(); } // Clean up this.gattServers.delete(deviceId); this.characteristics.delete(deviceId); this.updateConnectionState(deviceId, BLEConnectionState.DISCONNECTED); this.emitEvent(deviceId, 'disconnected'); } /** * Check if device is connected */ isDeviceConnected(deviceId: string): boolean { const server = this.gattServers.get(deviceId); return server?.connected || false; } /** * Update communication stats for a device */ private updateCommunicationStats(deviceKey: string, success: boolean, responseTime: number): void { const existing = this.communicationStats.get(deviceKey); const now = Date.now(); if (!existing) { this.communicationStats.set(deviceKey, { successfulCommands: success ? 1 : 0, failedCommands: success ? 0 : 1, averageResponseTime: responseTime, lastSuccessfulCommand: success ? now : 0, lastFailedCommand: success ? undefined : now, }); } else { const totalCommands = existing.successfulCommands + existing.failedCommands; const newAverage = (existing.averageResponseTime * totalCommands + responseTime) / (totalCommands + 1); this.communicationStats.set(deviceKey, { successfulCommands: existing.successfulCommands + (success ? 1 : 0), failedCommands: existing.failedCommands + (success ? 0 : 1), averageResponseTime: newAverage, lastSuccessfulCommand: success ? now : existing.lastSuccessfulCommand, lastFailedCommand: success ? existing.lastFailedCommand : now, }); } } /** * Send a command to a device and wait for response */ async sendCommand(deviceId: string, command: string): Promise { const startTime = Date.now(); const safeCommand = command.length > 20 ? command.substring(0, 20) + '...' : command; BLELogger.log(`[Web] Sending command to ${deviceId}: ${safeCommand}`); const characteristic = this.characteristics.get(deviceId); if (!characteristic) { throw new BLEError(BLEErrorCode.DEVICE_DISCONNECTED, { deviceId }); } return new Promise(async (resolve, reject) => { let responseReceived = false; let timeoutId: ReturnType | null = null; const cleanup = () => { if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } characteristic.removeEventListener('characteristicvaluechanged', handleNotification as EventListener); }; const handleNotification = (event: Event) => { const target = event.target as unknown as WebBluetoothGATTCharacteristic; if (!target.value || responseReceived) return; responseReceived = true; cleanup(); // Decode response const decoder = new TextDecoder('utf-8'); const response = decoder.decode(target.value); // Track successful command const responseTime = Date.now() - startTime; this.updateCommunicationStats(deviceId, true, responseTime); resolve(response); }; try { // Set up notification handler characteristic.addEventListener('characteristicvaluechanged', handleNotification as EventListener); // Send command const encoder = new TextEncoder(); const data = encoder.encode(command); await characteristic.writeValueWithResponse(data); // Set timeout timeoutId = setTimeout(() => { if (!responseReceived) { responseReceived = true; cleanup(); const responseTime = Date.now() - startTime; this.updateCommunicationStats(deviceId, false, responseTime); reject(new BLEError(BLEErrorCode.COMMAND_TIMEOUT, { deviceId, message: `Command timed out after ${BLE_CONFIG.COMMAND_TIMEOUT}ms`, })); } }, BLE_CONFIG.COMMAND_TIMEOUT); } catch (error: any) { cleanup(); const responseTime = Date.now() - startTime; this.updateCommunicationStats(deviceId, false, responseTime); reject(parseBLEError(error, { deviceId, operation: 'command' })); } }); } /** * Get WiFi networks list from sensor */ async getWiFiList(deviceId: string): Promise { BLELogger.log(`[Web] Getting WiFi list from device: ${deviceId}`); // Step 1: Unlock device const unlockResponse = await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK); if (!unlockResponse.includes('ok')) { throw new BLEError(BLEErrorCode.PIN_UNLOCK_FAILED, { deviceId }); } // Step 2: Get WiFi list const listResponse = await this.sendCommand(deviceId, BLE_COMMANDS.GET_WIFI_LIST); // Parse response: "mac,XXXXXX|w|COUNT|SSID1,RSSI1|SSID2,RSSI2|..." const parts = listResponse.split('|'); if (parts.length < 3) { throw new BLEError(BLEErrorCode.INVALID_RESPONSE, { deviceId, message: 'Invalid WiFi list response format', }); } const count = parseInt(parts[2], 10); if (count < 0) { if (count === -1) { throw new BLEError(BLEErrorCode.WIFI_SCAN_IN_PROGRESS, { deviceId }); } if (count === -2) { return []; // No networks found } } // Use Map to deduplicate by SSID const networksMap = new Map(); for (let i = 3; i < parts.length; i++) { const [ssid, rssiStr] = parts[i].split(','); if (ssid && rssiStr) { const trimmedSsid = ssid.trim(); const rssi = parseInt(rssiStr, 10); if (!trimmedSsid) continue; const existing = networksMap.get(trimmedSsid); if (!existing || rssi > existing.rssi) { networksMap.set(trimmedSsid, { ssid: trimmedSsid, rssi }); } } } return Array.from(networksMap.values()).sort((a, b) => b.rssi - a.rssi); } /** * Configure WiFi on sensor */ async setWiFi(deviceId: string, ssid: string, password: string): Promise { BLELogger.log(`[Web] Setting WiFi on device: ${deviceId}, SSID: ${ssid}`); // Validate credentials if (ssid.includes('|') || ssid.includes(',')) { throw new BLEError(BLEErrorCode.WIFI_INVALID_CREDENTIALS, { deviceId, message: 'Network name contains invalid characters', }); } if (password.includes('|')) { throw new BLEError(BLEErrorCode.WIFI_INVALID_CREDENTIALS, { deviceId, message: 'Password contains an invalid character (|)', }); } // Step 1: Unlock device const unlockResponse = await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK); if (!unlockResponse.includes('ok')) { throw new BLEError(BLEErrorCode.PIN_UNLOCK_FAILED, { deviceId }); } // Step 2: Set WiFi credentials const command = `${BLE_COMMANDS.SET_WIFI}|${ssid},${password}`; const setResponse = await this.sendCommand(deviceId, command); if (setResponse.includes('|W|ok')) { BLELogger.log(`[Web] WiFi configured successfully for ${ssid}`); return true; } if (setResponse.includes('|W|fail')) { throw new BLEError(BLEErrorCode.WIFI_PASSWORD_INCORRECT, { deviceId }); } throw new BLEError(BLEErrorCode.WIFI_CONFIG_FAILED, { deviceId, message: `Unexpected response: ${setResponse}`, }); } /** * Get current WiFi status from sensor */ async getCurrentWiFi(deviceId: string): Promise { BLELogger.log(`[Web] Getting current WiFi status from device: ${deviceId}`); // Step 1: Unlock device const unlockResponse = await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK); if (!unlockResponse.includes('ok')) { throw new BLEError(BLEErrorCode.PIN_UNLOCK_FAILED, { deviceId }); } // Step 2: Get current WiFi status const statusResponse = await this.sendCommand(deviceId, BLE_COMMANDS.GET_WIFI_STATUS); // Parse response: "mac,XXXXXX|a|SSID,RSSI" const parts = statusResponse.split('|'); if (parts.length < 3) { return null; } const [ssid, rssiStr] = parts[2].split(','); if (!ssid || ssid.trim() === '') { return null; } return { ssid: ssid.trim(), rssi: parseInt(rssiStr, 10), connected: true, }; } /** * Reboot sensor */ async rebootDevice(deviceId: string): Promise { BLELogger.log(`[Web] Rebooting device: ${deviceId}`); try { await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK); await this.sendCommand(deviceId, BLE_COMMANDS.REBOOT); } catch (error: any) { if (isBLEError(error)) throw error; throw new BLEError(BLEErrorCode.SENSOR_REBOOT_FAILED, { deviceId, originalError: error, }); } // Clean up after reboot this.gattServers.delete(deviceId); this.characteristics.delete(deviceId); } /** * Get sensor health metrics */ async getSensorHealth(wellId: number, mac: string): Promise { // Web Bluetooth requires user interaction for each device scan // Return cached metrics or null const deviceKey = `WP_${wellId}_${mac.slice(-6).toLowerCase()}`; return this.sensorHealthMetrics.get(deviceKey) || null; } /** * Get all cached sensor health metrics */ getAllSensorHealth(): Map { return new Map(this.sensorHealthMetrics); } /** * Cleanup all connections */ async cleanup(): Promise { BLELogger.log('[Web] Cleaning up BLE connections'); // Cancel all reconnect timers this.reconnectTimers.forEach((timer) => { clearTimeout(timer); }); this.reconnectTimers.clear(); this.reconnectStates.clear(); // Disconnect all devices const deviceIds = Array.from(this.gattServers.keys()); for (const deviceId of deviceIds) { try { await this.disconnectDevice(deviceId); } catch { // Continue cleanup } } // Clear all state this.connectedDevices.clear(); this.gattServers.clear(); this.characteristics.clear(); this.connectionStates.clear(); this.connectingDevices.clear(); this.sensorHealthMetrics.clear(); this.communicationStats.clear(); this.eventListeners = []; } /** * Bulk disconnect multiple devices */ async bulkDisconnect(deviceIds: string[]): Promise { const results: BulkOperationResult[] = []; for (const deviceId of deviceIds) { const connection = this.connectionStates.get(deviceId); const deviceName = connection?.deviceName || deviceId; try { await this.disconnectDevice(deviceId); results.push({ deviceId, deviceName, success: true }); } catch (error: any) { results.push({ deviceId, deviceName, success: false, error: error?.message || 'Disconnect failed', }); } } return results; } /** * Bulk reboot multiple devices */ async bulkReboot(deviceIds: string[]): Promise { const results: BulkOperationResult[] = []; for (const deviceId of deviceIds) { const connection = this.connectionStates.get(deviceId); const deviceName = connection?.deviceName || deviceId; try { await this.rebootDevice(deviceId); results.push({ deviceId, deviceName, success: true }); } catch (error: any) { results.push({ deviceId, deviceName, success: false, error: error?.message || 'Reboot failed', }); } } return results; } /** * Bulk WiFi configuration */ async bulkSetWiFi( devices: { id: string; name: string }[], ssid: string, password: string, onProgress?: (deviceId: string, status: 'connecting' | 'configuring' | 'rebooting' | 'success' | 'error', error?: string) => void ): Promise { const results: BulkWiFiResult[] = []; const total = devices.length; const batchStartTime = Date.now(); BLELogger.log(`[Web] Starting bulk WiFi setup for ${total} devices, SSID: ${ssid}`); for (let i = 0; i < devices.length; i++) { const { id: deviceId, name: deviceName } = devices[i]; const index = i + 1; try { // Step 1: Connect BLELogger.logBatchProgress(index, total, deviceName, 'connecting...'); onProgress?.(deviceId, 'connecting'); const connected = await this.connectDevice(deviceId); if (!connected) { throw new BLEError(BLEErrorCode.CONNECTION_FAILED, { deviceId, deviceName }); } // Step 2: Set WiFi BLELogger.logBatchProgress(index, total, deviceName, 'setting WiFi...'); onProgress?.(deviceId, 'configuring'); await this.setWiFi(deviceId, ssid, password); // Step 3: Reboot BLELogger.logBatchProgress(index, total, deviceName, 'rebooting...'); onProgress?.(deviceId, 'rebooting'); await this.rebootDevice(deviceId); // Success BLELogger.logBatchProgress(index, total, deviceName, 'SUCCESS', true); onProgress?.(deviceId, 'success'); results.push({ deviceId, deviceName, success: true }); } catch (error: any) { const bleError = isBLEError(error) ? error : parseBLEError(error, { deviceId, deviceName }); const errorMessage = bleError.userMessage.message; BLELogger.logBatchProgress(index, total, deviceName, `ERROR: ${errorMessage}`, false); onProgress?.(deviceId, 'error', errorMessage); results.push({ deviceId, deviceName, success: false, error: errorMessage }); } } // Log summary const succeeded = results.filter(r => r.success).length; const failed = results.filter(r => !r.success).length; const batchDuration = Date.now() - batchStartTime; BLELogger.logBatchSummary(total, succeeded, failed, batchDuration); return results; } // ==================== RECONNECT FUNCTIONALITY ==================== setReconnectConfig(config: Partial): void { this.reconnectConfig = { ...this.reconnectConfig, ...config }; } getReconnectConfig(): ReconnectConfig { return { ...this.reconnectConfig }; } enableAutoReconnect(deviceId: string, deviceName?: string): void { const device = this.connectedDevices.get(deviceId); this.reconnectStates.set(deviceId, { deviceId, deviceName: deviceName || device?.name || deviceId, attempts: 0, lastAttemptTime: 0, isReconnecting: false, }); } disableAutoReconnect(deviceId: string): void { this.cancelReconnect(deviceId); this.reconnectStates.delete(deviceId); } cancelReconnect(deviceId: string): void { const timer = this.reconnectTimers.get(deviceId); if (timer) { clearTimeout(timer); this.reconnectTimers.delete(deviceId); } const state = this.reconnectStates.get(deviceId); if (state?.isReconnecting) { this.reconnectStates.set(deviceId, { ...state, isReconnecting: false, nextAttemptTime: undefined, }); } } private scheduleReconnect(deviceId: string, deviceName: string): void { const state = this.reconnectStates.get(deviceId); if (!state) return; const delay = Math.min( this.reconnectConfig.delayMs * Math.pow(this.reconnectConfig.backoffMultiplier, state.attempts), this.reconnectConfig.maxDelayMs ); const nextAttemptTime = Date.now() + delay; this.reconnectStates.set(deviceId, { ...state, nextAttemptTime, isReconnecting: true, }); this.emitEvent(deviceId, 'state_changed', { state: BLEConnectionState.CONNECTING, reconnecting: true, nextAttemptIn: delay, }); const timer = setTimeout(() => { this.attemptReconnect(deviceId, deviceName); }, delay); this.reconnectTimers.set(deviceId, timer); } private async attemptReconnect(deviceId: string, deviceName: string): Promise { const state = this.reconnectStates.get(deviceId); if (!state) return; const newAttempts = state.attempts + 1; this.reconnectStates.set(deviceId, { ...state, attempts: newAttempts, lastAttemptTime: Date.now(), isReconnecting: true, }); try { const success = await this.connectDevice(deviceId); if (success) { this.reconnectStates.set(deviceId, { deviceId, deviceName, attempts: 0, lastAttemptTime: Date.now(), isReconnecting: false, }); this.emitEvent(deviceId, 'ready', { reconnected: true }); } else { throw new Error('Connection failed'); } } catch (error: any) { this.reconnectStates.set(deviceId, { ...state, attempts: newAttempts, lastAttemptTime: Date.now(), isReconnecting: newAttempts < this.reconnectConfig.maxAttempts, lastError: error?.message || 'Reconnection failed', }); if (newAttempts < this.reconnectConfig.maxAttempts) { this.scheduleReconnect(deviceId, deviceName); } else { this.updateConnectionState(deviceId, BLEConnectionState.ERROR, deviceName, 'Max reconnection attempts reached'); this.emitEvent(deviceId, 'connection_failed', { error: 'Max reconnection attempts reached', reconnectFailed: true, }); } } } async manualReconnect(deviceId: string): Promise { this.cancelReconnect(deviceId); const state = this.reconnectStates.get(deviceId); const connection = this.connectionStates.get(deviceId); const deviceName = state?.deviceName || connection?.deviceName || deviceId; this.reconnectStates.set(deviceId, { deviceId, deviceName, attempts: 0, lastAttemptTime: Date.now(), isReconnecting: true, }); try { const success = await this.connectDevice(deviceId); this.reconnectStates.set(deviceId, { deviceId, deviceName, attempts: 0, lastAttemptTime: Date.now(), isReconnecting: false, }); return success; } catch (error: any) { this.reconnectStates.set(deviceId, { deviceId, deviceName, attempts: 1, lastAttemptTime: Date.now(), isReconnecting: false, lastError: error?.message || 'Reconnection failed', }); return false; } } getReconnectState(deviceId: string): ReconnectState | undefined { return this.reconnectStates.get(deviceId); } getAllReconnectStates(): Map { return new Map(this.reconnectStates); } }