Intergating new protocol

This commit is contained in:
2026-07-18 12:49:48 +07:00
parent 245330d37a
commit 8af98f505f
6 changed files with 161 additions and 98 deletions
@@ -29,7 +29,7 @@ class WorkOrderApi(
suspend fun fetchAbleWorkOrders(woID: String, action: String): List<WorkOrderDto> { suspend fun fetchAbleWorkOrders(woID: String, action: String): List<WorkOrderDto> {
return client.get("$baseUrl/${Endpoints.ABLE_WO}") { return client.get("$baseUrl/${Endpoints.ABLE_WO}") {
parameter("woID", woID) parameter("wo_id", woID)
parameter("action", action) parameter("action", action)
}.body() }.body()
} }
@@ -71,7 +71,7 @@ class BleManager {
} }
suspend fun sendCommand(peripheral: Peripheral, command: String) { suspend fun sendCommand(peripheral: Peripheral, command: String) {
Napier.d(">>>>> Sent to uart: $command") 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()) peripheral.write(RX_CHAR, cmd.encodeToByteArray())
} }
fun observeRx(peripheral: Peripheral): Flow<String> = fun observeRx(peripheral: Peripheral): Flow<String> =
@@ -171,11 +171,12 @@ class ScannerViewModel(
bleManager.scanDevices() bleManager.scanDevices()
.catch { .catch {
Napier.d(">>>>> bleManager.scanDevices catch block")
_isScanning.value = false _isScanning.value = false
stopCleanupJob() stopCleanupJob()
} }
.collect { advertisement -> .collect { advertisement ->
if (advertisement.name == null) return@collect // if (advertisement.name == null) return@collect
val bleId: String = advertisement.identifier.toString() val bleId: String = advertisement.identifier.toString()
if(_autoConnect.value && isBonded(bleId)){ if(_autoConnect.value && isBonded(bleId)){
connect(advertisement) connect(advertisement)
@@ -285,9 +286,10 @@ class ScannerViewModel(
fun sendCommand(peripheral: Peripheral, command: String) { fun sendCommand(peripheral: Peripheral, command: String) {
val deviceId = peripheral.identifier.toString() val deviceId = peripheral.identifier.toString()
viewModelScope.launch { viewModelScope.launch {
bleManager.sendCommand(peripheral, Helper.buildUartCommand(command)) val data = Helper.buildUartCommand(command)
appendHistory(deviceId, "Sent: $command") bleManager.sendCommand(peripheral, data)
logRepository.appendLog(title = "Communicate - Sent", content = command, deviceId) appendHistory(deviceId, "Sent: $data")
logRepository.appendLog(title = "Communicate - Sent", content = data, deviceId)
} }
} }
@@ -361,7 +363,7 @@ class ScannerViewModel(
_connectedDevices.value -= deviceIdentifier _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 { viewModelScope.launch {
try { try {
val orders = scannerRepository.getAbleWorkOrders(woID = woID, action = action) val orders = scannerRepository.getAbleWorkOrders(woID = woID, action = action)
@@ -379,93 +381,106 @@ class ScannerViewModel(
sendCommand(peripheral, Helper.CMD_EMPTY_WO) sendCommand(peripheral, Helper.CMD_EMPTY_WO)
return@launch 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 torque = wo.services?.values?.firstOrNull()?.torqueRequired ?: 0.0
val actionCode = if (wo.status.lowercase() == "open") 0 else 1 val actionCode = if (wo.status.lowercase() == "open") 0 else 1
val wheelsMask = wo.computeWheelsNuts() 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) 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<String> = 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) { private fun handleMessage(peripheral: Peripheral, msg: String, deviceIdentifier: String) {
appendHistory(deviceIdentifier, "Received: $msg") appendHistory(deviceIdentifier, "Received: $msg")
logRepository.appendLog(title = "Communicate - Received", content = msg, deviceId = deviceIdentifier) logRepository.appendLog(title = "Communicate - Received", content = msg, deviceId = deviceIdentifier)
val decoded = Helper.decodeRxData(msg) ?: return val decoded = Helper.decodeRxData(msg) ?: return
val data = decoded.data Napier.d(">>>>> decoded rx message: $decoded")
val woId = decoded.id val cmd = Helper.Command.fromString(decoded.command)
val cmd = data.take(4) val type = Helper.TypeOfDevice.fromString(decoded.type)
val action = if (cmd == "back") "back" else "next" if(type != null) {
viewModelScope.launch { val woId = decoded.woId
val data = decoded.data
when (cmd) { when (cmd) {
"requ", "next", "back" -> fetchWorkOrders(peripheral, woId, action) Helper.Command.START_WO -> fetchWorkOrders(peripheral)
"conf" -> { Helper.Command.NEXT_WO -> fetchWorkOrders(peripheral, woId, "next")
try { Helper.Command.PREVIOUS_WO -> fetchWorkOrders(peripheral, woId, "back")
val res = scannerRepository.confirmWorkOrder(woId, deviceIdentifier, 0) Helper.Command.ACCEPT_WO -> acceptWorkOrder(peripheral, type.code, woId, deviceIdentifier)
var command = Helper.CMD_NOT_AVAILABLE_WO Helper.Command.FINISH_WO -> {
if (res) finishWorkOrder(peripheral, type.code, woId, data, deviceIdentifier)
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<String> = 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")
}
} }
null -> Napier.w("Unknown command: $cmd")
} }
} } else Napier.w("Unknown type: $type")
} }
fun addBondedDevice(id: String, name: String) { fun addBondedDevice(id: String, name: String) {
viewModelScope.launch { viewModelScope.launch {
deviceRepository.saveDevice(id, name) deviceRepository.saveDevice(id, name)
@@ -1,37 +1,78 @@
package com.digitoolsolutions.app.torquevaultkmp.utils package com.digitoolsolutions.app.torquevaultkmp.utils
object Helper { object Helper {
const val CMD_EMPTY_WO = $$"$e*\r\n" const val START_CHARACTER ="$"
const val CMD_RECEIVED_WO = $$"$r*\r\n" const val END_LINE_FEED = "#\n"
const val CMD_NOT_AVAILABLE_WO = $$"$n*\r\n" const val SECURITY_CRC16 = "00cr16"
const val CMD_UPLOADED_WO = $$"$u*\r\n" const val CMD_EMPTY_WO = "START_WO;empty\n$SECURITY_CRC16"
const val END_LINE_FEED = "#\r\n" 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 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 { object TorqueAction {
const val TORQUE = "0" const val TORQUE = "0"
const val RE_TORQUE = "1" const val RE_TORQUE = "1"
const val CANCEL = "2" const val CANCEL = "2"
} }
data class RxMessage( data class RxMessage(
val id: String, val command: String,
val data: 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 { fun buildUartCommand(cmd: String): String {
return "$cmd$END_LINE_FEED" return "$START_CHARACTER$cmd$END_LINE_FEED"
} }
fun decodeRxData(msg: String): RxMessage? { fun decodeRxData(msg: String): RxMessage? {
if (!msg.startsWith("$")) return null /** Check header and footer*/
val payload = msg.drop(1).trimEnd('*', ' ', '\r', '\n') if (!msg.startsWith("$") && !msg.endsWith("#\n")) return null
val parts = payload.split(",", limit = 2) /** Remove $ and trim end */
if (parts.size < 2) return null val payload = msg.drop(1).trimEnd('*', '#',' ', '\r', '\n')
val woID = parts[0] val parts = payload.split(";", limit = 3)
val data = parts[1] if(parts.size < 2) return null
return RxMessage(woID, data) 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<String>, nutsPerWheel: Int): Map<String, List<Double>> { fun parseWheelTorqueData(torqueData: List<String>, nutsPerWheel: Int): Map<String, List<Double>> {
@@ -2,6 +2,7 @@ package com.digitoolsolutions.app.torquevaultkmp
import androidx.compose.ui.window.ComposeUIViewController import androidx.compose.ui.window.ComposeUIViewController
import com.digitoolsolutions.app.torquevaultkmp.di.initKoin import com.digitoolsolutions.app.torquevaultkmp.di.initKoin
import com.juul.kable.CentralManager
import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.DebugAntilog
import io.github.aakira.napier.Napier import io.github.aakira.napier.Napier
@@ -9,6 +10,9 @@ fun MainViewController() = ComposeUIViewController(
configure = { configure = {
// Add logger lib // Add logger lib
Napier.base(DebugAntilog()) // NoOpAntilog() for release Napier.base(DebugAntilog()) // NoOpAntilog() for release
CentralManager.configure {
stateRestoration = true
}
initKoin() initKoin()
} }
) { App() } ) { App() }
+4 -1
View File
@@ -23,7 +23,10 @@
<string>The app requires Bluetooth to connect and manage Torque devices.</string> <string>The app requires Bluetooth to connect and manage Torque devices.</string>
<key>NSBluetoothPeripheralUsageDescription</key> <key>NSBluetoothPeripheralUsageDescription</key>
<string>The app requires Bluetooth to connect and manage Torque devices.</string> <string>The app requires Bluetooth to connect and manage Torque devices.</string>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
</array>
<key>UILaunchScreen</key> <key>UILaunchScreen</key>
<dict/> <dict/>
</dict> </dict>