349 lines
12 KiB
Plaintext
349 lines
12 KiB
Plaintext
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;
|
|
}
|
|
}
|