Joystick & Pid Configuration

Flexible PID configuration and joysticks were added
This commit is contained in:
2024-02-28 23:41:30 +07:00
parent 3517414ec1
commit 5e0f2f1bb7
26 changed files with 534 additions and 66 deletions

10
.idea/deploymentTargetDropDown.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<value>
<entry key="app">
<State />
</entry>
</value>
</component>
</project>

5
.idea/gradle.xml generated
View File

@@ -4,16 +4,15 @@
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="jbr-17" /> <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" /> <option value="$PROJECT_DIR$/app" />
</set> </set>
</option> </option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
</component> </component>

2
.idea/kotlinc.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="1.6.10" /> <option name="version" value="1.8.10" />
</component> </component>
</project> </project>

10
.idea/migrations.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

View File

@@ -1,8 +1,6 @@
plugins { plugins {
id("com.android.application") id("com.android.application")
id("org.jetbrains.kotlin.android") 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 { android {
@@ -65,6 +63,7 @@ dependencies {
implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1") implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1")
implementation("androidx.navigation:navigation-compose:2.6.0") implementation("androidx.navigation:navigation-compose:2.6.0")
implementation("com.squareup.moshi:moshi-kotlin:1.14.0") implementation("com.squareup.moshi:moshi-kotlin:1.14.0")
implementation("com.github.manalkaff:JetStick:1.2")
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.1.5")

View File

@@ -40,12 +40,12 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Testblue" android:theme="@style/Theme.Main"
tools:targetApi="31"> tools:targetApi="31">
<activity <activity
android:name="com.helible.pilot.MainActivity" android:name="com.helible.pilot.MainActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.Testblue"> android:theme="@style/Theme.Main">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />

View File

@@ -6,10 +6,8 @@ import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -18,8 +16,11 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.helible.pilot.components.CalibrationPage 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.DeviceControlScreen
import com.helible.pilot.components.deviceScreen.defaultDeviceActionsList 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.components.scannerScreen.ScannerScreen
import com.helible.pilot.permissions.PermissionsLauncher import com.helible.pilot.permissions.PermissionsLauncher
import com.helible.pilot.permissions.PermissionsRequest import com.helible.pilot.permissions.PermissionsRequest
@@ -141,13 +142,10 @@ class MainActivity : ComponentActivity() {
} }
composable("console/{title}") composable("console/{title}")
{ backStackEntry -> { backStackEntry ->
NotImplementedPage( ConsolePage(
title = backStackEntry.arguments?.getString("title") ?: "null", title = backStackEntry.arguments?.getString("title") ?: "null",
navigateBack = { navController.popBackStack() } navigateBack = { navController.popBackStack() }
) )
Button(onClick = { bluetoothViewModel.sendHelloWorld() }) {
Text("Click me!")
}
} }
composable("codeblocks/{title}") composable("codeblocks/{title}")
{ backStackEntry -> { backStackEntry ->
@@ -174,10 +172,20 @@ class MainActivity : ComponentActivity() {
} }
composable("pid_settings/{title}") composable("pid_settings/{title}")
{ backStackEntry -> { backStackEntry ->
NotImplementedPage( PidSettingsPage(
title = backStackEntry.arguments?.getString("title") ?: "null", 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}") composable("reports/{title}")
{ backStackEntry -> { backStackEntry ->

View File

@@ -1,4 +1,4 @@
package com.helible.pilot package com.helible.pilot.components
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth

View File

@@ -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")
}
}
}
}

View File

@@ -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<String>,
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) }
)
}
}
}
}

View File

@@ -16,7 +16,7 @@ import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import com.helible.pilot.viewmodels.BluetoothDataTransferService import com.helible.pilot.viewmodels.BluetoothDataTransferService
import com.helible.pilot.dataclasses.BluetoothDeviceDomain 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.BluetoothAdapterStateReceiver
import com.helible.pilot.receivers.BluetoothStateReceiver import com.helible.pilot.receivers.BluetoothStateReceiver
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -40,7 +40,7 @@ import java.util.UUID
sealed interface ConnectionResult { sealed interface ConnectionResult {
object ConnectionEstablished : 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 data class Error(val message: String) : ConnectionResult
} }
@@ -241,7 +241,7 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro
} catch (e: IOException) { } catch (e: IOException) {
socket.close() socket.close()
currentClientSocket = null currentClientSocket = null
Log.e("BluetoothController", "I/O exception: e") Log.e("BluetoothController", "I/O exception: ${e.message}")
emit(ConnectionResult.Error("Connection was interrupted")) emit(ConnectionResult.Error("Connection was interrupted"))
} }
} }

View File

@@ -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?
)

View File

@@ -12,4 +12,5 @@ data class DeviceState(
@Json(name = "p") val pitch: Float = 0f, @Json(name = "p") val pitch: Float = 0f,
@Json(name = "r") val roll: Float = 0f, @Json(name = "r") val roll: Float = 0f,
@Json(name = "zIn") val zInertial: Float = 0f, @Json(name = "zIn") val zInertial: Float = 0f,
val pidSettings: PidSettings? = null
) )

View File

@@ -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
)

View File

@@ -0,0 +1,6 @@
package com.helible.pilot.dataclasses
enum class MessageType {
UpdateMessage,
PidSettings
}

View File

@@ -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
)

View File

@@ -0,0 +1,8 @@
package com.helible.pilot.dataclasses
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class PidSettingRequiredMessage (
val pidSettingOpened: Boolean = true
)

View File

@@ -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
)

View File

@@ -0,0 +1,6 @@
package com.helible.pilot.dataclasses
data class StickPosition (
val x: Float,
val y: Float
)

View File

@@ -0,0 +1,5 @@
package com.helible.pilot.exceptions
import java.io.IOException
class TransferFailedException : IOException("Reading incoming data failed")

View File

@@ -4,6 +4,11 @@ import android.bluetooth.BluetoothSocket
import android.util.Log import android.util.Log
import com.helible.pilot.dataclasses.DeviceState import com.helible.pilot.dataclasses.DeviceState
import com.helible.pilot.dataclasses.DeviceStatusJsonAdapter 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.JsonEncodingException
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter import com.squareup.moshi.adapter
@@ -16,19 +21,18 @@ import kotlinx.coroutines.withContext
import java.io.IOException import java.io.IOException
class TransferFailedException : IOException("Reading incoming data failed") const val maxPackageSize = 512; // bytes
@ExperimentalStdlibApi @ExperimentalStdlibApi
class BluetoothDataTransferService( class BluetoothDataTransferService(
private val socket: BluetoothSocket, private val socket: BluetoothSocket,
) { ) {
fun listenForIncomingMessages(): Flow<DeviceState> { fun listenForIncomingMessages(): Flow<GeneralMessage> {
val moshi = Moshi.Builder().add(DeviceStatusJsonAdapter()).add(KotlinJsonAdapterFactory()).build()
val deviceStateMessageAdapter = moshi.adapter<DeviceState>()
return flow { return flow {
if (!socket.isConnected) if (!socket.isConnected)
return@flow return@flow
val buffer = ByteArray(512)
val buffer = ByteArray(maxPackageSize)
while (true) { while (true) {
val byteCount: Int = try { val byteCount: Int = try {
socket.inputStream.read(buffer) socket.inputStream.read(buffer)
@@ -36,23 +40,17 @@ class BluetoothDataTransferService(
Log.e("BluetoothController", "Failed to receive incoming data") Log.e("BluetoothController", "Failed to receive incoming data")
throw TransferFailedException() 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") && messageType != null) {
if (!messageData.endsWith("\n\r")) { 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", "Package end isn't valid.")
Log.i("BluetoothController", messageData) 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) }.flowOn(Dispatchers.IO)

View File

@@ -8,8 +8,14 @@ import com.helible.pilot.controllers.ConnectionResult
import com.helible.pilot.dataclasses.BluetoothDevice import com.helible.pilot.dataclasses.BluetoothDevice
import com.helible.pilot.dataclasses.BluetoothUiState import com.helible.pilot.dataclasses.BluetoothUiState
import com.helible.pilot.dataclasses.ChangedDeviceStatus import com.helible.pilot.dataclasses.ChangedDeviceStatus
import com.helible.pilot.dataclasses.DeviceState
import com.helible.pilot.dataclasses.DeviceStatus import com.helible.pilot.dataclasses.DeviceStatus
import com.helible.pilot.dataclasses.DeviceStatusJsonAdapter 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.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -25,7 +31,6 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.LocalTime
class BluetoothViewModel( class BluetoothViewModel(
private val bluetoothController: BluetoothController, private val bluetoothController: BluetoothController,
@@ -46,7 +51,10 @@ class BluetoothViewModel(
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), _state.value) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), _state.value)
private var deviceConnectionJob: Job? = null private var deviceConnectionJob: Job? = null
private val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).add(DeviceStatusJsonAdapter()).build() 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 { init {
bluetoothController.isConnected.onEach { isConnected -> bluetoothController.isConnected.onEach { isConnected ->
@@ -94,10 +102,35 @@ class BluetoothViewModel(
} }
is ConnectionResult.TransferSucceded -> { is ConnectionResult.TransferSucceded -> {
_state.update { try {
it.copy( when (result.message.type) {
deviceState = result.message 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 -> .catch { throwable ->
Log.e("BluetoothController", "Error occured while data transfer: ${throwable.message}") Log.e(
"BluetoothController",
"Error occured while data transfer: ${throwable.message}"
)
bluetoothController.closeConnection() bluetoothController.closeConnection()
_state.update { it.copy( _state.update {
isConnected = false, it.copy(
isConnecting = false, isConnected = false,
deviceState = null isConnecting = false,
) } deviceState = null
)
}
} }
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }
@@ -176,15 +214,59 @@ class BluetoothViewModel(
fun startImuCalibration() { fun startImuCalibration() {
viewModelScope.launch { viewModelScope.launch {
val message = newStatusMessageAdapter.toJson( val message = statusMessageAdapter.toJson(
ChangedDeviceStatus(DeviceStatus.IsImuCalibration) ChangedDeviceStatus(DeviceStatus.IsImuCalibration)
) + "\n\r" ) + "\n\r"
val success = bluetoothController.trySendMessage( val isSuccess = bluetoothController.trySendMessage(
message.toByteArray() message.toByteArray()
) )
if(!success) { if(!isSuccess) {
Log.e("BluetoothVM", "Failed to start IMU calibration: $message") 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}")
}
} }

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.Testblue" parent="android:Theme.Material.Light.NoActionBar" /> <style name="Theme.Main" parent="android:Theme.Material.Light.NoActionBar" />
</resources> </resources>

View File

@@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { 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.android") version "1.8.10" apply false
id("org.jetbrains.kotlin.jvm") version "1.8.10" apply false id("org.jetbrains.kotlin.jvm") version "1.8.10" apply false
} }

View File

@@ -1,6 +1,6 @@
#Sun Aug 13 15:00:54 KRAT 2023 #Sun Aug 13 15:00:54 KRAT 2023
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -3,6 +3,7 @@ pluginManagement {
google() google()
mavenCentral() mavenCentral()
gradlePluginPortal() gradlePluginPortal()
maven(url = "https://jitpack.io")
} }
} }
dependencyResolutionManagement { dependencyResolutionManagement {
@@ -10,6 +11,7 @@ dependencyResolutionManagement {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven(url = "https://jitpack.io")
} }
} }