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:
@@ -0,0 +1,24 @@
|
||||
package com.evsuite.hardware
|
||||
|
||||
/**
|
||||
* The block of vendor properties the head unit's driver-assistance manager
|
||||
* (`CarAdvancedAssistedDrivingManager`, its `AAD_*` ids) lives in: global vendor properties
|
||||
* `0x21x0A1xx`, of any value type — `0x2140A101` (int) to `0x2160A15B` (float, `AAD_EPTCCTRGTSPD`,
|
||||
* by its name the cruise control's target speed) and the rest of the same block. Used to choose
|
||||
* what the signal recorder listens to; nothing here is ever written. Pure, unit-tested.
|
||||
*/
|
||||
object AssistBlock {
|
||||
|
||||
private const val GROUP_MASK = 0xF0000000.toInt()
|
||||
private const val GROUP_VENDOR = 0x20000000
|
||||
private const val AREA_MASK = 0x0F000000
|
||||
private const val AREA_GLOBAL = 0x01000000
|
||||
private const val BLOCK_MASK = 0x0000FF00
|
||||
private const val BLOCK = 0x0000A100
|
||||
|
||||
/** Whether [propertyId] is a global vendor property in the assistance block. */
|
||||
fun contains(propertyId: Int): Boolean =
|
||||
propertyId and GROUP_MASK == GROUP_VENDOR &&
|
||||
propertyId and AREA_MASK == AREA_GLOBAL &&
|
||||
propertyId and BLOCK_MASK == BLOCK
|
||||
}
|
||||
@@ -320,6 +320,105 @@ object EVHardware {
|
||||
ignitionCallbacks.remove(callback)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Listening to properties (the app's signal recorder) — read only
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** A subscription made by [listenToProperties]; [close] ends it. */
|
||||
class PropertyListening internal constructor(
|
||||
/** Properties subscribed to, and those the car service refused. */
|
||||
val subscribed: Int,
|
||||
val refused: Int,
|
||||
private val stop: () -> Unit,
|
||||
) : AutoCloseable {
|
||||
override fun close() = stop()
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to every CarPropertyManager property whose id [admit] accepts, the way the
|
||||
* head unit's own managers do (`CarAdvancedAssistedDrivingManager` registers each of its
|
||||
* `AAD_*` ids for change events and never polls them), and hands each change event to
|
||||
* [onEvent] as (property id, area, value as text) on the car service's thread. A vendor HAL
|
||||
* may fill a property from the car network only while someone listens, so a poll alone can
|
||||
* read its default. ON_CHANGE properties are asked for every change, CONTINUOUS ones for
|
||||
* 1 Hz within their limits. Read only: nothing is set. Null when there is no
|
||||
* CarPropertyManager or no way to subscribe; [PropertyListening.close] unsubscribes.
|
||||
*/
|
||||
fun listenToProperties(admit: (Int) -> Boolean, onEvent: (Int, Int, String) -> Unit): PropertyListening? {
|
||||
val cpm = sCarPropertyManager ?: return null
|
||||
return try {
|
||||
val register = cpm.javaClass.methods.firstOrNull {
|
||||
it.name == "registerCallback" && it.parameterCount == 3
|
||||
} ?: return null
|
||||
val unregister = cpm.javaClass.methods.firstOrNull {
|
||||
it.name == "unregisterCallback" && it.parameterCount == 1
|
||||
}
|
||||
val callbackType = register.parameterTypes[0]
|
||||
if (!callbackType.isInterface) return null
|
||||
val proxy = Proxy.newProxyInstance(callbackType.classLoader, arrayOf(callbackType)) { self, method, args ->
|
||||
when (method.name) {
|
||||
"onChangeEvent" -> {
|
||||
val cpv = args?.getOrNull(0)
|
||||
if (cpv != null) {
|
||||
try {
|
||||
val type = cpv.javaClass
|
||||
val id = type.getMethod("getPropertyId").invoke(cpv) as Int
|
||||
val area = type.getMethod("getAreaId").invoke(cpv) as Int
|
||||
onEvent(id, area, com.evsuite.hardware.probe.VendorSurfaceRules.render(
|
||||
type.getMethod("getValue").invoke(cpv)))
|
||||
} catch (e: Exception) {
|
||||
AppLogger.d(TAG, "listen: event unreadable: ${e.readFailureReason()}")
|
||||
}
|
||||
}
|
||||
null
|
||||
}
|
||||
"onErrorEvent" -> {
|
||||
val id = args?.getOrNull(0) as? Int
|
||||
val area = args?.getOrNull(1) as? Int
|
||||
if (id != null) onEvent(id, area ?: AREA_GLOBAL, "error event")
|
||||
null
|
||||
}
|
||||
"equals" -> self === args?.getOrNull(0)
|
||||
"hashCode" -> System.identityHashCode(self)
|
||||
"toString" -> "PropertyListening"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
val configs = cpm.javaClass.getMethod("getPropertyList").invoke(cpm) as? List<*> ?: return null
|
||||
var subscribed = 0
|
||||
var refused = 0
|
||||
for (config in configs) {
|
||||
if (config == null) continue
|
||||
val type = config.javaClass
|
||||
val id = runCatching { type.getMethod("getPropertyId").invoke(config) as Int }.getOrNull() ?: continue
|
||||
if (!admit(id)) continue
|
||||
val continuous = runCatching { type.getMethod("getChangeMode").invoke(config) as Int }.getOrNull() == 2
|
||||
val rate = if (!continuous) 0f else {
|
||||
val min = runCatching { type.getMethod("getMinSampleRate").invoke(config) as Float }.getOrNull() ?: 0f
|
||||
val max = runCatching { type.getMethod("getMaxSampleRate").invoke(config) as Float }.getOrNull() ?: 1f
|
||||
1f.coerceIn(min, maxOf(min, max))
|
||||
}
|
||||
try {
|
||||
if (register.invoke(cpm, proxy, id, rate) == false) refused++ else subscribed++
|
||||
} catch (e: Exception) {
|
||||
refused++
|
||||
AppLogger.d(TAG, "listen: 0x%08X refused: %s".format(id, e.readFailureReason()))
|
||||
}
|
||||
}
|
||||
AppLogger.i(TAG, "listening to $subscribed properties ($refused refused)")
|
||||
PropertyListening(subscribed, refused) {
|
||||
try {
|
||||
unregister?.invoke(cpm, proxy)
|
||||
} catch (e: Exception) {
|
||||
AppLogger.d(TAG, "listen: unsubscribe failed: ${e.readFailureReason()}")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
AppLogger.w(TAG, "listenToProperties: ${e.readFailureReason()}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/** Unregisters the CarPropertyManager proxy (call from Service.onDestroy). */
|
||||
fun unregisterIgnitionPropertyCallback() {
|
||||
val cpm = sCarPropertyManager ?: return
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.evsuite.hardware
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class AssistBlockTest {
|
||||
|
||||
@Test
|
||||
fun `the assistance manager's properties are in the block`() {
|
||||
// First and last int, the target speed and the object distance (floats).
|
||||
for (id in intArrayOf(0x2140A101, 0x2140A171, 0x2160A15B, 0x2160A12C, 0x2160A114)) {
|
||||
assertTrue("0x%08X".format(id), AssistBlock.contains(id))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `other properties are not`() {
|
||||
for (id in intArrayOf(
|
||||
0x2140A0FF, // the block before
|
||||
0x2140A201, // the block after
|
||||
0x11400400, // a standard (system) property
|
||||
0x2340A101, // not global
|
||||
0x0140A101, // no group
|
||||
)) {
|
||||
assertFalse("0x%08X".format(id), AssistBlock.contains(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user