App 2.33.0: set the drive mode from Settings, only in P at a standstill

The owner lifted the read-only rule for the drive mode alone. Settings
has Eco, Normal, Sport, Snow and Custom; the lit button is what the car
reports. A tap reads gear and speed fresh and writes only in P at 0 km/h
(DriveModeWrite, unit-tested; unreadable is a refusal), through a CPM-only
write behind EVHardware's standstill gate, then reads the mode back and
says what the car reports. The wide layout's charging price joined the
battery card so the page still fits. Not tried on the car.

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-25 20:02:53 +01:00
co-authored by Claude Opus 5.5
parent 703bdfd2a3
commit ce2ebd7f58
15 changed files with 268 additions and 12 deletions
+8 -4
View File
@@ -35,9 +35,13 @@ Repository: `ssh://git@git.stinnesbeck.com:2222/nils/mg4.git` (remote `origin`),
`secrets.example.yaml` holds `CHANGE-ME` placeholders only. Stage files explicitly, check the
index for changes that are not yours (the owner works in parallel), and scan the staged diff
for secrets before every commit. Never print a token.
3. **Telemetry is read-only.** The app never writes a vehicle setting. The only system setting it
touches is `policy_control`, and only to remove the CarPlay entry the withdrawn full-screen
experiment (2.19.2–2.19.5) may have left.
3. **Telemetry is read-only; the drive mode is the one exception.** The app writes no vehicle
setting except the drive mode (owner's decision, 2026-10-25): from Settings only, a tap by the
driver, only in P at 0 km/h with both read fresh (unreadable means no write), through
EVHardware's standstill gate, and the result shown is what the car reports back
(`DriveModeWrite`). Nothing else gets written, and nothing writes on its own. The only system
setting it touches is `policy_control`, and only to remove the CarPlay entry the withdrawn
full-screen experiment (2.19.2–2.19.5) may have left.
4. **A failed read is not a zero.** Unreadable values are `null`, omitted from payloads and
shown as "—". Sending 0 for SOC tells ABRP the battery is empty.
5. **Fail closed.** Updates (origin, SHA-256, package, signature), credentials, unknown state.
@@ -73,7 +77,7 @@ Repository: `ssh://git@git.stinnesbeck.com:2222/nils/mg4.git` (remote `origin`),
| Consumption / Charging | period totals, daily log, charging sessions and curve |
| Live data | on/off, ABRP API key and user token, upload frequency |
| Server | URL, pairing by QR, ingest token, installation ID (read-only), config import |
| Settings | full screen, voice key, Overview style with the accent colour beside it, the Mascot character (only for that style) (offered only for styles drawn in it, `OverviewStyle.usesAccent`; Retro and Terminal add "Original" for their own palette, `hasOriginal`), tyre unit, battery, charging price, version, update check, install from USB, About |
| Settings | drive mode (Eco, Normal, Sport, Snow, Custom; the lit one is what the car reports; changes only in P at a standstill, `DriveModeWrite`), full screen, voice key, Overview style with the accent colour beside it, the Mascot character (only for that style) (offered only for styles drawn in it, `OverviewStyle.usesAccent`; Retro and Terminal add "Original" for their own palette, `hasOriginal`), tyre unit, battery and charging price (one card on wide screens), version, update check, install from USB, About |
| Logs | signal recorder (`SignalRecorder`/`SignalRecording`: every property and vendor getter via `VendorSurfaceProbe.capture`, again and again for 3 minutes, plus steering-wheel key broadcasts; lists what changed, rarest first; saved to the app and a USB stick; how to find where the car publishes something), update log (every install step, with the platform's words), upload log |
Key classes (`app/src/main/java/com/evsuite/abrp/`): `AbrpUploadService` (foreground service,
@@ -2432,6 +2432,16 @@ object EVHardware {
return ok
}
/**
* The drive mode write Nils' MG4 uses: the CarPropertyManager write alone, behind
* [VehicleWriteGate] like every write here, without [setDriveMode]'s fallback binder
* transaction or its remembered "last mode". True means the write call went through, not
* that the car took it: read [getDriveModeRaw] back.
*/
@RequiresStandstill
fun writeDriveModeProperty(mode: DriveMode): Boolean =
setIntPropertyCPM(PROP_DRIVE_MODE, AREA_GLOBAL, mode.value)
@RequiresStandstill
fun setRegenLevel(level: RegenLevel): Boolean {
if (logEnabled) AppLogger.i(TAG, "setRegenLevel → ${level.label} (${level.value})")
+2 -1
View File
@@ -31,7 +31,8 @@ top. Those are Fatih's reads the car needs and upstream does not have — the ve
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
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
(`EVHardware.getGearRaw`, `EnergySnapshot.gearRaw`) and a drive mode write without the binder
fallback (`writeDriveModeProperty`). To take upstream again, merge three ways with
`e4e1158` as the base.
The car app links EVHardware into its APK, so the **built app** may only be used and passed on
+1 -1
View File
@@ -84,7 +84,7 @@ with the server using a token of its own, obtained by pairing with a phone (belo
| **Charging** | The current session (SOC, voltage, current, power, energy into the battery and from the grid, time, cost) and its SOC/power curve |
| **Live data** | Live data on/off, ABRP credentials for sending directly from the car, upload frequency |
| **Server** | Server URL, pairing by QR code, ingest token, installation ID, config import |
| **Settings** | Full screen, voice key, tyre pressure unit, accent colour, battery (manual SoH, usable capacity), charging price, entry on the phone, version, update check, install from USB stick, About |
| **Settings** | Drive mode (Eco, Normal, Sport, Snow, Custom: changes only in P at a standstill; the highlighted one is what the car reports), full screen, voice key, tyre pressure unit, accent colour, battery (manual SoH, usable capacity), charging price, entry on the phone, version, update check, install from USB stick, About |
| **Logs** | Update log (every install step), upload log |
- **Opening the app:** the MG4 launcher does not list third-party apps. **Double-press the
+14
View File
@@ -138,6 +138,20 @@ 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-25 — App 2.33.0 (branch `styles-retro-future`): **drive mode from Settings**, the first
vehicle setting the app writes. The owner lifted rule 3 for the drive mode only (AGENTS.md).
A card at the top of Settings' second column: Eco, Normal, Sport, Snow, Custom (codes 2, 3, 4,
6, 7 at `0x2140A17C`; the 2.32.0 recording read 2, Eco). The lit button is the mode the car
reports, from every 0.2 s reading. A tap reads gear and speed fresh and writes only in P at
0 km/h (`DriveModeWrite`, pure and unit-tested; unreadable gear or speed is a refusal), through
the new `EVHardware.writeDriveModeProperty`: the CarPropertyManager write behind
`VehicleWriteGate` (0 km/h, supported firmware), without `setDriveMode`'s fallback binder
transaction. Then the mode is read back for up to 3 s, and the status beside the title says
what the car reports ("set", "not applied: the car still reports …", "could not be read
back"). One change at a time, on the settings thread; each attempt goes to logcat. To fit the
page, the wide layout's charging price row moved into the battery card ("Battery and charging
price"). Emulator: the card fits, a tap is refused (no gear or speed there). Whether the car
takes the write at all is untested: the first try on the car decides.
- 2026-10-25 — App 2.32.1 (branch `styles-retro-future`): the cruise control work ends. The ACC
recording of 2.32.0 (ACC set to 101, 90 and 80 km/h, a car ahead) found nothing: see "Signals
the car does not provide". Removed: `CruiseLog` and its section on the Logs tab, and in
+2 -2
View File
@@ -34,8 +34,8 @@ android {
applicationId "com.stinnesbeck.nilsmg4"
minSdk 27
targetSdk 36
versionCode 131
versionName "2.32.1"
versionCode 132
versionName "2.33.0"
// 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.
@@ -0,0 +1,44 @@
package com.evsuite.abrp;
/**
* Changing the drive mode from Settings — the one vehicle setting this app writes (owner's
* decision, 2026-10-25; AGENTS.md rule 3). The rules, kept here so they are tested:
*
* - only in P and at 0 km/h, and nothing when the gear or the speed cannot be read (fail
* closed); EVHardware's own {@code VehicleWriteGate} checks the speed again at the write;
* - the result is what the car reports afterwards, never what was asked for.
*
* Codes are the car's (`0x2140A17C`, EVHardware's {@code DriveMode}): Eco 2, Normal 3, Sport 4,
* Snow 6, Custom 7. Pure Java, unit-tested.
*/
final class DriveModeWrite {
/** The modes offered, in the order of the buttons. */
static final int[] CODES = { 2, 3, 4, 6, 7 };
static final int P = 1;
enum Verdict { ALLOWED, NOT_IN_P, MOVING, UNREADABLE }
enum Outcome { APPLIED, NOT_APPLIED, UNREAD }
private DriveModeWrite() {}
static Verdict check(Float speedKmh, Integer gear) {
if (speedKmh == null || speedKmh.isNaN() || speedKmh < 0f || gear == null) return Verdict.UNREADABLE;
if (gear != P) return Verdict.NOT_IN_P;
if (speedKmh != 0f) return Verdict.MOVING;
return Verdict.ALLOWED;
}
static Outcome outcome(int asked, Integer readBack) {
if (readBack == null) return Outcome.UNREAD;
return readBack == asked ? Outcome.APPLIED : Outcome.NOT_APPLIED;
}
/** The button for a reported code, or -1 when the code is unread or not one offered. */
static int indexOf(Integer code) {
if (code == null) return -1;
for (int i = 0; i < CODES.length; i++) if (CODES[i] == code) return i;
return -1;
}
}
@@ -1177,6 +1177,7 @@ public class MainActivity extends AppCompatActivity {
prefs.edit().putBoolean(UploadSettings.KEY_VOICE_KEY_OPENS, checked).apply());
bindTireUnit();
bindMascotCharacter();
bindDriveMode();
bindOverviewStyle();
bindAccentColor();
fullScreenSwitch = settingsPane.findViewById(R.id.full_screen_switch);
@@ -1318,6 +1319,93 @@ public class MainActivity extends AppCompatActivity {
}
/** Who sits in the Mascot style; the drop-down shows only while that style is chosen. */
private static final int[] DRIVE_MODE_IDS = { R.id.drive_mode_eco, R.id.drive_mode_normal,
R.id.drive_mode_sport, R.id.drive_mode_snow, R.id.drive_mode_custom };
private static final int[] DRIVE_MODE_LABELS = { R.string.drive_mode_eco, R.string.drive_mode_normal,
R.string.drive_mode_sport, R.string.drive_mode_snow, R.string.drive_mode_custom };
/** True from a tap on a drive mode until the car's answer is in: one change at a time. */
private final java.util.concurrent.atomic.AtomicBoolean driveModeBusy =
new java.util.concurrent.atomic.AtomicBoolean();
/** The drive mode buttons on Settings: a tap asks the car, the highlight shows what it reports. */
private void bindDriveMode() {
for (int i = 0; i < DRIVE_MODE_IDS.length; i++) {
final int code = DriveModeWrite.CODES[i];
settingsPane.findViewById(DRIVE_MODE_IDS[i]).setOnClickListener(v -> requestDriveMode(code));
}
}
/** The highlight follows the car's reported mode, from every reading. */
private void showDriveMode(Integer code) {
if (settingsPane == null) return;
int selected = DriveModeWrite.indexOf(code);
for (int i = 0; i < DRIVE_MODE_IDS.length; i++) {
settingsPane.findViewById(DRIVE_MODE_IDS[i]).setSelected(i == selected);
}
}
private String driveModeName(Integer code) {
int i = DriveModeWrite.indexOf(code);
return i >= 0 ? getString(DRIVE_MODE_LABELS[i]) : getString(R.string.drive_mode_unknown, String.valueOf(code));
}
/**
* Writes the drive mode, on the settings thread: gear and speed read fresh (P, 0 km/h, or
* nothing), the write itself behind EVHardware's standstill gate, then the car's mode read
* back for up to three seconds. The status line says what the car reports, not what was asked.
*/
private void requestDriveMode(int code) {
if (!driveModeBusy.compareAndSet(false, true)) return;
TextView status = settingsPane.findViewById(R.id.drive_mode_status);
String name = driveModeName(code);
status.setText(getString(R.string.drive_mode_setting, name));
try {
settingsWriter.execute(() -> {
String message;
try {
message = writeDriveMode(code, name);
} catch (Throwable t) {
message = getString(R.string.drive_mode_write_failed, name);
}
android.util.Log.i("NilsMG4.DriveMode", "Drive mode " + code + ": " + message);
final String result = message;
runOnUiThread(() -> {
driveModeBusy.set(false);
if (!isDestroyed()) status.setText(result);
});
});
} catch (java.util.concurrent.RejectedExecutionException e) {
driveModeBusy.set(false); // the activity is going away
}
}
private String writeDriveMode(int code, String name) throws InterruptedException {
switch (DriveModeWrite.check(com.evsuite.hardware.EVHardware.INSTANCE.getVehicleSpeedKmh(),
com.evsuite.hardware.EVHardware.INSTANCE.getGearRaw())) {
case NOT_IN_P: return getString(R.string.drive_mode_not_in_p);
case MOVING: return getString(R.string.drive_mode_moving);
case UNREADABLE: return getString(R.string.drive_mode_unreadable);
default: break;
}
com.evsuite.hardware.model.DriveMode mode = com.evsuite.hardware.model.DriveMode.Companion.ofRaw(code);
if (mode == null || !com.evsuite.hardware.EVHardware.INSTANCE.writeDriveModeProperty(mode)) {
return getString(R.string.drive_mode_write_failed, name);
}
Integer now = null;
for (int i = 0; i < 10; i++) {
Thread.sleep(300);
now = com.evsuite.hardware.EVHardware.INSTANCE.getDriveModeRaw();
if (now != null && now == code) break;
}
final Integer reported = now;
runOnUiThread(() -> showDriveMode(reported));
switch (DriveModeWrite.outcome(code, now)) {
case APPLIED: return getString(R.string.drive_mode_applied, name);
case NOT_APPLIED: return getString(R.string.drive_mode_not_applied, name, driveModeName(now));
default: return getString(R.string.drive_mode_unread_back, name);
}
}
private void bindMascotCharacter() {
MascotCharacter[] characters = MascotCharacter.values();
String[] labels = { getString(R.string.mascot_character_classic), getString(R.string.mascot_character_chibi),
@@ -1808,6 +1896,7 @@ public class MainActivity extends AppCompatActivity {
showGauges(s);
showCockpit(s);
showDrawnStyles(s);
showDriveMode(s.getDriveModeRaw());
} catch (Throwable ignored) {
// Leave the last good reading on screen.
}
+3
View File
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- A choice's outline: the accent when it is the one in effect, the plain outline otherwise. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android"><item android:state_selected="true" android:color="?attr/evAccentStroke"/><item android:color="@color/ev_outline"/></selector>
@@ -44,8 +44,22 @@
</LinearLayout>
</LinearLayout>
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:layout_marginEnd="16dp">
<!-- The drive mode: the one vehicle setting the app writes, only in P (DriveModeWrite). -->
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:paddingStart="24dp" android:paddingEnd="24dp" android:paddingTop="16dp" android:paddingBottom="16dp" android:background="@drawable/bg_glass" android:layout_marginBottom="16dp">
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_vertical">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/label_drive_mode" android:textAppearance="@style/Text.EV.Eyebrow" />
<TextView android:id="@+id/drive_mode_status" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:gravity="end" android:text="@string/drive_mode_hint" android:textAppearance="@style/Text.EV.Secondary" />
</LinearLayout>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_marginTop="8dp">
<com.google.android.material.button.MaterialButton android:id="@+id/drive_mode_eco" style="@style/Widget.EV.Choice" android:layout_width="0dp" android:layout_height="64dp" android:layout_weight="1" android:layout_marginEnd="6dp" android:text="@string/drive_mode_eco" />
<com.google.android.material.button.MaterialButton android:id="@+id/drive_mode_normal" style="@style/Widget.EV.Choice" android:layout_width="0dp" android:layout_height="64dp" android:layout_weight="1" android:layout_marginEnd="6dp" android:text="@string/drive_mode_normal" />
<com.google.android.material.button.MaterialButton android:id="@+id/drive_mode_sport" style="@style/Widget.EV.Choice" android:layout_width="0dp" android:layout_height="64dp" android:layout_weight="1" android:layout_marginEnd="6dp" android:text="@string/drive_mode_sport" />
<com.google.android.material.button.MaterialButton android:id="@+id/drive_mode_snow" style="@style/Widget.EV.Choice" android:layout_width="0dp" android:layout_height="64dp" android:layout_weight="1" android:layout_marginEnd="6dp" android:text="@string/drive_mode_snow" />
<com.google.android.material.button.MaterialButton android:id="@+id/drive_mode_custom" style="@style/Widget.EV.Choice" android:layout_width="0dp" android:layout_height="64dp" android:layout_weight="1" android:text="@string/drive_mode_custom" />
</LinearLayout>
</LinearLayout>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:paddingStart="24dp" android:paddingEnd="24dp" android:paddingTop="16dp" android:paddingBottom="16dp" android:background="@drawable/bg_glass">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_battery_settings_title" android:textAppearance="@style/Text.EV.Eyebrow" />
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_battery_price_title" android:textAppearance="@style/Text.EV.Eyebrow" />
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:orientation="horizontal" android:gravity="center_vertical" android:baselineAligned="false">
<com.google.android.material.textfield.TextInputLayout android:id="@+id/consumption_soh_layout" style="@style/Widget.Material3.TextInputLayout.OutlinedBox" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:hint="@string/settings_manual_soh" app:suffixText="@string/unit_percent">
<com.google.android.material.textfield.TextInputEditText android:id="@+id/consumption_soh_input" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="numberDecimal" android:maxLength="5" android:minHeight="72dp"/>
@@ -60,9 +74,6 @@
<com.google.android.material.button.MaterialButton android:id="@+id/usable_capacity_save" style="@style/Widget.EV.Button" android:layout_width="wrap_content" android:layout_height="72dp" android:layout_marginStart="12dp" android:text="@string/btn_save_capacity"/>
</LinearLayout>
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/helper_usable_capacity" android:textAppearance="@style/Text.EV.Secondary"/>
</LinearLayout>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:paddingStart="24dp" android:paddingEnd="24dp" android:paddingTop="16dp" android:paddingBottom="16dp" android:background="@drawable/bg_glass" android:layout_marginTop="12dp">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_price_title" android:textAppearance="@style/Text.EV.Eyebrow" />
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:orientation="horizontal" android:gravity="center_vertical" android:baselineAligned="false">
<com.google.android.material.textfield.TextInputLayout android:id="@+id/charge_price_layout" style="@style/Widget.Material3.TextInputLayout.OutlinedBox" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:hint="@string/settings_unit_price" app:suffixText="@string/unit_eur_per_kwh">
<com.google.android.material.textfield.TextInputEditText android:id="@+id/charge_price_input" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="numberDecimal" android:maxLength="10" android:minHeight="72dp"/>
+14
View File
@@ -29,6 +29,20 @@
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</LinearLayout>
<!-- The drive mode: the one vehicle setting the app writes, only in P (DriveModeWrite). -->
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass" android:layout_marginTop="16dp">
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_vertical">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/label_drive_mode" android:textAppearance="@style/Text.EV.Eyebrow" />
<TextView android:id="@+id/drive_mode_status" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:gravity="end" android:text="@string/drive_mode_hint" android:textAppearance="@style/Text.EV.Secondary" />
</LinearLayout>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_marginTop="8dp">
<com.google.android.material.button.MaterialButton android:id="@+id/drive_mode_eco" style="@style/Widget.EV.Choice" android:layout_width="0dp" android:layout_height="64dp" android:layout_weight="1" android:layout_marginEnd="6dp" android:text="@string/drive_mode_eco" />
<com.google.android.material.button.MaterialButton android:id="@+id/drive_mode_normal" style="@style/Widget.EV.Choice" android:layout_width="0dp" android:layout_height="64dp" android:layout_weight="1" android:layout_marginEnd="6dp" android:text="@string/drive_mode_normal" />
<com.google.android.material.button.MaterialButton android:id="@+id/drive_mode_sport" style="@style/Widget.EV.Choice" android:layout_width="0dp" android:layout_height="64dp" android:layout_weight="1" android:layout_marginEnd="6dp" android:text="@string/drive_mode_sport" />
<com.google.android.material.button.MaterialButton android:id="@+id/drive_mode_snow" style="@style/Widget.EV.Choice" android:layout_width="0dp" android:layout_height="64dp" android:layout_weight="1" android:layout_marginEnd="6dp" android:text="@string/drive_mode_snow" />
<com.google.android.material.button.MaterialButton android:id="@+id/drive_mode_custom" style="@style/Widget.EV.Choice" android:layout_width="0dp" android:layout_height="64dp" android:layout_weight="1" android:text="@string/drive_mode_custom" />
</LinearLayout>
</LinearLayout>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass" android:layout_marginTop="16dp">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_battery_settings_title" android:textAppearance="@style/Text.EV.Eyebrow" />
<com.google.android.material.textfield.TextInputLayout android:id="@+id/consumption_soh_layout" style="@style/Widget.Material3.TextInputLayout.OutlinedBox" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:hint="@string/settings_manual_soh" app:suffixText="@string/unit_percent">
+16
View File
@@ -302,6 +302,22 @@
<!-- Call log -->
<string name="label_call_log">Recent uploads</string>
<string name="label_drive_mode">DRIVE MODE</string>
<string name="drive_mode_eco">Eco</string>
<string name="drive_mode_normal">Normal</string>
<string name="drive_mode_sport">Sport</string>
<string name="drive_mode_snow">Snow</string>
<string name="drive_mode_custom">Custom</string>
<string name="drive_mode_hint">Changes only in P, at a standstill</string>
<string name="drive_mode_setting">Setting %1$s…</string>
<string name="drive_mode_applied">%1$s set: the car reports it now.</string>
<string name="drive_mode_not_applied">%1$s not applied: the car still reports %2$s.</string>
<string name="drive_mode_unread_back">%1$s sent, but the car\'s mode could not be read back.</string>
<string name="drive_mode_write_failed">%1$s not set: the car did not accept the change.</string>
<string name="drive_mode_not_in_p">Not changed: shift to P first.</string>
<string name="drive_mode_moving">Not changed: only at a standstill.</string>
<string name="drive_mode_unreadable">Not changed: speed or gear unreadable.</string>
<string name="drive_mode_unknown">an unknown mode (%1$s)</string>
<string name="label_signal_recorder">Signal recorder</string>
<string name="signal_recorder_hint">Records every value the head unit offers for 3 minutes, plus the steering-wheel keys it reports, and lists what changed, the rarest first. To find a signal: start it, note the times you switch the feature on and off or change it, and look for the values that changed then. With a USB stick plugged in, the whole recording is saved on it too (Android/data/…/files).</string>
<string name="btn_signal_record">Record 3 minutes</string>
@@ -16,6 +16,7 @@
<string name="design_battery_settings_title">BATTERY</string>
<string name="design_app_title">APP</string>
<string name="design_price_title">CHARGING PRICE</string>
<string name="design_battery_price_title">BATTERY AND CHARGING PRICE</string>
<string name="design_phone_title">ON YOUR PHONE</string>
<string name="design_service_timing">UPDATE FREQUENCY</string>
<string name="design_car_desc">Tire pressure positions on the vehicle</string>
+7
View File
@@ -23,6 +23,13 @@
<item name="cornerRadius">18dp</item>
<item name="elevation">0dp</item>
</style>
<!-- One of a row of choices, highlighted (isSelected) like the navigation's current page. -->
<style name="Widget.EV.Choice" parent="Widget.EV.NavTab">
<item name="android:paddingStart">4dp</item>
<item name="android:paddingEnd">4dp</item>
<item name="android:gravity">center</item>
<item name="strokeColor">@color/choice_stroke</item>
</style>
<style name="Widget.EV.NavTab.Compact">
<item name="android:paddingStart">2dp</item>
<item name="android:paddingEnd">2dp</item>
@@ -0,0 +1,42 @@
package com.evsuite.abrp;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
/** The drive mode write's rules — pure JVM. */
public class DriveModeWriteTest {
@Test
public void onlyInPAtAStandstill() {
assertEquals(DriveModeWrite.Verdict.ALLOWED, DriveModeWrite.check(0f, 1));
assertEquals(DriveModeWrite.Verdict.NOT_IN_P, DriveModeWrite.check(0f, 4));
assertEquals(DriveModeWrite.Verdict.NOT_IN_P, DriveModeWrite.check(0f, 3));
assertEquals(DriveModeWrite.Verdict.NOT_IN_P, DriveModeWrite.check(30f, 2));
assertEquals(DriveModeWrite.Verdict.MOVING, DriveModeWrite.check(0.5f, 1));
}
@Test
public void unreadableIsRefused() {
assertEquals(DriveModeWrite.Verdict.UNREADABLE, DriveModeWrite.check(null, 1));
assertEquals(DriveModeWrite.Verdict.UNREADABLE, DriveModeWrite.check(0f, null));
assertEquals(DriveModeWrite.Verdict.UNREADABLE, DriveModeWrite.check(Float.NaN, 1));
assertEquals(DriveModeWrite.Verdict.UNREADABLE, DriveModeWrite.check(-1f, 1));
}
@Test
public void resultIsWhatTheCarReports() {
assertEquals(DriveModeWrite.Outcome.APPLIED, DriveModeWrite.outcome(4, 4));
assertEquals(DriveModeWrite.Outcome.NOT_APPLIED, DriveModeWrite.outcome(4, 2));
assertEquals(DriveModeWrite.Outcome.UNREAD, DriveModeWrite.outcome(4, null));
}
@Test
public void buttonsByCode() {
assertEquals(0, DriveModeWrite.indexOf(2));
assertEquals(2, DriveModeWrite.indexOf(4));
assertEquals(4, DriveModeWrite.indexOf(7));
assertEquals(-1, DriveModeWrite.indexOf(5));
assertEquals(-1, DriveModeWrite.indexOf(null));
}
}