App 2.31.1: log the cruise control while driving, with every assist value

The 2.27.3 log wrote a line only when the target speed changed, so the
car showed one line at the start. Now a line is written on any change of
a mode or the target and every 2 s while driving, with the raw
driver-assistance properties (target speed, assist and speed-assist
modes, the followed object) and the reason for any unread value. The
last 600 lines are kept in an appended file; the Logs tab shows 150.

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 09:33:17 +01:00
co-authored by Claude Opus 5.5
parent 4cd5f7a7ba
commit 36cc417638
10 changed files with 246 additions and 56 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ Repository: `ssh://git@git.stinnesbeck.com:2222/nils/mg4.git` (remote `origin`),
| Live data | on/off, ABRP API key and user token, upload frequency | | 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 | | 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 | 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 |
| Logs | update log (every install step, with the platform's words), upload log | | Logs | adaptive cruise control debug log (`CruiseLog`: the raw driver-assistance properties from `EVHardware.readAccDebug`, once a second; a line on change and every 2 s while driving; last 600 in a file), update log (every install step, with the platform's words), upload log |
Key classes (`app/src/main/java/com/evsuite/abrp/`): `AbrpUploadService` (foreground service, Key classes (`app/src/main/java/com/evsuite/abrp/`): `AbrpUploadService` (foreground service,
15 s tick), `TelemetryUploader` (+ `TelemetryQueue`, persisted), `TelemetryPayload` (server 15 s tick), `TelemetryUploader` (+ `TelemetryQueue`, persisted), `TelemetryPayload` (server
@@ -172,6 +172,9 @@ object ReadFailureLog {
lastReason.put(key, reason) != reason lastReason.put(key, reason) != reason
} }
/** Why [key] last failed, or null when it answered (or was never read). */
fun reason(key: String): String? = synchronized(lock) { lastReason[key] }
/** A property that starts answering again must not silence its next failure. */ /** A property that starts answering again must not silence its next failure. */
fun clear(key: String) { fun clear(key: String) {
synchronized(lock) { lastReason.remove(key) } synchronized(lock) { lastReason.remove(key) }
@@ -434,6 +434,53 @@ object EVHardware {
*/ */
private const val PROP_CRUISE_TARGET_SPEED = 0x2160A15B // vendor float private const val PROP_CRUISE_TARGET_SPEED = 0x2160A15B // vendor float
/**
* The other driver-assistance properties of `CarAdvancedAssistedDrivingManager` that may
* say what the adaptive cruise control is doing, read raw for the Logs tab while their
* meaning is established on the car: the operation mode of traffic-jam / intelligent
* cruise assist (`LANE_TJAICAOPMD`, by the manager 0 no request, 1 object following,
* 2 lane centre, 3 combined), the speed assist (`SPEED_ASST_SYS`, `SPEED_ASST_MODE`, by the
* manager 0 off, 2 manual limiter, 3 intelligent), the traffic-jam assist switch, and the
* object the ACC follows (`ACC_OBJ_*`: existence probability, longitudinal distance and
* relative speed).
*/
private val ACC_DEBUG_PROPS = listOf(
AccDebugProp("cc", PROP_CRUISE_TARGET_SPEED, float = true),
AccDebugProp("ica", 0x2140A12A, float = false), // AAD_LANE_TJAICAOPMD
AccDebugProp("tja", 0x2140A107, float = false), // AAD_TRAFFIC_JAM_ASST_SYS
AccDebugProp("sas", 0x2140A101, float = false), // AAD_SPEED_ASST_SYS
AccDebugProp("sam", 0x2140A102, float = false), // AAD_SPEED_ASST_MODE
AccDebugProp("obj", 0x2160A12F, float = true), // AAD_ACC_OBJ_EXIST_PRBLTY
AccDebugProp("dist", 0x2160A12C, float = true), // AAD_ACC_OBJ_LONG_RLTV_DIST
AccDebugProp("rel", 0x2160A12E, float = true), // AAD_ACC_OBJ_LONG_RLTV_SPD
)
private class AccDebugProp(val name: String, val id: Int, val float: Boolean)
/** The cruise control target speed as the car reports it, unmapped; null when unread. */
fun getCruiseTargetSpeedRaw(): Float? = supportedTelemetryRead {
getFloatPropertyCPM(PROP_CRUISE_TARGET_SPEED, AREA_GLOBAL)?.takeIf { it.isFinite() }
}
/**
* The driver-assistance properties above, one raw reading each, for debugging the adaptive
* cruise control on the car: name to value (floats to two decimals), or to "unread" with
* the reason the car gave. Read only; about eight calls, so not on the main thread.
*/
fun readAccDebug(): List<Pair<String, String>> = ACC_DEBUG_PROPS.map { prop ->
val value: Any? = supportedTelemetryRead {
if (prop.float) getFloatPropertyCPM(prop.id, AREA_GLOBAL)?.takeIf { it.isFinite() }
else readPropertyCPM(prop.id, AREA_GLOBAL, Int::class.javaObjectType) { sCpmGetIntMethod } as? Int
}
prop.name to when (value) {
is Float -> String.format(java.util.Locale.US, "%.2f", value)
null -> "unread" + (ReadFailureLog.reason(
"0x${Integer.toHexString(prop.id)}@0x${Integer.toHexString(AREA_GLOBAL)}")
?.let { " ($it)" } ?: "")
else -> value.toString()
}
}
/** /**
* `HVAC_AMBIENT_TEMPERATURE`, the one in-car candidate left on this firmware. * `HVAC_AMBIENT_TEMPERATURE`, the one in-car candidate left on this firmware.
* *
@@ -538,11 +585,6 @@ object EVHardware {
* answers, and this is what is left on a unit where it does not. * answers, and this is what is left on a unit where it does not.
* See [PROP_BMS_CHRG_STS] for the values. * See [PROP_BMS_CHRG_STS] for the values.
*/ */
/** The cruise control target speed as the car reports it, unmapped; null when unread. */
fun getCruiseTargetSpeedRaw(): Float? = supportedTelemetryRead {
getFloatPropertyCPM(PROP_CRUISE_TARGET_SPEED, AREA_GLOBAL)?.takeIf { it.isFinite() }
}
fun getVendorChargeStatus(): Int? = fun getVendorChargeStatus(): Int? =
getIntPropertyCPM(PROP_BMS_CHRG_STS, AREA_GLOBAL).takeIf { it >= 0 } getIntPropertyCPM(PROP_BMS_CHRG_STS, AREA_GLOBAL).takeIf { it >= 0 }
+2 -1
View File
@@ -32,7 +32,8 @@ ids `0x21401553`–`56`, the vendor mileage `0x21401566` for the odometer, the c
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 and, from this fork, the gear's raw code speed already in km/h — plus translated comments and, from this fork, the gear's raw code
(`EVHardware.getGearRaw`, `EnergySnapshot.gearRaw`) and the cruise control target speed (`EVHardware.getGearRaw`, `EnergySnapshot.gearRaw`) and the cruise control target speed
(`getCruiseTargetSpeedRaw`, `EnergySnapshot.cruiseTargetSpeedRaw`). To take upstream again, merge three ways with (`getCruiseTargetSpeedRaw`, `EnergySnapshot.cruiseTargetSpeedRaw`) with the other driver-assistance
properties read raw for debugging (`readAccDebug`, `ReadFailureLog.reason`). 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
+14
View File
@@ -131,6 +131,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 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-25 — App 2.31.1 (branch `styles-retro-future`): the cruise control log now logs while
driving. 2.27.3 wrote a line only when the target speed changed, so on the car it showed one
line at the start. Now `CruiseLog` writes a line at once when a mode or the target changes and
every 2 s while driving (D or R, or moving), with every driver-assistance property that may
describe the ACC, raw (`EVHardware.readAccDebug`, read once a second on the reader thread):
the target speed (`cc`, `0x2160A15B`), the traffic-jam / intelligent cruise assist operation
mode (`ica`, `AAD_LANE_TJAICAOPMD` `0x2140A12A`), the traffic-jam assist (`tja`, `0x2140A107`),
the speed assist system and mode (`sas`/`sam`, `0x2140A101`/`0x2140A102`) and the object the
ACC follows (`obj`/`dist`/`rel`, `AAD_ACC_OBJ_*` `0x2160A12F`/`0x2160A12C`/`0x2160A12E`; these
alone never write a line). An unread value carries the reason the car gave
(`ReadFailureLog.reason`). The last 600 lines live in `files/cruise_log.txt`, appended to; the
Logs tab shows the newest 150. The old change-only log in preferences is dropped. Only while the
app is open (the reader runs with the screen). Emulator: every value unread, the file written
and kept over a restart. Not tried on the car.
- 2026-10-25 — App 2.31.0 (branch `styles-retro-future`): the Overview's full screen moved into - 2026-10-25 — App 2.31.0 (branch `styles-retro-future`): the Overview's full screen moved into
the navigation: its button now sits beside the MG4 logo (at the end of the bottom bar on narrow the navigation: its button now sits beside the MG4 logo (at the end of the bottom bar on narrow
screens) instead of over the page's top-right corner, and only shows on the Overview. A swipe screens) instead of over the page's top-right corner, and only shows on the Overview. A swipe
+2 -2
View File
@@ -34,8 +34,8 @@ android {
applicationId "com.stinnesbeck.nilsmg4" applicationId "com.stinnesbeck.nilsmg4"
minSdk 27 minSdk 27
targetSdk 36 targetSdk 36
versionCode 128 versionCode 129
versionName "2.31.0" versionName "2.31.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.
+122 -38
View File
@@ -1,69 +1,153 @@
package com.evsuite.abrp; package com.evsuite.abrp;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
/** /**
* Every change of the cruise control target speed the car reports (`0x2160A15B`, raw, unit not * The adaptive cruise control debug log on the Logs tab: the driver-assistance properties the
* established) with the time, speed and gear, the last {@link #MAX_LINES}, shown on the Logs * car reports (`EVHardware.readAccDebug`, raw: the cruise control target speed, the assist
* tab. It is how the value gets matched to what the cluster shows: set the cruise control, change * modes, the object the ACC follows) with the time, speed and gear. It is how they get matched
* the speed, switch it off, and read what the car reported. Kept in preferences; a line is * to what the cluster shows: drive with the app open, set the cruise control, change the speed
* written only when the value (to 0.1) changes. * and the distance, switch it off, and read what the car reported.
*
* A line is written at once when a mode or the target speed changes, and every
* {@link #DRIVING_EVERY_MS} while driving (the followed object's values change all the time,
* so they alone never write one). The last {@link #MAX_LINES} lines are kept in a file in the
* app's own storage, appended to, so a drive does not rewrite it every few seconds.
*/ */
final class CruiseLog { final class CruiseLog {
static final int MAX_LINES = 12; /** About 20 minutes of driving at one line per two seconds. */
private static final String PREFS = "cruise_log"; static final int MAX_LINES = 600;
private static final String KEY = "lines"; /** The newest lines the Logs tab shows. */
static final int SHOWN_LINES = 150;
static final long DRIVING_EVERY_MS = 2000;
/** Values that change all the time while following a car: they never write a line alone. */
private static final List<String> MOVING = java.util.Arrays.asList("obj", "dist", "rel");
private static final String FILE = "cruise_log.txt";
/** The last value seen in this process, as text ("unread" when null); null before the first. */ private static ArrayDeque<String> lines;
private static String last; private static String lastSteady;
private static long lastWriteMs;
private static String current = "—";
private CruiseLog() {} private CruiseLog() {}
/** The value as the log writes it: one decimal, or "unread". */ /** The values that write a line when they change: everything but the followed object. */
static String format(Float raw) { static String steady(List<String[]> readings) {
return raw == null || !Float.isFinite(raw) ? "unread" : String.format(Locale.US, "%.1f", raw); StringBuilder sb = new StringBuilder();
for (String[] r : readings) {
if (!MOVING.contains(r[0])) sb.append(r[0]).append(' ').append(r[1]).append(" ");
}
return sb.toString().trim();
} }
/** Called with every reading; records a line when the value differs from the last one. */ /** All readings as the log writes them. */
static void note(Context context, Float raw, Float speedKmh, Integer gear) { static String values(List<String[]> readings) {
String value = format(raw); StringBuilder sb = new StringBuilder();
for (String[] r : readings) sb.append(r[0]).append(' ').append(r[1]).append(" ");
return sb.toString().trim();
}
/** Whether this reading gets a line: a changed mode or target, or time while driving. */
static boolean due(String steady, String lastSteady, long nowMs, long lastWriteMs, boolean driving) {
if (!steady.equals(lastSteady)) return true;
return driving && nowMs - lastWriteMs >= DRIVING_EVERY_MS;
}
/** Driving: in D or R, or moving. Unread values are not driving. */
static boolean driving(Float speedKmh, Integer gear) {
return (gear != null && (gear == 4 || gear == 2)) || (speedKmh != null && speedKmh > 0.5f);
}
/** Called with each reading, off the main thread. */
static void note(Context context, List<String[]> readings, Float speedKmh, Integer gear) {
if (context == null || readings == null || readings.isEmpty()) return;
long now = System.currentTimeMillis();
String steady = steady(readings);
synchronized (CruiseLog.class) { synchronized (CruiseLog.class) {
if (context == null || value.equals(last)) return; current = values(readings);
last = value; if (!due(steady, lastSteady, now, lastWriteMs, driving(speedKmh, gear))) return;
String line = new SimpleDateFormat("dd.MM. HH:mm:ss", Locale.US).format(new Date()) lastSteady = steady;
+ " target " + value lastWriteMs = now;
+ " at " + (speedKmh == null ? "—" : String.format(Locale.US, "%.0f km/h", speedKmh)) String label = Gear.label(gear);
+ " gear " + (Gear.label(gear).isEmpty() ? "?" : Gear.label(gear)); String line = new SimpleDateFormat("dd.MM. HH:mm:ss", Locale.US).format(new Date(now))
SharedPreferences prefs = context.getApplicationContext() + " " + (speedKmh == null ? "—" : String.format(Locale.US, "%.0f km/h", speedKmh))
.getSharedPreferences(PREFS, Context.MODE_PRIVATE); + " " + (label.isEmpty() ? "?" : label)
List<String> lines = UpdateLog.append(split(prefs.getString(KEY, null)), line, MAX_LINES); + " " + current;
prefs.edit().putString(KEY, String.join("\n", lines)).apply(); load(context);
lines.addLast(line);
File file = file(context);
if (lines.size() > MAX_LINES + 100) {
// Trim now and then: the file is otherwise only appended to.
while (lines.size() > MAX_LINES) lines.removeFirst();
write(file, lines, false);
} else {
List<String> one = new ArrayList<>();
one.add(line);
write(file, one, true);
}
} }
} }
/** The value now as text, "—" before the first reading. */ /** All readings now, "—" before the first. */
static synchronized String current() { static synchronized String current() {
return last == null ? "—" : last; return current;
} }
/** Newest first. */ /** The newest {@link #SHOWN_LINES}, newest first. */
static List<String> lines(Context context) { static synchronized List<String> lines(Context context) {
SharedPreferences prefs = context.getApplicationContext() load(context);
.getSharedPreferences(PREFS, Context.MODE_PRIVATE); List<String> newest = new ArrayList<>();
return split(prefs.getString(KEY, null)); Iterator<String> it = lines.descendingIterator();
while (it.hasNext() && newest.size() < SHOWN_LINES) newest.add(it.next());
return newest;
} }
private static List<String> split(String stored) { private static File file(Context context) {
if (stored == null || stored.isEmpty()) return new ArrayList<>(); return new File(context.getApplicationContext().getFilesDir(), FILE);
return new ArrayList<>(Arrays.asList(stored.split("\n"))); }
/** Reads the file once per process; drops the change-only log of 2.27.3. */
private static void load(Context context) {
if (lines != null) return;
lines = new ArrayDeque<>();
context.getApplicationContext().deleteSharedPreferences("cruise_log");
File file = file(context);
if (!file.exists()) return;
try (BufferedReader in = new BufferedReader(new InputStreamReader(
new FileInputStream(file), StandardCharsets.UTF_8))) {
String line;
while ((line = in.readLine()) != null) {
if (line.isEmpty()) continue;
lines.addLast(line);
if (lines.size() > MAX_LINES) lines.removeFirst();
}
} catch (Exception ignored) {
// An unreadable log starts again empty.
}
}
private static void write(File file, Iterable<String> text, boolean append) {
try (Writer out = new OutputStreamWriter(new FileOutputStream(file, append), StandardCharsets.UTF_8)) {
for (String line : text) out.write(line + "\n");
} catch (Exception ignored) {
// A debug log that cannot be written is not worth disturbing the screen for.
}
} }
} }
@@ -1503,6 +1503,9 @@ public class MainActivity extends AppCompatActivity {
* About twenty calls into the car's services: on a background thread, so the screen never * About twenty calls into the car's services: on a background thread, so the screen never
* waits for them; the values are shown on the main thread once they are in. * waits for them; the values are shown on the main thread once they are in.
*/ */
/** Counts readings on the reader thread, for the once-a-second cruise control log. */
private int accDebugTick;
private void readVehicle() { private void readVehicle() {
if (!reading.compareAndSet(false, true)) return; if (!reading.compareAndSet(false, true)) return;
try { try {
@@ -1514,6 +1517,14 @@ public class MainActivity extends AppCompatActivity {
// The timestamp is passed explicitly: its Kotlin default is not visible from Java. // The timestamp is passed explicitly: its Kotlin default is not visible from Java.
EnergySnapshot s = vehicleReader.read(System.currentTimeMillis()); EnergySnapshot s = vehicleReader.read(System.currentTimeMillis());
latestSnapshot = s; latestSnapshot = s;
// The cruise control debug log, once a second: about eight more calls.
if (accDebugTick++ % 5 == 0) {
java.util.List<String[]> acc = new java.util.ArrayList<>();
for (kotlin.Pair<String, String> r : com.evsuite.hardware.EVHardware.INSTANCE.readAccDebug()) {
acc.add(new String[] { r.getFirst(), r.getSecond() });
}
CruiseLog.note(getApplicationContext(), acc, s.getSpeedKmh(), s.getGearRaw());
}
runOnUiThread(() -> { runOnUiThread(() -> {
if (!isDestroyed()) showVehicle(s); if (!isDestroyed()) showVehicle(s);
}); });
@@ -1797,7 +1808,6 @@ 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()));
CruiseLog.note(this, s.getCruiseTargetSpeedRaw(), s.getSpeedKmh(), s.getGearRaw());
showGauges(s); showGauges(s);
showCockpit(s); showCockpit(s);
showDrawnStyles(s); showDrawnStyles(s);
+1 -1
View File
@@ -304,7 +304,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="cruise_log_title">Cruise control target speed, newest first (now: %1$s; raw, unit not known yet):</string> <string name="cruise_log_title">Adaptive cruise control as the car reports it, raw, newest first: a line when a mode or the target changes, and every 2 s while driving with this app open. cc target speed, ica assist mode, tja traffic jam assist, sas/sam speed assist, obj/dist/rel the car ahead (presence, distance, relative speed).\nNow: %1$s</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>
@@ -1,18 +1,54 @@
package com.evsuite.abrp; package com.evsuite.abrp;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
import java.util.List;
import org.junit.Test; import org.junit.Test;
/** The cruise control log's value format — pure JVM. */ /** The cruise control debug log's lines and when it writes one — pure JVM. */
public class CruiseLogTest { public class CruiseLogTest {
private static List<String[]> readings(String cc, String ica, String dist) {
return Arrays.asList(new String[] { "cc", cc }, new String[] { "ica", ica },
new String[] { "dist", dist });
}
@Test @Test
public void oneDecimalOrUnread() { public void lineHasEveryValue() {
assertEquals("120.0", CruiseLog.format(120f)); assertEquals("cc 120.00 ica 1 dist 35.20", CruiseLog.values(readings("120.00", "1", "35.20")));
assertEquals("33.3", CruiseLog.format(33.33f)); assertEquals("cc unread (not available) ica 0 dist unread",
assertEquals("0.0", CruiseLog.format(0f)); CruiseLog.values(readings("unread (not available)", "0", "unread")));
assertEquals("unread", CruiseLog.format(null)); }
assertEquals("unread", CruiseLog.format(Float.NaN));
@Test
public void followedObjectAloneIsNoChange() {
assertEquals(CruiseLog.steady(readings("120.00", "1", "35.20")),
CruiseLog.steady(readings("120.00", "1", "12.00")));
assertFalse(CruiseLog.steady(readings("120.00", "1", "1")).contains("dist"));
}
@Test
public void writesOnChangeAndEveryTwoSecondsWhileDriving() {
String a = CruiseLog.steady(readings("120.00", "1", "35"));
String b = CruiseLog.steady(readings("110.00", "1", "35"));
assertTrue(CruiseLog.due(a, null, 0, 0, false));
assertTrue(CruiseLog.due(b, a, 1000, 500, false));
assertFalse(CruiseLog.due(a, a, 1000, 500, true));
assertTrue(CruiseLog.due(a, a, 2500, 500, true));
// Parked: nothing new, nothing written.
assertFalse(CruiseLog.due(a, a, 60_000, 500, false));
}
@Test
public void drivingIsDOrROrMoving() {
assertTrue(CruiseLog.driving(null, 4));
assertTrue(CruiseLog.driving(0f, 2));
assertTrue(CruiseLog.driving(30f, 3));
assertFalse(CruiseLog.driving(0f, 1));
assertFalse(CruiseLog.driving(null, null));
} }
} }