'How to read data from notification characteristic BLE with Kotlin

I am able to connect to my BLE device and send data from my Android app, but I am not able to read the data from the BLE (I need to display this data in a graph), but when I try to retrieve the values, I get a null pointer.

Here is the code for the activity page:

package com.example.lightrdetect

import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattCharacteristic.*
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.example.lightrdetect.ble.ConnectionEventListener
import com.example.lightrdetect.ble.isReadable
import com.github.mikephil.charting.charts.ScatterChart
import com.github.mikephil.charting.components.XAxis
import com.github.mikephil.charting.components.YAxis
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.ScatterData
import com.github.mikephil.charting.data.ScatterDataSet
import com.punchthrough.blestarterappandroid.ble.ConnectionManager
import kotlinx.android.synthetic.main.activity_home_page.image_lightr
import kotlinx.android.synthetic.main.activity_tracking_page.*
import org.jetbrains.anko.alert
import java.text.SimpleDateFormat
import java.util.*


class TrackingPageActivity : AppCompatActivity() {

    private lateinit var device : BluetoothDevice
    private val dateFormatter = SimpleDateFormat("MMM d, HH:mm:ss", Locale.FRANCE)
    private var listeners: MutableSet<WeakReference<ConnectionEventListener>> = mutableSetOf()
    private val deviceGattMap = ConcurrentHashMap<BluetoothDevice, BluetoothGatt>()
    private val operationQueue = ConcurrentLinkedQueue<BleOperationType>()
    private var pendingOperation: BleOperationType? = null
    private val characteristic by lazy {
        ConnectionManager.servicesOnDevice(device)?.flatMap { service ->
            service.characteristics ?: listOf()
        } ?: listOf()
    }

    private val characteristicProperty by lazy {
        characteristic.map { characteristic->
            characteristic to mutableListOf<CharacteristicProperty>().apply {
                if(characteristic.isNotifiable()) add(CharacteristicProperty.Notifiable)
                if (characteristic.isIndicatable()) add(CharacteristicProperty.Indicatable)
                if(characteristic.isReadable()) add(CharacteristicProperty.Readable)
            }.toList()
        }.toMap()
    }

    private val characteristicAdapter: CharacteristicAdapter by lazy {
        CharacteristicAdapter(characteristic){characteristicProperty ->

        }
    }

    companion object{
        //var UUID_Read_notification = UUID.fromString("D973F2E1-B19E-11E2-9E96-0800200C9A66")
        var UUID_Read = "D973F2E1-B19E-11E2-9E96-0800200C9A66"
    }


    private var notifyingCharacteristics = mutableListOf<UUID>()

    override fun onCreate(savedInstanceState: Bundle?) {
        ConnectionManager.registerListener(connectionEventListener)
        super.onCreate(savedInstanceState)
        device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
            ?: error("Missing BluetoothDevice from Home Page Activity")
        setContentView(R.layout.activity_tracking_page)

        image_lightr.setOnClickListener {
            finish()
        }

        window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
        actionBar?.hide()
        supportActionBar?.hide()

        ScatterChartData()
    }


    private fun ScatterChartData(){
        readSensor(UUID_Read)

        val scatterEntry = ArrayList<Entry>()
        scatterEntry.add(Entry(0f, 3f))


        val sensorPosition = ArrayList<Entry>()
        sensorPosition.add(Entry(0f, 0f))

        val scatterDataSet_sensor = ScatterDataSet(sensorPosition, "Sensor")
        scatterDataSet_sensor.color = resources.getColor(R.color.white)
        scatterDataSet_sensor.setScatterShape(ScatterChart.ScatterShape.CHEVRON_DOWN)
        scatterDataSet_sensor.scatterShapeSize = 30f

        val scatterDataSet = ScatterDataSet(scatterEntry, "Target")
        scatterDataSet.color = resources.getColor(R.color.jaune_woodoo)
        scatterDataSet.setScatterShape(ScatterChart.ScatterShape.CIRCLE)
        scatterDataSet.valueTextColor = resources.getColor(R.color.transparent      )
        scatterDataSet.scatterShapeSize = 30f

        val scatterlistfinal = ArrayList<ScatterDataSet>()
        scatterlistfinal.add(scatterDataSet)
        scatterlistfinal.add(scatterDataSet_sensor)


        val scatterData = ScatterData(scatterlistfinal as List<ScatterDataSet>)
        chart1.data = scatterData
        chart1.setBackgroundColor(resources.getColor(R.color.transparent))
        chart1.animateXY(1000, 1000)
        chart1.legend.isEnabled = false


        val xAxis : XAxis = chart1.xAxis
        xAxis.position = XAxis.XAxisPosition.TOP
        //xAxis.setDrawGridLines(true)
        xAxis.axisLineColor = resources.getColor(R.color.white)
        xAxis.axisMaximum = 90f
        xAxis.axisMinimum = -90f
        xAxis.textColor = resources.getColor(R.color.white)
        xAxis.axisLineWidth = 5f

        val yAxisL : YAxis = chart1.axisLeft
        yAxisL.textColor = resources.getColor(R.color.white)
        yAxisL.isInverted = true
        yAxisL.axisMaximum = 5f
        yAxisL.axisMinimum = 0f
        yAxisL.axisLineWidth = 0f
        yAxisL.setLabelCount(6, true)
        yAxisL.axisLineColor = resources.getColor(R.color.transparent)

        val yAxisR : YAxis = chart1.axisRight
        yAxisR.textColor = resources.getColor(R.color.white)
        yAxisR.isInverted = true
        yAxisR.axisMaximum = 5f
        yAxisR.axisMinimum = 0f
        yAxisR.axisLineWidth = 0f
        yAxisR.setLabelCount(6, true)
        yAxisR.axisLineColor = resources.getColor(R.color.transparent)
    }
    

    private fun showCharacteristicOptions(characteristic: BluetoothGattCharacteristic) {
        characteristicProperty[characteristic]?.let { properties ->
            selector("Select an action to perform", properties.map { it.action }) { _, i ->
                when (properties[i]) {
                    CharacteristicProperty.Readable -> {
                        //log("Reading from ${characteristic.uuid}")
                        ConnectionManager.readCharacteristic(device, characteristic)
                    }
                    CharacteristicProperty.Notifiable, CharacteristicProperty.Indicatable -> {
                        if (notifyingCharacteristics.contains(characteristic.uuid)) {
                            //log("Disabling notifications on ${characteristic.uuid}")
                            ConnectionManager.disableNotifications(device, characteristic)
                        } else {
                            //log("Enabling notifications on ${characteristic.uuid}")
                            ConnectionManager.enableNotifications(device, characteristic)
                        }
                    }
                }
            }
        }
    }

    private fun readSensor(characteristic: String){
        var gattCharacteristic = BluetoothGattCharacteristic(UUID.fromString(characteristic), PROPERTY_READ, PERMISSION_READ_ENCRYPTED)
        showCharacteristicOptions(gattCharacteristic)
        var data : String
        if (gattCharacteristic !=null) {
            ConnectionManager.enableNotifications(device, gattCharacteristic)
            data = ConnectionManager.readCharacteristic(device, gattCharacteristic).toString()
            Log.d("sensor", "value " + data)
        }

    }


    private val connectionEventListener by lazy {
        ConnectionEventListener().apply {
            onDisconnect = {
                runOnUiThread {
                    alert {
                        title = "Disconnected"
                        message = "Disconnected from device."
                        positiveButton("ok"){onBackPressed()}
                    }.show()
                }
            }

            onCharacteristicRead = {_, characteristic ->
                Log.i("Tracking page","Read from ${characteristic.uuid}: ${characteristic.value.toHexString()}")
            }

            onNotificationsEnabled = {_,characteristic ->
                Log.i("Tracking page","Enabled notifications on ${characteristic.uuid}")
                notifyingCharacteristics.add(characteristic.uuid)
            }
        }
    }

    private enum class CharacteristicProperty {
        Readable,
        Writable,
        WritableWithoutResponse,
        Notifiable,
        Indicatable;

        val action
            get() = when (this) {
                Readable -> "Read"
                Writable -> "Write"
                WritableWithoutResponse -> "Write Without Response"
                Notifiable -> "Toggle Notifications"
                Indicatable -> "Toggle Indications"
            }
    }

}

and there is the error that I have

2022-03-17 10:49:54.768 31034-31034/com.example.lightrdetect E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.lightrdetect, PID: 31034
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.lightrdetect/com.example.lightrdetect.TrackingPageActivity}: java.lang.NullPointerException: gattCharacteristic.getValue() must not be null
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3851)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4027)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2336)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:247)
        at android.app.ActivityThread.main(ActivityThread.java:8676)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)
     Caused by: java.lang.NullPointerException: gattCharacteristic.getValue() must not be null
        at com.example.lightrdetect.TrackingPageActivity.ScatterChartData(TrackingPageActivity.kt:89)
        at com.example.lightrdetect.TrackingPageActivity.onCreate(TrackingPageActivity.kt:78)
        at android.app.Activity.performCreate(Activity.java:8215)
        at android.app.Activity.performCreate(Activity.java:8199)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3824)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4027) 
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85) 
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135) 
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2336) 
        at android.os.Handler.dispatchMessage(Handler.java:106) 
        at android.os.Looper.loop(Looper.java:247) 
        at android.app.ActivityThread.main(ActivityThread.java:8676) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130) 

this is a screenshot of the nRF application that shows me all the features: nRF screen shot

I checked with the BLE module support and they told me that:

The Android application can write on the Rx characteristic and automatically the data will be sent on the UART (Tera Term or a µC connected on UART) The µC or Tera Term to push data will have to emit on the Tx, this is what the Application code of the ST Serial Port Profile code does, when it receives on the UART an end of string character (CR+LF) (to be set in the Tera Term or in the STM32 Application code). However, for the Android application to receive the data, it must be registered on the notifications (slide 10 of the doc - the slide refers to an Ios mobile on Android to activate the notifications, you must click on the 3 arrows)

I checked with Tera Term, I can see the data on nRF. My question now is how can I read the characteristic notification?

Best regard.



Solution 1:[1]

The provided image shows the 3 services your device offers:

  • Generic Attribute
  • Generic Access
  • Unknown Service

The first two are pretty standard for a BLE device. The Generic Attribute Service offers you one characteristic called "Service Changed" which allows to get notified if your device changes something about its services during runtime. The Generic Access Service contains the device name and other information.

You probably want to talk to the Service labeled "Unknown Service" by nRF Connect. This simply means that the UUID used for this service is not in its list of know services. nRF Connect shows two characteristics for this service, one to receive data from which also allows receiving notifications, and one to send data. It basically looks like a UART over BLE implementation.

Based on your source code it seems like you are using the wrong UUID. Your companion object refers to the correct UUID for notifications, but not the correct one for reading:

companion object{
    var UUID_Read_notification = UUID.fromString("D973F2E1-B19E-11E2-9E96-0800200C9A66")
    var UUID_Read = UUID.fromString("00002A04-0000-1000-8000-00805F9B34FB")
}

The UUID to read from is the same as the notify UUID: D973F2E1-B19E-11E2-9E96-0800200C9A66. If you also want to write to the device you have to use the UUID D973F2E2-B19E-11E2-9E96-0800200C9A66.

As I said in the comments, the UUID you used before (00002A04-0000-1000-8000-00805F9B34FB) belongs to the "Peripheral Preferred Connection Parameters" characteristic of the Generic Access Service and only allows reading.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Michael Kotzjan