WellNuo/services/ble/errors.ts
Sergei 6960f248e0 Implement comprehensive BLE error handling system
- Add BLEError class with error codes, severity levels, and recovery actions
- Create error types for connection, permission, communication, WiFi, and sensor errors
- Add user-friendly error messages with localized titles
- Implement BLELogger for consistent logging with batch progress tracking
- Add parseBLEError utility to parse native BLE errors into typed BLEErrors
- Update BLEManager to use new error types with proper logging
- Update MockBLEManager to match error handling behavior for consistency
- Add comprehensive tests for error handling utilities (41 tests passing)

This enables proper error categorization, user-friendly messages, and
recovery suggestions for BLE operations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-01 09:19:38 -08:00

662 lines
20 KiB
TypeScript

// 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<BLEErrorCode, { title: string; message: string }> = {
// 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<BLEErrorCode, BLERecoveryAction[]> = {
// 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)`);
}
}