BlueCap provides a swift wrapper around CoreBluetooth and much more.
- A futures interface replacing protocol implementations.
- Connection events for connect, disconnect and timeout.
- Service scan timeout.
- Characteristic read/write timeout.
- A framework 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.
- Comprehensive test coverage.
- iOS 8.0+
- Xcode 6.3+
- Place the BlueCap somewhere in your project directory. You can either copy it or add it as a git submodule.
- Open the BluCap project folder and drag BlueCapKit.xcodeproj into the project navigator of your applications Xcode project.
- Under your Projects Info tab set the iOS Deployment Target to 8.0 and that the BlueCapKit.xcodeproj iOS Deployment Target is also 8.0.
- Under the General tab for your project target add the top BlueCapKit.framework as an Embedded Binary.
- Under the Build Phases tab add BlueCapKit.framework as a Target Dependency and under Link Binary With Libraries add CoreLocation.framework and CoreBluetooth.framework.
- To enable debug log output select your project target and the Build Settings tab. Under Other Swift Flags and Debug add -D DEBUG.
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 following sections will address each of these items in some detail. Example applications are also available.
Communication in BLE uses the Client-Server model. The Client is called a Central and the server a Peripheral. The Peripheral presents structured data to the Central organized into Services that have Characteristics. This called is a Generic Attribute Table (GATT) Profile. Characteristics contain the data values as well as attributes describing permissions and properties. At a high level Central and Peripheral use cases are,
Central | Peripheral |
---|---|
Scans for Services | Advertises Services |
Discovers Services and Characteristics | Supports GATT Profile |
Data Read/Write/Notifications | Data Read/Write/Notifications |
Serialization and deserialization of device messages requires protocol implementations. Then application objects can be converted to and from NSData objects using methods on Serde. This section will describe how this is done. Example implantations of each protocol can be found in the Ti Sensor Tag GATT profile available in BlueCapKit and the following examples are implemented in a BlueCap Playground.
For Strings Serde serialize and deserialize are defined by,
public static func deserialize(data:NSData, encoding:NSStringEncoding = NSUTF8StringEncoding) -> String?
public static func serialize(value:String, encoding:NSStringEncoding = NSUTF8StringEncoding) -> NSData?
NSStringEncoding supports many encodings.
to use,
if let data = Serde.serialize("Test") {
if let value = Serde.deserialize(data) {
println(value)
}
}
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,
public static func deserialize<T:Deserializable>(data:NSData) -> T?
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)")
}
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,
public static func deserialize<T:RawDeserializable where T.RawType:Deserializable>(data:NSData) -> T?
public static func serialize<T:RawDeserializable>(value:T) -> NSData
Note that RawType is required to be Deserializable. 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)”)
}
}
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,
public static func deserialize<T:RawArrayDeserializable where T.RawType:Deserializable>(data:NSData) -> T?
public static func serialize<T:RawArrayDeserializable>(value:T) -> NSData
Note that RawType is required to be Deserializable. 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)")
}
}
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,
public static func deserialize<T:RawPairDeserializable where T.RawType1:Deserializable, T.RawType2:Deserializable>(data:NSData) -> T?
public static func serialize<T:RawPairDeserializable>(value:T) -> NSData
Note that RawType1 and RawType2 are required to be Deserializable. 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)")
}
}
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,
public static func deserialize<T:RawArrayPairDeserializable where T.RawType1:Deserializable, T.RawType2:Deserializable>(data:NSData) -> T?
public static func serialize<T:RawArrayPairDeserializable>(value:T) -> NSData
Note that RawType1 and RawType2 are required to be Deserializable. 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 definitions are required to add support for a device to the BlueCap app but are not required 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
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 |
The CharacteristicConfigurable is used to specify Characteristic configuration and protocol 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 |
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 |
A ConfiguredServiceProfile object encapsulates a service configuration that cab be used to instantiate either Service of MutableService object.
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 CharacteristicProfiless belonging to a ServiceProfile are added using the method,
public func addCharacteristic(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 GATT profile it is sometimes convenient to specify that something be done to the Peripheral after a characteristic is discovered by the Central.
public func afterDiscovered(capacity:Int?) -> FutureStream<Characteristic>
A RawCharacteristicProfile object encapsulates configuration and String conversions for a characteristic implementing RawDeserializable. It can be used to instantiate both Characteristics and Mutable Characteristics.
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 | CBAttributePermissions.Writeable
static let initialValue : NSData? = Serde.serialize(Enabled.No.rawValue)
// StringDeserializable
static let stringValues = ["No", "Yes"]
init?(stringValue:[String:String]) {
if let value = stringValue[Enabled.name] {
switch value {
case "Yes":
self = Enabled.Yes
case "No":
self = Enabled.No
default:
return nil
}
} else {
return nil
}
}
var stringValue : [String:String] {
switch self {
case .No:
return [Enabled.name:"No"]
case .Yes:
return [Enabled.name:"Yes"]
}
}
}
To instantiate a profile,
let profile = RawCharacteristicProfile<Enabled>()
A RawArrayCharacteristicProfile object encapsulates configuration and String conversions for a characteristic implementing RawArrayDeserializable. It can be used to instantiate both Characteristics and Mutable Characteristics.
struct ArrayData : RawArrayDeserializable, CharacteristicConfigurable, StringDeserializable {
// CharacteristicConfigurable
static let uuid = "F000AA11-0451-4000-B000-000000000000"
static let name = "Accelerometer Data"
static let properties = CBCharacteristicProperties.Read | CBCharacteristicProperties.Notify
static let permissions = CBAttributePermissions.Readable | CBAttributePermissions.Writeable
static let initialValue : NSData? = Serde.serialize(ArrayData(rawValue:[1,2])!)
// RawArrayDeserializable
let rawValue : [Int8]
static let size = 2
init?(rawValue:[Int8]) {
if rawValue.count == 2 {
self.rawValue = rawValue
} else {
return nil
}
}
// StringDeserializable
static let stringValues = [String]()
var stringValue : Dictionary<String,String> {
return ["value1":"\(self.rawValue[0])",
"value2":"\(self.rawValue[1])"]
}
init?(stringValue:[String:String]) {
if let stringValue1 = stringValue["value1"],
stringValue2 = stringValue["value2"],
value1 = Int8(stringValue:stringValue1),
value2 = Int8(stringValue:stringValue2) {
self.rawValue = [value1, value2]
} else {
return nil
}
}
}
To instantiate a profile,
let profile = RawArrayCharacteristicProfile<ArrayData>()
A RawPairCharacteristicProfile object encapsulates configuration and String conversions for a characteristic implementing RawPairDeserializable. It can be used to instantiate both Characteristics and Mutable Characteristics.
struct PairData : RawPairDeserializable, CharacteristicConfigurable, StringDeserializable {
// CharacteristicConfigurable
static let uuid = "F000AA30-0451-4000-B000-000000000000"
static let name = "Magnetometer Data"
static let properties = CBCharacteristicProperties.Read | CBCharacteristicProperties.Notify
static let permissions = CBAttributePermissions.Readable | CBAttributePermissions.Writeable
static let initialValue : NSData? = Serde.serialize(PairData(rawValue1:10, rawValue2:-10)!)
// RawArrayDeserializable
let rawValue1 : UInt8
let rawValue2 : Int8
init?(rawValue1:UInt8, rawValue2:Int8) {
self.rawValue1 = rawValue1
self.rawValue2 = rawValue2
}
// StringDeserializable
static let stringValues = [String]()
var stringValue : Dictionary<String,String> {
return ["value1":"\(self.rawValue1)",
"value2":"\(self.rawValue2)"]}
init?(stringValue:[String:String]) {
if let stringValue1 = stringValue["value1"],
stringValue2 = stringValue["value2"],
value1 = UInt8(stringValue:stringValue1),
value2 = Int8(stringValue:stringValue2) {
self.rawValue1 = value1
self.rawValue2 = value2
} else {
return nil
}
}
}
To instantiate a profile,
let profile = RawPairCharacteristicProfile<PairData>()
A RawArrayPairCharacteristicProfile object encapsulates configuration and String conversions for a characteristic implementing RawArrayPairDeserializable. It can be used to instantiate both Characteristics and Mutable Characteristics.
struct ArrayPairData : RawArrayPairDeserializable, CharacteristicConfigurable, StringDeserializable {
// CharacteristicConfigurable
static let uuid = "F000AA11-0451-4000-B000-000000000000"
static let name = "Accelerometer Data"
static let properties = CBCharacteristicProperties.Read | CBCharacteristicProperties.Notify
static let permissions = CBAttributePermissions.Readable | CBAttributePermissions.Writeable
static let initialValue : NSData? = Serde.serialize()
// RawArrayDeserializable
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
}
}
// StringDeserializable
static let stringValues = [String]()
var stringValue : Dictionary<String,String> {
return ["value11":"\(self.rawValue1[0])",
"value12":"\(self.rawValue1[1])"],
"value21":"\(self.rawValue2[0])",
"value22":"\(self.rawValue2[1])"]}
init?(stringValue:[String:String]) {
if let stringValue11 = stringValue["value11"],
stringValue12 = stringValue["value12"]
value11 = Int8(stringValue:stringValue11),
value12 = Int8(stringValue:stringValue12),
stringValue21 = stringValue["value21"],
stringValue22 = stringValue["value22"]
value21 = Int8(stringValue:stringValue21),
value22 = Int8(stringValue:stringValue22) {
self.rawValue1 = [value11, value12]
self.rawValue2 = [value21, value22]
} else {
return nil
}
}
}
To instantiate a profile,
let profile = RawArrayPairCharacteristicProfile<ArrayPairData>()
A String Profile only requires the implementation of characteristic
struct SerialNumber : CharacteristicConfigurable {
// CharacteristicConfigurable
static let uuid = "2a25"
static let name = "Device Serial Number"
static let permissions = CBAttributePermissions.Readable | CBAttributePermissions.Writeable
static let properties = CBCharacteristicProperties.Read
static let initialValue = Serde.serialize("AAA11")
}
To instantiate a profile,
let profile = StringCharacteristicProfile<SerialNumber>()
ProfileManager is used by the BlueCap app as a repository of GATT profiles to be used to instantiate Services and Characteristics. ProfileManager can be used in an implementation but is not required by the framework.
To add ServiceProfiles and CharacteristicProfiles to ProfileManager,
let profileManager = ProfileManager.sharedInstance
let serviceProfile = ConfiguredServiceProfile<AccelerometerService>()
let enabledProfile = RawCharacteristicProfile<Enabled>()
let rawArrayProfile = RawArrayCharacteristicProfile<ArrayData>()
serviceProfile.addCharacteristic(enabledProfile)
serviceProfile.addCharacteristic(rawArrayProfile)
profileManager.addService(serviceProfile)
To add a GATT Profile to the BlueCap app you need to add a file to the project containing all Service and Characteristic profile definitions with public access level. See GnosusProfiles in the BlueCap Project fro an example. A very simple but illustrative example is to consider a Service with a single Characteristic.
public struct MyServices {
// Service
public struct NumberService : ServiceConfigurable {
public static let uuid = "F000AA10-0451-4000-B000-000000000000"
public static let name = "NumberService"
public static let tag = "My Services"
}
// Characteristic
public struct Number : RawDeserializable, StringDeserializable, CharacteristicConfigurable {
public let rawValue : Int16
public init?(rawValue:Int16) {
self.rawValue = rawValue
}
public static let uuid = "F000AA12-0451-4000-B000-000000000000"
public static let name = "Number"
public static let properties = CBCharacteristicProperties.Read | CBCharacteristicProperties.Write
public static let permissions = CBAttributePermissions.Readable | CBAttributePermissions.Writeable
public static let initialValue : NSData? = Serde.serialize(Int16(22))
public static let stringValues = [String]()
public init?(stringValue:[String:String]) {
if let svalue = stringValue[Number.name], value = Int16(stringValue:svalue) {
self.rawValue = value
} else {
return nil
}
}
public var stringValue : [String:String] {
return [Number.name:"\(self.rawValue)"]
}
}
// add to ProfileManager
public static func create() {
let profileManager = ProfileManager.sharedInstance
let service = ConfiguredServiceProfile<NumberService>()
let characteristic = RawCharacteristicProfile<Number>()
service.addCharacteristic(characteristic)
profileManager.addService(service)
}
}
Next place,
MyServices.create()
in the BlueCap AppDelegate.swift and rebuild the app.
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 connections 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.
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 yielding 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.
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 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()
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 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()
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 BlueCap 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.
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. To discover both BlueCap Services and Characteristics use,
// Discover services and characteristics for services with UUIDs
public func discoverPeripheralServices(services:[CBUUID]!) -> Future<Peripheral>
// Discover all services and characteristics
public func discoverAllPeripheralServices() -> Future<Peripheral>
Both methods return a SimpleFutures Future<Peripheral> yielding the connected peripheral.
An application can discover a BlueCap 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 to ensures 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.
After a peripherals characteristics are discovered writing characteristic values is possible. Several 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 func write<T:RawDeserializable>(value:T, timeout:Double) -> Future to ensure that characteristic has been discovered before writing. An error is returned if the characteristic is not found.
After a peripherals characteristics are discovered reading characteristic values is possible. Several 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 to ensure that characteristic has been discovered before reading. An error is returned if the characteristic is not found.
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 recieveNotificationUpdates(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.recieveNotificationUpdates(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 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 recieveNotificationUpdates(capacity:Int?) -> FutureStream 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()
}
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.
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.
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. 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,
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"
}
let service = MutableService(uuid:serviceUUID)
let characteristic = MutableCharacteristic(uuid:Enabled.uuid, properties:CBCharacteristicProperties.Read, permissions:CBAttributePermissions.Readable|CBAttributePermissions.Writeable, value:Serde.serialize(Enabled.No)!))
service.characteristics = [characteristic]
let manager = PeripheralManager.sharedInstance
let startAdvertiseFuture = manager.powerOn().flatmap {_ -> Future<Void> in
manager.removeAllServices()
}.flatmap {_ -> Future<Void> in
manager.addService(service)
}
First BlueCap MutableServices and MutableCharacteristics are created and the characteristic is added to the service. Then the BlueCap PeripheralManager powerOn() is flatmapped to removeAllServices() which is then flatmapped to addService(). This sequence ensures that the peripheral is powered and with no services before the new services are added.