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:
2026-10-24 21:58:12 +02:00
co-authored by Claude Opus 5.5
parent 1c85fd2d9f
commit 965a20cfb2
10 changed files with 128 additions and 4 deletions
@@ -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 }
@@ -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(
@@ -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. */