281 lines
8.4 KiB
Plaintext
281 lines
8.4 KiB
Plaintext
import type { BleDevice, BleConnectOptionsExt, BleConnectionState, BleConnectionStateChangeCallback, ScanDevicesOptions } from '../interface.uts';
|
|
import ble from '@ohos.bluetooth.ble';
|
|
import type { BusinessError } from '@ohos.base';
|
|
|
|
type PendingConnect = {
|
|
resolve: () => void;
|
|
reject: (err?: any) => void;
|
|
timer?: number;
|
|
};
|
|
|
|
function now(): number {
|
|
return Date.now();
|
|
}
|
|
|
|
export class DeviceManager {
|
|
private static instance: DeviceManager | null = null;
|
|
private central: any | null = null;
|
|
private devices = new Map<string, BleDevice>();
|
|
private connectionStates = new Map<string, BleConnectionState>();
|
|
private connectionListeners: BleConnectionStateChangeCallback[] = [];
|
|
private pendingConnects = new Map<string, PendingConnect>();
|
|
private scanOptions: ScanDevicesOptions | null = null;
|
|
private scanTimer: number | null = null;
|
|
private gattMap = new Map<string, any>();
|
|
private scanning: boolean = false;
|
|
private eventsBound: boolean = false;
|
|
|
|
private constructor() {}
|
|
|
|
static getInstance(): DeviceManager {
|
|
if (DeviceManager.instance == null) {
|
|
DeviceManager.instance = new DeviceManager();
|
|
}
|
|
return DeviceManager.instance!;
|
|
}
|
|
|
|
private ensureCentral(): any {
|
|
if (this.central != null) return this.central;
|
|
try {
|
|
this.central = ble.createBluetoothCentralManager();
|
|
this.bindCentralEvents();
|
|
} catch (e) {
|
|
console.warn('[AKBLE][Harmony] createBluetoothCentralManager failed', e);
|
|
throw e;
|
|
}
|
|
return this.central!;
|
|
}
|
|
|
|
private bindCentralEvents() {
|
|
if (this.eventsBound) return;
|
|
this.eventsBound = true;
|
|
const central = this.central;
|
|
if (central == null) return;
|
|
try {
|
|
central.on('scanResult', (result: any) => {
|
|
try { this.handleScanResult(result); } catch (e) { console.warn('[AKBLE][Harmony] scanResult handler error', e); }
|
|
});
|
|
} catch (e) {
|
|
console.warn('[AKBLE][Harmony] central.on scanResult failed', e);
|
|
}
|
|
try {
|
|
central.on('bleDeviceFind', (result: any) => {
|
|
try { this.handleScanResult(result); } catch (e) { console.warn('[AKBLE][Harmony] bleDeviceFind handler error', e); }
|
|
});
|
|
} catch (e) {
|
|
/* optional */
|
|
}
|
|
try {
|
|
central.on('BLEConnectionStateChange', (state: any) => {
|
|
try { this.handleConnectionEvent(state); } catch (err) { console.warn('[AKBLE][Harmony] connection event error', err); }
|
|
});
|
|
} catch (e) {
|
|
console.warn('[AKBLE][Harmony] central.on connection change failed', e);
|
|
}
|
|
}
|
|
|
|
private handleScanResult(result: any) {
|
|
const list: any[] = result?.devices ?? result ?? [];
|
|
if (!Array.isArray(list)) return;
|
|
for (let i = 0; i < list.length; i++) {
|
|
const item = list[i];
|
|
if (item == null) continue;
|
|
const deviceId: string = item.deviceId ?? item.device?.deviceId ?? '';
|
|
if (!deviceId) continue;
|
|
const name: string = item.name ?? item.deviceName ?? 'Unknown';
|
|
const rssi: number = item.rssi ?? item.RSSI ?? 0;
|
|
let device = this.devices.get(deviceId);
|
|
if (device == null) {
|
|
device = { deviceId, name, rssi, lastSeen: now() };
|
|
this.devices.set(deviceId, device);
|
|
} else {
|
|
device.name = name;
|
|
device.rssi = rssi;
|
|
device.lastSeen = now();
|
|
}
|
|
const cb = this.scanOptions?.onDeviceFound;
|
|
if (cb != null) {
|
|
try { cb(device); } catch (e) { console.warn('[AKBLE][Harmony] onDeviceFound error', e); }
|
|
}
|
|
}
|
|
}
|
|
|
|
private handleConnectionEvent(evt: any) {
|
|
const deviceId: string = evt?.deviceId ?? evt?.device?.deviceId ?? '';
|
|
if (!deviceId) return;
|
|
const connected = evt?.state === ble.BLEConnectionState.STATE_CONNECTED || evt?.connected === true;
|
|
const state: BleConnectionState = connected ? 2 : 0;
|
|
this.connectionStates.set(deviceId, state);
|
|
const pending = this.pendingConnects.get(deviceId);
|
|
if (pending != null) {
|
|
this.pendingConnects.delete(deviceId);
|
|
if (pending.timer != null) clearTimeout(pending.timer);
|
|
if (connected) {
|
|
try { pending.resolve(); } catch (e) { }
|
|
} else {
|
|
try { pending.reject(new Error('连接断开')); } catch (e) { }
|
|
}
|
|
}
|
|
for (let i = 0; i < this.connectionListeners.length; i++) {
|
|
const listener = this.connectionListeners[i];
|
|
try { listener(deviceId, state); } catch (e) { console.warn('[AKBLE][Harmony] listener error', e); }
|
|
}
|
|
}
|
|
|
|
async startScan(options: ScanDevicesOptions): Promise<void> {
|
|
const central = this.ensureCentral();
|
|
this.scanOptions = options ?? {} as ScanDevicesOptions;
|
|
if (this.scanning) {
|
|
await this.stopScan();
|
|
}
|
|
this.scanning = true;
|
|
if (this.scanTimer != null) {
|
|
clearTimeout(this.scanTimer);
|
|
this.scanTimer = null;
|
|
}
|
|
return new Promise<void>((resolve, reject) => {
|
|
try {
|
|
const filter = { interval: 0 } as any;
|
|
const res = central.startScan?.(filter) ?? central.startScan?.();
|
|
if (this.scanOptions?.timeout != null && this.scanOptions.timeout > 0) {
|
|
this.scanTimer = setTimeout(() => {
|
|
this.stopScanInternal();
|
|
}, this.scanOptions.timeout);
|
|
}
|
|
if (res instanceof Promise) {
|
|
res.then(() => resolve()).catch((err: BusinessError) => {
|
|
this.scanning = false;
|
|
reject(err);
|
|
});
|
|
} else {
|
|
resolve();
|
|
}
|
|
} catch (e) {
|
|
this.scanning = false;
|
|
reject(e);
|
|
}
|
|
});
|
|
}
|
|
|
|
async stopScan(): Promise<void> {
|
|
this.stopScanInternal();
|
|
return Promise.resolve();
|
|
}
|
|
|
|
private stopScanInternal() {
|
|
if (!this.scanning) return;
|
|
this.scanning = false;
|
|
try {
|
|
this.central?.stopScan?.();
|
|
} catch (e) {
|
|
console.warn('[AKBLE][Harmony] stopScan failed', e);
|
|
}
|
|
if (this.scanTimer != null) {
|
|
clearTimeout(this.scanTimer);
|
|
this.scanTimer = null;
|
|
}
|
|
const finished = this.scanOptions?.onScanFinished;
|
|
this.scanOptions = null;
|
|
if (finished != null) {
|
|
try { finished(); } catch (e) { console.warn('[AKBLE][Harmony] onScanFinished error', e); }
|
|
}
|
|
}
|
|
|
|
async connectDevice(deviceId: string, options?: BleConnectOptionsExt): Promise<void> {
|
|
this.ensureCentral();
|
|
const timeout = options?.timeout ?? 15000;
|
|
if (this.connectionStates.get(deviceId) == 2) {
|
|
return Promise.resolve();
|
|
}
|
|
return new Promise<void>((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
this.pendingConnects.delete(deviceId);
|
|
reject(new Error('连接超时'));
|
|
}, timeout);
|
|
this.pendingConnects.set(deviceId, { resolve, reject, timer });
|
|
try {
|
|
let gatt = this.gattMap.get(deviceId);
|
|
if (gatt == null) {
|
|
gatt = ble.createGattClientDevice(deviceId);
|
|
this.gattMap.set(deviceId, gatt);
|
|
}
|
|
const connectRes = gatt.connect?.(true);
|
|
if (connectRes instanceof Promise) {
|
|
connectRes.then(() => {
|
|
clearTimeout(timer);
|
|
this.pendingConnects.delete(deviceId);
|
|
this.connectionStates.set(deviceId, 2);
|
|
resolve();
|
|
}).catch((err: BusinessError) => {
|
|
clearTimeout(timer);
|
|
this.pendingConnects.delete(deviceId);
|
|
reject(err);
|
|
});
|
|
} else {
|
|
// Some implementations return immediate state; rely on connection event to resolve.
|
|
}
|
|
} catch (e) {
|
|
clearTimeout(timer);
|
|
this.pendingConnects.delete(deviceId);
|
|
reject(e);
|
|
}
|
|
});
|
|
}
|
|
|
|
async disconnectDevice(deviceId: string): Promise<void> {
|
|
const gatt = this.gattMap.get(deviceId);
|
|
if (gatt == null) return;
|
|
try {
|
|
const res = gatt.disconnect?.();
|
|
if (res instanceof Promise) {
|
|
await res;
|
|
}
|
|
} catch (e) {
|
|
console.warn('[AKBLE][Harmony] disconnect failed', e);
|
|
}
|
|
this.connectionStates.set(deviceId, 0);
|
|
for (let i = 0; i < this.connectionListeners.length; i++) {
|
|
const listener = this.connectionListeners[i];
|
|
try { listener(deviceId, 0); } catch (err) { console.warn('[AKBLE][Harmony] disconnect listener error', err); }
|
|
}
|
|
}
|
|
|
|
reconnectDevice(deviceId: string, options?: BleConnectOptionsExt): Promise<void> {
|
|
const attempts = options?.maxAttempts ?? 3;
|
|
const interval = options?.interval ?? 3000;
|
|
let count = 0;
|
|
const attempt = (): Promise<void> => {
|
|
return this.connectDevice(deviceId, options).catch((err) => {
|
|
count++;
|
|
if (count >= attempts) throw err;
|
|
return new Promise<void>((resolve) => {
|
|
setTimeout(() => resolve(attempt()), interval);
|
|
});
|
|
});
|
|
};
|
|
return attempt();
|
|
}
|
|
|
|
getConnectedDevices(): BleDevice[] {
|
|
const result: BleDevice[] = [];
|
|
this.devices.forEach((device, id) => {
|
|
if (this.connectionStates.get(id) == 2) {
|
|
result.push(device);
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
|
|
onConnectionStateChange(listener: BleConnectionStateChangeCallback) {
|
|
this.connectionListeners.push(listener);
|
|
}
|
|
|
|
getDevice(deviceId: string): BleDevice | null {
|
|
return this.devices.get(deviceId) ?? null;
|
|
}
|
|
|
|
getGatt(deviceId: string): any | null {
|
|
return this.gattMap.get(deviceId) ?? null;
|
|
}
|
|
}
|