Update Readme.md, add docstring
This commit is contained in:
@@ -1,35 +1,72 @@
|
|||||||
This is a Kotlin Multiplatform project targeting Android, iOS.
|
# TorqueVaultKMP
|
||||||
|
|
||||||
* [/composeApp](./composeApp/src) is for code that will be shared across your Compose Multiplatform applications.
|
TorqueVaultKMP is a Kotlin Multiplatform (KMP) application designed for managing torque measurements, connecting to digital torque devices via Bluetooth Low Energy (BLE), and handling work orders.
|
||||||
It contains several subfolders:
|
|
||||||
- [commonMain](./composeApp/src/commonMain/kotlin) is for code that’s common for all targets.
|
|
||||||
- Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name.
|
|
||||||
For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app,
|
|
||||||
the [iosMain](./composeApp/src/iosMain/kotlin) folder would be the right place for such calls.
|
|
||||||
Similarly, if you want to edit the Desktop (JVM) specific part, the [jvmMain](./composeApp/src/jvmMain/kotlin)
|
|
||||||
folder is the appropriate location.
|
|
||||||
|
|
||||||
* [/iosApp](./iosApp/iosApp) contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform,
|
The project targets **Android** and **iOS** using **Compose Multiplatform** for a shared UI and logic.
|
||||||
you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project.
|
|
||||||
|
|
||||||
### Build and Run Android Application
|
## 🚀 Key Features
|
||||||
|
|
||||||
To build and run the development version of the Android app, use the run configuration from the run widget
|
- **BLE Device Management**: Scan, connect, and bond with digital torque wrenches using the [Kable](https://github.com/JuulLabs/kable) library.
|
||||||
in your IDE’s toolbar or build it directly from the terminal:
|
- **Adaptive Scanning**: Automatically calibrates device discovery expiration by observing advertisement intervals, ensuring a responsive UI when devices stop broadcasting.
|
||||||
- on macOS/Linux
|
- **Work Order Workflow**:
|
||||||
```shell
|
- Fetch "Open" work orders from a remote API.
|
||||||
./gradlew :composeApp:assembleDebug
|
- Synchronize work order details (License Plate, Make, Torque requirements) with connected BLE devices.
|
||||||
```
|
- Receive and upload measurement results (Torque values, wheel/nut indices) back to the server.
|
||||||
- on Windows
|
- **Bonding & Persistence**: Automatically remembers and auto-connects to bonded devices using **SQLDelight** for local storage.
|
||||||
```shell
|
- **Diagnostic Logging**: A dedicated in-app logging system (`LogRepository`) that tracks system events, BLE communication, and HTTP requests. Supports exporting logs as CSV or Text for remote debugging.
|
||||||
.\gradlew.bat :composeApp:assembleDebug
|
- **Theme Support**: Built-in support for Dark and Light modes.
|
||||||
```
|
|
||||||
|
|
||||||
### Build and Run iOS Application
|
## 🛠 Tech Stack
|
||||||
|
|
||||||
To build and run the development version of the iOS app, use the run configuration from the run widget
|
- **Kotlin**: `2.3.21`
|
||||||
in your IDE’s toolbar or open the [/iosApp](./iosApp) directory in Xcode and run it from there.
|
- **UI Framework**: [Compose Multiplatform](https://www.jetbrains.com/lp/compose-multiplatform/) `1.11.0`
|
||||||
|
- **Dependency Injection**: [Koin](https://insert-koin.io/) `4.2.1`
|
||||||
|
- **Networking**: [Ktor](https://ktor.io/) `3.4.3`
|
||||||
|
- **Database**: [SQLDelight](https://cashapp.github.io/sqldelight/) `2.3.2`
|
||||||
|
- **Bluetooth**: [Kable](https://github.com/JuulLabs/kable) `0.43.0`
|
||||||
|
- **Logging**: [Napier](https://github.com/aakira/Napier) `2.7.1`
|
||||||
|
- **Time/Date**: [Kotlinx Datetime](https://github.com/Kotlin/kotlinx-datetime) `0.8.0`
|
||||||
|
- **Serialization**: [Kotlinx Serialization](https://github.com/Kotlin/kotlinx.serialization) `2.3.21`
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
- `composeApp/src/commonMain`: Shared logic, UI, and data management.
|
||||||
|
- `screens/`: UI modules for Scanner, Logs, Home, Settings, and Auth.
|
||||||
|
- `data/`: Network DTOs, SQLDelight DB schema, and Repositories.
|
||||||
|
- `kable/`: BLE abstraction and manager.
|
||||||
|
- `domain/`: Business logic models and shared interfaces.
|
||||||
|
- `composeApp/src/androidMain`: Android-specific implementations (e.g., Bluetooth permissions, Splash screen).
|
||||||
|
- `composeApp/src/iosMain`: iOS-specific implementations (e.g., Native sharing sheets, CoreBluetooth delegates).
|
||||||
|
- `iosApp`: Entry point for the iOS application.
|
||||||
|
|
||||||
|
## 🏗 Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Android Studio (Ladybug or newer) / IntelliJ IDEA.
|
||||||
|
- Xcode 15+ (for iOS development).
|
||||||
|
- Kotlin Multiplatform plugin.
|
||||||
|
|
||||||
|
### Build and Run
|
||||||
|
|
||||||
|
#### Android
|
||||||
|
```bash
|
||||||
|
./gradlew :composeApp:assembleDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
#### iOS
|
||||||
|
Open the `iosApp` directory in Xcode or use the run configuration in Android Studio.
|
||||||
|
|
||||||
|
## ⚙️ Communication Protocol
|
||||||
|
|
||||||
|
The app uses a custom UART protocol to communicate with torque devices. Commands include:
|
||||||
|
- `START_WO`: Initiate work order fetch.
|
||||||
|
- `ACCEPT_WO`: Confirm a specific work order selection.
|
||||||
|
- `FINISH_WO`: Send measurement data back to the app for server upload.
|
||||||
|
|
||||||
|
## 📜 Logging
|
||||||
|
|
||||||
|
Logs are stored in memory (up to 1000 entries) and categorized into **System** and **Device** logs. They can be shared via the **Logs** screen using platform-native sharing components.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
Developed by **Digitool Solutions**.
|
||||||
Learn more about [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html)…
|
|
||||||
|
|||||||
+6
@@ -16,6 +16,12 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android implementation of [BluetoothManager].
|
||||||
|
*
|
||||||
|
* Manages Bluetooth adapter state and handles runtime permissions (BLUETOOTH_SCAN,
|
||||||
|
* BLUETOOTH_CONNECT for API 31+, and ACCESS_FINE_LOCATION for older versions).
|
||||||
|
*/
|
||||||
class AndroidBluetoothManager(private val context: Context) : BluetoothManager {
|
class AndroidBluetoothManager(private val context: Context) : BluetoothManager {
|
||||||
private val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as AndroidBluetoothManager
|
private val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as AndroidBluetoothManager
|
||||||
private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
|
private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ import com.digitoolsolutions.app.torquevaultkmp.screens.scanner.ScannerViewModel
|
|||||||
import com.digitoolsolutions.app.torquevaultkmp.theme.TorqueVaultTheme
|
import com.digitoolsolutions.app.torquevaultkmp.theme.TorqueVaultTheme
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for the Compose Multiplatform application.
|
||||||
|
*
|
||||||
|
* It manages the root theme, ensures system requirements are met via [RequirementWrapper],
|
||||||
|
* and hosts the [MainScreen] which contains the application's navigation graph.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
fun App(
|
fun App(
|
||||||
|
|||||||
+31
@@ -13,41 +13,72 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core ViewModel for managing global application state across all screens.
|
||||||
|
*
|
||||||
|
* Responsibilities include:
|
||||||
|
* - Persisting and retrieving theme preferences.
|
||||||
|
* - Monitoring authentication session expiration.
|
||||||
|
* - Controlling visibility of diagnostic logs.
|
||||||
|
*/
|
||||||
class AppViewModel(
|
class AppViewModel(
|
||||||
private val tokenManager: TokenManager,
|
private val tokenManager: TokenManager,
|
||||||
private val storage: AppStorage
|
private val storage: AppStorage
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable state for the current theme.
|
||||||
|
* If null, the system theme is used.
|
||||||
|
*/
|
||||||
private val _isDarkTheme = mutableStateOf<Boolean?>(storage.load(ReferKeys.THEME))
|
private val _isDarkTheme = mutableStateOf<Boolean?>(storage.load(ReferKeys.THEME))
|
||||||
val isDarkTheme: State<Boolean?> = _isDarkTheme
|
val isDarkTheme: State<Boolean?> = _isDarkTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the UI when the authentication token has expired.
|
||||||
|
*/
|
||||||
private val _sessionExpired = mutableStateOf(false)
|
private val _sessionExpired = mutableStateOf(false)
|
||||||
val sessionExpired: State<Boolean> = _sessionExpired
|
val sessionExpired: State<Boolean> = _sessionExpired
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the Debug Logs tab should be visible in the navigation bar.
|
||||||
|
*/
|
||||||
private val _showLogs = MutableStateFlow(false)
|
private val _showLogs = MutableStateFlow(false)
|
||||||
val showLogs: StateFlow<Boolean> = _showLogs
|
val showLogs: StateFlow<Boolean> = _showLogs
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
// Observe the token manager for session expiration events
|
||||||
tokenManager.sessionExpired.collectLatest {
|
tokenManager.sessionExpired.collectLatest {
|
||||||
_sessionExpired.value = true
|
_sessionExpired.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the session expiration alert.
|
||||||
|
*/
|
||||||
fun dismissSessionExpired() {
|
fun dismissSessionExpired() {
|
||||||
_sessionExpired.value = false
|
_sessionExpired.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a valid session exists.
|
||||||
|
*/
|
||||||
fun isLoggedIn(): Boolean {
|
fun isLoggedIn(): Boolean {
|
||||||
return tokenManager.getRefreshToken() != null
|
return tokenManager.getRefreshToken() != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles between Light and Dark mode and persists the choice.
|
||||||
|
*/
|
||||||
fun toggleTheme(isDark: Boolean) {
|
fun toggleTheme(isDark: Boolean) {
|
||||||
_isDarkTheme.value = isDark
|
_isDarkTheme.value = isDark
|
||||||
storage.save(ReferKeys.THEME, isDark)
|
storage.save(ReferKeys.THEME, isDark)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables or disables the visibility of diagnostic logs.
|
||||||
|
*/
|
||||||
fun setShowLogs(enabled: Boolean) {
|
fun setShowLogs(enabled: Boolean) {
|
||||||
_showLogs.value = enabled
|
_showLogs.value = enabled
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -4,4 +4,12 @@ import com.digitoolsolutions.app.torquevaultkmp.data.network.api.AuthApi
|
|||||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.TokenManager
|
import com.digitoolsolutions.app.torquevaultkmp.data.storage.TokenManager
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function to create a platform-specific [HttpClient].
|
||||||
|
*
|
||||||
|
* Each platform (Android, iOS) provides its own implementation to handle
|
||||||
|
* specific requirements like OkHttp or Darwin engines, and to integrate
|
||||||
|
* with the [TokenManager] for automated auth header injection and
|
||||||
|
* token refreshing.
|
||||||
|
*/
|
||||||
expect fun createPlatformHttpClient(tokenManager: TokenManager, authApi: AuthApi?): HttpClient
|
expect fun createPlatformHttpClient(tokenManager: TokenManager, authApi: AuthApi?): HttpClient
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
package com.digitoolsolutions.app.torquevaultkmp
|
package com.digitoolsolutions.app.torquevaultkmp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface representing the current operating platform (Android or iOS).
|
||||||
|
*/
|
||||||
interface Platform {
|
interface Platform {
|
||||||
|
/** The name of the platform (e.g., "Android 34", "iOS 17.2"). */
|
||||||
val name: String
|
val name: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the [Platform] implementation for the current target.
|
||||||
|
*/
|
||||||
expect fun getPlatform(): Platform
|
expect fun getPlatform(): Platform
|
||||||
+10
@@ -25,6 +25,16 @@ import torquevaultkmp.composeapp.generated.resources.baseline_menu_24
|
|||||||
import torquevaultkmp.composeapp.generated.resources.menu_item_accessibility
|
import torquevaultkmp.composeapp.generated.resources.menu_item_accessibility
|
||||||
import torquevaultkmp.composeapp.generated.resources.navigation_item_accessibility
|
import torquevaultkmp.composeapp.generated.resources.navigation_item_accessibility
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A customized [TopAppBar] consistent with the application's theme.
|
||||||
|
*
|
||||||
|
* Supports a back button, a hamburger menu button, and custom actions.
|
||||||
|
*
|
||||||
|
* @param title The composable to be displayed as the title.
|
||||||
|
* @param onNavigationButtonClick Callback for the back navigation button.
|
||||||
|
* @param onHamburgerButtonClick Callback for the side menu button.
|
||||||
|
* @param actions The actions to be displayed on the right side of the app bar.
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AppBar(
|
fun AppBar(
|
||||||
|
|||||||
+6
@@ -12,6 +12,9 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A large icon component used for placeholder screens or large visual indicators.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
internal fun BigIcon(
|
internal fun BigIcon(
|
||||||
imageVector: ImageVector,
|
imageVector: ImageVector,
|
||||||
@@ -27,6 +30,9 @@ internal fun BigIcon(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A version of [BigIcon] that uses a [Painter] instead of an [ImageVector].
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
internal fun BigIcon(
|
internal fun BigIcon(
|
||||||
painterResource: Painter,
|
painterResource: Painter,
|
||||||
|
|||||||
+11
-1
@@ -14,7 +14,11 @@ import org.jetbrains.compose.resources.painterResource
|
|||||||
import torquevaultkmp.composeapp.generated.resources.Res
|
import torquevaultkmp.composeapp.generated.resources.Res
|
||||||
import torquevaultkmp.composeapp.generated.resources.baseline_close_24
|
import torquevaultkmp.composeapp.generated.resources.baseline_close_24
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A customized [FilterChip] component used for toggling filters in lists (e.g., in the Logs screen).
|
||||||
|
*
|
||||||
|
* When selected, it displays a close icon; otherwise, it displays the provided [icon].
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun FilterButton(
|
fun FilterButton(
|
||||||
title: String,
|
title: String,
|
||||||
@@ -34,6 +38,9 @@ fun FilterButton(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A version of [FilterButton] that takes a [Painter].
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun FilterButton(
|
fun FilterButton(
|
||||||
title: String,
|
title: String,
|
||||||
@@ -60,6 +67,9 @@ fun FilterButton(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A version of [FilterButton] that takes an [ImageVector].
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun FilterButton(
|
fun FilterButton(
|
||||||
title: String,
|
title: String,
|
||||||
|
|||||||
+7
-1
@@ -10,7 +10,10 @@ import androidx.compose.ui.text.AnnotatedString
|
|||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reusable text component typically used for displaying hints or
|
||||||
|
* instructional labels with centered alignment by default.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
internal fun Hint(
|
internal fun Hint(
|
||||||
text: String,
|
text: String,
|
||||||
@@ -28,6 +31,9 @@ internal fun Hint(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An [AnnotatedString] version of the [Hint] component.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
internal fun Hint(
|
internal fun Hint(
|
||||||
text: AnnotatedString,
|
text: AnnotatedString,
|
||||||
|
|||||||
+3
@@ -47,6 +47,9 @@ fun RssiIcon(rssi: Int) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Determines the appropriate signal strength icon resource based on the RSSI value.
|
||||||
|
*/
|
||||||
private fun getImageRes(rssi: Int): DrawableResource = when {
|
private fun getImageRes(rssi: Int): DrawableResource = when {
|
||||||
rssi < MEDIUM_RSSI -> Res.drawable.ic_signal_min
|
rssi < MEDIUM_RSSI -> Res.drawable.ic_signal_min
|
||||||
rssi < MAX_RSSI -> Res.drawable.ic_signal_medium
|
rssi < MAX_RSSI -> Res.drawable.ic_signal_medium
|
||||||
|
|||||||
+13
@@ -14,6 +14,11 @@ import io.ktor.http.ContentType
|
|||||||
import io.ktor.http.contentType
|
import io.ktor.http.contentType
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API service for authentication-related operations.
|
||||||
|
*
|
||||||
|
* Handles user login and authentication token refreshing.
|
||||||
|
*/
|
||||||
class AuthApi(
|
class AuthApi(
|
||||||
private val client: HttpClient,
|
private val client: HttpClient,
|
||||||
private val tokenManager: TokenManager
|
private val tokenManager: TokenManager
|
||||||
@@ -21,6 +26,9 @@ class AuthApi(
|
|||||||
private val baseUrl: String?
|
private val baseUrl: String?
|
||||||
get() = tokenManager.getServerUrl()
|
get() = tokenManager.getServerUrl()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to authenticate the user and stores the received tokens.
|
||||||
|
*/
|
||||||
suspend fun login(username: String, password: String): Boolean {
|
suspend fun login(username: String, password: String): Boolean {
|
||||||
return try {
|
return try {
|
||||||
val res: LoginResponseDto = client.post("$baseUrl${Endpoints.AUTH_TOKEN}") {
|
val res: LoginResponseDto = client.post("$baseUrl${Endpoints.AUTH_TOKEN}") {
|
||||||
@@ -34,6 +42,11 @@ class AuthApi(
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Attempts to refresh the access token using the stored refresh token.
|
||||||
|
*
|
||||||
|
* @return The new access token if successful, or null if the refresh token is invalid/expired.
|
||||||
|
*/
|
||||||
suspend fun refreshToken(): String? {
|
suspend fun refreshToken(): String? {
|
||||||
val refreshToken = tokenManager.getRefreshToken() ?: return null
|
val refreshToken = tokenManager.getRefreshToken() ?: return null
|
||||||
return try {
|
return try {
|
||||||
|
|||||||
+3
@@ -1,5 +1,8 @@
|
|||||||
package com.digitoolsolutions.app.torquevaultkmp.data.network.api
|
package com.digitoolsolutions.app.torquevaultkmp.data.network.api
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constant values for API endpoints.
|
||||||
|
*/
|
||||||
object Endpoints {
|
object Endpoints {
|
||||||
const val AUTH_TOKEN = "/token/"
|
const val AUTH_TOKEN = "/token/"
|
||||||
const val AUTH_REFRESH = "/token/refresh/"
|
const val AUTH_REFRESH = "/token/refresh/"
|
||||||
|
|||||||
+12
@@ -20,6 +20,9 @@ import io.ktor.http.HttpStatusCode
|
|||||||
import io.ktor.http.contentType
|
import io.ktor.http.contentType
|
||||||
import io.ktor.http.path
|
import io.ktor.http.path
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API service for managing work orders and torque measurements.
|
||||||
|
*/
|
||||||
class WorkOrderApi(
|
class WorkOrderApi(
|
||||||
private val client: HttpClient,
|
private val client: HttpClient,
|
||||||
private val tokenManager: TokenManager
|
private val tokenManager: TokenManager
|
||||||
@@ -27,6 +30,9 @@ class WorkOrderApi(
|
|||||||
private val baseUrl: String?
|
private val baseUrl: String?
|
||||||
get() = tokenManager.getServerUrl()
|
get() = tokenManager.getServerUrl()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a list of work orders available for the device.
|
||||||
|
*/
|
||||||
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("wo_id", woID)
|
parameter("wo_id", woID)
|
||||||
@@ -34,6 +40,9 @@ class WorkOrderApi(
|
|||||||
}.body()
|
}.body()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirms that a specific device is handling a work order.
|
||||||
|
*/
|
||||||
suspend fun confirm(woID: String, deviceId: String, type: Int): Boolean {
|
suspend fun confirm(woID: String, deviceId: String, type: Int): Boolean {
|
||||||
return try {
|
return try {
|
||||||
val response = client.patch("$baseUrl/${Endpoints.WORK_ORDERS}/$woID/${Endpoints.CONFIRM}"){
|
val response = client.patch("$baseUrl/${Endpoints.WORK_ORDERS}/$woID/${Endpoints.CONFIRM}"){
|
||||||
@@ -46,6 +55,9 @@ class WorkOrderApi(
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Uploads measurement results for a specific work order.
|
||||||
|
*/
|
||||||
suspend fun sendMeasurement(woID: String, bodyPayload: SendMeasureDto, isRemeasure: Boolean = false): Result<MeasureResDto> {
|
suspend fun sendMeasurement(woID: String, bodyPayload: SendMeasureDto, isRemeasure: Boolean = false): Result<MeasureResDto> {
|
||||||
return try {
|
return try {
|
||||||
val endPath = if(isRemeasure) Endpoints.RE_MEASURE else Endpoints.MEASURE
|
val endPath = if(isRemeasure) Endpoints.RE_MEASURE else Endpoints.MEASURE
|
||||||
|
|||||||
-1
@@ -2,7 +2,6 @@ package com.digitoolsolutions.app.torquevaultkmp.data.network.dto
|
|||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class WorkOrderDto(
|
data class WorkOrderDto(
|
||||||
|
|||||||
+4
@@ -9,6 +9,10 @@ import com.digitoolsolutions.app.torquevaultkmp.domain.model.LoginResponse
|
|||||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.RefreshRequest
|
import com.digitoolsolutions.app.torquevaultkmp.domain.model.RefreshRequest
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.RefreshResponse
|
import com.digitoolsolutions.app.torquevaultkmp.domain.model.RefreshResponse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension functions to map Authentication Network DTOs to Domain models.
|
||||||
|
*/
|
||||||
|
|
||||||
fun LoginRequestDto.toDomain(): LoginRequest {
|
fun LoginRequestDto.toDomain(): LoginRequest {
|
||||||
return LoginRequest(username,password)
|
return LoginRequest(username,password)
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -6,8 +6,10 @@ import com.digitoolsolutions.app.torquevaultkmp.domain.model.MeasResult
|
|||||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.MeasureResult
|
import com.digitoolsolutions.app.torquevaultkmp.domain.model.MeasureResult
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.Service
|
import com.digitoolsolutions.app.torquevaultkmp.domain.model.Service
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder
|
import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder
|
||||||
import kotlinx.serialization.json.doubleOrNull
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
/**
|
||||||
|
* Extension functions to map Work Order Network DTOs to Domain models.
|
||||||
|
*/
|
||||||
|
|
||||||
fun WorkOrderDto.toDomain(): WorkOrder {
|
fun WorkOrderDto.toDomain(): WorkOrder {
|
||||||
return WorkOrder(
|
return WorkOrder(
|
||||||
@@ -46,6 +48,7 @@ fun WorkOrderDto.toDomain(): WorkOrder {
|
|||||||
updatedAt = updatedAt
|
updatedAt = updatedAt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MeasureResDto.toDomain(): MeasureResult {
|
fun MeasureResDto.toDomain(): MeasureResult {
|
||||||
return MeasureResult(
|
return MeasureResult(
|
||||||
status = status,
|
status = status,
|
||||||
|
|||||||
+30
@@ -8,24 +8,48 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.IO
|
import kotlinx.coroutines.IO
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository for managing bonded BLE devices in the local database.
|
||||||
|
*
|
||||||
|
* It uses SQLDelight to persist device identifiers and names, facilitating
|
||||||
|
* automatic reconnection features.
|
||||||
|
*/
|
||||||
class DeviceRepository(
|
class DeviceRepository(
|
||||||
private val dbQueries: AppDatabaseQueries
|
private val dbQueries: AppDatabaseQueries
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val PAGE_SIZE = 20L
|
const val PAGE_SIZE = 20L
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists a device to the local database.
|
||||||
|
* If the device already exists, it will be updated.
|
||||||
|
*/
|
||||||
suspend fun saveDevice(id: String, name: String) {
|
suspend fun saveDevice(id: String, name: String) {
|
||||||
dbQueries.insertDevice(id, name)
|
dbQueries.insertDevice(id, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a flow of all saved devices.
|
||||||
|
*/
|
||||||
fun getAllDevices(): Flow<List<Device>> =
|
fun getAllDevices(): Flow<List<Device>> =
|
||||||
dbQueries.getAllDevices()
|
dbQueries.getAllDevices()
|
||||||
.asFlow()
|
.asFlow()
|
||||||
.mapToList(Dispatchers.IO)
|
.mapToList(Dispatchers.IO)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a flow containing only the identifiers of all saved devices.
|
||||||
|
*/
|
||||||
suspend fun getAllDeviceIdsFlow(): Flow<List<String>> {
|
suspend fun getAllDeviceIdsFlow(): Flow<List<String>> {
|
||||||
return dbQueries.getAllDeviceIds().asFlow().mapToList(Dispatchers.IO)
|
return dbQueries.getAllDeviceIds().asFlow().mapToList(Dispatchers.IO)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a paged list of devices for efficient UI rendering.
|
||||||
|
*
|
||||||
|
* @param limit Maximum number of items to return.
|
||||||
|
* @param lastId The identifier of the last item in the previous page for cursor-based pagination.
|
||||||
|
*/
|
||||||
suspend fun getDevicesPaged(limit: Long = PAGE_SIZE, lastId: String? = null): List<Device> {
|
suspend fun getDevicesPaged(limit: Long = PAGE_SIZE, lastId: String? = null): List<Device> {
|
||||||
return if (lastId == null) {
|
return if (lastId == null) {
|
||||||
dbQueries.getDevicesFirstPage(limit).executeAsList()
|
dbQueries.getDevicesFirstPage(limit).executeAsList()
|
||||||
@@ -34,8 +58,14 @@ class DeviceRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a specific device by its identifier.
|
||||||
|
*/
|
||||||
suspend fun getDeviceById(id: String): Device? =
|
suspend fun getDeviceById(id: String): Device? =
|
||||||
dbQueries.getDeviceById(id).executeAsOneOrNull()
|
dbQueries.getDeviceById(id).executeAsOneOrNull()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a device from the bonded list.
|
||||||
|
*/
|
||||||
suspend fun deleteDevice(id: String) = dbQueries.deleteDeviceById(id)
|
suspend fun deleteDevice(id: String) = dbQueries.deleteDeviceById(id)
|
||||||
}
|
}
|
||||||
|
|||||||
+23
@@ -11,6 +11,16 @@ import kotlin.time.Clock
|
|||||||
import kotlin.uuid.ExperimentalUuidApi
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single log entry captured within the application.
|
||||||
|
*
|
||||||
|
* @property id Unique identifier for the log entry.
|
||||||
|
* @property title A short descriptive title of the event.
|
||||||
|
* @property content Detailed information about the event.
|
||||||
|
* @property timestamp Epoch time in milliseconds when the log was created.
|
||||||
|
* @property deviceId The identifier of the BLE device if the log is device-specific.
|
||||||
|
* @property source Categorization of the log (e.g., SYSTEM or DEVICE).
|
||||||
|
*/
|
||||||
data class Log(
|
data class Log(
|
||||||
val id: String,
|
val id: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
@@ -19,12 +29,25 @@ data class Log(
|
|||||||
val deviceId: String?,
|
val deviceId: String?,
|
||||||
val source: String,
|
val source: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository responsible for managing diagnostic logs in memory.
|
||||||
|
* It maintains a buffer of the last [MAX_LOGS] entries and provides
|
||||||
|
* filtering capabilities for the UI.
|
||||||
|
*/
|
||||||
class LogRepository() {
|
class LogRepository() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val MAX_LOGS = 1000
|
private const val MAX_LOGS = 1000
|
||||||
}
|
}
|
||||||
private val _logs = MutableStateFlow<List<Log>>(emptyList())
|
private val _logs = MutableStateFlow<List<Log>>(emptyList())
|
||||||
val logs: StateFlow<List<Log>> = _logs
|
val logs: StateFlow<List<Log>> = _logs
|
||||||
|
/**
|
||||||
|
* Appends a new log entry to the buffer.
|
||||||
|
*
|
||||||
|
* @param title The title of the log.
|
||||||
|
* @param content The message or data to log.
|
||||||
|
* @param deviceId Optional device identifier. If null, the log is marked as [LogFilter.SYSTEM].
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
fun appendLog(title: String, content: String, deviceId: String? = null) {
|
fun appendLog(title: String, content: String, deviceId: String? = null) {
|
||||||
val source = if (deviceId == null) LogFilter.SYSTEM else LogFilter.DEVICE
|
val source = if (deviceId == null) LogFilter.SYSTEM else LogFilter.DEVICE
|
||||||
|
|||||||
+18
-3
@@ -1,25 +1,40 @@
|
|||||||
package com.digitoolsolutions.app.torquevaultkmp.data.repository
|
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.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.MeasureResult
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder
|
import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* High-level repository used by [ScannerViewModel] to manage work order state
|
||||||
|
* for connected BLE devices.
|
||||||
|
*
|
||||||
|
* It acts as a bridge between the BLE scanning logic and the [WorkOrderRepository].
|
||||||
|
*/
|
||||||
class ScannerRepository(private val woRepository: WorkOrderRepository) {
|
class ScannerRepository(private val woRepository: WorkOrderRepository) {
|
||||||
|
/**
|
||||||
|
* Fetches a list of work orders available for a device.
|
||||||
|
*/
|
||||||
suspend fun getAbleWorkOrders(woID: String = "0", action: String = "next"): List<WorkOrder> {
|
suspend fun getAbleWorkOrders(woID: String = "0", action: String = "next"): List<WorkOrder> {
|
||||||
return woRepository.fetchAbleWorkOrders(woID, action)
|
return woRepository.fetchAbleWorkOrders(woID, action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirms the selection of a specific work order on a device.
|
||||||
|
*/
|
||||||
suspend fun confirmWorkOrder(woID: String, deviceId: String, type: Int): Boolean {
|
suspend fun confirmWorkOrder(woID: String, deviceId: String, type: Int): Boolean {
|
||||||
return woRepository.confirm(woID, deviceId, type)
|
return woRepository.confirm(woID, deviceId, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads measurement results received from the BLE device to the server.
|
||||||
|
*/
|
||||||
suspend fun sendMeasurementResult(woId: String, data: SendMeasureDto, isReTorque: Boolean = false): Result<MeasureResult> {
|
suspend fun sendMeasurementResult(woId: String, data: SendMeasureDto, isReTorque: Boolean = false): Result<MeasureResult> {
|
||||||
return woRepository.sendMeasurement(woId, data, isReTorque)
|
return woRepository.sendMeasurement(woId, data, isReTorque)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the server that a work order has been cancelled by the operator on the device.
|
||||||
|
*/
|
||||||
suspend fun sendCancelWorkOrder(woId: String, data: SendMeasureDto): Result<WorkOrder> {
|
suspend fun sendCancelWorkOrder(woId: String, data: SendMeasureDto): Result<WorkOrder> {
|
||||||
return woRepository.cancelWorkOrder(woId, data)
|
return woRepository.cancelWorkOrder(woId, data)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -1,10 +1,12 @@
|
|||||||
package com.digitoolsolutions.app.torquevaultkmp.data.repository
|
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.data.network.dto.SendMeasureDto
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.MeasureResult
|
import com.digitoolsolutions.app.torquevaultkmp.domain.model.MeasureResult
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder
|
import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface defining the operations for managing work orders via remote API.
|
||||||
|
*/
|
||||||
interface WorkOrderRepository {
|
interface WorkOrderRepository {
|
||||||
suspend fun fetchAbleWorkOrders(woId: String, action: String): List<WorkOrder>
|
suspend fun fetchAbleWorkOrders(woId: String, action: String): List<WorkOrder>
|
||||||
suspend fun confirm(woID: String, deviceId: String, type: Int): Boolean
|
suspend fun confirm(woID: String, deviceId: String, type: Int): Boolean
|
||||||
|
|||||||
+4
-1
@@ -5,6 +5,9 @@ import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.SendMeasureDto
|
|||||||
import com.digitoolsolutions.app.torquevaultkmp.data.network.mapper.toDomain
|
import com.digitoolsolutions.app.torquevaultkmp.data.network.mapper.toDomain
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.MeasureResult
|
import com.digitoolsolutions.app.torquevaultkmp.domain.model.MeasureResult
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder
|
import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder
|
||||||
|
/**
|
||||||
|
* Implementation of [WorkOrderRepository] that communicates with the backend API.
|
||||||
|
*/
|
||||||
class WorkOrderRepositoryImpl(private val api: WorkOrderApi) : WorkOrderRepository {
|
class WorkOrderRepositoryImpl(private val api: WorkOrderApi) : WorkOrderRepository {
|
||||||
override suspend fun fetchAbleWorkOrders(woId: String, action: String): List<WorkOrder> {
|
override suspend fun fetchAbleWorkOrders(woId: String, action: String): List<WorkOrder> {
|
||||||
val dto = api.fetchAbleWorkOrders(woId, action)
|
val dto = api.fetchAbleWorkOrders(woId, action)
|
||||||
@@ -16,7 +19,7 @@ class WorkOrderRepositoryImpl(private val api: WorkOrderApi) : WorkOrderReposito
|
|||||||
deviceId: String,
|
deviceId: String,
|
||||||
type: Int
|
type: Int
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return api.confirm(woID,deviceId, type)
|
return api.confirm(woID, deviceId, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sendMeasurement(woId: String, data: SendMeasureDto, isReTorque: Boolean): Result<MeasureResult> {
|
override suspend fun sendMeasurement(woId: String, data: SendMeasureDto, isReTorque: Boolean): Result<MeasureResult> {
|
||||||
|
|||||||
+22
@@ -3,10 +3,32 @@ package com.digitoolsolutions.app.torquevaultkmp.data.storage
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Platform-agnostic interface for persistent key-value storage.
|
||||||
|
*
|
||||||
|
* Implementations should handle storing simple data types like Booleans,
|
||||||
|
* Strings, and Numbers using native mechanisms (e.g., SharedPreferences on Android,
|
||||||
|
* NSUserDefaults on iOS).
|
||||||
|
*/
|
||||||
interface AppStorage {
|
interface AppStorage {
|
||||||
|
/**
|
||||||
|
* Persists a value for the given key.
|
||||||
|
*/
|
||||||
fun <T : Any> save(key: String, value: T)
|
fun <T : Any> save(key: String, value: T)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a value for the given key.
|
||||||
|
*/
|
||||||
fun <T : Any> load(key: String, type: KClass<T>): T?
|
fun <T : Any> load(key: String, type: KClass<T>): T?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the data associated with the given key.
|
||||||
|
*/
|
||||||
fun remove(key: String)
|
fun remove(key: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a flow that emits updates whenever the value for the given key changes.
|
||||||
|
*/
|
||||||
fun <T : Any> observe(key: String, type: KClass<T>): Flow<T>
|
fun <T : Any> observe(key: String, type: KClass<T>): Flow<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
@@ -1,9 +1,21 @@
|
|||||||
package com.digitoolsolutions.app.torquevaultkmp.data.storage
|
package com.digitoolsolutions.app.torquevaultkmp.data.storage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants used as keys for persistent storage throughout the application.
|
||||||
|
*/
|
||||||
object ReferKeys {
|
object ReferKeys {
|
||||||
|
/** The access token used for authenticated API requests. */
|
||||||
const val ACCESS_TOKEN = "access_token"
|
const val ACCESS_TOKEN = "access_token"
|
||||||
|
|
||||||
|
/** The refresh token used to obtain new access tokens. */
|
||||||
const val REFRESH_TOKEN = "refresh_token"
|
const val REFRESH_TOKEN = "refresh_token"
|
||||||
|
|
||||||
|
/** The base URL of the remote server. */
|
||||||
const val SERVER_URL = "server_url"
|
const val SERVER_URL = "server_url"
|
||||||
|
|
||||||
|
/** User preference for Dark Mode (Boolean). */
|
||||||
const val THEME = "theme"
|
const val THEME = "theme"
|
||||||
|
|
||||||
|
/** User preference for automatic reconnection to bonded devices (Boolean). */
|
||||||
const val DEVICE_AUTO_CONNECT = "device_auto_connect"
|
const val DEVICE_AUTO_CONNECT = "device_auto_connect"
|
||||||
}
|
}
|
||||||
|
|||||||
+12
@@ -3,10 +3,22 @@ package com.digitoolsolutions.app.torquevaultkmp.data.storage
|
|||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages authentication tokens and server URL configuration.
|
||||||
|
*
|
||||||
|
* This class handles token caching, persistence via [AppStorage], and
|
||||||
|
* notifies the application when the user session has expired.
|
||||||
|
*/
|
||||||
class TokenManager(private val storage: AppStorage) {
|
class TokenManager(private val storage: AppStorage) {
|
||||||
|
/**
|
||||||
|
* A flow that emits when the authentication session has expired.
|
||||||
|
*/
|
||||||
private val _sessionExpired = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
private val _sessionExpired = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||||
val sessionExpired = _sessionExpired.asSharedFlow()
|
val sessionExpired = _sessionExpired.asSharedFlow()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers the session expiration event, typically called by a Ktor interceptor.
|
||||||
|
*/
|
||||||
fun triggerSessionExpired() {
|
fun triggerSessionExpired() {
|
||||||
_sessionExpired.tryEmit(Unit)
|
_sessionExpired.tryEmit(Unit)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ package com.digitoolsolutions.app.torquevaultkmp.di
|
|||||||
import org.koin.core.context.startKoin
|
import org.koin.core.context.startKoin
|
||||||
import org.koin.dsl.KoinAppDeclaration
|
import org.koin.dsl.KoinAppDeclaration
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the Koin dependency injection framework.
|
||||||
|
*
|
||||||
|
* This function is called from both Android and iOS entry points to
|
||||||
|
* set up the shared and platform-specific modules.
|
||||||
|
*/
|
||||||
fun initKoin(config: KoinAppDeclaration? = null) {
|
fun initKoin(config: KoinAppDeclaration? = null) {
|
||||||
startKoin {
|
startKoin {
|
||||||
config?.invoke(this)
|
config?.invoke(this)
|
||||||
|
|||||||
+15
@@ -25,14 +25,23 @@ import org.koin.core.module.dsl.singleOf
|
|||||||
import org.koin.core.qualifier.named
|
import org.koin.core.qualifier.named
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Platform-specific module to be provided by each target (Android, iOS).
|
||||||
|
*/
|
||||||
expect val platformModule: Module
|
expect val platformModule: Module
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared application module providing core utility classes.
|
||||||
|
*/
|
||||||
val appModule = module {
|
val appModule = module {
|
||||||
// TokenManager use AppStorage
|
// TokenManager use AppStorage
|
||||||
single { TokenManager(get()) }
|
single { TokenManager(get()) }
|
||||||
singleOf(::BleManager)
|
singleOf(::BleManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared storage module providing database, repositories, and local persistence logic.
|
||||||
|
*/
|
||||||
val storageModule = module {
|
val storageModule = module {
|
||||||
single { AppDatabase(get()) }
|
single { AppDatabase(get()) }
|
||||||
single { get<AppDatabase>().appDatabaseQueries }
|
single { get<AppDatabase>().appDatabaseQueries }
|
||||||
@@ -41,6 +50,9 @@ val storageModule = module {
|
|||||||
single { ScannerRepository(get()) }
|
single { ScannerRepository(get()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared network module providing Ktor clients, API services, and network-bound repositories.
|
||||||
|
*/
|
||||||
val networkModule = module {
|
val networkModule = module {
|
||||||
// Auth client (no interceptor to avoid circular dependency and recursion)
|
// Auth client (no interceptor to avoid circular dependency and recursion)
|
||||||
single(named("authClient")) { createPlatformHttpClient(get(), null) }
|
single(named("authClient")) { createPlatformHttpClient(get(), null) }
|
||||||
@@ -56,6 +68,9 @@ val networkModule = module {
|
|||||||
factory { FetchWorkOrdersUseCase(get()) }
|
factory { FetchWorkOrdersUseCase(get()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared ViewModel module providing state management for the UI screens.
|
||||||
|
*/
|
||||||
val viewModelModule = module {
|
val viewModelModule = module {
|
||||||
factoryOf(::HomeViewModel)
|
factoryOf(::HomeViewModel)
|
||||||
factory { AuthViewModel(get(), get()) }
|
factory { AuthViewModel(get(), get()) }
|
||||||
|
|||||||
+21
-6
@@ -1,13 +1,21 @@
|
|||||||
package com.digitoolsolutions.app.torquevaultkmp.domain.model
|
package com.digitoolsolutions.app.torquevaultkmp.domain.model
|
||||||
|
/**
|
||||||
|
* Mapping of wheel positions to bitmask values for the UART protocol.
|
||||||
|
*/
|
||||||
val wheelsCode = mapOf(
|
val wheelsCode = mapOf(
|
||||||
"DF" to 0b0001,
|
"DF" to 0b0001, // Driver Front
|
||||||
"PF" to 0b0010,
|
"PF" to 0b0010, // Passenger Front
|
||||||
"DR" to 0b0100,
|
"DR" to 0b0100, // Driver Rear
|
||||||
"PR" to 0b1000,
|
"PR" to 0b1000, // Passenger Rear
|
||||||
"DRO" to 0b00010000,
|
"DRO" to 0b00010000, // Driver Rear Outer
|
||||||
"PRO" to 0b100000
|
"PRO" to 0b100000 // Passenger Rear Outer
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes a bitmask representing the wheels and the number of nuts for the protocol.
|
||||||
|
* The resulting 16-bit integer contains the wheel mask in the upper 8 bits
|
||||||
|
* and the nut count in the lower 8 bits.
|
||||||
|
*/
|
||||||
fun WorkOrder.computeWheelsNuts(): Int {
|
fun WorkOrder.computeWheelsNuts(): Int {
|
||||||
var wNut = 0
|
var wNut = 0
|
||||||
var nutCount = 0
|
var nutCount = 0
|
||||||
@@ -22,6 +30,9 @@ fun WorkOrder.computeWheelsNuts(): Int {
|
|||||||
return (wNut shl 8) or nutCount
|
return (wNut shl 8) or nutCount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain model representing a Work Order.
|
||||||
|
*/
|
||||||
data class WorkOrder(
|
data class WorkOrder(
|
||||||
val id: String,
|
val id: String,
|
||||||
val status: String,
|
val status: String,
|
||||||
@@ -35,6 +46,10 @@ data class WorkOrder(
|
|||||||
val createdAt: String,
|
val createdAt: String,
|
||||||
val updatedAt: String?
|
val updatedAt: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Details of a service requested within a [WorkOrder], specifically for torque tasks.
|
||||||
|
*/
|
||||||
data class Service(
|
data class Service(
|
||||||
val nut: Int?,
|
val nut: Int?,
|
||||||
val status: Int,
|
val status: Int,
|
||||||
|
|||||||
+10
@@ -3,7 +3,17 @@ package com.digitoolsolutions.app.torquevaultkmp.domain.usecase
|
|||||||
import com.digitoolsolutions.app.torquevaultkmp.data.repository.WorkOrderRepository
|
import com.digitoolsolutions.app.torquevaultkmp.data.repository.WorkOrderRepository
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder
|
import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use case responsible for retrieving work orders from the repository.
|
||||||
|
*/
|
||||||
class FetchWorkOrdersUseCase(private val repository: WorkOrderRepository) {
|
class FetchWorkOrdersUseCase(private val repository: WorkOrderRepository) {
|
||||||
|
/**
|
||||||
|
* Executes the use case to fetch work orders.
|
||||||
|
*
|
||||||
|
* @param page Current page index (for pagination).
|
||||||
|
* @param woId Pivot work order identifier for cursor-based navigation.
|
||||||
|
* @param action Navigation direction: "next" or "back".
|
||||||
|
*/
|
||||||
suspend operator fun invoke(page: Int, woId: String = "0", action: String = "next"): List<WorkOrder> {
|
suspend operator fun invoke(page: Int, woId: String = "0", action: String = "next"): List<WorkOrder> {
|
||||||
return repository.fetchAbleWorkOrders(woId, action)
|
return repository.fetchAbleWorkOrders(woId, action)
|
||||||
}
|
}
|
||||||
|
|||||||
+36
@@ -15,11 +15,22 @@ import kotlin.uuid.ExperimentalUuidApi
|
|||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper around the Kable library to handle Bluetooth Low Energy operations.
|
||||||
|
*
|
||||||
|
* This manager provides high-level abstractions for scanning, connecting, and
|
||||||
|
* communicating with digital torque devices using a custom UART protocol.
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
class BleManager {
|
class BleManager {
|
||||||
companion object {
|
companion object {
|
||||||
|
/** The primary Service UUID for the torque device UART protocol. */
|
||||||
val SERVICE_UUID = Uuid.parse("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
|
val SERVICE_UUID = Uuid.parse("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
|
||||||
|
|
||||||
|
/** The characteristic used to send commands to the device. */
|
||||||
val RX_CHAR = characteristicOf(SERVICE_UUID, Uuid.parse("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"))
|
val RX_CHAR = characteristicOf(SERVICE_UUID, Uuid.parse("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"))
|
||||||
|
|
||||||
|
/** The characteristic used to receive data from the device. */
|
||||||
val TX_CHAR = characteristicOf(SERVICE_UUID, Uuid.parse("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"))
|
val TX_CHAR = characteristicOf(SERVICE_UUID, Uuid.parse("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +42,11 @@ class BleManager {
|
|||||||
fun getAdvertisement(identifier: String): Advertisement? = advertisements[identifier]
|
fun getAdvertisement(identifier: String): Advertisement? = advertisements[identifier]
|
||||||
fun getAdvertisementName(identifier: String): String = advertisements[identifier]?.name.toString()
|
fun getAdvertisementName(identifier: String): String = advertisements[identifier]?.name.toString()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans for nearby torque devices that support the [SERVICE_UUID].
|
||||||
|
*
|
||||||
|
* @return A flow of [Advertisement] discovered during the scan.
|
||||||
|
*/
|
||||||
fun scanDevices(): Flow<Advertisement> = Scanner {
|
fun scanDevices(): Flow<Advertisement> = Scanner {
|
||||||
filters {
|
filters {
|
||||||
match {
|
match {
|
||||||
@@ -47,6 +63,12 @@ class BleManager {
|
|||||||
it
|
it
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Establishes a connection to a specific peripheral.
|
||||||
|
*
|
||||||
|
* @param advertisement The advertisement discovered during scanning.
|
||||||
|
* @return A [Peripheral] instance for further interaction.
|
||||||
|
*/
|
||||||
suspend fun connect(advertisement: Advertisement): Peripheral {
|
suspend fun connect(advertisement: Advertisement): Peripheral {
|
||||||
val identifier = advertisement.identifier.toString()
|
val identifier = advertisement.identifier.toString()
|
||||||
Napier.d(">>>>> Connecting to $identifier")
|
Napier.d(">>>>> Connecting to $identifier")
|
||||||
@@ -57,23 +79,37 @@ class BleManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnects a peripheral by its identifier.
|
||||||
|
*/
|
||||||
suspend fun disconnect(identifier: String) {
|
suspend fun disconnect(identifier: String) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
peripherals.remove(identifier)?.disconnect()
|
peripherals.remove(identifier)?.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnects all currently active BLE connections.
|
||||||
|
*/
|
||||||
suspend fun disconnectAll() {
|
suspend fun disconnectAll() {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
peripherals.values.forEach { it.disconnect() }
|
peripherals.values.forEach { it.disconnect() }
|
||||||
peripherals.clear()
|
peripherals.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a UART command string to the connected peripheral.
|
||||||
|
*/
|
||||||
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("\\n", "\n")
|
val cmd = command.replace("\\n", "\n")
|
||||||
peripheral.write(RX_CHAR, cmd.encodeToByteArray())
|
peripheral.write(RX_CHAR, cmd.encodeToByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a flow that emits incoming UART data from the peripheral.
|
||||||
|
*/
|
||||||
fun observeRx(peripheral: Peripheral): Flow<String> =
|
fun observeRx(peripheral: Peripheral): Flow<String> =
|
||||||
peripheral.observe(TX_CHAR).map { it.decodeToString() }
|
peripheral.observe(TX_CHAR).map { it.decodeToString() }
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -3,11 +3,22 @@ package com.digitoolsolutions.app.torquevaultkmp.screens
|
|||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import org.jetbrains.compose.resources.DrawableResource
|
import org.jetbrains.compose.resources.DrawableResource
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for different types of icons used in the navigation system.
|
||||||
|
*/
|
||||||
sealed class AppIcon {
|
sealed class AppIcon {
|
||||||
|
/** Uses a compiled Compose Multiplatform resource. */
|
||||||
data class Resource(val resId: DrawableResource) : AppIcon()
|
data class Resource(val resId: DrawableResource) : AppIcon()
|
||||||
|
|
||||||
|
/** Uses a standard Material [ImageVector]. */
|
||||||
data class Vector(val imageVector: ImageVector) : AppIcon()
|
data class Vector(val imageVector: ImageVector) : AppIcon()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for all navigation destinations within the app.
|
||||||
|
*
|
||||||
|
* Each destination defines its display [label], associated [icon], and unique [route].
|
||||||
|
*/
|
||||||
open class Destinations(
|
open class Destinations(
|
||||||
val label: String,
|
val label: String,
|
||||||
val icon: AppIcon,
|
val icon: AppIcon,
|
||||||
|
|||||||
+21
@@ -8,6 +8,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the state of the login process.
|
||||||
|
*/
|
||||||
sealed class LoginUiState {
|
sealed class LoginUiState {
|
||||||
object Idle : LoginUiState()
|
object Idle : LoginUiState()
|
||||||
object Loading : LoginUiState()
|
object Loading : LoginUiState()
|
||||||
@@ -15,21 +18,39 @@ sealed class LoginUiState {
|
|||||||
data class Error(val message: String) : LoginUiState()
|
data class Error(val message: String) : LoginUiState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewModel for the Login screen.
|
||||||
|
*
|
||||||
|
* Handles user authentication and server URL configuration during the login process.
|
||||||
|
*/
|
||||||
class AuthViewModel(
|
class AuthViewModel(
|
||||||
private val authApi: AuthApi,
|
private val authApi: AuthApi,
|
||||||
private val tokenManager: TokenManager
|
private val tokenManager: TokenManager
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable state of the login operation.
|
||||||
|
*/
|
||||||
private val _loginState = MutableStateFlow<LoginUiState>(LoginUiState.Idle)
|
private val _loginState = MutableStateFlow<LoginUiState>(LoginUiState.Idle)
|
||||||
val loginState = _loginState.asStateFlow()
|
val loginState = _loginState.asStateFlow()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current server URL entered by the user or loaded from storage.
|
||||||
|
*/
|
||||||
private val _serverUrl = MutableStateFlow(tokenManager.getBaseUrl() ?: "http://digitoolsolutions.synology.me:3000")
|
private val _serverUrl = MutableStateFlow(tokenManager.getBaseUrl() ?: "http://digitoolsolutions.synology.me:3000")
|
||||||
val serverUrl = _serverUrl.asStateFlow()
|
val serverUrl = _serverUrl.asStateFlow()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the server URL in the UI state.
|
||||||
|
*/
|
||||||
fun onChangeServerUrl(url: String) {
|
fun onChangeServerUrl(url: String) {
|
||||||
_serverUrl.value = url
|
_serverUrl.value = url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to log in the user with the provided credentials.
|
||||||
|
* Before logging in, it saves the current [serverUrl] to [TokenManager].
|
||||||
|
*/
|
||||||
fun login(username: String, password: String) {
|
fun login(username: String, password: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_loginState.value = LoginUiState.Loading
|
_loginState.value = LoginUiState.Loading
|
||||||
|
|||||||
+6
@@ -68,6 +68,9 @@ import torquevaultkmp.composeapp.generated.resources.lbl_server_url
|
|||||||
import torquevaultkmp.composeapp.generated.resources.lbl_username
|
import torquevaultkmp.composeapp.generated.resources.lbl_username
|
||||||
import torquevaultkmp.composeapp.generated.resources.server_url_placeholder
|
import torquevaultkmp.composeapp.generated.resources.server_url_placeholder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Screen for user authentication.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun LoginScreen(
|
fun LoginScreen(
|
||||||
navController: NavController
|
navController: NavController
|
||||||
@@ -92,6 +95,9 @@ fun LoginScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stateless UI content for the login screen.
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LoginScreenContent(
|
fun LoginScreenContent(
|
||||||
|
|||||||
+14
@@ -10,16 +10,27 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewModel for the Bonded Devices screen.
|
||||||
|
*
|
||||||
|
* Manages the list of devices that have been previously connected and saved
|
||||||
|
* to the local database, supporting cursor-based pagination.
|
||||||
|
*/
|
||||||
class BondedViewModel(
|
class BondedViewModel(
|
||||||
private val deviceRepository: DeviceRepository
|
private val deviceRepository: DeviceRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable list of bonded devices.
|
||||||
|
*/
|
||||||
private val _bondedDevices = MutableStateFlow<List<Device>>(emptyList())
|
private val _bondedDevices = MutableStateFlow<List<Device>>(emptyList())
|
||||||
val bondedDevices: StateFlow<List<Device>> = _bondedDevices.asStateFlow()
|
val bondedDevices: StateFlow<List<Device>> = _bondedDevices.asStateFlow()
|
||||||
|
|
||||||
private var lastId: String? = null
|
private var lastId: String? = null
|
||||||
private val pageSize = DeviceRepository.PAGE_SIZE
|
private val pageSize = DeviceRepository.PAGE_SIZE
|
||||||
private var isLastPage = false
|
private var isLastPage = false
|
||||||
|
|
||||||
|
/** Indicates if a background data fetch is currently in progress. */
|
||||||
var isLoading = MutableStateFlow(false)
|
var isLoading = MutableStateFlow(false)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -27,6 +38,9 @@ class BondedViewModel(
|
|||||||
loadNextPage()
|
loadNextPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the next page of bonded devices from the database.
|
||||||
|
*/
|
||||||
fun loadNextPage() {
|
fun loadNextPage() {
|
||||||
if (isLoading.value || isLastPage) return
|
if (isLoading.value || isLastPage) return
|
||||||
|
|
||||||
|
|||||||
+6
@@ -27,6 +27,12 @@ import torquevaultkmp.composeapp.generated.resources.Res
|
|||||||
import torquevaultkmp.composeapp.generated.resources.action_cancel
|
import torquevaultkmp.composeapp.generated.resources.action_cancel
|
||||||
import torquevaultkmp.composeapp.generated.resources.action_retry
|
import torquevaultkmp.composeapp.generated.resources.action_retry
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Screen responsible for real-time UART communication with a connected BLE device.
|
||||||
|
*
|
||||||
|
* It manages different UI states based on the connection status (Connecting, Connected, Disconnected)
|
||||||
|
* and provides a terminal-like interface for sending and receiving messages.
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun UartCommunicationScreen(
|
fun UartCommunicationScreen(
|
||||||
|
|||||||
+5
@@ -29,6 +29,11 @@ import torquevaultkmp.composeapp.generated.resources.Res
|
|||||||
import torquevaultkmp.composeapp.generated.resources.home_title
|
import torquevaultkmp.composeapp.generated.resources.home_title
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The landing screen after a successful login.
|
||||||
|
*
|
||||||
|
* Displays currently connected devices and provides a gateway to communicate with them.
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen(
|
fun HomeScreen(
|
||||||
|
|||||||
+13
@@ -12,11 +12,24 @@ import kotlinx.coroutines.flow.asSharedFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewModel for the Home Screen.
|
||||||
|
*
|
||||||
|
* Manages the fetching and display of the list of work orders available
|
||||||
|
* for processing.
|
||||||
|
*/
|
||||||
class HomeViewModel(
|
class HomeViewModel(
|
||||||
private val fetchWorkOrdersUseCase: FetchWorkOrdersUseCase
|
private val fetchWorkOrdersUseCase: FetchWorkOrdersUseCase
|
||||||
): ViewModel() {
|
): ViewModel() {
|
||||||
|
/**
|
||||||
|
* Observable list of work orders.
|
||||||
|
*/
|
||||||
private val _workOrders = MutableStateFlow<List<WorkOrder>>(emptyList())
|
private val _workOrders = MutableStateFlow<List<WorkOrder>>(emptyList())
|
||||||
val workOrders = _workOrders.asStateFlow()
|
val workOrders = _workOrders.asStateFlow()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the initial page of work orders.
|
||||||
|
*/
|
||||||
fun loadWorkOrders() {
|
fun loadWorkOrders() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+9
@@ -62,6 +62,12 @@ import org.koin.compose.viewmodel.koinViewModel
|
|||||||
import torquevaultkmp.composeapp.generated.resources.Res
|
import torquevaultkmp.composeapp.generated.resources.Res
|
||||||
import torquevaultkmp.composeapp.generated.resources.log_title
|
import torquevaultkmp.composeapp.generated.resources.log_title
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Screen used to view, filter, and export diagnostic logs.
|
||||||
|
*
|
||||||
|
* Logs are displayed in chronological order (newest first) and can be filtered by source
|
||||||
|
* (System or specific Device). Users can also export logs as Plain Text or CSV files.
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LogScreen(
|
fun LogScreen(
|
||||||
@@ -80,6 +86,9 @@ fun LogScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stateless UI content for the diagnostic logs screen.
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun LogContent(
|
internal fun LogContent(
|
||||||
|
|||||||
+25
@@ -29,15 +29,27 @@ import com.digitoolsolutions.app.torquevaultkmp.utils.NetworkStatus
|
|||||||
import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandler
|
import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandler
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data class representing the current state of system requirements.
|
||||||
|
*/
|
||||||
data class Requirements(
|
data class Requirements(
|
||||||
|
/** Whether the device supports Bluetooth Low Energy. */
|
||||||
val hasBleFeature: Boolean,
|
val hasBleFeature: Boolean,
|
||||||
|
/** Whether Bluetooth is currently turned on. */
|
||||||
val isEnabled: Boolean,
|
val isEnabled: Boolean,
|
||||||
|
/** Whether the app has been granted necessary Bluetooth/Location permissions. */
|
||||||
val hasPermission: Boolean,
|
val hasPermission: Boolean,
|
||||||
|
/** Whether the user has permanently denied the required permissions. */
|
||||||
val hasPermanentlyDenied: Boolean,
|
val hasPermanentlyDenied: Boolean,
|
||||||
|
/** Whether Location services are enabled (required for BLE on some Android versions). */
|
||||||
val isLocationEnabled: Boolean,
|
val isLocationEnabled: Boolean,
|
||||||
|
/** Whether the device has active internet connectivity. */
|
||||||
val networkAvailable: Boolean,
|
val networkAvailable: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sealed class representing the mutually exclusive states of requirement fulfillment.
|
||||||
|
*/
|
||||||
sealed class RequirementState {
|
sealed class RequirementState {
|
||||||
object BleUnsupported : RequirementState()
|
object BleUnsupported : RequirementState()
|
||||||
object BluetoothDisabled : RequirementState()
|
object BluetoothDisabled : RequirementState()
|
||||||
@@ -48,6 +60,9 @@ sealed class RequirementState {
|
|||||||
object Ready : RequirementState()
|
object Ready : RequirementState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the raw [Requirements] data into a simplified [RequirementState].
|
||||||
|
*/
|
||||||
fun Requirements.toState(): RequirementState {
|
fun Requirements.toState(): RequirementState {
|
||||||
return when {
|
return when {
|
||||||
!hasBleFeature -> RequirementState.BleUnsupported
|
!hasBleFeature -> RequirementState.BleUnsupported
|
||||||
@@ -64,6 +79,16 @@ fun Requirements.toState(): RequirementState {
|
|||||||
else -> RequirementState.Ready
|
else -> RequirementState.Ready
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* A wrapper component that ensures all system requirements (BLE, Permissions, Location, Network)
|
||||||
|
* are met before allowing interaction with the main application content.
|
||||||
|
*
|
||||||
|
* It displays an overlay if any requirement is missing and provides actions to resolve them.
|
||||||
|
*
|
||||||
|
* @param onReady Callback triggered when all requirements are satisfied.
|
||||||
|
* @param onPause Callback triggered when a requirement is lost.
|
||||||
|
* @param content The main application content to be displayed (usually behind the requirement overlay).
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun RequirementWrapper(
|
fun RequirementWrapper(
|
||||||
onReady: () -> Unit,
|
onReady: () -> Unit,
|
||||||
|
|||||||
+9
-4
@@ -33,6 +33,12 @@ import torquevaultkmp.composeapp.generated.resources.scan_empty_title
|
|||||||
import torquevaultkmp.composeapp.generated.resources.scanner_title
|
import torquevaultkmp.composeapp.generated.resources.scanner_title
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Screen used to discover nearby Bluetooth Low Energy devices.
|
||||||
|
*
|
||||||
|
* Users can initiate scans, view a list of found devices with their signal strength (RSSI),
|
||||||
|
* and tap on a device to connect or navigate to its communication interface.
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ScannerScreen(
|
fun ScannerScreen(
|
||||||
@@ -113,12 +119,11 @@ fun ScannerScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LaunchedEffect(Unit) {
|
|
||||||
// viewModel.startScan()
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list item representing a discovered Bluetooth device.
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DeviceListItem(
|
fun DeviceListItem(
|
||||||
|
|||||||
+8
-2
@@ -111,8 +111,14 @@ class ScannerViewModel(
|
|||||||
private var stabilized = false
|
private var stabilized = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record advertisement: collect (20 times) intervals between consecutive ads
|
* Records advertisement intervals to calibrate the cleanup expiration time.
|
||||||
* to calibrate the cleanup expiration time when a device stops advertising.
|
*
|
||||||
|
* Digital torque wrenches may have different advertising intervals. By observing the
|
||||||
|
* time delta between consecutive advertisements, the app calculates a dynamic
|
||||||
|
* [ADV_EXPIRATION_TIME]. This ensures that when a device stops advertising,
|
||||||
|
* it is removed from the list promptly but without flickering.
|
||||||
|
*
|
||||||
|
* Calibration stabilizes after collecting 20 samples from a single device.
|
||||||
*/
|
*/
|
||||||
private fun calibrateCleanupTime(deviceId: String, now: TimeMark) {
|
private fun calibrateCleanupTime(deviceId: String, now: TimeMark) {
|
||||||
if (stabilized) return
|
if (stabilized) return
|
||||||
|
|||||||
+24
@@ -9,36 +9,60 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewModel for the Settings screen.
|
||||||
|
*
|
||||||
|
* Allows users to configure application-wide preferences such as server URL,
|
||||||
|
* authentication tokens, and automatic reconnection settings.
|
||||||
|
*/
|
||||||
class SettingViewModel(
|
class SettingViewModel(
|
||||||
private val tokenManager: TokenManager,
|
private val tokenManager: TokenManager,
|
||||||
private val storage: AppStorage
|
private val storage: AppStorage
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
/** The base server URL. */
|
||||||
private val _serverUrl = MutableStateFlow(tokenManager.getBaseUrl() ?: "")
|
private val _serverUrl = MutableStateFlow(tokenManager.getBaseUrl() ?: "")
|
||||||
val serverUrl: StateFlow<String> = _serverUrl.asStateFlow()
|
val serverUrl: StateFlow<String> = _serverUrl.asStateFlow()
|
||||||
|
|
||||||
|
/** The current refresh token. */
|
||||||
private val _refreshToken = MutableStateFlow(tokenManager.getRefreshToken() ?: "")
|
private val _refreshToken = MutableStateFlow(tokenManager.getRefreshToken() ?: "")
|
||||||
val refreshToken: StateFlow<String> = _refreshToken.asStateFlow()
|
val refreshToken: StateFlow<String> = _refreshToken.asStateFlow()
|
||||||
|
|
||||||
|
/** Whether the app should automatically attempt to connect to known devices. */
|
||||||
private val _autoConnect = MutableStateFlow(storage.load<Boolean>(ReferKeys.DEVICE_AUTO_CONNECT) ?: false)
|
private val _autoConnect = MutableStateFlow(storage.load<Boolean>(ReferKeys.DEVICE_AUTO_CONNECT) ?: false)
|
||||||
val autoConnect: StateFlow<Boolean> = _autoConnect.asStateFlow()
|
val autoConnect: StateFlow<Boolean> = _autoConnect.asStateFlow()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the server URL in the UI state.
|
||||||
|
*/
|
||||||
fun updateServerUrl(url: String) {
|
fun updateServerUrl(url: String) {
|
||||||
_serverUrl.value = url
|
_serverUrl.value = url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the refresh token in the UI state.
|
||||||
|
*/
|
||||||
fun updateRefreshToken(token: String) {
|
fun updateRefreshToken(token: String) {
|
||||||
_refreshToken.value = token
|
_refreshToken.value = token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists the current server URL and tokens to storage.
|
||||||
|
*/
|
||||||
fun saveSettings() {
|
fun saveSettings() {
|
||||||
tokenManager.saveServerUrl(_serverUrl.value)
|
tokenManager.saveServerUrl(_serverUrl.value)
|
||||||
tokenManager.saveTokens("", _refreshToken.value)
|
tokenManager.saveTokens("", _refreshToken.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all authentication tokens and logs the user out.
|
||||||
|
*/
|
||||||
fun logout() {
|
fun logout() {
|
||||||
tokenManager.clearTokens()
|
tokenManager.clearTokens()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the auto-connect preference and persists it.
|
||||||
|
*/
|
||||||
fun updateAutoConnect(enabled: Boolean) {
|
fun updateAutoConnect(enabled: Boolean) {
|
||||||
_autoConnect.value = enabled
|
_autoConnect.value = enabled
|
||||||
storage.save(ReferKeys.DEVICE_AUTO_CONNECT, enabled)
|
storage.save(ReferKeys.DEVICE_AUTO_CONNECT, enabled)
|
||||||
|
|||||||
+14
-1
@@ -2,10 +2,23 @@ package com.digitoolsolutions.app.torquevaultkmp.utils
|
|||||||
|
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when a remote API request fails with a non-success status code.
|
||||||
|
*
|
||||||
|
* @property status The HTTP status code returned by the server.
|
||||||
|
* @property body The raw response body containing error details.
|
||||||
|
*/
|
||||||
class ApiException(val status: HttpStatusCode, val body: String) : Exception(
|
class ApiException(val status: HttpStatusCode, val body: String) : Exception(
|
||||||
"API error ${status.value}: $body"
|
"API error ${status.value}: $body"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception used internally to trigger a token refresh and request retry.
|
||||||
|
*/
|
||||||
class RetryWithNewTokenException : Exception(">>>>> Access token expired, retry with new token")
|
class RetryWithNewTokenException : Exception(">>>>> Access token expired, retry with new token")
|
||||||
class ForceLogoutException : Exception(">>>>> Refresh token expired, force logout")
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception used to signal that both access and refresh tokens are invalid,
|
||||||
|
* requiring the user to re-authenticate.
|
||||||
|
*/
|
||||||
|
class ForceLogoutException : Exception(">>>>> Refresh token expired, force logout")
|
||||||
|
|||||||
+6
@@ -1,5 +1,11 @@
|
|||||||
package com.digitoolsolutions.app.torquevaultkmp.utils
|
package com.digitoolsolutions.app.torquevaultkmp.utils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface providing basic application metadata.
|
||||||
|
*/
|
||||||
interface AppInfo {
|
interface AppInfo {
|
||||||
|
/**
|
||||||
|
* The human-readable version name of the application (e.g., "1.0.2").
|
||||||
|
*/
|
||||||
val version: String?
|
val version: String?
|
||||||
}
|
}
|
||||||
+28
@@ -2,14 +2,42 @@ package com.digitoolsolutions.app.torquevaultkmp.utils
|
|||||||
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface defining the platform-specific operations for managing Bluetooth state and permissions.
|
||||||
|
*/
|
||||||
interface BluetoothManager {
|
interface BluetoothManager {
|
||||||
|
/**
|
||||||
|
* Observable state indicating if Bluetooth is currently enabled on the device.
|
||||||
|
*/
|
||||||
val isBluetoothEnabled: StateFlow<Boolean>
|
val isBluetoothEnabled: StateFlow<Boolean>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable state indicating if the app has the necessary Bluetooth permissions.
|
||||||
|
*/
|
||||||
val hasBluetoothPermission: StateFlow<Boolean>
|
val hasBluetoothPermission: StateFlow<Boolean>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers a check of the current Bluetooth hardware state.
|
||||||
|
*/
|
||||||
fun checkBluetoothState()
|
fun checkBluetoothState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers a check of the current Bluetooth permission status.
|
||||||
|
*/
|
||||||
fun checkBluetoothPermission()
|
fun checkBluetoothPermission()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the system settings for the application, allowing the user to grant permissions.
|
||||||
|
*/
|
||||||
fun openSettings()
|
fun openSettings()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests the user to enable Bluetooth if it is currently disabled.
|
||||||
|
*/
|
||||||
fun enableBluetooth()
|
fun enableBluetooth()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function to create a platform-specific [BluetoothManager].
|
||||||
|
*/
|
||||||
expect fun createBluetoothManager(): BluetoothManager
|
expect fun createBluetoothManager(): BluetoothManager
|
||||||
|
|||||||
+48
-5
@@ -1,16 +1,28 @@
|
|||||||
package com.digitoolsolutions.app.torquevaultkmp.utils
|
package com.digitoolsolutions.app.torquevaultkmp.utils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility object containing protocol constants and helper functions for
|
||||||
|
* decoding and encoding data exchanged with torque devices.
|
||||||
|
*/
|
||||||
object Helper {
|
object Helper {
|
||||||
const val START_CHARACTER ="$"
|
const val START_CHARACTER ="$"
|
||||||
const val END_LINE_FEED = "#\n"
|
const val END_LINE_FEED = "#\n"
|
||||||
const val SECURITY_CRC16 = "00cr16"
|
const val SECURITY_CRC16 = "00cr16"
|
||||||
const val CMD_EMPTY_WO = "START_WO;empty\n$SECURITY_CRC16"
|
|
||||||
|
/** Command sent to indicate no work orders are available. */
|
||||||
|
const val CMD_EMPTY_WO = "START_WO;empty"
|
||||||
const val WO_RES = "WO_RES"
|
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" }
|
/** Generates a success response for a specific command. */
|
||||||
|
val CMD_RES_OK: (String) -> String = { cmd -> "$cmd;OK" }
|
||||||
|
|
||||||
|
/** Generates a failure response for a specific command. */
|
||||||
|
val CMD_RES_NG: (String) -> String = { cmd -> "$cmd;NG" }
|
||||||
|
|
||||||
const val BLE_NUTS_POS = 2
|
const val BLE_NUTS_POS = 2
|
||||||
const val BLE_TORQUE_POS = BLE_NUTS_POS + 1
|
const val BLE_TORQUE_POS = BLE_NUTS_POS + 1
|
||||||
|
|
||||||
|
/** Commands recognized by the torque device protocol. */
|
||||||
enum class Command {
|
enum class Command {
|
||||||
START_WO,
|
START_WO,
|
||||||
NEXT_WO,
|
NEXT_WO,
|
||||||
@@ -22,6 +34,8 @@ object Helper {
|
|||||||
try { valueOf(code) } catch (e: IllegalArgumentException) { null }
|
try { valueOf(code) } catch (e: IllegalArgumentException) { null }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Categories of devices supported by the protocol. */
|
||||||
enum class TypeOfDevice(val code: Int) {
|
enum class TypeOfDevice(val code: Int) {
|
||||||
WHEEL(0),
|
WHEEL(0),
|
||||||
OIL_FILTER(1),
|
OIL_FILTER(1),
|
||||||
@@ -36,11 +50,17 @@ object Helper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Action codes sent by the device during the finish workflow. */
|
||||||
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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a decoded message received from a torque device.
|
||||||
|
*/
|
||||||
data class RxMessage(
|
data class RxMessage(
|
||||||
val command: String,
|
val command: String,
|
||||||
val type: String, // Type of device
|
val type: String, // Type of device
|
||||||
@@ -49,12 +69,24 @@ object Helper {
|
|||||||
val securityCode: String,
|
val securityCode: String,
|
||||||
val crc16: String
|
val crc16: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a command string with the protocol's start, security, crc16 and end characters.
|
||||||
|
*/
|
||||||
fun buildUartCommand(cmd: String): String {
|
fun buildUartCommand(cmd: String): String {
|
||||||
return "$START_CHARACTER$cmd$END_LINE_FEED"
|
return "$START_CHARACTER$cmd\n$SECURITY_CRC16$END_LINE_FEED"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes a raw string message received from the device into an [RxMessage].
|
||||||
|
*
|
||||||
|
* The protocol format is: $COMMAND;TYPE;[WOID;DATA...]\nSECURITYCRC#\n
|
||||||
|
*
|
||||||
|
* @return The decoded message or null if the format is invalid.
|
||||||
|
*/
|
||||||
fun decodeRxData(msg: String): RxMessage? {
|
fun decodeRxData(msg: String): RxMessage? {
|
||||||
/** Check header and footer*/
|
/** Check header and footer*/
|
||||||
if (!msg.startsWith("$") && !msg.endsWith("#\n")) return null
|
if (!msg.startsWith("$") || !msg.endsWith("#\n")) return null
|
||||||
/** Remove $ and trim end */
|
/** Remove $ and trim end */
|
||||||
val payload = msg.drop(1).trimEnd('*', '#',' ', '\r', '\n')
|
val payload = msg.drop(1).trimEnd('*', '#',' ', '\r', '\n')
|
||||||
val parts = payload.split(";", limit = 3)
|
val parts = payload.split(";", limit = 3)
|
||||||
@@ -75,6 +107,13 @@ object Helper {
|
|||||||
return RxMessage(cmd, type, woId, data, security, crc16)
|
return RxMessage(cmd, type, woId, data, security, crc16)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the torque result data for multiple wheels and nuts.
|
||||||
|
*
|
||||||
|
* @param torqueData List of strings containing wheel names followed by nut torque values.
|
||||||
|
* @param nutsPerWheel The expected number of nuts for each wheel.
|
||||||
|
* @return A map where keys are wheel identifiers and values are lists of torque measurements.
|
||||||
|
*/
|
||||||
fun parseWheelTorqueData(torqueData: List<String>, nutsPerWheel: Int): Map<String, List<Double>> {
|
fun parseWheelTorqueData(torqueData: List<String>, nutsPerWheel: Int): Map<String, List<Double>> {
|
||||||
val result = mutableMapOf<String, List<Double>>()
|
val result = mutableMapOf<String, List<Double>>()
|
||||||
var i = 0
|
var i = 0
|
||||||
@@ -93,6 +132,10 @@ object Helper {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standardizes device identifiers (MAC addresses or UUIDs) for display.
|
||||||
|
*/
|
||||||
fun formatDeviceId(deviceId: String?): String {
|
fun formatDeviceId(deviceId: String?): String {
|
||||||
if(deviceId == null) return ""
|
if(deviceId == null) return ""
|
||||||
return if (deviceId.contains(":")) {
|
return if (deviceId.contains(":")) {
|
||||||
|
|||||||
+17
@@ -2,12 +2,29 @@ package com.digitoolsolutions.app.torquevaultkmp.utils
|
|||||||
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the connectivity status of the device.
|
||||||
|
*/
|
||||||
enum class NetworkStatus {
|
enum class NetworkStatus {
|
||||||
Available, Unavailable
|
Available, Unavailable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for monitoring real-time network connectivity changes.
|
||||||
|
*/
|
||||||
interface NetworkMonitor {
|
interface NetworkMonitor {
|
||||||
|
/**
|
||||||
|
* Observable stream of the current [NetworkStatus].
|
||||||
|
*/
|
||||||
val status: StateFlow<NetworkStatus>
|
val status: StateFlow<NetworkStatus>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the network monitoring process.
|
||||||
|
*/
|
||||||
fun start()
|
fun start()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the network monitoring process.
|
||||||
|
*/
|
||||||
fun stop()
|
fun stop()
|
||||||
}
|
}
|
||||||
+4
@@ -3,6 +3,10 @@ package com.digitoolsolutions.app.torquevaultkmp.utils
|
|||||||
import io.github.aakira.napier.Antilog
|
import io.github.aakira.napier.Antilog
|
||||||
import io.github.aakira.napier.LogLevel
|
import io.github.aakira.napier.LogLevel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A silent logger implementation for Napier that discards all log messages.
|
||||||
|
* Used in production builds or specific scenarios where console logging is not desired.
|
||||||
|
*/
|
||||||
class NoOpAntilog : Antilog() {
|
class NoOpAntilog : Antilog() {
|
||||||
override fun isEnable(priority: LogLevel, tag: String?): Boolean = false
|
override fun isEnable(priority: LogLevel, tag: String?): Boolean = false
|
||||||
override fun performLog(priority: LogLevel, tag: String?, throwable: Throwable?, message: String?) {}
|
override fun performLog(priority: LogLevel, tag: String?, throwable: Throwable?, message: String?) {}
|
||||||
|
|||||||
+26
@@ -3,14 +3,40 @@ package com.digitoolsolutions.app.torquevaultkmp.utils
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface defining platform-specific logic for handling runtime permissions and hardware features.
|
||||||
|
*/
|
||||||
interface PermissionHandler {
|
interface PermissionHandler {
|
||||||
|
/**
|
||||||
|
* Composable function that encapsulates the logic for requesting permissions.
|
||||||
|
*
|
||||||
|
* @param onPermissionResult Callback invoked with `true` if permissions were permanently denied.
|
||||||
|
* @param content Composable providing a function to trigger the permission request.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun HandlePermissionRequest(
|
fun HandlePermissionRequest(
|
||||||
onPermissionResult: (Boolean) -> Unit,
|
onPermissionResult: (Boolean) -> Unit,
|
||||||
content: @Composable (requestPermissions: () -> Unit) -> Unit
|
content: @Composable (requestPermissions: () -> Unit) -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the device hardware supports Bluetooth Low Energy.
|
||||||
|
*/
|
||||||
val hasBleFeature: Boolean
|
val hasBleFeature: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable state indicating if Location services are currently enabled.
|
||||||
|
* (Location is often a prerequisite for BLE scanning on Android).
|
||||||
|
*/
|
||||||
val isLocationEnabled: StateFlow<Boolean>
|
val isLocationEnabled: StateFlow<Boolean>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests the user to enable Location services.
|
||||||
|
*/
|
||||||
fun enableLocation()
|
fun enableLocation()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers a check of the current Location service state.
|
||||||
|
*/
|
||||||
fun checkLocationState()
|
fun checkLocationState()
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -1,6 +1,16 @@
|
|||||||
package com.digitoolsolutions.app.torquevaultkmp.utils
|
package com.digitoolsolutions.app.torquevaultkmp.utils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface defining platform-specific logic for sharing log files.
|
||||||
|
*/
|
||||||
interface ShareLog {
|
interface ShareLog {
|
||||||
|
/**
|
||||||
|
* Exports and shares the provided [content] as a plain text file.
|
||||||
|
*/
|
||||||
fun exportTextFile(content: String)
|
fun exportTextFile(content: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports and shares the provided [content] as a CSV file.
|
||||||
|
*/
|
||||||
fun exportCSVFile(content: String)
|
fun exportCSVFile(content: String)
|
||||||
}
|
}
|
||||||
+6
@@ -10,6 +10,12 @@ import platform.darwin.NSObject
|
|||||||
import platform.darwin.dispatch_async
|
import platform.darwin.dispatch_async
|
||||||
import platform.darwin.dispatch_get_main_queue
|
import platform.darwin.dispatch_get_main_queue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* iOS implementation of [BluetoothManager] using CoreBluetooth.
|
||||||
|
*
|
||||||
|
* This class monitors the CBCentralManager state to track Bluetooth availability
|
||||||
|
* and manages permission status on iOS devices.
|
||||||
|
*/
|
||||||
class IosBluetoothManager : BluetoothManager {
|
class IosBluetoothManager : BluetoothManager {
|
||||||
private val _isBluetoothEnabled = MutableStateFlow(false)
|
private val _isBluetoothEnabled = MutableStateFlow(false)
|
||||||
override val isBluetoothEnabled: StateFlow<Boolean> = _isBluetoothEnabled.asStateFlow()
|
override val isBluetoothEnabled: StateFlow<Boolean> = _isBluetoothEnabled.asStateFlow()
|
||||||
|
|||||||
Reference in New Issue
Block a user