Auto connect ble, update realtime setting
This commit is contained in:
+23
@@ -1,9 +1,14 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.AppStorage
|
||||
import kotlin.reflect.KClass
|
||||
import androidx.core.content.edit
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
|
||||
class AppStoragePlatform(context: Context) : AppStorage {
|
||||
private val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||
@@ -30,4 +35,22 @@ class AppStoragePlatform(context: Context) : AppStorage {
|
||||
override fun remove(key: String) {
|
||||
prefs.edit { remove(key) }
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : Any> observe(key: String, type: KClass<T>): Flow<T> = callbackFlow {
|
||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, changedKey ->
|
||||
if (changedKey == key) {
|
||||
val value: T? = when (type) {
|
||||
String::class -> prefs.getString(key, null) as T?
|
||||
Int::class -> prefs.getInt(key, -1) as T?
|
||||
Boolean::class -> prefs.getBoolean(key, false) as T?
|
||||
else -> throw IllegalArgumentException("Unsupported type")
|
||||
}
|
||||
value?.let { trySend(it) }
|
||||
}
|
||||
}
|
||||
prefs.registerOnSharedPreferenceChangeListener(listener)
|
||||
load(key, type)?.let { trySend(it) }
|
||||
awaitClose { prefs.unregisterOnSharedPreferenceChangeListener(listener) }
|
||||
}.filterNotNull()
|
||||
}
|
||||
+7
-1
@@ -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 {
|
||||
|
||||
+5
@@ -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)
|
||||
}
|
||||
+3
@@ -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,
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
-1
@@ -105,7 +105,6 @@ fun ScannerScreen(
|
||||
} else {
|
||||
viewModel.disconnect(deviceId)
|
||||
}
|
||||
viewModel.addBondedDevice(deviceId, device.name ?: "No name")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+50
-31
@@ -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,7 +95,10 @@ class ScannerViewModel(
|
||||
}
|
||||
.collect { advertisement ->
|
||||
if (_onlyWithName.value && advertisement.name == null) return@collect
|
||||
|
||||
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()
|
||||
@@ -114,6 +106,7 @@ class ScannerViewModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startCleanupJob() {
|
||||
if (cleanupJob?.isActive == true) return
|
||||
@@ -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
|
||||
|
||||
+3
@@ -24,6 +24,9 @@ VALUES (?, ?);
|
||||
getAllDevices:
|
||||
SELECT * FROM Device;
|
||||
|
||||
getAllDeviceIds:
|
||||
SELECT id FROM Device;
|
||||
|
||||
getDevicesFirstPage:
|
||||
SELECT * FROM Device ORDER BY id ASC LIMIT :limit;
|
||||
|
||||
|
||||
+22
@@ -1,7 +1,14 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp
|
||||
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.AppStorage
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import platform.Foundation.NSNotificationCenter
|
||||
import platform.Foundation.NSUserDefaults
|
||||
import platform.Foundation.NSUserDefaultsDidChangeNotification
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class AppStoragePlatform : AppStorage {
|
||||
@@ -28,4 +35,19 @@ class AppStoragePlatform : AppStorage {
|
||||
override fun remove(key: String) {
|
||||
defaults.removeObjectForKey(key)
|
||||
}
|
||||
|
||||
override fun <T : Any> observe(key: String, type: KClass<T>): Flow<T> = callbackFlow {
|
||||
val observer = NSNotificationCenter.defaultCenter.addObserverForName(
|
||||
name = NSUserDefaultsDidChangeNotification,
|
||||
`object` = null,
|
||||
queue = null
|
||||
) { _ ->
|
||||
load(key, type)?.let { trySend(it) }
|
||||
}
|
||||
// emit initial value
|
||||
load(key, type)?.let { trySend(it) }
|
||||
awaitClose {
|
||||
NSNotificationCenter.defaultCenter.removeObserver(observer)
|
||||
}
|
||||
}.filterNotNull().map { it }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user