Skip to content
This repository has been archived by the owner on Aug 30, 2022. It is now read-only.

Split Gatt interface into GattConnection and GattIo #59

Merged
merged 3 commits into from
Apr 28, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 1 addition & 162 deletions core/src/main/java/gatt/Gatt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,167 +2,6 @@
* Copyright 2020 JUUL Labs, Inc.
*/

@file:Suppress("RedundantUnitReturnType")

package com.juul.able.gatt

import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGatt.GATT_SUCCESS
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothGattService
import android.bluetooth.BluetoothProfile
import android.bluetooth.BluetoothProfile.STATE_DISCONNECTED
import android.os.RemoteException
import com.juul.able.Able
import java.util.UUID
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.onEach

/**
* Represents the possible GATT connection statuses as defined in the Android source code.
*
* - [GATT_SUCCESS]
* - [GATT_CONN_L2C_FAILURE]
* - [GATT_CONN_L2C_FAILURE]
* - [GATT_CONN_TIMEOUT]
* - [GATT_CONN_TERMINATE_PEER_USER]
* - [GATT_CONN_TERMINATE_LOCAL_HOST]
* - [GATT_CONN_FAIL_ESTABLISH]
* - [GATT_CONN_LMP_TIMEOUT]
* - [GATT_CONN_CANCEL]
*/
typealias GattConnectionStatus = Int

/**
* Represents the possible GATT states as defined in [BluetoothProfile]:
*
* - [BluetoothProfile.STATE_DISCONNECTED]
* - [BluetoothProfile.STATE_CONNECTING]
* - [BluetoothProfile.STATE_CONNECTED]
* - [BluetoothProfile.STATE_DISCONNECTING]
*/
typealias GattConnectionState = Int

/**
* Represents the possible GATT statuses as defined in [BluetoothGatt]:
*
* - [BluetoothGatt.GATT_SUCCESS]
* - [BluetoothGatt.GATT_READ_NOT_PERMITTED]
* - [BluetoothGatt.GATT_WRITE_NOT_PERMITTED]
* - [BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION]
* - [BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED]
* - [BluetoothGatt.GATT_INSUFFICIENT_ENCRYPTION]
* - [BluetoothGatt.GATT_INVALID_OFFSET]
* - [BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH]
* - [BluetoothGatt.GATT_CONNECTION_CONGESTED]
* - [BluetoothGatt.GATT_FAILURE]
*/
typealias GattStatus = Int

/**
* Represents the possible [BluetoothGattCharacteristic] write types:
*
* - [BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT]
* - [BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE]
* - [BluetoothGattCharacteristic.WRITE_TYPE_SIGNED]
*/
typealias WriteType = Int

class ConnectionLost : Exception()

class GattStatusFailure(
val event: OnConnectionStateChange
) : IllegalStateException("Received $event")

interface Gatt {

@FlowPreview
val onConnectionStateChange: Flow<OnConnectionStateChange>

@FlowPreview
val onCharacteristicChanged: Flow<OnCharacteristicChanged>

/**
* @throws [RemoteException] if underlying [BluetoothGatt.discoverServices] returns `false`.
* @throws [ConnectionLost] if [Gatt] disconnects while method is executing.
*/
suspend fun discoverServices(): GattStatus

val services: List<BluetoothGattService>
fun getService(uuid: UUID): BluetoothGattService?

/**
* @throws [RemoteException] if underlying [BluetoothGatt.requestMtu] returns `false`.
* @throws [ConnectionLost] if [Gatt] disconnects while method is executing.
*/
suspend fun requestMtu(mtu: Int): OnMtuChanged
suspend fun readRemoteRssi(): OnReadRemoteRssi

/**
* @throws [RemoteException] if underlying [BluetoothGatt.readCharacteristic] returns `false`.
* @throws [ConnectionLost] if [Gatt] disconnects while method is executing.
*/
suspend fun readCharacteristic(
characteristic: BluetoothGattCharacteristic
): OnCharacteristicRead

/**
* @param value applied to [characteristic] when characteristic is written.
* @param writeType applied to [characteristic] when characteristic is written.
* @throws [RemoteException] if underlying [BluetoothGatt.writeCharacteristic] returns `false`.
* @throws [ConnectionLost] if [Gatt] disconnects while method is executing.
*/
suspend fun writeCharacteristic(
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
writeType: WriteType
): OnCharacteristicWrite

/**
* @param value applied to [descriptor] when descriptor is written.
* @throws [RemoteException] if underlying [BluetoothGatt.writeDescriptor] returns `false`.
* @throws [ConnectionLost] if [Gatt] disconnects while method is executing.
*/
suspend fun writeDescriptor(
descriptor: BluetoothGattDescriptor,
value: ByteArray
): OnDescriptorWrite

fun setCharacteristicNotification(
characteristic: BluetoothGattCharacteristic,
enable: Boolean
): Boolean

suspend fun disconnect(): Unit
}

suspend fun Gatt.writeCharacteristic(
characteristic: BluetoothGattCharacteristic,
value: ByteArray
): OnCharacteristicWrite = writeCharacteristic(characteristic, value, WRITE_TYPE_DEFAULT)

internal suspend fun Gatt.suspendUntilConnectionState(state: GattConnectionState) {
Able.debug { "Suspending until ${state.asGattConnectionStateString()}" }
onConnectionStateChange
.onEach { event ->
Able.verbose { "← Received $event while waiting for ${state.asGattConnectionStateString()}" }
if (event.status != GATT_SUCCESS) throw GattStatusFailure(event)
}
.firstOrNull { (_, newState) -> newState == state }
.also {
if (it == null) { // Upstream Channel closed due to STATE_DISCONNECTED.
if (state == STATE_DISCONNECTED) {
Able.info { "Reached (implicit) STATE_DISCONNECTED" }
} else {
throw ConnectionLost()
}
}
}
?.also { (_, newState) ->
Able.info { "Reached ${newState.asGattConnectionStateString()}" }
}
}
interface Gatt : GattConnection, GattIo
77 changes: 77 additions & 0 deletions core/src/main/java/gatt/GattConnection.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2020 JUUL Labs, Inc.
*/

@file:Suppress("RedundantUnitReturnType")

package com.juul.able.gatt

import android.bluetooth.BluetoothGatt.GATT_SUCCESS
import android.bluetooth.BluetoothProfile
import android.bluetooth.BluetoothProfile.STATE_DISCONNECTED
import com.juul.able.Able
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.onEach

/**
* Represents the possible GATT connection statuses as defined in the Android source code.
*
* - [GATT_SUCCESS]
* - [GATT_CONN_L2C_FAILURE]
* - [GATT_CONN_L2C_FAILURE]
* - [GATT_CONN_TIMEOUT]
* - [GATT_CONN_TERMINATE_PEER_USER]
* - [GATT_CONN_TERMINATE_LOCAL_HOST]
* - [GATT_CONN_FAIL_ESTABLISH]
* - [GATT_CONN_LMP_TIMEOUT]
* - [GATT_CONN_CANCEL]
*/
typealias GattConnectionStatus = Int

/**
* Represents the possible GATT states as defined in [BluetoothProfile]:
*
* - [BluetoothProfile.STATE_DISCONNECTED]
* - [BluetoothProfile.STATE_CONNECTING]
* - [BluetoothProfile.STATE_CONNECTED]
* - [BluetoothProfile.STATE_DISCONNECTING]
*/
typealias GattConnectionState = Int

interface GattConnection {

@FlowPreview
val onConnectionStateChange: Flow<OnConnectionStateChange>

suspend fun disconnect(): Unit
}

class GattStatusFailure(
val event: OnConnectionStateChange
) : IllegalStateException("Received $event")

class ConnectionLost : Exception()

internal suspend fun GattConnection.suspendUntilConnectionState(state: GattConnectionState) {
Able.debug { "Suspending until ${state.asGattConnectionStateString()}" }
onConnectionStateChange
.onEach { event ->
Able.verbose { "← Received $event while waiting for ${state.asGattConnectionStateString()}" }
if (event.status != GATT_SUCCESS) throw GattStatusFailure(event)
}
.firstOrNull { (_, newState) -> newState == state }
.also {
if (it == null) { // Upstream Channel closed due to STATE_DISCONNECTED.
if (state == STATE_DISCONNECTED) {
Able.info { "Reached (implicit) STATE_DISCONNECTED" }
} else {
throw ConnectionLost()
}
}
}
?.also { (_, newState) ->
Able.info { "Reached ${newState.asGattConnectionStateString()}" }
}
}
103 changes: 103 additions & 0 deletions core/src/main/java/gatt/GattIo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright 2020 JUUL Labs, Inc.
*/

package com.juul.able.gatt

import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothGattService
import android.os.RemoteException
import java.util.UUID
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow

/**
* Represents the possible GATT statuses as defined in [BluetoothGatt]:
*
* - [BluetoothGatt.GATT_SUCCESS]
* - [BluetoothGatt.GATT_READ_NOT_PERMITTED]
* - [BluetoothGatt.GATT_WRITE_NOT_PERMITTED]
* - [BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION]
* - [BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED]
* - [BluetoothGatt.GATT_INSUFFICIENT_ENCRYPTION]
* - [BluetoothGatt.GATT_INVALID_OFFSET]
* - [BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH]
* - [BluetoothGatt.GATT_CONNECTION_CONGESTED]
* - [BluetoothGatt.GATT_FAILURE]
*/
typealias GattStatus = Int

/**
* Represents the possible [BluetoothGattCharacteristic] write types:
*
* - [BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT]
* - [BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE]
* - [BluetoothGattCharacteristic.WRITE_TYPE_SIGNED]
*/
typealias WriteType = Int

interface GattIo {

/**
* @throws [RemoteException] if underlying [BluetoothGatt.discoverServices] returns `false`.
* @throws [ConnectionLost] if [Gatt] disconnects while method is executing.
*/
suspend fun discoverServices(): GattStatus

val services: List<BluetoothGattService>
fun getService(uuid: UUID): BluetoothGattService?

@FlowPreview
val onCharacteristicChanged: Flow<OnCharacteristicChanged>

/**
* @throws [RemoteException] if underlying [BluetoothGatt.requestMtu] returns `false`.
* @throws [ConnectionLost] if [Gatt] disconnects while method is executing.
*/
suspend fun requestMtu(mtu: Int): OnMtuChanged

/**
* @throws [RemoteException] if underlying [BluetoothGatt.readCharacteristic] returns `false`.
* @throws [ConnectionLost] if [Gatt] disconnects while method is executing.
*/
suspend fun readCharacteristic(
characteristic: BluetoothGattCharacteristic
): OnCharacteristicRead

/**
* @param value applied to [characteristic] when characteristic is written.
* @param writeType applied to [characteristic] when characteristic is written.
* @throws [RemoteException] if underlying [BluetoothGatt.writeCharacteristic] returns `false`.
* @throws [ConnectionLost] if [Gatt] disconnects while method is executing.
*/
suspend fun writeCharacteristic(
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
writeType: WriteType
): OnCharacteristicWrite

/**
* @param value applied to [descriptor] when descriptor is written.
* @throws [RemoteException] if underlying [BluetoothGatt.writeDescriptor] returns `false`.
* @throws [ConnectionLost] if [Gatt] disconnects while method is executing.
*/
suspend fun writeDescriptor(
descriptor: BluetoothGattDescriptor,
value: ByteArray
): OnDescriptorWrite

fun setCharacteristicNotification(
characteristic: BluetoothGattCharacteristic,
enable: Boolean
): Boolean

suspend fun readRemoteRssi(): OnReadRemoteRssi
}

suspend fun GattIo.writeCharacteristic(
characteristic: BluetoothGattCharacteristic,
value: ByteArray
): OnCharacteristicWrite = writeCharacteristic(characteristic, value, WRITE_TYPE_DEFAULT)
8 changes: 4 additions & 4 deletions processor/src/main/java/GattProcessor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ package com.juul.able.processor

import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import com.juul.able.gatt.Gatt
import com.juul.able.gatt.GattIo
import com.juul.able.gatt.OnCharacteristicRead
import com.juul.able.gatt.OnCharacteristicWrite
import com.juul.able.gatt.OnDescriptorWrite
import com.juul.able.gatt.WriteType

fun Gatt.withProcessors(vararg processors: Processor) = GattProcessor(this, processors)
fun GattIo.withProcessors(vararg processors: Processor) = GattProcessor(this, processors)

class GattProcessor(
private val gatt: Gatt,
private val gatt: GattIo,
private val processors: Array<out Processor>
) : Gatt by gatt {
) : GattIo by gatt {

override suspend fun readCharacteristic(
characteristic: BluetoothGattCharacteristic
Expand Down
Loading