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>
This commit is contained in:
Sergei 2026-02-01 09:19:38 -08:00
parent 263cb10b62
commit 6960f248e0
5 changed files with 1410 additions and 67 deletions

View File

@ -0,0 +1,411 @@
import {
BLEError,
BLEErrorCode,
BLEErrorSeverity,
BLERecoveryAction,
BLE_ERROR_MESSAGES,
BLE_RECOVERY_ACTIONS,
parseBLEError,
isBLEError,
getErrorInfo,
getRecoveryActionLabel,
BLELogger,
} from '@/services/ble/errors';
describe('BLEError', () => {
describe('constructor', () => {
it('creates error with correct code and default message', () => {
const error = new BLEError(BLEErrorCode.CONNECTION_FAILED);
expect(error.code).toBe(BLEErrorCode.CONNECTION_FAILED);
expect(error.name).toBe('BLEError');
expect(error.userMessage.title).toBe('Connection Failed');
expect(error.userMessage.message).toContain('Could not connect');
expect(error.timestamp).toBeGreaterThan(0);
});
it('creates error with custom message', () => {
const error = new BLEError(BLEErrorCode.CONNECTION_TIMEOUT, {
message: 'Custom timeout message',
});
expect(error.message).toBe('Custom timeout message');
expect(error.userMessage.title).toBe('Connection Timeout');
});
it('creates error with device context', () => {
const error = new BLEError(BLEErrorCode.DEVICE_NOT_FOUND, {
deviceId: 'device-123',
deviceName: 'WP_497_81a14c',
});
expect(error.deviceId).toBe('device-123');
expect(error.deviceName).toBe('WP_497_81a14c');
});
it('wraps original error', () => {
const originalError = new Error('Native BLE error');
const error = new BLEError(BLEErrorCode.COMMAND_FAILED, {
originalError,
});
expect(error.originalError).toBe(originalError);
});
it('sets correct severity for critical errors', () => {
const permissionError = new BLEError(BLEErrorCode.PERMISSION_DENIED);
const bluetoothError = new BLEError(BLEErrorCode.BLUETOOTH_DISABLED);
expect(permissionError.severity).toBe(BLEErrorSeverity.CRITICAL);
expect(bluetoothError.severity).toBe(BLEErrorSeverity.CRITICAL);
});
it('sets correct severity for warning errors', () => {
const busyError = new BLEError(BLEErrorCode.DEVICE_BUSY);
const scanError = new BLEError(BLEErrorCode.WIFI_SCAN_IN_PROGRESS);
expect(busyError.severity).toBe(BLEErrorSeverity.WARNING);
expect(scanError.severity).toBe(BLEErrorSeverity.WARNING);
});
it('sets recovery actions from mapping', () => {
const connectionError = new BLEError(BLEErrorCode.CONNECTION_FAILED);
const passwordError = new BLEError(BLEErrorCode.WIFI_PASSWORD_INCORRECT);
expect(connectionError.recoveryActions).toContain(BLERecoveryAction.RETRY);
expect(connectionError.recoveryActions).toContain(BLERecoveryAction.MOVE_CLOSER);
expect(passwordError.recoveryActions).toContain(BLERecoveryAction.CHECK_WIFI_PASSWORD);
});
});
describe('toLogString', () => {
it('formats error for logging with device name', () => {
const error = new BLEError(BLEErrorCode.CONNECTION_FAILED, {
deviceName: 'WP_497_81a14c',
message: 'Connection failed',
});
const logString = error.toLogString();
expect(logString).toContain('[BLE]');
expect(logString).toContain('[BLE_CONNECTION_FAILED]');
expect(logString).toContain('[WP_497_81a14c]');
expect(logString).toContain('Connection failed');
});
it('uses deviceId when deviceName is not available', () => {
const error = new BLEError(BLEErrorCode.COMMAND_TIMEOUT, {
deviceId: 'device-123',
});
const logString = error.toLogString();
expect(logString).toContain('[device-123]');
});
});
describe('isRetryable', () => {
it('returns true for retryable errors', () => {
const connectionError = new BLEError(BLEErrorCode.CONNECTION_FAILED);
const timeoutError = new BLEError(BLEErrorCode.COMMAND_TIMEOUT);
expect(connectionError.isRetryable()).toBe(true);
expect(timeoutError.isRetryable()).toBe(true);
});
it('returns false for non-retryable errors', () => {
const cancelledError = new BLEError(BLEErrorCode.OPERATION_CANCELLED);
const alreadyConnectedError = new BLEError(BLEErrorCode.ALREADY_CONNECTED);
expect(cancelledError.isRetryable()).toBe(false);
expect(alreadyConnectedError.isRetryable()).toBe(false);
});
});
describe('isSkippable', () => {
it('returns true for skippable errors in batch operations', () => {
const commandError = new BLEError(BLEErrorCode.COMMAND_FAILED);
const rebootError = new BLEError(BLEErrorCode.SENSOR_REBOOT_FAILED);
expect(commandError.isSkippable()).toBe(true);
expect(rebootError.isSkippable()).toBe(true);
});
it('returns false for non-skippable errors', () => {
const permissionError = new BLEError(BLEErrorCode.PERMISSION_DENIED);
const bluetoothError = new BLEError(BLEErrorCode.BLUETOOTH_DISABLED);
expect(permissionError.isSkippable()).toBe(false);
expect(bluetoothError.isSkippable()).toBe(false);
});
});
});
describe('parseBLEError', () => {
it('parses permission denied errors', () => {
const error = new Error('Bluetooth permissions not granted');
const bleError = parseBLEError(error);
expect(bleError.code).toBe(BLEErrorCode.PERMISSION_DENIED);
});
it('parses bluetooth disabled errors', () => {
const error = new Error('Bluetooth is disabled');
const bleError = parseBLEError(error);
expect(bleError.code).toBe(BLEErrorCode.BLUETOOTH_DISABLED);
});
it('parses timeout errors', () => {
const error = new Error('Connection timeout');
const bleError = parseBLEError(error);
expect(bleError.code).toBe(BLEErrorCode.CONNECTION_TIMEOUT);
});
it('parses connection in progress errors', () => {
const error = new Error('Connection already in progress');
const bleError = parseBLEError(error);
expect(bleError.code).toBe(BLEErrorCode.CONNECTION_IN_PROGRESS);
});
it('parses WiFi password incorrect errors', () => {
const error = new Error('WiFi password is incorrect');
const bleError = parseBLEError(error);
expect(bleError.code).toBe(BLEErrorCode.WIFI_PASSWORD_INCORRECT);
});
it('parses WiFi network not found errors', () => {
const error = new Error('WiFi network not found');
const bleError = parseBLEError(error);
expect(bleError.code).toBe(BLEErrorCode.WIFI_NETWORK_NOT_FOUND);
});
it('parses device disconnected errors', () => {
const error = new Error('Device disconnected unexpectedly');
const bleError = parseBLEError(error);
expect(bleError.code).toBe(BLEErrorCode.DEVICE_DISCONNECTED);
});
it('parses cancelled operation errors', () => {
const error = new Error('Operation was cancelled');
const bleError = parseBLEError(error);
expect(bleError.code).toBe(BLEErrorCode.OPERATION_CANCELLED);
});
it('includes device context when provided', () => {
const error = new Error('Connection timeout');
const bleError = parseBLEError(error, {
deviceId: 'device-123',
deviceName: 'WP_497_81a14c',
});
expect(bleError.deviceId).toBe('device-123');
expect(bleError.deviceName).toBe('WP_497_81a14c');
});
it('defaults to unknown error for unrecognized messages', () => {
const error = new Error('Some random error message');
const bleError = parseBLEError(error);
expect(bleError.code).toBe(BLEErrorCode.UNKNOWN_ERROR);
});
it('handles non-Error objects', () => {
const bleError = parseBLEError('string error');
expect(bleError).toBeInstanceOf(BLEError);
expect(bleError.code).toBe(BLEErrorCode.UNKNOWN_ERROR);
});
});
describe('isBLEError', () => {
it('returns true for BLEError instances', () => {
const error = new BLEError(BLEErrorCode.CONNECTION_FAILED);
expect(isBLEError(error)).toBe(true);
});
it('returns false for regular Error instances', () => {
const error = new Error('Regular error');
expect(isBLEError(error)).toBe(false);
});
it('returns false for non-error objects', () => {
expect(isBLEError('string')).toBe(false);
expect(isBLEError(null)).toBe(false);
expect(isBLEError(undefined)).toBe(false);
expect(isBLEError({})).toBe(false);
});
});
describe('getErrorInfo', () => {
it('extracts info from BLEError', () => {
const error = new BLEError(BLEErrorCode.WIFI_PASSWORD_INCORRECT);
const info = getErrorInfo(error);
expect(info.code).toBe(BLEErrorCode.WIFI_PASSWORD_INCORRECT);
expect(info.title).toBe('Wrong Password');
expect(info.message).toContain('incorrect');
expect(info.severity).toBe(BLEErrorSeverity.ERROR);
expect(info.recoveryActions).toContain(BLERecoveryAction.CHECK_WIFI_PASSWORD);
expect(info.isRetryable).toBe(true);
});
it('parses and extracts info from regular Error', () => {
const error = new Error('Bluetooth is disabled');
const info = getErrorInfo(error);
expect(info.code).toBe(BLEErrorCode.BLUETOOTH_DISABLED);
expect(info.title).toBe('Bluetooth Disabled');
expect(info.severity).toBe(BLEErrorSeverity.CRITICAL);
expect(info.recoveryActions).toContain(BLERecoveryAction.ENABLE_BLUETOOTH);
});
});
describe('getRecoveryActionLabel', () => {
it('returns correct labels for all actions', () => {
expect(getRecoveryActionLabel(BLERecoveryAction.RETRY)).toBe('Retry');
expect(getRecoveryActionLabel(BLERecoveryAction.SKIP)).toBe('Skip');
expect(getRecoveryActionLabel(BLERecoveryAction.CANCEL)).toBe('Cancel');
expect(getRecoveryActionLabel(BLERecoveryAction.ENABLE_BLUETOOTH)).toBe('Enable Bluetooth');
expect(getRecoveryActionLabel(BLERecoveryAction.GRANT_PERMISSIONS)).toBe('Grant Permission');
expect(getRecoveryActionLabel(BLERecoveryAction.MOVE_CLOSER)).toBe('Move Closer');
expect(getRecoveryActionLabel(BLERecoveryAction.CHECK_WIFI_PASSWORD)).toBe('Check Password');
expect(getRecoveryActionLabel(BLERecoveryAction.CONTACT_SUPPORT)).toBe('Contact Support');
});
});
describe('BLE_ERROR_MESSAGES', () => {
it('has messages for all error codes', () => {
const errorCodes = Object.values(BLEErrorCode);
for (const code of errorCodes) {
expect(BLE_ERROR_MESSAGES[code]).toBeDefined();
expect(BLE_ERROR_MESSAGES[code].title).toBeTruthy();
expect(BLE_ERROR_MESSAGES[code].message).toBeTruthy();
}
});
});
describe('BLE_RECOVERY_ACTIONS', () => {
it('has recovery actions for all error codes', () => {
const errorCodes = Object.values(BLEErrorCode);
for (const code of errorCodes) {
expect(BLE_RECOVERY_ACTIONS[code]).toBeDefined();
expect(Array.isArray(BLE_RECOVERY_ACTIONS[code])).toBe(true);
}
});
});
describe('BLELogger', () => {
// Spy on console methods
const consoleSpy = {
log: jest.spyOn(console, 'log').mockImplementation(),
warn: jest.spyOn(console, 'warn').mockImplementation(),
error: jest.spyOn(console, 'error').mockImplementation(),
};
beforeEach(() => {
jest.clearAllMocks();
BLELogger.enable();
});
afterAll(() => {
consoleSpy.log.mockRestore();
consoleSpy.warn.mockRestore();
consoleSpy.error.mockRestore();
});
it('logs messages with BLE prefix', () => {
BLELogger.log('Test message');
expect(consoleSpy.log).toHaveBeenCalledWith('[BLE] Test message', '');
});
it('logs warnings with BLE prefix', () => {
BLELogger.warn('Warning message');
expect(consoleSpy.warn).toHaveBeenCalledWith('[BLE] Warning message', '');
});
it('logs errors with BLE prefix', () => {
BLELogger.error('Error message');
expect(consoleSpy.error).toHaveBeenCalledWith('[BLE] Error message', '');
});
it('logs BLEError with formatted string', () => {
const error = new BLEError(BLEErrorCode.CONNECTION_FAILED, {
deviceName: 'WP_497',
});
BLELogger.error('Failed', error);
expect(consoleSpy.error).toHaveBeenCalled();
const loggedMessage = consoleSpy.error.mock.calls[0][0];
expect(loggedMessage).toContain('[BLE]');
expect(loggedMessage).toContain('[BLE_CONNECTION_FAILED]');
expect(loggedMessage).toContain('[WP_497]');
});
it('does not log when disabled', () => {
BLELogger.disable();
BLELogger.log('Should not appear');
BLELogger.warn('Should not appear');
BLELogger.error('Should not appear');
expect(consoleSpy.log).not.toHaveBeenCalled();
expect(consoleSpy.warn).not.toHaveBeenCalled();
expect(consoleSpy.error).not.toHaveBeenCalled();
});
describe('logBatchProgress', () => {
it('logs batch progress with success indicator', () => {
BLELogger.logBatchProgress(1, 5, 'WP_497', 'connected', true);
expect(consoleSpy.log).toHaveBeenCalled();
const loggedMessage = consoleSpy.log.mock.calls[0][0];
expect(loggedMessage).toContain('[1/5]');
expect(loggedMessage).toContain('WP_497');
expect(loggedMessage).toContain('✓');
expect(loggedMessage).toContain('connected');
});
it('logs batch progress with failure indicator', () => {
BLELogger.logBatchProgress(2, 5, 'WP_498', 'connection failed', false);
expect(consoleSpy.log).toHaveBeenCalled();
const loggedMessage = consoleSpy.log.mock.calls[0][0];
expect(loggedMessage).toContain('[2/5]');
expect(loggedMessage).toContain('✗');
});
it('logs batch progress with duration', () => {
BLELogger.logBatchProgress(3, 5, 'WP_499', 'SUCCESS', true, 8500);
expect(consoleSpy.log).toHaveBeenCalled();
const loggedMessage = consoleSpy.log.mock.calls[0][0];
expect(loggedMessage).toContain('(8.5s)');
});
});
describe('logBatchSummary', () => {
it('logs batch summary with counts', () => {
BLELogger.logBatchSummary(5, 4, 1, 45000);
expect(consoleSpy.log).toHaveBeenCalled();
const loggedMessage = consoleSpy.log.mock.calls[0][0];
expect(loggedMessage).toContain('4/5 succeeded');
expect(loggedMessage).toContain('1 failed');
expect(loggedMessage).toContain('45.0s');
});
});
});

View File

@ -23,6 +23,13 @@ import {
ReconnectState, ReconnectState,
DEFAULT_RECONNECT_CONFIG, DEFAULT_RECONNECT_CONFIG,
} from './types'; } from './types';
import {
BLEError,
BLEErrorCode,
BLELogger,
parseBLEError,
isBLEError,
} from './errors';
import { requestBLEPermissions, checkBluetoothEnabled } from './permissions'; import { requestBLEPermissions, checkBluetoothEnabled } from './permissions';
import base64 from 'react-native-base64'; import base64 from 'react-native-base64';
@ -127,16 +134,27 @@ export class RealBLEManager implements IBLEManager {
} }
async scanDevices(): Promise<WPDevice[]> { async scanDevices(): Promise<WPDevice[]> {
BLELogger.log('Starting device scan...');
const startTime = Date.now();
// Check permissions with graceful fallback // Check permissions with graceful fallback
const permissionStatus = await requestBLEPermissions(); const permissionStatus = await requestBLEPermissions();
if (!permissionStatus.granted) { if (!permissionStatus.granted) {
throw new Error(permissionStatus.error || 'Bluetooth permissions not granted'); const error = new BLEError(BLEErrorCode.PERMISSION_DENIED, {
message: permissionStatus.error,
});
BLELogger.error('Scan failed: permission denied', error);
throw error;
} }
// Check Bluetooth state // Check Bluetooth state
const bluetoothStatus = await checkBluetoothEnabled(this.manager); const bluetoothStatus = await checkBluetoothEnabled(this.manager);
if (!bluetoothStatus.enabled) { if (!bluetoothStatus.enabled) {
throw new Error(bluetoothStatus.error || 'Bluetooth is disabled. Please enable it in settings.'); const error = new BLEError(BLEErrorCode.BLUETOOTH_DISABLED, {
message: bluetoothStatus.error,
});
BLELogger.error('Scan failed: Bluetooth disabled', error);
throw error;
} }
const foundDevices = new Map<string, WPDevice>(); const foundDevices = new Map<string, WPDevice>();
@ -150,7 +168,9 @@ export class RealBLEManager implements IBLEManager {
(error, device) => { (error, device) => {
if (error) { if (error) {
this.scanning = false; this.scanning = false;
reject(error); const bleError = parseBLEError(error, { operation: 'scan' });
BLELogger.error('Scan error', bleError);
reject(bleError);
return; return;
} }
@ -170,6 +190,8 @@ export class RealBLEManager implements IBLEManager {
rssi: device.rssi || -100, rssi: device.rssi || -100,
wellId, wellId,
}); });
BLELogger.log(`Found device: ${device.name} (RSSI: ${device.rssi})`);
} }
} }
); );
@ -177,7 +199,10 @@ export class RealBLEManager implements IBLEManager {
// Stop scan after timeout // Stop scan after timeout
setTimeout(() => { setTimeout(() => {
this.stopScan(); this.stopScan();
resolve(Array.from(foundDevices.values())); const duration = Date.now() - startTime;
const devices = Array.from(foundDevices.values());
BLELogger.log(`Scan complete: found ${devices.length} devices (${(duration / 1000).toFixed(1)}s)`);
resolve(devices);
}, BLE_CONFIG.SCAN_TIMEOUT); }, BLE_CONFIG.SCAN_TIMEOUT);
}); });
} }
@ -190,10 +215,15 @@ export class RealBLEManager implements IBLEManager {
} }
async connectDevice(deviceId: string): Promise<boolean> { async connectDevice(deviceId: string): Promise<boolean> {
const startTime = Date.now();
BLELogger.log(`Connecting to device: ${deviceId}`);
try { try {
// Check if connection is already in progress // Check if connection is already in progress
if (this.connectingDevices.has(deviceId)) { if (this.connectingDevices.has(deviceId)) {
throw new Error('Connection already in progress for this device'); const error = new BLEError(BLEErrorCode.CONNECTION_IN_PROGRESS, { deviceId });
BLELogger.warn('Connection already in progress', error);
throw error;
} }
// Check if already connected // Check if already connected
@ -201,6 +231,7 @@ export class RealBLEManager implements IBLEManager {
if (existingDevice) { if (existingDevice) {
const isConnected = await existingDevice.isConnected(); const isConnected = await existingDevice.isConnected();
if (isConnected) { if (isConnected) {
BLELogger.log(`Device already connected: ${deviceId}`);
this.updateConnectionState(deviceId, BLEConnectionState.READY, existingDevice.name || undefined); this.updateConnectionState(deviceId, BLEConnectionState.READY, existingDevice.name || undefined);
this.emitEvent(deviceId, 'ready'); this.emitEvent(deviceId, 'ready');
return true; return true;
@ -219,19 +250,27 @@ export class RealBLEManager implements IBLEManager {
// Step 0: Check permissions (required for Android 12+) // Step 0: Check permissions (required for Android 12+)
const permissionStatus = await requestBLEPermissions(); const permissionStatus = await requestBLEPermissions();
if (!permissionStatus.granted) { if (!permissionStatus.granted) {
const error = permissionStatus.error || 'Bluetooth permissions not granted'; const error = new BLEError(BLEErrorCode.PERMISSION_DENIED, {
this.updateConnectionState(deviceId, BLEConnectionState.ERROR, undefined, error); deviceId,
this.emitEvent(deviceId, 'connection_failed', { error }); message: permissionStatus.error,
throw new Error(error); });
this.updateConnectionState(deviceId, BLEConnectionState.ERROR, undefined, error.userMessage.message);
this.emitEvent(deviceId, 'connection_failed', { error: error.message, code: error.code });
BLELogger.error('Connection failed: permission denied', error);
throw error;
} }
// Step 0.5: Check Bluetooth is enabled // Step 0.5: Check Bluetooth is enabled
const bluetoothStatus = await checkBluetoothEnabled(this.manager); const bluetoothStatus = await checkBluetoothEnabled(this.manager);
if (!bluetoothStatus.enabled) { if (!bluetoothStatus.enabled) {
const error = bluetoothStatus.error || 'Bluetooth is disabled. Please enable it in settings.'; const error = new BLEError(BLEErrorCode.BLUETOOTH_DISABLED, {
this.updateConnectionState(deviceId, BLEConnectionState.ERROR, undefined, error); deviceId,
this.emitEvent(deviceId, 'connection_failed', { error }); message: bluetoothStatus.error,
throw new Error(error); });
this.updateConnectionState(deviceId, BLEConnectionState.ERROR, undefined, error.userMessage.message);
this.emitEvent(deviceId, 'connection_failed', { error: error.message, code: error.code });
BLELogger.error('Connection failed: Bluetooth disabled', error);
throw error;
} }
const device = await this.manager.connectToDevice(deviceId, { const device = await this.manager.connectToDevice(deviceId, {
@ -240,17 +279,21 @@ export class RealBLEManager implements IBLEManager {
// Update state to CONNECTED // Update state to CONNECTED
this.updateConnectionState(deviceId, BLEConnectionState.CONNECTED, device.name || undefined); this.updateConnectionState(deviceId, BLEConnectionState.CONNECTED, device.name || undefined);
BLELogger.log(`Connected to device: ${device.name || deviceId}`);
// Update state to DISCOVERING // Update state to DISCOVERING
this.updateConnectionState(deviceId, BLEConnectionState.DISCOVERING, device.name || undefined); this.updateConnectionState(deviceId, BLEConnectionState.DISCOVERING, device.name || undefined);
await device.discoverAllServicesAndCharacteristics(); await device.discoverAllServicesAndCharacteristics();
BLELogger.log(`Services discovered for: ${device.name || deviceId}`);
// Request larger MTU for Android (default is 23 bytes which is too small) // Request larger MTU for Android (default is 23 bytes which is too small)
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
try { try {
await device.requestMTU(512); await device.requestMTU(512);
BLELogger.log('MTU increased to 512');
} catch { } catch {
// MTU request may fail on some devices - continue anyway // MTU request may fail on some devices - continue anyway
BLELogger.warn('MTU request failed, continuing with default');
} }
} }
@ -260,11 +303,19 @@ export class RealBLEManager implements IBLEManager {
this.updateConnectionState(deviceId, BLEConnectionState.READY, device.name || undefined); this.updateConnectionState(deviceId, BLEConnectionState.READY, device.name || undefined);
this.emitEvent(deviceId, 'ready'); this.emitEvent(deviceId, 'ready');
const duration = Date.now() - startTime;
BLELogger.log(`Device ready: ${device.name || deviceId} (${(duration / 1000).toFixed(1)}s)`);
return true; return true;
} catch (error: any) { } catch (error: any) {
const errorMessage = error?.message || 'Connection failed'; // Parse error if not already a BLEError
const bleError = isBLEError(error) ? error : parseBLEError(error, { deviceId });
const errorMessage = bleError.userMessage.message;
this.updateConnectionState(deviceId, BLEConnectionState.ERROR, undefined, errorMessage); this.updateConnectionState(deviceId, BLEConnectionState.ERROR, undefined, errorMessage);
this.emitEvent(deviceId, 'connection_failed', { error: errorMessage }); this.emitEvent(deviceId, 'connection_failed', { error: errorMessage, code: bleError.code });
BLELogger.error(`Connection failed for ${deviceId}`, bleError);
return false; return false;
} finally { } finally {
// Always remove from connecting set when done (success or failure) // Always remove from connecting set when done (success or failure)
@ -336,10 +387,15 @@ export class RealBLEManager implements IBLEManager {
async sendCommand(deviceId: string, command: string): Promise<string> { async sendCommand(deviceId: string, command: string): Promise<string> {
const startTime = Date.now(); const startTime = Date.now();
// Only log first 20 chars to avoid logging passwords
const safeCommand = command.length > 20 ? command.substring(0, 20) + '...' : command;
BLELogger.log(`Sending command to ${deviceId}: ${safeCommand}`);
const device = this.connectedDevices.get(deviceId); const device = this.connectedDevices.get(deviceId);
if (!device) { if (!device) {
throw new Error('Device not connected'); const error = new BLEError(BLEErrorCode.DEVICE_DISCONNECTED, { deviceId });
BLELogger.error('Command failed: device not connected', error);
throw error;
} }
// Verify device is still connected // Verify device is still connected
@ -347,10 +403,18 @@ export class RealBLEManager implements IBLEManager {
const isConnected = await device.isConnected(); const isConnected = await device.isConnected();
if (!isConnected) { if (!isConnected) {
this.connectedDevices.delete(deviceId); this.connectedDevices.delete(deviceId);
throw new Error('Device disconnected'); const error = new BLEError(BLEErrorCode.DEVICE_DISCONNECTED, { deviceId });
BLELogger.error('Command failed: device disconnected', error);
throw error;
} }
} catch { } catch (err) {
throw new Error('Failed to verify connection'); if (isBLEError(err)) throw err;
const error = new BLEError(BLEErrorCode.DEVICE_DISCONNECTED, {
deviceId,
originalError: err instanceof Error ? err : undefined,
});
BLELogger.error('Command failed: connection verification failed', error);
throw error;
} }
// Generate unique transaction ID to prevent Android null pointer issues // Generate unique transaction ID to prevent Android null pointer issues
@ -398,10 +462,15 @@ export class RealBLEManager implements IBLEManager {
const deviceKey = `${deviceId}`; const deviceKey = `${deviceId}`;
this.updateCommunicationStats(deviceKey, false, responseTime); this.updateCommunicationStats(deviceKey, false, responseTime);
// Ensure error has a valid message (fixes Android NullPointerException) // Parse and wrap error with proper BLEError
const errorMessage = error?.message || error?.reason || 'BLE operation failed'; const bleError = parseBLEError(error, {
deviceId,
deviceName: device.name || undefined,
operation: 'command',
});
reject(new Error(`[${errorCode}] ${errorMessage}`)); BLELogger.error(`Command failed for ${deviceId}`, bleError);
reject(bleError);
}; };
try { try {
@ -455,7 +524,12 @@ export class RealBLEManager implements IBLEManager {
// Timeout // Timeout
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
if (!responseReceived) { if (!responseReceived) {
safeReject(new Error('Command timeout')); const timeoutError = new BLEError(BLEErrorCode.COMMAND_TIMEOUT, {
deviceId,
deviceName: device.name || undefined,
message: `Command timed out after ${BLE_CONFIG.COMMAND_TIMEOUT}ms`,
});
safeReject(timeoutError);
} }
}, BLE_CONFIG.COMMAND_TIMEOUT); }, BLE_CONFIG.COMMAND_TIMEOUT);
} catch (error: any) { } catch (error: any) {
@ -465,11 +539,23 @@ export class RealBLEManager implements IBLEManager {
} }
async getWiFiList(deviceId: string): Promise<WiFiNetwork[]> { async getWiFiList(deviceId: string): Promise<WiFiNetwork[]> {
BLELogger.log(`Getting WiFi list from device: ${deviceId}`);
// Step 1: Unlock device // Step 1: Unlock device
const unlockResponse = await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK); try {
if (!unlockResponse.includes('ok')) { const unlockResponse = await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK);
throw new Error('Failed to unlock device'); if (!unlockResponse.includes('ok')) {
throw new BLEError(BLEErrorCode.PIN_UNLOCK_FAILED, { deviceId });
}
BLELogger.log('Device unlocked successfully');
} catch (err) {
if (isBLEError(err)) throw err;
const error = new BLEError(BLEErrorCode.PIN_UNLOCK_FAILED, {
deviceId,
originalError: err instanceof Error ? err : undefined,
});
BLELogger.error('Failed to unlock device', error);
throw error;
} }
// Step 2: Get WiFi list // Step 2: Get WiFi list
@ -478,15 +564,23 @@ export class RealBLEManager implements IBLEManager {
// Parse response: "mac,XXXXXX|w|COUNT|SSID1,RSSI1|SSID2,RSSI2|..." // Parse response: "mac,XXXXXX|w|COUNT|SSID1,RSSI1|SSID2,RSSI2|..."
const parts = listResponse.split('|'); const parts = listResponse.split('|');
if (parts.length < 3) { if (parts.length < 3) {
throw new Error('Invalid WiFi list response'); const error = new BLEError(BLEErrorCode.INVALID_RESPONSE, {
deviceId,
message: 'Invalid WiFi list response format',
});
BLELogger.error('Invalid response', error);
throw error;
} }
const count = parseInt(parts[2], 10); const count = parseInt(parts[2], 10);
if (count < 0) { if (count < 0) {
if (count === -1) { if (count === -1) {
throw new Error('WiFi scan in progress, please wait'); const error = new BLEError(BLEErrorCode.WIFI_SCAN_IN_PROGRESS, { deviceId });
BLELogger.warn('WiFi scan in progress', error);
throw error;
} }
if (count === -2) { if (count === -2) {
BLELogger.log('No WiFi networks found');
return []; // No networks found return []; // No networks found
} }
} }
@ -518,13 +612,25 @@ export class RealBLEManager implements IBLEManager {
} }
async setWiFi(deviceId: string, ssid: string, password: string): Promise<boolean> { async setWiFi(deviceId: string, ssid: string, password: string): Promise<boolean> {
BLELogger.log(`Setting WiFi on device: ${deviceId}, SSID: ${ssid}`);
// Pre-validate credentials before BLE transmission // Pre-validate credentials before BLE transmission
// Check for characters that would break BLE protocol parsing // Check for characters that would break BLE protocol parsing
if (ssid.includes('|') || ssid.includes(',')) { if (ssid.includes('|') || ssid.includes(',')) {
throw new Error('Network name contains invalid characters. Please select a different network.'); const error = new BLEError(BLEErrorCode.WIFI_INVALID_CREDENTIALS, {
deviceId,
message: 'Network name contains invalid characters',
});
BLELogger.error('Invalid SSID characters', error);
throw error;
} }
if (password.includes('|')) { if (password.includes('|')) {
throw new Error('Password contains an invalid character (|). Please use a different password.'); const error = new BLEError(BLEErrorCode.WIFI_INVALID_CREDENTIALS, {
deviceId,
message: 'Password contains an invalid character (|)',
});
BLELogger.error('Invalid password characters', error);
throw error;
} }
// Step 1: Unlock device // Step 1: Unlock device
@ -532,15 +638,25 @@ export class RealBLEManager implements IBLEManager {
try { try {
unlockResponse = await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK); unlockResponse = await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK);
} catch (err: any) { } catch (err: any) {
if (err.message?.includes('timeout')) { if (isBLEError(err) && err.code === BLEErrorCode.COMMAND_TIMEOUT) {
throw new Error('Sensor not responding. Please move closer and try again.'); const error = new BLEError(BLEErrorCode.SENSOR_NOT_RESPONDING, {
deviceId,
originalError: err,
});
BLELogger.error('Sensor not responding during unlock', error);
throw error;
} }
throw new Error(`Cannot communicate with sensor: ${err.message}`); const error = parseBLEError(err, { deviceId, operation: 'unlock' });
BLELogger.error('Unlock failed', error);
throw error;
} }
if (!unlockResponse.includes('ok')) { if (!unlockResponse.includes('ok')) {
throw new Error('Sensor authentication failed. Please try reconnecting.'); const error = new BLEError(BLEErrorCode.PIN_UNLOCK_FAILED, { deviceId });
BLELogger.error('PIN unlock rejected', error);
throw error;
} }
BLELogger.log('Device unlocked for WiFi configuration');
// Step 1.5: Check if already connected to the target WiFi // Step 1.5: Check if already connected to the target WiFi
// This prevents "W|fail" when sensor uses old saved credentials // This prevents "W|fail" when sensor uses old saved credentials
@ -565,14 +681,23 @@ export class RealBLEManager implements IBLEManager {
try { try {
setResponse = await this.sendCommand(deviceId, command); setResponse = await this.sendCommand(deviceId, command);
} catch (err: any) { } catch (err: any) {
if (err.message?.includes('timeout')) { if (isBLEError(err) && err.code === BLEErrorCode.COMMAND_TIMEOUT) {
throw new Error('Sensor did not respond to WiFi config. Please try again.'); const error = new BLEError(BLEErrorCode.SENSOR_NOT_RESPONDING, {
deviceId,
message: 'Sensor did not respond to WiFi config',
originalError: err,
});
BLELogger.error('WiFi config timeout', error);
throw error;
} }
throw new Error(`WiFi configuration failed: ${err.message}`); const error = parseBLEError(err, { deviceId, operation: 'wifi_config' });
BLELogger.error('WiFi config failed', error);
throw error;
} }
// Parse response: "mac,XXXXXX|W|ok" or "mac,XXXXXX|W|fail" or other errors // Parse response: "mac,XXXXXX|W|ok" or "mac,XXXXXX|W|fail" or other errors
if (setResponse.includes('|W|ok')) { if (setResponse.includes('|W|ok')) {
BLELogger.log(`WiFi configured successfully for ${ssid}`);
return true; return true;
} }
@ -587,6 +712,7 @@ export class RealBLEManager implements IBLEManager {
// If connected to target SSID (using old credentials), consider it success // If connected to target SSID (using old credentials), consider it success
if (currentSsid && currentSsid.trim().toLowerCase() === ssid.toLowerCase() && rssi < 0) { if (currentSsid && currentSsid.trim().toLowerCase() === ssid.toLowerCase() && rssi < 0) {
BLELogger.log(`Sensor already connected to ${ssid} (using existing credentials)`);
return true; return true;
} }
} }
@ -595,27 +721,53 @@ export class RealBLEManager implements IBLEManager {
} }
// Password was definitely wrong // Password was definitely wrong
throw new Error('WiFi password is incorrect. Please check and try again.'); const error = new BLEError(BLEErrorCode.WIFI_PASSWORD_INCORRECT, { deviceId });
BLELogger.error('WiFi password incorrect', error);
throw error;
} }
if (setResponse.includes('timeout') || setResponse.includes('Timeout')) { if (setResponse.includes('timeout') || setResponse.includes('Timeout')) {
throw new Error('Sensor did not respond to WiFi config. Please try again.'); const error = new BLEError(BLEErrorCode.SENSOR_NOT_RESPONDING, {
deviceId,
message: 'Sensor did not respond to WiFi config',
});
BLELogger.error('WiFi config timeout', error);
throw error;
} }
// Check for specific error patterns // Check for specific error patterns
if (setResponse.includes('not found') || setResponse.includes('no network')) { if (setResponse.includes('not found') || setResponse.includes('no network')) {
throw new Error('WiFi network not found. Make sure the sensor is within range of your router.'); const error = new BLEError(BLEErrorCode.WIFI_NETWORK_NOT_FOUND, { deviceId });
BLELogger.error('WiFi network not found', error);
throw error;
} }
// Unknown error - provide helpful message // Unknown error - provide helpful message
throw new Error('WiFi configuration failed. Please try again or contact support.'); const error = new BLEError(BLEErrorCode.WIFI_CONFIG_FAILED, {
deviceId,
message: `Unexpected response: ${setResponse}`,
});
BLELogger.error('WiFi config failed with unknown error', error);
throw error;
} }
async getCurrentWiFi(deviceId: string): Promise<WiFiStatus | null> { async getCurrentWiFi(deviceId: string): Promise<WiFiStatus | null> {
BLELogger.log(`Getting current WiFi status from device: ${deviceId}`);
// Step 1: Unlock device // Step 1: Unlock device
const unlockResponse = await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK); try {
if (!unlockResponse.includes('ok')) { const unlockResponse = await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK);
throw new Error('Failed to unlock device'); if (!unlockResponse.includes('ok')) {
throw new BLEError(BLEErrorCode.PIN_UNLOCK_FAILED, { deviceId });
}
} catch (err) {
if (isBLEError(err)) throw err;
const error = new BLEError(BLEErrorCode.PIN_UNLOCK_FAILED, {
deviceId,
originalError: err instanceof Error ? err : undefined,
});
BLELogger.error('Failed to unlock device for WiFi status', error);
throw error;
} }
// Step 2: Get current WiFi status // Step 2: Get current WiFi status
@ -624,27 +776,49 @@ export class RealBLEManager implements IBLEManager {
// Parse response: "mac,XXXXXX|a|SSID,RSSI" or "mac,XXXXXX|a|,0" (not connected) // Parse response: "mac,XXXXXX|a|SSID,RSSI" or "mac,XXXXXX|a|,0" (not connected)
const parts = statusResponse.split('|'); const parts = statusResponse.split('|');
if (parts.length < 3) { if (parts.length < 3) {
BLELogger.log('No WiFi status available (invalid response)');
return null; return null;
} }
const [ssid, rssiStr] = parts[2].split(','); const [ssid, rssiStr] = parts[2].split(',');
if (!ssid || ssid.trim() === '') { if (!ssid || ssid.trim() === '') {
BLELogger.log('Sensor not connected to any WiFi network');
return null; // Not connected return null; // Not connected
} }
const rssi = parseInt(rssiStr, 10);
BLELogger.log(`Current WiFi: ${ssid} (RSSI: ${rssi})`);
return { return {
ssid: ssid.trim(), ssid: ssid.trim(),
rssi: parseInt(rssiStr, 10), rssi: rssi,
connected: true, connected: true,
}; };
} }
async rebootDevice(deviceId: string): Promise<void> { async rebootDevice(deviceId: string): Promise<void> {
// Step 1: Unlock device BLELogger.log(`Rebooting device: ${deviceId}`);
await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK);
// Step 2: Reboot (device will disconnect) try {
await this.sendCommand(deviceId, BLE_COMMANDS.REBOOT); // Step 1: Unlock device
await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK);
// Step 2: Reboot (device will disconnect)
await this.sendCommand(deviceId, BLE_COMMANDS.REBOOT);
BLELogger.log(`Reboot command sent to ${deviceId}`);
} catch (err) {
if (isBLEError(err)) {
BLELogger.error('Reboot failed', err);
throw err;
}
const error = new BLEError(BLEErrorCode.SENSOR_REBOOT_FAILED, {
deviceId,
originalError: err instanceof Error ? err : undefined,
});
BLELogger.error('Reboot failed', error);
throw error;
}
// Remove from connected devices // Remove from connected devices
this.connectedDevices.delete(deviceId); this.connectedDevices.delete(deviceId);
@ -930,27 +1104,41 @@ export class RealBLEManager implements IBLEManager {
onProgress?: (deviceId: string, status: 'connecting' | 'configuring' | 'rebooting' | 'success' | 'error', error?: string) => void onProgress?: (deviceId: string, status: 'connecting' | 'configuring' | 'rebooting' | 'success' | 'error', error?: string) => void
): Promise<BulkWiFiResult[]> { ): Promise<BulkWiFiResult[]> {
const results: BulkWiFiResult[] = []; const results: BulkWiFiResult[] = [];
const total = devices.length;
const batchStartTime = Date.now();
for (const device of devices) { BLELogger.log(`Starting bulk WiFi setup for ${total} devices, SSID: ${ssid}`);
const { id: deviceId, name: deviceName } = device;
for (let i = 0; i < devices.length; i++) {
const { id: deviceId, name: deviceName } = devices[i];
const deviceStartTime = Date.now();
const index = i + 1;
try { try {
// Step 1: Connect // Step 1: Connect
BLELogger.logBatchProgress(index, total, deviceName, 'connecting...');
onProgress?.(deviceId, 'connecting'); onProgress?.(deviceId, 'connecting');
const connected = await this.connectDevice(deviceId); const connected = await this.connectDevice(deviceId);
if (!connected) { if (!connected) {
throw new Error('Could not connect to device'); throw new BLEError(BLEErrorCode.CONNECTION_FAILED, { deviceId, deviceName });
} }
BLELogger.logBatchProgress(index, total, deviceName, 'connected', true);
// Step 2: Set WiFi // Step 2: Set WiFi
BLELogger.logBatchProgress(index, total, deviceName, 'setting WiFi...');
onProgress?.(deviceId, 'configuring'); onProgress?.(deviceId, 'configuring');
await this.setWiFi(deviceId, ssid, password); await this.setWiFi(deviceId, ssid, password);
BLELogger.logBatchProgress(index, total, deviceName, 'WiFi configured', true);
// Step 3: Reboot // Step 3: Reboot
BLELogger.logBatchProgress(index, total, deviceName, 'rebooting...');
onProgress?.(deviceId, 'rebooting'); onProgress?.(deviceId, 'rebooting');
await this.rebootDevice(deviceId); await this.rebootDevice(deviceId);
BLELogger.logBatchProgress(index, total, deviceName, 'rebooted', true);
// Success // Success
const duration = Date.now() - deviceStartTime;
BLELogger.logBatchProgress(index, total, deviceName, 'SUCCESS', true, duration);
onProgress?.(deviceId, 'success'); onProgress?.(deviceId, 'success');
results.push({ results.push({
deviceId, deviceId,
@ -958,7 +1146,10 @@ export class RealBLEManager implements IBLEManager {
success: true, success: true,
}); });
} catch (error: any) { } catch (error: any) {
const errorMessage = error?.message || 'WiFi configuration failed'; 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); onProgress?.(deviceId, 'error', errorMessage);
results.push({ results.push({
deviceId, deviceId,
@ -969,6 +1160,12 @@ export class RealBLEManager implements IBLEManager {
} }
} }
// 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; return results;
} }

View File

@ -19,6 +19,13 @@ import {
ReconnectState, ReconnectState,
DEFAULT_RECONNECT_CONFIG, DEFAULT_RECONNECT_CONFIG,
} from './types'; } from './types';
import {
BLEError,
BLEErrorCode,
BLELogger,
isBLEError,
parseBLEError,
} from './errors';
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
@ -126,7 +133,9 @@ export class MockBLEManager implements IBLEManager {
} }
async scanDevices(): Promise<WPDevice[]> { async scanDevices(): Promise<WPDevice[]> {
BLELogger.log('[Mock] Starting device scan...');
await delay(2000); // Simulate scan delay await delay(2000); // Simulate scan delay
BLELogger.log(`[Mock] Scan complete: found ${this.mockDevices.length} devices`);
return this.mockDevices; return this.mockDevices;
} }
@ -134,14 +143,19 @@ export class MockBLEManager implements IBLEManager {
} }
async connectDevice(deviceId: string): Promise<boolean> { async connectDevice(deviceId: string): Promise<boolean> {
BLELogger.log(`[Mock] Connecting to device: ${deviceId}`);
try { try {
// Check if connection is already in progress // Check if connection is already in progress
if (this.connectingDevices.has(deviceId)) { if (this.connectingDevices.has(deviceId)) {
throw new Error('Connection already in progress for this device'); const error = new BLEError(BLEErrorCode.CONNECTION_IN_PROGRESS, { deviceId });
BLELogger.warn('[Mock] Connection already in progress', error);
throw error;
} }
// Check if already connected // Check if already connected
if (this.connectedDevices.has(deviceId)) { if (this.connectedDevices.has(deviceId)) {
BLELogger.log(`[Mock] Device already connected: ${deviceId}`);
this.updateConnectionState(deviceId, BLEConnectionState.READY); this.updateConnectionState(deviceId, BLEConnectionState.READY);
this.emitEvent(deviceId, 'ready'); this.emitEvent(deviceId, 'ready');
return true; return true;
@ -163,11 +177,15 @@ export class MockBLEManager implements IBLEManager {
this.updateConnectionState(deviceId, BLEConnectionState.READY); this.updateConnectionState(deviceId, BLEConnectionState.READY);
this.emitEvent(deviceId, 'ready'); this.emitEvent(deviceId, 'ready');
BLELogger.log(`[Mock] Device ready: ${deviceId}`);
return true; return true;
} catch (error: any) { } catch (error: any) {
const errorMessage = error?.message || 'Connection failed'; const bleError = isBLEError(error) ? error : parseBLEError(error, { deviceId });
const errorMessage = bleError.userMessage.message;
this.updateConnectionState(deviceId, BLEConnectionState.ERROR, undefined, errorMessage); this.updateConnectionState(deviceId, BLEConnectionState.ERROR, undefined, errorMessage);
this.emitEvent(deviceId, 'connection_failed', { error: errorMessage }); this.emitEvent(deviceId, 'connection_failed', { error: errorMessage, code: bleError.code });
BLELogger.error(`[Mock] Connection failed for ${deviceId}`, bleError);
return false; return false;
} finally { } finally {
// Always remove from connecting set when done (success or failure) // Always remove from connecting set when done (success or failure)
@ -223,14 +241,25 @@ export class MockBLEManager implements IBLEManager {
ssid: string, ssid: string,
password: string password: string
): Promise<boolean> { ): Promise<boolean> {
BLELogger.log(`[Mock] Setting WiFi on device: ${deviceId}, SSID: ${ssid}`);
await delay(2000); await delay(2000);
// Pre-validate credentials (same as real BLEManager) // Pre-validate credentials (same as real BLEManager)
if (ssid.includes('|') || ssid.includes(',')) { if (ssid.includes('|') || ssid.includes(',')) {
throw new Error('Network name contains invalid characters. Please select a different network.'); 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('|')) { if (password.includes('|')) {
throw new Error('Password contains an invalid character (|). Please use a different password.'); 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 // Simulate various failure scenarios for testing
@ -240,34 +269,56 @@ export class MockBLEManager implements IBLEManager {
// Simulate wrong password // Simulate wrong password
if (lowerPassword === 'wrongpass' || lowerPassword === 'wrong') { if (lowerPassword === 'wrongpass' || lowerPassword === 'wrong') {
throw new Error('WiFi password is incorrect. Please check and try again.'); const error = new BLEError(BLEErrorCode.WIFI_PASSWORD_INCORRECT, { deviceId });
BLELogger.error('[Mock] Wrong password simulated', error);
throw error;
} }
// Simulate network not found // Simulate network not found
if (lowerSsid.includes('notfound') || lowerSsid === 'hidden_network') { if (lowerSsid.includes('notfound') || lowerSsid === 'hidden_network') {
throw new Error('WiFi network not found. Make sure the sensor is within range of your router.'); const error = new BLEError(BLEErrorCode.WIFI_NETWORK_NOT_FOUND, { deviceId });
BLELogger.error('[Mock] Network not found simulated', error);
throw error;
} }
// Simulate timeout // Simulate timeout
if (lowerSsid.includes('timeout') || lowerPassword === 'timeout') { if (lowerSsid.includes('timeout') || lowerPassword === 'timeout') {
throw new Error('Sensor did not respond to WiFi config. Please try again.'); 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 // Simulate sensor not responding
if (lowerSsid.includes('offline')) { if (lowerSsid.includes('offline')) {
throw new Error('Sensor not responding. Please move closer and try again.'); const error = new BLEError(BLEErrorCode.SENSOR_NOT_RESPONDING, { deviceId });
BLELogger.error('[Mock] Sensor offline simulated', error);
throw error;
} }
// Validate password length (WPA/WPA2 requirement) // Validate password length (WPA/WPA2 requirement)
if (password.length < 8) { if (password.length < 8) {
throw new Error('Password must be at least 8 characters for WPA/WPA2 networks.'); 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) { if (password.length > 63) {
throw new Error('Password cannot exceed 63 characters.'); 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 // Success for all other cases
BLELogger.log(`[Mock] WiFi configured successfully for ${ssid}`);
return true; return true;
} }
@ -430,25 +481,36 @@ export class MockBLEManager implements IBLEManager {
onProgress?: (deviceId: string, status: 'connecting' | 'configuring' | 'rebooting' | 'success' | 'error', error?: string) => void onProgress?: (deviceId: string, status: 'connecting' | 'configuring' | 'rebooting' | 'success' | 'error', error?: string) => void
): Promise<BulkWiFiResult[]> { ): Promise<BulkWiFiResult[]> {
const results: BulkWiFiResult[] = []; const results: BulkWiFiResult[] = [];
const total = devices.length;
const batchStartTime = Date.now();
for (const device of devices) { BLELogger.log(`[Mock] Starting bulk WiFi setup for ${total} devices, SSID: ${ssid}`);
const { id: deviceId, name: deviceName } = device;
for (let i = 0; i < devices.length; i++) {
const { id: deviceId, name: deviceName } = devices[i];
const index = i + 1;
try { try {
// Step 1: Connect (mock) // Step 1: Connect (mock)
BLELogger.logBatchProgress(index, total, deviceName, 'connecting...');
onProgress?.(deviceId, 'connecting'); onProgress?.(deviceId, 'connecting');
await delay(800); await delay(800);
await this.connectDevice(deviceId); await this.connectDevice(deviceId);
BLELogger.logBatchProgress(index, total, deviceName, 'connected', true);
// Step 2: Set WiFi (mock) // Step 2: Set WiFi (mock)
BLELogger.logBatchProgress(index, total, deviceName, 'setting WiFi...');
onProgress?.(deviceId, 'configuring'); onProgress?.(deviceId, 'configuring');
await delay(1200); await delay(1200);
await this.setWiFi(deviceId, ssid, password); await this.setWiFi(deviceId, ssid, password);
BLELogger.logBatchProgress(index, total, deviceName, 'WiFi configured', true);
// Step 3: Reboot (mock) // Step 3: Reboot (mock)
BLELogger.logBatchProgress(index, total, deviceName, 'rebooting...');
onProgress?.(deviceId, 'rebooting'); onProgress?.(deviceId, 'rebooting');
await delay(600); await delay(600);
await this.rebootDevice(deviceId); await this.rebootDevice(deviceId);
BLELogger.logBatchProgress(index, total, deviceName, 'SUCCESS', true);
// Success // Success
onProgress?.(deviceId, 'success'); onProgress?.(deviceId, 'success');
@ -458,7 +520,10 @@ export class MockBLEManager implements IBLEManager {
success: true, success: true,
}); });
} catch (error: any) { } catch (error: any) {
const errorMessage = error?.message || 'WiFi configuration failed'; 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); onProgress?.(deviceId, 'error', errorMessage);
results.push({ results.push({
deviceId, deviceId,
@ -469,6 +534,12 @@ export class MockBLEManager implements IBLEManager {
} }
} }
// 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; return results;
} }

661
services/ble/errors.ts Normal file
View File

@ -0,0 +1,661 @@
// 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)`);
}
}

View File

@ -63,3 +63,6 @@ export * from './types';
// Re-export permission utilities // Re-export permission utilities
export * from './permissions'; export * from './permissions';
// Re-export error types and utilities
export * from './errors';