BimmerGestalt / AAIdrive

Implementations of some Android Auto features as unofficial IDrive apps
MIT License
558 stars 92 forks source link

Advanced Vehicle Info App #653

Closed Katana1234 closed 1 year ago

Katana1234 commented 1 year ago

Hi there,

I wanted to show a few values on the IDrive system that you're not able to see by default. So i did some modifications (super quick and dirty!), mainly to the ReadoutApp, to get what i wanted. This is the result: image

Wouldn't it be nice to properly implement a screen like this? :) My understanding of this whole project is sooooo bad so i guess i am not the right one to do this properly. I mostly achieved my goal by copy+paste and trial+error ;) But for any dev being more involved into this project it should be a easy task. I also don't know about the graphical capabilities of the system. I simply used the RHMI list.

In theory the output of the screen could be configurable in the main app. (Which values and where to place them + update interval of the cds data) Please let me know if i can help out any further.

berseker commented 1 year ago

nice! I also recall some similar concepts requests which i collect here below for any possible futher improvements!

https://github.com/BimmerGestalt/IDriveConnectAddons/issues/5 https://github.com/BimmerGestalt/AAIdrive/issues/175

hufman commented 1 year ago

That looks so good! Congrats on figuring it out without any help! Your approach of using a List is the correct one. There aren't very strong graphical capabilities in the system, the closest being manually sending over PNGs to take the place of some of the table cells. It would also be possible to add an AM App Icon to the Car Information menu in the car, as another entry point besides the unlabeled entryButton in the OnlineServices menu.

Do you imagine it should stay in the Readout app? I was planning for such a detailed view to be a separate addon app, but that would add an extra unlabeled icon to the car. I also have been unsure of how to build the UI to configure the displayed data. A first version could simply show a static list, easily enough.

Would you like to contribute a Pull Request, or may I copy code from your branch and add some polish?

6i6i commented 1 year ago

Very nice. I also tried to implement such feature but I wasn't able to. I would very appreciate it if it could get integrated into AAIDrive (or within a extra application).

Thank you

Katana1234 commented 1 year ago

Thanks, i also love these advanced infos :)

I will post my code changes here and explain why it's not possible to polish the code ;) I only changed 2 files in the worst possible way to get this screen working. It's been almost a decade since i touched Android apps for the last time and i clearly have to admit that your nice and structured way of implementing things highly exceeds my android coding skills.

I added the ListUpdater class to the ReadoutApp. I was not able to properly reuse existing code from the CarDrivingStatsModel. This is why i'm parsing the json values manually. I added some more props to the CarDrivingStatsModel to be able to read them. Things that have to be polished... My code:

I think it's a very good idea to implement this into the ReadoutApp instead of creating a new one. The ReadoutApp is currently just showing a static message which doesn't bring any advantage to the user. :) So i'm totally fine with extending this app with some more infos. I also think having some selected values non configurable is a nice way to start this. Due to the fact that i am reusing the cds data that has been grabed by the advanced info app the update interval of the cds data is 1s (when running in foreground) and 5s (when running in background) Showing realtime data makes no real sense for me. but this is just my opinion. E.g. showing the current gear is a nice feature but not fast enough when the app is running in background.

Considerations for a full featured implementation: Create a config activity for this with the option

I wish i would be able to help out here, but i think i'm on the wrong level to contribute code...

ReadoutApp.kt:

package me.hufman.androidautoidrive.carapp.notifications

import android.os.Build
import android.os.Handler
import android.util.Log
import androidx.annotation.RequiresApi
import com.google.gson.Gson
import de.bmw.idrive.BMWRemoting
import de.bmw.idrive.BMWRemotingServer
import de.bmw.idrive.BaseBMWRemotingClient
import io.bimmergestalt.idriveconnectkit.CDS
import io.bimmergestalt.idriveconnectkit.IDriveConnection
import io.bimmergestalt.idriveconnectkit.Utils.rhmi_setResourceCached
import io.bimmergestalt.idriveconnectkit.android.CarAppResources
import io.bimmergestalt.idriveconnectkit.android.IDriveConnectionStatus
import io.bimmergestalt.idriveconnectkit.android.security.SecurityAccess
import io.bimmergestalt.idriveconnectkit.rhmi.*
import me.hufman.androidautoidrive.*
import me.hufman.androidautoidrive.carapp.*
import me.hufman.androidautoidrive.phoneui.LiveDataHelpers.map
import me.hufman.androidautoidrive.utils.GsonNullable.tryAsInt
import me.hufman.androidautoidrive.utils.GsonNullable.tryAsJsonPrimitive
import org.json.JSONObject

@RequiresApi(Build.VERSION_CODES.O)
class ReadoutApp(val iDriveConnectionStatus: IDriveConnectionStatus, val securityAccess: SecurityAccess, carAppAssets: CarAppResources) {
    val carConnection: BMWRemotingServer
    val carApp: RHMIApplication
    val infoState: RHMIState.PlainState
    val readoutController: ReadoutController

    init {
        val cdsData = CDSDataProvider()
        val listener = ReadoutAppListener(cdsData)
        carConnection = IDriveConnection.getEtchConnection(iDriveConnectionStatus.host
                ?: "127.0.0.1", iDriveConnectionStatus.port ?: 8003, listener)
        val readoutCert = carAppAssets.getAppCertificate(iDriveConnectionStatus.brand
                ?: "")?.readBytes() as ByteArray
        val sas_challenge = carConnection.sas_certificate(readoutCert)
        val sas_login = securityAccess.signChallenge(challenge = sas_challenge)
        carConnection.sas_login(sas_login)

        // create the app in the car
        val rhmiHandle = carConnection.rhmi_create(null, BMWRemoting.RHMIMetaData("me.hufman.androidautoidrive.notification.readout", BMWRemoting.VersionInfo(0, 1, 0),
                "me.hufman.androidautoidrive.notification.readout", "me.hufman"))
        carConnection.rhmi_setResourceCached(rhmiHandle, BMWRemoting.RHMIResourceType.DESCRIPTION, carAppAssets.getUiDescription())
        // no icons or text, so sneaky
        carConnection.rhmi_initialize(rhmiHandle)

        carApp = RHMIApplicationSynchronized(RHMIApplicationIdempotent(RHMIApplicationEtch(carConnection, rhmiHandle)), carConnection)
        carApp.loadFromXML(carAppAssets.getUiDescription()?.readBytes() as ByteArray)
        this.readoutController = ReadoutController.build(carApp, "NotificationReadout")

        val destStateId = carApp.components.values.filterIsInstance<RHMIComponent.EntryButton>().first().getAction()?.asHMIAction()?.target!!
        this.infoState = carApp.states[destStateId] as RHMIState.PlainState

        initWidgets()

        // register for readout updates
        cdsData.setConnection(CDSConnectionEtch(carConnection))
        cdsData.subscriptions[CDS.HMI.TTS] = {
            val state = try {
                Gson().fromJson(it["TTSState"], TTSState::class.java)
            } catch (e: Exception) {
                null
            }
            if (state != null) {
                readoutController.onTTSEvent(state)
            }
        }

        ListUpdater(Handler(), infoState).schedule()

    }

    class ReadoutAppListener(val cdsEventHandler: CDSEventHandler) : BaseBMWRemotingClient() {
        override fun cds_onPropertyChangedEvent(handle: Int?, ident: String?, propertyName: String?, propertyValue: String?) {
            cdsEventHandler.onPropertyChangedEvent(ident, propertyValue)
        }
    }

    fun initWidgets() {
        val list = infoState.componentsList.filterIsInstance<RHMIComponent.List>().first()
        list.setEnabled(false)
        list.setVisible(true)
        val data = RHMIModel.RaListModel.RHMIListConcrete(1)
        data.addRow(arrayOf(L.READOUT_DESCRIPTION))
        list.getModel()?.setValue(data, 0, 1, 1)
    }

    fun disconnect() {
        try {
            IDriveConnection.disconnectEtchConnection(carConnection)
        } catch (e: java.io.IOError) {
        } catch (e: RuntimeException) {
        }
    }

    class ListUpdater(val handler: Handler, val infoState: RHMIState.PlainState) {
        companion object {
            const val DELAY = 1000L
        }

        @RequiresApi(Build.VERSION_CODES.O)
        val runnable = Runnable {

            val list = infoState.componentsList.filterIsInstance<RHMIComponent.List>().first()
            val data = RHMIModel.RaListModel.RHMIListConcrete(2)
            val carInfo = CarInformationObserver()

            try {

                val engineTemp = JSONObject(carInfo.cdsData[CDS.ENGINE.TEMPERATURE].toString()).getJSONObject("temperature").getInt("engine").toString() + "°C Engine"
                val oilTemp = JSONObject(carInfo.cdsData[CDS.ENGINE.TEMPERATURE].toString()).getJSONObject("temperature").getInt("oil").toString() + "°C Oil"
                val bat = JSONObject(carInfo.cdsData[CDS.SENSORS.BATTERY].toString()).getInt("battery").toString() + "% Battery"
                val fuel = JSONObject(carInfo.cdsData[CDS.SENSORS.FUEL].toString()).getJSONObject("fuel").getInt("tanklevel").toString() + "L Fuel" //range, reserve
                val tempint = JSONObject(carInfo.cdsData[CDS.SENSORS.TEMPERATUREINTERIOR].toString()).getDouble("temperatureInterior").toString() + "°C Interior"
                val tempext = JSONObject(carInfo.cdsData[CDS.SENSORS.TEMPERATUREEXTERIOR].toString()).getDouble("temperatureExterior").toString() + "°C Exterior"
                val tempexch = JSONObject(carInfo.cdsData[CDS.CLIMATE.ACSYSTEMTEMPERATURES].toString()).getJSONObject(("ACSystemTemperatures")).getDouble("heatExchanger").toString() + "°C Exchange"
                var gear = JSONObject(carInfo.cdsData[CDS.DRIVING.GEAR].toString()).getInt("gear")

                //val t = carInfo.cdsData[CDS.CLIMATE.ACSYSTEMTEMPERATURES].toString()

                var gearname = "-"
                if (gear == 1) {
                    gearname = "N"
                } else if (gear == 2) {
                    gearname = "R"
                } else if (gear == 3) {
                    gearname = "P"
                } else if (gear >= 5) {
                    gear -= 4
                    gearname = "D$gear"
                }
                gearname = "Gear " + gearname

                data.addRow(arrayOf("Advanced Vehicle Information"))
                data.addRow(arrayOf(""))
                data.addRow(arrayOf(engineTemp, tempext))
                data.addRow(arrayOf(oilTemp, tempint))
                data.addRow(arrayOf(bat, tempexch))
                data.addRow(arrayOf(fuel, gearname))

                list.getModel()?.setValue(data, 0, data.height, data.height)

            } catch (e: Exception) {
            }

            schedule()

        }

        @RequiresApi(Build.VERSION_CODES.O)
        fun schedule() {
            handler.removeCallbacks(runnable)
            handler.postDelayed(runnable, DELAY)
        }
    }

}

CarDrivingStatsModel.kt:

class CarDrivingStatsModel(carInfoOverride: CarInformation? = null, val showAdvancedSettings: BooleanLiveSetting): ViewModel() {
    companion object {
        val CACHED_KEYS = setOf(
                CDS.VEHICLE.VIN,
                CDS.VEHICLE.UNITS,
                CDS.DRIVING.ODOMETER,
                CDS.DRIVING.AVERAGECONSUMPTION,
                CDS.DRIVING.AVERAGESPEED,
                CDS.DRIVING.DISPLAYRANGEELECTRICVEHICLE,        // doesn't need unit conversion
                CDS.DRIVING.DRIVINGSTYLE,
                CDS.DRIVING.ECORANGEWON,
                CDS.ENGINE.RANGECALC,
                CDS.NAVIGATION.GPSPOSITION,
                CDS.NAVIGATION.CURRENTPOSITIONDETAILEDINFO,
                CDS.NAVIGATION.GPSEXTENDEDINFO,
                CDS.SENSORS.BATTERY,
                CDS.SENSORS.FUEL,
                CDS.SENSORS.SOCBATTERYHYBRID,
                CDS.VEHICLE.TIME,
                CDS.ENGINE.TEMPERATURE,
                CDS.CONTROLS.SUNROOF,
                CDS.CONTROLS.WINDOWDRIVERFRONT,
                CDS.CONTROLS.WINDOWPASSENGERFRONT,
                CDS.CONTROLS.WINDOWDRIVERREAR,
                CDS.CONTROLS.WINDOWPASSENGERREAR,
                CDS.DRIVING.PARKINGBRAKE,
                CDS.SENSORS.TEMPERATUREINTERIOR,
                CDS.SENSORS.TEMPERATUREEXTERIOR,
                CDS.CLIMATE.ACSYSTEMTEMPERATURES,
                CDS.DRIVING.GEAR
        )
    }
...
ogakul commented 1 year ago

Any updates on this?

hufman commented 1 year ago

I've been busy with other projects and haven't had time to look into this.

ogakul commented 1 year ago

@Katana1234 can you maybe describe how you achieved the menu in the screenshot? this would be enough for my needs.

If it's a self compiled APK maybe just share it here?

Katana1234 commented 1 year ago

@ogakul I already posted all my code changes necessary to show this menu + my objections why not to use this code above ;-)

hufman commented 1 year ago

I've posted a test build for this feature branch, try it out!

HaivanJV commented 1 year ago

Nice! I tested it and it seems to be working fine

image

Do you plan to add an icon and a title to the "app" in the idrive? or is it not much possible? I can see that the Mapbox navi doesn't have a title

berseker commented 1 year ago

Nice! I tested it and it seems to be working fine image

Do you plan to add an icon and a title to the "app" in the idrive? or is it not much possible? I can see that the Mapbox navi doesn't have a title

0°C oil & 255°C battery seems a bit off :D anyway I quickly tested it and it works, even if I have to test it more deeply in the next days and i'll let have my feedback on this

HaivanJV commented 1 year ago

I had my car off, and that's what AAiDrive displays on the car info screen. When on, I have the correct oil temp.

I am not sure about the Battery though. It is fixed at 255C no matter if on or off. I also wonder how did I not fry inside :D

hufman commented 1 year ago

Thanks for the early feedback! In some places I have the code reject obvious bad data, I guess I need to add that for battery temp :) I can't change the app icon, but I plan on adding a shortcut icon like the Notifications app.

hufman commented 1 year ago

Added an app icon to the Vehicle Info screen of the car: image

HaivanJV commented 1 year ago

thanks! this looks pretty cool, also that it's within the My Car menu! I also noticed that you removed the battery temp info and added the Gear info. One thing that remained in the interface was the gap (empty cell) where the Batt temp was.

Do you know what the HVAC temp is?

6i6i commented 1 year ago

Great work @hufman . That's really nice.

Katana1234 commented 1 year ago

thanks! this looks pretty cool, also that it's within the My Car menu! I also noticed that you removed the battery temp info and added the Gear info. One thing that remained in the interface was the gap (empty cell) where the Batt temp was.

Do you know what the HVAC temp is?

@HaivanJV HVAC stands for Heat Ventilation and Air Condition. This value represents the temperature of the heat exchanger and is more or less the temperature of the air comming out of the air vents. (I assume!)

Katana1234 commented 1 year ago

@hufman Nice implemenatation! Thank you very much for taking your time on this! I also like your concept of having multiple categories :) In theory the layout of the categories, rows and column could be configurable within the app. But i'm already super happy with this solution and everything that follows is just cosmetics!

Side note: The clutch value seems to be a state instead of a percentage value. At least for my car with an automatic gearbox. Values: 3 -> "Open / Idle?" -> This value is returned while gear P is engaged 2 -> "Uncoupled" -> This value is returned while a gear is engaged but the torque converter is uncoupled (when accellerating from standing still) 1 -> ??? -> not yet seen 0 -> "Coupled" -> This value is returned while a gear is engaged and the torque converter is coupled (typically while driving)

hufman commented 1 year ago

Thank you! I'm pleased with how it's shaping up, after I got over the motivational cliff to add a new data pipeline to the app :) The Battery Temp only shows a value if it's lower than 255C. If nobody sees any valid data for this field, it's easy enough to remove. I wasn't sure what the Clutch field would show, the CDS API just calls it clutchPedal.position, just like acceleratorPedal.position, so I guessed :) Similarly, the brakePedal in my car only goes from 10-25, somehow haha.

Katana1234 commented 1 year ago

I guess battery temperature will only work for electric cars. Maybe we can determine if a car is electric when engine.electricVehicleMode returns a value. just guessing... :)

And i guess clutchPedal.position works different between manual and automatic gearboxes. BMW engineers are traditionally reusing properties in different ways for different configurations :D We can look into this in a later step. In theory we have the engine.info.gearboxType to determine what gearbox the car is equipped with. We'll probably have to compare some infos of different cars. but this is already too much for this GitHub issue :)

hufman commented 1 year ago

My Mini EV does have a battery temp there, it seems! I fixed a crash-on-disconnect I was seeing in analytics and merged it to the main branch, and will probably push it to the beta app store users after a week for translations to roll in.

hufman commented 1 year ago

@Katana1234 please experiment with the code and come up with more screens of data that you'd like to see :) I don't want to build a configuration system, but would accept ideas for how to organize the data into pre-built screens.

Vinsvens commented 1 year ago

First of all, thank you for the great effort made to create this app, which allows us to obtain new features in our cars. Would it be possible to add the following data point to CDSMetrics: driving.acceleration {longitudinal= , lateral= } ?

I don't know if I'm wrong, but I think they are the different forces exerted on the car, when accelerating, braking or taking a curve in (m/s²), if we divide this value by 9.81 we could obtain the different G forces and display them on the screen.

It's just an idea and I don't know if it's correct.

Thank you.

Vinsvens commented 1 year ago

@hufman

Katana1234 commented 1 year ago

@hufman Feel free to try my branch: https://github.com/Katana1234/AAIdrive/tree/car_info_data_selection

It definitely needs a good polish. Strings are hardcoded and many parts are copied / modified from another class for my purposes.

hufman commented 1 year ago

Very cool! I posted a branch with your Driving Details updates and the Windows screen. However, isn't the GPS information is already available elsewhere in the car, like in one of the side panels? I'm reluctant to add more work for the translators than necessary :) I'm considering hiding the Windows and maybe GPS behind the Advanced Settings toggle, and then I can justify not translating them.

Katana1234 commented 1 year ago

@hufman Nice refactoring, thanks :) Yes the GPS info is kind of redundant, but you will not be able to see all of this infos in one place within the IDRIVE system. And the altitude is inaccessible in IDRIVE.

Did you take a look at my brake interpretations? The brake value is not a percentage value. It is a bit coded state: https://github.com/Katana1234/AAIdrive/blob/bf259b1a284b956077e62676a21e53247b50505f/app/src/main/java/me/hufman/androidautoidrive/cds/CDSMetrics.kt#L201

I also added a interpretation for the clutch values, at least for automatic gearboxes. Otherwise the original number will be returned: https://github.com/Katana1234/AAIdrive/blob/bf259b1a284b956077e62676a21e53247b50505f/app/src/main/java/me/hufman/androidautoidrive/cds/CDSMetrics.kt#L236

hufman commented 1 year ago

Yes I saw the brake and clutch values. I feel my car has more of a smooth gradient for brake values than a bitfield would indicate, so I didn't want to commit to that interpretation and thus the strings, if they were incorrect. I should display them as binary in my car to see how it looks. As a rough approximation I suppose those labels would be more useful than the current arbitrary number. My car is electric so I don't know how the values correlate to a gas car.

Katana1234 commented 1 year ago

Feel free to try your brakeContact values here: https://docs.google.com/spreadsheets/d/10FfyFMfW5VGKQGMyOlp4iCyeXoOleGbB5na0-ZFpMX8/edit?usp=sharing

This way you can validate my "interpratation" easily :)

image

hufman commented 1 year ago

Indeed, I see those values too, I'll code them up.

hufman commented 1 year ago

I updated my branch with these changes, what do you think? Any last ideas before I merge it to the main branch?

Katana1234 commented 1 year ago

@hufman I didn't check the compiled version, but the values selected in the code looks good to me :)

berseker commented 1 year ago

dear @hufman , i tried this test branch. the only misreading I have to highlight is the sunroof one, since I do not have one :D

image

hufman commented 1 year ago

@berseker please try the latest build from the Readme, which should include a fix for your sunroof!

berseker commented 1 year ago

image @hufman now seems fine