188 lines
6.5 KiB
Plaintext
188 lines
6.5 KiB
Plaintext
// 设备管理相关:扫描、连接、断开、重连
|
||
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<void> {
|
||
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<boolean> {
|
||
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<void> {
|
||
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<boolean> {
|
||
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];
|
||
}
|
||
} |