Data tranfering improvement

This commit is contained in:
2024-02-09 19:28:49 +07:00
parent 3b62743481
commit 3517414ec1
16 changed files with 251 additions and 82 deletions

View File

@@ -1,23 +0,0 @@
package com.helible.pilot
// Todo: add checksum
// Todo: add arguments names
data class KMessage(
val r1: UShort,
val r2: UShort,
val r3: UShort,
val emergStop: Boolean,
val alarm: Boolean,
)
fun KMessage.toByteArray(): ByteArray {
return "$$r1;$r2;$r3;$emergStop;$alarm\r\n".encodeToByteArray()
}
fun String.toKMessage(): KMessage {
// TODO: implement
return KMessage(
0u, 0u, 0u, emergStop = false, alarm = false
)
}

View File

@@ -17,6 +17,7 @@ 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.CalibrationPage
import com.helible.pilot.components.deviceScreen.DeviceControlScreen
import com.helible.pilot.components.deviceScreen.defaultDeviceActionsList
import com.helible.pilot.components.scannerScreen.ScannerScreen
@@ -33,11 +34,7 @@ import com.helible.pilot.viewmodels.SavedPreferencesImpl
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))
@@ -46,6 +43,7 @@ class MainActivity : ComponentActivity() {
PreferencesViewModel(preferences)
}
@ExperimentalStdlibApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
@@ -68,14 +66,6 @@ class MainActivity : ComponentActivity() {
permissionLauncher.launch()
}
LaunchedEffect(key1 = bluetoothState) {
if (bluetoothState.isConnected) {
Toast.makeText(applicationContext, "Подключение завершено", Toast.LENGTH_SHORT)
.show()
}
}
val navController = rememberNavController()
LaunchedEffect(key1 = bluetoothState.errorMessage) {
@@ -168,7 +158,9 @@ class MainActivity : ComponentActivity() {
}
composable("imu_calibration/{title}")
{ backStackEntry ->
NotImplementedPage(
CalibrationPage(
deviceStatus = bluetoothState.deviceState?.status,
startCalibration = { bluetoothViewModel.startImuCalibration() },
title = backStackEntry.arguments?.getString("title") ?: "null",
navigateBack = { navController.popBackStack() }
)

View File

@@ -0,0 +1,84 @@
package com.helible.pilot.components
import android.widget.Spinner
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.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
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.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
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.dataclasses.ChangedDeviceStatus
import com.helible.pilot.dataclasses.DeviceState
import com.helible.pilot.dataclasses.DeviceStatus
@Composable
fun CalibrationPage(
deviceStatus: DeviceStatus?,
title: String,
startCalibration: () -> Unit,
navigateBack: () -> Unit
) {
BlankPage(title = title, navigateBack = navigateBack) {
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = LocalContext.current.getString(R.string.calibration_description),
color = MaterialTheme.colorScheme.onSecondaryContainer,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
)
Button(
enabled = deviceStatus != DeviceStatus.IsImuCalibration,
onClick = startCalibration,
modifier = Modifier.padding(10.dp)
) {
if(deviceStatus != DeviceStatus.IsImuCalibration) {
Icon(
painter = painterResource(id = R.drawable.tune),
contentDescription = null,
modifier = Modifier.padding(3.dp)
)
Text(
text = "Начать калибровку"
)
} else {
CircularProgressIndicator ()
Text(
text = "Калибровка...",
modifier = Modifier.padding(5.dp)
)
}
}
}
}
}
@Preview
@Composable
fun CalibrationPagePreview() {
Surface {
CalibrationPage(
DeviceStatus.IsImuCalibration,
title = "Калибровка гироскопа и акселерометра",
startCalibration = {},
navigateBack = {}
)
}
}

View File

@@ -19,6 +19,7 @@ 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.graphics.graphicsLayer
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
@@ -26,6 +27,7 @@ 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.dataclasses.DeviceStatus
import com.helible.pilot.viewmodels.AppPreferences
@Composable
@@ -65,7 +67,19 @@ fun DeviceBadge(
fontWeight = FontWeight.Bold
)
DeviceConnectionStatus(bluetoothUiState)
Text(text = "Заряд батареи: 79%")
if(bluetoothUiState.isConnected) {
val deviceStatus = bluetoothUiState.deviceState?.status
if (deviceStatus != null) {
Text(text = "Заряд батареи: ${bluetoothUiState.deviceState.batteryCharge}%")
if (deviceStatus == DeviceStatus.ChargeRequired) {
Text(text = "Аккумулятор разряжен", color = Color.Red)
} else {
Text(text = deviceStatus.description())
}
} else {
Text(text = "Ожиданием рукопожатия...")
}
}
}
Box(
contentAlignment = Alignment.CenterEnd,

View File

@@ -51,6 +51,8 @@ fun DeviceConnectionStatus(bluetoothState: BluetoothUiState) {
.padding(2.dp)
)
Text("Подключение...")
} else {
Text("Попытка подключения не удалась.")
}
}
}

View File

@@ -15,8 +15,8 @@ import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import com.helible.pilot.viewmodels.BluetoothDataTransferService
import com.helible.pilot.KMessage
import com.helible.pilot.dataclasses.BluetoothDeviceDomain
import com.helible.pilot.dataclasses.DeviceState
import com.helible.pilot.receivers.BluetoothAdapterStateReceiver
import com.helible.pilot.receivers.BluetoothStateReceiver
import kotlinx.coroutines.CoroutineScope
@@ -40,7 +40,7 @@ import java.util.UUID
sealed interface ConnectionResult {
object ConnectionEstablished : ConnectionResult
data class TransferSucceded(val message: String) : ConnectionResult
data class TransferSucceded(val message: DeviceState) : ConnectionResult
data class Error(val message: String) : ConnectionResult
}
@@ -61,6 +61,7 @@ interface BluetoothController {
fun onDestroy()
}
@ExperimentalStdlibApi
class AndroidBluetoothController(private val context: Context) : BluetoothController {
private val bluetoothManager by lazy {
@@ -228,21 +229,26 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro
try {
socket.connect()
emit(ConnectionResult.ConnectionEstablished)
BluetoothDataTransferService(socket).also { it ->
BluetoothDataTransferService(socket).also {
dataTransferService = it
emitAll(
it.listenForIncomingMessages()
.map { ConnectionResult.TransferSucceded(it) }
.map { deviceState ->
ConnectionResult.TransferSucceded(deviceState)
}
)
}
} catch (e: IOException) {
socket.close()
currentClientSocket = null
Log.e("BluetoothController", e.toString())
Log.e("BluetoothController", "I/O exception: e")
emit(ConnectionResult.Error("Connection was interrupted"))
}
}
}.onCompletion { closeConnection() }.flowOn(Dispatchers.IO)
}.onCompletion {
Log.i("BluetoothController", "Connection closed on flow completion.")
closeConnection()
}.flowOn(Dispatchers.IO)
}
override suspend fun trySendMessage(message: ByteArray): Boolean {

View File

@@ -9,4 +9,5 @@ data class BluetoothUiState(
val errorMessage: String? = null,
val scannedBluetoothDevices: List<BluetoothDevice> = emptyList(),
val pairedBluetoothDevices: List<BluetoothDevice> = emptyList(),
val deviceState: DeviceState? = null
)

View File

@@ -0,0 +1,6 @@
package com.helible.pilot.dataclasses
data class ChangedDeviceStatus(
val status: DeviceStatus
)

View File

@@ -0,0 +1,15 @@
package com.helible.pilot.dataclasses
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class DeviceState(
val status: DeviceStatus = DeviceStatus.ChargeRequired,
@Json(name = "charge") val batteryCharge: Int = 10,
val flightHeight: Float = 0f,
@Json(name = "y") val yaw: Float = 0f,
@Json(name = "p") val pitch: Float = 0f,
@Json(name = "r") val roll: Float = 0f,
@Json(name = "zIn") val zInertial: Float = 0f,
)

View File

@@ -0,0 +1,21 @@
package com.helible.pilot.dataclasses
enum class DeviceStatus {
Idle,
IsPreparingForTakeoff,
IsFlying,
IsBoarding,
IsImuCalibration,
ChargeRequired;
fun description(): String {
return when (this) {
Idle -> "Готово к работе"
IsPreparingForTakeoff -> "Подготовка к полёту"
IsFlying -> "В полёте"
IsBoarding -> "Посадка"
IsImuCalibration -> "Калибровка..."
ChargeRequired -> "Аккумулятор разряжен"
}
}
}

View File

@@ -0,0 +1,22 @@
package com.helible.pilot.dataclasses
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.ToJson
class DeviceStatusJsonAdapter {
@FromJson
fun fromJson(deviceStatus: String): DeviceStatus {
try {
val index: UInt = deviceStatus.toUInt()
return DeviceStatus.values()[index.toInt()]
} catch (e: IndexOutOfBoundsException) {
throw JsonDataException("Impossible conversation from String to DeviceStatus")
}
}
@ToJson
fun toJson(deviceStatus: DeviceStatus): String {
return DeviceStatus.values().indexOf(deviceStatus).toString()
}
}

View File

@@ -1,12 +0,0 @@
package com.helible.pilot.dataclasses
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class RotorsSpeedMessage(val r1: Short, val r2: Short, val r3: Short)
@JsonClass(generateAdapter = true)
data class EmergStopMessage(val emergStop: Boolean)
@JsonClass(generateAdapter = true)
data class AlarmStateMessage(val isAlarmOn: Boolean)

View File

@@ -2,8 +2,12 @@ package com.helible.pilot.viewmodels
import android.bluetooth.BluetoothSocket
import android.util.Log
import com.helible.pilot.KMessage
import com.helible.pilot.toKMessage
import com.helible.pilot.dataclasses.DeviceState
import com.helible.pilot.dataclasses.DeviceStatusJsonAdapter
import com.squareup.moshi.JsonEncodingException
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
@@ -14,25 +18,42 @@ import java.io.IOException
class TransferFailedException : IOException("Reading incoming data failed")
@ExperimentalStdlibApi
class BluetoothDataTransferService(
private val socket: BluetoothSocket,
) {
fun listenForIncomingMessages(): Flow<String> {
fun listenForIncomingMessages(): Flow<DeviceState> {
val moshi = Moshi.Builder().add(DeviceStatusJsonAdapter()).add(KotlinJsonAdapterFactory()).build()
val deviceStateMessageAdapter = moshi.adapter<DeviceState>()
return flow {
if (!socket.isConnected)
return@flow
val buffer = ByteArray(128)
val buffer = ByteArray(512)
while (true) {
val byteCount: Int = try {
socket.inputStream.read(buffer)
} catch (e: IOException) {
Log.e("BluetoothController", "Failed to receive incoming data")
throw TransferFailedException()
}
val strData: String = buffer.decodeToString(endIndex = byteCount)
emit(
strData
)
Log.i("BluetoothController", "Received: ${strData.dropLast(2)}")
val messageData: String = buffer.decodeToString(endIndex = byteCount)
if (!messageData.endsWith("\n\r")) {
Log.i("BluetoothController", "Package end isn't valid.")
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)
}
@@ -42,10 +63,11 @@ class BluetoothDataTransferService(
try {
socket.outputStream.write(bytes)
} catch (e: IOException) {
e.printStackTrace()
Log.e("BluetoothController", "Failed to write message: $e")
return@withContext false
}
true
}
}
}
}

View File

@@ -1,18 +1,18 @@
package com.helible.pilot.viewmodels
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.helible.pilot.controllers.BluetoothController
import com.helible.pilot.controllers.ConnectionResult
import com.helible.pilot.dataclasses.AlarmStateMessage
import com.helible.pilot.dataclasses.BluetoothDevice
import com.helible.pilot.dataclasses.BluetoothUiState
import com.helible.pilot.dataclasses.EmergStopMessage
import com.helible.pilot.dataclasses.RotorsSpeedMessage
import com.helible.pilot.dataclasses.ChangedDeviceStatus
import com.helible.pilot.dataclasses.DeviceStatus
import com.helible.pilot.dataclasses.DeviceStatusJsonAdapter
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
@@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.time.LocalTime
class BluetoothViewModel(
private val bluetoothController: BluetoothController,
@@ -43,10 +44,9 @@ class BluetoothViewModel(
pairedBluetoothDevices = pairedDevices
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), _state.value)
private val moshi: Moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
private val rotorsStateMessegeAdapter = moshi.adapter(RotorsSpeedMessage::class.java)
private val alarmStateMessageAdapter = moshi.adapter(AlarmStateMessage::class.java)
private val emergStopMessageAdapter = moshi.adapter(EmergStopMessage::class.java)
private var deviceConnectionJob: Job? = null
private val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).add(DeviceStatusJsonAdapter()).build()
private val newStatusMessageAdapter = moshi.adapter(ChangedDeviceStatus::class.java)
init {
bluetoothController.isConnected.onEach { isConnected ->
@@ -94,9 +94,11 @@ class BluetoothViewModel(
}
is ConnectionResult.TransferSucceded -> {
_state.update { it.copy(
) }
_state.update {
it.copy(
deviceState = result.message
)
}
}
is ConnectionResult.Error -> {
@@ -111,19 +113,19 @@ class BluetoothViewModel(
}
}
.catch { throwable ->
Log.e("BluetoothController", "Error occured while data transfer: ${throwable.message}")
bluetoothController.closeConnection()
_state.update { it.copy(
isConnected = false,
isConnecting = false
isConnecting = false,
deviceState = null
) }
}
.launchIn(viewModelScope)
}
private var deviceConnectionJob: Job? = null
fun connectToDevice(device: String) {
if (_state.value.isConnected and _state.value.isConnecting) {
if (_state.value.isConnected or _state.value.isConnecting) {
return
}
_state.update { it.copy(isConnecting = true) }
@@ -138,7 +140,8 @@ class BluetoothViewModel(
_state.update {
it.copy(
isConnecting = false,
isConnected = false
isConnected = false,
deviceState = null
)
}
}
@@ -170,4 +173,18 @@ class BluetoothViewModel(
)
}
}
fun startImuCalibration() {
viewModelScope.launch {
val message = newStatusMessageAdapter.toJson(
ChangedDeviceStatus(DeviceStatus.IsImuCalibration)
) + "\n\r"
val success = bluetoothController.trySendMessage(
message.toByteArray()
)
if(!success) {
Log.e("BluetoothVM", "Failed to start IMU calibration: $message")
}
}
}
}

View File

@@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.helible.pilot.controllers.AndroidBluetoothController
@ExperimentalStdlibApi
@Suppress("UNCHECKED_CAST")
class BluetoothViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {