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 = 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 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.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}") } }