diff --git a/jest.setup.js b/jest.setup.js index 4c3abc3..258a82b 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -88,6 +88,32 @@ jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper', () => ({ default: {}, }), { virtual: true }); +// Mock react-native-ble-plx +jest.mock('react-native-ble-plx', () => ({ + BleManager: jest.fn().mockImplementation(() => ({ + startDeviceScan: jest.fn(), + stopDeviceScan: jest.fn(), + connectToDevice: jest.fn(), + state: jest.fn(() => Promise.resolve('PoweredOn')), + })), + State: { + Unknown: 'Unknown', + Resetting: 'Resetting', + Unsupported: 'Unsupported', + Unauthorized: 'Unauthorized', + PoweredOff: 'PoweredOff', + PoweredOn: 'PoweredOn', + }, + BleError: class BleError extends Error {}, + BleErrorCode: {}, +})); + +// Mock react-native-base64 +jest.mock('react-native-base64', () => ({ + encode: jest.fn((str) => Buffer.from(str).toString('base64')), + decode: jest.fn((str) => Buffer.from(str, 'base64').toString()), +})); + // Silence console warnings in tests global.console = { ...console, diff --git a/services/ble/BLEManager.ts b/services/ble/BLEManager.ts index 1a015b6..2d6a8a3 100644 --- a/services/ble/BLEManager.ts +++ b/services/ble/BLEManager.ts @@ -1,14 +1,27 @@ // Real BLE Manager для физических устройств -import { BleManager, Device, State } from 'react-native-ble-plx'; +import { BleManager, Device } from 'react-native-ble-plx'; import { Platform } from 'react-native'; -import { IBLEManager, WPDevice, WiFiNetwork, WiFiStatus, BLE_CONFIG, BLE_COMMANDS } from './types'; +import { + IBLEManager, + WPDevice, + WiFiNetwork, + WiFiStatus, + BLE_CONFIG, + BLE_COMMANDS, + BLEConnectionState, + BLEDeviceConnection, + BLEEventListener, + BLEConnectionEvent, +} from './types'; import { requestBLEPermissions, checkBluetoothEnabled } from './permissions'; import base64 from 'react-native-base64'; export class RealBLEManager implements IBLEManager { private _manager: BleManager | null = null; private connectedDevices = new Map(); + private connectionStates = new Map(); + private eventListeners: BLEEventListener[] = []; private scanning = false; // Lazy initialization to prevent crash on app startup @@ -19,8 +32,78 @@ export class RealBLEManager implements IBLEManager { return this._manager; } - constructor() { - // Don't initialize BleManager here - use lazy initialization + // Empty constructor - using lazy initialization for BleManager + + /** + * Update connection state and notify listeners + */ + private updateConnectionState( + deviceId: string, + state: BLEConnectionState, + deviceName?: string, + error?: string + ): void { + const existing = this.connectionStates.get(deviceId); + const now = Date.now(); + + const connection: BLEDeviceConnection = { + deviceId, + deviceName: deviceName || existing?.deviceName || deviceId, + state, + error, + connectedAt: state === BLEConnectionState.CONNECTED ? now : existing?.connectedAt, + lastActivity: now, + }; + + this.connectionStates.set(deviceId, connection); + this.emitEvent(deviceId, 'state_changed', { state, error }); + } + + /** + * Emit event to all registered listeners + */ + private emitEvent(deviceId: string, event: BLEConnectionEvent, data?: any): void { + this.eventListeners.forEach((listener) => { + try { + listener(deviceId, event, data); + } catch { + // Listener error should not crash the app + } + }); + } + + /** + * Get current connection state for a device + */ + getConnectionState(deviceId: string): BLEConnectionState { + const connection = this.connectionStates.get(deviceId); + return connection?.state || BLEConnectionState.DISCONNECTED; + } + + /** + * Get all active connections + */ + getAllConnections(): Map { + return new Map(this.connectionStates); + } + + /** + * Add event listener + */ + addEventListener(listener: BLEEventListener): void { + if (!this.eventListeners.includes(listener)) { + this.eventListeners.push(listener); + } + } + + /** + * Remove event listener + */ + removeEventListener(listener: BLEEventListener): void { + const index = this.eventListeners.indexOf(listener); + if (index > -1) { + this.eventListeners.splice(index, 1); + } } async scanDevices(): Promise { @@ -88,16 +171,25 @@ export class RealBLEManager implements IBLEManager { async connectDevice(deviceId: string): Promise { try { + // Update state to CONNECTING + this.updateConnectionState(deviceId, BLEConnectionState.CONNECTING); + // Step 0: Check permissions (required for Android 12+) const permissionStatus = await requestBLEPermissions(); if (!permissionStatus.granted) { - throw new Error(permissionStatus.error || 'Bluetooth permissions not granted'); + const error = permissionStatus.error || 'Bluetooth permissions not granted'; + this.updateConnectionState(deviceId, BLEConnectionState.ERROR, undefined, error); + this.emitEvent(deviceId, 'connection_failed', { error }); + throw new Error(error); } // Step 0.5: Check Bluetooth is enabled const bluetoothStatus = await checkBluetoothEnabled(this.manager); if (!bluetoothStatus.enabled) { - throw new Error(bluetoothStatus.error || 'Bluetooth is disabled. Please enable it in settings.'); + const error = bluetoothStatus.error || 'Bluetooth is disabled. Please enable it in settings.'; + this.updateConnectionState(deviceId, BLEConnectionState.ERROR, undefined, error); + this.emitEvent(deviceId, 'connection_failed', { error }); + throw new Error(error); } // Check if already connected @@ -105,29 +197,46 @@ export class RealBLEManager implements IBLEManager { if (existingDevice) { const isConnected = await existingDevice.isConnected(); if (isConnected) { + this.updateConnectionState(deviceId, BLEConnectionState.READY, existingDevice.name || undefined); + this.emitEvent(deviceId, 'ready'); return true; } // Device was in map but disconnected, remove it this.connectedDevices.delete(deviceId); + this.updateConnectionState(deviceId, BLEConnectionState.DISCONNECTED); } const device = await this.manager.connectToDevice(deviceId, { timeout: 10000, // 10 second timeout }); + // Update state to CONNECTED + this.updateConnectionState(deviceId, BLEConnectionState.CONNECTED, device.name || undefined); + + // Update state to DISCOVERING + this.updateConnectionState(deviceId, BLEConnectionState.DISCOVERING, device.name || undefined); await device.discoverAllServicesAndCharacteristics(); // Request larger MTU for Android (default is 23 bytes which is too small) if (Platform.OS === 'android') { try { - const mtu = await device.requestMTU(512); - } catch (mtuError) { + await device.requestMTU(512); + } catch { + // MTU request may fail on some devices - continue anyway } } this.connectedDevices.set(deviceId, device); + + // Update state to READY + this.updateConnectionState(deviceId, BLEConnectionState.READY, device.name || undefined); + this.emitEvent(deviceId, 'ready'); + return true; } catch (error: any) { + const errorMessage = error?.message || 'Connection failed'; + this.updateConnectionState(deviceId, BLEConnectionState.ERROR, undefined, errorMessage); + this.emitEvent(deviceId, 'connection_failed', { error: errorMessage }); return false; } } @@ -136,14 +245,27 @@ export class RealBLEManager implements IBLEManager { const device = this.connectedDevices.get(deviceId); if (device) { try { + // Update state to DISCONNECTING + this.updateConnectionState(deviceId, BLEConnectionState.DISCONNECTING); + // Cancel any pending operations before disconnecting // This helps prevent Android NullPointerException in monitor callbacks await device.cancelConnection(); - } catch (error: any) { + + // Update state to DISCONNECTED + this.updateConnectionState(deviceId, BLEConnectionState.DISCONNECTED); + this.emitEvent(deviceId, 'disconnected'); + } catch { // Log but don't throw - device may already be disconnected + this.updateConnectionState(deviceId, BLEConnectionState.DISCONNECTED); + this.emitEvent(deviceId, 'disconnected'); } finally { this.connectedDevices.delete(deviceId); } + } else { + // Not in connected devices map, just update state + this.updateConnectionState(deviceId, BLEConnectionState.DISCONNECTED); + this.emitEvent(deviceId, 'disconnected'); } } @@ -165,7 +287,7 @@ export class RealBLEManager implements IBLEManager { this.connectedDevices.delete(deviceId); throw new Error('Device disconnected'); } - } catch (checkError: any) { + } catch { throw new Error('Failed to verify connection'); } @@ -185,7 +307,7 @@ export class RealBLEManager implements IBLEManager { if (subscription) { try { subscription.remove(); - } catch (removeError) { + } catch { // Ignore errors during cleanup - device may already be disconnected } subscription = null; @@ -343,7 +465,8 @@ export class RealBLEManager implements IBLEManager { return true; } } - } catch (statusError) { + } catch { + // Ignore status check errors - continue with WiFi config } // Step 2: Set WiFi credentials @@ -370,7 +493,8 @@ export class RealBLEManager implements IBLEManager { return true; } } - } catch (recheckError) { + } catch { + // Ignore recheck errors - password was rejected } throw new Error('WiFi credentials rejected by sensor. Check password.'); @@ -439,13 +563,17 @@ export class RealBLEManager implements IBLEManager { for (const deviceId of deviceIds) { try { await this.disconnectDevice(deviceId); - } catch (error) { + } catch { // Continue cleanup even if one device fails } } - // Clear the map + // Clear the maps this.connectedDevices.clear(); + this.connectionStates.clear(); + + // Clear event listeners + this.eventListeners = []; } } diff --git a/services/ble/MockBLEManager.ts b/services/ble/MockBLEManager.ts index 3197ae5..9b28ba2 100644 --- a/services/ble/MockBLEManager.ts +++ b/services/ble/MockBLEManager.ts @@ -1,11 +1,22 @@ // Mock BLE Manager для iOS Simulator (Bluetooth недоступен) -import { IBLEManager, WPDevice, WiFiNetwork, WiFiStatus } from './types'; +import { + IBLEManager, + WPDevice, + WiFiNetwork, + WiFiStatus, + BLEConnectionState, + BLEDeviceConnection, + BLEEventListener, + BLEConnectionEvent, +} from './types'; const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); export class MockBLEManager implements IBLEManager { private connectedDevices = new Set(); + private connectionStates = new Map(); + private eventListeners: BLEEventListener[] = []; private mockDevices: WPDevice[] = [ { id: 'mock-743', @@ -23,6 +34,78 @@ export class MockBLEManager implements IBLEManager { }, ]; + /** + * Update connection state and notify listeners + */ + private updateConnectionState( + deviceId: string, + state: BLEConnectionState, + deviceName?: string, + error?: string + ): void { + const existing = this.connectionStates.get(deviceId); + const now = Date.now(); + + const connection: BLEDeviceConnection = { + deviceId, + deviceName: deviceName || existing?.deviceName || deviceId, + state, + error, + connectedAt: state === BLEConnectionState.CONNECTED ? now : existing?.connectedAt, + lastActivity: now, + }; + + this.connectionStates.set(deviceId, connection); + this.emitEvent(deviceId, 'state_changed', { state, error }); + } + + /** + * Emit event to all registered listeners + */ + private emitEvent(deviceId: string, event: BLEConnectionEvent, data?: any): void { + this.eventListeners.forEach((listener) => { + try { + listener(deviceId, event, data); + } catch { + // Listener error should not crash the app + } + }); + } + + /** + * Get current connection state for a device + */ + getConnectionState(deviceId: string): BLEConnectionState { + const connection = this.connectionStates.get(deviceId); + return connection?.state || BLEConnectionState.DISCONNECTED; + } + + /** + * Get all active connections + */ + getAllConnections(): Map { + return new Map(this.connectionStates); + } + + /** + * Add event listener + */ + addEventListener(listener: BLEEventListener): void { + if (!this.eventListeners.includes(listener)) { + this.eventListeners.push(listener); + } + } + + /** + * Remove event listener + */ + removeEventListener(listener: BLEEventListener): void { + const index = this.eventListeners.indexOf(listener); + if (index > -1) { + this.eventListeners.splice(index, 1); + } + } + async scanDevices(): Promise { await delay(2000); // Simulate scan delay return this.mockDevices; @@ -32,14 +115,35 @@ export class MockBLEManager implements IBLEManager { } async connectDevice(deviceId: string): Promise { - await delay(1000); - this.connectedDevices.add(deviceId); - return true; + try { + this.updateConnectionState(deviceId, BLEConnectionState.CONNECTING); + await delay(500); + + this.updateConnectionState(deviceId, BLEConnectionState.CONNECTED); + await delay(300); + + this.updateConnectionState(deviceId, BLEConnectionState.DISCOVERING); + await delay(200); + + this.connectedDevices.add(deviceId); + this.updateConnectionState(deviceId, BLEConnectionState.READY); + this.emitEvent(deviceId, 'ready'); + + return true; + } catch (error: any) { + const errorMessage = error?.message || 'Connection failed'; + this.updateConnectionState(deviceId, BLEConnectionState.ERROR, undefined, errorMessage); + this.emitEvent(deviceId, 'connection_failed', { error: errorMessage }); + return false; + } } async disconnectDevice(deviceId: string): Promise { + this.updateConnectionState(deviceId, BLEConnectionState.DISCONNECTING); await delay(500); this.connectedDevices.delete(deviceId); + this.updateConnectionState(deviceId, BLEConnectionState.DISCONNECTED); + this.emitEvent(deviceId, 'disconnected'); } isDeviceConnected(deviceId: string): boolean { @@ -114,5 +218,7 @@ export class MockBLEManager implements IBLEManager { } this.connectedDevices.clear(); + this.connectionStates.clear(); + this.eventListeners = []; } } diff --git a/services/ble/__tests__/BLEManager.stateMachine.test.ts b/services/ble/__tests__/BLEManager.stateMachine.test.ts new file mode 100644 index 0000000..fceb56f --- /dev/null +++ b/services/ble/__tests__/BLEManager.stateMachine.test.ts @@ -0,0 +1,307 @@ +/** + * Tests for BLE Connection State Machine + */ + +import { MockBLEManager } from '../MockBLEManager'; +import { BLEConnectionState, BLEConnectionEvent } from '../types'; + +describe('BLE Connection State Machine', () => { + let manager: MockBLEManager; + + beforeEach(() => { + manager = new MockBLEManager(); + }); + + afterEach(async () => { + await manager.cleanup(); + }); + + describe('State Transitions', () => { + it('should start in DISCONNECTED state', () => { + const state = manager.getConnectionState('test-device'); + expect(state).toBe(BLEConnectionState.DISCONNECTED); + }); + + it('should transition through states during connection', async () => { + const states: BLEConnectionState[] = []; + const listener = (deviceId: string, event: BLEConnectionEvent, data?: any) => { + if (event === 'state_changed') { + states.push(data.state); + } + }; + + manager.addEventListener(listener); + + await manager.connectDevice('test-device'); + + // Should go through: CONNECTING -> CONNECTED -> DISCOVERING -> READY + expect(states).toContain(BLEConnectionState.CONNECTING); + expect(states).toContain(BLEConnectionState.CONNECTED); + expect(states).toContain(BLEConnectionState.DISCOVERING); + expect(states).toContain(BLEConnectionState.READY); + + // Final state should be READY + expect(manager.getConnectionState('test-device')).toBe(BLEConnectionState.READY); + }); + + it('should transition to DISCONNECTING then DISCONNECTED on disconnect', async () => { + const states: BLEConnectionState[] = []; + const listener = (deviceId: string, event: BLEConnectionEvent, data?: any) => { + if (event === 'state_changed') { + states.push(data.state); + } + }; + + manager.addEventListener(listener); + + await manager.connectDevice('test-device'); + states.length = 0; // Clear connection states + + await manager.disconnectDevice('test-device'); + + expect(states).toContain(BLEConnectionState.DISCONNECTING); + expect(states).toContain(BLEConnectionState.DISCONNECTED); + expect(manager.getConnectionState('test-device')).toBe(BLEConnectionState.DISCONNECTED); + }); + }); + + describe('Event Emission', () => { + it('should emit ready event when connection completes', async () => { + let readyEmitted = false; + const listener = (deviceId: string, event: BLEConnectionEvent) => { + if (event === 'ready') { + readyEmitted = true; + } + }; + + manager.addEventListener(listener); + await manager.connectDevice('test-device'); + + expect(readyEmitted).toBe(true); + }); + + it('should emit disconnected event on disconnect', async () => { + let disconnectedEmitted = false; + const listener = (deviceId: string, event: BLEConnectionEvent) => { + if (event === 'disconnected') { + disconnectedEmitted = true; + } + }; + + manager.addEventListener(listener); + await manager.connectDevice('test-device'); + await manager.disconnectDevice('test-device'); + + expect(disconnectedEmitted).toBe(true); + }); + + it('should emit state_changed event on each state transition', async () => { + let stateChangedCount = 0; + const listener = (deviceId: string, event: BLEConnectionEvent) => { + if (event === 'state_changed') { + stateChangedCount++; + } + }; + + manager.addEventListener(listener); + await manager.connectDevice('test-device'); + + // Should have multiple state changes during connection + expect(stateChangedCount).toBeGreaterThanOrEqual(4); + }); + }); + + describe('Multiple Devices', () => { + it('should track state for multiple devices independently', async () => { + await manager.connectDevice('device-1'); + await manager.connectDevice('device-2'); + + expect(manager.getConnectionState('device-1')).toBe(BLEConnectionState.READY); + expect(manager.getConnectionState('device-2')).toBe(BLEConnectionState.READY); + + await manager.disconnectDevice('device-1'); + + expect(manager.getConnectionState('device-1')).toBe(BLEConnectionState.DISCONNECTED); + expect(manager.getConnectionState('device-2')).toBe(BLEConnectionState.READY); + }); + + it('should emit events with correct deviceId', async () => { + const events: Array<{ deviceId: string; event: BLEConnectionEvent }> = []; + const listener = (deviceId: string, event: BLEConnectionEvent) => { + events.push({ deviceId, event }); + }; + + manager.addEventListener(listener); + + await manager.connectDevice('device-1'); + await manager.connectDevice('device-2'); + + const device1Events = events.filter(e => e.deviceId === 'device-1'); + const device2Events = events.filter(e => e.deviceId === 'device-2'); + + expect(device1Events.length).toBeGreaterThan(0); + expect(device2Events.length).toBeGreaterThan(0); + }); + }); + + describe('Connection Info', () => { + it('should store connection metadata', async () => { + await manager.connectDevice('test-device'); + + const connections = manager.getAllConnections(); + const deviceConnection = connections.get('test-device'); + + expect(deviceConnection).toBeDefined(); + expect(deviceConnection?.deviceId).toBe('test-device'); + expect(deviceConnection?.state).toBe(BLEConnectionState.READY); + expect(deviceConnection?.connectedAt).toBeDefined(); + expect(deviceConnection?.lastActivity).toBeDefined(); + }); + + it('should update lastActivity on state changes', async () => { + await manager.connectDevice('test-device'); + + const connections1 = manager.getAllConnections(); + const firstActivity = connections1.get('test-device')?.lastActivity; + + // Wait a bit and disconnect + await new Promise(resolve => setTimeout(resolve, 100)); + await manager.disconnectDevice('test-device'); + + const connections2 = manager.getAllConnections(); + const secondActivity = connections2.get('test-device')?.lastActivity; + + expect(secondActivity).toBeGreaterThan(firstActivity!); + }); + }); + + describe('Event Listeners', () => { + it('should add and remove listeners', () => { + const listener1 = jest.fn(); + const listener2 = jest.fn(); + + manager.addEventListener(listener1); + manager.addEventListener(listener2); + manager.removeEventListener(listener1); + + // Trigger an event + manager.connectDevice('test-device'); + + // listener2 should be called, listener1 should not + expect(listener2).toHaveBeenCalled(); + // Note: In real implementation, this would work, but here we need to wait + }); + + it('should not add duplicate listeners', async () => { + let callCount = 0; + const listener = () => { + callCount++; + }; + + manager.addEventListener(listener); + manager.addEventListener(listener); // Try to add again + + await manager.connectDevice('test-device'); + + // Each event should only increment once + // (multiple state changes will still cause multiple calls) + const expectedCalls = 4; // CONNECTING, CONNECTED, DISCOVERING, READY + expect(callCount).toBeLessThan(expectedCalls * 2); // Not doubled + }); + + it('should handle listener errors gracefully', async () => { + const errorListener = () => { + throw new Error('Listener error'); + }; + const goodListener = jest.fn(); + + manager.addEventListener(errorListener); + manager.addEventListener(goodListener); + + await manager.connectDevice('test-device'); + + // Good listener should still be called despite error listener + expect(goodListener).toHaveBeenCalled(); + }); + }); + + describe('Cleanup', () => { + it('should disconnect all devices on cleanup', async () => { + await manager.connectDevice('device-1'); + await manager.connectDevice('device-2'); + await manager.connectDevice('device-3'); + + expect(manager.getConnectionState('device-1')).toBe(BLEConnectionState.READY); + expect(manager.getConnectionState('device-2')).toBe(BLEConnectionState.READY); + expect(manager.getConnectionState('device-3')).toBe(BLEConnectionState.READY); + + await manager.cleanup(); + + expect(manager.getConnectionState('device-1')).toBe(BLEConnectionState.DISCONNECTED); + expect(manager.getConnectionState('device-2')).toBe(BLEConnectionState.DISCONNECTED); + expect(manager.getConnectionState('device-3')).toBe(BLEConnectionState.DISCONNECTED); + }); + + it('should clear all connection states on cleanup', async () => { + await manager.connectDevice('device-1'); + await manager.connectDevice('device-2'); + + let connections = manager.getAllConnections(); + expect(connections.size).toBeGreaterThan(0); + + await manager.cleanup(); + + connections = manager.getAllConnections(); + expect(connections.size).toBe(0); + }); + + it('should clear all event listeners on cleanup', async () => { + const listener = jest.fn(); + manager.addEventListener(listener); + + await manager.cleanup(); + + // Try to trigger events + await manager.connectDevice('test-device'); + + // Listener should not be called after cleanup + // (Will be called during cleanup disconnect, but not after) + const callsBeforeCleanup = listener.mock.calls.length; + await manager.connectDevice('another-device'); + expect(listener.mock.calls.length).toBe(callsBeforeCleanup); + }); + }); + + describe('Edge Cases', () => { + it('should handle disconnect of non-connected device', async () => { + // Should not throw + await expect(manager.disconnectDevice('non-existent')).resolves.not.toThrow(); + + expect(manager.getConnectionState('non-existent')).toBe(BLEConnectionState.DISCONNECTED); + }); + + it('should handle reconnection to same device', async () => { + await manager.connectDevice('test-device'); + await manager.disconnectDevice('test-device'); + await manager.connectDevice('test-device'); + + expect(manager.getConnectionState('test-device')).toBe(BLEConnectionState.READY); + }); + + it('should preserve connection info across state changes', async () => { + await manager.connectDevice('test-device'); + + const connections1 = manager.getAllConnections(); + const connectedAt = connections1.get('test-device')?.connectedAt; + + // Trigger state change without disconnecting + // (In real implementation, this would be a command send) + await manager.sendCommand('test-device', 'test'); + + const connections2 = manager.getAllConnections(); + const stillConnectedAt = connections2.get('test-device')?.connectedAt; + + expect(stillConnectedAt).toBe(connectedAt); + }); + }); +}); diff --git a/services/ble/index.ts b/services/ble/index.ts index 6e973fa..c41b595 100644 --- a/services/ble/index.ts +++ b/services/ble/index.ts @@ -13,9 +13,11 @@ function getBLEManager(): IBLEManager { if (!_bleManager) { // Dynamic import to prevent crash on Android startup if (isBLEAvailable) { + // eslint-disable-next-line @typescript-eslint/no-require-imports const { RealBLEManager } = require('./BLEManager'); _bleManager = new RealBLEManager(); } else { + // eslint-disable-next-line @typescript-eslint/no-require-imports const { MockBLEManager } = require('./MockBLEManager'); _bleManager = new MockBLEManager(); } @@ -30,6 +32,10 @@ export const bleManager: IBLEManager = { connectDevice: (deviceId: string) => getBLEManager().connectDevice(deviceId), disconnectDevice: (deviceId: string) => getBLEManager().disconnectDevice(deviceId), isDeviceConnected: (deviceId: string) => getBLEManager().isDeviceConnected(deviceId), + getConnectionState: (deviceId: string) => getBLEManager().getConnectionState(deviceId), + getAllConnections: () => getBLEManager().getAllConnections(), + addEventListener: (listener) => getBLEManager().addEventListener(listener), + removeEventListener: (listener) => getBLEManager().removeEventListener(listener), sendCommand: (deviceId: string, command: string) => getBLEManager().sendCommand(deviceId, command), getWiFiList: (deviceId: string) => getBLEManager().getWiFiList(deviceId), setWiFi: (deviceId: string, ssid: string, password: string) => getBLEManager().setWiFi(deviceId, ssid, password), diff --git a/services/ble/types.ts b/services/ble/types.ts index 32b6696..50fa294 100644 --- a/services/ble/types.ts +++ b/services/ble/types.ts @@ -45,6 +45,37 @@ export const BLE_CONFIG = { DEVICE_NAME_PREFIX: 'WP_', }; +// BLE Connection States +export enum BLEConnectionState { + DISCONNECTED = 'disconnected', + CONNECTING = 'connecting', + CONNECTED = 'connected', + DISCOVERING = 'discovering', + READY = 'ready', + DISCONNECTING = 'disconnecting', + ERROR = 'error', +} + +// BLE Connection Event Types +export type BLEConnectionEvent = + | 'state_changed' + | 'connection_failed' + | 'disconnected' + | 'ready'; + +// BLE Device Connection Info +export interface BLEDeviceConnection { + deviceId: string; + deviceName: string; + state: BLEConnectionState; + error?: string; + connectedAt?: number; + lastActivity?: number; +} + +// BLE Event Listener +export type BLEEventListener = (deviceId: string, event: BLEConnectionEvent, data?: any) => void; + // Interface для BLE Manager (и real и mock) export interface IBLEManager { scanDevices(): Promise; @@ -52,6 +83,10 @@ export interface IBLEManager { connectDevice(deviceId: string): Promise; disconnectDevice(deviceId: string): Promise; isDeviceConnected(deviceId: string): boolean; + getConnectionState(deviceId: string): BLEConnectionState; + getAllConnections(): Map; + addEventListener(listener: BLEEventListener): void; + removeEventListener(listener: BLEEventListener): void; sendCommand(deviceId: string, command: string): Promise; getWiFiList(deviceId: string): Promise; setWiFi(deviceId: string, ssid: string, password: string): Promise;