# CarDash — OBD-II and temperature design note _Status: updated in CarDash 1.2.10; OBD hardware still needs a real-car test. Last updated: 2026-11-02_ ## 1. Scope CarDash optionally reads live data from a Bluetooth Classic ELM327 adapter in the car's OBD-II port and shows it on the home page. Settings has separate **OBD-II**, **Speed** and **Temperature** cards. Speed can be hidden, sourced from OBD-II or GPS, and temperature can be hidden, sourced from OBD-II ambient or GPS + the Open-Meteo weather provider. ### Implemented values | Value | PID | Notes | |---|---|---| | Vehicle speed, km/h | `0D` | Digital home readout | | Throttle, % | `49`, fallback `11` | Horizontal bar | | Ambient temperature, °C | `46` | OBD temperature row | There is no fuel-economy figure. The previous rolling estimate was removed in 1.2.10: `5E` is not available on every vehicle, and the MAF/AFR fallback was not accurate enough across petrol and diesel engines to present as economy. The dashboard shows speed and throttle only. ### Out of scope - Fuel economy, fuel level and range. - RPM, coolant, module voltage, MIL/DTC contents. - Mode `04`/`08` writes; CarDash is read-only. - Wi-Fi, USB and BLE adapters. - Cabin temperature. - Background location. The OBD connection is kept alive across short app switches so speed does not drop to GPS; GPS and weather polling still stop when the app is backgrounded. ## 2. Decisions | # | Decision | |---|---| | D1 | Bluetooth Classic SPP only; only devices already paired in Android Settings. No scan UI. | | D2 | Throttle uses accelerator `49`, falling back to plate `11`. | | D3 | No fuel-economy estimate: remove it rather than show an inaccurate figure. | | D4 | Small live speed card above the volume card; temperature row below the date. | | D5 | The OBD connection survives app backgrounding (navigation/app switches) and is closed only on detach. | | D6 | No fuel level or range. | | D7 | OBD ambient wins over weather while the OBD temperature source is selected. | | D8 | Speed and temperature each have an independent show/hide switch in Settings. | | D9 | Source switches appear only while an OBD-II adapter is selected; GPS/weather is the fallback. | | D10 | Temperature starts off on fresh installs; upgrades that already used OBD or weather keep it on. | | D11 | The picker defaults to devices that look like Bluetooth Classic OBD-II adapters; **Show all paired devices** is the explicit escape hatch. | | D12 | Every Bluetooth binder call runs on one serialized native worker, never on the MethodChannel/main thread. | | D13 | Connection attempts stop after three failures; the card offers **Retry connection**. There is no infinite reconnect loop. | ## 3. Permissions | Feature | Manifest | Runtime | |---|---|---| | Bluetooth 29–30 | `BLUETOOTH`, `BLUETOOTH_ADMIN` (`maxSdkVersion="30"`) | install-time | | Bluetooth 31+ | `BLUETOOTH_CONNECT` | requested from the OBD-II card | | Weather | `INTERNET`, `ACCESS_FINE_LOCATION`, `ACCESS_COARSE_LOCATION` | location requested when temperature/weather is enabled | There is no `BLUETOOTH_SCAN`. The app lists bonded devices only and requests Bluetooth permission from the OBD-II card before listing them. Location is one-shot. Coordinates and place names are held only in memory for the current weather value; they are not persisted. The adapter address, speed and temperature visibility, source choices and weather preference are persisted in `SettingsRepository`. ## 4. Architecture ``` Kotlin TelemetryBridge MethodChannel "com.wasp.cardash_launcher/telemetry" - bluetoothState / listBondedDevices / ensureBluetoothPermission - connectObd(mac) / disconnectObd / getObdState - ensureLocationPermission / getLocation (last-known or single fix + Geocoder) - all Bluetooth binder work runs on the cardash-bluetooth worker ObdService one worker thread over BluetoothSocket/RFCOMM SPP - handshake ATZ, ATE0, ATL0, ATS0, ATSP0 - request/response serialized, prompt-terminated - 12 s connect watchdog, three attempts, then retryExhausted Dart NativeTelemetryBridge platform boundary ObdController connection lifecycle, 1 s snapshot polling, status TelemetrySettingsController persisted speed/temperature visibility and source WeatherController permission, one-shot location, 15 min weather cache WeatherService keyless Open-Meteo current temperature_2m TelemetryBridge models BluetoothDeviceInfo / LocationReading / ObdState SettingsRepository adapter address, speed/temperature flags, weather ``` The native `ObdService` warms the adapter with a `0100` request after `ATSP0` and drops any stale input before each command. It polls speed and throttle roughly once per second; ambient temperature is refreshed every 30 poll cycles. The OBD connection is not closed when the app is backgrounded, so returning from navigation does not reconnect or fall back to GPS. A snapshot carries `updatedAt`; the Dart side treats data older than five seconds as stale and hides the live card. Connection failures surface as a short Settings status and keep the dashboard quiet. A failure after three attempts sets `retryExhausted`; the card then shows **Retry connection** and the controller does not auto-retry on the next resume. ## 5. Home UI - **Temperature row** immediately below the date, only while the Temperature switch is on: - OBD source: `Outside, 7°C` - Weather source: `Sofia, 7°C` (or `Current location, 7°C`) - Red below 3°C, otherwise normal ink. - OBD ambient is preferred while its source switch is on. If OBD is disconnected or has no ambient value, the location weather fallback is used when enabled and available. - If OBD has no temperature and weather is off/unavailable, no row is shown. - **Speed card** in the left column above the media-volume card, only while the Speed switch is on and a fresh source value exists: ``` ● OBD-II LIVE 44 km/h Throttle 22% [========------------------] ``` When the GPS source is selected (or OBD is unavailable), the card reads `GPS / FROM LOCATION` with the GPS speed instead. - The card and row disappear cleanly when off/no adapter/no fresh data. The 16-button grid keeps its own space. ## 6. Settings cards **OBD-II** - Adapter dropdown containing `None` and paired Bluetooth Classic devices that look like OBD-II adapters (known name, SPP UUID, or a plausible major class). **Show all paired devices** reveals phones, headsets and other bonded devices. - `Find`/`Allow` requests Bluetooth permission and refreshes the list. - Connection status: Off / starting / turning off / unavailable / Connecting / Connected / waiting for data / error. - Bluetooth-off action: **Open Bluetooth settings**. On return, the controller checks the adapter state and reuses the cached list; it does not run a bond query on every resume. - After three failed connection attempts: **Retry connection**. - Read-only note: CarDash never writes to the ECU. - This card contains adapter settings only. **Speed** - Show/hide switch for the speed card. - While an OBD-II adapter is selected: **Use OBD-II for speed**. Off selects GPS. - The source switch is hidden while no adapter is selected; GPS is then used. **Temperature** - Show/hide switch for the temperature row. - While an OBD-II adapter is selected: **Use OBD-II for temperature**. Off selects GPS + Open-Meteo weather. - Enabling temperature or choosing the weather source asks for location permission. OBD temperature still works if permission is denied. Selecting a device persists its MAC address and connects immediately when the app is active. Selecting `None` disconnects. Polling stops when CarDash leaves the foreground and reconnects on resume. ## 7. Weather fallback `WeatherController`: 1. Requires the Temperature switch to be on and location permission granted. 2. Gets one location fix through `TelemetryBridge.getLocation`. 3. Reverse-geocodes a display name with Android `Geocoder`. 4. Queries Open-Meteo: `https://api.open-meteo.com/v1/forecast?...¤t=temperature_2m&timezone=auto` 5. Refreshes every 15 minutes while enabled. Any failure clears the temperature and hides the row. No API key, account or coordinates are stored. ## 8. Failure and freshness - Missing PID → null, never zero. - OBD data older than 5 s → stale; hide live card and ambient. - Ambient `-40`/`215` sentinel values are rejected. - Weather failure → no row. - Permission denial is a normal state; the Temperature card shows the weather error while OBD temperature can still work. - Three consecutive connect/handshake failures stop the worker and expose **Retry connection**; this prevents an absent adapter from keeping the Bluetooth stack in a connect loop. - Bluetooth power transitions show a status instead of listing devices while the adapter is still turning on or off. - The OBD status is visible only in Settings; the dashboard stays quiet. ## 9. Testing - Dart tests cover the OBD card, adapter classification/filtering, bounded-retry state, resume caching and the speed/temperature preference controller. - Settings persistence covers the adapter address, visibility switches, source choices and weather preference. - The API 35 emulator verifies the location permission path, reverse geocoding, Open-Meteo fetch and the temperature row (`Ukiah, 17°C` during implementation). - The Android bridge compiles for API 29/35. - **Not yet verified:** a real ELM327 socket, PID responses, clone timing and reconnect behavior in a car. This is the main remaining soak test. ## 10. Decision log - 2026-10-29 — Classic SPP, bonded-only, read-only subset: speed, throttle, MAF economy, ambient. - 2026-10-29 — Added Open-Meteo/location fallback at the user's request; INTERNET and location permissions accepted for this feature only. - 2026-10-29 — Implemented in 0.6.0 with Car data Settings, live OBD card and temperature row; hardware verification deferred to a car. - 2026-10-30 — Split Settings into OBD-II, Speed and Temperature cards, added independent visibility toggles and OBD/GPS-weather source switches. - 2026-10-30 — Retired the two-minute process-start onboarding hint in favor of a ten-minute first-install window that never returns after an upgrade. - 2026-10-31 — After a freeze that followed a Bluetooth settings return, moved all Bluetooth binder calls to a serialized worker, filtered the adapter picker, bounded connection attempts to three, added a 12 s connect watchdog and made resume reuse the cached bonded list. - 2026-11-02 — Fuel economy now prefers standard PID `5E` (engine fuel rate): it matches the car's trip computer and is the only reliable MAF-free source for a diesel engine. MAF/AFR remains the petrol-only fallback. - 2026-11-02 — Fixed the 1.2.7 economy regression where the card stayed at `—`: the one-shot supported-PID query before the poll could race the `ATSP0` protocol search and shift every later response by one command. The warm-up request, stale-input drain and in-loop `5E` probe keep the ELM327 stream aligned and make an unsupported `5E` cost at most one probe per 30 cycles. - 2026-11-02 — Removed the rolling fuel economy entirely. `5E` is not reported by every vehicle and the MAF/AFR fallback was not accurate enough across petrol and diesel, so the card now shows only speed and throttle. The OBD connection stays alive while the app is backgrounded and is stopped only on detach, so returning from navigation keeps OBD speed instead of falling back to GPS.