Previously in this series we’ve looked at the various steps that we need to take before we can begin to get temperature and humidity notifications from a TI SensorTag. In the final article in this series we’ll complete things by registering to receive notifications, and receiving them
Now that the local proxy component knows about the services provided by the sensor we can actually begin to use them. First we need to obtain the service, then the characteristics within that service, and finally the descriptors within the characteristics in order to use them.
A GATT service is represented as a BluetoothGattService object which we need to obtain from the BluetoothGatt instance using the appropriate UUID; A GATT characteristic is represented as a BluetoothGattCharacteristic object which we obtain from the BluetoothGattService using the appropriate UUID; and a GATT descriptor is represented by a BluetoothGattDescriptor object which we obtain from the BluetoothGattCharacteristic using the appropriate UUID:
private static final UUID UUID_HUMIDITY_SERVICE = UUID.fromString("f000aa20-0451-4000-b000-000000000000"); private static final UUID UUID_HUMIDITY_DATA = UUID.fromString("f000aa21-0451-4000-b000-000000000000"); private static final UUID UUID_HUMIDITY_CONF = UUID.fromString("f000aa22-0451-4000-b000-000000000000"); private static final UUID UUID_CCC = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); . . . private void subscribe(BluetoothGatt gatt) { BluetoothGattService humidityService = gatt.getService(UUID_HUMIDITY_SERVICE); if(humidityService != null) { BluetoothGattCharacteristic humidityCharacteristic = humidityService.getCharacteristic(UUID_HUMIDITY_DATA); BluetoothGattCharacteristic humidityConf = humidityService.getCharacteristic(UUID_HUMIDITY_CONF); if(humidityCharacteristic != null && humidityConf != null) { BluetoothGattDescriptor config = humidityCharacteristic.getDescriptor(UUID_CCC); if(config != null) { } } } }
I have excluded any kind of error reporting for the sake of clarity of the example code, but in a real world app you should consider how to inform the user that something has gone wrong.
Reading the value from the UUID_HUMIDITY_DATA
characteristic is simply a matter of calling gatt.readCharacteristic(humidityCharacteristic)
, and overriding onReadCharacteristic in out BluetoothGattCallback implementation (once again an async operation so it’s OK to call readCharacteristic()
on the UI thread). This is fine for one-off operations, but for our app we want to constantly monitor the temperature and humidity and report these back to the user. Instead we can get notifications whenever the value changes, which is much more efficient that polling and performing a readCharacteristic()
periodically.
To register for notifications we actually have to do three things. First we must turn the sensor on, so that the SensorTag actually starts gathering the appropriate data (actually we need to do this if we’re using readCharacteristic() as well). Then we have to register with both the sensor and the local proxy that we require notifications. This step is something specific to the SensorTag, and other devices may not require this, or they may require an alternative approach.
To turn the sensor on we must write a value of 0x01 to the UUID_HUMIDITY_CONF
characteristic; to enable notifications we must first call setCharacteristicNotification() on the BluetoothGatt instance (which sets up the local proxy for notifications), and then write a value of {0x00,0x01} (two bytes) to the UUID_CCC descriptor. The UUID_CCC descriptor, as we mentioned earlier, is not specific to the SensorTag (as are all of the other UUID identified items), it is a standard UUID for switching notifications on and off.
Up to now, we have benefitted greatly from the asynchronous nature of the BLE APIs because we don’t ned to worry about calling them from the UI thread. Unfortunately, it does cause us a small problem here. We would expect that the following code would do what we require, but running it will not start notifications rolling in:
gatt.setCharacteristicNotification(humidityCharacteristic, true); humidityConf.setValue(ENABLE_SENSOR); gatt.writeCharacteristic(humidityConf); config.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); gatt.writeDescriptor(config);
The reason that this doesn’t work is that our writes are asynchronous, and we must wait for one write to finish before we can start the next. If we fail to wait and attempt a second write while the first is still in flight, then the second will simply vanish in to the ether. This isn’t a difficult problem to overcome, we can implement a write queue and trigger the next write when we receive a notification the the previous write has completed:
private static final byte[] ENABLE_SENSOR = {0x01}; private static final Queue<Object> sWriteQueue = new ConcurrentLinkedQueue<Object>(); private static boolean sIsWriting = false; private BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { . . . @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { Log.v(TAG, "onCharacteristicWrite: " + status); sIsWriting = false; nextWrite(); } @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { Log.v(TAG, "onDescriptorWrite: " + status); sIsWriting = false; nextWrite(); } } . . . private void subscribe(BluetoothGatt gatt) { BluetoothGattService humidityService = gatt.getService(UUID_HUMIDITY_SERVICE); if(humidityService != null) { BluetoothGattCharacteristic humidityCharacteristic = humidityService.getCharacteristic(UUID_HUMIDITY_DATA); BluetoothGattCharacteristic humidityConf = humidityService.getCharacteristic(UUID_HUMIDITY_CONF); if(humidityCharacteristic != null && humidityConf != null) { BluetoothGattDescriptor config = humidityCharacteristic.getDescriptor(UUID_CCC); if(config != null) { gatt.setCharacteristicNotification( humidityCharacteristic, true); humidityConf.setValue(ENABLE_SENSOR); write(humidityConf); config.setValue( BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); write(config); } } } } private synchronized void write(Object o) { if(sWriteQueue.isEmpty() && !sIsWriting) { doWrite(o); } else { sWriteQueue.add(o); } } private synchronized void nextWrite() { if(!sWriteQueue.isEmpty() && !sIsWriting) { doWrite(sWriteQueue.poll()); } } private synchronized void doWrite(Object o) { if(o instanceof BluetoothGattCharacteristic) { sIsWriting = true; mGatt.writeCharacteristic( (BluetoothGattCharacteristic)o); } else if(o instanceof BluetoothGattDescriptor) { sIsWriting = true; mGatt.writeDescriptor((BluetoothGattDescriptor) o); } else { nextWrite(); } }
So after adding this simple write queue, we just need to start listening for notification events by adding the appropriate callback to our BluetoothGattCallback implementation:
private BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { . . . @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { Log.v(TAG, "onCharacteristicChanged: " + characteristic.getUuid()); if(characteristic.getUuid().equals(UUID_HUMIDITY_DATA)) { int t = shortUnsignedAtOffset(characteristic, 0); int h = shortUnsignedAtOffset(characteristic, 2); t = t - (t % 4); h = h - (h % 4); float humidity = (-6f) + 125f * (h / 65535f); float temperature = -46.85f + 175.72f/65536f * (float)t; Log.d(TAG, "Value: " + humidity + ":" + temperature); Message msg = Message.obtain(null, MSG_DEVICE_DATA); msg.arg1 = (int)(temperature * 100); msg.arg2 = (int)(humidity * 100); sendMessage(msg); } } } private static Integer shortUnsignedAtOffset( BluetoothGattCharacteristic characteristic, int offset) { Integer lowerByte = characteristic.getIntValue( BluetoothGattCharacteristic.FORMAT_UINT8, offset); Integer upperByte = characteristic.getIntValue( BluetoothGattCharacteristic.FORMAT_UINT8, offset + 1); return (upperByte << 8) + lowerByte; }
The data in the characteristic contains both the temperature value in bytes zero and one and the humidity value in bytes two and three. We then strip off any unwanted status bits. The calculations are taken directly from the SensorTag documentation
In order to transfer these data to the UI, we use the arg1 and arg2 fields of the message, and we multiply by 100 before converting to the required int values. At the other end we divide by 100 to return to a float value. As before, I will not cover the UI code here, but it’s available in the sources.
So if we run this we can see that we’re now displaying the temperature in °C and the humidity as a percentage:
So we have a very functional app which does what it needs to, but this blog is titled Styling Android, so next week we’ll begin a new series where we start with this functional app, and actually make the UX a bit slicker, and make the UI look a bit nicer!
The source code for this article can be found here.
© 2014, Mark Allison. All rights reserved.
Copyright © 2014 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.
Hello
Thanks for the tutorial you have here. I have followed your tutorial from part 1 and tried write new code from your code but the app crashed everytime I try to run it.
Is there anything I can check ?
Without seeing your code or having details of the crash it’s almost impossible to suggest anything. But even with that I simply don’t have the bandwidth to provide free developer support.
Hello
I got it run now, but when the app launched, it did nothing. I tried refresh button but nothing happened
Mark
Thanks for this awesome series about BLE. Its written great and clear and the example is clear and it helped me a lot implementing my first BLE code.
Hi Mark,
Excellent article, and code. Question: I have a peripheral that needs to send a few bytes once or twice a day. On the APP side, how is it possible to make it so, that after a phone re-boot a service still runs and passes the message to the APP ?