App 2.32.1: drop the cruise control reads; the recorder becomes general

The 2.32.0 recording (ACC set to 101, 90 and 80 km/h behind another car)
found none of it among 1,194 values: the head unit does not get the ACC
data. Removed the cruise log and the set-speed read (EVHardware is as
before 2.27.3); the finding is under "Signals the car does not provide".
The recorder stays as the Signal recorder, for finding other signals.

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 19:51:42 +01:00
co-authored by Claude Opus 5.5
parent 2e6bb9cb83
commit 703bdfd2a3
16 changed files with 84 additions and 352 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 |
| 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 |
| Logs | ACC recorder (`AccRecorder`/`AccRecording`: 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), 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 |
| 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,
15 s tick), `TelemetryUploader` (+ `TelemetryQueue`, persisted), `TelemetryPayload` (server
@@ -172,9 +172,6 @@ object ReadFailureLog {
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. */
fun clear(key: String) {
synchronized(lock) { lastReason.remove(key) }
@@ -427,59 +427,6 @@ object EVHardware {
private const val PROP_CCU_ONBD_PLUG = 0x2140F43E // vendor int, AC socket
private const val PROP_CCU_OFFBD_PLUG = 0x2140F43F // vendor int, DC socket
private const val PROP_ENERGY_SINCE_CHARGE = 0x2160A1BB // vendor float kWh
/**
* `AAD_EPTCCTRGTSPD` in the head unit's `CarAdvancedAssistedDrivingManager`: by its name the
* powertrain's cruise control target speed. Unit and the value while cruise control is off
* are not established; it is passed on raw.
*/
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.
@@ -50,8 +50,6 @@ data class EnergySnapshot(
val regenLevelRaw: Int? = null,
/** 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,
/** Cruise control target speed as reported (`0x2160A15B`), unit not established; null unread. */
val cruiseTargetSpeedRaw: Float? = null,
) {
val hasVehicleData: Boolean
get() = socPercent != null || rangeKm != null || speedKmh != null ||
@@ -61,7 +59,7 @@ data class EnergySnapshot(
vehicleConsumedKwh != null || vehicleRegeneratedKwh != null ||
parked != null || climate.hasData || tirePressures.hasData ||
widestWindowPercent != null || driveModeRaw != null || regenLevelRaw != null ||
gearRaw != null || cruiseTargetSpeedRaw != null
gearRaw != null
}
data class TirePressureSnapshot(
@@ -29,8 +29,6 @@ interface EnergySignalSource {
fun parked(): Boolean?
/** The gear's raw code (MG4 SWI68: 1 P, 2 R, 3 N, 4 D). Null where unread. */
fun gearRaw(): Int? = null
/** The cruise control target speed, raw (unit not established). Null where unread. */
fun cruiseTargetSpeedRaw(): Float? = null
fun batteryVoltageV(): Float? = null
fun batteryCurrentA(): Float? = null
/** Widest window opening in percent; null where the firmware has no read. */
@@ -79,7 +77,6 @@ class EvHardwareEnergySignalSource(context: Context) : EnergySignalSource {
override fun vehicleRegeneratedKwh() = SaicCharging.regeneratedKwhSinceCharge()
override fun parked() = EVHardware.isVehicleInPark()
override fun gearRaw() = EVHardware.getGearRaw()
override fun cruiseTargetSpeedRaw() = EVHardware.getCruiseTargetSpeedRaw()
override fun batteryVoltageV() = EVHardware.getVendorBatteryVoltageV()
override fun batteryCurrentA() = EVHardware.getVendorBatteryCurrentA()
override fun widestWindowPercent() = SaicVehicleControl.widestWindowPercent()
@@ -161,7 +158,6 @@ class EnergyTelemetryReader(private val source: EnergySignalSource) {
driveModeRaw = source.driveModeRaw(),
regenLevelRaw = source.regenLevelRaw(),
gearRaw = source.gearRaw(),
cruiseTargetSpeedRaw = source.cruiseTargetSpeedRaw(),
)
/** Reads CP-004 candidates only when an unstable evidence capture explicitly requests them. */
+1 -3
View File
@@ -31,9 +31,7 @@ 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`) and the cruise control target speed
(`getCruiseTargetSpeedRaw`, `EnergySnapshot.cruiseTargetSpeedRaw`) with the other driver-assistance
properties read raw for debugging (`readAccDebug`, `ReadFailureLog.reason`). To take upstream again, merge three ways with
(`EVHardware.getGearRaw`, `EnergySnapshot.gearRaw`). 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
+19
View File
@@ -70,6 +70,13 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does *
- **Pack energy (`soe`) and capacity:** not published. The Live dashboard computes energy in pack
as SOC × usable capacity, from the hidden dashboard constant `usableKwh` (50.8 kWh for the
51 kWh battery) in `k8s/grafana/dashboards/live.json`.
- **Adaptive cruise control (set speed, distance to the car ahead, ACC state):** the head unit gets
none of it (SWI68, checked 2026-10-25). The `AAD_*` properties of `CarAdvancedAssistedDrivingManager`
exist and answer, but stay at their defaults with ACC engaged behind another car
(`AAD_EPTCCTRGTSPD` `0x2160A15B` 0.0, `AAD_ACC_OBJ_*` 0.0 / -128.0, modes 0). A signal recording
of all 1,194 values while the set speed was 101, 90 and 80 km/h shows none of those numbers
(also tried as m/s, mph and ×2/×10/×16/×64) outside the actual speed, and the ACC buttons send
no key broadcast. Most likely only the instrument cluster has it.
## To do
@@ -131,6 +138,18 @@ 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.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
EVHardware the set-speed read (`getCruiseTargetSpeedRaw`, `EnergySnapshot.cruiseTargetSpeedRaw`),
`readAccDebug` and `ReadFailureLog.reason` — EVHardware is again as before 2.27.3. The old log
files are deleted on start. The recorder stays as a general tool, renamed **Signal recorder**
(`SignalRecorder`, `SignalRecording`, `signal-recording-<time>.txt`). What the recording did
show, for later: `0x2140A15A` (`AAD_ELECVEHSYSMD`) flips 3/7 with accelerating and slowing,
`0x21409326` is the turn indicator (0 off, 1 and 2 the sides), `0x2140159D` (unnamed) shows
values like 300–1425 only while slowing down, `0x2140B970`/`0x2140B97B` are the parking sensors
in R. A full capture takes about 5 s on the car. Tests and lint; emulator: the recorder starts,
stops and saves under its new name. Not tried on the car.
- 2026-10-25 — App 2.32.0 (branch `styles-retro-future`): the **ACC recorder** on the Logs tab.
The 2.31.1 log on the car (a drive with ACC engaged behind another car) showed every assist
property constant: `cc 0.00`, `ica 0`, `tja 1`, `sas 0`, `sam 0`, `obj 0.00`, `dist 0.00`,
+2 -2
View File
@@ -34,8 +34,8 @@ android {
applicationId "com.stinnesbeck.nilsmg4"
minSdk 27
targetSdk 36
versionCode 130
versionName "2.32.0"
versionCode 131
versionName "2.32.1"
// 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,153 +0,0 @@
package com.evsuite.abrp;
import android.content.Context;
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.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
/**
* 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 {
/** 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";
private static ArrayDeque<String> lines;
private static String lastSteady;
private static long lastWriteMs;
private static String current = "—";
private CruiseLog() {}
/** 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();
}
/** 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) {
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);
}
}
}
/** All readings now, "—" before the first. */
static synchronized String current() {
return current;
}
/** 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 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.
}
}
}
@@ -363,6 +363,9 @@ public class MainActivity extends AppCompatActivity {
settingsPane, logPane };
UpdateLog.noteVersion(this, BuildConfig.VERSION_NAME);
// The cruise control logs of 2.31.1–2.32.0, gone with the feature (SPECS.md).
deleteFile("cruise_log.txt");
deleteFile("acc_recording.txt");
bindVehiclePane();
bindGauges();
bindCockpit();
@@ -375,10 +378,10 @@ public class MainActivity extends AppCompatActivity {
statusText = servicePane.findViewById(R.id.status_text);
callLogText = logPane.findViewById(R.id.call_log_text);
logPane.findViewById(R.id.flush_now_button).setOnClickListener(v -> manualFlush());
logPane.findViewById(R.id.acc_record_button).setOnClickListener(v -> {
if (AccRecorder.running()) AccRecorder.stop();
else AccRecorder.start(this);
refreshAccRecorder();
logPane.findViewById(R.id.signal_record_button).setOnClickListener(v -> {
if (SignalRecorder.running()) SignalRecorder.stop();
else SignalRecorder.start(this);
refreshSignalRecorder();
});
setUpPager();
@@ -1508,9 +1511,6 @@ 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 {
@@ -1522,14 +1522,6 @@ 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);
});
@@ -2134,11 +2126,11 @@ public class MainActivity extends AppCompatActivity {
}
/** The ACC recorder's button and what it has found so far. */
private void refreshAccRecorder() {
MaterialButton button = logPane.findViewById(R.id.acc_record_button);
button.setText(AccRecorder.running() ? R.string.btn_acc_stop : R.string.btn_acc_record);
TextView text = logPane.findViewById(R.id.acc_record_text);
java.util.List<String> status = AccRecorder.status(this);
private void refreshSignalRecorder() {
MaterialButton button = logPane.findViewById(R.id.signal_record_button);
button.setText(SignalRecorder.running() ? R.string.btn_signal_stop : R.string.btn_signal_record);
TextView text = logPane.findViewById(R.id.signal_record_text);
java.util.List<String> status = SignalRecorder.status(this);
text.setVisibility(status.isEmpty() ? View.GONE : View.VISIBLE);
// The rarest changes are the leads; the rest is in the saved recording.
if (status.size() > 81) status = status.subList(0, 81);
@@ -2146,18 +2138,11 @@ public class MainActivity extends AppCompatActivity {
}
private void refreshCallLog() {
refreshAccRecorder();
refreshSignalRecorder();
java.util.List<UploadLog.Entry> entries = AbrpUploadService.log().recent();
// 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 cruise control target speed as the car reports it, to match it on the car.
java.util.List<String> cruise = CruiseLog.lines(this);
if (!cruise.isEmpty()) {
sb.append(getString(R.string.cruise_log_title, CruiseLog.current()));
for (String line : cruise) 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);
@@ -23,12 +23,12 @@ import java.util.Locale;
import java.util.Map;
/**
* The ACC change recorder on the Logs tab: for {@link #DURATION_MS} it reads every value the
* The signal recorder on the Logs tab: for {@link #DURATION_MS} it reads every value the
* head unit offers (`VendorSurfaceProbe.capture`: every CarPropertyManager property and every
* value getter of the vendor objects, read only) again and again, and every steering-wheel key
* the head unit reports, into an {@link AccRecording}. The driver notes the times they switch
* the cruise control on, change the set speed and the distance, and switch it off; the values
* that change then are the leads.
* the head unit reports, into a {@link SignalRecording}. The driver notes the times they do
* what is to be found; the values that change then are the leads. (Built to find the adaptive
* cruise control, 2.32.0: on SWI68 the head unit gets none of it, see SPECS.md.)
*
* A full capture is far heavier than the Overview's reading, so the next one waits at least
* {@link #MIN_INTERVAL_MS} and at least as long as the last one took: the recorder uses at most
@@ -36,19 +36,19 @@ import java.util.Map;
* keeps the process), and stops by itself. At the end the recording goes to the app's storage
* and to the app's folder on every USB stick plugged in.
*/
final class AccRecorder {
final class SignalRecorder {
static final long DURATION_MS = 3 * 60_000L;
static final long MIN_INTERVAL_MS = 2_000L;
static final String FILE = "acc_recording.txt";
static final String FILE = "signal_recording.txt";
private static AccRecording recording;
private static SignalRecording recording;
private static volatile boolean running;
private static long startedMs;
private static String result;
private static Thread thread;
private AccRecorder() {}
private SignalRecorder() {}
static synchronized boolean running() {
return running;
@@ -57,11 +57,11 @@ final class AccRecorder {
static synchronized void start(Context context) {
if (running) return;
Context app = context.getApplicationContext();
recording = new AccRecording();
recording = new SignalRecording();
result = null;
startedMs = System.currentTimeMillis();
running = true;
thread = new Thread(() -> record(app), "acc-recorder");
thread = new Thread(() -> record(app), "signal-recorder");
thread.start();
}
@@ -71,7 +71,7 @@ final class AccRecorder {
}
private static void record(Context app) {
AccRecording rec = recording;
SignalRecording rec = recording;
BroadcastReceiver keys = new BroadcastReceiver() {
@Override public void onReceive(Context c, Intent intent) {
rec.note(System.currentTimeMillis(), "steering-wheel key: " + extras(intent.getExtras()));
@@ -107,7 +107,7 @@ final class AccRecorder {
}
}
String saved = save(app, rec);
synchronized (AccRecorder.class) {
synchronized (SignalRecorder.class) {
running = false;
result = saved;
}
@@ -122,10 +122,10 @@ final class AccRecorder {
}
/** Writes the recording; says where it went. */
private static String save(Context app, AccRecording rec) {
private static String save(Context app, SignalRecording rec) {
List<String> text = new ArrayList<>();
text.add("ACC recording, " + rec.captures() + " captures from "
+ AccRecording.time(startedMs) + ", app " + BuildConfig.VERSION_NAME);
text.add("Signal recording, " + rec.captures() + " captures from "
+ SignalRecording.time(startedMs) + ", app " + BuildConfig.VERSION_NAME);
text.add("");
text.add("Steering-wheel keys and notes:");
text.addAll(rec.notes());
@@ -137,7 +137,7 @@ final class AccRecorder {
text.addAll(rec.lines());
List<String> places = new ArrayList<>();
if (write(new File(app.getFilesDir(), FILE), text)) places.add("app");
String name = "acc-recording-" + new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US)
String name = "signal-recording-" + new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US)
.format(new Date(startedMs)) + ".txt";
File[] dirs = app.getExternalFilesDirs(null);
for (File dir : dirs == null ? new File[0] : dirs) {
@@ -167,10 +167,10 @@ final class AccRecorder {
if (recording == null) return out;
long elapsed = (running ? System.currentTimeMillis() : startedMs + DURATION_MS) - startedMs;
if (running) {
out.add(context.getString(R.string.acc_recorder_running, elapsed / 60_000, (elapsed / 1000) % 60,
out.add(context.getString(R.string.signal_recorder_running, elapsed / 60_000, (elapsed / 1000) % 60,
recording.captures()));
} else {
out.add(context.getString(R.string.acc_recorder_done, AccRecording.time(startedMs),
out.add(context.getString(R.string.signal_recorder_done, SignalRecording.time(startedMs),
recording.captures(), result == null ? "…" : result));
}
// Steering-wheel keys and the like first: few, and each one a lead.
@@ -11,14 +11,14 @@ import java.util.Map;
import java.util.TreeSet;
/**
* One recording of the ACC change recorder ({@link AccRecorder}): every value the head unit
* offers, captured again and again while the driver switches the adaptive cruise control on,
* changes the set speed and the distance, and switches it off. It keeps what changed, when,
* and per value how often and through which values, so the value that follows the cruise
* control stands out: it changes a few times, at the moments the driver noted.
* One recording of the signal recorder ({@link SignalRecorder}): every value the head unit
* offers, captured again and again while the driver does what is to be found (switches a
* feature on and off, changes a setting). It keeps what changed, when, and per value how
* often and through which values, so the value that follows the action stands out: it
* changes a few times, at the moments the driver noted.
* Pure Java, unit-tested.
*/
final class AccRecording {
final class SignalRecording {
/** Distinct values kept per key for the summary. */
static final int VALUES_KEPT = 6;
@@ -89,8 +89,8 @@ final class AccRecording {
}
/**
* Every value that changed, the rarest first (the cruise control's switch and set speed
* change a handful of times; speed and power all the time): key, how often, the values.
* Every value that changed, the rarest first (a switch the driver flips changes a handful
* of times; speed and power all the time): key, how often, the values.
*/
synchronized List<String> summary() {
List<String> keys = new ArrayList<>(changes.keySet());
+5 -5
View File
@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<com.evsuite.abrp.StableScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" android:clipToPadding="false">
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:paddingStart="24dp" android:paddingEnd="24dp" android:paddingTop="12dp" android:paddingBottom="12dp">
<!-- The ACC change recorder (AccRecorder): every value the head unit offers, recorded while driving. -->
<!-- The signal recorder (SignalRecorder): every value the head unit offers, recorded over 3 minutes. -->
<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:layout_marginBottom="16dp" android:background="@drawable/bg_glass">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_acc_recorder" android:textAppearance="@style/Text.EV.Eyebrow" />
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:text="@string/acc_recorder_hint" android:textAppearance="@style/Text.EV.Secondary" />
<com.google.android.material.button.MaterialButton android:id="@+id/acc_record_button" android:layout_width="match_parent" android:layout_height="72dp" android:layout_marginTop="16dp" android:text="@string/btn_acc_record" style="@style/Widget.EV.Button.Outlined" />
<TextView android:id="@+id/acc_record_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:textAppearance="@style/Text.EV.Body" android:fontFamily="monospace" android:textSize="17sp" android:lineSpacingExtra="8dp" android:textIsSelectable="true" android:visibility="gone" />
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_signal_recorder" android:textAppearance="@style/Text.EV.Eyebrow" />
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:text="@string/signal_recorder_hint" android:textAppearance="@style/Text.EV.Secondary" />
<com.google.android.material.button.MaterialButton android:id="@+id/signal_record_button" android:layout_width="match_parent" android:layout_height="72dp" android:layout_marginTop="16dp" android:text="@string/btn_signal_record" style="@style/Widget.EV.Button.Outlined" />
<TextView android:id="@+id/signal_record_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:textAppearance="@style/Text.EV.Body" android:fontFamily="monospace" android:textSize="17sp" android:lineSpacingExtra="8dp" android:textIsSelectable="true" android:visibility="gone" />
</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/label_call_log" android:textAppearance="@style/Text.EV.Eyebrow" />
+6 -7
View File
@@ -302,15 +302,14 @@
<!-- Call log -->
<string name="label_call_log">Recent uploads</string>
<string name="label_acc_recorder">ACC recorder</string>
<string name="acc_recorder_hint">Records every value the head unit offers for 3 minutes, plus the steering-wheel keys it reports, and lists what changed. Start it while driving, note the times you switch the cruise control on, change the set speed and the distance, and switch it off. With a USB stick plugged in, the whole recording is saved on it too (Android/data/…/files).</string>
<string name="btn_acc_record">Record 3 minutes</string>
<string name="btn_acc_stop">Stop recording</string>
<string name="acc_recorder_running">Recording: %1$d:%2$02d of 3:00, %3$d captures. Keys reported, then values changed so far, the rarest first:</string>
<string name="acc_recorder_done">Recording from %1$s: %2$d captures, %3$s. Keys reported, then values changed, the rarest first:</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>
<string name="btn_signal_stop">Stop recording</string>
<string name="signal_recorder_running">Recording: %1$d:%2$02d of 3:00, %3$d captures. Keys reported, then values changed so far, the rarest first:</string>
<string name="signal_recorder_done">Recording from %1$s: %2$d captures, %3$s. Keys reported, then values changed, the rarest first:</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">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,54 +0,0 @@
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 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 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));
}
}
@@ -9,20 +9,20 @@ import java.util.TreeMap;
import org.junit.Test;
/** The ACC change recorder's bookkeeping — pure JVM. */
public class AccRecordingTest {
/** The signal recorder's bookkeeping — pure JVM. */
public class SignalRecordingTest {
private static Map<String, String> capture(String speed, String acc) {
private static Map<String, String> capture(String speed, String feature) {
Map<String, String> m = new TreeMap<>();
m.put("speed", speed);
m.put("acc", acc);
m.put("feature", feature);
m.put("steady", "1");
return m;
}
@Test
public void rarestChangeFirst() {
AccRecording r = new AccRecording();
SignalRecording r = new SignalRecording();
r.add(0, capture("10", "0"));
r.add(2000, capture("20", "0"));
r.add(4000, capture("30", "120"));
@@ -30,14 +30,14 @@ public class AccRecordingTest {
r.add(8000, capture("50", "0"));
List<String> summary = r.summary();
assertEquals(2, summary.size());
assertEquals("acc 2× 0 → 120", summary.get(0));
assertEquals("feature 2× 0 → 120", summary.get(0));
assertTrue(summary.get(1).startsWith("speed 4× 10 → 20 → 30 → 40 → 50"));
assertEquals(5, r.captures());
}
@Test
public void appearingAndVanishingKeysAreChanges() {
AccRecording r = new AccRecording();
SignalRecording r = new SignalRecording();
Map<String, String> a = capture("1", "0");
Map<String, String> b = capture("1", "0");
b.put("new", "5");
@@ -50,13 +50,13 @@ public class AccRecordingTest {
@Test
public void longValuesAreShortened() {
String v = "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]";
assertEquals(AccRecording.VALUE_CHARS, AccRecording.shorten(v).length());
assertEquals("short", AccRecording.shorten("short"));
assertEquals(SignalRecording.VALUE_CHARS, SignalRecording.shorten(v).length());
assertEquals("short", SignalRecording.shorten("short"));
}
@Test
public void firstCaptureIsKeptWhole() {
AccRecording r = new AccRecording();
SignalRecording r = new SignalRecording();
r.add(0, capture("1", "0"));
r.note(500, "key 287 down");
assertEquals(5, r.lines().size());