i99dash docs
BYD API

Bridge surfaces

The five method-channel verbs and the registry EventChannel that the Dart SDK consumes — wire shapes, threading, error envelopes.

The whole BYD API reaches Dart through one MethodChannel and one EventChannel. Everything else builds on these two surfaces.

The bridge

┌────────────────────────────────────────────────────────────────────┐
│ MethodChannel  i99dash/car                                         │
│   allFeaturesAuto      → Map<String, int>   live registry snapshot │
│   allKnownFeatures     → Map<String, int>   full framework catalog │
│   labelToCatalog       → Map<String, String>   legacy bridge       │
│   registryStats        → Map<String, Object?>  diagnostic          │
│   watchdogStats        → Map<String, Object?>  daemon health       │
├────────────────────────────────────────────────────────────────────┤
│ EventChannel   i99dash/car/registry                                │
│   per-feature push deltas as { name: String, value: int }          │
└────────────────────────────────────────────────────────────────────┘

There are two additional event channels (i99dash/car/status, i99dash/car/observers) that the legacy label-keyed status path and Phase-9 ContentProvider observers use. They are documented elsewhere (see The bridge) and are not part of the BYD API end-state surface — new code should use the registry channel.

Method channel verbs

allFeaturesAuto

Future<Map<String, int>> allFeaturesAuto();

Live snapshot of every catalog name currently bound on this car (~9k on Leopard 8). Keys are full catalog names (prefixed where applicable), values are the most-recent push value.

Returns an empty map until AutoCarRegistry.build() completes (~5 s after app start on first boot, ~150 ms on subsequent boots from the disk cache).

Use this for the seed read; subscribe to the registry EventChannel for deltas.

final live = await CarBridge(cfg).allFeaturesAuto();
print(live['Ac.AC_POWER_STATE']);  // 1
print(live.length);                 // 9579

allKnownFeatures

Future<Map<String, int>> allKnownFeatures();

The full framework catalog (~21k entries on DiLink 5.1+). Values are the framework feature-id constants, not runtime values. Stable across boots on the same ROM.

Use this for capability matrices, dev tools, or any "what's possible" surface. Diff against allFeaturesAuto() to find names the framework defines but this car doesn't expose.

final known = await CarBridge(cfg).allKnownFeatures();
final live  = await CarBridge(cfg).allFeaturesAuto();
final gap   = known.keys.toSet()..removeAll(live.keys);
// gap.length ≈ 12000 on Leopard 8 — expected, see /docs/byd-api/live-vs-known

labelToCatalog

Future<Map<String, String>> labelToCatalog();

The bridge between the legacy StatusKey.label key space and the catalog-name key space the registry uses.

final m = await CarBridge(cfg).labelToCatalog();
m['door_lock'];   // "Door.DOOR_LOCK_COMMAND_AREA_LEFT_FRONT"
m['battery_pct']; // "Statistic.STATISTIC_SOC_BATTERY_PERCENTAGE"

Pulled once at SDK construct; stable for the app lifetime.

Use this if you're migrating a label-keyed consumer (the legacy dashboard widgets) onto the catalog-name-keyed registry. New code should not need it.

registryStats

Future<Map<String, dynamic>> registryStats();

Diagnostic snapshot.

{
  "built": true,
  "totalEntries": 9579,
  "pushFramesReceived": 142315
}

pushFramesReceived increasing over time means the framework is actively dispatching to us. Stuck at zero with built: true means push registration succeeded but the framework isn't sending — almost always a wrong-context construction bug.

watchdogStats

Future<Map<String, dynamic>> watchdogStats();

Daemon health.

{
  "pingsTotal": 1234,
  "pingsOk": 1231,
  "consecutiveFailures": 0,
  "retriesAttempted": 2,
  "retriesSucceeded": 2,
  "currentBackoffMs": 30000,
  "lastRetryAtMs": 1747000000000
}

The watchdog auto-recovers the shell-UID daemon (used for writes) after 3 consecutive ping failures, with exponential backoff capped at 5 minutes.

Registry EventChannel

const channel = EventChannel('i99dash/car/registry');
channel.receiveBroadcastStream().listen((event) {
  if (event is! Map) return;
  final name  = event['name']  as String?;
  final value = event['value'] as int?;
  if (name == null || value == null) return;
  // …
});

The single push path from the framework to Dart. Every push frame that the in-app BydPushDevice receives forwards onto this channel as {name, value}. Latency is ~30–50 ms p99 from CAN frame to EventSink (one framework dispatch + one main-thread post).

You normally don't subscribe directly — CarRegistryClient owns the single subscription app-wide; every consumer reads from its per-name broadcast controllers via featureValueProvider.

Threading

  • Push frames arrive on the framework's dispatcher thread (Binder worker, varies per ROM).
  • EventSink.success must run on the platform main thread, so the Kotlin side hops via Handler(Looper.getMainLooper()).post.
  • Each emit pays one main-thread scheduler tick. Negligible at observed push rates (low hundreds/sec peak under heavy CAN activity).

Single-listener

AutoCarRegistry.onChange is a single ((name, value) → Unit)? slot, not a list. The bridge writes to it on EventChannel.onListen and clears it on onCancel. Multi-consumer fan-out happens on the Dart side through CarRegistryClient's broadcast controllers.

Error envelopes

Method calls that fail surface as PlatformException:

try {
  final m = await CarBridge(cfg).allFeaturesAuto();
} on PlatformException catch (e) {
  // e.code    — exception class name
  // e.message — message
  // e.details — stack trace string
}

In practice the CarBridge wrapper catches PlatformException and returns an empty map for read-side surfaces, so callers don't need to handle it.

Method calls run on a background queue

All i99dash/car handlers run on a Flutter background task queue (messenger.makeBackgroundTaskQueue()). Daemon TCP and Binder reflection are safe to call from a method handler — no NetworkOnMainThreadException risk.

What's deliberately NOT in this surface

  • No per-feature getInt(name) method. Use the live snapshot or the EventChannel. A per-feature method would re-introduce the curation cost the registry was built to eliminate.
  • No write method on this channel. Writes go through runAction(actionId). Action IDs are defined in the textproto registry; the engine resolves them to the right (dt, key, value).
  • No subsystem-specific channels. One method channel, one event channel, every feature. The Dart-side featureValueProvider family does the per-name fanout.

On this page