BLE in Android-Kotlin

Nithinjith Pushpakaran
16 min readSep 20, 2018

--

BLE (Bluetooth Low Energy) devices are using to get a small amount of data from a hardware on a long time with profound energy. If your Application not required enhanced data, the BLE is a better solution for that implementation. BLE has its own power and limitation based on the business goal. Suggested Business goals which can use BLE are given below.

BLE: Several Business Goals

  1. Suppose you want to track an employ action in an organization. You can use Beacon devices associated with the Application which can track the distance between the beacon and the device. The same logic can apply in Schools, Hospitals, and different Shops. The business goal is enormous.
  2. Another important business goal is to get the data from hardware continuously. For example, suppose you want to check the vehicle system in a Service center whether it has any damage or not. The BLE devices can continuously notify the test result to associated Android Application. Serial communication protocol can use in this type of application.
  3. Yet another important area that the BLE can use is an occasional notifier in a physical system. For example, you want to track the temperature level of a room. If the room reaches a particular temperature the Application should receive a notification. In this case, you can construct a BLE device with a thermometer. With the help of the associated BLE chip, the BLE device can notify the temperature level to the room. The same logic can use Health care domain (e.g.: Analyze the heartbeat, Analyze the blood pressure etc.)
  4. You can use BLE in safety areas also. An important example is the women safety. You can set the small BLE chip in the women’s wearable’s(Rings, Watch, Bangles etc.) and the BLE can notify the concerned authority with a single button press if an emergency occurred.

BLE: In Depth

The major difference between Classic Bluetooth and BLE is its small Power consumption. The BLE devices have long battery life compared to classic Bluetooth devices. Bluetooth is a wireless communication protocol. So you can design your own BLE devices on top of Bluetooth Protocol. For this, you need to specially design the firmware based on your business logic. Android is supporting BLE in SDK version 18 Onward. However, some manufacturers may avoid LE hardware support for their devices. So you need to check whether the hardware has an LE peculiarity or not before calling the API’s. Nowadays almost all Android devices are supporting BLE.

BLE is designed with the help of GATT (Generic Attribute) profile. The Profile is defined as a hierarchical data structure to communicate with the BLE devices. Two major part of the GATT profile is Services and Characteristics.

Services: BLE GATT will contain one or more services. Each service represents a specific feature in the BLE device. For example, almost all BLE devices have the battery Service. The battery service presents the battery level of the BLE device. The services can iterate using a service UUID.

Characteristics: Characteristics are the sub-portion in the Service. One service contains one or more characteristics. The characteristics will contain three parts: Property, Value and Descriptor. The Characteristics can iterate through characteristics UUID.

Three major functions used in BLE Android Implementation is read (), write () and notify ().

Read (): The function is using to read a characteristic value for a service. For example, suppose you want to read the battery level of the device from battery service. You can use read () with Characteristics UUID as a parameter.

Write (): The function is used to write some command to the BLE device. You can write the value to the BLE as byte form.

Notify (): Notify function will notify the app either continuously or in a particular situation. The application needs to filter the notification with characteristics UUID and Service UUID.

The functions are optional, and the firmware designer can either enable/disable the functions based on the business goal.

Android Implementation

The implementation logic given below is a sample for general BLE design.This covers General BLE read-write and notify properties of a BLE device. Moreover, I have provided the complete codebase of the implementation through GitHub. The URL is enclosed in the bottom section of the document. The Application mainly focusing on the BLE Scanning, Permission handling, Connection process etc. The activity provides a small UI with different services read/write functionality.

Manifest

In order to integrate the BLE API in the Application, you need to request user’s permission in the manifest. To get the Bluetooth API, you need to integrate below permissions in the manifest.

If we specifically design our application for BLE, you can neglect the BLE unsupported device from the play store itself. In order to achieve this, you can set the below users- feature in the manifest.

The Application uses a bounded service to call the BLE API’s. So we want to mention the service in Application Manifest.

<uses-feature                               android:name="android.hardware.bluetooth_le"                               android:required="true" />

Another important point before sinking to the code is the Location permission. Unlike classic Bluetooth, the BLE API’s are under the Location section in Android. So the user should accept the location permission before accessing the BLE API’s. This is a confusing feature for most of the customers. But the developer needs to convince the BLE implementation to them.

The location permission is using to scan the surrounding BLE devices. In order to get the scan result, the App must implement either ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION. Otherwise, the scan result will be empty.

Application Implementation

The Server-Client implementation model is adopted in the given BLE Android sample. For that, I have created a Bounded service and the Service will bind to the MainActivity. The Service will act as a BLE Service Provider and the Activity will act as a client to request some BLE Actions.

MainActivity.kt

The Very First thing we need to integrate with the Activity is the Location Permission. The Permission implementation is given below.

/**
* Check the Location Permission before calling the BLE API's
*/
private fun checkLocationPermission() {
if (isAboveMarshmallow()) {
when {
isLocationPermissionEnabled() -> initBLEModule()
ActivityCompat.shouldShowRequestPermissionRationale(this,
Page 4
BLE Android Kotlin September 16, 2018
Manifest.permission.ACCESS_COARSE_LOCATION) -> displayRationale()
else -> requestLocationPermission()
}
} else {
initBLEModule()
}
}
/**
* Request Location API
* If the request go to Android system and the System will throw a dialog message
* user can accept or decline the permission from there
*/
private fun requestLocationPermission() {
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
REQUEST_LOCATION_PERMISSION)
}
/**
* If the user decline the Permission request and tick the never ask again message
* Then the application can't proceed further steps
* In such situation- App need to prompt the user to do the change form Settings Manually
*/
private fun displayRationale() {
AlertDialog.Builder(this)
.setMessage(getString(R.string.location_permission_disabled))
.setPositiveButton(getString(R.string.ok)
) { _, _ -> requestLocationPermission() }
.setNegativeButton(getString(R.string.cancel)
) { _, _ -> }
.show()
}
/**
* If the user either accept or reject the Permission- The requested App will get a callback
* Form the call back we can filter the user response with the help of request key
* If the user accept the same- We can proceed further steps
*/
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
grantResults: IntArray) {when (requestCode) {
REQUEST_LOCATION_PERMISSION -> {
if (permissions.size != 1 || grantResults.size != 1) {
throw RuntimeException("Error on requesting location permission.")
}
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
initBLEModule()
} else {
Toast.makeText(this, R.string.location_permission_not_granted,
Toast.LENGTH_SHORT).show()
}
}
BLE Android Kotlin September 16, 2018
}
}
/**
* Check with the system- If the permission already enabled or not
*/
private fun isLocationPermissionEnabled(): Boolean {
return ContextCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_COARSE_LOCATION) ==PackageManager.PERMISSION_GRANTED
}
/**
* The location permission is incorporated in Marshmallow and Above
*/
private fun isAboveMarshmallow(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
}

After receiving the location permission, the application needs to initialize the BLE Module into our activity. The implementation here is as like a server-client model. So a bounded service should be registered in the Activity first. The implementation is given below.

/***After receive the Location Permission, the Application need to initialize the* BLE Module and BLE Service*/private fun initBLEModule() {// BLE initializationif (!BLEDeviceManager.init(this)) {Toast.makeText(this, "BLE NOT SUPPORTED", Toast.LENGTH_SHORT).show()return}registerServiceReceiver()BLEDeviceManager.setListener(this)if (!BLEDeviceManager.isEnabled()) {val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)}BLEConnectionManager.initBLEService(this@MainActivity)}
/***After receive the Location Permission, the Application need to initialize the
* BLE Module and BLE Service*/private fun initBLEModule() {// BLE initializationif (!BLEDeviceManager.init(this)) {Toast.makeText(this, "BLE NOT SUPPORTED", Toast.LENGTH_SHORT).show()return
}
registerServiceReceiver()BLEDeviceManager.setListener(this)if (!BLEDeviceManager.isEnabled()) {val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)}
BLEConnectionManager.initBLEService(this@MainActivity)
}
/*** Register GATT update receiver
*/
private fun registerServiceReceiver() {this.registerReceiver(mGattUpdateReceiver, makeGattUpdateIntentFilter())}/*** Intent filter for Handling BLEService broadcast.*/private fun makeGattUpdateIntentFilter(): IntentFilter {val intentFilter = IntentFilter()intentFilter.addAction(BLEConstants.ACTION_GATT_CONNECTED)intentFilter.addAction(BLEConstants.ACTION_GATT_DISCONNECTED)intentFilter.addAction(BLEConstants.ACTION_GATT_SERVICES_DISCOVERED)intentFilter.addAction(BLEConstants.ACTION_DATA_AVAILABLE)intentFilter.addAction(BLEConstants.ACTION_DATA_WRITTEN)return intentFilter}

Two Singleton class is used in the Application to handle the BLE Service.

1.BLEConnectionManager

2.BLEDeviceManager

BLEDeviceManager:

An important functionality in the BLE Module is the Scanning process. The class is using to handle the Scanning process in this BLE module. You need to scan the surrounding active BLE devices and list it out to the Application.

[Some BLE devices are pin protected based on the associated firmware. The pin authentication will be different based on the implemented firmware protocol. The present implementation considering only open BLE’s]

You can deep dive into the Scanning Process. In order to scan the BLE device, you need to consider the device OS version. If you are supporting below Lollipop, the scanning implementation will be slightly different. I have included both implementations in separate functions.

Scan Function

/*** Scan The BLE Device* Check the available BLE devices in the Surrounding* If the device is Already scanning then stop Scanning* Else start Scanning and check 10 seconds* Send the available devices as a callback to the system* Finish Scanning after 10 Seconds*/fun scanBLEDevice(isContinuesScan: Boolean) {try {mIsContinuesScan = isContinuesScanif (mScanThread != null) {/*** Already Running - No need to rescan*/return}mScanThread = Thread(mScanRunnable)mScanThread.start()/*** Stop Scanning after a Period of Time* Set a 10 Sec delay time and Stop Scanning* collect all the available devices in the 10 Second*/if (!isContinuesScan) {mHandler?.postDelayed({// Set a delay time to ScanningstopScan(mDeviceObject)}, BLEConstants.SCAN_PERIOD) // Delay Period}} catch (e: Exception) {Log.e(TAG, e.message)}}private val mScanRunnable = Runnable {if (mBluetoothAdapter != null && mBluetoothAdapter!!.isEnabled) {scan()}}private fun scan() {if (isLollyPopOrAbove()) {// Start Scanning For Lollipop devicesmBluetoothAdapter?.bluetoothLeScanner?.startScan(/*scanFilters(),scanSettings(),*/scanCallback) // Start BLE device Scanning in a separate thread} else {mBluetoothAdapter?.startLeScan(mLeScanCallback) // Start Scanning for Below Lollipop device}}

Some BLE devices need scanning as a background process to get the BLE devices. Some BLE devices are pretty straightforward and you can scan directly. The Implementation here is the scanning as a worker thread. If the device is above lollipop, you can scan the BLE using the function “startScan()”. The startScan() is expecting two parameters as input. The first one is the scan filter and another one is scanCallBack. The callback implementation will be different for below and above lollipop devices.

In the scan filter, we can cross-check the surrounding BLE device is our own or a third party device. For that, you can check if the surrounding devices will have our Service or not. The Services are mapping with Service UUID. You can neglect the scan filter while scanning a device. This will provide all the surrounding devices. You need to filter the devices after getting the scan result. You can pass either a single scan filter Service UUID or a list of UUID’s to get the Proper result.

Above Lollipo Devices- Use this function

/*** ScanCallback for Lollipop and above* The Callback will trigger the Nearest available BLE devices* Search the BLE device in Range and pull the Name and Mac Address from it*/private fun createScanCallBackAboveLollipop() {scanCallback = object : ScanCallback() {override fun onScanResult(callbackType: Int, result: ScanResult?) {super.onScanResult(callbackType, result)if (null != mOnDeviceScanListener && result != null &&result.device != null && result.device.address != null) {val data = BleDeviceData()data.mDeviceName = if (result.device.name != null)result.device.name else "Unknown"// Some case the Device Name will return as Null from BLE// because of Swathing from one device to anotherdata.mDeviceAddress = (result.device.address)/*** Save the Valid Device info into a list* The List will display to the UI as a popup* User has an option to select one BLE from the popup* After selecting one BLE, the connection will establish and* communication channel will create if its valid device.*/if (data.mDeviceName.contains("myDevice") || data.mDeviceName.contains("myDevice")) {mDeviceObject = datastopScan(mDeviceObject)}}}}}

Below Lollipo Devices- Use this function

/*** ScanCallback for below Lollipop.* The Callback will trigger the Nearest available BLE devices* Search the BLE device in Range and pull the Name and Mac Address from it*/private fun createScanCallBackBelowLollipop() {mLeScanCallback = BluetoothAdapter.LeScanCallback { device, _, _ ->if (device != null && device.address != null && null != mOnDeviceScanListener) {// Some case the Device Name will return as Null from BLE because of Swathing from one device to anotherval data = BleDeviceData()data.mDeviceName  = (device.name)data.mDeviceAddress = (device.address)/*** Save the Valid Device info into a list* The List will display to the UI as a popup* User has an option to select one BLE from the popup* After selecting one BLE, the connection will establish and* communication channel will create if its valid device.*//*** Save the Valid Device info into a list* The List will display to the UI as a popup* User has an option to select one BLE from the popup* After selecting one BLE, the connection will establish and communication* channel will create if its valid device.*/if (data.mDeviceName.contains("myDevice") || data.mDeviceName.contains("myDevice")) {mDeviceObject = datastopScan(mDeviceObject)}}}}

The Scan result callback will contain a Device Object with device information such as device address(MAC address), device name etc. In the case of beacon devices, you will get the RxSignal strength also in the scan result. The result will be a negative value representing the distance between the device and the BLE.The application using BLE devices can filter with device name from scan callback. But sometime the device name will return as Null from the BLE.So better practice is to use the scan filter.

BLEConnectionManager:

Another class developed by the singleton design pattern is the BLEConnectionManager. This class primarily acts as an intermediate between the BLE Service and Activity. The Activity request some BLE call, the call will be processed from the class and send to the service. The service will broadcast to the activity. After connecting the device you need to filter the services and characteristics from the BLE. The class is responsible to filter the service as well.

/*** findBLEGattService*/fun findBLEGattService(mContext: Context) {if (mBLEService == null) {return}if (mBLEService!!.getSupportedGattServices() == null) {return}var uuid: StringmDataBLEForEmergency = nullval serviceList = mBLEService!!.getSupportedGattServices()if (serviceList != null) {for (gattService in serviceList) {if (gattService.getUuid().toString().equals(mContext.getString(R.string.char_uuid_emergency),ignoreCase = true)) {val gattCharacteristics = gattService.characteristicsfor (gattCharacteristic in gattCharacteristics) {uuid = if (gattCharacteristic.uuid != null) gattCharacteristic.uuid.toString() else ""if (uuid.equals(mContext.resources.getString(R.string.char_uuid_emergency_call),ignoreCase = true)) {var newChar = gattCharacteristicnewChar = setProperties(newChar)mDataBLEForEmergency = newChar}}}}}}private fun setProperties(gattCharacteristic: BluetoothGattCharacteristic):BluetoothGattCharacteristic {val characteristicProperties = gattCharacteristic.propertiesif (characteristicProperties and BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0) {mBLEService?.setCharacteristicNotification(gattCharacteristic, true)}if (characteristicProperties and BluetoothGattCharacteristic.PROPERTY_INDICATE > 0) {mBLEService?.setCharacteristicIndication(gattCharacteristic, true)}if (characteristicProperties and BluetoothGattCharacteristic.PROPERTY_WRITE > 0) {gattCharacteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT}if (characteristicProperties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE > 0) {gattCharacteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE}if (characteristicProperties and BluetoothGattCharacteristic.PROPERTY_READ > 0) {gattCharacteristic.writeType = BluetoothGattCharacteristic.PROPERTY_READ}return gattCharacteristic}

To identify the available service in the BLE, you need to know the Service and Characteristics UUID initially. In this implementation, the UUID’s are listed under the Settings file.

The setProperties() method is checking what are the Properties available in a service. The properties assigning to a service is in the firmware development time.

BLEService

The Service is the most important class in the BLE module. The class is responsible to request(Write) some data to the BLE. The BLE will response to the Service through a callback(GattCallBack). In this implementation, I have created a bounded service for the module. The service has bounded to the activity with the help of a singleton class.

The initial process in the Service is Initializing the Bluetooth Manager and Bluetooth Adapter.

// Initialize by getting the BluetoothManager and BluetoothAdapterfun initialize(): Boolean {if (mBluetoothManager == null) {                                                //See if we do not already have the BluetoothManagermBluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager //Get the BluetoothManagerif (mBluetoothManager == null) {                                            //See if we failedLog.i(TAG, "Unable to initialize BluetoothManager.")return false                                                           //Report the error}}mBluetoothAdapter = mBluetoothManager!!.adapter                             //Ask the BluetoothManager to get the BluetoothAdapterif (mBluetoothAdapter == null) {                                                //See if we failedLog.i(TAG, "Unable to obtain a BluetoothAdapter.")return false                                                               //Report the error}return true                                                                    //Success, we have a BluetoothAdapter to control the radio}

After Initialize the Adapter we can request a connection with the user selected device. As mentioned earlier, the selected device should have a device address. You need to filter the remote device from the Adapter. This will return a Bluetooth device. You can make a connection with the selected device.

  1. You can make an auto connection with the device if the device has the capability.
  2. You can keep the connected device object if it’s a single device connection.
fun connect(address: String?): Boolean {try {if (mBluetoothAdapter == null || address == null) {                             //Check that we have a Bluetooth adappter and device addressLog.i(TAG, "BluetoothAdapter not initialized or unspecified address.")     //Log a warning that something went wrongreturn false                                                               //Failed to connect}// Previously connected device.  Try to reconnect.if (mBluetoothDeviceAddress != null && address == mBluetoothDeviceAddress && mBluetoothGatt != null) { //See if there was previous connection to the deviceLog.i(TAG, "Trying to use an existing mBluetoothGatt for connection.")//See if we can connect with the existing BluetoothGatt to connect//Success//Were not able to connectreturn mBluetoothGatt!!.connect()}val device = mBluetoothAdapter!!.getRemoteDevice(address)?: //Check whether a device was returnedreturn false                                                               //Failed to find the device//No previous device so get the Bluetooth device by referencing its addressmBluetoothGatt = device.connectGatt(this, false, mGattCallback)                //Directly connect to the device so autoConnect is falsemBluetoothDeviceAddress = address                                              //Record the address in case we need to reconnect with the existing BluetoothGattreturn true} catch (e: Exception) {Log.i(TAG, e.message)}return false}

An important implementation in the BLE section is the GattCallBack. All the BLE action will get a callback in this section. For example, if the user initiates a connection with a scanner device, a callback will be initiated under the overridden method called onConnectionStateChange(). Similarly, all the read, write, notify actions will get the corresponding callback. We need to filter the result with the corresponding key to track the proper result.

private val mGattCallback = object : BluetoothGattCallback() {override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {//Change in connection stateif (newState == BluetoothProfile.STATE_CONNECTED) {//See if we are connectedLog.i(TAG, "**ACTION_SERVICE_CONNECTED**$status")broadcastUpdate(BLEConstants.ACTION_GATT_CONNECTED)//Go broadcast an intent to say we are connectedgatt.discoverServices()mBluetoothGatt?.discoverServices()//Discover services on connected BLE device} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {//See if we are not connectedLog.i(TAG, "**ACTION_SERVICE_DISCONNECTED**" + status);broadcastUpdate(BLEConstants.ACTION_GATT_DISCONNECTED)//Go broadcast an intent to say we are disconnected}}override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {              //BLE service discovery completeif (status == BluetoothGatt.GATT_SUCCESS) {                                 //See if the service discovery was successfulLog.i(TAG, "**ACTION_SERVICE_DISCOVERED**$status")broadcastUpdate(BLEConstants.ACTION_GATT_SERVICES_DISCOVERED)                       //Go broadcast an intent to say we have discovered services} else {                                                                      //Service discovery failed so log a warningLog.i(TAG, "onServicesDiscovered received: $status")}}//For information only. This application uses Indication to receive updated characteristic data, not Readoverride fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { //A request to Read has completed//String value = characteristic.getStringValue(0);//int value = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0);val values = characteristic.valueval clearValue = byteArrayOf(255.toByte())var value = 0if (null != values) {Log.i(TAG, "ACTION_DATA_READ VALUE: " + values.size)Log.i(TAG, "ACTION_DATA_READ VALUE: " + (values[0] and 0xFF.toByte()))value = (values[0] and 0xFF.toByte()).toInt()}BLEConnectionManager.writeEmergencyGatt(clearValue)if (status == BluetoothGatt.GATT_SUCCESS) {//See if the read was successfulLog.i(TAG, "**ACTION_DATA_READ**$characteristic")broadcastUpdate(BLEConstants.ACTION_DATA_AVAILABLE, characteristic)                 //Go broadcast an intent with the characteristic data} else {Log.i(TAG, "ACTION_DATA_READ: Error$status")}}//For information only. This application sends small packets infrequently and does not need to know what the previous write completedoverride fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { //A request to Write has completedif (status == BluetoothGatt.GATT_SUCCESS) {                                 //See if the write was successfulLog.e(TAG, "**ACTION_DATA_WRITTEN**$characteristic")broadcastUpdate(BLEConstants.ACTION_DATA_WRITTEN, characteristic)                   //Go broadcast an intent to say we have have written data}}override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic?) {if (characteristic != null && characteristic.properties == BluetoothGattCharacteristic.PROPERTY_NOTIFY) {Log.e(TAG, "**THIS IS A NOTIFY MESSAGE")}if (characteristic != null) {broadcastUpdate(BLEConstants.ACTION_DATA_AVAILABLE, characteristic)}                     //Go broadcast an intent with the characteristic data}}

The connection process is accomplished and the expected callback caught in the gattCallback section, you need to initiate service finding method. This method is basically collect all the available service in the connected BLE. gatt.discoverServices() method is used to discover the services. If the gatt successfully identify the service, a service callback will receive under the gattCallback. The overridden method is onServicesDiscovered(). The implementation is given below.

override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {              //BLE service discovery completeif (status == BluetoothGatt.GATT_SUCCESS) {                                 //See if the service discovery was successfulLog.i(TAG, "**ACTION_SERVICE_DISCOVERED**$status")broadcastUpdate(BLEConstants.ACTION_GATT_SERVICES_DISCOVERED)                       //Go broadcast an intent to say we have discovered services} else {                                                                      //Service discovery failed so log a warningLog.i(TAG, "onServicesDiscovered received: $status")}}

The service filtering methods are included in the “BLEConnectionManager” singleton class. You can cross check the implementation. The next step in the BLE is to read or write a data. The pinpointing logic here is if you want to read or write data, you need to know which service you are using and which characteristics you are using. The discoverService() will return that result.

The Read/Write implementation are given below.

// Request a read of a given BluetoothGattCharacteristic. The Read result is reported asynchronously through the// BluetoothGattCallback onCharacteristicRead callback method.// For information only. This application uses Indication to receive updated characteristic data, not Readfun readCharacteristic(characteristic: BluetoothGattCharacteristic) {if (mBluetoothAdapter == null || mBluetoothGatt == null) {                      //Check that we have access to a Bluetooth radioreturn}val status = mBluetoothGatt!!.readCharacteristic(characteristic)                              //Request the BluetoothGatt to Read the characteristicLog.i(TAG, "READ STATUS $status")}
// Write to a given characteristic. The completion of the write is reported asynchronously through the
// BluetoothGattCallback onCharacteristic Wire callback method.fun writeCharacteristic(characteristic: BluetoothGattCharacteristic) {try {if (mBluetoothAdapter == null || mBluetoothGatt == null) { //Check that we have access to a Bluetooth radioreturn}val test = characteristic.properties //Get the properties of the characteristicif (test and BluetoothGattCharacteristic.PROPERTY_WRITE == 0 && test and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE == 0) { //Check that the property is writablereturn}if (mBluetoothGatt!!.writeCharacteristic(characteristic)) { //Request the BluetoothGatt to do the WriteLog.i(TAG, "****************WRITE CHARACTERISTIC SUCCESSFUL**$characteristic")//The request was accepted, this does not mean the write completed/* if(characteristic.getUuid().toString().equalsIgnoreCase(getString(R.string.char_uuid_missed_connection))){}*/} else {Log.i(TAG, "writeCharacteristic failed") //Write request was not accepted by the BluetoothGatt}} catch (e: Exception) {Log.i(TAG, e.message)}}

The Read/Write callback will be received in the callback section which already mentioned earlier. You can cross-check whether the read/write got success or a failure from the callback. Also, you can track the available result from the callback. For example, in the business logic, you need to check the battery level of the BLE. You can User Battery service, Battery level characteristics for reading the level. The read result will receive with a value in the callback

The last section in this module is the notification process. If you got a service, you need to check the service has a notify property. If it’s having that property, you can make a set notify property call. The implementation is given below.

// Enable notification on a characteristic// For information only. This application uses Indication, not Notificationfun setCharacteristicNotification(characteristic: BluetoothGattCharacteristic, enabled: Boolean) {try {if (mBluetoothAdapter == null || mBluetoothGatt == null) {                      //Check that we have a GATT connectionLog.i(TAG, "BluetoothAdapter not initialized")return}mBluetoothGatt!!.setCharacteristicNotification(characteristic, enabled)          //Enable notification and indication for the characteristicfor (des in characteristic.descriptors) {if (null != des) {des.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE         //Set the value of the descriptor to enable notificationmBluetoothGatt!!.writeDescriptor(des)}}} catch (e: Exception) {Log.i(TAG, e.message)}}

Stack Overflow Discussions

I have included the links of my stack overflow discussion’s and major issues I have faced while developing the App. Cross check the below links.

https://stackoverflow.com/questions/48983333/getservicedata-of-le-scanrecord-returning-null/48987029#48987029

https://stackoverflow.com/questions/48185118/ble-scan-failed/48187562#48187562

https://stackoverflow.com/questions/48116897/oncharacteristicwriterequest-the-override-is-red-underlined/48186107#48186107

https://stackoverflow.com/questions/47628633/ble-oncharacteristicread-callback-not-called/47843647#47843647

Complete Codebase

You can access the complete codebase from my Github repo

Link: https://github.com/Nithinjith/LEKotlin-Android

--

--