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 @@
-
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 3b9bab8..53e9805 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
- id("com.android.application") version "8.1.0" apply false
+ id("com.android.application") version "8.2.2" apply false
id("org.jetbrains.kotlin.android") version "1.8.10" apply false
id("org.jetbrains.kotlin.jvm") version "1.8.10" apply false
}
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 99bd28f..c99c16a 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Sun Aug 13 15:00:54 KRAT 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/settings.gradle.kts b/settings.gradle.kts
index fb3fc77..8114d19 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -3,6 +3,7 @@ pluginManagement {
google()
mavenCentral()
gradlePluginPortal()
+ maven(url = "https://jitpack.io")
}
}
dependencyResolutionManagement {
@@ -10,6 +11,7 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
+ maven(url = "https://jitpack.io")
}
}