class="nav-up">

OBD-2 Bluetooth communication in Android with Kotlin

16

Apr. 21

3.74 K

VIEWS

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. 

How does OBD work?

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.

OBD communication with Android App

Prerequisites

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. 

Installing OBD-2 Simulator

Windows

  • Download the latest version of OBDSim from https://icculus.org/obdgpslogger/downloads/obdsimwindows-latest.zip
  • Extract the zip file.
  • Go to Windows Bluetooth Settings -> COM Ports -> Add -> Incoming
  • Note the port, here, COM5
  • Go to obdsimwindows directory, open the command line in that folder.
  • Enter the following command: (Use the same incoming port in the command)
obdsim.exe -g gui_fltk -w COM5
  • It’ll open the GUI like this, which has several controls to simulate data change.
  • Now you’re good to connect your app with the OBD Simulator!

Linux

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 

Steps of Development

We’ll be following these steps during the app development:

  1. Bluetooth Setup
  2. Discover nearby OBD devices using bluetooth
  3. Connect to selected OBD device
  4. Read values from OBD device

1. Bluetooth Setup

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.

2. Discover nearby OBD devices using Bluetooth

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,

3. Connect to selected OBD 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.

4. Read values from the OBD device

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.

How Let’s Nurture helps for building apps with the latest technology like OBD with wireless Bluetooth connection?

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.

Author

Lets Nurture
Posted by Lets Nurture
We use cookies to give you tailored experiences on our website.
Okay