Initial commit of akmon project
This commit is contained in:
483
uni_modules/ak-sbsrv/utssdk/app-ios/service_manager.uts
Normal file
483
uni_modules/ak-sbsrv/utssdk/app-ios/service_manager.uts
Normal file
@@ -0,0 +1,483 @@
|
||||
import type { BleService, BleCharacteristic, BleDataReceivedCallback, BleCharacteristicProperties, WriteCharacteristicOptions, AutoDiscoverAllResult } from '../interface.uts';
|
||||
import { CBPeripheral, CBService, CBCharacteristic, CBCharacteristicWriteType } from 'CoreBluetooth';
|
||||
import { Data, NSError } from 'Foundation';
|
||||
import { DeviceManager } from './device_manager.uts';
|
||||
|
||||
function toUint8Array(value: Uint8Array | ArrayBuffer): Uint8Array {
|
||||
if (value instanceof Uint8Array) return value;
|
||||
return new Uint8Array(value);
|
||||
}
|
||||
|
||||
function dataToUint8Array(data: Data | null): Uint8Array {
|
||||
if (data == null) return new Uint8Array(0);
|
||||
const base64 = data.base64EncodedString(options = 0);
|
||||
if (base64 == null) return new Uint8Array(0);
|
||||
const raw = atob(base64);
|
||||
const out = new Uint8Array(raw.length);
|
||||
for (let i = 0; i < raw.length; i++) {
|
||||
out[i] = raw.charCodeAt(i) & 0xff;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function uint8ArrayToData(bytes: Uint8Array): Data {
|
||||
if (bytes.length == 0) {
|
||||
return new Data();
|
||||
}
|
||||
let binary = '';
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
const base64 = btoa(binary);
|
||||
const data = new Data(base64Encoded = base64);
|
||||
return data != null ? data! : new Data();
|
||||
}
|
||||
|
||||
function iterateNSArray<T>(collection: any, handler: (item: T | null) => void) {
|
||||
if (collection == null) return;
|
||||
if (Array.isArray(collection)) {
|
||||
for (let i = 0; i < collection.length; i++) {
|
||||
handler(collection[i] as T);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const count = collection.count as number;
|
||||
if (typeof count === 'number') {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const item = collection.objectAtIndex(i);
|
||||
handler(item as T);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (e) { }
|
||||
try {
|
||||
const len = collection.length as number;
|
||||
if (typeof len === 'number') {
|
||||
for (let i = 0; i < len; i++) {
|
||||
handler(collection[i] as T);
|
||||
}
|
||||
}
|
||||
} catch (e2) { }
|
||||
}
|
||||
|
||||
type PendingCallback = {
|
||||
resolve: (data: any) => void;
|
||||
reject: (err?: any) => void;
|
||||
timer?: number;
|
||||
};
|
||||
|
||||
function makeCharProperties(flags: number): BleCharacteristicProperties {
|
||||
const read = (flags & 0x02) != 0;
|
||||
const write = (flags & 0x08) != 0;
|
||||
const notify = (flags & 0x10) != 0;
|
||||
const indicate = (flags & 0x20) != 0;
|
||||
const writeNoRsp = (flags & 0x04) != 0;
|
||||
return {
|
||||
read,
|
||||
write,
|
||||
notify,
|
||||
indicate,
|
||||
writeWithoutResponse: writeNoRsp,
|
||||
canRead: read,
|
||||
canWrite: write || writeNoRsp,
|
||||
canNotify: notify || indicate
|
||||
};
|
||||
}
|
||||
|
||||
function getCharPropertiesValue(characteristic: CBCharacteristic): number {
|
||||
try {
|
||||
const anyProps = characteristic.properties as any;
|
||||
if (anyProps != null && anyProps.rawValue != null) {
|
||||
return Number(anyProps.rawValue);
|
||||
}
|
||||
} catch (e) { }
|
||||
try {
|
||||
return Number((characteristic as any).properties);
|
||||
} catch (e2) { }
|
||||
return 0;
|
||||
}
|
||||
|
||||
export class ServiceManager {
|
||||
private static instance: ServiceManager | null = null;
|
||||
private services = new Map<string, BleService[]>();
|
||||
private characteristics = new Map<string, Map<string, BleCharacteristic[]>>();
|
||||
private serviceWaiters = new Map<string, ((list: BleService[] | null, error?: Error) => void)[]>();
|
||||
private characteristicWaiters = new Map<string, ((list: BleCharacteristic[] | null, error?: Error) => void)[]>();
|
||||
private pendingReads = new Map<string, PendingCallback>();
|
||||
private pendingWrites = new Map<string, PendingCallback>();
|
||||
private notifyCallbacks = new Map<string, BleDataReceivedCallback>();
|
||||
private deviceManager = DeviceManager.getInstance();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): ServiceManager {
|
||||
if (ServiceManager.instance == null) {
|
||||
ServiceManager.instance = new ServiceManager();
|
||||
}
|
||||
return ServiceManager.instance!;
|
||||
}
|
||||
|
||||
resetDiscoveryState(deviceId: string) {
|
||||
this.services.delete(deviceId);
|
||||
this.characteristics.forEach((_value, key) => {
|
||||
if (key.startsWith(deviceId + '|')) {
|
||||
this.characteristics.delete(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleServicesDiscovered(peripheral: CBPeripheral, error: NSError | null) {
|
||||
const deviceId = peripheral.identifier.UUIDString;
|
||||
if (error != null) {
|
||||
const err = new Error('服务发现失败: ' + error.localizedDescription);
|
||||
this.resolveServiceWaiters(deviceId, null, err);
|
||||
return;
|
||||
}
|
||||
const list: BleService[] = [];
|
||||
const native = peripheral.services;
|
||||
iterateNSArray<CBService>(native, (svc) => {
|
||||
if (svc == null) return;
|
||||
const uuid = svc.UUID.UUIDString;
|
||||
list.push({ uuid, isPrimary: svc.isPrimary });
|
||||
});
|
||||
this.services.set(deviceId, list);
|
||||
this.resolveServiceWaiters(deviceId, list, null);
|
||||
}
|
||||
|
||||
handleCharacteristicsDiscovered(peripheral: CBPeripheral, service: CBService, error: NSError | null) {
|
||||
const deviceId = peripheral.identifier.UUIDString;
|
||||
const serviceId = service.UUID.UUIDString;
|
||||
const key = this.characteristicKey(deviceId, serviceId);
|
||||
if (error != null) {
|
||||
const err = new Error('特征发现失败: ' + error.localizedDescription);
|
||||
this.resolveCharacteristicWaiters(key, null, err);
|
||||
return;
|
||||
}
|
||||
const list: BleCharacteristic[] = [];
|
||||
const chars = service.characteristics;
|
||||
iterateNSArray<CBCharacteristic>(chars, (ch) => {
|
||||
if (ch == null) return;
|
||||
const propsValue = getCharPropertiesValue(ch);
|
||||
const props = makeCharProperties(propsValue);
|
||||
list.push({
|
||||
uuid: ch.UUID.UUIDString,
|
||||
service: { uuid: serviceId, isPrimary: service.isPrimary },
|
||||
properties: props
|
||||
});
|
||||
});
|
||||
let map = this.characteristics.get(deviceId);
|
||||
if (map == null) {
|
||||
map = new Map<string, BleCharacteristic[]>();
|
||||
this.characteristics.set(deviceId, map);
|
||||
}
|
||||
map.set(serviceId, list);
|
||||
this.resolveCharacteristicWaiters(key, list, null);
|
||||
}
|
||||
|
||||
handleCharacteristicValueUpdated(peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError | null) {
|
||||
const deviceId = peripheral.identifier.UUIDString;
|
||||
const serviceId = characteristic.service.UUID.UUIDString;
|
||||
const charId = characteristic.UUID.UUIDString;
|
||||
const notifyKey = this.notifyKey(deviceId, serviceId, charId);
|
||||
const readKey = this.operationKey(deviceId, serviceId, charId, 'read');
|
||||
if (error != null) {
|
||||
const pending = this.pendingReads.get(readKey);
|
||||
if (pending != null) {
|
||||
this.pendingReads.delete(readKey);
|
||||
try { pending.reject(error); } catch (e) { }
|
||||
}
|
||||
return;
|
||||
}
|
||||
const bytes = dataToUint8Array(characteristic.value);
|
||||
const pending = this.pendingReads.get(readKey);
|
||||
if (pending != null) {
|
||||
this.pendingReads.delete(readKey);
|
||||
if (pending.timer != null) clearTimeout(pending.timer);
|
||||
try { pending.resolve(bytes.buffer as ArrayBuffer); } catch (e) { }
|
||||
}
|
||||
const cb = this.notifyCallbacks.get(notifyKey);
|
||||
if (cb != null) {
|
||||
try { cb(bytes); } catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
handleCharacteristicWrite(peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError | null) {
|
||||
const deviceId = peripheral.identifier.UUIDString;
|
||||
const serviceId = characteristic.service.UUID.UUIDString;
|
||||
const charId = characteristic.UUID.UUIDString;
|
||||
const writeKey = this.operationKey(deviceId, serviceId, charId, 'write');
|
||||
const pending = this.pendingWrites.get(writeKey);
|
||||
if (pending == null) return;
|
||||
this.pendingWrites.delete(writeKey);
|
||||
if (pending.timer != null) clearTimeout(pending.timer);
|
||||
if (error != null) {
|
||||
try { pending.reject(error); } catch (e) { }
|
||||
} else {
|
||||
try { pending.resolve(true); } catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
handleNotificationState(peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError | null) {
|
||||
if (error != null) {
|
||||
console.warn('[AKBLE][iOS] notify state change error', error.localizedDescription);
|
||||
}
|
||||
}
|
||||
|
||||
getServices(deviceId: string, callback?: (services: BleService[] | null, error?: Error) => void): Promise<BleService[]> {
|
||||
const cached = this.services.get(deviceId);
|
||||
if (cached != null && cached.length > 0) {
|
||||
if (callback != null) callback(cached, null);
|
||||
return Promise.resolve(cached);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this.enqueueServiceWaiter(deviceId, (list, err) => {
|
||||
if (err != null || list == null) {
|
||||
if (callback != null) callback(null, err ?? new Error('服务获取失败'));
|
||||
reject(err ?? new Error('服务获取失败'));
|
||||
} else {
|
||||
if (callback != null) callback(list, null);
|
||||
resolve(list);
|
||||
}
|
||||
});
|
||||
const peripheral = this.deviceManager.getPeripheral(deviceId);
|
||||
if (peripheral == null) {
|
||||
const err = new Error('设备未连接');
|
||||
this.resolveServiceWaiters(deviceId, null, err);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
peripheral.discoverServices(serviceUUIDs = null);
|
||||
} catch (e) {
|
||||
const err = e instanceof Error ? e : new Error('服务发现失败');
|
||||
this.resolveServiceWaiters(deviceId, null, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getCharacteristics(deviceId: string, serviceId: string, callback?: (list: BleCharacteristic[] | null, error?: Error) => void): Promise<BleCharacteristic[]> {
|
||||
const cached = this.characteristics.get(deviceId)?.get(serviceId) ?? null;
|
||||
if (cached != null && cached.length > 0) {
|
||||
if (callback != null) callback(cached, null);
|
||||
return Promise.resolve(cached);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const key = this.characteristicKey(deviceId, serviceId);
|
||||
this.enqueueCharacteristicWaiter(key, (list, err) => {
|
||||
if (err != null || list == null) {
|
||||
if (callback != null) callback(null, err ?? new Error('特征获取失败'));
|
||||
reject(err ?? new Error('特征获取失败'));
|
||||
} else {
|
||||
if (callback != null) callback(list, null);
|
||||
resolve(list);
|
||||
}
|
||||
});
|
||||
const peripheral = this.deviceManager.getPeripheral(deviceId);
|
||||
if (peripheral == null) {
|
||||
const err = new Error('设备未连接');
|
||||
this.resolveCharacteristicWaiters(key, null, err);
|
||||
return;
|
||||
}
|
||||
const service = this.findNativeService(peripheral, serviceId);
|
||||
if (service == null) {
|
||||
const err = new Error('未找到服务');
|
||||
this.resolveCharacteristicWaiters(key, null, err);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
peripheral.discoverCharacteristics(characteristicUUIDs = null, forService = service);
|
||||
} catch (e) {
|
||||
const err = e instanceof Error ? e : new Error('特征发现失败');
|
||||
this.resolveCharacteristicWaiters(key, null, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async readCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<ArrayBuffer> {
|
||||
const peripheral = this.deviceManager.getPeripheral(deviceId);
|
||||
if (peripheral == null) throw new Error('设备未连接');
|
||||
const characteristic = this.findNativeCharacteristic(peripheral, serviceId, characteristicId);
|
||||
if (characteristic == null) throw new Error('未找到特征值');
|
||||
return new Promise<ArrayBuffer>((resolve, reject) => {
|
||||
const key = this.operationKey(deviceId, serviceId, characteristicId, 'read');
|
||||
const timer = setTimeout(() => {
|
||||
this.pendingReads.delete(key);
|
||||
reject(new Error('读取超时'));
|
||||
}, 10000);
|
||||
this.pendingReads.set(key, { resolve, reject, timer });
|
||||
try {
|
||||
peripheral.readValueForCharacteristic(characteristic);
|
||||
} catch (e) {
|
||||
clearTimeout(timer);
|
||||
this.pendingReads.delete(key);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, value: Uint8Array | ArrayBuffer, options?: WriteCharacteristicOptions): Promise<boolean> {
|
||||
const peripheral = this.deviceManager.getPeripheral(deviceId);
|
||||
if (peripheral == null) throw new Error('设备未连接');
|
||||
const characteristic = this.findNativeCharacteristic(peripheral, serviceId, characteristicId);
|
||||
if (characteristic == null) throw new Error('未找到特征值');
|
||||
const payload = uint8ArrayToData(toUint8Array(value));
|
||||
const waitForResponse = options?.waitForResponse ?? true;
|
||||
const key = this.operationKey(deviceId, serviceId, characteristicId, 'write');
|
||||
if (!waitForResponse) {
|
||||
try {
|
||||
peripheral.writeValue(payload, forCharacteristic = characteristic, type = CBCharacteristicWriteType.withoutResponse);
|
||||
return true;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
this.pendingWrites.delete(key);
|
||||
reject(new Error('写入超时'));
|
||||
}, options?.giveupTimeoutMs ?? 10000);
|
||||
this.pendingWrites.set(key, { resolve, reject, timer });
|
||||
try {
|
||||
peripheral.writeValue(payload, forCharacteristic = characteristic, type = CBCharacteristicWriteType.withResponse);
|
||||
} catch (e) {
|
||||
clearTimeout(timer);
|
||||
this.pendingWrites.delete(key);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async subscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, callback: BleDataReceivedCallback): Promise<void> {
|
||||
const peripheral = this.deviceManager.getPeripheral(deviceId);
|
||||
if (peripheral == null) throw new Error('设备未连接');
|
||||
const characteristic = this.findNativeCharacteristic(peripheral, serviceId, characteristicId);
|
||||
if (characteristic == null) throw new Error('未找到特征值');
|
||||
const key = this.notifyKey(deviceId, serviceId, characteristicId);
|
||||
this.notifyCallbacks.set(key, callback);
|
||||
try {
|
||||
peripheral.setNotifyValue(true, forCharacteristic = characteristic);
|
||||
} catch (e) {
|
||||
this.notifyCallbacks.delete(key);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async unsubscribeCharacteristic(deviceId: string, serviceId: string, characteristicId: string): Promise<void> {
|
||||
const peripheral = this.deviceManager.getPeripheral(deviceId);
|
||||
if (peripheral == null) return;
|
||||
const characteristic = this.findNativeCharacteristic(peripheral, serviceId, characteristicId);
|
||||
if (characteristic == null) return;
|
||||
const key = this.notifyKey(deviceId, serviceId, characteristicId);
|
||||
this.notifyCallbacks.delete(key);
|
||||
try {
|
||||
peripheral.setNotifyValue(false, forCharacteristic = characteristic);
|
||||
} catch (e) {
|
||||
console.warn('[AKBLE][iOS] unsubscribe failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
async autoDiscoverAll(deviceId: string): Promise<AutoDiscoverAllResult> {
|
||||
const services = await this.getServices(deviceId);
|
||||
const allCharacteristics: BleCharacteristic[] = [];
|
||||
for (let i = 0; i < services.length; i++) {
|
||||
const svc = services[i];
|
||||
const chars = await this.getCharacteristics(deviceId, svc.uuid);
|
||||
for (let j = 0; j < chars.length; j++) {
|
||||
allCharacteristics.push(chars[j]);
|
||||
}
|
||||
}
|
||||
return { services, characteristics: allCharacteristics };
|
||||
}
|
||||
|
||||
async subscribeAllNotifications(deviceId: string, callback: BleDataReceivedCallback): Promise<void> {
|
||||
const services = await this.getServices(deviceId);
|
||||
for (let i = 0; i < services.length; i++) {
|
||||
const svc = services[i];
|
||||
const chars = await this.getCharacteristics(deviceId, svc.uuid);
|
||||
for (let j = 0; j < chars.length; j++) {
|
||||
const charDef = chars[j];
|
||||
if (charDef.properties != null && (charDef.properties.notify || charDef.properties.indicate)) {
|
||||
try {
|
||||
await this.subscribeCharacteristic(deviceId, svc.uuid, charDef.uuid, callback);
|
||||
} catch (e) {
|
||||
console.warn('[AKBLE][iOS] subscribeAllNotifications failed', svc.uuid, charDef.uuid, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enqueueServiceWaiter(deviceId: string, waiter: (list: BleService[] | null, error?: Error) => void) {
|
||||
let queue = this.serviceWaiters.get(deviceId);
|
||||
if (queue == null) {
|
||||
queue = [];
|
||||
this.serviceWaiters.set(deviceId, queue);
|
||||
}
|
||||
queue.push(waiter);
|
||||
}
|
||||
|
||||
private enqueueCharacteristicWaiter(key: string, waiter: (list: BleCharacteristic[] | null, error?: Error) => void) {
|
||||
let queue = this.characteristicWaiters.get(key);
|
||||
if (queue == null) {
|
||||
queue = [];
|
||||
this.characteristicWaiters.set(key, queue);
|
||||
}
|
||||
queue.push(waiter);
|
||||
}
|
||||
|
||||
private resolveServiceWaiters(deviceId: string, list: BleService[] | null, error: Error | null) {
|
||||
const queue = this.serviceWaiters.get(deviceId);
|
||||
if (queue == null) return;
|
||||
this.serviceWaiters.delete(deviceId);
|
||||
for (let i = 0; i < queue.length; i++) {
|
||||
const waiter = queue[i];
|
||||
try { waiter(list, error ?? undefined); } catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
private resolveCharacteristicWaiters(key: string, list: BleCharacteristic[] | null, error: Error | null) {
|
||||
const queue = this.characteristicWaiters.get(key);
|
||||
if (queue == null) return;
|
||||
this.characteristicWaiters.delete(key);
|
||||
for (let i = 0; i < queue.length; i++) {
|
||||
const waiter = queue[i];
|
||||
try { waiter(list, error ?? undefined); } catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
private findNativeService(peripheral: CBPeripheral, serviceId: string): CBService | null {
|
||||
const services = peripheral.services;
|
||||
let found: CBService | null = null;
|
||||
iterateNSArray<CBService>(services, (svc) => {
|
||||
if (found != null) return;
|
||||
if (svc != null && svc.UUID.UUIDString == serviceId) found = svc;
|
||||
});
|
||||
if (found != null) return found;
|
||||
return null;
|
||||
}
|
||||
|
||||
private findNativeCharacteristic(peripheral: CBPeripheral, serviceId: string, characteristicId: string): CBCharacteristic | null {
|
||||
const service = this.findNativeService(peripheral, serviceId);
|
||||
if (service == null) return null;
|
||||
const chars = service.characteristics;
|
||||
let found: CBCharacteristic | null = null;
|
||||
iterateNSArray<CBCharacteristic>(chars, (ch) => {
|
||||
if (found != null) return;
|
||||
if (ch != null && ch.UUID.UUIDString == characteristicId) found = ch;
|
||||
});
|
||||
if (found != null) return found;
|
||||
return null;
|
||||
}
|
||||
|
||||
private notifyKey(deviceId: string, serviceId: string, charId: string): string {
|
||||
return `${deviceId}|${serviceId}|${charId}|notify`;
|
||||
}
|
||||
|
||||
private operationKey(deviceId: string, serviceId: string, charId: string, op: string): string {
|
||||
return `${deviceId}|${serviceId}|${charId}|${op}`;
|
||||
}
|
||||
|
||||
private characteristicKey(deviceId: string, serviceId: string): string {
|
||||
return `${deviceId}|${serviceId}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user