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:
- Scan devices advertising specific services
- Connect to a device
- Discover service(s)
- Discover characteristic(s)
- 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
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
Once service discovery is complete, Core Bluetooth calls your
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
didUpdateValueForCharacteristic() method. You can read the current value of the characteristic from the
CBCharacteristic parameter’s value property as
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
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_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
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
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_FINE_LOCATION permission in the manifest.
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.
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
onCharacteristicChanged() is called by the framework, and you can then get the current value by calling
getValue() on the supplied
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
writeCharacteristic() to send the data over to the peripheral. You can get results of the write operation to
onCharacteristicWrite() method if you’re interested in them.
So, there you have it. A few relevant links for more information: