Bluetooth with Raspberry Pi and bleno – part 4: iOS client

11.04.2018

Konrad Roj
Software Developer
Konrad Roj
 

The example presented in this post explains how to connect, read, and write values for a peripheral Bluetooth device using the BlueCapKit framework. The sample application connects to the calculator service that was built in part 2 of the series.

The other parts in this series talk about iBeacon (part 1) and notifications from the GATT server to the client (part 3).

The client source code is available here, while the GATT server code can be found here.

Discovering the peripheral device

Connecting to a Bluetooth device requires you to go through several steps.

The first one is to actually find the device you want to connect to.

We want to find a device that implements the calculator service, so we have to use the UUID service as a search parameter. If the phone/tablet has bluetooth turned on, we need to start searching for the peripheral. Otherwise, we need to inform the user that bluetooth is turned off (or that another problem has been detected).

let stateChangeFuture = manager.whenStateChanges()
let scanStream = stateChangeFuture.flatMap { [unowned self] state -> FutureStream<Peripheral> in
    switch state {
    case .poweredOn:
        return self.manager.startScanning(forServiceUUIDs: [GattUUID.service])
    case .poweredOff, .unauthorized, .unsupported, .resetting, .unknown:
        self.disconnectFromGatt()
        throw CapError.serviceNotFound
    }
}

Establishing a connection

Once the device has been found, we can stop the search. The next step is to store the reference to the device and connect to it.

let peripherialStream = scanStream.flatMap { [unowned self] discoveredPeripheral  -> FutureStream<Void> in
    self.manager.stopScanning()

    self.peripheral = discoveredPeripheral

    return self.peripheral!.connect(connectionTimeout: 10, capacity: 5)
}

Find Characteristics

After connecting to the device and the selected service, we need to discover the characteristics that will be used to write arguments, and read the result of the calculation.

let discoveryStream = peripherialStream.flatMap { [unowned self] () -> Future<Void> in
    return (self.peripheral?.discoverServices([GattUUID.service]))!
}.flatMap { [unowned self] () -> Future<Void> in
    let service = self.peripheral?.services(withUUID: GattUUID.service)?.first

    DispatchQueue.main.async {
        self.connectButton.setTitle("Connected: \(service!.uuid)", for: .normal)
    }

    return service!.discoverCharacteristics([GattUUID.arg1, GattUUID.arg2, GattUUID.result])
}

Save characteristics and prepare UI

Finally, we save the references of the discovered characteristics and prepare the UI.

_ = discoveryStream.andThen { [unowned self] in
    let service = self.peripheral?.services(withUUID: GattUUID.service)?.first

    guard let resultCharacteristic = service?.characteristics(withUUID: GattUUID.result)?.first else {
        self.disconnectFromGatt()
        return
    }

    guard let arg1Characteristic = service?.characteristics(withUUID: GattUUID.arg1)?.first else {
        self.disconnectFromGatt()
        return
    }

    guard let arg2Characteristic = service?.characteristics(withUUID: GattUUID.arg2)?.first else {
        self.disconnectFromGatt()
        return
    }

    self.resultChar = resultCharacteristic
    self.arg1Char = arg1Characteristic
    self.arg2Char = arg2Characteristic

    self.prepareUI()
}

Data format conversion

We need a way to translate back and forth between the string and byte array (which is used to communicate with the Bluetooth device).

let uint = text.map { (val) -> UInt8 in
    return UInt8(String(val))!
}

let data = Data(bytes: uint, count: MemoryLayout<UInt8>.size)




let uint: [UInt8] = [UInt8](self.resultChar!.dataValue!)
let string = String(describing: uint.first ?? 0)

Writing for the characteristic

The following code writes an argument for the respective characteristic.

func writeArg1(text: String) {
    let uint = text.map { (val) -> UInt8 in
        return UInt8(String(val))!
    }

    let data = Data(bytes: uint, count: MemoryLayout<UInt8>.size)
    let writeFuture = self.arg1Char?.write(data: data)
    writeFuture?.onSuccess(completion: { [unowned self] (_) in
        print("write succes")

        self.readResult()
    })
    writeFuture?.onFailure(completion: { (e) in
        print("write failed")
    })
}

Read from the characteristic

Here’s the method that reads the result of the calculation from the device.

func readResult() {
    let readFuture = self.resultChar?.read(timeout: 5)
    readFuture?.onSuccess { [unowned self] (_) in
        let uint: [UInt8] = [UInt8](self.resultChar!.dataValue!)
        DispatchQueue.main.async {
            self.resultLabel.text = String(describing: uint.first ?? 0)
        }
    }
    readFuture?.onFailure { (_) in
        print("read error")
    }
}

After putting all of it together, we can do the calculations and observe the results in the UI.

Summary

BlueCapKit is a wrapper over CoreBluetooth that provides an interface based on futures / streams. This allows you to build the logical application flow using closures, so it’s easier to read (you can get rid of many delegates from your code).