Initial commit of akmon project
This commit is contained in:
344
uni_modules/ak-sbsrv/utssdk/app-harmony/service_manager.uts
Normal file
344
uni_modules/ak-sbsrv/utssdk/app-harmony/service_manager.uts
Normal file
@@ -0,0 +1,344 @@
|
||||
import type { BleService, BleCharacteristic, BleCharacteristicProperties, WriteCharacteristicOptions, AutoDiscoverAllResult, BleDataReceivedCallback } from '../interface.uts';
|
||||
import type { BleDevice } from '../interface.uts';
|
||||
import ble from '@ohos.bluetooth.ble';
|
||||
import type { BusinessError } from '@ohos.base';
|
||||
import { DeviceManager } from './device_manager.uts';
|
||||
|
||||
type PendingRead = {
|
||||
resolve: (data: ArrayBuffer) => void;
|
||||
reject: (err?: any) => void;
|
||||
timer?: number;
|
||||
};
|
||||
|
||||
type CharacteristicChange = {
|
||||
serviceUuid?: string;
|
||||
characteristicUuid?: string;
|
||||
value?: ArrayBuffer | Uint8Array | number[];
|
||||
};
|
||||
|
||||
function toArrayBuffer(value: ArrayBuffer | Uint8Array | number[] | null | undefined): ArrayBuffer {
|
||||
if (value == null) return new ArrayBuffer(0);
|
||||
if (value instanceof ArrayBuffer) return value;
|
||||
if (value instanceof Uint8Array) {
|
||||
return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const buf = new Uint8Array(value.length);
|
||||
for (let i = 0; i < value.length; i++) buf[i] = value[i] ?? 0;
|
||||
return buf.buffer;
|
||||
}
|
||||
return new ArrayBuffer(0);
|
||||
}
|
||||
|
||||
function toUint8Array(value: ArrayBuffer | Uint8Array | number[] | null | undefined): Uint8Array {
|
||||
if (value instanceof Uint8Array) {
|
||||
return value;
|
||||
}
|
||||
return new Uint8Array(toArrayBuffer(value));
|
||||
}
|
||||
|
||||
function buildProperties(raw: any): BleCharacteristicProperties {
|
||||
const props = raw?.properties ?? raw ?? {};
|
||||
const read = props.read === true;
|
||||
const write = props.write === true || props.writeWithoutResponse === true;
|
||||
const notify = props.notify === true;
|
||||
const indicate = props.indicate === true;
|
||||
const writeNoRsp = props.writeWithoutResponse === true || props.writeNoResponse === true;
|
||||
return {
|
||||
read,
|
||||
write,
|
||||
notify,
|
||||
indicate,
|
||||
writeWithoutResponse: writeNoRsp,
|
||||
canRead: read,
|
||||
canWrite: write || writeNoRsp,
|
||||
canNotify: notify || indicate
|
||||
};
|
||||
}
|
||||
|
||||
export class ServiceManager {
|
||||
private static instance: ServiceManager | null = null;
|
||||
private services = new Map<string, BleService[]>();
|
||||
private characteristics = new Map<string, Map<string, BleCharacteristic[]>>();
|
||||
private pendingReads = new Map<string, PendingRead>();
|
||||
private notifyCallbacks = new Map<string, BleDataReceivedCallback>();
|
||||
private boundGattDevices = new Set<string>();
|
||||
private deviceManager = DeviceManager.getInstance();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): ServiceManager {
|
||||
if (ServiceManager.instance == null) {
|
||||
ServiceManager.instance = new ServiceManager();
|
||||
}
|
||||
return ServiceManager.instance!;
|
||||
}
|
||||
|
||||
private getGattOrThrow(deviceId: string): any {
|
||||
const gatt = this.deviceManager.getGatt(deviceId);
|
||||
if (gatt == null) throw new Error('设备未连接');
|
||||
return gatt;
|
||||
}
|
||||
|
||||
private cacheServices(deviceId: string, services: any[]): BleService[] {
|
||||
const list: BleService[] = [];
|
||||
for (let i = 0; i < services.length; i++) {
|
||||
const svc = services[i];
|
||||
if (svc == null) continue;
|
||||
const uuid = svc.uuid ?? svc.serviceUuid ?? '';
|
||||
if (!uuid) continue;
|
||||
list.push({ uuid, isPrimary: svc.isPrimary === true });
|
||||
}
|
||||
this.services.set(deviceId, list);
|
||||
return list;
|
||||
}
|
||||
|
||||
private async ensureServices(deviceId: string, gatt: any): Promise<BleService[]> {
|
||||
const cached = this.services.get(deviceId);
|
||||
if (cached != null && cached.length > 0) return cached;
|
||||
try {
|
||||
await gatt.discoverServices?.();
|
||||
} catch (e) {
|
||||
console.warn('[AKBLE][Harmony] discoverServices failed', e);
|
||||
}
|
||||
let services: any[] = [];
|
||||
try {
|
||||
services = gatt.getServices?.() ?? [];
|
||||
} catch (e) {
|
||||
console.warn('[AKBLE][Harmony] getServices failed', e);
|
||||
}
|
||||
if (!Array.isArray(services)) services = [];
|
||||
return this.cacheServices(deviceId, services);
|
||||
}
|
||||
|
||||
private cacheCharacteristics(deviceId: string, serviceId: string, chars: any[]): BleCharacteristic[] {
|
||||
const list: BleCharacteristic[] = [];
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
const ch = chars[i];
|
||||
if (ch == null) continue;
|
||||
const uuid = ch.uuid ?? ch.characteristicUuid ?? '';
|
||||
if (!uuid) continue;
|
||||
list.push({
|
||||
uuid,
|
||||
service: { uuid: serviceId, isPrimary: true },
|
||||
properties: buildProperties(ch)
|
||||
});
|
||||
}
|
||||
let map = this.characteristics.get(deviceId);
|
||||
if (map == null) {
|
||||
map = new Map<string, BleCharacteristic[]>();
|
||||
this.characteristics.set(deviceId, map);
|
||||
}
|
||||
map.set(serviceId, list);
|
||||
return list;
|
||||
}
|
||||
|
||||
private async ensureCharacteristics(deviceId: string, serviceId: string, gatt: any): Promise<BleCharacteristic[]> {
|
||||
const perDevice = this.characteristics.get(deviceId);
|
||||
const cached = perDevice != null ? perDevice.get(serviceId) : null;
|
||||
if (cached != null && cached.length > 0) return cached;
|
||||
let list: any[] = [];
|
||||
try {
|
||||
list = gatt.getCharacteristics?.(serviceId) ?? [];
|
||||
} catch (e) {
|
||||
console.warn('[AKBLE][Harmony] getCharacteristics failed', e);
|
||||
}
|
||||
if (!Array.isArray(list)) list = [];
|
||||
return this.cacheCharacteristics(deviceId, serviceId, list);
|
||||
}
|
||||
|
||||
private bindGattListener(deviceId: string, gatt: any) {
|
||||
if (this.boundGattDevices.has(deviceId)) return;
|
||||
this.boundGattDevices.add(deviceId);
|
||||
try {
|
||||
gatt.on?.('characteristicChange', (change: CharacteristicChange) => {
|
||||
try { this.handleCharacteristicChange(deviceId, change); } catch (err) { console.warn('[AKBLE][Harmony] notify handler error', err); }
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('[AKBLE][Harmony] bind characteristicChange failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
private pendingKey(deviceId: string, serviceId: string, characteristicId: string): string {
|
||||
return `${deviceId}|${serviceId}|${characteristicId}|read`;
|
||||
}
|
||||
|
||||
private notifyKey(deviceId: string, serviceId: string, characteristicId: string): string {
|
||||
return `${deviceId}|${serviceId}|${characteristicId}`;
|
||||
}
|
||||
|
||||
private handleCharacteristicChange(deviceId: string, change: CharacteristicChange) {
|
||||
const serviceId = change?.serviceUuid ?? '';
|
||||
const characteristicId = change?.characteristicUuid ?? '';
|
||||
if (!serviceId || !characteristicId) return;
|
||||
const buffer = toArrayBuffer(change?.value);
|
||||
const pending = this.pendingReads.get(this.pendingKey(deviceId, serviceId, characteristicId));
|
||||
if (pending != null) {
|
||||
this.pendingReads.delete(this.pendingKey(deviceId, serviceId, characteristicId));
|
||||
if (pending.timer != null) clearTimeout(pending.timer);
|
||||
try { pending.resolve(buffer); } catch (e) { }
|
||||
}
|
||||
const cb = this.notifyCallbacks.get(this.notifyKey(deviceId, serviceId, characteristicId));
|
||||
if (cb != null) {
|
||||
try { cb(toUint8Array(buffer)); } catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
async getServices(deviceId: string, callback?: (services: BleService[] | null, error?: Error) => void): Promise<BleService[]> {
|
||||
const gatt = this.getGattOrThrow(deviceId);
|
||||
this.bindGattListener(deviceId, gatt);
|
||||
try {
|
||||
const services = await this.ensureServices(deviceId, gatt);
|
||||
if (callback != null) callback(services, null);
|
||||
return services;
|
||||
} catch (err) {
|
||||
if (callback != null) callback(null, err as Error);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async getCharacteristics(deviceId: string, serviceId: string, callback?: (list: BleCharacteristic[] | null, error?: Error) => void): Promise<BleCharacteristic[]> {
|
||||
const gatt = this.getGattOrThrow(deviceId);
|
||||
this.bindGattListener(deviceId, gatt);
|
||||
try {
|
||||
await this.ensureServices(deviceId, gatt);
|
||||
const list = await this.ensureCharacteristics(deviceId, serviceId, gatt);
|
||||
if (callback != null) callback(list, null);
|
||||
return list;
|
||||
} catch (err) {
|
||||
if (callback != null) callback(null, err as Error);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async readCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<ArrayBuffer> {
|
||||
const gatt = this.getGattOrThrow(deviceId);
|
||||
this.bindGattListener(deviceId, gatt);
|
||||
return new Promise<ArrayBuffer>((resolve, reject) => {
|
||||
const key = this.pendingKey(deviceId, serviceId, characteristicId);
|
||||
const timer = setTimeout(() => {
|
||||
this.pendingReads.delete(key);
|
||||
reject(new Error('读取超时'));
|
||||
}, 10000);
|
||||
this.pendingReads.set(key, { resolve, reject, timer });
|
||||
try {
|
||||
const result = gatt.readCharacteristicValue?.({ serviceUuid: serviceId, characteristicUuid: characteristicId });
|
||||
if (result instanceof Promise) {
|
||||
result.then((value: any) => {
|
||||
const buf = toArrayBuffer(value?.value ?? value);
|
||||
const pending = this.pendingReads.get(key);
|
||||
if (pending != null) {
|
||||
this.pendingReads.delete(key);
|
||||
if (pending.timer != null) clearTimeout(pending.timer);
|
||||
try { pending.resolve(buf); } catch (e) { }
|
||||
}
|
||||
}).catch((err: BusinessError) => {
|
||||
this.pendingReads.delete(key);
|
||||
clearTimeout(timer);
|
||||
reject(err);
|
||||
});
|
||||
} else if (result != null) {
|
||||
const buf = toArrayBuffer((result as any)?.value ?? result);
|
||||
this.pendingReads.delete(key);
|
||||
clearTimeout(timer);
|
||||
resolve(buf);
|
||||
}
|
||||
} catch (e) {
|
||||
this.pendingReads.delete(key);
|
||||
clearTimeout(timer);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, value: Uint8Array | ArrayBuffer, options?: WriteCharacteristicOptions): Promise<boolean> {
|
||||
const gatt = this.getGattOrThrow(deviceId);
|
||||
const payload = value instanceof Uint8Array ? value : new Uint8Array(value ?? new ArrayBuffer(0));
|
||||
const writeType = options?.forceWriteTypeNoResponse === true || options?.waitForResponse === false
|
||||
? ble.GattWriteType?.WRITE_TYPE_NO_RESPONSE ?? 1
|
||||
: ble.GattWriteType?.WRITE_TYPE_DEFAULT ?? 0;
|
||||
try {
|
||||
const res = gatt.writeCharacteristicValue?.({
|
||||
serviceUuid: serviceId,
|
||||
characteristicUuid: characteristicId,
|
||||
value: payload.buffer.slice(payload.byteOffset, payload.byteOffset + payload.byteLength),
|
||||
writeType
|
||||
});
|
||||
if (res instanceof Promise) {
|
||||
await res;
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.warn('[AKBLE][Harmony] writeCharacteristic failed', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async subscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, callback: BleDataReceivedCallback): Promise<void> {
|
||||
const gatt = this.getGattOrThrow(deviceId);
|
||||
this.bindGattListener(deviceId, gatt);
|
||||
const key = this.notifyKey(deviceId, serviceId, characteristicId);
|
||||
this.notifyCallbacks.set(key, callback);
|
||||
try {
|
||||
const res = gatt.setCharacteristicValueChangeNotification?.({
|
||||
serviceUuid: serviceId,
|
||||
characteristicUuid: characteristicId,
|
||||
enable: true
|
||||
});
|
||||
if (res instanceof Promise) {
|
||||
await res;
|
||||
}
|
||||
} catch (e) {
|
||||
this.notifyCallbacks.delete(key);
|
||||
console.warn('[AKBLE][Harmony] enable notify failed', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async unsubscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<void> {
|
||||
const gatt = this.getGattOrThrow(deviceId);
|
||||
this.bindGattListener(deviceId, gatt);
|
||||
this.notifyCallbacks.delete(this.notifyKey(deviceId, serviceId, characteristicId));
|
||||
try {
|
||||
const res = gatt.setCharacteristicValueChangeNotification?.({
|
||||
serviceUuid: serviceId,
|
||||
characteristicUuid: characteristicId,
|
||||
enable: false
|
||||
});
|
||||
if (res instanceof Promise) {
|
||||
await res;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[AKBLE][Harmony] disable notify failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
async autoDiscoverAll(deviceId: string): Promise<AutoDiscoverAllResult> {
|
||||
const gatt = this.getGattOrThrow(deviceId);
|
||||
this.bindGattListener(deviceId, gatt);
|
||||
const services = await this.ensureServices(deviceId, gatt);
|
||||
const characteristics: BleCharacteristic[] = [];
|
||||
for (let i = 0; i < services.length; i++) {
|
||||
const svc = services[i];
|
||||
const list = await this.ensureCharacteristics(deviceId, svc.uuid, gatt);
|
||||
for (let j = 0; j < list.length; j++) {
|
||||
characteristics.push(list[j]);
|
||||
}
|
||||
}
|
||||
return { services, characteristics };
|
||||
}
|
||||
|
||||
async subscribeAllNotifications(deviceId: string, callback: BleDataReceivedCallback): Promise<void> {
|
||||
const { services, characteristics } = await this.autoDiscoverAll(deviceId);
|
||||
for (let i = 0; i < characteristics.length; i++) {
|
||||
const ch = characteristics[i];
|
||||
if (ch.properties != null && (ch.properties.notify || ch.properties.indicate)) {
|
||||
try {
|
||||
await this.subscribeCharacteristic(deviceId, ch.service.uuid, ch.uuid, callback);
|
||||
} catch (e) {
|
||||
console.warn('[AKBLE][Harmony] subscribeAll skip', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user