In Part 1 of Bluetooth Low Energy on Android, we set up a BLE Server and Client and established a connection between them. In this second part, it’s time to take a look at how to work with GATT Characteristics to send and receive data.

Characteristics have various attributes, including a UUID, Properties, Permissions, and Value. Properties describe what can be done with the Characteristic, such as read, write, and notify. Permissions describe what should be allowed, e.g. read and write. As you will see, not all of these attributes are needed to achieve the functionality we want.

Let’s add a Characteristic that will allow Clients to send a message to the Server. The Server will reverse the message and send it back using a BLE Notification. Notifications allow the Client to know that a Characteristic’s value has changed. Add the Characteristic to our Server setup with write capabilities:

private void setupServer () { BluetoothGattService service = new BluetoothGattService ( SERVICE_UUID , BluetoothGattService . SERVICE_TYPE_PRIMARY ); BluetoothGattCharacteristic writeCharacteristic = new BluetoothGattCharacteristic ( CHARACTERISTIC_UUID , BluetoothGattCharacteristic . PROPERTY_WRITE , BluetoothGattCharacteristic . PERMISSION_WRITE ); service . addCharacteristic ( writeCharacteristic ); mGattServer . addService ( service ); }

We now have a Characteristic setup that has write Properties and Permissions, with no read or notify. Let’s switch to the Client and try to find the Characteristic.

Discover Services

Back in our BluetoothGattCallback , once the Client connects and we receive a GATT_SUCCESS and STATE_CONNECTED , we now must discover the services of the GATT Server.

public void onConnectionStateChange ( BluetoothGatt gatt , int status , int newState ) { ... if ( newState == BluetoothProfile . STATE_CONNECTED ) { mConnected = true ; gatt . discoverServices (); } else if ( newState == BluetoothProfile . STATE_DISCONNECTED ) { ...

This will bring us to the next callback we must implement, onServicesDiscovered .

public void onServicesDiscovered ( BluetoothGatt gatt , int status ) { super . onServicesDiscovered ( gatt , status ); if ( status != BluetoothGatt . GATT_SUCCESS ) { return ; } }

Just as you did before, check the status and return if it is not successful. If the discovery services was successful, we can now look for our Characteristic. Since we know the full UUID of both the Service and the Characteristic, we can access them directly.

public void onServicesDiscovered ( BluetoothGatt gatt , int status ) { ... return ; } BluetoothGattService service = gatt . getService ( SERVICE_UUID ); BluetoothGattCharacteristic characteristic = service . getCharacteristic ( CHARACTERISTIC_UUID ); }

Now that we have found our Characteristic, we need to set the write type and enable notifications.

public void onServicesDiscovered ( BluetoothGatt gatt , int status ) { ... BluetoothGattCharacteristic characteristic = service . getCharacteristic ( CHARACTERISTIC_UUID ); characteristic . setWriteType ( BluetoothGattCharacteristic . WRITE_TYPE_DEFAULT ); mInitialized = gatt . setCharacteristicNotification ( characteristic , true ); }

mInitialized is used to signify that our Characteristic is fully ready for use.

Without reaching this point, the Characteristic would not have the correct write type or notify us when there is a change. Make sure to set this to false when disconnecting from the GATT server. If you have added logs into each step, you should now be able to connect to the Server and see that the Client has initialized our Characteristic.

Characteristic Requests

Our Server now has a write Characteristic, the Client is connected, has found the Characteristic, and has signed up for notifications. However, the Server now needs to know what to do when a write request is received. Let’s take a look at BluetoothGattServerCallback.onCharacteristicWriteRequest :

public void onCharacteristicWriteRequest ( BluetoothDevice device , int requestId , BluetoothGattCharacteristic characteristic , boolean preparedWrite , boolean responseNeeded , int offset , byte [] value ) { super . onCharacteristicWriteRequest ( device , requestId , characteristic , preparedWrite , responseNeeded , offset , value ); if ( characteristic . getUuid (). equals ( CHARACTERISTIC_UUID )) { mGattServer . sendResponse ( device , requestId , BluetoothGatt . GATT_SUCCESS , 0 , null ); } }

First, we check to see if the UUID matches our Characteristic. Next, we send a Success response letting the Client know that the write request was received. Note that we do not need to check the Characteristic’s permissions, because the GATT server has already done so. Now let’s reverse the value of the Characteristic to differentiate the response from the request.

public void onCharacteristicWriteRequest ( BluetoothDevice device , ...) { ... mGattServer . sendResponse ( device , requestId , BluetoothGatt . GATT_SUCCESS , 0 , null ); int length = value . length ; byte [] reversed = new byte [ length ]; for ( int i = 0 ; i < length ; i ++) { reversed [ i ] = value [ length - ( i + 1 )]; } } }

After setting the Characteristic’s new value, we tell mGattServer to notify all connected devices for this characteristic.

public void onCharacteristicWriteRequest ( BluetoothDevice device , ...) { ... reversed [ i ] = value [ length - ( i + 1 )]; } characteristic . setValue ( reversed ); for ( BluetoothDevice device : mDevices ) { mGattServer . notifyCharacteristicChanged ( device , characteristic , false ); } } }

The final parameter is used to require confirmation of notification receipt.

This is used for Indications, but not for Notifications so we pass false .

Note: Since our Characteristic does not have read Permission, we do not have to implement onCharacteristicReadRequest , but it would be handled similarly to onCharacteristicWriteRequest .

Sending Data

Now that the Server is ready to handle our write request, let’s send a message! First wire up an EditText for user input and a Button to send the data. Before doing anything, make sure we are connected and our Characteristic is initialized. Then find our Characteristic and send the message.

private void sendMessage () { if (! mConnected || ! mEchoInitialized ) { return ; } BluetoothGattService service = gatt . getService ( SERVICE_UUID ); BluetoothGattCharacteristic characteristic = service . getCharacteristic ( CHARACTERISTIC_UUID ); String message = mBinding . messageEditText . getText (). toString (); }

In order to send the data we must first convert our String to byte[] .

private void sendMessage () { ... String message = mBinding . messageEditText . getText (). toString (); byte [] messageBytes = new byte [ 0 ]; try { messageBytes = message . getBytes ( "UTF-8" ); } catch ( UnsupportedEncodingException e ) { Log . e ( TAG , "Failed to convert message string to byte array" ); } }

Now set the value on the Characteristic and our message will be sent!

private void sendMessage () { ... Log . e ( TAG , "Failed to convert message string to byte array" ); } characteristic . setValue ( messageBytes ); boolean success = mGatt . writeCharacteristic ( characteristic ); }

Optionally, we could implement BluetoothGattCallback.onCharacteristicWrite and add a log to see if the write was successful.

Reading Data

At this point the Server has our message, will reverse the bytes and send back a notification. The client needs to handle that in BluetoothGattCallback.onCharacteristicChanged . Start by getting the value from the Characteristic, then convert it to a String and log it out.

public void onCharacteristicChanged ( BluetoothGatt gatt , BluetoothGattCharacteristic characteristic ) { super . onCharacteristicChanged ( gatt , characteristic ); byte [] messageBytes = characteristic . getValue (); String messageString = null ; try { messageString = new String ( bytes , "UTF-8" ); } catch ( UnsupportedEncodingException e ) { Log . e ( TAG , "Unable to convert message bytes to string" ); } Log . d ( "Received message: " + messageString ); }

And voila, we have successfully sent a message between our BLE Server and Client!

Wrap

If you have tried sending a very long message, you may have noticed that it gets truncated. Characteristics have a Maximum Transmission Unit (MTU) of 20 bytes, thus anything more is silently dropped. You must either loop through the data to send in parts or the Client may requestMtu of a larger size if the GATT Server supports it.

As before, you can find the full source for this post on my public GitHub repository. Stay tuned… Next time, we will go into a more advanced BLE attribute: Descriptors. The GATT server must handle their use manually, and can quickly become complicated.