From 5e0f2f1bb7d69b9368fa101a921d7d579f7879c2 Mon Sep 17 00:00:00 2001 From: gogacoder Date: Wed, 28 Feb 2024 23:41:30 +0700 Subject: [PATCH] Joystick & Pid Configuration Flexible PID configuration and joysticks were added --- .idea/deploymentTargetDropDown.xml | 10 + .idea/gradle.xml | 5 +- .idea/kotlinc.xml | 2 +- .idea/migrations.xml | 10 + app/build.gradle.kts | 3 +- app/src/main/AndroidManifest.xml | 4 +- .../java/com/helible/pilot/MainActivity.kt | 24 +- .../{ => components}/NotImplementedPage.kt | 2 +- .../pilot/components/console/ConsoleScreen.kt | 33 ++ .../components/pidSettings/PidSettingsPage.kt | 283 ++++++++++++++++++ .../pilot/controllers/BluetoothController.kt | 6 +- .../helible/pilot/controllers/DeviceState.kt | 9 - .../helible/pilot/dataclasses/DeviceState.kt | 1 + .../pilot/dataclasses/GeneralMessage.kt | 7 + .../helible/pilot/dataclasses/MessageType.kt | 6 + .../helible/pilot/dataclasses/PidParams.kt | 10 + .../dataclasses/PidSettingRequiredMessage.kt | 8 + .../helible/pilot/dataclasses/PidSettings.kt | 10 + .../pilot/dataclasses/StickPosition.kt | 6 + .../exceptions/TransferFailedException.kt | 5 + .../BluetoothDataTransferService.kt | 36 ++- .../pilot/viewmodels/BluetoothViewModel.kt | 112 ++++++- app/src/main/res/values/themes.xml | 2 +- build.gradle.kts | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle.kts | 2 + 26 files changed, 534 insertions(+), 66 deletions(-) create mode 100644 .idea/deploymentTargetDropDown.xml create mode 100644 .idea/migrations.xml rename app/src/main/java/com/helible/pilot/{ => components}/NotImplementedPage.kt (95%) create mode 100644 app/src/main/java/com/helible/pilot/components/console/ConsoleScreen.kt create mode 100644 app/src/main/java/com/helible/pilot/components/pidSettings/PidSettingsPage.kt delete mode 100644 app/src/main/java/com/helible/pilot/controllers/DeviceState.kt create mode 100644 app/src/main/java/com/helible/pilot/dataclasses/GeneralMessage.kt create mode 100644 app/src/main/java/com/helible/pilot/dataclasses/MessageType.kt create mode 100644 app/src/main/java/com/helible/pilot/dataclasses/PidParams.kt create mode 100644 app/src/main/java/com/helible/pilot/dataclasses/PidSettingRequiredMessage.kt create mode 100644 app/src/main/java/com/helible/pilot/dataclasses/PidSettings.kt create mode 100644 app/src/main/java/com/helible/pilot/dataclasses/StickPosition.kt create mode 100644 app/src/main/java/com/helible/pilot/exceptions/TransferFailedException.kt diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..0c0c338 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index ae388c2..0897082 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,16 +4,15 @@ diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 4515aa3..0fc3113 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 02c26aa..3c9381e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,8 +1,6 @@ plugins { id("com.android.application") id("org.jetbrains.kotlin.android") - id("com.google.devtools.ksp").version("1.6.10-1.0.4") - id("org.jlleitschuh.gradle.ktlint").version("12.0.3") } android { @@ -65,6 +63,7 @@ dependencies { implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1") implementation("androidx.navigation:navigation-compose:2.6.0") implementation("com.squareup.moshi:moshi-kotlin:1.14.0") + implementation("com.github.manalkaff:JetStick:1.2") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1ba0bdf..2f25e76 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,12 +40,12 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.Testblue" + android:theme="@style/Theme.Main" tools:targetApi="31"> + android:theme="@style/Theme.Main"> diff --git a/app/src/main/java/com/helible/pilot/MainActivity.kt b/app/src/main/java/com/helible/pilot/MainActivity.kt index 789bf76..953f007 100644 --- a/app/src/main/java/com/helible/pilot/MainActivity.kt +++ b/app/src/main/java/com/helible/pilot/MainActivity.kt @@ -6,10 +6,8 @@ import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent -import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -18,8 +16,11 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.helible.pilot.components.CalibrationPage +import com.helible.pilot.components.NotImplementedPage +import com.helible.pilot.components.console.ConsolePage import com.helible.pilot.components.deviceScreen.DeviceControlScreen import com.helible.pilot.components.deviceScreen.defaultDeviceActionsList +import com.helible.pilot.components.pidSettings.PidSettingsPage import com.helible.pilot.components.scannerScreen.ScannerScreen import com.helible.pilot.permissions.PermissionsLauncher import com.helible.pilot.permissions.PermissionsRequest @@ -141,13 +142,10 @@ class MainActivity : ComponentActivity() { } composable("console/{title}") { backStackEntry -> - NotImplementedPage( + ConsolePage( title = backStackEntry.arguments?.getString("title") ?: "null", navigateBack = { navController.popBackStack() } ) - Button(onClick = { bluetoothViewModel.sendHelloWorld() }) { - Text("Click me!") - } } composable("codeblocks/{title}") { backStackEntry -> @@ -174,10 +172,20 @@ class MainActivity : ComponentActivity() { } composable("pid_settings/{title}") { backStackEntry -> - NotImplementedPage( + PidSettingsPage( title = backStackEntry.arguments?.getString("title") ?: "null", - navigateBack = { navController.popBackStack() } + navigateBack = { + navController.popBackStack() + bluetoothViewModel.clearPidSettings() + }, + requestPidSettings = { bluetoothViewModel.requestPidSettings() }, + setPidSettings = {settings -> bluetoothViewModel.applyPidSettings(settings)}, + deviceState = bluetoothState.deviceState ) + BackHandler { + navController.popBackStack() + bluetoothViewModel.clearPidSettings() + } } composable("reports/{title}") { backStackEntry -> diff --git a/app/src/main/java/com/helible/pilot/NotImplementedPage.kt b/app/src/main/java/com/helible/pilot/components/NotImplementedPage.kt similarity index 95% rename from app/src/main/java/com/helible/pilot/NotImplementedPage.kt rename to app/src/main/java/com/helible/pilot/components/NotImplementedPage.kt index 4aeec28..d615fde 100644 --- a/app/src/main/java/com/helible/pilot/NotImplementedPage.kt +++ b/app/src/main/java/com/helible/pilot/components/NotImplementedPage.kt @@ -1,4 +1,4 @@ -package com.helible.pilot +package com.helible.pilot.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth diff --git a/app/src/main/java/com/helible/pilot/components/console/ConsoleScreen.kt b/app/src/main/java/com/helible/pilot/components/console/ConsoleScreen.kt new file mode 100644 index 0000000..d296409 --- /dev/null +++ b/app/src/main/java/com/helible/pilot/components/console/ConsoleScreen.kt @@ -0,0 +1,33 @@ +package com.helible.pilot.components.console + +import android.util.Log +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.helible.pilot.components.BlankPage +import com.helible.pilot.dataclasses.DeviceStatus +import com.manalkaff.jetstick.JoyStick + +@Composable +fun ConsolePage( + title: String, + navigateBack: () -> Unit +) { + BlankPage(title = title, navigateBack = navigateBack) { + Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + JoyStick( + Modifier.padding(30.dp), + size = 150.dp, + dotSize = 30.dp + ){ x: Float, y: Float -> + Log.d("JoyStick", "$x, $y") + } + + } + } +} diff --git a/app/src/main/java/com/helible/pilot/components/pidSettings/PidSettingsPage.kt b/app/src/main/java/com/helible/pilot/components/pidSettings/PidSettingsPage.kt new file mode 100644 index 0000000..78ab527 --- /dev/null +++ b/app/src/main/java/com/helible/pilot/components/pidSettings/PidSettingsPage.kt @@ -0,0 +1,283 @@ +package com.helible.pilot.components.pidSettings + +import android.util.Log +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.toSize +import com.helible.pilot.components.BlankPage +import com.helible.pilot.dataclasses.DeviceState +import com.helible.pilot.dataclasses.DeviceStatus +import com.helible.pilot.dataclasses.PidParams +import com.helible.pilot.dataclasses.PidSettings + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PidSettingsPage( + title: String, + navigateBack: () -> Unit, + requestPidSettings: () -> Unit, + setPidSettings: (PidSettings) -> Unit, + deviceState: DeviceState?, +) { + BlankPage(title = title, navigateBack = navigateBack) { + var pValue by remember { mutableStateOf("") } + var iValue by remember { mutableStateOf("") } + var dValue by remember { mutableStateOf("") } + var selectedRegulator by remember { mutableStateOf(1) } + val dropdownMenuItems = listOf("PID 1", "PID 2", "PID 3") + + LaunchedEffect(null) { + requestPidSettings() + } + LaunchedEffect(deviceState?.pidSettings) { + if (deviceState?.pidSettings != null) { + val pidSettings = deviceState.pidSettings + when(selectedRegulator){ + 1 -> { + pidSettings.p1.p.toString().also { pValue = it } + pidSettings.p1.i.toString().also { iValue = it } + pidSettings.p1.d.toString().also { dValue = it } + } + 2 -> { + pidSettings.p2.p.toString().also { pValue = it } + pidSettings.p2.i.toString().also { iValue = it } + pidSettings.p2.d.toString().also { dValue = it } + } + 3 -> { + pidSettings.p3.p.toString().also { pValue = it } + pidSettings.p3.i.toString().also { iValue = it } + pidSettings.p3.d.toString().also { dValue = it } + } + } + } + } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .padding(10.dp) + ) + { + if (deviceState?.status != DeviceStatus.Idle) { + Text( + text = "Этот раздел доступен только с подключенным заряженным бездействующим устройством.", + textAlign = TextAlign.Center + ) + } else if (deviceState.pidSettings == null) { + Column { + CircularProgressIndicator(modifier = Modifier.padding(10.dp)) + Text(text = "Синхронизация...") + } + } else { + val pidSettings = deviceState.pidSettings + + OutlinedDropdownMenu( + label = "ПИД регулятор", + suggestions = dropdownMenuItems, + onChange = { selected -> + Log.i("BluetoothVM", selected) + when (dropdownMenuItems.indexOf(selected)) { + 0 -> { + selectedRegulator = 1 + pidSettings.p1.p.toString().also { pValue = it } + pidSettings.p1.i.toString().also { iValue = it } + pidSettings.p1.d.toString().also { dValue = it } + } + 1 -> { + selectedRegulator = 2 + pidSettings.p2.p.toString().also { pValue = it } + pidSettings.p2.i.toString().also { iValue = it } + pidSettings.p2.d.toString().also { dValue = it } + } + 2 -> { + selectedRegulator = 3 + pidSettings.p3.p.toString().also { pValue = it } + pidSettings.p3.i.toString().also { iValue = it } + pidSettings.p3.d.toString().also { dValue = it } + } + } + }, + modifier = Modifier.padding(10.dp) + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier.padding(10.dp) + ) { + OutlinedTextField( + value = pValue, + onValueChange = { pValue = it }, + label = { Text(text = "P") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.weight(1f) + ) + + OutlinedTextField( + value = iValue, + onValueChange = { iValue = it }, + label = { Text(text = "I") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.weight(1f) + ) + + OutlinedTextField( + value = dValue, + onValueChange = { dValue = it }, + label = { Text(text = "D") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.weight(1f) + ) + + } + val p = pValue + val i = iValue + val d = dValue + Button( + onClick = { + when (selectedRegulator) { + 1 -> { + val newPidSettings = pidSettings.copy( + p1 = PidParams(p.toFloat(), i.toFloat(), d.toFloat()) + ) + setPidSettings(newPidSettings) + } + + 2 -> { + val newPidSettings = pidSettings.copy( + p2 = PidParams(p.toFloat(), i.toFloat(), d.toFloat()) + ) + setPidSettings(newPidSettings) + } + + 3 -> { + val newPidSettings = pidSettings.copy( + p3 = PidParams(p.toFloat(), i.toFloat(), d.toFloat()) + ) + setPidSettings(newPidSettings) + } + } + }, + enabled = isValidValue(p) && isValidValue(i) && isValidValue(d) + ) { + Text(text = "Применить") + } + } + } + } +} + +private fun isValidValue(k: String): Boolean { + return k.toFloatOrNull() != null && k.toFloat() >= 0f && k.toFloat() <= 2f +} + +@Preview(showBackground = true) +@Composable +fun PidSettingsPreview() { + PidSettingsPage( + title = "Настройки ПИД регуляторов", + navigateBack = { }, + requestPidSettings = { }, + setPidSettings = {}, + deviceState = DeviceState( + status = DeviceStatus.Idle, + pidSettings = PidSettings( + PidParams(1f, 1f, 1f), + PidParams(1f, 1f, 1f), + PidParams(1f, 1f, 1f) + ) + ) + ) +} + +@Preview(showBackground = true) +@Composable +fun DropdownDemo() { + OutlinedDropdownMenu(label = "", suggestions = listOf("A", "B"), onChange = {}) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun OutlinedDropdownMenu( + label: String, + suggestions: List, + onChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + var expanded by remember { mutableStateOf(false) } + var selectedText by remember { mutableStateOf(suggestions.first()) } + var textfieldSize by remember { mutableStateOf(Size.Zero) } + + val icon = if (expanded) + Icons.Filled.KeyboardArrowUp + else + Icons.Filled.KeyboardArrowDown + + + Column(modifier = modifier) { + OutlinedTextField( + value = selectedText, + onValueChange = { + selectedText = it + }, + modifier = Modifier + .fillMaxWidth() + .onGloballyPositioned { coordinates -> + // This value is used to assign to the DropDown the same width + textfieldSize = coordinates.size.toSize() + }, + label = { Text(label) }, + trailingIcon = { + Icon(icon, "contentDescription", + Modifier.clickable { expanded = !expanded }) + } + ) + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier + .width(with(LocalDensity.current) { textfieldSize.width.toDp() }) + ) { + suggestions.forEach { label -> + DropdownMenuItem(onClick = { + selectedText = label + expanded = false + onChange(selectedText) + }, + text = { Text(label) } + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/helible/pilot/controllers/BluetoothController.kt b/app/src/main/java/com/helible/pilot/controllers/BluetoothController.kt index 9372543..7bf725b 100644 --- a/app/src/main/java/com/helible/pilot/controllers/BluetoothController.kt +++ b/app/src/main/java/com/helible/pilot/controllers/BluetoothController.kt @@ -16,7 +16,7 @@ import android.widget.Toast import androidx.activity.ComponentActivity import com.helible.pilot.viewmodels.BluetoothDataTransferService import com.helible.pilot.dataclasses.BluetoothDeviceDomain -import com.helible.pilot.dataclasses.DeviceState +import com.helible.pilot.dataclasses.GeneralMessage import com.helible.pilot.receivers.BluetoothAdapterStateReceiver import com.helible.pilot.receivers.BluetoothStateReceiver import kotlinx.coroutines.CoroutineScope @@ -40,7 +40,7 @@ import java.util.UUID sealed interface ConnectionResult { object ConnectionEstablished : ConnectionResult - data class TransferSucceded(val message: DeviceState) : ConnectionResult + data class TransferSucceded(val message: GeneralMessage) : ConnectionResult data class Error(val message: String) : ConnectionResult } @@ -241,7 +241,7 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro } catch (e: IOException) { socket.close() currentClientSocket = null - Log.e("BluetoothController", "I/O exception: e") + Log.e("BluetoothController", "I/O exception: ${e.message}") emit(ConnectionResult.Error("Connection was interrupted")) } } diff --git a/app/src/main/java/com/helible/pilot/controllers/DeviceState.kt b/app/src/main/java/com/helible/pilot/controllers/DeviceState.kt deleted file mode 100644 index 73b9a87..0000000 --- a/app/src/main/java/com/helible/pilot/controllers/DeviceState.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.helible.pilot.controllers - -data class DeviceState( - val isHandshakeWaiting: Boolean = true, - val isIMUCalibrating: Boolean = false, - val flightMode: Boolean = false, - val batteryCharge: Int?, - val flightHeight: Float? -) \ No newline at end of file diff --git a/app/src/main/java/com/helible/pilot/dataclasses/DeviceState.kt b/app/src/main/java/com/helible/pilot/dataclasses/DeviceState.kt index 3f4fea3..9247999 100644 --- a/app/src/main/java/com/helible/pilot/dataclasses/DeviceState.kt +++ b/app/src/main/java/com/helible/pilot/dataclasses/DeviceState.kt @@ -12,4 +12,5 @@ data class DeviceState( @Json(name = "p") val pitch: Float = 0f, @Json(name = "r") val roll: Float = 0f, @Json(name = "zIn") val zInertial: Float = 0f, + val pidSettings: PidSettings? = null ) \ No newline at end of file diff --git a/app/src/main/java/com/helible/pilot/dataclasses/GeneralMessage.kt b/app/src/main/java/com/helible/pilot/dataclasses/GeneralMessage.kt new file mode 100644 index 0000000..0e20c6f --- /dev/null +++ b/app/src/main/java/com/helible/pilot/dataclasses/GeneralMessage.kt @@ -0,0 +1,7 @@ +package com.helible.pilot.dataclasses + +// This dataclass provide message content with its type without any markers +data class GeneralMessage( + val type: MessageType, + val data: String +) \ No newline at end of file diff --git a/app/src/main/java/com/helible/pilot/dataclasses/MessageType.kt b/app/src/main/java/com/helible/pilot/dataclasses/MessageType.kt new file mode 100644 index 0000000..61bfb4e --- /dev/null +++ b/app/src/main/java/com/helible/pilot/dataclasses/MessageType.kt @@ -0,0 +1,6 @@ +package com.helible.pilot.dataclasses + +enum class MessageType { + UpdateMessage, + PidSettings +} diff --git a/app/src/main/java/com/helible/pilot/dataclasses/PidParams.kt b/app/src/main/java/com/helible/pilot/dataclasses/PidParams.kt new file mode 100644 index 0000000..3eb47ef --- /dev/null +++ b/app/src/main/java/com/helible/pilot/dataclasses/PidParams.kt @@ -0,0 +1,10 @@ +package com.helible.pilot.dataclasses + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class PidParams ( + val p: Float, + val i: Float, + val d: Float +) \ No newline at end of file diff --git a/app/src/main/java/com/helible/pilot/dataclasses/PidSettingRequiredMessage.kt b/app/src/main/java/com/helible/pilot/dataclasses/PidSettingRequiredMessage.kt new file mode 100644 index 0000000..453b570 --- /dev/null +++ b/app/src/main/java/com/helible/pilot/dataclasses/PidSettingRequiredMessage.kt @@ -0,0 +1,8 @@ +package com.helible.pilot.dataclasses + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class PidSettingRequiredMessage ( + val pidSettingOpened: Boolean = true +) \ No newline at end of file diff --git a/app/src/main/java/com/helible/pilot/dataclasses/PidSettings.kt b/app/src/main/java/com/helible/pilot/dataclasses/PidSettings.kt new file mode 100644 index 0000000..e5b629d --- /dev/null +++ b/app/src/main/java/com/helible/pilot/dataclasses/PidSettings.kt @@ -0,0 +1,10 @@ +package com.helible.pilot.dataclasses + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class PidSettings ( + val p1: PidParams, + val p2: PidParams, + val p3: PidParams +) diff --git a/app/src/main/java/com/helible/pilot/dataclasses/StickPosition.kt b/app/src/main/java/com/helible/pilot/dataclasses/StickPosition.kt new file mode 100644 index 0000000..d3b516e --- /dev/null +++ b/app/src/main/java/com/helible/pilot/dataclasses/StickPosition.kt @@ -0,0 +1,6 @@ +package com.helible.pilot.dataclasses + +data class StickPosition ( + val x: Float, + val y: Float +) \ No newline at end of file diff --git a/app/src/main/java/com/helible/pilot/exceptions/TransferFailedException.kt b/app/src/main/java/com/helible/pilot/exceptions/TransferFailedException.kt new file mode 100644 index 0000000..1c8e5da --- /dev/null +++ b/app/src/main/java/com/helible/pilot/exceptions/TransferFailedException.kt @@ -0,0 +1,5 @@ +package com.helible.pilot.exceptions + +import java.io.IOException + +class TransferFailedException : IOException("Reading incoming data failed") \ No newline at end of file diff --git a/app/src/main/java/com/helible/pilot/viewmodels/BluetoothDataTransferService.kt b/app/src/main/java/com/helible/pilot/viewmodels/BluetoothDataTransferService.kt index 4e4322b..984162c 100644 --- a/app/src/main/java/com/helible/pilot/viewmodels/BluetoothDataTransferService.kt +++ b/app/src/main/java/com/helible/pilot/viewmodels/BluetoothDataTransferService.kt @@ -4,6 +4,11 @@ import android.bluetooth.BluetoothSocket import android.util.Log import com.helible.pilot.dataclasses.DeviceState import com.helible.pilot.dataclasses.DeviceStatusJsonAdapter +import com.helible.pilot.dataclasses.GeneralMessage +import com.helible.pilot.dataclasses.MessageType +import com.helible.pilot.dataclasses.PidSettings +import com.helible.pilot.exceptions.TransferFailedException +import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonEncodingException import com.squareup.moshi.Moshi import com.squareup.moshi.adapter @@ -16,19 +21,18 @@ import kotlinx.coroutines.withContext import java.io.IOException -class TransferFailedException : IOException("Reading incoming data failed") +const val maxPackageSize = 512; // bytes @ExperimentalStdlibApi class BluetoothDataTransferService( private val socket: BluetoothSocket, ) { - fun listenForIncomingMessages(): Flow { - val moshi = Moshi.Builder().add(DeviceStatusJsonAdapter()).add(KotlinJsonAdapterFactory()).build() - val deviceStateMessageAdapter = moshi.adapter() + fun listenForIncomingMessages(): Flow { return flow { if (!socket.isConnected) return@flow - val buffer = ByteArray(512) + + val buffer = ByteArray(maxPackageSize) while (true) { val byteCount: Int = try { socket.inputStream.read(buffer) @@ -36,23 +40,17 @@ class BluetoothDataTransferService( Log.e("BluetoothController", "Failed to receive incoming data") throw TransferFailedException() } + var messageData: String = buffer.decodeToString(endIndex = byteCount) + val messageType: MessageType? = MessageType.values() + .elementAtOrNull(messageData.split(";")[0].toInt()) - val messageData: String = buffer.decodeToString(endIndex = byteCount) - if (!messageData.endsWith("\n\r")) { + if (messageData.endsWith("\n\r") && messageType != null) { + messageData = messageData.dropLast(2).split(";")[1] + emit(GeneralMessage(messageType, messageData)) + Log.d("BluetoothController", "Received: $messageData") + } else { Log.i("BluetoothController", "Package end isn't valid.") Log.i("BluetoothController", messageData) - - } else { - val messageJson = messageData.dropLast(2) - try { - val deviceState = deviceStateMessageAdapter.fromJson(messageJson)!! - emit(deviceState) - Log.i("BluetoothController", "Received: $deviceState") - } catch (e: NullPointerException) { - Log.e("BluetoothController", "Nullable message received: $messageJson") - } catch (e: JsonEncodingException) { - Log.e("BluetoothController", "Invalid message received: $messageJson") - } } } }.flowOn(Dispatchers.IO) diff --git a/app/src/main/java/com/helible/pilot/viewmodels/BluetoothViewModel.kt b/app/src/main/java/com/helible/pilot/viewmodels/BluetoothViewModel.kt index 9520476..069f9a8 100644 --- a/app/src/main/java/com/helible/pilot/viewmodels/BluetoothViewModel.kt +++ b/app/src/main/java/com/helible/pilot/viewmodels/BluetoothViewModel.kt @@ -8,8 +8,14 @@ import com.helible.pilot.controllers.ConnectionResult import com.helible.pilot.dataclasses.BluetoothDevice import com.helible.pilot.dataclasses.BluetoothUiState import com.helible.pilot.dataclasses.ChangedDeviceStatus +import com.helible.pilot.dataclasses.DeviceState import com.helible.pilot.dataclasses.DeviceStatus import com.helible.pilot.dataclasses.DeviceStatusJsonAdapter +import com.helible.pilot.dataclasses.MessageType +import com.helible.pilot.dataclasses.PidSettingRequiredMessage +import com.helible.pilot.dataclasses.PidSettings +import com.squareup.moshi.JsonDataException +import com.squareup.moshi.JsonEncodingException import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import kotlinx.coroutines.Job @@ -25,7 +31,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import java.time.LocalTime class BluetoothViewModel( private val bluetoothController: BluetoothController, @@ -46,7 +51,10 @@ class BluetoothViewModel( }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), _state.value) private var deviceConnectionJob: Job? = null private val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).add(DeviceStatusJsonAdapter()).build() - private val newStatusMessageAdapter = moshi.adapter(ChangedDeviceStatus::class.java) + private val statusMessageAdapter = moshi.adapter(ChangedDeviceStatus::class.java) + private val deviceStateMessageAdapter = moshi.adapter(DeviceState::class.java) + private val pidSittingsMessageAdapter = moshi.adapter(PidSettings::class.java) + private val pidSittingsRequiredMessageAdapter = moshi.adapter(PidSettingRequiredMessage::class.java) init { bluetoothController.isConnected.onEach { isConnected -> @@ -94,10 +102,35 @@ class BluetoothViewModel( } is ConnectionResult.TransferSucceded -> { - _state.update { - it.copy( - deviceState = result.message - ) + try { + when (result.message.type) { + MessageType.PidSettings -> { + val newPidSettings = + pidSittingsMessageAdapter.fromJson(result.message.data) + _state.update { + it.copy( + deviceState = it.deviceState?.copy(pidSettings = newPidSettings) + ) + } + } + + MessageType.UpdateMessage -> { + val newDeviceState = + deviceStateMessageAdapter.fromJson(result.message.data) + if (newDeviceState != null) { + _state.update { + it.copy( + deviceState = newDeviceState.copy(pidSettings = it.deviceState?.pidSettings) + ) + } + } + + } + } + } catch (e: JsonDataException) { + Log.e("BluetoothVM", "Failed to parse message: ${result.message.data}") + } catch (e: JsonEncodingException) { + Log.e("BluetoothVM", "Failed to decode message: ${result.message.data}") } } @@ -113,13 +146,18 @@ class BluetoothViewModel( } } .catch { throwable -> - Log.e("BluetoothController", "Error occured while data transfer: ${throwable.message}") + Log.e( + "BluetoothController", + "Error occured while data transfer: ${throwable.message}" + ) bluetoothController.closeConnection() - _state.update { it.copy( - isConnected = false, - isConnecting = false, - deviceState = null - ) } + _state.update { + it.copy( + isConnected = false, + isConnecting = false, + deviceState = null + ) + } } .launchIn(viewModelScope) } @@ -176,15 +214,59 @@ class BluetoothViewModel( fun startImuCalibration() { viewModelScope.launch { - val message = newStatusMessageAdapter.toJson( + val message = statusMessageAdapter.toJson( ChangedDeviceStatus(DeviceStatus.IsImuCalibration) ) + "\n\r" - val success = bluetoothController.trySendMessage( + val isSuccess = bluetoothController.trySendMessage( message.toByteArray() ) - if(!success) { + if(!isSuccess) { Log.e("BluetoothVM", "Failed to start IMU calibration: $message") + } else { + _state.update { + it.copy( + deviceState = it.deviceState?.copy(status = DeviceStatus.IsImuCalibration) + ) + } } } } + + fun requestPidSettings() { + viewModelScope.launch { + val message = pidSittingsRequiredMessageAdapter.toJson(PidSettingRequiredMessage(true)) + "\n\r" + Log.i("BluetoothVM", "Requested PID settings: $message") + val isSuccess = bluetoothController.trySendMessage( + message.toByteArray() + ) + if(!isSuccess) { + Log.e("BluetoothVM", "Failed to request PID settings: $message") + } + } + } + + fun applyPidSettings(pidSettings: PidSettings) { + viewModelScope.launch { + val message = pidSittingsMessageAdapter.toJson(pidSettings) + "\n\r" + val isSuccess = bluetoothController.trySendMessage(message.toByteArray()) + if(!isSuccess) { + Log.e("BluetoothVM", "Failed to request PID settings: $message") + _state.update { + it.copy(errorMessage = "Не удалось обновить значения PID") + } + } else { + _state.update { + it.copy(deviceState = it.deviceState?.copy(pidSettings = pidSettings)) + } + } + } + } + + fun clearPidSettings() { + Log.i("BluetoothVM", "PidSettings cleared") + _state.update { + it.copy(deviceState = it.deviceState?.copy(pidSettings = null)) + } + Log.i("BluetoothVM", "PidSettings: ${_state.value.deviceState?.pidSettings}") + } } \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 21ebef7..6fe7e2b 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,5 @@ -