Skip to content

Commit b8484a2

Browse files
committed
Fixes race conditions and improves reliability in GATT operations, see #1259
1 parent e59301e commit b8484a2

1 file changed

Lines changed: 45 additions & 10 deletions

File tree

android_app/app/src/main/java/com/health/openscale/core/bluetooth/scales/GattScaleAdapter.kt

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import kotlinx.coroutines.launch
4343
import kotlinx.coroutines.sync.Mutex
4444
import kotlinx.coroutines.withTimeout
4545
import java.util.UUID
46+
import java.util.concurrent.ConcurrentHashMap
4647

4748
// -------------------------------------------------------------------------------------------------
4849
// GATT adapter (BLE)
@@ -66,7 +67,14 @@ class GattScaleAdapter(
6667

6768
private val opQueue = Channel<suspend () -> Unit>(Channel.UNLIMITED)
6869

69-
private val deferredMap = mutableMapOf<UUID, CompletableDeferred<Unit>>()
70+
private data class PendingOp(
71+
val id: Long,
72+
val deferred: CompletableDeferred<Unit>
73+
)
74+
75+
private val deferredMap = ConcurrentHashMap<UUID, PendingOp>()
76+
private var nextOpId = 0L
77+
7078
private val ioMutex = Mutex()
7179

7280
private var connectAttempts = 0
@@ -75,6 +83,11 @@ class GattScaleAdapter(
7583
// Worker coroutine processes queued BLE operations sequentially
7684
scope.launch {
7785
for (op in opQueue) {
86+
// wait until BLE connection is established
87+
while (!_isConnected.value) {
88+
delay(10)
89+
}
90+
7891
try {
7992
ioMutex.lock()
8093
op()
@@ -185,8 +198,8 @@ class GattScaleAdapter(
185198
) {
186199
LogManager.d(TAG,"\u2190 write response chr=${characteristic.uuid} len=${value.size} status=${status} ${value.toHexPreview(24)}")
187200

188-
deferredMap[characteristic.uuid]?.let {
189-
it.complete(Unit)
201+
deferredMap[characteristic.uuid]?.let { op ->
202+
op.deferred.complete(Unit)
190203
deferredMap.remove(characteristic.uuid)
191204
}
192205
}
@@ -198,8 +211,8 @@ class GattScaleAdapter(
198211
) {
199212
LogManager.d(TAG,"\u2190 notify state chr=${characteristic.uuid} status=${status}")
200213

201-
deferredMap[characteristic.uuid]?.let {
202-
it.complete(Unit)
214+
deferredMap[characteristic.uuid]?.let { op ->
215+
op.deferred.complete(Unit)
203216
deferredMap.remove(characteristic.uuid)
204217
}
205218
}
@@ -214,8 +227,8 @@ class GattScaleAdapter(
214227

215228
handler.handleNotification(characteristic.uuid, value)
216229

217-
deferredMap[characteristic.uuid]?.let {
218-
it.complete(Unit)
230+
deferredMap[characteristic.uuid]?.let { op ->
231+
op.deferred.complete(Unit)
219232
deferredMap.remove(characteristic.uuid)
220233
}
221234
}
@@ -231,14 +244,16 @@ class GattScaleAdapter(
231244
val p = currentPeripheral ?: return@trySend
232245
LogManager.d(TAG, "→ set notify on chr=$characteristic svc=$service")
233246

247+
val opId = ++nextOpId
234248
val deferred = CompletableDeferred<Unit>()
235-
deferredMap[characteristic] = deferred
249+
deferredMap[characteristic] = PendingOp(opId, deferred)
236250

237251
val started = p.startNotify(service, characteristic)
238252
if (!started) {
239253
LogManager.w(TAG, "Failed to initiate notify for $characteristic")
240254
//appCallbacks.onWarn(R.string.bt_warn_notify_failed, characteristic.toString()) // don't show message to the user
241255
deferred.complete(Unit)
256+
deferredMap.remove(characteristic)
242257
}
243258

244259
try {
@@ -248,6 +263,12 @@ class GattScaleAdapter(
248263
}
249264
} catch (e: Exception) {
250265
LogManager.w(TAG, "Timeout waiting for notify on $characteristic")
266+
} finally {
267+
val current = deferredMap[characteristic]
268+
if (current?.id == opId) {
269+
deferredMap.remove(characteristic)
270+
}
271+
deferred.cancel()
251272
}
252273

253274
ioGap(tuning.notifySetupDelayMs)
@@ -259,8 +280,9 @@ class GattScaleAdapter(
259280
val p = currentPeripheral ?: return@trySend
260281
val ch = p.getCharacteristic(service, characteristic) ?: return@trySend
261282

283+
val opId = ++nextOpId
262284
val deferred = CompletableDeferred<Unit>()
263-
deferredMap[characteristic] = deferred
285+
deferredMap[characteristic] = PendingOp(opId, deferred)
264286

265287
val supportsWriteNoResponse = ch.properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE != 0
266288
val supportsWriteResponse = ch.properties and BluetoothGattCharacteristic.PROPERTY_WRITE != 0
@@ -293,6 +315,12 @@ class GattScaleAdapter(
293315
}
294316
} catch (t: Throwable) {
295317
LogManager.w(TAG, "Timeout waiting for write on $characteristic")
318+
} finally {
319+
val current = deferredMap[characteristic]
320+
if (current?.id == opId) {
321+
deferredMap.remove(characteristic)
322+
}
323+
deferred.cancel()
296324
}
297325

298326
ioGap(tuning.postWriteDelayMs)
@@ -304,8 +332,9 @@ class GattScaleAdapter(
304332
val p = currentPeripheral ?: return@trySend
305333
val ch = p.getCharacteristic(service, characteristic) ?: return@trySend
306334

335+
val opId = ++nextOpId
307336
val deferred = CompletableDeferred<Unit>()
308-
deferredMap[characteristic] = deferred
337+
deferredMap[characteristic] = PendingOp(opId, deferred)
309338

310339
p.readCharacteristic(service, characteristic)
311340

@@ -317,6 +346,12 @@ class GattScaleAdapter(
317346
}
318347
} catch (t: Throwable) {
319348
LogManager.w(TAG, "Timeout waiting for read on $characteristic")
349+
} finally {
350+
val current = deferredMap[characteristic]
351+
if (current?.id == opId) {
352+
deferredMap.remove(characteristic)
353+
}
354+
deferred.cancel()
320355
}
321356

322357
ioGap(tuning.postReadDelayMs)

0 commit comments

Comments
 (0)