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, 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(); private peripherals = new Map(); private connectionStates = new Map(); private connectionStateChangeListeners: BleConnectionStateChangeCallback[] = []; private pendingConnects = new Map(); 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 { 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((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 { 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; } }