Auto connect ble, update realtime setting

This commit is contained in:
2026-05-30 14:52:37 +07:00
parent e55fcab158
commit 07611f3ace
9 changed files with 118 additions and 38 deletions
@@ -11,6 +11,9 @@ import kotlinx.coroutines.flow.Flow
class DeviceRepository(
private val dbQueries: AppDatabaseQueries
) {
companion object {
const val PAGE_SIZE = 20L
}
suspend fun saveDevice(id: String, name: String) {
dbQueries.insertDevice(id, name)
}
@@ -20,7 +23,10 @@ class DeviceRepository(
.asFlow()
.mapToList(Dispatchers.IO)
suspend fun getDevicesPaged(limit: Long, lastId: String?): List<Device> {
suspend fun getAllDeviceIdsFlow(): Flow<List<String>> {
return dbQueries.getAllDeviceIds().asFlow().mapToList(Dispatchers.IO)
}
suspend fun getDevicesPaged(limit: Long = PAGE_SIZE, lastId: String? = null): List<Device> {
return if (lastId == null) {
dbQueries.getDevicesFirstPage(limit).executeAsList()
} else {
@@ -1,13 +1,18 @@
package com.digitoolsolutions.app.torquevaultkmp.data.storage
import kotlinx.coroutines.flow.Flow
import kotlin.reflect.KClass
interface AppStorage {
fun <T : Any> save(key: String, value: T)
fun <T : Any> load(key: String, type: KClass<T>): T?
fun remove(key: String)
fun <T : Any> observe(key: String, type: KClass<T>): Flow<T>
}
inline fun <reified T : Any> AppStorage.load(key: String): T? {
return load(key, T::class)
}
inline fun <reified T : Any> AppStorage.observe(key: String): Flow<T> {
return observe(key, T::class)
}
@@ -51,6 +51,9 @@ fun BondedScreen(
val devices by viewModel.bondedDevices.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
LaunchedEffect(Unit) {
viewModel.refresh()
}
BondedContent(
devices = devices,
isLoading = isLoading,
@@ -18,7 +18,7 @@ class BondedViewModel(
val bondedDevices: StateFlow<List<Device>> = _bondedDevices.asStateFlow()
private var lastId: String? = null
private val pageSize = 20L
private val pageSize = DeviceRepository.PAGE_SIZE
private var isLastPage = false
var isLoading = MutableStateFlow(false)
private set
@@ -105,7 +105,6 @@ fun ScannerScreen(
} else {
viewModel.disconnect(deviceId)
}
viewModel.addBondedDevice(deviceId, device.name ?: "No name")
}
)
}
@@ -9,6 +9,7 @@ import com.digitoolsolutions.app.torquevaultkmp.data.repository.ScannerRepositor
import com.digitoolsolutions.app.torquevaultkmp.data.storage.AppStorage
import com.digitoolsolutions.app.torquevaultkmp.data.storage.ReferKeys
import com.digitoolsolutions.app.torquevaultkmp.data.storage.load
import com.digitoolsolutions.app.torquevaultkmp.data.storage.observe
import com.juul.kable.Advertisement
import com.juul.kable.Peripheral
import com.juul.kable.State
@@ -22,10 +23,12 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds
@@ -39,7 +42,9 @@ class ScannerViewModel(
private val deviceRepository: DeviceRepository,
private val scannerRepository: ScannerRepository
) : ViewModel() {
companion object {
private val SCAN_TIMEOUT = 1.seconds
}
private val _devices = MutableStateFlow<List<Advertisement>>(emptyList())
val devices: StateFlow<List<Advertisement>> = _devices.asStateFlow()
@@ -47,10 +52,9 @@ class ScannerViewModel(
val isScanning: StateFlow<Boolean> = _isScanning.asStateFlow()
private val _connectedDevices = MutableStateFlow<Map<String, State>>(emptyMap())
val connectedDevices: StateFlow<Map<String, State>> = _connectedDevices.asStateFlow()
private val _workOrders = MutableStateFlow<List<WorkOrder>>(emptyList())
private val _autoConnect = MutableStateFlow(storage.load<Boolean>(ReferKeys.DEVICE_AUTO_CONNECT) ?: false)
val autoConnect: StateFlow<Boolean> = _autoConnect.asStateFlow()
private val _autoConnect: StateFlow<Boolean> = storage.observe<Boolean>(ReferKeys.DEVICE_AUTO_CONNECT)
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
private val _onlyWithName = MutableStateFlow(true)
private var scanJob: Job? = null
@@ -59,36 +63,21 @@ class ScannerViewModel(
private val advertisementMap = mutableMapOf<String, Pair<Advertisement, TimeSource.Monotonic.ValueTimeMark>>()
companion object {
private val SCAN_TIMEOUT = 1.seconds
}
fun fetchWorkOrders(peripheral: Peripheral, woID: String = "0", action: String = "next") {
/** Bonded device */
private val bondedIds = mutableSetOf<String>() // Cache
suspend fun preloadBondedIds() {
viewModelScope.launch {
try {
val orders = scannerRepository.getAbleWorkOrders(woID = woID, action = action)
_workOrders.value = orders
sendWorkOrdersToDevice(peripheral,orders)
} catch (e: Exception) {
Napier.e("Failed to load work orders", e)
deviceRepository.getAllDeviceIdsFlow().collect { ids ->
bondedIds.clear()
bondedIds.addAll(ids)
}
}
}
fun isBonded(id: String): Boolean = bondedIds.contains(id)
private fun sendWorkOrdersToDevice(peripheral: Peripheral, workOrders: List<WorkOrder>) {
init {
viewModelScope.launch {
if (workOrders.isEmpty()) {
bleManager.sendCommand(peripheral, Helper.buildUartCommand(Helper.CMD_EMPTY_WO))
return@launch
}
val formatted = workOrders.joinToString(separator = "\r\n") { wo ->
val torque = wo.services?.values?.firstOrNull()?.torqueRequired ?: 0.0
val actionCode = if (wo.status.lowercase() == "open") 0 else 1
val wheelsMask = wo.computeWheelsNuts()
"$${wo.id},${actionCode},${wo.make},${wo.licensePlate},$torque,ft-lb,$wheelsMask*"
}
bleManager.sendCommand(peripheral, Helper.buildUartCommand(formatted))
preloadBondedIds()
}
}
@@ -106,11 +95,15 @@ class ScannerViewModel(
}
.collect { advertisement ->
if (_onlyWithName.value && advertisement.name == null) return@collect
val now = TimeSource.Monotonic.markNow()
advertisementMap[advertisement.identifier.toString()] = advertisement to now
startCleanupJob()
updateDeviceList()
val bleId: String = advertisement.identifier.toString()
if(_autoConnect.value && isBonded(bleId)){
connect(advertisement)
} else {
val now = TimeSource.Monotonic.markNow()
advertisementMap[advertisement.identifier.toString()] = advertisement to now
startCleanupJob()
updateDeviceList()
}
}
}
}
@@ -165,6 +158,8 @@ class ScannerViewModel(
launch {
peripheral.state.collect { state ->
_connectedDevices.value += (deviceIdentifier to state)
/** Add bonded device */
addBondedDevice(deviceIdentifier, adv.name ?: "No name")
if (state is State.Disconnected) {
this@coroutineScope.cancel(">>>>> Device disconnected")
}
@@ -191,7 +186,6 @@ class ScannerViewModel(
}
connectionJobs[deviceIdentifier] = job
}
fun disconnect(deviceIdentifier: String) {
viewModelScope.launch {
bleManager.disconnect(deviceIdentifier)
@@ -200,7 +194,32 @@ class ScannerViewModel(
_connectedDevices.value -= deviceIdentifier
}
}
fun fetchWorkOrders(peripheral: Peripheral, woID: String = "0", action: String = "next") {
viewModelScope.launch {
try {
val orders = scannerRepository.getAbleWorkOrders(woID = woID, action = action)
_workOrders.value = orders
sendWorkOrdersToDevice(peripheral,orders)
} catch (e: Exception) {
Napier.e("Failed to load work orders", e)
}
}
}
private fun sendWorkOrdersToDevice(peripheral: Peripheral, workOrders: List<WorkOrder>) {
viewModelScope.launch {
if (workOrders.isEmpty()) {
bleManager.sendCommand(peripheral, Helper.buildUartCommand(Helper.CMD_EMPTY_WO))
return@launch
}
val formatted = workOrders.joinToString(separator = "\r\n") { wo ->
val torque = wo.services?.values?.firstOrNull()?.torqueRequired ?: 0.0
val actionCode = if (wo.status.lowercase() == "open") 0 else 1
val wheelsMask = wo.computeWheelsNuts()
"$${wo.id},${actionCode},${wo.make},${wo.licensePlate},$torque,ft-lb,$wheelsMask*"
}
bleManager.sendCommand(peripheral, Helper.buildUartCommand(formatted))
}
}
private fun handleMessage(peripheral: Peripheral, msg: String, deviceIdentifier: String) {
val decoded = Helper.decodeRxData(msg) ?: return
val data = decoded.data
@@ -24,6 +24,9 @@ VALUES (?, ?);
getAllDevices:
SELECT * FROM Device;
getAllDeviceIds:
SELECT id FROM Device;
getDevicesFirstPage:
SELECT * FROM Device ORDER BY id ASC LIMIT :limit;