From 557681cbeb9f9f763e9cc88ee1b4d6185b950eaf Mon Sep 17 00:00:00 2001 From: lamtruong28 Date: Thu, 23 Jul 2026 13:20:13 +0700 Subject: [PATCH] Update Readme.md, add docstring --- README.md | 91 +++++++++++++------ .../utils/BluetoothManager.android.kt | 6 ++ .../app/torquevaultkmp/App.kt | 6 ++ .../app/torquevaultkmp/AppViewModel.kt | 31 +++++++ .../app/torquevaultkmp/NetworkEngine.kt | 8 ++ .../app/torquevaultkmp/Platform.kt | 9 +- .../app/torquevaultkmp/components/AppBar.kt | 10 ++ .../app/torquevaultkmp/components/BigIcon.kt | 8 +- .../torquevaultkmp/components/FilterButton.kt | 14 ++- .../app/torquevaultkmp/components/Hint.kt | 10 +- .../app/torquevaultkmp/components/RssiIcon.kt | 3 + .../data/network/api/AuthApi.kt | 13 +++ .../data/network/api/Endpoints.kt | 5 +- .../data/network/api/WorkOrderApi.kt | 12 +++ .../data/network/dto/WorkOrderDto.kt | 1 - .../data/network/mapper/AuthMapper.kt | 6 +- .../data/network/mapper/WorkOrderMapper.kt | 9 +- .../data/repository/DeviceRepository.kt | 30 ++++++ .../data/repository/LogRepository.kt | 23 +++++ .../data/repository/ScannerRepository.kt | 21 ++++- .../data/repository/WorkOrderRepository.kt | 4 +- .../repository/WorkOrderRepositoryImpl.kt | 5 +- .../torquevaultkmp/data/storage/AppStorage.kt | 22 +++++ .../torquevaultkmp/data/storage/ReferKeys.kt | 12 +++ .../data/storage/TokenManager.kt | 12 +++ .../app/torquevaultkmp/di/Koin.kt | 6 ++ .../app/torquevaultkmp/di/Modules.kt | 15 +++ .../torquevaultkmp/domain/model/WorkOrder.kt | 27 ++++-- .../domain/usecase/FetchWorkOrdersUseCase.kt | 10 ++ .../app/torquevaultkmp/kable/BleManager.kt | 36 ++++++++ .../app/torquevaultkmp/screens/Destination.kt | 13 ++- .../screens/auth/AuthViewModel.kt | 21 +++++ .../screens/auth/LoginScreen.kt | 6 ++ .../screens/bonded/BondedViewModel.kt | 14 +++ .../communication/UartCommunicationScreen.kt | 8 +- .../torquevaultkmp/screens/home/HomeScreen.kt | 5 + .../screens/home/HomeViewModel.kt | 13 +++ .../torquevaultkmp/screens/logs/LogScreen.kt | 9 ++ .../screens/requirement/RequirementWrapper.kt | 25 +++++ .../screens/scanner/ScannerScreen.kt | 13 ++- .../screens/scanner/ScannerViewModel.kt | 10 +- .../screens/settings/SettingViewModel.kt | 24 +++++ .../app/torquevaultkmp/utils/ApiException.kt | 15 ++- .../app/torquevaultkmp/utils/AppInfo.kt | 8 +- .../torquevaultkmp/utils/BluetoothManager.kt | 28 ++++++ .../app/torquevaultkmp/utils/Helper.kt | 53 ++++++++++- .../torquevaultkmp/utils/NetworkMonitor.kt | 19 +++- .../app/torquevaultkmp/utils/NoOpAntilog.kt | 6 +- .../torquevaultkmp/utils/PermissionHandler.kt | 26 ++++++ .../app/torquevaultkmp/utils/ShareLog.kt | 12 ++- .../utils/BluetoothManager.ios.kt | 6 ++ 51 files changed, 731 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 7e7d049..a107bea 100644 --- a/README.md +++ b/README.md @@ -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. - 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. +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. -* [/iosApp](./iosApp/iosApp) contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform, - you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project. +The project targets **Android** and **iOS** using **Compose Multiplatform** for a shared UI and logic. -### 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 -in your IDE’s toolbar or build it directly from the terminal: -- on macOS/Linux - ```shell - ./gradlew :composeApp:assembleDebug - ``` -- on Windows - ```shell - .\gradlew.bat :composeApp:assembleDebug - ``` +- **BLE Device Management**: Scan, connect, and bond with digital torque wrenches using the [Kable](https://github.com/JuulLabs/kable) library. +- **Adaptive Scanning**: Automatically calibrates device discovery expiration by observing advertisement intervals, ensuring a responsive UI when devices stop broadcasting. +- **Work Order Workflow**: + - Fetch "Open" work orders from a remote API. + - 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. +- **Bonding & Persistence**: Automatically remembers and auto-connects to bonded devices using **SQLDelight** for local storage. +- **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. +- **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 -in your IDE’s toolbar or open the [/iosApp](./iosApp) directory in Xcode and run it from there. +- **Kotlin**: `2.3.21` +- **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. --- - -Learn more about [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html)… \ No newline at end of file +Developed by **Digitool Solutions**. diff --git a/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/BluetoothManager.android.kt b/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/BluetoothManager.android.kt index 17ac3d8..2f562cb 100644 --- a/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/BluetoothManager.android.kt +++ b/composeApp/src/androidMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/BluetoothManager.android.kt @@ -16,6 +16,12 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow 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 { private val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as AndroidBluetoothManager private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/App.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/App.kt index 6333e78..4ce66a4 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/App.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/App.kt @@ -9,6 +9,12 @@ import com.digitoolsolutions.app.torquevaultkmp.screens.scanner.ScannerViewModel import com.digitoolsolutions.app.torquevaultkmp.theme.TorqueVaultTheme 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 @Preview fun App( diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/AppViewModel.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/AppViewModel.kt index 60347f2..55bcbc6 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/AppViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/AppViewModel.kt @@ -13,41 +13,72 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest 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( private val tokenManager: TokenManager, private val storage: AppStorage ) : ViewModel() { + /** + * Observable state for the current theme. + * If null, the system theme is used. + */ private val _isDarkTheme = mutableStateOf(storage.load(ReferKeys.THEME)) val isDarkTheme: State = _isDarkTheme + /** + * Notifies the UI when the authentication token has expired. + */ private val _sessionExpired = mutableStateOf(false) val sessionExpired: State = _sessionExpired + /** + * Determines if the Debug Logs tab should be visible in the navigation bar. + */ private val _showLogs = MutableStateFlow(false) val showLogs: StateFlow = _showLogs init { viewModelScope.launch { + // Observe the token manager for session expiration events tokenManager.sessionExpired.collectLatest { _sessionExpired.value = true } } } + /** + * Resets the session expiration alert. + */ fun dismissSessionExpired() { _sessionExpired.value = false } + /** + * Checks if a valid session exists. + */ fun isLoggedIn(): Boolean { return tokenManager.getRefreshToken() != null } + /** + * Toggles between Light and Dark mode and persists the choice. + */ fun toggleTheme(isDark: Boolean) { _isDarkTheme.value = isDark storage.save(ReferKeys.THEME, isDark) } + /** + * Enables or disables the visibility of diagnostic logs. + */ fun setShowLogs(enabled: Boolean) { _showLogs.value = enabled } diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/NetworkEngine.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/NetworkEngine.kt index a684906..88366b3 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/NetworkEngine.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/NetworkEngine.kt @@ -4,4 +4,12 @@ import com.digitoolsolutions.app.torquevaultkmp.data.network.api.AuthApi import com.digitoolsolutions.app.torquevaultkmp.data.storage.TokenManager 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 diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/Platform.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/Platform.kt index 946a8e9..43927ea 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/Platform.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/Platform.kt @@ -1,7 +1,14 @@ package com.digitoolsolutions.app.torquevaultkmp +/** + * Interface representing the current operating platform (Android or iOS). + */ interface Platform { + /** The name of the platform (e.g., "Android 34", "iOS 17.2"). */ val name: String } -expect fun getPlatform(): Platform \ No newline at end of file +/** + * Returns the [Platform] implementation for the current target. + */ +expect fun getPlatform(): Platform diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/AppBar.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/AppBar.kt index 7bf749f..e4d7a24 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/AppBar.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/AppBar.kt @@ -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.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) @Composable fun AppBar( diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/BigIcon.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/BigIcon.kt index 0dccc47..0732eea 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/BigIcon.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/BigIcon.kt @@ -12,6 +12,9 @@ import androidx.compose.ui.graphics.vector.ImageVector 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 internal fun BigIcon( imageVector: ImageVector, @@ -27,6 +30,9 @@ internal fun BigIcon( ) } +/** + * A version of [BigIcon] that uses a [Painter] instead of an [ImageVector]. + */ @Composable internal fun BigIcon( painterResource: Painter, @@ -40,4 +46,4 @@ internal fun BigIcon( modifier = modifier.size(size), colorFilter = ColorFilter.tint(color), ) -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/FilterButton.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/FilterButton.kt index 6dbbb9f..ee15be8 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/FilterButton.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/FilterButton.kt @@ -14,7 +14,11 @@ import org.jetbrains.compose.resources.painterResource import torquevaultkmp.composeapp.generated.resources.Res 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 fun FilterButton( title: String, @@ -34,6 +38,9 @@ fun FilterButton( ) } +/** + * A version of [FilterButton] that takes a [Painter]. + */ @Composable fun FilterButton( title: String, @@ -60,6 +67,9 @@ fun FilterButton( ) } +/** + * A version of [FilterButton] that takes an [ImageVector]. + */ @Composable fun FilterButton( title: String, @@ -77,4 +87,4 @@ fun FilterButton( containerColorDisabled = containerColorDisabled, onClick = onClick ) -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/Hint.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/Hint.kt index 91c962a..c0de59f 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/Hint.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/components/Hint.kt @@ -10,7 +10,10 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle 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 internal fun Hint( text: String, @@ -28,6 +31,9 @@ internal fun Hint( ) } +/** + * An [AnnotatedString] version of the [Hint] component. + */ @Composable internal fun Hint( text: AnnotatedString, @@ -43,4 +49,4 @@ internal fun Hint( modifier = modifier, textAlign = textAlign ) -} \ No newline at end of file +} 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 ceb9f84..d448b40 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 @@ -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 { rssi < MEDIUM_RSSI -> Res.drawable.ic_signal_min rssi < MAX_RSSI -> Res.drawable.ic_signal_medium 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 acb4b34..8f2e3e4 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 @@ -14,6 +14,11 @@ 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( private val client: HttpClient, private val tokenManager: TokenManager @@ -21,6 +26,9 @@ class AuthApi( private val baseUrl: String? get() = tokenManager.getServerUrl() + /** + * Attempts to authenticate the user and stores the received tokens. + */ suspend fun login(username: String, password: String): Boolean { return try { val res: LoginResponseDto = client.post("$baseUrl${Endpoints.AUTH_TOKEN}") { @@ -34,6 +42,11 @@ class AuthApi( 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? { val refreshToken = tokenManager.getRefreshToken() ?: return null return try { 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 40f43cb..e521f32 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 @@ -1,5 +1,8 @@ package com.digitoolsolutions.app.torquevaultkmp.data.network.api +/** + * Constant values for API endpoints. + */ object Endpoints { const val AUTH_TOKEN = "/token/" const val AUTH_REFRESH = "/token/refresh/" @@ -9,4 +12,4 @@ object Endpoints { 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 3314b7e..0f16ccb 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 @@ -20,6 +20,9 @@ import io.ktor.http.HttpStatusCode import io.ktor.http.contentType import io.ktor.http.path +/** + * API service for managing work orders and torque measurements. + */ class WorkOrderApi( private val client: HttpClient, private val tokenManager: TokenManager @@ -27,6 +30,9 @@ class WorkOrderApi( private val baseUrl: String? get() = tokenManager.getServerUrl() + /** + * Retrieves a list of work orders available for the device. + */ suspend fun fetchAbleWorkOrders(woID: String, action: String): List { return client.get("$baseUrl/${Endpoints.ABLE_WO}") { parameter("wo_id", woID) @@ -34,6 +40,9 @@ class WorkOrderApi( }.body() } + /** + * Confirms that a specific device is handling a work order. + */ suspend fun confirm(woID: String, deviceId: String, type: Int): Boolean { return try { val response = client.patch("$baseUrl/${Endpoints.WORK_ORDERS}/$woID/${Endpoints.CONFIRM}"){ @@ -46,6 +55,9 @@ class WorkOrderApi( false } } + /** + * Uploads measurement results for a specific work order. + */ suspend fun sendMeasurement(woID: String, bodyPayload: SendMeasureDto, isRemeasure: Boolean = false): Result { return try { val endPath = if(isRemeasure) Endpoints.RE_MEASURE else Endpoints.MEASURE 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 e855179..1b9a31d 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 @@ -2,7 +2,6 @@ package com.digitoolsolutions.app.torquevaultkmp.data.network.dto import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonElement @Serializable data class WorkOrderDto( diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/mapper/AuthMapper.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/mapper/AuthMapper.kt index 2bd94f8..5a2a82a 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/mapper/AuthMapper.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/network/mapper/AuthMapper.kt @@ -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.RefreshResponse +/** + * Extension functions to map Authentication Network DTOs to Domain models. + */ + fun LoginRequestDto.toDomain(): LoginRequest { return LoginRequest(username,password) } @@ -21,4 +25,4 @@ fun RefreshRequestDto.toDomain(): RefreshRequest { } fun RefreshResponseDto.toDomain(): RefreshResponse { return RefreshResponse(access) -} \ 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 ab6e25c..d655435 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 @@ -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.Service 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 { return WorkOrder( @@ -46,10 +48,11 @@ fun WorkOrderDto.toDomain(): WorkOrder { 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/DeviceRepository.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/DeviceRepository.kt index 9a5bccd..b8ac019 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/DeviceRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/DeviceRepository.kt @@ -8,24 +8,48 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO 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( private val dbQueries: AppDatabaseQueries ) { companion object { 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) { dbQueries.insertDevice(id, name) } + /** + * Returns a flow of all saved devices. + */ fun getAllDevices(): Flow> = dbQueries.getAllDevices() .asFlow() .mapToList(Dispatchers.IO) + /** + * Returns a flow containing only the identifiers of all saved devices. + */ suspend fun getAllDeviceIdsFlow(): Flow> { 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 { return if (lastId == null) { dbQueries.getDevicesFirstPage(limit).executeAsList() @@ -34,8 +58,14 @@ class DeviceRepository( } } + /** + * Retrieves a specific device by its identifier. + */ suspend fun getDeviceById(id: String): Device? = dbQueries.getDeviceById(id).executeAsOneOrNull() + /** + * Removes a device from the bonded list. + */ suspend fun deleteDevice(id: String) = dbQueries.deleteDeviceById(id) } diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/LogRepository.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/LogRepository.kt index 0dd8b63..c9ab70d 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/LogRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/LogRepository.kt @@ -11,6 +11,16 @@ import kotlin.time.Clock import kotlin.uuid.ExperimentalUuidApi 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( val id: String, val title: String, @@ -19,12 +29,25 @@ data class Log( val deviceId: 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() { companion object { private const val MAX_LOGS = 1000 } private val _logs = MutableStateFlow>(emptyList()) val logs: StateFlow> = _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) fun appendLog(title: String, content: String, deviceId: String? = null) { val source = if (deviceId == null) LogFilter.SYSTEM else LogFilter.DEVICE 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 index 6f9122a..5948dbb 100644 --- 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 @@ -1,25 +1,40 @@ 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 +/** + * 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) { + /** + * Fetches a list of work orders available for a device. + */ suspend fun getAbleWorkOrders(woID: String = "0", action: String = "next"): List { 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 { 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 { 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 { 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 af09345..e49fd63 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,10 +1,12 @@ 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 defining the operations for managing work orders via remote API. + */ interface WorkOrderRepository { suspend fun fetchAbleWorkOrders(woId: String, action: String): List suspend fun confirm(woID: String, deviceId: String, type: Int): Boolean 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 1dbea7c..fe8b9c9 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 @@ -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.domain.model.MeasureResult import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder +/** + * Implementation of [WorkOrderRepository] that communicates with the backend API. + */ class WorkOrderRepositoryImpl(private val api: WorkOrderApi) : WorkOrderRepository { override suspend fun fetchAbleWorkOrders(woId: String, action: String): List { val dto = api.fetchAbleWorkOrders(woId, action) @@ -16,7 +19,7 @@ class WorkOrderRepositoryImpl(private val api: WorkOrderApi) : WorkOrderReposito deviceId: String, type: Int ): Boolean { - return api.confirm(woID,deviceId, type) + return api.confirm(woID, deviceId, type) } override suspend fun sendMeasurement(woId: String, data: SendMeasureDto, isReTorque: Boolean): Result { diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/storage/AppStorage.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/storage/AppStorage.kt index eedfbca..437c0d9 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/storage/AppStorage.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/storage/AppStorage.kt @@ -3,10 +3,32 @@ package com.digitoolsolutions.app.torquevaultkmp.data.storage import kotlinx.coroutines.flow.Flow 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 { + /** + * Persists a value for the given key. + */ fun save(key: String, value: T) + + /** + * Retrieves a value for the given key. + */ fun load(key: String, type: KClass): T? + + /** + * Removes the data associated with the given key. + */ fun remove(key: String) + + /** + * Returns a flow that emits updates whenever the value for the given key changes. + */ fun observe(key: String, type: KClass): Flow } diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/storage/ReferKeys.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/storage/ReferKeys.kt index c86da1b..3bbbb3e 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/storage/ReferKeys.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/storage/ReferKeys.kt @@ -1,9 +1,21 @@ package com.digitoolsolutions.app.torquevaultkmp.data.storage +/** + * Constants used as keys for persistent storage throughout the application. + */ object ReferKeys { + /** The access token used for authenticated API requests. */ const val ACCESS_TOKEN = "access_token" + + /** The refresh token used to obtain new access tokens. */ const val REFRESH_TOKEN = "refresh_token" + + /** The base URL of the remote server. */ const val SERVER_URL = "server_url" + + /** User preference for Dark Mode (Boolean). */ const val THEME = "theme" + + /** User preference for automatic reconnection to bonded devices (Boolean). */ const val DEVICE_AUTO_CONNECT = "device_auto_connect" } diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/storage/TokenManager.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/storage/TokenManager.kt index 7d01c7f..4695d0c 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/storage/TokenManager.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/storage/TokenManager.kt @@ -3,10 +3,22 @@ package com.digitoolsolutions.app.torquevaultkmp.data.storage import kotlinx.coroutines.flow.MutableSharedFlow 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) { + /** + * A flow that emits when the authentication session has expired. + */ private val _sessionExpired = MutableSharedFlow(extraBufferCapacity = 1) val sessionExpired = _sessionExpired.asSharedFlow() + /** + * Triggers the session expiration event, typically called by a Ktor interceptor. + */ fun triggerSessionExpired() { _sessionExpired.tryEmit(Unit) } diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/Koin.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/Koin.kt index 0a490f8..091fa10 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/Koin.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/Koin.kt @@ -3,6 +3,12 @@ package com.digitoolsolutions.app.torquevaultkmp.di import org.koin.core.context.startKoin 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) { startKoin { config?.invoke(this) 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 ca05c84..bbb6b3b 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 @@ -25,14 +25,23 @@ import org.koin.core.module.dsl.singleOf import org.koin.core.qualifier.named import org.koin.dsl.module +/** + * Platform-specific module to be provided by each target (Android, iOS). + */ expect val platformModule: Module +/** + * Shared application module providing core utility classes. + */ val appModule = module { // TokenManager use AppStorage single { TokenManager(get()) } singleOf(::BleManager) } +/** + * Shared storage module providing database, repositories, and local persistence logic. + */ val storageModule = module { single { AppDatabase(get()) } single { get().appDatabaseQueries } @@ -41,6 +50,9 @@ val storageModule = module { single { ScannerRepository(get()) } } +/** + * Shared network module providing Ktor clients, API services, and network-bound repositories. + */ val networkModule = module { // Auth client (no interceptor to avoid circular dependency and recursion) single(named("authClient")) { createPlatformHttpClient(get(), null) } @@ -56,6 +68,9 @@ val networkModule = module { factory { FetchWorkOrdersUseCase(get()) } } +/** + * Shared ViewModel module providing state management for the UI screens. + */ val viewModelModule = module { factoryOf(::HomeViewModel) factory { AuthViewModel(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 72fa20c..98d98ed 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 @@ -1,13 +1,21 @@ package com.digitoolsolutions.app.torquevaultkmp.domain.model +/** + * Mapping of wheel positions to bitmask values for the UART protocol. + */ val wheelsCode = mapOf( - "DF" to 0b0001, - "PF" to 0b0010, - "DR" to 0b0100, - "PR" to 0b1000, - "DRO" to 0b00010000, - "PRO" to 0b100000 + "DF" to 0b0001, // Driver Front + "PF" to 0b0010, // Passenger Front + "DR" to 0b0100, // Driver Rear + "PR" to 0b1000, // Passenger Rear + "DRO" to 0b00010000, // Driver Rear Outer + "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 { var wNut = 0 var nutCount = 0 @@ -22,6 +30,9 @@ fun WorkOrder.computeWheelsNuts(): Int { return (wNut shl 8) or nutCount } +/** + * Domain model representing a Work Order. + */ data class WorkOrder( val id: String, val status: String, @@ -35,6 +46,10 @@ data class WorkOrder( val createdAt: String, val updatedAt: String? ) + +/** + * Details of a service requested within a [WorkOrder], specifically for torque tasks. + */ data class Service( val nut: Int?, val status: Int, 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 7285164..0fafe2e 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 @@ -3,7 +3,17 @@ package com.digitoolsolutions.app.torquevaultkmp.domain.usecase import com.digitoolsolutions.app.torquevaultkmp.data.repository.WorkOrderRepository import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder +/** + * Use case responsible for retrieving work orders from the repository. + */ 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 { 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 index 8f7a186..a20fdc7 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/kable/BleManager.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/kable/BleManager.kt @@ -15,11 +15,22 @@ import kotlin.uuid.ExperimentalUuidApi import kotlinx.coroutines.sync.Mutex 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) class BleManager { companion object { + /** The primary Service UUID for the torque device UART protocol. */ 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")) + + /** The characteristic used to receive data from the device. */ 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 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 = Scanner { filters { match { @@ -47,6 +63,12 @@ class BleManager { 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 { val identifier = advertisement.identifier.toString() Napier.d(">>>>> Connecting to $identifier") @@ -57,23 +79,37 @@ class BleManager { } } + /** + * Disconnects a peripheral by its identifier. + */ suspend fun disconnect(identifier: String) { mutex.withLock { peripherals.remove(identifier)?.disconnect() } } + /** + * Disconnects all currently active BLE connections. + */ suspend fun disconnectAll() { mutex.withLock { peripherals.values.forEach { it.disconnect() } peripherals.clear() } } + + /** + * Sends a UART command string to the connected peripheral. + */ suspend fun sendCommand(peripheral: Peripheral, command: String) { Napier.d(">>>>> Sent to uart: $command") val cmd = command.replace("\\n", "\n") peripheral.write(RX_CHAR, cmd.encodeToByteArray()) } + + /** + * Returns a flow that emits incoming UART data from the peripheral. + */ fun observeRx(peripheral: Peripheral): Flow = peripheral.observe(TX_CHAR).map { it.decodeToString() } } diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/Destination.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/Destination.kt index 2ddb152..5b4d7c5 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/Destination.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/Destination.kt @@ -3,13 +3,24 @@ package com.digitoolsolutions.app.torquevaultkmp.screens import androidx.compose.ui.graphics.vector.ImageVector import org.jetbrains.compose.resources.DrawableResource +/** + * Wrapper for different types of icons used in the navigation system. + */ sealed class AppIcon { + /** Uses a compiled Compose Multiplatform resource. */ data class Resource(val resId: DrawableResource) : AppIcon() + + /** Uses a standard Material [ImageVector]. */ 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( val label: String, val icon: AppIcon, val route: String = label.lowercase().replace(" ", "_") -) \ No newline at end of file +) diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/auth/AuthViewModel.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/auth/AuthViewModel.kt index 8aafa3d..2741061 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/auth/AuthViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/auth/AuthViewModel.kt @@ -8,6 +8,9 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +/** + * Represents the state of the login process. + */ sealed class LoginUiState { object Idle : LoginUiState() object Loading : LoginUiState() @@ -15,21 +18,39 @@ sealed class 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( private val authApi: AuthApi, private val tokenManager: TokenManager ) : ViewModel() { + /** + * Observable state of the login operation. + */ private val _loginState = MutableStateFlow(LoginUiState.Idle) 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") val serverUrl = _serverUrl.asStateFlow() + /** + * Updates the server URL in the UI state. + */ fun onChangeServerUrl(url: String) { _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) { viewModelScope.launch { _loginState.value = LoginUiState.Loading diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/auth/LoginScreen.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/auth/LoginScreen.kt index eac16a6..486d4f1 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/auth/LoginScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/auth/LoginScreen.kt @@ -68,6 +68,9 @@ import torquevaultkmp.composeapp.generated.resources.lbl_server_url import torquevaultkmp.composeapp.generated.resources.lbl_username import torquevaultkmp.composeapp.generated.resources.server_url_placeholder +/** + * Screen for user authentication. + */ @Composable fun LoginScreen( navController: NavController @@ -92,6 +95,9 @@ fun LoginScreen( ) } +/** + * Stateless UI content for the login screen. + */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun LoginScreenContent( diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/BondedViewModel.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/BondedViewModel.kt index 0c5b4e9..b36f8cd 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/BondedViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/BondedViewModel.kt @@ -10,16 +10,27 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch 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( private val deviceRepository: DeviceRepository ) : ViewModel() { + /** + * Observable list of bonded devices. + */ private val _bondedDevices = MutableStateFlow>(emptyList()) val bondedDevices: StateFlow> = _bondedDevices.asStateFlow() private var lastId: String? = null private val pageSize = DeviceRepository.PAGE_SIZE private var isLastPage = false + + /** Indicates if a background data fetch is currently in progress. */ var isLoading = MutableStateFlow(false) private set @@ -27,6 +38,9 @@ class BondedViewModel( loadNextPage() } + /** + * Fetches the next page of bonded devices from the database. + */ fun loadNextPage() { if (isLoading.value || isLastPage) return diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/communication/UartCommunicationScreen.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/communication/UartCommunicationScreen.kt index 343246d..9877d8e 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/communication/UartCommunicationScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/communication/UartCommunicationScreen.kt @@ -27,6 +27,12 @@ import torquevaultkmp.composeapp.generated.resources.Res import torquevaultkmp.composeapp.generated.resources.action_cancel 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) @Composable fun UartCommunicationScreen( @@ -122,4 +128,4 @@ fun UartCommunicationScreen( } } } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/HomeScreen.kt index 36747e1..58149d5 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/HomeScreen.kt @@ -29,6 +29,11 @@ import torquevaultkmp.composeapp.generated.resources.Res import torquevaultkmp.composeapp.generated.resources.home_title 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) @Composable fun HomeScreen( diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/HomeViewModel.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/HomeViewModel.kt index 9035ed2..7508d95 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/HomeViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/home/HomeViewModel.kt @@ -12,11 +12,24 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow 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( private val fetchWorkOrdersUseCase: FetchWorkOrdersUseCase ): ViewModel() { + /** + * Observable list of work orders. + */ private val _workOrders = MutableStateFlow>(emptyList()) val workOrders = _workOrders.asStateFlow() + + /** + * Loads the initial page of work orders. + */ fun loadWorkOrders() { viewModelScope.launch { try { diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogScreen.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogScreen.kt index daaa9a5..c5e6031 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogScreen.kt @@ -62,6 +62,12 @@ import org.koin.compose.viewmodel.koinViewModel import torquevaultkmp.composeapp.generated.resources.Res 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) @Composable fun LogScreen( @@ -80,6 +86,9 @@ fun LogScreen( ) } +/** + * Stateless UI content for the diagnostic logs screen. + */ @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun LogContent( diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/RequirementWrapper.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/RequirementWrapper.kt index 58f42cd..24f50c4 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/RequirementWrapper.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/requirement/RequirementWrapper.kt @@ -29,15 +29,27 @@ import com.digitoolsolutions.app.torquevaultkmp.utils.NetworkStatus import com.digitoolsolutions.app.torquevaultkmp.utils.PermissionHandler import org.koin.compose.koinInject +/** + * Data class representing the current state of system requirements. + */ data class Requirements( + /** Whether the device supports Bluetooth Low Energy. */ val hasBleFeature: Boolean, + /** Whether Bluetooth is currently turned on. */ val isEnabled: Boolean, + /** Whether the app has been granted necessary Bluetooth/Location permissions. */ val hasPermission: Boolean, + /** Whether the user has permanently denied the required permissions. */ val hasPermanentlyDenied: Boolean, + /** Whether Location services are enabled (required for BLE on some Android versions). */ val isLocationEnabled: Boolean, + /** Whether the device has active internet connectivity. */ val networkAvailable: Boolean, ) +/** + * Sealed class representing the mutually exclusive states of requirement fulfillment. + */ sealed class RequirementState { object BleUnsupported : RequirementState() object BluetoothDisabled : RequirementState() @@ -48,6 +60,9 @@ sealed class RequirementState { object Ready : RequirementState() } +/** + * Maps the raw [Requirements] data into a simplified [RequirementState]. + */ fun Requirements.toState(): RequirementState { return when { !hasBleFeature -> RequirementState.BleUnsupported @@ -64,6 +79,16 @@ fun Requirements.toState(): RequirementState { 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 fun RequirementWrapper( onReady: () -> Unit, 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 index a9d52c0..d80e4fd 100644 --- 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 @@ -33,6 +33,12 @@ import torquevaultkmp.composeapp.generated.resources.scan_empty_title import torquevaultkmp.composeapp.generated.resources.scanner_title 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) @Composable fun ScannerScreen( @@ -113,12 +119,11 @@ fun ScannerScreen( } } } - -// LaunchedEffect(Unit) { -// viewModel.startScan() -// } } +/** + * A list item representing a discovered Bluetooth device. + */ @OptIn(ExperimentalUuidApi::class) @Composable fun DeviceListItem( diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/scanner/ScannerViewModel.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/scanner/ScannerViewModel.kt index efce7d5..73e34b9 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/scanner/ScannerViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/scanner/ScannerViewModel.kt @@ -111,8 +111,14 @@ class ScannerViewModel( private var stabilized = false /** - * Record advertisement: collect (20 times) intervals between consecutive ads - * to calibrate the cleanup expiration time when a device stops advertising. + * Records advertisement intervals to calibrate the cleanup expiration time. + * + * 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) { if (stabilized) return diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/settings/SettingViewModel.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/settings/SettingViewModel.kt index a72ae6e..43ddb28 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/settings/SettingViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/settings/SettingViewModel.kt @@ -9,36 +9,60 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow 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( private val tokenManager: TokenManager, private val storage: AppStorage ) : ViewModel() { + /** The base server URL. */ private val _serverUrl = MutableStateFlow(tokenManager.getBaseUrl() ?: "") val serverUrl: StateFlow = _serverUrl.asStateFlow() + /** The current refresh token. */ private val _refreshToken = MutableStateFlow(tokenManager.getRefreshToken() ?: "") val refreshToken: StateFlow = _refreshToken.asStateFlow() + /** Whether the app should automatically attempt to connect to known devices. */ private val _autoConnect = MutableStateFlow(storage.load(ReferKeys.DEVICE_AUTO_CONNECT) ?: false) val autoConnect: StateFlow = _autoConnect.asStateFlow() + /** + * Updates the server URL in the UI state. + */ fun updateServerUrl(url: String) { _serverUrl.value = url } + /** + * Updates the refresh token in the UI state. + */ fun updateRefreshToken(token: String) { _refreshToken.value = token } + /** + * Persists the current server URL and tokens to storage. + */ fun saveSettings() { tokenManager.saveServerUrl(_serverUrl.value) tokenManager.saveTokens("", _refreshToken.value) } + /** + * Clears all authentication tokens and logs the user out. + */ fun logout() { tokenManager.clearTokens() } + /** + * Updates the auto-connect preference and persists it. + */ fun updateAutoConnect(enabled: Boolean) { _autoConnect.value = enabled storage.save(ReferKeys.DEVICE_AUTO_CONNECT, enabled) diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/ApiException.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/ApiException.kt index 3418223..1a2231f 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/ApiException.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/ApiException.kt @@ -2,10 +2,23 @@ package com.digitoolsolutions.app.torquevaultkmp.utils 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( "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 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") diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/AppInfo.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/AppInfo.kt index 897b0a7..279230a 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/AppInfo.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/AppInfo.kt @@ -1,5 +1,11 @@ package com.digitoolsolutions.app.torquevaultkmp.utils +/** + * Interface providing basic application metadata. + */ interface AppInfo { + /** + * The human-readable version name of the application (e.g., "1.0.2"). + */ val version: String? -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/BluetoothManager.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/BluetoothManager.kt index d177489..f3d5f93 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/BluetoothManager.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/BluetoothManager.kt @@ -2,14 +2,42 @@ package com.digitoolsolutions.app.torquevaultkmp.utils import kotlinx.coroutines.flow.StateFlow +/** + * Interface defining the platform-specific operations for managing Bluetooth state and permissions. + */ interface BluetoothManager { + /** + * Observable state indicating if Bluetooth is currently enabled on the device. + */ val isBluetoothEnabled: StateFlow + + /** + * Observable state indicating if the app has the necessary Bluetooth permissions. + */ val hasBluetoothPermission: StateFlow + /** + * Triggers a check of the current Bluetooth hardware state. + */ fun checkBluetoothState() + + /** + * Triggers a check of the current Bluetooth permission status. + */ fun checkBluetoothPermission() + + /** + * Opens the system settings for the application, allowing the user to grant permissions. + */ fun openSettings() + + /** + * Requests the user to enable Bluetooth if it is currently disabled. + */ fun enableBluetooth() } +/** + * Factory function to create a platform-specific [BluetoothManager]. + */ expect fun createBluetoothManager(): BluetoothManager diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/Helper.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/Helper.kt index 27b7c41..c6d826d 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/Helper.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/Helper.kt @@ -1,16 +1,28 @@ 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 { const val START_CHARACTER ="$" const val END_LINE_FEED = "#\n" 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" - 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_TORQUE_POS = BLE_NUTS_POS + 1 + + /** Commands recognized by the torque device protocol. */ enum class Command { START_WO, NEXT_WO, @@ -22,6 +34,8 @@ object Helper { try { valueOf(code) } catch (e: IllegalArgumentException) { null } } } + + /** Categories of devices supported by the protocol. */ enum class TypeOfDevice(val code: Int) { WHEEL(0), OIL_FILTER(1), @@ -36,11 +50,17 @@ object Helper { } } } + + /** Action codes sent by the device during the finish workflow. */ object TorqueAction { const val TORQUE = "0" const val RE_TORQUE = "1" const val CANCEL = "2" } + + /** + * Represents a decoded message received from a torque device. + */ data class RxMessage( val command: String, val type: String, // Type of device @@ -49,12 +69,24 @@ object Helper { val securityCode: String, val crc16: String ) + + /** + * Wraps a command string with the protocol's start, security, crc16 and end characters. + */ 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? { /** Check header and footer*/ - if (!msg.startsWith("$") && !msg.endsWith("#\n")) return null + if (!msg.startsWith("$") || !msg.endsWith("#\n")) return null /** Remove $ and trim end */ val payload = msg.drop(1).trimEnd('*', '#',' ', '\r', '\n') val parts = payload.split(";", limit = 3) @@ -75,6 +107,13 @@ object Helper { 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, nutsPerWheel: Int): Map> { val result = mutableMapOf>() var i = 0 @@ -93,6 +132,10 @@ object Helper { } return result } + + /** + * Standardizes device identifiers (MAC addresses or UUIDs) for display. + */ fun formatDeviceId(deviceId: String?): String { if(deviceId == null) return "" return if (deviceId.contains(":")) { diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/NetworkMonitor.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/NetworkMonitor.kt index 824e9eb..71574d0 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/NetworkMonitor.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/NetworkMonitor.kt @@ -2,12 +2,29 @@ package com.digitoolsolutions.app.torquevaultkmp.utils import kotlinx.coroutines.flow.StateFlow +/** + * Represents the connectivity status of the device. + */ enum class NetworkStatus { Available, Unavailable } +/** + * Interface for monitoring real-time network connectivity changes. + */ interface NetworkMonitor { + /** + * Observable stream of the current [NetworkStatus]. + */ val status: StateFlow + + /** + * Starts the network monitoring process. + */ fun start() + + /** + * Stops the network monitoring process. + */ fun stop() -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/NoOpAntilog.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/NoOpAntilog.kt index 5ecb272..8979a19 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/NoOpAntilog.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/NoOpAntilog.kt @@ -3,7 +3,11 @@ package com.digitoolsolutions.app.torquevaultkmp.utils import io.github.aakira.napier.Antilog 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() { override fun isEnable(priority: LogLevel, tag: String?): Boolean = false override fun performLog(priority: LogLevel, tag: String?, throwable: Throwable?, message: String?) {} -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/PermissionHandler.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/PermissionHandler.kt index 9b3df51..09857f5 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/PermissionHandler.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/PermissionHandler.kt @@ -3,14 +3,40 @@ package com.digitoolsolutions.app.torquevaultkmp.utils import androidx.compose.runtime.Composable import kotlinx.coroutines.flow.StateFlow +/** + * Interface defining platform-specific logic for handling runtime permissions and hardware features. + */ 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 fun HandlePermissionRequest( onPermissionResult: (Boolean) -> Unit, content: @Composable (requestPermissions: () -> Unit) -> Unit ) + + /** + * Indicates if the device hardware supports Bluetooth Low Energy. + */ 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 + + /** + * Requests the user to enable Location services. + */ fun enableLocation() + + /** + * Triggers a check of the current Location service state. + */ fun checkLocationState() } diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/ShareLog.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/ShareLog.kt index 7fa271f..e16693f 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/ShareLog.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/ShareLog.kt @@ -1,6 +1,16 @@ package com.digitoolsolutions.app.torquevaultkmp.utils +/** + * Interface defining platform-specific logic for sharing log files. + */ interface ShareLog { + /** + * Exports and shares the provided [content] as a plain text file. + */ fun exportTextFile(content: String) + + /** + * Exports and shares the provided [content] as a CSV file. + */ fun exportCSVFile(content: String) -} \ No newline at end of file +} diff --git a/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/BluetoothManager.ios.kt b/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/BluetoothManager.ios.kt index 0e60641..ee89b28 100644 --- a/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/BluetoothManager.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/utils/BluetoothManager.ios.kt @@ -10,6 +10,12 @@ import platform.darwin.NSObject import platform.darwin.dispatch_async 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 { private val _isBluetoothEnabled = MutableStateFlow(false) override val isBluetoothEnabled: StateFlow = _isBluetoothEnabled.asStateFlow()