Provides keep-alive (reconnects if connection drops) GATT communication.
A keep-alive GATT is created by calling the keepAliveGatt extension function on
CoroutineScope, which has the following signature:
fun CoroutineScope.keepAliveGatt(
androidContext: Context,
bluetoothDevice: BluetoothDevice,
disconnectTimeoutMillis: Long
): KeepAliveGatt| Parameter | Description |
|---|---|
androidContext |
The Android Context for establishing Bluetooth Low-Energy connections. |
bluetoothDevice |
BluetoothDevice to maintain a connection with. |
disconnectTimeoutMillis |
Duration (in milliseconds) to wait for connection to gracefully spin down (after disconnect) before forcefully closing. |
For example, to create a KeepAliveGatt as a child of Android's viewModelScope:
class ExampleViewModel(application: Application) : AndroidViewModel(application) {
private const val MAC_ADDRESS = ...
private val gatt = viewModelScope.keepAliveGatt(
application,
bluetoothAdapter.getRemoteDevice(MAC_ADDRESS),
disconnectTimeoutMillis = 5_000L // 5 seconds
)
fun connect() {
gatt.connect()
}
}When the parent CoroutineScope (viewModelScope in the above example) cancels, the
KeepAliveGatt also cancels (and disconnects).
When cancelled, a KeepAliveGatt will end in a Cancelled state. Once a KeepAliveGatt is
Cancelled it cannot be reconnected (calls to connect will throw IllegalStateException); a
new KeepAliveGatt must be created.
A KeepAliveGatt will start in a Disconnected state. When connect is called, KeepAliveGatt
will attempt to establish a connection (Connecting). If the connection is rejected (e.g. BLE is
turned off), then KeepAliveGatt will settle at Disconnected state. The connect function can be
called again to re-attempt to establish a connection:
If a connection cannot be established (e.g. BLE device out-of-range) then KeepAliveGatt will retry
indefinitely:
Once Connected, if the connection drops, then KeepAliveGatt will automatically reconnect:
To disconnect an established connection or cancel an in-flight connection attempt, disconnect can
be called (it will suspend until underlying BluetoothGatt has disconnected).
The status of a KeepAliveGatt can be monitored via either Events or States. The major
distinction between the two is:
State:States are propagated over conflated data streams. If states are changing quickly, then someStates may be missed (skipped over). For this reason, they're useful for informing a user of the current state of the connection; as missing a state is acceptable since subsequent states will overwrite the currently reflected state anyways.States should not be used if a specific condition (e.g.Connected) needs to trigger an action (useEventinstead).
Event:Events allow a developer to integrate actions into the connection process. If consumers are slow tocollectevents, then the connection handling process pauses (suspends) until consumers are ready tocollectmore events.
States and Events occur in the following order:
Events can be collected via the events Flow, for example:
val keepAliveGatt = GlobalScope.keepAliveGatt(...)
viewModelScope.launch {
keepAliveGatt.events.collect { event ->
event.onConnected {
// Actions to perform on initial connect *and* subsequent reconnects:
discoverServicesOrThrow()
}
event.onDisconnected {
// todo: retry strategy (e.g. exponentially increasing delay)
}
}
}For example, if is desired to retry connection if a failure occurs while setting up a connection,
simply call disconnect() and KeepAliveGatt will (as usual) attempt to reconnect the lost
connection:
keepAliveGatt.events.collect { event ->
event.onConnected { // `this` is the underlying `Gatt`.
try {
// todo: On connect actions.
} catch (e: Exception) {
disconnect() // Instructs underlying Gatt to disconnect.
// KeepAliveGatt will react by attempting another connection.
}
}
}Alternatively, if you want to cancel the connection process (and settle on a Disconnect state) you
can instruct the KeepAliveGatt to disconnect():
keepAliveGatt.events.collect { event ->
event.onConnected {
try {
// todo: On connect actions.
} catch (e: Exception) {
keepAliveGatt.disconnect() // Instructs `KeepAliveGatt` to settle on a `Disconnected` state.
}
}
}Connection state can be monitored via the state Flow property:
val gatt = scope.keepAliveGatt(...)
gatt.state.collect { println("State: $it") }If a Gatt operation (e.g. discoverServices, writeCharacteristic, readCharacteristic, etc) is
unable to be performed due to a GATT connection being unavailable (i.e. current State is not
Connected), then it will immediately throw NotReadyException.
It is the responsibility of the caller to handle retrying, for example:
class GattCancelledException : Exception()
suspend fun KeepAliveGatt.readCharacteristicWithRetry(
characteristic: BluetoothGattCharacteristic,
retryCount: Int = Int.MAX_VALUE
): OnCharacteristicRead {
repeat(retryCount) {
suspendUntilConnected()
try {
return readCharacteristicOrThrow(characteristic)
} catch (exception: Exception) {
// todo: retry strategy (e.g. exponentially increasing delay)
}
}
error("Failed to read characteristic $characteristic")
}
private suspend fun KeepAliveGatt.suspendUntilConnected() {
state
.onEach { if (it is Cancelled) throw GattCancelledException() }
.first { it == Connected }
}When a KeepAliveGatt is created, it immediately provides a Flow for incoming characteristic
changes (onCharacteristicChange property). The Flow is a hot stream, so characteristic change
events emitted before subscribers have subscribed are dropped. To prevent characteristic change
events from being lost, be sure to setup subscribers before calling KeepAliveGatt.connect, for
example:
val gatt = scope.keepAliveGatt(...)
fun connect() {
// `CoroutineStart.UNDISPATCHED` executes within `launch` up to the `collect` (then suspends),
// before allowing continued execution of `gatt.connect()` (below).
launch(start = CoroutineStart.UNDISPATCHED) {
gatt.onCharacteristicChange.collect {
println("Characteristic changed: $it")
}
}
gatt.connect()
}If the underlying BluetoothGatt connection is dropped, the characteristic change event stream
remains open (and all subscriptions will continue to collect). When a new BluetoothGatt
connection is established, all it's characteristic change events are automatically routed to the
existing subscribers of the KeepAliveGatt.
When connection failures occur, the corresponding Exceptions are propagated to KeepAliveGatt's
parent CoroutineScope and can be inspected via CoroutineExceptionHandler:
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
println(throwable)
}
val scope = CoroutineScope(Job() + exceptionHandler)
val gatt = scope.keepAliveGatt(...)repositories {
jcenter() // or mavenCentral()
}
dependencies {
implementation "com.juul.able:keep-alive:$version"
}


