Drone3D-Team/Drone3D

View on GitHub
app/src/main/java/ch/epfl/sdp/drone3d/service/impl/drone/DroneDataImpl.kt

Summary

Maintainability
A
0 mins
Test Coverage
A
97%
/*
 * Copyright (C) 2021  Drone3D-Team
 * The license can be found in LICENSE at root of the repository
 */

package ch.epfl.sdp.drone3d.service.impl.drone

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import ch.epfl.sdp.drone3d.service.api.drone.DroneData
import ch.epfl.sdp.drone3d.service.api.drone.DroneDataEditable
import ch.epfl.sdp.drone3d.service.api.drone.DroneService
import com.mapbox.mapboxsdk.geometry.LatLng
import io.mavsdk.System
import io.mavsdk.mission.Mission
import io.mavsdk.telemetry.Telemetry
import io.reactivex.Flowable
import io.reactivex.disposables.Disposable
import io.reactivex.functions.Consumer
import timber.log.Timber
import kotlin.math.pow
import kotlin.math.sqrt

/**
 * This class regroup all the data from the drone. This is inspired a lot by the Drone class from Fly2Find project.
 *
 * With minor adjustments such as:
 *  - Remove unused properties of the drone
 *  - Add functionality used by our app
 *  - Convert certain types to our owns
 *  - Change from object to api/implementation
 */
class DroneDataImpl constructor(val provider: DroneService) : DroneDataEditable {

    private val disposables: MutableList<Disposable> = ArrayList()

    private val position: MutableLiveData<LatLng> = MutableLiveData()
    private val batteryLevel: MutableLiveData<Float> = MutableLiveData()
    private val absoluteAltitude: MutableLiveData<Float> = MutableLiveData()
    private val relativeAltitude: MutableLiveData<Float> = MutableLiveData()
    private val speed: MutableLiveData<Float> = MutableLiveData()
    private val homeLocation: MutableLiveData<Telemetry.Position> = MutableLiveData()
    private val isFlying: MutableLiveData<Boolean> = MutableLiveData(false)
    private val isConnected: MutableLiveData<Boolean> = MutableLiveData(false)
    private val isMissionPaused: MutableLiveData<Boolean> = MutableLiveData(true)
    private val cameraResolution: MutableLiveData<DroneData.CameraResolution> = MutableLiveData()
    private val videoStreamUri: MutableLiveData<String> = MutableLiveData()
    private val mission: MutableLiveData<List<Mission.MissionItem>> = MutableLiveData()
    private val missionProgress: MutableLiveData<Float> = MutableLiveData()
    private val focalLength: MutableLiveData<Float> = MutableLiveData()
    private val sensorSize: MutableLiveData<DroneData.SensorSize> = MutableLiveData()
    private val droneStatus: MutableLiveData<DroneData.DroneStatus> = MutableLiveData(DroneData.DroneStatus.ARMING)

    init {
        createDefaultSubs()
    }

    /**
     * Setup the observers for the data of the drone we keep track of
     */
    private fun createDefaultSubs() {

        val droneInstance = provider.provideDrone()

        if (droneInstance == null) {
            // Reset
            position.postValue(null)
            batteryLevel.postValue(null)
            absoluteAltitude.postValue(null)
            speed.postValue(null)
            homeLocation.postValue(null)
            isFlying.postValue(false)
            isConnected.postValue(false)
            mission.postValue(null)
            missionProgress.postValue(null)
            isMissionPaused.postValue(true)
            cameraResolution.postValue(null)
            droneStatus.postValue(DroneData.DroneStatus.IDLE)
        } else {
            addFlightModeSubscriptions(droneInstance)
            addArmedSubscriptions(droneInstance)
            addPositionSubscriptions(droneInstance)
            addBatteryLevelSubscriptions(droneInstance)
            addSpeedSubscriptions(droneInstance)
            addIsFlyingSubscriptions(droneInstance)
            addHomeSubscriptions(droneInstance)
            addMissionProgressSubscriptions(droneInstance)
            addIsConnectedSubscriptions(droneInstance)
            addCameraSubscriptions(droneInstance)
            addVideoStreamSubscriptions(droneInstance)
        }
    }

    private fun addFlightModeSubscriptions(droneInstance: System) {
        addSubscription(droneInstance.telemetry.flightMode, "Flight Mode") { flightMode ->
            if (flightMode == Telemetry.FlightMode.HOLD) isMissionPaused.postValue(true)
            if (flightMode == Telemetry.FlightMode.MISSION) isMissionPaused.postValue(false)
        }
    }

    private fun addArmedSubscriptions(droneInstance: System) {
        addSubscription(droneInstance.telemetry.armed, "Armed") { armed ->
            if (!armed) isMissionPaused.postValue(true)
        }
    }

    private fun addPositionSubscriptions(droneInstance: System) {
        addSubscription(droneInstance.telemetry.position, "Telemetry Position") { position ->
            val latLng = LatLng(position.latitudeDeg, position.longitudeDeg)
            this.position.postValue(latLng)
            //Absolute altitude is the altitude w.r. to the sea level
            absoluteAltitude.postValue(position.absoluteAltitudeM)
            relativeAltitude.postValue(position.relativeAltitudeM)
        }
    }

    private fun addBatteryLevelSubscriptions(droneInstance: System) {
        addSubscription(droneInstance.telemetry.battery, "Battery") { battery ->
            batteryLevel.postValue(battery.remainingPercent)
        }
    }

    private fun addSpeedSubscriptions(droneInstance: System) {
        addSubscription(
            droneInstance.telemetry.positionVelocityNed,
            "GroundSpeedNed"
        ) { vector_speed ->
            speed.postValue(
                sqrt(
                    vector_speed.velocity.eastMS.pow(2) + vector_speed.velocity.northMS.pow(2)
                )
            )
        }
    }

    private fun addIsFlyingSubscriptions(droneInstance: System) {
        addSubscription(droneInstance.telemetry.inAir, "inAir") { isFlying ->
            this.isFlying.postValue(isFlying)
        }
    }

    private fun addHomeSubscriptions(droneInstance: System) {
        addSubscription(droneInstance.telemetry.home, "home") { home ->
            homeLocation.postValue(home)
        }
    }

    private fun addIsConnectedSubscriptions(droneInstance: System) {
        addSubscription(droneInstance.core.connectionState, "connectionState") { state ->
            isConnected.postValue(state.isConnected)
        }
    }

    private fun addMissionProgressSubscriptions(droneInstance: System) {
        addSubscription(droneInstance.mission.missionProgress, "progress") { progress ->
            this.missionProgress.postValue(progress.current.toFloat() / progress.total)
        }
    }

    private fun addCameraSubscriptions(droneInstance: System) {
        addSubscription(droneInstance.camera.information, "cameraResolution") { i ->
            cameraResolution.postValue(
                DroneData.CameraResolution(
                    i.horizontalResolutionPx,
                    i.verticalResolutionPx
                )
            )
            focalLength.postValue(i.focalLengthMm)
            sensorSize.postValue(
                DroneData.SensorSize(
                    i.horizontalSensorSizeMm,
                    i.verticalSensorSizeMm
                )
            )
        }
    }

    private fun addVideoStreamSubscriptions(droneInstance: System) {
        addSubscription(droneInstance.camera.videoStreamInfo.map { it.settings.uri }, "videoStreamUri") { uri ->
            videoStreamUri.postValue(uri)
        }
    }

    @Synchronized
    private fun disposeOfAll() {
        disposables.forEach(Disposable::dispose)
        disposables.clear()
    }

    override fun getPosition(): LiveData<LatLng> = position

    override fun getBatteryLevel(): LiveData<Float> = batteryLevel

    override fun getAbsoluteAltitude(): LiveData<Float> = absoluteAltitude

    override fun getRelativeAltitude(): LiveData<Float> = relativeAltitude

    override fun getSpeed(): LiveData<Float> = speed

    override fun getHomeLocation(): LiveData<Telemetry.Position> = homeLocation

    override fun isFlying(): LiveData<Boolean> = isFlying

    override fun isConnected(): LiveData<Boolean> = isConnected

    override fun getCameraResolution(): LiveData<DroneData.CameraResolution> = cameraResolution

    override fun getVideoStreamUri(): LiveData<String> = videoStreamUri

    override fun getMissionProgress(): LiveData<Float> = missionProgress

    override fun getFocalLength(): LiveData<Float> = focalLength

    override fun getSensorSize(): LiveData<DroneData.SensorSize> = sensorSize

    override fun refresh() {
        disposeOfAll()
        createDefaultSubs()
    }

    @Synchronized
    override fun purge() {
        disposables.removeAll { it.isDisposed }
    }

    override fun getMutableMission(): MutableLiveData<List<Mission.MissionItem>> = mission

    override fun getMutableMissionPaused(): MutableLiveData<Boolean> = isMissionPaused

    override fun getMutableDroneStatus(): MutableLiveData<DroneData.DroneStatus> = droneStatus

    private fun <T> addSubscription(flow: Flowable<T>, name: String, onNext: Consumer<in T>) {
        disposables.add(
            flow.distinctUntilChanged().subscribe(
                onNext,
                { error -> Timber.e(error, "Error $name : $error") }
            )
        )
    }

    protected fun finalize() {
        disposeOfAll()
    }
}