Overview
iOS provides a library called CoreBluetooth which allows to communicate with BLE devices. It hides many low level operations, therefore we don’t have to worry about protocol specification. However, we need to get familiar with some new terms related with BLE, so let’s start with basic definitions to understand how the communication works.
Bluetooth Low Energy (BLE, Bluetooth Smart)
Bluetooth Smart is a wireless technology designed for integration with devices related to healthcare, home automation, beacons, etc. Energy consumption has been reduced significantly, which is perfect for small bands and detectors. Some devices can function for 1 – 2 years using only a single small battery.
Central
Central is a device, which is able to detect and communicate with BLE devices called peripherals. It could be for example an iPhone.
Peripheral
Peripheral is a Bluetooth low energy device like activity tracker or blood pressure monitor.
Service
Service contains a set of similar information and functionalities. Peripheral may expose multiple services like battery service, signal service, steps counter service etc.
Characteristic
Each service contains characteristics with a data value. For steps counter service it could be a number of steps and a bit flag to reset its value.
Broadcasting
Peripheral transmits an advertisement packet at regular intervals, this process is called broadcasting. Central device can detect peripherals by intercepting those packets. It’s similar to detecting Wi-Fi networks. Some peripherals stop broadcasting once they are connected.
Discovering
Discovery is a process in which central device intercepts packets and detects peripherals, services and characteristics.
Pairing
Pairing is a process in which the peripheral becomes bounded with the central. For example, an iPhone will connect to paired peripheral each time it’s in range.
Notifications
It’s possible to subscribe for changes in chosen characteristics. Peripheral will send us a notification once a value is updated.
Identifier (UUID)
Each device, service, and characteristic has a unique identifier, which can be used to find specific information. There are also some well-known identifiers for services like battery service (UUID: 0x180F) or device information service (UUID: 0x180A). You can find more identifiers here.
For some reason, property CBPeripheral.UUID throws an exception, use CBPeripheral.Identifier instead to get UUID of device.
Swift Implementation
You can find Swift implementation here:
Swift – Bluetooth Low Energy communication using Flow Controllers.
Implementation
CoreBluetooth framework provides several classes, which represent things defined above like CBCentralManager, CBPeripheral, CBService and CBCharacteristic. CBCentralManager is the main class to communicate with BLE devices. Default CoreBluetooth interface is based on events, I will transform it into more user-friendly asynchronous methods, which return Task.
Below I will provide code samples for each step of communication. Presented code is written in Xamarin.iOS technology.
An iOS app linked on or after iOS 10.0 must include in its Info.plist file the usage description keys for the types of data it needs to access or it will crash. To access Bluetooth peripheral data specifically, it must include NSBluetoothPeripheralUsageDescription.
1. Discovering devices
Scanning for devices uses a lot of energy, therefore it should be running only when necessary and for a short time period. CBCentralManager provides a method, which takes a service UUID in order to find desired device. It’s also possible to provide an empty array to look for all available BLE devices.
For some reason, discovering devices with specific service doesn’t work sometimes. Probably it depends on device specification.
1 2 3 |
this.manager.ScanForPeripherals(CBUUID.FromString(serviceUuid)); or this.manager.ScanForPeripherals(new CBUUID[0]); |
Before invoking any operation we need to make sure that CBCentralManager is in a proper state:
CBCentralManagerState.PoweredOn.
Below there is a full code sample with implemented scanning. In further paragraphs, I will implement the next methods, which should be appended to this class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
using System; using CoreBluetooth; using System.Diagnostics; using System.Threading.Tasks; using System.Linq; using Foundation; namespace Bluetooth { public class BluetoothService : IDisposable { private readonly CBCentralManager manager = new CBCentralManager(); public EventHandler DiscoveredDevice; public EventHandler StateChanged; public BluetoothService() { this.manager.DiscoveredPeripheral += this.DiscoveredPeripheral; this.manager.UpdatedState += this.UpdatedState; } public void Dispose() { this.manager.DiscoveredPeripheral -= this.DiscoveredPeripheral; this.manager.UpdatedState -= this.UpdatedState; } public async Task Scan(int scanDuration, string serviceUuid = "") { Debug.WriteLine("Scanning started"); var uuids = string.IsNullOrEmpty(serviceUuid) ? new CBUUID[0] : new[] { CBUUID.FromString(serviceUuid) }; this.manager.ScanForPeripherals(uuids); await Task.Delay(scanDuration); this.StopScan(); } public void StopScan() { this.manager.StopScan(); Debug.WriteLine("Scanning stopped"); } private void DiscoveredPeripheral(object sender, CBDiscoveredPeripheralEventArgs args) { var device = $"{args.Peripheral.Name} - {args.Peripheral.Identifier?.Description}"; Debug.WriteLine($"Discovered {device}"); this.DiscoveredDevice?.Invoke(sender, args.Peripheral); } private void UpdatedState(object sender, EventArgs args) { Debug.WriteLine($"State = {this.manager.State}"); this.StateChanged?.Invoke(sender, this.manager.State); } } } |
2. Connecting to peripheral
To connect to discovered peripheral we can just call:
1 |
this.manager.ConnectPeripheral(peripheral); |
However, in real life it’s useful to have an asynchronous version with connection timeout:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
private const int ConnectionTimeout = 10000; public async Task ConnectTo(CBPeripheral peripheral) { var taskCompletion = new TaskCompletionSource(); var task = taskCompletion.Task; EventHandler connectedHandler = (s, e) => { if (e.Peripheral.Identifier?.ToString() == peripheral.Identifier?.ToString()) { taskCompletion.SetResult(true); } }; try { this.manager.ConnectedPeripheral += connectedHandler; this.manager.ConnectPeripheral(peripheral); await this.WaitForTaskWithTimeout(task, ConnectionTimeout); Debug.WriteLine($"Bluetooth device connected = {peripheral.Name}"); } finally { this.manager.ConnectedPeripheral -= connectedHandler; } } public void Disconnect(CBPeripheral peripheral) { this.manager.CancelPeripheralConnection(peripheral); Debug.WriteLine($"Device {peripheral.Name} disconnected"); } private async Task WaitForTaskWithTimeout(Task task, int timeout) { await Task.WhenAny(task, Task.Delay(ConnectionTimeout)); if (!task.IsCompleted) { throw new TimeoutException(); } } |
3. Discovering services and characteristics
To discover a service we need to call DiscoverServices method:
1 2 |
peripheral.DiscoveredService += this.ServiceDiscovered; peripheral.DiscoverServices(new[] { CBUUID.FromString(serviceUuid) }); |
We can do better and implement another asynchronous method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
public async Task GetService(CBPeripheral peripheral, string serviceUuid) { var service = this.GetServiceIfDiscovered(peripheral, serviceUuid); if (service != null) { return service; } var taskCompletion = new TaskCompletionSource(); var task = taskCompletion.Task; EventHandler handler = (s, e) => { if (this.GetServiceIfDiscovered(peripheral, serviceUuid) != null) { taskCompletion.SetResult(true); } }; try { peripheral.DiscoveredService += handler; peripheral.DiscoverServices(new[] { CBUUID.FromString(serviceUuid) }); await this.WaitForTaskWithTimeout(task, ConnectionTimeout); return this.GetServiceIfDiscovered(peripheral, serviceUuid); } finally { peripheral.DiscoveredService -= handler; } } public CBService GetServiceIfDiscovered(CBPeripheral peripheral, string serviceUuid) { serviceUuid = serviceUuid.ToLowerInvariant(); return peripheral.Services ?.FirstOrDefault(x => x.UUID?.Uuid?.ToLowerInvariant() == serviceUuid); } public async Task<CBCharacteristic[]> GetCharacteristics(CBPeripheral peripheral, CBService service, int scanTime) { peripheral.DiscoverCharacteristics(service); await Task.Delay(scanTime); return service.Characteristics; } |
4. Reading and writing characteristic value
CBCharacteristic has property Value which returns NSData with raw bytes. However, before accessing property value we need to call peripheral.ReadValue(characteristic) and wait for data in event handler.
Similary we can write value by calling:
peripheral.WriteValue(value, characteristic, CBCharacteristicWriteType.WithoutResponse).
Asynchronous versions of those methods could look like that:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
public async Task ReadValue(CBPeripheral peripheral, CBCharacteristic characteristic) { var taskCompletion = new TaskCompletionSource(); var task = taskCompletion.Task; EventHandler handler = (s, e) => { if (e.Characteristic.UUID?.Uuid == characteristic.UUID?.Uuid) { taskCompletion.SetResult(true); } }; try { peripheral.UpdatedCharacterteristicValue += handler; peripheral.ReadValue(characteristic); await this.WaitForTaskWithTimeout(task, ConnectionTimeout); return characteristic.Value; } finally { peripheral.UpdatedCharacterteristicValue -= handler; } } public async Task WriteValue(CBPeripheral peripheral, CBCharacteristic characteristic, NSData value) { var taskCompletion = new TaskCompletionSource(); var task = taskCompletion.Task; EventHandler handler = (s, e) => { if (e.Characteristic.UUID?.Uuid == characteristic.UUID?.Uuid) { taskCompletion.SetResult(e.Error); } }; try { peripheral.WroteCharacteristicValue += handler; peripheral.WriteValue(value, characteristic, CBCharacteristicWriteType.WithResponse); await this.WaitForTaskWithTimeout(task, ConnectionTimeout); return task.Result; } finally { peripheral.WroteCharacteristicValue -= handler; } } |
5. Retrieving connected/paired devices
1 2 3 4 |
public CBPeripheral[] GetConnectedDevices(string serviceUuid) { return this.manager.RetrieveConnectedPeripherals(new[] { CBUUID.FromString(serviceUuid) }); } |
This method returns connected BLE devices with iPhone. It’s useful because some peripherals stop broadcasting once they are connected, so it wouldn’t be possible to discover them using the scan method. You can read more in Swift – Bluetooth Low Energy – how to get paired devices?
6. Sample usage
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
using System; using System.Diagnostics; using System.Linq; using CoreBluetooth; using UIKit; namespace Bluetooth { public partial class ViewController : UIViewController { private const int ScanTime = 5000; private const string DeviceName = "GoPro"; private BluetoothService bluetoothService; private bool alreadyScanned; private bool alreadyDiscovered; protected ViewController(IntPtr handle) : base(handle) { } public override void ViewWillAppear(bool animated) { base.ViewWillAppear(animated); this.alreadyScanned = false; this.alreadyDiscovered = false; this.bluetoothService = new BluetoothService(); this.bluetoothService.DiscoveredDevice += DiscoveredDevice; this.bluetoothService.StateChanged += StateChanged; } public override void ViewWillDisappear(bool animated) { base.ViewWillDisappear(animated); this.bluetoothService.DiscoveredDevice -= DiscoveredDevice; this.bluetoothService.StateChanged -= StateChanged; this.bluetoothService.Dispose(); } private async void StateChanged(object sender, CBCentralManagerState state) { if (!this.alreadyScanned && state == CBCentralManagerState.PoweredOn) { try { this.alreadyScanned = true; var connectedDevice = this.bluetoothService.GetConnectedDevices("180A") ?.FirstOrDefault(x => x.Name.StartsWith(DeviceName, StringComparison.InvariantCulture)); if (connectedDevice != null) { this.DiscoveredDevice(this, connectedDevice); } else { await this.bluetoothService.Scan(ScanTime); } } catch (Exception ex) { Debug.WriteLine(ex); } } } private async void DiscoveredDevice(object sender, CBPeripheral peripheral) { if (!this.alreadyDiscovered && peripheral.Name.StartsWith(DeviceName, StringComparison.InvariantCulture)) { try { this.alreadyDiscovered = true; await this.bluetoothService.ConnectTo(peripheral); var service = await this.bluetoothService.GetService(peripheral, "180A"); if (service != null) { var characteristics = await this.bluetoothService.GetCharacteristics(peripheral, service, ScanTime); foreach (var characteristic in characteristics) { var value = await this.bluetoothService.ReadValue(peripheral, characteristic); Debug.WriteLine($"{characteristic.UUID.Description} = {value}"); } } } catch (Exception ex) { Debug.WriteLine(ex); } finally { this.bluetoothService.Disconnect(peripheral); } } } } } |
Complete BluetoothService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
using System; using CoreBluetooth; using System.Diagnostics; using System.Threading.Tasks; using System.Linq; using Foundation; namespace Bluetooth { public class BluetoothService : IDisposable { private const int ConnectionTimeout = 10000; private readonly CBCentralManager manager = new CBCentralManager(); public EventHandler DiscoveredDevice; public EventHandler StateChanged; public BluetoothService() { this.manager.DiscoveredPeripheral += this.DiscoveredPeripheral; this.manager.UpdatedState += this.UpdatedState; } public void Dispose() { this.manager.DiscoveredPeripheral -= this.DiscoveredPeripheral; this.manager.UpdatedState -= this.UpdatedState; this.StopScan(); } public async Task Scan(int scanDuration, string serviceUuid = "") { Debug.WriteLine("Scanning started"); var uuids = string.IsNullOrEmpty(serviceUuid) ? new CBUUID[0] : new[] { CBUUID.FromString(serviceUuid) }; this.manager.ScanForPeripherals(uuids); await Task.Delay(scanDuration); this.StopScan(); } public void StopScan() { this.manager.StopScan(); Debug.WriteLine("Scanning stopped"); } public async Task ConnectTo(CBPeripheral peripheral) { var taskCompletion = new TaskCompletionSource(); var task = taskCompletion.Task; EventHandler connectedHandler = (s, e) => { if (e.Peripheral.Identifier?.ToString() == peripheral.Identifier?.ToString()) { taskCompletion.SetResult(true); } }; try { this.manager.ConnectedPeripheral += connectedHandler; this.manager.ConnectPeripheral(peripheral); await this.WaitForTaskWithTimeout(task, ConnectionTimeout); Debug.WriteLine($"Bluetooth device connected = {peripheral.Name}"); } finally { this.manager.ConnectedPeripheral -= connectedHandler; } } public void Disconnect(CBPeripheral peripheral) { this.manager.CancelPeripheralConnection(peripheral); Debug.WriteLine($"Device {peripheral.Name} disconnected"); } public CBPeripheral[] GetConnectedDevices(string serviceUuid) { return this.manager.RetrieveConnectedPeripherals(new[] { CBUUID.FromString(serviceUuid) }); } public async Task GetService(CBPeripheral peripheral, string serviceUuid) { var service = this.GetServiceIfDiscovered(peripheral, serviceUuid); if (service != null) { return service; } var taskCompletion = new TaskCompletionSource(); var task = taskCompletion.Task; EventHandler handler = (s, e) => { if (this.GetServiceIfDiscovered(peripheral, serviceUuid) != null) { taskCompletion.SetResult(true); } }; try { peripheral.DiscoveredService += handler; peripheral.DiscoverServices(new[] { CBUUID.FromString(serviceUuid) }); await this.WaitForTaskWithTimeout(task, ConnectionTimeout); return this.GetServiceIfDiscovered(peripheral, serviceUuid); } finally { peripheral.DiscoveredService -= handler; } } public CBService GetServiceIfDiscovered(CBPeripheral peripheral, string serviceUuid) { serviceUuid = serviceUuid.ToLowerInvariant(); return peripheral.Services ?.FirstOrDefault(x => x.UUID?.Uuid?.ToLowerInvariant() == serviceUuid); } public async Task<CBCharacteristic[]> GetCharacteristics(CBPeripheral peripheral, CBService service, int scanTime) { peripheral.DiscoverCharacteristics(service); await Task.Delay(scanTime); return service.Characteristics; } public async Task ReadValue(CBPeripheral peripheral, CBCharacteristic characteristic) { var taskCompletion = new TaskCompletionSource(); var task = taskCompletion.Task; EventHandler handler = (s, e) => { if (e.Characteristic.UUID?.Uuid == characteristic.UUID?.Uuid) { taskCompletion.SetResult(true); } }; try { peripheral.UpdatedCharacterteristicValue += handler; peripheral.ReadValue(characteristic); await this.WaitForTaskWithTimeout(task, ConnectionTimeout); return characteristic.Value; } finally { peripheral.UpdatedCharacterteristicValue -= handler; } } public async Task WriteValue(CBPeripheral peripheral, CBCharacteristic characteristic, NSData value) { var taskCompletion = new TaskCompletionSource(); var task = taskCompletion.Task; EventHandler handler = (s, e) => { if (e.Characteristic.UUID?.Uuid == characteristic.UUID?.Uuid) { taskCompletion.SetResult(e.Error); } }; try { peripheral.WroteCharacteristicValue += handler; peripheral.WriteValue(value, characteristic, CBCharacteristicWriteType.WithResponse); await this.WaitForTaskWithTimeout(task, ConnectionTimeout); return task.Result; } finally { peripheral.WroteCharacteristicValue -= handler; } } private void DiscoveredPeripheral(object sender, CBDiscoveredPeripheralEventArgs args) { var device = $"{args.Peripheral.Name} - {args.Peripheral.Identifier?.Description}"; Debug.WriteLine($"Discovered {device}"); this.DiscoveredDevice?.Invoke(sender, args.Peripheral); } private void UpdatedState(object sender, EventArgs args) { Debug.WriteLine($"State = {this.manager.State}"); this.StateChanged?.Invoke(sender, this.manager.State); } private async Task WaitForTaskWithTimeout(Task task, int timeout) { await Task.WhenAny(task, Task.Delay(ConnectionTimeout)); if (!task.IsCompleted) { throw new TimeoutException(); } } } } |
Background mode
Read my new post to find out how to handle background mode:
Bluetooth Low Energy – background mode on iOS
References
You can find more information here: