Exploring iOS Core Bluetooth: Read Requests

Apple’s Core Bluetooth Progamming Guide does a good job of explaining how to use Core Bluetooth in an iOS app. And there are countless blog post tutorials that walk through scanning for peripherals, connecting, discovering services and characteristics, and reading/writing values. These resources were great for getting started, but eventually I ran into some questions that I couldn’t answer. Things like:

  • What happens when consecutive requests are made for multiple characteristics without waiting for a response each time?
  • Can read requests be made from multiple threads concurrently?

In this post I’ll discuss some experiments I ran to explore the -readValueForCharacteristic: method of the CBPeripheral class to try to get a better understanding of how it works.

Tools

The Bluetooth Low Energy peripheral I used was a TI SensorTag. I used a custom app (running in iOS 7 on a 5th gen iPod Touch) that I wrote to connect to the SensorTag and read characteristic values. And finally I captured the BLE packets using the TI SmartRF Protocol Packet Sniffer software along with a TI CC2540 USB Dongle.

Back-to-Back Reads

What happens if you call -readValueForCharacteristic: for a handful of characteristics back-to-back, without waiting for a response from one request before making the next? The answer appears to be that Core Bluetooth queues up the requests and makes them one at a time, calling didUpdateValueForCharacteristic when the response packets arrive back from the peripheral.

After connecting to the SensorTag, I made five straight read requests, each for a different characteristic:

  CBService *cbService = peripheral.services[0];
  [peripheral readValueForCharacteristic:cbService.characteristics[0]];
  [peripheral readValueForCharacteristic:cbService.characteristics[1]];
  [peripheral readValueForCharacteristic:cbService.characteristics[2]];
  [peripheral readValueForCharacteristic:cbService.characteristics[3]];
  [peripheral readValueForCharacteristic:cbService.characteristics[4]];

And then in the didUpdateValueForCharacteristic callback I log the responses with:

  
NSLog(@"Response: Characteristic %@ (val: %@)", 
          cbCharacteristic.UUID.UUIDString, 
          cbCharacteristic.value);

Here’s what was logged in the Xcode console:

  2014-10-18 09:40:37.301 Characteristic 2A23 (val: <84af6400 004c99b4>)
  2014-10-18 09:40:37.361 Characteristic 2A24 (val: <4e2e412e 00>)
  2014-10-18 09:40:37.420 Characteristic 2A25 (val: <4e2e412e 00>)
  2014-10-18 09:40:37.481 Characteristic 2A26 (val: <312e3520 284f6374 20323320 32303133 2900>)
  2014-10-18 09:40:37.541 Characteristic 2A27 (val: <4e2e412e 00>)

As you can see, the callbacks appear to have been called in the same order that the reads were called. You can also see that each response comes in just about 60ms apart. iOS devices use a 30ms connection interval by default (mentioned here and confirmed through packet sniffing). A 60ms gap between read responses makes sense given a 30ms connection interval (the next read request after a response comes in would be on the next connection event 30ms later, and then another 30ms later the response would come back on a connection event).

This means that no matter how fast the read requests are made to Core Bluetooth, the responses appear to come back single-file, on every other connection event.

Tracing Packets

Core Bluetooth abstracts away a lot of the details of working with Bluetooth LE. But when something isn’t working the way you expect it, or you just want to better understand what is going on, then using a packet sniffer is a great option. Using the TI CC2540 USB Dongle I mentioned previously, I was able to capture the read/response packets from my test.

Here is part of the sequence showing a read request, the response, another read request, and the second response:

Bluetooth Sniffer Packet Trace

This trace confirms that a read request (ATT_Read_Req) goes out with one connection event, and the response (ATT_Read_Rsp) comes back on the next, 30ms later. And then another 30ms after that the next request goes out (note that in the trace the times are in microseconds, not milliseconds).

Read Requests from Multiple Threads

What happens if you call -readValueForCharacteristic: for a handful of characteristics at the same time, from different threads? The answer appears to be that Core Bluetooth safely handles this, queueing up the requests in the order in which they were received. To determine this I changed my read requests from above to the following:

  dispatch_queue_t queue = 
      dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
  CBService *cbService = self.peripheral.peripheral.services[0];
  for (int i = 0; i < 100; i++) {
      dispatch_async(queue, ^{
          [peripheral readValueForCharacteristic:cbService.characteristics[0]];
      });
      dispatch_async(queue, ^{
          [peripheral readValueForCharacteristic:cbService.characteristics[1]];
      });
      dispatch_async(queue, ^{
          [peripheral readValueForCharacteristic:cbService.characteristics[2]];
      });
      dispatch_async(queue, ^{
          [peripheral readValueForCharacteristic:cbService.characteristics[3]];
      });
      dispatch_async(queue, ^{
          [peripheral readValueForCharacteristic:cbService.characteristics[4]];
      });
  }

This code makes 100 read requests for each characteristic, and it makes each request from a concurrent background queue, meaning that some of the requests will be coming from different threads. I tallied up the responses and did in fact receive 100 responses for each characteristic. While this doesn’t prove definitively that read requests are thread-safe, it’s a pretty good indicator.

Resources

If you decide to try out the TI CC2541 mini development kit, this User’s Guide is the best of the various documentation I came across.

Also, while some of the documentation for the USB dongle indicates that it comes with the sniffer firmware pre-installed, I did not find that to be the case. I needed to flash it with the sniffer firmware before I could use it to capture packets. Additionally, only the older SmartRF Flash Programmer software would work (the first of the download options on this page) to load the sniffer firmware.