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);
+3 -3
View File
@@ -223,11 +223,11 @@
<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="signal_recorder_hint">Records every value the head unit offers for 3 minutes, listens to the driver-assistance properties for every change they report, and notes the steering-wheel keys; then 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="signal_recorder_running">Recording: %1$d:%2$02d of 3:00, %3$d captures, %4$d change events heard. Keys reported, then assistance properties heard, then values changed so far, the rarest first:</string>
<string name="signal_recorder_done">Recording from %1$s: %2$d captures, %3$d change events heard, %4$s. Keys reported, then assistance properties heard, 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="btn_flush_now">Upload now</string>
@@ -63,4 +63,22 @@ public class SignalRecordingTest {
assertTrue(r.lines().get(4).endsWith("key 287 down"));
assertEquals(1, r.notes().size());
}
@Test
public void heardPropertiesWithChangingValuesComeFirst() {
SignalRecording r = new SignalRecording();
for (int i = 0; i < 5; i++) r.event(i * 1000, "CPM 0x2140A10A/0", "1"); // repeats one value
r.event(1000, "CPM 0x2160A15B/0", "0.0");
r.event(2000, "CPM 0x2160A15B/0", "101.0");
r.event(3000, "CPM 0x2160A15B/0", "101.0");
r.event(4000, "CPM 0x2160A15B/0", "90.0");
List<String> heard = r.heard();
assertEquals(2, heard.size());
assertEquals("CPM 0x2160A15B/0 4 events 0.0 → 101.0 → 90.0", heard.get(0));
assertEquals("CPM 0x2140A10A/0 5 events 1", heard.get(1));
assertEquals(9, r.events());
// The timeline holds only the changes: 0.0, 101.0, 90.0 and the first 1.
long lines = r.lines().stream().filter(l -> l.contains("heard ")).count();
assertEquals(4, lines);
}
}