diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts
index 804f27f..25bc799 100644
--- a/composeApp/build.gradle.kts
+++ b/composeApp/build.gradle.kts
@@ -67,6 +67,8 @@ kotlin {
implementation(libs.sqldelight.coroutines)
implementation(libs.kotlinx.datetime)
+ implementation(libs.ble.kable)
+
implementation(libs.logger.napier)
}
iosMain.dependencies {
diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index 41a7e7c..448aacf 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -3,7 +3,7 @@
Back
Menu
- %d dBm
+ dBm
Home
@@ -31,4 +31,8 @@
Filters
Sort by
Clear
+ Scanner
+ CAN'T SEE YOUR DEVICE?
+ 1. Make sure the device is connected to a power source and powered on.\n\n2. Make sure the appropriate firmware and SoftDevice are flashed.
+
diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/RssiIcon.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/RssiIcon.kt
index 3ca6aed..ceb9f84 100644
--- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/RssiIcon.kt
+++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/RssiIcon.kt
@@ -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
)
}
diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/api/AuthApi.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/api/AuthApi.kt
index 9fccb14..acb4b34 100644
--- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/api/AuthApi.kt
+++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/api/AuthApi.kt
@@ -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
}
}
diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/api/Endpoints.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/api/Endpoints.kt
index cbcb51c..40f43cb 100644
--- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/api/Endpoints.kt
+++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/api/Endpoints.kt
@@ -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/"
}
\ No newline at end of file
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 971b2cd..9f3ea46 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
@@ -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 {
- return client.get("$baseUrl${Endpoints.WORK_ORDERS}") {
- parameter("page", page)
+ suspend fun fetchAbleWorkOrders(woID: String, action: String): List {
+ 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 {
+ 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())
+ } 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 {
+ 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())
+ } else {
+ Napier.e("Error status: ${response.status}")
+ Result.failure(Exception("HTTP ${response.status}"))
+ }
+ } catch (e: Exception) {
+ Napier.e("Exception: $e")
+ Result.failure(e)
}
}
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/dto/WorkOrderDto.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/dto/WorkOrderDto.kt
index 20a4940..e855179 100644
--- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/dto/WorkOrderDto.kt
+++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/dto/WorkOrderDto.kt
@@ -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?,
- @SerialName("meas_result") val measResult: Map?,
- @SerialName("meas_history") val measHistory: Map?,
+ @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>? = 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>? = 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
)
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/mapper/WorkOrderMapper.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/mapper/WorkOrderMapper.kt
index c9304ba..ab6e25c 100644
--- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/mapper/WorkOrderMapper.kt
+++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/mapper/WorkOrderMapper.kt
@@ -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()
+ )
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/ScannerRepository.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/ScannerRepository.kt
new file mode 100644
index 0000000..6f9122a
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/ScannerRepository.kt
@@ -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 {
+ 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 {
+ return woRepository.sendMeasurement(woId, data, isReTorque)
+ }
+
+ suspend fun sendCancelWorkOrder(woId: String, data: SendMeasureDto): Result {
+ return woRepository.cancelWorkOrder(woId, data)
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/WorkOrderRepository.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/WorkOrderRepository.kt
index 03bb541..af09345 100644
--- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/WorkOrderRepository.kt
+++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/WorkOrderRepository.kt
@@ -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
- suspend fun confirm(mac: String, type: Int)
+ suspend fun fetchAbleWorkOrders(woId: String, action: String): List
+ suspend fun confirm(woID: String, deviceId: String, type: Int): Boolean
+ suspend fun sendMeasurement(woId: String, data: SendMeasureDto, isReTorque: Boolean = false): Result
+ suspend fun cancelWorkOrder(woId: String, data: SendMeasureDto): Result
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/WorkOrderRepositoryImpl.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/WorkOrderRepositoryImpl.kt
index 85bcc2d..1dbea7c 100644
--- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/WorkOrderRepositoryImpl.kt
+++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/WorkOrderRepositoryImpl.kt
@@ -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 {
- val dto = api.fetchAbleWorkOrders(page)
+ override suspend fun fetchAbleWorkOrders(woId: String, action: String): List {
+ 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 {
+ val dto = api.sendMeasurement(woId, data, isReTorque)
+ return dto.map { it.toDomain() }
+ }
+
+ override suspend fun cancelWorkOrder(woId: String, data: SendMeasureDto): Result {
+ val dto = api.cancelWorkOrder(woId, data)
+ return dto.map { it.toDomain() }
}
}
diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/Modules.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/Modules.kt
index ae9dc05..c30a4d1 100644
--- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/Modules.kt
+++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/Modules.kt
@@ -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().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()) }
}
diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/domain/model/WorkOrder.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/domain/model/WorkOrder.kt
index d96620a..72fa20c 100644
--- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/domain/model/WorkOrder.kt
+++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/domain/model/WorkOrder.kt
@@ -29,8 +29,8 @@ data class WorkOrder(
val make: String?,
val licensePlate: String,
val services: Map?,
- val measResult: Map?,
- val measHistory: Map?,
+ 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>
+)
+
+data class MeasureResult(
+ val status: String,
+ val message: String,
+ val data: WorkOrder
+)
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/domain/usecase/FetchWorkOrdersUseCase.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/domain/usecase/FetchWorkOrdersUseCase.kt
index 784759b..7285164 100644
--- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/domain/usecase/FetchWorkOrdersUseCase.kt
+++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/domain/usecase/FetchWorkOrdersUseCase.kt
@@ -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 {
- return repository.fetchAbleWorkOrders(page)
+ suspend operator fun invoke(page: Int, woId: String = "0", action: String = "next"): List {
+ return repository.fetchAbleWorkOrders(woId, action)
}
}
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
new file mode 100644
index 0000000..41f7c5e
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/kable/BleManager.kt
@@ -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()
+ private val mutex = Mutex()
+
+ fun scanDevices(): Flow = 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 =
+ peripheral.observe(TX_CHAR).map { it.decodeToString() }
+}
diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/MainScreen.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/MainScreen.kt
index 2349af7..2007e54 100644
--- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/MainScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/MainScreen.kt
@@ -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) {
diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/scanner/ScannerDestination.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/scanner/ScannerDestination.kt
new file mode 100644
index 0000000..7c2b9b2
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/scanner/ScannerDestination.kt
@@ -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"
+)
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/scanner/ScannerScreen.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/scanner/ScannerScreen.kt
new file mode 100644
index 0000000..aebe1b6
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/scanner/ScannerScreen.kt
@@ -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)
+ }
+ }
+ )
+ }
+}
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
new file mode 100644
index 0000000..8feee17
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/scanner/ScannerViewModel.kt
@@ -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>(emptyList())
+ val devices: StateFlow> = _devices.asStateFlow()
+
+ private val _isScanning = MutableStateFlow(false)
+ val isScanning: StateFlow = _isScanning.asStateFlow()
+ private val _connectedDevices = MutableStateFlow