Implemented comprehensive bulk operations for BLE sensor management to improve efficiency when working with multiple sensors simultaneously. Features Added: - bulkDisconnect: Disconnect multiple sensors at once - bulkReboot: Reboot multiple sensors sequentially - bulkSetWiFi: Configure WiFi for multiple sensors with progress tracking Implementation Details: - Added BulkOperationResult and BulkWiFiResult types to track operation outcomes - Implemented bulk operations in both RealBLEManager and MockBLEManager - Exposed bulk operations through BLEContext for easy UI integration - Sequential processing ensures reliable operation completion - Progress callbacks for real-time UI updates during bulk operations Testing: - Added comprehensive test suite with 14 test cases - Tests cover success scenarios, error handling, and edge cases - All tests passing with appropriate timeout configurations - Verified both individual and sequential bulk operations Technical Notes: - Bulk operations maintain device connection state consistency - Error handling allows graceful continuation despite individual failures - MockBLEManager includes realistic delays for testing - Integration with existing BLE service architecture preserved 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
423 lines
11 KiB
TypeScript
423 lines
11 KiB
TypeScript
// Mock BLE Manager для iOS Simulator (Bluetooth недоступен)
|
|
|
|
import {
|
|
IBLEManager,
|
|
WPDevice,
|
|
WiFiNetwork,
|
|
WiFiStatus,
|
|
BLEConnectionState,
|
|
BLEDeviceConnection,
|
|
BLEEventListener,
|
|
BLEConnectionEvent,
|
|
SensorHealthMetrics,
|
|
SensorHealthStatus,
|
|
WiFiSignalQuality,
|
|
CommunicationHealth,
|
|
BulkOperationResult,
|
|
BulkWiFiResult,
|
|
} 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
|
|
|
|
// Health monitoring state (mock)
|
|
private sensorHealthMetrics = new Map<string, SensorHealthMetrics>();
|
|
private communicationStats = new Map<string, CommunicationHealth>();
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Get sensor health metrics (mock implementation)
|
|
*/
|
|
async getSensorHealth(wellId: number, mac: string): Promise<SensorHealthMetrics | null> {
|
|
await delay(1000);
|
|
|
|
const deviceKey = `WP_${wellId}_${mac.slice(-6).toLowerCase()}`;
|
|
|
|
// Generate mock health metrics
|
|
const now = Date.now();
|
|
const mockMetrics: SensorHealthMetrics = {
|
|
deviceId: `mock-${wellId}`,
|
|
deviceName: deviceKey,
|
|
overallHealth: SensorHealthStatus.GOOD,
|
|
connectionStatus: 'online',
|
|
lastSeen: new Date(),
|
|
lastSeenMinutesAgo: 2,
|
|
wifiSignalQuality: WiFiSignalQuality.GOOD,
|
|
wifiRssi: -60,
|
|
wifiSsid: 'FrontierTower',
|
|
wifiConnected: true,
|
|
communication: {
|
|
successfulCommands: 45,
|
|
failedCommands: 2,
|
|
averageResponseTime: 350,
|
|
lastSuccessfulCommand: now - 120000, // 2 minutes ago
|
|
lastFailedCommand: now - 3600000, // 1 hour ago
|
|
},
|
|
bleRssi: -55,
|
|
bleConnectionAttempts: 10,
|
|
bleConnectionFailures: 1,
|
|
lastHealthCheck: now,
|
|
lastBleConnection: now - 120000,
|
|
};
|
|
|
|
this.sensorHealthMetrics.set(deviceKey, mockMetrics);
|
|
return mockMetrics;
|
|
}
|
|
|
|
/**
|
|
* Get all cached sensor health metrics (mock)
|
|
*/
|
|
getAllSensorHealth(): Map<string, SensorHealthMetrics> {
|
|
return new Map(this.sensorHealthMetrics);
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
|
|
// Clear health monitoring data
|
|
this.sensorHealthMetrics.clear();
|
|
this.communicationStats.clear();
|
|
|
|
this.eventListeners = [];
|
|
}
|
|
|
|
/**
|
|
* Bulk disconnect multiple devices (mock)
|
|
*/
|
|
async bulkDisconnect(deviceIds: string[]): Promise<BulkOperationResult[]> {
|
|
const results: BulkOperationResult[] = [];
|
|
|
|
for (const deviceId of deviceIds) {
|
|
await delay(100); // Simulate disconnect time
|
|
|
|
const mockDevice = this.mockDevices.find(d => d.id === deviceId);
|
|
const deviceName = mockDevice?.name || deviceId;
|
|
|
|
try {
|
|
await this.disconnectDevice(deviceId);
|
|
results.push({
|
|
deviceId,
|
|
deviceName,
|
|
success: true,
|
|
});
|
|
} catch (error: any) {
|
|
results.push({
|
|
deviceId,
|
|
deviceName,
|
|
success: false,
|
|
error: error?.message || 'Disconnect failed',
|
|
});
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Bulk reboot multiple devices (mock)
|
|
*/
|
|
async bulkReboot(deviceIds: string[]): Promise<BulkOperationResult[]> {
|
|
const results: BulkOperationResult[] = [];
|
|
|
|
for (const deviceId of deviceIds) {
|
|
await delay(500); // Simulate reboot time
|
|
|
|
const mockDevice = this.mockDevices.find(d => d.id === deviceId);
|
|
const deviceName = mockDevice?.name || deviceId;
|
|
|
|
try {
|
|
// Mock reboot success
|
|
await this.rebootDevice(deviceId);
|
|
results.push({
|
|
deviceId,
|
|
deviceName,
|
|
success: true,
|
|
});
|
|
} catch (error: any) {
|
|
results.push({
|
|
deviceId,
|
|
deviceName,
|
|
success: false,
|
|
error: error?.message || 'Reboot failed',
|
|
});
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Bulk WiFi configuration for multiple devices (mock)
|
|
*/
|
|
async bulkSetWiFi(
|
|
devices: Array<{ id: string; name: string }>,
|
|
ssid: string,
|
|
password: string,
|
|
onProgress?: (deviceId: string, status: 'connecting' | 'configuring' | 'rebooting' | 'success' | 'error', error?: string) => void
|
|
): Promise<BulkWiFiResult[]> {
|
|
const results: BulkWiFiResult[] = [];
|
|
|
|
for (const device of devices) {
|
|
const { id: deviceId, name: deviceName } = device;
|
|
|
|
try {
|
|
// Step 1: Connect (mock)
|
|
onProgress?.(deviceId, 'connecting');
|
|
await delay(800);
|
|
await this.connectDevice(deviceId);
|
|
|
|
// Step 2: Set WiFi (mock)
|
|
onProgress?.(deviceId, 'configuring');
|
|
await delay(1200);
|
|
await this.setWiFi(deviceId, ssid, password);
|
|
|
|
// Step 3: Reboot (mock)
|
|
onProgress?.(deviceId, 'rebooting');
|
|
await delay(600);
|
|
await this.rebootDevice(deviceId);
|
|
|
|
// Success
|
|
onProgress?.(deviceId, 'success');
|
|
results.push({
|
|
deviceId,
|
|
deviceName,
|
|
success: true,
|
|
});
|
|
} catch (error: any) {
|
|
const errorMessage = error?.message || 'WiFi configuration failed';
|
|
onProgress?.(deviceId, 'error', errorMessage);
|
|
results.push({
|
|
deviceId,
|
|
deviceName,
|
|
success: false,
|
|
error: errorMessage,
|
|
});
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
}
|