Rotors test page was added

This commit is contained in:
2024-03-08 23:05:38 +07:00
parent 0763c2e1df
commit c8abfd94c3
4 changed files with 227 additions and 11 deletions

View File

@@ -17,6 +17,7 @@ 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.NotImplementedPage
import com.helible.pilot.components.RotorsTestPage
import com.helible.pilot.components.console.ConsolePage 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
@@ -62,6 +63,7 @@ class MainActivity : ComponentActivity() {
val bluetoothState by bluetoothViewModel.state.collectAsState() val bluetoothState by bluetoothViewModel.state.collectAsState()
val selectedDevice by bluetoothViewModel.selectedDevice.collectAsState() val selectedDevice by bluetoothViewModel.selectedDevice.collectAsState()
val rotorsDuty by bluetoothViewModel.rotorsDuty.collectAsState()
LaunchedEffect(key1 = null) { LaunchedEffect(key1 = null) {
permissionLauncher.launch() permissionLauncher.launch()
@@ -165,10 +167,23 @@ class MainActivity : ComponentActivity() {
} }
composable("motor_test/{title}") composable("motor_test/{title}")
{ backStackEntry -> { backStackEntry ->
NotImplementedPage( RotorsTestPage(
title = backStackEntry.arguments?.getString("title") ?: "null", title = backStackEntry.arguments?.getString("title") ?: "null",
navigateBack = { navController.popBackStack() } rotorsDuty = rotorsDuty,
setRotorsDuty = { bluetoothViewModel.setRotorsDuty(it) },
startTelemetrySending = { bluetoothViewModel.startRotorsConfigurationTelemetry() },
stopRotors = { bluetoothViewModel.stopRotors() },
navigateBack = {
navController.popBackStack()
bluetoothViewModel.stopRotorsConfigurationTelemetry()
bluetoothViewModel.stopRotors()
}
) )
BackHandler {
navController.popBackStack()
bluetoothViewModel.stopRotorsConfigurationTelemetry()
bluetoothViewModel.stopRotors()
}
} }
composable("pid_settings/{title}") composable("pid_settings/{title}")
{ backStackEntry -> { backStackEntry ->

View File

@@ -0,0 +1,118 @@
package com.helible.pilot.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.helible.pilot.dataclasses.RotorsDuty
import com.helible.pilot.ui.theme.TestblueTheme
@Composable
fun RotorsTestPage(
title: String,
rotorsDuty: RotorsDuty,
setRotorsDuty: (duty: RotorsDuty) -> Unit,
startTelemetrySending: () -> Unit,
stopRotors: () -> Unit,
navigateBack: () -> Unit,
) {
BlankPage(title = title, navigateBack = navigateBack) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
LaunchedEffect(null) {
startTelemetrySending()
}
Text(
text = "R1",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(top = 10.dp)
)
Slider(
value = rotorsDuty.r1.toFloat(),
onValueChange = { setRotorsDuty(rotorsDuty.copy(r1 = it.toInt().toShort())) },
valueRange = 0f..5000f,
steps = 10
)
Text(
text = "При перемещении слайдера вправо ротор 1 должен вращаться против часовой стрелки, если смотреть сверху.",
textAlign = TextAlign.Center
)
Text(
text = "R2",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(top = 10.dp)
)
Slider(
value = rotorsDuty.r2.toFloat(),
onValueChange = { setRotorsDuty(rotorsDuty.copy(r2 = it.toInt().toShort())) },
valueRange = -5000f..5000f,
steps = 10
)
Text(
text = "При перемещении слайдера вправо ротор 1 должен вращаться по часовой стрелке, если смотреть сверху.",
textAlign = TextAlign.Center
)
Text(
text = "R3",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(top = 10.dp)
)
Slider(
value = rotorsDuty.r3.toFloat(),
onValueChange = { setRotorsDuty(rotorsDuty.copy(r3 = it.toInt().toShort())) },
valueRange = 0f..5000f,
steps = 10
)
Text(
text = "При отклонении слайдера вправо от центра ротор 1 должен вращаться по часовой стрелке, а при отклонении влево - против часовой, если смотреть сверху.",
textAlign = TextAlign.Center
)
FloatingActionButton(
onClick = { stopRotors() },
containerColor = Color(245, 47, 7),
modifier = Modifier.padding(top = 10.dp)
) {
Text(
text = "СТОП",
style = MaterialTheme.typography.headlineLarge,
color = Color.White,
modifier = Modifier.padding(10.dp)
)
}
}
}
}
@Preview(showBackground = true)
@Composable
fun RotorsTestPagePreview() {
TestblueTheme {
RotorsTestPage(
title = "Тестирование моторов",
rotorsDuty = RotorsDuty(5, 5, 5),
setRotorsDuty = { _ -> },
startTelemetrySending = {},
stopRotors = {},
navigateBack = { /*TODO*/ }
)
}
}

View File

@@ -0,0 +1,10 @@
package com.helible.pilot.dataclasses
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class RotorsDuty(
val r1: Short,
val r2: Short,
val r3: Short
)

View File

@@ -14,11 +14,14 @@ import com.helible.pilot.dataclasses.DeviceStatusJsonAdapter
import com.helible.pilot.dataclasses.MessageType import com.helible.pilot.dataclasses.MessageType
import com.helible.pilot.dataclasses.PidSettingRequiredMessage import com.helible.pilot.dataclasses.PidSettingRequiredMessage
import com.helible.pilot.dataclasses.PidSettings import com.helible.pilot.dataclasses.PidSettings
import com.helible.pilot.dataclasses.RotorsDuty
import com.helible.pilot.dataclasses.StopMessage
import com.squareup.moshi.JsonDataException 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.kotlin.reflect.KotlinJsonAdapterFactory import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
@@ -26,6 +29,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
@@ -49,14 +53,28 @@ class BluetoothViewModel(
pairedBluetoothDevices = pairedDevices pairedBluetoothDevices = pairedDevices
) )
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), _state.value) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), _state.value)
private val _rotorsDuty: MutableStateFlow<RotorsDuty> = MutableStateFlow(RotorsDuty(0, 0, 0))
private val _isRotorsTelemetryEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
val rotorsDuty: StateFlow<RotorsDuty>
get() = _rotorsDuty.asStateFlow()
private var rotorsTelemetryJob: Job? = null
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 statusMessageAdapter = moshi.adapter(ChangedDeviceStatus::class.java) private val statusMessageAdapter = moshi.adapter(ChangedDeviceStatus::class.java)
private val deviceStateMessageAdapter = moshi.adapter(DeviceState::class.java) private val deviceStateMessageAdapter = moshi.adapter(DeviceState::class.java)
private val pidSittingsMessageAdapter = moshi.adapter(PidSettings::class.java) private val pidSittingsMessageAdapter = moshi.adapter(PidSettings::class.java)
private val pidSittingsRequiredMessageAdapter = moshi.adapter(PidSettingRequiredMessage::class.java) private val pidSittingsRequiredMessageAdapter =
moshi.adapter(PidSettingRequiredMessage::class.java)
private val rotorDutyMessageAdapter = moshi.adapter(RotorsDuty::class.java)
private val stopAllRotorsMessageAdapter = moshi.adapter(StopMessage::class.java)
companion object { companion object {
const val messageDelimeter = "\n" const val messageDelimiter = "\n"
const val telemetryPauseDuractionMs: Long = 100
} }
init { init {
@@ -210,7 +228,7 @@ class BluetoothViewModel(
viewModelScope.launch { viewModelScope.launch {
val message = statusMessageAdapter.toJson( val message = statusMessageAdapter.toJson(
ChangedDeviceStatus(DeviceStatus.IsImuCalibration) ChangedDeviceStatus(DeviceStatus.IsImuCalibration)
) + messageDelimeter ) + messageDelimiter
val isSuccess = bluetoothController.trySendMessage( val isSuccess = bluetoothController.trySendMessage(
message.toByteArray() message.toByteArray()
) )
@@ -228,7 +246,8 @@ class BluetoothViewModel(
fun requestPidSettings() { fun requestPidSettings() {
viewModelScope.launch { viewModelScope.launch {
val message = pidSittingsRequiredMessageAdapter.toJson(PidSettingRequiredMessage(true)) + messageDelimeter val message =
pidSittingsRequiredMessageAdapter.toJson(PidSettingRequiredMessage(true)) + messageDelimiter
Log.i("BluetoothVM", "Requested PID settings: $message") Log.i("BluetoothVM", "Requested PID settings: $message")
val isSuccess = bluetoothController.trySendMessage( val isSuccess = bluetoothController.trySendMessage(
message.toByteArray() message.toByteArray()
@@ -241,7 +260,7 @@ class BluetoothViewModel(
fun applyPidSettings(pidSettings: PidSettings) { fun applyPidSettings(pidSettings: PidSettings) {
viewModelScope.launch { viewModelScope.launch {
val message = pidSittingsMessageAdapter.toJson(pidSettings) + messageDelimeter val message = pidSittingsMessageAdapter.toJson(pidSettings) + messageDelimiter
val isSuccess = bluetoothController.trySendMessage(message.toByteArray()) val isSuccess = bluetoothController.trySendMessage(message.toByteArray())
if (!isSuccess) { if (!isSuccess) {
Log.e("BluetoothVM", "Failed to request PID settings: $message") Log.e("BluetoothVM", "Failed to request PID settings: $message")
@@ -263,4 +282,58 @@ class BluetoothViewModel(
} }
Log.i("BluetoothVM", "PidSettings: ${_state.value.deviceState?.pidSettings}") Log.i("BluetoothVM", "PidSettings: ${_state.value.deviceState?.pidSettings}")
} }
private fun sendRotorsDuty() {
viewModelScope.launch {
val message = rotorDutyMessageAdapter.toJson(
_rotorsDuty.value
) + messageDelimiter
val isSuccess = bluetoothController.trySendMessage(
message.toByteArray()
)
if (!isSuccess) {
Log.e("BluetoothVM", "Failed to send rotors telemetry: $message")
}
}
}
fun startRotorsConfigurationTelemetry() {
Log.i("BluetoothVM", "Start send rotors configuration telemetry...")
if(_isRotorsTelemetryEnabled.value) return
_isRotorsTelemetryEnabled.update { true }
flow {
while(_isRotorsTelemetryEnabled.value) {
emit(Unit)
delay(telemetryPauseDuractionMs)
}
}.onEach{
sendRotorsDuty()
Log.d("BluetoothVM", "Sended rotors telemetry")
}.launchIn(viewModelScope)
}
fun stopRotorsConfigurationTelemetry() {
Log.i("BluetoothVM", "Stop send rotors configuration periodically...")
rotorsTelemetryJob = null
_isRotorsTelemetryEnabled.update { false }
}
fun setRotorsDuty(newRotorsDuty: RotorsDuty) {
_rotorsDuty.update { newRotorsDuty }
}
fun stopRotors() {
viewModelScope.launch {
val message = stopAllRotorsMessageAdapter.toJson(StopMessage()) + messageDelimiter
val isSuccess = bluetoothController.trySendMessage(message.toByteArray())
if (!isSuccess) {
Log.e("BluetoothVM", "Failed to stop all rotors: $message")
_state.update {
it.copy(errorMessage = "Не удалось остановить моторы!")
}
} else {
_rotorsDuty.update { RotorsDuty(0, 0, 0) }
}
}
}
} }