Device screen was implemented

This commit is contained in:
2024-01-01 21:56:23 +07:00
parent d7f3bf386d
commit efa93ab912
28 changed files with 546 additions and 185 deletions

View File

@@ -15,10 +15,11 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.helible.pilot.components.BluetoothScannerScreen
import com.helible.pilot.components.FlightControlScreen
import com.helible.pilot.components.AppPreferences
import com.helible.pilot.components.SavedPreferencesImpl
import com.helible.pilot.components.scannerScreen.BluetoothScannerScreen
import com.helible.pilot.components.deviceScreen.DeviceControlScreen
import com.helible.pilot.viewmodels.AppPreferences
import com.helible.pilot.viewmodels.SavedPreferencesImpl
import com.helible.pilot.components.deviceScreen.defaultDeviceActionsList
import com.helible.pilot.permissions.PermissionsLauncher
import com.helible.pilot.permissions.PermissionsRequest
import com.helible.pilot.permissions.RequestHardwareFeatures
@@ -31,8 +32,10 @@ import com.helible.pilot.viewmodels.PreferencesViewModel
class MainActivity : ComponentActivity() {
// TODO: device screen logic
// TODO: constrain text size
// TODO: add Bluetooth telemetry...
// TODO: move text strings to resources
// TODO: review permissions logic
private val preferences by lazy {
SavedPreferencesImpl(getSharedPreferences(packageName, MODE_PRIVATE))
@@ -127,32 +130,48 @@ class MainActivity : ComponentActivity() {
}
composable("device")
{
FlightControlScreen(
DeviceControlScreen(
bluetoothUiState = bluetoothState,
getPreferences = { preferencesViewModel.preferences },
navigateToScanner = { navController.navigate("scanner") },
navigateToPage = { page -> navController.navigate(page) },
connectToDevice = { device ->
bluetoothViewModel.connectToDevice(
device
)
},
sendRotorsState = { message ->
bluetoothViewModel.sendRotorsDutySpeed(
message
)
},
disconnectFromDevice = { bluetoothViewModel.disconnectFromDevice() },
sendEmergStop = { bluetoothViewModel.sendEmergStop() },
sendAlarm = { message -> bluetoothViewModel.sendAlarmState(message) },
sendR3Duty = { duty -> bluetoothViewModel.sendR3Duty(duty) }
deviceActionsList = defaultDeviceActionsList()
)
if (preferencesViewModel.preferences != null) BackHandler {}
}
composable("console")
{
}
composable("codeblocks")
{
}
composable("imu_calibration")
{
}
composable("motor_test")
{
}
composable("pid_settings")
{
}
composable("reports")
{
}
}
}
}
}
}
}

View File

@@ -1,25 +0,0 @@
package com.helible.pilot.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DeviceScreen() {
Scaffold(
topBar = {
Text(text = "")
}
) { innerPadding ->
Column(
modifier = Modifier.padding(innerPadding)
) {
}
}
}

View File

@@ -1,130 +0,0 @@
package com.helible.pilot.components
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Slider
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.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.helible.pilot.dataclasses.AlarmStateMessage
import com.helible.pilot.dataclasses.BluetoothUiState
import com.helible.pilot.dataclasses.RotorsSpeedMessage
import kotlin.math.roundToInt
@Composable
fun FlightControlScreen(
bluetoothUiState: BluetoothUiState,
getPreferences: () -> AppPreferences?,
navigateToScanner: () -> Unit,
connectToDevice: (String) -> Unit,
disconnectFromDevice: () -> Unit,
sendRotorsState: (RotorsSpeedMessage) -> Unit,
sendAlarm: (AlarmStateMessage) -> Unit,
sendEmergStop: () -> Unit,
sendR3Duty: (Int) -> Unit,
) {
LaunchedEffect(Unit) {
val preferences: AppPreferences? = getPreferences()
if (preferences == null) {
navigateToScanner()
} else {
connectToDevice(preferences.deviceAddress)
}
}
var rotor1Duty by remember { mutableStateOf(0f) }
var rotor2Duty by remember { mutableStateOf(0f) }
var rotor3Duty by remember { mutableStateOf(0f) }
BackHandler {
disconnectFromDevice()
Log.i("FlightScreen", "Disconnected from the device")
navigateToScanner()
}
when {
bluetoothUiState.isConnecting -> {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
CircularProgressIndicator()
Text(text = "Подключение...", textAlign = TextAlign.Center)
}
}
else -> {
Column(modifier = Modifier.fillMaxSize()) {
Text(
text = "Device name: ${getPreferences()?.deviceName ?: "(устройство отключено)"}",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
Text(text = "Rotor 1 value: $rotor1Duty", textAlign = TextAlign.Center)
Slider(
value = rotor1Duty,
onValueChange = { rotor1Duty = it.roundToInt().toFloat() },
valueRange = 0f..1000f,
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
)
Text(text = "Rotor 2 value: $rotor2Duty", textAlign = TextAlign.Center)
Slider(
value = rotor2Duty,
onValueChange = { rotor2Duty = it.roundToInt().toFloat() },
valueRange = 0f..1000f,
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
)
Text(text = "Rotor 3 value: $rotor1Duty", textAlign = TextAlign.Center)
Slider(
value = rotor3Duty,
onValueChange = { rotor3Duty = it.roundToInt().toFloat() },
valueRange = 0f..1000f,
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
)
FilledIconButton(
onClick = { sendEmergStop() },
modifier = Modifier.padding(10.dp)
) {
Icon(
Icons.Default.Warning,
contentDescription = null,
modifier = Modifier.padding(3.dp)
)
Text(
text = "СТОП",
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(3.dp)
)
}
}
}
}
}

View File

@@ -14,6 +14,6 @@ fun Title(text: String, modifier: Modifier = Modifier) {
textAlign = TextAlign.Center,
modifier = modifier,
fontSize = 23.sp,
fontWeight = FontWeight.Bold
fontWeight = FontWeight.ExtraBold
)
}

View File

@@ -0,0 +1,96 @@
package com.helible.pilot.components.deviceScreen
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.helible.pilot.R
import com.helible.pilot.dataclasses.BluetoothUiState
import com.helible.pilot.viewmodels.AppPreferences
@Composable
fun DeviceBadge(
bluetoothUiState: BluetoothUiState,
tryToReconnect: () -> Unit,
getPreferences: () -> AppPreferences?
) {
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 15.dp),
shape = RoundedCornerShape(15)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 15.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(modifier = Modifier
.size(60.dp)
.graphicsLayer {
clip = true
shape = RoundedCornerShape(15)
}
.fillMaxSize()) {
Image(
painter = painterResource(id = R.drawable.helicopter),
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}
Column(modifier = Modifier.padding(horizontal = 8.dp)) {
Text(
text = getPreferences()?.deviceName ?: "null",
fontWeight = FontWeight.Bold
)
DeviceConnectionStatus(bluetoothUiState)
Text(text = "Заряд батареи: 79%")
}
Box(
contentAlignment = Alignment.CenterEnd,
modifier = Modifier
.padding(2.dp)
.fillMaxWidth()
) {
Icon(
Icons.Default.Refresh,
contentDescription = null,
modifier = Modifier
.requiredSize(Icons.Default.Refresh.defaultWidth)
.clickable { tryToReconnect() }
)
}
}
}
}
@Preview
@Composable
fun DeviceBadgePreview() {
DeviceBadge(
bluetoothUiState = BluetoothUiState(isConnected = true),
tryToReconnect = {},
getPreferences = {AppPreferences("Helicopter", "AA:BB:CC:FF:DD")}
)
}

View File

@@ -0,0 +1,66 @@
package com.helible.pilot.components.deviceScreen
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.helible.pilot.R
import com.helible.pilot.dataclasses.BluetoothUiState
@Composable
fun DeviceConnectionStatus(bluetoothState: BluetoothUiState) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (bluetoothState.isConnected) {
Icon(
Icons.Default.CheckCircle,
contentDescription = null,
tint = Color(56, 200, 35),
modifier = Modifier
.requiredSize(Icons.Default.CheckCircle.defaultWidth)
.padding(2.dp)
)
Text ("На связи")
}
else if (bluetoothState.errorMessage != null) {
Icon(
painter = painterResource(id = R.drawable.cancel),
contentDescription = null,
tint = Color(255, 24, 35),
modifier = Modifier
.requiredSize(R.drawable.cancel.dp)
.padding(2.dp)
)
Text ("Ошибка: ${bluetoothState.errorMessage}")
}
else if (bluetoothState.isConnecting) {
Icon(
painter = painterResource(id = R.drawable.sync),
contentDescription = null,
tint = Color(40, 123, 207),
modifier = Modifier
.requiredSize(R.drawable.sync.dp)
.padding(2.dp)
)
Text ("Подключение...")
}
}
}
@Preview
@Composable
fun DeviceConnectionStatusPreview() {
Surface {
DeviceConnectionStatus(bluetoothState = BluetoothUiState(isConnecting = true))
}
}

View File

@@ -0,0 +1,143 @@
package com.helible.pilot.components.deviceScreen
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
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.graphics.graphicsLayer
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.helible.pilot.R
import com.helible.pilot.components.Title
import com.helible.pilot.dataclasses.BluetoothUiState
import com.helible.pilot.viewmodels.AppPreferences
@Composable
fun DeviceControlScreen(
bluetoothUiState: BluetoothUiState,
getPreferences: () -> AppPreferences?,
navigateToPage: (String) -> Unit,
connectToDevice: (String) -> Unit,
disconnectFromDevice: () -> Unit,
deviceActionsList: Map<String, Array<Pair<String, Pair<Pair<Int, Color>, String>>>>,
scannerPageName: String = "scanner",
) {
LaunchedEffect(Unit) {
val preferences: AppPreferences? = getPreferences()
if (preferences == null) {
navigateToPage(scannerPageName)
} else {
connectToDevice(preferences.deviceAddress)
}
}
LaunchedEffect(key1 = bluetoothUiState.isEnabled) {
/* Trying to reconnect, when bluetooth is turned on */
val preferences = getPreferences()
if(preferences != null && bluetoothUiState.isEnabled)
connectToDevice(preferences.deviceAddress)
}
LaunchedEffect(key1 = bluetoothUiState.isLocationEnabled) {
/* Trying to reconnect, when location is turned on */
val preferences = getPreferences()
if(preferences != null && bluetoothUiState.isLocationEnabled)
connectToDevice(preferences.deviceAddress)
}
Column(
Modifier
.fillMaxSize()
.padding(5.dp)
) {
Title(
text = "Ваше устройство",
modifier = Modifier.padding(vertical = 15.dp, horizontal = 10.dp)
)
DeviceBadge(
bluetoothUiState = bluetoothUiState,
tryToReconnect = {
/* Trying to reconnect, when error occurred */
val preferences = getPreferences()
if(preferences != null)
connectToDevice(preferences.deviceAddress)
},
getPreferences = getPreferences
)
Column(modifier = Modifier.padding(horizontal = 3.dp)) {
for (section in deviceActionsList) {
Text(section.key,
color = Color.Gray,
fontWeight = FontWeight.Light,
modifier = Modifier.padding(vertical = 15.dp, horizontal = 10.dp)
)
for (action in section.value) {
TextButton(onClick = { /* TODO */}) {
Icon(
painter = painterResource(id = action.second.first.first),
tint = action.second.first.second,
contentDescription = null,
modifier = Modifier.size(25.dp)
)
Text(
text = action.second.second,
color = MaterialTheme.colorScheme.inverseSurface,
modifier = Modifier.padding(horizontal = 5.dp)
)
}
}
}
TextButton(onClick = {
disconnectFromDevice()
navigateToPage(scannerPageName)
}, modifier = Modifier.padding(vertical = 10.dp)) {
Icon(painterResource(id = R.drawable.logout), contentDescription = null)
Text(
text = "Отвязать устройство",
color = MaterialTheme.colorScheme.inverseSurface,
modifier = Modifier.padding(horizontal = 4.dp)
)
}
}
}
}
@Preview
@Composable
fun DeviceControlScreenPreview() {
Surface {
DeviceControlScreen(
bluetoothUiState = BluetoothUiState(isConnected = true),
getPreferences = { AppPreferences("Helicopter", "AA:BB:CC:DD:FF") },
navigateToPage = { /*TODO*/ },
connectToDevice = {},
disconnectFromDevice = { /*TODO*/ },
deviceActionsList = defaultDeviceActionsList()
)
}
}

View File

@@ -0,0 +1,64 @@
package com.helible.pilot.components.deviceScreen
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import com.helible.pilot.R
@Composable
fun defaultDeviceActionsList(): Map<String, Array<Pair<String, Pair<Pair<Int, Color>, String>>>> {
return mapOf(
Pair(
"Управление",
arrayOf(
Pair(
"console",
Pair(
Pair(R.drawable.joystick, MaterialTheme.colorScheme.primary),
"Пульт управления"
)
),
Pair(
"codeblocks",
Pair(
Pair(R.drawable.code_blocks, MaterialTheme.colorScheme.primary),
"Палитра команд"
)
)
)
),
Pair(
"Настройки",
arrayOf(
Pair(
"imu_calibration",
Pair(
Pair(R.drawable.tune, MaterialTheme.colorScheme.primary),
"Калибровка гироскопа и акселерометра"
)
),
Pair(
"motor_test",
Pair(
Pair(R.drawable.helicopter_icon, MaterialTheme.colorScheme.primary),
"Тестирование двигателей"
)
),
Pair(
"pid_settings",
Pair(
Pair(R.drawable.controller_gen, MaterialTheme.colorScheme.primary),
"Настройки ПИД регуляторов"
)
),
Pair(
"reports",
Pair(
Pair(R.drawable.construction, MaterialTheme.colorScheme.primary),
"Отчеты о полётах"
)
)
)
)
)
}

View File

@@ -1,4 +1,4 @@
package com.helible.pilot.components
package com.helible.pilot.components.scannerScreen
import android.annotation.SuppressLint
import android.util.Log
@@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import com.helible.pilot.components.Title
import com.helible.pilot.dataclasses.BluetoothUiState
import com.helible.pilot.dataclasses.BluetoothDevice
@@ -103,7 +104,6 @@ fun BluetoothScannerScreen(
Text(text = "Далее")
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
package com.helible.pilot.components
package com.helible.pilot.components.scannerScreen
import android.annotation.SuppressLint
import androidx.compose.foundation.clickable
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
@@ -19,6 +20,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.helible.pilot.dataclasses.BluetoothDevice
import com.helible.pilot.R
@@ -42,7 +44,7 @@ fun DeviceItem(
)
) {
Row(modifier = Modifier.padding(8.dp)) {
Column(verticalArrangement = Arrangement.Center) {
Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
Text(
text = deviceInfo.name,
fontWeight = FontWeight.Bold,
@@ -74,4 +76,15 @@ fun getSignalIconForRssiValue(rssi: Short): Int {
else if (rssi >= -90) return R.drawable.signal_icon3
else if (rssi >= -100) return R.drawable.signal_icon2
return R.drawable.signal_icon1
}
@Preview
@Composable
fun DeviceItemPreview() {
DeviceItem(
BluetoothDevice("Helicopter", "AA:BB:CC:DD:FF", -90, true),
null,
{_ -> },
modifier = Modifier.size(500.dp, 60.dp)
)
}

View File

@@ -1,4 +1,4 @@
package com.helible.pilot.components
package com.helible.pilot.components.scannerScreen
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.helible.pilot.components.scannerScreen.DeviceItem
import com.helible.pilot.dataclasses.BluetoothUiState
import com.helible.pilot.dataclasses.BluetoothDevice

View File

@@ -57,24 +57,24 @@ class BluetoothViewModel(
it.copy(errorMessage = error)
}
}.launchIn(viewModelScope)
bluetoothController.isScanning.onEach { result ->
bluetoothController.isScanning.onEach { isDiscovering ->
_state.update {
it.copy(
isDiscovering = result,
isDiscovering = isDiscovering,
)
}
}.launchIn(viewModelScope)
bluetoothController.isEnabled.onEach { result ->
bluetoothController.isEnabled.onEach { isEnabled ->
_state.update {
it.copy(
isEnabled = result,
isEnabled = isEnabled,
)
}
}.launchIn(viewModelScope)
bluetoothController.isLocationEnabled.onEach { result ->
bluetoothController.isLocationEnabled.onEach { isLocationEnabled ->
_state.update {
it.copy(
isLocationEnabled = result
isLocationEnabled = isLocationEnabled
)
}
}.launchIn(viewModelScope)
@@ -123,6 +123,9 @@ class BluetoothViewModel(
private var deviceConnectionJob: Job? = null
fun connectToDevice(device: String) {
if(_state.value.isConnected) {
return
}
_state.update { it.copy(isConnecting = true) }
deviceConnectionJob = bluetoothController
.connectToDevice(device)

View File

@@ -2,8 +2,6 @@ package com.helible.pilot.viewmodels
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import com.helible.pilot.components.AppPreferences
import com.helible.pilot.components.SavedPreferences
class PermissionDialogViewModel : ViewModel() {
val visiblePermissionDialogQueue = mutableStateListOf<String>()

View File

@@ -1,4 +1,4 @@
package com.helible.pilot.components
package com.helible.pilot.viewmodels
import android.content.SharedPreferences
import com.squareup.moshi.JsonAdapter