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.helible.pilot.dataclasses.RotorsDuty import com.helible.pilot.dataclasses.SticksPosition 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 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 import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch class BluetoothViewModel( private val bluetoothController: BluetoothController, ) : ViewModel() { private val _selectedDevice: MutableStateFlow = MutableStateFlow(null) val selectedDevice: StateFlow get() = _selectedDevice.asStateFlow() private val _state: MutableStateFlow = MutableStateFlow(BluetoothUiState()) val state: StateFlow = combine(bluetoothController.scannedDevices, bluetoothController.pairedDevices, _state) { scannedDevices, pairedDevices, state -> state.copy( scannedBluetoothDevices = scannedDevices.toList(), pairedBluetoothDevices = pairedDevices ) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), _state.value) private val _rotorsDuty: MutableStateFlow = MutableStateFlow(RotorsDuty(0, 0, 0)) private val _isRotorsTelemetryEnabled: MutableStateFlow = MutableStateFlow(false) private val _isConsoleTelemetryEnabled: MutableStateFlow = MutableStateFlow(false) private val _sticksPosition: MutableStateFlow = MutableStateFlow(SticksPosition(0, 0, 0)) val rotorsDuty: StateFlow get() = _rotorsDuty.asStateFlow() 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) private val rotorDutyMessageAdapter = moshi.adapter(RotorsDuty::class.java) private val stopAllRotorsMessageAdapter = moshi.adapter(StopMessage::class.java) private val consoleStateMessageAdapter = moshi.adapter(SticksPosition::class.java) companion object { const val messageDelimiter = "\n" const val telemetryPauseDuractionMs: Long = 100 } 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.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}") } catch (e: Exception) { Log.e("BluetoothVM", "Unknown error on message: ${result.message.data}") } } is ConnectionResult.Error -> { _state.update { it.copy( isConnected = false, isConnecting = false, errorMessage = result.message ) } } } } .catch { throwable -> Log.e( "BluetoothVM", "Error occured while data transfer: ${throwable.localizedMessage}" ) 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() _isConsoleTelemetryEnabled.update { false } _isRotorsTelemetryEnabled.update { false } _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 startImuCalibration() { viewModelScope.launch { val message = statusMessageAdapter.toJson( ChangedDeviceStatus(DeviceStatus.IsImuCalibration) ) + messageDelimiter 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)) + messageDelimiter 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) + messageDelimiter 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}") } 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...") _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) } _isConsoleTelemetryEnabled.update { false } } } } fun startTakeoff() { viewModelScope.launch { val message = statusMessageAdapter.toJson(ChangedDeviceStatus(DeviceStatus.IsFlying)) + messageDelimiter val isSuccess = bluetoothController.trySendMessage(message.toByteArray()) if(!isSuccess) { Log.e("BluetoothVM", "Failed to start takeoff: $message") _state.update { it.copy(errorMessage = "Не удалось начать полёт!") } } else { _state.update { it.copy(deviceState = it.deviceState?.copy(status = DeviceStatus.IsFlying)) } startConsoleTelemetrySending() } } } fun startOnboarding() { viewModelScope.launch { val message = statusMessageAdapter.toJson(ChangedDeviceStatus(DeviceStatus.IsBoarding)) + messageDelimiter val isSuccess = bluetoothController.trySendMessage(message.toByteArray()) if(!isSuccess) { Log.e("BluetoothVM", "Failed to start onboarding: $message") } else { _state.update { it.copy(deviceState = it.deviceState?.copy(status = DeviceStatus.IsBoarding)) } stopConsoleTelemetry() } } } fun changeHeightStickPosition(newHeightStickPosition: Int) { _sticksPosition.update { it.copy(heightStick = newHeightStickPosition) } } fun changeYawStickPosition(newYawStickPosition: Int) { _sticksPosition.update { it.copy(yawStick = newYawStickPosition) } } fun changePitchStickPosition(newPitchStickPosition: Int) { _sticksPosition.update { it.copy(pitchStick = newPitchStickPosition) } } private fun sendConsoleState() { viewModelScope.launch { val message = consoleStateMessageAdapter.toJson( _sticksPosition.value ) + messageDelimiter val isSuccess = bluetoothController.trySendMessage( message.toByteArray() ) Log.i("BluetoothVM", "Sended telemetry message: $message") if (!isSuccess) { Log.e("BluetoothVM", "Failed to send console telemetry: $message") } } } private fun startConsoleTelemetrySending() { if(_isConsoleTelemetryEnabled.value) { return } _isConsoleTelemetryEnabled.update { true } flow { while (_isConsoleTelemetryEnabled.value) { emit(Unit) delay(telemetryPauseDuractionMs) } }.onEach { sendConsoleState() }.launchIn(viewModelScope) } private fun stopConsoleTelemetry() { _isConsoleTelemetryEnabled.update { false } } }