Vehicle profile
How the host identifies the active car, what capabilities it exposes, and how the catalog filter + CarCommandRouter both consult one source of truth.
The host's CarProfile is the single object every gate consults when it needs to answer "what works on this car right now?". One source of truth, two consumers, four-tuple identity, five-tier fallback chain. This page is the canonical home for the model; guides and recipes link back here for the why.
Mini-apps run with full host capability — there is no manifest-side
gating. What you read at runtime from display.list().vehicle.capabilities
is the complete, authoritative answer for the active car. See
Capability detection for the
runtime patterns.
ProfileKey — the four-tuple identity
A vehicle profile is keyed on:
type ProfileKey = {
dilinkFamily: "di5.0" | "di5.1" | "unknown";
variantId: "l5" | "l8" | "l5l" | "l7" | "han_l" | "" /* aggregate */;
subTrim: "flagship" | "navigator" | "ultra" | "lidar" | "base" | "";
fingerprint: string /* ro.build.fingerprint, exact */;
};The four slots are most-coarse → most-precise on purpose. Empty strings are valid wire values and represent fallback aggregate rows the backend walks server-side:
| Slot empty | Reads | Use case |
|---|---|---|
fingerprint | sub-trim aggregate | Brand-new ROM build, sub-trim already probed |
subTrim | trim aggregate | Sub-trim couldn't be detected from outswver |
variantId | DiLink-default | Unknown trim on a known DiLink generation |
Why this shape and not just variantId: one "L5" badge ships in
multiple sub-trims (Flagship has massage seats + HUD; Navigator
doesn't), and the same Leopard 5 hardware ships under multiple ROM
fingerprints over its lifetime. Collapsing to variantId alone
loses both axes — the catalog filter would silently let a tile
through that depends on a feature only Flagship has, on a Navigator.
The host derives the active key on boot via CarIdentity:
// android/.../car/CarIdentity.kt — abridged
val outsw = systemProperty("apps.setting.product.outswver") // "34.1.23"
val vt = systemProperty("ro.vehicle.type") // "Di5.0_5.0UI"
val dilinkFamily = when {
vt.startsWith("Di5.1") -> "di5.1"
vt.startsWith("Di5.0") -> "di5.0"
else -> "unknown"
}
val subTrim = when (outsw?.take(7)) {
"34.1.23" -> SubTrim.ULTRA // L5 Ultra
"23.1.4" -> SubTrim.NAVIGATOR // L5 base trim
// ...
}Mini-app code never builds a ProfileKey — it reads the resolved
one from display.list().vehicle.
The 5-tier fallback chain
When the host needs the active car's capability set, it asks the backend for the precise ProfileKey row. The backend walks four tiers; if all four miss, the host falls back to its own compiled-in seed:
┌─────────────────────────────────────────────────────────┐
│ Tier 1 — Backend precise │
│ (dilink, variant, subTrim, fingerprint) exact │ ← isFallback: false
└─────────────────────────────────────────────────────────┘
│ miss
↓
┌─────────────────────────────────────────────────────────┐
│ Tier 2 — Backend sub-trim aggregate │
│ (dilink, variant, subTrim, "") — UNION across ROMs │ ← unknown_fingerprint
└─────────────────────────────────────────────────────────┘
│ miss
↓
┌─────────────────────────────────────────────────────────┐
│ Tier 3 — Backend trim aggregate │
│ (dilink, variant, "", "") — UNION across sub-trims │ ← unknown_sub_trim
└─────────────────────────────────────────────────────────┘
│ miss
↓
┌─────────────────────────────────────────────────────────┐
│ Tier 4 — Backend DiLink-default │
│ (dilink, "", "", "") — generation-only floor │ ← unknown_variant
└─────────────────────────────────────────────────────────┘
│ miss
↓
┌─────────────────────────────────────────────────────────┐
│ Tier 5 — Static seed (compiled into the APK) │
│ Per-trim VehicleProfile + DiShare/cluster heuristics │ ← static_default
└─────────────────────────────────────────────────────────┘Each tier sets isFallback: true and a fallbackReason string on
the snapshot so the UI can render a soft "best-effort" hint without
changing functional behaviour. Tier 1 is the only "no fallback"
state. Mini-apps consume the verdict via
VehicleCapabilitiesSnapshot.isFallback.
The backend's UPSERT semantics keep this monotonic: every probe report writes the precise row AND the three aggregate rows in one transaction, OR-ing the new bits into each. A flaky probe missing a cap can never strip it from any row — bits only ever turn on.
Who reads what
The same CarProfile is consumed by two gates and (occasionally) mini-app code:
| Reader | What it consults | When | Hot path? |
|---|---|---|---|
Catalog filter (_mergeInstallState) | capabilityBits only | Every catalog render | Yes — single bitmask AND per app |
| CarCommandRouter (host's CarGate) | actionSupport[actionId] | Every car-actuator dispatch | Yes — short-circuits known-bad before rate-limit + bridge round-trip |
| Mini-app JS | vehicle.capabilities / vehicle.capabilityBits from display.list | Per-render gating UI | No — cache the first snapshot for the session |
The catalog and CarGate consult the same provider — carProfileProvider
on the Dart side, backed by the i99dash/car_profile MethodChannel.
There's no second source of truth; if the host's idea of "what works
on this car" changes (probe sync, profile-override toggle), both
gates pick it up on the next provider invalidation.
When the snapshot is stale
The CarProfile snapshot is process-stable — the underlying
signals (getprop, ROM fingerprint, trim id) don't change without
a reboot. The host caches the snapshot for the process lifetime;
the only time it re-reads is after a successful
CapabilityProber.sync() writes a new backend overlay.
Mini-app code can rely on this: read the vehicle block from your
first display.list() call and cache it for the session. You don't
need to re-fetch on tab switch, app foreground/background, etc.
Static seed coverage
When tier 5 (compiled-in seed) answers, the bitmask comes from
CarProfileSeed which derives it from
VehicleProfile
booleans:
| Capability | Seeded as supported when |
|---|---|
display.read / pkg.read | Always (every BYD trim) |
pkg.launch.ivi / surface.write.ivi | Always (default display) |
pkg.launch.passenger / surface.write.passenger | profile.showFission2 == true |
pkg.launch.cluster.pixel / surface.write.cluster | profile.showCluster == true |
pkg.launch.cluster.icons | L5 family (l5 / l5u / l5l) — MCU mux works even when pixel control doesn't |
pkg.launch.dishare | Di5.0 trims (l5 / l5u) — passenger panel reachable only via DiShare |
cursor.write / gesture.dispatch | Always (a11y bridge granted by AdbBootstrap) |
ac.get / ac.set / door.set / window.set | Always (every BYD trim ships the binders) |
For the per-trim breakdown of which sub-trims have which capabilities by default, see the trim × capability matrix.
Display-side fields the profile contributes
Beyond the capability bits, the active VehicleProfile annotates
each entry in display.list().displays:
Field on DisplaySnapshot | Profile source | What it means |
|---|---|---|
hidden | hiddenDisplayIds.contains(id) | Shadow / duplicate of another addressable display (e.g. id 3 is the L8 cluster's mirror of id 5). Pickers should hide these. |
overrideLabel | overrideLabels[id] | Friendlier per-trim label. The reserved value 'Driver' (enumerated in RESERVED_OVERRIDE_LABELS on the SDK side) means the driver-eyeline display on this trim, regardless of role — cluster overlay on L8 / L5 Lidar, labeled passenger panel on Song Plus / L7 / HAN L. See the driver-eyeline guide section for the resolution priority. |
clusterAvailable | showCluster | True when the active profile says the cluster is reachable (L8 / L5 Lidar). Mirrors the pkg.launch.cluster.pixel cap bit; mini-apps gate cluster UI on either. |
cursorDisplayId / inputSourceDisplayId / zoomDisplayId | cursorRemap / inputRemap / zoomRemap | Per-id remaps for the launch-vs-input asymmetry on XDJA-virtualised clusters (see the cluster section). |
These fields are emitted starting with host 1.6 and surface in
the SDK's DisplaySnapshot interface from 2.0; older host
emissions arrive as undefined on each field, never as null or
empty defaults.
Probe-driven refinement
The static seed is conservative — it grants every BYD trim the
basic actuators because every BYD trim ships the binders.
Whether a specific actuator (e.g. set_seat_massage) actually
fires depends on hardware: Flagship has massage seats, Navigator
doesn't.
The CapabilityProber (host-side, not yet shipped — see roadmap)
runs once on first-boot for each unfamiliar (variant, sub-trim,
fingerprint) tuple. It tries each candidate cap, records the
verdict, and POSTs the result to the backend. The backend folds it
into the precise row + 3 aggregate rows. Subsequent boots on the
same car see Tier 1 directly; cars on the same sub-trim but a
fresh ROM see Tier 2; etc.
Quick Report — the
1-tap thumb-up/thumb-down on action-failure — feeds the same
aggregator from the user side. When enough thumbs-down corrections
accrue for a (ProfileKey, action) pair, the offline aggregator
flips the static seed to mark it as supported.
Worked example — Leopard 5 Flagship vs Navigator
A car-i99dash host on a Leopard 5 Flagship boots:
CarIdentityreadsoutsw=34.1.23 → SubTrim.ULTRA. Wait, that's Ultra, not Flagship — Flagship's outsw key isn't yet recorded.subTrimresolves tonull→ wire string"".- Host POSTs
GET /api/v1/vehicle-capabilities?dilinkFamily=di5.0&variantId=l5&subTrim=&fingerprint=BYD/.... - Backend walks: tier-1 miss (empty subTrim), tier-2 miss (same), tier-3 hits (the variant-aggregate row for L5 has accumulated probes from L5L / Ultra / Navigator).
- Backend returns
{capabilityBits: 0xXXXX, isFallback: true, fallbackReason: 'unknown_sub_trim'}. - Mini-app code reads
vehicle.capabilitiesfromdisplay.list(). The seat-massage cap bit isn't set — even though Flagship actually has massage seats — because the trim aggregate UNIONs over Navigator (which doesn't), so the bit isn't set. - UI hides the seat-massage control on this snapshot. User taps the (always-visible) "Try anyway" or invokes via voice; the daemon says yes.
- Quick Report banner: "Feature unavailable — your car's sub-trim wasn't detected. [Correct] [Works]".
- User taps Works. Backend logs the correction.
- Two months later, when 50 Flagship users have submitted similar
thumbs-down on
set_seat_massage, the offline aggregator stamps the bit on (variant=l5, subTrim=flagship). Future Flagship hosts that successfully detect their sub-trim get Tier-2 with the bit set. Hosts that still detect as unknown sub-trim continue getting tier-3 — but Tier-3 itself updates over time because it's a UNION over all sub-trims that ever probed.
That's the loop. Static seed for safety, probe + Quick Report for empirical refinement, fallback chain so unknown cars still launch.
Related
ProfileKey— the type reference.VehicleCapabilitiesSnapshot— the backend wire shape.VEHICLE_CAPABILITIES— the canonical capability list.- Capability detection guide — runtime patterns for reading the snapshot.
- Trim × capability matrix — what's seeded per trim.