diff --git a/LittleBlueTooth.xcodeproj/project.pbxproj b/LittleBlueTooth.xcodeproj/project.pbxproj index bc30e4a..6743ed1 100644 --- a/LittleBlueTooth.xcodeproj/project.pbxproj +++ b/LittleBlueTooth.xcodeproj/project.pbxproj @@ -21,11 +21,16 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 654BCE8B24E03544002FC7A4 /* Extraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654BCE8A24E03544002FC7A4 /* Extraction.swift */; }; + 654BCE8E24E077DC002FC7A4 /* Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654BCE8D24E077DC002FC7A4 /* Helper.swift */; }; + 654BCE8F24E07804002FC7A4 /* Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654BCE8D24E077DC002FC7A4 /* Helper.swift */; }; 655330DD24BF159D007D299B /* CentralRestorer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 655330DC24BF159D007D299B /* CentralRestorer.swift */; }; 655F49E224CF3DA400C24461 /* StateRestoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 655330DE24BF5446007D299B /* StateRestoration.swift */; }; 658A75C224BB74FC00F874EF /* CoreBluetoothTypeAliases.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_28 /* CoreBluetoothTypeAliases.swift */; }; 658A75C524BB765C00F874EF /* LittleBlueTooth.h in Headers */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* LittleBlueTooth.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6590D38224D0030000BEE864 /* WriteWithoutResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6590D38124D0030000BEE864 /* WriteWithoutResponse.swift */; }; + 65A6B35024DD29000068FE1C /* Loggable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65A6B34F24DD29000068FE1C /* Loggable.swift */; }; + 65A6B35124DD29070068FE1C /* Loggable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65A6B34F24DD29000068FE1C /* Loggable.swift */; }; 65AD4BD424D437B700C0CBE6 /* LittleBlueTooth.h in Headers */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* LittleBlueTooth.h */; settings = {ATTRIBUTES = (Public, ); }; }; 65AD4BD624D437B700C0CBE6 /* LittleBlueToothError.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* LittleBlueToothError.swift */; }; 65AD4BD724D437B700C0CBE6 /* AdvertisingData.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_15 /* AdvertisingData.swift */; }; @@ -86,9 +91,12 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 654BCE8A24E03544002FC7A4 /* Extraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extraction.swift; sourceTree = ""; }; + 654BCE8D24E077DC002FC7A4 /* Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helper.swift; sourceTree = ""; }; 655330DC24BF159D007D299B /* CentralRestorer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CentralRestorer.swift; sourceTree = ""; }; 655330DE24BF5446007D299B /* StateRestoration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRestoration.swift; sourceTree = ""; }; 6590D38124D0030000BEE864 /* WriteWithoutResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WriteWithoutResponse.swift; sourceTree = ""; }; + 65A6B34F24DD29000068FE1C /* Loggable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Loggable.swift; sourceTree = ""; }; 65AD4BEB24D437B700C0CBE6 /* LittleBlueToothForTest.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LittleBlueToothForTest.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 65AD4BEC24D437B700C0CBE6 /* LittleBlueTooth copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "LittleBlueTooth copy-Info.plist"; path = "/Users/Andrea/Documents/GitHub/LittleBlueTooth/LittleBlueTooth copy-Info.plist"; sourceTree = ""; }; 65BA2D0E24CDDE99008B4BD7 /* LittleBluetoothConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LittleBluetoothConfiguration.swift; sourceTree = ""; }; @@ -150,6 +158,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 654BCE8C24E077B3002FC7A4 /* Extension */ = { + isa = PBXGroup; + children = ( + 654BCE8D24E077DC002FC7A4 /* Helper.swift */, + ); + path = Extension; + sourceTree = ""; + }; 65AD4BEF24D438AD00C0CBE6 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -160,6 +176,7 @@ OBJ_11 /* Classes */ = { isa = PBXGroup; children = ( + 654BCE8C24E077B3002FC7A4 /* Extension */, OBJ_12 /* Error */, OBJ_14 /* Model */, OBJ_19 /* Proxies */, @@ -185,6 +202,7 @@ OBJ_18 /* PeripheralDiscovery.swift */, 655330DC24BF159D007D299B /* CentralRestorer.swift */, 65BA2D0E24CDDE99008B4BD7 /* LittleBluetoothConfiguration.swift */, + 65A6B34F24DD29000068FE1C /* Loggable.swift */, ); path = Model; sourceTree = ""; @@ -238,6 +256,7 @@ OBJ_40 /* WriteReadTest.swift */, 655330DE24BF5446007D299B /* StateRestoration.swift */, 6590D38124D0030000BEE864 /* WriteWithoutResponse.swift */, + 654BCE8A24E03544002FC7A4 /* Extraction.swift */, ); name = LittleBlueToothTests; path = Tests/LittleBlueToothTests; @@ -439,7 +458,9 @@ 65AD4BD924D437B700C0CBE6 /* Peripheral.swift in Sources */, 65AD4BDA24D437B700C0CBE6 /* PeripheralDiscovery.swift in Sources */, 65AD4BDB24D437B700C0CBE6 /* CentralRestorer.swift in Sources */, + 65A6B35124DD29070068FE1C /* Loggable.swift in Sources */, 65AD4BDC24D437B700C0CBE6 /* CBCentralManagerDelegateProxy.swift in Sources */, + 654BCE8F24E07804002FC7A4 /* Helper.swift in Sources */, 65AD4BDD24D437B700C0CBE6 /* CBPeripheralProxy.swift in Sources */, 65AD4BDE24D437B700C0CBE6 /* ReplaySubject.swift in Sources */, 65AD4BDF24D437B700C0CBE6 /* ReplaySubjectSubscription.swift in Sources */, @@ -461,7 +482,9 @@ OBJ_55 /* Peripheral.swift in Sources */, OBJ_56 /* PeripheralDiscovery.swift in Sources */, 655330DD24BF159D007D299B /* CentralRestorer.swift in Sources */, + 65A6B35024DD29000068FE1C /* Loggable.swift in Sources */, OBJ_57 /* CBCentralManagerDelegateProxy.swift in Sources */, + 654BCE8E24E077DC002FC7A4 /* Helper.swift in Sources */, OBJ_58 /* CBPeripheralProxy.swift in Sources */, OBJ_59 /* ReplaySubject.swift in Sources */, OBJ_60 /* ReplaySubjectSubscription.swift in Sources */, @@ -488,6 +511,7 @@ OBJ_82 /* ConnectionTest.swift in Sources */, 655F49E224CF3DA400C24461 /* StateRestoration.swift in Sources */, OBJ_83 /* ListenTest.swift in Sources */, + 654BCE8B24E03544002FC7A4 /* Extraction.swift in Sources */, OBJ_84 /* LittleBlueToothTests.swift in Sources */, OBJ_85 /* MockPeripherals.swift in Sources */, 6590D38224D0030000BEE864 /* WriteWithoutResponse.swift in Sources */, @@ -584,7 +608,7 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 0.3.1; + MARKETING_VERSION = 0.4.0; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) -DTEST"; @@ -675,7 +699,7 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 0.3.1; + MARKETING_VERSION = 0.4.0; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited)"; @@ -709,7 +733,7 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 0.3.1; + MARKETING_VERSION = 0.4.0; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) -DTEST"; @@ -743,7 +767,7 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 0.3.1; + MARKETING_VERSION = 0.4.0; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited)"; @@ -882,7 +906,7 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 0.3.1; + MARKETING_VERSION = 0.4.0; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited)"; @@ -916,7 +940,7 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 0.3.1; + MARKETING_VERSION = 0.4.0; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited)"; @@ -1119,7 +1143,7 @@ repositoryURL = "https://github.com/NordicSemiconductor/IOS-CoreBluetooth-Mock.git"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 0.9.0; + minimumVersion = 0.11.1; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/LittleBlueTooth.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LittleBlueTooth.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index d344065..0000000 --- a/LittleBlueTooth.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,16 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "CoreBluetoothMock", - "repositoryURL": "https://github.com/enricodk/IOS-CoreBluetooth-Mock.git", - "state": { - "branch": null, - "revision": "835c9785d41b824218f86c78154a61a3b3abb297", - "version": "0.8.0" - } - } - ] - }, - "version": 1 -} diff --git a/Package.swift b/Package.swift index 45ef51e..ed742bd 100644 --- a/Package.swift +++ b/Package.swift @@ -25,7 +25,7 @@ let package = Package( // Dependencies declare other packages that this package depends on. .package(name: "CoreBluetoothMock", url: "https://github.com/NordicSemiconductor/IOS-CoreBluetooth-Mock.git", - .upToNextMinor(from: "0.9.0")), + .upToNextMinor(from: "0.11.0")), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/README.md b/README.md index 374d2fb..6c3dc20 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ The library is still on development so use at own you risk. Add the following to your Cartfile: ``` -github "DrAma999/LittleBlueTooth" ~> 0.3.0 +github "DrAma999/LittleBlueTooth" ~> 0.4.0 ``` Since the framework supports most of the Apple devices, you probably want to to build for a specific platform by adding the option `--platform` after the `carthage update` command. For instance: ``` @@ -35,7 +35,7 @@ The library has a sub-dependency with Nordic library [Core Bluetooth Mock](https ### Swift Package Manager Add the following dependency to your Package.swift file: ``` -.package(url: "https://github.com/DrAma999/LittleBlueTooth.git", from: "0.3.0") +.package(url: "https://github.com/DrAma999/LittleBlueTooth.git", from: "0.4.0") ``` Or simply add the URL from XCode menu Swift packages. @@ -520,13 +520,26 @@ Note: * Restoration can happen in background and foreground * The Peripheral object returned can be in different state depending on what has been restored. If a peripheral has been disconnected and an `autoconnectionHandler` is provided LittleBluetooth will try to re-establish a connection. +### CBCentralManager CBPeripheral extraction +Sometimes it could be uselful to extract an already connected peripheral and a central manger and pass them to another framework. For instance if you need to make an OTA firmware update using the nordic library this would be required. +The extraction is made exaclty for this purpuse. +``` +let extractedState = littleBT.extract() +``` +Before extraction you need to stop listen to all the characteristics you where listening to. +The extracted state is a tuple `(central: CBCentralManager, peripheral: CBPeripheral?)` that contains the used `CBCentralManger` and a `CBPeripheral` if connected. +You can also *restart* LittleBlueTooth instance by passing the same object that you have extracted. +``` +self.littleBT.restart(with: extractedState.central, peripheral: extractedState.peripheral) +``` + ## ROADMAP - [x] SwiftPM support - [x] State preservation and state restoration - [ ] Improve code coverage -- [ ] `CBManager` and `CBPeripheral` extraction -- [ ] Add multiple peripheral support +- [x] `CBManager` and `CBPeripheral` extraction - [x] Add support to: **macOS**, **watchOS**, **tvOS**, **macOS catalyst** +- [] Implement custom operator ## ISSUES Please use Gihub, explaining what you did, how you did, what you expect and what you get. diff --git a/Sources/LittleBlueTooth/Classes/Extension/Helper.swift b/Sources/LittleBlueTooth/Classes/Extension/Helper.swift new file mode 100644 index 0000000..b46330f --- /dev/null +++ b/Sources/LittleBlueTooth/Classes/Extension/Helper.swift @@ -0,0 +1,56 @@ +// +// Helper.swift +// LittleBlueTooth +// +// Created by Andrea Finollo on 09/08/2020. +// + +import Foundation +import Combine +import os.log +#if TEST +import CoreBluetoothMock +#else +import CoreBluetooth +#endif + +extension AnyCancellable { + func store(in dictionary: inout [UUID : AnyCancellable], + for key: UUID) { + dictionary[key] = self + } +} +extension Publisher { + + func flatMapLatest(_ transform: @escaping (Self.Output) -> T) -> AnyPublisher where T.Failure == Self.Failure { + return map(transform).switchToLatest().eraseToAnyPublisher() + } +} + +extension TimeInterval { + var dispatchInterval: DispatchTimeInterval { + let microseconds = Int64(self * TimeInterval(USEC_PER_SEC)) // perhaps use nanoseconds, though would more often be > Int.max + return microseconds < Int.max ? DispatchTimeInterval.microseconds(Int(microseconds)) : DispatchTimeInterval.seconds(Int(self)) + } +} + +extension OSLog { + public static var Subsystem = "it.vanillagorilla.LittleBlueTooth" + public static var General = "General" + public static var CentralManager = "CentralManager" + public static var Peripheral = "Peripheral" + public static var Restore = "Restore" + + public static let LittleBT_Log_General = OSLog(subsystem: Subsystem, category: General) + public static let LittleBT_Log_CentralManager = OSLog(subsystem: Subsystem, category: CentralManager) + public static let LittleBT_Log_Peripheral = OSLog(subsystem: Subsystem, category: Peripheral) + public static let LittleBT_Log_Restore = OSLog(subsystem: Subsystem, category: Restore) + +} +#if TEST +extension CBMPeripheral { + public var description: String { + return "Test peripheral" + } +} +#endif diff --git a/Sources/LittleBlueTooth/Classes/Model/LittleBluetoothConfiguration.swift b/Sources/LittleBlueTooth/Classes/Model/LittleBluetoothConfiguration.swift index 76209a1..13c96a7 100644 --- a/Sources/LittleBlueTooth/Classes/Model/LittleBluetoothConfiguration.swift +++ b/Sources/LittleBlueTooth/Classes/Model/LittleBluetoothConfiguration.swift @@ -29,6 +29,8 @@ public struct LittleBluetoothConfiguration { /// Handler used to manage state restoration. `Restored` object will contain the restored information /// could be a peripheral, a scan or nothing public var restoreHandler: ((Restored) -> Void)? + /// Enable logging, log is made using os_log and it exposes some information even in release configuration + public var isLogEnabled = false public init() {} } diff --git a/Sources/LittleBlueTooth/Classes/Model/Loggable.swift b/Sources/LittleBlueTooth/Classes/Model/Loggable.swift new file mode 100644 index 0000000..3d1de43 --- /dev/null +++ b/Sources/LittleBlueTooth/Classes/Model/Loggable.swift @@ -0,0 +1,26 @@ +// +// Loggable.swift +// LittleBlueTooth +// +// Created by Andrea Finollo on 07/08/2020. +// + +import Foundation +import os.log + +protocol Loggable { + var isLogEnabled: Bool {get set} + func log(_ message: StaticString, log: OSLog, type: OSLogType, arg: CVarArg...) +} + + +extension Loggable { + func log(_ message: StaticString, log: OSLog, type: OSLogType, arg: CVarArg...) { + #if !TEST + guard isLogEnabled else { + return + } + os_log(type, log: log, message, arg) + #endif + } +} diff --git a/Sources/LittleBlueTooth/Classes/Model/Peripheral.swift b/Sources/LittleBlueTooth/Classes/Model/Peripheral.swift index ff7fe07..c4fe46f 100644 --- a/Sources/LittleBlueTooth/Classes/Model/Peripheral.swift +++ b/Sources/LittleBlueTooth/Classes/Model/Peripheral.swift @@ -59,7 +59,16 @@ public class Peripheral: Identifiable { public let cbPeripheral: CBPeripheral public var rssi: Int? - private let peripheralProxy = CBPeripheralDelegateProxy() + + var isLogEnabled: Bool { + get { + return _isLogEnabled + } + set { + _isLogEnabled = newValue + peripheralProxy.isLogEnabled = newValue + } + } lazy var changesPublisher: AnyPublisher = peripheralProxy.peripheralChangesPublisher @@ -81,6 +90,9 @@ public class Peripheral: Identifiable { .eraseToAnyPublisher() let peripheralStatePublisher: AnyPublisher + + private let peripheralProxy = CBPeripheralDelegateProxy() + private var _isLogEnabled: Bool = false init(_ peripheral: CBPeripheral) { self.cbPeripheral = peripheral @@ -170,6 +182,25 @@ public class Peripheral: Identifiable { return discovery } + func readRSSI() -> AnyPublisher { + let readRSSI = + peripheralProxy.peripheralRSSIPublisher + .tryMap { (value) -> Int in + switch value { + case let (_, error?): + throw error + case let (rssi, _): + return rssi + } + } + .mapError {$0 as! LittleBluetoothError} + .eraseToAnyPublisher() + defer { + cbPeripheral.readRSSI() + } + return readRSSI + } + func read(from charateristicUUID: CBUUID, of serviceUUID: CBUUID) -> AnyPublisher { let read = discoverCharacteristic(charateristicUUID, fromService: serviceUUID) .flatMap { characteristic -> AnyPublisher in @@ -331,3 +362,5 @@ extension Peripheral: CustomDebugStringConvertible { """ } } + +extension Peripheral: Loggable {} diff --git a/Sources/LittleBlueTooth/Classes/Model/PeripheralDiscovery.swift b/Sources/LittleBlueTooth/Classes/Model/PeripheralDiscovery.swift index efff7d4..3ab3719 100644 --- a/Sources/LittleBlueTooth/Classes/Model/PeripheralDiscovery.swift +++ b/Sources/LittleBlueTooth/Classes/Model/PeripheralDiscovery.swift @@ -79,7 +79,7 @@ extension PeripheralDiscovery: CustomDebugStringConvertible { return """ Name: \(name ?? "not available") CB Peripheral: \(cbPeripheral) - Adv: \(advertisement) + Adv: \(advertisement.debugDescription) RSSI: \(rssi) """ } diff --git a/Sources/LittleBlueTooth/Classes/Proxies/CBCentralManagerDelegateProxy.swift b/Sources/LittleBlueTooth/Classes/Proxies/CBCentralManagerDelegateProxy.swift index fcef3e3..a6d606b 100644 --- a/Sources/LittleBlueTooth/Classes/Proxies/CBCentralManagerDelegateProxy.swift +++ b/Sources/LittleBlueTooth/Classes/Proxies/CBCentralManagerDelegateProxy.swift @@ -47,8 +47,10 @@ public enum BluetoothState { self = .poweredOff case .poweredOn: self = .poweredOn + #if !TEST @unknown default: fatalError() + #endif } } } @@ -67,7 +69,7 @@ class CBCentralManagerDelegateProxy: NSObject { let _centralStatePublisher = CurrentValueSubject(BluetoothState.unknown) let _willRestoreStatePublisher = PassthroughSubject() - + var isLogEnabled: Bool = false var isAutoconnectionActive = false var stateRestorationCancellable: AnyCancellable! @@ -80,20 +82,29 @@ class CBCentralManagerDelegateProxy: NSObject { extension CBCentralManagerDelegateProxy: CBCentralManagerDelegate { func centralManagerDidUpdateState(_ central: CBCentralManager) { -// os_log("CBCMD DidUpdateState %{public}d", log: OSLog.LittleBT_Log_General, type: .debug, central.state.rawValue) + log("CBCMD DidUpdateState %{public}d", + log: OSLog.LittleBT_Log_CentralManager, + type: .debug, + arg: central.state.rawValue) _centralStatePublisher.send(BluetoothState(central.state)) } /// Scan func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { -// os_log("CBCMD DidDiscover %{public}@", log: OSLog.LittleBT_Log_General, type: .debug, peripheral.description) + log("CBCMD DidDiscover %{public}@", + log: OSLog.LittleBT_Log_CentralManager, + type: .debug, + arg: peripheral.description) let peripheraldiscovery = PeripheralDiscovery(peripheral, advertisement: advertisementData, rssi: RSSI) centralDiscoveriesPublisher.send(peripheraldiscovery) } /// Monitoring connection func centralManager(_ central: CBCentralManager, didConnect: CBPeripheral) { -// os_log("CBCMD DidConnect %{public}@", log: OSLog.LittleBT_Log_General, type: .debug, didConnect.description) + log("CBCMD DidConnect %{public}@", + log: OSLog.LittleBT_Log_CentralManager, + type: .debug, + arg: didConnect.description) if isAutoconnectionActive { isAutoconnectionActive = false let event = ConnectionEvent.autoConnected(didConnect) @@ -105,9 +116,11 @@ extension CBCentralManagerDelegateProxy: CBCentralManagerDelegate { } func centralManager(_ central: CBCentralManager, didDisconnectPeripheral: CBPeripheral, error: Error?) { -// #if !TEST -// os_log("CBCMD DidDisconnect %{public}@, Error %{public}@", log: OSLog.LittleBT_Log_General, type: .debug, didDisconnectPeripheral.description, error?.localizedDescription ?? "") -// #endif + log("CBCMD DidDisconnect %{public}@, Error %{public}@", + log: OSLog.LittleBT_Log_CentralManager, + type: .debug, + arg: didDisconnectPeripheral.description, + error?.localizedDescription ?? "") isAutoconnectionActive = false var lttlError: LittleBluetoothError? if let error = error { @@ -128,7 +141,10 @@ extension CBCentralManagerDelegateProxy: CBCentralManagerDelegate { } func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { -// os_log("CBCMD WillRestoreState %{public}@", log: OSLog.LittleBT_Log_General, type: .debug, dict.description) + log("CBCMD WillRestoreState %{public}@", + log: OSLog.LittleBT_Log_Restore, + type: .debug, + arg: dict.description) _willRestoreStatePublisher.send(CentralRestorer(centralManager: central, restoredInfo: dict)) } @@ -137,3 +153,5 @@ extension CBCentralManagerDelegateProxy: CBCentralManagerDelegate { #endif } + +extension CBCentralManagerDelegateProxy: Loggable {} diff --git a/Sources/LittleBlueTooth/Classes/Proxies/CBPeripheralProxy.swift b/Sources/LittleBlueTooth/Classes/Proxies/CBPeripheralProxy.swift index 8192184..f011003 100644 --- a/Sources/LittleBlueTooth/Classes/Proxies/CBPeripheralProxy.swift +++ b/Sources/LittleBlueTooth/Classes/Proxies/CBPeripheralProxy.swift @@ -35,22 +35,32 @@ class CBPeripheralDelegateProxy: NSObject { let peripheralUpdatedValueForDescriptor = PassthroughSubject<(CBDescriptor, LittleBluetoothError?), Never>() let peripheralWrittenValueForDescriptor = PassthroughSubject<(CBDescriptor, LittleBluetoothError?), Never>() + var isLogEnabled: Bool = false + } extension CBPeripheralDelegateProxy: CBPeripheralDelegate { func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral){ -// os_log("CBPD ReadyToSendWRiteWOResp", log: OSLog.LittleBT_Log_General, type: .debug) + log("CBPD ReadyToSendWRiteWOResp", + log: OSLog.LittleBT_Log_Peripheral, + type: .debug) peripheralIsReadyToSendWriteWithoutResponse.send() } func peripheralDidUpdateName(_ peripheral: CBPeripheral) { -// os_log("CBPD DidUpdateName %{public}@", log: OSLog.LittleBT_Log_General, type: .debug, peripheral.name ?? "na") + log("CBPD DidUpdateName %{public}@", + log: OSLog.LittleBT_Log_Peripheral, + type: .debug, + arg: peripheral.name ?? "na") peripheralChangesPublisher.send(.name(peripheral.name)) } func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]){ -// os_log("CBPD DidModifyServices %{public}@", log: OSLog.LittleBT_Log_General, type: .debug, invalidatedServices.description) + log("CBPD DidModifyServices %{public}@", + log: OSLog.LittleBT_Log_Peripheral, + type: .debug, + arg: invalidatedServices.description) peripheralChangesPublisher.send(.invalidatedServices(invalidatedServices)) } @@ -63,7 +73,10 @@ extension CBPeripheralDelegateProxy: CBPeripheralDelegate { } func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?){ -// os_log("CBPD DidDiscoverServices, Error %{public}@", log: OSLog.LittleBT_Log_General, type: .debug, error?.localizedDescription ?? "") + log("CBPD DidDiscoverServices, Error %{public}@", + log: OSLog.LittleBT_Log_Peripheral, + type: .debug, + arg: error?.localizedDescription ?? "") if let error = error { peripheralDiscoveredServicesPublisher.send((nil,.serviceNotFound(error))) } else { @@ -72,7 +85,11 @@ extension CBPeripheralDelegateProxy: CBPeripheralDelegate { } func peripheral(_ peripheral: CBPeripheral, didDiscoverIncludedServicesFor service: CBService, error: Error?) { -// os_log("CBPD DidDiscoverIncludedServices %{public}@, Error %{public}@", log: OSLog.LittleBT_Log_General, type: .debug, service.description, error?.localizedDescription ?? "") + log("CBPD DidDiscoverIncludedServices %{public}@, Error %{public}@", + log: OSLog.LittleBT_Log_Peripheral, + type: .debug, + arg: service.description, + error?.localizedDescription ?? "") if let error = error { peripheralDiscoveredIncludedServicesPublisher.send((service, error)) } else { @@ -81,7 +98,11 @@ extension CBPeripheralDelegateProxy: CBPeripheralDelegate { } func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?){ -// os_log("CBPD DidDiscoverCharacteristic %{public}@, Error %{public}@", log: OSLog.LittleBT_Log_General, type: .debug, service.description, error?.localizedDescription ?? "") + log("CBPD DidDiscoverCharacteristic %{public}@, Error %{public}@", + log: OSLog.LittleBT_Log_Peripheral, + type: .debug, + arg: service.description, + error?.localizedDescription ?? "") if let error = error { peripheralDiscoveredCharacteristicsForServicePublisher.send((service, .characteristicNotFound(error))) } else { @@ -90,7 +111,11 @@ extension CBPeripheralDelegateProxy: CBPeripheralDelegate { } func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?){ -// os_log("CBPD DidUpdateValue %{public}@, Error %{public}@", log: OSLog.LittleBT_Log_General, type: .debug, characteristic.description, error?.localizedDescription ?? "") + log("CBPD DidUpdateValue %{public}@, Error %{public}@", + log: OSLog.LittleBT_Log_Peripheral, + type: .debug, + arg: characteristic.description, + error?.localizedDescription ?? "") if let error = error { peripheralUpdatedValueForCharacteristicPublisher.send((characteristic, .couldNotReadFromCharacteristic(characteristic: characteristic.uuid, error: error))) } else { @@ -103,7 +128,11 @@ extension CBPeripheralDelegateProxy: CBPeripheralDelegate { } func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { -// os_log("CBPD DidWriteValue %{public}@, Error %{public}@", log: OSLog.LittleBT_Log_General, type: .debug, characteristic.description, error?.localizedDescription ?? "") + log("CBPD DidWriteValue %{public}@, Error %{public}@", + log: OSLog.LittleBT_Log_Peripheral, + type: .debug, + arg: characteristic.description, + error?.localizedDescription ?? "") if let error = error { peripheralWrittenValueForCharacteristicPublisher.send((characteristic, .couldNotWriteFromCharacteristic(characteristic: characteristic.uuid, error: error))) } else { @@ -112,7 +141,11 @@ extension CBPeripheralDelegateProxy: CBPeripheralDelegate { } func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?){ -// os_log("CBPD DidUpdateNotifState %{public}@, Error %{public}@", log: OSLog.LittleBT_Log_General, type: .debug, characteristic.description, error?.localizedDescription ?? "") + log("CBPD DidUpdateNotifState %{public}@, Error %{public}@", + log: OSLog.LittleBT_Log_Peripheral, + type: .debug, + arg: characteristic.description, + error?.localizedDescription ?? "") if let error = error { peripheralUpdatedNotificationStateForCharacteristicPublisher.send((characteristic, .couldNotUpdateListenState(characteristic: characteristic.uuid, error: error))) } else { @@ -121,7 +154,9 @@ extension CBPeripheralDelegateProxy: CBPeripheralDelegate { } // MARK: - Descriptors - func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?){} - func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor descriptor: CBDescriptor, error: Error?){} - func peripheral(_ peripheral: CBPeripheral, didWriteValueFor descriptor: CBDescriptor, error: Error?){} +// func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?){} +// func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor descriptor: CBDescriptor, error: Error?){} +// func peripheral(_ peripheral: CBPeripheral, didWriteValueFor descriptor: CBDescriptor, error: Error?){} } + +extension CBPeripheralDelegateProxy: Loggable {} diff --git a/Sources/LittleBlueTooth/LittleBlueTooth.swift b/Sources/LittleBlueTooth/LittleBlueTooth.swift index 4b377ec..25300c6 100644 --- a/Sources/LittleBlueTooth/LittleBlueTooth.swift +++ b/Sources/LittleBlueTooth/LittleBlueTooth.swift @@ -49,7 +49,14 @@ public class LittleBlueTooth: Identifiable { public var autoconnectionHandler: AutoconnectionHandler? /// Connected peripheral. `nil` if not connected or a connection is not requested - public var peripheral: Peripheral? + public var peripheral: Peripheral? { + didSet { + guard let per = peripheral else { + return + } + per.isLogEnabled = isLogEnabled + } + } /// Publisher that streams peripheral state available only when a connection is requested for fine grained control public var peripheralStatePublisher: AnyPublisher { @@ -79,7 +86,19 @@ public class LittleBlueTooth: Identifiable { public var restoreStatePublisher: AnyPublisher { return centralProxy.willRestoreStatePublisher } - + + /// Enable logging disabled by default + /// Enable logging, log is made using os_log and it exposes some information even in release configuration + public var isLogEnabled: Bool { + get { + return _isLogEnabled + } + set { + _isLogEnabled = newValue + centralProxy.isLogEnabled = newValue + } + } + // MARK: - Private variables /// Cancellable operation idendified by a `UUID` key private var disposeBag = [UUID : AnyCancellable]() @@ -95,8 +114,8 @@ public class LittleBlueTooth: Identifiable { private var connectionEventSubscriber: AnyCancellable? private var connectionEventSubscriberPeri: AnyCancellable? - private lazy var _listenPublisher: Publishers.Multicast, PassthroughSubject> - = { [unowned self] in + private var _listenPublisher: Publishers.Multicast, PassthroughSubject> { + if _listenPublisher_ == nil { let pub = ensureBluetoothState() .flatMap { [unowned self] _ in @@ -107,36 +126,53 @@ public class LittleBlueTooth: Identifiable { } .share() .eraseToAnyPublisher() - return Publishers.Multicast(upstream: pub, createSubject:{ PassthroughSubject() }) - }() + + _listenPublisher_ = Publishers.Multicast(upstream: pub, createSubject:{ PassthroughSubject() }) + return _listenPublisher_! + } + return _listenPublisher_! + } + + private var _listenPublisher_: Publishers.Multicast, PassthroughSubject>? + /// Peripheral state connectable publisher. It will be connected after `Peripheral` instance creation. - private lazy var _peripheralStatePublisher: Publishers.MakeConnectable> = { [unowned self] in - let statePublisher = - Just(()) - .flatMap { - self.peripheral!.peripheralStatePublisher + private var _peripheralStatePublisher: Publishers.MakeConnectable> { + if _peripheralStatePublisher_ == nil { + _peripheralStatePublisher_ = Just(()) + .flatMap { + self.peripheral!.peripheralStatePublisher + } + .eraseToAnyPublisher() + .makeConnectable() + return _peripheralStatePublisher_! } - .eraseToAnyPublisher() - .makeConnectable() - return statePublisher - }() + return _peripheralStatePublisher_! + } + + private var _peripheralStatePublisher_: Publishers.MakeConnectable>? + /// Peripheral changes connectable publisher. It will be connected after `Peripheral` instance creation. - private lazy var _peripheralChangesPublisher: Publishers.MakeConnectable> = { [unowned self] in - let changesPublisher = - Just(()) - .flatMap { - self.peripheral!.changesPublisher + private var _peripheralChangesPublisher: Publishers.MakeConnectable> { + if _peripheralChangesPublisher_ == nil { + _peripheralChangesPublisher_ = + Just(()) + .flatMap { + self.peripheral!.changesPublisher + } + .eraseToAnyPublisher() + .makeConnectable() + return _peripheralChangesPublisher_! + } - .eraseToAnyPublisher() - .makeConnectable() - return changesPublisher - }() - - private var restoreStateCancellable: AnyCancellable? + return _peripheralChangesPublisher_! + } - /// Used to inject error to ensure peripheral is connected before any operation, it buffers the last result and throw error if peripheral disconnect for a specific error + private var _peripheralChangesPublisher_: Publishers.MakeConnectable>? + private var restoreStateCancellable: AnyCancellable? + private var _isLogEnabled: Bool = false + var cbCentral: CBCentralManager var centralProxy = CBCentralManagerDelegateProxy() @@ -155,12 +191,13 @@ public class LittleBlueTooth: Identifiable { print("If you want to use state preservation/restoration you should probablu want to implement the `restoreHandler`") } attachSubscribers(with: configuration.restoreHandler) -// os_log( -// "LBT init options %{public}@", -// log: OSLog.LittleBT_Log_General, -// type: .debug, -// configuration.centralManagerOptions?.description ?? "" -// ) + self._isLogEnabled = configuration.isLogEnabled + log( + "LBT init options %{public}@", + log: OSLog.LittleBT_Log_General, + type: .debug, + arg: configuration.centralManagerOptions?.description ?? "" + ) } func attachSubscribers(with restorehandler: ((Restored) -> Void)?) { @@ -241,6 +278,36 @@ public class LittleBlueTooth: Identifiable { cbCentral.cancelPeripheralConnection(peri.cbPeripheral) } + // MARK: - RSSI + public func readRSSI() -> AnyPublisher { + let rssiSubject = PassthroughSubject() + let key = UUID() + + self.ensureBluetoothState() + .print("Read RSSI") + .flatMap { [unowned self] _ in + self.ensurePeripheralReady() + } + .flatMap { [unowned self] _ in + self.peripheral!.readRSSI() + } + .sink(receiveCompletion: { [unowned self, key] (completion) in + switch completion { + case .finished: + break + case .failure(let error): + rssiSubject.send(completion: .failure(error)) + self.removeAndCancelSubscriber(for: key) + } + }) { [unowned self, key] (rssi) in + rssiSubject.send(rssi) + rssiSubject.send(completion: .finished) + self.removeAndCancelSubscriber(for: key) + } + .store(in: &disposeBag, for: key) + + return rssiSubject.eraseToAnyPublisher() + } // MARK: - Listen @@ -603,10 +670,24 @@ public class LittleBlueTooth: Identifiable { /// Starts connection for `PeripheralIdentifier` /// - parameter options: Connecting options same as CoreBluetooth central manager option. /// - returns: A publisher with the just connected `Peripheral`. - private func connect(to peripheralIdentifier: PeripheralIdentifier, timeout: TimeInterval? = nil, options: [String : Any]? = nil, queue: DispatchQueue = DispatchQueue.main) -> AnyPublisher { + func connect(to peripheralIdentifier: PeripheralIdentifier, timeout: TimeInterval? = nil, options: [String : Any]? = nil, queue: DispatchQueue = DispatchQueue.main) -> AnyPublisher { return connect(to: peripheralIdentifier, options: options, queue: queue, autoreconnect: false) } + /// Starts connection for `PeripheralDiscovery` + /// - parameter options: Connecting options same as CoreBluetooth central manager option. + /// - returns: A publisher with the just connected `Peripheral`. + func connect(to discovery: PeripheralDiscovery, timeout: TimeInterval? = nil, options: [String : Any]? = nil) -> AnyPublisher { + if cbCentral.isScanning { + scanning?.cancel() + scanning = nil + cbCentral.stopScan() + } + let peripheralIdentifier = PeripheralIdentifier(peripheral: discovery.cbPeripheral) + + return connect(to: peripheralIdentifier, timeout: timeout, options: options) + } + private func connect(to peripheralIdentifier: PeripheralIdentifier, timeout: TimeInterval? = nil, options: [String : Any]? = nil, queue: DispatchQueue = DispatchQueue.main, autoreconnect: Bool) -> AnyPublisher { if let periph = peripheral, periph.state == .connecting || periph.state == .connected { return Result.Publisher(.failure(.peripheralAlreadyConnectedOrConnecting(periph))).eraseToAnyPublisher() @@ -700,20 +781,6 @@ public class LittleBlueTooth: Identifiable { return connectSubject.eraseToAnyPublisher() } - - /// Starts connection for `PeripheralDiscovery` - /// - parameter options: Connecting options same as CoreBluetooth central manager option. - /// - returns: A publisher with the just connected `Peripheral`. - public func connect(to discovery: PeripheralDiscovery, timeout: TimeInterval? = nil, options: [String : Any]? = nil) -> AnyPublisher { - if cbCentral.isScanning { - scanning?.cancel() - scanning = nil - cbCentral.stopScan() - } - let peripheralIdentifier = PeripheralIdentifier(peripheral: discovery.cbPeripheral) - - return connect(to: peripheralIdentifier, timeout: timeout, options: options) - } // MARK: - Disconnect @@ -755,6 +822,32 @@ public class LittleBlueTooth: Identifiable { return disconnectionSubject.eraseToAnyPublisher() } + // MARK: - Extraction and restart + /// Sometimes you may need to extract `CBCentralManager` and `CBPeripheral` + /// During this operation everything is stopped, delegates are set to nil current operation cancelled + /// - returns: A tuple with the central and the peripheral if connected + public func extract() -> (central: CBCentralManager, peripheral: CBPeripheral?) { + let cbCentral = self.cbCentral + let cbPeripheral = self.peripheral?.cbPeripheral + cbCentral.delegate = nil + cbPeripheral?.delegate = nil + // Clean operation + cleanUpForExtraction() + return (central: cbCentral, peripheral: cbPeripheral) + } + + public func restart(with central: CBCentralManager, peripheral: CBPeripheral? = nil) { + cbCentral = central + cbCentral.delegate = centralProxy + if let periph = peripheral { + self.peripheral = Peripheral(periph) + listenPublisherCancellable = _listenPublisher.connect() + peripheralStatePublisherCancellable = _peripheralStatePublisher.connect() + peripheralChangesPublisherCancellable = _peripheralChangesPublisher.connect() + } + + } + // MARK: - Private private func restore(_ restorer: CentralRestorer) -> Restored { @@ -763,7 +856,10 @@ public class LittleBlueTooth: Identifiable { let restoreDiscoverServices = restorer.services let restoreScanOptions = restorer.scanOptions let restoreDiscoveryPublisher = self.startDiscovery(withServices: restoreDiscoverServices, options: restoreScanOptions) -// os_log("LBT Scan restore %{public}@", log: OSLog.LittleBT_Log_General, type: .debug, restorer.centralManager.isScanning ? "true" : "false") + log("LBT Scan restore %{public}@", + log: OSLog.LittleBT_Log_Restore, + type: .debug, + arg: restorer.centralManager.isScanning ? "true" : "false") return .scan(discoveryPublisher: restoreDiscoveryPublisher) } if let periph = restorer.peripherals.first, let cbPeripheral = periph.cbPeripheral { @@ -793,9 +889,12 @@ public class LittleBlueTooth: Identifiable { @unknown default: fatalError("Connection event in default not handled") } -// #if !TEST -// os_log("LBT Periph restore %{public}@, has delegate: %{public}@ state %{public}d", log: OSLog.LittleBT_Log_General, type: .debug, cbPeripheral.description, cbPeripheral.delegate != nil ? "true" : "false", cbPeripheral.state.rawValue) -// #endif + log("LBT Periph restore %{public}@, has delegate: %{public}@ state %{public}d", + log: OSLog.LittleBT_Log_Restore, + type: .debug, + arg: cbPeripheral.description, + cbPeripheral.delegate != nil ? "true" : "false", + cbPeripheral.state.rawValue) return Restored.peripheral(self.peripheral!) } return Restored.nothing @@ -876,41 +975,25 @@ public class LittleBlueTooth: Identifiable { } private func cleanUpForDisconnection() { - self.listenPublisherCancellable?.cancel() - self.listenPublisherCancellable = nil - self.peripheralStatePublisherCancellable?.cancel() - self.peripheralStatePublisherCancellable = nil - self.peripheralChangesPublisherCancellable?.cancel() - self.peripheralChangesPublisherCancellable = nil - self.peripheral = nil + listenPublisherCancellable?.cancel() + listenPublisherCancellable = nil + _listenPublisher_ = nil + peripheralStatePublisherCancellable?.cancel() + peripheralStatePublisherCancellable = nil + _peripheralStatePublisher_ = nil + peripheralChangesPublisherCancellable?.cancel() + peripheralChangesPublisherCancellable = nil + _peripheralChangesPublisher_ = nil + peripheral = nil } - -} - -extension AnyCancellable { - func store(in dictionary: inout [UUID : AnyCancellable], - for key: UUID) { - dictionary[key] = self - } -} -extension Publisher { - - func flatMapLatest(_ transform: @escaping (Self.Output) -> T) -> AnyPublisher where T.Failure == Self.Failure { - return map(transform).switchToLatest().eraseToAnyPublisher() - } -} - -extension TimeInterval { - var dispatchInterval: DispatchTimeInterval { - let microseconds = Int64(self * TimeInterval(USEC_PER_SEC)) // perhaps use nanoseconds, though would more often be > Int.max - return microseconds < Int.max ? DispatchTimeInterval.microseconds(Int(microseconds)) : DispatchTimeInterval.seconds(Int(self)) + + private func cleanUpForExtraction() { + cbCentral.stopScan() + scanning?.cancel() + scanning = nil + cleanUpForDisconnection() } + } -extension OSLog { - public static var Subsystem = Bundle.main.bundleIdentifier! - public static var General = "LittleBluetooth" - - public static let LittleBT_Log_General = OSLog(subsystem: Subsystem, category: General) - -} +extension LittleBlueTooth: Loggable {} diff --git a/Tests/LittleBlueToothTests/ConnectionTest.swift b/Tests/LittleBlueToothTests/ConnectionTest.swift index 2577918..24f20ea 100644 --- a/Tests/LittleBlueToothTests/ConnectionTest.swift +++ b/Tests/LittleBlueToothTests/ConnectionTest.swift @@ -54,6 +54,40 @@ class ConnectionTest: LittleBlueToothTests { } + func testPeripheralConnectionReadRSSI() { + disposeBag.removeAll() + + blinky.simulateProximityChange(.near) + let readRSSIExpectation = expectation(description: "Read RSSI expectation") + + var rssiRead: Int? + + littleBT.startDiscovery(withServices: nil) + .flatMap { discovery in + self.littleBT.connect(to: discovery) + } + .flatMap{ _ in + self.littleBT.readRSSI() + } + .sink(receiveCompletion: { completion in + print("Completion \(completion)") + }) { (rssi) in + print("RSSI \(rssi)") + rssiRead = rssi + self.littleBT.disconnect().sink(receiveCompletion: { _ in + }) { _ in + readRSSIExpectation.fulfill() + } + .store(in: &self.disposeBag) + } + .store(in: &disposeBag) + + waitForExpectations(timeout: 15) + XCTAssertNotNil(rssiRead) + XCTAssert(rssiRead! < 70) + + } + func testMultipleConnection() { disposeBag.removeAll() @@ -305,6 +339,45 @@ class ConnectionTest: LittleBlueToothTests { XCTAssertNotNil(ledState) XCTAssert(!ledState!.isOn) XCTAssertEqual(connectedPeripheral!.cbPeripheral.identifier, blinky.identifier) + } + + func testConnectionFailed() { + disposeBag.removeAll() + + blinky.simulateProximityChange(.immediate) + blinky.simulateReset() + let foundExpectation = XCTestExpectation(description: "Device found expectation") + + var discovery: PeripheralDiscovery? + + littleBT.startDiscovery(withServices: nil) + .sink(receiveCompletion: { completion in + print("Completion \(completion)") + }) { (disc) in + print("Discovery \(disc)") + discovery = disc + foundExpectation.fulfill() + } + .store(in: &disposeBag) + wait(for: [foundExpectation], timeout: 3) + XCTAssertNotNil(discovery) + + blinky.simulateProximityChange(.outOfRange) + // Should never happen + let connected = XCTestExpectation(description: "Connected expectation") + connected.isInverted = true + + littleBT.connect(to: discovery!) + .sink(receiveCompletion: { (completion) in + print("Completion \(completion)") + }) { (periph) in + print("Peripheral \(periph)") + connected.fulfill() + } + .store(in: &disposeBag) + + wait(for: [connected], timeout: 3) + littleBT.disconnect() } } diff --git a/Tests/LittleBlueToothTests/Extraction.swift b/Tests/LittleBlueToothTests/Extraction.swift new file mode 100644 index 0000000..5636757 --- /dev/null +++ b/Tests/LittleBlueToothTests/Extraction.swift @@ -0,0 +1,105 @@ +// +// Extraction.swift +// LittleBlueToothTests +// +// Created by Andrea Finollo on 09/08/2020. +// + +import XCTest +import CoreBluetoothMock +import Combine +@testable import LittleBlueToothForTest + +class Extraction: LittleBlueToothTests { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + try super.setUpWithError() + + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExtractionWithPeriph() { + disposeBag.removeAll() + littleBT = LittleBlueTooth(with: LittleBluetoothConfiguration()) + + blinky.simulateProximityChange(.immediate) + let extractionExpectation = expectation(description: "Extraction expectation") + + var connectedPeripheral: Peripheral? + var extractedState: (central: CBCentralManager, peripheral: CBPeripheral?)? + + littleBT.startDiscovery(withServices: nil) + .flatMap { discovery in + self.littleBT.connect(to: discovery) + } + .sink(receiveCompletion: { completion in + print("Completion \(completion)") + }) { (connectedPeriph) in + print("Discovery \(connectedPeriph)") + connectedPeripheral = connectedPeriph + // Extract state + extractedState = self.littleBT.extract() + extractionExpectation.fulfill() + } + .store(in: &disposeBag) + + waitForExpectations(timeout: 15) + XCTAssertNotNil(connectedPeripheral) + XCTAssertEqual(connectedPeripheral!.cbPeripheral.identifier, blinky.identifier) + XCTAssertNotNil(extractedState) + XCTAssertEqual(extractedState!.peripheral!.identifier, blinky.identifier) + XCTAssertEqual(extractedState!.peripheral!.state, CBPeripheralState.connected) + self.littleBT.disconnect() + + } + + func testExtractionWithoutPeriph() { + disposeBag.removeAll() + littleBT = LittleBlueTooth(with: LittleBluetoothConfiguration()) + + let extractedState = self.littleBT.extract() + + XCTAssertNil(extractedState.peripheral) + XCTAssertNotNil(extractedState.central) + } + + func testRestart() { + disposeBag.removeAll() + littleBT = LittleBlueTooth(with: LittleBluetoothConfiguration()) + blinky.simulateDisconnection() + blinky.simulateProximityChange(.immediate) + let restartExpectation = expectation(description: "Restart expectation") + + var connectedPeripheral: Peripheral? + var extractedState: (central: CBCentralManager, peripheral: CBPeripheral?)? + + littleBT.startDiscovery(withServices: nil) + .flatMap { discovery in + self.littleBT.connect(to: discovery) + } + .sink(receiveCompletion: { completion in + print("Completion \(completion)") + }) { (connectedPeriph) in + print("Discovery \(connectedPeriph)") + connectedPeripheral = connectedPeriph + // Extract state + extractedState = self.littleBT.extract() + XCTAssertNotNil(connectedPeripheral) + XCTAssertEqual(connectedPeripheral!.cbPeripheral.identifier, blinky.identifier) + XCTAssertNotNil(extractedState) + XCTAssertEqual(extractedState!.peripheral!.identifier, blinky.identifier) + self.littleBT.restart(with: extractedState!.central, peripheral: extractedState!.peripheral!) + restartExpectation.fulfill() + } + .store(in: &disposeBag) + + waitForExpectations(timeout: 10) + XCTAssertNotNil(littleBT.peripheral) + self.littleBT.disconnect() + + } +} diff --git a/Tests/LittleBlueToothTests/ListenTest.swift b/Tests/LittleBlueToothTests/ListenTest.swift index ccbb546..a5411f2 100644 --- a/Tests/LittleBlueToothTests/ListenTest.swift +++ b/Tests/LittleBlueToothTests/ListenTest.swift @@ -284,7 +284,7 @@ class ListenTest: LittleBlueToothTests { wait(for: [firstListenExpectation, secondListenExpectation], timeout: 30) littleBT.disconnect() XCTAssert(sub1Event.count == sub2Event.count) - + scheduler.cancel() } func testPowerOffWhileListen() { @@ -298,7 +298,7 @@ class ListenTest: LittleBlueToothTests { let scheduler: AnyCancellable = Timer.publish(every: 0.5, on: .main, in: .common) .autoconnect() .map {_ in - var data = UInt8.random(in: 0...1) + let data = UInt8.random(in: 0...1) blinky.simulateValueUpdate(Data([data]), for: CBMCharacteristicMock.buttonCharacteristic) }.sink { value in print("Led value:\(value)") @@ -337,7 +337,7 @@ class ListenTest: LittleBlueToothTests { waitForExpectations(timeout: 10) XCTAssert(isPowerOff) - + scheduler.cancel() } } diff --git a/Tests/LittleBlueToothTests/ScanDiscoveryTest.swift b/Tests/LittleBlueToothTests/ScanDiscoveryTest.swift index d5c8a81..406fd16 100644 --- a/Tests/LittleBlueToothTests/ScanDiscoveryTest.swift +++ b/Tests/LittleBlueToothTests/ScanDiscoveryTest.swift @@ -34,23 +34,23 @@ class ScanDiscoveryTest: LittleBlueToothTests { var discovery: PeripheralDiscovery? littleBT.startDiscovery(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : false]) - .sink(receiveCompletion: { completion in - print("Completion \(completion)") - }) { (discov) in - print("Discovery \(discov)") - discovery = discov - self.littleBT.stopDiscovery() - .sink(receiveCompletion: {_ in - }) { () in - discoveryExpectation.fulfill() - } - .store(in: &self.disposeBag) + .sink(receiveCompletion: { completion in + print("Completion \(completion)") + }) { (discov) in + print("Discovery \(discov)") + discovery = discov + self.littleBT.stopDiscovery() + .sink(receiveCompletion: {_ in + }) { () in + discoveryExpectation.fulfill() + } + .store(in: &self.disposeBag) } .store(in: &disposeBag) waitForExpectations(timeout: 10) XCTAssertNotNil(discovery) - let name = discovery!.name + _ = discovery!.name let peripheral = discovery!.cbPeripheral let advInfo = discovery!.advertisement XCTAssertEqual(discovery!.cbPeripheral.identifier, blinky.identifier) diff --git a/Tests/LittleBlueToothTests/StateRestoration.swift b/Tests/LittleBlueToothTests/StateRestoration.swift index 3bd8760..db6a8dc 100644 --- a/Tests/LittleBlueToothTests/StateRestoration.swift +++ b/Tests/LittleBlueToothTests/StateRestoration.swift @@ -17,16 +17,6 @@ class StateRestoration: LittleBlueToothTests { override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. try super.setUpWithError() - let peri = FakePeriph() - - CBMCentralManagerFactory.simulateStateRestoration = { (identifier) -> [String : Any] in - return [ CBCentralManagerRestoredStatePeripheralsKey : [peri], - CBCentralManagerRestoredStateScanOptionsKey : [CBCentralManagerScanOptionAllowDuplicatesKey : false], - CBCentralManagerRestoredStateScanServicesKey : [self.fakeCBUUID] - ] - } - - } override func tearDownWithError() throws { @@ -34,28 +24,65 @@ class StateRestoration: LittleBlueToothTests { } func testStateRestore() { - // Cannot simulate the restore of peripheral since all in CentralRestore I check against the retrive peripheral that must be of CBPeripheral type - let restoreExpectation = expectation(description: "State restoration") + let discoveryExpectation = expectation(description: "Discovery expectation") + + blinky.simulateProximityChange(.immediate) + + var littleBTConf = LittleBluetoothConfiguration() + littleBTConf.centralManagerOptions = [CBMCentralManagerOptionRestoreIdentifierKey : "myIdentifier"] + littleBT = LittleBlueTooth(with: littleBTConf) var periph: [PeripheralIdentifier]? var scanOptions: [String : Any]? var scanServices: [CBUUID]? - var littleBTConf = LittleBluetoothConfiguration() + var discoveredPeri: CBPeripheral? + littleBT.startDiscovery(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : false]) + .sink(receiveCompletion: { completion in + print("Completion \(completion)") + }) { (discov) in + print("Discovery \(discov)") + discoveredPeri = discov.cbPeripheral + self.littleBT.stopDiscovery() + .sink(receiveCompletion: {_ in + }) { () in + discoveryExpectation.fulfill() + } + .store(in: &self.disposeBag) + } + .store(in: &disposeBag) + + waitForExpectations(timeout: 10) + + let restoreExpectation = expectation(description: "State restoration") + + CBMCentralManagerFactory.simulateStateRestoration = { (identifier) -> [String : Any] in + return [ + CBCentralManagerRestoredStatePeripheralsKey : [discoveredPeri], + CBCentralManagerRestoredStateScanOptionsKey : [CBCentralManagerScanOptionAllowDuplicatesKey : false], + CBCentralManagerRestoredStateScanServicesKey : [self.fakeCBUUID] + ] + } + + littleBTConf = LittleBluetoothConfiguration() + littleBTConf.restoreHandler = { restore in + print("Restorer \(restore)") + } littleBTConf.centralManagerOptions = [CBMCentralManagerOptionRestoreIdentifierKey : "myIdentifier"] littleBT = LittleBlueTooth(with: littleBTConf) + littleBT.restoreStatePublisher - .sink { (restorer) in - print(restorer) - periph = restorer.peripherals - scanOptions = restorer.scanOptions - scanServices = restorer.services - restoreExpectation.fulfill() + .sink { (restorer) in + print(restorer) + periph = restorer.peripherals + scanOptions = restorer.scanOptions + scanServices = restorer.services + restoreExpectation.fulfill() } .store(in: &disposeBag) - waitForExpectations(timeout: 10) + XCTAssertNotNil(periph) XCTAssertNotNil(scanOptions) XCTAssertNotNil(scanServices) diff --git a/Tests/LittleBlueToothTests/UtilityTest.swift b/Tests/LittleBlueToothTests/UtilityTest.swift index fc718fa..31647cf 100644 --- a/Tests/LittleBlueToothTests/UtilityTest.swift +++ b/Tests/LittleBlueToothTests/UtilityTest.swift @@ -8,6 +8,7 @@ import XCTest import CoreBluetoothMock +import Combine @testable import LittleBlueToothForTest class UtilityTest: LittleBlueToothTests { @@ -58,6 +59,12 @@ class UtilityTest: LittleBlueToothTests { XCTAssert(characteristicOne == characteristicTwo) } + func testCharacteristicEqualityFail() { + let characteristicOne = LittleBlueToothCharacteristic(characteristic: CBMUUID.buttonCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString) + let characteristicTwo = LittleBlueToothCharacteristic(characteristic: "00001524-1212-EFDE-1523-785FEABCD123", for: "00001523-1212-EFDE-1523-785FEABCD127") + XCTAssertFalse(characteristicOne == characteristicTwo) + } + func testCharacteristicHash() { let characteristicOne = LittleBlueToothCharacteristic(characteristic: CBMUUID.buttonCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString) let characteristicTwo = LittleBlueToothCharacteristic(characteristic: "00001524-1212-EFDE-1523-785FEABCD123", for: "00001523-1212-EFDE-1523-785FEABCD123") @@ -105,5 +112,36 @@ class UtilityTest: LittleBlueToothTests { periphId = try? PeripheralIdentifier(string: "") XCTAssertNil(periphId) } + + func testShareReplay() { + var event1: Set = [] + var event2: Set = [] + + let cvs = CurrentValueSubject("Hello") + + let shareTest = + cvs + .shareReplay(1) + .eraseToAnyPublisher() + + let sub1 = shareTest.sink(receiveValue: { value in + event1.insert(value) + print("subscriber1: \(value)\n") + }) + print("Sub1: \(sub1)") + + let sub2 = shareTest.sink(receiveValue: { value in + event2.insert(value) + print("subscriber2: \(value)\n") + }) + print("Sub2: \(sub2)") + + cvs.send("World") + cvs.send(completion: .finished) + cvs.send("Huge") + + XCTAssert(event1.count == 2) + XCTAssert(event1 == event2) + } } diff --git a/Tests/LittleBlueToothTests/WriteWithoutResponse.swift b/Tests/LittleBlueToothTests/WriteWithoutResponse.swift index 4c2a7d8..dfb30d9 100644 --- a/Tests/LittleBlueToothTests/WriteWithoutResponse.swift +++ b/Tests/LittleBlueToothTests/WriteWithoutResponse.swift @@ -15,8 +15,7 @@ class WriteWithoutResponse: LittleBlueToothTests { override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. try super.setUpWithError() - var lttlCon = LittleBluetoothConfiguration() - lttlCon.centralManagerQueue = DispatchQueue.global() + let lttlCon = LittleBluetoothConfiguration() littleBT = LittleBlueTooth(with: lttlCon) } @@ -56,5 +55,39 @@ class WriteWithoutResponse: LittleBlueToothTests { .store(in: &disposeBag) waitForExpectations(timeout: 10) } + + func testWriteWOResponseMoreBuffer() { + disposeBag.removeAll() + blinky.simulateProximityChange(.outOfRange) + blinkyWOR.simulateProximityChange(.immediate) + let charateristic = LittleBlueToothCharacteristic(characteristic: CBUUID.ledCharacteristic.uuidString, for: CBUUID.nordicBlinkyService.uuidString) + let writeWOResp = expectation(description: "Write without response expectation") + + var data = Data() + (0..<30).forEach { (val) in + data.append(val) + } + littleBT.startDiscovery(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : false]) + .flatMap { discovery in + self.littleBT.connect(to: discovery) + } + .flatMap { _ in + self.littleBT.write(to: charateristic, value: data, response: false) + } + .sink(receiveCompletion: { completion in + print("Completion \(completion)") + }) { (answer) in + print("Answer \(answer)") + self.littleBT.disconnect().sink(receiveCompletion: {_ in + }) { (_) in + writeWOResp.fulfill() + } + .store(in: &self.disposeBag) + + } + .store(in: &disposeBag) + waitForExpectations(timeout: 10) + } + }