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(); private characteristics = new Map>(); private pendingReads = new Map(); private notifyCallbacks = new Map(); private boundGattDevices = new Set(); 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 { 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(); this.characteristics.set(deviceId, map); } map.set(serviceId, list); return list; } private async ensureCharacteristics(deviceId: string, serviceId: string, gatt: any): Promise { 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 { 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 { 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 { const gatt = this.getGattOrThrow(deviceId); this.bindGattListener(deviceId, gatt); return new Promise((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 { 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 { 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 { 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 { 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 { 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); } } } } }