Implementing OBD-II Diagnostics in Mobile Applications
An OBD-II adapter (ELM327-compatible or professional) connects to the 16-pin port under the steering wheel and communicates with the mobile app via Bluetooth Classic, Bluetooth LE, or Wi-Fi. CAN bus protocols behind OBD-II — SAE J1979 (PID request standard), ISO 15765-4 (CAN), ISO 14230 (KWP2000) — depend on manufacturer and vehicle year. Developer's task: parse ELM327 AT commands, decode PID responses, interpret DTC error codes.
ELM327 and AT Commands
ELM327 is a bridge between the vehicle's CAN bus and serial port. Initialization and basic requests:
ATZ → adapter reset
ATL0 → disable line feed
ATE0 → disable echo
ATH0 → disable CAN frame headers
ATSP0 → auto-select protocol
0100 → request supported PIDs (01-20)
Response to 0100: 41 00 BE 3F A8 13 — bits show which PIDs ECU supports. 41 is the response to mode 01, 00 is PID. Next 4 bytes are bitmask.
Decoding:
fun parseSupportedPids(response: String): Set<Int> {
val bytes = response.trim().split(" ").map { it.toInt(16) }
if (bytes.size < 6 || bytes[0] != 0x41 || bytes[1] != 0x00) return emptySet()
val supported = mutableSetOf<Int>()
var bitMask = (bytes[2].toLong() shl 24) or (bytes[3].toLong() shl 16) or
(bytes[4].toLong() shl 8) or bytes[5].toLong()
for (bit in 31 downTo 0) {
if ((bitMask and (1L shl bit)) != 0L) {
supported.add(32 - bit)
}
}
return supported
}
Bluetooth Classic vs Bluetooth LE
ELM327 clones use Bluetooth Classic (SPP profile). On Android — BluetoothSocket with UUID 00001101-0000-1000-8000-00805F9B34FB. On iOS, Bluetooth Classic is unavailable for third-party apps — only MFi-certified accessories or Wi-Fi adapters.
For cross-platform Flutter, recommend Wi-Fi ELM327 adapters (TCP 192.168.0.10:35000) or next-gen BLE adapters (OBDLink MX+, Veepeak OBDCheck BLE+):
class OBD2WifiConnector {
late Socket _socket;
final StreamController<String> _responseController = StreamController();
Stream<String> get responses => _responseController.stream;
Future<void> connect(String host, int port) async {
_socket = await Socket.connect(host, port,
timeout: const Duration(seconds: 5));
_socket.listen(
(data) {
final response = String.fromCharCodes(data).trim();
if (response.endsWith('>')) {
final clean = response.replaceAll('>', '').trim();
if (clean.isNotEmpty) _responseController.add(clean);
}
},
onError: (error) => _reconnect(),
);
await _initializeAdapter();
}
Future<String> sendCommand(String command) async {
final completer = Completer<String>();
late StreamSubscription sub;
sub = responses.first.asStream().listen((response) {
sub.cancel();
completer.complete(response);
});
_socket.write('$command\r');
return completer.future.timeout(const Duration(seconds: 3));
}
}
Real-Time PID Polling
Popular mode 01 (real-time) PIDs:
| PID | Parameter | Formula |
|---|---|---|
| 0C | Engine RPM | (A*256+B)/4 |
| 0D | Vehicle Speed km/h | A |
| 05 | Coolant Temperature °C | A-40 |
| 0F | Intake Air Temperature °C | A-40 |
| 11 | Throttle Position % | A*100/255 |
| 04 | Engine Load % | A*100/255 |
| 0B | Manifold Pressure kPa | A |
Poll multiple PIDs sequentially with minimal delay:
Future<void> startPolling(List<int> pids) async {
while (_isPolling) {
for (final pid in pids) {
final pidHex = pid.toRadixString(16).padLeft(2, '0').toUpperCase();
final response = await sendCommand('01$pidHex');
_parsePidResponse(pid, response);
await Future.delayed(const Duration(milliseconds: 50));
}
}
}
50 ms between requests is minimum for stable operation of most adapters. Faster risks ELM327 buffer overflow.
Reading and Clearing DTC Error Codes
Mode 03 — request active DTC (Diagnostic Trouble Codes):
fun parseDtcResponse(response: String): List<String> {
val bytes = response.trim().split(" ").map { it.toInt(16) }
val dtcs = mutableListOf<String>()
var i = 2 // skip mode and count
while (i + 1 < bytes.size) {
val byte1 = bytes[i]
val byte2 = bytes[i + 1]
if (byte1 == 0 && byte2 == 0) break
val prefix = when ((byte1 shr 6) and 0x03) {
0 -> "P"; 1 -> "C"; 2 -> "B"; 3 -> "U"; else -> "P"
}
val digit2 = (byte1 shr 4) and 0x03
val digit3 = byte1 and 0x0F
val digits45 = byte2.toString(16).padStart(2, '0').uppercase()
dtcs.add("$prefix$digit2$digit3$digits45")
i += 2
}
return dtcs
}
P0300 — random misfires, P0171 — lean fuel mixture, P0420 — catalyst bank 1. DTC decoding — separate database (SAE J2012 for standard, OEM for manufacturers).
Clearing errors: mode 04, command 04. Confirmation via dialog is mandatory — clearing deletes Readiness Monitor data, which can cause vehicle inspection failure.
Developing OBD-II diagnostics application with BLE/Wi-Fi connection, PID polling and DTC reading: 3-5 weeks. Adding DTC decoding, trends and multi-vehicle support: 6-8 weeks. Cost calculated individually.







