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); // 9579allKnownFeatures
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-knownlabelToCatalog
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.successmust run on the platform main thread, so the Kotlin side hops viaHandler(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
featureValueProviderfamily does the per-name fanout.
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.
Consumer SDK
CarRegistryClient and featureValueProvider — the minimal Dart surface for consuming the BYD framework. Discovery, reactive watch, write.