BLE under the microscope. Android. Part 3

part 1, part 2

Introduction

Before we move on, I want to make three clarifications. The first is to give link to the project we created in the previous article and finalized in the current one. For those who didn't have enough strength to finish the previous lesson.

The second is to clarify the error that is related to the permissions check. checkPermission. As I found out, this error is generated by a tool called Lint, which helps developers catch potential problems before the code is compiled. You can read about it here. In a nutshell, it is a built-in code checking tool. It looks for potential errors. In our case, it was indignant that we do not check critical permissions before starting/finishing the broadcast scan. But the thing is that we do check them, though in a different function. Therefore, to remove the error message, you need to write a line before the function that cancels the check – @SuppressLint(“MissingPermission”). Then we will disable the message about this particular error. In fact, the same commands are also in the functions onScanResult() And listShow(). If you remember, my lesson was based on the project le_scan_classic_connectin which suppressive instructions were used @SuppressLint(“MissingPermission”). So in our code, before the functions startScanning() And stopScanning(), They also need to be registered.

The third clarification concerns debugging the application. There is a very convenient thing called Logcat. If you look in the AndroidStudio environment in the lower left corner, you will see several different tabs there. We are interested in the tab with the cat. In the picture below, it is open. Having opened this tab, during the execution of our program, we will see many lines indicating the time, the source of the event log and the log itself.

Logcat

Logcat

From the text of our applications It is easy to see that the messages in the log are sent by lines starting with Log.d And Log.e and they have different colors. In this guide you can learn more about how to use this code debugging tool. I will give a short list of its features. Depending on the extension, the color of the log text will be different.

Log.e() – errors (error) red
Log.w() – warnings (warning) brown
Log.i() – info (info) green
Log.d() – debug (degub) blue
Log.v() – details (verbose) black

BluetoothGattCallback() Callbacks

So, let's finally move on to our application. Let's continue filling it with new functions. Let's add the following text at the bottom of the MainActivity file, which is responsible for BluetoothGattCallback.

    //************************************************************************************
    //                      C O N N E C T   C A L L   B A C K
    //************************************************************************************
    //    The connectGatt method requires a BluetoothGattCallback
    //    Here the results of connection state changes and services discovery would be delivered asynchronously.

    protected BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
        //*********************************************************************************
        private volatile boolean isOnCharacteristicReadRunning = false;

        @SuppressLint("MissingPermission")
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            super.onConnectionStateChange(gatt, status, newState);
            String address = gatt.getDevice().getAddress();

            if (status == BluetoothGatt.GATT_SUCCESS) {
                if (newState == BluetoothGatt.STATE_CONNECTED) {
                    Log.w(TAG, "onConnectionStateChangeMy() - Successfully connected to " + address);
                    boolean discoverServicesOk = gatt.discoverServices();
                    Log.i(TAG, "onConnectionStateChange: discovered Services: " + discoverServicesOk);
                } else if (newState == BluetoothGatt.STATE_DISCONNECTED) {
                    Log.w(TAG, "onConnectionStateChangeMy() - Successfully disconnected from " + address);
                    gatt.close();
                }
            } else {
                Log.w(TAG, "onConnectionStateChangeMy: Error " + status + " encountered for " + address);
            }
        }


    };
}

I hope no one is surprised by the presence of new classes highlighted in red? We do as usual – we import classes. We move the mouse to the red font and agree with the proposed import.

Processing Callbacks

Processing Callbacks

In this part of the code we will handle callbacks BluetoothGatt stack. Under one roof BluetoothGattCallback various Callbacks can be found. For now, we only use onConnectionStateChange(), which is responsible for the initial connection to the BLE device. I will immediately provide a full list of possible Callbacks. As you can see, we can intercept various events – reading and writing characteristics and descriptors. And also some other events, for example, reading the RSSI level.

public void onServicesDiscovered(BluetoothGatt g, int stat) {}
public void onConnectionStateChange(BluetoothGatt g, int stat, int newState) {}
public void onCharacteristicRead(BluetoothGatt g, BluetoothGattCharacteristic c, int stat) {}
public void onCharacteristicWrite(BluetoothGatt g, BluetoothGattCharacteristic c, int stat) {}
public void onDescriptorRead(BluetoothGatt g, BluetoothGattDescriptor d, int stat) {}
public void onDescriptorWrite(BluetoothGatt g, BluetoothGattDescriptor d, int stat) {}
public void onReliableWriteCompleted(BluetoothGatt g, int stat) {}
public void onReadRemoteRssi(BluetoothGatt g, int rssi, int stat) {}
public void onCharacteristicChanged(BluetoothGatt g, BluetoothGattCharacteristic c) {}

Let's move on. To get into callback processing BluetoothGatt, There's one more thing that needs to be done. You need to unlock this line in the initialization block.

Initializing callbacks

Initializing callbacks

And also initialize the mBluetoothGatt object. In addition, so that our callbacks do not irritate the Lint static analysis tool, before the onCreate() function, you need to insert the instruction @SuppressLint(“MissingPermission”). These two additions are highlighted in the figure below.

We supplement the code with new content

We supplement the code with new content

Now that all the current changes are ready, let's try to run the project. We won't see anything new in the behavior of the application itself. However, let's apply our new knowledge about Logcat. Download the project to your smartphone, open the Logcat tab in Android Studio and, after scanning the air, try to connect to the BLE device. This is done by simply clicking on the line with one of the found devices. Then the deviceListView.setOnItemClickListener() click function will work and we will get to the BLE onConnectionStateChange() callback. If our gadget allows you to connect to it by sending ADV_IND type packets, then we will see approximately the following event log.

Device connection log

Device connection log

Take a closer look at it. Some of the lines belong to the BLE stack itself, and the other part is our work 🙂 In the future, by sending messages to Logcat, we will be able to understand the state of our application and fix any errors that arise. Let's move on. To do this, we will slightly correct our application.

Reading UUIDs of services and characteristics

To work with services and characteristics, we need to somehow display them on the screen. At first, I planned to display them in another window, but decided not to complicate things. Let's just move our listView up and add another similar element to our form. We will immediately bind it to the edges of the screen. The environment gives it the name listView2.

Adding a new listView

Adding a new listView

We will add such a header in our activity file. We need all these elements to manage a new element, which we will call a list of services and characteristics 🙂

    ListView listServChar;
    ArrayAdapter<String> adapterServChar;
    ArrayList<String> listServCharUUID = new ArrayList<>();
Adding a list of services and characteristics

Adding a list of services and characteristics

In the initialization section, we will designate a new element, and also specify the function for processing clicks on its lines. Let's call it CharactRxData(). It will allow us to read data.

        listServChar = findViewById(R.id.listView2);
        adapterServChar = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1);
        listServChar.setAdapter(adapterServChar);
        listServChar.setOnItemClickListener((adapterView, view, position, id) -> {
            String uuid =  listServCharUUID.get(position);
            CharactRxData(uuid);
        });
Initializing listServChar

Initializing listServChar

Then below the onConnectionStateChange() function we add another callback function – onServicesDiscovered() and a function for handling clicks on the list listServChar.

        //************************************************************************************
        @SuppressLint("MissingPermission")
        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            super.onServicesDiscovered(gatt, status);
            final List<BluetoothGattService> services = gatt.getServices();
            runOnUiThread(() -> {

                for (int i = 0; i < services.size(); i++) {
                    BluetoothGattService service = services.get(i);
                    List<BluetoothGattCharacteristic> characteristics = service.getCharacteristics();
                    StringBuilder log = new StringBuilder("\nService Id: \n" + "UUID: " + service.getUuid().toString());

                    adapterServChar.add("     Service UUID : " + service.getUuid().toString());
                    listServCharUUID.add(service.getUuid().toString());

                    for (int j = 0; j < characteristics.size(); j++) {
                        BluetoothGattCharacteristic characteristic = characteristics.get(j);
                        String characteristicUuid = characteristic.getUuid().toString();

                        log.append("\n   Characteristic: ");
                        log.append("\n   UUID: ").append(characteristicUuid);

                        adapterServChar.add("Charact UUID : " + characteristicUuid);
                        listServCharUUID.add(service.getUuid().toString());
                    }
                    Log.d(TAG, "\nonServicesDiscovered: New Service: " + log);
                 }
            });
        }
    };
    //************************************************************************************
    public void CharactRxData(String uuid)
    {
        Log.d(TAG, "UUID : " + uuid);
        textViewTemp.setText(uuid);
    }
}
//****************************************************************************************

Then everything is as usual, we agree to import new classes.

Processing requests for services and characteristics

Processing requests for services and characteristics

Now let's try to run our application. If we did everything correctly, we should not have any errors. In order to see all the possibilities of our program, we will enable anti-loss and launch the application.

Let's figure out how to get this picture. After we pressed the button Startin the upper window you need to find our anti-loss. It will have the name iTAG. Click on this line. Our application will try to connect to this gadget and read services and characteristics from it. This action is not fast and it occurs in the callback function onServicesDiscovered(). So that our application does not hang, we execute it in a separate thread, using the instruction runOnUiThread(). Everything that was read from the device will be written in the second window. If you now click any line in it, the UUID of the service or characteristic will be written to textViewTemp. This is not the end point of our application, but without making sure that we are reading the UUID of the device correctly, there is no point in going further. In the onServicesDiscovered() function, we also write new lines with the UUID to Logcat. Go to this tab and see for yourself. The picture there is beautiful 🙂

Writing and reading characteristics

Let's pause for a moment and formulate our next steps. So, we can already connect to the iTAG device and read what services and characteristics it has. But we don't know how to work with them yet. If you look in the wonderful program nRF Connect to our iTAG, we will see that it has service 0x1802 – Immediate Alert. And if you open it, there is a characteristic 0x2A06, which allows you to write some values ​​to it. If you write 0x01, as in the picture below, our anti-loss will start beeping. If you write 0x00, it will go silent. That's why I want to add two buttons to our application. The first one will invert the bit flag and write it to the characteristic 0x2A06.

Writing a byte to characteristic 0x2A06

Writing a byte to characteristic 0x2A06

The second button will read the output power level value from the 0x2A07 characteristic of the 0x1804 service. In the figure below, you can see that it is equal to 0x07.

Reading bit from characteristic 0x2A07

Reading bit from characteristic 0x2A07

So let's get started. Let's add two more buttons to our form and bind them to the screen borders. I made these buttons square. Just to diversify our application 🙂 You can do the same in the settings on the right, in the style section, or leave everything as is. To make everything look nice, you need to move the other elements of the screen a little.

Adding new buttons

Adding new buttons

Now let's denote them as TX And RX and initialize. In the header we will write

    Button txButton;
    Button rxButton;

And in the initialization field

        txButton = findViewById(R.id.button3);
        txButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {CharactTx();}
        });
        rxButton = findViewById(R.id.button4);
        rxButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {CharactRx();}
        });
Initialize new buttons

Initialize new buttons

Here we introduce new functions CharactTx() and CharactRx(), the processing of which we will write below in the text. In the first, we will form a packet for transmission to the characteristic 0x2A06. And we will receive the response from the stack in the callback function onCharacteristicWrite(). In this case, these will be statuses showing whether the recording was made or not. In addition, in the textViewTemp line we will write what we sent to the characteristic. We will place this function immediately after onServicesDiscovered().

  @Override
  public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
      super.onCharacteristicWrite(gatt, characteristic, status);

      if (status == BluetoothGatt.GATT_SUCCESS) {
          textViewTemp.setText("Запись " + Integer.toString(alertStatus) + " успешна");
          Log.i(TAG, "onCharacteristicRead: Write characteristic: UUID: " + characteristic.getUuid().toString());
      } else if (status == BluetoothGatt.GATT_READ_NOT_PERMITTED) {
          Log.e(TAG, "onCharacteristicRead: Write not permitted for " + characteristic.getUuid().toString());
      } else {
          Log.e(TAG, "onCharacteristicRead: Characteristic write failed for " + characteristic.getUuid().toString());
      }
  }
onCharacteristicWrite() callback function

onCharacteristicWrite() callback function

Now let's add the CharactTx() function itself, and instead of CharactRx() we'll put a stub.

    //************************************************************************************
    public boolean CharactTx()
    {
        if ((mBluetoothGatt == null) || (serviceAlert == null) || (characteristicAlert == null)) {
            return false;
        }
        alertStatus ^= 0x01;
        byte[] alert = new byte[1];
        alert[0] = (byte)alertStatus;

        characteristicAlert.setValue(alert);
        characteristicAlert.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);

        if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.BLUETOOTH_CONNECT}, 0);
        }
        mBluetoothGatt.writeCharacteristic(characteristicAlert);
        return true;
    }
    //************************************************************************************
    public void CharactRx()
    {
    }

However, to make it work, we will have to add a number of elements again. In the header after initializing the buttons, we will introduce new elements, namely, the alarm switching flag and global variables of services and characteristics. We use some for transmission, others for reading.

    int alertStatus = 0;
    BluetoothGattService serviceAlert, servicePower;
    BluetoothGattCharacteristic characteristicAlert, characteristicPower;
Adding new elements

Adding new elements

To access the characteristics, we need to know their UUID and other parameters. The easiest way is to read them and save them in global variables when we request the entire list of attributes. To do this, we make changes to the code again and again 🙂 This time in the onServicesDiscovered() function. When we see our characteristics 0x2A06 and 0x2A07, we save their values. We need to check the full 128-bit UUID. In the figure below, I underlined the 16-bit UUID as part of the full 128-bit one.

    if (characteristicUuid.equals("00002a06-0000-1000-8000-00805f9b34fb")) {
        characteristicAlert = characteristic;
        serviceAlert = service;
    }
    if (characteristicUuid.equals("00002a07-0000-1000-8000-00805f9b34fb")) {
        characteristicPower = characteristic;
        servicePower = service;
    }
We save the attributes we need

We save the attributes we need

It's time to check how the recording in the characteristic works. Launch iTAG, launch our application. Click on the button Startand then find the iTAG device in the upper window and click on it. After a while, a list of attributes will appear in the lower window. Now click on the button TX. The status bar will show the message “Record 1 successful”and our anti-lost will start beeping. Press the button TX once more – she will shut up. Hurray, we have learned how to record data in the characteristic.

Writing data to a characteristic

Writing data to a characteristic

Now let's learn how to read data from the characteristic. We've already prepared a lot for this. Let's write two new functions. The first is the onCharacteristicRead() callback function. Let's write it right after onCharacteristicWrite(). In both cases, when reading and writing data, I used a buffer approach. So that in the future, when you process more than one byte, you don't have to redo anything.

public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {

    super.onCharacteristicRead(gatt, characteristic, status);
    if (status == BluetoothGatt.GATT_SUCCESS) {
        Log.i(TAG, "onCharacteristicRead: Read characteristic: UUID: " + characteristic.getUuid().toString());

        byte[] valueInputCode = new byte[characteristic.getValue().length];
        System.arraycopy(characteristic.getValue(), 0, valueInputCode, 0, characteristic.getValue().length);

        StringBuffer sb1 = new StringBuffer();
        for (int j = 0; j < valueInputCode.length; j++) {
            sb1.append(String.format("%02X", valueInputCode[j]));
        }
        Log.i(TAG, "onCharacteristicRead: Value: " + sb1);
        textViewTemp.setText("Уровень мощности = " + sb1.toString());

    } else if (status == BluetoothGatt.GATT_READ_NOT_PERMITTED) {
        Log.e(TAG, "onCharacteristicRead: Read not permitted for " + characteristic.getUuid().toString());
    } else {
        Log.e(TAG, "onCharacteristicRead: Characteristic read failed for " + characteristic.getUuid().toString());
    }
}
onCharacteristicRead() callback function

onCharacteristicRead() callback function

The second function is a request to read the characteristic. We already had a stub for this function. Now let's fill it with content. To prevent Lint from complaining, let's block it.

    @SuppressLint("MissingPermission")
    public void CharactRx()
    {
        mBluetoothGatt.readCharacteristic(characteristicPower);
    }
Handling RX button press

Handling RX button press

We launch iTAG, our application and do the same as before, when we checked the data transfer. After receiving a list of services and characteristics from the device, we press the button RX. The status bar should show the signal level, I underlined it in red. You may have to stretch the status bar a little, as I showed in the picture.

.

Conclusion

So my series of articles about Android has come to an end. I tried to teach you in as much detail as possible how to write simple programs for managing BLE devices. I think it will be useful to many hardware programmers. At the time, I was unable to find anything similar, so I had to figure it out myself. If you have any comments, write them. But only on the merits, and even better, write a good article yourself. This way you will earn bonuses for yourself and help others. I updated my project on GitHub taking into account the modifications made. I wish everyone success in their professional growth.

Pecherskikh Vladimir

Employee of the Caesar Satellite Group of Companies

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *