From c6643e7df2cb610cd059152406f5c1c3d5cfe47e Mon Sep 17 00:00:00 2001 From: Nils Stinnesbeck Date: Sat, 24 Oct 2026 19:10:03 +0200 Subject: [PATCH] App 2.22.2: gear badge shows P, R, N and D MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codes read on the owner's car with 2.22.1: 1 P, 2 R, 3 N, 4 D. Gear maps them (unit-tested); other codes show no letter, unread "—". The temporary gear list on the Logs tab is removed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6 --- AGENTS.md | 1 + .../java/com/evsuite/hardware/EVHardware.kt | 6 +- .../com/evsuite/hardware/saic/SaicVehicle.kt | 3 +- .../hardware/telemetry/EnergySnapshot.kt | 2 +- .../telemetry/EnergyTelemetryReader.kt | 2 +- SPECS.md | 6 ++ app/build.gradle | 4 +- app/src/main/java/com/evsuite/abrp/Gear.java | 15 ++--- .../main/java/com/evsuite/abrp/GearLog.java | 63 ------------------- .../java/com/evsuite/abrp/MainActivity.java | 9 --- app/src/main/res/values/strings.xml | 1 - .../test/java/com/evsuite/abrp/GearTest.java | 15 +++-- 12 files changed, 33 insertions(+), 94 deletions(-) delete mode 100644 app/src/main/java/com/evsuite/abrp/GearLog.java diff --git a/AGENTS.md b/AGENTS.md index 05adf13..64085f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,6 +53,7 @@ Repository: `ssh://git@git.stinnesbeck.com:2222/nils/mg4.git` (remote `origin`), - Display 1920×720 px at **160 dpi**; the app window is **1778×720 px** (the car's own sidebar), about **1778×640 dp** for layouts. Pages must fit that without scrolling where possible. - Signed with the public **AOSP test platform key**; the car build runs as `android.uid.system`. +- Gear codes (`EVHardware.getGearRaw`): 1 P, 2 R, 3 N, 4 D (read off the lever). - Steering-wheel keys arrive as the broadcast `com.saic.keyevent.hardkey.report` (`android.intent.extra.hardkey.keycode` / `.down` / `.longpress`); the voice key is **287**. - Not published by the car: battery, cabin and outside temperature, pack energy and capacity, 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 9e2c1c9..90733a5 100644 --- a/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt +++ b/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt @@ -2118,9 +2118,9 @@ object EVHardware { 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. + * The gear as the car's own code, null if unreadable. P = 1 on every studied firmware (see + * [isVehicleInPark]); on an MG4 with SWI68 (SWI68-29958-1300R69) the owner read 1 P, 2 R, + * 3 N, 4 D by shifting through the lever. */ fun getGearRaw(): Int? { val gen = FirmwareInfo.getGeneration() diff --git a/EVHardware/lib/src/main/java/com/evsuite/hardware/saic/SaicVehicle.kt b/EVHardware/lib/src/main/java/com/evsuite/hardware/saic/SaicVehicle.kt index 708c20e..525ec1d 100644 --- a/EVHardware/lib/src/main/java/com/evsuite/hardware/saic/SaicVehicle.kt +++ b/EVHardware/lib/src/main/java/com/evsuite/hardware/saic/SaicVehicle.kt @@ -585,7 +585,8 @@ object SaicVehicleControl { * own `GEAR_PARK_VALUE`, two head-unit apps using `carGear == 1` as the condition for playing * video on the centre screen (`UsbVideoView`, `VideoListFragment`), and * `IBackUpBinder.isSecureForBackUp`, which accepts gears 1 and 3 alongside `speed == 0`. - * Nothing pins 2, 3 and 4, so nothing here names them. + * Nothing in the code pins 2, 3 and 4; on an MG4 with SWI68-29958-1300R69 the owner read them + * off the lever as 2 R, 3 N, 4 D (the app's `Gear` names them; this layer passes codes on). * * `getCarSpeed` is on this interface too and is deliberately unused: the binder returns a * float with no unit in any source, and a speed read in the wrong unit would open the write 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 dc174f0..7fd5cab 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 @@ -48,7 +48,7 @@ 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. */ + /** 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 hasVehicleData: Boolean 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 66e14a8..1ba47b7 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 @@ -27,7 +27,7 @@ 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. */ + /** The gear's raw code (MG4 SWI68: 1 P, 2 R, 3 N, 4 D). Null where unread. */ fun gearRaw(): Int? = null fun batteryVoltageV(): Float? = null fun batteryCurrentA(): Float? = null diff --git a/SPECS.md b/SPECS.md index a22d611..a3a008f 100644 --- a/SPECS.md +++ b/SPECS.md @@ -131,6 +131,12 @@ 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.22.2 (branch `dashboard-cockpit`): the gear badge names all four gears. + The owner shifted through the lever with 2.22.1 and read the codes off the Logs tab: 1 P, + 2 R, 3 N, 4 D (firmware SWI68-29958-1300R69). `Gear.label` maps them (unit-tested); any + other code shows no letter, unread "—". The gear list on the Logs tab, which existed only for + this, is gone again. Codes recorded in AGENTS.md and EVHardware's comments. Unit tests only; + the mapping itself comes from the car. - 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 diff --git a/app/build.gradle b/app/build.gradle index 40e08f0..95635f8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,8 +34,8 @@ android { applicationId "com.stinnesbeck.nilsmg4" minSdk 27 targetSdk 36 - versionCode 112 - versionName "2.22.1" + versionCode 113 + versionName "2.22.2" // 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/Gear.java b/app/src/main/java/com/evsuite/abrp/Gear.java index 9ab04ab..120783a 100644 --- a/app/src/main/java/com/evsuite/abrp/Gear.java +++ b/app/src/main/java/com/evsuite/abrp/Gear.java @@ -1,23 +1,20 @@ 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 "—". + * The gear letter for the dashboards, from the car's raw gear code: 1 P, 2 R, 3 N, 4 D, read + * on the owner's car (firmware SWI68-29958-1300R69) by shifting through the lever. Any other + * code shows no letter rather than a guess; unread is "—". */ final class Gear { - static final int PARK = 1; + private static final String[] LETTERS = { null, "P", "R", "N", "D" }; private Gear() {} - /** "P", "" for a gear not yet named, "—" when unread. */ + /** "P", "R", "N" or "D"; "" for an unknown code, "—" when unread. */ static String label(Integer raw) { if (raw == null) return "—"; - if (raw == PARK) return "P"; + if (raw > 0 && raw < LETTERS.length) return LETTERS[raw]; return ""; } } diff --git a/app/src/main/java/com/evsuite/abrp/GearLog.java b/app/src/main/java/com/evsuite/abrp/GearLog.java deleted file mode 100644 index f2fd972..0000000 --- a/app/src/main/java/com/evsuite/abrp/GearLog.java +++ /dev/null @@ -1,63 +0,0 @@ -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 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 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 a7bd38b..4239d5f 100644 --- a/app/src/main/java/com/evsuite/abrp/MainActivity.java +++ b/app/src/main/java/com/evsuite/abrp/MainActivity.java @@ -1575,7 +1575,6 @@ 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())); - GearLog.note(this, s.getGearRaw(), s.getSpeedKmh()); showDashboard(s); showCockpit(s); } catch (Throwable ignored) { @@ -1900,14 +1899,6 @@ 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 car's gear codes, for matching R, N and D to the lever (only P = 1 is known). - java.util.List 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()) { 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 16d26b1..07b3c08 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -187,7 +187,6 @@ Recent uploads No upload attempted yet. Updates, newest first: - Gear codes, newest first (now: %1$s; only 1 = P is known): Upload now Starting… Running — waiting for first upload diff --git a/app/src/test/java/com/evsuite/abrp/GearTest.java b/app/src/test/java/com/evsuite/abrp/GearTest.java index 60db355..67fdf2f 100644 --- a/app/src/test/java/com/evsuite/abrp/GearTest.java +++ b/app/src/test/java/com/evsuite/abrp/GearTest.java @@ -8,11 +8,18 @@ import org.junit.Test; public class GearTest { @Test - public void onlyParkIsNamed() { + public void theFourCodesReadOnTheCar() { assertEquals("P", Gear.label(1)); - assertEquals("", Gear.label(2)); - assertEquals("", Gear.label(3)); - assertEquals("", Gear.label(4)); + assertEquals("R", Gear.label(2)); + assertEquals("N", Gear.label(3)); + assertEquals("D", Gear.label(4)); + } + + @Test + public void unknownCodesShowNoLetter() { + assertEquals("", Gear.label(0)); + assertEquals("", Gear.label(5)); + assertEquals("", Gear.label(-1)); } @Test