header

Getting started with iOS10 / Swift and Bluetooth 4.0 for wearables / hardware

Bluetooth 4.0 or Bluetooth Low Energy (BLE) has been around for years now and has enabled the development of wearable technology to really take off. Where once Bluetooth was only useful for sending low grade mobile photos or polyphonic ringtones to other mobile phones, the low energy variety of Bluetooth allows smartwatches, smart light bulbs and even smart clothes to stay constantly connected to your mobile phone. So let’s get started with making a new awesome smart product / wearable!

This tutorial will cover getting started with an iOS device (iPhone / iPad / iPod) that has Bluetooth 4.0 (Anything newer than the iPhone 3GS, so you should be safe), and a DFRobot Bluno Beetle development board, as shown below:

1

This remarkable development 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”.

First up, we’ll create our iOS project. Create a new project in XCode and select ‘Single View Application’, we will be using Swift for this tutorial so make sure that is selected. Once the project is created, we need to make sure that the project includes the CoreBluetooth Framework as that is what we will be using to connect to the device. So go to ‘Build Phases’ in the project settings and make sure ‘CoreBluetooth.framework’ is added in the ‘Link Binary with Libraries’ dropdown.

2

In your project, you should have a ViewController.swift file, we need to rename this to MainViewController.swift, and inside this file, change the class name from ViewController to MainViewController. We also need to add another file, so go ahead and click File -> New -> File (or CMD+N) and select ‘Swift File’. Let’s call this new file ‘ScanTableViewController.swift’.

Next up is configuring our storyboard, it should consist of a Navigation Controller whose root view controller is a ViewController of custom class MainViewController, a TableViewController of custom class ScanTableViewController and a segue between these two view controllers with an identifier of ‘scan-segue’. In the ScanTableViewController, the table view cell should have an identifier of ‘scanTableCell’, and in the MainViewController we have a button with the title ‘Send “Hello World!” and two labels. One that says ‘Message Received’ and one that says ‘…’ which we will change to display a received message. Make sure you create an IBAction for the button by ctrl+dragging to the MainViewController.swift file, and an IBOutlet for the ‘…’ label. It should look like the below screenshot. If you do not know how to configure these properties, you should be able to copy the storyboard and swift files from the finished project linked below.

3

After configuring the storyboard let’s move on to the MainViewController class. Just under import UIKit we need to import the CoreBluetooth framework, after that we need to make sure that MainViewController inherits from CBCentralManagerDelegate and CBPeripheralDelegate as well as UIViewController.

At the start of the class we need to define 3 optionals, one for the CBCentralManager, one for a CBPeripheral where we will be storing a reference to the desired Bluetooth peripheral and one for its writeable characteristic (where we will be sending data). We also need to define two constants, one for the particular Bluno service address, and one for the writable Bluno characteristic. The service being ‘DFB0’ and the characteristic being ‘DFB1’.

After defining these class variables we can move on to viewDidLoad(). Here we will instantiate the CBCentralManager and initially set its delegate to MainViewController or ‘self’. After this we will call customiseNavigationBar(). This is a custom method that is defined below viewDidLoad() and essentially adds a custom UIBarButton to the top navigation bar. This is the button that the user will press to start scanning for nearby Bluetooth devices and will go to the ScanTableView to display the available devices. You will also notice that it actually adds a different button if mainPeripheral is not nil, this is because we will toggle the functionality of this button to disconnect the Bluetooth device if one is already connected. You can see this code below.


import UIKit
import CoreBluetooth

class MainViewController: UIViewController, CBCentralManagerDelegate, CBPeripheralDelegate {
    var manager:CBCentralManager? = nil
    var mainPeripheral:CBPeripheral? = nil
    var mainCharacteristic:CBCharacteristic? = nil
    
    let BLEService = "DFB0"
    let BLECharacteristic = "DFB1"
    
    @IBOutlet weak var recievedMessageText: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        manager = CBCentralManager(delegate: self, queue: nil);
        
        customiseNavigationBar()
    }
    
    func customiseNavigationBar () {
        
        self.navigationItem.rightBarButtonItem = nil
        
        let rightButton = UIButton()
        
        if (mainPeripheral == nil) {
            rightButton.setTitle("Scan", for: [])
            rightButton.setTitleColor(UIColor.blue(), for: [])
            rightButton.frame = CGRect(origin: CGPoint(x: 0,y :0), size: CGSize(width: 60, height: 30))
            rightButton.addTarget(self, action: #selector(self.scanButtonPressed), for: .touchUpInside)
        } else {
            rightButton.setTitle("Disconnect", for: [])
            rightButton.setTitleColor(UIColor.blue(), for: [])
            rightButton.frame = CGRect(origin: CGPoint(x: 0,y :0), size: CGSize(width: 100, height: 30))
            rightButton.addTarget(self, action: #selector(self.disconnectButtonPressed), for: .touchUpInside)
        }
        
        let rightBarButton = UIBarButtonItem()
        rightBarButton.customView = rightButton
        self.navigationItem.rightBarButtonItem = rightBarButton
        
    }

In the above code, you’ll notice that the buttons that we add to the navigation bar call scanButtonPressed() or disconnectButtonPressed(), we will define those now. The first method, scanButtonPressed simply triggers the ‘scan-segue’ segue that takes the user to the ScanTableView. The disconnectButtonPressed() method will just call cancelPeripheralConnection() on the CBCentralManager with the mainPeripheral passed to it. This should result in the Bluetooth device being disconnected, but as the comment in the code notes, it is not guaranteed.

We also need to override prepare for segue so that we can do some setup before we go to the ScanTableView. First we check that we are running during the right segue, then we get a reference to the new ViewController which happens to be ScanTableViewController. After that we will set the CBCentralManager’s delegate to the new view so we can deal with scanning and connecting the device in that class and then we set some optionals in the class, a reference to that CBCentralManager and a reference to the view we are coming from. The code is below and should go just after customiseNavigationBar()


    override func prepare(for segue: UIStoryboardSegue, sender: AnyObject?) {
        
        if (segue.identifier == "scan-segue") {
            let scanController : ScanTableViewController = segue.destinationViewController as! ScanTableViewController
            
            //set the manager's delegate to the scan view so it can call relevant connection methods
            manager?.delegate = scanController
            scanController.manager = manager
            scanController.parentView = self
        }
        
    }
    
    // MARK: Button Methods
    func scanButtonPressed() {
        performSegue(withIdentifier: "scan-segue", sender: nil)
    }
    
    func disconnectButtonPressed() {
        //this will call didDisconnectPeripheral, but if any other apps are using the device it will not immediately disconnect
        manager?.cancelPeripheralConnection(mainPeripheral!)
    }

Moving on to the ScanTableViewController class, we need to import CoreBluetooth here as well and as this view has a TableView in it we need this class to inherit from UITableViewController as well as CBCentralManagerDelegate. At the top of the class we need to define an array for holding the scanned peripherals, an optional for the CBCentralManager passed from the MainViewController and an optional for the MainViewController itself.
After the class variables we can leave viewDidLoad() as it is but below we need to define some methods to conform to the TableView delegate; the first is the numberOfSections method where we can simply return 1 as we only have one section. The second method is the numberOfRowsInSection method where we need to return the number of peripherals in the peripherals array. Then finally we define cellForRowAt where we will configure the table cells, here we use the identifier for the table cell we defined in the storyboard (‘scanTableCell’) and we set the table cell’s text property to the peripheral’s name.


import UIKit
import CoreBluetooth

class ScanTableViewController: UITableViewController, CBCentralManagerDelegate {
    
    var peripherals:[CBPeripheral] = []
    var manager:CBCentralManager? = nil
    var parentView:MainViewController? = nil
    
    override func viewDidLoad() {
        super.viewDidLoad()    
    }
    

    // MARK: - Table view data source
    
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // #warning Incomplete implementation, return the number of rows
        return peripherals.count
    }
    
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "scanTableCell", for: indexPath)
        let peripheral = peripherals[indexPath.row]
        cell.textLabel?.text = peripheral.name
        
        return cell
    }

We can now move on to the CBCentralManagerDelegate methods and methods for scanning for Bluetooth devices. First of all, let’s define a function for scanning; I’ve named it scanBLEDevices() and it uses the CBCentralManager to scan for peripherals that advertise their CBUUID that matches the BLEService variable we defined in MainViewController (DFB0), that way only DFRobot Bluno devices will be detected. The method then stops scanning after 3 seconds, as we do not want it to continue scanning forever, it does this by using DispatchQueue.main.after() and we call stopScanForBLEDevices(). This is defined immediately below and simply calls CBCentralManager.stopScan(). Because we want the scanning to happen immediately after the user opens this view, we should override viewDidAppear and call scanBLEDevices within it. Let’s put viewDidAppear just after viewDidLoad.


override func viewDidAppear(_ animated: Bool) {
        scanBLEDevices()
    }


    // MARK: BLE Scanning
    func scanBLEDevices() {
        manager?.scanForPeripherals(withServices: [CBUUID.init(string: parentView!.BLEService)], options: nil)
        
        //stop scanning after 3 seconds
        DispatchQueue.main.after(when: .now() + 3.0) {
			self.stopScanForBLEDevices()
        }
    }
    
    func stopScanForBLEDevices() {
        manager?.stopScan()
    }    

Now that we have the scanning methods in, we need to define the CBCentralManagerDelegate methods. The first one is the didDiscover method which we will use to populate the peripherals array and then reload the table data, this will cause the table to display the relevant peripheral when it is discovered. The second method is centralManagerDidUpdateState. We don’t actually need to use this one, so we will just print the state that has changed as it still needs to be included, otherwise the compiler will complain that the class doesn’t conform to the delegate correctly. The third method is didConnect. In this method we will do some setup before going back to the MainView (essentially doing the opposite of what we did in prepare forSegue in the last class). First we set the optional mainPeripheral to the peripheral connected, then we set the peripheral’s delegate to the MainViewController so it can deal with discovering services when we get there, then we start discovering the peripheral’s services. After that we switch the CBCentralManager’s delegate back to the MainViewController and then call the MainViewController’s customiseNavigationBar() method which should mean that the ‘Scan’ button in the top bar is replaced with ‘Disconnect’ as mainPeripheral is no longer nil, we then popViewController to go back to the MainView. We then have a 4th method, didFailToConnect which we aren’t going to do anything with except for printing the resulting error.

The last thing to do in this class is to define what to do when a user clicks on the relevant table row. We want to initiate connecting to the device selected, so what we need to do is overwrite tableView didSelectAtRow and get the relevant peripheral from the peripherals array and call connect on the CBCentralManager with it.


override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let peripheral = peripherals[indexPath.row]
        
        manager?.connect(peripheral, options: nil)
    }


    // MARK: - CBCentralManagerDelegate Methods
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : AnyObject], rssi RSSI: NSNumber) {
        
        if(!peripherals.contains(peripheral)) {
            peripherals.append(peripheral)
        }
        
        self.tableView.reloadData()
    }
    
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        print(central.state)
    }
    
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        
        //pass reference to connected peripheral to parent view
        parentView?.mainPeripheral = peripheral
        peripheral.delegate = parentView
        peripheral.discoverServices(nil)
        
        //set the manager's delegate view to parent so it can call relevant disconnect methods
        manager?.delegate = parentView
        parentView?.customiseNavigationBar()
        
        if let navController = self.navigationController {
            navController.popViewController(animated: true)
        }
        
        print("Connected to " +  peripheral.name!)
    }
    
    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: NSError?) {
        print(error)
    }

So now we’ve finished with ScanTableViewController.swift, the user should be able to press the scan button and then select any nearby Bluno devices and then be taken back to MainView. The problem is that the compiler will still complain at this point as we have yet to implement the CBCentralManagerDelegate Methods or CBPeripheralDelegate Methods in MainViewController. The former will deal with disconnecting an already connected peripheral, and the latter will be for discovering the connected peripheral’s services and characteristics.

Let’s implement the remaining CBCentralManagerDelegate Methods, the first one is didDisconnectPeripheral() and as the name suggests will be triggered when a peripheral is requested to be disconnected, so here we will set mainPeripheral to nil and then call customiseNavigationBar() again so that the top bar button is set to ‘Scan’ again. The second method is centralManagerDidUpdateState(), again this is a required method but we will not be using it, so we are simply printing the updated state.

    // MARK: - CBCentralManagerDelegate Methods
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: NSError?) {
        mainPeripheral = nil
        customiseNavigationBar()
        print("Disconnected" + peripheral.name!)
    }
    
    
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        print(central.state)
    }

Next up are the CBPeripheralDelegateMethods that deal with discovering the peripheral’s services and characteristics. The first method is didDiscoverServices() and here we simply loop through the peripherals services array and look at their unique identifier (UUID). The first two we look for are generic identifiers common to every Bluetooth device; 180A is the identifier for the device information service and 1800 is the identifier for the GAP (Generic Access Profile). The third one is the one we are interested in and it looks for the identifier for the Bluno specific service. In all cases though we call peripheral.discoverCharacteristics().

The next method is didDiscoverCharacteristicsFor(), as we did in the previous method we look for the two generic service identifiers and the one Bluno service identifier we are interested in, and then we look for the characteristic identifiers within. In the generic service cases we look for the device name characteristic (2A00), the manufacturer name characteristic (2A29) and the system ID (2A23). A list of these generic characteristics can be found here https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicsHome.aspx

. Then in the Bluno Service, we also loop through the characteristics but we look for the Bluno characteristic we defined in the BLECharacteristic constant, when we find that we call setNotifyValue() on the peripheral with that characteristic and set mainCharacteristic to the characteristic too. This means that every time this characteristics value changes, the phone will be notified; we will be changing this value on the Bluno device, so this is how we will be sending values to the iOS device.

The last method in this delegate is the didUpdateValueFor() method, and as with the previous we look at all the generic services / characteristics and act accordingly (print out values). But if the characteristic matches the characteristic we defined in BLECharacteristic, we grab the updated value as a string and we update the label in the UI with the message we received, which should be the message we sent (Hello World!).


// MARK: CBPeripheralDelegate Methods
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: NSError?) {
        
        for service in peripheral.services! {
            
            print("Service found with UUID: " + service.uuid.uuidString)
            
            //device information service
            if (service.uuid.uuidString == "180A") {
                peripheral.discoverCharacteristics(nil, for: service)
            }
            
            //GAP (Generic Access Profile) for Device Name
            // This replaces the deprecated CBUUIDGenericAccessProfileString
            if (service.uuid.uuidString == "1800") {
                peripheral.discoverCharacteristics(nil, for: service)
            }
            
            //Bluno Service
            if (service.uuid.uuidString == BLEService) {
                peripheral.discoverCharacteristics(nil, for: service)
            }
            
        }
    }
    
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: NSError?) {

        //get device name
        if (service.uuid.uuidString == "1800") {
            
            for characteristic in service.characteristics! {
                
                if (characteristic.uuid.uuidString == "2A00") {
                    peripheral.readValue(for: characteristic)
                    print("Found Device Name Characteristic")
                }
                
            }
            
        }
        
        if (service.uuid.uuidString == "180A") {
            
            for characteristic in service.characteristics! {
                
                if (characteristic.uuid.uuidString == "2A29") {
                    peripheral.readValue(for: characteristic)
                    print("Found a Device Manufacturer Name Characteristic")
                } else if (characteristic.uuid.uuidString == "2A23") {
                    peripheral.readValue(for: characteristic)
                    print("Found System ID")
                }
                
            }
            
        }
        
        if (service.uuid.uuidString == BLEService) {
            
            for characteristic in service.characteristics! {
                
                if (characteristic.uuid.uuidString == BLECharacteristic) {
                    //we'll save the reference, we need it to write data
                    mainCharacteristic = characteristic
                    
                    //Set Notify is useful to read incoming data async
                    peripheral.setNotifyValue(true, for: characteristic)
                    print("Found Bluno Data Characteristic")
                }
                
            }
            
        }
        
    }
    
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: NSError?) {
        
        
        if (characteristic.uuid.uuidString == "2A00") {
            //value for device name recieved
            let deviceName = characteristic.value
            print(deviceName)
        } else if (characteristic.uuid.uuidString == "2A29") {
            //value for manufacturer name recieved
            let manufacturerName = characteristic.value
            print(manufacturerName)
        } else if (characteristic.uuid.uuidString == "2A23") {
            //value for system ID recieved
            let systemID = characteristic.value
            print(systemID)
        } else if (characteristic.uuid.uuidString == BLECharacteristic) {
            //data recieved
            if(characteristic.value != nil) {
                let stringValue = String(data: characteristic.value!, encoding: String.Encoding.utf8)!
            
                recievedMessageText.text = stringValue
            }        }
        
        
    }

The very last thing to do in the iOS app is to define the IBAction we setup from our storyboard to the MainViewController. Here we will be sending the text to our Bluno device, the method first encodes the “Hello World!” string to data, then it checks if we have a mainPeripheral defined (i.e. connected), if we have, it sends the data; if not, it prints “haven’t discovered device yet”.


    @IBAction func sendButtonPressed(_ sender: AnyObject) {
        let helloWorld = "Hello World!"
        let dataToSend = helloWorld.data(using: String.Encoding.utf8)
        
        if (mainPeripheral != nil) {
            mainPeripheral?.writeValue(dataToSend!, for: mainCharacteristic!, type: CBCharacteristicWriteType.withoutResponse)
        } else {
            print("haven't discovered device yet")
        }
    }

That is it for our iOS code. If you want to skip the above, you can download all the code from here (link to public repo). The next thing to do is get some code on to our Bluno device so that it receives what we send, and for it to talk back. The code below just sets up the serial port in setup and then in the main loop looks to see if there is anything available in serial and writes back what its received. Simple!


    void setup() {
        Serial.begin(115200);  //init the Serial
    }

    void loop() {
        if (Serial.available())  {
            Serial.write(Serial.read());//send what has been received
        }
    }

Now if you power up the Bluno and open up the app, you should be able to see the below:

Screen Shot 2016-08-24 at 11.48.01

You can find all the code for the iOS app here, and if you have any questions, let us know!