Performing Common Central Role Tasks

Devices that implement the central role in Bluetooth low energy communication perform a number of common tasks—for example, discovering and connecting to available peripherals, and exploring and interacting with the data that peripherals have to offer. In contrast, devices that implement the peripheral role also perform a number of common, but different, tasks—for example, publishing and advertising services, and responding to read, write, and subscription requests from connected centrals.

In this chapter, you will learn how to use the Core Bluetooth framework to perform the most common types of Bluetooth low energy tasks from the central side. The code-based examples that follow will assist you in developing your app to implement the central role on your local device. Specifically, you will learn how to:

In the next chapter, you will learn how to develop your app to implement the peripheral role on your local device.

The code examples that you find in this chapter are simple and abstract; you may need to make appropriate changes to incorporate them into your real world app. More advanced topics related to implementing the central role—including tips, tricks, and best practices—are covered in the later chapters, Core Bluetooth Background Processing for iOS Apps and Best Practices for Interacting with a Remote Peripheral Device.

Starting Up a Central Manager

Since a CBCentralManager object is the Core Bluetooth object-oriented representation of a local central device, you must allocate and initialize a central manager instance before you can perform any Bluetooth low energy transactions. You can start up your central manager by calling the initWithDelegate:queue:options: method of the CBCentralManager class, like this:

    myCentralManager =
        [[CBCentralManager alloc] initWithDelegate:self queue:nil options:nil];

In this example, self is set as the delegate to receive any central role events. By specifying the dispatch queue as nil, the central manager dispatches central role events using the main queue.

When you create a central manager, the central manager calls the centralManagerDidUpdateState: method of its delegate object. You must implement this delegate method to ensure that Bluetooth low energy is supported and available to use on the central device. For more information about how to implement this delegate method, see CBCentralManagerDelegate Protocol Reference.

Discovering Peripheral Devices That Are Advertising

One of the first central-side tasks that you are likely to perform is to discover what peripheral devices are available for your app to connect to. As mentioned earlier in Centrals Discover and Connect to Peripherals That Are Advertising, advertising is the primary way that peripherals make their presence known. You can discover any peripheral devices that are advertising by calling the scanForPeripheralsWithServices:options: method of the CBCentralManager class, like this:

    [myCentralManager scanForPeripheralsWithServices:nil options:nil];

After you call the scanForPeripheralsWithServices:options: method to discover what peripherals are available, the central manager calls the centralManager:didDiscoverPeripheral:advertisementData:RSSI: method of its delegate object each time a peripheral is discovered. Any peripheral that is discovered is returned as a CBPeripheral object. As the following shows, you can implement this delegate method to list any peripheral that is discovered:

- (void)centralManager:(CBCentralManager *)central
 didDiscoverPeripheral:(CBPeripheral *)peripheral
     advertisementData:(NSDictionary *)advertisementData
                  RSSI:(NSNumber *)RSSI {
 
    NSLog(@"Discovered %@", peripheral.name);
    ...

When you have found a peripheral device that you’re interested in connecting to, stop scanning for other devices in order to save power.

    [myCentralManager stopScan];
    NSLog(@"Scanning stopped");

Connecting to a Peripheral Device After You’ve Discovered It

After you have discovered a peripheral device that is advertising services you are interested in, you can request a connection to the peripheral by calling the connectPeripheral:options: method of the CBCentralManager class. Simply call this method and specify the discovered peripheral that you want to connect to, like this:

    [myCentralManager connectPeripheral:peripheral options:nil];

Assuming that the connection request is successful, the central manager calls the centralManager:didConnectPeripheral: method of its delegate object, which you can implement to log that the connection is established, as the following shows:

- (void)centralManager:(CBCentralManager *)central
  didConnectPeripheral:(CBPeripheral *)peripheral {
 
    NSLog(@"Peripheral connected");
    ...

Before you begin interacting with the peripheral, you should set the peripheral’s delegate to ensure that it receives the appropriate callbacks, like this:

    peripheral.delegate = self;

Discovering the Services of a Peripheral That You’re Connected To

After you have established a connection to a peripheral, you can begin to explore its data. The first step in exploring what a peripheral has to offer is discovering its available services. Because there are size restrictions on the amount of data a peripheral can advertise, you may discover that a peripheral has more services than what it advertises (in its advertising packets). You can discover all of the services that a peripheral offers by calling the discoverServices: method of the CBPeripheral class, like this:

    [peripheral discoverServices:nil];

When the specified services are discovered, the peripheral (the CBPeripheral object you’re connected to) calls the peripheral:didDiscoverServices: method of its delegate object. Core Bluetooth creates an array of CBService objects—one for each service that is discovered on the peripheral. As the following shows, you can implement this delegate method to access the array of discovered services:

- (void)peripheral:(CBPeripheral *)peripheral
didDiscoverServices:(NSError *)error {
 
    for (CBService *service in peripheral.services) {
        NSLog(@"Discovered service %@", service);
        ...
    }
    ...

Discovering the Characteristics of a Service

Assuming that you have found a service that you are interested in, the next step in exploring what a peripheral has to offer is discovering all of the service’s characteristics. Discovering all of the characteristics of a service is as simple as calling the discoverCharacteristics:forService: method of the CBPeripheral class, specifying the appropriate service, like this:

    NSLog(@"Discovering characteristics for service %@", interestingService);
    [peripheral discoverCharacteristics:nil forService:interestingService];

The peripheral calls the peripheral:didDiscoverCharacteristicsForService:error: method of its delegate object when the characteristics of the specified service are discovered. Core Bluetooth creates an array of CBCharacteristic objects—one for each characteristic that is discovered. The following example shows how you can implement this delegate method to simply log every characteristic that is discovered:

- (void)peripheral:(CBPeripheral *)peripheral
didDiscoverCharacteristicsForService:(CBService *)service
             error:(NSError *)error {
 
    for (CBCharacteristic *characteristic in service.characteristics) {
        NSLog(@"Discovered characteristic %@", characteristic);
        ...
    }
    ...

Retrieving the Value of a Characteristic

A characteristic contains a single value that represents more information about a peripheral’s service. For example, a temperature measurement characteristic of a health thermometer service may have a value that indicates a temperature in Celsius. You can retrieve the value of a characteristic by reading it directly or by subscribing to it.

Reading the Value of a Characteristic

After you have found a characteristic of a service that you are interested in, you can read the characteristic’s value by calling the readValueForCharacteristic: method of the CBPeripheral class, specifying the appropriate characteristic, like this:

    NSLog(@"Reading value for characteristic %@", interestingCharacteristic);
    [peripheral readValueForCharacteristic:interestingCharacteristic];

When you attempt to read the value of a characteristic, the peripheral calls the peripheral:didUpdateValueForCharacteristic:error: method of its delegate object to retrieve the value. If the value is successfully retrieved, you can access it through the characteristic’s value property, like this:

- (void)peripheral:(CBPeripheral *)peripheral
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
             error:(NSError *)error {
 
    NSData *data = characteristic.value;
    // parse the data as needed
    ...

Subscribing to a Characteristic’s Value

Though reading the value of a characteristic using the readValueForCharacteristic: method can be effective for some use cases, it is not the most efficient way to retrieve a value that changes. For most characteristic values that change—for instance, your heart rate at any given time—you should retrieve them by subscribing to them. When you subscribe to a characteristic’s value, you receive a notification from the peripheral when the value changes.

You can subscribe to the value of a characteristic that you are interested in by calling the setNotifyValue:forCharacteristic: method of the CBPeripheral class, specifying the first parameter as YES, like this:

    [peripheral setNotifyValue:YES forCharacteristic:interestingCharacteristic];

When you attempt to subscribe (or unsubscribe) to a characteristic’s value, the peripheral calls the peripheral:didUpdateNotificationStateForCharacteristic:error: method of its delegate object. If the subscription request fails for any reason, you can implement this delegate method to access the cause of the error, as the following example shows:

- (void)peripheral:(CBPeripheral *)peripheral
didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic
             error:(NSError *)error {
 
    if (error) {
        NSLog(@"Error changing notification state: %@",
           [error localizedDescription]);
    }
    ...

After you have successfully subscribed to a characteristic’s value, the peripheral device notifies your app when the value has changed. Each time the value changes, the peripheral calls the peripheral:didUpdateValueForCharacteristic:error: method of its delegate object. To retrieve the updated value, you can implement this method in the same way as described above in Reading the Value of a Characteristic.

Writing the Value of a Characteristic

For some use cases, it makes sense to write the value of a characteristic. For example, if your app interacts with a Bluetooth low energy digital thermostat, you may want to provide the thermostat with a value at which to set the room’s temperature. If a characteristic’s value is writeable, you can write its value with some data (an instance of NSData) by calling the writeValue:forCharacteristic:type: method of the CBPeripheral class, like this:

    NSLog(@"Writing value for characteristic %@", interestingCharacteristic);
    [peripheral writeValue:dataToWrite forCharacteristic:interestingCharacteristic
        type:CBCharacteristicWriteWithResponse];

When you attempt to write the value of a characteristic, you specify what type of write you want to perform. In the example above, the write type is specified as CBCharacteristicWriteWithResponse, which indicates that the peripheral lets your app know whether the write is successful. For more information about the write types that are supported in the Core Bluetooth framework, see the CBCharacteristicWriteType enumeration in CBPeripheral Class Reference.

The peripheral responds to write requests that are specified as CBCharacteristicWriteWithResponse by calling the peripheral:didWriteValueForCharacteristic:error: method of its delegate object. If the write fails for any reason, you can implement this delegate method to access the cause of the error, as the following example shows:

- (void)peripheral:(CBPeripheral *)peripheral
didWriteValueForCharacteristic:(CBCharacteristic *)characteristic
             error:(NSError *)error {
 
    if (error) {
        NSLog(@"Error writing characteristic value: %@",
            [error localizedDescription]);
    }
    ...