Add logs communicate and display on log screen

This commit is contained in:
2026-06-05 18:51:17 +07:00
parent 196623fb25
commit 9dcdf68c63
8 changed files with 42 additions and 47 deletions
@@ -13,7 +13,7 @@ import kotlin.time.Clock
class LogRepository( class LogRepository(
private val dbQueries: AppDatabaseQueries private val dbQueries: AppDatabaseQueries
) { ) {
suspend fun addLog(title: String, content: String, deviceId: String? = null) { fun addLog(title: String, content: String, deviceId: String? = null) {
val source = if (deviceId == null) LogFilter.System.name else LogFilter.Device.name val source = if (deviceId == null) LogFilter.System.name else LogFilter.Device.name
dbQueries.insertLog( dbQueries.insertLog(
title = title, title = title,
@@ -63,5 +63,5 @@ val viewModelModule = module {
factoryOf(::SettingViewModel) factoryOf(::SettingViewModel)
factoryOf(::BondedViewModel) factoryOf(::BondedViewModel)
factoryOf(::LogViewModel) factoryOf(::LogViewModel)
single { ScannerViewModel(get(), get(), get(), get()) } single { ScannerViewModel(get(), get(), get(), get(), get()) }
} }
@@ -130,7 +130,7 @@ fun MainScreen(
HomeScreen(navController) HomeScreen(navController)
} }
composable(BondedDestination.route) { composable(BondedDestination.route) {
BondedScreen(navController) BondedScreen()
} }
composable(ScannerDestination.route) { composable(ScannerDestination.route) {
ScannerScreen(onDeviceClick = { device -> ScannerScreen(onDeviceClick = { device ->
@@ -144,7 +144,7 @@ fun MainScreen(
val deviceId = backStackEntry.savedStateHandle.get<String>("deviceId") ?: "" val deviceId = backStackEntry.savedStateHandle.get<String>("deviceId") ?: ""
UartCommunicationScreen(navController, deviceId) UartCommunicationScreen(navController, deviceId)
} }
composable(LogDestination.route) { LogScreen(navController) } composable(LogDestination.route) { LogScreen() }
composable(SettingDestination.route) { composable(SettingDestination.route) {
SettingScreen(navController, isDarkTheme, onThemeToggle) SettingScreen(navController, isDarkTheme, onThemeToggle)
} }
@@ -45,7 +45,6 @@ import torquevaultkmp.composeapp.generated.resources.bonded_title
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun BondedScreen( fun BondedScreen(
navController: NavController,
viewModel: BondedViewModel = koinViewModel() viewModel: BondedViewModel = koinViewModel()
) { ) {
val devices by viewModel.bondedDevices.collectAsState() val devices by viewModel.bondedDevices.collectAsState()
@@ -57,7 +56,6 @@ fun BondedScreen(
BondedContent( BondedContent(
devices = devices, devices = devices,
isLoading = isLoading, isLoading = isLoading,
onAddFake = { viewModel.addFakeDevice() },
onDelete = { viewModel.removeDevice(it.id) }, onDelete = { viewModel.removeDevice(it.id) },
onLoadMore = { viewModel.loadNextPage() }, onLoadMore = { viewModel.loadNextPage() },
onRefresh = { viewModel.refresh() } onRefresh = { viewModel.refresh() }
@@ -69,7 +67,6 @@ fun BondedScreen(
internal fun BondedContent( internal fun BondedContent(
devices: List<Device>, devices: List<Device>,
isLoading: Boolean, isLoading: Boolean,
onAddFake: () -> Unit,
onDelete: (Device) -> Unit, onDelete: (Device) -> Unit,
onLoadMore: () -> Unit, onLoadMore: () -> Unit,
onRefresh: () -> Unit onRefresh: () -> Unit
@@ -101,11 +98,11 @@ internal fun BondedContent(
title = {Text(stringResource(Res.string.bonded_title))}, title = {Text(stringResource(Res.string.bonded_title))},
) )
}, },
floatingActionButton = { // floatingActionButton = {
FloatingActionButton(onClick = onAddFake) { // FloatingActionButton(onClick = {}) {
Icon(Icons.Default.Add, contentDescription = "Add Fake Device") // Icon(Icons.Default.Add, contentDescription = "Add")
} // }
}, // },
snackbarHost = { SnackbarHost(snackbarHostState) }) snackbarHost = { SnackbarHost(snackbarHostState) })
{ padding -> { padding ->
val pullToRefreshState = rememberPullToRefreshState() val pullToRefreshState = rememberPullToRefreshState()
@@ -55,16 +55,6 @@ class BondedViewModel(
loadNextPage() loadNextPage()
} }
} }
fun addFakeDevice() {
viewModelScope.launch {
val id = "AA:BB:CC:DD:EE:${Random.nextInt(10, 99)}"
val name = "Torque Tool ${Random.nextInt(1, 100)}"
deviceRepository.saveDevice(id, name)
refresh()
}
}
fun removeDevice(id: String) { fun removeDevice(id: String) {
viewModelScope.launch { viewModelScope.launch {
deviceRepository.deleteDevice(id) deviceRepository.deleteDevice(id)
@@ -9,9 +9,9 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.DeleteForever import androidx.compose.material.icons.filled.DeleteForever
import androidx.compose.material.icons.outlined.FilterList import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.rounded.KeyboardArrowUp
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@@ -32,13 +32,13 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
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.data.storage.db.Log import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.Log
import com.digitoolsolutions.app.torquevaultkmp.domain.model.LogFilter import com.digitoolsolutions.app.torquevaultkmp.domain.model.LogFilter
@@ -47,6 +47,7 @@ import com.digitoolsolutions.app.torquevaultkmp.screens.logs.view.FilterBottomSh
import com.digitoolsolutions.app.torquevaultkmp.screens.logs.view.LogItem import com.digitoolsolutions.app.torquevaultkmp.screens.logs.view.LogItem
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource 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
@@ -55,19 +56,20 @@ import torquevaultkmp.composeapp.generated.resources.log_title
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LogScreen( fun LogScreen(
navController: NavController,
viewModel: LogViewModel = koinViewModel() viewModel: LogViewModel = koinViewModel()
) { ) {
val logs by viewModel.logs.collectAsState() val logs by viewModel.logs.collectAsState()
val activeFilter by viewModel.activeFilter.collectAsState() val activeFilter by viewModel.activeFilter.collectAsState()
val isLoading by viewModel.isLoading.collectAsState() val isLoading by viewModel.isLoading.collectAsState()
LaunchedEffect(Unit) {
viewModel.refresh()
}
LogContent( LogContent(
logs = logs, logs = logs,
activeFilter = activeFilter, activeFilter = activeFilter,
isLoading = isLoading, isLoading = isLoading,
onFilterSelected = { viewModel.setFilter(it) }, onFilterSelected = { viewModel.setFilter(it) },
onAddFakeLog = { viewModel.addFakeLog() },
onDeleteAllLogs = { viewModel.clearAllLogs() }, onDeleteAllLogs = { viewModel.clearAllLogs() },
onLoadMore = { viewModel.loadNextPage() }, onLoadMore = { viewModel.loadNextPage() },
onRefresh = { viewModel.refresh() } onRefresh = { viewModel.refresh() }
@@ -81,7 +83,6 @@ internal fun LogContent(
activeFilter: LogFilter, activeFilter: LogFilter,
isLoading: Boolean, isLoading: Boolean,
onFilterSelected: (LogFilter) -> Unit, onFilterSelected: (LogFilter) -> Unit,
onAddFakeLog: () -> Unit,
onDeleteAllLogs: () -> Unit, onDeleteAllLogs: () -> Unit,
onLoadMore: () -> Unit, onLoadMore: () -> Unit,
onRefresh: () -> Unit onRefresh: () -> Unit
@@ -91,6 +92,7 @@ internal fun LogContent(
var showBottomSheet by remember { mutableStateOf(false) } var showBottomSheet by remember { mutableStateOf(false) }
val listState = rememberLazyListState() val listState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
// Infinite Scroll logic // Infinite Scroll logic
val shouldLoadMore = remember { val shouldLoadMore = remember {
@@ -100,6 +102,12 @@ internal fun LogContent(
} }
} }
LaunchedEffect(logs) {
if (logs.isNotEmpty()) {
listState.scrollToItem(0)
}
}
LaunchedEffect(shouldLoadMore) { LaunchedEffect(shouldLoadMore) {
snapshotFlow { shouldLoadMore.value } snapshotFlow { shouldLoadMore.value }
.distinctUntilChanged() .distinctUntilChanged()
@@ -133,8 +141,13 @@ internal fun LogContent(
) )
}, },
floatingActionButton = { floatingActionButton = {
FloatingActionButton(onClick = onAddFakeLog) { FloatingActionButton(onClick = {
Icon(Icons.Default.Add, contentDescription = "Add Fake Log") onRefresh()
coroutineScope.launch {
listState.animateScrollToItem(0)
}
}) {
Icon(Icons.Rounded.KeyboardArrowUp, contentDescription = "Go to top")
} }
}, },
snackbarHost = { SnackbarHost(snackbarHostState) }) snackbarHost = { SnackbarHost(snackbarHostState) })
@@ -57,7 +57,7 @@ class LogViewModel(
val lastLog = newLogs.last() val lastLog = newLogs.last()
lastTimestamp = lastLog.timestamp lastTimestamp = lastLog.timestamp
lastId = lastLog.id lastId = lastLog.id
_logs.value = _logs.value + newLogs _logs.value += newLogs
} }
isLoading.value = false isLoading.value = false
@@ -73,19 +73,6 @@ class LogViewModel(
loadNextPage() loadNextPage()
} }
} }
fun addFakeLog() {
viewModelScope.launch {
val titles = listOf("System Alert", "Device Connected", "Torque Applied", "Calibration Sync")
val contents = listOf("Voltage stable", "Tool #42 joined", "15.5 Nm recorded", "Sync successful")
logRepository.addLog(
title = titles.random(),
content = contents.random()
)
refresh()
}
}
fun clearAllLogs() { fun clearAllLogs() {
viewModelScope.launch { viewModelScope.launch {
logRepository.clearLogs() logRepository.clearLogs()
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.SendMeasureDto import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.SendMeasureDto
import com.digitoolsolutions.app.torquevaultkmp.kable.BleManager import com.digitoolsolutions.app.torquevaultkmp.kable.BleManager
import com.digitoolsolutions.app.torquevaultkmp.data.repository.DeviceRepository import com.digitoolsolutions.app.torquevaultkmp.data.repository.DeviceRepository
import com.digitoolsolutions.app.torquevaultkmp.data.repository.LogRepository
import com.digitoolsolutions.app.torquevaultkmp.data.repository.ScannerRepository import com.digitoolsolutions.app.torquevaultkmp.data.repository.ScannerRepository
import com.digitoolsolutions.app.torquevaultkmp.data.storage.AppStorage import com.digitoolsolutions.app.torquevaultkmp.data.storage.AppStorage
import com.digitoolsolutions.app.torquevaultkmp.data.storage.ReferKeys import com.digitoolsolutions.app.torquevaultkmp.data.storage.ReferKeys
@@ -17,6 +18,7 @@ import com.juul.kable.State
import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder
import com.digitoolsolutions.app.torquevaultkmp.domain.model.computeWheelsNuts import com.digitoolsolutions.app.torquevaultkmp.domain.model.computeWheelsNuts
import com.digitoolsolutions.app.torquevaultkmp.utils.Helper import com.digitoolsolutions.app.torquevaultkmp.utils.Helper
import com.juul.kable.ExperimentalApi
import io.github.aakira.napier.Napier import io.github.aakira.napier.Napier
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -41,8 +43,9 @@ import kotlin.uuid.ExperimentalUuidApi
@OptIn(ExperimentalUuidApi::class) @OptIn(ExperimentalUuidApi::class)
class ScannerViewModel( class ScannerViewModel(
private val bleManager: BleManager, private val bleManager: BleManager,
private val storage: AppStorage, storage: AppStorage,
private val deviceRepository: DeviceRepository, private val deviceRepository: DeviceRepository,
private val logRepository: LogRepository,
private val scannerRepository: ScannerRepository private val scannerRepository: ScannerRepository
) : ViewModel() { ) : ViewModel() {
companion object { companion object {
@@ -118,6 +121,7 @@ class ScannerViewModel(
} }
} }
} }
logRepository.addLog(title="Scanner status", content="Scanning ...")
} }
private fun startCleanupJob() { private fun startCleanupJob() {
@@ -181,17 +185,19 @@ class ScannerViewModel(
sendCommand(peripheral, command) sendCommand(peripheral, command)
} }
} }
fun sendCommand(peripheral: Peripheral, command: String) { fun sendCommand(peripheral: Peripheral, command: String) {
viewModelScope.launch { viewModelScope.launch {
bleManager.sendCommand(peripheral, Helper.buildUartCommand(command)) bleManager.sendCommand(peripheral, Helper.buildUartCommand(command))
_rxMessages.value += "Sent: $command" _rxMessages.value += "Sent: $command"
logRepository.addLog(title = "Communicate - Sent", content = command, peripheral.identifier.toString())
} }
} }
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
logRepository.addLog(title = "Connection - Status", deviceId = deviceIdentifier ,content = "Device [${adv.name}] is connecting...")
val job = viewModelScope.launch { val job = viewModelScope.launch {
try { try {
val peripheral = bleManager.connect(adv) val peripheral = bleManager.connect(adv)
@@ -206,6 +212,7 @@ class ScannerViewModel(
/** Add bonded device */ /** Add bonded device */
addBondedDevice(deviceIdentifier, adv.name ?: "No name") addBondedDevice(deviceIdentifier, adv.name ?: "No name")
_connectionReasons.value -= deviceIdentifier _connectionReasons.value -= deviceIdentifier
logRepository.addLog(title = "Connection - Status", deviceId = deviceIdentifier ,content = "Device [${adv.name}] has been connected.")
} }
if (state is State.Disconnected && hasEmitted) { if (state is State.Disconnected && hasEmitted) {
if (_connectionReasons.value[deviceIdentifier] == null) { if (_connectionReasons.value[deviceIdentifier] == null) {
@@ -233,8 +240,10 @@ class ScannerViewModel(
if (e is TimeoutCancellationException) { if (e is TimeoutCancellationException) {
Napier.e(">>>>> Connection timed out for $deviceIdentifier") Napier.e(">>>>> Connection timed out for $deviceIdentifier")
_connectionReasons.value += (deviceIdentifier to ConnectionReason.TIMEOUT) _connectionReasons.value += (deviceIdentifier to ConnectionReason.TIMEOUT)
logRepository.addLog(title = "Connection - Status", content = "Connect to device [${adv.name}] failed. ${e.message}")
} else if (e !is CancellationException) { } else if (e !is CancellationException) {
Napier.e(">>>>> Connection failed for $deviceIdentifier", e) Napier.e(">>>>> Connection failed for $deviceIdentifier", e)
logRepository.addLog(title = "Connection - Status", content = "Connect to device [${adv.name}] failed. ${e.message}")
} }
} finally { } finally {
_connectedDevices.value -= deviceIdentifier _connectedDevices.value -= deviceIdentifier
@@ -284,13 +293,12 @@ class ScannerViewModel(
} }
private fun handleMessage(peripheral: Peripheral, msg: String, deviceIdentifier: String) { private fun handleMessage(peripheral: Peripheral, msg: String, deviceIdentifier: String) {
_rxMessages.value += "Received: $msg" _rxMessages.value += "Received: $msg"
logRepository.addLog(title = "Communicate - Received", content = msg, deviceId = deviceIdentifier)
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
val cmd = data.take(4) val cmd = data.take(4)
val action = if (cmd == "back") "back" else "next" val action = if (cmd == "back") "back" else "next"
Napier.d(">>>>> Received data from $deviceIdentifier: $msg", tag="ScannerViewModel.kt")
Napier.d(">>>>> Data: $data", tag="ScannerViewModel.kt")
viewModelScope.launch { viewModelScope.launch {
when (cmd) { when (cmd) {
"requ", "next", "back" -> fetchWorkOrders(peripheral, woId, action) "requ", "next", "back" -> fetchWorkOrders(peripheral, woId, action)