The BlueCap
CentralManager
implementation replaces CBCentralManagerDelegate
and CBPeripheralDelegate
protocol implementations with with a Scala Futures interface using SimpleFutures
. Futures provide an interface for performing nonblocking asynchronous requests and serialization of multiple requests. BlueCap also provides connection management events, scan, discovery and connection timeouts and much more. This section will give example implementations for supported use cases.
- State Change: Detect when the
CBCentralManager
changes state. - Service Scanning: Scan for services.
- Peripheral Advertisements: Access Advertisements of discovered Peripherals.
- Peripheral Connection: Connect to discovered Peripherals.
- Service and Characteristic Discovery: Discover Services and Characteristics of connected Peripherals.
- Characteristic Write: Write a characteristic value to a connected Peripheral.
- Characteristic Read: Read a characteristic value from a connected Peripheral.
- Characteristic Update Notifications: Subscribe to characteristic value updates on a connected Peripheral.
- Retrieve Peripherals: Retrieve
Peripheral
objects cached byCoreBluetooth
. - Peripheral RSSI: Retrieve and poll for RSSI.
- State Restoration: Restore state of
CentralManager
using iOS state restoration. - Errors: Description of all errors.
- Statistics: Peripheral connection statistics.
ManagerState
is a direct mapping to CBManagerState
namely,
public enum ManagerState: CustomStringConvertible {
case unauthorized
case unknown
case unsupported
case resetting
case poweredOff
case poweredOn
case unlikely
}
The state of CBCentralManager
is communicated to an application by the CentralManager
method,
public func whenStateChanges() -> FutureStream<ManagerState>
To process ManagerState
change events use,
let manager = CentralManager(options [CBCentralManagerOptionRestoreIdentifierKey : "us.gnos.BlueCap.documentation-manager" as NSString])
let stateChangeFuture = manager.whenStateChanges()
stateChangeFuture.onSuccess { state in
switch state {
case .poweredOn:
break
case .poweredOff, .unauthorized:
break
case .resetting:
break
case .unknown:
break
case .unsupported:
break
}
}
Scans for advertising peripherals are initiated by calling the CentralManager
methods,
// Scan promiscuously for all advertising peripherals
public func startScanning(capacity: Int = Int.max, timeout: TimeInterval = TimeInterval.infinity, options: [String : Any]? = nil) -> FutureStream<Peripheral>
// Scan for peripherals advertising services with UUIDs
public func startScanning(forServiceUUIDs uuids: [CBUUID]?, capacity: Int = Int.max, timeout: TimeInterval = TimeInterval.infinity, options: [String : Any]? = nil) -> FutureStream<Peripheral>
Both methods return a SimpleFutures FutureStream<Peripheral>
yielding the discovered Peripheral
.
The input parameters for both methods are,
uuids | Scanned service UUIDs. |
capacity | FutureStream capacity. The default value is infinite. |
timeout | Scan timeout. The default value is infinite. The error CentralManagerError.peripheralScanTimeout is thrown and scanning stops if nothing is discovered within the timeout. |
options | See CBCentralManager scanning options. |
An application starts scanning for Peripherals
advertising Services
with UUIDs
after power on with the following,
public enum AppError : Error {
case unauthorized
case unknown
case unsupported
case resetting
case poweredOff
case poweredOn
case unlikely
}
let manager = CentralManager(options [CBCentralManagerOptionRestoreIdentifierKey : "us.gnos.BlueCap.documentation-manager" as NSString])
let serviceUUID = CBUUID(string: TISensorTag.AccelerometerService.uuid)
Var discoveredPeripheral: Peripheral?
let scanFuture = manager.whenStateChanges().flatMap { [weak manager] state -> FutureStream<Peripheral> in
guard let manager = manager else {
throw AppError.unlikely
}
switch state {
case .poweredOn:
return manager.startScanning(forServiceUUIDs: [serviceUUID], capacity: 10)
case .poweredOff:
throw AppError.poweredOff
case .unauthorized, .unsupported:
throw AppError.invalidState
case .resetting:
throw AppError.resetting
case .unknown:
throw AppError.unknown
}
}
scanFuture.onSuccess { peripheral in
discoveredPeripheral = peripheral
}
scanFuture.onFailure { [weak manager] error in
guard let manager = manager, let appError = error as? else {
return
}
switch appError {
case AppError.invalidState:
manager.stopScanning()
break
case AppError.resetting:
manager.stopScanning()
manager.reset()
case AppError.poweredOff:
manager.stopScanning()
break
case AppError.unknown:
manager.stopScanning()
break
case default:
break
}
}
Here the scan is started when CentralManager
transitions to .powerOn
.
Also, An error was added to handle CentralManager
state transitions other than .powerOn
. CentralManager#reset
is used to recreate CBCentralManager
.
To stop a peripheral scan use the CentralManager
method,
public func stopScanning()
Peripheral
advertisements are encapsulated by the PeripheralAdvertisements
struct
defined by,
public struct PeripheralAdvertisements {
// Local peripheral name with key CBAdvertisementDataLocalNameKey
public var localName: String?
// Manufacture data with key CBAdvertisementDataManufacturerDataKey
public var manufactuereData: Data?
// Tx power with with key CBAdvertisementDataTxPowerLevelKey
public var txPower: NSNumber?
// Is connectable with key CBAdvertisementDataIsConnectable
public var isConnectable: NSNumber?
// Advertised service UUIDs with key CBAdvertisementDataServiceUUIDsKey
public var serviceUUIDs: [CBUUID]?
// Advertised service data with key CBAdvertisementDataServiceDataKey
public var serviceData: [CBUUID : Data]?
// Advertised overflow services with key CBAdvertisementDataOverflowServiceUUIDsKey
public var overflowServiceUUIDs: [CBUUID]?
// Advertised solicited services with key CBAdvertisementDataSolicitedServiceUUIDsKey
public var solicitedServiceUUIDs: [CBUUID]?
}
The PeripheralAdvertisements
struct
is accessible through the property Peripheral#advertisements
.
public let advertisements: PeripheralAdvertisements
After discovering a Peripheral
a connection must be established to run discovery and 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
Peripherals
have a configurable connection attempt timeout and errors indicating force disconnect, connection attempt timeout and formatting of CoreBluetooth
disconnection and errors.
To connect to a Peripheral
use The Peripheral
method,
public func connect(connectionTimeout: TimeInterval = TimeInterval.infinity, capacity: Int = Int.max) -> FutureStream<Void>
The method returns a SimpleFutures FutureStream<Void>
.
The input parameters are,
connectionTimeout | Connection timeout in seconds. The default value is infinite. If the timeout is exceeded PeripheralError.connectionTimeout is thrown. |
capacity | FutureStream capacity. The default value is infinite. |
// Reconnect with specified delay
public func reconnect(withDelay delay: Double = 0.0)
// Force disconnect from peripheral
public func disconnect()
// Disconnect from peripheral and remove it from application
// cache
public func terminate()
The Peripheral#reconnect
method is used to establish a connection to a previously connected Peripheral
. The method takes a single parameter reconnectDelay
used to specify a delay, in seconds, before trying to reconnect. The default value is 0.0
seconds. If it is called before Peripheral#connect
a connection with default parameters will be attempted.
Peripheral#disconnect
preforms and immediate disconnection from the connected Peripheral
and throws
PeripheralError.forcedDisconnect
.
Peripheral#terminate
performs a Peripheral#disconnect
and also removes the Peripheral
from the application cache.
After a Peripheral
is discovered an application connects using,
let connectionFuture = scanFuture.flatMap { [weak manager] () -> FutureStream<Void> in
manager?.stopScanning()
discoveredPeripheral = peripheral
return peripheral.connect(timeoutRetries:5, disconnectRetries:5, connectionTimeout: 10.0)
}
connectionFuture.onSuccess { [weak peripheral] in
// handle successful connection
}
connectionFuture.onFailure { error in
switch error {
case PeripheralError.disconnected:
discoveredPeripheral.reconnect()
break
case PeripheralError.forcedDisconnect:
break
case PeripheralError.connectionTimeout:
break
default:
break
}
}
Here the scanFuture
is completed after Peripheral
discovery and flatMap
combines it with the connection FutureStream
. This ensures that connections are made after Peripherals
are discovered. Connection errors are handled in onFailure
. A reconnection attempt is made on PeripheralError.disconnected
.
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.
The Peripheral
methods used to discover Services
are,
// Discover all services supported by peripheral
public func discoverAllServices(timeout: TimeInterval = TimeInterval.infinity) -> Future<Void>
// Discover services with specified UUIDs
public func discoverServices(_ services: [CBUUID]?, timeout: TimeInterval = TimeInterval.infinity) -> Future<Void>
Both methods return a SimpleFutures Future<Void>
and only take a timeout
parameter. If the timeout is exceeded the PeripheralError.serviceDiscoveryTimeout
is thrown.
The Service
methods used to discover Characteristics
are,
// Discover all characteristics supported by service
public func discoverAllCharacteristics(timeout: TimeInterval = TimeInterval.infinity) -> Future<Void>
// Discover characteristics with specified UUIDs
public func discoverCharacteristics(_ characteristics: [CBUUID], timeout: TimeInterval = TimeInterval.infinity) -> Future<Void>
Both methods return a SimpleFutures Future<Void>
and only take a timeout
parameter. If the timeout is exceeded ServiceError.characteristicDiscoveryTimeout
is thrown.
After a Peripheral
is connected Services
and Characteristics
are discovered using,
public enum AppError : Error {
case serviceNotFound
}
let serviceUUID = CBUUID(string: TISensorTag.AccelerometerService.uuid)
let characteristicUUID = CBUUID(string: TISensorTag.AccelerometerService.Data.uuid)
let discoveryFuture = connectionFuture.flatMap { [weak peripheral] () -> Future<Void> in
guard let peripheral = peripheral else {
throw AppError.unlikely
}
peripheral.discoverServices([serviceUUID])
}.flatMap { [peripheral weak] -> Future<Void> in
guard let peripheral = peripheral, let service = peripheral.service.characteristics(withUUID: serviceUUID)?.first else {
throw AppError.serviceNotFound
}
return service.discoverCharacteristics([dataUUID])
}
discoveryFuture.onFailure { error in
switch error {
case PeripheralError.disconnected:
break
case PeripheralError.forcedDisconnect:
break
case PeripheralError.connectionTimeout:
break
case PeripheralError.serviceDiscoveryTimeout:
break
case ServiceError.characteristicDiscoveryTimeout:
break
case AppError.serviceNotFound:
break
default:
break
}
}
Here the connectionFuture
is completed after the Peripheral
connects and flatMap
combines it with the Service
discovery Future
and another flatMap
combines with Characteristic
discovery.
Discovery of all supported Peripheral
Services
and Characteristics
could be done in a single flatMap
using,
let discoveryFuture = connectionFuture.flatMap { [weak peripheral] -> Future<[Void]> in
guard let peripheral = peripheral else {
throw AppError.unlikely
}
peripheral.services.map { $0.discoverAllCharacteristics() }.sequence()
}
Here the SimpleFutures Future#sequence
method is used to create a Future
that completes when all Characteristic
discovery tasks complete.
After Peripheral
Characteristics
are discovered writing Characteristic
values is possible. Characteristic
methods available for writing, where each supports a value of a different type,
// Write an Data object to characteristic value
public func write(data value: Data, timeout: TimeInterval = TimeInterval.infinity, type: CBCharacteristicWriteType = .withResponse) -> Future<Void>
// Write a characteristic String Dictionary value
public func write(string stringValue: [String: String], timeout: TimeInterval = TimeInterval.infinity, type: CBCharacteristicWriteType = .withResponse) -> Future<Void>
// Write a Deserializable characteristic value
public func write<T: Deserializable>(_ value: T, timeout: TimeInterval = TimeInterval.infinity, type: CBCharacteristicWriteType = .withResponse) -> Future<Void>
// Write a RawDeserializable characteristic value
public func write<T: RawDeserializable>(_ value: T, timeout: TimeInterval = TimeInterval.infinity, type: CBCharacteristicWriteType = .withResponse) -> Future<Void>
// Write a RawArrayDeserializable characteristic value
public func write<T: RawArrayDeserializable>(_ value: T, timeout: TimeInterval = TimeInterval.infinity, type: CBCharacteristicWriteType = .withResponse) -> Future<Void>
// Write a RawPairDeserializable characteristic value
public func write<T: RawPairDeserializable>(_ value: T, timeout: TimeInterval = TimeInterval.infinity, type: CBCharacteristicWriteType = .withResponse) -> Future<Void>
// Write a RawArrayPairDeserializable characteristic value
public func write<T: RawArrayPairDeserializable>(_ value: T, timeout: TimeInterval = TimeInterval.infinity, type: CBCharacteristicWriteType = .withResponse) -> Future<Void>
Each of the write
method takes a writable input of a different type. The other parameters are the same. Each returns a SimpleFutures Future<Void>
,
The input parameters are,
timeout | Write timeout in seconds. The default value is infinite. If timeout is exceeded CharacteristicError.writeTimeout is shown. |
type | Characteristic write types, see CBCharacteristicWriteType type, The default value is .WithResponse. |
Using the RawDeserializable enum an application can write a Characteristic
when a connected Peripheral
is available and Services
and Characteristics
are discovered,
let writeFuture = characteristic.write(Enabled.yes)
Here the characteristic
is assumed to belong to a connected Peripheral
. This could also be part of a flatMap
chain,
public enum AppError : Error {
case unlikely
}
let writeFuture = discoveryFuture.flatMap { [weak characteristic] () -> Future<Void> in
guard let let characteristic = characteristic else {
throw AppError.unlikely
}
return characteristic.write(Enabled.yes)
}
Here discoveryFuture
is completed after Characteristic
discovery and ``flatMapis used to combine with
Characteristic#write`.
After Peripheral
Characteristics
are discovered reading Characteristic
values is possible. Characteristic
provides the following method to retrieve values from connected Peripherals
,
// Read a characteristic from a peripheral service
public func read(timeout: TimeInterval = TimeInterval.infinity) -> Future<Void>
The read
method takes a single input parameter, used to specify the timeout. The default value for timeout
is infinite. If the timeout is exceeded CharacteristicError.readTimout
is thrown. read
returns a SimpleFutures Future<Void>
.
To retrieve the Characteristic
value after a successful read the following methods are available. Each returns values a different type,
// Return the characteristic value as and NSData object
public var dataValue: Data?
// 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>() -> T? where T.RawType: Deserializable
// Return a RawArrayDeserializable characteristic value
public func value<T: RawArrayDeserializable>() -> T? where T.RawType: Deserializable
// Return a RawPairDeserializable characteristic value
public func value<T: RawPairDeserializable>() -> T? where T.RawType1: Deserializable, T.RawType2: Deserializable
Using the RawDeserializable enum an application can read a Characteristic
after connecting to a Peripheral
and running Service
and Characteristic
discovery with the following,
let readFuture = characteristic.read()
Here the characteristic
is assumed to belong to a connected Peripheral
. This could also be part of a flatMap
chain,
let readFuture = discoveryFuture.flatMap { [weak characteristic] () -> Future<Void> in
return characteristic.read()
}
Here discoveryFuture
is completed after Characteristic
discovery and flatMap
is used to combine with Characteristic#read
.
After Peripheral
Characteristics
are discovered subscribing to Characteristic
value update notifications is possible. Several Characteristic
methods are available,
// Subscribe to characteristic update
public func startNotifying() -> Future<Void>
// Receive characteristic value updates
public func receiveNotificationUpdates(capacity: Int = Int.max) -> FutureStream<Data)>
// Unsubscribe from characteristic updates
public func stopNotifying() -> Future<Void>
// Stop receiving characteristic value updates
public func stopNotificationUpdates()
The work flow for receiving notification updates is to first subscribe to the notifications using Characteristic#startNotifying
. The application will then start receiving notifications. To process the notifications call Characteristic#receiveNotificationUpdates
which returns a SimpleFutures FutureStream<Data?>
from which the updated Characteristic
value can be obtained.
To stop processing notifications call Characteristic#stopNotificationUpdates
and to unsubscribe to notifications call Characteristic#stopNotifying
.
Using the RawDeserializable enum an application can receive notifications form a Characteristic
after connecting to a Peripheral
and running Service
and Characteristic
discovery with the following,
let notificationFuture = characteristics.startNotifying().flatMap { [weak characteristic] () -> FutureStream<Data?> in
guard let let characteristic = characteristic else {
throw AppError.unlikely
}
characteristic.receiveNotificationUpdates(capacity: 10)
}
An application can unsubscribe to Characteristic
value notifications and stop receiving updates by using the following,
characteristic.stopNotificationUpdates()
characteristic.stopNotifying()
Discovered Peripherals
can be retrieved from the system cache using the following CentralManager
methods,
// Retrieve the connected peripherals with specified service UUIDs
public func retrieveConnectedPeripherals(withServices services: [CBUUID]) -> [Peripheral]
// Retrieve peripherals with UUIDs
public func retrievePeripherals(withIdentifiers identifiers: [UUID]) -> [Peripheral]
// Retrive peripherals using framework cached peripheral UUIDs
public func retrievePeripherals() -> [Peripheral]
Each of these methods will repopulate the BlueCap framework cache with the retrieved peripherals overwriting any that collide.
An application would populate the framework cache from the system cache with,
let manager = CentralManager(options [CBCentralManagerOptionRestoreIdentifierKey : "us.gnos.BlueCap.documentation-manager" as NSString])
let serviceUUID = CBUUID(string: TISensorTag.AccelerometerService.uuid)
let peripherals = central.retrieveConnectedPeripherals([serviceUUID])
Peripheral
provides the following methods to retrieve RSSI,
// read current RSSI
public func readRSSI() -> Future<Int>
// Start polling RSSI at the specified period
public func startPollingRSSI(period: Double = 10.0, capacity: Int = Int.max) -> FutureStream<Int>
// Stop polling RSSI
public func stopPollingRSSI()
CoreBluetooth provides state restoration for apps that have declared bluetooth-central
background execution permission. Apps with this permission can be restarted with a previous state if evicted from memory while in the background.
CentralManager
provides the following method to process the restored application state,
public func whenStateRestored() -> Future<Void>
public enum CharacteristicError : Swift.Error {
// Thrown by read when timeout is exceeded
case readTimeout.
// Thrown by write when timeout is exceeded
case writeTimeout.
// Thrown by write if given string cannot be serialized
case notSerializable.
// Thrown by read if Characteristic read property is not enabled
case readNotSupported
// Thrown by write if Characteristic read property is not enabled.
case writeNotSupported
// Thrown by startNotifying if Characteristic notifiy or indicate property is not enabled.
case notifyNotSupported
// Characteristic needs to be added to a PeripheralManager.
case unconfigured
}
public enum PeripheralError : Swift.Error {
// Thrown by any method that requires a Peripheral be connected if the peripheral is not connected or is disconnected.
case disconnected
// Peripheral was disconnected by application.
case forcedDisconnect
// Thrown by Peripheral connect when the specified timeout is exceeded.
case connectionTimeout
// Thrown by discoverAllServices and discoverServices if service discovery timeout is exceeded.
case serviceDiscoveryTimeout
}
public enum CentralManagerError : Swift.Error {
case isScanning
// Thrown by startScanning if scan is started and CentralManager is poweredOff.
case isPoweredOff
// Thrown on state restoration failure.
case restoreFailed
// Thrown by startScanning if scan timeout is exceeded.
case serviceScanTimeout
}
public enum ServiceError : Swift.Error {
// Thrown by discoverAllCharcteristics and discoverCharcteristics if service discovery timeout is exceeded.
case characteristicDiscoveryTimeout
// Service has no associated Peripheral.
case unconfigured
}
Peripheral
provides the following properties to monitor performance,
discoveredAt: Date | Date of discovery. |
connectedAt: Date | Date of last connection. |
disconnectedAt: Date | Date of last disconnection |
timeoutCount: UInt | Number of connection timeouts. |
disconnectionCount: UInt | Number of disconnections |
connectionCount: UInt | Number of successful connections. |
secondsConnected: TimeInterval | Seconds of current connection if Peripheral is connected or seconds of last connection if Peripheral is disconnected. |
totalSecondsConnected: TimeInterval | Total seconds since discovery has been connected excluding current connection if connected. |
cumlativeSecondsConnected: TimeInterval | Total seconds since discovery has been connected including the current connection if connected. |
cumlativeSecondsDisconnected: TimeInterval | Total seconds since discovery disconnected. |