Skip to content

Latest commit

 

History

History
397 lines (303 loc) · 15.3 KB

PeripheralManager.md

File metadata and controls

397 lines (303 loc) · 15.3 KB

PeripheralManager

The BlueCap PeripheralManager implementation replaces CBPeripheralManagerDelegate protocol implementations with with a Scala Futures interface using SimpleFutures. Futures provide an interface for performing nonblocking asynchronous requests and serialization of multiple requests. This section will give example implementations for supported use cases.

Contents

ManagerState is a direct mapping to CBManagerState namely,

public enum ManagerState: CustomStringConvertible {
    case unauthorized
    case unknown
    case unsupported
    case resetting
    case poweredOff
    case poweredOn
}

The state of CBPeripheralManager is communicated to an application by the PeripheralManager method,

public func whenStateChanges() -> FutureStream<ManagerState>

To process events,

let manager = PeripheralManager(options: [CBPeripheralManagerOptionRestoreIdentifierKey : "us.gnos.BlueCap.peripheral-manager-documentation" 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
    }
}

Services and Characteristics are added to a PeripheralManager application before advertising.

PeripheralManager provides the following methods used for managing Services are,

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

// remove a service
public func remove(_ service: MutableService)

// remove all services
public func removeAllServices()

MutableService provides the methods for adding MutableCharacteristics,

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

// create characteristics from profiles
public func characteristicsFromProfiles()

A PeripheralManager application adds MutableServices and MutableCharacteristics using,

enum AppError: Error {
    case invalidState
    case resetting
    case poweredOff
    case unsupported
    case unlikely
}

let manager = PeripheralManager(options: [CBPeripheralManagerOptionRestoreIdentifierKey : "us.gnos.BlueCap.peripheral-manager-documentation" as NSString])

let service = MutableService(uuid: TISensorTag.AccelerometerService.uuid)

let characteristic = MutableCharacteristic(profile: RawArrayCharacteristicProfile<TISensorTag.AccelerometerService.Data>())

// Add Characteristic to Service
service.characteristics = [characteristic]
 
let addServiceFuture = manager.whenStateChanges().flatMap { state -> Future<Void> in
    guard let manager = manager else {
        throw AppError.unlikely
    }    
    switch state {
    case .poweredOn:
        manager.removeAllServices()
        return manager.add(self.accelerometerService)
    case .poweredOff:
        throw AppError.poweredOff
    case .unauthorized, .unknown:
        throw AppError.invalidState
    case .unsupported:
        throw AppError.unsupported
    case .resetting:
        throw AppError.resetting
    }
}

startAdvertiseFuture.onFailure { error in
    guard let manager = manager else {
        return
    }    
    switch error {
    case AppError.poweredOff:
        manager.reset()
    case AppError.resetting:
        manager.reset()
    case AppError.unsupported:
        break
    default:
        manager.reset()
    }
}

First MutableServices and MutableCharacteristics are created using the TISensorTag.AccelerometerService profile. The Characteristic is then added to the Service and when .powerOn is received existing services are removed and the new 'Service` added.

Also, An error was added to handle PeripheralManager state transitions other than .powerOn and PeripheralManager#reset is used to recreate CBPripheralManager.

After Services and Characteristics have been added the PeripheralManager is ready to begin advertising. PeripheralManager provides the following methods to manage advertisement,

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

// stop advertising
public func stopAdvertising(timeout: TimeInterval = 10.0) -> Future<Void>

A PeripheralManager application can start advertising after MutableServices and MutableCharacteristics are added,

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

let startAdvertisingFuture = addServiceFuture.flatMap { _ -> Future<Void> in 
	  guard let manager = manager else {
        throw AppError.unlikely
    }    
	manager.startAdvertising(TISensorTag.AccelerometerService.name, uuids: [serviceUUID])
}

Here the addServiceFuture is completed after Services are added and PeripheralManager is advertising that it supports the TISensorTag.AccelerometerService.

A MutableCharacteristic value can be set any time after its supporting MutableService has been successfully added to PeripheralManager. The value is defined by,

var value : NSData? {get set}

A PeripheralManager application can set a MutableCharacteristic value using,

characteristic.value = Serde.serialize(Enabled.yes)

guard let value: Enabled = characteristic.value {
    return
}

If a MutableCharacteristic supports either CBCharacteristicProperties of CBCharacteristicProperties.notify, CBCharacteristicProperties.indicate, CBCharacteristicProperties.notifyEncryptionRequired, or CBCharacteristicProperties.indicateEncryptionRequired and a MutableService supporting the MutableCharacteristic have been successfully added to PeripheralManager a Central can subscribe to value updates. In addition to setting the new value an update notification must be sent. MutableCharacteristic provides the following methods to support notification updates,

// update with Data
public func update(withData value: Data) throws

// update with String
public func update(withString value: [String:String]) throws

// update with object supporting Deserializable
public func update<T: Deserializable>(_ value: T) throws {

// update with object supporting RawDeserializable
public func update<T: RawDeserializable>(_ value: T) throws

// update with object supporting RawArrayDeserializable
public func update<T: RawArrayDeserializable>(_ value: T) throws

// update with object supporting RawPairDeserializable
public func update<T: RawPairDeserializable>(_ value: T) throws

// update with object supporting RawArrayPairDeserializable
public func update<T: RawArrayPairDeserializable>(_ value: T) throws

All methods throw if the MutableCharacteristic either has not been added to PeripheralManager or supports none of the notify CBCharacteristicProperties. Additionally update(withString:) will throw if the String value cannot be serialized. If the value is updated and there are no subscribers or the system CoreBluetooth update fails the update will be queued and sent when a Central subscribes or the system indicates that updates can continue. In addition to sending an update notification to a subscribing Central update sets the MutabaleCharacteristic value.

Peripheral applications would send notification updates using,

let updateStatus = try characteristic.updateValue(Enabled.no)

If a MutableCharacteristic supports CBCharacteristicProperties.write a Central can change the MutableCharacteristic value. MutableCharacteristic supports the following methods supporting write requests,

// start processing write requests with specified stream capacity
public func startRespondingToWriteRequests(capacity: Int = Int.max) -> FutureStream<(request: CBATTRequestInjectable, central: CBCentralInjectable)>

// respond to received write request
public func respondToRequest(_ request: CBATTRequestInjectable, withResult result: CBATTError.Code)

// stop processing write requests
public func stopRespondingToWriteRequests()

CBATTRequest encapsulates Central write requests, CBATTError encapsulates the response error code and a SimpleFutures.

PeripheralManager applications would start responding to Central writes requests using,

let writeResponseFuture = characteristic.startRespondingToWriteRequests(capacity: 10)

writeResponseFuture.onSuccess { [weak characteristic] (request, _) in
    guard let characteristic = characteristic else {
        throw AppError.unlikely
    }    
    guard request.value.length == 1 else {
        characteristic.respondToRequest(request, withResult:CBATTError.InvalidAttributeValueLength)
        Return
    }
    characteristic.value = request.value
    characteristic.respondToRequest(request, withResult:CBATTError.Success)
}

Here the length of the Characteristic value is expected to be 1 byte.

PeripheralManager applications will stop responding to write requests using,

characteristic.stopProcessingWriteRequests()

iBeacon emulation does not require MutableServices or MutableCharcteristics to be added to PeripheralManager. Only advertising is required. PeripheralManager provides the following methods supporting iBeacon advertisement,

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

// stop advertising
public func stopAdvertising(timeout: TimeInterval = 10.0) -> Future<Void>

Creation of a FutureLocation BeaconRegion is also required,

public convenience init(proximityUUID: UUID, identifier: String, major: UInt16, minor: UInt16, capacity: Int = Int.max)

The BeaconRegion init parameters are,

proximityUUID The proximityUUID of the beacon targeted.
identifier A unique identifier for region used by application.
major The major value can be used to distinguish between different beacons with the same proximityUUID.
minor The minor value can be used to distinguish between different beacons with the same proximityUUID and major value.

A PeripheralManager application would use the flooring to advertise and iBeacon,

enum AppError: Error {
    case invalidState
    case resetting
    case poweredOff
    case unsupported
}

let manager = PeripheralManager(options: [CBPeripheralManagerOptionRestoreIdentifierKey : "us.gnos.BlueCap.peripheral-manager-documentation" as NSString])

let uuid = UUID(uuidString: "B9407F30-F5F8-466E-AFF9-25556B57FE6D")!

BeaconRegion(proximityUUID: uuid, identifier: "iBeacon", major: 1, minor: 1)

let startAdvertiseFuture = manager.whenStateChanges().flatMap { state -> Future<Void> in
    guard let manager = manager else {
        throw AppError.unlikely
    }    
   switch state {
    case .poweredOn:
        return manager.startAdvertising(beaconRegion)
    case .poweredOff:
        throw AppError.poweredOff
    case .unauthorized, .unknown:
        throw AppError.invalidState
    case .unsupported:
        throw AppError.unsupported
    case .resetting:
        throw AppError.resetting
    }
}

startAdvertiseFuture.onFailure { error in
    switch error {
    case AppError.poweredOff:
        manager.reset()
    case AppError.resetting:
        manager.reset()
    case AppError.unsupported:
        break
    default:
        manager.reset()
    }
    _ = manager.stopAdvertising()
}

See the Beacon Example for details.

CoreBluetooth provides state restoration for apps that have declared bluetooth-peripheral background execution permission. Apps with this permission can be restarted with a previous state if evicted from memory while in the background.

PeripheralManager provides the following method to process the restored application state,

public func whenStateRestored() -> Future<PeripheralAdvertisements>
public enum PeripheralManagerError: Swift.Error {
    // Thrown by startAdvertising if the PeripheralManager is already advertising
    case isAdvertising
	  // Thrown is state restoration fails    
    case restoreFailed
    // Thrown if the stop advertising timeout is exceeded
    case stopAdvertisingTimeout
}

public enum MutableServiceError: Swift.Error {
		// MutableService has no CBMutableService.
    case unconfigured
}
 
public enum MutableCharacteristicError : Swift.Error {
    // Thrown by startRespondingToWriteRequests and update if Mutablecharcteristic has not been added to a PeripheralManager
    case unconfigured
    // Thrown by update(withString:) if String Characteristic value cannot be serialized
    case notSerializable
    // Thrown by update if Characteristic notifiy or indicate property is not enabled
    case notifyNotSupported
}