Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

View File

@@ -0,0 +1,281 @@
import type {
BleDevice,
BleConnectionState,
BleEvent,
BleEventCallback,
BleEventPayload,
BleConnectOptionsExt,
AutoBleInterfaces,
SendDataPayload,
BleOptions,
MultiProtocolDevice,
BleProtocolType,
ScanDevicesOptions
} from '../interface.uts';
import { ProtocolHandler } from '../protocol_handler.uts';
import { BluetoothService } from '../interface.uts';
import { DeviceManager } from './device_manager.uts';
type RawProtocolHandler = {
protocol?: BleProtocolType;
scanDevices?: (options?: ScanDevicesOptions) => Promise<void>;
connect?: (device: BleDevice, options?: BleConnectOptionsExt) => Promise<void>;
disconnect?: (device: BleDevice) => Promise<void>;
sendData?: (device: BleDevice, payload?: SendDataPayload, options?: BleOptions) => Promise<void>;
autoConnect?: (device: BleDevice, options?: BleConnectOptionsExt) => Promise<AutoBleInterfaces>;
}
class DeviceContext {
device: BleDevice;
protocol: BleProtocolType;
state: BleConnectionState;
handler: ProtocolHandler;
constructor(device: BleDevice, protocol: BleProtocolType, handler: ProtocolHandler) {
this.device = device;
this.protocol = protocol;
this.state = 0;
this.handler = handler;
}
}
const deviceMap = new Map<string, DeviceContext>();
let activeProtocol: BleProtocolType = 'standard';
let activeHandler: ProtocolHandler | null = null;
const eventListeners = new Map<BleEvent, Set<BleEventCallback>>();
let defaultBluetoothService: BluetoothService | null = null;
let connectionHooked = false;
function emit(event: BleEvent, payload: BleEventPayload) {
const listeners = eventListeners.get(event);
if (listeners != null) {
listeners.forEach(cb => {
try { cb(payload); } catch (e) { }
});
}
}
class ProtocolHandlerWrapper extends ProtocolHandler {
private _raw: RawProtocolHandler | null;
constructor(raw?: RawProtocolHandler, bluetoothService?: BluetoothService) {
super(bluetoothService);
this._raw = raw ?? null;
}
override async scanDevices(options?: ScanDevicesOptions): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.scanDevices == 'function') {
await rawTyped.scanDevices(options);
}
return;
}
override async connect(device: BleDevice, options?: BleConnectOptionsExt): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.connect == 'function') {
await rawTyped.connect(device, options);
}
return;
}
override async disconnect(device: BleDevice): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.disconnect == 'function') {
await rawTyped.disconnect(device);
}
return;
}
override async sendData(device: BleDevice, payload?: SendDataPayload, options?: BleOptions): Promise<void> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.sendData == 'function') {
await rawTyped.sendData(device, payload, options);
}
return;
}
override async autoConnect(device: BleDevice, options?: BleConnectOptionsExt): Promise<AutoBleInterfaces> {
const rawTyped = this._raw;
if (rawTyped != null && typeof rawTyped.autoConnect == 'function') {
return await rawTyped.autoConnect(device, options);
}
return { serviceId: '', writeCharId: '', notifyCharId: '' };
}
}
function isRawProtocolHandler(x: any): boolean {
if (x == null || typeof x !== 'object') return false;
const r = x as Record<string, unknown>;
if (typeof r['scanDevices'] == 'function') return true;
if (typeof r['connect'] == 'function') return true;
if (typeof r['disconnect'] == 'function') return true;
if (typeof r['sendData'] == 'function') return true;
if (typeof r['autoConnect'] == 'function') return true;
if (typeof r['protocol'] == 'string') return true;
return false;
}
function ensureConnectionHook() {
if (connectionHooked) return;
connectionHooked = true;
const dm = DeviceManager.getInstance();
dm.onConnectionStateChange((deviceId, state) => {
let handled = false;
deviceMap.forEach((ctx) => {
if (ctx.device.deviceId == deviceId) {
ctx.state = state;
emit('connectionStateChanged', { event: 'connectionStateChanged', device: ctx.device, protocol: ctx.protocol, state });
handled = true;
}
});
if (!handled) {
emit('connectionStateChanged', { event: 'connectionStateChanged', device: { deviceId, name: '', rssi: 0 }, protocol: activeProtocol, state });
}
});
}
export const registerProtocolHandler = (handler: any) => {
if (handler == null) return;
let proto: BleProtocolType = 'standard';
if (handler instanceof ProtocolHandler) {
try { proto = (handler as ProtocolHandler).protocol as BleProtocolType; } catch (e) { }
activeHandler = handler as ProtocolHandler;
} else if (isRawProtocolHandler(handler)) {
try { proto = (handler as RawProtocolHandler).protocol as BleProtocolType; } catch (e) { }
activeHandler = new ProtocolHandlerWrapper(handler as RawProtocolHandler, defaultBluetoothService);
(activeHandler as ProtocolHandler).protocol = proto;
} else {
console.warn('[AKBLE][Harmony] registerProtocolHandler unsupported handler', handler);
return;
}
activeProtocol = proto;
ensureConnectionHook();
}
export const scanDevices = async (options?: ScanDevicesOptions): Promise<void> => {
ensureDefaultProtocolHandler();
if (activeHandler == null) {
console.log('[AKBLE][Harmony] no active scan handler');
return;
}
const handler = activeHandler as ProtocolHandler;
const original = options ?? null;
const scanOptions: ScanDevicesOptions = {} as ScanDevicesOptions;
if (original != null) {
if (original.protocols != null) scanOptions.protocols = original.protocols;
if (original.optionalServices != null) scanOptions.optionalServices = original.optionalServices;
if (original.timeout != null) scanOptions.timeout = original.timeout;
}
const userFound = original?.onDeviceFound ?? null;
scanOptions.onDeviceFound = (device: BleDevice) => {
emit('deviceFound', { event: 'deviceFound', device });
if (userFound != null) {
try { userFound(device); } catch (err) { }
}
};
const userFinished = original?.onScanFinished ?? null;
scanOptions.onScanFinished = () => {
emit('scanFinished', { event: 'scanFinished' });
if (userFinished != null) {
try { userFinished(); } catch (err) { }
}
};
try {
await handler.scanDevices(scanOptions);
} catch (e) {
console.warn('[AKBLE][Harmony] scan handler error', e);
}
}
export const connectDevice = async (deviceId: string, protocol: BleProtocolType, options?: BleConnectOptionsExt): Promise<void> => {
const handler = activeHandler;
if (handler == null) throw new Error('No protocol handler');
const device: BleDevice = { deviceId, name: '', rssi: 0 };
await handler.connect(device, options);
const ctx = new DeviceContext(device, protocol, handler);
ctx.state = 2;
deviceMap.set(getDeviceKey(deviceId, protocol), ctx);
emit('connectionStateChanged', { event: 'connectionStateChanged', device, protocol, state: 2 });
}
export const disconnectDevice = async (deviceId: string, protocol: BleProtocolType): Promise<void> => {
const ctx = deviceMap.get(getDeviceKey(deviceId, protocol));
if (ctx == null || ctx.handler == null) return;
await ctx.handler.disconnect(ctx.device);
ctx.state = 0;
emit('connectionStateChanged', { event: 'connectionStateChanged', device: ctx.device, protocol, state: 0 });
deviceMap.delete(getDeviceKey(deviceId, protocol));
}
export const sendData = async (payload: SendDataPayload, options?: BleOptions): Promise<void> => {
const ctx = deviceMap.get(getDeviceKey(payload.deviceId, payload.protocol));
if (ctx == null) throw new Error('Device not connected');
if (ctx.handler == null) throw new Error('sendData not supported for this protocol');
await ctx.handler.sendData(ctx.device, payload, options);
emit('dataSent', { event: 'dataSent', device: ctx.device, protocol: payload.protocol, data: payload.data });
}
export const getConnectedDevices = (): MultiProtocolDevice[] => {
const result: MultiProtocolDevice[] = [];
deviceMap.forEach((ctx: DeviceContext) => {
result.push({
deviceId: ctx.device.deviceId,
name: ctx.device.name,
rssi: ctx.device.rssi,
protocol: ctx.protocol
});
});
return result;
}
export const getConnectionState = (deviceId: string, protocol: BleProtocolType): BleConnectionState => {
const ctx = deviceMap.get(getDeviceKey(deviceId, protocol));
if (ctx == null) return 0;
return ctx.state;
}
export const on = (event: BleEvent, callback: BleEventCallback) => {
if (!eventListeners.has(event)) eventListeners.set(event, new Set());
eventListeners.get(event)!.add(callback);
}
export const off = (event: BleEvent, callback?: BleEventCallback) => {
if (callback == null) {
eventListeners.delete(event);
} else {
eventListeners.get(event)?.delete(callback as BleEventCallback);
}
}
function getDeviceKey(deviceId: string, protocol: BleProtocolType): string {
return `${deviceId}|${protocol}`;
}
export const autoConnect = async (deviceId: string, protocol: BleProtocolType, options?: BleConnectOptionsExt): Promise<AutoBleInterfaces> => {
const handler = activeHandler;
if (handler == null) throw new Error('autoConnect not supported for this protocol');
const device: BleDevice = { deviceId, name: '', rssi: 0 };
return await handler.autoConnect(device, options) as AutoBleInterfaces;
}
function ensureDefaultProtocolHandler(): void {
if (activeHandler != null) return;
const service = defaultBluetoothService;
if (service == null) return;
try {
const dm = DeviceManager.getInstance();
const raw: RawProtocolHandler = {
protocol: 'standard',
scanDevices: (options?: ScanDevicesOptions) => dm.startScan(options ?? {} as ScanDevicesOptions),
connect: (device, options?: BleConnectOptionsExt) => dm.connectDevice(device.deviceId, options),
disconnect: (device) => dm.disconnectDevice(device.deviceId),
autoConnect: () => Promise.resolve({ serviceId: '', writeCharId: '', notifyCharId: '' })
};
const wrapper = new ProtocolHandlerWrapper(raw, service);
activeHandler = wrapper;
activeProtocol = raw.protocol as BleProtocolType;
ensureConnectionHook();
console.log('[AKBLE][Harmony] default protocol handler registered', activeProtocol);
} catch (e) {
console.warn('[AKBLE][Harmony] register default protocol handler failed', e);
}
}
export const setDefaultBluetoothService = (service: BluetoothService) => {
defaultBluetoothService = service;
ensureDefaultProtocolHandler();
};

View File

@@ -0,0 +1,6 @@
{
"dependencies": [
"@ohos.bluetooth.ble",
"@ohos.base"
]
}

View File

@@ -0,0 +1,280 @@
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;
}
}

View File

@@ -0,0 +1,9 @@
import type { DfuManagerType, DfuOptions } from '../interface.uts';
class HarmonyDfuManager implements DfuManagerType {
async startDfu(_deviceId: string, _firmwareBytes: Uint8Array, _options?: DfuOptions): Promise<void> {
throw new Error('鸿蒙平台暂未实现 DFU 功能');
}
}
export const dfuManager = new HarmonyDfuManager();

View File

@@ -0,0 +1,90 @@
import * as BluetoothManager from './bluetooth_manager.uts';
import { ServiceManager } from './service_manager.uts';
import type { ScanDevicesOptions, BleConnectOptionsExt, MultiProtocolDevice, BleEvent, BleEventCallback, BleService, BleCharacteristic, WriteCharacteristicOptions, AutoBleInterfaces, BleDataReceivedCallback, BleProtocolType, BluetoothService as BluetoothServiceContract } from '../interface.uts';
const serviceManager = ServiceManager.getInstance();
class HarmonyBluetoothService implements BluetoothServiceContract {
scanDevices(options?: ScanDevicesOptions | null): Promise<void> {
return BluetoothManager.scanDevices(options ?? null);
}
async connectDevice(deviceId: string, protocol?: string, options?: BleConnectOptionsExt | null): Promise<void> {
const proto = (protocol != null && protocol !== '') ? (protocol as BleProtocolType) : 'standard';
return BluetoothManager.connectDevice(deviceId, proto, options ?? null);
}
async disconnectDevice(deviceId: string, protocol?: string): Promise<void> {
const proto = (protocol != null && protocol !== '') ? (protocol as BleProtocolType) : 'standard';
return BluetoothManager.disconnectDevice(deviceId, proto);
}
getConnectedDevices(): MultiProtocolDevice[] {
return BluetoothManager.getConnectedDevices();
}
on(event: BleEvent | string, callback: BleEventCallback): void {
BluetoothManager.on(event as BleEvent, callback);
}
off(event: BleEvent | string, callback?: BleEventCallback | null): void {
BluetoothManager.off(event as BleEvent, callback ?? null);
}
getServices(deviceId: string): Promise<BleService[]> {
return serviceManager.getServices(deviceId);
}
getCharacteristics(deviceId: string, serviceId: string): Promise<BleCharacteristic[]> {
return serviceManager.getCharacteristics(deviceId, serviceId);
}
async getAutoBleInterfaces(deviceId: string): Promise<AutoBleInterfaces> {
const services = await this.getServices(deviceId);
if (services.length == 0) throw new Error('未发现服务');
let targetService = services[0].uuid;
for (let i = 0; i < services.length; i++) {
const uuid = services[i].uuid ?? '';
if (/^bae/i.test(uuid)) {
targetService = uuid;
break;
}
}
const characteristics = await this.getCharacteristics(deviceId, targetService);
if (characteristics.length == 0) throw new Error('未发现特征值');
let writeCharId = '';
let notifyCharId = '';
for (let i = 0; i < characteristics.length; i++) {
const c = characteristics[i];
if ((writeCharId == null || writeCharId == '') && c.properties != null && (c.properties.write || c.properties.writeWithoutResponse == true)) {
writeCharId = c.uuid;
}
if ((notifyCharId == null || notifyCharId == '') && c.properties != null && (c.properties.notify || c.properties.indicate)) {
notifyCharId = c.uuid;
}
}
if (writeCharId == '' || notifyCharId == '') throw new Error('未找到合适的写入或通知特征');
return { serviceId: targetService, writeCharId, notifyCharId };
}
subscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, onData: BleDataReceivedCallback): Promise<void> {
return serviceManager.subscribeCharacteristic(deviceId, serviceId, characteristicId, onData);
}
readCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<ArrayBuffer> {
return serviceManager.readCharacteristic(deviceId, serviceId, characteristicId);
}
writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, value: Uint8Array | ArrayBuffer, options?: WriteCharacteristicOptions): Promise<boolean> {
return serviceManager.writeCharacteristic(deviceId, serviceId, characteristicId, value, options);
}
unsubscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<void> {
return serviceManager.unsubscribeCharacteristic(deviceId, serviceId, characteristicId);
}
autoDiscoverAll(deviceId: string): Promise<any> {
return serviceManager.autoDiscoverAll(deviceId);
}
subscribeAllNotifications(deviceId: string, onData: BleDataReceivedCallback): Promise<void> {
return serviceManager.subscribeAllNotifications(deviceId, onData);
}
}
export class BluetoothServiceShape extends HarmonyBluetoothService {}
const bluetoothServiceInstance = new BluetoothServiceShape();
BluetoothManager.setDefaultBluetoothService(bluetoothServiceInstance);
export const bluetoothService: BluetoothServiceContract = bluetoothServiceInstance;
export function getBluetoothService(): BluetoothServiceShape {
return bluetoothServiceInstance;
}
export { dfuManager } from './dfu_manager.uts';

View 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);
}
}
}
}
}