From 97205adac334f4fb24b767a50d77bbcba31798b6 Mon Sep 17 00:00:00 2001 From: Travis Wyatt Date: Sat, 9 Apr 2022 23:06:48 -0700 Subject: [PATCH] Add support for writing without response on Apple --- core/src/appleMain/kotlin/Connection.kt | 3 +++ core/src/appleMain/kotlin/Peripheral.kt | 24 +++++++++++-------- .../appleMain/kotlin/PeripheralDelegate.kt | 14 ++++------- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/core/src/appleMain/kotlin/Connection.kt b/core/src/appleMain/kotlin/Connection.kt index 2b3b8fdd3..e57adf380 100644 --- a/core/src/appleMain/kotlin/Connection.kt +++ b/core/src/appleMain/kotlin/Connection.kt @@ -1,5 +1,6 @@ package com.juul.kable +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit @@ -7,6 +8,8 @@ internal class Connection( val delegate: PeripheralDelegate, ) { + val canSendWriteWithoutResponse = MutableStateFlow(false) + // Using Semaphore as Mutex never relinquished lock when multiple concurrent `withLock`s are executed. val semaphore = Semaphore(1) diff --git a/core/src/appleMain/kotlin/Peripheral.kt b/core/src/appleMain/kotlin/Peripheral.kt index 53036f6dd..9eb1a9b4e 100644 --- a/core/src/appleMain/kotlin/Peripheral.kt +++ b/core/src/appleMain/kotlin/Peripheral.kt @@ -50,6 +50,7 @@ import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onSubscription import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.job import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.withContext @@ -60,7 +61,6 @@ 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 import platform.CoreBluetooth.CBErrorConnectionFailed @@ -155,6 +155,8 @@ public class ApplePeripheral internal constructor( .launchIn(scope) } + private val canSendWriteWithoutResponse = MutableStateFlow(cbPeripheral.canSendWriteWithoutResponse) + private val _discoveredServices = atomic?>(null) private val discoveredServices: List get() = _discoveredServices.value @@ -187,7 +189,7 @@ public class ApplePeripheral internal constructor( try { // todo: Create in `connectPeripheral`. - val delegate = PeripheralDelegate(logging, cbPeripheral.identifier.UUIDString) + val delegate = PeripheralDelegate(canSendWriteWithoutResponse, logging, cbPeripheral.identifier.UUIDString) val connection = centralManager.connectPeripheral(cbPeripheral, delegate).also { _connection.value = it @@ -297,8 +299,16 @@ public class ApplePeripheral internal constructor( } val platformCharacteristic = discoveredServices.obtain(characteristic, writeType.properties) - connection.execute { - centralManager.write(cbPeripheral, data, platformCharacteristic, writeType.cbWriteType) + when (writeType) { + WithResponse -> connection.execute { + centralManager.write(cbPeripheral, data, platformCharacteristic, CBCharacteristicWriteWithResponse) + } + WithoutResponse -> connection.semaphore.withPermit { + if (!canSendWriteWithoutResponse.updateAndGet { cbPeripheral.canSendWriteWithoutResponse }) { + canSendWriteWithoutResponse.first { it } + } + centralManager.write(cbPeripheral, data, platformCharacteristic, CBCharacteristicWriteWithoutResponse) + } } } @@ -408,12 +418,6 @@ public class ApplePeripheral internal constructor( override fun toString(): String = "Peripheral(cbPeripheral=$cbPeripheral)" } -private val WriteType.cbWriteType: CBCharacteristicWriteType - get() = when (this) { - WithResponse -> CBCharacteristicWriteWithResponse - WithoutResponse -> CBCharacteristicWriteWithoutResponse - } - private suspend fun Flow.firstOrThrow( predicate: suspend (Data) -> Boolean, ): NSData = when (val value = first { it !is Data || predicate.invoke(it) }) { diff --git a/core/src/appleMain/kotlin/PeripheralDelegate.kt b/core/src/appleMain/kotlin/PeripheralDelegate.kt index 883fd4e27..41cf8c0bd 100644 --- a/core/src/appleMain/kotlin/PeripheralDelegate.kt +++ b/core/src/appleMain/kotlin/PeripheralDelegate.kt @@ -6,7 +6,6 @@ import com.juul.kable.PeripheralDelegate.Response.DidReadRssi import com.juul.kable.PeripheralDelegate.Response.DidUpdateNotificationStateForCharacteristic import com.juul.kable.PeripheralDelegate.Response.DidUpdateValueForDescriptor import com.juul.kable.PeripheralDelegate.Response.DidWriteValueForCharacteristic -import com.juul.kable.PeripheralDelegate.Response.IsReadyToSendWriteWithoutResponse import com.juul.kable.logs.LogMessage import com.juul.kable.logs.Logger import com.juul.kable.logs.Logging @@ -15,6 +14,7 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.BUFFERED import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import platform.CoreBluetooth.CBCharacteristic import platform.CoreBluetooth.CBDescriptor @@ -30,8 +30,9 @@ import platform.darwin.NSObject // https://developer.apple.com/documentation/corebluetooth/cbperipheraldelegate internal class PeripheralDelegate( + private val canSendWriteWithoutResponse: MutableStateFlow, logging: Logging, - identifier: String + identifier: String, ) : NSObject(), CBPeripheralDelegateProtocol { sealed class Response { @@ -68,11 +69,6 @@ internal class PeripheralDelegate( override val error: NSError?, ) : Response() - data class IsReadyToSendWriteWithoutResponse( - override val peripheralIdentifier: NSUUID, - override val error: NSError?, - ) : Response() - data class DidReadRssi( override val peripheralIdentifier: NSUUID, val rssi: NSNumber, @@ -252,9 +248,7 @@ internal class PeripheralDelegate( logger.debug { message = "${peripheral.identifier} peripheralIsReadyToSendWriteWithoutResponse" } - _response.sendBlocking( - IsReadyToSendWriteWithoutResponse(peripheral.identifier, error = null) - ) + canSendWriteWithoutResponse.value = true } /* Managing Notifications for a Characteristic’s Value */