If you own a car, you might have heard of the device called OBD. It is referred to as On-Board Diagnostics which is used to collect diagnostic information from the vehicle, which can be useful to detect problems or to know the health of the vehicle. Many vehicle garages use the OBD device nowadays to troubleshoot issues with the cars.
Today, almost every car has a hidden computer system in it which collects the data from the car’s components such as engine temperature, engine RPM, speed, fuel level and many more things that you might have never heard of 😉
In order to connect with this computer system to read the collected data about cars, we need the OBD device. Every car has a dedicated port to connect OBD devices. If you’re not sure where the port is located in your car, check out this app: Where is my OBD2 port? Find it! – Apps on Google Play
OBD devices come as both wired and wireless options. This article is focused on the wireless option of the OBD device which is usually called ELM-327 OBD-2, which uses classic Bluetooth 2.1 for connection.
Before we begin coding for connection to OBD devices, we need to know how we can test the connection and reading values from OBD. It’s required to connect an OBD device in the car to read values from it. But, It’s unlikely you’ll be sitting with your laptop in your car while developing the app for OBD connection 🙂
So, to test during development, we need the OBD-2 Simulator installed. OBDSim is one such simulator that can be installed on Windows/Linux/Mac for testing.
obdsim.exe -g gui_fltk -w COM5
Running OBDSim in Linux requires few more steps than Windows, refer the following StackOverflow answer to install OBDSim in Linux: https://stackoverflow.com/questions/25720469/connect-obdsim-to-torqueandroid-app-ubuntu/26878598#26878598
We’ll be following these steps during the app development:
Let’s start with adding required permissions for scanning and connecting to Bluetooth devices, add the following permissions in AndroidManifest.xml
<uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> |
It is required to enable location settings before we can discover nearby Bluetooth devices. So, we need to add the code to request location permissions at run time before going to the next steps.
Once we have the location permission, we should check if the location is turned on, if not, navigate the user to location settings to turn it on.
After turning on the location, we need to check whether Bluetooth is turned on or not, if not, we should request the user to enable Bluetooth via an implicit intent.
Once Bluetooth is turned on, we can move forward with the process of discovering the nearby Bluetooth devices.
For simplicity and reusability, we can create a Bluetooth setup lifecycle observer with the latest activity result APIs.
class BluetoothSetupObserver( private val context: Context, private val activityResultRegistry: ActivityResultRegistry, private val bluetoothSetupCallback: BluetoothSetupCallback ) : DefaultLifecycleObserver { private var bluetoothAdapter: BluetoothAdapter? = null private lateinit var locationPermissionLauncher: ActivityResultLauncher<String> private lateinit var locationSettingsLauncher: ActivityResultLauncher<Intent> private lateinit var bluetoothEnableLauncher: ActivityResultLauncher<Intent> override fun onCreate(owner: LifecycleOwner) { super.onCreate(owner) bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() locationPermissionLauncher = activityResultRegistry.register( "locationPermissionLauncher", owner, ActivityResultContracts.RequestPermission() ) { isGranted -> if (isGranted) { checkIfLocationEnabled() } else { bluetoothSetupCallback.locationDenied() } } locationSettingsLauncher = activityResultRegistry.register( "locationSettingsLauncher", owner, ActivityResultContracts.StartActivityForResult() ) { if (context.isLocationEnabled()) { enableBluetooth() } else { bluetoothSetupCallback.locationTurnedOff() } } bluetoothEnableLauncher = activityResultRegistry.register( "bluetoothEnableLauncher", owner, ActivityResultContracts.StartActivityForResult() ) { if (it.resultCode == Activity.RESULT_OK) { bluetoothSetupCallback.bluetoothTurnedOn() } else { bluetoothSetupCallback.bluetoothRequestCancelled() } } } private fun checkIfLocationEnabled() { if (context.isLocationEnabled()) { enableBluetooth() } else { context.showToast("Please enable location services") locationSettingsLauncher.launch( Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) ) } } private fun enableBluetooth() { if (bluetoothAdapter?.isEnabled == false) { val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) bluetoothEnableLauncher.launch(enableBtIntent) } else { bluetoothSetupCallback.bluetoothTurnedOn() } } fun checkPermissionsAndEnableBluetooth() { locationPermissionLauncher.launch( Manifest.permission.ACCESS_FINE_LOCATION ) } interface BluetoothSetupCallback { fun bluetoothTurnedOn() fun bluetoothRequestCancelled() fun locationDenied() fun locationTurnedOff() } } fun Context.isLocationEnabled(): Boolean { val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager return LocationManagerCompat.isLocationEnabled(locationManager) } fun Context.showToast(message: String) { Toast.makeText(this, message, Toast.LENGTH_SHORT).show() } |
Add the observer in the respective activity/fragment’s onCreate() method using lifecycle.addObserver(bluetoothSetupObserver)
Call checkPermissionsAndEnableBluetooth() function when you want to start the Bluetooth setup.
Bluetooth device discovery uses the Broadcast Receiver to get the callback of nearby devices, we have to register the broadcast for
BluetoothDevice.ACTION_FOUND,
BluetoothAdapter.ACTION_DISCOVERY_STARTED
BluetoothAdapter.ACTION_DISCOVERY_FINISHED
in order to get the callbacks about discovery start, end, and when a nearby device is discovered.
Create a broadcast receiver to handle these broadcast actions:
private val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when(intent.action) { BluetoothAdapter.ACTION_DISCOVERY_STARTED -> { // Device discovery started, show loading indicator } BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> { // unregister receiver and hide loading indicator } BluetoothDevice.ACTION_FOUND -> { // Discovery has found a device. Get the BluetoothDevice // object and its info from the Intent. val device: BluetoothDevice? = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) // Add device to a set or list // Note: You may get one device multiple times in this broadcast event // make sure you don't show the same device multiple times in the UI } } } } |
Now, just register the receiver and call startDiscovery() on BluetoothAdapter
private fun startDiscovery() { val intentFilter = IntentFilter().apply { addAction(BluetoothDevice.ACTION_FOUND) addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED) addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED) } activity.registerReceiver(receiver, intentFilter) bluetoothAdapter?.startDiscovery() } |
The Bluetooth discovery process can use a lot of CPU and battery resources. So, make sure we do the bluetoothAdapter.cancelDiscovery() whenever the user closes the screen or proceeds with device connection.
The devices found during the discovery process are of the type BluetoothDevice instance. The instance offers several useful getters to get information about a Bluetooth device. It has methods to get name and address (MAC) of the device,
Once we find the OBD device from the discovery process, we can use the BluetoothDevice instance in this step for connection.
Before writing the connection code, let’s declare the different states of Bluetooth connection process using a sealed class:
sealed class ConnectionState { class Connecting(val bluetoothDevice: BluetoothDevice) : ConnectionState() class Connected(val socket: BluetoothSocket): ConnectionState() class ConnectionFailed(val failureReason: String): ConnectionState() object Disconnected: ConnectionState() } |
The connection of the device must be done on a background thread, otherwise, the main thread will be blocked and the app may face ANR.
So, in order to switch to a different thread, we’ll be using Kotlin Coroutines and Flow.
// This is the standard UUID which can be used for all kind of device connection private val STANDARD_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB") fun connectToDevice(bluetoothDevice: BluetoothDevice) = flow { emit(ConnectionState.Connecting(bluetoothDevice)) bluetoothAdapter.cancelDiscovery() try { socket = bluetoothDevice.createInsecureRfcommSocketToServiceRecord(STANDARD_UUID)?.also { it.connect() } connectedDevice = bluetoothDevice socket?.let { emit(ConnectionState.Connected(it)) } } catch (e: Exception) { emit(ConnectionState.ConnectionFailed(e.message ?: "Failed to connect")) } }.flowOn(Dispatchers.IO) |
In the ViewModel, we can collect the Flow and update the connection state LiveData to reflect in the UI.
private val _obdDataListLiveData = MutableLiveData<List<ObdDataModel>>() val obdDataListLiveData: LiveData<List<ObdDataModel>> = _obdDataListLiveData fun connectToDevice(device: BluetoothDevice) { viewModelScope.launch { ObdConnectionManager.connectToDevice(device).collect { state -> _deviceConnectionStatus.value = state } } } |
That’s it, once you’re connected to an OBD device, you can use the BluetoothSocket instance to read/write data using input and output streams.
Reading values from an OBD device requires the understanding of different commands and protocols. But to make it easier, we’ll use one of the popular libraries for OBD.
Add the following dependency in your app module’s Gradle file:
implementation 'com.github.pires:obd-java-api:1.0' |
The library provides different classes of OBD commands such as SpeedCommand, RPMCommand, ThrottlePositionCommand. All these classes are extended from the abstract class OBDCommand.
In order to read values from an OBD device, we just need to call the run method on any command instance, providing the connected device socket’s input and output stream.
command.run(socket?.inputStream, socket?.outputStream)
|
This is a blocking call, which means it should be run on a background thread.
Once the values are read from the OBD device, the same command instance can be used to display values using command.getFormattedResult()
Since we want to get live updates from the OBD device, we have to continuously fire these OBD commands until our Bluetooth connection is active. We’ll again use coroutines and flow here to do that.
Before starting reading from an OBD device, there are some initial configuration commands which are required to be fired to the OBD device, otherwise, you may get wrong values. These commands are fire-and-forget commands, which means we don’t want the result of these commands.
private val initialConfigCommands get() = listOf( ObdResetCommand(), EchoOffCommand(), LineFeedOffCommand(), TimeoutCommand(42), SelectProtocolCommand(ObdProtocols.AUTO), AmbientAirTemperatureCommand() ) |
Now, let’s define the list of commands that we want to get values from.
private val commandList get() = listOf( SpeedCommand(), RPMCommand(), ThrottlePositionCommand(), EngineCoolantTemperatureCommand(), MassAirFlowCommand() ) |
The return values of these commands can be configured from the OBDSim software that we just installed for simulating the OBD device.
Use the following function that returns flow of OBDCommand and works on the IO thread:
fun startObdCommandFlow() = flow { try { initialConfigCommands.forEach { it.run(socket?.inputStream, socket?.outputStream) if (it is ObdResetCommand) { delay(500) } } } catch (e: Exception) { e.printStackTrace() } while (socket?.isConnected == true) { // indefinite loop to keep running commands try { commandList.forEach { it.run(socket?.inputStream, socket?.outputStream) // blocking call emit(it) // read complete, emit value } } catch (e: Exception) { e.printStackTrace() } } }.flowOn(Dispatchers.IO) // all operations happen on IO thread |
We can call the function from the viewModelScope.launch and present the values to UI.
That’s it, we’re good read values from any OBD-2 Bluetooth 2.1 devices.
Let’s Nurture, a top mobile app development company in India has got the expertise in providing many solutions based applications with E-Commerce, Social Networking, Health sector, IoT & Machine learning. Our dedicated team of Android developers at LetsNurture provides best solutions for IoT applications such as connecting to devices that can help you diagnose the vehicle and get rich visual information through the app. If you want to know more about this technology in your existing mobile app, get a free consultation from our experts at Let’s Nurture.
In the digital age, understanding the hidden forces driving user behavior is essential. By strategically…
What is haptics? Haptics refers to the use of touch feedback technology to simulate…
In today's fast-paced and technologically driven world, businesses are constantly seeking innovative ways to stay…
The Garden State, more popularly known as New Jersey, is not only known for its…
In today's digital age, mobile apps have become an indispensable tool for businesses across all…
In today's digital era, a seamless and enjoyable user experience is crucial for the success…