i99dash docs
BYD API

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:

  • AutoCarRegistry holds an in-memory name → 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:

dtSubsystem
1000AC
1001BODY (cluster, tires, battery stats)
1023SETTING / SENSOR
1038GEAR
1040WHEEL
1041DOORLOCK
1045TIRE

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:

ValueMeaning
-10011Feature key not bound on this trim
-10013Statistics-class signal not yet computed
-10006Value not initialised (CAN signal hasn't fired)
-10005Permission denied for current UID
-10001Framework still booting
65535uint16 "no data"
Int.MIN_VALUEDevice-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:

OperationProcess
setInt (writes)Shell-UID daemon — needs system permission
getIntEither 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:

  1. Read line 0; compare against current catalogSignature(catalog).
  2. If they match, load all tuples, subscribe push, do one bulk getIntArray per dt to seed initial values, return in ~150 ms.
  3. If they mismatch, invalidate and run a fresh probe.

Live count expectations

TrimApproximate 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

ScenarioBehaviour
Daemon not ready at probe timeEngine 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 dtsPartial 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 constantSignature 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).

On this page