From e4aabecccb8c1c71f91da0d95cae77275969133b Mon Sep 17 00:00:00 2001 From: Sylwester Zielinski Date: Fri, 24 Mar 2023 13:08:08 +0100 Subject: [PATCH] Migrate UART profile to use the new library --- .../android/service/BleManagerStatus.kt | 67 ------- .../service/CloseableCoroutineScope.kt | 45 ----- .../service/ConnectionObserverAdapter.kt | 93 --------- .../android/prx/repository/PRXRepository.kt | 1 + .../android/prx/repository/PRXService.kt | 2 + profile_uart/build.gradle.kts | 1 + .../android/uart/data/UARTManager.kt | 189 ------------------ .../data/{UARTData.kt => UARTServiceData.kt} | 6 +- .../android/uart/repository/UARTRepository.kt | 86 ++++---- .../android/uart/repository/UARTService.kt | 79 +++++++- .../android/uart/view/UARTContentView.kt | 4 +- .../android/uart/view/UARTScreen.kt | 59 ++---- .../nordicsemi/android/uart/view/UARTState.kt | 13 +- .../android/uart/viewmodel/UARTViewModel.kt | 13 +- 14 files changed, 149 insertions(+), 509 deletions(-) delete mode 100644 lib_service/src/main/java/no/nordicsemi/android/service/BleManagerStatus.kt delete mode 100644 lib_service/src/main/java/no/nordicsemi/android/service/CloseableCoroutineScope.kt delete mode 100644 lib_service/src/main/java/no/nordicsemi/android/service/ConnectionObserverAdapter.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTManager.kt rename profile_uart/src/main/java/no/nordicsemi/android/uart/data/{UARTData.kt => UARTServiceData.kt} (89%) diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/BleManagerStatus.kt b/lib_service/src/main/java/no/nordicsemi/android/service/BleManagerStatus.kt deleted file mode 100644 index c533b04d..00000000 --- a/lib_service/src/main/java/no/nordicsemi/android/service/BleManagerStatus.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.service - -import android.annotation.SuppressLint -import android.bluetooth.BluetoothDevice - -sealed interface BleManagerResult { - - fun isRunning(): Boolean { - return this is SuccessResult - } - - fun hasBeenDisconnected(): Boolean { - return this is LinkLossResult || this is DisconnectedResult || this is MissingServiceResult - } - - fun hasBeenDisconnectedWithoutLinkLoss(): Boolean { - return this is DisconnectedResult || this is MissingServiceResult - } -} - -sealed class DeviceHolder(val device: BluetoothDevice) { - - @SuppressLint("MissingPermission") - fun deviceName(): String = device.name ?: device.address - -} - -class IdleResult : BleManagerResult -class ConnectingResult(device: BluetoothDevice) : DeviceHolder(device), BleManagerResult -class ConnectedResult(device: BluetoothDevice) : DeviceHolder(device), BleManagerResult -class SuccessResult(device: BluetoothDevice, val data: T) : DeviceHolder(device), BleManagerResult - -class LinkLossResult(device: BluetoothDevice, val data: T?) : DeviceHolder(device), BleManagerResult -class DisconnectedResult(device: BluetoothDevice) : DeviceHolder(device), BleManagerResult -class UnknownErrorResult(device: BluetoothDevice) : DeviceHolder(device), BleManagerResult -class MissingServiceResult(device: BluetoothDevice) : DeviceHolder(device), BleManagerResult diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/CloseableCoroutineScope.kt b/lib_service/src/main/java/no/nordicsemi/android/service/CloseableCoroutineScope.kt deleted file mode 100644 index da30f631..00000000 --- a/lib_service/src/main/java/no/nordicsemi/android/service/CloseableCoroutineScope.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.service - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.cancelChildren -import java.io.Closeable -import kotlin.coroutines.CoroutineContext - -class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope { - override val coroutineContext: CoroutineContext = context - - override fun close() { - coroutineContext.cancelChildren() - } -} diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/ConnectionObserverAdapter.kt b/lib_service/src/main/java/no/nordicsemi/android/service/ConnectionObserverAdapter.kt deleted file mode 100644 index 87c6de7c..00000000 --- a/lib_service/src/main/java/no/nordicsemi/android/service/ConnectionObserverAdapter.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -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 - -class ConnectionObserverAdapter : ConnectionObserver { - - private val TAG = "BLE-CONNECTION" - - private val _status = MutableStateFlow>(IdleResult()) - val status = _status.asStateFlow() - - private var lastValue: T? = null - - private fun getData(): T? { - return (_status.value as? SuccessResult)?.data - } - - override fun onDeviceConnecting(device: BluetoothDevice) { - Log.d(TAG, "onDeviceConnecting()") - _status.value = ConnectingResult(device) - } - - override fun onDeviceConnected(device: BluetoothDevice) { - Log.d(TAG, "onDeviceConnected()") - _status.value = ConnectedResult(device) - } - - override fun onDeviceFailedToConnect(device: BluetoothDevice, reason: Int) { - Log.d(TAG, "onDeviceFailedToConnect(), reason: $reason") - _status.value = MissingServiceResult(device) - } - - override fun onDeviceReady(device: BluetoothDevice) { - Log.d(TAG, "onDeviceReady()") - _status.value = SuccessResult(device, lastValue!!) - } - - override fun onDeviceDisconnecting(device: BluetoothDevice) { - Log.d(TAG, "onDeviceDisconnecting()") - } - - override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) { - Log.d(TAG, "onDeviceDisconnected(), reason: $reason") - _status.value = when (reason) { - ConnectionObserver.REASON_NOT_SUPPORTED -> MissingServiceResult(device) - ConnectionObserver.REASON_LINK_LOSS -> LinkLossResult(device, getData()) - ConnectionObserver.REASON_SUCCESS -> DisconnectedResult(device) - else -> UnknownErrorResult(device) - } - } - - fun setValue(value: T) { - lastValue = value - (_status.value as? SuccessResult)?.let { - _status.value = SuccessResult(it.device, value) - } - } -} diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXRepository.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXRepository.kt index 4fa5bd42..4add8c10 100644 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXRepository.kt +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXRepository.kt @@ -103,6 +103,7 @@ class PRXRepository @Inject internal constructor( } fun release() { + _data.value = PRXServiceData() _remoteAlarmLevel.tryEmit(AlarmLevel.NONE) _stopEvent.tryEmit(DisconnectAndStopEvent()) } diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXService.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXService.kt index d5b5862d..55c6d953 100644 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXService.kt +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXService.kt @@ -33,6 +33,7 @@ package no.nordicsemi.android.prx.repository import android.annotation.SuppressLint import android.content.Intent +import android.util.Log import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.filterNotNull @@ -197,6 +198,7 @@ internal class PRXService : NotificationService() { private fun stopIfDisconnected(connectionState: GattConnectionState, connectionStatus: BleGattConnectionStatus) { if (connectionState == GattConnectionState.STATE_DISCONNECTED && !connectionStatus.isLinkLoss) { server.stopServer() + repository.release() stopSelf() } } diff --git a/profile_uart/build.gradle.kts b/profile_uart/build.gradle.kts index 8890e92e..aaec288c 100644 --- a/profile_uart/build.gradle.kts +++ b/profile_uart/build.gradle.kts @@ -68,6 +68,7 @@ dependencies { implementation(libs.nordic.uiscanner) implementation(libs.nordic.navigation) implementation(libs.nordic.uilogger) + implementation(libs.nordic.core) implementation(libs.androidx.dataStore.core) implementation(libs.androidx.dataStore.preferences) diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTManager.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTManager.kt deleted file mode 100644 index fe0c165f..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTManager.kt +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package no.nordicsemi.android.uart.data - -import android.annotation.SuppressLint -import android.bluetooth.BluetoothGatt -import android.bluetooth.BluetoothGattCharacteristic -import android.bluetooth.BluetoothGattService -import android.content.Context -import android.util.Log -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* -import no.nordicsemi.android.ble.BleManager -import no.nordicsemi.android.ble.WriteRequest -import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelResponse -import no.nordicsemi.android.ble.ktx.asFlow -import no.nordicsemi.android.ble.ktx.asValidResponseFlow -import no.nordicsemi.android.ble.ktx.suspend -import no.nordicsemi.android.common.logger.NordicLogger -import no.nordicsemi.android.service.ConnectionObserverAdapter -import no.nordicsemi.android.utils.EMPTY -import no.nordicsemi.android.utils.launchWithCatch -import java.util.* - -val UART_SERVICE_UUID: UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E") -private val UART_RX_CHARACTERISTIC_UUID = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E") -private val UART_TX_CHARACTERISTIC_UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E") - -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 UARTManager( - context: Context, - private val scope: CoroutineScope, - private val logger: NordicLogger -) : BleManager(context) { - - private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null - - private var rxCharacteristic: BluetoothGattCharacteristic? = null - private var txCharacteristic: BluetoothGattCharacteristic? = null - - private var useLongWrite = true - - private val data = MutableStateFlow(UARTData()) - val dataHolder = ConnectionObserverAdapter() - - init { - connectionObserver = dataHolder - - data.onEach { - dataHolder.setValue(it) - }.launchIn(scope) - } - - override fun log(priority: Int, message: String) { - logger.log(priority, message) - } - - override fun getMinLogPriority(): Int { - return Log.VERBOSE - } - - private inner class UARTManagerGattCallback : BleManagerGattCallback() { - - @SuppressLint("WrongConstant") - override fun initialize() { - setNotificationCallback(txCharacteristic).asFlow() - .flowOn(Dispatchers.IO) - .map { - val text: String = it.getStringValue(0) ?: String.EMPTY - log(10, "\"$text\" received") - val messages = data.value.messages + UARTRecord(text, UARTRecordType.OUTPUT) - messages.takeLast(50) - } - .onEach { - data.value = data.value.copy(messages = it) - }.launchIn(scope) - - requestMtu(517).enqueue() - enableNotifications(txCharacteristic).enqueue() - - setNotificationCallback(batteryLevelCharacteristic).asValidResponseFlow() - .onEach { - data.value = data.value.copy(batteryLevel = it.batteryLevel) - }.launchIn(scope) - enableNotifications(batteryLevelCharacteristic).enqueue() - } - - override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean { - val service: BluetoothGattService? = gatt.getService(UART_SERVICE_UUID) - if (service != null) { - rxCharacteristic = service.getCharacteristic(UART_RX_CHARACTERISTIC_UUID) - txCharacteristic = service.getCharacteristic(UART_TX_CHARACTERISTIC_UUID) - } - var writeRequest = false - var writeCommand = false - - rxCharacteristic?.let { - val rxProperties: Int = it.properties - writeRequest = rxProperties and BluetoothGattCharacteristic.PROPERTY_WRITE > 0 - writeCommand = - rxProperties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE > 0 - - // Set the WRITE REQUEST type when the characteristic supports it. - // This will allow to send long write (also if the characteristic support it). - // In case there is no WRITE REQUEST property, this manager will divide texts - // longer then MTU-3 bytes into up to MTU-3 bytes chunks. - if (!writeRequest) { - useLongWrite = false - } - } - gatt.getService(BATTERY_SERVICE_UUID)?.run { - batteryLevelCharacteristic = getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID) - } - return rxCharacteristic != null && txCharacteristic != null && (writeRequest || writeCommand) - } - - override fun onServicesInvalidated() { - batteryLevelCharacteristic = null - rxCharacteristic = null - txCharacteristic = null - useLongWrite = true - } - } - - @SuppressLint("WrongConstant") - fun send(text: String) { - if (rxCharacteristic == null) return - scope.launchWithCatch { - val writeType = if (useLongWrite) { - BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT - } else { - BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE - } - val request: WriteRequest = - writeCharacteristic(rxCharacteristic, text.toByteArray(), writeType) - if (!useLongWrite) { - request.split() - } - request.suspend() - data.value = data.value.copy( - messages = data.value.messages + UARTRecord( - text, - UARTRecordType.INPUT - ) - ) - log(10, "\"$text\" sent") - } - } - - fun clearItems() { - data.value = data.value.copy(messages = emptyList()) - } - - override fun getGattCallback(): BleManagerGattCallback { - return UARTManagerGattCallback() - } -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTData.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTServiceData.kt similarity index 89% rename from profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTData.kt rename to profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTServiceData.kt index a1e8722d..162b1307 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTData.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTServiceData.kt @@ -31,9 +31,13 @@ package no.nordicsemi.android.uart.data -internal data class UARTData( +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState + +internal data class UARTServiceData( val messages: List = emptyList(), + val connectionState: GattConnectionState = GattConnectionState.STATE_DISCONNECTED, val batteryLevel: Int? = null, + val deviceName: String? = null ) { val displayMessages = messages diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTRepository.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTRepository.kt index 47deff88..19827880 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTRepository.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTRepository.kt @@ -33,27 +33,23 @@ package no.nordicsemi.android.uart.repository import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch +import no.nordicsemi.android.common.core.simpleSharedFlow import no.nordicsemi.android.common.logger.NordicLogger -import no.nordicsemi.android.common.logger.NordicLoggerFactory import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.service.BleManagerResult -import no.nordicsemi.android.service.IdleResult +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState +import no.nordicsemi.android.service.DisconnectAndStopEvent import no.nordicsemi.android.service.ServiceManager import no.nordicsemi.android.uart.data.ConfigurationDataSource import no.nordicsemi.android.uart.data.MacroEol -import no.nordicsemi.android.uart.data.UARTData import no.nordicsemi.android.uart.data.UARTMacro -import no.nordicsemi.android.uart.data.UARTManager +import no.nordicsemi.android.uart.data.UARTRecord +import no.nordicsemi.android.uart.data.UARTRecordType +import no.nordicsemi.android.uart.data.UARTServiceData import no.nordicsemi.android.uart.data.parseWithNewLineChar -import no.nordicsemi.android.ui.view.StringConst -import no.nordicsemi.android.utils.EMPTY import javax.inject.Inject import javax.inject.Singleton @@ -62,18 +58,20 @@ class UARTRepository @Inject internal constructor( @ApplicationContext private val context: Context, private val serviceManager: ServiceManager, - private val configurationDataSource: ConfigurationDataSource, - private val loggerFactory: NordicLoggerFactory, - private val stringConst: StringConst + private val configurationDataSource: ConfigurationDataSource ) { - private var manager: UARTManager? = null private var logger: NordicLogger? = null - private val _data = MutableStateFlow>(IdleResult()) + private val _data = MutableStateFlow(UARTServiceData()) internal val data = _data.asStateFlow() - val isRunning = data.map { it.isRunning() } - val hasBeenDisconnected = data.map { it.hasBeenDisconnected() } + private val _stopEvent = simpleSharedFlow() + internal val stopEvent = _stopEvent.asSharedFlow() + + private val _command = simpleSharedFlow() + internal val command = _command.asSharedFlow() + + val isRunning = data.map { it.connectionState == GattConnectionState.STATE_CONNECTED } val lastConfigurationName = configurationDataSource.lastConfigurationName @@ -81,33 +79,39 @@ class UARTRepository @Inject internal constructor( serviceManager.startService(UARTService::class.java, device) } - fun start(device: ServerDevice, scope: CoroutineScope) { - val createdLogger = loggerFactory.create(stringConst.APP_NAME, "UART", device.address).also { - logger = it - } - val manager = UARTManager(context, scope, createdLogger) - this.manager = manager + fun onConnectionStateChanged(connectionState: GattConnectionState) { + _data.value = _data.value.copy(connectionState = connectionState) + } - manager.dataHolder.status.onEach { - _data.value = it - }.launchIn(scope) + fun onBatteryLevelChanged(batteryLevel: Int) { + _data.value = _data.value.copy(batteryLevel = batteryLevel) + } - scope.launch { - manager.start(device) - } + fun onNewMessageReceived(value: String) { + _data.value = _data.value.copy(messages = _data.value.messages + UARTRecord(value, UARTRecordType.OUTPUT)) + } + + fun onNewMessageSent(value: String) { + _data.value = _data.value.copy(messages = _data.value.messages + UARTRecord(value, UARTRecordType.INPUT)) + } + + fun onInitComplete(device: ServerDevice) { + _data.value = _data.value.copy(deviceName = device.name) } fun sendText(text: String, newLineChar: MacroEol) { - manager?.send(text.parseWithNewLineChar(newLineChar)) + _command.tryEmit(text.parseWithNewLineChar(newLineChar)) } fun runMacro(macro: UARTMacro) { - val command = macro.command?.parseWithNewLineChar(macro.newLineChar) - manager?.send(command ?: String.EMPTY) + if (macro.command == null) { + return + } + _command.tryEmit(macro.command.parseWithNewLineChar(macro.newLineChar)) } fun clearItems() { - manager?.clearItems() + _data.value = _data.value.copy(messages = emptyList()) } fun openLogger() { @@ -118,20 +122,8 @@ class UARTRepository @Inject internal constructor( configurationDataSource.saveConfigurationName(name) } - private suspend fun UARTManager.start(device: ServerDevice) { -// try { -// connect(device.device) -// .useAutoConnect(false) -// .retry(3, 100) -// .suspend() -// } catch (e: Exception) { -// e.printStackTrace() -// } - } - fun release() { - manager?.disconnect()?.enqueue() - manager = null + _stopEvent.tryEmit(DisconnectAndStopEvent()) logger = null } } diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt index 611fa329..e5d93161 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt @@ -31,33 +31,104 @@ package no.nordicsemi.android.uart.repository +import android.annotation.SuppressLint import android.content.Intent import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import no.nordicsemi.android.kotlin.ble.core.ServerDevice +import no.nordicsemi.android.kotlin.ble.core.client.callback.BleGattClient +import no.nordicsemi.android.kotlin.ble.core.client.service.BleGattServices +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState +import no.nordicsemi.android.kotlin.ble.profile.battery.BatteryLevelParser +import no.nordicsemi.android.kotlin.ble.profile.hrs.HRSDataParser import no.nordicsemi.android.service.DEVICE_DATA import no.nordicsemi.android.service.NotificationService +import java.util.* import javax.inject.Inject +val UART_SERVICE_UUID: UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E") +private val UART_RX_CHARACTERISTIC_UUID = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E") +private val UART_TX_CHARACTERISTIC_UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E") + +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") + +@SuppressLint("MissingPermission") @AndroidEntryPoint internal class UARTService : NotificationService() { @Inject lateinit var repository: UARTRepository + private lateinit var client: BleGattClient + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) val device = intent!!.getParcelableExtra(DEVICE_DATA)!! - repository.start(device, lifecycleScope) + startGattClient(device) - repository.hasBeenDisconnected.onEach { - if (it) stopSelf() - }.launchIn(lifecycleScope) + repository.stopEvent + .onEach { disconnect() } + .launchIn(lifecycleScope) return START_REDELIVER_INTENT } + + private fun startGattClient(device: ServerDevice) = lifecycleScope.launch { + client = device.connect(this@UARTService) + + client.connectionState + .onEach { repository.onConnectionStateChanged(it) } + .filterNotNull() + .onEach { stopIfDisconnected(it) } + .launchIn(lifecycleScope) + + client.services + .filterNotNull() + .onEach { configureGatt(it, device) } + .launchIn(lifecycleScope) + + client.requestMtu(517) + } + + private suspend fun configureGatt(services: BleGattServices, device: ServerDevice) { + val uartService = services.findService(UART_SERVICE_UUID)!! + val rxCharacteristic = uartService.findCharacteristic(UART_RX_CHARACTERISTIC_UUID)!! + val txCharacteristic = uartService.findCharacteristic(UART_TX_CHARACTERISTIC_UUID)!! + + val batteryService = services.findService(BATTERY_SERVICE_UUID) + + batteryService?.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)?.getNotifications() + ?.mapNotNull { BatteryLevelParser.parse(it) } + ?.onEach { repository.onBatteryLevelChanged(it) } + ?.launchIn(lifecycleScope) + + txCharacteristic.getNotifications() + .onEach { repository.onNewMessageReceived(String(it)) } + .launchIn(lifecycleScope) + + repository.command + .onEach { rxCharacteristic.write(it.toByteArray()) } + .onEach { repository.onNewMessageSent(it) } + .launchIn(lifecycleScope) + + repository.onInitComplete(device) + } + + private fun stopIfDisconnected(connectionState: GattConnectionState) { + if (connectionState == GattConnectionState.STATE_DISCONNECTED) { + stopSelf() + } + } + + private fun disconnect() { + client.disconnect() + } } diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTContentView.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTContentView.kt index 5527a910..c918480c 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTContentView.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTContentView.kt @@ -42,12 +42,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import no.nordicsemi.android.uart.data.UARTData +import no.nordicsemi.android.uart.data.UARTServiceData import no.nordicsemi.android.ui.view.ScreenSection @Composable internal fun UARTContentView( - state: UARTData, + state: UARTServiceData, onEvent: (UARTViewEvent) -> Unit ) { Column( diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTScreen.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTScreen.kt index 87f0b7d3..39d9f159 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTScreen.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTScreen.kt @@ -54,15 +54,7 @@ import no.nordicsemi.android.common.theme.view.PagerViewItem import no.nordicsemi.android.common.ui.scanner.view.DeviceConnectingView import no.nordicsemi.android.common.ui.scanner.view.DeviceDisconnectedView import no.nordicsemi.android.common.ui.scanner.view.Reason -import no.nordicsemi.android.service.ConnectedResult -import no.nordicsemi.android.service.ConnectingResult -import no.nordicsemi.android.service.DeviceHolder -import no.nordicsemi.android.service.DisconnectedResult -import no.nordicsemi.android.service.IdleResult -import no.nordicsemi.android.service.LinkLossResult -import no.nordicsemi.android.service.MissingServiceResult -import no.nordicsemi.android.service.SuccessResult -import no.nordicsemi.android.service.UnknownErrorResult +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState import no.nordicsemi.android.uart.R import no.nordicsemi.android.uart.viewmodel.UARTViewModel import no.nordicsemi.android.ui.view.BackIconAppBar @@ -78,22 +70,19 @@ fun UARTScreen() { val navigateUp = { viewModel.onEvent(NavigateUp) } Scaffold( - topBar = { AppBar(state, navigateUp) { viewModel.onEvent(it) } } + topBar = { AppBar(state, navigateUp, viewModel) } ) { Column( modifier = Modifier.padding(it) ) { - when (state.uartManagerState) { - NoDeviceState -> PaddingBox { DeviceConnectingView() } - is WorkingState -> when (state.uartManagerState.result) { - is IdleResult, - is ConnectingResult -> PaddingBox { DeviceConnectingView { NavigateUpButton(navigateUp) } } - is ConnectedResult -> PaddingBox { DeviceConnectingView { NavigateUpButton(navigateUp) } } - is DisconnectedResult -> PaddingBox { DeviceDisconnectedView(Reason.USER) { NavigateUpButton(navigateUp) } } - is LinkLossResult -> PaddingBox { DeviceDisconnectedView(Reason.LINK_LOSS) { NavigateUpButton(navigateUp) } } - is MissingServiceResult -> PaddingBox { DeviceDisconnectedView(Reason.MISSING_SERVICE) { NavigateUpButton(navigateUp) } } - is UnknownErrorResult -> PaddingBox { DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) } } - is SuccessResult -> SuccessScreen() + if (state.uartManagerState.deviceName == null) { + DeviceConnectingView() + } else { + when (state.uartManagerState.connectionState) { + GattConnectionState.STATE_CONNECTING -> PaddingBox { DeviceConnectingView { NavigateUpButton(navigateUp) } } + GattConnectionState.STATE_DISCONNECTED, + GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) } + GattConnectionState.STATE_CONNECTED -> SuccessScreen() } } } @@ -108,17 +97,13 @@ private fun PaddingBox(content: @Composable () -> Unit) { } @Composable -private fun AppBar(state: UARTViewState, navigateUp: () -> Unit, onEvent: (UARTViewEvent) -> Unit) { - val toolbarName = (state.uartManagerState as? WorkingState)?.let { - (it.result as? DeviceHolder)?.deviceName() - } - - if (toolbarName == null) { - BackIconAppBar(stringResource(id = R.string.uart_title), navigateUp) - } else { - LoggerIconAppBar(toolbarName, navigateUp, { onEvent(DisconnectEvent) }) { - onEvent(OpenLogger) +private fun AppBar(state: UARTViewState, navigateUp: () -> Unit, viewModel: UARTViewModel) { + if (state.uartManagerState.deviceName?.isNotBlank() == true) { + LoggerIconAppBar(state.uartManagerState.deviceName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) { + viewModel.onEvent(OpenLogger) } + } else { + BackIconAppBar(stringResource(id = R.string.uart_title), navigateUp) } } @@ -146,22 +131,14 @@ private fun SuccessScreen() { private fun KeyboardView() { val viewModel: UARTViewModel = hiltViewModel() val state = viewModel.state.collectAsState().value - (state.uartManagerState as? WorkingState)?.let { - (state.uartManagerState.result as? SuccessResult)?.let { - UARTContentView(it.data) { viewModel.onEvent(it) } - } - } + UARTContentView(state.uartManagerState) { viewModel.onEvent(it) } } @Composable private fun MacroView() { val viewModel: UARTViewModel = hiltViewModel() val state = viewModel.state.collectAsState().value - (state.uartManagerState as? WorkingState)?.let { - (state.uartManagerState.result as? SuccessResult)?.let { - MacroSection(state) { viewModel.onEvent(it) } - } - } + MacroSection(state) { viewModel.onEvent(it) } } @Composable diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTState.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTState.kt index 0cd0bfa1..ff9fe7ad 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTState.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTState.kt @@ -31,9 +31,8 @@ package no.nordicsemi.android.uart.view -import no.nordicsemi.android.service.BleManagerResult import no.nordicsemi.android.uart.data.UARTConfiguration -import no.nordicsemi.android.uart.data.UARTData +import no.nordicsemi.android.uart.data.UARTServiceData import no.nordicsemi.android.uart.data.UARTMacro internal data class UARTViewState( @@ -41,7 +40,7 @@ internal data class UARTViewState( val selectedConfigurationName: String? = null, val isConfigurationEdited: Boolean = false, val configurations: List = emptyList(), - val uartManagerState: HTSManagerState = NoDeviceState, + val uartManagerState: UARTServiceData = UARTServiceData(), val isInputVisible: Boolean = true ) { val showEditDialog: Boolean = editedPosition != null @@ -54,11 +53,3 @@ internal data class UARTViewState( } } } - -internal sealed class HTSManagerState - -internal data class WorkingState( - val result: BleManagerResult -) : HTSManagerState() - -internal object NoDeviceState : HTSManagerState() diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/viewmodel/UARTViewModel.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/viewmodel/UARTViewModel.kt index 9e3a0409..69dce4b8 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/viewmodel/UARTViewModel.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/viewmodel/UARTViewModel.kt @@ -52,15 +52,14 @@ import no.nordicsemi.android.analytics.UARTSendAnalyticsEvent import no.nordicsemi.android.common.navigation.NavigationResult import no.nordicsemi.android.common.navigation.Navigator import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.service.ConnectedResult -import no.nordicsemi.android.service.IdleResult +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId import no.nordicsemi.android.uart.data.MacroEol import no.nordicsemi.android.uart.data.UARTConfiguration import no.nordicsemi.android.uart.data.UARTMacro import no.nordicsemi.android.uart.data.UARTPersistentDataSource -import no.nordicsemi.android.uart.data.UART_SERVICE_UUID import no.nordicsemi.android.uart.repository.UARTRepository +import no.nordicsemi.android.uart.repository.UART_SERVICE_UUID import no.nordicsemi.android.uart.view.ClearOutputItems import no.nordicsemi.android.uart.view.DisconnectEvent import no.nordicsemi.android.uart.view.MacroInputSwitchClick @@ -78,7 +77,6 @@ import no.nordicsemi.android.uart.view.OnRunMacro import no.nordicsemi.android.uart.view.OpenLogger import no.nordicsemi.android.uart.view.UARTViewEvent import no.nordicsemi.android.uart.view.UARTViewState -import no.nordicsemi.android.uart.view.WorkingState import javax.inject.Inject @HiltViewModel @@ -100,12 +98,9 @@ internal class UARTViewModel @Inject constructor( } repository.data.onEach { - if (it is IdleResult) { - return@onEach - } - _state.value = _state.value.copy(uartManagerState = WorkingState(it)) + _state.value = _state.value.copy(uartManagerState = it) - (it as? ConnectedResult)?.let { + if (it.connectionState == GattConnectionState.STATE_CONNECTED) { analytics.logEvent(ProfileConnectedEvent(Profile.UART)) } }.launchIn(viewModelScope)