illustration
Blog Posts

iOS vs Android: Bluetooth LE Overview

EventBus makes it possible to receive results on specific threads and is also easier on the keyboard because it requires you to write less boilerplate code.

To access Bluetooth Low Energy (aka Bluetooth Smart) devices from an iOS or Android device it’s useful to know a bit of related terminology first.

GAP (Generic Access Profile) is concerned with scanning and connecting to devices. GATT (Generic Attribute Profile) comes to play when you have connected to a device, and want to access its services and characteristics.

In the GAP context, the iOS/Android device usually acts as the central, while the BLE accessory is the peripheral. Note that the roles described here are conceptual, i.e. an iOS/Android device can act as a peripheral as well (with limitations), which can be really useful when e.g. generating data for testing.

When scanning for peripherals (i.e. listening to advertisements from peripherals), there are two specialized roles: observer and broadcaster. The broadcaster sends relevant information in its advertisement package to any observer that might be listening. In other words, a broadcaster doesn’t care how many (if any) observers there are listening to it’s advertisements at any given time. The iOS/Android device normally acts as the observer, and the BLE accessory is the broadcaster.

To send custom data to any device that may be listening, you can write such information to e.g. the manufacturer data field of the advertisement package. When only sending and listening advertisement packages (potentially with context-specific data), no connection needs to be established between the devices.

Be wary that both certain fields of the advertisement package may get cached. For example, the local name field tends to get cached on Android, and can therefore not reliably be used for broadcasting information that is bound to change.

The basic procedure for setting up a BLE connection is roughly as follows:

  1. Scan devices advertising specific services
  2. Connect to a device
  3. Discover service(s)
  4. Discover characteristic(s)
  5. Register for characteristic notifications

To connect your central to a peripheral (steps 1 & 2) in order to e.g. read and write data only between the two devices, you start the connection process by scanning for peripherals that provide a specific service (or services) you require. In a viable situation where there are multiple peripherals advertising themselves in the vicinity of a central device, you may want to send customized identifier in the advertisement package (therefore fulfilling the broadcaster role, see above) so that the central can observe the sent identifiers and distinguish a particular peripheral from others.

Steps 3 to 5 are covered by GATT, which comes into play when you have actually established a connection with a BLE peripheral. Once connected, you can scan the peripheral’s services and their related characteristics.

A BLE device profile consists of one or more services, each of which can have one or more characteristics. A common case is that there’s one service, which has one characteristic for reading, and one for writing. Services are basically a logical collection of read/write characteristics. Each service and characteristic is identified by a 16/128-bit unique identifier. The 16-bit identifiers are defined by Bluetooth SIG to ensure/encourage interoperability between BLE devices as needed, and 128-bit indentifiers are available for building customized services/characteristics. Each characteristic can also have descriptors that can describe the characteristic’s value, set minimum/maximum limits, or whatever you may need. Dealing with descriptors is normally not necessary, except in one particular case on Android that will be covered later.

You can read a characteristic’s current value, or register to get notified when the value changes. Notifications allow you to get updates whenever they happen, instead of polling the current value repeatedly. When writing to a characteristic, it’s possible to get confirmation of a successful write operation, assuming the characteristic has been configured to support this on the peripheral.

iOS Implementation Details

Communication with BLE devices on iOS is handled using the Core Bluetooth framework. You start the connection process by initializing an instance of CBCentralManager, supplying a CBCentralManagerDelegateas argument to the initializer. When initialization is complete, Core Bluetooth calls your delegate’s centralManagerDidUpdateState() method, where you can take further action as needed.

In case everything is OK, CBCentralManager’s state property will be PoweredOn. If, however, the user has Bluetooth turned off (state PoweredOff), they will be presented at this point with a dialog requesting to turn it on.

After initializing the CBCentralManager instance, you can start to scan for devices by first setting up a CBCentralManager delegate, and then starting the scan by calling CBCentralManager‘s scanForPeripheralsWithServices() method, where you pass an array of CBUUID objects representing BLE services the scanned peripherals must provide. Often there is just one service, but more complex devices may have more.

Once a matching peripheral is found, Core Bluetooth calls the didDiscoverPeripheral() delegate method. (Note that the actual function name is much more convoluted, mostly due to legacy reasons, but this and other delegates are commonly known by these shortened forms.) Here you can read the discovered device’s advertising data. In case your required data is already contained in the advertisement packet, you can extract it here and continue to listen to further advertisements. Advertisement data can also contain information you need to figure out if you want to connect to the discovered device. Note that you need to store the CBPeripheral object locally, otherwise it gets deinitialized.

To initiate connection, call the connectPeripheral method of your CBCentralManager. When connected, Core Bluetooth calls your central manager delegate’s didConnectPeripheral() method, where you can set up a CBPeripheralManager delegate and start discovering services on the peripheral by calling discoverServices() on the CBPeripheral object.

Once service discovery is complete, Core Bluetooth calls your CBPeripheralDelegate‘s didDiscoverServices() method. Once you have discovered the service you’re interested in, you’ll want to discover its characteristics by calling discoverCharacteristics() on the CBPeripheral object. Once complete, you’ll get a call to the didDiscoverCharacteristicsForService() method. Here you can e.g. register to be notified on changes to a readable characteristic’s value, or store a writable characteristic for further use.

To listen to notifications when a readable characteristic’s value changes, call setNotifyValue() on the CBPeripheral object, with the characteristic as argument. Note that for this to work the characteristic must be configured to support notifications on the peripheral. When a characteristic’s value is subsequently updated, Core Bluetooth will call your CBPeripheralDelegate‘s didUpdateValueForCharacteristic() method. You can read the current value of the characteristic from the CBCharacteristic parameter’s value property as NSData.

Writing data to a characteristic is done by calling the writeValue() method on the CBPeripheral object. You can choose to either get a write result (in case the peripheral supports it), or ignore it. In the former case, your CBPeripheralManager‘s didWriteValueForCharacteristic() gets called with the result of the write operation.

Android Implementation Details

Bluetooth LE on Android is a bit of a wild west, especially when dealing with proprietary Bluetooth stack implementations, but here’s the general idea on how to get started using the public API. First you need to request both BLUETOOTH and BLUETOOTH_ADMIN permissions in your app’s manifest. High-level Bluetooth operations are done through the BluetoothAdapter instance, which is common to all apps on the system. You can get the instance through BluetoothManager‘s getAdapter() method.

Scanning peripherals requires that you have implemented the callback interface for getting scan results. In case you need to support API levels 18 to 20, call BluetoothAdapter‘s startLeScan() and supply an instance of BluetoothAdapter.LeScanCallback implementation. For API levels 21 onward, first call BluetoothAdapter'sgetBluetoothLeScanner() to get an instance of BluetoothLeScanner, and then call startScan() on the instance, supplying a ScanCallback where you can handle scan results. Also note that to get scan results on Lollipop (5.0) and newer you will need to declare ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION permission in the manifest.

In your ScanCallback‘s onScanResult() you can get the peripheral from the supplied ScanResult by calling getDevice(), which returns a BluetoothDevice object. To initiate connection, call connectGatt() on the instance, giving a BluetoothGattCallback instance as argument.

Your BluetoothGattCallback‘s onConnectionStateChanged() is called, as the name implies, when the connected to or disconnected from the peripheral. Once you’ve established connection, call discoverServices() on the supplied BluetoothGatt instance. This will result in a call to onServicesDiscovered(), where you can set up read and write characteristics as needed.

You can get a BluetoothGattService instance by calling getService() on the supplied BluetoothGattobject. To register to notifications when a characteristic’s value changes on the peripheral, call setCharacteristicNotification() with true as argument on the BluetoothGattCharacteristic object you get by calling getCharacteristic() on the service instance. An important thing is to also remember to enable notifications on the Client Characteristic Configuration descriptor by calling setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) on the descriptor instance. You can get the descriptor by calling getDescriptor() on the BluetoothGattCharacteristic object, supplying the UUID of the descriptor (whose Bluetooth SIG assigned number is 0x2902. Incidentally, on iOS this is done automatically for you.)

When a characteristic’s value is changed on the peripheral and you’ve registered to get notifications for the characteristic, your BluetoothGattCallback‘s onCharacteristicChanged() is called by the framework, and you can then get the current value by calling getValue() on the supplied BluetoothGattCharacteristicobject.

Writing to a writable characteristic is done by first setting the required data to the BluetoothGattCharacteristic object using one of the setValue() overloads, and then calling BluetoothLeGatt‘s writeCharacteristic() to send the data over to the peripheral. You can get results of the write operation to BluetoothGattCallback‘s onCharacteristicWrite() method if you’re interested in them.

So, there you have it. A few relevant links for more information:

Do you want to hear more about this subject? Leave your contact details and we will be in touch.

By submitting the form, I consent to the storage and processing of my data in accordance with the privacy statement.