diff --git a/core/src/main/java/gatt/Gatt.kt b/core/src/main/java/gatt/Gatt.kt index 2bf525e..f5e9a7b 100644 --- a/core/src/main/java/gatt/Gatt.kt +++ b/core/src/main/java/gatt/Gatt.kt @@ -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 - - @FlowPreview - val onCharacteristicChanged: Flow - - /** - * @throws [RemoteException] if underlying [BluetoothGatt.discoverServices] returns `false`. - * @throws [ConnectionLost] if [Gatt] disconnects while method is executing. - */ - suspend fun discoverServices(): GattStatus - - val services: List - 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 diff --git a/core/src/main/java/gatt/GattConnection.kt b/core/src/main/java/gatt/GattConnection.kt new file mode 100644 index 0000000..169be91 --- /dev/null +++ b/core/src/main/java/gatt/GattConnection.kt @@ -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 + + 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()}" } + } +} diff --git a/core/src/main/java/gatt/GattIo.kt b/core/src/main/java/gatt/GattIo.kt new file mode 100644 index 0000000..dd69cc0 --- /dev/null +++ b/core/src/main/java/gatt/GattIo.kt @@ -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 + fun getService(uuid: UUID): BluetoothGattService? + + @FlowPreview + val onCharacteristicChanged: Flow + + /** + * @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) diff --git a/processor/src/main/java/GattProcessor.kt b/processor/src/main/java/GattProcessor.kt index 0ef2f38..11d3433 100644 --- a/processor/src/main/java/GattProcessor.kt +++ b/processor/src/main/java/GattProcessor.kt @@ -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 -) : Gatt by gatt { +) : GattIo by gatt { override suspend fun readCharacteristic( characteristic: BluetoothGattCharacteristic diff --git a/throw/src/main/java/GattOrThrow.kt b/throw/src/main/java/GattOrThrow.kt index 1b728af..5bab1da 100644 --- a/throw/src/main/java/GattOrThrow.kt +++ b/throw/src/main/java/GattOrThrow.kt @@ -7,13 +7,13 @@ package com.juul.able.throwable import android.bluetooth.BluetoothGatt.GATT_SUCCESS 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.WriteType /** - * @throws [IllegalStateException] if [Gatt.discoverServices] call does not return [GATT_SUCCESS]. + * @throws [IllegalStateException] if [GattIo.discoverServices] call does not return [GATT_SUCCESS]. */ -suspend fun Gatt.discoverServicesOrThrow() { +suspend fun GattIo.discoverServicesOrThrow() { discoverServices() .also { status -> check(status == GATT_SUCCESS) { @@ -23,9 +23,9 @@ suspend fun Gatt.discoverServicesOrThrow() { } /** - * @throws [IllegalStateException] if [Gatt.readRemoteRssi] call does not return [GATT_SUCCESS]. + * @throws [IllegalStateException] if [GattIo.readRemoteRssi] call does not return [GATT_SUCCESS]. */ -suspend fun Gatt.readRemoteRssiOrThrow(): Int { +suspend fun GattIo.readRemoteRssiOrThrow(): Int { return readRemoteRssi() .also { (_, status) -> check(status == GATT_SUCCESS) { @@ -35,9 +35,9 @@ suspend fun Gatt.readRemoteRssiOrThrow(): Int { } /** - * @throws [IllegalStateException] if [Gatt.readCharacteristic] call does not return [GATT_SUCCESS]. + * @throws [IllegalStateException] if [GattIo.readCharacteristic] call does not return [GATT_SUCCESS]. */ -suspend fun Gatt.readCharacteristicOrThrow( +suspend fun GattIo.readCharacteristicOrThrow( characteristic: BluetoothGattCharacteristic ): ByteArray { return readCharacteristic(characteristic) @@ -49,9 +49,9 @@ suspend fun Gatt.readCharacteristicOrThrow( } /** - * @throws [IllegalStateException] if [Gatt.setCharacteristicNotification] call returns `false`. + * @throws [IllegalStateException] if [GattIo.setCharacteristicNotification] call returns `false`. */ -fun Gatt.setCharacteristicNotificationOrThrow( +fun GattIo.setCharacteristicNotificationOrThrow( characteristic: BluetoothGattCharacteristic, enable: Boolean ) { @@ -62,9 +62,9 @@ fun Gatt.setCharacteristicNotificationOrThrow( } /** - * @throws [IllegalStateException] if [Gatt.writeCharacteristic] call does not return [GATT_SUCCESS]. + * @throws [IllegalStateException] if [GattIo.writeCharacteristic] call does not return [GATT_SUCCESS]. */ -suspend fun Gatt.writeCharacteristicOrThrow( +suspend fun GattIo.writeCharacteristicOrThrow( characteristic: BluetoothGattCharacteristic, value: ByteArray, writeType: WriteType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT @@ -78,9 +78,9 @@ suspend fun Gatt.writeCharacteristicOrThrow( } /** - * @throws [IllegalStateException] if [Gatt.writeDescriptor] call does not return [GATT_SUCCESS]. + * @throws [IllegalStateException] if [GattIo.writeDescriptor] call does not return [GATT_SUCCESS]. */ -suspend fun Gatt.writeDescriptorOrThrow( +suspend fun GattIo.writeDescriptorOrThrow( descriptor: BluetoothGattDescriptor, value: ByteArray ) {