Implementing Car Status Monitoring via Mobile App
Every car since 1996 has an OBD-II port. Through it, an ELM327-compatible adapter (Viecar, KONNWEI, Veepeak) via Bluetooth or WiFi gives PID requests—app gets engine RPM, load, coolant temperature, speed, battery voltage, DTC error codes. Sounds simple, but implementation hits several non-trivial points.
ELM327 Protocol and Common Pitfalls
ELM327 adapters speak via AT commands over serial port. Connection via Classic Bluetooth—BluetoothSocket on Android with UUID 00001101-0000-1000-8000-00805F9B34FB (SPP profile). On iOS, Classic Bluetooth for third-party apps is closed; only path—BLE ELM327 adapters (Viecar EA400-P, OBDLink CX) via Core Bluetooth.
Most common mistake: send next PID request without waiting for > (prompt) in response. Adapter buffers commands unpredictably, app gets ? or NO DATA instead of RPM value. Right polling cycle—sequential, with ~200 ms prompt wait timeout:
class OBDConnection(private val socket: BluetoothSocket) {
private val input = socket.inputStream.bufferedReader()
private val output = socket.outputStream
suspend fun sendCommand(command: String): String = withContext(Dispatchers.IO) {
output.write("$command\r".toByteArray())
val sb = StringBuilder()
var char: Int
while (input.read().also { char = it } != -1) {
val c = char.toChar()
sb.append(c)
if (c == '>') break
}
sb.toString().trim().removeSuffix(">").trim()
}
suspend fun readPID(mode: String, pid: String): String {
return sendCommand("$mode$pid")
}
}
Adapter initialization before polling required: ATZ (reset), ATE0 (disable echo), ATL0 (no line wrap), ATSP0 (auto-select protocol). Without ATE0, parsing responses much harder—command echoes in stream with response.
PID Polling: What Actually Reads
SAE J1979 standard defines Mode 01 (current data) and Mode 03 (error codes). Not all PIDs supported by all cars—first query PID 0x00 (supported PIDs 01-20), then 0x20, 0x40, 0x60, to build map of available parameters.
| PID | Parameter | Formula |
|---|---|---|
| 0x04 | Engine load | A * 100 / 255 % |
| 0x05 | Coolant temperature | A - 40 °C |
| 0x0C | Engine RPM | (256*A + B) / 4 RPM |
| 0x0D | Speed | A km/h |
| 0x11 | Throttle position | A * 100 / 255 % |
| 0x42 | Battery voltage | (256*A + B) / 1000 V |
| 0x5E | Fuel consumption | (256*A + B) / 20 l/h |
Error codes (Mode 03) return DTC list, 2 bytes per code. First two bits determine system: 00—engine (P), 01—transmission (P1xxx), 10—chassis (C), 11—body (B). DTC decoding to readable descriptions requires database—open options: CSV from hfreire/ecu-can-bus-decoder repo or paid database from OBD Solutions.
App Architecture
Android—foreground service with PRIORITY_LOW notification (else Android 8+ kills process within minutes). Service manages adapter connection and polling cycle, UI subscribes via StateFlow. iOS—foreground-only, Core Bluetooth only works in background for Heart Rate and few other profiles; polling while screen active.
Polling frequency: RPM and speed—every 100-200 ms, temperature and consumption—every 1-2 seconds. Don't poll all PIDs at same frequency—overloads adapter, noticeably slows CAN bus.
Bonus: TPMS and Cabin Camera
Tire pressure sensors (TPMS) on most cars use separate RF protocol (315/433 MHz), not accessible via OBD-II. For monitoring, need external BLE sensors (e.g., Meneea, Fobo Tire Plus) stuck on valve transmitting pressure and temperature. Integrated via standard Core Bluetooth / Android BLE API.
Developing basic car status app with ELM327 connection, monitoring 10-15 PIDs, DTC reading: 3-4 weeks. Full app with trip history, geolocation, fuel consumption calculation, TPMS: 6-8 weeks. Cost individually quoted after target platform clarification and parameter list.







