diff --git a/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt b/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt index 90733a5..f30c588 100644 --- a/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt +++ b/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt @@ -427,6 +427,12 @@ object EVHardware { private const val PROP_CCU_ONBD_PLUG = 0x2140F43E // vendor int, AC socket private const val PROP_CCU_OFFBD_PLUG = 0x2140F43F // vendor int, DC socket private const val PROP_ENERGY_SINCE_CHARGE = 0x2160A1BB // vendor float kWh + /** + * `AAD_EPTCCTRGTSPD` in the head unit's `CarAdvancedAssistedDrivingManager`: by its name the + * powertrain's cruise control target speed. Unit and the value while cruise control is off + * are not established; it is passed on raw. + */ + private const val PROP_CRUISE_TARGET_SPEED = 0x2160A15B // vendor float /** * `HVAC_AMBIENT_TEMPERATURE`, the one in-car candidate left on this firmware. @@ -532,6 +538,11 @@ object EVHardware { * answers, and this is what is left on a unit where it does not. * See [PROP_BMS_CHRG_STS] for the values. */ + /** The cruise control target speed as the car reports it, unmapped; null when unread. */ + fun getCruiseTargetSpeedRaw(): Float? = supportedTelemetryRead { + getFloatPropertyCPM(PROP_CRUISE_TARGET_SPEED, AREA_GLOBAL)?.takeIf { it.isFinite() } + } + fun getVendorChargeStatus(): Int? = getIntPropertyCPM(PROP_BMS_CHRG_STS, AREA_GLOBAL).takeIf { it >= 0 } diff --git a/EVHardware/lib/src/main/java/com/evsuite/hardware/telemetry/EnergySnapshot.kt b/EVHardware/lib/src/main/java/com/evsuite/hardware/telemetry/EnergySnapshot.kt index 7fd5cab..c792730 100644 --- a/EVHardware/lib/src/main/java/com/evsuite/hardware/telemetry/EnergySnapshot.kt +++ b/EVHardware/lib/src/main/java/com/evsuite/hardware/telemetry/EnergySnapshot.kt @@ -50,6 +50,8 @@ data class EnergySnapshot( val regenLevelRaw: Int? = null, /** The gear's raw code (MG4 SWI68: 1 P, 2 R, 3 N, 4 D); null where the car did not answer. */ val gearRaw: Int? = null, + /** Cruise control target speed as reported (`0x2160A15B`), unit not established; null unread. */ + val cruiseTargetSpeedRaw: Float? = null, ) { val hasVehicleData: Boolean get() = socPercent != null || rangeKm != null || speedKmh != null || @@ -59,7 +61,7 @@ data class EnergySnapshot( vehicleConsumedKwh != null || vehicleRegeneratedKwh != null || parked != null || climate.hasData || tirePressures.hasData || widestWindowPercent != null || driveModeRaw != null || regenLevelRaw != null || - gearRaw != null + gearRaw != null || cruiseTargetSpeedRaw != null } data class TirePressureSnapshot( diff --git a/EVHardware/lib/src/main/java/com/evsuite/hardware/telemetry/EnergyTelemetryReader.kt b/EVHardware/lib/src/main/java/com/evsuite/hardware/telemetry/EnergyTelemetryReader.kt index 1ba47b7..0c70ba2 100644 --- a/EVHardware/lib/src/main/java/com/evsuite/hardware/telemetry/EnergyTelemetryReader.kt +++ b/EVHardware/lib/src/main/java/com/evsuite/hardware/telemetry/EnergyTelemetryReader.kt @@ -29,6 +29,8 @@ interface EnergySignalSource { fun parked(): Boolean? /** The gear's raw code (MG4 SWI68: 1 P, 2 R, 3 N, 4 D). Null where unread. */ fun gearRaw(): Int? = null + /** The cruise control target speed, raw (unit not established). Null where unread. */ + fun cruiseTargetSpeedRaw(): Float? = null fun batteryVoltageV(): Float? = null fun batteryCurrentA(): Float? = null /** Widest window opening in percent; null where the firmware has no read. */ @@ -77,6 +79,7 @@ class EvHardwareEnergySignalSource(context: Context) : EnergySignalSource { override fun vehicleRegeneratedKwh() = SaicCharging.regeneratedKwhSinceCharge() override fun parked() = EVHardware.isVehicleInPark() override fun gearRaw() = EVHardware.getGearRaw() + override fun cruiseTargetSpeedRaw() = EVHardware.getCruiseTargetSpeedRaw() override fun batteryVoltageV() = EVHardware.getVendorBatteryVoltageV() override fun batteryCurrentA() = EVHardware.getVendorBatteryCurrentA() override fun widestWindowPercent() = SaicVehicleControl.widestWindowPercent() @@ -158,6 +161,7 @@ class EnergyTelemetryReader(private val source: EnergySignalSource) { driveModeRaw = source.driveModeRaw(), regenLevelRaw = source.regenLevelRaw(), gearRaw = source.gearRaw(), + cruiseTargetSpeedRaw = source.cruiseTargetSpeedRaw(), ) /** Reads CP-004 candidates only when an unstable evidence capture explicitly requests them. */ diff --git a/LICENSE.md b/LICENSE.md index e3073fe..ff980ae 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -31,7 +31,8 @@ top. Those are Fatih's reads the car needs and upstream does not have — the ve ids `0x21401553`–`56`, the vendor mileage `0x21401566` for the odometer, the charging status from `0x2140F409`, SoC, range, power, pack voltage and current on SWI69 as on SWI68, and SWI69 speed already in km/h — plus translated comments and, from this fork, the gear's raw code -(`EVHardware.getGearRaw`, `EnergySnapshot.gearRaw`). To take upstream again, merge three ways with +(`EVHardware.getGearRaw`, `EnergySnapshot.gearRaw`) and the cruise control target speed +(`getCruiseTargetSpeedRaw`, `EnergySnapshot.cruiseTargetSpeedRaw`). To take upstream again, merge three ways with `e4e1158` as the base. The car app links EVHardware into its APK, so the **built app** may only be used and passed on diff --git a/SPECS.md b/SPECS.md index 0310958..33d993d 100644 --- a/SPECS.md +++ b/SPECS.md @@ -131,6 +131,16 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does * confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator` (Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked). +- 2026-10-24 — App 2.27.3 (branch `styles-retro-future`): first step to the cruise control (ACC) + set speed. The head unit's `CarAdvancedAssistedDrivingManager` names a vendor float + `AAD_EPTCCTRGTSPD` = `0x2160A15B`, by its name the powertrain's cruise control target speed; + nothing else in the decompiled SDK looks like a set speed (the other ACC properties are + objects ahead: distances, relative speeds). EVHardware reads it raw + (`getCruiseTargetSpeedRaw`, `EnergySnapshot.cruiseTargetSpeedRaw`; read-only), and the Logs + tab lists every change with time, speed and gear (`CruiseLog`, last 12). Unit and the value + while cruise control is off are unknown until read on the car; the dashboards show nothing of + it yet. Emulator: the property is absent there, one "unread" line, no crash. Not tried on the + car. - 2026-10-24 — App 2.27.2 (branch `styles-retro-future`): Terminal can be drawn in the accent colour too (text in its light shade, the command line in the accent, plain black; the low battery stays amber). Instead of separate "(accent colour)" entries, Retro and Terminal are one diff --git a/app/build.gradle b/app/build.gradle index 415f3d0..e229fd2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,8 +34,8 @@ android { applicationId "com.stinnesbeck.nilsmg4" minSdk 27 targetSdk 36 - versionCode 123 - versionName "2.27.2" + versionCode 124 + versionName "2.27.3" // Nothing about the server ships in the APK: its URL and ingest token are entered in // the app's Server tab (ServerConfig), and the database is only reachable by the server. diff --git a/app/src/main/java/com/evsuite/abrp/CruiseLog.java b/app/src/main/java/com/evsuite/abrp/CruiseLog.java new file mode 100644 index 0000000..2108189 --- /dev/null +++ b/app/src/main/java/com/evsuite/abrp/CruiseLog.java @@ -0,0 +1,69 @@ +package com.evsuite.abrp; + +import android.content.Context; +import android.content.SharedPreferences; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +/** + * Every change of the cruise control target speed the car reports (`0x2160A15B`, raw, unit not + * established) with the time, speed and gear, the last {@link #MAX_LINES}, shown on the Logs + * tab. It is how the value gets matched to what the cluster shows: set the cruise control, change + * the speed, switch it off, and read what the car reported. Kept in preferences; a line is + * written only when the value (to 0.1) changes. + */ +final class CruiseLog { + + static final int MAX_LINES = 12; + private static final String PREFS = "cruise_log"; + private static final String KEY = "lines"; + + /** The last value seen in this process, as text ("unread" when null); null before the first. */ + private static String last; + + private CruiseLog() {} + + /** The value as the log writes it: one decimal, or "unread". */ + static String format(Float raw) { + return raw == null || !Float.isFinite(raw) ? "unread" : String.format(Locale.US, "%.1f", raw); + } + + /** Called with every reading; records a line when the value differs from the last one. */ + static void note(Context context, Float raw, Float speedKmh, Integer gear) { + String value = format(raw); + synchronized (CruiseLog.class) { + if (context == null || value.equals(last)) return; + last = value; + String line = new SimpleDateFormat("dd.MM. HH:mm:ss", Locale.US).format(new Date()) + + " target " + value + + " at " + (speedKmh == null ? "—" : String.format(Locale.US, "%.0f km/h", speedKmh)) + + " gear " + (Gear.label(gear).isEmpty() ? "?" : Gear.label(gear)); + SharedPreferences prefs = context.getApplicationContext() + .getSharedPreferences(PREFS, Context.MODE_PRIVATE); + List lines = UpdateLog.append(split(prefs.getString(KEY, null)), line, MAX_LINES); + prefs.edit().putString(KEY, String.join("\n", lines)).apply(); + } + } + + /** The value now as text, "—" before the first reading. */ + static synchronized String current() { + return last == null ? "—" : last; + } + + /** Newest first. */ + static List lines(Context context) { + SharedPreferences prefs = context.getApplicationContext() + .getSharedPreferences(PREFS, Context.MODE_PRIVATE); + return split(prefs.getString(KEY, null)); + } + + private static List split(String stored) { + if (stored == null || stored.isEmpty()) return new ArrayList<>(); + return new ArrayList<>(Arrays.asList(stored.split("\n"))); + } +} diff --git a/app/src/main/java/com/evsuite/abrp/MainActivity.java b/app/src/main/java/com/evsuite/abrp/MainActivity.java index b070545..3e1ec78 100644 --- a/app/src/main/java/com/evsuite/abrp/MainActivity.java +++ b/app/src/main/java/com/evsuite/abrp/MainActivity.java @@ -1731,6 +1731,7 @@ public class MainActivity extends AppCompatActivity { vehTireFr.setText(unit.format(s.getTirePressures().getFrontRightKpa())); vehTireRl.setText(unit.format(s.getTirePressures().getRearLeftKpa())); vehTireRr.setText(unit.format(s.getTirePressures().getRearRightKpa())); + CruiseLog.note(this, s.getCruiseTargetSpeedRaw(), s.getSpeedKmh(), s.getGearRaw()); showGauges(s); showCockpit(s); showDrawnStyles(s); @@ -2056,6 +2057,13 @@ public class MainActivity extends AppCompatActivity { // Update steps first: after a failed install, they are what the driver came here for. java.util.List updates = UpdateLog.lines(this); StringBuilder sb = new StringBuilder(); + // The cruise control target speed as the car reports it, to match it on the car. + java.util.List cruise = CruiseLog.lines(this); + if (!cruise.isEmpty()) { + sb.append(getString(R.string.cruise_log_title, CruiseLog.current())); + for (String line : cruise) sb.append('\n').append(line); + sb.append("\n\n"); + } if (!updates.isEmpty()) { sb.append(getString(R.string.update_log_title)); for (String line : updates) sb.append('\n').append(line); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7738ab2..8b33004 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -198,6 +198,7 @@ Recent uploads No upload attempted yet. Updates, newest first: + Cruise control target speed, newest first (now: %1$s; raw, unit not known yet): Upload now Starting… Running — waiting for first upload diff --git a/app/src/test/java/com/evsuite/abrp/CruiseLogTest.java b/app/src/test/java/com/evsuite/abrp/CruiseLogTest.java new file mode 100644 index 0000000..9bf32fe --- /dev/null +++ b/app/src/test/java/com/evsuite/abrp/CruiseLogTest.java @@ -0,0 +1,18 @@ +package com.evsuite.abrp; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** The cruise control log's value format — pure JVM. */ +public class CruiseLogTest { + + @Test + public void oneDecimalOrUnread() { + assertEquals("120.0", CruiseLog.format(120f)); + assertEquals("33.3", CruiseLog.format(33.33f)); + assertEquals("0.0", CruiseLog.format(0f)); + assertEquals("unread", CruiseLog.format(null)); + assertEquals("unread", CruiseLog.format(Float.NaN)); + } +}