BlueCap: Swifter CoreBluetooth

BlueCap provides a swift wrapper around CoreBluetooth and much more.

Features

  • A futures interface replacing protocol implementations.
  • Connection events for connect, disconnect and timeout.
  • Service scan timeout.
  • Characteristic read/write timeout.
  • A DSL for specification of GATT profiles.
  • Characteristic profile types encapsulating serialization and deserialization.
  • Example applications implementing Central and Peripheral.
  • A full featured extendable Central scanner and Peripheral emulator available in the App Store.
  • Thread safe.
  • Comprehensive test coverage.

Requirements

  • iOS 8.0+
  • Xcode 7.3+

Installation

CocoaPods

CocoaPods is an Xcode dependency manager. It is installed with the following command,

$ gem install cocoapods

Requires CocoaPods 1.0+

Add BluCapKit to your to your projects Podfile,

platform :ios, '8.0'
use_frameworks!

target 'Your Target Name' do
  pod 'BlueCapKit', '~> 0.1'
end

To enable DBUG output add the post_install hook,

post_install do |installer|
    installer.pods_project.targets.each do |target|
        if target.name == 'BlueCapKit'
            target.build_configurations.each do |config|
                if config.name == 'Debug'
                    config.build_settings['OTHER_SWIFT_FLAGS'] = '-DDEBUG'
                    else
                    config.build_settings['OTHER_SWIFT_FLAGS'] = ''
                end
            end
        end
    end
end

To install run the command,

$ pod install

Carthage

Carthage is a decentralized dependency manager for Xcode projects. It can be installed using Homebrew,

$ brew update
$ brew install carthage

To add BlueCapKit to your Cartfile

github "troystribling/BlueCap" ~> 0.1

To download and build BlueCapKit.framework run the command,

carthage update

then add BlueCapKit.framework to your project.

If desired use the --no-build option,

carthage update --no-build

This will only download the BlueCapKit project. Then follow the steps in Manual to add it to a project.

Manual

  1. Place the BlueCap somewhere in your project directory. You can either copy it or add it as a git submodule.
  2. Open the BlueCap project folder and drag BlueCapKit.xcodeproj into the project navigator of your applications Xcode project.
  3. Under your Projects Info tab set the iOS Deployment Target to 8.0 and verify that the BlueCapKit.xcodeproj iOS Deployment Target is also 8.0.
  4. Under the General tab for your project target add the top BlueCapKit.framework as an Embedded Binary.
  5. Under the Build Phases tab add BlueCapKit.framework as a Target Dependency and under Link Binary With Libraries add CoreLocation.framework and CoreBluetooth.framework.

Getting Started

With BlueCap it is possible to easily implement Central and Peripheral applications, serialize and deserialize messages exchanged with bluetooth devices and define reusable GATT profile definitions. The BlueCap asynchronous interface uses futures instead of the usual block interface or the protocol-delegate pattern. Futures can be chained with the result of the previous passed as input to the next. This simplifies application implementation because the persistence of state between asynchronous calls is eliminated and code will not be distributed over multiple files, which is the case for protocol-delegate, or be deeply nested, which is the case for block interfaces. In this section a brief overview of how an application is constructed will be given. Following sections will describe all use cases supported in some detail. Example applications are also available.

Central Implementation

A simple Central implementation that scans for Peripherals advertising a TI SensorTag Accelerometer Service and connects on peripheral discovery will be constructed.

All applications begin by calling CentralManager#whenPowerOn which returns a Future<Void> completed when the CBCentralManager state is set to CBCentralManagerState.PoweredOn.

let manager = CentralManager()
let powerOnFuture = manager.whenPowerOn()

To start scanning for peripherals advertising the TI SensorTag Accelerometer Service powerOnFuture will chained to CentralManager#startScanningForServiceUUIDs using the Future#flatmap combinator.

let manager = CentralManager()
let serviceUUID = CBUUID(string:TISensorTag.AccelerometerService.uuid)!

let scanningFuture = manager.whenPowerOn().flatmap {
    manager.startScanningForServiceUUIDs([serviceUUID])
}

CentralManager#startScanningForServiceUUIDs returns FutureStream<Peripheral>. scanningFuture will be completed once for each peripheral discovered.

To connect a discovered peripheral use FutureStream#flatmap to call Peripheral#connect() which returns FutureStream<(peripheral: Peripheral, connectionEvent: ConnectionEvent)>.

let manager = CentralManager()
let serviceUUID = CBUUID(string:TISensorTag.AccelerometerService.uuid)!

let connectionFuture = manager.whenPowerOn().flatmap {
    manager.startScanningForServiceUUIDs([serviceUUID])
}.flatmap {peripheral -> FutureStream<(Peripheral, ConnectionEvent)> in
    peripheral.connect()
}

connectionFuture.onSuccess{ (peripheral, connectionEvent) in
    switch connectionEvent {
    case .Connect:
        break
    case .Timeout:
        peripheral.reconnect()
    case .Disconnect:
        peripheral.reconnect()
    case .ForceDisconnect:
        break
    case .GiveUp:
        peripheral.terminate()
     }
}

Here on .Timeout and .Disconnect try to reconnect and on .Giveup terminate connection

See the Central Example application for a more detailed implementation that additionally discovers the peripheral and subscribed to accelerometer update notifications.

Peripheral Implementation

A simple Peripheral application that emulates a TI SensorTag Accelerometer Service with all characteristics and responds to characteristic writes is listed below,

// create service and characteristics using profile definitions
let accelerometerService = MutableService(profile:ConfiguredServiceProfile<TISensorTag.AccelerometerService>())
let accelerometerDataCharacteristic = MutableCharacteristic(profile:RawArrayCharacteristicProfile<TISensorTag.AccelerometerService.Data>())
let accelerometerEnabledCharacteristic = MutableCharacteristic(profile:RawCharacteristicProfile<TISensorTag.AccelerometerService.Enabled>())
let accelerometerUpdatePeriodCharacteristic = MutableCharacteristic(profile:RawCharacteristicProfile<TISensorTag.AccelerometerService.UpdatePeriod>())

// add characteristics to service
accelerometerService.characteristics = [accelerometerDataCharacteristic, accelerometerEnabledCharacteristic, accelerometerUpdatePeriodCharacteristic]

// respond to update period write requests
let accelerometerUpdatePeriodFuture = accelerometerUpdatePeriodCharacteristic.startRespondingToWriteRequests(capacity:2)
accelerometerUpdatePeriodFuture.onSuccess {request in
    if request.value.length > 0 &&  request.value.length <= 8 {
        accelerometerUpdatePeriodCharacteristic.value = request.value
        accelerometerUpdatePeriodCharacteristic.respondToRequest(request, withResult:CBATTError.Success)
    } else {
        accelerometerUpdatePeriodCharacteristic.respondToRequest(request, withResult:CBATTError.InvalidAttributeValueLength)
    }
}

// respond to enabled write requests
let accelerometerEnabledFuture = self.accelerometerEnabledCharacteristic.startRespondingToWriteRequests(capacity:2)
accelerometerEnabledFuture.onSuccess {request in  
    if request.value.length == 1 {
        accelerometerEnabledCharacteristic.value = request.value
        accelerometerEnabledCharacteristic.respondToRequest(request, withResult:CBATTError.Success)
    } else {
        accelerometerEnabledCharacteristic.respondToRequest(request, withResult:CBATTError.InvalidAttributeValueLength)
    }
}

// power on remove all services add service and start advertising
let manager = PeripheralManager.sharedInstance
let startAdvertiseFuture = manager.powerOn().flatmap {_ -> Future<Void> in
    manager.removeAllServices()
}.flatmap {_ -> Future<Void> in
    manager.addService(accelerometerService)
}.flatmap {_ -> Future<Void> in
    manager.startAdvertising(TISensorTag.AccelerometerService.name, uuids:[uuid])
}
startAdvertiseFuture.onSuccess {
    …
}
startAdvertiseFuture.onFailure {error in
    …
}

Examples

Usage

BlueCap supports many features that simplify writing Bluetooth LE applications. This section will describe all features in detail and provide code examples.

  1. CentralManager: The BlueCap CentralManager implementation replaces CBCentralManagerDelegate and CBPeripheralDelegate protocol implementations with a Scala Futures interface using SimpleFutures.

  2. PeripheralManager: The BlueCap PeripheralManager implementation replaces CBPeripheralManagerDelegate protocol implementations with a Scala Futures interface using SimpleFutures.

  3. Serialization/Deserialization: Serialization and deserialization of device messages.

  4. GATT Profile Definition: Define reusable GATT profiles and add profiles to the BlueCap app.

CentralManager

The BlueCap CentralManager implementation replaces CBCentralManagerDelegate and CBPeripheralDelegate protocol implementations with with a Scala Futures interface using SimpleFutures. Futures provide inline implementation of asynchronous callbacks and allow chaining asynchronous calls as well as error handling and recovery. Also, provided are callbacks for connection events and connection and service scan timeouts. This section will describe interfaces and give example implementations for all supported use cases. Simple example applications can be found in the BlueCap project.

PowerOn/PowerOff

The state of the Bluetooth transceiver on a device is communicated to BlueCap CentralManager by the powerOn and powerOff futures,

public func powerOn() -> Future<Void>
public func powerOff() -> Future<Void>

Both methods return a SimpleFutures Future<Void>. For an application to process events,

let manager = CentralManager.sharedInstance
let powerOnFuture = manager.powerOn()
powerOnFuture.onSuccess {
  …
}
let powerOffFuture = manager.powerOff()
powerOffFuture.onSuccess {
    …
}

When CentralManager is instantiated a message giving the current Bluetooth transceiver state is received and while the CentralManager is instantiated messages are received if the transceiver is powered or powered off.

Service Scanning

Central scans for advertising peripherals are initiated by calling the BlueCap CentralManager methods,

// Scan promiscuously for all advertising peripherals
public func startScanning(capacity:Int? = nil) -> FutureStream<Peripheral>

// Scan for peripherals advertising services with UUIDs
public func startScanningForServiceUUIDs(uuids:[CBUUID]!, capacity:Int? = nil) -> FutureStream<Peripheral>

Both methods return a SimpleFutures FutureStream<Peripheral> yielding the discovered Peripheral and take the FutureStream capacity as input.

For an application to scan for Peripherals advertising Services with uuids after powerOn,

let manager = CentralManager.sharedInstance
let serviceUUID = CBUUID(string:"F000AA10-0451-4000-B000-000000000000")!

let peripheraDiscoveredFuture = manager.powerOn().flatmap {_ -> FutureStream<Peripheral> in
    manager.startScanningForServiceUUIDs([serviceUUID], capacity:10)
}
peripheraDiscoveredFuture.onSuccess {peripheral in
    …
}

Here the powerOn future has been flatmapped to startScanning(capacity:Int?) -> FutureStream<Peripheral> to ensure that the service scan starts after the bluetooth transceiver is powered on.

To stop a peripheral scan use the CentralManager method,

public func stopScanning()

and in an application,

let manager = CentralManager.sharedInstance
manager.stopScanning()

Service Scanning with Timeout

BlueCap CentralManager can scan for advertising peripherals with a timeout. TimedScannerator methods are used to start a scan instead ob the CentralManager methods. The declarations include a timeout parameter but are otherwise the same,

// Scan promiscuously for all advertising peripherals
public func startScanning(timeoutSeconds:Double, capacity:Int? = nil) -> FutureStream<Peripheral>

// Scan for peripherals advertising services with UUIDs
public func startScanningForServiceUUIDs(timeoutSeconds:Double, uuids:[CBUUID]!, capacity:Int? = nil) -> FutureStream<Peripheral>

Both methods return a SimpleFutures FutureStream<Peripheral> yielding the discovered peripheral and take the FutureStream capacity as input.

For an application to scan for Peripherals advertising Services with UUIDs and a specified timeout after powerOn,

let manager = CentralManager.sharedInstance
let serviceUUID = CBUUID(string:"F000AA10-0451-4000-B000-000000000000")!

let peripheraDiscoveredFuture = manager.powerOn().flatmap {_ -> FutureStream<Peripheral> in
    TimedScannerator.sharedinstance.startScanningForServiceUUIDs(10.0, uuids:[serviceUUID], capacity:10)
}
peripheraDiscoveredFuture.onSuccess {peripheral in
    …
}
peripheraDiscoveredFuture.onFailure {error in
    …
}

Here the powerOn future has been flatmapped to startScanning(capacity:Int?) -> FutureStream<Peripheral> to ensure that the service scan starts after the bluetooth transceiver is powered on. On timeout peripheraDiscoveredFuture will complete with error BCError.peripheralDiscoveryTimeout.

To stop a peripheral scan use the TimedScannerator method,

public func stopScanning()

and in an application,

TimedScannerator.sharedInstance.stopScanning()

Peripheral Advertisements

Peripheral advertisements are can be obtained using the following Peripheral properties,


// Local peripheral name with key CBAdvertisementDataLocalNameKey
public var advertisedLocalName : String? 

// Manufacture data with key CBAdvertisementDataManufacturerDataKey    
public var advertisedManufactuereData : NSData? 

// Tx power with with key CBAdvertisementDataTxPowerLevelKey
public var advertisedTxPower : NSNumber? 

// Is connectable with key CBAdvertisementDataIsConnectable
public var advertisedIsConnectable : NSNumber? 

// Advertised service UUIDs with key CBAdvertisementDataServiceUUIDsKey
public var advertisedServiceUUIDs : [CBUUID]? 

// Advertised service data with key CBAdvertisementDataServiceDataKey
public var advertisedServiceData : [CBUUID:NSData]? 

// Advertised overflow services with key CBAdvertisementDataOverflowServiceUUIDsKey
public var advertisedOverflowServiceUUIDs : [CBUUID]? 

// Advertised solicited services with key CBAdvertisementDataSolicitedServiceUUIDsKey
public var advertisedSolicitedServiceUUIDs : [CBUUID]? 

Peripheral Connection

After discovering a peripheral a connection must be established to begin messaging. Connecting and maintaining a connection to a bluetooth device can be difficult since signals are weak and devices may have relative motion. BlueCap provides connection events to enable applications to easily handle anything that can happen. ConnectionEvent is an enum with values,

Event Description
Connect Connected to peripheral
Timeout Connection attempt timeout
Disconnect Peripheral disconnected
ForceDisconnect Peripheral disconnected by application
Failed Connection failed without error
GiveUp Give-up trying to connect.

To connect to a peripheral use The BlueCap Peripheral method,

public func connect(capacity:Int? = nil, timeoutRetries:UInt? = nil, disconnectRetries:UInt? = nil, connectionTimeout:Double = 10.0) -> FutureStream<(Peripheral, ConnectionEvent)>

Discussion

BlueCap Peripheral connect returns a SimpleFutures FutureStream<(Peripheral, ConnectionEvent)> yielding a tuple containing the connected Peripheral and the ConnectionEvent.

capacity FutureStream capacity
timeoutRetries Number of connection retries on timeout. Equals 0 if nil.
disconnectRetries Number of connection retries on disconnect. Equals 0 if nil.
connectionTimeout Connection timeout in seconds. Default is 10s.

Other BlueCap Peripheral connection management methods are,

// Reconnect peripheral if disconnected
public func reconnect()

// Disconnect peripheral
public func disconnect()

// Terminate peripheral
public func terminate()

An application can connect a Peripheral using,

let manager = CentralManager.sharedInstance
let serviceUUID = CBUUID(string:"F000AA10-0451-4000-B000-000000000000")!

let peripheralConnectFuture = manager.powerOn().flatmap {_ -> FutureStream<Peripheral> in
    manager.startScanningForServiceUUIDs([serviceUUID], capacity:10)
}.flatmap{peripheral -> FutureStream<(Peripheral, ConnectionEvent)> in
    return peripheral.connect(capacity:10, timeoutRetries:5, disconnectRetries:5, connectionTimeout:10.0)
}
peripheralConnectFuture.onSuccess {(peripheral, connectionEvent) in
    switch connectionEvent {
  case .Connect:
      …
  case .Timeout:
    peripheral.reconnect()
        …
  case .Disconnect:
    peripheral.reconnect()
        …
  case .ForceDisconnect:
      …
  case .Failed:
      …
  case .GiveUp:
      peripheral.terminate()
        …
  }
}
peripheralConnectFuture.onFailure {error in
    …
}

Here the peripheraDiscoveredFuture from the previous section is flatmapped to connect(capacity:Int? = nil, timeoutRetries:UInt, disconnectRetries:UInt?, connectionTimeout:Double) -> FutureStream<(Peripheral, ConnectionEvent)> to ensure that connections are made after Peripherals are discovered. When ConnectionEvents of .Timeout and .Disconnect are received an attempt is made to reconnect the Peripheral. The connection is configured for a maximum of 5 timeout retries and 5 disconnect retries. If either of these thresholds is exceeded a .GiveUp event is received and the Peripheral connection is terminated ending all reconnection attempts.

Service and Characteristic Discovery

After a Peripheral is connected its Services and Characteristics must be discovered before Characteristic values can be read or written to or update notifications can be received.

There are several BlueCap Peripheral methods that can be used to discover Services and Characteristics.

// Discover services and characteristics for services with UUIDs
public func discoverPeripheralServices(services:[CBUUID]!) -> Future<Peripheral>

// Discover all services and characteristics supported by peripheral
public func discoverAllPeripheralServices() -> Future<Peripheral>

Both methods return a SimpleFutures Future<Peripheral> yielding the connected Peripheral.

An application can discover a Peripheral using,

// errors
public enum ApplicationErrorCode : Int {
    case PeripheralNotConnected = 1
}

public struct ApplicationError {
    public static let domain = "Application"
    public static let peripheralNotConnected = NSError(domain:domain, code:ApplicationErrorCode.PeripheralNotConnected.rawValue, userInfo:[NSLocalizedDescriptionKey:"Peripheral not connected"])
}
…
// peripheralConnectFuture and serviceUUID are defined in previous section
…
let characteristicsDiscoveredFuture = peripheralConnectFuture.flatmap {(peripheral, connectionEvent) -> Future<Peripheral> in
    if peripheral.state == .Connected {
      return peripheral.discoverPeripheralServices([serviceUUID])
    } else {
      let promise = Promise<Peripheral>()
    promise.failure(ApplicationError.peripheralNotConnected)
    return promise.future
  }
}
characteristicsDiscoveredFuture.onSuccess {peripheral in
    …
}
characteristicsDiscoveredFuture.onFailure {error in
    …
}

Here the peripheralConnectFuture from the previous section is flatmapped to discoverPeripheralServices(services:[CBUUID]!) -> Future<Peripheral> to ensure that the Peripheral is connected before Service and Characteristic discovery starts. Also, the Peripheral is discovered only if it is connected and an error is returned if the Peripheral is not connected.

Characteristic Write

After a Peripherals Characteristics are discovered writing Characteristic values is possible. Many BlueCap Characteristic methods are available,

// Write an NSData object to characteristic value
public func writeData(value:NSData, timeout:Double = 10.0) -> Future<Characteristic>

// Write a characteristic String Dictionary value
public func writeString(stringValue:[String:String], timeout:Double = 10.0) -> Future<Characteristic>

// Write a Deserializable characteristic value
public func write<T:Deserializable>(value:T, timeout:Double = 10.0) -> Future<Characteristic>

// Write a RawDeserializable characteristic value
public func write<T:RawDeserializable>(value:T, timeout:Double = 10.0) -> Future<Characteristic>

// Write a RawArrayDeserializable characteristic value
public func write<T:RawArrayDeserializable>(value:T, timeout:Double = 10.0) -> Future<Characteristic>

// Write a RawPairDeserializable characteristic value
public func write<T:RawPairDeserializable>(value:T, timeout:Double = 10.0) -> Future<Characteristic>

// Write a RawArrayPairDeserializable characteristic value
public func write<T:RawArrayPairDeserializable>(value:T, timeout:Double = 10.0) -> Future<Characteristic>

Using the RawDeserializable enum an application can write a BlueCap Characteristic as follows,

// errors
public enum ApplicationErrorCode : Int {
    case CharacteristicNotFound = 1
}

public struct ApplicationError {
    public static let domain = "Application"
    public static let characteristicNotFound = NSError(domain:domain, code:ApplicationErrorCode.CharacteristicNotFound.rawValue, userInfo:[NSLocalizedDescriptionKey:"Characteristic Not Found"])
}

// RawDeserializable enum
enum Enabled : UInt8, RawDeserializable {
    case No  = 0
    case Yes = 1
    public static let uuid = "F000AA12-0451-4000-B000-000000000000"
}
let enabledUUID = CBUUID(string:Enabled.uuid)!
…
// characteristicsDiscoveredFuture and serviceUUID are defined in a previous section
…
let writeCharacteristicFuture = characteristicsDiscoveredFuture.flatmap {peripheral -> Future<Characteristic> in
    if let service = peripheral.service(serviceUUID), characteristic = service.characteristic(enabledUUID) {
        return characteristic.write(Enabled.Yes, timeout:20.0)
    } else {
        let promise = Promise<Characteristic>()
        promise.failure(ApplicationError.characteristicNotFound)
        return promise.future
    }
}
writeCharacteristicFuture.onSuccess {characteristic in
    …
}
writeCharacteristicFuture.onFailure {error in
    …
}

Here the characteristicsDiscoveredFuture previously defined is flatmapped to write<T:RawDeserializable>(value:T, timeout:Double) -> Future<Characteristic> to ensure that characteristic has been discovered before writing. An error is returned if the characteristic is not found.

Characteristic Read

After a Peripherals Characteristics are discovered reading Characteristic values is possible. Many BlueCap Characteristic methods are available,

// Read a characteristic from a peripheral service
public func read(timeout:Double = 10.0) -> Future<Characteristic>

// Return the characteristic value as and NSData object
public var dataValue : NSData!

// Return the characteristic value as a String Dictionary.
public var stringValue :[String:String]?

// Return a Deserializable characteristic value
public func value<T:Deserializable>() -> T?

// Return a RawDeserializable characteristic value
public func value<T:RawDeserializable where T.RawType:Deserializable>() -> T?

// Return a RawArrayDeserializable characteristic value
public func value<T:RawArrayDeserializable where T.RawType:Deserializable>() -> T?

// Return a RawPairDeserializable characteristic value
public func value<T:RawPairDeserializable where T.RawType1:Deserializable, T.RawType2:Deserializable>() -> T?

Using the RawDeserializable enum an application can read a BlueCap Characteristic as follows,

// errors
public enum ApplicationErrorCode : Int {
    case CharacteristicNotFound = 1
}

public struct ApplicationError {
    public static let domain = "Application"
    public static let characteristicNotFound = NSError(domain:domain, code:ApplicationErrorCode.CharacteristicNotFound.rawValue, userInfo:[NSLocalizedDescriptionKey:"Characteristic Not Found"])
}

// RawDeserializable enum
enum Enabled : UInt8, RawDeserializable {
    case No  = 0
    case Yes = 1
    public static let uuid = "F000AA12-0451-4000-B000-000000000000"
}
let enabledUUID = CBUUID(string:Enabled.uuid)!
…
// characteristicsDiscoveredFuture and serviceUUID 
// are defined in a previous section
…
let readCharacteristicFuture = characteristicsDiscoveredFuture.flatmap {peripheral -> Future<Characteristic> in
    if let service = peripheral.service(serviceUUID), characteristic = service.characteristic(enabledUUID) {
        return characteristic.read(timeout:20.0)
    } else {
        let promise = Promise<Characteristic>()
        promise.failure(ApplicationError.characteristicNotFound)
        return promise.future
    }
}
writeCharacteristicFuture.onSuccess {characteristic in
    if let value : Enabled = characteristic.value {
        …
    }
}
writeCharacteristicFuture.onFailure {error in
    …
}

Here the characteristicsDiscoveredFuture previously defined is flatmapped to read(timeout:Double) -> Future<Characteristic> to ensure that characteristic has been discovered before reading. An error is returned if the characteristic is not found.

Characteristic Update Notifications

After a Peripherals Characteristics are discovered subscribing to Characteristic value update notifications is possible. Several BlueCap Characteristic methods are available,

// subscribe to characteristic update
public func startNotifying() -> Future<Characteristic>

// receive characteristic value updates
public func receiveNotificationUpdates(capacity:Int? = nil) -> FutureStream<Characteristic>

// unsubscribe from characteristic updates
public func stopNotifying() -> Future<Characteristic>

// stop receiving characteristic value updates
public func stopNotificationUpdates()

Using the RawDeserializable enum an application can receive notifications from a BlueCap Characteristic as follows,

// errors
public enum ApplicationErrorCode : Int {
    case CharacteristicNotFound = 1
}

public struct ApplicationError {
    public static let domain = "Application"
    public static let characteristicNotFound = NSError(domain:domain, code:ApplicationErrorCode.CharacteristicNotFound.rawValue, userInfo:[NSLocalizedDescriptionKey:"Characteristic Not Found"])
}

// RawDeserializable enum
enum Enabled : UInt8, RawDeserializable {
    case No  = 0
    case Yes = 1
    public static let uuid = "F000AA12-0451-4000-B000-000000000000"
}
let enabledUUID = CBUUID(string:Enabled.uuid)!
…
// characteristicsDiscoveredFuture and serviceUUID are defined in a previous section
…
let subscribeCharacteristicFuture = characteristicsDiscoveredFuture.flatmap {peripheral -> Future<Characteristic> in
    if let service = peripheral.service(serviceUUID), characteristic = service.characteristic(enabledUUID) {
        return characteristic.startNotifying()
    } else {
        let promise = Promise<Characteristic>()
        promise.failure(ApplicationError.characteristicNotFound)
        return promise.future
    }
}
subscribeCharacteristicFuture.onSuccess {characteristic in
    …
}
subscribeCharacteristicFuture.onFailure {error in
    …
}

let updateCharacteristicFuture = subscribeCharacteristicFuture.flatmap{characteristic -> FutureStream<Characteristic> in
    return characteristic.receiveNotificationUpdates(capacity:10)
}
updateCharacteristicFuture.onSuccess {characteristic in
    if let value : Enabled = characteristic.value {
        …
    }
}
updateCharacteristicFuture.onFailure {error in 
}

Here the characteristicsDiscoveredFuture previously defined is flatmapped to startNotifying() -> Future<Characteristic> to ensure that characteristic has been discovered before subscribing to updates. An error is returned if the characteristic is not found. Then updateCharacteristicFuture is flatmapped again to receiveNotificationUpdates(capacity:Int?) -> FutureStream<Characteristic> to ensure that the subsections is completed before receiving updates.

For an application to unsubscribe to Characteristic value updates and stop receiving updates,

// serviceUUID and enabledUUID are define in the example above
if let service = peripheral.service(serviceUUID), characteristic = service.characteristic(enabledUUID) {

    // stop receiving updates
    characteristic.stopNotificationUpdates()

    // unsubscribe to notifications
    characteristic.stopNotifying()
}

PeripheralManager

The BlueCap PeripheralManager implementation replaces CBPeripheralManagerDelegate protocol implementations with with a Scala Futures interface using SimpleFutures. Futures provide inline implementation of asynchronous callbacks and allows chaining asynchronous calls as well as error handling and recovery. This section will describe interfaces and give example implementations for all supported use cases. Simple example applications can be found in the BlueCap github repository.

PowerOn/PowerOff

The state of the Bluetooth transceiver on a device is communicated to BlueCap PeripheralManager by the powerOn and powerOff futures,

public func powerOn() -> Future<Void>
public func powerOff() -> Future<Void>

Both methods return a SimpleFutures Future yielding Void. For an application to process events,

let manager = PeripheralManager.sharedInstance
let powerOnFuture = manager.powerOn()
powerOnFuture.onSuccess {
  …
}
let powerOffFuture = manager.powerOff()
powerOffFuture.onSuccess {
    …
}

When PeripheralManager is instantiated a message giving the current bluetooth transceiver state is received and while the PeripheralManager is instantiated messages are received if the transceiver is powered or powered off.

Add Services and Characteristics

Services and characteristics are added to a peripheral application before advertising. The BlueCap PeripheralManager methods used for managing services are,

// add a single service
public func addService(service:MutableService) -> Future<Void>

// add multiple services
public func addServices(services:[MutableService]) -> Future<Void>

// remove a service
public func removeService(service:MutableService) -> Future<Void>

// remove all services
public func removeAllServices() -> Future<Void>

All methods return a SimpleFutures Future<Void>. The methods can only be used before PeripheralManager begins advertising.

The BlueCap MutableService methods are,

// add characteristics
public var characteristics : [MutableCharacteristic] {get set}

// create characteristics from profiles
public func characteristicsFromProfiles(profiles:[CharacteristicProfile])

A Peripheral application will add Services and Characteristics using,

// service UUId and characteristic value definition
let serviceUUID = CBUUID(string:"F000AA10-0451-4000-B000-000000000000")
enum Enabled : UInt8, RawDeserializable {
    case No  = 0
    case Yes = 1
    public static let uuid = "F000AA12-0451-4000-B000-000000000000"
}

// create service and characteristic
let service = MutableService(uuid:serviceUUID)
let characteristic = MutableCharacteristic(uuid:Enabled.uuid,                                            properties:CBCharacteristicProperties.Read|CBCharacteristicProperties.Write,                                                 permissions:CBAttributePermissions.Readable|CBAttributePermissions.Writeable,                                                value:Serde.serialize(Enabled.No)))

// add characteristics to service 
service.characteristics = [characteristic]

// add service to peripheral
let manager = PeripheralManager.sharedInstance
let addServiceFuture = manager.powerOn().flatmap {_ -> Future<Void> in
    manager.removeAllServices()
}.flatmap {_ -> Future<Void> in
    manager.addService(service)
}

addServiceFuture.onSuccess {
    …
}
addServiceFuture.onFailure {error in
    …
}

First BlueCap MutableServices and MutableCharacteristics are created and CBCharacteristicProperties and CBAttributePermissions are specified. The Characteristic is then added to the Service. Then the PeripheralManager powerOn() -> Future<Void> is flatmapped to removeAllServices() -> Future<Void> which is then flatmapped to addServices(services:[MutableService]) -> Future<Void>. This sequence ensures that the Peripheral is powered and with no services before the new services are added.

If Service and Characteristic GATT profile definitions are available creating Services and Characteristics is a little simpler,

let  service = MutableService(profile:ConfiguredServiceProfile<TISensorTag.AccelerometerService>())
let characteristic = MutableCharacteristic(profile:RawCharacteristicProfile<TISensorTag.AccelerometerService.Enabled>())

Here the BlueCap the TiSensorTag GATT profile was used.

Advertising

After services and characteristics have been added the peripheral is ready to begin advertising using the methods,

// start advertising with name and services
public func startAdvertising(name:String, uuids:[CBUUID]?) -> Future<Void>

// start advertising with name and no services
public func startAdvertising(name:String) -> Future<Void> 

// stop advertising
public func stopAdvertising() -> Future<Void>

All methods return a SimpleFutures Future<Void>. For a Peripheral application to advertise,

// use service and characteristic defined in previous section
let manager = PeripheralManager.sharedInstance
let startAdvertiseFuture = manager.powerOn().flatmap {_ -> Future<Void> in
    manager.removeAllServices()
}.flatmap {_ -> Future<Void> in
    manager.addService(service)
}.flatmap {_ -> Future<Void> in
    manager.startAdvertising("My Service", uuids:[serviceUUID])
}

startAdvertiseFuture.onSuccess {
    …
}
startAdvertiseFuture.onFailure {error in
    …
}

Here the addServiceFuture of the previous section is flatmapped to startAdvertising(name:String, uuids:[CBUUID]?) -> Future<Void> ensuring that services and characteristics are available before advertising begins.

Set Characteristic Value

A BlueCap Characteristic value can be set any time after creation of the Characteristic. The BlueCap MutableCharacteristic methods used are,

var value : NSData? {get set}

It is not necessary for the PeripheralManager to be powered on or advertising to set a characteristic value.

A peripheral application can set a characteristic value using,

// Enabled and characteristic defined above
characteristic.value = Serde.serialize(Enabled.Yes)

Updating Characteristic Value

If a Characteristic value supports the property CBCharacteristicProperties.Notify a Central can subscribe to value updates. In addition to setting the new value an update notification must be sent. The BlueCap MutableCharacteristic methods used are,

// update with NSData
func updateValueWithData(value:NSData) -> Bool

// update with String Dictionary
public func updateValueWithString(value:Dictionary<String, String>) -> Bool

// update with object supporting Deserializable
public func updateValue<T:Deserializable>(value:T) -> Bool

// update with object supporting RawDeserializable
public func updateValue<T:RawDeserializable>(value:T) -> Bool

// update with object supporting RawArrayDeserializable
public func updateValue<T:RawArrayDeserializable>(value:T) -> Bool

// update with object supporting RawPairDeserializable
public func updateValue<T:RawPairDeserializable>(value:T) -> Bool

// update with object supporting RawArrayPairDeserializable
public func updateValue<T:RawArrayPairDeserializable>(value:T) -> Bool

All methods return a Bool which is true if the update succeeds and false if either there are no subscribers, CBCharacteristicProperties.Notify is not supported or the length of the update queue is exceeded. In addition to sending an update notification to a subscribing Central the Characteristic value is set. A BlueCap Characteristic value can be updated any time after creation of the characteristic. It is not necessary for the PeripheralManager to be powered on or advertising. Though in this case the update will fail and return false.

Peripheral applications would send notification updates using,

// Enabled and characteristic defined above
characteristic.updateValue(Enabled.No)

Respond to Characteristic Write

If a Characteristic value supports the property CBCharacteristicProperties.Write a Central can change the Characteristic value. The BlueCap MutableCharacteristic methods used are,

// start processing write requests with stream capacity
public func startRespondingToWriteRequests(capacity:Int? = nil) -> FutureStream<CBATTRequest>

// respond to received write request
func respondToRequest(request:CBATTRequest, withResult result:CBATTError)

// stop processing write requests
public func stopProcessingWriteRequests()

CBATTRequest encapsulates Central write requests, CBATTError encapsulates response the code and a SimpleFutures FutureStream<CBATTRequest> is used to respond to write requests.

Peripheral applications would start responding to Central writes using,

let writeFuture = characteristic.startRespondingToWriteRequests(capacity:10)
writeFuture.onSuccess {request in
    if request.value.length == 1 {
        characteristic.value = request.value
        characteristic.respondToRequest(request, withResult:CBATTError.Success)
    } else {  
        characteristic.respondToRequest(request, withResult:CBATTError.InvalidAttributeValueLength)
    }
}

Peripheral applications would stop responding to write requests using,

characteristic.stopProcessingWriteRequests()

iBeacon Emulation

iBeacon emulation does not require Services and Characteristics. Only advertising is required. The BlueCap PeripheralManager methods used are,

// start advertising beceacon region
public func startAdvertising(region:BeaconRegion) -> Future<Void>

// stop advertising
public func stopAdvertising() -> Future<Void>

All methods return a SimpleFutures Future<Void>. Creation of a FutureLocation BeaconRegion is also required,

public convenience init(proximityUUID:NSUUID, identifier:String, major:UInt16, minor:UInt16)
proximityUUID The proximity ID of the beacon targeted
identifier A unique identifier for region used by application
major The major value used to identify one or more beacons
minor The minor value used to identify a specific beacon

For an iBeacon application to advertise,

// use service and characteristic defined in previous section
let regionUUID = CBUUID(string:"DE6E8DAD-8D99-4E20-8C4B-D9CC2F9A7E83")!
let startAdvertiseFuture = manager.powerOn().flatmap {_ -> Future<Void> in
    let beaconRegion = BeaconRegion(proximityUUID:regionUUID, identifier:"My iBeacon", major:100, minor:1, capacity:10)
    manager.startAdvertising(beaconRegion)
}

startAdvertiseFuture.onSuccess {
    …
}
startAdvertiseFuture.onFailure {error in
    …
}

Here the powerOn() -> Future<Void> flatmapped to startAdvertising(region:BeaconRegion) -> Future<Void> ensuring that the bluetooth transceiver is powered on before advertising begins.

Serialization/Deserialization

Serialization and deserialization of device messages requires protocol implementations. Then application objects can be converted to and from NSData objects using methods on Serde. Example implantations of each protocol can be found in the TiSensorTag GATT profile available in BlueCapKit and the following examples are implemented in a BlueCap Playground.

Strings

For Strings Serde serialize and deserialize are defined by,

// Deserialize Strings
public static func deserialize(data:NSData, encoding:NSStringEncoding = NSUTF8StringEncoding) -> String?

// Serialize Strings
public static func serialize(value:String, encoding:NSStringEncoding = NSUTF8StringEncoding) -> NSData?

NSStringEncoding supports many encodings.

to use in an application,

if let data = Serde.serialize("Test") {
    if let value = Serde.deserialize(data) {
        println(value)
    }
}

Deserializable Protocol

The Deserializable protocol is used to define deserialization of numeric objects and is defined by,

public protocol Deserializable {
    static var size : Int {get}
    static func deserialize(data:NSData) -> Self?
    static func deserialize(data:NSData, start:Int) -> Self?
    static func deserialize(data:NSData) -> [Self]
    init?(stringValue:String)
}

Description

size Size of object in bytes
deserialize(data:NSData) -> Self? Deserialize entire message to object
deserialize(data:NSData, start:Int) -> Self? Deserialize message starting at offset to object
deserialize(data:NSData) -> [Self] Deserialize entire message to array of objects
init?(stringValue:String) Create object from string

BlueCalKit provides implementation of Deserializable for UInt8, Int8, UInt16 and Int16. The Serde serialize and deserialize are defined by,

// Deserialize objects supporting Deserializable
public static func deserialize<T:Deserializable>(data:NSData) -> T?

// Serialize objects supporting Deserializable
public static func serialize<T:Deserializable>(value:T) -> NSData

For UInt8 data,

let data = Serde.serialize(UInt8(31))
if let value : UInt8 = Serde.deserialize(data) {
    println("\(value)")
}

RawDeserializable Protocol

The RawDeserializable protocol is used to define a message that contains a single value and is defined by,

public protocol RawDeserializable {
    typealias RawType
    static var uuid   : String  {get}
    var rawValue      : RawType {get}
    init?(rawValue:RawType)
}

Description

uuid Characteristic UUID
rawValue Characteristic RawType value
init?(rawValue:RawType) Create object from rawValue

The Serde serialize and deserialize are defined by,

// Deserialize objects supporting RawDeserializable
public static func deserialize<T:RawDeserializable where T.RawType:Deserializable>(data:NSData) -> T?

// Serialize objects supporting RawDeserializable
public static func serialize<T:RawDeserializable>(value:T) -> NSData

Note that RawType is required to be Deserializable to be deserialized. An Enum partially supports RawDeserializable, so,

enum Enabled : UInt8, RawDeserializable {
    case No  = 0
    case Yes = 1
    public static let uuid = "F000AA12-0451-4000-B000-000000000000"
}

and,

let data = Serde.serialize(Enabled.Yes)
if let value : Enabled = Serde.deserialize(data) {
    println("\(value.rawValue)")
}

RawDeserializable can also be implemented in a struct or class.

struct Value : RawDeserializable {
    let rawValue : UInt8
    static let uuid = "F000AA13-0451-4000-B000-000000000000"
    init?(rawValue:UInt8) {
      self.rawValue = rawValue
    }
}

and,

if let initValue = Value(rawValue:10) {
    let data = Serde.serialize(initValue)
    if let value : Value = Serde.deserialize(data) {
        println(“\(value.rawValue)”)
    }
}

RawArrayDeserializable Protocol

The RawArrayDeserializable protocol is used to define a message that contains multiple values of a single type and is defined by,

public protocol RawArrayDeserializable {
    typealias RawType
    static var uuid   : String    {get}
    static var size   : Int       {get}
    var rawValue      : [RawType] {get}
    init?(rawValue:[RawType])
}

Description

uuid Characteristic UUID
size Size of array
rawValue Characteristic RawType values
init?(rawValue:[RawType]) Create object from rawValues

The Serde serialize and deserialize are defined by,

// Deserialize objects supporting RawArrayDeserializable
public static func deserialize<T:RawArrayDeserializable where T.RawType:Deserializable>(data:NSData) -> T?

// Serialize objects supporting RawArrayDeserializable
public static func serialize<T:RawArrayDeserializable>(value:T) -> NSData

Note that RawType is required to be Deserializable to be deserialized. RawArrayDeserializable can be implemented in a struct or class.

struct RawArrayValue : RawArrayDeserializable {    
    let rawValue : [UInt8]
    static let uuid = "F000AA13-0451-4000-B000-000000000000"
    static let size = 2

    init?(rawValue:[UInt8]) {
        if rawValue.count == 2 {
            self.rawValue = rawValue
        } else {
            return nil
        }
    }
}

and,

if let initValue = RawArrayValue(rawValue:[4,10]) {
    let data = Serde.serialize(initValue)
    if let value : RawArrayValue = Serde.deserialize(data) {
        println("\(value.rawValue)")
    }
}

RawPairDeserializable Protocol

The RawPairDeserializable is used to define a message that contains two values of different types and is defined by,

public protocol RawPairDeserializable {
    typealias RawType1
    typealias RawType2
    static var uuid : String   {get}
    var rawValue1   : RawType1 {get}
    var rawValue2   : RawType2 {get}
    init?(rawValue1:RawType1, rawValue2:RawType2)
}

Description

uuid Characteristic UUID
rawValue1 Characteristic RawType1 value
rawValue2 Characteristic RawType2 value
init?(rawValue1:RawType1, rawValue2:RawType2) Create object from rawValues

The Serde serialize and deserialize are defined by,

// Deserialize objects supporting RawPairDeserializable
public static func deserialize<T:RawPairDeserializable where T.RawType1:Deserializable, T.RawType2:Deserializable>(data:NSData) -> T?

// Serialize objects supporting RawPairDeserializable
public static func serialize<T:RawPairDeserializable>(value:T) -> NSData

Note that RawType1 and RawType2 are required to be Deserializable to be deserialized. RawPairDeserializable can be implemented in a struct or class.

struct RawPairValue : RawPairDeserializable {
    let rawValue1 : UInt8
    let rawValue2 : Int8
    static let uuid = "F000AA13-0451-4000-B000-000000000000"

    init?(rawValue1:UInt8, rawValue2:Int8) {
        self.rawValue1 = rawValue1
        self.rawValue2 = rawValue2
    }
}

and,

if let initValue = RawPairValue(rawValue1:10, rawValue2:-10) {
    let data = Serde.serialize(initValue)
    if let value : RawPairValue = Serde.deserialize(data) {
        println("\(value.rawValue1)")
        println("\(value.rawValue2)")
    }
}

RawArrayPairDeserializable Protocol

The RawArrayPairDeserializable is used to define a message that contains multiple values of two different types and is defined by,

public protocol RawArrayPairDeserializable {
    typealias RawType1
    typealias RawType2
    static var uuid   : String     {get}
    static var size1  : Int        {get}
    static var size2  : Int        {get}
    var rawValue1     : [RawType1] {get}
    var rawValue2     : [RawType2] {get}
    init?(rawValue1:[RawType1], rawValue2:[RawType2])
}

Description

uuid Characteristic UUID
size1 Size of RawType1 array
size2 Size of RawType2 array
rawValue1 Characteristic RawType1 value
rawValue2 Characteristic RawType2 value
init?(rawValue1:[RawType1], rawValue2:[RawType2]) Create object from rawValues

The Serde serialize and deserialize are defined by,

// Deserialize objects supporting RawPairDeserializable
public static func deserialize<T:RawArrayPairDeserializable where T.RawType1:Deserializable,  T.RawType2:Deserializable>(data:NSData) -> T?

// Deserialize objects supporting RawPairDeserializable
public static func serialize<T:RawArrayPairDeserializable>(value:T) -> NSData

Note that RawType1 and RawType2 are required to be Deserializable to be deserialized. RawArrayPairDeserializable can be implemented in a struct or class.

struct RawArrayPairValue : RawArrayPairDeserializable {
    let rawValue1 : [UInt8]
    let rawValue2 : [Int8]
    static let uuid = "F000AA13-0451-4000-B000-000000000000"
    static let size1 = 2
    static let size2 = 2

    init?(rawValue1:[UInt8], rawValue2:[Int8]) {
        if rawValue1.count == 2 && rawValue2.count == 2 {
            self.rawValue1 = rawValue1
            self.rawValue2 = rawValue2
        } else {
            return nil
        }
    }
}

and,

if let initValue = RawArrayPairValue(rawValue1:[10, 100], rawValue2:[-10, -100]) {
    let data = Serde.serialize(initValue)
    if let value : RawArrayPairValue = Serde.deserialize(data) {
        println("\(value.rawValue1)")
        println("\(value.rawValue2)")
    }
}

GATT Profile Definition

GATT profile definitions are required to add support for a device to the BlueCap app but are not required to build a functional application using the framework. Implementing a GATT profile for a device allows the framework to automatically identify and configure Services and Characteristics and provides serialization and deserialization of Characteristic values to and from Strings. The examples in this section are also available in a BlueCap Playground

ServiceConfigurable Protocol

The ServiceConfigurable protocol is used to specify Service configuration and is defined by,

public protocol ServiceConfigurable {
    static var name  : String {get}
    static var uuid  : String {get}
    static var tag   : String {get}
}

Description

name Service name
uuid Service UUID
tag Used to organize services in the BlueCap app profile browser

CharacteristicConfigurable Protocol

The CharacteristicConfigurable is used to specify Characteristic configuration and is defined by,

public protocol CharacteristicConfigurable {
    static var name          : String {get}
    static var uuid          : String {get}
    static var permissions   : CBAttributePermissions {get}
    static var properties    : CBCharacteristicProperties {get}
    static var initialValue  : NSData? {get}
}

Description

name Characteristic name
uuid Characteristic UUID
permissions CBAttributePermissions
properties CBCharacteristicProperties
initialValue Characteristic initial value

StringDeserializable Protocol

The StringDeserializable protocol is used to specify conversion of rawValues to Strings and is defined by,

public protocol StringDeserializable {
    static var stringValues : [String] {get}
    var stringValue         : [String:String] {get}
    init?(stringValue:[String:String])
}

Description

stringValues Used for enums to specify Strings for values but ignored for other types
stringValue The String values of the rawType
init?(stringValue:[String:String]) Create object from stringValue

ConfiguredServiceProfile

A ConfiguredServiceProfile object encapsulates a service configuration and can be used to instantiate either Service or MutableService objects.

struct AccelerometerService : ServiceConfigurable  {
  static let uuid  = "F000AA10-0451-4000-B000-000000000000"
  static let name  = "TI Accelerometer"
  static let tag   = "TI Sensor Tag"
}
let serviceProfile = ConfiguredServiceProfile<AccelerometerService>() 

The CharacteristicProfiles belonging to a ServiceProfile are added using the method,

public func addCharacteristic(characteristicProfile:CharacteristicProfile)

CharacteristicProfile

CharacteristicProfile is the base class for each of the following profile types and is instantiated as the characteristic profile if a profile is not explicitly defined for a discovered Characteristic. In this case, with no String conversions implemented in a GATT Profile definition, a Characteristic will support String conversions to a from hexadecimal Strings.

When defining a GATT profile it is sometimes convenient to specify that something be done after a Characteristic is discovered by a Central.

public func afterDiscovered(capacity:Int?) -> FutureStream<Characteristic>

RawCharacteristicProfile

A RawCharacteristicProfile object encapsulates configuration and String conversions for a Characteristic implementing RawDeserializable. It can be used to instantiate both Characteristic and MutableCharacteristic objects.


enum Enabled : UInt8, RawDeserializable, StringDeserializable, CharacteristicConfigurable {
  case No     = 0
  case Yes    = 1

  // CharacteristicConfigurable
  static let uuid = "F000AA12-0451-4000-B000-000000000000"
  static let name = "Accelerometer Enabled"
  static let properties = CBCharacteristicProperties.Read | CBCharacteristicProperties.Write
  static let permissions = CBAttributePermissions.Readable |

Related Repositories

awesome-swift

awesome-swift

A collaborative list of awesome swift resources. Feel free to contribute! ...

summer-of-swift

summer-of-swift

An ephemeral contest to learn Swift by doing ...

awesome-swift

awesome-swift

A collaborative list of awesome swift resources. Feel free to contribute! ...

awesome-swift

awesome-swift

A collaborative list of awesome swift resources. Feel free to contribute! ...

SwiftBLEWrapper

SwiftBLEWrapper

a Swift BLE wrapper ...


Top Contributors

troystribling cbowns jeffreybergier zittix jessepollak richardlc68 xuyuanme

Releases

-   0.1.0 zip tar