// Mock BLE Manager для iOS Simulator (Bluetooth недоступен) import { IBLEManager, WPDevice, WiFiNetwork, WiFiStatus, 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'; const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); export class MockBLEManager implements IBLEManager { private connectedDevices = new Set(); private connectionStates = new Map(); private eventListeners: BLEEventListener[] = []; private connectingDevices = new Set(); // Track devices currently being connected // Health monitoring state (mock) private sensorHealthMetrics = new Map(); private communicationStats = new Map(); // Reconnect state (mock) private reconnectConfig: ReconnectConfig = { ...DEFAULT_RECONNECT_CONFIG }; private reconnectStates = new Map(); private mockDevices: WPDevice[] = [ { id: 'mock-743', name: 'WP_497_81a14c', mac: '142B2F81A14C', rssi: -55, wellId: 497, }, { id: 'mock-769', name: 'WP_523_81aad4', mac: '142B2F81AAD4', rssi: -67, wellId: 523, }, ]; /** * 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); } } async scanDevices(): Promise { BLELogger.log('[Mock] Starting device scan...'); await delay(2000); // Simulate scan delay BLELogger.log(`[Mock] Scan complete: found ${this.mockDevices.length} devices`); return this.mockDevices; } stopScan(): void { } async connectDevice(deviceId: string): Promise { BLELogger.log(`[Mock] Connecting to device: ${deviceId}`); try { // Check if connection is already in progress if (this.connectingDevices.has(deviceId)) { const error = new BLEError(BLEErrorCode.CONNECTION_IN_PROGRESS, { deviceId }); BLELogger.warn('[Mock] Connection already in progress', error); throw error; } // Check if already connected if (this.connectedDevices.has(deviceId)) { BLELogger.log(`[Mock] Device already connected: ${deviceId}`); this.updateConnectionState(deviceId, BLEConnectionState.READY); this.emitEvent(deviceId, 'ready'); return true; } // Mark device as connecting this.connectingDevices.add(deviceId); this.updateConnectionState(deviceId, BLEConnectionState.CONNECTING); await delay(500); this.updateConnectionState(deviceId, BLEConnectionState.CONNECTED); await delay(300); this.updateConnectionState(deviceId, BLEConnectionState.DISCOVERING); await delay(200); this.connectedDevices.add(deviceId); this.updateConnectionState(deviceId, BLEConnectionState.READY); this.emitEvent(deviceId, 'ready'); BLELogger.log(`[Mock] Device ready: ${deviceId}`); 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(`[Mock] Connection failed for ${deviceId}`, bleError); return false; } finally { // Always remove from connecting set when done (success or failure) this.connectingDevices.delete(deviceId); } } async disconnectDevice(deviceId: string): Promise { this.updateConnectionState(deviceId, BLEConnectionState.DISCONNECTING); await delay(500); this.connectedDevices.delete(deviceId); this.updateConnectionState(deviceId, BLEConnectionState.DISCONNECTED); this.emitEvent(deviceId, 'disconnected'); } isDeviceConnected(deviceId: string): boolean { return this.connectedDevices.has(deviceId); } async sendCommand(deviceId: string, command: string): Promise { await delay(500); // Simulate responses if (command === 'pin|7856') { return 'pin|ok'; } if (command === 'w') { return 'mac,142b2f81a14c|w|3|FrontierTower,-55|HomeNetwork,-67|TP-Link_5G,-75'; } if (command === 'a') { return 'mac,142b2f81a14c|a|FrontierTower,-67'; } if (command.startsWith('W|')) { return 'mac,142b2f81a14c|W|ok'; } return 'ok'; } async getWiFiList(deviceId: string): Promise { await delay(1500); return [ { ssid: 'FrontierTower', rssi: -55 }, { ssid: 'HomeNetwork', rssi: -67 }, { ssid: 'TP-Link_5G', rssi: -75 }, { ssid: 'Office-WiFi', rssi: -80 }, ]; } async setWiFi( deviceId: string, ssid: string, password: string ): Promise { BLELogger.log(`[Mock] Setting WiFi on device: ${deviceId}, SSID: ${ssid}`); await delay(2000); // Pre-validate credentials (same as real BLEManager) if (ssid.includes('|') || ssid.includes(',')) { const error = new BLEError(BLEErrorCode.WIFI_INVALID_CREDENTIALS, { deviceId, message: 'Network name contains invalid characters', }); BLELogger.error('[Mock] Invalid SSID characters', error); throw error; } if (password.includes('|')) { const error = new BLEError(BLEErrorCode.WIFI_INVALID_CREDENTIALS, { deviceId, message: 'Password contains an invalid character (|)', }); BLELogger.error('[Mock] Invalid password characters', error); throw error; } // Simulate various failure scenarios for testing // Use special SSIDs/passwords to trigger specific errors const lowerSsid = ssid.toLowerCase(); const lowerPassword = password.toLowerCase(); // Simulate wrong password if (lowerPassword === 'wrongpass' || lowerPassword === 'wrong') { const error = new BLEError(BLEErrorCode.WIFI_PASSWORD_INCORRECT, { deviceId }); BLELogger.error('[Mock] Wrong password simulated', error); throw error; } // Simulate network not found if (lowerSsid.includes('notfound') || lowerSsid === 'hidden_network') { const error = new BLEError(BLEErrorCode.WIFI_NETWORK_NOT_FOUND, { deviceId }); BLELogger.error('[Mock] Network not found simulated', error); throw error; } // Simulate timeout if (lowerSsid.includes('timeout') || lowerPassword === 'timeout') { const error = new BLEError(BLEErrorCode.SENSOR_NOT_RESPONDING, { deviceId, message: 'Sensor did not respond to WiFi config', }); BLELogger.error('[Mock] Timeout simulated', error); throw error; } // Simulate sensor not responding if (lowerSsid.includes('offline')) { const error = new BLEError(BLEErrorCode.SENSOR_NOT_RESPONDING, { deviceId }); BLELogger.error('[Mock] Sensor offline simulated', error); throw error; } // Validate password length (WPA/WPA2 requirement) if (password.length < 8) { const error = new BLEError(BLEErrorCode.WIFI_INVALID_CREDENTIALS, { deviceId, message: 'Password must be at least 8 characters for WPA/WPA2 networks', }); BLELogger.error('[Mock] Password too short', error); throw error; } if (password.length > 63) { const error = new BLEError(BLEErrorCode.WIFI_INVALID_CREDENTIALS, { deviceId, message: 'Password cannot exceed 63 characters', }); BLELogger.error('[Mock] Password too long', error); throw error; } // Success for all other cases BLELogger.log(`[Mock] WiFi configured successfully for ${ssid}`); return true; } async getCurrentWiFi(deviceId: string): Promise { await delay(1000); return { ssid: 'FrontierTower', rssi: -67, connected: true, }; } async rebootDevice(deviceId: string): Promise { await delay(500); this.connectedDevices.delete(deviceId); } /** * Get sensor health metrics (mock implementation) */ async getSensorHealth(wellId: number, mac: string): Promise { await delay(1000); const deviceKey = `WP_${wellId}_${mac.slice(-6).toLowerCase()}`; // Generate mock health metrics const now = Date.now(); const mockMetrics: SensorHealthMetrics = { deviceId: `mock-${wellId}`, deviceName: deviceKey, overallHealth: SensorHealthStatus.GOOD, connectionStatus: 'online', lastSeen: new Date(), lastSeenMinutesAgo: 2, wifiSignalQuality: WiFiSignalQuality.GOOD, wifiRssi: -60, wifiSsid: 'FrontierTower', wifiConnected: true, communication: { successfulCommands: 45, failedCommands: 2, averageResponseTime: 350, lastSuccessfulCommand: now - 120000, // 2 minutes ago lastFailedCommand: now - 3600000, // 1 hour ago }, bleRssi: -55, bleConnectionAttempts: 10, bleConnectionFailures: 1, lastHealthCheck: now, lastBleConnection: now - 120000, }; this.sensorHealthMetrics.set(deviceKey, mockMetrics); return mockMetrics; } /** * Get all cached sensor health metrics (mock) */ getAllSensorHealth(): Map { return new Map(this.sensorHealthMetrics); } /** * Cleanup all BLE connections and state * Should be called on app logout to properly release resources */ async cleanup(): Promise { // Disconnect all connected devices const deviceIds = Array.from(this.connectedDevices); for (const deviceId of deviceIds) { await this.disconnectDevice(deviceId); } this.connectedDevices.clear(); this.connectionStates.clear(); this.connectingDevices.clear(); // Clear health monitoring data this.sensorHealthMetrics.clear(); this.communicationStats.clear(); this.eventListeners = []; } /** * Bulk disconnect multiple devices (mock) */ async bulkDisconnect(deviceIds: string[]): Promise { const results: BulkOperationResult[] = []; for (const deviceId of deviceIds) { await delay(100); // Simulate disconnect time const mockDevice = this.mockDevices.find(d => d.id === deviceId); const deviceName = mockDevice?.name || 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 (mock) */ async bulkReboot(deviceIds: string[]): Promise { const results: BulkOperationResult[] = []; for (const deviceId of deviceIds) { await delay(500); // Simulate reboot time const mockDevice = this.mockDevices.find(d => d.id === deviceId); const deviceName = mockDevice?.name || deviceId; try { // Mock reboot success 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 for multiple devices (mock) */ 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(`[Mock] 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 (mock) BLELogger.logBatchProgress(index, total, deviceName, 'connecting...'); onProgress?.(deviceId, 'connecting'); await delay(800); await this.connectDevice(deviceId); BLELogger.logBatchProgress(index, total, deviceName, 'connected', true); // Step 2: Set WiFi (mock) BLELogger.logBatchProgress(index, total, deviceName, 'setting WiFi...'); onProgress?.(deviceId, 'configuring'); await delay(1200); await this.setWiFi(deviceId, ssid, password); BLELogger.logBatchProgress(index, total, deviceName, 'WiFi configured', true); // Step 3: Reboot (mock) BLELogger.logBatchProgress(index, total, deviceName, 'rebooting...'); onProgress?.(deviceId, 'rebooting'); await delay(600); await this.rebootDevice(deviceId); BLELogger.logBatchProgress(index, total, deviceName, 'SUCCESS', true); // Success 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 (Mock) ==================== /** * Set reconnect configuration (mock) */ setReconnectConfig(config: Partial): void { this.reconnectConfig = { ...this.reconnectConfig, ...config }; } /** * Get current reconnect configuration (mock) */ getReconnectConfig(): ReconnectConfig { return { ...this.reconnectConfig }; } /** * Enable auto-reconnect for a device (mock - no-op in simulator) */ enableAutoReconnect(deviceId: string, deviceName?: string): void { const device = this.mockDevices.find(d => d.id === deviceId); this.reconnectStates.set(deviceId, { deviceId, deviceName: deviceName || device?.name || deviceId, attempts: 0, lastAttemptTime: 0, isReconnecting: false, }); } /** * Disable auto-reconnect for a device (mock) */ disableAutoReconnect(deviceId: string): void { this.reconnectStates.delete(deviceId); } /** * Cancel a pending reconnect attempt (mock) */ cancelReconnect(deviceId: string): void { const state = this.reconnectStates.get(deviceId); if (state && state.isReconnecting) { this.reconnectStates.set(deviceId, { ...state, isReconnecting: false, nextAttemptTime: undefined, }); } } /** * Manually trigger a reconnect attempt (mock) */ async manualReconnect(deviceId: string): Promise { const state = this.reconnectStates.get(deviceId); const device = this.mockDevices.find(d => d.id === deviceId); const deviceName = state?.deviceName || device?.name || 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; } } /** * Get reconnect state for a device (mock) */ getReconnectState(deviceId: string): ReconnectState | undefined { return this.reconnectStates.get(deviceId); } /** * Get all reconnect states (mock) */ getAllReconnectStates(): Map { return new Map(this.reconnectStates); } }