Creating an iOS app to talk Bluetooth 4.0 using a Bluno board

Bluetooth 4.0 (Bluetooth Low Energy, BLE) is quite popular in Internet of Things devices. iOS and OSX have official support for it, embedded in the CoreBluetooth framework. I will not delve into the full specs of Bluetooth 4.0, but I encourage the reader to find more here.

In this post we’re going to create a simple iOS app that communicates to a bluetooth 4.0 development board called Bluno. The following code can also be applied to communicate with other Bluetooth 4.0 development boards (with small code changes). If you want to skip the tutorial you can download the code from here.

The Bluno Board is actually an Arduino Board with a Bluetooth 4.0 IC (TI CC2540). The Bluetooth IC reads and writes data to the Atmega328p (the Arduino Core)through UART. The Bluetooth IC also performs the function of USB-UART in the board, meaning that this IC can also use Arduino Ide’s serial console to send/receive data to the Bluetooth 4.0 Central, a “Serial-BLE Adapter”.

In order to test the app, it’s absolutely required a Bluetooth 4.0 enabled iOS device (iPhone 4S, iPhone5, iPhone5C, iPhone5s, iPhone6, iPhone6+, iPod 5th Generation, iPad Mini 1/2/3, iPad Retina, iPad Air 1/2) and a developer account to download code on it, and of course, a Bluno board (you can get one here).

It’s not complicated to create an iOS app that supports Bluetooth 4.0 communication. CoreBluetooth provides delegate methods that handle a lot of the job. Full documentation for CoreBluetooth can be found here. In order to communicate with a BLE device we need to configure our iOS device as a Central, the following picture illustrate the communication.

First, we’ll create a new Xcode Project, it’s the ideal thing to use the Single-View Template. We’ll be using Objective-C, targeting iPad or iPhone. Xcode 6.2 and iOS 7.1+ is recommended.

Your UI should look something like this:

screenshot3

We need to add the CoreBluetooth framework, and we also need to define the main service and characteristics of the BLE Device we need to connect to. For the Bluno board we have as Service UUID “dfb0”, and as Characteristic UUID “dfb1”, here we can change these values if your using other board than Bluno, refer to the manufacturer information. Remember that services contain characteristics, characteristics are the actual source of data and we can subscribe to receive notifications whenever the value of a characteristic is updated. Add the following code to MasterViewController.h:


#import <CoreBluetooth/CoreBluetooth.h>

#define BLEService @"dfb0"
#define BLECharacteristic @"dfb1"

In the MasterViewController.h we need to create a NSMutableArray that stores a reference of all found BLE Devices, a CBPeripheral to reference the device to connect to, we also need to set this class as a CBCentralManagerDelegate and a CBPeripheralDelegate. The UI requires a tableview to display BLE Devices nearby, one textfield and a button to send data to the Bluno, and one UILabel to display received data. The UITableView needs that our class implements UITableViewDataSource and UITableViewDelegate methods. Don’t forget to add the IBAction sendData method in MasterViewController.m


@interface MasterViewController : UIViewController<UITableViewDataSource, UITableViewDelegate, CBCentralManagerDelegate, CBPeripheralDelegate, UITextFieldDelegate>{
    CBCentralManager *manager;
    CBPeripheral *mainPeripheral;
    CBCharacteristic *mainCharacteristic;
    
    NSMutableArray *peripherals;
}

@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (weak, nonatomic) IBOutlet UITextField *dataField;
@property (weak, nonatomic) IBOutlet UILabel *receiveText;

- (IBAction)sendData:(id)sender;

We need to initialise the CBCentralManager to handle BLE connections. Modify the viewDidLoad function, and make it match the following code:


- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    //init CBCentralManager and its delegate
    manager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
    
    peripherals = [[NSMutableArray alloc] init];
}

We’ll implement the CBCentralManagerDelegate methods in order to start scanning for BLE Devices. Add the following lines of code to MasterViewController.m:


#pragma mark CBCentralManagerDelegate

//Every time we successfully connect to a peripheral this function will be called
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
    NSLog(@"Connected to %@", peripheral.name);
}

//This function is invoked after a connected device is disconnected, we also remove reference of its delegate
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
    NSLog(@"%@ disconnected...", peripheral.name);
    [peripheral setDelegate:nil];
}

//If any error ocurrs it will be notified in this function
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
    NSLog(@"%@", error);
}

//Add any discovered peripheral to the peripherals array
-(void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
{
    if(![peripherals containsObject:peripheral]){
        [peripherals addObject:peripheral];
    }
    
    [self.tableView reloadData];
}

//The manager detects the bluetooth state from the iOS device and notifies
- (void)centralManagerDidUpdateState:(CBCentralManager *)central{
    char* managerStrings[] = {
        "Unknown", "Resetting", "Unsupported",
        "Unauthorized", "PoweredOff", "PoweredOn"
    };
    
    NSString *auxString = [NSString stringWithFormat:@"Manager State: %s", managerStrings[central.state]];
    NSLog(@"%@", auxString);
}

Add/modify the following UITableView methods:


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [peripherals count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];

    //We set the cell title according to the peripheral's name
    CBPeripheral *peripheral = [peripherals objectAtIndex:indexPath.row];
    cell.textLabel.text = peripheral.name;
    
    return cell;
}

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    CBPeripheral *peripheral = [peripherals objectAtIndex:indexPath.row];
    
    [manager connectPeripheral:peripheral options:nil];
}

Up to this point we have declared all necessary methods to handle the connection of a BLE Device, but we haven't added something really important: the device scanning. Apple suggests to scan only when necessary, to avoid CPU utilisation and battery draining. So, we will only scan when the user requests and for a short period of time. In this case, we will scan for 2 seconds when the user presses a button. Add the following code to the viewDidLoad function:


UIBarButtonItem *scanButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(scanBLEDevices:)];
self.navigationItem.rightBarButtonItem = scanButton;

Now we need to create the scanBLEDevices function and a stop scanning function, we employ a NSTimer to help us:


- (void)scanBLEDevices:(id)sender {
    //we will search for devices that contain the service that our device is programmed to have
    [manager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:BLEService]] options:nil];
    
    //we are going to trigger a NSTimer to stop scanning after 2 seconds
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(stopScan:) userInfo:nil repeats:NO];
}

- (void) stopScan:(id)sender{
    NSTimer *timer = (NSTimer *)sender;
    [timer invalidate];
    
    [manager stopScan];
}

If we run our code right now, we'll have our app capable of scanning and displaying nearby Bluno boards, and if we click on a table view cell we'll be able to connect to the BLE Device, if connected the console will notify us.

Good enough, now it's time to start discovering services and characteristics of the BLE Device. We need to implement CBPeripheralDelegate methods in order to do this.


#pragma mark CBPeripheral Delegate

- (void) peripheral:(CBPeripheral *)aPeripheral didDiscoverServices:(NSError *)error{
    for (CBService *aService in aPeripheral.services){
        NSLog(@"Service found with UUID: %@", aService.UUID);
        
        /* Device Information Service */
        if ([aService.UUID isEqual:[CBUUID UUIDWithString:@"180A"]]){
            [aPeripheral discoverCharacteristics:nil forService:aService];
        }
        
        /* GAP (Generic Access Profile) for Device Name */
        if ([aService.UUID isEqual:[CBUUID UUIDWithString:CBUUIDGenericAccessProfileString]]){
            [aPeripheral discoverCharacteristics:nil forService:aService];
        }
        
        /* Bluno Service */
        if([aService.UUID isEqual:[CBUUID UUIDWithString:BLEService]]){
            [aPeripheral discoverCharacteristics:nil forService:aService];
        }
    }
}

- (void) peripheral:(CBPeripheral *)aPeripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error{
    
    if([service.UUID isEqual:[CBUUID UUIDWithString:CBUUIDGenericAccessProfileString]] ){
        for(CBCharacteristic *aChar in service.characteristics){
            /* Read device name */
            if([aChar.UUID isEqual:[CBUUID UUIDWithString:CBUUIDDeviceNameString]]){
                [aPeripheral readValueForCharacteristic:aChar];
                NSLog(@"Found a Device Name Characteristic");
            }
        }
    }
    if([service.UUID isEqual:[CBUUID UUIDWithString:@"180A"]]){
        for(CBCharacteristic *aChar in service.characteristics){
            /* Read manufacturer name */
            if([aChar.UUID isEqual:[CBUUID UUIDWithString:@"2A29"]]){
                [aPeripheral readValueForCharacteristic:aChar];
                NSLog(@"Found a Device Manufacturer Name Characteristic");
            }else if([aChar.UUID isEqual:[CBUUID UUIDWithString:@"2A23"]]){
                [aPeripheral readValueForCharacteristic:aChar];
                NSLog(@"Found System ID");
            }
        }
    }
    
    if ([service.UUID isEqual:[CBUUID UUIDWithString:BLEService]]){
        for (CBCharacteristic *aChar in service.characteristics){
            /* Read DATA Characteristic */
            if ([aChar.UUID isEqual:[CBUUID UUIDWithString:BLECharacteristic]]){
                //we'll save the reference, we need it to write data
                mainCharacteristic = aChar;
                //Set Notify is extremely useful to read incoming data asynchronously
                [aPeripheral setNotifyValue:YES forCharacteristic:aChar];
                NSLog(@"Found Bluno DATA Characteristic");
            }
        }
    }
}

- (void) peripheral:(CBPeripheral *)aPeripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{
    /* Value for device Name received */
    if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:CBUUIDDeviceNameString]]){
        NSString * deviceName = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];
        NSLog(@"Device Name = %@", deviceName);
    }
    /* Value for manufacturer name received */
    else if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"2A29"]]){
        NSString *manufacturer = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];
        NSLog(@"Manufacturer Name = %@", manufacturer);
    }
    /* Value for manufacturer name received */
    else if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"2A23"]]){
        NSString *systemID = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];
        NSLog(@"System ID = %@", systemID);
    }
    /* Data received */
    else if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:BLECharacteristic]]){
        NSString *data = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];
        NSLog(@"Received Data = %@", data);

        [_receiveText setText:data];
    }
}

Before going further it's important to note that upon discovering a service, we need to discover its characteristics. A characteristic holds a determined value, sometimes it's fixed, like a manufacturer name, but other times it's continually changing (extremely useful for getting sensor data). In the first cases we need to read the characteristic's value once, but for the second cases we need to subscribe to it by using "notifications" (not the ones we are used to), and every time the value is updated the corresponding delegate method will execute, and that's where we have to code and read the updated data.

Before testing the code we need to set the peripheral to respond to the delegate of MasterViewController. So we need to set this just after we succeeded connected to the BLE Device. We need to modify the following method:


//Every time we successfully connect to a peripheral this function will be called
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
    NSLog(@"Connected to %@", peripheral.name);
    
    //we'll save the reference, we'll use it when writing data
    mainPeripheral = peripheral;
    
    //set delegate and discover all available services
    [peripheral setDelegate:self];
    [peripheral discoverServices:nil];
}

It's time to test the code so far. We're now able to explore some of the services and characteristics that our BLE Device has. The delegate method didUpdateValueForCharacteristic provides us with a simple way of reading and displaying data sent by the Bluno. Remember that the Bluno Board employs the "dfb1" characteristic to send data transmitted by UART from the Atmega328p in the board. You could bypass programming and only open the Arduino Ide's serial console having the Bluno connected and write text (at a BaudRate of 115200), you'll see this text displayed on the app and the console!

Now we have to send data, it's not really completed. Just put the following code in the IBAction corresponding to the send button.


- (IBAction)sendData:(id)sender{
    //Read data to send, convert it to NSData
    NSString *auxText = [_dataField text];
    NSData *auxData = [auxText dataUsingEncoding:NSUTF8StringEncoding];
    
    //Send the data by using the stored CBCharacteristic and CBPeripheral references
    [mainPeripheral writeValue:auxData forCharacteristic:mainCharacteristic type:CBCharacteristicWriteWithoutResponse];
}

Don't forget to implement UITextFieldDelegate and set the delegate in the UITextField in order to dismiss keyboard!


#pragma mark UITextField Delegate

- (BOOL)textFieldShouldReturn:(UITextField *)aTextField
{
    [aTextField resignFirstResponder];
    return YES;
}

That's it! We are now sending and receiving data from the Bluno! Hopefully you haven't had any problem coming this far. Sometimes the Bluno doesn't display data coming from the iOS device, but you can see the RX (receiving) led flash when sending data from the iOS Device. So we can implement an echo function in the Atmega328p, to send back everything that is sent from the iOS Device.

IMG_1085

If you need the complete iOS code, you can download it from here.

Advertisements
This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s