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:
parent
263cb10b62
commit
6960f248e0
411
__tests__/services/ble/errors.test.ts
Normal file
411
__tests__/services/ble/errors.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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
|
||||||
|
try {
|
||||||
const unlockResponse = await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK);
|
const unlockResponse = await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK);
|
||||||
if (!unlockResponse.includes('ok')) {
|
if (!unlockResponse.includes('ok')) {
|
||||||
throw new Error('Failed to unlock device');
|
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
|
||||||
|
try {
|
||||||
const unlockResponse = await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK);
|
const unlockResponse = await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK);
|
||||||
if (!unlockResponse.includes('ok')) {
|
if (!unlockResponse.includes('ok')) {
|
||||||
throw new Error('Failed to unlock device');
|
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,28 +776,50 @@ 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> {
|
||||||
|
BLELogger.log(`Rebooting device: ${deviceId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
// Step 1: Unlock device
|
// Step 1: Unlock device
|
||||||
await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK);
|
await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK);
|
||||||
|
|
||||||
// Step 2: Reboot (device will disconnect)
|
// Step 2: Reboot (device will disconnect)
|
||||||
await this.sendCommand(deviceId, BLE_COMMANDS.REBOOT);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
661
services/ble/errors.ts
Normal 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)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user