From 5b9e021746cc23aab0e8ae61d4a32b8ec22bd21d Mon Sep 17 00:00:00 2001 From: pwespi Date: Fri, 19 Mar 2021 21:04:54 +0100 Subject: [PATCH] fix(): do not connect after connection timeout (#80) --- .../community/plugins/bluetoothle/Device.kt | 13 +++++++++-- ios/Plugin/Device.swift | 4 ++-- ios/Plugin/DeviceManager.swift | 19 +++++++++++++-- src/timeout.ts | 13 +++++++++++ src/web.ts | 23 ++++++++++++++++++- 5 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 src/timeout.ts diff --git a/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Device.kt b/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Device.kt index ea00cf93..ce4de119 100644 --- a/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Device.kt +++ b/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Device.kt @@ -26,7 +26,7 @@ class Device( private const val STATE_CONNECTED = 2 private const val CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb" private const val DEFAULT_TIMEOUT: Long = 5000 - private const val CONNECT_TIMEOUT: Long = 7500 + private const val CONNECTION_TIMEOUT: Long = 10000 private const val REQUEST_MTU = 512 } @@ -159,7 +159,7 @@ class Device( callbackMap["connect"] = callback bluetoothGatt = device.connectGatt(context, false, gattCallback) connectionState = STATE_CONNECTING - setTimeout("connect", "Connection timeout.", CONNECT_TIMEOUT) + setConnectionTimeout("connect", "Connection timeout.", bluetoothGatt) } fun isConnected(): Boolean { @@ -294,4 +294,13 @@ class Device( reject(key, message) }, timeout) } + + private fun setConnectionTimeout(key: String, message: String, gatt: BluetoothGatt?, timeout: Long = CONNECTION_TIMEOUT) { + val handler = Handler() + timeoutMap[key] = handler + handler.postDelayed({ + gatt?.disconnect() + reject(key, message) + }, timeout) + } } \ No newline at end of file diff --git a/ios/Plugin/Device.swift b/ios/Plugin/Device.swift index ff4ed9fc..a0388af1 100644 --- a/ios/Plugin/Device.swift +++ b/ios/Plugin/Device.swift @@ -35,7 +35,7 @@ class Device: NSObject, CBPeripheralDelegate { func setOnConnected(_ callback: @escaping Callback) { let key = "connect" self.callbackMap[key] = callback - self.setTimeout(key, "Connection timeout", 7.5) + self.setTimeout(key, "Connection timeout", connectionTimeout) } func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { @@ -200,7 +200,7 @@ class Device: NSObject, CBPeripheralDelegate { } } - private func setTimeout(_ key: String, _ message: String, _ timeout: Double = 5) { + private func setTimeout(_ key: String, _ message: String, _ timeout: Double = defaultTimeout) { let workItem = DispatchWorkItem { self.reject(key, message) } diff --git a/ios/Plugin/DeviceManager.swift b/ios/Plugin/DeviceManager.swift index e158d15f..6a937730 100644 --- a/ios/Plugin/DeviceManager.swift +++ b/ios/Plugin/DeviceManager.swift @@ -1,6 +1,9 @@ import Foundation import CoreBluetooth +let defaultTimeout: Double = 5 +let connectionTimeout: Double = 10 + class DeviceManager: NSObject, CBCentralManagerDelegate { typealias Callback = (_ success: Bool, _ message: String) -> Void typealias StateReceiver = (_ enabled: Bool) -> Void @@ -181,7 +184,7 @@ class DeviceManager: NSObject, CBCentralManagerDelegate { self.callbackMap[key] = callback print("Connecting to peripheral", device.getPeripheral()) self.centralManager.connect(device.getPeripheral(), options: nil) - self.setTimeout(key, "Connection timeout.", 7.5) + self.setConnectionTimeout(key, "Connection timeout.", device) } // didConnect @@ -267,11 +270,23 @@ class DeviceManager: NSObject, CBCentralManagerDelegate { } } - private func setTimeout(_ key: String, _ message: String, _ timeout: Double = 5) { + private func setTimeout(_ key: String, _ message: String, _ timeout: Double = defaultTimeout) { let workItem = DispatchWorkItem { self.reject(key, message) } self.timeoutMap[key] = workItem DispatchQueue.main.asyncAfter(deadline: .now() + timeout, execute: workItem) } + + private func setConnectionTimeout(_ key: String, _ message: String, _ device: Device, _ timeout: Double = connectionTimeout) { + let workItem = DispatchWorkItem { + // do not call onDisconnnected, which is triggered by cancelPeripheralConnection + let key = "onDisconnected|\(device.getId())" + self.callbackMap[key] = nil + self.centralManager.cancelPeripheralConnection(device.getPeripheral()) + self.reject(key, message) + } + self.timeoutMap[key] = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + timeout, execute: workItem) + } } diff --git a/src/timeout.ts b/src/timeout.ts new file mode 100644 index 00000000..1615a5e8 --- /dev/null +++ b/src/timeout.ts @@ -0,0 +1,13 @@ +export async function runWithTimeout( + promise: Promise, + time: number, + exception: symbol, +): Promise { + let timer: NodeJS.Timeout; + return Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout(() => reject(exception), time); + }), + ]).finally(() => clearTimeout(timer)); +} diff --git a/src/web.ts b/src/web.ts index 8665a281..e6287c4b 100644 --- a/src/web.ts +++ b/src/web.ts @@ -15,12 +15,14 @@ import type { ScanResultInternal, WriteOptions, } from './definitions'; +import { runWithTimeout } from './timeout'; export class BluetoothLeWeb extends WebPlugin implements BluetoothLePlugin { private deviceMap = new Map(); private discoverdDevices = new Map(); private scan: BluetoothLEScan | null = null; private requestBleDeviceOptions: RequestBleDeviceOptions | undefined; + private CONNECTION_TIMEOUT = 10000; async initialize(): Promise { if (typeof navigator === 'undefined' || !navigator.bluetooth) { @@ -108,7 +110,26 @@ export class BluetoothLeWeb extends WebPlugin implements BluetoothLePlugin { const device = await this.getDevice(options.deviceId); device.removeEventListener('gattserverdisconnected', this.onDisconnected); device.addEventListener('gattserverdisconnected', this.onDisconnected); - await device.gatt?.connect(); + const timeoutError = Symbol(); + if (device.gatt === undefined) { + throw new Error('No gatt server available.'); + } + try { + await runWithTimeout( + device.gatt.connect(), + this.CONNECTION_TIMEOUT, + timeoutError, + ); + } catch (error) { + // cancel pending connect call, does not work yet in chromium because of a bug: + // https://bugs.chromium.org/p/chromium/issues/detail?id=684073 + await device.gatt?.disconnect(); + if (error === timeoutError) { + throw new Error('Connection timeout'); + } else { + throw error; + } + } } private onDisconnected(event: Event) {