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(
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
dbQueries.insertLog(
title = title,
@@ -63,5 +63,5 @@ val viewModelModule = module {
factoryOf(::SettingViewModel)
factoryOf(::BondedViewModel)
factoryOf(::LogViewModel)
single { ScannerViewModel(get(), get(), get(), get()) }
single { ScannerViewModel(get(), get(), get(), get(), get()) }
}
@@ -130,7 +130,7 @@ fun MainScreen(
HomeScreen(navController)
}
composable(BondedDestination.route) {
BondedScreen(navController)
BondedScreen()
}
composable(ScannerDestination.route) {
ScannerScreen(onDeviceClick = { device ->
@@ -144,7 +144,7 @@ fun MainScreen(
val deviceId = backStackEntry.savedStateHandle.get<String>("deviceId") ?: ""
UartCommunicationScreen(navController, deviceId)
}
composable(LogDestination.route) { LogScreen(navController) }
composable(LogDestination.route) { LogScreen() }
composable(SettingDestination.route) {
SettingScreen(navController, isDarkTheme, onThemeToggle)
}
@@ -45,7 +45,6 @@ import torquevaultkmp.composeapp.generated.resources.bonded_title
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BondedScreen(
navController: NavController,
viewModel: BondedViewModel = koinViewModel()
) {
val devices by viewModel.bondedDevices.collectAsState()
@@ -57,7 +56,6 @@ fun BondedScreen(
BondedContent(
devices = devices,
isLoading = isLoading,
onAddFake = { viewModel.addFakeDevice() },
onDelete = { viewModel.removeDevice(it.id) },
onLoadMore = { viewModel.loadNextPage() },
onRefresh = { viewModel.refresh() }
@@ -69,7 +67,6 @@ fun BondedScreen(
internal fun BondedContent(
devices: List<Device>,
isLoading: Boolean,
onAddFake: () -> Unit,
onDelete: (Device) -> Unit,
onLoadMore: () -> Unit,
onRefresh: () -> Unit
@@ -101,11 +98,11 @@ internal fun BondedContent(
title = {Text(stringResource(Res.string.bonded_title))},
)
},
floatingActionButton = {
FloatingActionButton(onClick = onAddFake) {
Icon(Icons.Default.Add, contentDescription = "Add Fake Device")
}
},
// floatingActionButton = {
// FloatingActionButton(onClick = {}) {
// Icon(Icons.Default.Add, contentDescription = "Add")
// }
// },
snackbarHost = { SnackbarHost(snackbarHostState) })
{ padding ->
val pullToRefreshState = rememberPullToRefreshState()
@@ -55,16 +55,6 @@ class BondedViewModel(
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) {
viewModelScope.launch {
deviceRepository.deleteDevice(id)
@@ -9,9 +9,9 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
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.outlined.FilterList
import androidx.compose.material.icons.rounded.KeyboardArrowUp
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -32,13 +32,13 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.digitoolsolutions.app.torquevaultkmp.components.AppBar
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.Log
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 kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
import torquevaultkmp.composeapp.generated.resources.Res
@@ -55,19 +56,20 @@ import torquevaultkmp.composeapp.generated.resources.log_title
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LogScreen(
navController: NavController,
viewModel: LogViewModel = koinViewModel()
) {
val logs by viewModel.logs.collectAsState()
val activeFilter by viewModel.activeFilter.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
LaunchedEffect(Unit) {
viewModel.refresh()
}
LogContent(
logs = logs,
activeFilter = activeFilter,
isLoading = isLoading,
onFilterSelected = { viewModel.setFilter(it) },
onAddFakeLog = { viewModel.addFakeLog() },
onDeleteAllLogs = { viewModel.clearAllLogs() },
onLoadMore = { viewModel.loadNextPage() },
onRefresh = { viewModel.refresh() }
@@ -81,7 +83,6 @@ internal fun LogContent(
activeFilter: LogFilter,
isLoading: Boolean,
onFilterSelected: (LogFilter) -> Unit,
onAddFakeLog: () -> Unit,
onDeleteAllLogs: () -> Unit,
onLoadMore: () -> Unit,
onRefresh: () -> Unit
@@ -91,6 +92,7 @@ internal fun LogContent(
var showBottomSheet by remember { mutableStateOf(false) }
val listState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
// Infinite Scroll logic
val shouldLoadMore = remember {
@@ -100,6 +102,12 @@ internal fun LogContent(
}
}
LaunchedEffect(logs) {
if (logs.isNotEmpty()) {
listState.scrollToItem(0)
}
}
LaunchedEffect(shouldLoadMore) {
snapshotFlow { shouldLoadMore.value }
.distinctUntilChanged()
@@ -133,8 +141,13 @@ internal fun LogContent(
)
},
floatingActionButton = {
FloatingActionButton(onClick = onAddFakeLog) {
Icon(Icons.Default.Add, contentDescription = "Add Fake Log")
FloatingActionButton(onClick = {
onRefresh()
coroutineScope.launch {
listState.animateScrollToItem(0)
}
}) {
Icon(Icons.Rounded.KeyboardArrowUp, contentDescription = "Go to top")
}
},
snackbarHost = { SnackbarHost(snackbarHostState) })
@@ -57,7 +57,7 @@ class LogViewModel(
val lastLog = newLogs.last()
lastTimestamp = lastLog.timestamp
lastId = lastLog.id
_logs.value = _logs.value + newLogs
_logs.value += newLogs
}
isLoading.value = false
@@ -73,19 +73,6 @@ class LogViewModel(
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() {
viewModelScope.launch {
logRepository.clearLogs()
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.SendMeasureDto
import com.digitoolsolutions.app.torquevaultkmp.kable.BleManager
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.storage.AppStorage
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.computeWheelsNuts
import com.digitoolsolutions.app.torquevaultkmp.utils.Helper
import com.juul.kable.ExperimentalApi
import io.github.aakira.napier.Napier
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@@ -41,8 +43,9 @@ import kotlin.uuid.ExperimentalUuidApi
@OptIn(ExperimentalUuidApi::class)
class ScannerViewModel(
private val bleManager: BleManager,
private val storage: AppStorage,
storage: AppStorage,
private val deviceRepository: DeviceRepository,
private val logRepository: LogRepository,
private val scannerRepository: ScannerRepository
) : ViewModel() {
companion object {
@@ -118,6 +121,7 @@ class ScannerViewModel(
}
}
}
logRepository.addLog(title="Scanner status", content="Scanning ...")
}
private fun startCleanupJob() {
@@ -181,17 +185,19 @@ class ScannerViewModel(
sendCommand(peripheral, command)
}
}
fun sendCommand(peripheral: Peripheral, command: String) {
viewModelScope.launch {
bleManager.sendCommand(peripheral, Helper.buildUartCommand(command))
_rxMessages.value += "Sent: $command"
logRepository.addLog(title = "Communicate - Sent", content = command, peripheral.identifier.toString())
}
}
fun connect(adv: Advertisement) {
val deviceIdentifier = adv.identifier.toString()
if (connectionJobs[deviceIdentifier]?.isActive == true) return
logRepository.addLog(title = "Connection - Status", deviceId = deviceIdentifier ,content = "Device [${adv.name}] is connecting...")
val job = viewModelScope.launch {
try {
val peripheral = bleManager.connect(adv)
@@ -206,6 +212,7 @@ class ScannerViewModel(
/** Add bonded device */
addBondedDevice(deviceIdentifier, adv.name ?: "No name")
_connectionReasons.value -= deviceIdentifier
logRepository.addLog(title = "Connection - Status", deviceId = deviceIdentifier ,content = "Device [${adv.name}] has been connected.")
}
if (state is State.Disconnected && hasEmitted) {
if (_connectionReasons.value[deviceIdentifier] == null) {
@@ -233,8 +240,10 @@ class ScannerViewModel(
if (e is TimeoutCancellationException) {
Napier.e(">>>>> Connection timed out for $deviceIdentifier")
_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) {
Napier.e(">>>>> Connection failed for $deviceIdentifier", e)
logRepository.addLog(title = "Connection - Status", content = "Connect to device [${adv.name}] failed. ${e.message}")
}
} finally {
_connectedDevices.value -= deviceIdentifier
@@ -284,13 +293,12 @@ class ScannerViewModel(
}
private fun handleMessage(peripheral: Peripheral, msg: String, deviceIdentifier: String) {
_rxMessages.value += "Received: $msg"
logRepository.addLog(title = "Communicate - Received", content = msg, deviceId = deviceIdentifier)
val decoded = Helper.decodeRxData(msg) ?: return
val data = decoded.data
val woId = decoded.id
val cmd = data.take(4)
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 {
when (cmd) {
"requ", "next", "back" -> fetchWorkOrders(peripheral, woId, action)