From 46c231c2f31e9db3275712639d0c28ef4e2cbabe Mon Sep 17 00:00:00 2001 From: Daniel Burnham Date: Tue, 29 Mar 2022 18:02:11 -0400 Subject: [PATCH 1/9] Added connect to peripheral while Bluetooth turned off in IOS --- core/src/appleMain/kotlin/Peripheral.kt | 32 ++++++------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/core/src/appleMain/kotlin/Peripheral.kt b/core/src/appleMain/kotlin/Peripheral.kt index 3c20bec19..a2ff0501b 100644 --- a/core/src/appleMain/kotlin/Peripheral.kt +++ b/core/src/appleMain/kotlin/Peripheral.kt @@ -25,6 +25,7 @@ import com.juul.kable.State.Disconnected.Status.Unknown import com.juul.kable.State.Disconnected.Status.UnknownDevice import com.juul.kable.WriteType.WithResponse import com.juul.kable.WriteType.WithoutResponse +import com.juul.kable.launchIn import com.juul.kable.logs.Logger import com.juul.kable.logs.Logging import com.juul.kable.logs.detail @@ -38,35 +39,12 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onSubscription -import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.job import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.withContext -import platform.CoreBluetooth.CBCentralManagerStatePoweredOff -import platform.CoreBluetooth.CBCharacteristicWriteType -import platform.CoreBluetooth.CBCharacteristicWriteWithResponse -import platform.CoreBluetooth.CBCharacteristicWriteWithoutResponse -import platform.CoreBluetooth.CBErrorConnectionFailed -import platform.CoreBluetooth.CBErrorConnectionLimitReached -import platform.CoreBluetooth.CBErrorConnectionTimeout -import platform.CoreBluetooth.CBErrorEncryptionTimedOut -import platform.CoreBluetooth.CBErrorOperationCancelled -import platform.CoreBluetooth.CBErrorPeripheralDisconnected -import platform.CoreBluetooth.CBErrorUnknownDevice -import platform.CoreBluetooth.CBPeripheral -import platform.CoreBluetooth.CBService -import platform.CoreBluetooth.CBUUID +import platform.CoreBluetooth.* import platform.Foundation.NSData import platform.Foundation.NSError import kotlin.coroutines.CoroutineContext @@ -179,6 +157,7 @@ public class ApplePeripheral internal constructor( if (identifier == cbPeripheral.identifier) onDisconnected() }.launchIn(connectionScope) + try { // todo: Create in `connectPeripheral`. val delegate = PeripheralDelegate(logging, cbPeripheral.identifier.UUIDString) @@ -224,6 +203,9 @@ public class ApplePeripheral internal constructor( } public override suspend fun connect() { + //need to check that coreblueooth is turned on and available + centralManager.delegate.state + .first { state -> state == CBCentralManagerStatePoweredOn } connectJob.updateAndGet { it ?: connectAsync() }!!.await() } From 5a7dbfc63d92aefa018132a3cb42c441c0e3e75c Mon Sep 17 00:00:00 2001 From: Daniel Burnham Date: Thu, 31 Mar 2022 13:16:43 -0400 Subject: [PATCH 2/9] Added exception for attempting to connect to bluetooth device when Bluetooth not turned on in IOS --- .../kotlin/CentralManagerDelegate.kt | 7 +++- core/src/appleMain/kotlin/Peripheral.kt | 39 +++++++++++++++---- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/core/src/appleMain/kotlin/CentralManagerDelegate.kt b/core/src/appleMain/kotlin/CentralManagerDelegate.kt index e2b7c9a84..03c52c8da 100644 --- a/core/src/appleMain/kotlin/CentralManagerDelegate.kt +++ b/core/src/appleMain/kotlin/CentralManagerDelegate.kt @@ -7,12 +7,15 @@ import com.juul.kable.CentralManagerDelegate.Response.DidDiscoverPeripheral import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filterNotNull import platform.CoreBluetooth.CBCentralManager import platform.CoreBluetooth.CBCentralManagerDelegateProtocol import platform.CoreBluetooth.CBManagerState import platform.CoreBluetooth.CBPeripheral +import platform.CoreBluetooth.* import platform.Foundation.NSError import platform.Foundation.NSNumber import platform.Foundation.NSUUID @@ -24,8 +27,8 @@ internal class CentralManagerDelegate : NSObject(), CBCentralManagerDelegateProt private val _onDisconnected = MutableSharedFlow() internal val onDisconnected = _onDisconnected.asSharedFlow() - private val _state = MutableStateFlow(null) - val state: Flow = _state.filterNotNull() + private val _state = MutableStateFlow(CBCentralManagerStateUnknown) + internal val state: StateFlow = _state.asStateFlow() sealed class Response { diff --git a/core/src/appleMain/kotlin/Peripheral.kt b/core/src/appleMain/kotlin/Peripheral.kt index a2ff0501b..9f0a17be0 100644 --- a/core/src/appleMain/kotlin/Peripheral.kt +++ b/core/src/appleMain/kotlin/Peripheral.kt @@ -25,7 +25,6 @@ import com.juul.kable.State.Disconnected.Status.Unknown import com.juul.kable.State.Disconnected.Status.UnknownDevice import com.juul.kable.WriteType.WithResponse import com.juul.kable.WriteType.WithoutResponse -import com.juul.kable.launchIn import com.juul.kable.logs.Logger import com.juul.kable.logs.Logging import com.juul.kable.logs.detail @@ -39,12 +38,36 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onSubscription +import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.job import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.withContext -import platform.CoreBluetooth.* +import platform.CoreBluetooth.CBCentralManagerStatePoweredOff +import platform.CoreBluetooth.CBCentralManagerStatePoweredOn +import platform.CoreBluetooth.CBCharacteristicWriteType +import platform.CoreBluetooth.CBCharacteristicWriteWithResponse +import platform.CoreBluetooth.CBCharacteristicWriteWithoutResponse +import platform.CoreBluetooth.CBErrorConnectionFailed +import platform.CoreBluetooth.CBErrorConnectionLimitReached +import platform.CoreBluetooth.CBErrorConnectionTimeout +import platform.CoreBluetooth.CBErrorEncryptionTimedOut +import platform.CoreBluetooth.CBErrorOperationCancelled +import platform.CoreBluetooth.CBErrorPeripheralDisconnected +import platform.CoreBluetooth.CBErrorUnknownDevice +import platform.CoreBluetooth.CBPeripheral +import platform.CoreBluetooth.CBService +import platform.CoreBluetooth.CBUUID import platform.Foundation.NSData import platform.Foundation.NSError import kotlin.coroutines.CoroutineContext @@ -203,10 +226,12 @@ public class ApplePeripheral internal constructor( } public override suspend fun connect() { - //need to check that coreblueooth is turned on and available - centralManager.delegate.state - .first { state -> state == CBCentralManagerStatePoweredOn } - connectJob.updateAndGet { it ?: connectAsync() }!!.await() + //Check CBCentral State since connecting can result in an api misuse message + if (centralManager.delegate.state.value == CBCentralManagerStatePoweredOn) { + connectJob.updateAndGet { it ?: connectAsync() }!!.await() + } else { + throw NotReadyException("Attempted to connect to device before Bluetooth is powered on") + } } public override suspend fun disconnect() { From 0d4eee89408f394eb219b31e92d420f2a4c36d1e Mon Sep 17 00:00:00 2001 From: Daniel Burnham Date: Thu, 31 Mar 2022 13:24:18 -0400 Subject: [PATCH 3/9] Changed visibility of the state property in CentralManagerDelegate --- core/src/appleMain/kotlin/CentralManagerDelegate.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/appleMain/kotlin/CentralManagerDelegate.kt b/core/src/appleMain/kotlin/CentralManagerDelegate.kt index 03c52c8da..c15515259 100644 --- a/core/src/appleMain/kotlin/CentralManagerDelegate.kt +++ b/core/src/appleMain/kotlin/CentralManagerDelegate.kt @@ -28,7 +28,7 @@ internal class CentralManagerDelegate : NSObject(), CBCentralManagerDelegateProt internal val onDisconnected = _onDisconnected.asSharedFlow() private val _state = MutableStateFlow(CBCentralManagerStateUnknown) - internal val state: StateFlow = _state.asStateFlow() + val state: StateFlow = _state.asStateFlow() sealed class Response { From 2b104a791711f77e5771bebfa94a1c8d240074ba Mon Sep 17 00:00:00 2001 From: Daniel Burnham Date: Thu, 31 Mar 2022 22:25:08 -0400 Subject: [PATCH 4/9] Fixed Linting issues --- core/src/appleMain/kotlin/CentralManagerDelegate.kt | 2 +- core/src/appleMain/kotlin/Peripheral.kt | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/appleMain/kotlin/CentralManagerDelegate.kt b/core/src/appleMain/kotlin/CentralManagerDelegate.kt index c15515259..fe0f8d584 100644 --- a/core/src/appleMain/kotlin/CentralManagerDelegate.kt +++ b/core/src/appleMain/kotlin/CentralManagerDelegate.kt @@ -13,9 +13,9 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filterNotNull import platform.CoreBluetooth.CBCentralManager import platform.CoreBluetooth.CBCentralManagerDelegateProtocol +import platform.CoreBluetooth.CBCentralManagerStateUnknown import platform.CoreBluetooth.CBManagerState import platform.CoreBluetooth.CBPeripheral -import platform.CoreBluetooth.* import platform.Foundation.NSError import platform.Foundation.NSNumber import platform.Foundation.NSUUID diff --git a/core/src/appleMain/kotlin/Peripheral.kt b/core/src/appleMain/kotlin/Peripheral.kt index 9f0a17be0..bd53c7ac7 100644 --- a/core/src/appleMain/kotlin/Peripheral.kt +++ b/core/src/appleMain/kotlin/Peripheral.kt @@ -180,7 +180,6 @@ public class ApplePeripheral internal constructor( if (identifier == cbPeripheral.identifier) onDisconnected() }.launchIn(connectionScope) - try { // todo: Create in `connectPeripheral`. val delegate = PeripheralDelegate(logging, cbPeripheral.identifier.UUIDString) @@ -226,7 +225,7 @@ public class ApplePeripheral internal constructor( } public override suspend fun connect() { - //Check CBCentral State since connecting can result in an api misuse message + // Check CBCentral State since connecting can result in an api misuse message if (centralManager.delegate.state.value == CBCentralManagerStatePoweredOn) { connectJob.updateAndGet { it ?: connectAsync() }!!.await() } else { From c471ee2a01bb41c347dedb803565243996262941 Mon Sep 17 00:00:00 2001 From: burnhamd Date: Tue, 5 Apr 2022 09:54:08 -0400 Subject: [PATCH 5/9] Update core/src/appleMain/kotlin/CentralManagerDelegate.kt Co-authored-by: Travis Wyatt --- core/src/appleMain/kotlin/CentralManagerDelegate.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/appleMain/kotlin/CentralManagerDelegate.kt b/core/src/appleMain/kotlin/CentralManagerDelegate.kt index fe0f8d584..a4a7b515b 100644 --- a/core/src/appleMain/kotlin/CentralManagerDelegate.kt +++ b/core/src/appleMain/kotlin/CentralManagerDelegate.kt @@ -27,7 +27,7 @@ internal class CentralManagerDelegate : NSObject(), CBCentralManagerDelegateProt private val _onDisconnected = MutableSharedFlow() internal val onDisconnected = _onDisconnected.asSharedFlow() - private val _state = MutableStateFlow(CBCentralManagerStateUnknown) + private val _state = MutableStateFlow(CBCentralManagerStateUnknown) val state: StateFlow = _state.asStateFlow() sealed class Response { From f30f10936f6c4d134520d53494c3d560cebb0144 Mon Sep 17 00:00:00 2001 From: burnhamd Date: Tue, 5 Apr 2022 09:56:33 -0400 Subject: [PATCH 6/9] Update core/src/appleMain/kotlin/Peripheral.kt Co-authored-by: Travis Wyatt --- core/src/appleMain/kotlin/Peripheral.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/core/src/appleMain/kotlin/Peripheral.kt b/core/src/appleMain/kotlin/Peripheral.kt index bd53c7ac7..578a2a3ee 100644 --- a/core/src/appleMain/kotlin/Peripheral.kt +++ b/core/src/appleMain/kotlin/Peripheral.kt @@ -226,11 +226,8 @@ public class ApplePeripheral internal constructor( public override suspend fun connect() { // Check CBCentral State since connecting can result in an api misuse message - if (centralManager.delegate.state.value == CBCentralManagerStatePoweredOn) { - connectJob.updateAndGet { it ?: connectAsync() }!!.await() - } else { - throw NotReadyException("Attempted to connect to device before Bluetooth is powered on") - } + checkBluetoothState(CBCentralManagerStatePoweredOn) + connectJob.updateAndGet { it ?: connectAsync() }!!.await() } public override suspend fun disconnect() { From 097fc5509ed4947ffbba73e07589dd3b2b3e4f35 Mon Sep 17 00:00:00 2001 From: Daniel Burnham Date: Tue, 5 Apr 2022 09:58:11 -0400 Subject: [PATCH 7/9] Added matching checkBluetoothState method from Android in ios peripheral --- core/src/appleMain/kotlin/Peripheral.kt | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/core/src/appleMain/kotlin/Peripheral.kt b/core/src/appleMain/kotlin/Peripheral.kt index 578a2a3ee..c9a07e519 100644 --- a/core/src/appleMain/kotlin/Peripheral.kt +++ b/core/src/appleMain/kotlin/Peripheral.kt @@ -53,8 +53,13 @@ import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.job import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.withContext +import platform.CoreBluetooth.CBCentralManagerState import platform.CoreBluetooth.CBCentralManagerStatePoweredOff import platform.CoreBluetooth.CBCentralManagerStatePoweredOn +import platform.CoreBluetooth.CBCentralManagerStateResetting +import platform.CoreBluetooth.CBCentralManagerStateUnauthorized +import platform.CoreBluetooth.CBCentralManagerStateUnknown +import platform.CoreBluetooth.CBCentralManagerStateUnsupported import platform.CoreBluetooth.CBCharacteristicWriteType import platform.CoreBluetooth.CBCharacteristicWriteWithResponse import platform.CoreBluetooth.CBCharacteristicWriteWithoutResponse @@ -164,6 +169,24 @@ public class ApplePeripheral internal constructor( private val connectJob = atomic?>(null) + private fun checkBluetoothState(expected: CBCentralManagerState) { + fun nameFor(value: Int) = when (value) { + CBCentralManagerStatePoweredOff -> "PoweredOff" + CBCentralManagerStatePoweredOn -> "PoweredOn" + CBCentralManagerStateResetting -> "Resetting" + CBCentralManagerStateUnauthorized -> "Unauthorized" + CBCentralManagerStateUnknown -> "Unknown" + CBCentralManagerStateUnsupported -> "Unsupported" + else -> "Unknown" + } + val actual = centralManager.delegate.state.value + if (expected != actual) { + val actualName = nameFor(actual) + val expectedName = nameFor(expected) + throw BluetoothDisabledException("Bluetooth state is $actualName ($actual), but $expectedName ($expected) was required.") + } + } + private fun onDisconnected() { logger.info { message = "Disconnected" } connectJob.value?.cancel() From bb73a4054dae7b26c26f7a40cec176f28dd0ed8a Mon Sep 17 00:00:00 2001 From: Travis Wyatt Date: Tue, 5 Apr 2022 20:26:53 -0700 Subject: [PATCH 8/9] Change type for `nameFor` function to fix compilation error --- core/src/appleMain/kotlin/Peripheral.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/appleMain/kotlin/Peripheral.kt b/core/src/appleMain/kotlin/Peripheral.kt index c9a07e519..8a334e3d7 100644 --- a/core/src/appleMain/kotlin/Peripheral.kt +++ b/core/src/appleMain/kotlin/Peripheral.kt @@ -170,7 +170,7 @@ public class ApplePeripheral internal constructor( private val connectJob = atomic?>(null) private fun checkBluetoothState(expected: CBCentralManagerState) { - fun nameFor(value: Int) = when (value) { + fun nameFor(value: Number) = when (value) { CBCentralManagerStatePoweredOff -> "PoweredOff" CBCentralManagerStatePoweredOn -> "PoweredOn" CBCentralManagerStateResetting -> "Resetting" From 937d5dfb65e5e81dbd74f2657cecd984e5adfb2a Mon Sep 17 00:00:00 2001 From: Travis Wyatt Date: Tue, 5 Apr 2022 20:46:44 -0700 Subject: [PATCH 9/9] Refactor `checkBluetoothState` as extension function of `CentralManager` --- core/src/appleMain/kotlin/Peripheral.kt | 38 ++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/core/src/appleMain/kotlin/Peripheral.kt b/core/src/appleMain/kotlin/Peripheral.kt index 8a334e3d7..53036f6dd 100644 --- a/core/src/appleMain/kotlin/Peripheral.kt +++ b/core/src/appleMain/kotlin/Peripheral.kt @@ -169,24 +169,6 @@ public class ApplePeripheral internal constructor( private val connectJob = atomic?>(null) - private fun checkBluetoothState(expected: CBCentralManagerState) { - fun nameFor(value: Number) = when (value) { - CBCentralManagerStatePoweredOff -> "PoweredOff" - CBCentralManagerStatePoweredOn -> "PoweredOn" - CBCentralManagerStateResetting -> "Resetting" - CBCentralManagerStateUnauthorized -> "Unauthorized" - CBCentralManagerStateUnknown -> "Unknown" - CBCentralManagerStateUnsupported -> "Unsupported" - else -> "Unknown" - } - val actual = centralManager.delegate.state.value - if (expected != actual) { - val actualName = nameFor(actual) - val expectedName = nameFor(expected) - throw BluetoothDisabledException("Bluetooth state is $actualName ($actual), but $expectedName ($expected) was required.") - } - } - private fun onDisconnected() { logger.info { message = "Disconnected" } connectJob.value?.cancel() @@ -249,7 +231,7 @@ public class ApplePeripheral internal constructor( public override suspend fun connect() { // Check CBCentral State since connecting can result in an api misuse message - checkBluetoothState(CBCentralManagerStatePoweredOn) + centralManager.checkBluetoothState(CBCentralManagerStatePoweredOn) connectJob.updateAndGet { it ?: connectAsync() }!!.await() } @@ -456,3 +438,21 @@ private fun NSError.toStatus(): State.Disconnected.Status = when (code) { CBErrorEncryptionTimedOut -> EncryptionTimedOut else -> Unknown(code.toInt()) } + +private fun CentralManager.checkBluetoothState(expected: CBCentralManagerState) { + val actual = delegate.state.value + if (expected != actual) { + fun nameFor(value: Number) = when (value) { + CBCentralManagerStatePoweredOff -> "PoweredOff" + CBCentralManagerStatePoweredOn -> "PoweredOn" + CBCentralManagerStateResetting -> "Resetting" + CBCentralManagerStateUnauthorized -> "Unauthorized" + CBCentralManagerStateUnknown -> "Unknown" + CBCentralManagerStateUnsupported -> "Unsupported" + else -> "Unknown" + } + val actualName = nameFor(actual) + val expectedName = nameFor(expected) + throw BluetoothDisabledException("Bluetooth state is $actualName ($actual), but $expectedName ($expected) was required.") + } +}