Compare commits

...

2 Commits

Author SHA1 Message Date
027116e18e PID contrllers settings page was added 2024-03-08 23:07:18 +07:00
c8abfd94c3 Rotors test page was added 2024-03-08 23:05:38 +07:00
8 changed files with 319 additions and 53 deletions

View File

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

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

@@ -18,6 +18,7 @@ import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -30,12 +31,14 @@ 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.LocalContext
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.R
import com.helible.pilot.components.BlankPage
import com.helible.pilot.dataclasses.DeviceState
import com.helible.pilot.dataclasses.DeviceStatus
@@ -55,8 +58,9 @@ fun PidSettingsPage(
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")
var selectedRegulator by remember { mutableStateOf("") }
val dropdownMenuItems =
listOf("Контроллер высоты", "Контроллер крена", "Контроллер рысканья")
LaunchedEffect(null) {
requestPidSettings()
@@ -64,21 +68,23 @@ fun PidSettingsPage(
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 }
when (selectedRegulator) {
dropdownMenuItems[0] -> {
pidSettings.heightControllerParams.p.toString().also { pValue = it }
pidSettings.heightControllerParams.i.toString().also { iValue = it }
pidSettings.heightControllerParams.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 }
dropdownMenuItems[1] -> {
pidSettings.yawControllerParams.p.toString().also { pValue = it }
pidSettings.yawControllerParams.i.toString().also { iValue = it }
pidSettings.yawControllerParams.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 }
dropdownMenuItems[2] -> {
pidSettings.pitchControllerParams.p.toString().also { pValue = it }
pidSettings.pitchControllerParams.i.toString().also { iValue = it }
pidSettings.pitchControllerParams.d.toString().also { dValue = it }
}
}
}
@@ -87,7 +93,6 @@ fun PidSettingsPage(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
)
{
if (deviceState?.status != DeviceStatus.Idle) {
@@ -96,13 +101,32 @@ fun PidSettingsPage(
textAlign = TextAlign.Center
)
} else if (deviceState.pidSettings == null) {
Column {
CircularProgressIndicator(modifier = Modifier.padding(10.dp))
Text(text = "Синхронизация...")
}
CircularProgressIndicator(modifier = Modifier.padding(10.dp))
Text(text = "Синхронизация...")
} else {
val pidSettings = deviceState.pidSettings
Column(modifier = Modifier.padding(horizontal = 10.dp).padding(bottom = 10.dp)) {
Text(
"Рекомендации по настройке ПИД регуляторов",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier
.padding(vertical = 10.dp)
)
Text(
text = LocalContext.current.getString(R.string.p_pid_value_description),
style = MaterialTheme.typography.bodyMedium
)
Text(
text = LocalContext.current.getString(R.string.i_pid_value_description),
style = MaterialTheme.typography.bodyMedium
)
Text(
text = LocalContext.current.getString(R.string.d_pid_value_description),
style = MaterialTheme.typography.bodyMedium
)
}
OutlinedDropdownMenu(
label = "ПИД регулятор",
suggestions = dropdownMenuItems,
@@ -110,22 +134,24 @@ fun PidSettingsPage(
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 }
selectedRegulator = dropdownMenuItems[0]
pidSettings.heightControllerParams.p.toString().also { pValue = it }
pidSettings.heightControllerParams.i.toString().also { iValue = it }
pidSettings.heightControllerParams.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 }
selectedRegulator = dropdownMenuItems[1]
pidSettings.yawControllerParams.p.toString().also { pValue = it }
pidSettings.yawControllerParams.i.toString().also { iValue = it }
pidSettings.yawControllerParams.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 }
selectedRegulator = dropdownMenuItems[2]
pidSettings.pitchControllerParams.p.toString().also { pValue = it }
pidSettings.pitchControllerParams.i.toString().also { iValue = it }
pidSettings.pitchControllerParams.d.toString().also { dValue = it }
}
}
},
@@ -167,23 +193,35 @@ fun PidSettingsPage(
Button(
onClick = {
when (selectedRegulator) {
1 -> {
dropdownMenuItems[0] -> {
val newPidSettings = pidSettings.copy(
p1 = PidParams(p.toFloat(), i.toFloat(), d.toFloat())
heightControllerParams = PidParams(
p.toFloat(),
i.toFloat(),
d.toFloat()
)
)
setPidSettings(newPidSettings)
}
2 -> {
dropdownMenuItems[1] -> {
val newPidSettings = pidSettings.copy(
p2 = PidParams(p.toFloat(), i.toFloat(), d.toFloat())
yawControllerParams = PidParams(
p.toFloat(),
i.toFloat(),
d.toFloat()
)
)
setPidSettings(newPidSettings)
}
3 -> {
dropdownMenuItems[2] -> {
val newPidSettings = pidSettings.copy(
p3 = PidParams(p.toFloat(), i.toFloat(), d.toFloat())
pitchControllerParams = PidParams(
p.toFloat(),
i.toFloat(),
d.toFloat()
)
)
setPidSettings(newPidSettings)
}
@@ -199,7 +237,7 @@ fun PidSettingsPage(
}
private fun isValidValue(k: String): Boolean {
return k.toFloatOrNull() != null && k.toFloat() >= 0f && k.toFloat() <= 2f
return k.toFloatOrNull() != null && k.toFloat() >= 0f && k.toFloat() <= 15f
}
@Preview(showBackground = true)

View File

@@ -1,10 +1,11 @@
package com.helible.pilot.dataclasses
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class PidSettings (
val p1: PidParams,
val p2: PidParams,
val p3: PidParams
@Json(name = "p1") val heightControllerParams: PidParams,
@Json(name = "p2") val yawControllerParams: PidParams,
@Json(name = "p3") val pitchControllerParams: PidParams
)

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

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

View File

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

View File

@@ -1,4 +1,7 @@
<resources>
<string name="app_name">Digital Pilot</string>
<string name="calibration_description">Расположите устройство на ровной горизонтальной поверхности, чтобы сани вертолета полностью лежали на ней. Нажмите кнопку калибровки ниже и ждите её окончания, не создавая тряски.</string>
<string name="p_pid_value_description">Сначала подберите значение коэффицента P, которое балансирует между слишком низкой и слишком высокой чувствительностью.</string>
<string name="i_pid_value_description">Затем подберите значение коэффицента I, которое уберёт нежелательный дрейв, но не повлияет на отзывчивость.</string>
<string name="d_pid_value_description">После установите значение коэффицента D таким образом, чтобы обеспечить более стабильное и плавное управление.</string>
</resources>