Skip to content

Commit a8e32c0

Browse files
authored
Add timeout for write-without-response (#1186)
1 parent 16020ee commit a8e32c0

10 files changed

Lines changed: 42 additions & 1 deletion

File tree

kable-core/api/android/kable-core.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ public final class com/juul/kable/PeripheralBuilder {
325325
public final fun autoConnectIf (Lkotlin/jvm/functions/Function0;)V
326326
public final fun getDisconnectTimeout-UwyO8pc ()J
327327
public final fun getForceCharacteristicEqualityByUuid ()Z
328+
public final fun getWriteWithoutResponseTimeout-UwyO8pc ()J
328329
public final fun getPhy ()Lcom/juul/kable/Phy;
329330
public final fun getThreadingStrategy ()Lcom/juul/kable/ThreadingStrategy;
330331
public final fun getTransport ()Lcom/juul/kable/Transport;
@@ -333,6 +334,7 @@ public final class com/juul/kable/PeripheralBuilder {
333334
public final fun onServicesDiscovered (Lkotlin/jvm/functions/Function2;)V
334335
public final fun setDisconnectTimeout-LRDsOJo (J)V
335336
public final fun setForceCharacteristicEqualityByUuid (Z)V
337+
public final fun setWriteWithoutResponseTimeout-LRDsOJo (J)V
336338
public final fun setPhy (Lcom/juul/kable/Phy;)V
337339
public final fun setThreadingStrategy (Lcom/juul/kable/ThreadingStrategy;)V
338340
public final fun setTransport (Lcom/juul/kable/Transport;)V

kable-core/api/jvm/kable-core.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,11 +272,13 @@ public final class com/juul/kable/Peripheral$DefaultImpls {
272272
public final class com/juul/kable/PeripheralBuilder {
273273
public final fun getDisconnectTimeout-UwyO8pc ()J
274274
public final fun getForceCharacteristicEqualityByUuid ()Z
275+
public final fun getWriteWithoutResponseTimeout-UwyO8pc ()J
275276
public final fun logging (Lkotlin/jvm/functions/Function1;)V
276277
public final fun observationExceptionHandler (Lkotlin/jvm/functions/Function3;)V
277278
public final fun onServicesDiscovered (Lkotlin/jvm/functions/Function2;)V
278279
public final fun setDisconnectTimeout-LRDsOJo (J)V
279280
public final fun setForceCharacteristicEqualityByUuid (Z)V
281+
public final fun setWriteWithoutResponseTimeout-LRDsOJo (J)V
280282
}
281283

282284
public final class com/juul/kable/PeripheralKt {

kable-core/src/androidMain/kotlin/PeripheralBuilder.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,6 @@ public actual class PeripheralBuilder internal actual constructor() {
119119
public actual var disconnectTimeout: Duration = defaultDisconnectTimeout
120120

121121
public actual var forceCharacteristicEqualityByUuid: Boolean = false
122+
123+
public actual var writeWithoutResponseTimeout: Duration = defaultWriteWithoutResponseTimeout
122124
}

kable-core/src/appleMain/kotlin/CBPeripheralCoreBluetoothPeripheral.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import com.juul.kable.logs.Logging.DataProcessor.Operation.Write
1515
import com.juul.kable.logs.detail
1616
import kotlinx.coroutines.CancellationException
1717
import kotlinx.coroutines.CoroutineScope
18+
import kotlinx.coroutines.TimeoutCancellationException
1819
import kotlinx.coroutines.cancel
1920
import kotlinx.coroutines.flow.Flow
2021
import kotlinx.coroutines.flow.MutableStateFlow
@@ -28,6 +29,7 @@ import kotlinx.coroutines.flow.onEach
2829
import kotlinx.coroutines.flow.onSubscription
2930
import kotlinx.coroutines.flow.updateAndGet
3031
import kotlinx.coroutines.sync.withLock
32+
import kotlinx.coroutines.withTimeout
3133
import kotlinx.io.IOException
3234
import platform.CoreBluetooth.CBCharacteristicWriteWithResponse
3335
import platform.CoreBluetooth.CBCharacteristicWriteWithoutResponse
@@ -56,6 +58,7 @@ internal class CBPeripheralCoreBluetoothPeripheral(
5658
private val logging: Logging,
5759
private val disconnectTimeout: Duration,
5860
private val forceCharacteristicEqualityByUuid: Boolean,
61+
private val writeWithoutResponseTimeout: Duration,
5962
) : BasePeripheral(cbPeripheral.identifier.toUuid()), CoreBluetoothPeripheral {
6063

6164
private val central = CentralManager.Default
@@ -213,7 +216,15 @@ internal class CBPeripheralCoreBluetoothPeripheral(
213216
}
214217
WithoutResponse -> connectionOrThrow().guard.withLock {
215218
if (!canSendWriteWithoutResponse.updateAndGet { cbPeripheral.canSendWriteWithoutResponse }) {
216-
canSendWriteWithoutResponse.first { it }
219+
try {
220+
withTimeout(writeWithoutResponseTimeout) {
221+
canSendWriteWithoutResponse.first { it }
222+
}
223+
} catch (e: TimeoutCancellationException) {
224+
logger.warn {
225+
message = "Timed out waiting for canSendWriteWithoutResponse, proceeding with write attempt"
226+
}
227+
}
217228
}
218229
central.writeValue(cbPeripheral, data, platformCharacteristic, CBWithoutResponse)
219230
}

kable-core/src/appleMain/kotlin/Peripheral.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,6 @@ public fun Peripheral(
3333
builder.logging,
3434
builder.disconnectTimeout,
3535
builder.forceCharacteristicEqualityByUuid,
36+
builder.writeWithoutResponseTimeout,
3637
)
3738
}

kable-core/src/appleMain/kotlin/PeripheralBuilder.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,6 @@ public actual class PeripheralBuilder internal actual constructor() {
6060
public actual var disconnectTimeout: Duration = defaultDisconnectTimeout
6161

6262
public actual var forceCharacteristicEqualityByUuid: Boolean = false
63+
64+
public actual var writeWithoutResponseTimeout: Duration = defaultWriteWithoutResponseTimeout
6365
}

kable-core/src/appleMain/kotlin/PeripheralDelegate.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ internal class PeripheralDelegate(
323323
fun close(cause: Throwable?) {
324324
_response.close(NotConnectedException(cause = cause))
325325
characteristicChanges.emitBlocking(ObservationEvent.Disconnected)
326+
canSendWriteWithoutResponse.value = true
326327
}
327328
}
328329

kable-core/src/commonMain/kotlin/PeripheralBuilder.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.juul.kable
33
import com.juul.kable.logs.LoggingBuilder
44
import kotlinx.coroutines.flow.StateFlow
55
import kotlin.time.Duration
6+
import kotlin.time.Duration.Companion.milliseconds
67
import kotlin.time.Duration.Companion.seconds
78

89
public expect class ServicesDiscoveredPeripheral {
@@ -35,6 +36,7 @@ internal typealias ServicesDiscoveredAction = suspend ServicesDiscoveredPeripher
3536
internal typealias ObservationExceptionHandler = suspend ObservationExceptionPeripheral.(cause: Exception) -> Unit
3637

3738
internal val defaultDisconnectTimeout = 5.seconds
39+
internal val defaultWriteWithoutResponseTimeout = 30.milliseconds
3840

3941
public expect class PeripheralBuilder internal constructor() {
4042
public fun logging(init: LoggingBuilder)
@@ -91,4 +93,18 @@ public expect class PeripheralBuilder internal constructor() {
9193
*/
9294
@ObsoleteKableApi // Will be removed after https://github.com/JuulLabs/kable/issues/1016 is fixed.
9395
public var forceCharacteristicEqualityByUuid: Boolean
96+
97+
/**
98+
* Amount of time to wait for `peripheralIsReady(toSendWriteWithoutResponse:)` before
99+
* proceeding with the write.
100+
*
101+
* On some iOS versions/chipsets, the callback may be delayed or never fired, causing the
102+
* write pipeline to stall indefinitely. When the timeout expires, a warning is logged and the
103+
* write proceeds.
104+
*
105+
* Set to a large duration to effectively disable the timeout.
106+
*
107+
* Only applicable on Apple.
108+
*/
109+
public var writeWithoutResponseTimeout: Duration
94110
}

kable-core/src/jvmMain/kotlin/com/juul/kable/PeripheralBuilder.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,6 @@ public actual class PeripheralBuilder internal actual constructor() {
5252
public actual var disconnectTimeout: Duration = defaultDisconnectTimeout
5353

5454
public actual var forceCharacteristicEqualityByUuid: Boolean = false
55+
56+
public actual var writeWithoutResponseTimeout: Duration = defaultWriteWithoutResponseTimeout
5557
}

kable-core/src/webMain/kotlin/PeripheralBuilder.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ public actual class PeripheralBuilder internal actual constructor() {
5656

5757
public actual var forceCharacteristicEqualityByUuid: Boolean = false
5858

59+
public actual var writeWithoutResponseTimeout: Duration = defaultWriteWithoutResponseTimeout
60+
5961
internal fun build(bluetoothDevice: BluetoothDevice) =
6062
BluetoothDeviceWebBluetoothPeripheral(
6163
bluetoothDevice,

0 commit comments

Comments
 (0)