Data tranfering improvement
This commit is contained in:
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
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.deviceScreen.DeviceControlScreen
|
import com.helible.pilot.components.deviceScreen.DeviceControlScreen
|
||||||
import com.helible.pilot.components.deviceScreen.defaultDeviceActionsList
|
import com.helible.pilot.components.deviceScreen.defaultDeviceActionsList
|
||||||
import com.helible.pilot.components.scannerScreen.ScannerScreen
|
import com.helible.pilot.components.scannerScreen.ScannerScreen
|
||||||
@@ -33,11 +34,7 @@ import com.helible.pilot.viewmodels.SavedPreferencesImpl
|
|||||||
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
// TODO: device screen logic
|
|
||||||
// TODO: constrain text size
|
|
||||||
// TODO: add Bluetooth telemetry...
|
|
||||||
// TODO: move text strings to resources
|
// TODO: move text strings to resources
|
||||||
// TODO: review permissions logic
|
|
||||||
|
|
||||||
private val preferences by lazy {
|
private val preferences by lazy {
|
||||||
SavedPreferencesImpl(getSharedPreferences(packageName, MODE_PRIVATE))
|
SavedPreferencesImpl(getSharedPreferences(packageName, MODE_PRIVATE))
|
||||||
@@ -46,6 +43,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
PreferencesViewModel(preferences)
|
PreferencesViewModel(preferences)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalStdlibApi
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
@@ -68,14 +66,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
permissionLauncher.launch()
|
permissionLauncher.launch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
LaunchedEffect(key1 = bluetoothState) {
|
|
||||||
if (bluetoothState.isConnected) {
|
|
||||||
Toast.makeText(applicationContext, "Подключение завершено", Toast.LENGTH_SHORT)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
LaunchedEffect(key1 = bluetoothState.errorMessage) {
|
LaunchedEffect(key1 = bluetoothState.errorMessage) {
|
||||||
@@ -168,7 +158,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
composable("imu_calibration/{title}")
|
composable("imu_calibration/{title}")
|
||||||
{ backStackEntry ->
|
{ backStackEntry ->
|
||||||
NotImplementedPage(
|
CalibrationPage(
|
||||||
|
deviceStatus = bluetoothState.deviceState?.status,
|
||||||
|
startCalibration = { bluetoothViewModel.startImuCalibration() },
|
||||||
title = backStackEntry.arguments?.getString("title") ?: "null",
|
title = backStackEntry.arguments?.getString("title") ?: "null",
|
||||||
navigateBack = { navController.popBackStack() }
|
navigateBack = { navController.popBackStack() }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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 androidx.compose.ui.unit.dp
|
||||||
import com.helible.pilot.R
|
import com.helible.pilot.R
|
||||||
import com.helible.pilot.dataclasses.BluetoothUiState
|
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||||
|
import com.helible.pilot.dataclasses.DeviceStatus
|
||||||
import com.helible.pilot.viewmodels.AppPreferences
|
import com.helible.pilot.viewmodels.AppPreferences
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -65,7 +67,19 @@ fun DeviceBadge(
|
|||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
DeviceConnectionStatus(bluetoothUiState)
|
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(
|
Box(
|
||||||
contentAlignment = Alignment.CenterEnd,
|
contentAlignment = Alignment.CenterEnd,
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ fun DeviceConnectionStatus(bluetoothState: BluetoothUiState) {
|
|||||||
.padding(2.dp)
|
.padding(2.dp)
|
||||||
)
|
)
|
||||||
Text("Подключение...")
|
Text("Подключение...")
|
||||||
|
} else {
|
||||||
|
Text("Попытка подключения не удалась.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import android.util.Log
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import com.helible.pilot.viewmodels.BluetoothDataTransferService
|
import com.helible.pilot.viewmodels.BluetoothDataTransferService
|
||||||
import com.helible.pilot.KMessage
|
|
||||||
import com.helible.pilot.dataclasses.BluetoothDeviceDomain
|
import com.helible.pilot.dataclasses.BluetoothDeviceDomain
|
||||||
|
import com.helible.pilot.dataclasses.DeviceState
|
||||||
import com.helible.pilot.receivers.BluetoothAdapterStateReceiver
|
import com.helible.pilot.receivers.BluetoothAdapterStateReceiver
|
||||||
import com.helible.pilot.receivers.BluetoothStateReceiver
|
import com.helible.pilot.receivers.BluetoothStateReceiver
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -40,7 +40,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
sealed interface ConnectionResult {
|
sealed interface ConnectionResult {
|
||||||
object ConnectionEstablished : 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
|
data class Error(val message: String) : ConnectionResult
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +61,7 @@ interface BluetoothController {
|
|||||||
fun onDestroy()
|
fun onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalStdlibApi
|
||||||
class AndroidBluetoothController(private val context: Context) : BluetoothController {
|
class AndroidBluetoothController(private val context: Context) : BluetoothController {
|
||||||
|
|
||||||
private val bluetoothManager by lazy {
|
private val bluetoothManager by lazy {
|
||||||
@@ -228,21 +229,26 @@ class AndroidBluetoothController(private val context: Context) : BluetoothContro
|
|||||||
try {
|
try {
|
||||||
socket.connect()
|
socket.connect()
|
||||||
emit(ConnectionResult.ConnectionEstablished)
|
emit(ConnectionResult.ConnectionEstablished)
|
||||||
BluetoothDataTransferService(socket).also { it ->
|
BluetoothDataTransferService(socket).also {
|
||||||
dataTransferService = it
|
dataTransferService = it
|
||||||
emitAll(
|
emitAll(
|
||||||
it.listenForIncomingMessages()
|
it.listenForIncomingMessages()
|
||||||
.map { ConnectionResult.TransferSucceded(it) }
|
.map { deviceState ->
|
||||||
|
ConnectionResult.TransferSucceded(deviceState)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
socket.close()
|
socket.close()
|
||||||
currentClientSocket = null
|
currentClientSocket = null
|
||||||
Log.e("BluetoothController", e.toString())
|
Log.e("BluetoothController", "I/O exception: e")
|
||||||
emit(ConnectionResult.Error("Connection was interrupted"))
|
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 {
|
override suspend fun trySendMessage(message: ByteArray): Boolean {
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ data class BluetoothUiState(
|
|||||||
val errorMessage: String? = null,
|
val errorMessage: String? = null,
|
||||||
val scannedBluetoothDevices: List<BluetoothDevice> = emptyList(),
|
val scannedBluetoothDevices: List<BluetoothDevice> = emptyList(),
|
||||||
val pairedBluetoothDevices: List<BluetoothDevice> = emptyList(),
|
val pairedBluetoothDevices: List<BluetoothDevice> = emptyList(),
|
||||||
|
val deviceState: DeviceState? = null
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.helible.pilot.dataclasses
|
||||||
|
|
||||||
|
|
||||||
|
data class ChangedDeviceStatus(
|
||||||
|
val status: DeviceStatus
|
||||||
|
)
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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 -> "Аккумулятор разряжен"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
@@ -2,8 +2,12 @@ package com.helible.pilot.viewmodels
|
|||||||
|
|
||||||
import android.bluetooth.BluetoothSocket
|
import android.bluetooth.BluetoothSocket
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.helible.pilot.KMessage
|
import com.helible.pilot.dataclasses.DeviceState
|
||||||
import com.helible.pilot.toKMessage
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
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")
|
class TransferFailedException : IOException("Reading incoming data failed")
|
||||||
|
|
||||||
|
@ExperimentalStdlibApi
|
||||||
class BluetoothDataTransferService(
|
class BluetoothDataTransferService(
|
||||||
private val socket: BluetoothSocket,
|
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 {
|
return flow {
|
||||||
if (!socket.isConnected)
|
if (!socket.isConnected)
|
||||||
return@flow
|
return@flow
|
||||||
val buffer = ByteArray(128)
|
val buffer = ByteArray(512)
|
||||||
while (true) {
|
while (true) {
|
||||||
val byteCount: Int = try {
|
val byteCount: Int = try {
|
||||||
socket.inputStream.read(buffer)
|
socket.inputStream.read(buffer)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
|
Log.e("BluetoothController", "Failed to receive incoming data")
|
||||||
throw TransferFailedException()
|
throw TransferFailedException()
|
||||||
}
|
}
|
||||||
val strData: String = buffer.decodeToString(endIndex = byteCount)
|
|
||||||
emit(
|
val messageData: String = buffer.decodeToString(endIndex = byteCount)
|
||||||
strData
|
if (!messageData.endsWith("\n\r")) {
|
||||||
)
|
Log.i("BluetoothController", "Package end isn't valid.")
|
||||||
Log.i("BluetoothController", "Received: ${strData.dropLast(2)}")
|
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)
|
}.flowOn(Dispatchers.IO)
|
||||||
}
|
}
|
||||||
@@ -42,10 +63,11 @@ class BluetoothDataTransferService(
|
|||||||
try {
|
try {
|
||||||
socket.outputStream.write(bytes)
|
socket.outputStream.write(bytes)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
e.printStackTrace()
|
Log.e("BluetoothController", "Failed to write message: $e")
|
||||||
return@withContext false
|
return@withContext false
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
package com.helible.pilot.viewmodels
|
package com.helible.pilot.viewmodels
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.helible.pilot.controllers.BluetoothController
|
import com.helible.pilot.controllers.BluetoothController
|
||||||
import com.helible.pilot.controllers.ConnectionResult
|
import com.helible.pilot.controllers.ConnectionResult
|
||||||
import com.helible.pilot.dataclasses.AlarmStateMessage
|
|
||||||
import com.helible.pilot.dataclasses.BluetoothDevice
|
import com.helible.pilot.dataclasses.BluetoothDevice
|
||||||
import com.helible.pilot.dataclasses.BluetoothUiState
|
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||||
import com.helible.pilot.dataclasses.EmergStopMessage
|
import com.helible.pilot.dataclasses.ChangedDeviceStatus
|
||||||
import com.helible.pilot.dataclasses.RotorsSpeedMessage
|
import com.helible.pilot.dataclasses.DeviceStatus
|
||||||
|
import com.helible.pilot.dataclasses.DeviceStatusJsonAdapter
|
||||||
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
|
||||||
@@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.onEach
|
|||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.time.LocalTime
|
||||||
|
|
||||||
class BluetoothViewModel(
|
class BluetoothViewModel(
|
||||||
private val bluetoothController: BluetoothController,
|
private val bluetoothController: BluetoothController,
|
||||||
@@ -43,10 +44,9 @@ class BluetoothViewModel(
|
|||||||
pairedBluetoothDevices = pairedDevices
|
pairedBluetoothDevices = pairedDevices
|
||||||
)
|
)
|
||||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), _state.value)
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), _state.value)
|
||||||
private val moshi: Moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
|
private var deviceConnectionJob: Job? = null
|
||||||
private val rotorsStateMessegeAdapter = moshi.adapter(RotorsSpeedMessage::class.java)
|
private val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).add(DeviceStatusJsonAdapter()).build()
|
||||||
private val alarmStateMessageAdapter = moshi.adapter(AlarmStateMessage::class.java)
|
private val newStatusMessageAdapter = moshi.adapter(ChangedDeviceStatus::class.java)
|
||||||
private val emergStopMessageAdapter = moshi.adapter(EmergStopMessage::class.java)
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
bluetoothController.isConnected.onEach { isConnected ->
|
bluetoothController.isConnected.onEach { isConnected ->
|
||||||
@@ -94,9 +94,11 @@ class BluetoothViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is ConnectionResult.TransferSucceded -> {
|
is ConnectionResult.TransferSucceded -> {
|
||||||
_state.update { it.copy(
|
_state.update {
|
||||||
|
it.copy(
|
||||||
) }
|
deviceState = result.message
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is ConnectionResult.Error -> {
|
is ConnectionResult.Error -> {
|
||||||
@@ -111,19 +113,19 @@ class BluetoothViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.catch { throwable ->
|
.catch { throwable ->
|
||||||
|
Log.e("BluetoothController", "Error occured while data transfer: ${throwable.message}")
|
||||||
bluetoothController.closeConnection()
|
bluetoothController.closeConnection()
|
||||||
_state.update { it.copy(
|
_state.update { it.copy(
|
||||||
isConnected = false,
|
isConnected = false,
|
||||||
isConnecting = false
|
isConnecting = false,
|
||||||
|
deviceState = null
|
||||||
) }
|
) }
|
||||||
}
|
}
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var deviceConnectionJob: Job? = null
|
|
||||||
|
|
||||||
fun connectToDevice(device: String) {
|
fun connectToDevice(device: String) {
|
||||||
if (_state.value.isConnected and _state.value.isConnecting) {
|
if (_state.value.isConnected or _state.value.isConnecting) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_state.update { it.copy(isConnecting = true) }
|
_state.update { it.copy(isConnecting = true) }
|
||||||
@@ -138,7 +140,8 @@ class BluetoothViewModel(
|
|||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isConnecting = false,
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import com.helible.pilot.controllers.AndroidBluetoothController
|
import com.helible.pilot.controllers.AndroidBluetoothController
|
||||||
|
|
||||||
|
@ExperimentalStdlibApi
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
class BluetoothViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
|
class BluetoothViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Digital Pilot</string>
|
<string name="app_name">Digital Pilot</string>
|
||||||
|
<string name="calibration_description">Расположите устройство на ровной горизонтальной поверхности, чтобы сани вертолета полностью лежали на ней. Нажмите кнопку калибровки ниже и ждите её окончания, не создавая тряски.</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user