Auto-discover
How AutoCarRegistry turns the BYD framework catalog into a per-car live feature index — once at boot, push-driven thereafter, with zero per-trim curation.
The framework catalog tells you what BYD's software knows about. The auto-discover engine tells you what your specific car actually exposes. It's a one-shot brute-force probe that runs on first boot, then caches its result and rides on push subscriptions for live values.
What it does
for each name in BydAutoFeatureIdsCatalog (~21k):
for each dt in WARM_DT (7 device-types):
value = framework.getInt(dt, catalog[name])
if value is not a sentinel:
register (name, dt, key, value) # first dt wins per name
subscribe push for (dt, key)After this runs once:
AutoCarRegistryholds an in-memoryname → Entry(dt, key, value)index for every live feature.- Every entry has a push subscription. Value changes arrive via the framework's own dispatcher, no polling.
- The next boot reads the
(name, dt, key)tuples from a disk cache in ~150 ms and re-subscribes to push immediately.
The WARM_DT device-types
BYD's framework partitions features by device-type — an integer that picks which AIDL service the call routes through. The engine registers a push device for seven of them:
| dt | Subsystem |
|---|---|
| 1000 | AC |
| 1001 | BODY (cluster, tires, battery stats) |
| 1023 | SETTING / SENSOR |
| 1038 | GEAR |
| 1040 | WHEEL |
| 1041 | DOORLOCK |
| 1045 | TIRE |
Features scoped to other device-types (ADAS, mirrors, energy
management) are not discovered today. The roadmap covers expanding
this set as we ship the corresponding BydPushDevice subclasses.
The probe
The actual call sequence:
catalog size ~21,000
WARM_DT 7 device-types
chunk size 64 keys per call
binder round-trips ceil(21000/64) × 7 ≈ 2,300
wall-clock ~2-5 s on real hardware (first boot only)Each chunk goes through getIntArray(dt, keys[]) on the shell-UID
daemon — one TCP round-trip per chunk, not per key.
Subsequent boots skip the probe entirely. The disk cache is
signature-keyed (v1|catalog.size|firstName|lastName); a ROM bump
changes the signature and forces a fresh probe.
The sentinel filter
A value counts as live only if it isn't one of the BYD framework error codes:
| Value | Meaning |
|---|---|
-10011 | Feature key not bound on this trim |
-10013 | Statistics-class signal not yet computed |
-10006 | Value not initialised (CAN signal hasn't fired) |
-10005 | Permission denied for current UID |
-10001 | Framework still booting |
65535 | uint16 "no data" |
Int.MIN_VALUE | Device-type not registered |
Sentinels are silently dropped. The catalog name is not added to the registry; no subscription is created.
First-wins dedup
A few catalog names resolve under multiple device-types (typically
because the constant pool has duplicates from the bare/prefixed
storage). The first non-sentinel value across the WARM_DT walk
wins; subsequent dts that also return live values are skipped via
putIfAbsent. One name produces at most one registry entry.
Push subscribe
For every live entry, the engine calls:
push.subscribe(rec.dt, rec.key) { newValue ->
rec.value = newValue
rec.lastUpdateMs = now
onChange?.invoke(rec.name, newValue)
}The push manager owns one BydPushDevice per WARM_DT. The
framework dispatches AbsBYDAutoDevice.onPostEvent(IBYDAutoEvent)
on every value change; the manager fans out to per-(dt, key)
callback lists.
Why push runs in-app, not in the daemon — verified empirically
on DiLink 5.1: the framework dispatches callbacks only to instances
constructed with a bound Application context. The shell-UID
daemon uses ActivityThread.getSystemContext() and never receives
a frame. The split is:
| Operation | Process |
|---|---|
setInt (writes) | Shell-UID daemon — needs system permission |
getInt | Either path works |
Push (onPostEvent) | App process only |
Cache fast-path
After a successful probe, the engine writes
files/auto_registry.cache:
v1|21043|Ac.AC_BLOW_MODE|Wheel.WHEEL_TIRE_TEMP_RR
Ac.AC_CYCLE_MODE 1000 1610612737
Ac.AC_POWER_STATE 1000 1610612738
…Line 0 is the signature; subsequent lines are tab-separated
(name, dt, key) tuples. Values are intentionally not cached —
push fills them in seconds, and a stale cached value would surface
as "ancient last-known" before the first frame arrives.
On the next boot:
- Read line 0; compare against current
catalogSignature(catalog). - If they match, load all tuples, subscribe push, do one bulk
getIntArrayper dt to seed initial values, return in ~150 ms. - If they mismatch, invalidate and run a fresh probe.
Live count expectations
| Trim | Approximate live entries |
|---|---|
| Han L4 | ~7,500 |
| Tang L4 | ~8,200 |
| Leopard 8 Flagship | ~9,500 |
| Yangwang U8 (with ADAS dt probed) | ~10,200 |
These numbers are catalog-size-stable across DiLink 5.1+. A ROM bump usually moves them by ±200.
See Live vs. known for the full math behind why ~21k catalog → ~9k live.
Observability
The engine exposes three stats surfaces:
final stats = await CarBridge(cfg).registryStats();
// {built: true, totalEntries: 9579, pushFramesReceived: 142315}And writes a diagnostic dump to files/auto_registry.txt after
every build, sortable and grep-friendly.
Edge cases
| Scenario | Behaviour |
|---|---|
| Daemon not ready at probe time | Engine waits up to 30 s. After timeout, registry stays unbuilt; cold-path readStatus continues to work via the daemon batch fallback. |
BydPushDevice construction fails for some dts | Partial coverage. Other dts still register; inAppPush.stats().devicesRegistered shows which succeeded. |
| Framework class absent (non-BYD device) | Catalog is empty, registry never builds, every value() returns null. Dart consumers degrade to "feature unsupported". |
| ROM bump renames a constant | Signature mismatch on next boot → fresh probe. The renamed constant flows in under its new name. Any consumer watching the old name silently stops receiving updates (caller's responsibility to handle). |
3D mini-apps
How to build a real-time 3D mini-app of the active car — load the GLB via `client.car.asset`, animate it with the catalog signals, and switch paint/wheels/glass variants.
Live vs. known
Why the BYD framework reports ~21k catalog entries but your car only exposes ~9k live features — the dedup, the device-type coverage, and the write-only commands that account for the gap.