Skip to content

Commit

Permalink
Fix write "without response" support (#312)
Browse files Browse the repository at this point in the history
  • Loading branch information
twyatt authored Apr 13, 2022
1 parent 81b4fcc commit 90c45b1
Show file tree
Hide file tree
Showing 3 changed files with 21 additions and 20 deletions.
3 changes: 3 additions & 0 deletions core/src/appleMain/kotlin/Connection.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.juul.kable

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit

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)

Expand Down
24 changes: 14 additions & 10 deletions core/src/appleMain/kotlin/Peripheral.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ 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
import platform.CoreBluetooth.CBCharacteristicWriteType
import platform.CoreBluetooth.CBCharacteristicWriteWithResponse
import platform.CoreBluetooth.CBCharacteristicWriteWithoutResponse
import platform.CoreBluetooth.CBErrorConnectionFailed
Expand Down Expand Up @@ -155,6 +155,8 @@ public class ApplePeripheral internal constructor(
.launchIn(scope)
}

private val canSendWriteWithoutResponse = MutableStateFlow(cbPeripheral.canSendWriteWithoutResponse)

private val _discoveredServices = atomic<List<DiscoveredService>?>(null)
private val discoveredServices: List<DiscoveredService>
get() = _discoveredServices.value
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -297,8 +299,16 @@ public class ApplePeripheral internal constructor(
}

val platformCharacteristic = discoveredServices.obtain(characteristic, writeType.properties)
connection.execute<DidWriteValueForCharacteristic> {
centralManager.write(cbPeripheral, data, platformCharacteristic, writeType.cbWriteType)
when (writeType) {
WithResponse -> connection.execute<DidWriteValueForCharacteristic> {
centralManager.write(cbPeripheral, data, platformCharacteristic, CBCharacteristicWriteWithResponse)
}
WithoutResponse -> connection.semaphore.withPermit {
if (!canSendWriteWithoutResponse.updateAndGet { cbPeripheral.canSendWriteWithoutResponse }) {
canSendWriteWithoutResponse.first { it }
}
centralManager.write(cbPeripheral, data, platformCharacteristic, CBCharacteristicWriteWithoutResponse)
}
}
}

Expand Down Expand Up @@ -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<DidUpdateValueForCharacteristic>.firstOrThrow(
predicate: suspend (Data) -> Boolean,
): NSData = when (val value = first { it !is Data || predicate.invoke(it) }) {
Expand Down
14 changes: 4 additions & 10 deletions core/src/appleMain/kotlin/PeripheralDelegate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -30,8 +30,9 @@ import platform.darwin.NSObject

// https://developer.apple.com/documentation/corebluetooth/cbperipheraldelegate
internal class PeripheralDelegate(
private val canSendWriteWithoutResponse: MutableStateFlow<Boolean>,
logging: Logging,
identifier: String
identifier: String,
) : NSObject(), CBPeripheralDelegateProtocol {

sealed class Response {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 */
Expand Down

0 comments on commit 90c45b1

Please sign in to comment.