Prevents race conditions when multiple connection attempts are made to the same device simultaneously by: - Adding connectingDevices Set to track in-progress connections - Checking for concurrent connections before starting new attempt - Returning early if device is already connected - Using finally block to ensure cleanup of connecting state - Clearing connectingDevices set on cleanup Includes comprehensive test suite for concurrent connection scenarios including edge cases like rapid connect/disconnect cycles and cleanup during connection attempts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
245 lines
6.4 KiB
TypeScript
245 lines
6.4 KiB
TypeScript
// Mock BLE Manager для iOS Simulator (Bluetooth недоступен)
|
|
|
|
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<string>();
|
|
private connectionStates = new Map<string, BLEDeviceConnection>();
|
|
private eventListeners: BLEEventListener[] = [];
|
|
private connectingDevices = new Set<string>(); // Track devices currently being connected
|
|
private mockDevices: WPDevice[] = [
|
|
{
|
|
id: 'mock-743',
|
|
name: 'WP_497_81a14c',
|
|
mac: '142B2F81A14C',
|
|
rssi: -55,
|
|
wellId: 497,
|
|
},
|
|
{
|
|
id: 'mock-769',
|
|
name: 'WP_523_81aad4',
|
|
mac: '142B2F81AAD4',
|
|
rssi: -67,
|
|
wellId: 523,
|
|
},
|
|
];
|
|
|
|
/**
|
|
* 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<string, BLEDeviceConnection> {
|
|
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<WPDevice[]> {
|
|
await delay(2000); // Simulate scan delay
|
|
return this.mockDevices;
|
|
}
|
|
|
|
stopScan(): void {
|
|
}
|
|
|
|
async connectDevice(deviceId: string): Promise<boolean> {
|
|
try {
|
|
// Check if connection is already in progress
|
|
if (this.connectingDevices.has(deviceId)) {
|
|
throw new Error('Connection already in progress for this device');
|
|
}
|
|
|
|
// Check if already connected
|
|
if (this.connectedDevices.has(deviceId)) {
|
|
this.updateConnectionState(deviceId, BLEConnectionState.READY);
|
|
this.emitEvent(deviceId, 'ready');
|
|
return true;
|
|
}
|
|
|
|
// Mark device as connecting
|
|
this.connectingDevices.add(deviceId);
|
|
|
|
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;
|
|
} finally {
|
|
// Always remove from connecting set when done (success or failure)
|
|
this.connectingDevices.delete(deviceId);
|
|
}
|
|
}
|
|
|
|
async disconnectDevice(deviceId: string): Promise<void> {
|
|
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 {
|
|
return this.connectedDevices.has(deviceId);
|
|
}
|
|
|
|
async sendCommand(deviceId: string, command: string): Promise<string> {
|
|
await delay(500);
|
|
|
|
// Simulate responses
|
|
if (command === 'pin|7856') {
|
|
return 'pin|ok';
|
|
}
|
|
if (command === 'w') {
|
|
return 'mac,142b2f81a14c|w|3|FrontierTower,-55|HomeNetwork,-67|TP-Link_5G,-75';
|
|
}
|
|
if (command === 'a') {
|
|
return 'mac,142b2f81a14c|a|FrontierTower,-67';
|
|
}
|
|
if (command.startsWith('W|')) {
|
|
return 'mac,142b2f81a14c|W|ok';
|
|
}
|
|
|
|
return 'ok';
|
|
}
|
|
|
|
async getWiFiList(deviceId: string): Promise<WiFiNetwork[]> {
|
|
await delay(1500);
|
|
|
|
return [
|
|
{ ssid: 'FrontierTower', rssi: -55 },
|
|
{ ssid: 'HomeNetwork', rssi: -67 },
|
|
{ ssid: 'TP-Link_5G', rssi: -75 },
|
|
{ ssid: 'Office-WiFi', rssi: -80 },
|
|
];
|
|
}
|
|
|
|
async setWiFi(
|
|
deviceId: string,
|
|
ssid: string,
|
|
password: string
|
|
): Promise<boolean> {
|
|
await delay(2000);
|
|
return true;
|
|
}
|
|
|
|
async getCurrentWiFi(deviceId: string): Promise<WiFiStatus | null> {
|
|
await delay(1000);
|
|
|
|
return {
|
|
ssid: 'FrontierTower',
|
|
rssi: -67,
|
|
connected: true,
|
|
};
|
|
}
|
|
|
|
async rebootDevice(deviceId: string): Promise<void> {
|
|
await delay(500);
|
|
this.connectedDevices.delete(deviceId);
|
|
}
|
|
|
|
/**
|
|
* Cleanup all BLE connections and state
|
|
* Should be called on app logout to properly release resources
|
|
*/
|
|
async cleanup(): Promise<void> {
|
|
|
|
// Disconnect all connected devices
|
|
const deviceIds = Array.from(this.connectedDevices);
|
|
for (const deviceId of deviceIds) {
|
|
await this.disconnectDevice(deviceId);
|
|
}
|
|
|
|
this.connectedDevices.clear();
|
|
this.connectionStates.clear();
|
|
this.connectingDevices.clear();
|
|
this.eventListeners = [];
|
|
}
|
|
}
|