App 2.22.2: gear badge shows P, R, N and D
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-2
@@ -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.
|
||||
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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")));
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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<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()) {
|
||||
sb.append(getString(R.string.update_log_title));
|
||||
for (String line : updates) sb.append('\n').append(line);
|
||||
|
||||
@@ -187,7 +187,6 @@
|
||||
<string name="label_call_log">Recent uploads</string>
|
||||
<string name="call_log_empty">No upload attempted yet.</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="status_starting">Starting…</string>
|
||||
<string name="status_running_no_upload">Running — waiting for first upload</string>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user