// 设备管理相关:扫描、连接、断开、重连 import { BleDevice, BLE_CONNECTION_STATE } from '../interface.uts'; import type { BleConnectOptionsExt } from '../interface.uts'; export class DeviceManager { private devices = {}; private servers = {}; private connectionStates = {}; private allowedServices = {}; private reconnectAttempts: number = 0; private maxReconnectAttempts: number = 5; private reconnectDelay: number = 2000; private reconnectTimeoutId: number = 0; private autoReconnect: boolean = false; private connectionStateChangeListeners: Function[] = []; private deviceFoundListeners: ((device: BleDevice) => void)[] = []; private scanFinishedListeners: (() => void)[] = []; onDeviceFound(listener: (device: BleDevice) => void) { this.deviceFoundListeners.push(listener); } onScanFinished(listener: () => void) { this.scanFinishedListeners.push(listener); } private emitDeviceFound(device: BleDevice) { for (const listener of this.deviceFoundListeners) { try { listener(device); } catch (e) {} } } private emitScanFinished() { for (const listener of this.scanFinishedListeners) { try { listener(); } catch (e) {} } } async startScan(options?: { optionalServices?: string[] } ): Promise { if (!navigator.bluetooth) throw new Error('Web Bluetooth API not supported'); try { const scanOptions: any = { acceptAllDevices: true }; // allow callers to request optionalServices (required by Web Bluetooth to access custom services) if (options && Array.isArray(options.optionalServices) && options.optionalServices.length > 0) { scanOptions.optionalServices = options.optionalServices; } // Log the exact options passed to requestDevice for debugging optionalServices propagation try { console.log('[DeviceManager] requestDevice options:', JSON.stringify(scanOptions)); } catch (e) { console.log('[DeviceManager] requestDevice options (raw):', scanOptions); } const device = await navigator.bluetooth.requestDevice(scanOptions); try { console.log('[DeviceManager] requestDevice result:', device); } catch (e) { console.log('[DeviceManager] requestDevice result (raw):', device); } if (device) { console.log(device) // 格式化 deviceId 为 MAC 地址格式 const formatDeviceId = (id: string): string => { // 如果是12位16进制字符串(如 'AABBCCDDEEFF'),转为 'AA:BB:CC:DD:EE:FF' if (/^[0-9A-Fa-f]{12}$/.test(id)) { return id.match(/.{1,2}/g)!.join(":").toUpperCase(); } // 如果是base64,无法直接转MAC,保留原样 // 你可以根据实际情况扩展此处 return id; }; const isConnected = !!this.servers[device.id]; const formattedId = formatDeviceId(device.id); const bleDevice = { deviceId: formattedId, name: device.name, connected: isConnected }; this.devices[formattedId] = device; this.emitDeviceFound(bleDevice); } this.emitScanFinished(); } catch (e) { this.emitScanFinished(); throw e; } } onConnectionStateChange(listener: (deviceId: string, state: string) => void) { this.connectionStateChangeListeners.push(listener); } private emitConnectionStateChange(deviceId: string, state: string) { for (const listener of this.connectionStateChangeListeners) { try { listener(deviceId, state); } catch (e) { // 忽略单个回调异常 } } } async connectDevice(deviceId: string, options?: BleConnectOptionsExt): Promise { this.autoReconnect = options?.autoReconnect ?? false; try { const device = this.devices[deviceId]; if (!device) throw new Error('设备未找到'); const server = await device.gatt.connect(); this.servers[deviceId] = server; this.connectionStates[deviceId] = BLE_CONNECTION_STATE.CONNECTED; this.reconnectAttempts = 0; this.emitConnectionStateChange(deviceId, 'connected'); // 监听物理断开 if (device.gatt) { device.gatt.onconnectionstatechanged = null; device.gatt.onconnectionstatechanged = () => { if (!device.gatt.connected) { this.emitConnectionStateChange(deviceId, 'disconnected'); } }; } return true; } catch (error) { if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) { return this.scheduleReconnect(deviceId); } throw error; } } async disconnectDevice(deviceId: string): Promise { const device = this.devices[deviceId]; if (!device) throw new Error('设备未找到'); try { if (device.gatt && device.gatt.connected) { device.gatt.disconnect(); } delete this.servers[deviceId]; this.connectionStates[deviceId] = BLE_CONNECTION_STATE.DISCONNECTED; this.emitConnectionStateChange(deviceId, 'disconnected'); } catch (e) { throw e; } } getConnectedDevices(): BleDevice[] { const connectedDevices: BleDevice[] = []; for (const deviceId in this.servers) { const device = this.devices[deviceId]; if (device) { connectedDevices.push({ deviceId: device.id, name: device.name || '未知设备', connected: true }); } } return connectedDevices; } handleDisconnect(deviceId: string) { this.connectionStates[deviceId] = BLE_CONNECTION_STATE.DISCONNECTED; this.emitConnectionStateChange(deviceId, 'disconnected'); if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) { this.scheduleReconnect(deviceId); } } private scheduleReconnect(deviceId: string): Promise { this.reconnectAttempts++; return new Promise((resolve, reject) => { this.reconnectTimeoutId = setTimeout(() => { this.connectDevice(deviceId, { autoReconnect: true }) .then(resolve) .catch(reject); }, this.reconnectDelay); }); } cancelReconnect() { if (this.reconnectTimeoutId) { clearTimeout(this.reconnectTimeoutId); this.reconnectTimeoutId = 0; } this.autoReconnect = false; this.reconnectAttempts = 0; } setMaxReconnectAttempts(attempts: number) { this.maxReconnectAttempts = attempts; } setReconnectDelay(delay: number) { this.reconnectDelay = delay; } isDeviceConnected(deviceId: string): boolean { return !!this.servers[deviceId]; } }