diff --git a/Calliope App/Bluetooth/BluetoothConstants.swift b/Calliope App/Bluetooth/BluetoothConstants.swift index 32a5507f..a86f6426 100644 --- a/Calliope App/Bluetooth/BluetoothConstants.swift +++ b/Calliope App/Bluetooth/BluetoothConstants.swift @@ -10,10 +10,13 @@ import Foundation struct BluetoothConstants { static let discoveryTimeout = 20.0 static let connectTimeout = 5 - static let serviceDiscoveryTimeout = 10.0 + static let serviceDiscoveryTimeout = 10.0 static let readTimeout = 10.0 static let writeTimeout = 10.0 - static let startDfuProcessDelay = 2.0 + static let startDfuProcessDelay = 2.0 + + static let maxRetryCount = 5; + static let retryDelay = 5; //set this to 0 if coupling is not necessary static let couplingDelay = 0.0 diff --git a/Calliope App/Model/CalliopeModels/CalliopeDiscovery.swift b/Calliope App/Model/CalliopeModels/CalliopeDiscovery.swift index 7dd54a92..657e887c 100644 --- a/Calliope App/Model/CalliopeModels/CalliopeDiscovery.swift +++ b/Calliope App/Model/CalliopeModels/CalliopeDiscovery.swift @@ -11,45 +11,49 @@ import UniformTypeIdentifiers class CalliopeDiscovery: NSObject, CBCentralManagerDelegate, UIDocumentPickerDelegate { - enum CalliopeDiscoveryState { - case initialized //no discovered calliopes, doing nothing - case discoveryWaitingForBluetooth //invoked discovery but waiting for the system bluetooth (might be off) - case discovering //running the discovery process now, but not discovered anything yet - case discovered //discovery list is not empty, still searching - case discoveredAll //discovery has finished with discovered calliopes - case connecting //connecting to some calliope - case connected //connected to some calliope + enum CalliopeDiscoveryState { + case initialized //no discovered calliopes, doing nothing + case discoveryWaitingForBluetooth //invoked discovery but waiting for the system bluetooth (might be off) + case discovering //running the discovery process now, but not discovered anything yet + case discovered //discovery list is not empty, still searching + case discoveredAll //discovery has finished with discovered calliopes + case connecting //connecting to some calliope + case connected //connected to some calliope case usbConnecting // connecting to a usb calliope case usbConnected // connected to a usb calliope - } + } private static let usbCalliopeName = "USB_CALLIOPE" - var updateQueue = DispatchQueue.main - var updateBlock: () -> () = {} - var errorBlock: (Error) -> () = {_ in } + var updateQueue = DispatchQueue.main + var updateBlock: () -> () = { + } + var errorBlock: (Error) -> () = { _ in + } - var calliopeBuilder: (_ peripheral: CBPeripheral, _ name: String) -> DiscoveredBLEDDevice + var calliopeBuilder: (_ peripheral: CBPeripheral, _ name: String) -> DiscoveredBLEDDevice - private(set) var state : CalliopeDiscoveryState = .initialized { - didSet { - LogNotify.log("calliope discovery state: \(state)") - updateQueue.async { self.updateBlock() } - } - } + private(set) var state: CalliopeDiscoveryState = .initialized { + didSet { + LogNotify.log("calliope discovery state: \(state)") + updateQueue.async { + self.updateBlock() + } + } + } - private(set) var discoveredCalliopes : [String : DiscoveredDevice] = [:] { - didSet { - LogNotify.log("discovered: \(discoveredCalliopes)") - redetermineState() - } - } + private(set) var discoveredCalliopes: [String: DiscoveredDevice] = [:] { + didSet { + LogNotify.log("discovered: \(discoveredCalliopes)") + redetermineState() + } + } - private var discoveredCalliopeUUIDNameMap : [UUID : String] = [:] + private var discoveredCalliopeUUIDNameMap: [UUID: String] = [:] - private(set) var connectingCalliope: DiscoveredDevice? { - didSet { - if let connectingCalliope = self.connectingCalliope { + private(set) var connectingCalliope: DiscoveredDevice? { + didSet { + if let connectingCalliope = self.connectingCalliope { if connectingCalliope is DiscoveredBLEDDevice { LogNotify.log("Connect to Bluetooth Calliope") let connectingBLECalliope = connectingCalliope as! DiscoveredBLEDDevice @@ -60,7 +64,9 @@ class CalliopeDiscovery: NSObject, CBCentralManagerDelegate, UIDocumentPickerDel if self.connectedCalliope == nil { LogNotify.log("disabling auto connect for \(connectingCalliope)") self.centralManager.cancelPeripheralConnection(connectingBLECalliope.peripheral) - self.updateQueue.async { self.errorBlock( NSLocalizedString("Connection to calliope timed out!", comment: "") ) } + self.updateQueue.async { + self.errorBlock(NSLocalizedString("Connection to calliope timed out!", comment: "")) + } } } } @@ -77,25 +83,25 @@ class CalliopeDiscovery: NSObject, CBCentralManagerDelegate, UIDocumentPickerDel } - } - redetermineState() - } - } - - private(set) var connectedCalliope: DiscoveredBLEDDevice? { - didSet { - if let uuid = connectedCalliope?.peripheral.identifier, - let name = discoveredCalliopeUUIDNameMap[uuid] { - lastConnected = (uuid, name) - } - oldValue?.hasDisconnected() - connectedCalliope?.hasConnected() + } + redetermineState() + } + } + + private(set) var connectedCalliope: DiscoveredBLEDDevice? { + didSet { + if let uuid = connectedCalliope?.peripheral.identifier, + let name = discoveredCalliopeUUIDNameMap[uuid] { + lastConnected = (uuid, name) + } + oldValue?.hasDisconnected() + connectedCalliope?.hasConnected() if (connectedCalliope != nil) { connectingCalliope = nil } redetermineState() - } - } + } + } private(set) var connectedUSBCalliope: DiscoveredUSBDevice? { didSet { @@ -109,79 +115,88 @@ class CalliopeDiscovery: NSObject, CBCentralManagerDelegate, UIDocumentPickerDel } } - private let bluetoothQueue = DispatchQueue.global(qos: .userInitiated) - private lazy var centralManager: CBCentralManager = { - return CBCentralManager(delegate: nil, queue: bluetoothQueue) - }() - - private var lastConnected: (UUID, String)? { - get { - let defaults = UserDefaults.standard - guard let dict = defaults.dictionary(forKey: BluetoothConstants.lastConnectedKey), - let name = dict[BluetoothConstants.lastConnectedNameKey] as? String, - let uuidString = dict[BluetoothConstants.lastConnectedUUIDKey] as? String, - let uuid = UUID(uuidString: uuidString) - else { return nil } - return (uuid, name) - } - set { - let defaults = UserDefaults.standard - guard let newUUIDString = newValue?.0.uuidString, - let newName = newValue?.1 - else { - defaults.removeObject(forKey: BluetoothConstants.lastConnectedKey) - return - } - defaults.setValue([BluetoothConstants.lastConnectedNameKey: newName, - BluetoothConstants.lastConnectedUUIDKey: newUUIDString], - forKey: BluetoothConstants.lastConnectedKey) - } - } - - init(_ calliopeBuilder: @escaping (_ peripheral: CBPeripheral, _ name: String) -> DiscoveredBLEDDevice) { - self.calliopeBuilder = calliopeBuilder - super.init() + private let bluetoothQueue = DispatchQueue.global(qos: .userInitiated) + private lazy var centralManager: CBCentralManager = { + return CBCentralManager(delegate: nil, queue: bluetoothQueue) + }() + + private var lastConnected: (UUID, String)? { + get { + let defaults = UserDefaults.standard + guard let dict = defaults.dictionary(forKey: BluetoothConstants.lastConnectedKey), + let name = dict[BluetoothConstants.lastConnectedNameKey] as? String, + let uuidString = dict[BluetoothConstants.lastConnectedUUIDKey] as? String, + let uuid = UUID(uuidString: uuidString) + else { + return nil + } + return (uuid, name) + } + set { + let defaults = UserDefaults.standard + guard let newUUIDString = newValue?.0.uuidString, + let newName = newValue?.1 + else { + defaults.removeObject(forKey: BluetoothConstants.lastConnectedKey) + return + } + defaults.setValue([BluetoothConstants.lastConnectedNameKey: newName, + BluetoothConstants.lastConnectedUUIDKey: newUUIDString], + forKey: BluetoothConstants.lastConnectedKey) + } + } + + private var retryCount = 0; + private var retrying = false; + + + init(_ calliopeBuilder: @escaping (_ peripheral: CBPeripheral, _ name: String) -> DiscoveredBLEDDevice) { + self.calliopeBuilder = calliopeBuilder + super.init() if centralManager.state == .poweredOn { attemptReconnect() } - centralManager.delegate = self - } + centralManager.delegate = self + } - private func redetermineState() { - if connectedCalliope != nil { - state = .connected + private func redetermineState() { + if connectedCalliope != nil { + state = .connected } else if connectedUSBCalliope != nil { state = .usbConnected } else if connectingCalliope != nil { - state = .connecting - } else if centralManager.isScanning && MatrixConnectionViewController.instance != nil && !MatrixConnectionViewController.instance.isInUsbMode{ - state = discoveredCalliopes.isEmpty ? .discovering : .discovered - } else { - state = discoveredCalliopes.isEmpty ? .initialized : .discoveredAll - } - } - - private func attemptReconnect() { - LogNotify.log("attempt reconnect") - guard let (lastConnectedUUID, lastConnectedName) = self.lastConnected, - let lastCalliope = centralManager.retrievePeripherals(withIdentifiers: [lastConnectedUUID]).first - else { return } - - let calliope = calliopeBuilder(lastCalliope, lastConnectedName) - - self.discoveredCalliopes.updateValue(calliope, forKey: lastConnectedName) - self.discoveredCalliopeUUIDNameMap.updateValue(lastConnectedName, forKey: lastCalliope.identifier) - //auto-reconnect - LogNotify.log("reconnect to: \(calliope)") + state = .connecting + } else if centralManager.isScanning && MatrixConnectionViewController.instance != nil && !MatrixConnectionViewController.instance.isInUsbMode { + state = discoveredCalliopes.isEmpty ? .discovering : .discovered + } else { + state = discoveredCalliopes.isEmpty ? .initialized : .discoveredAll + } + } + + private func attemptReconnect() { + LogNotify.log("attempt reconnect") + guard let (lastConnectedUUID, lastConnectedName) = self.lastConnected, + let lastCalliope = centralManager.retrievePeripherals(withIdentifiers: [lastConnectedUUID]).first + else { + return + } + + let calliope = calliopeBuilder(lastCalliope, lastConnectedName) + + self.discoveredCalliopes.updateValue(calliope, forKey: lastConnectedName) + self.discoveredCalliopeUUIDNameMap.updateValue(lastConnectedName, forKey: lastCalliope.identifier) + //auto-reconnect + LogNotify.log("reconnect to: \(calliope)") delay(time: 0) { self.connectingCalliope = calliope } - } + } /// allows another CalliopeBLEDiscovery to use lastConnected variable to reconnect to the same calliope public func giveUpResponsibility() { - self.updateBlock = {} + self.updateBlock = { + } self.stopCalliopeDiscovery() //we disconnect manually here after switching off delegate, since we donĀ“t want to wipe lastconnected setting centralManager.delegate = nil @@ -190,78 +205,105 @@ class CalliopeDiscovery: NSObject, CBCentralManagerDelegate, UIDocumentPickerDel } } - // MARK: discovery + // MARK: discovery - func startCalliopeDiscovery() { - //start scan only if central manger already connected to bluetooth system service (=poweredOn) - //alternatively, this is invoked after the state of the central mananger changed to poweredOn. - if centralManager.state != .poweredOn { + func startCalliopeDiscovery() { + //start scan only if central manger already connected to bluetooth system service (=poweredOn) + //alternatively, this is invoked after the state of the central mananger changed to poweredOn. + if centralManager.state != .poweredOn { if !MatrixConnectionViewController.instance.isInUsbMode { - updateQueue.async { self.errorBlock(NSLocalizedString("Activate Bluetooth!", comment: "")) } + updateQueue.async { + self.errorBlock(NSLocalizedString("Activate Bluetooth!", comment: "")) + } state = .discoveryWaitingForBluetooth } - } else if !centralManager.isScanning { + } else if !centralManager.isScanning { discoveredCalliopes = [:] discoveredCalliopeUUIDNameMap = [:] - centralManager.scanForPeripherals(withServices: nil, options: nil) - //stop the search after some time. The user can invoke it again later. - bluetoothQueue.asyncAfter(deadline: DispatchTime.now() + BluetoothConstants.discoveryTimeout) { - self.stopCalliopeDiscovery() - } - redetermineState() - } - } - - func stopCalliopeDiscovery() { - if centralManager.isScanning { - self.centralManager.stopScan() - } - redetermineState() - } - - func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { - if let connectable = advertisementData[CBAdvertisementDataIsConnectable] as? NSNumber, - connectable.boolValue == true, - let localName = advertisementData[CBAdvertisementDataLocalNameKey], - let lowerName = (localName as? String)?.lowercased(), - BluetoothConstants.deviceNames.map({ lowerName.contains($0) }).contains(true), - let friendlyName = Matrix.full2Friendly(fullName: lowerName) { - //FIXME: hard-coded name for testing - //let friendlyName = Optional("gepeg") { - //never create a calliope twice, since this would clear its state - if discoveredCalliopes[friendlyName] == nil { - //we found a calliope device (or one that pretends to be a calliope at least) - let calliope = calliopeBuilder(peripheral, friendlyName) - discoveredCalliopes.updateValue(calliope, forKey: friendlyName) - discoveredCalliopeUUIDNameMap.updateValue(friendlyName, forKey: peripheral.identifier) - } - } - } - - // MARK: connection - - func connectToCalliope(_ calliope: DiscoveredDevice) { - //when we first connect, we stop searching further - stopCalliopeDiscovery() - //do not connect twice - guard calliope != connectedCalliope else { return } - //reset last connected, we attempt to connect to a new callipoe now - lastConnected = nil - connectingCalliope = calliope - } - - func disconnectFromCalliope() { - if let connectedCalliope = self.connectedCalliope { - self.centralManager.cancelPeripheralConnection(connectedCalliope.peripheral) - } - //preemptively update connected calliope, in case delegate call does not happen + centralManager.scanForPeripherals(withServices: nil, options: nil) + //stop the search after some time. The user can invoke it again later. + bluetoothQueue.asyncAfter(deadline: DispatchTime.now() + BluetoothConstants.discoveryTimeout) { + self.stopCalliopeDiscovery() + } + redetermineState() + } + } + + func retryCalliopeDiscovery(_ targetCalliope: DiscoveredDevice) { + if retrying || self.connectedCalliope != nil { + LogNotify.log("Stopping retrying due to: retrying - \(retrying) or connected - \(connectedCalliope != nil)") + return + } + + if self.retryCount >= BluetoothConstants.maxRetryCount { + LogNotify.log("Stopping retrying to connect, as max (\(BluetoothConstants.maxRetryCount)) tries reached") + retryCount = 0 + return + } + + retrying = true; + self.retryCount += 1 + LogNotify.log("Trying reconnection \(retryCount)/\(BluetoothConstants.maxRetryCount)") + self.connectToCalliope(targetCalliope) + + bluetoothQueue.asyncAfter(deadline: DispatchTime.now() + .seconds(BluetoothConstants.retryDelay)) { + self.retrying = false; + self.retryCalliopeDiscovery(targetCalliope) + } + } + + func stopCalliopeDiscovery() { + if centralManager.isScanning { + self.centralManager.stopScan() + } + redetermineState() + } + + func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { + if let connectable = advertisementData[CBAdvertisementDataIsConnectable] as? NSNumber, + connectable.boolValue == true, + let localName = advertisementData[CBAdvertisementDataLocalNameKey], + let lowerName = (localName as? String)?.lowercased(), + BluetoothConstants.deviceNames.map({ lowerName.contains($0) }).contains(true), + let friendlyName = Matrix.full2Friendly(fullName: lowerName) { + //FIXME: hard-coded name for testing + //let friendlyName = Optional("gepeg") { + //never create a calliope twice, since this would clear its state + if discoveredCalliopes[friendlyName] == nil { + //we found a calliope device (or one that pretends to be a calliope at least) + let calliope = calliopeBuilder(peripheral, friendlyName) + discoveredCalliopes.updateValue(calliope, forKey: friendlyName) + discoveredCalliopeUUIDNameMap.updateValue(friendlyName, forKey: peripheral.identifier) + } + } + } + + // MARK: connection + + func connectToCalliope(_ calliope: DiscoveredDevice) { + //when we first connect, we stop searching further + stopCalliopeDiscovery() + //do not connect twice + guard calliope != connectedCalliope else { + return + } + //reset last connected, we attempt to connect to a new callipoe now + lastConnected = nil + connectingCalliope = calliope + } + + func disconnectFromCalliope() { + if let connectedCalliope = self.connectedCalliope { + self.centralManager.cancelPeripheralConnection(connectedCalliope.peripheral) + } + //preemptively update connected calliope, in case delegate call does not happen if connectedUSBCalliope != nil { connectedCalliope = nil discoveredCalliopes.removeValue(forKey: CalliopeDiscovery.usbCalliopeName) } self.connectedUSBCalliope = nil - self.connectedCalliope = nil - } + self.connectedCalliope = nil + } func initializeConnectionToUsbCalliope(view: UIViewController) { state = .usbConnecting @@ -271,7 +313,7 @@ class CalliopeDiscovery: NSObject, CBCentralManagerDelegate, UIDocumentPickerDel view.present(documentPicker, animated: true, completion: nil) } - func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]){ + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { let url = urls.first let discoveredCalliope = DiscoveredUSBDevice(url: url!, name: CalliopeDiscovery.usbCalliopeName) if (discoveredCalliope == nil) { @@ -283,16 +325,19 @@ class CalliopeDiscovery: NSObject, CBCentralManagerDelegate, UIDocumentPickerDel } } - func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { - guard let name = discoveredCalliopeUUIDNameMap[peripheral.identifier], - let calliope = discoveredCalliopes[name] else { - updateQueue.async { self.errorBlock(NSLocalizedString("Could not find connected calliope in discovered calliopes", comment: "")) } + func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { + guard let name = discoveredCalliopeUUIDNameMap[peripheral.identifier], + let calliope = discoveredCalliopes[name] + else { + updateQueue.async { + self.errorBlock(NSLocalizedString("Could not find connected calliope in discovered calliopes", comment: "")) + } return - } + } connectedCalliope = calliope as? DiscoveredBLEDDevice - } + } - func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { + func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { LogNotify.log("disconnected from \(peripheral.name ?? "unknown device"), with error: \(error?.localizedDescription ?? "none")") // If Usage Ready Calliope is rebooting, automatically reconnect to the calliope if let connectedCalliope = connectedCalliope, connectedCalliope.shouldReconnectAfterReboot() { @@ -302,41 +347,50 @@ class CalliopeDiscovery: NSObject, CBCentralManagerDelegate, UIDocumentPickerDel bluetoothQueue.asyncAfter(deadline: DispatchTime.now() + BluetoothConstants.restartDuration) { self.connectToCalliope(connectedCalliope) } - } else { + return + } else if let connectedCalliope = connectedCalliope { + self.connectedCalliope = nil connectingCalliope = nil - connectedCalliope = nil lastConnected = nil + self.retryCalliopeDiscovery(connectedCalliope) + return } - } + connectedCalliope = nil + connectingCalliope = nil + lastConnected = nil + } - func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { + func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { if let error = error { - updateQueue.async { self.errorBlock(error) } + updateQueue.async { + self.errorBlock(error) + } } - connectingCalliope = nil - } + connectingCalliope = nil + } - // MARK: state of the bluetooth manager +// MARK: state of the bluetooth manager - func centralManagerDidUpdateState(_ central: CBCentralManager) { - switch central.state { - case .poweredOn: - startCalliopeDiscovery() - if lastConnected != nil { + func centralManagerDidUpdateState(_ central: CBCentralManager) { + switch central.state { + case .poweredOn: + startCalliopeDiscovery() + if lastConnected != nil { self.attemptReconnect() - } - case .unknown, .resetting, .unsupported, .unauthorized, .poweredOff: + } + case .unknown, .resetting, .unsupported, .unauthorized, .poweredOff: if (state == .usbConnected || state == .usbConnecting) { return } - //bluetooth is in a state where we cannot do anything + //bluetooth is in a state where we cannot do anything connectingCalliope = nil connectedCalliope = nil - discoveredCalliopes = [:] - discoveredCalliopeUUIDNameMap = [:] - state = .initialized - @unknown default: - break - } - } + discoveredCalliopes = [:] + discoveredCalliopeUUIDNameMap = [:] + state = .initialized + @unknown default: + break + } + } + } diff --git a/Calliope App/Model/CalliopeModels/ConnectedCalliopes/BLECalliope.swift b/Calliope App/Model/CalliopeModels/ConnectedCalliopes/BLECalliope.swift index ae74e709..6e44eec9 100644 --- a/Calliope App/Model/CalliopeModels/ConnectedCalliopes/BLECalliope.swift +++ b/Calliope App/Model/CalliopeModels/ConnectedCalliopes/BLECalliope.swift @@ -11,56 +11,64 @@ import NordicDFU import CoreBluetooth class BLECalliope: Calliope { - + //the services required for the usage - var requiredServices : Set { + var requiredServices: Set { [] } - + //servcies that are not strictly necessary - var optionalServices : Set { [] } - + var optionalServices: Set { + [] + } + final var discoveredOptionalServices: Set = [] - - lazy var requiredServicesUUIDs: Set = Set(requiredServices.map { $0.uuid }) - lazy var optionalServicesUUIDs: Set = Set(optionalServices.map { $0.uuid }) - + lazy var requiredServicesUUIDs: Set = Set(requiredServices.map { + $0.uuid + }) + + lazy var optionalServicesUUIDs: Set = Set(optionalServices.map { + $0.uuid + }) + var updateQueue = DispatchQueue.main - - - let peripheral : CBPeripheral - let name : String + + + let peripheral: CBPeripheral + let name: String let servicesChangedCallback: () -> ()? - - required init?(peripheral: CBPeripheral, name: String, discoveredServices: Set, discoveredCharacteristicUUIDsForServiceUUID: [CBUUID : Set], servicesChangedCallback: @escaping () -> ()?) { + + required init?(peripheral: CBPeripheral, name: String, discoveredServices: Set, discoveredCharacteristicUUIDsForServiceUUID: [CBUUID: Set], servicesChangedCallback: @escaping () -> ()?) { self.peripheral = peripheral self.name = name self.servicesChangedCallback = servicesChangedCallback super.init() - + self.discoveredOptionalServices = discoveredServices.intersection(optionalServices) guard validateServicesAndCharacteristics(discoveredServices, peripheral, discoveredCharacteristicUUIDsForServiceUUID) else { LogNotify.log("failed to find required services or a way to activate them for \(String(describing: self))") return nil } - + LogNotify.log("successfully validated Calliope Type \(String(describing: self))") } - - private func validateServicesAndCharacteristics(_ discoveredServices: Set, _ peripheral: CBPeripheral, _ discoveredCharacteristicUUIDsForServiceUUID: [CBUUID : Set]) -> Bool { + + private func validateServicesAndCharacteristics(_ discoveredServices: Set, _ peripheral: CBPeripheral, _ discoveredCharacteristicUUIDsForServiceUUID: [CBUUID: Set]) -> Bool { LogNotify.log("start validating optional and required services") //Validate Services, are required Services discovered - let requiredServicesUUIDs = Set(requiredServices.map { return $0.uuid }) + let requiredServicesUUIDs = Set(requiredServices.map { + return $0.uuid + }) let discoveredOptionalServices = optionalServices.intersection(discoveredServices) - + guard requiredServices.isSubset(of: discoveredServices) else { return false } LogNotify.log("found all of \(requiredServicesUUIDs.count) required services:\n\(requiredServices)") LogNotify.log("found \(discoveredOptionalServices.count) of \(optionalServices.count) optional services") - + //Validate Characteristics, are characteristics discovered for all optional and required services for service in requiredServices { guard let foundCharacteristics = discoveredCharacteristicUUIDsForServiceUUID[service.uuid], foundCharacteristics.isSuperset(of: CalliopeBLEProfile.serviceCharacteristicUUIDMap[service.uuid] ?? []) else { @@ -69,7 +77,7 @@ class BLECalliope: Calliope { } LogNotify.log("All characteristics \(foundCharacteristics) found for service \(service.uuid)") } - + return true } @@ -80,16 +88,16 @@ class BLECalliope: Calliope { let readWriteSem = DispatchSemaphore(value: 1) var readWriteGroup: DispatchGroup? = nil - var writeError : Error? = nil - var writingCharacteristic : CBCharacteristic? = nil + var writeError: Error? = nil + var writingCharacteristic: CBCharacteristic? = nil + + var readError: Error? = nil + var readingCharacteristic: CBCharacteristic? = nil + var readValue: Data? = nil - var readError : Error? = nil - var readingCharacteristic : CBCharacteristic? = nil - var readValue : Data? = nil - - var setNotifyError : Error? = nil + var setNotifyError: Error? = nil var setNotifyingCharacteristic: CBCharacteristic? = nil - + func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { if let writingCharac = writingCharacteristic, characteristic.uuid == writingCharac.uuid { explicitWriteResponse(error) @@ -101,7 +109,7 @@ class BLECalliope: Calliope { func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) { LogNotify.log("Calliope \(peripheral.name ?? "[no name]") invalidated services \(invalidatedServices). Re-evaluate mode.") - + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.servicesChangedCallback() } @@ -116,20 +124,20 @@ class BLECalliope: Calliope { guard error == nil, let value = characteristic.value else { LogNotify.log(readError?.localizedDescription ?? - "characteristic \(characteristic.uuid) does not have a value") + "characteristic \(characteristic.uuid) does not have a value") return } guard let calliopeCharacteristic = CalliopeBLEProfile.uuidCharacteristicMap[characteristic.uuid] - else { - LogNotify.log("received value from unknown characteristic: \(characteristic.uuid)") - return + else { + LogNotify.log("received value from unknown characteristic: \(characteristic.uuid)") + return } handleValueUpdateInternal(calliopeCharacteristic, value) handleValueUpdate(calliopeCharacteristic, value) } - + func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { if let writingCharac = writingCharacteristic, characteristic.uuid == writingCharac.uuid { explicitSetNotifyResponse(error) @@ -184,16 +192,22 @@ class BLECalliope: Calliope { } readWriteGroup?.leave() } - + func getCBCharacteristic(_ characteristic: CalliopeCharacteristic) -> CBCharacteristic? { guard let serviceUuid = CalliopeBLEProfile.characteristicServiceMap[characteristic]?.uuid - else { return nil } + else { + return nil + } let uuid = characteristic.uuid - return peripheral.services?.first { $0.uuid == serviceUuid }? - .characteristics?.first { $0.uuid == uuid } + return peripheral.services?.first { + $0.uuid == serviceUuid + }? + .characteristics?.first { + $0.uuid == uuid + } } - - func write (_ data: Data, for characteristic: CalliopeCharacteristic) throws { + + func write(_ data: Data, for characteristic: CalliopeCharacteristic) throws { let cbCharacteristic = try checkWritePreconditions(for: characteristic) try write(data, for: cbCharacteristic) } @@ -204,10 +218,14 @@ class BLECalliope: Calliope { } private func checkWritePreconditions(for characteristic: CalliopeCharacteristic) throws -> CBCharacteristic { - guard let serviceForCharacteristic = CalliopeBLEProfile.characteristicServiceMap[characteristic], - requiredServices.contains(serviceForCharacteristic) || discoveredOptionalServices.contains(serviceForCharacteristic) - else { throw "Not ready to write to characteristic \(characteristic)" } - guard let cbCharacteristic = getCBCharacteristic(characteristic) else { throw "characteristic \(characteristic) not available" } + guard let serviceForCharacteristic = CalliopeBLEProfile.characteristicServiceMap[characteristic], + requiredServices.contains(serviceForCharacteristic) || discoveredOptionalServices.contains(serviceForCharacteristic) + else { + throw "Not ready to write to characteristic \(characteristic)" + } + guard let cbCharacteristic = getCBCharacteristic(characteristic) else { + throw "characteristic \(characteristic) not available" + } return cbCharacteristic } @@ -238,11 +256,12 @@ class BLECalliope: Calliope { } } - - + func read(characteristic: CalliopeCharacteristic) throws -> Data? { guard let cbCharacteristic = getCBCharacteristic(characteristic) - else { throw "no service that contains characteristic \(characteristic)" } + else { + throw "no service that contains characteristic \(characteristic)" + } return try read(characteristic: cbCharacteristic) } @@ -275,17 +294,19 @@ class BLECalliope: Calliope { return data } } - + func setNotify(characteristic: CalliopeCharacteristic, _ activate: Bool) throws { guard let cbCharacteristic = getCBCharacteristic(characteristic) - else { throw "no service that contains characteristic \(characteristic)" } + else { + throw "no service that contains characteristic \(characteristic)" + } return try setNotify(characteristic: cbCharacteristic, activate) } - + func setNotify(characteristic: CBCharacteristic, _ activate: Bool) throws { return try applySemaphore(readWriteSem) { setNotifyingCharacteristic = characteristic - + asyncAndWait(on: readWriteQueue) { //read value and wait for delegate call (or error) self.readWriteGroup = DispatchGroup(); diff --git a/Calliope App/Model/CalliopeModels/DiscoveryCalliopes/DiscoveredBLEDevice.swift b/Calliope App/Model/CalliopeModels/DiscoveryCalliopes/DiscoveredBLEDevice.swift index 359d3d5c..a54f2475 100644 --- a/Calliope App/Model/CalliopeModels/DiscoveryCalliopes/DiscoveredBLEDevice.swift +++ b/Calliope App/Model/CalliopeModels/DiscoveryCalliopes/DiscoveredBLEDevice.swift @@ -9,91 +9,98 @@ import UIKit import CoreBluetooth class DiscoveredBLEDDevice: DiscoveredDevice { - + public static let usageReadyNotificationName = NSNotification.Name("calliope_is_usage_ready") public static let disconnectedNotificationName = NSNotification.Name("calliope_connection_lost") - + private let bluetoothQueue = DispatchQueue.global(qos: .userInitiated) - + let peripheral: CBPeripheral - - var serviceToDiscoveredCharacteristicsMap = [CBUUID : Set]() - - + + var serviceToDiscoveredCharacteristicsMap = [CBUUID: Set]() + + lazy var servicesWithUndiscoveredCharacteristics: Set = { return discoveredServicesUUIDs }() - + required init(peripheral: CBPeripheral, name: String) { self.peripheral = peripheral super.init(name: name) peripheral.delegate = self - + } - + public func shouldReconnectAfterReboot() -> Bool { return usageReadyCalliope?.shouldRebootOnDisconnect ?? false } - + + // MARK: Services discovery - + func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { - + guard error == nil else { - + LogNotify.log("Error discovering services \(error!)") - + LogNotify.log(error!.localizedDescription) state = .wrongMode return } - + let services = peripheral.services ?? [] - let uuidSet = Set(services.map { return $0.uuid }) - + let uuidSet = Set(services.map { + return $0.uuid + }) + LogNotify.log("Did discover services \(services)") - + let discoveredServiceUUIDs = uuidSet - discoveredServices = Set(discoveredServiceUUIDs.compactMap { CalliopeBLEProfile.uuidServiceMap[$0] }) + discoveredServices = Set(discoveredServiceUUIDs.compactMap { + CalliopeBLEProfile.uuidServiceMap[$0] + }) services .forEach { service in peripheral.discoverCharacteristics( CalliopeBLEProfile.serviceCharacteristicUUIDMap[service.uuid], for: service) } - + //Discovered All Services, Flashablecalliope with correct Version can now be created } - + // MARK: Characteristics discovery - + func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { - + guard error == nil else { LogNotify.log("Error discovering characteristics \(error!)") - + LogNotify.log(error!.localizedDescription) state = .wrongMode return } - + let characteristics = service.characteristics ?? [] - let uuidSet = Set(characteristics.map { return $0.uuid }) + let uuidSet = Set(characteristics.map { + return $0.uuid + }) serviceToDiscoveredCharacteristicsMap[service.uuid] = uuidSet - + LogNotify.log("Did discover characteristics \(uuidSet)") - + //Only continue once every discovered service has atleast been checked for characteristic servicesWithUndiscoveredCharacteristics.remove(service.uuid) - - if(servicesWithUndiscoveredCharacteristics.isEmpty) { - + + if (servicesWithUndiscoveredCharacteristics.isEmpty) { + LogNotify.log("Did discover characteristics for all discovered services") - + guard let validBLECalliope = FlashableCalliopeFactory.getFlashableCalliopeForBLEDevice(device: self) else { state = .wrongMode return } - + if let rebootingCalliope = rebootingCalliope, type(of: rebootingCalliope) === type(of: validBLECalliope) { LogNotify.log("Choose rebooting calliope for use") // We saved a calliope in a reboot process, use that one @@ -102,22 +109,22 @@ class DiscoveredBLEDDevice: DiscoveredDevice { //new calliope found, delegate was set in initialization process usageReadyCalliope = validBLECalliope } - + self.peripheral.delegate = usageReadyCalliope - + rebootingCalliope = nil - + state = .usageReady } } - + internal override func handleStateUpdate(_ oldState: DiscoveredDevice.CalliopeBLEDeviceState) { super.handleStateUpdate(oldState) if state == .evaluateMode { peripheral.delegate = self } } - + override func evaluateMode() { if let usageReadyCalliope = usageReadyCalliope, usageReadyCalliope.rebootingIntoDFUMode { LogNotify.log("Calliope is Rebooting For Firmwareupgrade, do not evaluate mode") @@ -138,7 +145,7 @@ extension DiscoveredBLEDDevice { /*static func == (lhs: CalliopeBLEDevice, rhs: CalliopeBLEDevice) -> Bool { return lhs.peripheral == rhs.peripheral }*/ - + override func isEqual(_ object: Any?) -> Bool { return self.peripheral == (object as? DiscoveredBLEDDevice)?.peripheral } diff --git a/Calliope App/Model/CalliopeModels/DiscoveryCalliopes/DiscoveredDevice.swift b/Calliope App/Model/CalliopeModels/DiscoveryCalliopes/DiscoveredDevice.swift index 1677aa2a..c2d35015 100644 --- a/Calliope App/Model/CalliopeModels/DiscoveryCalliopes/DiscoveredDevice.swift +++ b/Calliope App/Model/CalliopeModels/DiscoveryCalliopes/DiscoveredDevice.swift @@ -10,27 +10,33 @@ import Foundation import CoreBluetooth class DiscoveredDevice: NSObject, CBPeripheralDelegate { - + private let bluetoothQueue = DispatchQueue.global(qos: .userInitiated) - + var updateQueue = DispatchQueue.main - var updateBlock: () -> () = {} - var errorBlock: (Error) -> () = { _ in } - - let name : String - + var updateBlock: () -> () = { + } + var errorBlock: (Error) -> () = { _ in + } + + let name: String + var usageReadyCalliope: Calliope? - + var rebootingCalliope: Calliope? = nil - + //discoverable Services of the BLE Devices static var discoverableServices: Set = [.secureDfuService, .dfuControlService, .partialFlashing, .accelerometer, .led, .temperature, .uart] - static var discoverableServicesUUIDs: Set = Set(discoverableServices.map { $0.uuid }) - + static var discoverableServicesUUIDs: Set = Set(discoverableServices.map { + $0.uuid + }) + //discovered Services of the BLE Device final var discoveredServices: Set = [] - lazy var discoveredServicesUUIDs: Set = Set(discoveredServices.map { $0.uuid }) - + lazy var discoveredServicesUUIDs: Set = Set(discoveredServices.map { + $0.uuid + }) + enum CalliopeBLEDeviceState { case discovered //discovered and ready to connect, not connected yet case connected //connected, but services and characteristics have not (yet) been found @@ -39,7 +45,7 @@ class DiscoveredDevice: NSObject, CBPeripheralDelegate { case wrongMode //required services and characteristics not available, put into right mode } - var state : CalliopeBLEDeviceState = .discovered { + var state: CalliopeBLEDeviceState = .discovered { didSet { LogNotify.log("calliope state: \(state)") handleStateUpdate(oldValue) @@ -48,14 +54,16 @@ class DiscoveredDevice: NSObject, CBPeripheralDelegate { } } } - + init(name: String) { self.name = name super.init() } - + internal func handleStateUpdate(_ oldState: CalliopeBLEDeviceState) { - updateQueue.async { self.updateBlock() } + updateQueue.async { + self.updateBlock() + } if state == .discovered { discoveredServices = [] if oldState == .usageReady { @@ -70,7 +78,9 @@ class DiscoveredDevice: NSObject, CBPeripheralDelegate { self.bluetoothQueue.asyncAfter(deadline: DispatchTime.now() + BluetoothConstants.serviceDiscoveryTimeout) { //has not discovered all services in time, probably stuck if self.state == .evaluateMode { - self.updateQueue.async { self.errorBlock(NSLocalizedString("Service discovery on calliope has timed out!", comment: "")) } + self.updateQueue.async { + self.errorBlock(NSLocalizedString("Service discovery on calliope has timed out!", comment: "")) + } self.state = .wrongMode } } @@ -79,7 +89,7 @@ class DiscoveredDevice: NSObject, CBPeripheralDelegate { object: self) } } - + /// evaluate whether calliope is in correct mode public func evaluateMode() { if let usageReadyCalliope = usageReadyCalliope, usageReadyCalliope.rebootingIntoDFUMode { @@ -94,13 +104,13 @@ class DiscoveredDevice: NSObject, CBPeripheralDelegate { state = .evaluateMode } } - + public func hasConnected() { if state == .discovered { state = .connected } } - + public func hasDisconnected() { if state != .discovered { state = .discovered