App 2.27.3: read the cruise control target speed, log it raw
The head unit names a vendor float AAD_EPTCCTRGTSPD (0x2160A15B), the cruise control target speed by its name. EVHardware reads it raw; the Logs tab lists each change with time, speed and gear, to establish the unit and the off value on the car. Emulator only (absent there). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
@@ -427,6 +427,12 @@ object EVHardware {
|
|||||||
private const val PROP_CCU_ONBD_PLUG = 0x2140F43E // vendor int, AC socket
|
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_CCU_OFFBD_PLUG = 0x2140F43F // vendor int, DC socket
|
||||||
private const val PROP_ENERGY_SINCE_CHARGE = 0x2160A1BB // vendor float kWh
|
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.
|
* `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.
|
* answers, and this is what is left on a unit where it does not.
|
||||||
* See [PROP_BMS_CHRG_STS] for the values.
|
* 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? =
|
fun getVendorChargeStatus(): Int? =
|
||||||
getIntPropertyCPM(PROP_BMS_CHRG_STS, AREA_GLOBAL).takeIf { it >= 0 }
|
getIntPropertyCPM(PROP_BMS_CHRG_STS, AREA_GLOBAL).takeIf { it >= 0 }
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ data class EnergySnapshot(
|
|||||||
val regenLevelRaw: Int? = null,
|
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. */
|
/** 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,
|
val gearRaw: Int? = null,
|
||||||
|
/** Cruise control target speed as reported (`0x2160A15B`), unit not established; null unread. */
|
||||||
|
val cruiseTargetSpeedRaw: Float? = null,
|
||||||
) {
|
) {
|
||||||
val hasVehicleData: Boolean
|
val hasVehicleData: Boolean
|
||||||
get() = socPercent != null || rangeKm != null || speedKmh != null ||
|
get() = socPercent != null || rangeKm != null || speedKmh != null ||
|
||||||
@@ -59,7 +61,7 @@ data class EnergySnapshot(
|
|||||||
vehicleConsumedKwh != null || vehicleRegeneratedKwh != null ||
|
vehicleConsumedKwh != null || vehicleRegeneratedKwh != null ||
|
||||||
parked != null || climate.hasData || tirePressures.hasData ||
|
parked != null || climate.hasData || tirePressures.hasData ||
|
||||||
widestWindowPercent != null || driveModeRaw != null || regenLevelRaw != null ||
|
widestWindowPercent != null || driveModeRaw != null || regenLevelRaw != null ||
|
||||||
gearRaw != null
|
gearRaw != null || cruiseTargetSpeedRaw != null
|
||||||
}
|
}
|
||||||
|
|
||||||
data class TirePressureSnapshot(
|
data class TirePressureSnapshot(
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ interface EnergySignalSource {
|
|||||||
fun parked(): Boolean?
|
fun parked(): Boolean?
|
||||||
/** The gear's raw code (MG4 SWI68: 1 P, 2 R, 3 N, 4 D). Null where unread. */
|
/** The gear's raw code (MG4 SWI68: 1 P, 2 R, 3 N, 4 D). Null where unread. */
|
||||||
fun gearRaw(): Int? = null
|
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 batteryVoltageV(): Float? = null
|
||||||
fun batteryCurrentA(): Float? = null
|
fun batteryCurrentA(): Float? = null
|
||||||
/** Widest window opening in percent; null where the firmware has no read. */
|
/** 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 vehicleRegeneratedKwh() = SaicCharging.regeneratedKwhSinceCharge()
|
||||||
override fun parked() = EVHardware.isVehicleInPark()
|
override fun parked() = EVHardware.isVehicleInPark()
|
||||||
override fun gearRaw() = EVHardware.getGearRaw()
|
override fun gearRaw() = EVHardware.getGearRaw()
|
||||||
|
override fun cruiseTargetSpeedRaw() = EVHardware.getCruiseTargetSpeedRaw()
|
||||||
override fun batteryVoltageV() = EVHardware.getVendorBatteryVoltageV()
|
override fun batteryVoltageV() = EVHardware.getVendorBatteryVoltageV()
|
||||||
override fun batteryCurrentA() = EVHardware.getVendorBatteryCurrentA()
|
override fun batteryCurrentA() = EVHardware.getVendorBatteryCurrentA()
|
||||||
override fun widestWindowPercent() = SaicVehicleControl.widestWindowPercent()
|
override fun widestWindowPercent() = SaicVehicleControl.widestWindowPercent()
|
||||||
@@ -158,6 +161,7 @@ class EnergyTelemetryReader(private val source: EnergySignalSource) {
|
|||||||
driveModeRaw = source.driveModeRaw(),
|
driveModeRaw = source.driveModeRaw(),
|
||||||
regenLevelRaw = source.regenLevelRaw(),
|
regenLevelRaw = source.regenLevelRaw(),
|
||||||
gearRaw = source.gearRaw(),
|
gearRaw = source.gearRaw(),
|
||||||
|
cruiseTargetSpeedRaw = source.cruiseTargetSpeedRaw(),
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Reads CP-004 candidates only when an unstable evidence capture explicitly requests them. */
|
/** Reads CP-004 candidates only when an unstable evidence capture explicitly requests them. */
|
||||||
|
|||||||
+2
-1
@@ -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
|
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
|
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
|
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.
|
`e4e1158` as the base.
|
||||||
|
|
||||||
The car app links EVHardware into its APK, so the **built app** may only be used and passed on
|
The car app links EVHardware into its APK, so the **built app** may only be used and passed on
|
||||||
|
|||||||
@@ -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
|
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`
|
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).
|
(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
|
- 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
|
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
|
battery stays amber). Instead of separate "(accent colour)" entries, Retro and Terminal are one
|
||||||
|
|||||||
+2
-2
@@ -34,8 +34,8 @@ android {
|
|||||||
applicationId "com.stinnesbeck.nilsmg4"
|
applicationId "com.stinnesbeck.nilsmg4"
|
||||||
minSdk 27
|
minSdk 27
|
||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode 123
|
versionCode 124
|
||||||
versionName "2.27.2"
|
versionName "2.27.3"
|
||||||
|
|
||||||
// Nothing about the server ships in the APK: its URL and ingest token are entered in
|
// 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.
|
// the app's Server tab (ServerConfig), and the database is only reachable by the server.
|
||||||
|
|||||||
@@ -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<String> 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<String> lines(Context context) {
|
||||||
|
SharedPreferences prefs = context.getApplicationContext()
|
||||||
|
.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
|
||||||
|
return split(prefs.getString(KEY, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> split(String stored) {
|
||||||
|
if (stored == null || stored.isEmpty()) return new ArrayList<>();
|
||||||
|
return new ArrayList<>(Arrays.asList(stored.split("\n")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1731,6 +1731,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
vehTireFr.setText(unit.format(s.getTirePressures().getFrontRightKpa()));
|
vehTireFr.setText(unit.format(s.getTirePressures().getFrontRightKpa()));
|
||||||
vehTireRl.setText(unit.format(s.getTirePressures().getRearLeftKpa()));
|
vehTireRl.setText(unit.format(s.getTirePressures().getRearLeftKpa()));
|
||||||
vehTireRr.setText(unit.format(s.getTirePressures().getRearRightKpa()));
|
vehTireRr.setText(unit.format(s.getTirePressures().getRearRightKpa()));
|
||||||
|
CruiseLog.note(this, s.getCruiseTargetSpeedRaw(), s.getSpeedKmh(), s.getGearRaw());
|
||||||
showGauges(s);
|
showGauges(s);
|
||||||
showCockpit(s);
|
showCockpit(s);
|
||||||
showDrawnStyles(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.
|
// Update steps first: after a failed install, they are what the driver came here for.
|
||||||
java.util.List<String> updates = UpdateLog.lines(this);
|
java.util.List<String> updates = UpdateLog.lines(this);
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
// The cruise control target speed as the car reports it, to match it on the car.
|
||||||
|
java.util.List<String> 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()) {
|
if (!updates.isEmpty()) {
|
||||||
sb.append(getString(R.string.update_log_title));
|
sb.append(getString(R.string.update_log_title));
|
||||||
for (String line : updates) sb.append('\n').append(line);
|
for (String line : updates) sb.append('\n').append(line);
|
||||||
|
|||||||
@@ -198,6 +198,7 @@
|
|||||||
<string name="label_call_log">Recent uploads</string>
|
<string name="label_call_log">Recent uploads</string>
|
||||||
<string name="call_log_empty">No upload attempted yet.</string>
|
<string name="call_log_empty">No upload attempted yet.</string>
|
||||||
<string name="update_log_title">Updates, newest first:</string>
|
<string name="update_log_title">Updates, newest first:</string>
|
||||||
|
<string name="cruise_log_title">Cruise control target speed, newest first (now: %1$s; raw, unit not known yet):</string>
|
||||||
<string name="btn_flush_now">Upload now</string>
|
<string name="btn_flush_now">Upload now</string>
|
||||||
<string name="status_starting">Starting…</string>
|
<string name="status_starting">Starting…</string>
|
||||||
<string name="status_running_no_upload">Running — waiting for first upload</string>
|
<string name="status_running_no_upload">Running — waiting for first upload</string>
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user