Merge pull request #113 from NordicSemiconductor/feature/uart_improvement

Feature/uart improvement
This commit is contained in:
Sylwester Zieliński
2022-05-10 15:43:46 +02:00
committed by GitHub
72 changed files with 1070 additions and 712 deletions

View File

@@ -34,6 +34,7 @@
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTask"
android:windowSoftInputMode="stateVisible|adjustResize"
android:theme="@style/AppTheme.SplashScreen">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@@ -2,7 +2,6 @@ package no.nordicsemi.android.nrftoolbox
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@@ -13,7 +12,6 @@ import no.nordicsemi.android.material.you.NordicActivity
import no.nordicsemi.android.material.you.NordicTheme
import no.nordicsemi.android.navigation.NavigationView
import no.nordicsemi.android.nrftoolbox.repository.ActivitySignals
import no.nordicsemi.android.nrftoolbox.viewmodel.HomeViewModel
import no.nordicsemi.ui.scanner.ScannerDestinations
import javax.inject.Inject

View File

@@ -1,5 +1,8 @@
package no.nordicsemi.android.service
import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice
sealed class BleManagerResult <T> {
fun isRunning(): Boolean {
@@ -15,8 +18,13 @@ sealed class BleManagerResult <T> {
}
}
class IdleResult<T> : BleManagerResult<T>()
class ConnectingResult<T> : BleManagerResult<T>()
data class SuccessResult<T>(val data: T) : BleManagerResult<T>()
data class SuccessResult<T>(val device: BluetoothDevice, val data: T) : BleManagerResult<T>() {
@SuppressLint("MissingPermission")
fun deviceName(): String = device.name ?: device.address
}
class LinkLossResult<T>(val data: T) : BleManagerResult<T>()
class DisconnectedResult<T> : BleManagerResult<T>()

View File

@@ -34,7 +34,7 @@ class ConnectionObserverAdapter<T> : ConnectionObserver {
override fun onDeviceReady(device: BluetoothDevice) {
Log.d(TAG, "onDeviceReady()")
_status.value = SuccessResult(lastValue!!)
_status.value = SuccessResult(device, lastValue!!)
}
override fun onDeviceDisconnecting(device: BluetoothDevice) {
@@ -53,8 +53,8 @@ class ConnectionObserverAdapter<T> : ConnectionObserver {
fun setValue(value: T) {
lastValue = value
if (_status.value.isRunning()) {
_status.value = SuccessResult(value)
(_status.value as? SuccessResult)?.let {
_status.value = SuccessResult(it.device, value)
}
}
}

View File

@@ -4,16 +4,10 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -56,6 +50,39 @@ fun TitleAppBar(text: String) {
)
}
@Composable
fun LoggerBackIconAppBar(text: String, onClick: () -> Unit) {
SmallTopAppBar(
title = { Text(text) },
colors = TopAppBarDefaults.smallTopAppBarColors(
scrolledContainerColor = MaterialTheme.colorScheme.primary,
containerColor = colorResource(id = R.color.appBarColor),
titleContentColor = MaterialTheme.colorScheme.onPrimary,
actionIconContentColor = MaterialTheme.colorScheme.onPrimary,
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
),
navigationIcon = {
IconButton(onClick = { onClick() }) {
Icon(
Icons.Default.ArrowBack,
tint = MaterialTheme.colorScheme.onPrimary,
contentDescription = stringResource(id = R.string.back_screen),
)
}
},
actions = {
IconButton(onClick = { onClick() }) {
Icon(
painterResource(id = R.drawable.ic_logger),
contentDescription = stringResource(id = R.string.open_logger),
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(24.dp)
)
}
}
)
}
@Composable
fun BackIconAppBar(text: String, onClick: () -> Unit) {
SmallTopAppBar(
@@ -71,25 +98,16 @@ fun BackIconAppBar(text: String, onClick: () -> Unit) {
IconButton(onClick = { onClick() }) {
Icon(
Icons.Default.ArrowBack,
tint = MaterialTheme.colorScheme.onPrimary,
contentDescription = stringResource(id = R.string.back_screen),
)
}
},
actions = {
IconButton(onClick = { onClick() }) {
Icon(
painterResource(id = R.drawable.ic_logger),
contentDescription = stringResource(id = R.string.back_screen),
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(24.dp)
)
}
}
)
}
@Composable
fun LoggerIconAppBar(text: String, onClick: () -> Unit, onLoggerClick: () -> Unit) {
fun LoggerIconAppBar(text: String, onClick: () -> Unit, onDisconnectClick: () -> Unit, onLoggerClick: () -> Unit) {
SmallTopAppBar(
title = { Text(text) },
colors = TopAppBarDefaults.smallTopAppBarColors(
@@ -103,15 +121,25 @@ fun LoggerIconAppBar(text: String, onClick: () -> Unit, onLoggerClick: () -> Uni
IconButton(onClick = { onClick() }) {
Icon(
Icons.Default.ArrowBack,
tint = MaterialTheme.colorScheme.onPrimary,
contentDescription = stringResource(id = R.string.back_screen),
)
}
},
actions = {
TextButton(
onClick = { onDisconnectClick() },
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onPrimary
)
) {
Text(stringResource(id = R.string.disconnect))
}
IconButton(onClick = { onLoggerClick() }) {
Icon(
painterResource(id = R.drawable.ic_logger),
contentDescription = stringResource(id = R.string.back_screen),
contentDescription = stringResource(id = R.string.open_logger),
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(24.dp)
)

View File

@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -17,43 +18,16 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import no.nordicsemi.android.material.you.Card
import no.nordicsemi.android.theme.R
@Composable
fun StringListDialog(config: StringListDialogConfig) {
Dialog(onDismissRequest = { config.onResult(FlowCanceled) }) {
StringListView(config)
}
}
@Composable
fun StringListView(config: StringListDialogConfig) {
Card(
modifier = Modifier.height(300.dp),
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(10.dp),
elevation = 0.dp
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
Column(
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = config.title ?: stringResource(id = R.string.dialog).toAnnotatedString(),
style = MaterialTheme.typography.headlineMedium
)
}
Spacer(modifier = Modifier.size(8.dp))
AlertDialog(
onDismissRequest = { config.onResult(FlowCanceled) },
title = { Text(text = config.title ?: stringResource(id = R.string.dialog).toAnnotatedString()) },
text = {
Column(
modifier = Modifier
.fillMaxHeight(0.8f)
.verticalScroll(rememberScrollState())
) {
@@ -81,17 +55,13 @@ fun StringListView(config: StringListDialogConfig) {
}
}
}
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.End
) {
},
confirmButton = {
TextButton(onClick = { config.onResult(FlowCanceled) }) {
Text(
text = stringResource(id = R.string.cancel),
)
}
}
}
}
)
}

View File

@@ -3,12 +3,13 @@
<string name="app_name">nRF Toolbox</string>
<string name="dialog">Dialog</string>
<string name="cancel">CANCEL</string>
<string name="cancel">Cancel</string>
<string name="go_up">Back</string>
<string name="close_app">Close the application.</string>
<string name="back_screen">Close the current screen.</string>
<string name="open_logger">Open logger application.</string>
<string name="disconnect">Disconnect</string>
<string name="field_battery">Battery</string>

View File

@@ -61,7 +61,7 @@ internal class BPSManager(
val dataHolder = ConnectionObserverAdapter<BPSData>()
init {
setConnectionObserver(dataHolder)
connectionObserver = dataHolder
data.onEach {
dataHolder.setValue(it)

View File

@@ -1,6 +1,5 @@
package no.nordicsemi.android.bps.repository
import android.bluetooth.BluetoothDevice
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ViewModelScoped
@@ -14,6 +13,7 @@ import no.nordicsemi.android.bps.data.BPSManager
import no.nordicsemi.android.logger.ToolboxLogger
import no.nordicsemi.android.logger.ToolboxLoggerFactory
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject
@ViewModelScoped
@@ -25,9 +25,9 @@ internal class BPSRepository @Inject constructor(
private var logger: ToolboxLogger? = null
fun downloadData(device: BluetoothDevice): Flow<BleManagerResult<BPSData>> = callbackFlow {
fun downloadData(device: DiscoveredBluetoothDevice): Flow<BleManagerResult<BPSData>> = callbackFlow {
val scope = this
val createdLogger = toolboxLoggerFactory.create("BPS", device.address).also {
val createdLogger = toolboxLoggerFactory.create("BPS", device.address()).also {
logger = it
}
val manager = BPSManager(context, scope, createdLogger)
@@ -36,7 +36,7 @@ internal class BPSRepository @Inject constructor(
trySend(it)
}.launchIn(scope)
manager.connect(device)
manager.connect(device.device)
.useAutoConnect(false)
.retry(3, 100)
.enqueue()

View File

@@ -15,7 +15,7 @@ import no.nordicsemi.android.bps.R
import no.nordicsemi.android.bps.data.BPSData
@Composable
internal fun BPSContentView(state: BPSData, onEvent: (BPSScreenViewEvent) -> Unit) {
internal fun BPSContentView(state: BPSData, onEvent: (BPSViewEvent) -> Unit) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(16.dp)

View File

@@ -1,5 +1,6 @@
package no.nordicsemi.android.bps.view
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -9,14 +10,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.bps.R
import no.nordicsemi.android.bps.data.BPSData
import no.nordicsemi.android.bps.viewmodel.BPSViewModel
import no.nordicsemi.android.service.*
import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.LoggerIconAppBar
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.ui.scanner.ui.Reason
@Composable
@@ -27,16 +29,13 @@ fun BPSScreen() {
Column {
val navigateUp = { viewModel.onEvent(DisconnectEvent) }
LoggerIconAppBar(stringResource(id = R.string.bps_title), {
viewModel.onEvent(DisconnectEvent)
}) {
viewModel.onEvent(OpenLoggerEvent)
}
AppBar(state = state, navigateUp = navigateUp, viewModel = viewModel)
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) {
NoDeviceState -> NoDeviceView()
is WorkingState -> when (state.result) {
is IdleResult,
is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
@@ -48,3 +47,20 @@ fun BPSScreen() {
}
}
}
@Composable
private fun AppBar(state: BPSViewState, navigateUp: () -> Unit, viewModel: BPSViewModel) {
val toolbarName = (state as? WorkingState)?.let {
(it.result as? SuccessResult<BPSData>)?.deviceName()
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.bps_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, {
viewModel.onEvent(DisconnectEvent)
}, { viewModel.onEvent(DisconnectEvent) }) {
viewModel.onEvent(OpenLoggerEvent)
}
}
}

View File

@@ -1,7 +0,0 @@
package no.nordicsemi.android.bps.view
internal sealed class BPSScreenViewEvent
internal object DisconnectEvent : BPSScreenViewEvent()
internal object OpenLoggerEvent : BPSScreenViewEvent()

View File

@@ -0,0 +1,7 @@
package no.nordicsemi.android.bps.view
internal sealed class BPSViewEvent
internal object DisconnectEvent : BPSViewEvent()
internal object OpenLoggerEvent : BPSViewEvent()

View File

@@ -2,8 +2,12 @@ package no.nordicsemi.android.bps.view
import no.nordicsemi.android.bps.data.BPSData
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
internal sealed class BPSViewState
internal data class WorkingState(val result: BleManagerResult<BPSData>) : BPSViewState()
internal data class WorkingState(
val result: BleManagerResult<BPSData>
) : BPSViewState()
internal object NoDeviceState : BPSViewState()

View File

@@ -43,7 +43,7 @@ internal class BPSViewModel @Inject constructor(
}.exhaustive
}
fun onEvent(event: BPSScreenViewEvent) {
fun onEvent(event: BPSViewEvent) {
when (event) {
DisconnectEvent -> navigationManager.navigateUp()
OpenLoggerEvent -> repository.openLogger()
@@ -51,7 +51,7 @@ internal class BPSViewModel @Inject constructor(
}
private fun connectDevice(device: DiscoveredBluetoothDevice) {
repository.downloadData(device.device).onEach {
repository.downloadData(device).onEach {
_state.value = WorkingState(it)
}.launchIn(viewModelScope)
}

View File

@@ -85,7 +85,7 @@ internal class CGMManager(
val dataHolder = ConnectionObserverAdapter<CGMData>()
init {
setConnectionObserver(dataHolder)
connectionObserver = dataHolder
data.onEach {
dataHolder.setValue(it)

View File

@@ -14,6 +14,7 @@ import no.nordicsemi.android.logger.ToolboxLoggerFactory
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.service.ConnectingResult
import no.nordicsemi.android.service.ServiceManager
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject
import javax.inject.Singleton
@@ -33,12 +34,12 @@ class CGMRepository @Inject constructor(
val isRunning = data.map { it.isRunning() }
val hasBeenDisconnected = data.map { it.hasBeenDisconnected() }
fun launch(device: BluetoothDevice) {
fun launch(device: DiscoveredBluetoothDevice) {
serviceManager.startService(CGMService::class.java, device)
}
fun start(device: BluetoothDevice, scope: CoroutineScope) {
val createdLogger = toolboxLoggerFactory.create("CGMS", device.address).also {
fun start(device: DiscoveredBluetoothDevice, scope: CoroutineScope) {
val createdLogger = toolboxLoggerFactory.create("CGMS", device.address()).also {
logger = it
}
val manager = CGMManager(context, scope, createdLogger)
@@ -53,9 +54,9 @@ class CGMRepository @Inject constructor(
}
}
private suspend fun CGMManager.start(device: BluetoothDevice) {
private suspend fun CGMManager.start(device: DiscoveredBluetoothDevice) {
try {
connect(device)
connect(device.device)
.useAutoConnect(false)
.retry(3, 100)
.suspend()

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.service.DEVICE_DATA
import no.nordicsemi.android.service.NotificationService
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject
@AndroidEntryPoint
@@ -19,7 +20,7 @@ internal class CGMService : NotificationService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val device = intent!!.getParcelableExtra<BluetoothDevice>(DEVICE_DATA)!!
val device = intent!!.getParcelableExtra<DiscoveredBluetoothDevice>(DEVICE_DATA)!!
repository.start(device, lifecycleScope)

View File

@@ -9,32 +9,32 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.cgms.R
import no.nordicsemi.android.cgms.viewmodel.CGMScreenViewModel
import no.nordicsemi.android.cgms.data.CGMData
import no.nordicsemi.android.cgms.viewmodel.CGMViewModel
import no.nordicsemi.android.service.*
import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.LoggerIconAppBar
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.ui.scanner.ui.Reason
@Composable
fun CGMScreen() {
val viewModel: CGMScreenViewModel = hiltViewModel()
val viewModel: CGMViewModel = hiltViewModel()
val state = viewModel.state.collectAsState().value
Column {
val navigateUp = { viewModel.onEvent(NavigateUp) }
LoggerIconAppBar(stringResource(id = R.string.cgms_title), navigateUp) {
viewModel.onEvent(OpenLoggerEvent)
}
AppBar(state, navigateUp, viewModel)
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) {
NoDeviceState -> NoDeviceView()
is WorkingState -> when (state.result) {
is IdleResult,
is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
@@ -46,3 +46,18 @@ fun CGMScreen() {
}
}
}
@Composable
private fun AppBar(state: CGMViewState, navigateUp: () -> Unit, viewModel: CGMViewModel) {
val toolbarName = (state as? WorkingState)?.let {
(it.result as? SuccessResult<CGMData>)?.deviceName()
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.cgms_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) {
viewModel.onEvent(OpenLoggerEvent)
}
}
}

View File

@@ -3,7 +3,7 @@ package no.nordicsemi.android.cgms.view
import no.nordicsemi.android.cgms.data.CGMData
import no.nordicsemi.android.service.BleManagerResult
internal sealed class BPSViewState
internal sealed class CGMViewState
internal data class WorkingState(val result: BleManagerResult<CGMData>) : BPSViewState()
internal object NoDeviceState : BPSViewState()
internal data class WorkingState(val result: BleManagerResult<CGMData>) : CGMViewState()
internal object NoDeviceState : CGMViewState()

View File

@@ -16,12 +16,12 @@ import no.nordicsemi.ui.scanner.ScannerDestinationId
import javax.inject.Inject
@HiltViewModel
internal class CGMScreenViewModel @Inject constructor(
internal class CGMViewModel @Inject constructor(
private val repository: CGMRepository,
private val navigationManager: NavigationManager
) : ViewModel() {
private val _state = MutableStateFlow<BPSViewState>(NoDeviceState)
private val _state = MutableStateFlow<CGMViewState>(NoDeviceState)
val state = _state.asStateFlow()
init {
@@ -58,7 +58,7 @@ internal class CGMScreenViewModel @Inject constructor(
private fun handleArgs(args: DestinationResult) {
when (args) {
is CancelDestinationResult -> navigationManager.navigateUp()
is SuccessDestinationResult -> repository.launch(args.getDevice().device)
is SuccessDestinationResult -> repository.launch(args.getDevice())
}.exhaustive
}

View File

@@ -59,7 +59,7 @@ internal class CSCManager(
val dataHolder = ConnectionObserverAdapter<CSCData>()
init {
setConnectionObserver(dataHolder)
connectionObserver = dataHolder
data.onEach {
dataHolder.setValue(it)

View File

@@ -15,6 +15,7 @@ import no.nordicsemi.android.logger.ToolboxLoggerFactory
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.service.ConnectingResult
import no.nordicsemi.android.service.ServiceManager
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject
import javax.inject.Singleton
@@ -34,12 +35,12 @@ class CSCRepository @Inject constructor(
val isRunning = data.map { it.isRunning() }
val hasBeenDisconnected = data.map { it.hasBeenDisconnected() }
fun launch(device: BluetoothDevice) {
fun launch(device: DiscoveredBluetoothDevice) {
serviceManager.startService(CSCService::class.java, device)
}
fun start(device: BluetoothDevice, scope: CoroutineScope) {
val createdLogger = toolboxLoggerFactory.create("CSC", device.address).also {
fun start(device: DiscoveredBluetoothDevice, scope: CoroutineScope) {
val createdLogger = toolboxLoggerFactory.create("CSC", device.address()).also {
logger = it
}
val manager = CSCManager(context, scope, createdLogger)
@@ -58,9 +59,9 @@ class CSCRepository @Inject constructor(
manager?.setWheelSize(wheelSize)
}
private suspend fun CSCManager.start(device: BluetoothDevice) {
private suspend fun CSCManager.start(device: DiscoveredBluetoothDevice) {
try {
connect(device)
connect(device.device)
.useAutoConnect(false)
.retry(3, 100)
.suspend()

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.service.DEVICE_DATA
import no.nordicsemi.android.service.NotificationService
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject
@AndroidEntryPoint
@@ -19,7 +20,7 @@ internal class CSCService : NotificationService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val device = intent!!.getParcelableExtra<BluetoothDevice>(DEVICE_DATA)!!
val device = intent!!.getParcelableExtra<DiscoveredBluetoothDevice>(DEVICE_DATA)!!
repository.start(device, lifecycleScope)

View File

@@ -9,14 +9,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.csc.R
import no.nordicsemi.android.csc.data.CSCData
import no.nordicsemi.android.csc.viewmodel.CSCViewModel
import no.nordicsemi.android.service.*
import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.LoggerIconAppBar
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.ui.scanner.ui.Reason
@Composable
@@ -27,14 +28,13 @@ fun CSCScreen() {
Column {
val navigateUp = { viewModel.onEvent(NavigateUp) }
LoggerIconAppBar(stringResource(id = R.string.csc_title), navigateUp) {
viewModel.onEvent(OpenLogger)
}
AppBar(state, navigateUp, viewModel)
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state.cscManagerState) {
NoDeviceState -> NoDeviceView()
is WorkingState -> when (state.cscManagerState.result) {
is IdleResult,
is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(OnDisconnectButtonClick) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
@@ -46,3 +46,18 @@ fun CSCScreen() {
}
}
}
@Composable
private fun AppBar(state: CSCViewState, navigateUp: () -> Unit, viewModel: CSCViewModel) {
val toolbarName = (state.cscManagerState as? WorkingState)?.let {
(it.result as? SuccessResult<CSCData>)?.deviceName()
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.csc_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, navigateUp, { viewModel.onEvent(OnDisconnectButtonClick) }) {
viewModel.onEvent(OpenLogger)
}
}
}

View File

@@ -48,7 +48,7 @@ internal class CSCViewModel @Inject constructor(
private fun handleArgs(args: DestinationResult) {
when (args) {
is CancelDestinationResult -> navigationManager.navigateUp()
is SuccessDestinationResult -> repository.launch(args.getDevice().device)
is SuccessDestinationResult -> repository.launch(args.getDevice())
}.exhaustive
}

View File

@@ -71,7 +71,7 @@ internal class GLSManager(
val dataHolder = ConnectionObserverAdapter<GLSData>()
init {
setConnectionObserver(dataHolder)
connectionObserver = dataHolder
data.onEach {
dataHolder.setValue(it)

View File

@@ -6,7 +6,7 @@ import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.gls.R
import no.nordicsemi.android.gls.details.viewmodel.GLSDetailsViewModel
import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.LoggerBackIconAppBar
@Composable
internal fun GLSDetailsScreen() {
@@ -14,7 +14,7 @@ internal fun GLSDetailsScreen() {
val record = viewModel.record
Column {
BackIconAppBar(stringResource(id = R.string.gls_title)) {
LoggerBackIconAppBar(stringResource(id = R.string.gls_title)) {
viewModel.navigateBack()
}

View File

@@ -9,14 +9,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.gls.R
import no.nordicsemi.android.gls.data.GLSData
import no.nordicsemi.android.gls.main.viewmodel.GLSViewModel
import no.nordicsemi.android.service.*
import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.LoggerIconAppBar
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.ui.scanner.ui.Reason
@Composable
@@ -27,16 +28,13 @@ fun GLSScreen() {
Column {
val navigateUp = { viewModel.onEvent(DisconnectEvent) }
LoggerIconAppBar(stringResource(id = R.string.gls_title), {
viewModel.onEvent(DisconnectEvent)
}) {
viewModel.onEvent(OpenLoggerEvent)
}
AppBar(state, navigateUp, viewModel)
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) {
NoDeviceState -> NoDeviceView()
is WorkingState -> when (state.result) {
is IdleResult,
is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
@@ -48,3 +46,20 @@ fun GLSScreen() {
}
}
}
@Composable
private fun AppBar(state: GLSViewState, navigateUp: () -> Unit, viewModel: GLSViewModel) {
val toolbarName = (state as? WorkingState)?.let {
(it.result as? SuccessResult<GLSData>)?.deviceName()
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.gls_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, {
viewModel.onEvent(DisconnectEvent)
}, { viewModel.onEvent(DisconnectEvent) }) {
viewModel.onEvent(OpenLoggerEvent)
}
}
}

View File

@@ -3,7 +3,7 @@ package no.nordicsemi.android.gls.main.view
import no.nordicsemi.android.gls.data.GLSData
import no.nordicsemi.android.service.BleManagerResult
internal sealed class BPSViewState
internal sealed class GLSViewState
internal data class WorkingState(val result: BleManagerResult<GLSData>) : BPSViewState()
internal object NoDeviceState : BPSViewState()
internal data class WorkingState(val result: BleManagerResult<GLSData>) : GLSViewState()
internal object NoDeviceState : GLSViewState()

View File

@@ -21,7 +21,7 @@ internal class GLSViewModel @Inject constructor(
private val navigationManager: NavigationManager
) : ViewModel() {
private val _state = MutableStateFlow<BPSViewState>(NoDeviceState)
private val _state = MutableStateFlow<GLSViewState>(NoDeviceState)
val state = _state.asStateFlow()
init {
@@ -52,7 +52,7 @@ internal class GLSViewModel @Inject constructor(
}
private fun connectDevice(device: DiscoveredBluetoothDevice) {
repository.downloadData(device.device).onEach {
repository.downloadData(device).onEach {
_state.value = WorkingState(it)
}.launchIn(viewModelScope)
}

View File

@@ -18,6 +18,7 @@ import no.nordicsemi.android.logger.ToolboxLogger
import no.nordicsemi.android.logger.ToolboxLoggerFactory
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject
@ViewModelScoped
@@ -30,14 +31,14 @@ internal class GLSRepository @Inject constructor(
private var manager: GLSManager? = null
private var logger: ToolboxLogger? = null
fun downloadData(device: BluetoothDevice): Flow<BleManagerResult<GLSData>> = callbackFlow {
fun downloadData(device: DiscoveredBluetoothDevice): Flow<BleManagerResult<GLSData>> = callbackFlow {
val scope = this
val createdLogger = toolboxLoggerFactory.create("GLS", device.address).also {
val createdLogger = toolboxLoggerFactory.create("GLS", device.address()).also {
logger = it
}
val managerInstance = manager ?: GLSManager(context, scope, createdLogger).apply {
try {
connect(device)
connect(device.device)
.useAutoConnect(false)
.retry(3, 100)
.suspend()

View File

@@ -61,7 +61,7 @@ internal class HRSManager(
val dataHolder = ConnectionObserverAdapter<HRSData>()
init {
setConnectionObserver(dataHolder)
connectionObserver = dataHolder
data.onEach {
dataHolder.setValue(it)

View File

@@ -14,6 +14,7 @@ import no.nordicsemi.android.logger.ToolboxLoggerFactory
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.service.ConnectingResult
import no.nordicsemi.android.service.ServiceManager
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject
import javax.inject.Singleton
@@ -33,12 +34,12 @@ class HRSRepository @Inject constructor(
val isRunning = data.map { it.isRunning() }
val hasBeenDisconnected = data.map { it.hasBeenDisconnected() }
fun launch(device: BluetoothDevice) {
fun launch(device: DiscoveredBluetoothDevice) {
serviceManager.startService(HRSService::class.java, device)
}
fun start(device: BluetoothDevice, scope: CoroutineScope) {
val createdLogger = toolboxLoggerFactory.create("HRS", device.address).also {
fun start(device: DiscoveredBluetoothDevice, scope: CoroutineScope) {
val createdLogger = toolboxLoggerFactory.create("HRS", device.address()).also {
logger = it
}
val manager = HRSManager(context, scope, createdLogger)
@@ -57,9 +58,9 @@ class HRSRepository @Inject constructor(
logger?.openLogger()
}
private suspend fun HRSManager.start(device: BluetoothDevice) {
private suspend fun HRSManager.start(device: DiscoveredBluetoothDevice) {
try {
connect(device)
connect(device.device)
.useAutoConnect(false)
.retry(3, 100)
.suspend()

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.service.DEVICE_DATA
import no.nordicsemi.android.service.NotificationService
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject
@AndroidEntryPoint
@@ -19,7 +20,7 @@ internal class HRSService : NotificationService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val device = intent!!.getParcelableExtra<BluetoothDevice>(DEVICE_DATA)!!
val device = intent!!.getParcelableExtra<DiscoveredBluetoothDevice>(DEVICE_DATA)!!
repository.start(device, lifecycleScope)

View File

@@ -9,8 +9,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.hrs.R
import no.nordicsemi.android.hrs.data.HRSData
import no.nordicsemi.android.hrs.viewmodel.HRSViewModel
import no.nordicsemi.android.service.*
import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.LoggerIconAppBar
import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
@@ -26,14 +28,13 @@ fun HRSScreen() {
Column {
val navigateUp = { viewModel.onEvent(NavigateUpEvent) }
LoggerIconAppBar(stringResource(id = R.string.hrs_title), navigateUp) {
viewModel.onEvent(OpenLoggerEvent)
}
AppBar(state, navigateUp, viewModel)
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) {
NoDeviceState -> NoDeviceView()
is WorkingState -> when (state.result) {
is IdleResult,
is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
@@ -45,3 +46,18 @@ fun HRSScreen() {
}
}
}
@Composable
private fun AppBar(state: HRSViewState, navigateUp: () -> Unit, viewModel: HRSViewModel) {
val toolbarName = (state as? WorkingState)?.let {
(it.result as? SuccessResult<HRSData>)?.deviceName()
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.hrs_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) {
viewModel.onEvent(OpenLoggerEvent)
}
}
}

View File

@@ -49,7 +49,7 @@ internal class HRSViewModel @Inject constructor(
private fun handleArgs(args: DestinationResult) {
when (args) {
is CancelDestinationResult -> navigationManager.navigateUp()
is SuccessDestinationResult -> repository.launch(args.getDevice().device)
is SuccessDestinationResult -> repository.launch(args.getDevice())
}.exhaustive
}

View File

@@ -56,7 +56,7 @@ internal class HTSManager internal constructor(
val dataHolder = ConnectionObserverAdapter<HTSData>()
init {
setConnectionObserver(dataHolder)
connectionObserver = dataHolder
data.onEach {
dataHolder.setValue(it)

View File

@@ -14,6 +14,7 @@ import no.nordicsemi.android.logger.ToolboxLoggerFactory
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.service.ConnectingResult
import no.nordicsemi.android.service.ServiceManager
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject
import javax.inject.Singleton
@@ -33,12 +34,12 @@ class HTSRepository @Inject constructor(
val isRunning = data.map { it.isRunning() }
val hasBeenDisconnected = data.map { it.hasBeenDisconnected() }
fun launch(device: BluetoothDevice) {
fun launch(device: DiscoveredBluetoothDevice) {
serviceManager.startService(HTSService::class.java, device)
}
fun start(device: BluetoothDevice, scope: CoroutineScope) {
val createdLogger = toolboxLoggerFactory.create("HTS", device.address).also {
fun start(device: DiscoveredBluetoothDevice, scope: CoroutineScope) {
val createdLogger = toolboxLoggerFactory.create("HTS", device.address()).also {
logger = it
}
val manager = HTSManager(context, scope, createdLogger)
@@ -57,9 +58,9 @@ class HTSRepository @Inject constructor(
logger?.openLogger()
}
private suspend fun HTSManager.start(device: BluetoothDevice) {
private suspend fun HTSManager.start(device: DiscoveredBluetoothDevice) {
try {
connect(device)
connect(device.device)
.useAutoConnect(false)
.retry(3, 100)
.suspend()

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.service.DEVICE_DATA
import no.nordicsemi.android.service.NotificationService
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject
@AndroidEntryPoint
@@ -19,7 +20,7 @@ internal class HTSService : NotificationService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val device = intent!!.getParcelableExtra<BluetoothDevice>(DEVICE_DATA)!!
val device = intent!!.getParcelableExtra<DiscoveredBluetoothDevice>(DEVICE_DATA)!!
repository.start(device, lifecycleScope)

View File

@@ -9,14 +9,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.hts.R
import no.nordicsemi.android.hts.data.HTSData
import no.nordicsemi.android.hts.viewmodel.HTSViewModel
import no.nordicsemi.android.service.*
import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.LoggerIconAppBar
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.ui.scanner.ui.Reason
@Composable
@@ -27,14 +28,13 @@ fun HTSScreen() {
Column {
val navigateUp = { viewModel.onEvent(NavigateUp) }
LoggerIconAppBar(stringResource(id = R.string.hts_title), navigateUp) {
viewModel.onEvent(OpenLoggerEvent)
}
AppBar(state, navigateUp, viewModel)
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state.htsManagerState) {
NoDeviceState -> NoDeviceView()
is WorkingState -> when (state.htsManagerState.result) {
is IdleResult,
is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
@@ -46,3 +46,19 @@ fun HTSScreen() {
}
}
}
@Composable
private fun AppBar(state: HTSViewState, navigateUp: () -> Unit, viewModel: HTSViewModel) {
val toolbarName = (state.htsManagerState as? WorkingState)?.let {
(it.result as? SuccessResult<HTSData>)?.deviceName()
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.hts_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) {
viewModel.onEvent(OpenLoggerEvent)
}
}
}

View File

@@ -48,7 +48,7 @@ internal class HTSViewModel @Inject constructor(
private fun handleArgs(args: DestinationResult) {
when (args) {
is CancelDestinationResult -> navigationManager.navigateUp()
is SuccessDestinationResult -> repository.launch(args.getDevice().device)
is SuccessDestinationResult -> repository.launch(args.getDevice())
}.exhaustive
}

View File

@@ -68,7 +68,7 @@ internal class PRXManager(
val dataHolder = ConnectionObserverAdapter<PRXData>()
init {
setConnectionObserver(dataHolder)
connectionObserver = dataHolder
data.onEach {
dataHolder.setValue(it)

View File

@@ -12,6 +12,7 @@ import no.nordicsemi.android.prx.data.PRXData
import no.nordicsemi.android.prx.data.PRXManager
import no.nordicsemi.android.prx.data.ProximityServerManager
import no.nordicsemi.android.service.*
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject
import javax.inject.Singleton
@@ -34,13 +35,13 @@ class PRXRepository @Inject internal constructor(
val isRunning = data.map { it.isRunning() }
val hasBeenDisconnectedWithoutLinkLoss = data.map { it.hasBeenDisconnectedWithoutLinkLoss() }
fun launch(device: BluetoothDevice) {
fun launch(device: DiscoveredBluetoothDevice) {
serviceManager.startService(PRXService::class.java, device)
proximityServerManager.open()
}
fun start(device: BluetoothDevice, scope: CoroutineScope) {
val createdLogger = toolboxLoggerFactory.create("PRX", device.address).also {
fun start(device: DiscoveredBluetoothDevice, scope: CoroutineScope) {
val createdLogger = toolboxLoggerFactory.create("PRX", device.address()).also {
logger = it
}
val manager = PRXManager(context, scope, createdLogger)
@@ -52,7 +53,7 @@ class PRXRepository @Inject internal constructor(
handleLocalAlarm(it)
}.launchIn(scope)
manager.connect(device)
manager.connect(device.device)
.useAutoConnect(true)
.retry(3, 100)
.enqueue()

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.service.DEVICE_DATA
import no.nordicsemi.android.service.NotificationService
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject
@AndroidEntryPoint
@@ -19,7 +20,7 @@ internal class PRXService : NotificationService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val device = intent!!.getParcelableExtra<BluetoothDevice>(DEVICE_DATA)!!
val device = intent!!.getParcelableExtra<DiscoveredBluetoothDevice>(DEVICE_DATA)!!
repository.start(device, lifecycleScope)

View File

@@ -10,14 +10,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.prx.R
import no.nordicsemi.android.prx.data.PRXData
import no.nordicsemi.android.prx.viewmodel.PRXViewModel
import no.nordicsemi.android.service.*
import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.LoggerIconAppBar
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.ui.scanner.ui.Reason
@Composable
@@ -28,14 +29,13 @@ fun PRXScreen() {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
val navigateUp = { viewModel.onEvent(NavigateUpEvent) }
LoggerIconAppBar(stringResource(id = R.string.prx_title), navigateUp) {
viewModel.onEvent(OpenLoggerEvent)
}
AppBar(state, navigateUp, viewModel)
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) {
NoDeviceState -> NoDeviceView()
is WorkingState -> when (state.result) {
is IdleResult,
is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
is LinkLossResult -> DeviceOutOfRangeView { viewModel.onEvent(DisconnectEvent) }
@@ -47,3 +47,18 @@ fun PRXScreen() {
}
}
}
@Composable
private fun AppBar(state: PRXViewState, navigateUp: () -> Unit, viewModel: PRXViewModel) {
val toolbarName = (state as? WorkingState)?.let {
(it.result as? SuccessResult<PRXData>)?.deviceName()
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.prx_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) {
viewModel.onEvent(OpenLoggerEvent)
}
}
}

View File

@@ -48,7 +48,7 @@ internal class PRXViewModel @Inject constructor(
private fun handleArgs(args: DestinationResult) {
when (args) {
is CancelDestinationResult -> navigationManager.navigateUp()
is SuccessDestinationResult -> repository.launch(args.getDevice().device)
is SuccessDestinationResult -> repository.launch(args.getDevice())
}.exhaustive
}

View File

@@ -56,7 +56,7 @@ internal class RSCSManager internal constructor(
val dataHolder = ConnectionObserverAdapter<RSCSData>()
init {
setConnectionObserver(dataHolder)
connectionObserver = dataHolder
data.onEach {
dataHolder.setValue(it)

View File

@@ -14,6 +14,7 @@ import no.nordicsemi.android.rscs.data.RSCSManager
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.service.ConnectingResult
import no.nordicsemi.android.service.ServiceManager
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject
import javax.inject.Singleton
@@ -33,12 +34,12 @@ class RSCSRepository @Inject constructor(
val isRunning = data.map { it.isRunning() }
val hasBeenDisconnected = data.map { it.hasBeenDisconnected() }
fun launch(device: BluetoothDevice) {
fun launch(device: DiscoveredBluetoothDevice) {
serviceManager.startService(RSCSService::class.java, device)
}
fun start(device: BluetoothDevice, scope: CoroutineScope) {
val createdLogger = toolboxLoggerFactory.create("RSCS", device.address).also {
fun start(device: DiscoveredBluetoothDevice, scope: CoroutineScope) {
val createdLogger = toolboxLoggerFactory.create("RSCS", device.address()).also {
logger = it
}
val manager = RSCSManager(context, scope, createdLogger)
@@ -57,9 +58,9 @@ class RSCSRepository @Inject constructor(
logger?.openLogger()
}
private suspend fun RSCSManager.start(device: BluetoothDevice) {
private suspend fun RSCSManager.start(device: DiscoveredBluetoothDevice) {
try {
connect(device)
connect(device.device)
.useAutoConnect(false)
.retry(3, 100)
.suspend()

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.service.DEVICE_DATA
import no.nordicsemi.android.service.NotificationService
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject
@AndroidEntryPoint
@@ -19,7 +20,7 @@ internal class RSCSService : NotificationService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val device = intent!!.getParcelableExtra<BluetoothDevice>(DEVICE_DATA)!!
val device = intent!!.getParcelableExtra<DiscoveredBluetoothDevice>(DEVICE_DATA)!!
repository.start(device, lifecycleScope)

View File

@@ -9,14 +9,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.rscs.R
import no.nordicsemi.android.rscs.data.RSCSData
import no.nordicsemi.android.rscs.viewmodel.RSCSViewModel
import no.nordicsemi.android.service.*
import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.LoggerIconAppBar
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.ui.scanner.ui.Reason
@Composable
@@ -27,14 +28,13 @@ fun RSCSScreen() {
Column {
val navigateUp = { viewModel.onEvent(NavigateUpEvent) }
LoggerIconAppBar(stringResource(id = R.string.rscs_title), navigateUp) {
viewModel.onEvent(OpenLoggerEvent)
}
AppBar(state, navigateUp, viewModel)
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) {
NoDeviceState -> NoDeviceView()
is WorkingState -> when (state.result) {
is IdleResult,
is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
@@ -46,3 +46,18 @@ fun RSCSScreen() {
}
}
}
@Composable
private fun AppBar(state: RSCSViewState, navigateUp: () -> Unit, viewModel: RSCSViewModel) {
val toolbarName = (state as? WorkingState)?.let {
(it.result as? SuccessResult<RSCSData>)?.deviceName()
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.rscs_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) {
viewModel.onEvent(OpenLoggerEvent)
}
}
}

View File

@@ -48,7 +48,7 @@ internal class RSCSViewModel @Inject constructor(
private fun handleArgs(args: DestinationResult) {
when (args) {
is CancelDestinationResult -> navigationManager.navigateUp()
is SuccessDestinationResult -> repository.launch(args.getDevice().device)
is SuccessDestinationResult -> repository.launch(args.getDevice())
}.exhaustive
}

View File

@@ -1,14 +1,19 @@
package no.nordicsemi.android.uart.data
internal data class UARTData(
val messages: List<UARTOutputRecord> = emptyList(),
val messages: List<UARTRecord> = emptyList(),
val batteryLevel: Int? = null,
) {
val displayMessages = messages.reversed().take(10)
val displayMessages = messages
}
internal data class UARTOutputRecord(
internal data class UARTRecord(
val text: String,
val type: UARTRecordType,
val timestamp: Long = System.currentTimeMillis()
)
enum class UARTRecordType {
INPUT, OUTPUT
}

View File

@@ -29,9 +29,9 @@ import android.content.Context
import android.text.TextUtils
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
import no.nordicsemi.android.ble.BleManager
import no.nordicsemi.android.ble.WriteRequest
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelResponse
@@ -49,7 +49,8 @@ private val UART_RX_CHARACTERISTIC_UUID = UUID.fromString("6E400002-B5A3-F393-E0
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")
private val BATTERY_LEVEL_CHARACTERISTIC_UUID =
UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
internal class UARTManager(
context: Context,
@@ -68,7 +69,7 @@ internal class UARTManager(
val dataHolder = ConnectionObserverAdapter<UARTData>()
init {
setConnectionObserver(dataHolder)
connectionObserver = dataHolder
data.onEach {
dataHolder.setValue(it)
@@ -87,16 +88,23 @@ internal class UARTManager(
@SuppressLint("WrongConstant")
override fun initialize() {
setNotificationCallback(txCharacteristic).asFlow().onEach {
setNotificationCallback(txCharacteristic).asFlow()
.flowOn(Dispatchers.IO)
.map {
val text: String = it.getStringValue(0) ?: String.EMPTY
log(10, "\"$text\" received")
data.value = data.value.copy(messages = data.value.messages + UARTOutputRecord(text))
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<BatteryLevelResponse>().onEach {
setNotificationCallback(batteryLevelCharacteristic).asValidResponseFlow<BatteryLevelResponse>()
.onEach {
data.value = data.value.copy(batteryLevel = it.batteryLevel)
}.launchIn(scope)
enableNotifications(batteryLevelCharacteristic).enqueue()
@@ -114,7 +122,8 @@ internal class UARTManager(
rxCharacteristic?.let {
val rxProperties: Int = it.properties
writeRequest = rxProperties and BluetoothGattCharacteristic.PROPERTY_WRITE > 0
writeCommand = rxProperties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE > 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).
@@ -141,22 +150,27 @@ internal class UARTManager(
@SuppressLint("WrongConstant")
fun send(text: String) {
if (rxCharacteristic == null) return
if (!TextUtils.isEmpty(text)) {
scope.launchWithCatch {
val writeType = if (useLongWrite) {
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
} else {
BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
}
val request: WriteRequest = writeCharacteristic(rxCharacteristic, text.toByteArray(), writeType)
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())

View File

@@ -10,9 +10,11 @@ import no.nordicsemi.android.ble.ktx.suspend
import no.nordicsemi.android.logger.ToolboxLogger
import no.nordicsemi.android.logger.ToolboxLoggerFactory
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.service.ConnectingResult
import no.nordicsemi.android.service.IdleResult
import no.nordicsemi.android.service.ServiceManager
import no.nordicsemi.android.uart.data.*
import no.nordicsemi.android.utils.EMPTY
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject
import javax.inject.Singleton
@@ -22,12 +24,12 @@ class UARTRepository @Inject internal constructor(
private val context: Context,
private val serviceManager: ServiceManager,
private val configurationDataSource: ConfigurationDataSource,
private val toolboxLoggerFactory: ToolboxLoggerFactory
private val toolboxLoggerFactory: ToolboxLoggerFactory,
) {
private var manager: UARTManager? = null
private var logger: ToolboxLogger? = null
private val _data = MutableStateFlow<BleManagerResult<UARTData>>(ConnectingResult())
private val _data = MutableStateFlow<BleManagerResult<UARTData>>(IdleResult())
internal val data = _data.asStateFlow()
val isRunning = data.map { it.isRunning() }
@@ -35,12 +37,12 @@ class UARTRepository @Inject internal constructor(
val lastConfigurationName = configurationDataSource.lastConfigurationName
fun launch(device: BluetoothDevice) {
fun launch(device: DiscoveredBluetoothDevice) {
serviceManager.startService(UARTService::class.java, device)
}
fun start(device: BluetoothDevice, scope: CoroutineScope) {
val createdLogger = toolboxLoggerFactory.create("UART", device.address).also {
fun start(device: DiscoveredBluetoothDevice, scope: CoroutineScope) {
val createdLogger = toolboxLoggerFactory.create("UART", device.address()).also {
logger = it
}
val manager = UARTManager(context, scope, createdLogger)
@@ -60,9 +62,8 @@ class UARTRepository @Inject internal constructor(
}
fun runMacro(macro: UARTMacro) {
macro.command?.parseWithNewLineChar(macro.newLineChar)?.let {
manager?.send(it)
}
val command = macro.command?.parseWithNewLineChar(macro.newLineChar)
manager?.send(command ?: String.EMPTY)
}
fun clearItems() {
@@ -77,9 +78,9 @@ class UARTRepository @Inject internal constructor(
configurationDataSource.saveConfigurationName(name)
}
private suspend fun UARTManager.start(device: BluetoothDevice) {
private suspend fun UARTManager.start(device: DiscoveredBluetoothDevice) {
try {
connect(device)
connect(device.device)
.useAutoConnect(false)
.retry(3, 100)
.suspend()

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.service.DEVICE_DATA
import no.nordicsemi.android.service.NotificationService
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject
@AndroidEntryPoint
@@ -19,7 +20,7 @@ internal class UARTService : NotificationService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val device = intent!!.getParcelableExtra<BluetoothDevice>(DEVICE_DATA)!!
val device = intent!!.getParcelableExtra<DiscoveredBluetoothDevice>(DEVICE_DATA)!!
repository.start(device, lifecycleScope)

View File

@@ -0,0 +1,101 @@
package no.nordicsemi.android.uart.view
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.material.you.RadioButtonGroup
import no.nordicsemi.android.material.you.RadioButtonItem
import no.nordicsemi.android.material.you.RadioGroupViewEntity
import no.nordicsemi.android.material.you.ScreenSection
import no.nordicsemi.android.theme.view.SectionTitle
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.MacroEol
import no.nordicsemi.android.utils.EMPTY
@Composable
internal fun InputSection(onEvent: (UARTViewEvent) -> Unit) {
val text = rememberSaveable { mutableStateOf(String.EMPTY) }
val hint = stringResource(id = R.string.uart_input_hint)
val checkedItem = rememberSaveable { mutableStateOf(MacroEol.values()[0]) }
Row(verticalAlignment = Alignment.CenterVertically) {
Box(modifier = Modifier.weight(1f)) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.height(60.dp)
.verticalScroll(rememberScrollState()),
value = text.value,
label = { Text(hint) },
onValueChange = { newValue: String ->
text.value = newValue
}
)
}
Spacer(modifier = Modifier.size(16.dp))
Button(
onClick = {
onEvent(OnRunInput(text.value, checkedItem.value))
text.value = String.EMPTY
},
modifier = Modifier.padding(top = 6.dp)
) {
Text(text = stringResource(id = R.string.uart_send))
}
}
}
@Composable
internal fun EditInputSection(onEvent: (UARTViewEvent) -> Unit) {
val checkedItem = rememberSaveable { mutableStateOf(MacroEol.values()[0]) }
val items = MacroEol.values().map {
RadioButtonItem(it.toDisplayString(), it == checkedItem.value)
}
val viewEntity = RadioGroupViewEntity(items)
ScreenSection {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
SectionTitle(
resId = R.drawable.ic_input,
title = stringResource(R.string.uart_input),
menu = {
IconButton(onClick = { onEvent(MacroInputSwitchClick) }) {
Icon(
painterResource(id = R.drawable.ic_macro),
contentDescription = stringResource(id = R.string.uart_input_macro),
)
}
}
)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResource(id = R.string.uart_macro_dialog_eol),
style = MaterialTheme.typography.labelLarge
)
RadioButtonGroup(viewEntity) {
val i = items.indexOf(it)
checkedItem.value = MacroEol.values()[i]
}
}
Spacer(modifier = Modifier.size(16.dp))
}
}
}

View File

@@ -0,0 +1,131 @@
package no.nordicsemi.android.uart.view
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.material.you.ScreenSection
import no.nordicsemi.android.theme.view.SectionTitle
import no.nordicsemi.android.uart.R
@Composable
internal fun MacroSection(viewState: UARTViewState, onEvent: (UARTViewEvent) -> Unit) {
val showAddDialog = rememberSaveable { mutableStateOf(false) }
val showDeleteDialog = rememberSaveable { mutableStateOf(false) }
if (showAddDialog.value) {
UARTAddConfigurationDialog(onEvent) { showAddDialog.value = false }
}
if (showDeleteDialog.value) {
DeleteConfigurationDialog(onEvent) { showDeleteDialog.value = false }
}
if (viewState.showEditDialog) {
UARTAddMacroDialog(viewState.selectedMacro) { onEvent(it) }
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
ScreenSection {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
SectionTitle(
resId = R.drawable.ic_macro,
title = stringResource(R.string.uart_macros),
menu = {
viewState.selectedConfiguration?.let {
if (!viewState.isConfigurationEdited) {
IconButton(onClick = { onEvent(OnEditConfiguration) }) {
Icon(
Icons.Default.Edit,
stringResource(id = R.string.uart_configuration_edit)
)
}
} else {
IconButton(onClick = { onEvent(OnEditConfiguration) }) {
Icon(
painterResource(id = R.drawable.ic_pencil_off),
stringResource(id = R.string.uart_configuration_edit)
)
}
}
IconButton(onClick = { showDeleteDialog.value = true }) {
Icon(
Icons.Default.Delete,
stringResource(id = R.string.uart_configuration_delete)
)
}
}
}
)
Spacer(modifier = Modifier.height(16.dp))
Row {
Box(modifier = Modifier.weight(1f)) {
UARTConfigurationPicker(viewState, onEvent)
}
Spacer(modifier = Modifier.size(16.dp))
Button(onClick = { showAddDialog.value = true }) {
Text(stringResource(id = R.string.uart_configuration_add))
}
}
viewState.selectedConfiguration?.let {
Spacer(modifier = Modifier.height(16.dp))
UARTMacroView(it, viewState.isConfigurationEdited, onEvent)
}
}
}
}
}
@Composable
private fun DeleteConfigurationDialog(onEvent: (UARTViewEvent) -> Unit, onDismiss: () -> Unit) {
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
text = stringResource(id = R.string.uart_delete_dialog_title),
style = MaterialTheme.typography.headlineSmall
)
},
text = {
Text(text = stringResource(id = R.string.uart_delete_dialog_info))
},
confirmButton = {
TextButton(onClick = {
onDismiss()
onEvent(OnDeleteConfiguration)
}) {
Text(text = stringResource(id = R.string.uart_delete_dialog_confirm))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(text = stringResource(id = R.string.uart_delete_dialog_cancel))
}
}
)
}

View File

@@ -0,0 +1,162 @@
package no.nordicsemi.android.uart.view
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import no.nordicsemi.android.theme.view.SectionTitle
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.UARTRecord
import no.nordicsemi.android.uart.data.UARTRecordType
import java.text.SimpleDateFormat
import java.util.*
@Composable
internal fun OutputSection(records: List<UARTRecord>, onEvent: (UARTViewEvent) -> Unit) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
SectionTitle(
resId = R.drawable.ic_output,
title = stringResource(R.string.uart_output),
modifier = Modifier,
menu = { Menu(onEvent) }
)
}
Spacer(modifier = Modifier.size(16.dp))
val scrollState = rememberLazyListState()
val scrollDown = remember {
derivedStateOf { scrollState.isScrolledToTheEnd() }
}
LazyColumn(
modifier = Modifier.fillMaxWidth(),
state = scrollState
) {
if (records.isEmpty()) {
item { Text(text = stringResource(id = R.string.uart_output_placeholder)) }
} else {
records.forEach {
item {
when (it.type) {
UARTRecordType.INPUT -> MessageItemInput(record = it)
UARTRecordType.OUTPUT -> MessageItemOutput(record = it)
}
Spacer(modifier = Modifier.height(16.dp))
}
}
}
}
LaunchedEffect(records, scrollDown.value) {
if (!scrollDown.value || records.isEmpty()) {
return@LaunchedEffect
}
launch {
scrollState.scrollToItem(records.lastIndex)
}
}
}
}
fun LazyListState.isScrolledToTheEnd() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1
@Composable
private fun MessageItemInput(record: UARTRecord) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.End
) {
Text(
text = record.timeToString(),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(4.dp))
Column(
modifier = Modifier
.clip(RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp, bottomStart = 10.dp))
.background(MaterialTheme.colorScheme.secondary)
.padding(8.dp),
horizontalAlignment = Alignment.End
) {
Text(
text = record.text,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSecondary
)
}
}
}
@Composable
private fun MessageItemOutput(record: UARTRecord) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start
) {
Text(
text = record.timeToString(),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(4.dp))
Column(
modifier = Modifier
.clip(RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp, bottomEnd = 10.dp))
.background(MaterialTheme.colorScheme.primary)
.padding(8.dp)
) {
Text(
text = record.text,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimary
)
}
}
}
@Composable
private fun Menu(onEvent: (UARTViewEvent) -> Unit) {
Row {
IconButton(onClick = { onEvent(ClearOutputItems) }) {
Icon(
Icons.Default.Delete,
contentDescription = stringResource(id = R.string.uart_clear_items),
)
}
}
}
private val datFormatter = SimpleDateFormat("dd MMMM yyyy, HH:mm:ss", Locale.ENGLISH)
private fun UARTRecord.timeToString(): String {
return datFormatter.format(timestamp)
}

View File

@@ -1,21 +1,12 @@
package no.nordicsemi.android.uart.view
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import no.nordicsemi.android.material.you.TextField
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.utils.EMPTY
@@ -24,33 +15,11 @@ internal fun UARTAddConfigurationDialog(onEvent: (UARTViewEvent) -> Unit, onDism
val name = rememberSaveable { mutableStateOf(String.EMPTY) }
val isError = rememberSaveable { mutableStateOf(false) }
Dialog(onDismissRequest = { onDismiss() }) {
Surface(
color = MaterialTheme.colorScheme.background,
shape = RoundedCornerShape(10.dp),
shadowElevation = 2.dp,
) {
Column(verticalArrangement = Arrangement.SpaceBetween) {
Text(
text = stringResource(id = R.string.uart_configuration_dialog_title),
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
NameInput(name, isError)
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { onDismiss() }) {
Text(stringResource(id = R.string.uart_macro_dialog_dismiss))
}
Spacer(modifier = Modifier.size(16.dp))
AlertDialog(
onDismissRequest = { onDismiss() },
title = { Text(stringResource(id = R.string.uart_configuration_dialog_title)) },
text = { NameInput(name, isError) },
confirmButton = {
TextButton(onClick = {
if (isNameValid(name.value)) {
onDismiss()
@@ -61,10 +30,13 @@ internal fun UARTAddConfigurationDialog(onEvent: (UARTViewEvent) -> Unit, onDism
}) {
Text(stringResource(id = R.string.uart_macro_dialog_confirm))
}
},
dismissButton = {
TextButton(onClick = { onDismiss() }) {
Text(stringResource(id = R.string.uart_macro_dialog_dismiss))
}
}
}
}
)
}
@Composable
@@ -72,14 +44,17 @@ private fun NameInput(
name: MutableState<String>,
isError: MutableState<Boolean>
) {
Column(modifier = Modifier.padding(16.dp)) {
TextField(
text = name.value,
hint = stringResource(id = R.string.uart_configuration_hint)
) {
Column {
OutlinedTextField(
value = name.value,
label = { Text(stringResource(id = R.string.uart_configuration_hint)) },
singleLine = true,
onValueChange = {
isError.value = false
name.value = it
}
)
val errorText = if (isError.value) {
stringResource(id = R.string.uart_name_empty)

View File

@@ -1,18 +1,12 @@
package no.nordicsemi.android.uart.view
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.GridCells
import androidx.compose.foundation.lazy.GridItemSpan
import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
@@ -25,11 +19,9 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import no.nordicsemi.android.material.you.RadioButtonGroup
import no.nordicsemi.android.material.you.RadioButtonItem
import no.nordicsemi.android.material.you.RadioGroupViewEntity
import no.nordicsemi.android.material.you.TextField
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.MacroEol
import no.nordicsemi.android.uart.data.MacroIcon
@@ -38,34 +30,36 @@ import no.nordicsemi.android.utils.EMPTY
private const val GRID_SIZE = 5
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun UARTAddMacroDialog(macro: UARTMacro?, onEvent: (UARTViewEvent) -> Unit) {
val newLineChar = rememberSaveable { mutableStateOf(macro?.newLineChar ?: MacroEol.LF) }
val command = rememberSaveable { mutableStateOf(macro?.command ?: String.EMPTY) }
val isError = rememberSaveable { mutableStateOf(false) }
val selectedIcon = rememberSaveable { mutableStateOf(macro?.icon ?: MacroIcon.values()[0]) }
Dialog(onDismissRequest = { onEvent(OnEditFinish) }) {
Surface(
color = MaterialTheme.colorScheme.background,
shape = RoundedCornerShape(10.dp),
shadowElevation = 0.dp,
) {
Column {
AlertDialog(
onDismissRequest = { onEvent(OnEditFinish) },
dismissButton = {
TextButton(onClick = { onEvent(OnDeleteMacro) }) {
Text(stringResource(id = R.string.uart_macro_dialog_delete))
}
},
confirmButton = {
TextButton(onClick = {
onEvent(OnCreateMacro(UARTMacro(selectedIcon.value, command.value, newLineChar.value)))
}) {
Text(stringResource(id = R.string.uart_macro_dialog_confirm))
}
},
title = {
Text(
text = stringResource(id = R.string.uart_macro_dialog_title),
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
style = MaterialTheme.typography.headlineSmall
)
},
text = {
LazyVerticalGrid(
cells = GridCells.Fixed(GRID_SIZE),
modifier = Modifier
.padding(horizontal = 16.dp)
.wrapContentHeight()
columns = GridCells.Fixed(GRID_SIZE),
modifier = Modifier.wrapContentHeight()
) {
item(span = { GridItemSpan(GRID_SIZE) }) {
Column {
@@ -76,7 +70,7 @@ internal fun UARTAddMacroDialog(macro: UARTMacro?, onEvent: (UARTViewEvent) -> U
}
item(span = { GridItemSpan(GRID_SIZE) }) {
CommandInput(command, isError)
CommandInput(command)
}
items(20) { item ->
@@ -98,70 +92,23 @@ internal fun UARTAddMacroDialog(macro: UARTMacro?, onEvent: (UARTViewEvent) -> U
.background(background)
)
}
item(span = { GridItemSpan(GRID_SIZE) }) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { onEvent(OnEditFinish) }) {
Text(stringResource(id = R.string.uart_macro_dialog_dismiss))
}
Spacer(modifier = Modifier.size(16.dp))
TextButton(onClick = { onEvent(OnDeleteMacro) }) {
Text(stringResource(id = R.string.uart_macro_dialog_delete))
}
Spacer(modifier = Modifier.size(16.dp))
TextButton(onClick = {
if (isCommandValid(command.value)) {
onEvent(
OnCreateMacro(
UARTMacro(
selectedIcon.value,
command.value,
newLineChar.value
)
)
)
} else {
isError.value = true
}
}) {
Text(stringResource(id = R.string.uart_macro_dialog_confirm))
}
}
}
}
}
}
}
}
@Composable
private fun CommandInput(
command: MutableState<String>,
isError: MutableState<Boolean>
) {
private fun CommandInput(command: MutableState<String>) {
Column {
TextField(
text = command.value,
hint = stringResource(id = R.string.uart_macro_dialog_command)
) {
isError.value = false
OutlinedTextField(
modifier = Modifier
.fillMaxWidth(),
value = command.value,
label = { Text(stringResource(id = R.string.uart_macro_dialog_command)) },
onValueChange = {
command.value = it
}
if (isError.value) {
Text(
text = stringResource(id = R.string.uart_macro_error),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.error
)
}
Spacer(modifier = Modifier.size(16.dp))
}
@@ -186,7 +133,3 @@ private fun NewLineCharSection(checkedItem: MacroEol, onItemClick: (MacroEol) ->
}
}
}
private fun isCommandValid(command: String): Boolean {
return command.isNotBlank()
}

View File

@@ -1,265 +1,40 @@
package no.nordicsemi.android.uart.view
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.material.you.*
import no.nordicsemi.android.theme.view.SectionTitle
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.MacroEol
import no.nordicsemi.android.material.you.Card
import no.nordicsemi.android.uart.data.UARTData
import no.nordicsemi.android.uart.data.UARTOutputRecord
import no.nordicsemi.android.utils.EMPTY
import java.text.SimpleDateFormat
import java.util.*
@Composable
internal fun UARTContentView(
state: UARTData,
viewState: UARTViewState,
onEvent: (UARTViewEvent) -> Unit
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(16.dp)
) {
InputSection(onEvent = onEvent)
Spacer(modifier = Modifier.size(16.dp))
MacroSection(viewState, onEvent)
Spacer(modifier = Modifier.size(16.dp))
OutputSection(state.displayMessages, onEvent)
Spacer(modifier = Modifier.size(16.dp))
Button(
onClick = { onEvent(DisconnectEvent) }
) {
Text(text = stringResource(id = R.string.disconnect))
}
}
}
@Composable
private fun InputSection(onEvent: (UARTViewEvent) -> Unit) {
val text = rememberSaveable { mutableStateOf(String.EMPTY) }
val hint = stringResource(id = R.string.uart_input_hint)
val checkedItem = rememberSaveable { mutableStateOf(MacroEol.values()[0]) }
val items = MacroEol.values().map {
RadioButtonItem(it.toDisplayString(), it == checkedItem.value)
}
val viewEntity = RadioGroupViewEntity(items)
ScreenSection {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
SectionTitle(resId = R.drawable.ic_input, title = stringResource(R.string.uart_input))
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResource(id = R.string.uart_macro_dialog_eol),
style = MaterialTheme.typography.labelLarge
)
RadioButtonGroup(viewEntity) {
val i = items.indexOf(it)
checkedItem.value = MacroEol.values()[i]
}
}
Spacer(modifier = Modifier.size(16.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Box(modifier = Modifier.weight(1f)) {
TextField(text = text.value, hint = hint) {
text.value = it
}
}
Spacer(modifier = Modifier.size(16.dp))
Button(
onClick = { onEvent(OnRunInput(text.value, checkedItem.value)) },
modifier = Modifier.padding(top = 6.dp)
) {
Text(text = stringResource(id = R.string.uart_send))
}
}
}
}
}
@Composable
private fun MacroSection(viewState: UARTViewState, onEvent: (UARTViewEvent) -> Unit) {
val showAddDialog = rememberSaveable { mutableStateOf(false) }
val showDeleteDialog = rememberSaveable { mutableStateOf(false) }
if (showAddDialog.value) {
UARTAddConfigurationDialog(onEvent) { showAddDialog.value = false }
}
if (showDeleteDialog.value) {
DeleteConfigurationDialog(onEvent) { showDeleteDialog.value = false }
}
ScreenSection {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
SectionTitle(resId = R.drawable.ic_input, title = stringResource(R.string.uart_macros))
Spacer(modifier = Modifier.height(16.dp))
Row {
Box(modifier = Modifier.weight(1f)) {
UARTConfigurationPicker(viewState, onEvent)
}
IconButton(onClick = { showAddDialog.value = true }) {
Icon(Icons.Default.Add, stringResource(id = R.string.uart_configuration_add))
}
viewState.selectedConfiguration?.let {
if (!viewState.isConfigurationEdited) {
IconButton(onClick = { onEvent(OnEditConfiguration) }) {
Icon(
Icons.Default.Edit,
stringResource(id = R.string.uart_configuration_edit)
)
}
} else {
IconButton(onClick = { onEvent(OnEditConfiguration) }) {
Icon(
painterResource(id = R.drawable.ic_pencil_off),
stringResource(id = R.string.uart_configuration_edit)
)
}
}
IconButton(onClick = { showDeleteDialog.value = true }) {
Icon(
Icons.Default.Delete,
stringResource(id = R.string.uart_configuration_delete)
)
}
}
}
viewState.selectedConfiguration?.let {
Spacer(modifier = Modifier.height(16.dp))
UARTMacroView(it, viewState.isConfigurationEdited, onEvent)
}
}
}
}
@Composable
private fun DeleteConfigurationDialog(onEvent: (UARTViewEvent) -> Unit, onDismiss: () -> Unit) {
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
text = stringResource(id = R.string.uart_delete_dialog_title),
style = MaterialTheme.typography.headlineSmall
)
},
text = {
Text(text = stringResource(id = R.string.uart_delete_dialog_info))
},
confirmButton = {
Button(onClick = {
onDismiss()
onEvent(OnDeleteConfiguration)
}) {
Text(text = stringResource(id = R.string.uart_delete_dialog_confirm))
}
},
dismissButton = {
Button(onClick = onDismiss) {
Text(text = stringResource(id = R.string.uart_delete_dialog_cancel))
}
}
)
}
@Composable
private fun OutputSection(records: List<UARTOutputRecord>, onEvent: (UARTViewEvent) -> Unit) {
ScreenSection {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
SectionTitle(
resId = R.drawable.ic_output,
title = stringResource(R.string.uart_output),
modifier = Modifier
)
.padding(16.dp)
.fillMaxSize()
) {
IconButton(onClick = { onEvent(ClearOutputItems) }) {
Icon(
Icons.Default.Delete,
contentDescription = "Clear items.",
)
Card(
modifier = Modifier
.weight(1f)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, top = 16.dp, end = 16.dp)
) {
OutputSection(state.displayMessages, onEvent)
}
}
Spacer(modifier = Modifier.size(16.dp))
Column(modifier = Modifier.fillMaxWidth()) {
if (records.isEmpty()) {
Text(text = stringResource(id = R.string.uart_output_placeholder))
} else {
records.forEach {
MessageItem(record = it)
Spacer(modifier = Modifier.height(16.dp))
}
}
}
}
InputSection(onEvent = onEvent)
}
}
@Composable
private fun MessageItem(record: UARTOutputRecord) {
Column {
Text(
text = record.timeToString(),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.outline
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = record.text,
style = MaterialTheme.typography.bodyMedium
)
}
}
private val datFormatter = SimpleDateFormat("dd MMMM yyyy, HH:mm:ss", Locale.ENGLISH)
private fun UARTOutputRecord.timeToString(): String {
return datFormatter.format(timestamp)
}

View File

@@ -13,13 +13,13 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.UARTConfiguration
import no.nordicsemi.android.uart.data.UARTMacro
private val divider = 4.dp
private val buttonSize = 80.dp
@Composable
internal fun UARTMacroView(
@@ -27,34 +27,42 @@ internal fun UARTMacroView(
isEdited: Boolean,
onEvent: (UARTViewEvent) -> Unit
) {
BoxWithConstraints {
val buttonSize = if (maxWidth < 260.dp) {
48.dp //Minimum touch area
} else {
80.dp
}
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
Row {
Item(configuration, isEdited, 0, onEvent)
Item(configuration, isEdited, 0, buttonSize, onEvent)
Spacer(modifier = Modifier.size(divider))
Item(configuration, isEdited, 1, onEvent)
Item(configuration, isEdited, 1, buttonSize, onEvent)
Spacer(modifier = Modifier.size(divider))
Item(configuration, isEdited, 2, onEvent)
Item(configuration, isEdited, 2, buttonSize, onEvent)
}
Spacer(modifier = Modifier.size(divider))
Row {
Item(configuration, isEdited, 3, onEvent)
Item(configuration, isEdited, 3, buttonSize, onEvent)
Spacer(modifier = Modifier.size(divider))
Item(configuration, isEdited, 4, onEvent)
Item(configuration, isEdited, 4, buttonSize, onEvent)
Spacer(modifier = Modifier.size(divider))
Item(configuration, isEdited, 5, onEvent)
Item(configuration, isEdited, 5, buttonSize, onEvent)
}
Spacer(modifier = Modifier.size(divider))
Row {
Item(configuration, isEdited, 6, onEvent)
Item(configuration, isEdited, 6, buttonSize, onEvent)
Spacer(modifier = Modifier.size(divider))
Item(configuration, isEdited, 7, onEvent)
Item(configuration, isEdited, 7, buttonSize, onEvent)
Spacer(modifier = Modifier.size(divider))
Item(configuration, isEdited, 8, onEvent)
Item(configuration, isEdited, 8, buttonSize, onEvent)
}
}
}
}
@@ -64,14 +72,15 @@ private fun Item(
configuration: UARTConfiguration,
isEdited: Boolean,
position: Int,
buttonSize: Dp,
onEvent: (UARTViewEvent) -> Unit
) {
val macro = configuration.macros.getOrNull(position)
if (macro == null) {
EmptyButton(isEdited, position, onEvent)
EmptyButton(isEdited, position, buttonSize, onEvent)
} else {
MacroButton(macro, position, isEdited, onEvent)
MacroButton(macro, position, isEdited, buttonSize, onEvent)
}
}
@@ -80,6 +89,7 @@ private fun MacroButton(
macro: UARTMacro,
position: Int,
isEdited: Boolean,
buttonSize: Dp,
onEvent: (UARTViewEvent) -> Unit
) {
Image(
@@ -104,6 +114,7 @@ private fun MacroButton(
private fun EmptyButton(
isEdited: Boolean,
position: Int,
buttonSize: Dp,
onEvent: (UARTViewEvent) -> Unit
) {
Box(

View File

@@ -8,15 +8,19 @@ 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.material.you.PagerView
import no.nordicsemi.android.material.you.PagerViewEntity
import no.nordicsemi.android.material.you.PagerViewItem
import no.nordicsemi.android.service.*
import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.LoggerIconAppBar
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.UARTData
import no.nordicsemi.android.uart.viewmodel.UARTViewModel
import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.ui.scanner.ui.Reason
@Composable
@@ -24,29 +28,59 @@ fun UARTScreen() {
val viewModel: UARTViewModel = hiltViewModel()
val state = viewModel.state.collectAsState().value
if (state.showEditDialog) {
UARTAddMacroDialog(state.selectedMacro) { viewModel.onEvent(it) }
}
Column {
val navigateUp = { viewModel.onEvent(NavigateUp) }
LoggerIconAppBar(stringResource(id = R.string.uart_title), navigateUp) {
viewModel.onEvent(OpenLogger)
}
AppBar(state = state, navigateUp = navigateUp) { viewModel.onEvent(it) }
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state.uartManagerState) {
NoDeviceState -> NoDeviceView()
is WorkingState -> when (state.uartManagerState.result) {
is ConnectingResult -> 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 UnknownErrorResult -> DeviceDisconnectedView(Reason.UNKNOWN, navigateUp)
is SuccessResult -> UARTContentView(state.uartManagerState.result.data, state) { viewModel.onEvent(it) }
is IdleResult,
is ConnectingResult -> Scroll { DeviceConnectingView { viewModel.onEvent(DisconnectEvent) } }
is DisconnectedResult -> Scroll { DeviceDisconnectedView(Reason.USER, navigateUp) }
is LinkLossResult -> Scroll { DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp) }
is MissingServiceResult -> Scroll { DeviceDisconnectedView(Reason.MISSING_SERVICE, navigateUp) }
is UnknownErrorResult -> Scroll { DeviceDisconnectedView(Reason.UNKNOWN, navigateUp) }
is SuccessResult -> SuccessScreen(state.uartManagerState.result.data, state, viewModel)
}
}.exhaustive
}
}
@Composable
private fun AppBar(state: UARTViewState, navigateUp: () -> Unit, onEvent: (UARTViewEvent) -> Unit) {
val toolbarName = (state.uartManagerState as? WorkingState)?.let {
(it.result as? SuccessResult<UARTData>)?.deviceName()
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.uart_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, navigateUp, { onEvent(DisconnectEvent) }) {
onEvent(OpenLogger)
}
}
}
@Composable
private fun SuccessScreen(data: UARTData, state: UARTViewState, viewModel: UARTViewModel) {
val viewEntity = PagerViewEntity(
listOf(
PagerViewItem(stringResource(id = R.string.uart_input)) {
UARTContentView(data) { viewModel.onEvent(it) }
},
PagerViewItem(stringResource(id = R.string.uart_macros)) {
MacroSection(state) { viewModel.onEvent(it) }
}
)
)
PagerView(viewEntity)
}
@Composable
fun Scroll(content: @Composable () -> Unit) {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
content()
}
}

View File

@@ -10,7 +10,8 @@ internal data class UARTViewState(
val selectedConfigurationName: String? = null,
val isConfigurationEdited: Boolean = false,
val configurations: List<UARTConfiguration> = emptyList(),
val uartManagerState: HTSManagerState = NoDeviceState
val uartManagerState: HTSManagerState = NoDeviceState,
val isInputVisible: Boolean = true
) {
val showEditDialog: Boolean = editedPosition != null
@@ -25,6 +26,8 @@ internal data class UARTViewState(
internal sealed class HTSManagerState
internal data class WorkingState(val result: BleManagerResult<UARTData>) : HTSManagerState()
internal data class WorkingState(
val result: BleManagerResult<UARTData>
) : HTSManagerState()
internal object NoDeviceState : HTSManagerState()

View File

@@ -23,3 +23,5 @@ internal object DisconnectEvent : UARTViewEvent()
internal object NavigateUp : UARTViewEvent()
internal object OpenLogger : UARTViewEvent()
internal object MacroInputSwitchClick : UARTViewEvent()

View File

@@ -7,6 +7,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import no.nordicsemi.android.navigation.*
import no.nordicsemi.android.service.IdleResult
import no.nordicsemi.android.uart.data.UARTConfiguration
import no.nordicsemi.android.uart.data.UARTMacro
import no.nordicsemi.android.uart.data.UARTPersistentDataSource
@@ -36,6 +37,9 @@ internal class UARTViewModel @Inject constructor(
}
repository.data.onEach {
if (it is IdleResult) {
return@onEach
}
_state.value = _state.value.copy(uartManagerState = WorkingState(it))
}.launchIn(viewModelScope)
@@ -63,7 +67,7 @@ internal class UARTViewModel @Inject constructor(
private fun handleArgs(args: DestinationResult) {
when (args) {
is CancelDestinationResult -> navigationManager.navigateUp()
is SuccessDestinationResult -> repository.launch(args.getDevice().device)
is SuccessDestinationResult -> repository.launch(args.getDevice())
}.exhaustive
}
@@ -83,9 +87,14 @@ internal class UARTViewModel @Inject constructor(
ClearOutputItems -> repository.clearItems()
OpenLogger -> repository.openLogger()
is OnRunInput -> repository.sendText(event.text, event.newLineChar)
MacroInputSwitchClick -> onMacroInputSwitch()
}.exhaustive
}
private fun onMacroInputSwitch() {
_state.value = _state.value.copy(isInputVisible = !state.value.isInputVisible)
}
private fun onEditConfiguration() {
val isEdited = _state.value.isConfigurationEdited
_state.value = _state.value.copy(isConfigurationEdited = !isEdited)

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M4,8h4L8,4L4,4v4zM10,20h4v-4h-4v4zM4,20h4v-4L4,16v4zM4,14h4v-4L4,10v4zM10,14h4v-4h-4v4zM16,4v4h4L20,4h-4zM10,8h4L14,4h-4v4zM16,14h4v-4h-4v4zM16,20h4v-4h-4v4z" />
</vector>

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3.8,12.18c-0.2,-0.86 -0.3,-1.76 -0.3,-2.68c0,-2.84 0.99,-5.45 2.63,-7.5L7.2,3.07C5.82,4.85 5,7.08 5,9.5c0,0.88 0.11,1.74 0.32,2.56l1.62,-1.62L8,11.5L4.5,15L1,11.5l1.06,-1.06L3.8,12.18zM13.85,11.62l-2.68,-5.37c-0.37,-0.74 -1.27,-1.04 -2.01,-0.67C8.41,5.96 8.11,6.86 8.48,7.6l4.81,9.6L10.05,18c-0.33,0.09 -0.59,0.33 -0.7,0.66L9,19.78l6.19,2.25c0.5,0.17 1.28,0.02 1.75,-0.22l5.51,-2.75c0.89,-0.45 1.32,-1.48 1,-2.42l-1.43,-4.27c-0.27,-0.82 -1.04,-1.37 -1.9,-1.37h-4.56c-0.31,0 -0.62,0.07 -0.89,0.21L13.85,11.62" />
</vector>

View File

@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3.8,12.18c-0.2,-0.86 -0.3,-1.76 -0.3,-2.68c0,-2.84 0.99,-5.45 2.63,-7.5L7.2,3.07C5.82,4.85 5,7.08 5,9.5c0,0.88 0.11,1.74 0.32,2.56l1.62,-1.62L8,11.5L4.5,15L1,11.5l1.06,-1.06L3.8,12.18zM13.85,11.62l-2.68,-5.37c-0.37,-0.74 -1.27,-1.04 -2.01,-0.67C8.41,5.96 8.11,6.86 8.48,7.6l4.81,9.6L10.05,18c-0.33,0.09 -0.59,0.33 -0.7,0.66L9,19.78l6.19,2.25c0.5,0.17 1.28,0.02 1.75,-0.22l5.51,-2.75c0.89,-0.45 1.32,-1.48 1,-2.42l-1.43,-4.27c-0.27,-0.82 -1.04,-1.37 -1.9,-1.37h-4.56c-0.31,0 -0.62,0.07 -0.89,0.21L13.85,11.62" />
<path
android:fillColor="@android:color/white"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
</vector>

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="uart_title">UART</string>
<string name="uart_no_macros_info">Please define a macro to send command to the device.</string>
<string name="uart_configuration_add">Add selected configuration.</string>
<string name="uart_configuration_add">Add</string>
<string name="uart_configuration_delete">Delete selected configuration.</string>
<string name="uart_configuration_edit">Edit selected configuration.</string>
@@ -52,4 +52,13 @@
<string name="uart_delete_dialog_info">Are you sure that you want to delete this configuration? Your data will be irretrievably lost.</string>
<string name="uart_delete_dialog_confirm">Confirm</string>
<string name="uart_delete_dialog_cancel">Cancel</string>
<string name="uart_input_macro">Click to switch between text input and macro input.</string>
<string name="uart_clear_items">Clear items.</string>
<string name="uart_scroll_down">Click to constantly scroll view to the latest available log.</string>
<string name="uart_input_log">--&gt; %s</string>
<string name="uart_output_log" tools:ignore="TypographyDashes">&lt;-- %s</string>
<string name="uart_settings">Settings</string>
<string name="uart_settings_button">Go to settings screen.</string>
</resources>

View File

@@ -9,8 +9,8 @@ dependencyResolutionManagement {
versionCatalogs {
libs {
library('nordic-ble-common', 'no.nordicsemi.android:ble-common:2.4.0')
library('nordic-ble-ktx', 'no.nordicsemi.android:ble-ktx:2.4.0')
library('nordic-ble-common', 'no.nordicsemi.android:ble-common:2.4.1')
library('nordic-ble-ktx', 'no.nordicsemi.android:ble-ktx:2.4.1')
library('nordic-scanner', 'no.nordicsemi.android.support.v18:scanner:1.6.0')
library('nordic-log', 'no.nordicsemi.android:log:2.3.0')
@@ -21,7 +21,7 @@ dependencyResolutionManagement {
library('nordic-theme', 'no.nordicsemi.android.common', 'theme').versionRef('commonlibraries')
library('localbroadcastmanager', 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0')
library('material', 'com.google.android.material:material:1.6.0-alpha02')
library('material', 'com.google.android.material:material:1.6.0-rc01')
version('lifecycle', '2.4.1')
library('lifecycle-activity', 'androidx.lifecycle', 'lifecycle-runtime-ktx').versionRef('lifecycle')
@@ -37,9 +37,9 @@ dependencyResolutionManagement {
library('datastore-protobuf', 'com.google.protobuf:protobuf-javalite:3.18.0')
bundle('datastore', ['datastore-core', 'datastore-prefs', 'datastore-protobuf'])
version('compose', '1.1.0')
version('compose', '1.2.0-alpha07')
library('compose-ui', 'androidx.compose.ui', 'ui').versionRef('compose')
library('compose-material', 'androidx.compose.material3:material3:1.0.0-alpha05')
library('compose-material', 'androidx.compose.material3:material3:1.0.0-alpha09')
library('compose-tooling-preview', 'androidx.compose.ui', 'ui-tooling-preview').versionRef('compose')
library('compose-navigation', 'androidx.navigation:navigation-compose:2.4.1')
bundle('compose', ['compose-ui', 'compose-material', 'compose-tooling-preview', 'compose-navigation'])