App 2.22.1: gear badge shows only P; Logs list the raw gear codes

The badge said "D" for every gear that was not P, so N and R read as D.
EVHardware now passes on the raw gear code; the badge shows P for 1,
no letter for codes not yet mapped, "—" when unread. The Logs tab lists
each change of the code with time and speed, to match R, N and D on
the car. Emulator only.

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 16:59:44 +02:00
co-authored by Claude Opus 5.5
parent cd0dfcd612
commit 16cc2746ef
12 changed files with 151 additions and 11 deletions
@@ -2115,7 +2115,14 @@ object EVHardware {
private val sGearLog = StateChangeLog<Int>()
private val sIgnitionLog = StateChangeLog<Int>()
fun isVehicleInPark(): Boolean? {
fun isVehicleInPark(): Boolean? = getGearRaw()?.let { it == GEAR_PARK_VALUE }
/**
* The gear as the car's own code, null if unreadable. Only P = 1 is established (see
* [isVehicleInPark]); the other codes are passed on unnamed so the app can show them and
* the driver can match them to R, N and D.
*/
fun getGearRaw(): Int? {
val gen = FirmwareInfo.getGeneration()
val gear = when {
gen == FirmwareInfo.Gen.SWI133 ->
@@ -2132,7 +2139,7 @@ object EVHardware {
if (sGearLog.accept(gear)) {
AppLogger.i(TAG, "isVehicleInPark — gen=$gen gear=$gear (P=$GEAR_PARK_VALUE)")
}
return gear == GEAR_PARK_VALUE
return gear
}
// ── Read gear SWI68/165 (poll direct) + A9 (CarStateClient) ───────────
@@ -48,6 +48,8 @@ data class EnergySnapshot(
*/
val driveModeRaw: Int? = null,
val regenLevelRaw: Int? = null,
/** The gear's raw code, unmapped beyond P = 1; null where the car did not answer. */
val gearRaw: Int? = null,
) {
val hasVehicleData: Boolean
get() = socPercent != null || rangeKm != null || speedKmh != null ||
@@ -56,7 +58,8 @@ data class EnergySnapshot(
odometerKm != null || chargePortConnected != null || chargingStatus != null ||
vehicleConsumedKwh != null || vehicleRegeneratedKwh != null ||
parked != null || climate.hasData || tirePressures.hasData ||
widestWindowPercent != null || driveModeRaw != null || regenLevelRaw != null
widestWindowPercent != null || driveModeRaw != null || regenLevelRaw != null ||
gearRaw != null
}
data class TirePressureSnapshot(
@@ -27,6 +27,8 @@ interface EnergySignalSource {
fun vehicleConsumedKwh(): Float? = null
fun vehicleRegeneratedKwh(): Float? = null
fun parked(): Boolean?
/** The gear's raw code; only 1 (P) is named. Null where unread. */
fun gearRaw(): Int? = null
fun batteryVoltageV(): Float? = null
fun batteryCurrentA(): Float? = null
/** Widest window opening in percent; null where the firmware has no read. */
@@ -74,6 +76,7 @@ class EvHardwareEnergySignalSource(context: Context) : EnergySignalSource {
override fun vehicleConsumedKwh() = SaicCharging.consumedKwhSinceCharge()
override fun vehicleRegeneratedKwh() = SaicCharging.regeneratedKwhSinceCharge()
override fun parked() = EVHardware.isVehicleInPark()
override fun gearRaw() = EVHardware.getGearRaw()
override fun batteryVoltageV() = EVHardware.getVendorBatteryVoltageV()
override fun batteryCurrentA() = EVHardware.getVendorBatteryCurrentA()
override fun widestWindowPercent() = SaicVehicleControl.widestWindowPercent()
@@ -154,6 +157,7 @@ class EnergyTelemetryReader(private val source: EnergySignalSource) {
widestWindowPercent = source.widestWindowPercent()?.takeIf { it in 0..100 },
driveModeRaw = source.driveModeRaw(),
regenLevelRaw = source.regenLevelRaw(),
gearRaw = source.gearRaw(),
)
/** Reads CP-004 candidates only when an unstable evidence capture explicitly requests them. */