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:
@@ -1,69 +1,153 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
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.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Every change of the cruise control target speed the car reports (`0x2160A15B`, raw, unit not
|
||||
* established) with the time, speed and gear, the last {@link #MAX_LINES}, shown on the Logs
|
||||
* tab. It is how the value gets matched to what the cluster shows: set the cruise control, change
|
||||
* the speed, switch it off, and read what the car reported. Kept in preferences; a line is
|
||||
* written only when the value (to 0.1) changes.
|
||||
* The adaptive cruise control debug log on the Logs tab: the driver-assistance properties the
|
||||
* car reports (`EVHardware.readAccDebug`, raw: the cruise control target speed, the assist
|
||||
* modes, the object the ACC follows) with the time, speed and gear. It is how they get matched
|
||||
* to what the cluster shows: drive with the app open, set the cruise control, change the speed
|
||||
* 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 {
|
||||
|
||||
static final int MAX_LINES = 12;
|
||||
private static final String PREFS = "cruise_log";
|
||||
private static final String KEY = "lines";
|
||||
/** About 20 minutes of driving at one line per two seconds. */
|
||||
static final int MAX_LINES = 600;
|
||||
/** 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 String last;
|
||||
private static ArrayDeque<String> lines;
|
||||
private static String lastSteady;
|
||||
private static long lastWriteMs;
|
||||
private static String current = "—";
|
||||
|
||||
private CruiseLog() {}
|
||||
|
||||
/** The value as the log writes it: one decimal, or "unread". */
|
||||
static String format(Float raw) {
|
||||
return raw == null || !Float.isFinite(raw) ? "unread" : String.format(Locale.US, "%.1f", raw);
|
||||
/** The values that write a line when they change: everything but the followed object. */
|
||||
static String steady(List<String[]> readings) {
|
||||
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. */
|
||||
static void note(Context context, Float raw, Float speedKmh, Integer gear) {
|
||||
String value = format(raw);
|
||||
/** All readings as the log writes them. */
|
||||
static String values(List<String[]> readings) {
|
||||
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) {
|
||||
if (context == null || value.equals(last)) return;
|
||||
last = value;
|
||||
String line = new SimpleDateFormat("dd.MM. HH:mm:ss", Locale.US).format(new Date())
|
||||
+ " target " + value
|
||||
+ " at " + (speedKmh == null ? "—" : String.format(Locale.US, "%.0f km/h", speedKmh))
|
||||
+ " gear " + (Gear.label(gear).isEmpty() ? "?" : Gear.label(gear));
|
||||
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();
|
||||
current = values(readings);
|
||||
if (!due(steady, lastSteady, now, lastWriteMs, driving(speedKmh, gear))) return;
|
||||
lastSteady = steady;
|
||||
lastWriteMs = now;
|
||||
String label = Gear.label(gear);
|
||||
String line = new SimpleDateFormat("dd.MM. HH:mm:ss", Locale.US).format(new Date(now))
|
||||
+ " " + (speedKmh == null ? "—" : String.format(Locale.US, "%.0f km/h", speedKmh))
|
||||
+ " " + (label.isEmpty() ? "?" : label)
|
||||
+ " " + current;
|
||||
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() {
|
||||
return last == null ? "—" : last;
|
||||
return current;
|
||||
}
|
||||
|
||||
/** Newest first. */
|
||||
static List<String> lines(Context context) {
|
||||
SharedPreferences prefs = context.getApplicationContext()
|
||||
.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
|
||||
return split(prefs.getString(KEY, null));
|
||||
/** The newest {@link #SHOWN_LINES}, newest first. */
|
||||
static synchronized List<String> lines(Context context) {
|
||||
load(context);
|
||||
List<String> newest = new ArrayList<>();
|
||||
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) {
|
||||
if (stored == null || stored.isEmpty()) return new ArrayList<>();
|
||||
return new ArrayList<>(Arrays.asList(stored.split("\n")));
|
||||
private static File file(Context context) {
|
||||
return new File(context.getApplicationContext().getFilesDir(), FILE);
|
||||
}
|
||||
|
||||
/** 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
|
||||
* 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() {
|
||||
if (!reading.compareAndSet(false, true)) return;
|
||||
try {
|
||||
@@ -1514,6 +1517,14 @@ public class MainActivity extends AppCompatActivity {
|
||||
// The timestamp is passed explicitly: its Kotlin default is not visible from Java.
|
||||
EnergySnapshot s = vehicleReader.read(System.currentTimeMillis());
|
||||
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(() -> {
|
||||
if (!isDestroyed()) showVehicle(s);
|
||||
});
|
||||
@@ -1797,7 +1808,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()));
|
||||
CruiseLog.note(this, s.getCruiseTargetSpeedRaw(), s.getSpeedKmh(), s.getGearRaw());
|
||||
showGauges(s);
|
||||
showCockpit(s);
|
||||
showDrawnStyles(s);
|
||||
|
||||
@@ -304,7 +304,7 @@
|
||||
<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="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="status_starting">Starting…</string>
|
||||
<string name="status_running_no_upload">Running — waiting for first upload</string>
|
||||
|
||||
@@ -1,18 +1,54 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
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;
|
||||
|
||||
/** 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 {
|
||||
|
||||
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
|
||||
public void oneDecimalOrUnread() {
|
||||
assertEquals("120.0", CruiseLog.format(120f));
|
||||
assertEquals("33.3", CruiseLog.format(33.33f));
|
||||
assertEquals("0.0", CruiseLog.format(0f));
|
||||
assertEquals("unread", CruiseLog.format(null));
|
||||
assertEquals("unread", CruiseLog.format(Float.NaN));
|
||||
public void lineHasEveryValue() {
|
||||
assertEquals("cc 120.00 ica 1 dist 35.20", CruiseLog.values(readings("120.00", "1", "35.20")));
|
||||
assertEquals("cc unread (not available) ica 0 dist unread",
|
||||
CruiseLog.values(readings("unread (not available)", "0", "unread")));
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user