Skip to content

Commit 219b22d

Browse files
authored
Handle peripheral initiated MTU changes (#135)
1 parent be53cd3 commit 219b22d

7 files changed

Lines changed: 65 additions & 20 deletions

File tree

core/api/core.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public final class com/juul/kable/Advertisement {
1515
public final class com/juul/kable/AndroidPeripheral : com/juul/kable/Peripheral {
1616
public fun connect (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
1717
public fun disconnect (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
18+
public final fun getMtu ()Lkotlinx/coroutines/flow/StateFlow;
1819
public fun getServices ()Ljava/util/List;
1920
public fun getState ()Lkotlinx/coroutines/flow/Flow;
2021
public fun observe (Lcom/juul/kable/Characteristic;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow;

core/src/androidMain/kotlin/BluetoothDevice.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,13 @@ internal fun BluetoothDevice.connect(
2626
transport: Transport,
2727
phy: Phy,
2828
state: MutableStateFlow<State>,
29+
mtu: MutableStateFlow<Int?>,
2930
invokeOnClose: () -> Unit,
3031
): Connection? =
3132
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
32-
connectApi26(context, transport, phy, state, invokeOnClose)
33+
connectApi26(context, transport, phy, state, mtu, invokeOnClose)
3334
} else {
34-
connectApi21(context, transport, state, invokeOnClose)
35+
connectApi21(context, transport, state, mtu, invokeOnClose)
3536
}
3637

3738
/**
@@ -42,9 +43,10 @@ private fun BluetoothDevice.connectApi21(
4243
context: Context,
4344
transport: Transport,
4445
state: MutableStateFlow<State>,
46+
mtu: MutableStateFlow<Int?>,
4547
invokeOnClose: () -> Unit,
4648
): Connection? {
47-
val callback = Callback(state)
49+
val callback = Callback(state, mtu)
4850
val bluetoothGatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
4951
connectGatt(context, false, callback, transport.intValue)
5052
} else {
@@ -73,13 +75,14 @@ private fun BluetoothDevice.connectApi26(
7375
transport: Transport,
7476
phy: Phy,
7577
state: MutableStateFlow<State>,
78+
mtu: MutableStateFlow<Int?>,
7679
invokeOnClose: () -> Unit,
7780
): Connection? {
7881
val thread = HandlerThread(threadName).apply { start() }
7982
try {
8083
val handler = Handler(thread.looper)
8184
val dispatcher = handler.asCoroutineDispatcher()
82-
val callback = Callback(state)
85+
val callback = Callback(state, mtu)
8386

8487
val bluetoothGatt =
8588
connectGatt(context, false, callback, transport.intValue, phy.intValue, handler)

core/src/androidMain/kotlin/Connection.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,31 @@ internal class Connection(
7272
)
7373
}
7474

75+
/**
76+
* Mimics [execute] in order to uphold the same sequential execution behavior, while having a dedicated channel for
77+
* receiving MTU change events (so that peripheral initiated MTU changes don't result in
78+
* [OutOfOrderGattCallbackException]).
79+
*
80+
* See https://github.com/JuulLabs/kable/issues/86 for more details.
81+
*
82+
* @throws GattRequestRejectedException if underlying `BluetoothGatt` method call returns `false`.
83+
* @throws GattStatusException if response has a non-`GATT_SUCCESS` status.
84+
*/
85+
suspend fun requestMtu(mtu: Int): Int = lock.withLock {
86+
withContext(dispatcher) {
87+
if (!bluetoothGatt.requestMtu(mtu)) throw GattRequestRejectedException()
88+
}
89+
90+
val response = try {
91+
callback.onMtuChanged.receive()
92+
} catch (e: ConnectionLostException) {
93+
throw ConnectionLostException(cause = e)
94+
}
95+
96+
if (response.status != GattSuccess) throw GattStatusException(response.toString())
97+
response.mtu
98+
}
99+
75100
fun close() {
76101
bluetoothGatt.close()
77102
invokeOnClose.invoke()

core/src/androidMain/kotlin/Peripheral.kt

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import com.juul.kable.gatt.Response.OnCharacteristicRead
2424
import com.juul.kable.gatt.Response.OnCharacteristicWrite
2525
import com.juul.kable.gatt.Response.OnDescriptorRead
2626
import com.juul.kable.gatt.Response.OnDescriptorWrite
27-
import com.juul.kable.gatt.Response.OnMtuChanged
2827
import com.juul.kable.gatt.Response.OnReadRemoteRssi
2928
import com.juul.kable.gatt.Response.OnServicesDiscovered
3029
import kotlinx.atomicfu.atomic
@@ -38,6 +37,7 @@ import kotlinx.coroutines.SupervisorJob
3837
import kotlinx.coroutines.async
3938
import kotlinx.coroutines.flow.Flow
4039
import kotlinx.coroutines.flow.MutableStateFlow
40+
import kotlinx.coroutines.flow.StateFlow
4141
import kotlinx.coroutines.flow.asStateFlow
4242
import kotlinx.coroutines.flow.combine
4343
import kotlinx.coroutines.flow.first
@@ -136,6 +136,15 @@ public class AndroidPeripheral internal constructor(
136136
private val _state = MutableStateFlow<State>(State.Disconnected())
137137
public override val state: Flow<State> = _state.asStateFlow()
138138

139+
private val _mtu = MutableStateFlow<Int?>(null)
140+
141+
/**
142+
* [StateFlow] of the most recently negotiated MTU. The MTU will change upon a successful request to change the MTU
143+
* (via [requestMtu]), or if the peripheral initiates an MTU change. [StateFlow]'s `value` will be `null` until MTU
144+
* is negotiated.
145+
*/
146+
public val mtu: StateFlow<Int?> = _mtu.asStateFlow()
147+
139148
private val observers = Observers(this)
140149

141150
@Volatile
@@ -168,6 +177,7 @@ public class AndroidPeripheral internal constructor(
168177
transport,
169178
phy,
170179
_state,
180+
_mtu,
171181
invokeOnClose = { connectJob.value = null }
172182
) ?: throw ConnectionRejectedException()
173183

@@ -232,12 +242,16 @@ public class AndroidPeripheral internal constructor(
232242
.map { it.toPlatformService() }
233243
}
234244

235-
/** @throws NotReadyException if invoked without an established [connection][Peripheral.connect]. */
236-
public suspend fun requestMtu(mtu: Int) {
237-
connection.execute<OnMtuChanged> {
238-
this@execute.requestMtu(mtu)
239-
}
240-
}
245+
/**
246+
* Requests that the current connection's MTU be changed. Suspends until the MTU changes, or failure occurs. The
247+
* negotiated MTU value is returned, which may not be [mtu] value requested if the remote peripheral negotiated an
248+
* alternate MTU.
249+
*
250+
* @throws NotReadyException if invoked without an established [connection][Peripheral.connect].
251+
* @throws GattRequestRejectedException if Android was unable to fulfill the MTU change request.
252+
* @throws GattStatusException if MTU change request failed.
253+
*/
254+
public suspend fun requestMtu(mtu: Int): Int = connection.requestMtu(mtu)
241255

242256
public override suspend fun write(
243257
characteristic: Characteristic,

core/src/androidMain/kotlin/PeripheralBuilder.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public actual class ServicesDiscoveredPeripheral internal constructor(
6767

6868
public suspend fun requestMtu(
6969
mtu: Int
70-
): Unit = peripheral.requestMtu(mtu)
70+
): Int = peripheral.requestMtu(mtu)
7171
}
7272

7373
public actual class PeripheralBuilder internal actual constructor() {

core/src/androidMain/kotlin/gatt/Callback.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import com.juul.kable.gatt.Response.OnCharacteristicRead
2626
import com.juul.kable.gatt.Response.OnCharacteristicWrite
2727
import com.juul.kable.gatt.Response.OnDescriptorRead
2828
import com.juul.kable.gatt.Response.OnDescriptorWrite
29-
import com.juul.kable.gatt.Response.OnMtuChanged
3029
import com.juul.kable.gatt.Response.OnReadRemoteRssi
3130
import com.juul.kable.gatt.Response.OnServicesDiscovered
3231
import kotlinx.coroutines.channels.Channel
@@ -49,7 +48,8 @@ internal data class OnCharacteristicChanged(
4948
}
5049

5150
internal class Callback(
52-
private val state: MutableStateFlow<State>
51+
private val state: MutableStateFlow<State>,
52+
private val mtu: MutableStateFlow<Int?>,
5353
) : BluetoothGattCallback() {
5454

5555
private var disconnectedAction: DisconnectedAction? = null
@@ -62,6 +62,7 @@ internal class Callback(
6262
_onCharacteristicChanged.consumeAsFlow()
6363

6464
val onResponse = Channel<Response>(CONFLATED)
65+
val onMtuChanged = Channel<OnMtuChanged>(CONFLATED)
6566

6667
override fun onPhyUpdate(
6768
gatt: BluetoothGatt,
@@ -169,7 +170,8 @@ internal class Callback(
169170
mtu: Int,
170171
status: Int,
171172
) {
172-
onResponse.trySendOrLog(OnMtuChanged(mtu, GattStatus(status)))
173+
onMtuChanged.trySendOrLog(OnMtuChanged(mtu, GattStatus(status)))
174+
if (status == GATT_SUCCESS) this.mtu.value = mtu
173175
}
174176
}
175177

core/src/androidMain/kotlin/gatt/Response.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,6 @@ internal sealed class Response {
5151
override val status: GattStatus,
5252
) : Response()
5353

54-
data class OnMtuChanged(
55-
val mtu: Int,
56-
override val status: GattStatus,
57-
) : Response()
58-
5954
data class OnServicesDiscovered(
6055
override val status: GattStatus,
6156
) : Response()
@@ -95,6 +90,11 @@ internal sealed class Response {
9590
}
9691
}
9792

93+
internal data class OnMtuChanged(
94+
val mtu: Int,
95+
override val status: GattStatus,
96+
) : Response()
97+
9898
/**
9999
* Represents the possible GATT statuses as defined in [BluetoothGatt]:
100100
*

0 commit comments

Comments
 (0)