In the last post I had an iOS device connected to a Red Bear Labs Bluetooth LE Arduino shield. In that process I used the BLE framework from Red Bear Labs, which got everything going but a fair bit of device control code to end up in the UIViewController.
In this example I will take you through the changes that I made to remove the BLE framework and instead talk directly to Core Bluetooth myself. If you want to look at the entire project the changes are in a branch on Github. The key change is the addition of the LTRedBearLabsController class to the project. As this class is the responsible for controlling the Core Bluetooth hardware it is implemented as a singleton, and it is accessed via class method sharedLTRedBearLabsController.
The controller has the following responsibilities: power on the bluetooth hardware, scan for peripherals, connect to the peripheral, finally it finds out the services the peripheral providers. As Core Bluetooth makes requests to external devices which may respond at any time all of the methods are provided by delegate methods. Once you connect to a peripheral CBPeripheral also has a bunch of delegate methods to update the state of a device.
To power up the Bluetooth hardware we need to create a CBCentralManager. We do this in our init method (self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];). The CBCentralManager nearly always calls back to delegate when the state of things change. As this is communicating with an external devices you never know when things will go out of range or it discovers a new device.
Once we are have the CBCentralManager created we will wait for the Bluetooth hardware to power up. This is generally pretty quick, but it is still implemented as a delegate. We implement the delegate method implemented using – (void)centralManagerDidUpdateState:(CBCentralManager *)central. We are currently looking at the powered on and powered off states. In a commercial app you might have to respond to some of the other states as well.
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
if (central.state == CBCentralManagerStatePoweredOn) {
self.bluetoothStatus = @"Bluetooth powered on";
if (self.lookForConnection)
[self startLookingForConnection];
} else if (central.state == CBCentralManagerStatePoweredOff) {
self.bluetoothStatus = @"Bluetooth powered off";
} else {
NSLog(@"Bluetooth state: %d", central.state);
}
}
As soon as we are powered on we will start looking for a connection to our peripheral. As we start scanning for peripherals we give the UUID of our device. This way we will only identify devices with the RedBearLab UUIDs.
- (void) startLookingForConnection {
if (self.centralManager.state == CBCentralManagerStatePoweredOn) {
[self.centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:BLE_DEVICE_SERVICE_UUID]] options:nil];
self.bluetoothStatus = @"Scanning for peripherals";
} else {
self.lookForConnection = YES;
}
When the CBCentralManager finds a peripheral matching our UUID the centralManager:didDiscoverPeripheral:advertisementData:RSSI: method is called. Here we have enough information to work out if we are going to want to connect to this peripheral.
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI {
if (self.activePeripheral == nil || ! [self.activePeripheral isEqualToPeripheral:peripheral]) {
//Connect to peripheral.
self.activePeripheral = peripheral;
peripheral.delegate = self;
[self.centralManager connectPeripheral:peripheral options:[NSDictionary dictionaryWithObject:@YES forKey:CBConnectPeripheralOptionNotifyOnDisconnectionKey]];
}
}
Our app only keeps track of one device at a time using the activePeripheral property. We also set ourself as the delegate method with the active peripheral, this time we will be called as services are discovered on the peripheral. Finally we ask to connect to the peripheral, noting that we want to know when it disconnects.
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
self.activePeripheral = peripheral;
[self.activePeripheral discoverServices:nil];
}
When the device is connected we can discover the services provided by the peripheral.
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error {
if (!error) {
for (CBService *service in peripheral.services) {
[peripheral discoverCharacteristics:nil forService:service];
}
} else {
NSLog(@"Service discovery was unsuccessful!\n");
}
}
Once all of this is completed the device can be written to. Previously the UIViewController created NSData objects that were fed to the BLE framework. In our custom controller we will remove this code and implement it in LTRedBearLabsController. This is done in the turnLight:on: method. This creates the NSData that contains the two bytes, the first to identify the light, the second whether to turn it on or off.
- (void) turnLight:(NSInteger)light on:(BOOL)lightOn {
UInt8 buf[2] = {0x01, 0x00};
buf[0] = (UInt8)light;
if (lightOn) //default is off
buf[1] = 0x01;
NSData *data = [[NSData alloc] initWithBytes:buf length:2];
[self writeToDevice:data];
}
When writing data to the device the main complexity is making sure we have the correct service and characteristic for the BLE shield. Once they are identified (by their UUIDs) we simply write the data to the device. Our Bluetooth Shield doesn’t give a response when the lights are changed so we are simply hoping that ‘set and forget’ will work for us. One consequence of this is that the shield and the iOS device can end up out of alignment if a command doesn’t go through.
- (void) writeToDevice:(NSData *)data {
CBUUID *serviceUUID = [CBUUID UUIDWithString:BLE_DEVICE_SERVICE_UUID];
CBService *service;
for (CBService *testService in self.activePeripheral.services) {
if ([testService.UUID isEqual:serviceUUID])
service = testService;
}
if (!service) {
NSLog(@"WARNING: No service found");
return;
}
CBUUID *characteristicUUID = [CBUUID UUIDWithString:BLE_DEVICE_TX_UUID];
CBCharacteristic *characteristic;
for (CBCharacteristic *testCharacteristic in service.characteristics) {
if ([testCharacteristic.UUID isEqual:characteristicUUID])
characteristic = testCharacteristic;
}
if (!characteristic) {
NSLog(@"WARNING: No characteristic found");
return;
}
[self.activePeripheral writeValue:data forCharacteristic:characteristic type:CBCharacteristicWriteWithoutResponse];
}
The main consequence of all these changes is that our UIViewController is a lot simpler. The LTRedBearLabsController is initialised and we immediately start looking for a connection. The switches now just call the LTRedBearLabsController to turn on/off the lights. One small enhancement would be to have all the switches call one method and use the tag property on the sender to set the light number.
- (void)viewDidLoad {
[super viewDidLoad];
[[LTRedBearLabsController sharedLTRedBearLabsController] startLookingForConnection];
}
- (IBAction) lightOneChanged:(UISwitch *)sender {
[[LTRedBearLabsController sharedLTRedBearLabsController] turnLight:1 on:sender.on];
}
- (IBAction) lightTwoChanged:(UISwitch *)sender {
[[LTRedBearLabsController sharedLTRedBearLabsController] turnLight:2 on:sender.on];
}
- (IBAction) lightThreeChanged:(UISwitch *)sender {
[[LTRedBearLabsController sharedLTRedBearLabsController] turnLight:3 on:sender.on];
}
Leave a Reply