Communication screen for a specific peripheral device
This commit is contained in:
@@ -33,6 +33,15 @@
|
|||||||
<string name="clear">Clear</string>
|
<string name="clear">Clear</string>
|
||||||
<string name="scanner_title">Scanner</string>
|
<string name="scanner_title">Scanner</string>
|
||||||
<string name="scan_empty_title">CAN'T SEE YOUR DEVICE?</string>
|
<string name="scan_empty_title">CAN'T SEE YOUR DEVICE?</string>
|
||||||
<string name="no_device_guide_info">1. Make sure the device is connected to a power source and <b>powered on</b>.\n\n2. Make sure the appropriate firmware and SoftDevice are flashed.</string>
|
<string name="no_device_guide_info">1. Make sure the device is connected to a power source and <b>powered on</b>.\n\n2. Make sure the appropriate firmware and Soft Device are flashed.</string>
|
||||||
|
<string name="device_disconnected">Disconnected</string>
|
||||||
|
<string name="device_reason_user">Device disconnected successfully.</string>
|
||||||
|
<string name="device_reason_timeout">The device is not available.\nPlease check if it is turned on and try again.</string>
|
||||||
|
<string name="device_reason_link_loss">The device got out of range, or has been turned off.</string>
|
||||||
|
<string name="device_reason_missing_service">The device is not supported.</string>
|
||||||
|
|
||||||
|
<string name="action_cancel">Cancel</string>
|
||||||
|
<string name="action_retry">Retry</string>
|
||||||
|
<string name="device_connecting">Connecting…</string>
|
||||||
|
<string name="device_explanation">It should just take a moment…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
+1
-1
@@ -63,5 +63,5 @@ val viewModelModule = module {
|
|||||||
factoryOf(::SettingViewModel)
|
factoryOf(::SettingViewModel)
|
||||||
factoryOf(::BondedViewModel)
|
factoryOf(::BondedViewModel)
|
||||||
factoryOf(::LogViewModel)
|
factoryOf(::LogViewModel)
|
||||||
factory { ScannerViewModel(get(), get(), get(), get()) }
|
single { ScannerViewModel(get(), get(), get(), get()) }
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package com.digitoolsolutions.app.torquevaultkmp.domain.model
|
||||||
|
|
||||||
|
enum class ConnectionReason {
|
||||||
|
USER, TIMEOUT, LINK_LOSS, MISSING_SERVICE
|
||||||
|
}
|
||||||
+10
-7
@@ -24,8 +24,13 @@ class BleManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val peripherals = mutableMapOf<String, Peripheral>()
|
private val peripherals = mutableMapOf<String, Peripheral>()
|
||||||
|
private val advertisements = mutableMapOf<String, Advertisement>()
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
fun getPeripheral(identifier: String): Peripheral? = peripherals[identifier]
|
||||||
|
fun getAdvertisement(identifier: String): Advertisement? = advertisements[identifier]
|
||||||
|
fun getAdvertisementName(identifier: String): String = advertisements[identifier]?.name.toString()
|
||||||
|
|
||||||
fun scanDevices(): Flow<Advertisement> = Scanner {
|
fun scanDevices(): Flow<Advertisement> = Scanner {
|
||||||
filters {
|
filters {
|
||||||
match {
|
match {
|
||||||
@@ -37,26 +42,25 @@ class BleManager {
|
|||||||
level = Logging.Level.Events
|
level = Logging.Level.Events
|
||||||
format = Logging.Format.Multiline
|
format = Logging.Format.Multiline
|
||||||
}
|
}
|
||||||
}.advertisements
|
}.advertisements.map {
|
||||||
|
advertisements[it.identifier.toString()] = it
|
||||||
|
it
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun connect(advertisement: Advertisement): Peripheral {
|
suspend fun connect(advertisement: Advertisement): Peripheral {
|
||||||
val identifier = advertisement.identifier.toString()
|
val identifier = advertisement.identifier.toString()
|
||||||
Napier.d(">>>>> Connecting to $identifier")
|
Napier.d(">>>>> Connecting to $identifier")
|
||||||
val peripheral = mutex.withLock {
|
return mutex.withLock {
|
||||||
peripherals.getOrPut(identifier) {
|
peripherals.getOrPut(identifier) {
|
||||||
Peripheral(advertisement)
|
Peripheral(advertisement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Connect outside the lock to allow parallel connections for different devices
|
|
||||||
peripheral.connect()
|
|
||||||
return peripheral
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun disconnect(identifier: String) {
|
suspend fun disconnect(identifier: String) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
peripherals.remove(identifier)?.disconnect()
|
peripherals.remove(identifier)?.disconnect()
|
||||||
}
|
}
|
||||||
Napier.d(">>>>> $identifier disconnected")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun disconnectAll() {
|
suspend fun disconnectAll() {
|
||||||
@@ -65,7 +69,6 @@ class BleManager {
|
|||||||
peripherals.clear()
|
peripherals.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendCommand(peripheral: Peripheral, command: String) {
|
suspend fun sendCommand(peripheral: Peripheral, command: String) {
|
||||||
Napier.d(">>>>> Sent to uart: $command")
|
Napier.d(">>>>> Sent to uart: $command")
|
||||||
peripheral.write(RX_CHAR, command.encodeToByteArray())
|
peripheral.write(RX_CHAR, command.encodeToByteArray())
|
||||||
|
|||||||
+25
-5
@@ -10,24 +10,29 @@ import androidx.compose.material3.NavigationBarItem
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.navigation.NavType
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import androidx.navigation.navArgument
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.AppViewModel
|
import com.digitoolsolutions.app.torquevaultkmp.AppViewModel
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.screens.auth.AuthDestination
|
import com.digitoolsolutions.app.torquevaultkmp.screens.auth.AuthDestination
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.screens.auth.LoginScreen
|
import com.digitoolsolutions.app.torquevaultkmp.screens.auth.LoginScreen
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.screens.bluetooth.BluetoothRequirementWrapper
|
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.screens.bonded.BondedDestination
|
import com.digitoolsolutions.app.torquevaultkmp.screens.bonded.BondedDestination
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.screens.bonded.BondedScreen
|
import com.digitoolsolutions.app.torquevaultkmp.screens.bonded.BondedScreen
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.screens.communication.CommunicationDestination
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.screens.communication.UartCommunicationScreen
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.screens.home.HomeDestination
|
import com.digitoolsolutions.app.torquevaultkmp.screens.home.HomeDestination
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.screens.home.HomeScreen
|
import com.digitoolsolutions.app.torquevaultkmp.screens.home.HomeScreen
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.screens.logs.LogDestination
|
import com.digitoolsolutions.app.torquevaultkmp.screens.logs.LogDestination
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.screens.logs.LogScreen
|
import com.digitoolsolutions.app.torquevaultkmp.screens.logs.LogScreen
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.screens.scanner.ScannerDestination
|
import com.digitoolsolutions.app.torquevaultkmp.screens.scanner.ScannerDestination
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.screens.scanner.ScannerScreen
|
import com.digitoolsolutions.app.torquevaultkmp.screens.scanner.ScannerScreen
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.screens.scanner.ScannerViewModel
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.screens.settings.SettingDestination
|
import com.digitoolsolutions.app.torquevaultkmp.screens.settings.SettingDestination
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.screens.settings.SettingScreen
|
import com.digitoolsolutions.app.torquevaultkmp.screens.settings.SettingScreen
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
@@ -35,13 +40,16 @@ import org.jetbrains.compose.resources.stringResource
|
|||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import torquevaultkmp.composeapp.generated.resources.Res
|
import torquevaultkmp.composeapp.generated.resources.Res
|
||||||
import torquevaultkmp.composeapp.generated.resources.btn_login
|
import torquevaultkmp.composeapp.generated.resources.btn_login
|
||||||
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
|
||||||
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
onThemeToggle: (Boolean) -> Unit,
|
onThemeToggle: (Boolean) -> Unit,
|
||||||
isLoggedIn: Boolean,
|
isLoggedIn: Boolean,
|
||||||
viewModel: AppViewModel = koinViewModel()
|
viewModel: AppViewModel = koinViewModel(),
|
||||||
|
scannerViewModel: ScannerViewModel = koinViewModel(),
|
||||||
) {
|
) {
|
||||||
val destinations = listOf(
|
val destinations = listOf(
|
||||||
HomeDestination,
|
HomeDestination,
|
||||||
@@ -53,6 +61,7 @@ fun MainScreen(
|
|||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
val currentDestination = navBackStackEntry?.destination
|
val currentDestination = navBackStackEntry?.destination
|
||||||
|
val noNavbar = listOf(AuthDestination.route, CommunicationDestination.route)
|
||||||
|
|
||||||
val sessionExpired by viewModel.sessionExpired
|
val sessionExpired by viewModel.sessionExpired
|
||||||
|
|
||||||
@@ -74,10 +83,14 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
scannerViewModel.startScan()
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
if (currentDestination?.route != AuthDestination.route)
|
if (currentDestination?.route !in noNavbar)
|
||||||
NavigationBar {
|
NavigationBar {
|
||||||
destinations.forEach { dest ->
|
destinations.forEach { dest ->
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
@@ -120,10 +133,17 @@ fun MainScreen(
|
|||||||
BondedScreen(navController)
|
BondedScreen(navController)
|
||||||
}
|
}
|
||||||
composable(ScannerDestination.route) {
|
composable(ScannerDestination.route) {
|
||||||
ScannerScreen(onDeviceClick = {
|
ScannerScreen(onDeviceClick = { device ->
|
||||||
// Handle device click, maybe navigate to details or connect
|
navController.navigate(CommunicationDestination.createRoute(device.identifier.toString()))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
composable(
|
||||||
|
route = CommunicationDestination.route,
|
||||||
|
arguments = listOf(navArgument("deviceId") { type = NavType.StringType })
|
||||||
|
) { backStackEntry ->
|
||||||
|
val deviceId = backStackEntry.savedStateHandle.get<String>("deviceId") ?: ""
|
||||||
|
UartCommunicationScreen(navController, deviceId)
|
||||||
|
}
|
||||||
composable(LogDestination.route) { LogScreen(navController) }
|
composable(LogDestination.route) { LogScreen(navController) }
|
||||||
composable(SettingDestination.route) {
|
composable(SettingDestination.route) {
|
||||||
SettingScreen(navController, isDarkTheme, onThemeToggle)
|
SettingScreen(navController, isDarkTheme, onThemeToggle)
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package com.digitoolsolutions.app.torquevaultkmp.screens.communication
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Bluetooth
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.screens.AppIcon
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.screens.Destinations
|
||||||
|
|
||||||
|
object CommunicationDestination: Destinations(
|
||||||
|
label = "Communication",
|
||||||
|
icon = AppIcon.Vector(Icons.Outlined.Bluetooth),
|
||||||
|
route = "communication/{deviceId}"
|
||||||
|
) {
|
||||||
|
fun createRoute(deviceId: String) = "communication/$deviceId"
|
||||||
|
}
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
package com.digitoolsolutions.app.torquevaultkmp.screens.communication
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.components.AppBar
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.domain.model.ConnectionReason
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.screens.communication.views.DeviceConnected
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.screens.communication.views.DeviceConnecting
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.screens.communication.views.DeviceDisconnected
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.screens.scanner.ScannerViewModel
|
||||||
|
import com.juul.kable.State
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import org.koin.compose.koinInject
|
||||||
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
import torquevaultkmp.composeapp.generated.resources.Res
|
||||||
|
import torquevaultkmp.composeapp.generated.resources.action_cancel
|
||||||
|
import torquevaultkmp.composeapp.generated.resources.action_retry
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun UartCommunicationScreen(
|
||||||
|
navController: NavController,
|
||||||
|
deviceId: String,
|
||||||
|
viewModel: ScannerViewModel = koinInject()
|
||||||
|
) {
|
||||||
|
val messages by viewModel.rxMessages.collectAsState()
|
||||||
|
val connectedDevices by viewModel.connectedDevices.collectAsState()
|
||||||
|
val connectionReasons by viewModel.connectionReasons.collectAsState()
|
||||||
|
|
||||||
|
val connectionState = remember(connectedDevices, deviceId) {
|
||||||
|
connectedDevices[deviceId] ?: State.Disconnected()
|
||||||
|
}
|
||||||
|
val reason = remember(connectionReasons, deviceId) {
|
||||||
|
connectionReasons[deviceId] ?: ConnectionReason.LINK_LOSS
|
||||||
|
}
|
||||||
|
val deviceName = remember(connectedDevices) { viewModel.getDeviceName(deviceId) }
|
||||||
|
|
||||||
|
LaunchedEffect(deviceId) {
|
||||||
|
viewModel.clearMessages()
|
||||||
|
viewModel.connectById(deviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
AppBar(
|
||||||
|
title = { Text(deviceName) },
|
||||||
|
onNavigationButtonClick = {
|
||||||
|
viewModel.disconnect(deviceId)
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
when (connectionState) {
|
||||||
|
is State.Connecting -> {
|
||||||
|
DeviceConnecting (
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
) { padding ->
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.disconnect(deviceId) },
|
||||||
|
modifier = Modifier.padding(padding),
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(Res.string.action_cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is State.Disconnected -> {
|
||||||
|
DeviceDisconnected(
|
||||||
|
reason = reason,
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
) { padding ->
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.connectById(deviceId) },
|
||||||
|
modifier = Modifier.padding(padding),
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(Res.string.action_retry))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is State.Connected -> {
|
||||||
|
DeviceConnected(messages = messages, onSend = { msg ->
|
||||||
|
viewModel.sendRawCommand(deviceId, msg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Text(text = "Another State")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
package com.digitoolsolutions.app.torquevaultkmp.screens.communication.views
|
||||||
|
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun DeviceConnected(
|
||||||
|
messages: List<String>,
|
||||||
|
onSend: (msg: String) -> Unit
|
||||||
|
) {
|
||||||
|
var txInput by rememberSaveable { mutableStateOf("") }
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp)
|
||||||
|
.imePadding(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.border(1.dp, Color.Gray, RoundedCornerShape(4.dp))
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
items(messages) { message ->
|
||||||
|
Text(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(messages.size) {
|
||||||
|
if (messages.isNotEmpty()) {
|
||||||
|
listState.animateScrollToItem(messages.size - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = txInput,
|
||||||
|
onValueChange = { txInput = it },
|
||||||
|
label = { Text("Manual Command") },
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
onSend(txInput)
|
||||||
|
txInput = ""
|
||||||
|
},
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
|
) {
|
||||||
|
Text("Send")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
package com.digitoolsolutions.app.torquevaultkmp.screens.communication.views
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.BluetoothAudio
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedCard
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.components.CircularIcon
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import torquevaultkmp.composeapp.generated.resources.Res
|
||||||
|
import torquevaultkmp.composeapp.generated.resources.device_connecting
|
||||||
|
import torquevaultkmp.composeapp.generated.resources.device_explanation
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun DeviceConnecting(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable ColumnScope.(PaddingValues) -> Unit = {}
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
OutlinedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(max = 460.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
CircularIcon(imageVector = Icons.Default.BluetoothAudio)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.device_connecting),
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.device_explanation),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content(PaddingValues(top = 16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun DeviceConnectingView_Preview() {
|
||||||
|
DeviceConnecting { padding ->
|
||||||
|
Button(
|
||||||
|
onClick = {},
|
||||||
|
modifier = Modifier.padding(padding)
|
||||||
|
) {
|
||||||
|
Text(text = "Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
package com.digitoolsolutions.app.torquevaultkmp.screens.communication.views
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.BluetoothDisabled
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedCard
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.components.CircularIcon
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.domain.model.ConnectionReason
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import torquevaultkmp.composeapp.generated.resources.Res
|
||||||
|
import torquevaultkmp.composeapp.generated.resources.device_disconnected
|
||||||
|
import torquevaultkmp.composeapp.generated.resources.device_reason_link_loss
|
||||||
|
import torquevaultkmp.composeapp.generated.resources.device_reason_missing_service
|
||||||
|
import torquevaultkmp.composeapp.generated.resources.device_reason_timeout
|
||||||
|
import torquevaultkmp.composeapp.generated.resources.device_reason_user
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun DeviceDisconnected(
|
||||||
|
reason: ConnectionReason,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable ColumnScope.(PaddingValues) -> Unit = {},
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
OutlinedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(max = 460.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
CircularIcon(imageVector = Icons.Default.BluetoothDisabled)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.device_disconnected),
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
|
||||||
|
val text = when (reason) {
|
||||||
|
ConnectionReason.USER -> stringResource(Res.string.device_reason_user)
|
||||||
|
ConnectionReason.LINK_LOSS -> stringResource(Res.string.device_reason_link_loss)
|
||||||
|
ConnectionReason.MISSING_SERVICE -> stringResource(Res.string.device_reason_missing_service)
|
||||||
|
ConnectionReason.TIMEOUT -> stringResource(Res.string.device_reason_timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content(PaddingValues(top = 16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun DeviceDisconnectedView_Preview() {
|
||||||
|
DeviceDisconnected(
|
||||||
|
reason = ConnectionReason.MISSING_SERVICE,
|
||||||
|
content = { padding ->
|
||||||
|
Button(
|
||||||
|
onClick = {},
|
||||||
|
modifier = Modifier.padding(padding)
|
||||||
|
) {
|
||||||
|
Text(text = "Retry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
+12
-5
@@ -18,6 +18,7 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavController
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.components.AppBar
|
import com.digitoolsolutions.app.torquevaultkmp.components.AppBar
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.components.CircularIcon
|
import com.digitoolsolutions.app.torquevaultkmp.components.CircularIcon
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.components.RssiIcon
|
import com.digitoolsolutions.app.torquevaultkmp.components.RssiIcon
|
||||||
@@ -27,6 +28,7 @@ import com.juul.kable.Advertisement
|
|||||||
import com.juul.kable.State
|
import com.juul.kable.State
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import org.koin.compose.koinInject
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import torquevaultkmp.composeapp.generated.resources.Res
|
import torquevaultkmp.composeapp.generated.resources.Res
|
||||||
import torquevaultkmp.composeapp.generated.resources.baseline_filter_list_24
|
import torquevaultkmp.composeapp.generated.resources.baseline_filter_list_24
|
||||||
@@ -40,13 +42,14 @@ import kotlin.uuid.ExperimentalUuidApi
|
|||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ScannerScreen(
|
fun ScannerScreen(
|
||||||
viewModel: ScannerViewModel = koinViewModel(),
|
viewModel: ScannerViewModel = koinInject(),
|
||||||
onDeviceClick: (Advertisement) -> Unit
|
onDeviceClick: (Advertisement) -> Unit
|
||||||
) {
|
) {
|
||||||
val devices by viewModel.devices.collectAsState()
|
val devices by viewModel.devices.collectAsState()
|
||||||
val connectedDevices by viewModel.connectedDevices.collectAsState()
|
val connectedDevices by viewModel.connectedDevices.collectAsState()
|
||||||
val isScanning by viewModel.isScanning.collectAsState()
|
val isScanning by viewModel.isScanning.collectAsState()
|
||||||
val pullToRefreshState = rememberPullToRefreshState()
|
val pullToRefreshState = rememberPullToRefreshState()
|
||||||
|
val autoConnect by viewModel.isAutoConnect.collectAsState()
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -101,7 +104,11 @@ fun ScannerScreen(
|
|||||||
connectionState = connectionState,
|
connectionState = connectionState,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (connectionState == null || connectionState is State.Disconnected) {
|
if (connectionState == null || connectionState is State.Disconnected) {
|
||||||
viewModel.connect(device)
|
if (!autoConnect) {
|
||||||
|
onDeviceClick(device)
|
||||||
|
} else {
|
||||||
|
viewModel.connect(device)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
viewModel.disconnect(deviceId)
|
viewModel.disconnect(deviceId)
|
||||||
}
|
}
|
||||||
@@ -113,9 +120,9 @@ fun ScannerScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
// LaunchedEffect(Unit) {
|
||||||
viewModel.startScan()
|
// viewModel.startScan()
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
|
|||||||
+78
-17
@@ -10,6 +10,7 @@ import com.digitoolsolutions.app.torquevaultkmp.data.storage.AppStorage
|
|||||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.ReferKeys
|
import com.digitoolsolutions.app.torquevaultkmp.data.storage.ReferKeys
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.load
|
import com.digitoolsolutions.app.torquevaultkmp.data.storage.load
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.observe
|
import com.digitoolsolutions.app.torquevaultkmp.data.storage.observe
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.domain.model.ConnectionReason
|
||||||
import com.juul.kable.Advertisement
|
import com.juul.kable.Advertisement
|
||||||
import com.juul.kable.Peripheral
|
import com.juul.kable.Peripheral
|
||||||
import com.juul.kable.State
|
import com.juul.kable.State
|
||||||
@@ -31,6 +32,8 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import kotlin.time.TimeSource
|
import kotlin.time.TimeSource
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
@@ -44,6 +47,7 @@ class ScannerViewModel(
|
|||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
companion object {
|
companion object {
|
||||||
private val SCAN_TIMEOUT = 1.seconds
|
private val SCAN_TIMEOUT = 1.seconds
|
||||||
|
private val CONNECTION_TIMEOUT = 15.seconds
|
||||||
}
|
}
|
||||||
private val _devices = MutableStateFlow<List<Advertisement>>(emptyList())
|
private val _devices = MutableStateFlow<List<Advertisement>>(emptyList())
|
||||||
val devices: StateFlow<List<Advertisement>> = _devices.asStateFlow()
|
val devices: StateFlow<List<Advertisement>> = _devices.asStateFlow()
|
||||||
@@ -52,11 +56,19 @@ class ScannerViewModel(
|
|||||||
val isScanning: StateFlow<Boolean> = _isScanning.asStateFlow()
|
val isScanning: StateFlow<Boolean> = _isScanning.asStateFlow()
|
||||||
private val _connectedDevices = MutableStateFlow<Map<String, State>>(emptyMap())
|
private val _connectedDevices = MutableStateFlow<Map<String, State>>(emptyMap())
|
||||||
val connectedDevices: StateFlow<Map<String, State>> = _connectedDevices.asStateFlow()
|
val connectedDevices: StateFlow<Map<String, State>> = _connectedDevices.asStateFlow()
|
||||||
|
|
||||||
|
private val _connectionReasons = MutableStateFlow<Map<String, ConnectionReason>>(emptyMap())
|
||||||
|
val connectionReasons: StateFlow<Map<String, ConnectionReason>> = _connectionReasons.asStateFlow()
|
||||||
|
|
||||||
|
private val _rxMessages = MutableStateFlow<List<String>>(emptyList())
|
||||||
|
val rxMessages: StateFlow<List<String>> = _rxMessages.asStateFlow()
|
||||||
|
|
||||||
private val _workOrders = MutableStateFlow<List<WorkOrder>>(emptyList())
|
private val _workOrders = MutableStateFlow<List<WorkOrder>>(emptyList())
|
||||||
private val _autoConnect: StateFlow<Boolean> = storage.observe<Boolean>(ReferKeys.DEVICE_AUTO_CONNECT)
|
private val _autoConnect: StateFlow<Boolean> = storage.observe<Boolean>(ReferKeys.DEVICE_AUTO_CONNECT)
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
||||||
private val _onlyWithName = MutableStateFlow(true)
|
val isAutoConnect: StateFlow<Boolean> = _autoConnect
|
||||||
|
|
||||||
|
private val _onlyWithName = MutableStateFlow(true)
|
||||||
private var scanJob: Job? = null
|
private var scanJob: Job? = null
|
||||||
private var cleanupJob: Job? = null
|
private var cleanupJob: Job? = null
|
||||||
private val connectionJobs = mutableMapOf<String, Job>()
|
private val connectionJobs = mutableMapOf<String, Job>()
|
||||||
@@ -89,8 +101,8 @@ class ScannerViewModel(
|
|||||||
advertisementMap.clear()
|
advertisementMap.clear()
|
||||||
|
|
||||||
bleManager.scanDevices()
|
bleManager.scanDevices()
|
||||||
.catch {
|
.catch {
|
||||||
_isScanning.value = false
|
_isScanning.value = false
|
||||||
stopCleanupJob()
|
stopCleanupJob()
|
||||||
}
|
}
|
||||||
.collect { advertisement ->
|
.collect { advertisement ->
|
||||||
@@ -145,6 +157,37 @@ class ScannerViewModel(
|
|||||||
startScan()
|
startScan()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun connectById(deviceId: String) {
|
||||||
|
val adv = bleManager.getAdvertisement(deviceId)
|
||||||
|
if (adv != null) {
|
||||||
|
connect(adv)
|
||||||
|
} else {
|
||||||
|
Napier.e("Cannot connect: Advertisement not found for $deviceId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun getDeviceName(identifier: String): String {
|
||||||
|
return bleManager.getAdvertisementName(identifier)
|
||||||
|
}
|
||||||
|
fun getPeripheral(identifier: String): Peripheral? {
|
||||||
|
return bleManager.getPeripheral(identifier)
|
||||||
|
}
|
||||||
|
fun clearMessages() {
|
||||||
|
_rxMessages.value = emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendRawCommand(deviceIdentifier: String, command: String) {
|
||||||
|
val peripheral = bleManager.getPeripheral(deviceIdentifier)
|
||||||
|
if (peripheral != null) {
|
||||||
|
sendCommand(peripheral, command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun sendCommand(peripheral: Peripheral, command: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
bleManager.sendCommand(peripheral, Helper.buildUartCommand(command))
|
||||||
|
_rxMessages.value += "Sent: $command"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun connect(adv: Advertisement) {
|
fun connect(adv: Advertisement) {
|
||||||
val deviceIdentifier = adv.identifier.toString()
|
val deviceIdentifier = adv.identifier.toString()
|
||||||
if (connectionJobs[deviceIdentifier]?.isActive == true) return
|
if (connectionJobs[deviceIdentifier]?.isActive == true) return
|
||||||
@@ -152,17 +195,25 @@ class ScannerViewModel(
|
|||||||
val job = viewModelScope.launch {
|
val job = viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val peripheral = bleManager.connect(adv)
|
val peripheral = bleManager.connect(adv)
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
// Tracking connection state
|
// Tracking connection state
|
||||||
|
var hasEmitted = false
|
||||||
launch {
|
launch {
|
||||||
peripheral.state.collect { state ->
|
peripheral.state.collect { state ->
|
||||||
_connectedDevices.value += (deviceIdentifier to state)
|
_connectedDevices.value += (deviceIdentifier to state)
|
||||||
/** Add bonded device */
|
if (state is State.Connected) {
|
||||||
addBondedDevice(deviceIdentifier, adv.name ?: "No name")
|
/** Add bonded device */
|
||||||
if (state is State.Disconnected) {
|
addBondedDevice(deviceIdentifier, adv.name ?: "No name")
|
||||||
|
_connectionReasons.value -= deviceIdentifier
|
||||||
|
}
|
||||||
|
if (state is State.Disconnected && hasEmitted) {
|
||||||
|
if (_connectionReasons.value[deviceIdentifier] == null) {
|
||||||
|
_connectionReasons.value += (deviceIdentifier to ConnectionReason.LINK_LOSS)
|
||||||
|
}
|
||||||
this@coroutineScope.cancel(">>>>> Device disconnected")
|
this@coroutineScope.cancel(">>>>> Device disconnected")
|
||||||
}
|
}
|
||||||
|
hasEmitted = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,9 +223,17 @@ class ScannerViewModel(
|
|||||||
handleMessage(peripheral, value, deviceIdentifier)
|
handleMessage(peripheral, value, deviceIdentifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withTimeout(CONNECTION_TIMEOUT) {
|
||||||
|
_connectionReasons.value -= deviceIdentifier
|
||||||
|
peripheral.connect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e !is CancellationException) {
|
if (e is TimeoutCancellationException) {
|
||||||
|
Napier.e(">>>>> Connection timed out for $deviceIdentifier")
|
||||||
|
_connectionReasons.value += (deviceIdentifier to ConnectionReason.TIMEOUT)
|
||||||
|
} else if (e !is CancellationException) {
|
||||||
Napier.e(">>>>> Connection failed for $deviceIdentifier", e)
|
Napier.e(">>>>> Connection failed for $deviceIdentifier", e)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -187,7 +246,9 @@ class ScannerViewModel(
|
|||||||
connectionJobs[deviceIdentifier] = job
|
connectionJobs[deviceIdentifier] = job
|
||||||
}
|
}
|
||||||
fun disconnect(deviceIdentifier: String) {
|
fun disconnect(deviceIdentifier: String) {
|
||||||
|
Napier.d(">>>>> disconnecting $deviceIdentifier")
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
_connectionReasons.value += (deviceIdentifier to ConnectionReason.USER)
|
||||||
bleManager.disconnect(deviceIdentifier)
|
bleManager.disconnect(deviceIdentifier)
|
||||||
connectionJobs[deviceIdentifier]?.cancel()
|
connectionJobs[deviceIdentifier]?.cancel()
|
||||||
connectionJobs.remove(deviceIdentifier)
|
connectionJobs.remove(deviceIdentifier)
|
||||||
@@ -208,7 +269,7 @@ class ScannerViewModel(
|
|||||||
private fun sendWorkOrdersToDevice(peripheral: Peripheral, workOrders: List<WorkOrder>) {
|
private fun sendWorkOrdersToDevice(peripheral: Peripheral, workOrders: List<WorkOrder>) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (workOrders.isEmpty()) {
|
if (workOrders.isEmpty()) {
|
||||||
bleManager.sendCommand(peripheral, Helper.buildUartCommand(Helper.CMD_EMPTY_WO))
|
sendCommand(peripheral, Helper.CMD_EMPTY_WO)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
val formatted = workOrders.joinToString(separator = "\r\n") { wo ->
|
val formatted = workOrders.joinToString(separator = "\r\n") { wo ->
|
||||||
@@ -217,10 +278,12 @@ class ScannerViewModel(
|
|||||||
val wheelsMask = wo.computeWheelsNuts()
|
val wheelsMask = wo.computeWheelsNuts()
|
||||||
"$${wo.id},${actionCode},${wo.make},${wo.licensePlate},$torque,ft-lb,$wheelsMask*"
|
"$${wo.id},${actionCode},${wo.make},${wo.licensePlate},$torque,ft-lb,$wheelsMask*"
|
||||||
}
|
}
|
||||||
bleManager.sendCommand(peripheral, Helper.buildUartCommand(formatted))
|
|
||||||
|
sendCommand(peripheral, formatted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun handleMessage(peripheral: Peripheral, msg: String, deviceIdentifier: String) {
|
private fun handleMessage(peripheral: Peripheral, msg: String, deviceIdentifier: String) {
|
||||||
|
_rxMessages.value += "Received: $msg"
|
||||||
val decoded = Helper.decodeRxData(msg) ?: return
|
val decoded = Helper.decodeRxData(msg) ?: return
|
||||||
val data = decoded.data
|
val data = decoded.data
|
||||||
val woId = decoded.id
|
val woId = decoded.id
|
||||||
@@ -237,12 +300,10 @@ class ScannerViewModel(
|
|||||||
var command = Helper.CMD_NOT_AVAILABLE_WO
|
var command = Helper.CMD_NOT_AVAILABLE_WO
|
||||||
if (res)
|
if (res)
|
||||||
command = Helper.CMD_RECEIVED_WO
|
command = Helper.CMD_RECEIVED_WO
|
||||||
bleManager.sendCommand(peripheral, Helper.buildUartCommand(command))
|
sendCommand(peripheral, command)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Napier.e(">>>>> Failed to handle message: $msg", e)
|
Napier.e(">>>>> Failed to handle message: $msg", e)
|
||||||
bleManager.sendCommand(
|
sendCommand(peripheral, Helper.CMD_NOT_AVAILABLE_WO)
|
||||||
peripheral, Helper.buildUartCommand(Helper.CMD_NOT_AVAILABLE_WO)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -265,7 +326,7 @@ class ScannerViewModel(
|
|||||||
Napier.e("An error occurred while sending the measurement", e, tag = "ScannerViewModel")
|
Napier.e("An error occurred while sending the measurement", e, tag = "ScannerViewModel")
|
||||||
} finally {
|
} finally {
|
||||||
// Currently, always send UPLOADED_WO regardless of success or failure
|
// Currently, always send UPLOADED_WO regardless of success or failure
|
||||||
bleManager.sendCommand(peripheral, Helper.buildUartCommand(Helper.CMD_UPLOADED_WO))
|
sendCommand(peripheral, Helper.CMD_UPLOADED_WO)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Helper.TorqueAction.RE_TORQUE -> {
|
Helper.TorqueAction.RE_TORQUE -> {
|
||||||
@@ -275,7 +336,7 @@ class ScannerViewModel(
|
|||||||
Napier.e("An error occurred while sending the re-measurement", e, tag = "ScannerViewModel")
|
Napier.e("An error occurred while sending the re-measurement", e, tag = "ScannerViewModel")
|
||||||
} finally {
|
} finally {
|
||||||
// Currently, always send UPLOADED_WO regardless of success or failure
|
// Currently, always send UPLOADED_WO regardless of success or failure
|
||||||
bleManager.sendCommand(peripheral, Helper.buildUartCommand(Helper.CMD_UPLOADED_WO))
|
sendCommand(peripheral, Helper.CMD_UPLOADED_WO)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Helper.TorqueAction.CANCEL -> {
|
Helper.TorqueAction.CANCEL -> {
|
||||||
@@ -285,7 +346,7 @@ class ScannerViewModel(
|
|||||||
Napier.e("An error occurred while cancel work order", e, tag = "ScannerViewModel")
|
Napier.e("An error occurred while cancel work order", e, tag = "ScannerViewModel")
|
||||||
} finally {
|
} finally {
|
||||||
// Currently, always send UPLOADED_WO regardless of success or failure
|
// Currently, always send UPLOADED_WO regardless of success or failure
|
||||||
bleManager.sendCommand(peripheral, Helper.buildUartCommand(Helper.CMD_UPLOADED_WO))
|
sendCommand(peripheral, Helper.CMD_UPLOADED_WO)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> Napier.w(">>>>> Unknown action: $act")
|
else -> Napier.w(">>>>> Unknown action: $act")
|
||||||
|
|||||||
Reference in New Issue
Block a user