- 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>
662 lines
20 KiB
TypeScript
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)`);
|
|
}
|
|
}
|