App 2.40.3: the signal recorder listens to the assistance properties

The ACC recording of 2.32.0 only polled, and the AAD_* properties read
their defaults. The head unit's own assistance manager never polls them:
it subscribes to each for change events, and a vendor HAL may fill a
value only while someone listens. EVHardware gains a read-only
listenToProperties and AssistBlock (global vendor properties 0x21x0A1xx,
all 108 AAD_* ids); the recorder subscribes to that block for its three
minutes and lists the properties heard, those with changing values first.

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-27 00:55:26 +01:00
co-authored by Claude Opus 5.5
parent f7c489858e
commit e90031d6b0
12 changed files with 282 additions and 14 deletions
@@ -7,6 +7,8 @@ import android.content.IntentFilter;
import android.os.Bundle;
import android.os.Environment;
import com.evsuite.hardware.AssistBlock;
import com.evsuite.hardware.EVHardware;
import com.evsuite.hardware.probe.VendorSurfaceProbe;
import java.io.File;
@@ -26,7 +28,11 @@ import java.util.Map;
* 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 a {@link SignalRecording}. The driver notes the times they do
* the head unit reports, into a {@link SignalRecording}. It also listens, as the head unit's own
* driver-assistance manager does, to the properties of the assistance block
* ({@link AssistBlock}): subscribed for change events the whole time, because a value that is
* only filled while someone listens reads as its default when polled (2.40.3, still the
* cruise control). 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.)
*
@@ -85,6 +91,21 @@ final class SignalRecorder {
} catch (RuntimeException ignored) {
// No key reports: the properties are recorded all the same.
}
EVHardware.PropertyListening listening = null;
try {
listening = EVHardware.INSTANCE.listenToProperties(AssistBlock.INSTANCE::contains,
(id, area, value) -> {
rec.event(System.currentTimeMillis(),
String.format(Locale.US, "CPM 0x%08X/%d", id, area), value);
return kotlin.Unit.INSTANCE;
});
} catch (Throwable t) {
rec.note(System.currentTimeMillis(), "listening failed: " + t.getClass().getSimpleName());
}
rec.note(System.currentTimeMillis(), listening == null
? "listening to the assistance properties: not possible (no car service)"
: "listening to " + listening.getSubscribed() + " assistance properties ("
+ listening.getRefused() + " refused)");
try {
long end = startedMs + DURATION_MS;
while (running && System.currentTimeMillis() < end) {
@@ -99,6 +120,13 @@ final class SignalRecorder {
} catch (Throwable t) {
rec.note(System.currentTimeMillis(), "recorder stopped: " + t.getClass().getSimpleName());
} finally {
if (listening != null) {
try {
listening.close();
} catch (Throwable ignored) {
// the car service is gone
}
}
if (keysRegistered) {
try {
app.unregisterReceiver(keys);
@@ -130,7 +158,10 @@ final class SignalRecorder {
text.add("Steering-wheel keys and notes:");
text.addAll(rec.notes());
text.add("");
text.add("Changed values, the rarest first:");
text.add("Assistance properties heard (" + rec.events() + " change events), changing ones first:");
text.addAll(rec.heard());
text.add("");
text.add("Changed values (polled), the rarest first:");
text.addAll(rec.summary());
text.add("");
text.add("Everything, in order:");
@@ -168,14 +199,17 @@ final class SignalRecorder {
long elapsed = (running ? System.currentTimeMillis() : startedMs + DURATION_MS) - startedMs;
if (running) {
out.add(context.getString(R.string.signal_recorder_running, elapsed / 60_000, (elapsed / 1000) % 60,
recording.captures()));
recording.captures(), recording.events()));
} else {
out.add(context.getString(R.string.signal_recorder_done, SignalRecording.time(startedMs),
recording.captures(), result == null ? "…" : result));
recording.captures(), recording.events(), result == null ? "…" : result));
}
// Steering-wheel keys and the like first: few, and each one a lead.
List<String> notes = recording.notes();
out.addAll(notes.subList(Math.max(0, notes.size() - 10), notes.size()));
// Then what the listening heard, changing properties first.
List<String> heard = recording.heard();
for (String line : heard.subList(0, Math.min(12, heard.size()))) out.add("heard " + line);
out.addAll(recording.summary());
return out;
}
@@ -16,6 +16,10 @@ import java.util.TreeSet;
* 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.
*
* Besides the captures (polled), it keeps the change events of the properties the recorder
* listens to ({@link #event}): how many per property and through which values. A value the
* head unit only fills while someone listens shows up there and nowhere else.
* Pure Java, unit-tested.
*/
final class SignalRecording {
@@ -31,6 +35,10 @@ final class SignalRecording {
private final List<String> notes = new ArrayList<>();
private final Map<String, Integer> changes = new HashMap<>();
private final Map<String, LinkedHashSet<String>> seen = new HashMap<>();
private int events;
private final Map<String, String> lastEvent = new HashMap<>();
private final Map<String, Integer> eventCounts = new HashMap<>();
private final Map<String, LinkedHashSet<String>> eventValues = new HashMap<>();
static String time(long ms) {
return new SimpleDateFormat("HH:mm:ss", Locale.US).format(new Date(ms));
@@ -65,6 +73,45 @@ final class SignalRecording {
notes.add(time(ms) + " " + what);
}
/**
* A change event from a property listened to. Every one is counted; the timeline gets a
* line only when the value differs from that property's last event.
*/
synchronized void event(long ms, String key, String value) {
events++;
eventCounts.merge(key, 1, Integer::sum);
String before = lastEvent.put(key, value);
if (value.equals(before)) return;
lines.add(time(ms) + " heard " + key + " = " + value);
LinkedHashSet<String> values = eventValues.computeIfAbsent(key, k -> new LinkedHashSet<>());
if (values.size() < VALUES_KEPT) values.add(shorten(value));
}
synchronized int events() {
return events;
}
/**
* Every property that sent events: first those that sent different values, the fewest
* events first (the leads), then those that only ever repeated one value.
*/
synchronized List<String> heard() {
List<String> keys = new ArrayList<>(eventCounts.keySet());
keys.sort((a, b) -> {
boolean va = eventValues.get(a).size() > 1, vb = eventValues.get(b).size() > 1;
if (va != vb) return va ? -1 : 1;
int c = Integer.compare(eventCounts.get(a), eventCounts.get(b));
return c != 0 ? c : a.compareTo(b);
});
List<String> out = new ArrayList<>();
for (String key : keys) {
LinkedHashSet<String> values = eventValues.get(key);
out.add(key + " " + eventCounts.get(key) + " events " + String.join(" → ", values)
+ (values.size() >= VALUES_KEPT ? " …" : ""));
}
return out;
}
/** The notes alone, in order. */
synchronized List<String> notes() {
return new ArrayList<>(notes);