Implementation BLE using kable lib, handle scan ble, connect and send data via uart service
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<string name="navigation_item_accessibility">Back</string>
|
||||
<string name="menu_item_accessibility">Menu</string>
|
||||
|
||||
<string name="dbm">%d dBm</string>
|
||||
<string name="dbm">dBm</string>
|
||||
|
||||
<string name="home_title">Home</string>
|
||||
|
||||
@@ -31,4 +31,8 @@
|
||||
<string name="filter_title">Filters</string>
|
||||
<string name="sort_by_title">Sort by</string>
|
||||
<string name="clear">Clear</string>
|
||||
<string name="scanner_title">Scanner</string>
|
||||
<string name="scan_empty_title">CAN'T SEE YOUR DEVICE?</string>
|
||||
<string name="no_device_guide_info">1. Make sure the device is connected to a power source and <b>powered on</b>.\n\n2. Make sure the appropriate firmware and SoftDevice are flashed.</string>
|
||||
|
||||
</resources>
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ fun RssiIcon(rssi: Int) {
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.dbm, rssi),
|
||||
text = "$rssi ${stringResource(Res.string.dbm)}",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@ import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.LoginResponseDt
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.RefreshRequestDto
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.RefreshResponseDto
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.TokenManager
|
||||
import io.github.aakira.napier.Napier
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.post
|
||||
@@ -29,7 +30,7 @@ class AuthApi(
|
||||
tokenManager.saveTokens(res.access, res.refresh)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
println(">>>>> [AuthApi.kt] Exception: $e")
|
||||
Napier.d(">>>>> [AuthApi.kt] Exception: $e")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -3,6 +3,10 @@ package com.digitoolsolutions.app.torquevaultkmp.data.network.api
|
||||
object Endpoints {
|
||||
const val AUTH_TOKEN = "/token/"
|
||||
const val AUTH_REFRESH = "/token/refresh/"
|
||||
const val WORK_ORDERS = "/work-orders/able-v2/"
|
||||
const val CONFIRM = "/confirm"
|
||||
const val WORK_ORDERS = "work-orders"
|
||||
const val ABLE_WO = "$WORK_ORDERS/able-v2/"
|
||||
const val CONFIRM = "connect-wrench/"
|
||||
const val MEASURE = "measure/"
|
||||
const val RE_MEASURE = "remeasure/"
|
||||
const val CANCEL = "cancel-by-wrench/"
|
||||
}
|
||||
+60
-6
@@ -1,14 +1,24 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.data.network.api
|
||||
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.ConfirmRequestDto
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.LoginRequestDto
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.LoginResponseDto
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.MeasureResDto
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.SendMeasureDto
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.WorkOrderDto
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.TokenManager
|
||||
import io.github.aakira.napier.Napier
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.parameter
|
||||
import io.ktor.client.request.patch
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.http.path
|
||||
|
||||
class WorkOrderApi(
|
||||
private val client: HttpClient,
|
||||
@@ -17,15 +27,59 @@ class WorkOrderApi(
|
||||
private val baseUrl: String?
|
||||
get() = tokenManager.getServerUrl()
|
||||
|
||||
suspend fun fetchAbleWorkOrders(page: Int): List<WorkOrderDto> {
|
||||
return client.get("$baseUrl${Endpoints.WORK_ORDERS}") {
|
||||
parameter("page", page)
|
||||
suspend fun fetchAbleWorkOrders(woID: String, action: String): List<WorkOrderDto> {
|
||||
return client.get("$baseUrl/${Endpoints.ABLE_WO}") {
|
||||
parameter("woID", woID)
|
||||
parameter("action", action)
|
||||
}.body()
|
||||
}
|
||||
|
||||
suspend fun confirm(mac: String, type: Int) {
|
||||
client.post("$baseUrl${Endpoints.CONFIRM}") {
|
||||
setBody(ConfirmRequestDto(mac, type))
|
||||
suspend fun confirm(woID: String, deviceId: String, type: Int): Boolean {
|
||||
return try {
|
||||
val response = client.patch("$baseUrl/${Endpoints.WORK_ORDERS}/$woID/${Endpoints.CONFIRM}"){
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(ConfirmRequestDto(deviceId, type))
|
||||
}
|
||||
response.status == HttpStatusCode.OK
|
||||
} catch (e: Exception) {
|
||||
Napier.e(">>>>> Exception: $e", tag="WorkOrderApi.kt")
|
||||
false
|
||||
}
|
||||
}
|
||||
suspend fun sendMeasurement(woID: String, bodyPayload: SendMeasureDto, isRemeasure: Boolean = false): Result<MeasureResDto> {
|
||||
return try {
|
||||
val endPath = if(isRemeasure) Endpoints.RE_MEASURE else Endpoints.MEASURE
|
||||
val response = client.patch("$baseUrl/${Endpoints.WORK_ORDERS}/$woID/$endPath") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(bodyPayload)
|
||||
}
|
||||
if (response.status.value in 200..299) {
|
||||
Result.success(response.body<MeasureResDto>())
|
||||
} else {
|
||||
Napier.e("Error status: ${response.status}")
|
||||
Result.failure(Exception("HTTP ${response.status}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Napier.e("Exception: $e")
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun cancelWorkOrder(woId: String, bodyPayload: SendMeasureDto): Result<WorkOrderDto> {
|
||||
return try {
|
||||
val response = client.patch("$baseUrl/${Endpoints.WORK_ORDERS}/$woId/${Endpoints.CANCEL}") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(bodyPayload)
|
||||
}
|
||||
if (response.status.value in 200..299) {
|
||||
Result.success(response.body<WorkOrderDto>())
|
||||
} else {
|
||||
Napier.e("Error status: ${response.status}")
|
||||
Result.failure(Exception("HTTP ${response.status}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Napier.e("Exception: $e")
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
-4
@@ -9,11 +9,11 @@ data class WorkOrderDto(
|
||||
@SerialName("wo_id") val id: String,
|
||||
val status: String,
|
||||
val car: String? = null,
|
||||
val make: String?,
|
||||
val make: String? = null,
|
||||
@SerialName("license_plate") val licensePlate: String,
|
||||
val services: Map<String, ServiceDto>?,
|
||||
@SerialName("meas_result") val measResult: Map<String, JsonElement>?,
|
||||
@SerialName("meas_history") val measHistory: Map<String, JsonElement>?,
|
||||
@SerialName("meas_result") val measResult: MeasResultDto? = null,
|
||||
@SerialName("meas_history") val measHistory: MeasResultDto? = null,
|
||||
@SerialName("finished_at") val finishedAt: String?,
|
||||
@SerialName("created_at") val createdAt: String,
|
||||
@SerialName("updated_at") val updatedAt: String? = null,
|
||||
@@ -26,9 +26,29 @@ data class ServiceDto(
|
||||
val wrench: String?,
|
||||
@SerialName("torque_required") val torqueRequired: Double
|
||||
)
|
||||
@Serializable
|
||||
data class MeasResultDto(
|
||||
val nut: Int? = null,
|
||||
@SerialName("torque_val") val torqueVal: Map<String, List<Double>>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ConfirmRequestDto(
|
||||
@SerialName("wrench_mac_addr") val mac: String,
|
||||
@SerialName("device_id") val deviceId: String,
|
||||
val type: Int
|
||||
)
|
||||
@Serializable
|
||||
data class SendMeasureDto(
|
||||
@SerialName("device_id") val deviceId: String,
|
||||
val type: Int,
|
||||
@SerialName("nuts_of_wheel") val nuts: Int? = null,
|
||||
@SerialName("torque_results") val torqueData: Map<String, List<Double>>? = null,
|
||||
@SerialName("oil_filter_val") val oilFilterVal: Double? = null,
|
||||
@SerialName("drain_plug_val") val drainPlugVal: Double? = null
|
||||
)
|
||||
@Serializable
|
||||
data class MeasureResDto(
|
||||
val status: String,
|
||||
val message: String,
|
||||
val data: WorkOrderDto
|
||||
)
|
||||
+24
-4
@@ -1,6 +1,9 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.data.network.mapper
|
||||
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.MeasureResDto
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.WorkOrderDto
|
||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.MeasResult
|
||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.MeasureResult
|
||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.Service
|
||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder
|
||||
import kotlinx.serialization.json.doubleOrNull
|
||||
@@ -22,14 +25,31 @@ fun WorkOrderDto.toDomain(): WorkOrder {
|
||||
torqueRequired = dto.torqueRequired
|
||||
)
|
||||
},
|
||||
measResult = measResult?.mapValues { (_, value) ->
|
||||
value.jsonPrimitive.doubleOrNull ?: Double.NaN
|
||||
measResult = measResult?.let {
|
||||
it.torqueVal?.let { torqueVal ->
|
||||
MeasResult(
|
||||
nut = it.nut,
|
||||
torqueVal = torqueVal
|
||||
)
|
||||
}
|
||||
},
|
||||
measHistory = measHistory?.mapValues { (_, value) ->
|
||||
value.jsonPrimitive.doubleOrNull ?: Double.NaN
|
||||
measHistory = measHistory?.let {
|
||||
it.torqueVal?.let { torqueVal ->
|
||||
MeasResult(
|
||||
nut = it.nut,
|
||||
torqueVal = torqueVal
|
||||
)
|
||||
}
|
||||
},
|
||||
finishedAt = finishedAt,
|
||||
createdAt = createdAt,
|
||||
updatedAt = updatedAt
|
||||
)
|
||||
}
|
||||
fun MeasureResDto.toDomain(): MeasureResult {
|
||||
return MeasureResult(
|
||||
status = status,
|
||||
message = message,
|
||||
data = data.toDomain()
|
||||
)
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.data.repository
|
||||
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.api.WorkOrderApi
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.MeasureResDto
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.SendMeasureDto
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.mapper.toDomain
|
||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.MeasureResult
|
||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder
|
||||
|
||||
class ScannerRepository(private val woRepository: WorkOrderRepository) {
|
||||
suspend fun getAbleWorkOrders(woID: String = "0", action: String = "next"): List<WorkOrder> {
|
||||
return woRepository.fetchAbleWorkOrders(woID, action)
|
||||
}
|
||||
|
||||
suspend fun confirmWorkOrder(woID: String, deviceId: String, type: Int): Boolean {
|
||||
return woRepository.confirm(woID, deviceId, type)
|
||||
}
|
||||
|
||||
suspend fun sendMeasurementResult(woId: String, data: SendMeasureDto, isReTorque: Boolean = false): Result<MeasureResult> {
|
||||
return woRepository.sendMeasurement(woId, data, isReTorque)
|
||||
}
|
||||
|
||||
suspend fun sendCancelWorkOrder(woId: String, data: SendMeasureDto): Result<WorkOrder> {
|
||||
return woRepository.cancelWorkOrder(woId, data)
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -1,8 +1,13 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.data.repository
|
||||
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.MeasureResDto
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.SendMeasureDto
|
||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.MeasureResult
|
||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder
|
||||
|
||||
interface WorkOrderRepository {
|
||||
suspend fun fetchAbleWorkOrders(page: Int): List<WorkOrder>
|
||||
suspend fun confirm(mac: String, type: Int)
|
||||
suspend fun fetchAbleWorkOrders(woId: String, action: String): List<WorkOrder>
|
||||
suspend fun confirm(woID: String, deviceId: String, type: Int): Boolean
|
||||
suspend fun sendMeasurement(woId: String, data: SendMeasureDto, isReTorque: Boolean = false): Result<MeasureResult>
|
||||
suspend fun cancelWorkOrder(woId: String, data: SendMeasureDto): Result<WorkOrder>
|
||||
}
|
||||
+20
-4
@@ -1,15 +1,31 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.data.repository
|
||||
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.api.WorkOrderApi
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.SendMeasureDto
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.mapper.toDomain
|
||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.MeasureResult
|
||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder
|
||||
class WorkOrderRepositoryImpl(private val api: WorkOrderApi) : WorkOrderRepository {
|
||||
override suspend fun fetchAbleWorkOrders(page: Int): List<WorkOrder> {
|
||||
val dto = api.fetchAbleWorkOrders(page)
|
||||
override suspend fun fetchAbleWorkOrders(woId: String, action: String): List<WorkOrder> {
|
||||
val dto = api.fetchAbleWorkOrders(woId, action)
|
||||
return dto.map { it.toDomain() }
|
||||
}
|
||||
|
||||
override suspend fun confirm(mac: String, type: Int) {
|
||||
api.confirm(mac, type)
|
||||
override suspend fun confirm(
|
||||
woID: String,
|
||||
deviceId: String,
|
||||
type: Int
|
||||
): Boolean {
|
||||
return api.confirm(woID,deviceId, type)
|
||||
}
|
||||
|
||||
override suspend fun sendMeasurement(woId: String, data: SendMeasureDto, isReTorque: Boolean): Result<MeasureResult> {
|
||||
val dto = api.sendMeasurement(woId, data, isReTorque)
|
||||
return dto.map { it.toDomain() }
|
||||
}
|
||||
|
||||
override suspend fun cancelWorkOrder(woId: String, data: SendMeasureDto): Result<WorkOrder> {
|
||||
val dto = api.cancelWorkOrder(woId, data)
|
||||
return dto.map { it.toDomain() }
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -6,15 +6,18 @@ import com.digitoolsolutions.app.torquevaultkmp.data.network.api.AuthApi
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.api.WorkOrderApi
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.repository.DeviceRepository
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.repository.LogRepository
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.repository.ScannerRepository
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.repository.WorkOrderRepository
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.repository.WorkOrderRepositoryImpl
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.TokenManager
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.AppDatabase
|
||||
import com.digitoolsolutions.app.torquevaultkmp.kable.BleManager
|
||||
import com.digitoolsolutions.app.torquevaultkmp.domain.usecase.FetchWorkOrdersUseCase
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.auth.AuthViewModel
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.bonded.BondedViewModel
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.home.HomeViewModel
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.logs.LogViewModel
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.scanner.ScannerViewModel
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.settings.SettingViewModel
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.module.dsl.factoryOf
|
||||
@@ -27,6 +30,7 @@ expect val platformModule: Module
|
||||
val appModule = module {
|
||||
// TokenManager use AppStorage
|
||||
single { TokenManager(get()) }
|
||||
singleOf(::BleManager)
|
||||
}
|
||||
|
||||
val storageModule = module {
|
||||
@@ -34,6 +38,7 @@ val storageModule = module {
|
||||
single { get<AppDatabase>().appDatabaseQueries }
|
||||
single { LogRepository(get()) }
|
||||
single { DeviceRepository(get()) }
|
||||
single { ScannerRepository(get()) }
|
||||
}
|
||||
|
||||
val networkModule = module {
|
||||
@@ -58,4 +63,5 @@ val viewModelModule = module {
|
||||
factoryOf(::SettingViewModel)
|
||||
factoryOf(::BondedViewModel)
|
||||
factoryOf(::LogViewModel)
|
||||
factory { ScannerViewModel(get(), get(), get(), get()) }
|
||||
}
|
||||
|
||||
+13
-2
@@ -29,8 +29,8 @@ data class WorkOrder(
|
||||
val make: String?,
|
||||
val licensePlate: String,
|
||||
val services: Map<String, Service>?,
|
||||
val measResult: Map<String, Double>?,
|
||||
val measHistory: Map<String, Double>?,
|
||||
val measResult: MeasResult?,
|
||||
val measHistory: MeasResult?,
|
||||
val finishedAt: String?,
|
||||
val createdAt: String,
|
||||
val updatedAt: String?
|
||||
@@ -42,3 +42,14 @@ data class Service(
|
||||
val wrench: String?,
|
||||
val torqueRequired: Double
|
||||
)
|
||||
|
||||
data class MeasResult(
|
||||
val nut: Int? = null,
|
||||
val torqueVal: Map<String, List<Double>>
|
||||
)
|
||||
|
||||
data class MeasureResult(
|
||||
val status: String,
|
||||
val message: String,
|
||||
val data: WorkOrder
|
||||
)
|
||||
+2
-2
@@ -4,7 +4,7 @@ import com.digitoolsolutions.app.torquevaultkmp.data.repository.WorkOrderReposit
|
||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder
|
||||
|
||||
class FetchWorkOrdersUseCase(private val repository: WorkOrderRepository) {
|
||||
suspend operator fun invoke(page: Int): List<WorkOrder> {
|
||||
return repository.fetchAbleWorkOrders(page)
|
||||
suspend operator fun invoke(page: Int, woId: String = "0", action: String = "next"): List<WorkOrder> {
|
||||
return repository.fetchAbleWorkOrders(woId, action)
|
||||
}
|
||||
}
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.kable
|
||||
|
||||
import com.juul.kable.Advertisement
|
||||
import com.juul.kable.Peripheral
|
||||
import com.juul.kable.Scanner
|
||||
import com.juul.kable.characteristicOf
|
||||
import com.juul.kable.logs.Logging
|
||||
import com.juul.kable.logs.SystemLogEngine
|
||||
import io.github.aakira.napier.Napier
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlin.uuid.Uuid
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
class BleManager {
|
||||
companion object {
|
||||
val SERVICE_UUID = Uuid.parse("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
|
||||
val RX_CHAR = characteristicOf(SERVICE_UUID, Uuid.parse("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"))
|
||||
val TX_CHAR = characteristicOf(SERVICE_UUID, Uuid.parse("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"))
|
||||
}
|
||||
|
||||
private val peripherals = mutableMapOf<String, Peripheral>()
|
||||
private val mutex = Mutex()
|
||||
|
||||
fun scanDevices(): Flow<Advertisement> = Scanner {
|
||||
filters {
|
||||
match {
|
||||
services = listOf(SERVICE_UUID)
|
||||
}
|
||||
}
|
||||
logging {
|
||||
engine = SystemLogEngine
|
||||
level = Logging.Level.Events
|
||||
format = Logging.Format.Multiline
|
||||
}
|
||||
}.advertisements
|
||||
|
||||
suspend fun connect(advertisement: Advertisement): Peripheral {
|
||||
val identifier = advertisement.identifier.toString()
|
||||
Napier.d(">>>>> Connecting to $identifier")
|
||||
val peripheral = mutex.withLock {
|
||||
peripherals.getOrPut(identifier) {
|
||||
Peripheral(advertisement)
|
||||
}
|
||||
}
|
||||
// Connect outside the lock to allow parallel connections for different devices
|
||||
peripheral.connect()
|
||||
return peripheral
|
||||
}
|
||||
|
||||
suspend fun disconnect(identifier: String) {
|
||||
mutex.withLock {
|
||||
peripherals.remove(identifier)?.disconnect()
|
||||
}
|
||||
Napier.d(">>>>> $identifier disconnected")
|
||||
}
|
||||
|
||||
suspend fun disconnectAll() {
|
||||
mutex.withLock {
|
||||
peripherals.values.forEach { it.disconnect() }
|
||||
peripherals.clear()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendCommand(peripheral: Peripheral, command: String) {
|
||||
Napier.d(">>>>> Sent to uart: $command")
|
||||
peripheral.write(RX_CHAR, command.encodeToByteArray())
|
||||
}
|
||||
fun observeRx(peripheral: Peripheral): Flow<String> =
|
||||
peripheral.observe(TX_CHAR).map { it.decodeToString() }
|
||||
}
|
||||
+10
-6
@@ -26,6 +26,8 @@ import com.digitoolsolutions.app.torquevaultkmp.screens.home.HomeDestination
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.home.HomeScreen
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.logs.LogDestination
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.logs.LogScreen
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.scanner.ScannerDestination
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.scanner.ScannerScreen
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.settings.SettingDestination
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.settings.SettingScreen
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
@@ -43,6 +45,7 @@ fun MainScreen(
|
||||
) {
|
||||
val destinations = listOf(
|
||||
HomeDestination,
|
||||
ScannerDestination,
|
||||
BondedDestination,
|
||||
LogDestination,
|
||||
SettingDestination,
|
||||
@@ -111,14 +114,15 @@ fun MainScreen(
|
||||
) {
|
||||
composable(AuthDestination.route) { LoginScreen(navController) }
|
||||
composable(HomeDestination.route) {
|
||||
BluetoothRequirementWrapper {
|
||||
HomeScreen(navController)
|
||||
}
|
||||
HomeScreen(navController)
|
||||
}
|
||||
composable(BondedDestination.route) {
|
||||
BluetoothRequirementWrapper {
|
||||
BondedScreen(navController)
|
||||
}
|
||||
BondedScreen(navController)
|
||||
}
|
||||
composable(ScannerDestination.route) {
|
||||
ScannerScreen(onDeviceClick = {
|
||||
// Handle device click, maybe navigate to details or connect
|
||||
})
|
||||
}
|
||||
composable(LogDestination.route) { LogScreen(navController) }
|
||||
composable(SettingDestination.route) {
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.screens.scanner
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Bluetooth
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.AppIcon
|
||||
import com.digitoolsolutions.app.torquevaultkmp.screens.Destinations
|
||||
|
||||
object ScannerDestination: Destinations(
|
||||
label = "Scanner",
|
||||
icon = AppIcon.Vector(Icons.Outlined.Bluetooth),
|
||||
route = "scanner"
|
||||
)
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.screens.scanner
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.BluetoothSearching
|
||||
import androidx.compose.material.icons.filled.Bluetooth
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.digitoolsolutions.app.torquevaultkmp.components.AppBar
|
||||
import com.digitoolsolutions.app.torquevaultkmp.components.CircularIcon
|
||||
import com.digitoolsolutions.app.torquevaultkmp.components.RssiIcon
|
||||
import com.digitoolsolutions.app.torquevaultkmp.components.WarningView
|
||||
import com.digitoolsolutions.app.torquevaultkmp.kable.BleManager
|
||||
import com.juul.kable.Advertisement
|
||||
import com.juul.kable.State
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import torquevaultkmp.composeapp.generated.resources.Res
|
||||
import torquevaultkmp.composeapp.generated.resources.baseline_filter_list_24
|
||||
import torquevaultkmp.composeapp.generated.resources.baseline_warning_24
|
||||
import torquevaultkmp.composeapp.generated.resources.filter_title
|
||||
import torquevaultkmp.composeapp.generated.resources.no_device_guide_info
|
||||
import torquevaultkmp.composeapp.generated.resources.scan_empty_title
|
||||
import torquevaultkmp.composeapp.generated.resources.scanner_title
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class)
|
||||
@Composable
|
||||
fun ScannerScreen(
|
||||
viewModel: ScannerViewModel = koinViewModel(),
|
||||
onDeviceClick: (Advertisement) -> Unit
|
||||
) {
|
||||
val devices by viewModel.devices.collectAsState()
|
||||
val connectedDevices by viewModel.connectedDevices.collectAsState()
|
||||
val isScanning by viewModel.isScanning.collectAsState()
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = { Text(stringResource(Res.string.scanner_title)) },
|
||||
actions = {
|
||||
if (isScanning) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.padding(4.dp).size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
IconButton(onClick = { viewModel.startScan() }) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = "Refresh")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
PullToRefreshBox(
|
||||
isRefreshing = false,
|
||||
onRefresh = {
|
||||
viewModel.refreshScan()
|
||||
},
|
||||
state = pullToRefreshState,
|
||||
modifier = Modifier.padding(paddingValues).fillMaxSize(),
|
||||
) {
|
||||
if (devices.isEmpty()) {
|
||||
WarningView(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
imageVector = Icons.AutoMirrored.Outlined.BluetoothSearching,
|
||||
title = stringResource(Res.string.scan_empty_title),
|
||||
hint = stringResource(Res.string.no_device_guide_info),
|
||||
hintTextAlign = TextAlign.Justify,
|
||||
){}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(devices) { device ->
|
||||
val deviceId = device.identifier.toString()
|
||||
val connectionState = connectedDevices[deviceId]
|
||||
|
||||
DeviceListItem(
|
||||
device = device,
|
||||
connectionState = connectionState,
|
||||
onClick = {
|
||||
if (connectionState == null || connectionState is State.Disconnected) {
|
||||
viewModel.connect(device)
|
||||
} else {
|
||||
viewModel.disconnect(deviceId)
|
||||
}
|
||||
viewModel.addBondedDevice(deviceId, device.name ?: "No name")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.startScan()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
@Composable
|
||||
fun DeviceListItem(
|
||||
device: Advertisement,
|
||||
connectionState: State?,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val statusText = when (connectionState) {
|
||||
is State.Connecting -> "Connecting..."
|
||||
is State.Connected -> "Connected"
|
||||
is State.Disconnecting -> "Disconnecting..."
|
||||
is State.Disconnected -> "Disconnected"
|
||||
null -> ""
|
||||
}
|
||||
|
||||
OutlinedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.clickable { onClick() },
|
||||
colors = if (connectionState is State.Connected) {
|
||||
CardDefaults.outlinedCardColors(containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f))
|
||||
} else {
|
||||
CardDefaults.outlinedCardColors()
|
||||
}
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = device.name ?: "No name",
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Column {
|
||||
Text(
|
||||
text = device.identifier.toString(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (statusText.isNotEmpty()) {
|
||||
Text(
|
||||
text = statusText,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (connectionState is State.Connected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
leadingContent = {
|
||||
CircularIcon(
|
||||
imageVector = Icons.Default.Bluetooth,
|
||||
backgroundColor = if (connectionState is State.Connected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondaryContainer,
|
||||
iconTint = if (connectionState is State.Connected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
if (connectionState is State.Connecting) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
|
||||
} else {
|
||||
RssiIcon(rssi = device.rssi)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.screens.scanner
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.SendMeasureDto
|
||||
import com.digitoolsolutions.app.torquevaultkmp.kable.BleManager
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.repository.DeviceRepository
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.repository.ScannerRepository
|
||||
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.juul.kable.Advertisement
|
||||
import com.juul.kable.Peripheral
|
||||
import com.juul.kable.State
|
||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder
|
||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.computeWheelsNuts
|
||||
import com.digitoolsolutions.app.torquevaultkmp.utils.Helper
|
||||
import io.github.aakira.napier.Napier
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.TimeSource
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
class ScannerViewModel(
|
||||
private val bleManager: BleManager,
|
||||
private val storage: AppStorage,
|
||||
private val deviceRepository: DeviceRepository,
|
||||
private val scannerRepository: ScannerRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _devices = MutableStateFlow<List<Advertisement>>(emptyList())
|
||||
val devices: StateFlow<List<Advertisement>> = _devices.asStateFlow()
|
||||
|
||||
private val _isScanning = MutableStateFlow(false)
|
||||
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 _onlyWithName = MutableStateFlow(true)
|
||||
|
||||
private var scanJob: Job? = null
|
||||
private var cleanupJob: Job? = null
|
||||
private val connectionJobs = mutableMapOf<String, Job>()
|
||||
|
||||
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") {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
fun startScan() {
|
||||
if (_isScanning.value) return
|
||||
scanJob = viewModelScope.launch {
|
||||
_isScanning.value = true
|
||||
_devices.value = emptyList()
|
||||
advertisementMap.clear()
|
||||
|
||||
bleManager.scanDevices()
|
||||
.catch {
|
||||
_isScanning.value = false
|
||||
stopCleanupJob()
|
||||
}
|
||||
.collect { advertisement ->
|
||||
if (_onlyWithName.value && advertisement.name == null) return@collect
|
||||
|
||||
val now = TimeSource.Monotonic.markNow()
|
||||
advertisementMap[advertisement.identifier.toString()] = advertisement to now
|
||||
startCleanupJob()
|
||||
updateDeviceList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startCleanupJob() {
|
||||
if (cleanupJob?.isActive == true) return
|
||||
cleanupJob = viewModelScope.launch {
|
||||
while (advertisementMap.isNotEmpty() && isActive) {
|
||||
delay(1000)
|
||||
val keysToRemove = advertisementMap.filter { it.value.second.elapsedNow() > SCAN_TIMEOUT }.keys
|
||||
if (keysToRemove.isNotEmpty()) {
|
||||
keysToRemove.forEach { advertisementMap.remove(it) }
|
||||
updateDeviceList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopCleanupJob() {
|
||||
cleanupJob?.cancel()
|
||||
cleanupJob = null
|
||||
}
|
||||
|
||||
private fun updateDeviceList() {
|
||||
_devices.value = advertisementMap.values
|
||||
.map { it.first }
|
||||
.sortedByDescending { it.rssi }
|
||||
}
|
||||
|
||||
fun stopScan() {
|
||||
scanJob?.cancel()
|
||||
scanJob = null
|
||||
_isScanning.value = false
|
||||
stopCleanupJob()
|
||||
}
|
||||
|
||||
fun refreshScan() {
|
||||
_devices.value = emptyList()
|
||||
startScan()
|
||||
}
|
||||
|
||||
fun connect(adv: Advertisement) {
|
||||
val deviceIdentifier = adv.identifier.toString()
|
||||
if (connectionJobs[deviceIdentifier]?.isActive == true) return
|
||||
|
||||
val job = viewModelScope.launch {
|
||||
try {
|
||||
val peripheral = bleManager.connect(adv)
|
||||
|
||||
coroutineScope {
|
||||
// Tracking connection state
|
||||
launch {
|
||||
peripheral.state.collect { state ->
|
||||
_connectedDevices.value += (deviceIdentifier to state)
|
||||
if (state is State.Disconnected) {
|
||||
this@coroutineScope.cancel(">>>>> Device disconnected")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Listen rx data
|
||||
launch {
|
||||
bleManager.observeRx(peripheral).collectLatest { value ->
|
||||
handleMessage(peripheral, value, deviceIdentifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e !is CancellationException) {
|
||||
Napier.e(">>>>> Connection failed for $deviceIdentifier", e)
|
||||
}
|
||||
} finally {
|
||||
_connectedDevices.value -= deviceIdentifier
|
||||
connectionJobs.remove(deviceIdentifier)
|
||||
bleManager.disconnect(deviceIdentifier)
|
||||
Napier.d(">>>>> Cleaned up connection for $deviceIdentifier")
|
||||
}
|
||||
}
|
||||
connectionJobs[deviceIdentifier] = job
|
||||
}
|
||||
|
||||
fun disconnect(deviceIdentifier: String) {
|
||||
viewModelScope.launch {
|
||||
bleManager.disconnect(deviceIdentifier)
|
||||
connectionJobs[deviceIdentifier]?.cancel()
|
||||
connectionJobs.remove(deviceIdentifier)
|
||||
_connectedDevices.value -= deviceIdentifier
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMessage(peripheral: Peripheral, msg: String, deviceIdentifier: String) {
|
||||
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"
|
||||
Napier.d(">>>>> Received data from $deviceIdentifier: $msg", tag="ScannerViewModel.kt")
|
||||
Napier.d(">>>>> Data: $data", tag="ScannerViewModel.kt")
|
||||
viewModelScope.launch {
|
||||
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
|
||||
bleManager.sendCommand(peripheral, Helper.buildUartCommand(command))
|
||||
} catch (e: Exception) {
|
||||
Napier.e(">>>>> Failed to handle message: $msg", e)
|
||||
bleManager.sendCommand(
|
||||
peripheral, Helper.buildUartCommand(Helper.CMD_NOT_AVAILABLE_WO)
|
||||
)
|
||||
}
|
||||
}
|
||||
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")
|
||||
} finally {
|
||||
// Currently, always send UPLOADED_WO regardless of success or failure
|
||||
bleManager.sendCommand(peripheral, Helper.buildUartCommand(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")
|
||||
} finally {
|
||||
// Currently, always send UPLOADED_WO regardless of success or failure
|
||||
bleManager.sendCommand(peripheral, Helper.buildUartCommand(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")
|
||||
} finally {
|
||||
// Currently, always send UPLOADED_WO regardless of success or failure
|
||||
bleManager.sendCommand(peripheral, Helper.buildUartCommand(Helper.CMD_UPLOADED_WO))
|
||||
}
|
||||
}
|
||||
else -> Napier.w(">>>>> Unknown action: $act")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addBondedDevice(id: String, name: String) {
|
||||
viewModelScope.launch {
|
||||
deviceRepository.saveDevice(id, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
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 BLE_NUTS_POS = 4
|
||||
const val BLE_TORQUE_POS = BLE_NUTS_POS + 1
|
||||
|
||||
object TorqueAction {
|
||||
const val TORQUE = "0"
|
||||
const val RE_TORQUE = "1"
|
||||
const val CANCEL = "2"
|
||||
}
|
||||
data class RxMessage(
|
||||
val id: String,
|
||||
val data: String
|
||||
)
|
||||
|
||||
fun buildUartCommand(cmd: String): String {
|
||||
return "$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)
|
||||
}
|
||||
|
||||
fun parseWheelTorqueData(torqueData: List<String>, nutsPerWheel: Int): Map<String, List<Double>> {
|
||||
val result = mutableMapOf<String, List<Double>>()
|
||||
var i = 0
|
||||
while (i < torqueData.size) {
|
||||
val wheel = torqueData[i] // Wheel name
|
||||
val torques = ArrayList<Double>(nutsPerWheel)
|
||||
for (j in 0 until nutsPerWheel) {
|
||||
val index = i + 1 + j
|
||||
if (index < torqueData.size) {
|
||||
val value = torqueData[index].toDoubleOrNull() ?: 0.0
|
||||
torques.add(value)
|
||||
}
|
||||
}
|
||||
result[wheel] = torques
|
||||
i += nutsPerWheel + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user