Initial commit of akmon project
This commit is contained in:
279
uni_modules/ak-sbsrv/utssdk/app-ios/bluetooth_manager.uts
Normal file
279
uni_modules/ak-sbsrv/utssdk/app-ios/bluetooth_manager.uts
Normal file
@@ -0,0 +1,279 @@
|
||||
import type {
|
||||
BleDevice,
|
||||
BleConnectionState,
|
||||
BleEvent,
|
||||
BleEventCallback,
|
||||
BleEventPayload,
|
||||
BleScanResult,
|
||||
BleConnectOptionsExt,
|
||||
AutoBleInterfaces,
|
||||
BleDataPayload,
|
||||
SendDataPayload,
|
||||
BleOptions,
|
||||
MultiProtocolDevice,
|
||||
ScanHandler,
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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] registerProtocolHandler: unsupported handler type, ignoring', handler);
|
||||
return;
|
||||
}
|
||||
activeProtocol = proto;
|
||||
}
|
||||
|
||||
export const scanDevices = async (options?: ScanDevicesOptions): Promise<void> => {
|
||||
ensureDefaultProtocolHandler();
|
||||
if (activeHandler == null) {
|
||||
console.log('[AKBLE] no active scan handler registered');
|
||||
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] scanDevices 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');
|
||||
const deviceCtx = ctx as DeviceContext;
|
||||
if (deviceCtx.handler == null) throw new Error('sendData not supported for this protocol');
|
||||
await deviceCtx.handler.sendData(deviceCtx.device, payload, options);
|
||||
emit('dataSent', { event: 'dataSent', device: deviceCtx.device, protocol: payload.protocol, data: payload.data });
|
||||
}
|
||||
|
||||
export const getConnectedDevices = (): MultiProtocolDevice[] => {
|
||||
const result: MultiProtocolDevice[] = [];
|
||||
deviceMap.forEach((ctx: DeviceContext) => {
|
||||
const dev: MultiProtocolDevice = {
|
||||
deviceId: ctx.device.deviceId,
|
||||
name: ctx.device.name,
|
||||
rssi: ctx.device.rssi,
|
||||
protocol: ctx.protocol
|
||||
};
|
||||
result.push(dev);
|
||||
});
|
||||
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) => {
|
||||
try {
|
||||
const scanOptions = options != null ? options : {} as ScanDevicesOptions;
|
||||
_dm.startScan(scanOptions);
|
||||
} catch (e) {
|
||||
console.warn('[AKBLE] DeviceManager.startScan failed', e);
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
connect: (device, options?: BleConnectOptionsExt) => {
|
||||
return _dm.connectDevice(device.deviceId, options);
|
||||
},
|
||||
disconnect: (device) => {
|
||||
return _dm.disconnectDevice(device.deviceId);
|
||||
},
|
||||
autoConnect: (device, _options?: any) => {
|
||||
const result: AutoBleInterfaces = { serviceId: '', writeCharId: '', notifyCharId: '' };
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
};
|
||||
const _wrapper = new ProtocolHandlerWrapper(_raw, service);
|
||||
activeHandler = _wrapper;
|
||||
activeProtocol = _raw.protocol as BleProtocolType;
|
||||
console.log('[AKBLE] default protocol handler registered', activeProtocol);
|
||||
} catch (e) {
|
||||
console.warn('[AKBLE] failed to register default protocol handler', e);
|
||||
}
|
||||
}
|
||||
|
||||
export const setDefaultBluetoothService = (service: BluetoothService) => {
|
||||
defaultBluetoothService = service;
|
||||
ensureDefaultProtocolHandler();
|
||||
};
|
||||
5
uni_modules/ak-sbsrv/utssdk/app-ios/config.json
Normal file
5
uni_modules/ak-sbsrv/utssdk/app-ios/config.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": [
|
||||
|
||||
]
|
||||
}
|
||||
348
uni_modules/ak-sbsrv/utssdk/app-ios/device_manager.uts
Normal file
348
uni_modules/ak-sbsrv/utssdk/app-ios/device_manager.uts
Normal file
@@ -0,0 +1,348 @@
|
||||
import type { BleDevice, BleConnectOptionsExt, BleConnectionState, BleConnectionStateChangeCallback, ScanDevicesOptions } from '../interface.uts';
|
||||
import { CBCentralManager, CBPeripheral, CBService, CBCharacteristic, CBCentralManagerDelegate, CBPeripheralDelegate, CBManagerState, CBUUID } from 'CoreBluetooth';
|
||||
import { NSObject, NSDictionary, NSNumber, NSError, NSUUID } from 'Foundation';
|
||||
import { DispatchQueue } from 'Dispatch';
|
||||
import { ServiceManager } from './service_manager.uts';
|
||||
|
||||
type PendingConnect = {
|
||||
resolve: () => void;
|
||||
reject: (err?: any) => void;
|
||||
timer?: number;
|
||||
};
|
||||
|
||||
class PendingConnectImpl implements PendingConnect {
|
||||
resolve: () => void;
|
||||
reject: (err?: any) => void;
|
||||
timer?: number;
|
||||
constructor(resolve: () => void, reject: (err?: any) => void, timer?: number) {
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
this.timer = timer;
|
||||
}
|
||||
}
|
||||
|
||||
class CentralDelegate extends NSObject implements CBCentralManagerDelegate, CBPeripheralDelegate {
|
||||
private owner: DeviceManager;
|
||||
constructor(owner: DeviceManager) {
|
||||
super();
|
||||
this.owner = owner;
|
||||
}
|
||||
override centralManagerDidUpdateState(central: CBCentralManager): void {
|
||||
this.owner.handleCentralStateUpdate(central.state);
|
||||
}
|
||||
override centralManager(central: CBCentralManager, didDiscoverPeripheral peripheral: CBPeripheral, advertisementData: NSDictionary<any, any>, RSSI: NSNumber): void {
|
||||
this.owner.handleDiscovered(peripheral, RSSI);
|
||||
}
|
||||
override centralManager(central: CBCentralManager, didConnectPeripheral peripheral: CBPeripheral): void {
|
||||
this.owner.handleConnected(peripheral);
|
||||
}
|
||||
override centralManager(central: CBCentralManager, didFailToConnectPeripheral peripheral: CBPeripheral, error: NSError | null): void {
|
||||
this.owner.handleConnectFailed(peripheral, error);
|
||||
}
|
||||
override centralManager(central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: NSError | null): void {
|
||||
this.owner.handleDisconnected(peripheral, error);
|
||||
}
|
||||
override peripheral(peripheral: CBPeripheral, didDiscoverServices error: NSError | null): void {
|
||||
ServiceManager.getInstance().handleServicesDiscovered(peripheral, error);
|
||||
}
|
||||
override peripheral(peripheral: CBPeripheral, didDiscoverCharacteristicsForService service: CBService, error: NSError | null): void {
|
||||
ServiceManager.getInstance().handleCharacteristicsDiscovered(peripheral, service, error);
|
||||
}
|
||||
override peripheral(peripheral: CBPeripheral, didUpdateValueForCharacteristic characteristic: CBCharacteristic, error: NSError | null): void {
|
||||
ServiceManager.getInstance().handleCharacteristicValueUpdated(peripheral, characteristic, error);
|
||||
}
|
||||
override peripheral(peripheral: CBPeripheral, didWriteValueForCharacteristic characteristic: CBCharacteristic, error: NSError | null): void {
|
||||
ServiceManager.getInstance().handleCharacteristicWrite(peripheral, characteristic, error);
|
||||
}
|
||||
override peripheral(peripheral: CBPeripheral, didUpdateNotificationStateForCharacteristic characteristic: CBCharacteristic, error: NSError | null): void {
|
||||
ServiceManager.getInstance().handleNotificationState(peripheral, characteristic, error);
|
||||
}
|
||||
}
|
||||
|
||||
export class DeviceManager {
|
||||
private static instance: DeviceManager | null = null;
|
||||
private central: CBCentralManager | null = null;
|
||||
private delegate: CentralDelegate | null = null;
|
||||
private queue: DispatchQueue | null = null;
|
||||
private devices = new Map<string, BleDevice>();
|
||||
private peripherals = new Map<string, CBPeripheral>();
|
||||
private connectionStates = new Map<string, BleConnectionState>();
|
||||
private connectionStateChangeListeners: BleConnectionStateChangeCallback[] = [];
|
||||
private pendingConnects = new Map<string, PendingConnect>();
|
||||
private centralState: number = CBManagerState.unknown;
|
||||
private scanOptions: ScanDevicesOptions | null = null;
|
||||
private scanTimer: number | null = null;
|
||||
private isScanning: boolean = false;
|
||||
private pendingScan: boolean = false;
|
||||
private pendingScanOptions: ScanDevicesOptions | null = null;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): DeviceManager {
|
||||
if (DeviceManager.instance == null) {
|
||||
DeviceManager.instance = new DeviceManager();
|
||||
}
|
||||
return DeviceManager.instance!;
|
||||
}
|
||||
|
||||
private ensureCentral(): CBCentralManager {
|
||||
if (this.central != null) return this.central!;
|
||||
if (this.queue == null) {
|
||||
this.queue = DispatchQueue.main;
|
||||
}
|
||||
this.delegate = new CentralDelegate(this);
|
||||
this.central = new CBCentralManager(delegate = this.delegate!, queue = this.queue);
|
||||
if (this.central != null) {
|
||||
this.centralState = this.central!.state;
|
||||
}
|
||||
return this.central!;
|
||||
}
|
||||
|
||||
handleCentralStateUpdate(state: number) {
|
||||
this.centralState = state;
|
||||
if (state == CBManagerState.poweredOn) {
|
||||
if (this.pendingScan) {
|
||||
const opts = this.pendingScanOptions ?? {} as ScanDevicesOptions;
|
||||
this.pendingScan = false;
|
||||
this.pendingScanOptions = null;
|
||||
this.beginScan(opts);
|
||||
}
|
||||
} else if (state == CBManagerState.poweredOff) {
|
||||
this.stopScanInternal();
|
||||
}
|
||||
}
|
||||
|
||||
startScan(options: ScanDevicesOptions): void {
|
||||
const central = this.ensureCentral();
|
||||
const opts = options ?? {} as ScanDevicesOptions;
|
||||
this.scanOptions = opts;
|
||||
if (this.centralState != CBManagerState.poweredOn) {
|
||||
this.pendingScan = true;
|
||||
this.pendingScanOptions = opts;
|
||||
console.warn('[AKBLE][iOS] Bluetooth not powered on yet, waiting for state update');
|
||||
return;
|
||||
}
|
||||
this.beginScan(opts, central);
|
||||
}
|
||||
|
||||
private beginScan(options: ScanDevicesOptions, central?: CBCentralManager | null) {
|
||||
const mgr = central ?? this.central ?? this.ensureCentral();
|
||||
if (this.isScanning) {
|
||||
this.stopScanInternal();
|
||||
}
|
||||
const serviceIds = options.optionalServices ?? null;
|
||||
let serviceUUIDs: CBUUID[] | null = null;
|
||||
if (serviceIds != null && serviceIds.length > 0) {
|
||||
serviceUUIDs = [];
|
||||
for (let i = 0; i < serviceIds.length; i++) {
|
||||
const sid = serviceIds[i];
|
||||
try {
|
||||
const uuid = CBUUID.UUIDWithString(sid);
|
||||
if (uuid != null) serviceUUIDs.push(uuid!);
|
||||
} catch (e) {
|
||||
console.warn('[AKBLE][iOS] invalid service uuid', sid, e);
|
||||
}
|
||||
}
|
||||
if (serviceUUIDs.length == 0) serviceUUIDs = null;
|
||||
}
|
||||
try {
|
||||
mgr.scanForPeripherals(withServices = serviceUUIDs, options = null);
|
||||
this.isScanning = true;
|
||||
if (options.timeout != null && options.timeout > 0) {
|
||||
this.scanTimer = setTimeout(() => {
|
||||
this.stopScanInternal();
|
||||
}, options.timeout);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[AKBLE][iOS] scanForPeripherals failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
stopScan(): void {
|
||||
this.stopScanInternal();
|
||||
}
|
||||
|
||||
private stopScanInternal() {
|
||||
if (!this.isScanning) return;
|
||||
try {
|
||||
this.central?.stopScan();
|
||||
} catch (e) {
|
||||
console.warn('[AKBLE][iOS] stopScan failed', e);
|
||||
}
|
||||
this.isScanning = false;
|
||||
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) { }
|
||||
}
|
||||
}
|
||||
|
||||
handleDiscovered(peripheral: CBPeripheral, RSSI: NSNumber) {
|
||||
const deviceId = peripheral.identifier.UUIDString;
|
||||
let rssiValue = 0;
|
||||
if (RSSI != null) {
|
||||
try {
|
||||
rssiValue = RSSI.intValue;
|
||||
} catch (e) {
|
||||
rssiValue = Number(RSSI);
|
||||
}
|
||||
}
|
||||
let bleDevice = this.devices.get(deviceId);
|
||||
if (bleDevice == null) {
|
||||
bleDevice = { deviceId, name: peripheral.name ?? 'Unknown', rssi: rssiValue, lastSeen: Date.now() };
|
||||
this.devices.set(deviceId, bleDevice);
|
||||
} else {
|
||||
bleDevice.rssi = rssiValue;
|
||||
bleDevice.name = peripheral.name ?? bleDevice.name;
|
||||
bleDevice.lastSeen = Date.now();
|
||||
}
|
||||
this.peripherals.set(deviceId, peripheral);
|
||||
peripheral.delegate = this.delegate;
|
||||
const onFound = this.scanOptions?.onDeviceFound;
|
||||
if (onFound != null) {
|
||||
try { onFound(bleDevice); } catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
async connectDevice(deviceId: string, options?: BleConnectOptionsExt): Promise<void> {
|
||||
const central = this.ensureCentral();
|
||||
const timeout = options?.timeout ?? 15000;
|
||||
const peripheral = this.obtainPeripheral(deviceId, central);
|
||||
if (peripheral == null) {
|
||||
throw new Error('未找到设备');
|
||||
}
|
||||
this.connectionStates.set(deviceId, 1);
|
||||
this.emitConnectionStateChange(deviceId, 1);
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
this.pendingConnects.delete(deviceId);
|
||||
this.connectionStates.set(deviceId, 0);
|
||||
this.emitConnectionStateChange(deviceId, 0);
|
||||
reject(new Error('连接超时'));
|
||||
}, timeout);
|
||||
const resolveAdapter = () => {
|
||||
clearTimeout(timer);
|
||||
this.pendingConnects.delete(deviceId);
|
||||
resolve();
|
||||
};
|
||||
const rejectAdapter = (err?: any) => {
|
||||
clearTimeout(timer);
|
||||
this.pendingConnects.delete(deviceId);
|
||||
reject(err);
|
||||
};
|
||||
this.pendingConnects.set(deviceId, new PendingConnectImpl(resolveAdapter, rejectAdapter, timer));
|
||||
try {
|
||||
peripheral.delegate = this.delegate;
|
||||
central.connect(peripheral = peripheral, options = null);
|
||||
} catch (e) {
|
||||
clearTimeout(timer);
|
||||
this.pendingConnects.delete(deviceId);
|
||||
this.connectionStates.set(deviceId, 0);
|
||||
this.emitConnectionStateChange(deviceId, 0);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async disconnectDevice(deviceId: string): Promise<void> {
|
||||
const central = this.ensureCentral();
|
||||
const peripheral = this.peripherals.get(deviceId);
|
||||
if (peripheral == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
central.cancelPeripheralConnection(peripheral = peripheral);
|
||||
} catch (e) {
|
||||
console.warn('[AKBLE][iOS] cancelPeripheralConnection failed', e);
|
||||
}
|
||||
this.connectionStates.set(deviceId, 0);
|
||||
this.emitConnectionStateChange(deviceId, 0);
|
||||
}
|
||||
|
||||
handleConnected(peripheral: CBPeripheral) {
|
||||
const deviceId = peripheral.identifier.UUIDString;
|
||||
const pending = this.pendingConnects.get(deviceId);
|
||||
if (pending != null) {
|
||||
const timer = pending.timer;
|
||||
if (timer != null) clearTimeout(timer);
|
||||
try { pending.resolve(); } catch (e) { }
|
||||
this.pendingConnects.delete(deviceId);
|
||||
}
|
||||
this.connectionStates.set(deviceId, 2);
|
||||
this.emitConnectionStateChange(deviceId, 2);
|
||||
this.peripherals.set(deviceId, peripheral);
|
||||
peripheral.delegate = this.delegate;
|
||||
try {
|
||||
peripheral.discoverServices(serviceUUIDs = null);
|
||||
} catch (e) {
|
||||
console.warn('[AKBLE][iOS] discoverServices failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
handleConnectFailed(peripheral: CBPeripheral, error: NSError | null) {
|
||||
const deviceId = peripheral.identifier.UUIDString;
|
||||
const pending = this.pendingConnects.get(deviceId);
|
||||
if (pending != null) {
|
||||
const timer = pending.timer;
|
||||
if (timer != null) clearTimeout(timer);
|
||||
try { pending.reject(error ?? new Error('连接失败')); } catch (e) { }
|
||||
this.pendingConnects.delete(deviceId);
|
||||
}
|
||||
this.connectionStates.set(deviceId, 0);
|
||||
this.emitConnectionStateChange(deviceId, 0);
|
||||
}
|
||||
|
||||
handleDisconnected(peripheral: CBPeripheral, _error: NSError | null) {
|
||||
const deviceId = peripheral.identifier.UUIDString;
|
||||
this.connectionStates.set(deviceId, 0);
|
||||
this.emitConnectionStateChange(deviceId, 0);
|
||||
}
|
||||
|
||||
getConnectedDevices(): BleDevice[] {
|
||||
const result: BleDevice[] = [];
|
||||
this.devices.forEach((device, deviceId) => {
|
||||
if (this.connectionStates.get(deviceId) == 2) {
|
||||
result.push(device);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
onConnectionStateChange(listener: BleConnectionStateChangeCallback) {
|
||||
this.connectionStateChangeListeners.push(listener);
|
||||
}
|
||||
|
||||
private emitConnectionStateChange(deviceId: string, state: BleConnectionState) {
|
||||
for (let i = 0; i < this.connectionStateChangeListeners.length; i++) {
|
||||
const listener = this.connectionStateChangeListeners[i];
|
||||
try { listener(deviceId, state); } catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
getPeripheral(deviceId: string): CBPeripheral | null {
|
||||
return this.peripherals.get(deviceId) ?? null;
|
||||
}
|
||||
|
||||
private obtainPeripheral(deviceId: string, central: CBCentralManager): CBPeripheral | null {
|
||||
let peripheral = this.peripherals.get(deviceId) ?? null;
|
||||
if (peripheral != null) return peripheral;
|
||||
try {
|
||||
const uuid = new NSUUID(UUIDString = deviceId);
|
||||
const list = central.retrievePeripherals(withIdentifiers = [uuid]);
|
||||
if (list != null && list.length > 0) {
|
||||
peripheral = list[0];
|
||||
if (peripheral != null) {
|
||||
this.peripherals.set(deviceId, peripheral!);
|
||||
peripheral!.delegate = this.delegate;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[AKBLE][iOS] retrievePeripherals failed', e);
|
||||
}
|
||||
return peripheral;
|
||||
}
|
||||
}
|
||||
9
uni_modules/ak-sbsrv/utssdk/app-ios/dfu_manager.uts
Normal file
9
uni_modules/ak-sbsrv/utssdk/app-ios/dfu_manager.uts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { DfuManagerType, DfuOptions } from '../interface.uts';
|
||||
|
||||
class IOSDfuManager implements DfuManagerType {
|
||||
async startDfu(_deviceId: string, _firmwareBytes: Uint8Array, _options?: DfuOptions): Promise<void> {
|
||||
throw new Error('iOS 平台暂未实现 DFU 功能');
|
||||
}
|
||||
}
|
||||
|
||||
export const dfuManager = new IOSDfuManager();
|
||||
106
uni_modules/ak-sbsrv/utssdk/app-ios/index.uts
Normal file
106
uni_modules/ak-sbsrv/utssdk/app-ios/index.uts
Normal file
@@ -0,0 +1,106 @@
|
||||
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 IOSBluetoothService 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 new Promise((resolve, reject) => {
|
||||
serviceManager.getServices(deviceId, (list, err) => {
|
||||
if (err != null) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(list ?? []);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
getCharacteristics(deviceId: string, serviceId: string): Promise<BleCharacteristic[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
serviceManager.getCharacteristics(deviceId, serviceId, (list, err) => {
|
||||
if (err != null) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(list ?? []);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
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 IOSBluetoothService {}
|
||||
|
||||
const bluetoothServiceInstance = new BluetoothServiceShape();
|
||||
BluetoothManager.setDefaultBluetoothService(bluetoothServiceInstance);
|
||||
export const bluetoothService: BluetoothServiceContract = bluetoothServiceInstance;
|
||||
export function getBluetoothService(): BluetoothServiceShape {
|
||||
return bluetoothServiceInstance;
|
||||
}
|
||||
|
||||
export { dfuManager } from './dfu_manager.uts';
|
||||
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