Overview
On one day, I got a new challenge at my workplace: Connecting a door lock with an android mobile app and performing lock and unlock operation via bluetooth connectivity.
I never did any bluetooth related operation during my work experience so I’ve started collecting data related to this BLE device. After many days I have completed this task, summed up all the conclusions and thought to post the blog on the same.
So let’s start with the basic information.
What is a BLE Device?
BLE stands for Bluetooth Low Energy. BLE is designed to do exactly what its name implies: low energy. BLE achieves this by emitting small amounts of data once every few milliseconds or longer at short range. This is different from traditional bluetooth which maintains a constant connection at all times. For the detailed information regarding the BLE device and its services kindly read this documentation.
Let’s Code
First of all declare permissions as mentioned below
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
Now one common question will raise in everyone’s mind why Location permission? Right?
BLE devices are sometimes associated with Location. So to get scanning results, we need to ask for location permission from the user.
If your app is targeted to Android 12, and you don’t want to ask for Location permission, do not to worry, because Android 12 introduces the 3 permissions BLUETOOTH_SCAN, BLUETOOTH_ADVERTISE, and BLUETOOTH_CONNECT to make it easier.
Define required UUIDs
You can say UUID is a Key Identifier. It is Universally Unique Identifier with standardised 128-bit string which is basically used to identify unique information of Bluetooth Services.
In my case I required below UDIDs to identify and perform action on BLE device
val BLE_SERVICE = "********-****-****-****-***********" val BLE_LOCK_CHARACTERISTIC = "********-****-****-****-***********" val BLE_BATTERY_CHARACTERISTIC = "********-****-****-****-***********" val BLE_BATTERY_DESCRIPTOR = "********-****-****-****-***********"
Setup BLE Objects
Here, We need to use some BLE classes and the good thing is that we don’t need to add any extra library or dependencies because android provides built in classes so we can use it directly
private lateinit var mBluetoothAdapter: BluetoothAdapter private val bluetoothManager: BluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager mBluetoothAdapter = bluetoothManager.getAdapter();
Now as we are implementing code for bluetooth communication, on the first we need to check that device supports bluetooth or not and if device supports bluetooth we need to enable it
mBluetoothAdapter.isEnabled
This returns whether bluetooth is enabled or not. If bluetooth is not enabled already we need to ask user to enable bluetooth
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) // Here REQUEST_ENABLE_BT is request code
Now Bluetooth Enable Intent will appear on the screen and ask the user to enable bluetooth. If the user allows, the OS will automatically turn on the bluetooth and the user can also deny the request.
All result will be catch in onActivityResult in Activity
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == REQUEST_ENABLE_BT) { if (resultCode == AppCompatActivity.RESULT_OK) // BlueTooth is now Enabled else if (resultCode == AppCompatActivity.RESULT_CANCELED) // User Denied request for enabling bluetooth or Error occurred while enabling Bluetooth } }
All set! Now for performing operation over bluetooth we need a nearby device list to check if the device is in range or not. For that we have to scan and get the nearby bluetooth device as a result.
Below is deprecated method you’ll see in all the older docs.
mBluetoothAdapter!!.startLeScan { device, rssi, scanRecord -> .. .. }
This is replaced by new method as mentioned below with minor modifications
mBluetoothAdapter.bluetoothLeScanner.startScan(object : ScanCallback() { override fun onScanFailed(errorCode: Int) { super.onScanFailed(errorCode) } override fun onScanResult(callbackType: Int, result: ScanResult?) { super.onScanResult(callbackType, result) // Here You’ll get all the nearby devices in result } override fun onBatchScanResults(results: MutableList?) { super.onBatchScanResults(results) } }) }
In onScanResults() we can get all the nearby devices one by one. You can also provide <ScanFilter> to filter out your device specific scan result.
If you found your device you can perform connection request on that device
// You can pass true as a second param if you want to auto connect device mBluetoothGatt = device.connectGatt(this, false, mBluetoothGattCallBack)
After this you’ll get all states in the callback mentioned below. Now below callback is called whenever connection state changes
private val mBluetoothGattCallBack = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { super.onConnectionStateChange(gatt, status, newState) Log.i("TAG", "onConnectionStateChange $newState") when (newState) { BluetoothGatt.STATE_DISCONNECTED -> { Log.i("BLE_TAG", "DISCONNECTED") } BluetoothGatt.STATE_CONNECTING -> { Log.i("BLE_TAG", "CONNECTING") } BluetoothGatt.STATE_CONNECTED -> { Log.i("BLE_TAG", "CONNECTED") mBluetoothGatt.discoverServices() } } }
Mainly there are 3 connection states captured by the above call back. Connecting, Connected and Disconnected.
When the State is connected, we need to discover services by using the discoverServices() function.
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { super.onServicesDiscovered(gatt, status) Log.i("BLE_TAG", "onServicesDiscovered $status") if (status == BluetoothGatt.GATT_SUCCESS) displayBleService(gatt.services) }
After that, if any services found by BLE device, onServicesDiscovered will be executed with the status and all the discovered services. Where we have to check that the status of discovered services are successful or not.
If it is successful we have to check that which services we want to access or use
In our example we have defined the BLE_SERVICE which will be in use.
fun displayBleService(gattServices: List) { val requiredService = gattServices.find { it.uuid == UUID.fromString(BLE_SERVICE) } if (requiredService != null) { if (requiredService.characteristics.find { it.uuid == UUID.fromString( BLE_LOCK_CHARACTERISTIC ) } != null) { enableNotification( requiredService.characteristics.find { it.uuid == UUID.fromString( BLE_BATTERY_CHARACTERISTIC ) }!!, UUID.fromString(BLE_BATTERY_DESCRIPTOR) ) } } }
If we found the required service, we are ready to perform commands or you can say write characteristics.
Here we’ll pass a lock and unlock command to the BLE device and It will perform a task based on it.
For writing characteristics we have to type following code
I have added 2 separate functions to Lock and Unlock door.
Here we have to pass a byte array, Service UUID and Characteristic UUID.
private fun lockDoor() { val lockArray = ByteArray(2) lockArray[0] = 1 lockArray[1] = 123 writeCharacteristics( lockArray, UUID.fromString(BLE_SERVICE), UUID.fromString(BLE_LOCK_CHARACTERISTIC) ) }
private fun unlockDoor() { val lockArray = ByteArray(2) lockArray[0] = 1 lockArray[1] = 123 writeCharacteristics( lockArray, UUID.fromString(BLE_SERVICE), UUID.fromString(BLE_LOCK_CHARACTERISTIC) ) }
Now let’s check what we have written in writeCharacteristic function
private fun writeCharacteristics(data: ByteArray?, DLE_SERVICE: UUID, DLE_WRITE_CHAR: UUID) { val service = mBluetoothGatt.getService(DLE_SERVICE) //Check that if service is available or not if (service == null) { Log.i("BLE_TAG", "service not found!") return } val charc1 = service.getCharacteristic(DLE_WRITE_CHAR) //Check that if Characteristic is available or not if (charc1 == null) { Log.i("BLE_TAG", "Characteristic not found!") return } charc1.value = data // return state of writeCharacteristic val stat = mBluetoothGatt.writeCharacteristic(charc1) Log.i("BLE_TAG", "writeCharacteristic $stat") }
Here, at first, we’ll again check that Service is available and if service is available we’ll check for characteristics.
If both are available we will write characteristic in to it
writeCharacteristic function returns the stats as true or false to identify if the requested command is successfully written or not.
You can use your own different services which are available in your BLE devices
That’s it.
For practical purposes you can clone git repo from here
Conclusion
As we can see in the blog, connection is a really important and easy task. We can connect any bluetooth low energy device if we have authenticated keys. Still we can feel some connection update issues with the current SDK code. Hoping for the best stable version in future.