Bluefruit-Playground/BluefruitPlayground/AdafruitKit/Ble/BleCentralMode/BlePeripheral.swift
2019-12-17 11:45:21 +01:00

627 lines
26 KiB
Swift

//
// BlePeripheral.swift
// NewtManager
//
// Created by Antonio García on 12/09/2016.
// Copyright © 2016 Adafruit. All rights reserved.
//
import Foundation
import CoreBluetooth
#if COMMANDLINE
#else
import MSWeakTimer
#endif
// TODO: Modernize completion blocks to use Swift.Result
class BlePeripheral: NSObject {
// Config
private static var kProfileCharacteristicUpdates = true
// Notifications
enum NotificationUserInfoKey: String {
case uuid = "uuid"
case name = "name"
case invalidatedServices = "invalidatedServices"
}
enum PeripheralError: Error {
case timeout
}
// Data
var peripheral: CBPeripheral
var rssi: Int? // rssi only is updated when a non undefined value is received from CoreBluetooth. Note: this is slighty different to the CoreBluetooth implementation, because it will not be updated with undefined values
var lastSeenTime: CFAbsoluteTime
var identifier: UUID {
return peripheral.identifier
}
var name: String? {
return peripheral.name
}
var state: CBPeripheralState {
return peripheral.state
}
struct Advertisement {
var advertisementData: [String: Any]
init(advertisementData: [String: Any]?) {
self.advertisementData = advertisementData ?? [String: Any]()
}
// Advertisement data formatted
var localName: String? {
return advertisementData[CBAdvertisementDataLocalNameKey] as? String
}
var manufacturerData: Data? {
return advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data
}
var manufacturerHexDescription: String? {
guard let manufacturerData = manufacturerData else { return nil }
return HexUtils.hexDescription(data: manufacturerData)
// return String(data: manufacturerData, encoding: .utf8)
}
var manufacturerIdentifier: Data? {
guard let manufacturerData = manufacturerData, manufacturerData.count >= 2 else { return nil }
let manufacturerIdentifierData = manufacturerData[0..<2]
return manufacturerIdentifierData
}
var services: [CBUUID]? {
return advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID]
}
var servicesOverflow: [CBUUID]? {
return advertisementData[CBAdvertisementDataOverflowServiceUUIDsKey] as? [CBUUID]
}
var servicesSolicited: [CBUUID]? {
return advertisementData[CBAdvertisementDataSolicitedServiceUUIDsKey] as? [CBUUID]
}
var serviceData: [CBUUID: Data]? {
return advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data]
}
var txPower: Int? {
let number = advertisementData[CBAdvertisementDataTxPowerLevelKey] as? NSNumber
return number?.intValue
}
var isConnectable: Bool? {
let connectableNumber = advertisementData[CBAdvertisementDataIsConnectable] as? NSNumber
return connectableNumber?.boolValue
}
}
var advertisement: Advertisement
typealias CapturedReadCompletionHandler = ((_ value: Any?, _ error: Error?) -> Void)
private class CaptureReadHandler {
var identifier: String
var result: CapturedReadCompletionHandler
var timeoutTimer: MSWeakTimer?
var timeoutAction: ((String)->())?
var isNotifyOmitted: Bool
init(identifier: String, result: @escaping CapturedReadCompletionHandler, timeout: Double?, timeoutAction:((String)->())?, isNotifyOmitted: Bool = false) {
self.identifier = identifier
self.result = result
self.isNotifyOmitted = isNotifyOmitted
if let timeout = timeout {
self.timeoutAction = timeoutAction
timeoutTimer = MSWeakTimer.scheduledTimer(withTimeInterval: timeout, target: self, selector: #selector(timerFired), userInfo: nil, repeats: false, dispatchQueue: .global(qos: .background))
}
}
@objc private func timerFired() {
timeoutTimer?.invalidate()
timeoutTimer = nil
result(nil, PeripheralError.timeout)
timeoutAction?(identifier)
}
}
private func timeOutRemoveCaptureHandler(identifier: String) { // Default behaviour for a capture handler timeout
guard captureReadHandlers.count > 0, let index = captureReadHandlers.firstIndex(where: {$0.identifier == identifier}) else { return }
// DLog("captureReadHandlers index: \(index) / \(captureReadHandlers.count)")
// Remove capture handler
captureReadHandlers.remove(at: index)
finishedExecutingCommand(error: PeripheralError.timeout)
}
// Internal data
private var notifyHandlers = [String: ((Error?) -> Void)]() // Nofify handlers for each service-characteristic
private var captureReadHandlers = [CaptureReadHandler]()
private var commandQueue = CommandQueue<BleCommand>()
// Profiling
//private var profileStartTime: CFTimeInterval = 0
// MARK: - Init
init(peripheral: CBPeripheral, advertisementData: [String: Any]?, rssi: Int?) {
self.peripheral = peripheral
self.advertisement = Advertisement(advertisementData: advertisementData)
self.rssi = rssi
self.lastSeenTime = CFAbsoluteTimeGetCurrent()
super.init()
self.peripheral.delegate = self
// DLog("create peripheral: \(peripheral.name ?? peripheral.identifier.uuidString)")
commandQueue.executeHandler = executeCommand
}
deinit {
//DLog("peripheral deinit")
}
func reset() {
rssi = nil
notifyHandlers.removeAll()
captureReadHandlers.removeAll()
commandQueue.first()?.isCancelled = true // Stop current command if is processing
commandQueue.removeAll()
}
// MARK: - Discover
func discover(serviceUuids: [CBUUID]?, completion: ((Error?) -> Void)?) {
let command = BleCommand(type: .discoverService, parameters: serviceUuids, completion: completion)
commandQueue.append(command)
}
func discover(characteristicUuids: [CBUUID]?, service: CBService, completion: ((Error?) -> Void)?) {
let command = BleCommand(type: .discoverCharacteristic, parameters: [characteristicUuids as Any, service], completion: completion)
commandQueue.append(command)
}
func discover(characteristicUuids: [CBUUID]?, serviceUuid: CBUUID, completion: ((Error?) -> Void)?) {
// Discover service
discover(serviceUuids: [serviceUuid]) { [unowned self] error in
guard error == nil else {
completion?(error)
return
}
guard let service = self.peripheral.services?.first(where: {$0.uuid == serviceUuid}) else {
completion?(BleCommand.CommandError.invalidService)
return
}
// Discover characteristic
self.discover(characteristicUuids: characteristicUuids, service: service, completion: completion)
}
}
func discoverDescriptors(characteristic: CBCharacteristic, completion: ((Error?) -> Void)?) {
let command = BleCommand(type: .discoverDescriptor, parameters: [characteristic], completion: completion)
commandQueue.append(command)
}
// MARK: - Service
func discoveredService(uuid: CBUUID) -> CBService? {
let service = peripheral.services?.first(where: {$0.uuid == uuid})
return service
}
func service(uuid: CBUUID, completion: ((CBService?, Error?) -> Void)?) {
if let discoveredService = discoveredService(uuid: uuid) { // Service was already discovered
completion?(discoveredService, nil)
} else {
discover(serviceUuids: [uuid], completion: { [unowned self] (error) in // Discover service
var discoveredService: CBService?
if error == nil {
discoveredService = self.discoveredService(uuid: uuid)
}
completion?(discoveredService, error)
})
}
}
// MARK: - Characteristic
func discoveredCharacteristic(uuid: CBUUID, service: CBService) -> CBCharacteristic? {
let characteristic = service.characteristics?.first(where: {$0.uuid == uuid})
return characteristic
}
func characteristic(uuid: CBUUID, service: CBService, completion: ((CBCharacteristic?, Error?) -> Void)?) {
if let discoveredCharacteristic = discoveredCharacteristic(uuid: uuid, service: service) { // Characteristic was already discovered
completion?(discoveredCharacteristic, nil)
} else {
discover(characteristicUuids: [uuid], service: service, completion: { [unowned self] (error) in // Discover characteristic
var discoveredCharacteristic: CBCharacteristic?
if error == nil {
discoveredCharacteristic = self.discoveredCharacteristic(uuid: uuid, service: service)
}
completion?(discoveredCharacteristic, error)
})
}
}
func characteristic(uuid: CBUUID, serviceUuid: CBUUID, completion: ((CBCharacteristic?, Error?) -> Void)?) {
if let discoveredService = discoveredService(uuid: serviceUuid) { // Service was already discovered
characteristic(uuid: uuid, service: discoveredService, completion: completion)
} else { // Discover service
service(uuid: serviceUuid) { (service, error) in
if let service = service, error == nil { // Discover characteristic
self.characteristic(uuid: uuid, service: service, completion: completion)
} else {
completion?(nil, error != nil ? error: BleCommand.CommandError.invalidService)
}
}
}
}
func enableNotify(for characteristic: CBCharacteristic, handler: ((Error?) -> Void)?, completion: ((Error?) -> Void)? = nil) {
let command = BleCommand(type: .setNotify, parameters: [characteristic, true, handler as Any], completion: completion)
commandQueue.append(command)
}
func disableNotify(for characteristic: CBCharacteristic, completion: ((Error?) -> Void)? = nil) {
let command = BleCommand(type: .setNotify, parameters: [characteristic, false], completion: completion)
commandQueue.append(command)
}
func updateNotifyHandler(for characteristic: CBCharacteristic, handler: ((Error?) -> Void)? = nil) {
let identifier = handlerIdentifier(from: characteristic)
if notifyHandlers[identifier] == nil {
DLog("Warning: trying to update inexistent notifyHandler")
}
notifyHandlers[identifier] = handler
}
func readCharacteristic(_ characteristic: CBCharacteristic, completion readCompletion: @escaping CapturedReadCompletionHandler) {
let command = BleCommand(type: .readCharacteristic, parameters: [characteristic, readCompletion as Any], completion: nil)
commandQueue.append(command)
}
func write(data: Data, for characteristic: CBCharacteristic, type: CBCharacteristicWriteType, completion: ((Error?) -> Void)? = nil) {
let command = BleCommand(type: .writeCharacteristic, parameters: [characteristic, type, data], completion: completion)
commandQueue.append(command)
}
func writeAndCaptureNotify(data: Data, for characteristic: CBCharacteristic, writeCompletion: ((Error?) -> Void)? = nil, readCharacteristic: CBCharacteristic, readTimeout: Double? = nil, readCompletion: CapturedReadCompletionHandler? = nil) {
let type: CBCharacteristicWriteType = .withResponse // Force write with response
let command = BleCommand(type: .writeCharacteristicAndWaitNofity, parameters: [characteristic, type, data, readCharacteristic, readCompletion as Any, readTimeout as Any], timeout: readTimeout, completion: writeCompletion)
commandQueue.append(command)
}
// MARK: - Descriptors
func readDescriptor(_ descriptor: CBDescriptor, completion readCompletion: @escaping CapturedReadCompletionHandler) {
let command = BleCommand(type: .readDescriptor, parameters: [descriptor, readCompletion as Any], completion: nil)
commandQueue.append(command)
}
// MARK: - Rssi
func readRssi() {
peripheral.readRSSI()
}
// MARK: - Command Queue
private class BleCommand: Equatable {
enum CommandType {
case discoverService
case discoverCharacteristic
case discoverDescriptor
case setNotify
case readCharacteristic
case writeCharacteristic
case writeCharacteristicAndWaitNofity
case readDescriptor
}
enum CommandError: Error {
case invalidService
}
var type: CommandType
var parameters: [Any]?
var completion: ((Error?) -> Void)?
var isCancelled = false
init(type: CommandType, parameters: [Any]?, timeout: Double? = nil, completion: ((Error?) -> Void)?) {
self.type = type
self.parameters = parameters
self.completion = completion
}
func completion(withError error: Error?) {
completion?(error)
}
static func == (left: BleCommand, right: BleCommand) -> Bool {
return left.type == right.type
}
}
private func executeCommand(command: BleCommand) {
switch command.type {
case .discoverService:
discoverService(with: command)
case .discoverCharacteristic:
discoverCharacteristic(with: command)
case .discoverDescriptor:
discoverDescriptor(with: command)
case .setNotify:
setNotify(with: command)
case .readCharacteristic:
readCharacteristic(with: command)
case .writeCharacteristic, .writeCharacteristicAndWaitNofity:
write(with: command)
case .readDescriptor:
readDescriptor(with: command)
}
}
private func handlerIdentifier(from characteristic: CBCharacteristic) -> String {
return "\(characteristic.service.uuid.uuidString)-\(characteristic.uuid.uuidString)"
}
private func handlerIdentifier(from descriptor: CBDescriptor) -> String {
return "\(descriptor.characteristic.service.uuid.uuidString)-\(descriptor.characteristic.uuid.uuidString)-\(descriptor.uuid.uuidString)"
}
private func finishedExecutingCommand(error: Error?) {
//DLog("finishedExecutingCommand")
// Result Callback
if let command = commandQueue.first(), !command.isCancelled {
command.completion(withError: error)
}
commandQueue.executeNext()
}
// MARK: - Commands
private func discoverService(with command: BleCommand) {
var serviceUuids = command.parameters as? [CBUUID]
let discoverAll = serviceUuids == nil
// Remove services already discovered from the query
if let services = peripheral.services, let serviceUuidsToDiscover = serviceUuids {
for (i, serviceUuid) in serviceUuidsToDiscover.enumerated().reversed() {
if services.contains(where: {$0.uuid == serviceUuid}) {
serviceUuids!.remove(at: i)
}
}
}
// Discover remaining uuids
if discoverAll || (serviceUuids != nil && serviceUuids!.count > 0) {
peripheral.discoverServices(serviceUuids)
} else {
// Everthing was already discovered
finishedExecutingCommand(error: nil)
}
}
private func discoverCharacteristic(with command: BleCommand) {
var characteristicUuids = command.parameters![0] as? [CBUUID]
let discoverAll = characteristicUuids == nil
let service = command.parameters![1] as! CBService
// Remove services already discovered from the query
if let characteristics = service.characteristics, let characteristicUuidsToDiscover = characteristicUuids {
for (i, characteristicUuid) in characteristicUuidsToDiscover.enumerated().reversed() {
if characteristics.contains(where: {$0.uuid == characteristicUuid}) {
characteristicUuids!.remove(at: i)
}
}
}
// Discover remaining uuids
if discoverAll || (characteristicUuids != nil && characteristicUuids!.count > 0) {
//DLog("discover \(characteristicUuids == nil ? "all": String(characteristicUuids!.count)) characteristics for \(service.uuid.uuidString)")
peripheral.discoverCharacteristics(characteristicUuids, for: service)
} else {
// Everthing was already discovered
finishedExecutingCommand(error: nil)
}
}
private func discoverDescriptor(with command: BleCommand) {
let characteristic = command.parameters![0] as! CBCharacteristic
peripheral.discoverDescriptors(for: characteristic)
}
private func setNotify(with command: BleCommand) {
let characteristic = command.parameters![0] as! CBCharacteristic
let enabled = command.parameters![1] as! Bool
let identifier = handlerIdentifier(from: characteristic)
if enabled {
let handler = command.parameters![2] as? ((Error?) -> Void)
notifyHandlers[identifier] = handler
} else {
notifyHandlers.removeValue(forKey: identifier)
}
peripheral.setNotifyValue(enabled, for: characteristic)
}
private func readCharacteristic(with command: BleCommand) {
let characteristic = command.parameters!.first as! CBCharacteristic
let completion = command.parameters![1] as! CapturedReadCompletionHandler
let identifier = handlerIdentifier(from: characteristic)
let captureReadHandler = CaptureReadHandler(identifier: identifier, result: completion, timeout: nil, timeoutAction: timeOutRemoveCaptureHandler)
captureReadHandlers.append(captureReadHandler)
peripheral.readValue(for: characteristic)
}
private func write(with command: BleCommand) {
let characteristic = command.parameters![0] as! CBCharacteristic
let writeType = command.parameters![1] as! CBCharacteristicWriteType
let data = command.parameters![2] as! Data
peripheral.writeValue(data, for: characteristic, type: writeType)
if writeType == .withoutResponse {
if !command.isCancelled, command.type == .writeCharacteristicAndWaitNofity {
let readCharacteristic = command.parameters![3] as! CBCharacteristic
let readCompletion = command.parameters![4] as! CapturedReadCompletionHandler
let timeout = command.parameters![5] as? Double
let identifier = handlerIdentifier(from: readCharacteristic)
let captureReadHandler = CaptureReadHandler(identifier: identifier, result: readCompletion, timeout: timeout, timeoutAction: timeOutRemoveCaptureHandler)
captureReadHandlers.append(captureReadHandler)
}
finishedExecutingCommand(error: nil)
}
}
private func readDescriptor(with command: BleCommand) {
let descriptor = command.parameters!.first as! CBDescriptor
let completion = command.parameters![1] as! CapturedReadCompletionHandler
let identifier = handlerIdentifier(from: descriptor)
let captureReadHandler = CaptureReadHandler(identifier: identifier, result: completion, timeout: nil, timeoutAction: timeOutRemoveCaptureHandler)
captureReadHandlers.append(captureReadHandler)
peripheral.readValue(for: descriptor)
}
}
// MARK: - CBPeripheralDelegate
extension BlePeripheral: CBPeripheralDelegate {
func peripheralDidUpdateName(_ peripheral: CBPeripheral) {
DLog("peripheralDidUpdateName: \(name ?? "{ No Name }")")
NotificationCenter.default.post(name: .peripheralDidUpdateName, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier, NotificationUserInfoKey.name.rawValue: name as Any])
}
func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {
DLog("didModifyServices")
NotificationCenter.default.post(name: .peripheralDidModifyServices, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier, NotificationUserInfoKey.invalidatedServices.rawValue: invalidatedServices])
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
DLog("didDiscoverServices for: \(peripheral.name ?? peripheral.identifier.uuidString)")
finishedExecutingCommand(error: error)
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
DLog("didDiscoverCharacteristicsFor: \(service.uuid.uuidString)")
finishedExecutingCommand(error: error)
}
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
finishedExecutingCommand(error: error)
}
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
let identifier = handlerIdentifier(from: characteristic)
/*
if (BlePeripheral.kProfileCharacteristicUpdates) {
let currentTime = CACurrentMediaTime()
let elapsedTime = currentTime - profileStartTime
DLog("elapsed: \(String(format: "%.1f", elapsedTime * 1000))")
profileStartTime = currentTime
}
*/
//DLog("didUpdateValueFor \(characteristic.uuid.uuidString): \(String(data: characteristic.value ?? Data(), encoding: .utf8) ?? "<invalid>")")
// Check if waiting to capture this read
var isNotifyOmitted = false
var hasCaptureHandler = false
if captureReadHandlers.count > 0, let index = captureReadHandlers.firstIndex(where: {$0.identifier == identifier}) {
hasCaptureHandler = true
// DLog("captureReadHandlers index: \(index) / \(captureReadHandlers.count)")
// Remove capture handler
let captureReadHandler = captureReadHandlers.remove(at: index)
// DLog("captureReadHandlers postRemove count: \(captureReadHandlers.count)")
// Cancel timeout timer
captureReadHandler.timeoutTimer?.invalidate()
captureReadHandler.timeoutTimer = nil
// Send result
let value = characteristic.value
// DLog("updated value: \(String(data: value!, encoding: .utf8)!)")
captureReadHandler.result(value, error)
isNotifyOmitted = captureReadHandler.isNotifyOmitted
}
// Notify
if !isNotifyOmitted {
if let notifyHandler = notifyHandlers[identifier] {
//let currentTime = CACurrentMediaTime()
notifyHandler(error)
//DLog("elapsed: \(String(format: "%.1f", (CACurrentMediaTime() - currentTime) * 1000))")
}
}
if hasCaptureHandler {
finishedExecutingCommand(error: error)
}
}
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
if let command = commandQueue.first(), !command.isCancelled, command.type == .writeCharacteristicAndWaitNofity {
let characteristic = command.parameters![3] as! CBCharacteristic
let readCompletion = command.parameters![4] as! CapturedReadCompletionHandler
let timeout = command.parameters![5] as? Double
let identifier = handlerIdentifier(from: characteristic)
//DLog("read timeout started")
let captureReadHandler = CaptureReadHandler(identifier: identifier, result: readCompletion, timeout: timeout, timeoutAction: timeOutRemoveCaptureHandler)
captureReadHandlers.append(captureReadHandler)
} else {
finishedExecutingCommand(error: error)
}
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?) {
finishedExecutingCommand(error: error)
}
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor descriptor: CBDescriptor, error: Error?) {
let identifier = handlerIdentifier(from: descriptor)
if captureReadHandlers.count > 0, let index = captureReadHandlers.firstIndex(where: {$0.identifier == identifier}) {
// Remove capture handler
let captureReadHandler = captureReadHandlers.remove(at: index)
// Send result
let value = descriptor.value
captureReadHandler.result(value, error)
finishedExecutingCommand(error: error)
}
}
func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
guard error == nil else { DLog("didReadRSSI error: \(error!.localizedDescription)"); return }
let rssi = RSSI.intValue
if rssi != 127 { // only update rssi value if is defined ( 127 means undefined )
self.rssi = rssi
}
NotificationCenter.default.post(name: .peripheralDidUpdateRssi, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier])
}
}
// MARK: - Custom Notifications
extension Notification.Name {
private static let kPrefix = Bundle.main.bundleIdentifier!
static let peripheralDidUpdateName = Notification.Name(kPrefix+".peripheralDidUpdateName")
static let peripheralDidModifyServices = Notification.Name(kPrefix+".peripheralDidModifyServices")
static let peripheralDidUpdateRssi = Notification.Name(kPrefix+".peripheralDidUpdateRssi")
}