Files
akmon/uni_modules/ak-sbsrv/utssdk/web/device-manager.uts
2026-01-20 08:04:15 +08:00

237 lines
8.5 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 设备管理相关:扫描、连接、断开、重连
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 key = this.resolveDeviceKey(deviceId)
console.log(key,deviceId)
if (!key) {
// better debugging: include a short sample of known device keys
const known = Object.keys(this.devices || {}).slice(0, 20)
throw new Error(`设备未找到: ${deviceId}; 已知设备: ${known.join(',')}`)
}
const device = this.devices[key]
const server = await device.gatt.connect();
this.servers[key] = server;
this.connectionStates[key] = BLE_CONNECTION_STATE.CONNECTED;
this.reconnectAttempts = 0;
this.emitConnectionStateChange(key, 'connected');
// 监听物理断开
if (device.gatt) {
device.gatt.onconnectionstatechanged = null;
device.gatt.onconnectionstatechanged = () => {
if (!device.gatt.connected) {
this.emitConnectionStateChange(key, '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 key = this.resolveDeviceKey(deviceId)
if (!key) throw new Error('设备未找到')
const device = this.devices[key]
try {
if (device.gatt && device.gatt.connected) {
device.gatt.disconnect();
}
delete this.servers[key];
this.connectionStates[key] = BLE_CONNECTION_STATE.DISCONNECTED;
this.emitConnectionStateChange(key, '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) {
const key = this.resolveDeviceKey(deviceId)
const idKey = key ?? deviceId
this.connectionStates[idKey] = BLE_CONNECTION_STATE.DISCONNECTED;
this.emitConnectionStateChange(idKey, 'disconnected');
if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
this.scheduleReconnect(idKey);
}
}
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);
});
}
// Resolve a device key used in this.devices map from a given deviceId.
// Accepts formatted IDs (with ':'), raw ids or case-insensitive hex strings.
private resolveDeviceKey(deviceId: string): string | null {
// Accept either a string id or an object that contains the id (UTSJSONObject or plain object)
if (deviceId == null) return null
let idCandidate: any = deviceId
if (typeof deviceId !== 'string') {
try {
// UTSJSONObject has getString
if (typeof (deviceId as any).getString === 'function') {
const got = (deviceId as any).getString('deviceId') || (deviceId as any).getString('device_id') || (deviceId as any).getString('id')
if (got) idCandidate = got
} else if (typeof deviceId === 'object') {
const got = (deviceId as any).deviceId || (deviceId as any).device_id || (deviceId as any).id
if (got) idCandidate = got
}
} catch (e) { /* ignore extraction errors */ }
}
if (!idCandidate) return null
if (this.devices[idCandidate]) return idCandidate
const normalize = (s: string) => (s || '').toString().replace(/:/g, '').toUpperCase()
const target = normalize(idCandidate)
for (const k in this.devices) {
if (k === deviceId) return k
const dev = this.devices[k]
try {
if (dev && dev.id && normalize(dev.id) === target) return k
} catch (e) { }
if (normalize(k) === target) return k
}
return null
}
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];
}
// Public helper to obtain the GATT server for a device id using flexible matching
getServer(deviceId: string): any | null {
const key = this.resolveDeviceKey(deviceId)
if (!key) return null
return this.servers[key] ?? null
}
}