272 lines
10 KiB
Kotlin
272 lines
10 KiB
Kotlin
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.BluetoothDevice
|
||
import com.helible.pilot.dataclasses.BluetoothUiState
|
||
import com.helible.pilot.dataclasses.ChangedDeviceStatus
|
||
import com.helible.pilot.dataclasses.DeviceState
|
||
import com.helible.pilot.dataclasses.DeviceStatus
|
||
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.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.flow.Flow
|
||
import kotlinx.coroutines.flow.MutableStateFlow
|
||
import kotlinx.coroutines.flow.SharingStarted
|
||
import kotlinx.coroutines.flow.StateFlow
|
||
import kotlinx.coroutines.flow.asStateFlow
|
||
import kotlinx.coroutines.flow.catch
|
||
import kotlinx.coroutines.flow.combine
|
||
import kotlinx.coroutines.flow.launchIn
|
||
import kotlinx.coroutines.flow.onEach
|
||
import kotlinx.coroutines.flow.stateIn
|
||
import kotlinx.coroutines.flow.update
|
||
import kotlinx.coroutines.launch
|
||
|
||
class BluetoothViewModel(
|
||
private val bluetoothController: BluetoothController,
|
||
) : ViewModel() {
|
||
|
||
private val _selectedDevice: MutableStateFlow<BluetoothDevice?> = MutableStateFlow(null)
|
||
val selectedDevice: StateFlow<BluetoothDevice?>
|
||
get() = _selectedDevice.asStateFlow()
|
||
|
||
private val _state: MutableStateFlow<BluetoothUiState> = MutableStateFlow(BluetoothUiState())
|
||
val state: StateFlow<BluetoothUiState> =
|
||
combine(bluetoothController.scannedDevices, bluetoothController.pairedDevices, _state)
|
||
{ scannedDevices, pairedDevices, state ->
|
||
state.copy(
|
||
scannedBluetoothDevices = scannedDevices.toList(),
|
||
pairedBluetoothDevices = pairedDevices
|
||
)
|
||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), _state.value)
|
||
private var deviceConnectionJob: Job? = null
|
||
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)
|
||
|
||
init {
|
||
bluetoothController.isConnected.onEach { isConnected ->
|
||
_state.update { it.copy(isConnected = isConnected) }
|
||
}.launchIn(viewModelScope)
|
||
bluetoothController.errors.onEach { error ->
|
||
_state.update {
|
||
it.copy(errorMessage = error)
|
||
}
|
||
}.launchIn(viewModelScope)
|
||
bluetoothController.isScanning.onEach { isDiscovering ->
|
||
_state.update {
|
||
it.copy(
|
||
isDiscovering = isDiscovering,
|
||
)
|
||
}
|
||
}.launchIn(viewModelScope)
|
||
bluetoothController.isEnabled.onEach { isEnabled ->
|
||
_state.update {
|
||
it.copy(
|
||
isEnabled = isEnabled,
|
||
)
|
||
}
|
||
}.launchIn(viewModelScope)
|
||
bluetoothController.isLocationEnabled.onEach { isLocationEnabled ->
|
||
_state.update {
|
||
it.copy(
|
||
isLocationEnabled = isLocationEnabled
|
||
)
|
||
}
|
||
}.launchIn(viewModelScope)
|
||
}
|
||
|
||
private fun Flow<ConnectionResult>.listen(): Job {
|
||
return onEach { result ->
|
||
when (result) {
|
||
ConnectionResult.ConnectionEstablished -> {
|
||
_state.update {
|
||
it.copy(
|
||
isConnected = true,
|
||
isConnecting = false,
|
||
errorMessage = null
|
||
)
|
||
}
|
||
}
|
||
|
||
is ConnectionResult.TransferSucceded -> {
|
||
try {
|
||
when (result.message.type) {
|
||
MessageType.PidSettings -> {
|
||
val newPidSettings =
|
||
pidSittingsMessageAdapter.fromJson(result.message.data)
|
||
_state.update {
|
||
it.copy(
|
||
deviceState = it.deviceState?.copy(pidSettings = newPidSettings)
|
||
)
|
||
}
|
||
}
|
||
|
||
MessageType.UpdateMessage -> {
|
||
val newDeviceState =
|
||
deviceStateMessageAdapter.fromJson(result.message.data)
|
||
if (newDeviceState != null) {
|
||
_state.update {
|
||
it.copy(
|
||
deviceState = newDeviceState.copy(pidSettings = it.deviceState?.pidSettings)
|
||
)
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|
||
} catch (e: JsonDataException) {
|
||
Log.e("BluetoothVM", "Failed to parse message: ${result.message.data}")
|
||
} catch (e: JsonEncodingException) {
|
||
Log.e("BluetoothVM", "Failed to decode message: ${result.message.data}")
|
||
}
|
||
}
|
||
|
||
is ConnectionResult.Error -> {
|
||
_state.update {
|
||
it.copy(
|
||
isConnected = false,
|
||
isConnecting = false,
|
||
errorMessage = result.message
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.catch { throwable ->
|
||
Log.e(
|
||
"BluetoothController",
|
||
"Error occured while data transfer: ${throwable.message}"
|
||
)
|
||
bluetoothController.closeConnection()
|
||
_state.update {
|
||
it.copy(
|
||
isConnected = false,
|
||
isConnecting = false,
|
||
deviceState = null
|
||
)
|
||
}
|
||
}
|
||
.launchIn(viewModelScope)
|
||
}
|
||
|
||
fun connectToDevice(device: String) {
|
||
if (_state.value.isConnected or _state.value.isConnecting) {
|
||
return
|
||
}
|
||
_state.update { it.copy(isConnecting = true) }
|
||
deviceConnectionJob = bluetoothController
|
||
.connectToDevice(device)
|
||
.listen()
|
||
}
|
||
|
||
fun disconnectFromDevice() {
|
||
deviceConnectionJob?.cancel()
|
||
bluetoothController.closeConnection()
|
||
_state.update {
|
||
it.copy(
|
||
isConnecting = false,
|
||
isConnected = false,
|
||
deviceState = null
|
||
)
|
||
}
|
||
}
|
||
|
||
fun selectDevice(selectedDevice: BluetoothDevice?) {
|
||
_selectedDevice.update { selectedDevice }
|
||
}
|
||
|
||
fun startScan() {
|
||
selectDevice(null)
|
||
bluetoothController.startDiscovery()
|
||
}
|
||
|
||
fun cancelScan() {
|
||
bluetoothController.cancelDiscovery()
|
||
}
|
||
|
||
override fun onCleared() {
|
||
cancelScan()
|
||
bluetoothController.onDestroy()
|
||
super.onCleared()
|
||
}
|
||
|
||
fun sendHelloWorld() {
|
||
viewModelScope.launch {
|
||
bluetoothController.trySendMessage(
|
||
"{\"p1\": {\"p\": 1.5, \"i\": 1.5, \"d\": 1.5}}\n\r".toByteArray()
|
||
//"{\"p1\": [1.5, 1.5, 1.5]}\n\r".toByteArray()
|
||
)
|
||
}
|
||
}
|
||
|
||
fun startImuCalibration() {
|
||
viewModelScope.launch {
|
||
val message = statusMessageAdapter.toJson(
|
||
ChangedDeviceStatus(DeviceStatus.IsImuCalibration)
|
||
) + "\n\r"
|
||
val isSuccess = bluetoothController.trySendMessage(
|
||
message.toByteArray()
|
||
)
|
||
if(!isSuccess) {
|
||
Log.e("BluetoothVM", "Failed to start IMU calibration: $message")
|
||
} else {
|
||
_state.update {
|
||
it.copy(
|
||
deviceState = it.deviceState?.copy(status = DeviceStatus.IsImuCalibration)
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fun requestPidSettings() {
|
||
viewModelScope.launch {
|
||
val message = pidSittingsRequiredMessageAdapter.toJson(PidSettingRequiredMessage(true)) + "\n\r"
|
||
Log.i("BluetoothVM", "Requested PID settings: $message")
|
||
val isSuccess = bluetoothController.trySendMessage(
|
||
message.toByteArray()
|
||
)
|
||
if(!isSuccess) {
|
||
Log.e("BluetoothVM", "Failed to request PID settings: $message")
|
||
}
|
||
}
|
||
}
|
||
|
||
fun applyPidSettings(pidSettings: PidSettings) {
|
||
viewModelScope.launch {
|
||
val message = pidSittingsMessageAdapter.toJson(pidSettings) + "\n\r"
|
||
val isSuccess = bluetoothController.trySendMessage(message.toByteArray())
|
||
if(!isSuccess) {
|
||
Log.e("BluetoothVM", "Failed to request PID settings: $message")
|
||
_state.update {
|
||
it.copy(errorMessage = "Не удалось обновить значения PID")
|
||
}
|
||
} else {
|
||
_state.update {
|
||
it.copy(deviceState = it.deviceState?.copy(pidSettings = pidSettings))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fun clearPidSettings() {
|
||
Log.i("BluetoothVM", "PidSettings cleared")
|
||
_state.update {
|
||
it.copy(deviceState = it.deviceState?.copy(pidSettings = null))
|
||
}
|
||
Log.i("BluetoothVM", "PidSettings: ${_state.value.deviceState?.pidSettings}")
|
||
}
|
||
} |