// BLE Error Types and Error Handling Utilities /** * BLE Error Codes * Categorized by operation type for easier handling */ export enum BLEErrorCode { // Connection errors (100-199) CONNECTION_FAILED = 'BLE_CONNECTION_FAILED', CONNECTION_TIMEOUT = 'BLE_CONNECTION_TIMEOUT', CONNECTION_IN_PROGRESS = 'BLE_CONNECTION_IN_PROGRESS', DEVICE_NOT_FOUND = 'BLE_DEVICE_NOT_FOUND', DEVICE_OUT_OF_RANGE = 'BLE_DEVICE_OUT_OF_RANGE', DEVICE_BUSY = 'BLE_DEVICE_BUSY', ALREADY_CONNECTED = 'BLE_ALREADY_CONNECTED', // Permission errors (200-299) PERMISSION_DENIED = 'BLE_PERMISSION_DENIED', BLUETOOTH_DISABLED = 'BLE_BLUETOOTH_DISABLED', LOCATION_DISABLED = 'BLE_LOCATION_DISABLED', // Communication errors (300-399) COMMAND_FAILED = 'BLE_COMMAND_FAILED', COMMAND_TIMEOUT = 'BLE_COMMAND_TIMEOUT', INVALID_RESPONSE = 'BLE_INVALID_RESPONSE', DEVICE_DISCONNECTED = 'BLE_DEVICE_DISCONNECTED', SERVICE_NOT_FOUND = 'BLE_SERVICE_NOT_FOUND', CHARACTERISTIC_NOT_FOUND = 'BLE_CHARACTERISTIC_NOT_FOUND', // Authentication errors (400-499) PIN_UNLOCK_FAILED = 'BLE_PIN_UNLOCK_FAILED', AUTHENTICATION_FAILED = 'BLE_AUTHENTICATION_FAILED', // WiFi configuration errors (500-599) WIFI_CONFIG_FAILED = 'BLE_WIFI_CONFIG_FAILED', WIFI_PASSWORD_INCORRECT = 'BLE_WIFI_PASSWORD_INCORRECT', WIFI_NETWORK_NOT_FOUND = 'BLE_WIFI_NETWORK_NOT_FOUND', WIFI_SCAN_IN_PROGRESS = 'BLE_WIFI_SCAN_IN_PROGRESS', WIFI_INVALID_CREDENTIALS = 'BLE_WIFI_INVALID_CREDENTIALS', // Sensor errors (600-699) SENSOR_NOT_RESPONDING = 'BLE_SENSOR_NOT_RESPONDING', SENSOR_REBOOT_FAILED = 'BLE_SENSOR_REBOOT_FAILED', SENSOR_ATTACH_FAILED = 'BLE_SENSOR_ATTACH_FAILED', // General errors (900-999) UNKNOWN_ERROR = 'BLE_UNKNOWN_ERROR', OPERATION_CANCELLED = 'BLE_OPERATION_CANCELLED', } /** * Error severity levels for UI display */ export enum BLEErrorSeverity { INFO = 'info', WARNING = 'warning', ERROR = 'error', CRITICAL = 'critical', } /** * Recovery action types */ export enum BLERecoveryAction { RETRY = 'retry', SKIP = 'skip', CANCEL = 'cancel', ENABLE_BLUETOOTH = 'enable_bluetooth', ENABLE_LOCATION = 'enable_location', GRANT_PERMISSIONS = 'grant_permissions', MOVE_CLOSER = 'move_closer', CHECK_SENSOR_POWER = 'check_sensor_power', CHECK_WIFI_PASSWORD = 'check_wifi_password', TRY_DIFFERENT_NETWORK = 'try_different_network', CONTACT_SUPPORT = 'contact_support', } /** * User-friendly error messages */ export const BLE_ERROR_MESSAGES: Record = { // Connection errors [BLEErrorCode.CONNECTION_FAILED]: { title: 'Connection Failed', message: 'Could not connect to the sensor. Make sure it is powered on and try again.', }, [BLEErrorCode.CONNECTION_TIMEOUT]: { title: 'Connection Timeout', message: 'The sensor did not respond in time. Move closer and try again.', }, [BLEErrorCode.CONNECTION_IN_PROGRESS]: { title: 'Connection in Progress', message: 'Already trying to connect to this sensor. Please wait.', }, [BLEErrorCode.DEVICE_NOT_FOUND]: { title: 'Sensor Not Found', message: 'Could not find the sensor. Make sure it is powered on and nearby.', }, [BLEErrorCode.DEVICE_OUT_OF_RANGE]: { title: 'Sensor Out of Range', message: 'The sensor is too far away. Move closer and try again.', }, [BLEErrorCode.DEVICE_BUSY]: { title: 'Sensor Busy', message: 'The sensor is busy. Wait a moment and try again.', }, [BLEErrorCode.ALREADY_CONNECTED]: { title: 'Already Connected', message: 'Already connected to this sensor.', }, // Permission errors [BLEErrorCode.PERMISSION_DENIED]: { title: 'Permission Denied', message: 'Bluetooth permission is required to connect to sensors. Please grant permission in Settings.', }, [BLEErrorCode.BLUETOOTH_DISABLED]: { title: 'Bluetooth Disabled', message: 'Bluetooth is turned off. Please enable Bluetooth in your device settings.', }, [BLEErrorCode.LOCATION_DISABLED]: { title: 'Location Required', message: 'Location access is required for Bluetooth scanning on Android. Please enable location services.', }, // Communication errors [BLEErrorCode.COMMAND_FAILED]: { title: 'Command Failed', message: 'Could not communicate with the sensor. Try reconnecting.', }, [BLEErrorCode.COMMAND_TIMEOUT]: { title: 'No Response', message: 'The sensor did not respond. Make sure it is powered on and nearby.', }, [BLEErrorCode.INVALID_RESPONSE]: { title: 'Invalid Response', message: 'Received an unexpected response from the sensor. Try again.', }, [BLEErrorCode.DEVICE_DISCONNECTED]: { title: 'Disconnected', message: 'Lost connection to the sensor. Try reconnecting.', }, [BLEErrorCode.SERVICE_NOT_FOUND]: { title: 'Sensor Error', message: 'The sensor may need a firmware update. Please contact support.', }, [BLEErrorCode.CHARACTERISTIC_NOT_FOUND]: { title: 'Sensor Error', message: 'The sensor may need a firmware update. Please contact support.', }, // Authentication errors [BLEErrorCode.PIN_UNLOCK_FAILED]: { title: 'Authentication Failed', message: 'Could not unlock the sensor. Try reconnecting.', }, [BLEErrorCode.AUTHENTICATION_FAILED]: { title: 'Authentication Failed', message: 'Sensor authentication failed. Try reconnecting.', }, // WiFi configuration errors [BLEErrorCode.WIFI_CONFIG_FAILED]: { title: 'WiFi Setup Failed', message: 'Could not configure WiFi on the sensor. Check your password and try again.', }, [BLEErrorCode.WIFI_PASSWORD_INCORRECT]: { title: 'Wrong Password', message: 'The WiFi password is incorrect. Please check and try again.', }, [BLEErrorCode.WIFI_NETWORK_NOT_FOUND]: { title: 'Network Not Found', message: 'WiFi network not found. Make sure the sensor is within range of your router.', }, [BLEErrorCode.WIFI_SCAN_IN_PROGRESS]: { title: 'Scanning', message: 'WiFi scan is in progress. Please wait a moment and try again.', }, [BLEErrorCode.WIFI_INVALID_CREDENTIALS]: { title: 'Invalid Credentials', message: 'The network name or password contains invalid characters.', }, // Sensor errors [BLEErrorCode.SENSOR_NOT_RESPONDING]: { title: 'Sensor Not Responding', message: 'The sensor is not responding. Check if it is powered on.', }, [BLEErrorCode.SENSOR_REBOOT_FAILED]: { title: 'Reboot Failed', message: 'Could not reboot the sensor. Try again or power cycle manually.', }, [BLEErrorCode.SENSOR_ATTACH_FAILED]: { title: 'Registration Failed', message: 'Could not register the sensor. Check your internet connection.', }, // General errors [BLEErrorCode.UNKNOWN_ERROR]: { title: 'Error', message: 'An unexpected error occurred. Please try again.', }, [BLEErrorCode.OPERATION_CANCELLED]: { title: 'Cancelled', message: 'Operation was cancelled.', }, }; /** * Map error codes to suggested recovery actions */ export const BLE_RECOVERY_ACTIONS: Record = { // Connection errors [BLEErrorCode.CONNECTION_FAILED]: [BLERecoveryAction.RETRY, BLERecoveryAction.MOVE_CLOSER, BLERecoveryAction.CHECK_SENSOR_POWER], [BLEErrorCode.CONNECTION_TIMEOUT]: [BLERecoveryAction.RETRY, BLERecoveryAction.MOVE_CLOSER], [BLEErrorCode.CONNECTION_IN_PROGRESS]: [], [BLEErrorCode.DEVICE_NOT_FOUND]: [BLERecoveryAction.RETRY, BLERecoveryAction.CHECK_SENSOR_POWER], [BLEErrorCode.DEVICE_OUT_OF_RANGE]: [BLERecoveryAction.MOVE_CLOSER, BLERecoveryAction.RETRY], [BLEErrorCode.DEVICE_BUSY]: [BLERecoveryAction.RETRY], [BLEErrorCode.ALREADY_CONNECTED]: [], // Permission errors [BLEErrorCode.PERMISSION_DENIED]: [BLERecoveryAction.GRANT_PERMISSIONS], [BLEErrorCode.BLUETOOTH_DISABLED]: [BLERecoveryAction.ENABLE_BLUETOOTH], [BLEErrorCode.LOCATION_DISABLED]: [BLERecoveryAction.ENABLE_LOCATION], // Communication errors [BLEErrorCode.COMMAND_FAILED]: [BLERecoveryAction.RETRY, BLERecoveryAction.SKIP], [BLEErrorCode.COMMAND_TIMEOUT]: [BLERecoveryAction.RETRY, BLERecoveryAction.MOVE_CLOSER], [BLEErrorCode.INVALID_RESPONSE]: [BLERecoveryAction.RETRY, BLERecoveryAction.CONTACT_SUPPORT], [BLEErrorCode.DEVICE_DISCONNECTED]: [BLERecoveryAction.RETRY], [BLEErrorCode.SERVICE_NOT_FOUND]: [BLERecoveryAction.CONTACT_SUPPORT], [BLEErrorCode.CHARACTERISTIC_NOT_FOUND]: [BLERecoveryAction.CONTACT_SUPPORT], // Authentication errors [BLEErrorCode.PIN_UNLOCK_FAILED]: [BLERecoveryAction.RETRY, BLERecoveryAction.SKIP], [BLEErrorCode.AUTHENTICATION_FAILED]: [BLERecoveryAction.RETRY, BLERecoveryAction.SKIP], // WiFi configuration errors [BLEErrorCode.WIFI_CONFIG_FAILED]: [BLERecoveryAction.RETRY, BLERecoveryAction.CHECK_WIFI_PASSWORD], [BLEErrorCode.WIFI_PASSWORD_INCORRECT]: [BLERecoveryAction.CHECK_WIFI_PASSWORD, BLERecoveryAction.RETRY], [BLEErrorCode.WIFI_NETWORK_NOT_FOUND]: [BLERecoveryAction.TRY_DIFFERENT_NETWORK, BLERecoveryAction.MOVE_CLOSER], [BLEErrorCode.WIFI_SCAN_IN_PROGRESS]: [BLERecoveryAction.RETRY], [BLEErrorCode.WIFI_INVALID_CREDENTIALS]: [BLERecoveryAction.TRY_DIFFERENT_NETWORK], // Sensor errors [BLEErrorCode.SENSOR_NOT_RESPONDING]: [BLERecoveryAction.CHECK_SENSOR_POWER, BLERecoveryAction.RETRY], [BLEErrorCode.SENSOR_REBOOT_FAILED]: [BLERecoveryAction.RETRY, BLERecoveryAction.SKIP], [BLEErrorCode.SENSOR_ATTACH_FAILED]: [BLERecoveryAction.RETRY, BLERecoveryAction.SKIP], // General errors [BLEErrorCode.UNKNOWN_ERROR]: [BLERecoveryAction.RETRY, BLERecoveryAction.CANCEL], [BLEErrorCode.OPERATION_CANCELLED]: [], }; /** * Custom BLE Error class with rich metadata */ export class BLEError extends Error { public readonly code: BLEErrorCode; public readonly severity: BLEErrorSeverity; public readonly recoveryActions: BLERecoveryAction[]; public readonly userMessage: { title: string; message: string }; public readonly deviceId?: string; public readonly deviceName?: string; public readonly timestamp: number; public readonly originalError?: Error; constructor( code: BLEErrorCode, options?: { message?: string; deviceId?: string; deviceName?: string; originalError?: Error; severity?: BLEErrorSeverity; } ) { const userMessage = BLE_ERROR_MESSAGES[code] || BLE_ERROR_MESSAGES[BLEErrorCode.UNKNOWN_ERROR]; const technicalMessage = options?.message || options?.originalError?.message || userMessage.message; super(technicalMessage); this.name = 'BLEError'; this.code = code; this.severity = options?.severity || getSeverityForErrorCode(code); this.recoveryActions = BLE_RECOVERY_ACTIONS[code] || []; this.userMessage = userMessage; this.deviceId = options?.deviceId; this.deviceName = options?.deviceName; this.timestamp = Date.now(); this.originalError = options?.originalError; // Ensure proper prototype chain Object.setPrototypeOf(this, BLEError.prototype); } /** * Get formatted error for logging */ toLogString(): string { const parts = [ `[BLE] [${this.code}]`, this.deviceName ? `[${this.deviceName}]` : this.deviceId ? `[${this.deviceId}]` : '', this.message, ]; return parts.filter(Boolean).join(' '); } /** * Check if error is retryable */ isRetryable(): boolean { return this.recoveryActions.includes(BLERecoveryAction.RETRY); } /** * Check if error can be skipped (for batch operations) */ isSkippable(): boolean { return this.recoveryActions.includes(BLERecoveryAction.SKIP); } } /** * Determine error severity based on error code */ function getSeverityForErrorCode(code: BLEErrorCode): BLEErrorSeverity { // Critical: Cannot proceed without user action if ([ BLEErrorCode.PERMISSION_DENIED, BLEErrorCode.BLUETOOTH_DISABLED, BLEErrorCode.LOCATION_DISABLED, ].includes(code)) { return BLEErrorSeverity.CRITICAL; } // Error: Operation failed but may be recoverable if ([ BLEErrorCode.CONNECTION_FAILED, BLEErrorCode.CONNECTION_TIMEOUT, BLEErrorCode.WIFI_PASSWORD_INCORRECT, BLEErrorCode.WIFI_CONFIG_FAILED, BLEErrorCode.PIN_UNLOCK_FAILED, BLEErrorCode.SENSOR_NOT_RESPONDING, ].includes(code)) { return BLEErrorSeverity.ERROR; } // Warning: Minor issue, may resolve automatically if ([ BLEErrorCode.DEVICE_BUSY, BLEErrorCode.WIFI_SCAN_IN_PROGRESS, BLEErrorCode.CONNECTION_IN_PROGRESS, ].includes(code)) { return BLEErrorSeverity.WARNING; } // Info: Expected states if ([ BLEErrorCode.ALREADY_CONNECTED, BLEErrorCode.OPERATION_CANCELLED, ].includes(code)) { return BLEErrorSeverity.INFO; } return BLEErrorSeverity.ERROR; } /** * Parse native BLE error and convert to BLEError */ export function parseBLEError( error: unknown, context?: { deviceId?: string; deviceName?: string; operation?: string; } ): BLEError { const originalError = error instanceof Error ? error : new Error(String(error)); const message = originalError.message?.toLowerCase() || ''; // Extract error code from message (format: [CODE] message) const codeMatch = originalError.message?.match(/^\[(\w+)\]/); const extractedCode = codeMatch?.[1]; // Permission/Bluetooth errors if (message.includes('permission') || message.includes('not granted')) { return new BLEError(BLEErrorCode.PERMISSION_DENIED, { ...context, originalError, }); } if (message.includes('bluetooth') && (message.includes('disabled') || message.includes('off'))) { return new BLEError(BLEErrorCode.BLUETOOTH_DISABLED, { ...context, originalError, }); } if (message.includes('location') && (message.includes('disabled') || message.includes('required'))) { return new BLEError(BLEErrorCode.LOCATION_DISABLED, { ...context, originalError, }); } // Connection errors if (message.includes('timeout')) { return new BLEError(BLEErrorCode.CONNECTION_TIMEOUT, { ...context, originalError, }); } if (message.includes('already in progress')) { return new BLEError(BLEErrorCode.CONNECTION_IN_PROGRESS, { ...context, originalError, }); } if (message.includes('not found') && message.includes('device')) { return new BLEError(BLEErrorCode.DEVICE_NOT_FOUND, { ...context, originalError, }); } if (message.includes('disconnect') || message.includes('not connected')) { return new BLEError(BLEErrorCode.DEVICE_DISCONNECTED, { ...context, originalError, }); } // WiFi errors if (message.includes('password') && (message.includes('incorrect') || message.includes('wrong'))) { return new BLEError(BLEErrorCode.WIFI_PASSWORD_INCORRECT, { ...context, originalError, }); } if (message.includes('network') && message.includes('not found')) { return new BLEError(BLEErrorCode.WIFI_NETWORK_NOT_FOUND, { ...context, originalError, }); } if (message.includes('wifi') && message.includes('scan') && message.includes('progress')) { return new BLEError(BLEErrorCode.WIFI_SCAN_IN_PROGRESS, { ...context, originalError, }); } if (message.includes('invalid character')) { return new BLEError(BLEErrorCode.WIFI_INVALID_CREDENTIALS, { ...context, originalError, }); } // PIN/Authentication errors if (message.includes('unlock') || message.includes('pin')) { return new BLEError(BLEErrorCode.PIN_UNLOCK_FAILED, { ...context, originalError, }); } // Sensor not responding if (message.includes('not responding') || message.includes('no response')) { return new BLEError(BLEErrorCode.SENSOR_NOT_RESPONDING, { ...context, originalError, }); } // Cancelled operation (Android errorCode 2) if (message.includes('cancelled') || extractedCode === '2') { return new BLEError(BLEErrorCode.OPERATION_CANCELLED, { ...context, originalError, }); } // Service/Characteristic not found if (message.includes('service') && message.includes('not found')) { return new BLEError(BLEErrorCode.SERVICE_NOT_FOUND, { ...context, originalError, }); } if (message.includes('characteristic') && message.includes('not found')) { return new BLEError(BLEErrorCode.CHARACTERISTIC_NOT_FOUND, { ...context, originalError, }); } // Default to connection failed for unrecognized errors if (message.includes('connect') || message.includes('connection')) { return new BLEError(BLEErrorCode.CONNECTION_FAILED, { ...context, originalError, }); } // Unknown error return new BLEError(BLEErrorCode.UNKNOWN_ERROR, { message: originalError.message, ...context, originalError, }); } /** * Check if an error is a BLEError */ export function isBLEError(error: unknown): error is BLEError { return error instanceof BLEError; } /** * Get user-friendly error info from any error */ export function getErrorInfo(error: unknown): { code: BLEErrorCode; title: string; message: string; severity: BLEErrorSeverity; recoveryActions: BLERecoveryAction[]; isRetryable: boolean; } { if (isBLEError(error)) { return { code: error.code, title: error.userMessage.title, message: error.userMessage.message, severity: error.severity, recoveryActions: error.recoveryActions, isRetryable: error.isRetryable(), }; } const parsedError = parseBLEError(error); return { code: parsedError.code, title: parsedError.userMessage.title, message: parsedError.userMessage.message, severity: parsedError.severity, recoveryActions: parsedError.recoveryActions, isRetryable: parsedError.isRetryable(), }; } /** * Get localized action button text for recovery actions */ export function getRecoveryActionLabel(action: BLERecoveryAction): string { switch (action) { case BLERecoveryAction.RETRY: return 'Retry'; case BLERecoveryAction.SKIP: return 'Skip'; case BLERecoveryAction.CANCEL: return 'Cancel'; case BLERecoveryAction.ENABLE_BLUETOOTH: return 'Enable Bluetooth'; case BLERecoveryAction.ENABLE_LOCATION: return 'Enable Location'; case BLERecoveryAction.GRANT_PERMISSIONS: return 'Grant Permission'; case BLERecoveryAction.MOVE_CLOSER: return 'Move Closer'; case BLERecoveryAction.CHECK_SENSOR_POWER: return 'Check Sensor'; case BLERecoveryAction.CHECK_WIFI_PASSWORD: return 'Check Password'; case BLERecoveryAction.TRY_DIFFERENT_NETWORK: return 'Try Different Network'; case BLERecoveryAction.CONTACT_SUPPORT: return 'Contact Support'; default: return 'OK'; } } /** * BLE Logger for consistent logging format */ export class BLELogger { private static enabled = true; private static prefix = '[BLE]'; static enable(): void { BLELogger.enabled = true; } static disable(): void { BLELogger.enabled = false; } static log(message: string, data?: any): void { if (!BLELogger.enabled) return; console.log(`${BLELogger.prefix} ${message}`, data !== undefined ? data : ''); } static warn(message: string, data?: any): void { if (!BLELogger.enabled) return; console.warn(`${BLELogger.prefix} ${message}`, data !== undefined ? data : ''); } static error(message: string, error?: any): void { if (!BLELogger.enabled) return; if (isBLEError(error)) { console.error(`${BLELogger.prefix} ${error.toLogString()}`); } else { console.error(`${BLELogger.prefix} ${message}`, error !== undefined ? error : ''); } } /** * Log batch operation progress */ static logBatchProgress( index: number, total: number, deviceName: string, step: string, success?: boolean, duration?: number ): void { if (!BLELogger.enabled) return; const status = success === undefined ? '●' : success ? '✓' : '✗'; const durationStr = duration !== undefined ? ` (${(duration / 1000).toFixed(1)}s)` : ''; console.log(`${BLELogger.prefix} [${index}/${total}] ${deviceName} — ${status} ${step}${durationStr}`); } /** * Log batch operation summary */ static logBatchSummary( total: number, succeeded: number, failed: number, duration: number ): void { if (!BLELogger.enabled) return; console.log(`${BLELogger.prefix} Batch complete: ${succeeded}/${total} succeeded, ${failed} failed (${(duration / 1000).toFixed(1)}s)`); } }