diff --git a/.maestro/config.yaml b/.maestro/config.yaml new file mode 100644 index 0000000..c584061 --- /dev/null +++ b/.maestro/config.yaml @@ -0,0 +1,7 @@ +flows: + - flows/* + +executionOrder: + continueOnFailure: false + flowsOrder: + - login \ No newline at end of file diff --git a/.maestro/flows/full-flows.yaml b/.maestro/flows/full-flows.yaml new file mode 100644 index 0000000..dfa1766 --- /dev/null +++ b/.maestro/flows/full-flows.yaml @@ -0,0 +1,10 @@ +appId: com.digitoolsolutions.app.torquevaultkmp +--- +- runFlow: + file: ../subflows/launch-app.yaml + +- runFlow: + file: ../subflows/login-steps.yaml + +- runFlow: + file: ../subflows/communicate-steps.yaml \ No newline at end of file diff --git a/.maestro/flows/login.yaml b/.maestro/flows/login.yaml new file mode 100644 index 0000000..b2fcdce --- /dev/null +++ b/.maestro/flows/login.yaml @@ -0,0 +1,6 @@ +appId: com.digitoolsolutions.app.torquevaultkmp +--- +- runFlow: + file: ../subflows/launch-app.yaml +- runFlow: + file: ../subflows/login-steps.yaml \ No newline at end of file diff --git a/.maestro/flows/scanner.yaml b/.maestro/flows/scanner.yaml new file mode 100644 index 0000000..4a6c849 --- /dev/null +++ b/.maestro/flows/scanner.yaml @@ -0,0 +1,6 @@ +appId: com.digitoolsolutions.app.torquevaultkmp +--- +#- runFlow: +# file: ../subflows/launch-app.yaml +- runFlow: + file: ../subflows/communicate-steps.yaml \ No newline at end of file diff --git a/.maestro/subflows/communicate-steps.yaml b/.maestro/subflows/communicate-steps.yaml new file mode 100644 index 0000000..db27037 --- /dev/null +++ b/.maestro/subflows/communicate-steps.yaml @@ -0,0 +1,32 @@ +appId: com.digitoolsolutions.app.torquevaultkmp +--- +- tapOn: "Scanner" +- extendedWaitUntil: + visible: + text: "3006" + timeout: 30000 # 30s +- tapOn: + text: "3006" + delay: 1500 +- waitForAnimationToEnd: + timeout: 1000 + +# In case of empty work order +# --------------------------- +#- tapOn: +# text: "Back" +# delay: 500 +# +#- assertVisible: "Scanner" +#- waitForAnimationToEnd: +# timeout: 1000 +#- extendedWaitUntil: +# visible: +# text: "3006" +# timeout: 30000 # 30s + +# In case of work orders +# -------------------------- +- tapOn: "Manual Command" +- inputText: "ACCEPT_WO;NG" +- tapOn: "Send" \ No newline at end of file diff --git a/.maestro/subflows/grant-permission.yaml b/.maestro/subflows/grant-permission.yaml new file mode 100644 index 0000000..aa8ff7e --- /dev/null +++ b/.maestro/subflows/grant-permission.yaml @@ -0,0 +1,16 @@ +appId: com.digitoolsolutions.app.torquevaultkmp +--- +- runFlow: + file: ../subflows/launch-app.yaml + +- runFlow: + when: + visible: "Enable" + commands: + - tapOn: "Enable" + +- runFlow: + when: + visible: "Cho phép" + commands: + - tapOn: "Cho phép" \ No newline at end of file diff --git a/.maestro/subflows/launch-app.yaml b/.maestro/subflows/launch-app.yaml new file mode 100644 index 0000000..c1dae5a --- /dev/null +++ b/.maestro/subflows/launch-app.yaml @@ -0,0 +1,6 @@ +appId: com.digitoolsolutions.app.torquevaultkmp +--- +- launchApp: + clearState: true +- waitForAnimationToEnd: + timeout: 3000 \ No newline at end of file diff --git a/.maestro/subflows/login-steps.yaml b/.maestro/subflows/login-steps.yaml new file mode 100644 index 0000000..82260e0 --- /dev/null +++ b/.maestro/subflows/login-steps.yaml @@ -0,0 +1,24 @@ +appId: com.digitoolsolutions.app.torquevaultkmp +--- +#- longPressOn: "Username" +#- tapOn: +# text: "Select All" +# optional: true +#- tapOn: +# text: "Chọn tất cả" +# optional: true +#- eraseText: 1 +#- inputText: "admin" +#- tapOn: "App Logo" +#- longPressOn: "Password" +#- tapOn: +# text: "Select All" +# optional: true +#- tapOn: +# text: "Chọn tất cả" +# optional: true +#- eraseText: 1 +#- inputText: "admin" +#- tapOn: "App Logo" +- tapOn: "Sign In" +- assertVisible: "Home" \ No newline at end of file diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index b39d011..b1b3930 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -89,7 +89,7 @@ android { applicationId = "com.digitoolsolutions.app.torquevaultkmp" minSdk = libs.versions.android.minSdk.get().toInt() targetSdk = libs.versions.android.targetSdk.get().toInt() - versionCode = 2 + versionCode = 3 versionName = "1.0.2" } packaging { 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 73e34b9..f226deb 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 @@ -253,6 +253,9 @@ class ScannerViewModel( val adv = bleManager.getAdvertisement(deviceId) if (adv != null) { connect(adv) + // Remove advertisement from list + advertisementMap.remove(deviceId) + updateDeviceList() } else { Napier.e("Cannot connect: Advertisement not found for $deviceId") } @@ -369,22 +372,22 @@ class ScannerViewModel( _connectedDevices.value -= deviceIdentifier } } - private fun fetchWorkOrders(peripheral: Peripheral, woID: String = "0", action: String = "next") { + private fun fetchWorkOrders(peripheral: Peripheral, command: Helper.Command, woID: String = "0", action: String = "next") { viewModelScope.launch { try { val orders = scannerRepository.getAbleWorkOrders(woID = woID, action = action) _workOrders.value = orders - sendWorkOrdersToDevice(peripheral,orders) + sendWorkOrdersToDevice(peripheral, command.name, orders) } catch (e: Exception) { Napier.e("Failed to load work orders", e) logRepository.appendLog(title = "HTTP", content = "${e.message}".substringBefore("[url=")) } } } - private fun sendWorkOrdersToDevice(peripheral: Peripheral, workOrders: List) { + private fun sendWorkOrdersToDevice(peripheral: Peripheral, command: String, workOrders: List) { viewModelScope.launch { if (workOrders.isEmpty()) { - sendCommand(peripheral, Helper.CMD_EMPTY_WO) + sendCommand(peripheral, Helper.CMD_EMPTY_WO(command)) return@launch } val formatted = workOrders.joinToString(separator = "\n") { wo -> @@ -398,7 +401,7 @@ class ScannerViewModel( } private fun acceptWorkOrder(peripheral: Peripheral, type: Int, woId: String, deviceIdentifier: String) { viewModelScope.launch { - var command = Helper.CMD_RES_OK(Helper.Command.ACCEPT_WO.name) + var command = Helper.CMD_RES_NG(Helper.Command.ACCEPT_WO.name) try { val res = scannerRepository.confirmWorkOrder(woId, deviceIdentifier, type) if (res) @@ -427,39 +430,40 @@ class ScannerViewModel( ) Napier.d(">>>>> DTO $dto") viewModelScope.launch { + var command = Helper.CMD_RES_NG(Helper.Command.FINISH_WO.name) when (val act = bleData.firstOrNull()) { Helper.TorqueAction.TORQUE -> { - try { - scannerRepository.sendMeasurementResult(woId, dto) - } catch (e: Exception) { + val res = scannerRepository.sendMeasurementResult(woId, dto) + if (res.isSuccess) { + command = Helper.CMD_RES_OK(Helper.Command.FINISH_WO.name) + } else { + val e = res.exceptionOrNull() 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)) + logRepository.appendLog(title = "HTTP", content = "${e?.message}".substringBefore("[url=")) } + sendCommand(peripheral, command) } Helper.TorqueAction.RE_TORQUE -> { - try { - scannerRepository.sendMeasurementResult(woId, dto, true) - } catch (e: Exception) { + val res = scannerRepository.sendMeasurementResult(woId, dto, true) + if (res.isSuccess) { + command = Helper.CMD_RES_OK(Helper.Command.FINISH_WO.name) + } else { + val e = res.exceptionOrNull() 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)) + logRepository.appendLog(title = "HTTP", content = "${e?.message}".substringBefore("[url=")) } + sendCommand(peripheral, command) } Helper.TorqueAction.CANCEL -> { - try { - scannerRepository.sendCancelWorkOrder(woId, dto) - } catch (e: Exception) { + val res = scannerRepository.sendCancelWorkOrder(woId, dto) + if (res.isSuccess) { + command = Helper.CMD_RES_OK(Helper.Command.FINISH_WO.name) + } else { + val e = res.exceptionOrNull() 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)) + logRepository.appendLog(title = "HTTP", content = "${e?.message}".substringBefore("[url=")) } + sendCommand(peripheral, command) } else -> Napier.w(">>>>> Unknown action: $act") } @@ -476,9 +480,9 @@ class ScannerViewModel( val woId = decoded.woId val data = decoded.data when (cmd) { - Helper.Command.START_WO -> fetchWorkOrders(peripheral) - Helper.Command.NEXT_WO -> fetchWorkOrders(peripheral, woId, "next") - Helper.Command.PREVIOUS_WO -> fetchWorkOrders(peripheral, woId, "back") + Helper.Command.START_WO -> fetchWorkOrders(peripheral, Helper.Command.START_WO) + Helper.Command.NEXT_WO -> fetchWorkOrders(peripheral, Helper.Command.NEXT_WO, woId, "next") + Helper.Command.PREVIOUS_WO -> fetchWorkOrders(peripheral, Helper.Command.PREVIOUS_WO, woId, "back") Helper.Command.ACCEPT_WO -> acceptWorkOrder(peripheral, type.code, woId, deviceIdentifier) Helper.Command.FINISH_WO -> { finishWorkOrder(peripheral, type.code, woId, data, deviceIdentifier) 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 c6d826d..782ff5d 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 @@ -8,11 +8,10 @@ object Helper { const val START_CHARACTER ="$" const val END_LINE_FEED = "#\n" const val SECURITY_CRC16 = "00cr16" - - /** Command sent to indicate no work orders are available. */ - const val CMD_EMPTY_WO = "START_WO;empty" + const val WO_RES = "WO_RES" - + /** Generates empty response for a specific command. */ + val CMD_EMPTY_WO: (String) -> String = {cmd -> "$cmd;empty"} /** Generates a success response for a specific command. */ val CMD_RES_OK: (String) -> String = { cmd -> "$cmd;OK" } diff --git a/composeApp/src/commonTest/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/UartCommunicationTest.kt b/composeApp/src/commonTest/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/UartCommunicationTest.kt index 19abd00..e33faad 100644 --- a/composeApp/src/commonTest/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/UartCommunicationTest.kt +++ b/composeApp/src/commonTest/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/UartCommunicationTest.kt @@ -27,7 +27,7 @@ class UartCommunicationTest { assertEquals("cr16", decoded.crc16) // --- STEP 2: App prepares response (Empty Case) --- - val emptyCase = Helper.buildUartCommand(Helper.CMD_EMPTY_WO) + val emptyCase = Helper.buildUartCommand(Helper.CMD_EMPTY_WO("START_WO")) assertEquals($$"$START_WO;empty\n00cr16#\n", emptyCase) // --- STEP 3: App prepares response (Multiple WO Case) ---