i99dash docs
Guides

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:

  1. Declare location.read in your manifest permissions — the host gates the handler against it.
  2. Wait for the host bridge first. On Di5.0 callHandler is undefined for a beat after your script runs, so calling it (or MiniAppClient.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

On this page