Initial commit of akmon project
This commit is contained in:
237
uni_modules/ak-sbsrv/utssdk/web/device-manager.uts
Normal file
237
uni_modules/ak-sbsrv/utssdk/web/device-manager.uts
Normal file
@@ -0,0 +1,237 @@
|
||||
// 设备管理相关:扫描、连接、断开、重连
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user