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:
@@ -2115,7 +2115,14 @@ object EVHardware {
|
|||||||
private val sGearLog = StateChangeLog<Int>()
|
private val sGearLog = StateChangeLog<Int>()
|
||||||
private val sIgnitionLog = 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 gen = FirmwareInfo.getGeneration()
|
||||||
val gear = when {
|
val gear = when {
|
||||||
gen == FirmwareInfo.Gen.SWI133 ->
|
gen == FirmwareInfo.Gen.SWI133 ->
|
||||||
@@ -2132,7 +2139,7 @@ object EVHardware {
|
|||||||
if (sGearLog.accept(gear)) {
|
if (sGearLog.accept(gear)) {
|
||||||
AppLogger.i(TAG, "isVehicleInPark — gen=$gen gear=$gear (P=$GEAR_PARK_VALUE)")
|
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) ───────────
|
// ── Read gear SWI68/165 (poll direct) + A9 (CarStateClient) ───────────
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ data class EnergySnapshot(
|
|||||||
*/
|
*/
|
||||||
val driveModeRaw: Int? = null,
|
val driveModeRaw: Int? = null,
|
||||||
val regenLevelRaw: 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
|
val hasVehicleData: Boolean
|
||||||
get() = socPercent != null || rangeKm != null || speedKmh != null ||
|
get() = socPercent != null || rangeKm != null || speedKmh != null ||
|
||||||
@@ -56,7 +58,8 @@ data class EnergySnapshot(
|
|||||||
odometerKm != null || chargePortConnected != null || chargingStatus != null ||
|
odometerKm != null || chargePortConnected != null || chargingStatus != null ||
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
data class TirePressureSnapshot(
|
data class TirePressureSnapshot(
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ interface EnergySignalSource {
|
|||||||
fun vehicleConsumedKwh(): Float? = null
|
fun vehicleConsumedKwh(): Float? = null
|
||||||
fun vehicleRegeneratedKwh(): Float? = null
|
fun vehicleRegeneratedKwh(): Float? = null
|
||||||
fun parked(): Boolean?
|
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 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. */
|
||||||
@@ -74,6 +76,7 @@ class EvHardwareEnergySignalSource(context: Context) : EnergySignalSource {
|
|||||||
override fun vehicleConsumedKwh() = SaicCharging.consumedKwhSinceCharge()
|
override fun vehicleConsumedKwh() = SaicCharging.consumedKwhSinceCharge()
|
||||||
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 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()
|
||||||
@@ -154,6 +157,7 @@ class EnergyTelemetryReader(private val source: EnergySignalSource) {
|
|||||||
widestWindowPercent = source.widestWindowPercent()?.takeIf { it in 0..100 },
|
widestWindowPercent = source.widestWindowPercent()?.takeIf { it in 0..100 },
|
||||||
driveModeRaw = source.driveModeRaw(),
|
driveModeRaw = source.driveModeRaw(),
|
||||||
regenLevelRaw = source.regenLevelRaw(),
|
regenLevelRaw = source.regenLevelRaw(),
|
||||||
|
gearRaw = source.gearRaw(),
|
||||||
)
|
)
|
||||||
|
|
||||||
/** 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
@@ -30,7 +30,8 @@ It is a copy, not a submodule: [malys/EVHardware](https://github.com/malys/EVHar
|
|||||||
top. Those are Fatih's reads the car needs and upstream does not have — the vendor tyre-pressure
|
top. Those are Fatih's reads the car needs and upstream does not have — the vendor tyre-pressure
|
||||||
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. To take upstream again, merge three ways with
|
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
|
||||||
`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,15 @@ 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.22.1 (branch `dashboard-cockpit`): the gear badge on the Dashboard and
|
||||||
|
Cockpit no longer guesses. It showed "D" for anything not in P, so N and R read as D. The app
|
||||||
|
only knew "in P or not"; EVHardware now passes on the car's raw gear code
|
||||||
|
(`EVHardware.getGearRaw`, `EnergySnapshot.gearRaw`; `isVehicleInPark` is built on it).
|
||||||
|
`Gear.label`: 1 → "P" (the only established code), any other code → no letter, unread → "—".
|
||||||
|
To map R, N and D, the Logs tab lists every change of the code with time and speed
|
||||||
|
(`GearLog`, last 12, kept in preferences). Checked on the emulator with a throwaway build
|
||||||
|
cycling codes 1–4 (pill shows P on 1, nothing on 2–4; the Logs tab lists each change). Not
|
||||||
|
tried on the car; the codes for R, N and D are still to be read there.
|
||||||
- 2026-10-24 — App 2.22.0 (branch `dashboard-cockpit`, from `dashboard-gauges`): a third Overview
|
- 2026-10-24 — App 2.22.0 (branch `dashboard-cockpit`, from `dashboard-gauges`): a third Overview
|
||||||
style, "Cockpit", designed without a reference: three segmented rings (`RingView`, Canvas) —
|
style, "Cockpit", designed without a reference: three segmented rings (`RingView`, Canvas) —
|
||||||
battery (0…100 %, range inside, below 20 % in the warning colour), power with the speed and the
|
battery (0…100 %, range inside, below 20 % in the warning colour), power with the speed and the
|
||||||
|
|||||||
+2
-2
@@ -34,8 +34,8 @@ android {
|
|||||||
applicationId "com.stinnesbeck.nilsmg4"
|
applicationId "com.stinnesbeck.nilsmg4"
|
||||||
minSdk 27
|
minSdk 27
|
||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode 111
|
versionCode 112
|
||||||
versionName "2.22.0"
|
versionName "2.22.1"
|
||||||
|
|
||||||
// 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,23 @@
|
|||||||
|
package com.evsuite.abrp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The gear letter for the dashboards, from the car's raw gear code.
|
||||||
|
*
|
||||||
|
* Only code 1 is known to be P (EVHardware, two head-unit apps and the reversing camera
|
||||||
|
* agree). The codes for R, N and D are not established yet, so any other code shows no letter
|
||||||
|
* rather than a guessed "D": the Logs tab lists the codes the car reports, for the driver to
|
||||||
|
* match them to the lever. Unread is "—".
|
||||||
|
*/
|
||||||
|
final class Gear {
|
||||||
|
|
||||||
|
static final int PARK = 1;
|
||||||
|
|
||||||
|
private Gear() {}
|
||||||
|
|
||||||
|
/** "P", "" for a gear not yet named, "—" when unread. */
|
||||||
|
static String label(Integer raw) {
|
||||||
|
if (raw == null) return "—";
|
||||||
|
if (raw == PARK) return "P";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
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 car's raw gear code with the time and speed, the last {@link #MAX_LINES},
|
||||||
|
* shown on the Logs tab. It is how the codes for R, N and D get matched to the lever: shift
|
||||||
|
* through them and read which code each one gave. Kept in preferences so the list survives
|
||||||
|
* the app being closed in between. Written only when the code changes.
|
||||||
|
*/
|
||||||
|
final class GearLog {
|
||||||
|
|
||||||
|
static final int MAX_LINES = 12;
|
||||||
|
private static final String PREFS = "gear_log";
|
||||||
|
private static final String KEY = "lines";
|
||||||
|
|
||||||
|
/** The last code seen in this process; MIN_VALUE before the first reading. */
|
||||||
|
private static int last = Integer.MIN_VALUE;
|
||||||
|
|
||||||
|
private GearLog() {}
|
||||||
|
|
||||||
|
/** Called with every reading; records a line when the code differs from the last one. */
|
||||||
|
static void note(Context context, Integer raw, Float speedKmh) {
|
||||||
|
int code = raw == null ? -1 : raw;
|
||||||
|
synchronized (GearLog.class) {
|
||||||
|
if (context == null || code == last) return;
|
||||||
|
last = code;
|
||||||
|
String line = new SimpleDateFormat("dd.MM. HH:mm:ss", Locale.US).format(new Date())
|
||||||
|
+ " code " + (raw == null ? "unread" : String.valueOf(raw))
|
||||||
|
+ (raw != null && raw == Gear.PARK ? " (P)" : "")
|
||||||
|
+ " at " + (speedKmh == null ? "—" : String.format(Locale.US, "%.0f km/h", speedKmh));
|
||||||
|
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 code now, null when unread or not read yet. */
|
||||||
|
static synchronized Integer current() {
|
||||||
|
return last < 0 ? 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")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1519,8 +1519,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
Float power = s.getBatteryPowerKw();
|
Float power = s.getBatteryPowerKw();
|
||||||
Float speed = s.getSpeedKmh();
|
Float speed = s.getSpeedKmh();
|
||||||
Boolean parked = s.getParked();
|
cockpitPower.setValue(power, Gear.label(s.getGearRaw()),
|
||||||
cockpitPower.setValue(power, parked == null ? "—" : parked ? "P" : "D",
|
|
||||||
speed == null ? null : String.format(us, "%.0f", speed), getString(R.string.dash_unit_kmh),
|
speed == null ? null : String.format(us, "%.0f", speed), getString(R.string.dash_unit_kmh),
|
||||||
power == null ? "—" : String.format(us, "%.1f kW", power), null);
|
power == null ? "—" : String.format(us, "%.1f kW", power), null);
|
||||||
cockpitTrace.add(android.os.SystemClock.elapsedRealtime(), power);
|
cockpitTrace.add(android.os.SystemClock.elapsedRealtime(), power);
|
||||||
@@ -1554,8 +1553,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
fmt(s.getRangeKm(), "%.0f km"));
|
fmt(s.getRangeKm(), "%.0f km"));
|
||||||
dashBatteryBar.setSoc(soc);
|
dashBatteryBar.setSoc(soc);
|
||||||
dashBatteryText.setText(fmt(soc, "%.0f %%"));
|
dashBatteryText.setText(fmt(soc, "%.0f %%"));
|
||||||
Boolean parked = s.getParked();
|
dashGear.setText(Gear.label(s.getGearRaw()));
|
||||||
dashGear.setText(parked == null ? "—" : parked ? "P" : "D");
|
|
||||||
dashClimate.setText(fmt(s.getClimate().getDriverTargetCelsius(), "%.0f °C"));
|
dashClimate.setText(fmt(s.getClimate().getDriverTargetCelsius(), "%.0f °C"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1577,6 +1575,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()));
|
||||||
|
GearLog.note(this, s.getGearRaw(), s.getSpeedKmh());
|
||||||
showDashboard(s);
|
showDashboard(s);
|
||||||
showCockpit(s);
|
showCockpit(s);
|
||||||
} catch (Throwable ignored) {
|
} catch (Throwable ignored) {
|
||||||
@@ -1901,6 +1900,14 @@ 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 car's gear codes, for matching R, N and D to the lever (only P = 1 is known).
|
||||||
|
java.util.List<String> gears = GearLog.lines(this);
|
||||||
|
if (!gears.isEmpty()) {
|
||||||
|
Integer now = GearLog.current();
|
||||||
|
sb.append(getString(R.string.gear_log_title, now == null ? "—" : String.valueOf(now)));
|
||||||
|
for (String line : gears) 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);
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ public final class RingView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inside: the label on top in a pill, the value, the unit, the captions.
|
// Inside: the label on top in a pill, the value, the unit, the captions.
|
||||||
if (top != null) {
|
if (top != null && !top.isEmpty()) {
|
||||||
topPaint.setTextSize(r * (topPill ? 0.11f : 0.075f));
|
topPaint.setTextSize(r * (topPill ? 0.11f : 0.075f));
|
||||||
topPaint.setLetterSpacing(topPill ? 0f : 0.16f);
|
topPaint.setLetterSpacing(topPill ? 0f : 0.16f);
|
||||||
float ty = cy - r * 0.40f;
|
float ty = cy - r * 0.40f;
|
||||||
|
|||||||
@@ -187,6 +187,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="gear_log_title">Gear codes, newest first (now: %1$s; only 1 = P is known):</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,22 @@
|
|||||||
|
package com.evsuite.abrp;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
/** The gear letter — pure JVM. */
|
||||||
|
public class GearTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void onlyParkIsNamed() {
|
||||||
|
assertEquals("P", Gear.label(1));
|
||||||
|
assertEquals("", Gear.label(2));
|
||||||
|
assertEquals("", Gear.label(3));
|
||||||
|
assertEquals("", Gear.label(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unreadIsADash() {
|
||||||
|
assertEquals("—", Gear.label(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user