Location + heading
How to source position + heading in v5 — the per-family `client.location` controller was removed; the unified `client.car` catalog will expose location signals in v5.1. Until then, use `navigator.geolocation` directly. Plus a heading/compass recipe.
v5 status — location signals land in v5.1. SDK v5 collapsed
every per-family controller into the single
CarController on
client.car. Position + heading will be exposed there under
category: 'location' once the host catalog ships them — none of
the brands wired into v5.0's catalog do yet. The legacy
client.location controller is gone (see
v5 migration). Until v5.1, use
the browser navigator.geolocation
API directly — it's what the old controller wrapped anyway and
the host pipes the WebView permission through the manifest. On
Di5.0 (Chromium 95) navigator.geolocation is broken — use the host
location.read bridge instead.
Discovering what's available
import { createClientOrSSR } from 'i99dash';
const client = createClientOrSSR();
if (!client) return; // SSR / no-host
// `category: 'location'` is empty in v5.0 — keep this call in
// place so v5.1's catalog auto-populates the UI with zero
// re-deploy.
const fixes = await client.car.list({ category: 'location' });
if (fixes.entries.length > 0) {
const off = await client.car.subscribe({
names: fixes.entries.map(e => e.name),
onEvent: (e) => render(e),
});
// …later
off();
}The catalog is the only source of truth for what signals a given
brand / host build ships — there's no compiled-in list in the SDK.
Today's BYD catalog ships climate, dynamics, cabin,
propulsion, safety, charging, doors, lights, sensors,
and statistics; location and navigation are queued for v5.1.
Until v5.1: navigator.geolocation directly
This works on Di5.1 and in a browser preview, but
navigator.geolocation is broken on Di5.0 (Chromium 95) — it never
resolves even with a live GNSS fix. If you target L5, read
Reading location on Di5.0 first and treat
the code below as the Di5.1/preview branch of your fallback chain.
On hosts whose WebView geolocation works, the standard browser API does
the job. The host (i99dash) gates per-origin permission against the
mini-app manifest via the same onGeolocationPermissionsShowPrompt
callback Chromium uses, so a mini-app that declared location.read in
its manifest gets positions inside the host the same way it gets them in
a browser preview.
function getFix(): Promise<GeolocationPosition> {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true,
timeout: 10_000,
maximumAge: 30_000,
});
});
}
try {
const pos = await getFix();
console.log({
lat: pos.coords.latitude,
lng: pos.coords.longitude,
heading: pos.coords.heading,
speedMps: pos.coords.speed,
accuracyM: pos.coords.accuracy,
at: new Date(pos.timestamp).toISOString(),
});
} catch (e) {
// GeolocationPositionError.code → PERMISSION_DENIED / POSITION_UNAVAILABLE / TIMEOUT
showStaleFallback();
}A 30-second-old cached fix is fine for weather refreshes, ETA estimates, AQI lookups; surveying isn't the use case.
Streaming updates
Use the standard watchPosition and fan it out to your renderer.
Cancel with clearWatch when the widget unmounts.
const watchId = navigator.geolocation.watchPosition(
(pos) => render(pos.coords),
(err) => console.warn('geolocation error', err),
{ enableHighAccuracy: true, maximumAge: 1_000 },
);
// ...later
navigator.geolocation.clearWatch(watchId);Reading location on Di5.0
On DiLink 5.0 (Chromium 95 — Song Plus, Leopard 5) and BYD
Leopard 8 / FangChengBao 8, navigator.geolocation.getCurrentPosition
times out with no error code, even with enableHighAccuracy: false
and a fresh GNSS fix sitting in Android's LocationManager. The frozen
Chromium stack on those WebViews silently swallows position updates from
the OEM's LocationManagerService. (Verified on a real Di5.0 Song Plus:
the car had a live 79-satellite fix while navigator.geolocation never
resolved — the app showed a default city until it switched to the bridge
below, which immediately returned the real coordinates.)
So on these vehicles navigator.geolocation is the fallback, not the
primary path. Read the host's native location.read bridge handler
instead — it reads straight from the OEM LocationManager and works on
Di5.0:
// Host bridge — works on Di5.0, where navigator.geolocation is dead.
const raw = await (window as any).flutter_inappwebview.callHandler('location.read');
const fix = raw?.data ?? raw; // { lat, lng, accuracyM, heading?, speedMps?, … }Two things make or break this:
- Declare
location.readin your manifestpermissions— the host gates the handler against it. - Wait for the host bridge first. On Di5.0
callHandlerisundefinedfor a beat after your script runs, so calling it (orMiniAppClient.fromWindow()) eagerly fails even though you are inside the host — the #1 cause of "can't get location on L5". Gate every bridge call behind a readiness wait: Wait for the host bridge.
The robust order of preference is location.read bridge →
navigator.geolocation (Di5.1 / browser preview) → last cached fix.
The shared l5compat.getLocation()
helper in the examples repo implements exactly that (awaiting
whenHostReady() first); copy it rather than re-deriving the order.
The v5.1 plan folds this into the unified car.* catalog under
category: 'location' (the host's Flutter side already wires
Geolocator to LocationManager).
Keep your client.car.list({ category: 'location' }) in place and it
lights up with no redeploy.
Heading and compass
GeolocationCoordinates.heading is GNSS course — the direction of
travel. It's null when stationary (course is undefined at zero
speed). For a magnetic compass (driver-facing rosette, qibla
finder, "point this way" indicator), GNSS heading isn't enough —
you need the device's magnetometer.
The SDK doesn't ship a compass controller; the
DeviceOrientationEvent
API in the WebView is the right primitive. A short helper:
function watchCompass(
onHeading: (deg: number) => void,
): () => void {
let installed = false;
const handler = (e: DeviceOrientationEvent) => {
// iOS exposes webkitCompassHeading (true heading); other
// platforms give us alpha (rotation around z-axis, ° from north).
const raw = (e as any).webkitCompassHeading ?? e.alpha ?? null;
if (raw === null) return;
// Normalise into [0, 360).
onHeading(((raw % 360) + 360) % 360);
};
const listenerName = 'deviceorientationabsolute' in window
? 'deviceorientationabsolute'
: 'deviceorientation';
window.addEventListener(listenerName, handler);
installed = true;
return () => {
if (!installed) return;
installed = false;
window.removeEventListener(listenerName, handler);
};
}
// Usage:
const offCompass = watchCompass((deg) => rotateRose(deg));
// ...later
offCompass();No SDK permission gates compass — DeviceOrientationEvent is part of
the WebView's standard surface and doesn't touch the bridge. Heads-up:
many car head-units ship no magnetometer, so DeviceOrientationEvent
may never fire — always handle the no-reading case (degrade to GNSS
course, or hide the rose) rather than leaving a stuck compass.
See also
CarController— unified car controller (location signals land here in v5.1)- v5 migration — why the per-family
LocationControllerwas removed - Host catalog status — what the v2 catalog ships now vs. v5.1
- Build for L5 + L8 — feature-detect + fallback so a missing source isn't a blank screen
Multi-display mini-apps
Render on the passenger screen, drive the cluster, build for cars with 1, 2, or 3 screens. The SDK's display + surface + gesture families end-to-end.
Error recovery
From a thrown SDK error or `success: false` envelope to the right user-facing recovery — retry, fallback, or report.