WellNuo/services/ble/MockBLEManager.ts
Sergei b5ab28aa3e Add bulk sensor operations API
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>
2026-01-31 16:40:36 -08:00

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;
}
}