diff --git a/app/src/main/java/com/helible/pilot/MainActivity.kt b/app/src/main/java/com/helible/pilot/MainActivity.kt index 953f007..a447e98 100644 --- a/app/src/main/java/com/helible/pilot/MainActivity.kt +++ b/app/src/main/java/com/helible/pilot/MainActivity.kt @@ -17,6 +17,7 @@ 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.RotorsTestPage import com.helible.pilot.components.console.ConsolePage import com.helible.pilot.components.deviceScreen.DeviceControlScreen import com.helible.pilot.components.deviceScreen.defaultDeviceActionsList @@ -62,6 +63,7 @@ class MainActivity : ComponentActivity() { val bluetoothState by bluetoothViewModel.state.collectAsState() val selectedDevice by bluetoothViewModel.selectedDevice.collectAsState() + val rotorsDuty by bluetoothViewModel.rotorsDuty.collectAsState() LaunchedEffect(key1 = null) { permissionLauncher.launch() @@ -165,10 +167,23 @@ class MainActivity : ComponentActivity() { } composable("motor_test/{title}") { backStackEntry -> - NotImplementedPage( + RotorsTestPage( 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}") { backStackEntry -> diff --git a/app/src/main/java/com/helible/pilot/components/RotorsTestPage.kt b/app/src/main/java/com/helible/pilot/components/RotorsTestPage.kt new file mode 100644 index 0000000..3a427cc --- /dev/null +++ b/app/src/main/java/com/helible/pilot/components/RotorsTestPage.kt @@ -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*/ } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/helible/pilot/dataclasses/RotorsDuty.kt b/app/src/main/java/com/helible/pilot/dataclasses/RotorsDuty.kt new file mode 100644 index 0000000..0445f81 --- /dev/null +++ b/app/src/main/java/com/helible/pilot/dataclasses/RotorsDuty.kt @@ -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 +) \ No newline at end of file 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 a0de883..a55ca09 100644 --- a/app/src/main/java/com/helible/pilot/viewmodels/BluetoothViewModel.kt +++ b/app/src/main/java/com/helible/pilot/viewmodels/BluetoothViewModel.kt @@ -14,11 +14,14 @@ 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.helible.pilot.dataclasses.RotorsDuty +import com.helible.pilot.dataclasses.StopMessage 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 +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -26,6 +29,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn @@ -49,14 +53,28 @@ class BluetoothViewModel( pairedBluetoothDevices = pairedDevices ) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), _state.value) + private val _rotorsDuty: MutableStateFlow = MutableStateFlow(RotorsDuty(0, 0, 0)) + private val _isRotorsTelemetryEnabled: MutableStateFlow = MutableStateFlow(false) + + val rotorsDuty: StateFlow + get() = _rotorsDuty.asStateFlow() + + private var rotorsTelemetryJob: 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 deviceStateMessageAdapter = moshi.adapter(DeviceState::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 { - const val messageDelimeter = "\n" + const val messageDelimiter = "\n" + const val telemetryPauseDuractionMs: Long = 100 } init { @@ -210,11 +228,11 @@ class BluetoothViewModel( viewModelScope.launch { val message = statusMessageAdapter.toJson( ChangedDeviceStatus(DeviceStatus.IsImuCalibration) - ) + messageDelimeter + ) + messageDelimiter val isSuccess = bluetoothController.trySendMessage( message.toByteArray() ) - if(!isSuccess) { + if (!isSuccess) { Log.e("BluetoothVM", "Failed to start IMU calibration: $message") } else { _state.update { @@ -228,12 +246,13 @@ class BluetoothViewModel( fun requestPidSettings() { viewModelScope.launch { - val message = pidSittingsRequiredMessageAdapter.toJson(PidSettingRequiredMessage(true)) + messageDelimeter + val message = + pidSittingsRequiredMessageAdapter.toJson(PidSettingRequiredMessage(true)) + messageDelimiter Log.i("BluetoothVM", "Requested PID settings: $message") val isSuccess = bluetoothController.trySendMessage( message.toByteArray() ) - if(!isSuccess) { + if (!isSuccess) { Log.e("BluetoothVM", "Failed to request PID settings: $message") } } @@ -241,9 +260,9 @@ class BluetoothViewModel( fun applyPidSettings(pidSettings: PidSettings) { viewModelScope.launch { - val message = pidSittingsMessageAdapter.toJson(pidSettings) + messageDelimeter + val message = pidSittingsMessageAdapter.toJson(pidSettings) + messageDelimiter val isSuccess = bluetoothController.trySendMessage(message.toByteArray()) - if(!isSuccess) { + if (!isSuccess) { Log.e("BluetoothVM", "Failed to request PID settings: $message") _state.update { it.copy(errorMessage = "Не удалось обновить значения PID") @@ -263,4 +282,58 @@ class BluetoothViewModel( } 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) } + } + } + } } \ No newline at end of file