Change UART service

This commit is contained in:
Sylwester Zieliński
2022-02-14 16:28:50 +01:00
parent 91e3bcc1ea
commit a6f0f58448
12 changed files with 206 additions and 142 deletions

View File

@@ -4,6 +4,5 @@ import no.nordicsemi.android.utils.EMPTY
internal data class UARTData(
val text: String = String.EMPTY,
val macros: List<UARTMacro> = emptyList(),
val batteryLevel: Int = 0
)

View File

@@ -1,58 +1,74 @@
package no.nordicsemi.android.uart.data
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import android.bluetooth.BluetoothDevice
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 no.nordicsemi.android.service.BleManagerStatus
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import no.nordicsemi.android.ble.ktx.suspend
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.service.ConnectingResult
import no.nordicsemi.android.service.ServiceManager
import no.nordicsemi.android.uart.repository.UARTManager
import no.nordicsemi.android.uart.repository.UARTService
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class UARTRepository @Inject constructor() {
class UARTRepository @Inject constructor(
@ApplicationContext
private val context: Context,
private val serviceManager: ServiceManager,
) {
private var manager: UARTManager? = null
private val _data = MutableStateFlow(UARTData())
val data = _data.asStateFlow()
private val _data = MutableStateFlow<BleManagerResult<UARTData>>(ConnectingResult())
internal val data = _data.asStateFlow()
private val _command = MutableSharedFlow<UARTServiceCommand>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST)
val command = _command.asSharedFlow()
private val _isRunning = MutableStateFlow(false)
val isRunning = _isRunning.asStateFlow()
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
val status = _status.asStateFlow()
fun addNewMacro(macro: UARTMacro) {
_data.tryEmit(_data.value.copy(macros = _data.value.macros + macro))
fun launch(device: BluetoothDevice) {
serviceManager.startService(UARTService::class.java, device)
}
fun deleteMacro(macro: UARTMacro) {
val macros = _data.value.macros.toMutableList().apply {
remove(macro)
}
_data.tryEmit(_data.value.copy(macros = macros))
}
fun start(device: BluetoothDevice, scope: CoroutineScope) {
val manager = UARTManager(context, scope)
this.manager = manager
fun emitNewMessage(message: String) {
_data.tryEmit(_data.value.copy(text = message))
}
manager.dataHolder.status.onEach {
_data.value = it
}.launchIn(scope)
fun emitNewBatteryLevel(batteryLevel: Int) {
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
}
fun sendNewCommand(command: UARTServiceCommand) {
if (_command.subscriptionCount.value > 0) {
_command.tryEmit(command)
} else {
_status.tryEmit(BleManagerStatus.DISCONNECTED)
scope.launch {
manager.start(device)
}
}
fun setNewStatus(status: BleManagerStatus) {
_status.value = status
fun runMacro(macro: UARTMacro) {
manager?.send(macro.command)
}
fun clear() {
_status.value = BleManagerStatus.CONNECTING
private suspend fun UARTManager.start(device: BluetoothDevice) {
try {
connect(device)
.useAutoConnect(false)
.retry(3, 100)
.suspend()
_isRunning.value = true
} catch (e: Exception) {
e.printStackTrace()
}
}
fun release() {
serviceManager.stopService(UARTService::class.java)
manager?.disconnect()?.enqueue()
manager = null
_isRunning.value = false
}
}

View File

@@ -26,53 +26,66 @@ import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattService
import android.content.Context
import android.text.TextUtils
import android.util.Log
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
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.suspend
import no.nordicsemi.android.service.BatteryManager
import no.nordicsemi.android.uart.data.UARTRepository
import no.nordicsemi.android.ble.ktx.asValidResponseFlow
import no.nordicsemi.android.service.ConnectionObserverAdapter
import no.nordicsemi.android.uart.data.UARTData
import no.nordicsemi.android.utils.EMPTY
import no.nordicsemi.android.utils.launchWithCatch
import java.util.*
val UART_SERVICE_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,
scope: CoroutineScope,
private val dataHolder: UARTRepository
) : BatteryManager(context, scope) {
private val scope: CoroutineScope,
) : BleManager(context) {
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
private var rxCharacteristic: BluetoothGattCharacteristic? = null
private var txCharacteristic: BluetoothGattCharacteristic? = null
private val exceptionHandler = CoroutineExceptionHandler { _, t->
Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t)
}
private var useLongWrite = true
private inner class UARTManagerGattCallback : BatteryManagerGattCallback() {
private val data = MutableStateFlow(UARTData())
val dataHolder = ConnectionObserverAdapter<UARTData>()
init {
setConnectionObserver(dataHolder)
data.onEach {
dataHolder.setValue(it)
}.launchIn(scope)
}
private inner class UARTManagerGattCallback : BleManagerGattCallback() {
override fun initialize() {
setNotificationCallback(txCharacteristic).asFlow().onEach {
val text: String = it.getStringValue(0) ?: String.EMPTY
dataHolder.emitNewMessage(text)
data.tryEmit(data.value.copy(text = text))
}
scope.launch(exceptionHandler) {
requestMtu(260).suspend()
}
requestMtu(260).enqueue()
enableNotifications(txCharacteristic).enqueue()
scope.launch(exceptionHandler) {
enableNotifications(txCharacteristic).suspend()
}
setNotificationCallback(batteryLevelCharacteristic).asValidResponseFlow<BatteryLevelResponse>().onEach {
data.value = data.value.copy(batteryLevel = it.batteryLevel)
}.launchIn(scope)
enableNotifications(batteryLevelCharacteristic).enqueue()
}
override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
@@ -100,24 +113,24 @@ internal class UARTManager(
useLongWrite = false
}
}
return rxCharacteristic != null && txCharacteristic != null && (writeRequest || writeCommand)
}
override fun onDeviceDisconnected() {
rxCharacteristic = null
txCharacteristic = null
useLongWrite = true
gatt.getService(BATTERY_SERVICE_UUID)?.run {
batteryLevelCharacteristic = getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)
}
return rxCharacteristic != null && txCharacteristic != null && batteryLevelCharacteristic != null && (writeRequest || writeCommand)
}
override fun onServicesInvalidated() {
batteryLevelCharacteristic = null
rxCharacteristic = null
txCharacteristic = null
useLongWrite = true
}
}
fun send(text: String) {
if (rxCharacteristic == null) return
if (!TextUtils.isEmpty(text)) {
scope.launch(exceptionHandler) {
scope.launchWithCatch {
val request: WriteRequest = writeCharacteristic(rxCharacteristic, text.toByteArray(), BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
if (!useLongWrite) {
request.split()
@@ -127,11 +140,7 @@ internal class UARTManager(
}
}
override fun onBatteryLevelChanged(batteryLevel: Int) {
dataHolder.emitNewBatteryLevel(batteryLevel)
}
override fun getGattCallback(): BatteryManagerGattCallback {
override fun getGattCallback(): BleManagerGattCallback {
return UARTManagerGattCallback()
}
}

View File

@@ -1,37 +1,27 @@
package no.nordicsemi.android.uart.repository
import android.bluetooth.BluetoothDevice
import android.content.Intent
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.service.ForegroundBleService
import no.nordicsemi.android.uart.data.DisconnectCommand
import no.nordicsemi.android.uart.data.SendTextCommand
import no.nordicsemi.android.service.DEVICE_DATA
import no.nordicsemi.android.service.NotificationService
import no.nordicsemi.android.uart.data.UARTRepository
import no.nordicsemi.android.utils.exhaustive
import javax.inject.Inject
@AndroidEntryPoint
internal class UARTService : ForegroundBleService() {
internal class UARTService : NotificationService() {
@Inject
lateinit var repository: UARTRepository
override val manager: UARTManager by lazy { UARTManager(this, scope, repository) }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
override fun onCreate() {
super.onCreate()
val device = intent!!.getParcelableExtra<BluetoothDevice>(DEVICE_DATA)!!
// status.onEach {
// val status = it.mapToSimpleManagerStatus()
// repository.setNewStatus(status)
// stopIfDisconnected(status)
// }.launchIn(scope)
repository.start(device, lifecycleScope)
repository.command.onEach {
when (it) {
DisconnectCommand -> stopSelf()
is SendTextCommand -> manager.send(it.command)
}.exhaustive
}.launchIn(scope)
return START_REDELIVER_INTENT
}
}

View File

@@ -18,19 +18,26 @@ import no.nordicsemi.android.theme.view.ScreenSection
import no.nordicsemi.android.theme.view.SectionTitle
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.UARTData
import no.nordicsemi.android.uart.data.UARTMacro
@Composable
internal fun UARTContentView(state: UARTData, onEvent: (UARTViewEvent) -> Unit) {
internal fun UARTContentView(state: UARTData, macros: List<UARTMacro>, onEvent: (UARTViewEvent) -> Unit) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(16.dp)
) {
InputSection(state, onEvent)
InputSection(macros, onEvent)
Spacer(modifier = Modifier.height(16.dp))
if (state.text.isNotEmpty()) {
OutputSection(state.text)
Spacer(modifier = Modifier.height(16.dp))
}
Button(
onClick = { onEvent(OnDisconnectButtonClick) }
onClick = { onEvent(DisconnectEvent) }
) {
Text(text = stringResource(id = R.string.disconnect))
}
@@ -38,7 +45,7 @@ internal fun UARTContentView(state: UARTData, onEvent: (UARTViewEvent) -> Unit)
}
@Composable
private fun InputSection(state: UARTData, onEvent: (UARTViewEvent) -> Unit) {
private fun InputSection(macros: List<UARTMacro>, onEvent: (UARTViewEvent) -> Unit) {
val showSearchDialog = remember { mutableStateOf(false) }
if (showSearchDialog.value) {
@@ -53,13 +60,13 @@ private fun InputSection(state: UARTData, onEvent: (UARTViewEvent) -> Unit) {
Spacer(modifier = Modifier.height(16.dp))
state.macros.forEach {
macros.forEach {
MacroItem(macro = it, onEvent = onEvent)
Spacer(modifier = Modifier.height(16.dp))
}
if (state.macros.isEmpty()) {
if (macros.isEmpty()) {
Text(
text = stringResource(id = R.string.uart_no_macros_info),
style = MaterialTheme.typography.bodyMedium
@@ -78,7 +85,7 @@ private fun InputSection(state: UARTData, onEvent: (UARTViewEvent) -> Unit) {
}
@Composable
private fun OutputSection(state: UARTData, onEvent: (UARTViewEvent) -> Unit) {
private fun OutputSection(text: String) {
ScreenSection {
Column(
horizontalAlignment = Alignment.CenterHorizontally
@@ -87,7 +94,7 @@ private fun OutputSection(state: UARTData, onEvent: (UARTViewEvent) -> Unit) {
Spacer(modifier = Modifier.height(16.dp))
Text(text = text)
}
}
}

View File

@@ -8,9 +8,15 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.service.*
import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.scanner.DeviceConnectingView
import no.nordicsemi.android.theme.view.scanner.DeviceDisconnectedView
import no.nordicsemi.android.theme.view.scanner.NoDeviceView
import no.nordicsemi.android.theme.view.scanner.Reason
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.viewmodel.UARTViewModel
import no.nordicsemi.android.utils.exhaustive
@Composable
fun UARTScreen() {
@@ -18,15 +24,25 @@ fun UARTScreen() {
val state = viewModel.state.collectAsState().value
Column {
val navigateUp = { viewModel.onEvent(NavigateUp) }
BackIconAppBar(stringResource(id = R.string.uart_title)) {
viewModel.onEvent(OnDisconnectButtonClick)
viewModel.onEvent(DisconnectEvent)
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
// when (state) {
// is DisplayDataState -> UARTContentView(state.data) { viewModel.onEvent(it) }
// LoadingState -> DeviceConnectingView()
// }.exhaustive
when (state.uartManagerState) {
NoDeviceState -> NoDeviceView()
is WorkingState -> when (state.uartManagerState.result) {
is ConnectingResult,
is ReadyResult
-> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE, navigateUp)
is SuccessResult -> UARTContentView(state.uartManagerState.result.data, state.macros) { viewModel.onEvent(it) }
}
}.exhaustive
}
}
}

View File

@@ -1,9 +1,16 @@
package no.nordicsemi.android.uart.view
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.uart.data.UARTData
import no.nordicsemi.android.uart.data.UARTMacro
internal sealed class UARTViewState
internal data class UARTViewState(
val macros: List<UARTMacro> = emptyList(),
val uartManagerState: HTSManagerState = NoDeviceState
)
internal object LoadingState : UARTViewState()
internal sealed class HTSManagerState
internal data class DisplayDataState(val data: UARTData) : UARTViewState()
internal data class WorkingState(val result: BleManagerResult<UARTData>) : HTSManagerState()
internal object NoDeviceState : HTSManagerState()

View File

@@ -9,4 +9,6 @@ internal data class OnDeleteMacro(val macro: UARTMacro) : UARTViewEvent()
internal data class OnRunMacro(val macro: UARTMacro) : UARTViewEvent()
internal object OnDisconnectButtonClick : UARTViewEvent()
internal object DisconnectEvent : UARTViewEvent()
internal object NavigateUp : UARTViewEvent()

View File

@@ -3,14 +3,13 @@ package no.nordicsemi.android.uart.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.navigation.*
import no.nordicsemi.android.service.BleManagerStatus
import no.nordicsemi.android.service.ServiceManager
import no.nordicsemi.android.uart.data.DisconnectCommand
import no.nordicsemi.android.uart.data.SendTextCommand
import no.nordicsemi.android.uart.data.UARTMacro
import no.nordicsemi.android.uart.data.UARTRepository
import no.nordicsemi.android.uart.repository.UARTService
import no.nordicsemi.android.uart.repository.UART_SERVICE_UUID
import no.nordicsemi.android.uart.view.*
import no.nordicsemi.android.utils.exhaustive
@@ -21,19 +20,23 @@ import javax.inject.Inject
@HiltViewModel
internal class UARTViewModel @Inject constructor(
private val repository: UARTRepository,
private val serviceManager: ServiceManager,
private val navigationManager: NavigationManager
) : ViewModel() {
val state = repository.data.combine(repository.status) { data, status ->
// when (status) {
// BleManagerStatus.CONNECTING -> LoadingState
// BleManagerStatus.OK,
// BleManagerStatus.DISCONNECTED -> DisplayDataState(data)
// }
}.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState)
private val _state = MutableStateFlow(UARTViewState())
val state = _state.asStateFlow()
init {
if (!repository.isRunning.value) {
requestBluetoothDevice()
}
repository.data.onEach {
_state.value = _state.value.copy(uartManagerState = WorkingState(it))
}.launchIn(viewModelScope)
}
private fun requestBluetoothDevice() {
navigationManager.navigateTo(ScannerDestinationId, UUIDArgument(UART_SERVICE_UUID))
navigationManager.recentResult.onEach {
@@ -41,32 +44,38 @@ internal class UARTViewModel @Inject constructor(
handleArgs(it)
}
}.launchIn(viewModelScope)
repository.status.onEach {
if (it == BleManagerStatus.DISCONNECTED) {
navigationManager.navigateUp()
}
}.launchIn(viewModelScope)
}
private fun handleArgs(args: DestinationResult) {
when (args) {
is CancelDestinationResult -> navigationManager.navigateUp()
is SuccessDestinationResult -> serviceManager.startService(UARTService::class.java, args.getDevice())
is SuccessDestinationResult -> repository.launch(args.getDevice().device)
}.exhaustive
}
fun onEvent(event: UARTViewEvent) {
when (event) {
is OnCreateMacro -> repository.addNewMacro(event.macro)
is OnDeleteMacro -> repository.deleteMacro(event.macro)
OnDisconnectButtonClick -> repository.sendNewCommand(DisconnectCommand)
is OnRunMacro -> repository.sendNewCommand(SendTextCommand(event.macro.command))
is OnCreateMacro -> addNewMacro(event.macro)
is OnDeleteMacro -> deleteMacro(event.macro)
DisconnectEvent -> disconnect()
is OnRunMacro -> repository.runMacro(event.macro)
NavigateUp -> navigationManager.navigateUp()
}.exhaustive
}
override fun onCleared() {
super.onCleared()
repository.clear()
private fun addNewMacro(macro: UARTMacro) {
_state.tryEmit(_state.value.copy(macros = _state.value.macros + macro))
}
private fun deleteMacro(macro: UARTMacro) {
val macros = _state.value.macros.toMutableList().apply {
remove(macro)
}
_state.tryEmit(_state.value.copy(macros = macros))
}
private fun disconnect() {
repository.release()
navigationManager.navigateUp()
}
}