PoC of new approach of handling ble manager

This commit is contained in:
Sylwester Zieliński
2022-02-04 17:16:38 +01:00
parent f6a188ae3d
commit d16a908d6b
9 changed files with 122 additions and 127 deletions

View File

@@ -5,6 +5,7 @@ dependencies {
implementation project(":lib_theme")
implementation libs.nordic.ble.common
implementation libs.nordic.ble.ktx
implementation libs.nordic.log
implementation libs.nordic.ui.scanner

View File

@@ -1,71 +1,32 @@
package no.nordicsemi.android.service
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.content.Context
import android.util.Log
import androidx.annotation.IntRange
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import no.nordicsemi.android.ble.BleManager
import no.nordicsemi.android.ble.callback.DataReceivedCallback
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelDataCallback
import no.nordicsemi.android.ble.data.Data
import no.nordicsemi.android.log.LogContract
import java.util.*
/**
* The Ble Manager with Battery Service support.
*
* @param <T> The profile callbacks type.
* @see BleManager
</T> */
abstract class BatteryManager(context: Context) : BleManager(context) {
private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb")
private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
abstract class BatteryManager(
context: Context,
protected val scope: CoroutineScope,
) : BleManager(context) {
private val TAG = "BLE-MANAGER"
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
private val batteryLevelDataCallback: DataReceivedCallback =
object : BatteryLevelDataCallback() {
override fun onBatteryLevelChanged(
device: BluetoothDevice,
@IntRange(from = 0, to = 100) batteryLevel: Int
) {
log(LogContract.Log.Level.APPLICATION, "Battery Level received: $batteryLevel%")
onBatteryLevelChanged(batteryLevel)
}
override fun onInvalidDataReceived(device: BluetoothDevice, data: Data) {
log(Log.WARN, "Invalid Battery Level data received: $data")
}
}
protected abstract fun onBatteryLevelChanged(batteryLevel: Int)
fun readBatteryLevelCharacteristic() {
if (isConnected) {
readCharacteristic(batteryLevelCharacteristic)
.with(batteryLevelDataCallback)
.fail { device: BluetoothDevice?, status: Int ->
log(Log.WARN, "Battery Level characteristic not found")
}
.enqueue()
}
}
fun enableBatteryLevelCharacteristicNotifications() {
if (isConnected) {
// If the Battery Level characteristic is null, the request will be ignored
setNotificationCallback(batteryLevelCharacteristic)
.with(batteryLevelDataCallback)
enableNotifications(batteryLevelCharacteristic)
.done { device: BluetoothDevice? ->
log(Log.INFO, "Battery Level notifications enabled")
}
.fail { device: BluetoothDevice?, status: Int ->
log(Log.WARN, "Battery Level characteristic not found")
}
.enqueue()
}
}
@@ -76,7 +37,6 @@ abstract class BatteryManager(context: Context) : BleManager(context) {
protected abstract inner class BatteryManagerGattCallback : BleManagerGattCallback() {
override fun initialize() {
readBatteryLevelCharacteristic()
enableBatteryLevelCharacteristicNotifications()
}
@@ -88,18 +48,12 @@ abstract class BatteryManager(context: Context) : BleManager(context) {
return batteryLevelCharacteristic != null
}
override fun onDeviceDisconnected() {
override fun onServicesInvalidated() {
batteryLevelCharacteristic = null
onBatteryLevelChanged(0)
}
}
companion object {
/** Battery Service UUID. */
private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb")
/** Battery Level characteristic UUID. */
private val BATTERY_LEVEL_CHARACTERISTIC_UUID =
UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
fun releaseScope() {
scope.cancel()
}
}

View File

@@ -1,18 +1,17 @@
package no.nordicsemi.android.service
enum class BleManagerStatus {
CONNECTING, OK, DISCONNECTED
CONNECTING, OK, LINK_LOSS, DISCONNECTED, MISSING_SERVICE
}
enum class BleServiceStatus {
CONNECTING, OK, DISCONNECTED, LINK_LOSS;
sealed class BleManagerResult <T>
class ConnectingResult<T> : BleManagerResult<T>()
class ReadyResult<T> : BleManagerResult<T>()
data class SuccessResult<T>(val data: T) : BleManagerResult<T>()
class LinkLossResult<T> : BleManagerResult<T>()
class DisconnectedResult<T> : BleManagerResult<T>()
class MissingServiceResult<T> : BleManagerResult<T>()
fun mapToSimpleManagerStatus(): BleManagerStatus {
return when (this) {
CONNECTING -> BleManagerStatus.CONNECTING
OK -> BleManagerStatus.OK
DISCONNECTED -> BleManagerStatus.DISCONNECTED
LINK_LOSS -> BleManagerStatus.DISCONNECTED
}
}
}

View File

@@ -22,19 +22,14 @@
package no.nordicsemi.android.service
import android.app.Service
import android.bluetooth.BluetoothDevice
import android.content.Intent
import android.os.Handler
import android.os.IBinder
import android.util.Log
import android.widget.Toast
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import no.nordicsemi.android.ble.BleManager
import no.nordicsemi.android.ble.observer.ConnectionObserver
import no.nordicsemi.android.log.ILogSession
import no.nordicsemi.android.log.Logger
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
@@ -46,9 +41,6 @@ abstract class BleProfileService : Service() {
protected abstract val manager: BleManager
private val _status = MutableStateFlow(BleServiceStatus.CONNECTING)
val status = _status.asStateFlow()
/**
* Returns a handler that is created in onCreate().
* The handler may be used to postpone execution of some operations or to run them in UI thread.
@@ -69,27 +61,6 @@ abstract class BleProfileService : Service() {
override fun onCreate() {
super.onCreate()
handler = Handler()
manager.setConnectionObserver(object : ConnectionObserverAdapter() {
override fun onDeviceConnected(device: BluetoothDevice) {
super.onDeviceConnected(device)
_status.value = BleServiceStatus.OK
}
override fun onDeviceFailedToConnect(device: BluetoothDevice, reason: Int) {
super.onDeviceFailedToConnect(device, reason)
_status.value = BleServiceStatus.DISCONNECTED
}
override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) {
super.onDeviceDisconnected(device, reason)
if (reason == ConnectionObserver.REASON_LINK_LOSS) {
_status.value = BleServiceStatus.LINK_LOSS
} else {
_status.value = BleServiceStatus.DISCONNECTED
}
}
})
}
protected fun stopIfDisconnected(status: BleManagerStatus) {

View File

@@ -2,22 +2,29 @@ package no.nordicsemi.android.service
import android.bluetooth.BluetoothDevice
import android.util.Log
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import no.nordicsemi.android.ble.observer.ConnectionObserver
abstract class ConnectionObserverAdapter : ConnectionObserver {
class ConnectionObserverAdapter<T> : ConnectionObserver {
private val TAG = "BLE-CONNECTION"
private val _status = MutableStateFlow<BleManagerResult<T>>(ConnectingResult())
val status = _status.asStateFlow()
override fun onDeviceConnecting(device: BluetoothDevice) {
Log.d(TAG, "onDeviceConnecting()")
}
override fun onDeviceConnected(device: BluetoothDevice) {
Log.d(TAG, "onDeviceConnected()")
_status.value = ReadyResult()
}
override fun onDeviceFailedToConnect(device: BluetoothDevice, reason: Int) {
Log.d(TAG, "onDeviceFailedToConnect()")
_status.value = DisconnectedResult()
}
override fun onDeviceReady(device: BluetoothDevice) {
@@ -30,5 +37,14 @@ abstract class ConnectionObserverAdapter : ConnectionObserver {
override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) {
Log.d(TAG, "onDeviceDisconnected()")
_status.value = when (reason) {
ConnectionObserver.REASON_NOT_SUPPORTED -> MissingServiceResult()
ConnectionObserver.REASON_LINK_LOSS -> LinkLossResult()
else -> DisconnectedResult()
}
}
fun setValue(value: T) {
_status.tryEmit(SuccessResult(value))
}
}

View File

@@ -38,13 +38,12 @@ import no.nordicsemi.android.ble.common.callback.bps.IntermediateCuffPressureRes
import no.nordicsemi.android.ble.ktx.asValidResponseFlow
import no.nordicsemi.android.ble.ktx.suspend
import no.nordicsemi.android.bps.data.BPSRepository
import no.nordicsemi.android.log.Logger
import no.nordicsemi.android.service.BatteryManager
import no.nordicsemi.android.service.CloseableCoroutineScope
import java.util.*
import javax.inject.Inject
val BPS_SERVICE_UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb")
val BPS_SERVICE_UUID: UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb")
private val BPM_CHARACTERISTIC_UUID = UUID.fromString("00002A35-0000-1000-8000-00805f9b34fb")
@@ -54,9 +53,7 @@ private val ICP_CHARACTERISTIC_UUID = UUID.fromString("00002A36-0000-1000-8000-0
internal class BPSManager @Inject constructor(
@ApplicationContext context: Context,
private val dataHolder: BPSRepository
) : BatteryManager(context) {
private val scope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
) : BatteryManager(context, CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)) {
private var bpmCharacteristic: BluetoothGattCharacteristic? = null
private var icpCharacteristic: BluetoothGattCharacteristic? = null
@@ -134,8 +131,4 @@ internal class BPSManager @Inject constructor(
override fun getGattCallback(): BleManagerGattCallback {
return BloodPressureManagerGattCallback()
}
fun release() {
scope.close()
}
}

View File

@@ -49,7 +49,7 @@ import no.nordicsemi.android.cgms.data.RequestStatus
import no.nordicsemi.android.service.BatteryManager
import java.util.*
val CGMS_SERVICE_UUID = UUID.fromString("0000181F-0000-1000-8000-00805f9b34fb")
val CGMS_SERVICE_UUID: UUID = UUID.fromString("0000181F-0000-1000-8000-00805f9b34fb")
private val CGM_STATUS_UUID = UUID.fromString("00002AA9-0000-1000-8000-00805f9b34fb")
private val CGM_FEATURE_UUID = UUID.fromString("00002AA8-0000-1000-8000-00805f9b34fb")
private val CGM_MEASUREMENT_UUID = UUID.fromString("00002AA7-0000-1000-8000-00805f9b34fb")
@@ -59,9 +59,9 @@ private val RACP_UUID = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb")
internal class CGMManager(
context: Context,
private val scope: CoroutineScope,
scope: CoroutineScope,
private val repository: CGMRepository
) : BatteryManager(context) {
) : BatteryManager(context, scope) {
private var cgmStatusCharacteristic: BluetoothGattCharacteristic? = null
private var cgmFeatureCharacteristic: BluetoothGattCharacteristic? = null
@@ -248,7 +248,7 @@ internal class CGMManager(
}
}
fun clear() {
private fun clear() {
records.clear()
}

View File

@@ -1,5 +1,6 @@
package no.nordicsemi.android.csc.data
import dagger.hilt.android.scopes.ServiceScoped
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
import no.nordicsemi.android.csc.view.SpeedUnit
@@ -7,7 +8,7 @@ import no.nordicsemi.android.service.BleManagerStatus
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
@ServiceScoped
internal class CSCRepository @Inject constructor() {
private val _data = MutableStateFlow(CSCData())

View File

@@ -21,43 +21,92 @@
*/
package no.nordicsemi.android.csc.repository
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.content.Context
import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelResponse
import no.nordicsemi.android.ble.common.callback.csc.CyclingSpeedAndCadenceMeasurementResponse
import no.nordicsemi.android.ble.ktx.asValidResponseFlow
import no.nordicsemi.android.ble.ktx.suspend
import no.nordicsemi.android.csc.data.CSCRepository
import no.nordicsemi.android.csc.data.CSCData
import no.nordicsemi.android.csc.data.WheelSize
import no.nordicsemi.android.service.BatteryManager
import no.nordicsemi.android.service.*
import java.util.*
import javax.inject.Inject
val CSC_SERVICE_UUID: UUID = UUID.fromString("00001816-0000-1000-8000-00805f9b34fb")
private val CSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A5B-0000-1000-8000-00805f9b34fb")
private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb")
private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
internal class CSCRepo @Inject constructor(
@ApplicationContext
private val context: Context,
) {
suspend fun downloadData(device: BluetoothDevice) = callbackFlow<BleManagerResult<CSCData>> {
val scope = CoroutineScope(coroutineContext)
val manager = CSCManager(context, scope)
manager.dataHolder.status.onEach {
trySend(it)
}.launchIn(scope)
scope.launch {
manager.connect(device)
.useAutoConnect(false)
.retry(3, 100)
.suspend()
}
awaitClose {
scope.launch {
manager.disconnect().suspend()
}
scope.cancel()
}
}
}
internal class CSCManager(
context: Context,
private val scope: CoroutineScope,
private val repository: CSCRepository
) : BatteryManager(context) {
scope: CoroutineScope,
// private val channel: SendChannel<BleManagerResult<CSCData>>
) : BatteryManager(context, scope) {
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
private var cscMeasurementCharacteristic: BluetoothGattCharacteristic? = null
private var wheelSize: WheelSize = WheelSize()
private var previousResponse: CyclingSpeedAndCadenceMeasurementResponse? = null
private val exceptionHandler = CoroutineExceptionHandler { context, t->
private val data = MutableStateFlow(CSCData())
val dataHolder = ConnectionObserverAdapter<CSCData>()
private val exceptionHandler = CoroutineExceptionHandler { context, t ->
Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t)
}
override fun onBatteryLevelChanged(batteryLevel: Int) {
repository.setBatteryLevel(batteryLevel)
init {
setConnectionObserver(dataHolder)
data.onEach {
dataHolder.setValue(it)
}.launchIn(scope)
}
override fun getGattCallback(): BatteryManagerGattCallback {
@@ -79,6 +128,9 @@ internal class CSCManager(
val totalDistance = it.getTotalDistance(wheelSize.value.toFloat())
val distance = it.getDistance(wheelCircumference, previousResponse)
val speed = it.getSpeed(wheelCircumference, previousResponse)
//todo
data.value.copy(totalDistance, )
repository.setNewDistance(totalDistance, distance, speed, wheelSize)
val crankCadence = it.getCrankCadence(previousResponse)
@@ -92,21 +144,29 @@ internal class CSCManager(
scope.launch(exceptionHandler) {
enableNotifications(cscMeasurementCharacteristic).suspend()
}
setNotificationCallback(batteryLevelCharacteristic).asValidResponseFlow<BatteryLevelResponse>().onEach {
data.value = data.value.copy(batteryLevel = it.batteryLevel)
}.launchIn(scope)
scope.launch {
enableNotifications(batteryLevelCharacteristic).suspend()
}
}
public override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
val service = gatt.getService(CSC_SERVICE_UUID)
if (service != null) {
cscMeasurementCharacteristic = service.getCharacteristic(CSC_MEASUREMENT_CHARACTERISTIC_UUID)
batteryLevelCharacteristic = service.getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)
}
return cscMeasurementCharacteristic != null
}
override fun onDeviceDisconnected() {
super.onDeviceDisconnected()
override fun onServicesInvalidated() {
super.onServicesInvalidated()
cscMeasurementCharacteristic = null
batteryLevelCharacteristic = null
}
override fun onServicesInvalidated() {}
}
}