From 8af98f505fe5ca4ca7b4b2fc86e8e1f2f5f8d048 Mon Sep 17 00:00:00 2001 From: lamtruong28 Date: Sat, 18 Jul 2026 12:49:48 +0700 Subject: [PATCH] Intergating new protocol --- .../data/network/api/WorkOrderApi.kt | 2 +- .../app/torquevaultkmp/kable/BleManager.kt | 2 +- .../screens/scanner/ScannerViewModel.kt | 169 ++++++++++-------- .../app/torquevaultkmp/utils/Helper.kt | 77 ++++++-- .../app/torquevaultkmp/MainViewController.kt | 4 + iosApp/iosApp/Info.plist | 5 +- 6 files changed, 161 insertions(+), 98 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/api/WorkOrderApi.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/api/WorkOrderApi.kt index 9f3ea46..3314b7e 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/api/WorkOrderApi.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/api/WorkOrderApi.kt @@ -29,7 +29,7 @@ class WorkOrderApi( suspend fun fetchAbleWorkOrders(woID: String, action: String): List { return client.get("$baseUrl/${Endpoints.ABLE_WO}") { - parameter("woID", woID) + parameter("wo_id", woID) parameter("action", action) }.body() } diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/kable/BleManager.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/kable/BleManager.kt index 92a8dee..8f7a186 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/kable/BleManager.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/kable/BleManager.kt @@ -71,7 +71,7 @@ class BleManager { } suspend fun sendCommand(peripheral: Peripheral, command: String) { Napier.d(">>>>> Sent to uart: $command") - val cmd = command.replace("\\r\\n", "\r\n") + val cmd = command.replace("\\n", "\n") peripheral.write(RX_CHAR, cmd.encodeToByteArray()) } fun observeRx(peripheral: Peripheral): Flow = diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/scanner/ScannerViewModel.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/scanner/ScannerViewModel.kt index 4a83576..efce7d5 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/scanner/ScannerViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/scanner/ScannerViewModel.kt @@ -171,11 +171,12 @@ class ScannerViewModel( bleManager.scanDevices() .catch { + Napier.d(">>>>> bleManager.scanDevices catch block") _isScanning.value = false stopCleanupJob() } .collect { advertisement -> - if (advertisement.name == null) return@collect +// if (advertisement.name == null) return@collect val bleId: String = advertisement.identifier.toString() if(_autoConnect.value && isBonded(bleId)){ connect(advertisement) @@ -285,9 +286,10 @@ class ScannerViewModel( fun sendCommand(peripheral: Peripheral, command: String) { val deviceId = peripheral.identifier.toString() viewModelScope.launch { - bleManager.sendCommand(peripheral, Helper.buildUartCommand(command)) - appendHistory(deviceId, "Sent: $command") - logRepository.appendLog(title = "Communicate - Sent", content = command, deviceId) + val data = Helper.buildUartCommand(command) + bleManager.sendCommand(peripheral, data) + appendHistory(deviceId, "Sent: $data") + logRepository.appendLog(title = "Communicate - Sent", content = data, deviceId) } } @@ -361,7 +363,7 @@ class ScannerViewModel( _connectedDevices.value -= deviceIdentifier } } - fun fetchWorkOrders(peripheral: Peripheral, woID: String = "0", action: String = "next") { + private fun fetchWorkOrders(peripheral: Peripheral, woID: String = "0", action: String = "next") { viewModelScope.launch { try { val orders = scannerRepository.getAbleWorkOrders(woID = woID, action = action) @@ -379,93 +381,106 @@ class ScannerViewModel( sendCommand(peripheral, Helper.CMD_EMPTY_WO) return@launch } - val formatted = workOrders.joinToString(separator = "\r\n") { wo -> + val formatted = workOrders.joinToString(separator = "\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*" + "${Helper.WO_RES};${wo.id};${actionCode};${wo.make};${wo.licensePlate};$torque;ft-lb;$wheelsMask" } - sendCommand(peripheral, formatted) } } + private fun acceptWorkOrder(peripheral: Peripheral, type: Int, woId: String, deviceIdentifier: String) { + viewModelScope.launch { + var command = Helper.CMD_RES_OK(Helper.Command.ACCEPT_WO.name) + try { + val res = scannerRepository.confirmWorkOrder(woId, deviceIdentifier, type) + if (res) + command = Helper.CMD_RES_OK(Helper.Command.ACCEPT_WO.name) + sendCommand(peripheral, command) + } catch (e: Exception) { + Napier.e(">>>>> Failed to handle message data", e) + sendCommand(peripheral, command) + logRepository.appendLog( + title = "HTTP", + content = "${e.message}".substringBefore("[url=") + ) + } + } + } + private fun finishWorkOrder(peripheral: Peripheral, type: Int, woId: String, data: String, deviceIdentifier: String) { + val bleData = data.trim(' ').split(";") + val nuts: Int = bleData[Helper.BLE_NUTS_POS].toInt() + val torqueData: List = bleData.subList(Helper.BLE_TORQUE_POS, bleData.size) + val wheelTorqueData = Helper.parseWheelTorqueData(torqueData, nuts) + val dto = SendMeasureDto( + deviceId = deviceIdentifier, + type = type, + nuts = nuts, + torqueData = wheelTorqueData + ) + Napier.d(">>>>> DTO $dto") + viewModelScope.launch { + when (val act = bleData.firstOrNull()) { + Helper.TorqueAction.TORQUE -> { + try { + scannerRepository.sendMeasurementResult(woId, dto) + } catch (e: Exception) { + Napier.e("An error occurred while sending the measurement", e, tag = "ScannerViewModel") + logRepository.appendLog(title = "HTTP", content = "${e.message}".substringBefore("[url=")) + } finally { + // Currently, always send UPLOADED_WO regardless of success or failure + sendCommand(peripheral, Helper.CMD_RES_OK(Helper.Command.FINISH_WO.name)) + } + } + Helper.TorqueAction.RE_TORQUE -> { + try { + scannerRepository.sendMeasurementResult(woId, dto, true) + } catch (e: Exception) { + Napier.e("An error occurred while sending the re-measurement", e, tag = "ScannerViewModel") + logRepository.appendLog(title = "HTTP", content = "${e.message}".substringBefore("[url=")) + } finally { + // Currently, always send UPLOADED_WO regardless of success or failure + sendCommand(peripheral, Helper.CMD_RES_OK(Helper.Command.FINISH_WO.name)) + } + } + Helper.TorqueAction.CANCEL -> { + try { + scannerRepository.sendCancelWorkOrder(woId, dto) + } catch (e: Exception) { + Napier.e("An error occurred while cancel work order", e, tag = "ScannerViewModel") + logRepository.appendLog(title = "HTTP", content = "${e.message}".substringBefore("[url=")) + } finally { + // Currently, always send UPLOADED_WO regardless of success or failure + sendCommand(peripheral, Helper.CMD_RES_OK(Helper.Command.FINISH_WO.name)) + } + } + else -> Napier.w(">>>>> Unknown action: $act") + } + } + } private fun handleMessage(peripheral: Peripheral, msg: String, deviceIdentifier: String) { appendHistory(deviceIdentifier, "Received: $msg") logRepository.appendLog(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" - viewModelScope.launch { + Napier.d(">>>>> decoded rx message: $decoded") + val cmd = Helper.Command.fromString(decoded.command) + val type = Helper.TypeOfDevice.fromString(decoded.type) + if(type != null) { + val woId = decoded.woId + val data = decoded.data when (cmd) { - "requ", "next", "back" -> fetchWorkOrders(peripheral, woId, action) - "conf" -> { - try { - val res = scannerRepository.confirmWorkOrder(woId, deviceIdentifier, 0) - var command = Helper.CMD_NOT_AVAILABLE_WO - if (res) - command = Helper.CMD_RECEIVED_WO - sendCommand(peripheral, command) - } catch (e: Exception) { - Napier.e(">>>>> Failed to handle message: $msg", e) - sendCommand(peripheral, Helper.CMD_NOT_AVAILABLE_WO) - logRepository.appendLog(title = "HTTP", content = "${e.message}".substringBefore("[url=")) - } - } - else -> { - /** Send torque, re-torque or cancel case */ - val bleData = data.trim(' ').trimEnd('*').split(",") - val nuts: Int = bleData[Helper.BLE_NUTS_POS].toInt() - val torqueData: List = bleData.subList(Helper.BLE_TORQUE_POS, bleData.size) - val wheelTorqueData = Helper.parseWheelTorqueData(torqueData, nuts) - val dto = SendMeasureDto( - deviceId = deviceIdentifier, - type = 0, // tbu - nuts = nuts, - torqueData = wheelTorqueData - ) - when (val act = bleData.firstOrNull()) { - Helper.TorqueAction.TORQUE -> { - try { - scannerRepository.sendMeasurementResult(woId, dto) - } catch (e: Exception) { - Napier.e("An error occurred while sending the measurement", e, tag = "ScannerViewModel") - logRepository.appendLog(title = "HTTP", content = "${e.message}".substringBefore("[url=")) - } finally { - // Currently, always send UPLOADED_WO regardless of success or failure - sendCommand(peripheral, Helper.CMD_UPLOADED_WO) - } - } - Helper.TorqueAction.RE_TORQUE -> { - try { - scannerRepository.sendMeasurementResult(woId, dto, true) - } catch (e: Exception) { - Napier.e("An error occurred while sending the re-measurement", e, tag = "ScannerViewModel") - logRepository.appendLog(title = "HTTP", content = "${e.message}".substringBefore("[url=")) - } finally { - // Currently, always send UPLOADED_WO regardless of success or failure - sendCommand(peripheral, Helper.CMD_UPLOADED_WO) - } - } - Helper.TorqueAction.CANCEL -> { - try { - scannerRepository.sendCancelWorkOrder(woId, dto) - } catch (e: Exception) { - Napier.e("An error occurred while cancel work order", e, tag = "ScannerViewModel") - logRepository.appendLog(title = "HTTP", content = "${e.message}".substringBefore("[url=")) - } finally { - // Currently, always send UPLOADED_WO regardless of success or failure - sendCommand(peripheral, Helper.CMD_UPLOADED_WO) - } - } - else -> Napier.w(">>>>> Unknown action: $act") - } + Helper.Command.START_WO -> fetchWorkOrders(peripheral) + Helper.Command.NEXT_WO -> fetchWorkOrders(peripheral, woId, "next") + Helper.Command.PREVIOUS_WO -> fetchWorkOrders(peripheral, woId, "back") + Helper.Command.ACCEPT_WO -> acceptWorkOrder(peripheral, type.code, woId, deviceIdentifier) + Helper.Command.FINISH_WO -> { + finishWorkOrder(peripheral, type.code, woId, data, deviceIdentifier) } + null -> Napier.w("Unknown command: $cmd") } - } + } else Napier.w("Unknown type: $type") } - fun addBondedDevice(id: String, name: String) { viewModelScope.launch { deviceRepository.saveDevice(id, name) diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/Helper.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/Helper.kt index 5ab624a..27b7c41 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/Helper.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/Helper.kt @@ -1,37 +1,78 @@ package com.digitoolsolutions.app.torquevaultkmp.utils object Helper { - const val CMD_EMPTY_WO = $$"$e*\r\n" - const val CMD_RECEIVED_WO = $$"$r*\r\n" - const val CMD_NOT_AVAILABLE_WO = $$"$n*\r\n" - const val CMD_UPLOADED_WO = $$"$u*\r\n" - const val END_LINE_FEED = "#\r\n" + const val START_CHARACTER ="$" + const val END_LINE_FEED = "#\n" + const val SECURITY_CRC16 = "00cr16" + const val CMD_EMPTY_WO = "START_WO;empty\n$SECURITY_CRC16" + const val WO_RES = "WO_RES" + val CMD_RES_OK: (String) -> String = { cmd -> "$cmd;OK\n$SECURITY_CRC16" } + val CMD_RES_NG: (String) -> String = { cmd -> "$cmd;NG\n$SECURITY_CRC16" } - const val BLE_NUTS_POS = 4 + const val BLE_NUTS_POS = 2 const val BLE_TORQUE_POS = BLE_NUTS_POS + 1 + enum class Command { + START_WO, + NEXT_WO, + PREVIOUS_WO, + ACCEPT_WO, + FINISH_WO; + companion object { + fun fromString(code: String): Command? = + try { valueOf(code) } catch (e: IllegalArgumentException) { null } + } + } + enum class TypeOfDevice(val code: Int) { + WHEEL(0), + OIL_FILTER(1), + DRAIN_PLUG(2); + companion object { + fun fromString(type: String): TypeOfDevice? = when (type) { + "wheel" -> WHEEL + "oil_filter" -> OIL_FILTER + "drain_plug" -> DRAIN_PLUG + else -> null + } + } + } object TorqueAction { const val TORQUE = "0" const val RE_TORQUE = "1" const val CANCEL = "2" } data class RxMessage( - val id: String, - val data: String + val command: String, + val type: String, // Type of device + val woId: String = "0", + val data: String, + val securityCode: String, + val crc16: String ) - fun buildUartCommand(cmd: String): String { - return "$cmd$END_LINE_FEED" + return "$START_CHARACTER$cmd$END_LINE_FEED" } - fun decodeRxData(msg: String): RxMessage? { - if (!msg.startsWith("$")) return null - val payload = msg.drop(1).trimEnd('*', ' ', '\r', '\n') - val parts = payload.split(",", limit = 2) - if (parts.size < 2) return null - val woID = parts[0] - val data = parts[1] - return RxMessage(woID, data) + /** Check header and footer*/ + if (!msg.startsWith("$") && !msg.endsWith("#\n")) return null + /** Remove $ and trim end */ + val payload = msg.drop(1).trimEnd('*', '#',' ', '\r', '\n') + val parts = payload.split(";", limit = 3) + if(parts.size < 2) return null + val cmd = parts.first() + var woId = "0" + var data = "" + val type = parts[1].substringBefore("\n") + /** Tail include security code and crc16 */ + val tail = parts.last().substringAfterLast("\n") + val security = tail.take(2) + val crc16 = tail.drop(2).take(4) + if(parts.size == 3) { + val payloadParts = parts.last().substringBeforeLast("\n").split(";") + woId = payloadParts.firstOrNull() ?: "0" + data = payloadParts.drop(1).joinToString(";") // exclude woId + } + return RxMessage(cmd, type, woId, data, security, crc16) } fun parseWheelTorqueData(torqueData: List, nutsPerWheel: Int): Map> { diff --git a/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/MainViewController.kt b/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/MainViewController.kt index 86e90fe..1cc120e 100644 --- a/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/MainViewController.kt +++ b/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/MainViewController.kt @@ -2,6 +2,7 @@ package com.digitoolsolutions.app.torquevaultkmp import androidx.compose.ui.window.ComposeUIViewController import com.digitoolsolutions.app.torquevaultkmp.di.initKoin +import com.juul.kable.CentralManager import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.Napier @@ -9,6 +10,9 @@ fun MainViewController() = ComposeUIViewController( configure = { // Add logger lib Napier.base(DebugAntilog()) // NoOpAntilog() for release + CentralManager.configure { + stateRestoration = true + } initKoin() } ) { App() } \ No newline at end of file diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist index dcbf3fa..09f480f 100644 --- a/iosApp/iosApp/Info.plist +++ b/iosApp/iosApp/Info.plist @@ -23,7 +23,10 @@ The app requires Bluetooth to connect and manage Torque devices. NSBluetoothPeripheralUsageDescription The app requires Bluetooth to connect and manage Torque devices. - + UIBackgroundModes + + bluetooth-central + UILaunchScreen