├── Sources ├── LittleBlueToothForTest │ ├── Classes │ ├── Info.plist │ ├── LittleBlueTooth.h │ ├── LittleBlueTooth.swift │ └── CoreBluetoothTypeAliases.swift └── LittleBlueTooth │ ├── Classes │ ├── CustomOperator │ │ ├── JustLittleBlueTooth.swift │ │ ├── Log.swift │ │ ├── Listen.swift │ │ ├── ReadAndWrite.swift │ │ └── ScanAndConnection.swift │ ├── Utilities │ │ ├── ShareAndReplayOperator │ │ │ ├── ShareReplay.swift │ │ │ ├── ReplaySubjectSubscription.swift │ │ │ └── ReplaySubject.swift │ │ └── Utilities.swift │ ├── Model │ │ ├── Loggable.swift │ │ ├── LittleBluetoothConfiguration.swift │ │ ├── CentralRestorer.swift │ │ ├── AdvertisingData.swift │ │ ├── PeripheralDiscovery.swift │ │ └── LittleBlueToothCharacteristic.swift │ ├── Error │ │ └── LittleBlueToothError.swift │ ├── Extension │ │ └── Helper.swift │ └── Proxies │ │ ├── CBCentralManagerDelegateProxy.swift │ │ └── CBPeripheralProxy.swift │ ├── LittleBlueTooth.h │ ├── Info.plist │ └── CoreBluetoothTypeAliases.swift ├── README └── Icon.png ├── .jazzy.yaml ├── LittleBlueTooth.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ ├── andreafinollo.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ │ └── Andrea.xcuserdatad │ │ │ └── IDEFindNavigatorScopes.plist │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── xcuserdata │ ├── andreafinollo.xcuserdatad │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ └── Andrea.xcuserdatad │ │ └── xcschemes │ │ ├── xcschememanagement.plist │ │ └── LittleBlueToothTests.xcscheme ├── LittleBlueToothTests_Info.plist ├── LittleBlueTooth_Info.plist └── xcshareddata │ └── xcschemes │ └── LittleBlueTooth.xcscheme ├── codecov.yml ├── .github └── workflows │ ├── swift3.yml │ ├── swift.yml │ └── swift2.yml ├── Tests └── LittleBlueToothTests │ ├── Info.plist │ ├── LittleBlueToothTests.swift │ ├── WriteWithoutResponse.swift │ ├── StateRestoration.swift │ ├── Extraction.swift │ ├── ScanDiscoveryTest.swift │ ├── UtilityTest.swift │ ├── Mocks │ └── MockPeripherals.swift │ ├── ConnectionTest.swift │ ├── WriteReadTest.swift │ ├── ListenTest.swift │ └── CustomOperator.swift ├── LittleBlueTooth copy-Info.plist ├── LICENSE ├── .gitignore └── Package.swift /Sources/LittleBlueToothForTest/Classes: -------------------------------------------------------------------------------- 1 | ../LittleBlueTooth/Classes -------------------------------------------------------------------------------- /Sources/LittleBlueToothForTest/Info.plist: -------------------------------------------------------------------------------- 1 | ../LittleBlueTooth/Info.plist -------------------------------------------------------------------------------- /Sources/LittleBlueToothForTest/LittleBlueTooth.h: -------------------------------------------------------------------------------- 1 | ../LittleBlueTooth/LittleBlueTooth.h -------------------------------------------------------------------------------- /README/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrAma999/LittleBlueTooth/HEAD/README/Icon.png -------------------------------------------------------------------------------- /Sources/LittleBlueToothForTest/LittleBlueTooth.swift: -------------------------------------------------------------------------------- 1 | ../LittleBlueTooth/LittleBlueTooth.swift -------------------------------------------------------------------------------- /Sources/LittleBlueToothForTest/CoreBluetoothTypeAliases.swift: -------------------------------------------------------------------------------- 1 | ../LittleBlueTooth/CoreBluetoothTypeAliases.swift -------------------------------------------------------------------------------- /.jazzy.yaml: -------------------------------------------------------------------------------- 1 | author_url: https://www.facebook.com/CloudInTouchLabs 2 | author: Andrea Finollo 3 | github_url: https://github.com/DrAma999 4 | theme: fullwidth 5 | module: LittleBlueTooth 6 | -------------------------------------------------------------------------------- /LittleBlueTooth.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LittleBlueTooth.xcodeproj/project.xcworkspace/xcuserdata/andreafinollo.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrAma999/LittleBlueTooth/HEAD/LittleBlueTooth.xcodeproj/project.xcworkspace/xcuserdata/andreafinollo.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /LittleBlueTooth.xcodeproj/project.xcworkspace/xcuserdata/Andrea.xcuserdatad/IDEFindNavigatorScopes.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LittleBlueTooth.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LittleBlueTooth.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: "70...100" 8 | 9 | parsers: 10 | gcov: 11 | branch_detection: 12 | conditional: yes 13 | loop: yes 14 | method: no 15 | macro: no 16 | comment: 17 | layout: "reach,diff,flags,files,footer" 18 | behavior: default 19 | require_changes: no 20 | -------------------------------------------------------------------------------- /.github/workflows/swift3.yml: -------------------------------------------------------------------------------- 1 | name: Publish Documentation 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | deploy_docs: 7 | runs-on: macos-11 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Publish Jazzy Docs 11 | uses: steven0351/publish-jazzy-docs@v1 12 | with: 13 | personal_access_token: ${{ secrets.JAZZY }} 14 | config: .jazzy.yaml 15 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/Classes/CustomOperator/JustLittleBlueTooth.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JustLittleBlueTooth.swift 3 | // LittleBlueTooth 4 | // 5 | // Created by Andrea Finollo on 28/08/2020. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | /// Syntactic sugar to start a `LittleBlueTooth` pipeline 11 | public var StartLittleBlueTooth: Result<(), LittleBluetoothError>.Publisher { 12 | Just(()).setFailureType(to: LittleBluetoothError.self) 13 | } 14 | -------------------------------------------------------------------------------- /LittleBlueTooth.xcodeproj/xcuserdata/andreafinollo.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | LittleBlueTooth.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/Classes/CustomOperator/Log.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Log.swift 3 | // LittleBlueTooth 4 | // 5 | // Created by Andrea Finollo on 29/01/21. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | extension Publisher { 12 | func customPrint(_ prefix: String = "", to: TextOutputStream? = nil, isEnabled: Bool = true) -> AnyPublisher { 13 | if isEnabled { 14 | return print(prefix, to: to).eraseToAnyPublisher() 15 | } 16 | return AnyPublisher(self) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/Classes/Utilities/ShareAndReplayOperator/ShareReplay.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | 4 | extension Publisher { 5 | /// Provides a subject that shares a single subscription to the upstream publisher and replays at most `bufferSize` items emitted by that publisher 6 | /// - Parameter bufferSize: limits the number of items that can be replayed 7 | public func shareReplay(_ bufferSize: Int) -> AnyPublisher { 8 | return multicast(subject: ReplaySubject(bufferSize)).autoconnect().eraseToAnyPublisher() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Test on PR and Push 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-12 13 | 14 | steps: 15 | - uses: maxim-lobanov/setup-xcode@v1 16 | with: 17 | xcode-version: latest-stable 18 | - uses: actions/checkout@v2 19 | - name: Build 20 | run: | 21 | swift package clean 22 | swift package reset 23 | swift build -v 24 | - name: Run tests 25 | run: | 26 | swift test -v 27 | bash <(curl -s https://codecov.io/bash) 28 | 29 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/LittleBlueTooth.h: -------------------------------------------------------------------------------- 1 | // 2 | // LittleBlueTooth.h 3 | // LittleBlueTooth 4 | // 5 | // Created by Andrea Finollo on 10/06/2020. 6 | // Copyright © 2020 Andrea Finollo. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for LittleBlueTooth. 12 | FOUNDATION_EXPORT double LittleBlueToothVersionNumber; 13 | 14 | //! Project version string for LittleBlueTooth. 15 | FOUNDATION_EXPORT const unsigned char LittleBlueToothVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Tests/LittleBlueToothTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /LittleBlueTooth.xcodeproj/LittleBlueToothTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/LittleBlueToothTests/LittleBlueToothTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LittleBlueToothTests.swift 3 | // LittleBlueToothTests 4 | // 5 | // Created by Andrea Finollo on 10/06/2020. 6 | // Copyright © 2020 Andrea Finollo. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import CoreBluetoothMock 11 | import Combine 12 | @testable import LittleBlueToothForTest 13 | 14 | class LittleBlueToothTests: XCTestCase { 15 | var littleBT: LittleBlueTooth! 16 | var disposeBag: Set = [] 17 | static var testInitialized: Bool = false 18 | 19 | override func setUpWithError() throws { 20 | try super.setUpWithError() 21 | if !Self.testInitialized { 22 | CBMCentralManagerMock.simulatePeripherals([blinky, blinkyWOR]) 23 | Self.testInitialized = true 24 | } 25 | CBMCentralManagerMock.simulateInitialState(.poweredOn) 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LittleBlueTooth copy-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LittleBlueTooth.xcodeproj/LittleBlueTooth_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Andrea Finollo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/Classes/Model/Loggable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Loggable.swift 3 | // LittleBlueTooth 4 | // 5 | // Created by Andrea Finollo on 07/08/2020. 6 | // 7 | 8 | import Foundation 9 | import os.log 10 | 11 | protocol Loggable { 12 | var isLogEnabled: Bool {get set} 13 | func log(_ message: StaticString, log: OSLog, type: OSLogType, arg: [CVarArg]) 14 | } 15 | 16 | 17 | extension Loggable { 18 | func log(_ message: StaticString, log: OSLog, type: OSLogType, arg: [CVarArg]) { 19 | assert(arg.count <= 3) 20 | #if !TEST 21 | // https://stackoverflow.com/questions/50937765/why-does-wrapping-os-log-cause-doubles-to-not-be-logged-correctly/50942917#50942917 22 | guard isLogEnabled else { 23 | return 24 | } 25 | switch arg.count { 26 | case 1: 27 | os_log(type, log: log, message, arg[0]) 28 | case 2: 29 | os_log(type, log: log, message, arg[0], arg[1]) 30 | case 3: 31 | os_log(type, log: log, message, arg[0], arg[1], arg[2]) 32 | default: 33 | os_log(type, log: log, message) 34 | } 35 | #endif 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LittleBlueTooth.xcodeproj/xcuserdata/Andrea.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | LittleBlueTooth-Package.xcscheme 8 | 9 | orderHint 10 | 3 11 | 12 | LittleBlueTooth.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | LittleBlueToothForTest.xcscheme 18 | 19 | orderHint 20 | 2 21 | 22 | LittleBlueToothPackageDescription.xcscheme_^#shared#^_ 23 | 24 | orderHint 25 | 5 26 | 27 | LittleBlueToothPackageTests.xcscheme_^#shared#^_ 28 | 29 | orderHint 30 | 6 31 | 32 | LittleBlueToothTests.xcscheme 33 | 34 | orderHint 35 | 1 36 | 37 | 38 | SuppressBuildableAutocreation 39 | 40 | 65AD4BD024D437B700C0CBE6 41 | 42 | primary 43 | 44 | 45 | E12C12E82490B60D00496496 46 | 47 | primary 48 | 49 | 50 | E12C12F12490B60D00496496 51 | 52 | primary 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | ## Build generated 5 | build/ 6 | DerivedData/ 7 | 8 | ## Various settings 9 | *.pbxuser 10 | !default.pbxuser 11 | *.mode1v3 12 | !default.mode1v3 13 | *.mode2v3 14 | !default.mode2v3 15 | *.perspectivev3 16 | !default.perspectivev3 17 | xcuserdata/ 18 | 19 | ## Other 20 | *.moved-aside 21 | *.xcuserstate 22 | ## Obj-C/Swift specific 23 | *.hmap 24 | *.ipa 25 | *.dSYM.zip 26 | *.dSYM 27 | 28 | ## Playgrounds 29 | timeline.xctimeline 30 | playground.xcworkspace 31 | 32 | # Swift Package Manager 33 | # 34 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 35 | Packages/ 36 | .build/ 37 | 38 | # CocoaPods 39 | # 40 | # We recommend against adding the Pods directory to your .gitignore. However 41 | # you should judge for yourself, the pros and cons are mentioned at: 42 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 43 | # 44 | # Pods/ 45 | 46 | # Carthage 47 | # 48 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 49 | # Carthage/Checkouts 50 | Carthage/Build 51 | 52 | # fastlane 53 | # 54 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 55 | # screenshots whenever they are needed. 56 | # For more information about the recommended setup visit: 57 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 58 | fastlane/report.xml 59 | fastlane/Preview.html 60 | fastlane/screenshots 61 | fastlane/test_output 62 | Package.resolved 63 | 64 | docs/ 65 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/Classes/Utilities/ShareAndReplayOperator/ReplaySubjectSubscription.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | 4 | /// A class representing the connection of a subscriber to a publisher. 5 | public final class ReplaySubjectSubscription: Subscription { 6 | private let downstream: AnySubscriber 7 | private var isCompleted = false 8 | private var demand: Subscribers.Demand = .none 9 | 10 | public init(downstream: AnySubscriber) { 11 | self.downstream = downstream 12 | } 13 | 14 | /// Tells a publisher that it may send more values to the subscriber. 15 | public func request(_ newDemand: Subscribers.Demand) { 16 | demand += newDemand 17 | } 18 | 19 | /// Cancel the subscription 20 | public func cancel() { 21 | isCompleted = true 22 | } 23 | /// Receive the value from the publisher 24 | public func receive(_ value: Output) { 25 | guard !isCompleted, demand > 0 else { return } 26 | 27 | demand += downstream.receive(value) 28 | demand -= 1 29 | } 30 | /// Receive the completion from the publisher 31 | public func receive(completion: Subscribers.Completion) { 32 | guard !isCompleted else { return } 33 | isCompleted = true 34 | downstream.receive(completion: completion) 35 | } 36 | /// Replay values in the buffer 37 | public func replay(_ values: [Output], completion: Subscribers.Completion?) { 38 | guard !isCompleted else { return } 39 | values.forEach { value in receive(value) } 40 | if let completion = completion { receive(completion: completion) } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/swift2.yml: -------------------------------------------------------------------------------- 1 | name: Test and Create Release 2 | 3 | # Create XCFramework when a version is tagged 4 | on: 5 | push: 6 | tags: 7 | - '*' 8 | 9 | env: 10 | XCODE_VER: '14.1' 11 | 12 | jobs: 13 | test: 14 | strategy: 15 | matrix: 16 | xcode_version: ['14.1'] 17 | runs-on: macos-12 18 | env: 19 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode_version }}.app 20 | steps: 21 | - name: Check out LittleBlueTooth 22 | uses: actions/checkout@v2 23 | - name: Build LittleBlueTooth 24 | run: | 25 | set -euo pipefail 26 | swift package clean 27 | swift build --sanitize="address" 28 | - name: Run Tests 29 | run: | 30 | set -euo pipefail 31 | swift test --sanitize="address" 32 | 33 | # Upload release assets for tags 34 | release: 35 | needs: test 36 | if: startsWith(github.ref, 'refs/tags/') 37 | runs-on: macos-12 38 | steps: 39 | - name: Check out LittleBlueTooth 40 | uses: actions/checkout@v2 41 | - name: Build XCFrameworks 42 | run: | 43 | set -euo pipefail 44 | sudo xcode-select -s /Applications/Xcode_${XCODE_VER}.app 45 | swift build --sanitize="address" 46 | ./buildXCFramework.sh 47 | cd xcframeworks 48 | cp ../LICENSE . 49 | zip -9r LittleBlueTooth.xcframeworks.zip *.xcframework LICENSE 50 | - name: Upload files to release draft 51 | uses: marvinpinto/action-automatic-releases@latest 52 | with: 53 | repo_token: ${{ secrets.GITHUB_TOKEN }} 54 | prerelease: false 55 | draft: true 56 | files: xcframeworks/LittleBlueTooth.xcframeworks.zip 57 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/Classes/Error/LittleBlueToothError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LittleBlueToothError.swift 3 | // LittleBlueTooth 4 | // 5 | // Created by Andrea Finollo on 10/06/2020. 6 | // Copyright © 2020 Andrea Finollo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | #if TEST 11 | @preconcurrency import CoreBluetoothMock 12 | #else 13 | @preconcurrency import CoreBluetooth 14 | #endif 15 | 16 | 17 | /// Collection of errors that can be returned by LittleBlueTooth 18 | public enum LittleBluetoothError: Error { 19 | case bluetoothPoweredOff 20 | case bluetoothUnauthorized 21 | case bluetoothUnsupported 22 | case alreadyScanning 23 | case scanTimeout 24 | case connectTimeout 25 | case writeAndListenTimeout 26 | case readTimeout 27 | case writeTimeout 28 | case operationTimeout 29 | case invalidUUID(String) 30 | case serviceNotFound(Error?) 31 | case characteristicNotFound(Error?) 32 | case couldNotConnectToPeripheral(PeripheralIdentifier, Error?) 33 | case couldNotReadRSSI(Error) 34 | case couldNotReadFromCharacteristic(characteristic: CBUUID, error: Error) 35 | case couldNotWriteFromCharacteristic(characteristic: CBUUID, error: Error) 36 | case couldNotUpdateListenState(characteristic: CBUUID, error: Error) 37 | case emptyData 38 | case couldNotConvertDataToRead(data: Data, type: String) 39 | case peripheralNotConnected(state: PeripheralState) 40 | case peripheralAlreadyConnectedOrConnecting(Peripheral) 41 | case peripheralNotConnectedOrAlreadyDisconnected 42 | case peripheralNotFound 43 | case peripheralDisconnected(PeripheralIdentifier, Error?) 44 | case fullfillConditionNotRespected 45 | case deserializationFailedDataOfBounds(start: Int, length: Int, count: Int) 46 | } 47 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "LittleBlueTooth", 8 | platforms: [ 9 | // Add support for all platforms starting from a specific version. 10 | .macOS(.v10_15), 11 | .iOS(.v13), 12 | .watchOS(.v6), 13 | .tvOS(.v13) 14 | ], 15 | products: [ 16 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 17 | .library( 18 | name: "LittleBlueTooth", 19 | targets: ["LittleBlueTooth"]), 20 | .library( 21 | name: "LittleBlueToothForTest", 22 | targets: ["LittleBlueToothForTest"]) 23 | ], 24 | dependencies: [ 25 | // Dependencies declare other packages that this package depends on. 26 | .package(name: "CoreBluetoothMock", 27 | url: "https://github.com/NordicSemiconductor/IOS-CoreBluetooth-Mock.git", 28 | .upToNextMinor(from: "0.18.0")), 29 | ], 30 | targets: [ 31 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 32 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 33 | .target( 34 | name: "LittleBlueTooth", 35 | dependencies: [], 36 | exclude: ["Info.plist"] 37 | ), 38 | .target( 39 | name: "LittleBlueToothForTest", 40 | dependencies: ["CoreBluetoothMock"], 41 | exclude: ["Info.plist"], 42 | swiftSettings: [.define("TEST")] 43 | ), 44 | .testTarget( 45 | name: "LittleBlueToothTests", 46 | dependencies: ["LittleBlueToothForTest","CoreBluetoothMock"], 47 | exclude: ["Info.plist"] 48 | ) 49 | ] 50 | ) 51 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/Classes/Model/LittleBluetoothConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LittleBluetoothConfiguration.swift 3 | // LittleBlueTooth 4 | // 5 | // Created by Andrea Finollo on 26/07/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Pass a `Peripheral` and an evetual `LittleBluetoothError` and expect a boolean as an answer 11 | public typealias AutoconnectionHandler = (PeripheralIdentifier, LittleBluetoothError?) -> Bool 12 | 13 | /// Configuration object that must be passed during the `LittleBlueTooth` initialization 14 | public struct LittleBluetoothConfiguration { 15 | /// `CBCentralManager` options dictionary for instance the restore identifier, thay are the same 16 | /// requested for `CBCentralManager` 17 | public var centralManagerOptions: [String : Any]? 18 | /// `CBCentralManager` queue 19 | public var centralManagerQueue: DispatchQueue? 20 | /// This handler must be used to handle connection process after a disconnession. 21 | /// You can inspect the error and decide if an automatic connection is necessary. 22 | /// If you return `true` the connection process will start, once the peripheral has been found a connection will be established. 23 | /// If you return `false` the system will not try to establish a connection 24 | /// Connection process will remain active also in background if the app has the right 25 | /// permission, to cancel just call `disconnect`. 26 | /// When a connection will be established an `.autoConnected(PeripheralIdentifier)` event will be streamed to 27 | /// the `connectionEventPublisher` 28 | public var autoconnectionHandler: AutoconnectionHandler? 29 | /// Handler used to manage state restoration. `Restored` object will contain the restored information 30 | /// could be a peripheral, a scan or nothing 31 | public var restoreHandler: ((Restored) -> Void)? 32 | /// Enable logging, log is made using os_log and it exposes some information even in release configuration 33 | public var isLogEnabled = false 34 | 35 | public init() {} 36 | } 37 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/Classes/Extension/Helper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Helper.swift 3 | // LittleBlueTooth 4 | // 5 | // Created by Andrea Finollo on 09/08/2020. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import os.log 11 | #if TEST 12 | import CoreBluetoothMock 13 | #else 14 | import CoreBluetooth 15 | #endif 16 | 17 | extension AnyCancellable { 18 | func store(in dictionary: inout [UUID : AnyCancellable], 19 | for key: UUID) { 20 | dictionary[key] = self 21 | } 22 | } 23 | extension Publisher { 24 | /// Republishes elements sent by the most recently received publisher. 25 | func flatMapLatest(_ transform: @escaping (Self.Output) -> T) -> AnyPublisher where T.Failure == Self.Failure { 26 | return map(transform).switchToLatest().eraseToAnyPublisher() 27 | } 28 | } 29 | 30 | extension TimeInterval { 31 | /// Get a `DispatchTimeInterval` from a TimeInterval. 32 | public var dispatchInterval: DispatchTimeInterval { 33 | let microseconds = Int64(self * TimeInterval(USEC_PER_SEC)) // perhaps use nanoseconds, though would more often be > Int.max 34 | return microseconds < Int.max ? DispatchTimeInterval.microseconds(Int(microseconds)) : DispatchTimeInterval.seconds(Int(self)) 35 | } 36 | } 37 | 38 | extension OSLog { 39 | public static let Subsystem = "it.vanillagorilla.LittleBlueTooth" 40 | public static let General = "General" 41 | public static let CentralManager = "CentralManager" 42 | public static let Peripheral = "Peripheral" 43 | public static let Restore = "Restore" 44 | 45 | public static let LittleBT_Log_General = OSLog(subsystem: Subsystem, category: General) 46 | public static let LittleBT_Log_CentralManager = OSLog(subsystem: Subsystem, category: CentralManager) 47 | public static let LittleBT_Log_Peripheral = OSLog(subsystem: Subsystem, category: Peripheral) 48 | public static let LittleBT_Log_Restore = OSLog(subsystem: Subsystem, category: Restore) 49 | 50 | } 51 | #if TEST 52 | extension CBMPeripheral { 53 | public var description: String { 54 | return "Test peripheral" 55 | } 56 | } 57 | #endif 58 | -------------------------------------------------------------------------------- /LittleBlueTooth.xcodeproj/xcuserdata/Andrea.xcuserdatad/xcschemes/LittleBlueToothTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 26 | 27 | 28 | 38 | 39 | 45 | 46 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/Classes/Utilities/ShareAndReplayOperator/ReplaySubject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | ///The ShareReplay is used to share a single subscription to the upstream publisher and replay items emitted by that one. 4 | public final class ReplaySubject: Subject { 5 | private var buffer = [Output]() 6 | private let bufferSize: Int 7 | private var subscriptions = [ReplaySubjectSubscription]() 8 | private var completion: Subscribers.Completion? 9 | private let lock = NSRecursiveLock() 10 | 11 | /// Initialize a `ReplaySubject` subject with a buffer size 12 | public init(_ bufferSize: Int = 0) { 13 | self.bufferSize = bufferSize 14 | } 15 | 16 | /// Provides this Subject an opportunity to establish demand for any new upstream subscriptions 17 | public func send(subscription: Subscription) { 18 | lock.lock(); defer { lock.unlock() } 19 | subscription.request(.unlimited) 20 | } 21 | 22 | /// Sends a value to the subscriber. 23 | public func send(_ value: Output) { 24 | lock.lock(); defer { lock.unlock() } 25 | buffer.append(value) 26 | buffer = buffer.suffix(bufferSize) 27 | subscriptions.forEach { $0.receive(value) } 28 | } 29 | 30 | /// Sends a completion signal to the subscriber. 31 | public func send(completion: Subscribers.Completion) { 32 | lock.lock(); defer { lock.unlock() } 33 | self.completion = completion 34 | subscriptions.forEach { subscription in subscription.receive(completion: completion) } 35 | } 36 | 37 | /// This function is called to attach the specified `Subscriber` to the`Publisher 38 | public func receive(subscriber: Downstream) where Downstream.Failure == Failure, Downstream.Input == Output { 39 | lock.lock(); defer { lock.unlock() } 40 | let subscription = ReplaySubjectSubscription(downstream: AnySubscriber(subscriber)) 41 | subscriber.receive(subscription: subscription) 42 | subscriptions.append(subscription) 43 | subscription.replay(buffer, completion: completion) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/Classes/Utilities/Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utilities.swift 3 | // LittleBlueTooth 4 | // 5 | // Created by Andrea Finollo on 12/06/2020. 6 | // Copyright © 2020 Andrea Finollo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | public extension Data { 11 | /// Deserialize a range of `Data` into a specific type 12 | /// - parameter start: start position 13 | /// - parameter lenght: number of bytes (8 bit) from start that you want to keep in range 14 | /// - returns: Deserializaztion into a specific type. 15 | func extract(start: Int, length: Int) throws -> T { 16 | if start + length > self.count { 17 | throw LittleBluetoothError.deserializationFailedDataOfBounds(start: start, length: length, count: self.count) 18 | } 19 | return self.subdata(in: start.. Data { 45 | var data = Data() 46 | 47 | writables.forEach { (bite) in 48 | data.append(bite.data) 49 | } 50 | 51 | return data 52 | } 53 | } 54 | 55 | extension OptionSet where RawValue: FixedWidthInteger { 56 | 57 | func elements() -> AnySequence { 58 | var remainingBits = rawValue 59 | var bitMask: RawValue = 1 60 | return AnySequence { 61 | return AnyIterator { 62 | while remainingBits != 0 { 63 | defer { bitMask = bitMask &* 2 } 64 | if remainingBits & bitMask != 0 { 65 | remainingBits = remainingBits & ~bitMask 66 | return Self(rawValue: bitMask) 67 | } 68 | } 69 | return nil 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /LittleBlueTooth.xcodeproj/xcshareddata/xcschemes/LittleBlueTooth.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 35 | 36 | 40 | 41 | 42 | 43 | 44 | 45 | 55 | 56 | 62 | 63 | 69 | 70 | 71 | 72 | 74 | 75 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/Classes/Model/CentralRestorer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CentralRestorer.swift 3 | // LittleBlueTooth 4 | // 5 | // Created by Andrea Finollo on 15/07/2020. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | #if TEST 11 | import CoreBluetoothMock 12 | #else 13 | import CoreBluetooth 14 | #endif 15 | /** 16 | This object contains parsed information passed from the `centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any])` method of `CBCentralManagerDelegate` dictionary 17 | */ 18 | public struct CentralRestorer { 19 | public unowned let centralManager: CBCentralManager 20 | public let restoredInfo: [String : Any] 21 | 22 | 23 | /// Array of `PeripheralIdentifier` objects which have been restored. 24 | /// These are peripherals that were connected to the central manager (or had a connection pending) 25 | /// at the time the app was terminated by the system. 26 | public var peripherals: [PeripheralIdentifier] { 27 | if let peripherals = restoredInfo[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] { 28 | return centralManager.retrievePeripherals(withIdentifiers: peripherals.map{$0.identifier}).map {PeripheralIdentifier(peripheral: $0)} 29 | } 30 | return [] 31 | } 32 | 33 | /// Dictionary that contains all of the peripheral scan options that were being used 34 | /// by the central manager at the time the app was terminated by the system. 35 | public var scanOptions: [String: AnyObject] { 36 | if let info = restoredInfo[CBCentralManagerRestoredStateScanOptionsKey] as? [String: AnyObject] { 37 | return info 38 | } 39 | return [:] 40 | } 41 | 42 | /// Array of `CBUUID` objects of services which have been restored. 43 | /// These are all the services the central manager was scanning for at the time the app 44 | /// was terminated by the system. 45 | public var services: [CBUUID] { 46 | if let servicesUUID = restoredInfo[CBCentralManagerRestoredStateScanServicesKey] as? [CBUUID] { 47 | return servicesUUID 48 | } 49 | return [] 50 | } 51 | } 52 | 53 | extension CentralRestorer: CustomDebugStringConvertible { 54 | /// Extended description of the `CentralRestorer` 55 | public var debugDescription: String { 56 | return """ 57 | Peripherals: \(peripherals) 58 | Scan options: \(scanOptions) 59 | Services: \(services) 60 | """ 61 | } 62 | } 63 | 64 | /** 65 | This object contains the restored action during state restoration 66 | */ 67 | public enum Restored: CustomDebugStringConvertible { 68 | /// Peripherals scan has been restored 69 | case scan(discoveryPublisher: AnyPublisher) 70 | /// Peripheral has been restored 71 | case peripheral(Peripheral) 72 | /// Nothing has been restored 73 | case nothing 74 | /// Extended description of the `Restored` object 75 | public var debugDescription: String { 76 | switch self { 77 | case .scan(_): 78 | return "Restored Scan" 79 | case .peripheral(let periph): 80 | return "Restored \(periph.debugDescription)" 81 | case .nothing: 82 | return "Nothing to be restored" 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Tests/LittleBlueToothTests/WriteWithoutResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WriteWithoutResponse.swift 3 | // LittleBlueToothTests 4 | // 5 | // Created by Andrea Finollo on 28/07/2020. 6 | // 7 | 8 | import XCTest 9 | import CoreBluetoothMock 10 | import Combine 11 | @testable import LittleBlueToothForTest 12 | 13 | class WriteWithoutResponse: LittleBlueToothTests { 14 | 15 | override func setUpWithError() throws { 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | try super.setUpWithError() 18 | var lttlCon = LittleBluetoothConfiguration() 19 | lttlCon.isLogEnabled = true 20 | littleBT = LittleBlueTooth(with: lttlCon) 21 | } 22 | 23 | override func tearDownWithError() throws { 24 | // Put teardown code here. This method is called after the invocation of each test method in the class. 25 | } 26 | 27 | func testWriteWOResponse() { 28 | disposeBag.removeAll() 29 | blinky.simulateProximityChange(.outOfRange) 30 | blinkyWOR.simulateProximityChange(.immediate) 31 | let charateristic = LittleBlueToothCharacteristic(characteristic: CBMUUID.ledCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString, properties: [.notify, .read, .write]) 32 | let writeWOResp = expectation(description: "Write without response expectation") 33 | 34 | var data = Data() 35 | (0..<23).forEach { (val) in 36 | data.append(val) 37 | } 38 | littleBT.startDiscovery(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : false]) 39 | .flatMap { discovery in 40 | self.littleBT.connect(to: discovery) 41 | } 42 | .flatMap { _ in 43 | self.littleBT.write(to: charateristic, value: data, response: false) 44 | } 45 | .sink(receiveCompletion: { completion in 46 | print("Completion \(completion)") 47 | }) { (answer) in 48 | print("Answer \(answer)") 49 | self.littleBT.disconnect().sink(receiveCompletion: {_ in 50 | }) { (_) in 51 | writeWOResp.fulfill() 52 | } 53 | .store(in: &self.disposeBag) 54 | 55 | } 56 | .store(in: &disposeBag) 57 | waitForExpectations(timeout: 10) 58 | } 59 | 60 | func testWriteWOResponseMoreBuffer() { 61 | disposeBag.removeAll() 62 | blinky.simulateProximityChange(.outOfRange) 63 | blinkyWOR.simulateProximityChange(.immediate) 64 | let charateristic = LittleBlueToothCharacteristic(characteristic: CBMUUID.ledCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString, properties: [.notify, .read, .write]) 65 | let writeWOResp = expectation(description: "Write without response expectation") 66 | 67 | var data = Data() 68 | (0..<30).forEach { (val) in 69 | data.append(val) 70 | } 71 | littleBT.startDiscovery(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : false]) 72 | .flatMap { discovery in 73 | self.littleBT.connect(to: discovery) 74 | } 75 | .flatMap { _ in 76 | self.littleBT.write(to: charateristic, value: data, response: false) 77 | } 78 | .sink(receiveCompletion: { completion in 79 | print("Completion \(completion)") 80 | }) { (answer) in 81 | print("Answer \(answer)") 82 | self.littleBT.disconnect().sink(receiveCompletion: {_ in 83 | }) { (_) in 84 | writeWOResp.fulfill() 85 | } 86 | .store(in: &self.disposeBag) 87 | 88 | } 89 | .store(in: &disposeBag) 90 | waitForExpectations(timeout: 10) 91 | } 92 | 93 | 94 | } 95 | -------------------------------------------------------------------------------- /Tests/LittleBlueToothTests/StateRestoration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StateRestoration.swift 3 | // LittleBlueToothTests 4 | // 5 | // Created by Andrea Finollo on 15/07/2020. 6 | // 7 | 8 | import XCTest 9 | import Combine 10 | import CoreBluetoothMock 11 | @testable import LittleBlueToothForTest 12 | 13 | 14 | class StateRestoration: LittleBlueToothTests { 15 | let fakeCBUUID = CBUUID(nsuuid: UUID()) 16 | 17 | override func setUpWithError() throws { 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | try super.setUpWithError() 20 | } 21 | 22 | override func tearDownWithError() throws { 23 | // Put teardown code here. This method is called after the invocation of each test method in the class. 24 | } 25 | 26 | func testStateRestore() { 27 | let discoveryExpectation = expectation(description: "Discovery expectation") 28 | 29 | blinky.simulateProximityChange(.immediate) 30 | 31 | var littleBTConf = LittleBluetoothConfiguration() 32 | littleBTConf.isLogEnabled = true 33 | littleBTConf.centralManagerOptions = [CBMCentralManagerOptionRestoreIdentifierKey : "myIdentifier"] 34 | littleBT = LittleBlueTooth(with: littleBTConf) 35 | 36 | var periph: [PeripheralIdentifier]? 37 | var scanOptions: [String : Any]? 38 | var scanServices: [CBUUID]? 39 | 40 | var discoveredPeri: CBPeripheral? 41 | littleBT.startDiscovery(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : false]) 42 | .sink(receiveCompletion: { completion in 43 | print("Completion \(completion)") 44 | }) { (discov) in 45 | print("Discovery \(discov)") 46 | discoveredPeri = discov.cbPeripheral 47 | self.littleBT.stopDiscovery() 48 | .sink(receiveCompletion: {_ in 49 | }) { () in 50 | discoveryExpectation.fulfill() 51 | } 52 | .store(in: &self.disposeBag) 53 | } 54 | .store(in: &disposeBag) 55 | 56 | waitForExpectations(timeout: 10) 57 | 58 | let restoreExpectation = expectation(description: "State restoration") 59 | 60 | CBMCentralManagerMock.simulateStateRestoration = { (_) -> [String : Any] in 61 | return [ 62 | CBCentralManagerRestoredStatePeripheralsKey : [discoveredPeri], 63 | CBCentralManagerRestoredStateScanOptionsKey : [CBCentralManagerScanOptionAllowDuplicatesKey : false], 64 | CBCentralManagerRestoredStateScanServicesKey : [self.fakeCBUUID] 65 | ] 66 | } 67 | 68 | littleBTConf = LittleBluetoothConfiguration() 69 | littleBTConf.isLogEnabled = true 70 | littleBTConf.restoreHandler = { restore in 71 | print("Restorer \(restore)") 72 | } 73 | littleBTConf.centralManagerOptions = [CBMCentralManagerOptionRestoreIdentifierKey : "myIdentifier"] 74 | littleBT = LittleBlueTooth(with: littleBTConf) 75 | 76 | littleBT.restoreStatePublisher 77 | .sink { (restorer) in 78 | print(restorer) 79 | periph = restorer.peripherals 80 | scanOptions = restorer.scanOptions 81 | scanServices = restorer.services 82 | restoreExpectation.fulfill() 83 | } 84 | .store(in: &disposeBag) 85 | 86 | waitForExpectations(timeout: 10) 87 | 88 | XCTAssertNotNil(periph) 89 | XCTAssertNotNil(scanOptions) 90 | XCTAssertNotNil(scanServices) 91 | XCTAssert(scanServices!.count == 1) 92 | XCTAssert(scanServices!.first! == fakeCBUUID) 93 | XCTAssert((scanOptions![CBCentralManagerScanOptionAllowDuplicatesKey] as! Bool) == false) 94 | 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/Classes/Model/AdvertisingData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdvertisingData.swift 3 | // LittleBlueTooth 4 | // 5 | // Created by Andrea Finollo on 10/06/2020. 6 | // Copyright © 2020 Andrea Finollo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | #if TEST 11 | import CoreBluetoothMock 12 | #else 13 | import CoreBluetooth 14 | #endif 15 | 16 | /// A wrapper around the advertisement data returned from a BLE device. 17 | public struct AdvertisingInfo { 18 | public let advertisementData: [String: Any] 19 | 20 | /// Creates advertisement data based on CoreBluetooth's dictionary 21 | /// - parameter advertisementData: Core Bluetooth's advertisement data 22 | public init(advertisementData: [String: Any]) { 23 | self.advertisementData = advertisementData 24 | } 25 | 26 | /// A string containing the local name of a peripheral. 27 | public var localName: String? { 28 | return advertisementData[CBAdvertisementDataLocalNameKey] as? String 29 | } 30 | 31 | /// A Data object containing the manufacturer data of a peripheral. 32 | public var manufacturerData: Data? { 33 | return advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data 34 | } 35 | 36 | /// A dictionary containing service-specific advertisement data. 37 | /// The keys are CBUUID objects, representing CBService UUIDs. The values are Data objects, 38 | /// representing service-specific data. 39 | public var serviceData: [CBUUID: Data]? { 40 | return advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data] 41 | } 42 | 43 | /// An array of service UUIDs. 44 | public var serviceUUIDs: [CBUUID]? { 45 | return advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] 46 | } 47 | 48 | /// An array of one or more CBUUID objects, representing CBService UUIDs that were found in the “overflow” 49 | /// area of the advertisement data. 50 | public var overflowServiceUUIDs: [CBUUID]? { 51 | return advertisementData[CBAdvertisementDataOverflowServiceUUIDsKey] as? [CBUUID] 52 | } 53 | 54 | /// A number (an instance of NSNumber) containing the transmit power of a peripheral. 55 | /// This key and value are available if the broadcaster (peripheral) 56 | /// provides its Tx power level in its advertising packet. 57 | /// Using the RSSI value and the Tx power level, it is possible to calculate path loss. 58 | public var txPowerLevel: NSNumber? { 59 | return advertisementData[CBAdvertisementDataTxPowerLevelKey] as? NSNumber 60 | } 61 | 62 | /// A Boolean value that indicates whether the advertising event type is connectable. 63 | /// The value for this key is an NSNumber object. You can use this value to determine whether 64 | /// a peripheral is connectable at a particular moment. 65 | public var isConnectable: Bool? { 66 | return advertisementData[CBAdvertisementDataIsConnectable] as? Bool 67 | } 68 | 69 | /// An array of one or more CBUUID objects, representing CBService UUIDs. 70 | public var solicitedServiceUUIDs: [CBUUID]? { 71 | return advertisementData[CBAdvertisementDataSolicitedServiceUUIDsKey] as? [CBUUID] 72 | } 73 | } 74 | 75 | extension AdvertisingInfo: CustomDebugStringConvertible { 76 | 77 | public var debugDescription: String { 78 | return """ 79 | Name: \(localName ?? "Not available") 80 | Manufacturer: \(manufacturerData?.description ?? "Not available") 81 | Service Data: \(serviceData ?? [:]) 82 | ServiceUUID: \(serviceUUIDs ?? []) 83 | OverflowService: \(overflowServiceUUIDs ?? []) 84 | TX: \(txPowerLevel?.stringValue ?? "Not available") 85 | Connectable: \(isConnectable?.description ?? "Not available") 86 | SolicitedService: \(solicitedServiceUUIDs ?? []) 87 | """ 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/Classes/Model/PeripheralDiscovery.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PeripheralDiscovery.swift 3 | // LittleBlueTooth 4 | // 5 | // Created by Andrea Finollo on 10/06/2020. 6 | // Copyright © 2020 Andrea Finollo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | #if TEST 11 | @preconcurrency import CoreBluetoothMock 12 | #else 13 | @preconcurrency import CoreBluetooth 14 | #endif 15 | 16 | 17 | 18 | public protocol PeripheralIdentifiable: Identifiable { 19 | var id: UUID {get set} 20 | var name: String? {get set} 21 | } 22 | /// An object that contains the unique identifier of the `CBPeripheral` and the name of it (if present) 23 | public struct PeripheralIdentifier: PeripheralIdentifiable, @unchecked Sendable { 24 | /// The `UUID`of the peripheral 25 | public var id: UUID 26 | /// The name of the peripheral 27 | public var name: String? 28 | /// The wrapped `CBPeripheral` 29 | public var cbPeripheral: CBPeripheral? 30 | 31 | /// Initialize a `PeripheralIdentifier` using a `CBPeripheral` 32 | public init(peripheral: CBPeripheral) { 33 | self.id = peripheral.identifier 34 | self.name = peripheral.name 35 | self.cbPeripheral = peripheral 36 | } 37 | /// Initialize a `PeripheralIdentifier`. 38 | /// - parameter uuid: the `UUID` of a peripheral. 39 | /// - parameter name: the name of a peripheral 40 | /// - returns: An instance of `PeripheralIdentifier`. 41 | public init(uuid: UUID, name: String? = nil) { 42 | self.id = uuid 43 | self.name = name 44 | } 45 | /// Initialize a `PeripheralIdentifier`. 46 | /// - parameter string: the uuid in string of a peripheral. 47 | /// - parameter name: the name of a peripheral 48 | /// - throws: and error is thrown if the converstion string->UUID fails 49 | /// - returns: An instance of `PeripheralIdentifier`. 50 | public init(string: String, name: String? = nil) throws { 51 | if let id = UUID(uuidString: string) { 52 | self.init(uuid: id, name: name) 53 | } else { 54 | throw LittleBluetoothError.invalidUUID(string) 55 | } 56 | } 57 | } 58 | 59 | extension PeripheralIdentifier: CustomStringConvertible { 60 | /// Extended description 61 | public var description: String { 62 | return """ 63 | UUID: \(id) 64 | Name: \(name ?? "not availbale") 65 | """ 66 | } 67 | } 68 | 69 | /** 70 | An object that contains the unique identifier of the `CBPeripheral`, the name of it (if present) and the advertising info. 71 | */ 72 | public struct PeripheralDiscovery: PeripheralIdentifiable { 73 | /// The `UUID` of the discovery 74 | public var id: UUID 75 | /// The name of the discovery 76 | public var name: String? 77 | /// The wrapped `CBPeripheral` of the discovery 78 | public let cbPeripheral: CBPeripheral 79 | /// The wrapped `AdvertisingInfo` of the discovery 80 | public let advertisement: AdvertisingInfo 81 | /// The wrapped rssi of the discovery 82 | public let rssi: Int 83 | /// Initialize a `PeripheralDiscovery`. 84 | /// - parameter peripheral: the `CBPeripheral` that you want to wrap 85 | /// - parameter advertisement: the advertising info as they are returned from `CBManager` 86 | /// - parameter rssi: the rssi iof the `CBPeripheral` 87 | /// - returns: An instance of `PeripheralDiscovery`. 88 | init(_ peripheral: CBPeripheral, advertisement: [String : Any], rssi: NSNumber) { 89 | self.cbPeripheral = peripheral 90 | self.name = peripheral.name 91 | self.id = peripheral.identifier 92 | self.rssi = rssi.intValue 93 | self.advertisement = AdvertisingInfo(advertisementData: advertisement) 94 | } 95 | } 96 | 97 | extension PeripheralDiscovery: CustomDebugStringConvertible { 98 | /// Extended description of the discovery 99 | public var debugDescription: String { 100 | return """ 101 | Name: \(name ?? "not available") 102 | CB Peripheral: \(cbPeripheral) 103 | Adv: \(advertisement.debugDescription) 104 | RSSI: \(rssi) 105 | """ 106 | } 107 | 108 | 109 | } 110 | 111 | extension PeripheralIdentifier: Equatable, Hashable { 112 | public static func == (lhs: Self, rhs: Self) -> Bool { 113 | if lhs.id == rhs.id { 114 | return true 115 | } 116 | return false 117 | } 118 | 119 | public func hash(into hasher: inout Hasher) { 120 | hasher.combine(id) 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/Classes/CustomOperator/Listen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Listen.swift 3 | // LittleBlueTooth 4 | // 5 | // Created by Andrea Finollo on 26/08/2020. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import os.log 11 | #if TEST 12 | import CoreBluetoothMock 13 | #else 14 | import CoreBluetooth 15 | #endif 16 | 17 | 18 | // MARK: - Listen 19 | 20 | extension Publisher where Self.Failure == LittleBluetoothError { 21 | 22 | /// Returns a publisher with the `LittleBlueToothCharacteristic` where the notify command has been activated. 23 | /// After starting the listen command you should subscribe to the `listenPublisher` to be notified. 24 | /// - parameter littleBluetooth: the `LittleBlueTooth` instance 25 | /// - parameter characteristic: Characteristc you want to be notified. 26 | /// - returns: A publisher with the `LittleBlueToothCharacteristic` where the notify command has been activated. 27 | /// - important: This publisher only activate the notification on a specific characteristic, it will not send notified values. 28 | /// After starting the listen command you should subscribe to the `listenPublisher` to be notified. 29 | public func enableListen(for littleBluetooth: LittleBlueTooth, 30 | from characteristic: LittleBlueToothCharacteristic) -> AnyPublisher { 31 | 32 | func enableListen(upstream: Upstream, 33 | for littleBluetooth: LittleBlueTooth, 34 | from characteristic: LittleBlueToothCharacteristic) -> AnyPublisher where Upstream.Failure == LittleBluetoothError { 35 | return upstream 36 | .flatMapLatest { _ in 37 | littleBluetooth.enableListen(from: characteristic) 38 | } 39 | } 40 | 41 | return enableListen(upstream: self, 42 | for: littleBluetooth, 43 | from: characteristic) 44 | } 45 | 46 | /// Returns a shared publisher for listening to a specific characteristic. 47 | /// - parameter littleBluetooth: the `LittleBlueTooth` instance 48 | /// - parameter characteristic: Characteristc you want to be notified. 49 | /// - returns: A shared publisher that will send out values of the type defined by the generic type. 50 | /// - important: The type of the value must be conform to `Readable` 51 | public func startListen(for littleBluetooth: LittleBlueTooth, 52 | from charact: LittleBlueToothCharacteristic) -> AnyPublisher { 53 | 54 | func startListen(upstream: Upstream, 55 | for littleBluetooth: LittleBlueTooth, 56 | from charact: LittleBlueToothCharacteristic) -> AnyPublisher where Upstream.Failure == LittleBluetoothError { 57 | return upstream 58 | .flatMapLatest { _ in 59 | littleBluetooth.startListen(from: charact) 60 | } 61 | } 62 | 63 | return startListen(upstream: self, 64 | for: littleBluetooth, 65 | from: charact) 66 | } 67 | 68 | /// Disable listen from a specific characteristic 69 | /// - parameter characteristic: characteristic you want to stop listen 70 | /// - returns: A publisher with that informs you about the successful or failed task 71 | public func disableListen(for littleBluetooth: LittleBlueTooth, 72 | from characteristic: LittleBlueToothCharacteristic) -> AnyPublisher { 73 | func disableListen(upstream: Upstream, 74 | for littleBluetooth: LittleBlueTooth, 75 | from characteristic: LittleBlueToothCharacteristic) -> AnyPublisher where Upstream.Failure == LittleBluetoothError { 76 | return upstream 77 | .flatMapLatest { _ in 78 | littleBluetooth.disableListen(from: characteristic) 79 | } 80 | } 81 | return disableListen(upstream: self, 82 | for: littleBluetooth, 83 | from: characteristic) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Tests/LittleBlueToothTests/Extraction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extraction.swift 3 | // LittleBlueToothTests 4 | // 5 | // Created by Andrea Finollo on 09/08/2020. 6 | // 7 | 8 | import XCTest 9 | import CoreBluetoothMock 10 | import Combine 11 | @testable import LittleBlueToothForTest 12 | 13 | class Extraction: LittleBlueToothTests { 14 | 15 | override func setUpWithError() throws { 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | try super.setUpWithError() 18 | var configuration = LittleBluetoothConfiguration() 19 | configuration.isLogEnabled = true 20 | littleBT = LittleBlueTooth(with: configuration) 21 | 22 | } 23 | 24 | override func tearDownWithError() throws { 25 | // Put teardown code here. This method is called after the invocation of each test method in the class. 26 | } 27 | 28 | func testExtractionWithPeriph() { 29 | disposeBag.removeAll() 30 | var configuration = LittleBluetoothConfiguration() 31 | configuration.isLogEnabled = true 32 | littleBT = LittleBlueTooth(with: configuration) 33 | 34 | blinky.simulateProximityChange(.immediate) 35 | let extractionExpectation = expectation(description: "Extraction expectation") 36 | 37 | var connectedPeripheral: Peripheral? 38 | var extractedState: (central: CBCentralManager, peripheral: CBPeripheral?)? 39 | 40 | littleBT.startDiscovery(withServices: nil) 41 | .flatMap { discovery in 42 | self.littleBT.connect(to: discovery) 43 | } 44 | .sink(receiveCompletion: { completion in 45 | print("Completion \(completion)") 46 | }) { (connectedPeriph) in 47 | print("Discovery \(connectedPeriph)") 48 | connectedPeripheral = connectedPeriph 49 | // Extract state 50 | extractedState = self.littleBT.extract() 51 | extractionExpectation.fulfill() 52 | } 53 | .store(in: &disposeBag) 54 | 55 | waitForExpectations(timeout: 15) 56 | XCTAssertNotNil(connectedPeripheral) 57 | XCTAssertEqual(connectedPeripheral!.cbPeripheral.identifier, blinky.identifier) 58 | XCTAssertNotNil(extractedState) 59 | XCTAssertEqual(extractedState!.peripheral!.identifier, blinky.identifier) 60 | XCTAssertEqual(extractedState!.peripheral!.state, CBPeripheralState.connected) 61 | self.littleBT.disconnect() 62 | 63 | } 64 | 65 | func testExtractionWithoutPeriph() { 66 | disposeBag.removeAll() 67 | var configuration = LittleBluetoothConfiguration() 68 | configuration.isLogEnabled = true 69 | littleBT = LittleBlueTooth(with: configuration) 70 | 71 | let extractedState = self.littleBT.extract() 72 | 73 | XCTAssertNil(extractedState.peripheral) 74 | XCTAssertNotNil(extractedState.central) 75 | } 76 | 77 | func testRestart() { 78 | disposeBag.removeAll() 79 | var configuration = LittleBluetoothConfiguration() 80 | configuration.isLogEnabled = true 81 | littleBT = LittleBlueTooth(with: configuration) 82 | blinky.simulateDisconnection() 83 | blinky.simulateProximityChange(.immediate) 84 | let restartExpectation = expectation(description: "Restart expectation") 85 | 86 | var connectedPeripheral: Peripheral? 87 | var extractedState: (central: CBCentralManager, peripheral: CBPeripheral?)? 88 | 89 | littleBT.startDiscovery(withServices: nil) 90 | .flatMap { discovery in 91 | self.littleBT.connect(to: discovery) 92 | } 93 | .sink(receiveCompletion: { completion in 94 | print("Completion \(completion)") 95 | }) { (connectedPeriph) in 96 | print("Discovery \(connectedPeriph)") 97 | connectedPeripheral = connectedPeriph 98 | // Extract state 99 | extractedState = self.littleBT.extract() 100 | XCTAssertNotNil(connectedPeripheral) 101 | XCTAssertEqual(connectedPeripheral!.cbPeripheral.identifier, blinky.identifier) 102 | XCTAssertNotNil(extractedState) 103 | XCTAssertEqual(extractedState!.peripheral!.identifier, blinky.identifier) 104 | self.littleBT.restart(with: extractedState!.central, peripheral: extractedState!.peripheral!) 105 | restartExpectation.fulfill() 106 | } 107 | .store(in: &disposeBag) 108 | 109 | waitForExpectations(timeout: 10) 110 | XCTAssertNotNil(littleBT.peripheral) 111 | self.littleBT.disconnect() 112 | 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/CoreBluetoothTypeAliases.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, Nordic Semiconductor 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, 6 | * are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, this 12 | * list of conditions and the following disclaimer in the documentation and/or 13 | * other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of the copyright holder nor the names of its contributors may 16 | * be used to endorse or promote products derived from this software without 17 | * specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 22 | * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 23 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 24 | * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 26 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 | * POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | #if TEST 31 | import CoreBluetoothMock 32 | import Combine 33 | import Foundation 34 | // Copy this file to your project to start using CoreBluetoothMock classes 35 | // without having to refactor any of your code. You will just have to remove 36 | // the imports to CoreBluetooth to fix conflicts and initiate the manager 37 | // using CBCentralManagerFactory, instad of just creating a CBCentralManager. 38 | 39 | public typealias CBCentralManagerFactory = CBMCentralManagerFactory 40 | public typealias CBUUID = CBMUUID 41 | public typealias CBError = CBMError 42 | public typealias CBATTError = CBMATTError 43 | public typealias CBManagerState = CBMManagerState 44 | public typealias CBPeripheralState = CBMPeripheralState 45 | public typealias CBCentralManager = CBMCentralManager 46 | public typealias CBCentralManagerDelegate = CBMCentralManagerDelegate 47 | public typealias CBPeripheral = CBMPeripheral 48 | public typealias CBPeripheralDelegate = CBMPeripheralDelegate 49 | public typealias CBService = CBMService 50 | public typealias CBCharacteristic = CBMCharacteristic 51 | public typealias CBCharacteristicWriteType = CBMCharacteristicWriteType 52 | public typealias CBCharacteristicProperties = CBMCharacteristicProperties 53 | public typealias CBDescriptor = CBMDescriptor 54 | public typealias CBConnectionEvent = CBMConnectionEvent 55 | public typealias CBConnectionEventMatchingOption = CBMConnectionEventMatchingOption 56 | @available(iOS 11.0, tvOS 11.0, watchOS 4.0, *) 57 | public typealias CBL2CAPPSM = CBML2CAPPSM 58 | @available(iOS 11.0, tvOS 11.0, watchOS 4.0, *) 59 | public typealias CBL2CAPChannel = CBML2CAPChannel 60 | 61 | public let CBCentralManagerScanOptionAllowDuplicatesKey = CBMCentralManagerScanOptionAllowDuplicatesKey 62 | public let CBCentralManagerOptionShowPowerAlertKey = CBMCentralManagerOptionShowPowerAlertKey 63 | public let CBCentralManagerOptionRestoreIdentifierKey = CBMCentralManagerOptionRestoreIdentifierKey 64 | public let CBCentralManagerScanOptionSolicitedServiceUUIDsKey = CBMCentralManagerScanOptionSolicitedServiceUUIDsKey 65 | public let CBConnectPeripheralOptionStartDelayKey = CBMConnectPeripheralOptionStartDelayKey 66 | #if !os(macOS) 67 | @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *) 68 | public let CBConnectPeripheralOptionRequiresANCS = CBMConnectPeripheralOptionRequiresANCS 69 | #endif 70 | public let CBCentralManagerRestoredStatePeripheralsKey = CBMCentralManagerRestoredStatePeripheralsKey 71 | public let CBCentralManagerRestoredStateScanServicesKey = CBMCentralManagerRestoredStateScanServicesKey 72 | public let CBCentralManagerRestoredStateScanOptionsKey = CBMCentralManagerRestoredStateScanOptionsKey 73 | 74 | public let CBAdvertisementDataLocalNameKey = CBMAdvertisementDataLocalNameKey 75 | public let CBAdvertisementDataServiceUUIDsKey = CBMAdvertisementDataServiceUUIDsKey 76 | public let CBAdvertisementDataIsConnectable = CBMAdvertisementDataIsConnectable 77 | public let CBAdvertisementDataTxPowerLevelKey = CBMAdvertisementDataTxPowerLevelKey 78 | public let CBAdvertisementDataServiceDataKey = CBMAdvertisementDataServiceDataKey 79 | public let CBAdvertisementDataManufacturerDataKey = CBMAdvertisementDataManufacturerDataKey 80 | public let CBAdvertisementDataOverflowServiceUUIDsKey = CBMAdvertisementDataOverflowServiceUUIDsKey 81 | public let CBAdvertisementDataSolicitedServiceUUIDsKey = CBMAdvertisementDataSolicitedServiceUUIDsKey 82 | 83 | public let CBConnectPeripheralOptionNotifyOnConnectionKey = CBMConnectPeripheralOptionNotifyOnConnectionKey 84 | public let CBConnectPeripheralOptionNotifyOnDisconnectionKey = CBMConnectPeripheralOptionNotifyOnDisconnectionKey 85 | public let CBConnectPeripheralOptionNotifyOnNotificationKey = CBMConnectPeripheralOptionNotifyOnNotificationKey 86 | #endif 87 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/Classes/CustomOperator/ReadAndWrite.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadAndWrite.swift 3 | // LittleBlueTooth 4 | // 5 | // Created by Andrea Finollo on 26/08/2020. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import os.log 11 | #if TEST 12 | import CoreBluetoothMock 13 | #else 14 | import CoreBluetooth 15 | #endif 16 | 17 | extension Publisher where Self.Failure == LittleBluetoothError { 18 | // MARK: - RSSI 19 | /// Returns a publisher with the `Int`value of the RSSI. 20 | /// - parameter littleBluetooth: the `LittleBlueTooth` instance 21 | /// - returns: A publisher with the `Int` value of the RSSI. 22 | public func readRSSI(for littleBluetooth: LittleBlueTooth) -> AnyPublisher { 23 | 24 | func readRSSI(upstream: Upstream, 25 | for littleBluetooth: LittleBlueTooth) -> AnyPublisher where Upstream.Failure == LittleBluetoothError { 26 | return upstream 27 | .flatMapLatest { _ in 28 | littleBluetooth.readRSSI() 29 | } 30 | } 31 | return readRSSI(upstream: self, 32 | for: littleBluetooth) 33 | } 34 | 35 | // MARK: - Read 36 | 37 | /// Read a value from a specific charteristic 38 | /// - parameter littleBluetooth: the `LittleBlueTooth` instance 39 | /// - parameter characteristic: characteristic where you want to read 40 | /// - returns: A publisher with the value you want to read. 41 | /// - important: The type of the value must be conform to `Readable` 42 | public func read(for littleBluetooth: LittleBlueTooth, 43 | from characteristic: LittleBlueToothCharacteristic) -> AnyPublisher { 44 | 45 | func read(upstream: Upstream, 46 | for littleBluetooth: LittleBlueTooth, 47 | from characteristic: LittleBlueToothCharacteristic) -> AnyPublisher where Upstream.Failure == LittleBluetoothError { 48 | return upstream 49 | .flatMapLatest { _ in 50 | littleBluetooth.read(from: characteristic) 51 | } 52 | } 53 | 54 | return read(upstream: self, 55 | for: littleBluetooth, 56 | from: characteristic) 57 | } 58 | 59 | // MARK: - Write 60 | 61 | /// Write a value to a specific charteristic 62 | /// - parameter littleBluetooth: the `LittleBlueTooth` instance 63 | /// - parameter characteristic: characteristic where you want to write 64 | /// - parameter value: The value you want to write 65 | /// - parameter response: An optional `Bool` value that will look for error after write operation 66 | /// - returns: A publisher with that informs you about eventual error 67 | /// - important: The type of the value must be conform to `Writable` 68 | public func write(for littleBluetooth: LittleBlueTooth, 69 | to characteristic: LittleBlueToothCharacteristic, 70 | value: T, 71 | response: Bool = true) -> AnyPublisher { 72 | 73 | func write(upstream: Upstream, 74 | for littleBluetooth: LittleBlueTooth, 75 | to characteristic: LittleBlueToothCharacteristic, 76 | value: T, 77 | response: Bool = true) -> AnyPublisher where Upstream.Failure == LittleBluetoothError { 78 | return upstream 79 | .flatMapLatest { _ in 80 | littleBluetooth.write(to: characteristic, value: value, response: response) 81 | } 82 | } 83 | 84 | return write(upstream: self, 85 | for: littleBluetooth, 86 | to: characteristic, 87 | value: value, 88 | response: response) 89 | } 90 | 91 | /// Write a value to a specific charteristic and wait for a response 92 | /// - parameter littleBluetooth: the `LittleBlueTooth` instance 93 | /// - parameter characteristic: characteristic where you want to write and listen 94 | /// - parameter value: The value you want to write must conform to `Writable` 95 | /// - returns: A publisher with that post and error or the response of the write requests. 96 | /// - important: Written value must conform to `Writable`, response must conform to `Readable` 97 | public func writeAndListen(for littleBluetooth: LittleBlueTooth, 98 | from characteristic: LittleBlueToothCharacteristic, 99 | value: W) -> AnyPublisher { 100 | func writeAndListen(upstream: Upstream, 101 | for littleBluetooth: LittleBlueTooth, 102 | from characteristic: LittleBlueToothCharacteristic, 103 | value: W) -> AnyPublisher where Upstream.Failure == LittleBluetoothError { 104 | return upstream 105 | .flatMapLatest { _ in 106 | littleBluetooth.writeAndListen(from: characteristic, 107 | value: value) 108 | } 109 | } 110 | return writeAndListen(upstream: self, 111 | for: littleBluetooth, 112 | from: characteristic, 113 | value: value) 114 | } 115 | 116 | } 117 | 118 | -------------------------------------------------------------------------------- /Tests/LittleBlueToothTests/ScanDiscoveryTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScanDiscoveryTest.swift 3 | // LittleBlueToothTests 4 | // 5 | // Created by Andrea Finollo on 26/06/2020. 6 | // Copyright © 2020 Andrea Finollo. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Combine 11 | import CoreBluetoothMock 12 | @testable import LittleBlueToothForTest 13 | 14 | class ScanDiscoveryTest: LittleBlueToothTests { 15 | 16 | 17 | override func setUpWithError() throws { 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | try super.setUpWithError() 20 | var configuration = LittleBluetoothConfiguration() 21 | configuration.isLogEnabled = true 22 | littleBT = LittleBlueTooth(with: configuration) 23 | } 24 | 25 | override func tearDownWithError() throws { 26 | // Put teardown code here. This method is called after the invocation of each test method in the class. 27 | } 28 | 29 | 30 | func testPeripheralDiscoveryPowerOn() { 31 | disposeBag.removeAll() 32 | 33 | blinky.simulateProximityChange(.immediate) 34 | 35 | let discoveryExpectation = expectation(description: "Discovery expectation") 36 | var discovery: PeripheralDiscovery? 37 | 38 | littleBT.startDiscovery(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : false]) 39 | .sink(receiveCompletion: { completion in 40 | print("Completion \(completion)") 41 | }) { (discov) in 42 | print("Discovery \(discov)") 43 | discovery = discov 44 | self.littleBT.stopDiscovery() 45 | .sink(receiveCompletion: {_ in 46 | }) { () in 47 | discoveryExpectation.fulfill() 48 | } 49 | .store(in: &self.disposeBag) 50 | } 51 | .store(in: &disposeBag) 52 | 53 | waitForExpectations(timeout: 10) 54 | XCTAssertNotNil(discovery) 55 | _ = discovery!.name 56 | let peripheral = discovery!.cbPeripheral 57 | let advInfo = discovery!.advertisement 58 | XCTAssertEqual(discovery!.cbPeripheral.identifier, blinky.identifier) 59 | XCTAssertEqual(peripheral.identifier, blinky.identifier) 60 | XCTAssertNotNil(advInfo) 61 | } 62 | 63 | 64 | func testPeripheralDiscoveryPowerOff() { 65 | disposeBag.removeAll() 66 | CBMCentralManagerMock.simulateInitialState(.poweredOff) 67 | 68 | blinky.simulateProximityChange(.immediate) 69 | 70 | let discoveryExpectation = expectation(description: "Discovery Expectation") 71 | var isPowerOff = false 72 | littleBT.startDiscovery(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : false]) 73 | .sink(receiveCompletion: { completion in 74 | print("Completion \(completion)") 75 | switch completion { 76 | case .failure(let error): 77 | isPowerOff = false 78 | if case LittleBluetoothError.bluetoothPoweredOff = error { 79 | isPowerOff = true 80 | } 81 | self.littleBT.stopDiscovery() 82 | .sink(receiveCompletion: {_ in 83 | }) { () in 84 | discoveryExpectation.fulfill() 85 | } 86 | .store(in: &self.disposeBag) 87 | case .finished: 88 | break 89 | } 90 | }) { (discovery) in 91 | print("Discovery \(discovery)") 92 | } 93 | .store(in: &disposeBag) 94 | 95 | waitForExpectations(timeout: 10) 96 | XCTAssertTrue(isPowerOff) 97 | } 98 | 99 | func testPeripheralDiscoveryStopScan() { 100 | disposeBag.removeAll() 101 | 102 | blinky.simulateProximityChange(.immediate) 103 | let discoveryExpectation = expectation(description: "Discovery Expectation") 104 | var isScanning = true 105 | 106 | littleBT.startDiscovery(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : false]) 107 | .map { disc -> PeripheralDiscovery in 108 | print("Discovery discovery \(disc)") 109 | return disc 110 | } 111 | .flatMap {discovery in 112 | self.littleBT.stopDiscovery().map {discovery} 113 | } 114 | .sink(receiveCompletion: { completion in 115 | print("Completion \(completion)") 116 | }) { (answer) in 117 | print("Value \(answer)") 118 | isScanning = self.littleBT.cbCentral.isScanning 119 | discoveryExpectation.fulfill() 120 | } 121 | .store(in: &disposeBag) 122 | waitForExpectations(timeout: 10) 123 | XCTAssertFalse(isScanning) 124 | } 125 | 126 | func testPeripheralScanTimeout() { 127 | disposeBag.removeAll() 128 | 129 | blinky.simulateProximityChange(.outOfRange) 130 | let discoveryExpectation = expectation(description: "Discovery Expectation") 131 | 132 | var isScanTimeout = false 133 | let timeout = TimeInterval(3) 134 | littleBT.startDiscovery(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : false]) 135 | .timeout(DispatchQueue.SchedulerTimeType.Stride(timeout.dispatchInterval), scheduler: DispatchQueue.main, options: nil, error: .scanTimeout) 136 | .flatMap { discovery in 137 | self.littleBT.connect(to: discovery) 138 | } 139 | .sink(receiveCompletion: { completion in 140 | print("Completion \(completion)") 141 | switch completion { 142 | case .failure(let error): 143 | isScanTimeout = false 144 | if case LittleBluetoothError.scanTimeout = error { 145 | isScanTimeout = true 146 | } 147 | discoveryExpectation.fulfill() 148 | case .finished: 149 | break 150 | } 151 | }) { (connectedPeriph) in 152 | print("Connected periph: \(connectedPeriph)") 153 | } 154 | .store(in: &disposeBag) 155 | wait(for: [discoveryExpectation], timeout: 15) 156 | 157 | XCTAssert(isScanTimeout) 158 | 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Tests/LittleBlueToothTests/UtilityTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UtilityTest.swift 3 | // LittleBlueToothTests 4 | // 5 | // Created by Andrea Finollo on 05/07/2020. 6 | // Copyright © 2020 Andrea Finollo. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import CoreBluetoothMock 11 | import Combine 12 | import os.log 13 | @testable import LittleBlueToothForTest 14 | 15 | class UtilityTest: LittleBlueToothTests { 16 | 17 | override func setUpWithError() throws { 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | struct LedStateWrong: Readable { 26 | let isOn: Bool 27 | 28 | init(from data: Data) throws { 29 | let answer: Bool = try data.extract(start: 0, length: 4) 30 | self.isOn = answer 31 | } 32 | 33 | } 34 | 35 | func testReadable() { 36 | let ledState = try? LedState(from: Data([0x01])) 37 | XCTAssertNotNil(ledState) 38 | 39 | do { 40 | let _ = try LedStateWrong(from: Data([0x01])) 41 | } catch let error { 42 | if case LittleBluetoothError.deserializationFailedDataOfBounds(_,_,_) = error { 43 | XCTAssert(true) 44 | } else { 45 | XCTAssert(false) 46 | } 47 | } 48 | } 49 | 50 | struct WritableMock: Writable { 51 | var data: Data { 52 | return LittleBlueTooth.assemble([UInt8(0x01), UInt8(0x02)]) 53 | } 54 | 55 | } 56 | 57 | func testCharacteristicEquality() { 58 | let characteristicOne = LittleBlueToothCharacteristic(characteristic: CBMUUID.buttonCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString, properties: [.read, .notify]) 59 | let characteristicTwo = LittleBlueToothCharacteristic(characteristic: "00001524-1212-EFDE-1523-785FEABCD123", for: "00001523-1212-EFDE-1523-785FEABCD123", properties: [.read, .notify]) 60 | XCTAssert(characteristicOne == characteristicTwo) 61 | } 62 | 63 | func testCharacteristicEqualityFail() { 64 | let characteristicOne = LittleBlueToothCharacteristic(characteristic: CBMUUID.buttonCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString, properties: [.read, .notify]) 65 | let characteristicTwo = LittleBlueToothCharacteristic(characteristic: "00001524-1212-EFDE-1523-785FEABCD123", for: "00001523-1212-EFDE-1523-785FEABCD127", properties: [.read, .notify]) 66 | XCTAssertFalse(characteristicOne == characteristicTwo) 67 | } 68 | 69 | func testCharacteristicHash() { 70 | let characteristicOne = LittleBlueToothCharacteristic(characteristic: CBMUUID.buttonCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString, properties: [.read, .notify]) 71 | let characteristicTwo = LittleBlueToothCharacteristic(characteristic: "00001524-1212-EFDE-1523-785FEABCD123", for: "00001523-1212-EFDE-1523-785FEABCD123", properties: [.read, .notify]) 72 | XCTAssert(characteristicOne.hashValue == characteristicTwo.hashValue) 73 | } 74 | 75 | func testWritable() { 76 | let writable = WritableMock() 77 | XCTAssert(writable.data == Data([0x01, 0x02])) 78 | } 79 | 80 | func testExtension() { 81 | let uintdt = UInt8(0x01).data 82 | XCTAssert(uintdt == Data([0x01])) 83 | 84 | let dtuint = UInt8(from: Data([0x01])) 85 | XCTAssert(dtuint == 0x01) 86 | 87 | let data = Data(from: Data([0x01])) 88 | XCTAssert(data == Data([0x01])) 89 | 90 | let dataData = data.data 91 | XCTAssert(dataData == Data([0x01])) 92 | 93 | } 94 | 95 | func testPeripheralIdentifier() { 96 | let uuid = UUID() 97 | var periphId: PeripheralIdentifier? = PeripheralIdentifier(uuid: uuid, name: "foo") 98 | XCTAssertTrue(periphId!.name! == "foo") 99 | XCTAssertTrue(periphId!.id == uuid) 100 | 101 | periphId = PeripheralIdentifier(uuid: uuid) 102 | XCTAssertTrue(periphId!.id == uuid) 103 | 104 | periphId = try? PeripheralIdentifier(string: uuid.uuidString, name: "foo") 105 | XCTAssertNotNil(periphId) 106 | XCTAssertTrue(periphId!.name! == "foo") 107 | XCTAssertTrue(periphId!.id == uuid) 108 | 109 | periphId = try? PeripheralIdentifier(string: uuid.uuidString) 110 | XCTAssertNotNil(periphId) 111 | XCTAssertTrue(periphId!.id == uuid) 112 | 113 | 114 | 115 | var periphIdTwo = PeripheralIdentifier(uuid: periphId!.id) 116 | XCTAssertTrue(periphId == periphIdTwo) 117 | 118 | periphIdTwo = PeripheralIdentifier(uuid: UUID()) 119 | XCTAssertFalse(periphId == periphIdTwo) 120 | 121 | periphId = try? PeripheralIdentifier(string: "") 122 | XCTAssertNil(periphId) 123 | } 124 | 125 | func testShareReplay() { 126 | var event1: Set = [] 127 | var event2: Set = [] 128 | 129 | let cvs = CurrentValueSubject("Hello") 130 | 131 | let shareTest = 132 | cvs 133 | .shareReplay(1) 134 | .eraseToAnyPublisher() 135 | 136 | let sub1 = shareTest.sink(receiveValue: { value in 137 | event1.insert(value) 138 | print("subscriber1: \(value)\n") 139 | }) 140 | print("Sub1: \(sub1)") 141 | 142 | let sub2 = shareTest.sink(receiveValue: { value in 143 | event2.insert(value) 144 | print("subscriber2: \(value)\n") 145 | }) 146 | print("Sub2: \(sub2)") 147 | 148 | cvs.send("World") 149 | cvs.send(completion: .finished) 150 | cvs.send("Huge") 151 | 152 | XCTAssert(event1.count == 2) 153 | XCTAssert(event1 == event2) 154 | } 155 | 156 | // MARK: - Loggable 157 | 158 | /// Note that the current implementation does not actually `throw` but rather uses `assert`. Nevertheless 159 | /// wrapping with `XCTAssertNoThrow` accomplishes the goal of verifying the function behaviour while adding 160 | /// a tiny future proof should assert be replaced by a thrown Error. 161 | func testLoggableShouldAccept3Args() { 162 | 163 | class MockLogger: Loggable { 164 | var isLogEnabled = true 165 | } 166 | 167 | XCTAssertNoThrow( 168 | MockLogger().log("3 may pass", log: OSLog.LittleBT_Log_General, type: .info, 169 | arg: ["arg1", "arg2", "arg3"]) 170 | ) 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/Classes/Model/LittleBlueToothCharacteristic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Task.swift 3 | // LittleBlueTooth 4 | // 5 | // Created by Andrea Finollo on 10/06/2020. 6 | // Copyright © 2020 Andrea Finollo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | #if TEST 11 | import CoreBluetoothMock 12 | #else 13 | import CoreBluetooth 14 | #endif 15 | 16 | /// Type alias for a CBUUID string used to identify services 17 | public typealias LittleBlueToothServiceIndentifier = String 18 | /// Type alias for a CBUUID string used to identify characteristic 19 | public typealias LittleBlueToothCharacteristicIndentifier = String 20 | 21 | 22 | /// A representation of a bluetooth characteristic 23 | public struct LittleBlueToothCharacteristic: Identifiable { 24 | /// The `CBUUID` of the characteristic 25 | public let id: CBUUID 26 | /// The `CBUUID` of the service 27 | public let service: CBUUID 28 | /// Properties of the characteristic. They are mapped from `CBCharacteristicProperties` 29 | public let properties: Properties 30 | /// Inner value of the `CBCaharacteristic` 31 | public var rawValue: Data? { 32 | cbCharacteristic?.value 33 | } 34 | 35 | private var cbCharacteristic: CBCharacteristic? 36 | 37 | /// Initialize a `LittleBlueToothCharacteristic`. 38 | /// - parameter characteristic: the `LittleBlueToothCharacteristicIndentifier` instance, basically a string 39 | /// - parameter service: the `LittleBlueToothServiceIndentifier` instance, basically a string 40 | /// - parameter properties: an option set of properties 41 | /// - returns: An instance of `LittleBlueToothCharacteristic`. 42 | public init(characteristic: LittleBlueToothCharacteristicIndentifier, for service: LittleBlueToothServiceIndentifier, properties: LittleBlueToothCharacteristic.Properties) { 43 | self.id = CBUUID(string: characteristic) 44 | self.service = CBUUID(string: service) 45 | self.properties = properties 46 | } 47 | /// Initialize a `LittleBlueToothCharacteristic` from a `CBCharacteristic` 48 | /// - parameter characteristic: the `CBCharacteristic` instance that you want to use 49 | /// - returns: An instance of `LittleBlueToothCharacteristic`. 50 | public init(with characteristic: CBCharacteristic) { 51 | // Couldn't get rid of this orrible compiler flags but it is present to make work SPM build and Xcode build 52 | #if swift(>=5.5) 53 | guard let service = characteristic.service else { 54 | fatalError("There must be a service associated to the characteristic") 55 | } 56 | #else 57 | let service = characteristic.service 58 | #endif 59 | self.id = characteristic.uuid 60 | self.service = service.uuid 61 | self.cbCharacteristic = characteristic 62 | self.properties = Properties(properties: characteristic.properties) 63 | } 64 | 65 | /// A helper method to get a concrete value from the value contained in the characteristic. 66 | /// The type must conform to the `Readable` protocol 67 | /// - parameter characteristic: the `CBCharacteristic` instance that you want to use 68 | /// - returns: An instance of of the requested type. 69 | /// - throws: If the transformation from the `Data` to the `T` type cannot be made an error is thrown 70 | public func value() throws -> T { 71 | guard let data = rawValue else { 72 | throw LittleBluetoothError.emptyData 73 | } 74 | return try T.init(from: data) 75 | } 76 | } 77 | 78 | extension LittleBlueToothCharacteristic: Equatable, Hashable { 79 | /// If two `LittleBlueToothCharacteristic` are compared and they have the same characteristic and service identifier they are equal 80 | public static func == (lhs: Self, rhs: Self) -> Bool { 81 | if lhs.id == rhs.id && 82 | lhs.service == rhs.service { 83 | return true 84 | } 85 | return false 86 | } 87 | 88 | /// Combute the hash of a `LittleBlueToothCharacteristic` 89 | public func hash(into hasher: inout Hasher) { 90 | hasher.combine(id) 91 | hasher.combine(service) 92 | } 93 | 94 | } 95 | 96 | public extension LittleBlueToothCharacteristic { 97 | /// Permitted operations on the characteristic they already exist in CBCharacteristic need to remap when initialized from CBCharacteristic 98 | struct Properties: OptionSet, Sendable { 99 | public let rawValue: UInt8 100 | 101 | public static let broadcast = Properties(rawValue: 1 << 0) 102 | public static let read = Properties(rawValue: 1 << 1) 103 | public static let writeWithoutResponse = Properties(rawValue: 1 << 2) 104 | public static let write = Properties(rawValue: 1 << 3) 105 | public static let notify = Properties(rawValue: 1 << 4) 106 | public static let indicate = Properties(rawValue: 1 << 5) 107 | public static let authenticatedSignedWrites = Properties(rawValue: 1 << 6) 108 | public static let extendedProperties = Properties(rawValue: 1 << 7) 109 | public static let notifyEncryptionRequired = Properties(rawValue: 1 << 8) 110 | public static let indicateEncryptionRequired = Properties(rawValue: 1 << 9) 111 | 112 | public init(rawValue: UInt8) { 113 | self.rawValue = rawValue 114 | } 115 | 116 | public init(properties: CBCharacteristicProperties) { 117 | self = Self.mapToProperties(values: properties) 118 | } 119 | 120 | static func mapToProperties(values: CBCharacteristicProperties) -> Properties { 121 | var properties: Properties = [] 122 | values.elements().forEach { (prop) in 123 | switch prop { 124 | case .broadcast: 125 | properties.update(with: .broadcast) 126 | case .read: 127 | properties.update(with: .read) 128 | case .writeWithoutResponse: 129 | properties.update(with: .writeWithoutResponse) 130 | case .write: 131 | properties.update(with: .write) 132 | case .notify: 133 | properties.update(with: .notify) 134 | case .indicate: 135 | properties.update(with: .indicate) 136 | case .authenticatedSignedWrites: 137 | properties.update(with: .authenticatedSignedWrites) 138 | case .extendedProperties: 139 | properties.update(with: .extendedProperties) 140 | case .notifyEncryptionRequired: 141 | properties.update(with: .notifyEncryptionRequired) 142 | case .indicateEncryptionRequired: 143 | properties.update(with: .indicateEncryptionRequired) 144 | default: 145 | print("NO mapping") 146 | } 147 | } 148 | return properties 149 | } 150 | } 151 | } 152 | 153 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/Classes/Proxies/CBCentralManagerDelegateProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CBManagerDelegateProxy.swift 3 | // LittleBlueTooth 4 | // 5 | // Created by Andrea Finollo on 10/06/2020. 6 | // Copyright © 2020 Andrea Finollo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | import os.log 12 | #if TEST 13 | import CoreBluetoothMock 14 | #else 15 | import CoreBluetooth 16 | #endif 17 | /// An enum representing the connection event that has occurred 18 | public enum ConnectionEvent { 19 | /// Peripheral is connected but not ready to receive command 20 | case connected(CBPeripheral) 21 | /// Peripheral is connected after a disconnection automatically but not ready to receive command 22 | case autoConnected(CBPeripheral) 23 | /// Peripheral is ready to receive command 24 | case ready(CBPeripheral) 25 | /// Peripheral is not ready probaly due to some unexpected disconnection 26 | case notReady(CBPeripheral, error: LittleBluetoothError?) 27 | /// Process of connection has failed 28 | case connectionFailed(CBPeripheral, error: LittleBluetoothError?) 29 | /// Peripheral has been disconnected, if it was unexpected a `LittleBluetoothError` is returned 30 | case disconnected(CBPeripheral, error: LittleBluetoothError?) 31 | } 32 | 33 | /// An enumeration representing the state of the bluetooth stack of the device 34 | public enum BluetoothState { 35 | /// Unknown, probably transient 36 | case unknown 37 | /// Bluetooth is resetting 38 | case resetting 39 | /// Bluetooth is not supported for this device 40 | case unsupported 41 | /// The application is not authorized to use bluetooth 42 | case unauthorized 43 | /// The bluetooth is off 44 | case poweredOff 45 | /// The bluetooth is on and ready 46 | case poweredOn 47 | 48 | /// Inizialize using a `CBManagerState` 49 | init(_ state: CBManagerState) { 50 | switch state { 51 | case .unknown: 52 | self = .unknown 53 | case .resetting: 54 | self = .resetting 55 | case .unsupported: 56 | self = .unsupported 57 | case .unauthorized: 58 | self = .unauthorized 59 | case .poweredOff: 60 | self = .poweredOff 61 | case .poweredOn: 62 | self = .poweredOn 63 | #if !TEST 64 | @unknown default: 65 | fatalError() 66 | #endif 67 | } 68 | } 69 | } 70 | 71 | final class CBCentralManagerDelegateProxy: NSObject { 72 | 73 | let centralDiscoveriesPublisher = PassthroughSubject() 74 | let connectionEventPublisher = PassthroughSubject() 75 | lazy var centralStatePublisher: AnyPublisher 76 | = { 77 | self._centralStatePublisher.eraseToAnyPublisher() 78 | }() 79 | 80 | lazy var willRestoreStatePublisher: AnyPublisher = { 81 | _willRestoreStatePublisher.shareReplay(1).eraseToAnyPublisher() 82 | }() 83 | 84 | let _centralStatePublisher = CurrentValueSubject(.unknown) 85 | let _willRestoreStatePublisher = PassthroughSubject() 86 | 87 | var isLogEnabled: Bool = false 88 | var isAutoconnectionActive = false 89 | var stateRestorationCancellable: AnyCancellable! 90 | 91 | override init() { 92 | super.init() 93 | self.stateRestorationCancellable = willRestoreStatePublisher.sink { _ in } 94 | } 95 | 96 | } 97 | 98 | extension CBCentralManagerDelegateProxy: CBCentralManagerDelegate { 99 | func centralManagerDidUpdateState(_ central: CBCentralManager) { 100 | log("[LBT: CBCMD] DidUpdateState %{public}d", 101 | log: OSLog.LittleBT_Log_CentralManager, 102 | type: .debug, 103 | arg: [central.state.rawValue]) 104 | _centralStatePublisher.send(BluetoothState(central.state)) 105 | } 106 | 107 | /// Scan 108 | func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { 109 | log("[LBT: CBCMD] DidDiscover %{public}@", 110 | log: OSLog.LittleBT_Log_CentralManager, 111 | type: .debug, 112 | arg: [peripheral.description]) 113 | let peripheraldiscovery = PeripheralDiscovery(peripheral, advertisement: advertisementData, rssi: RSSI) 114 | centralDiscoveriesPublisher.send(peripheraldiscovery) 115 | } 116 | 117 | /// Monitoring connection 118 | func centralManager(_ central: CBCentralManager, didConnect: CBPeripheral) { 119 | log("[LBT: CBCMD] DidConnect %{public}@", 120 | log: OSLog.LittleBT_Log_CentralManager, 121 | type: .debug, 122 | arg: [didConnect.description]) 123 | if isAutoconnectionActive { 124 | isAutoconnectionActive = false 125 | let event = ConnectionEvent.autoConnected(didConnect) 126 | connectionEventPublisher.send(event) 127 | } else { 128 | let event = ConnectionEvent.connected(didConnect) 129 | connectionEventPublisher.send(event) 130 | } 131 | } 132 | 133 | func centralManager(_ central: CBCentralManager, didDisconnectPeripheral: CBPeripheral, error: Error?) { 134 | log("[LBT: CBCMD] DidDisconnect %{public}@, Error %{public}@", 135 | log: OSLog.LittleBT_Log_CentralManager, 136 | type: .debug, 137 | arg: [didDisconnectPeripheral.description, 138 | error?.localizedDescription ?? ""]) 139 | isAutoconnectionActive = false 140 | var lttlError: LittleBluetoothError? 141 | if let error = error { 142 | lttlError = .peripheralDisconnected(PeripheralIdentifier(peripheral: didDisconnectPeripheral), error) 143 | } 144 | let event = ConnectionEvent.disconnected(didDisconnectPeripheral, error: lttlError) 145 | connectionEventPublisher.send(event) 146 | } 147 | 148 | func centralManager(_ central: CBCentralManager, didFailToConnect: CBPeripheral, error: Error?) { 149 | isAutoconnectionActive = false 150 | var lttlError: LittleBluetoothError? 151 | if let error = error { 152 | lttlError = .couldNotConnectToPeripheral(PeripheralIdentifier(peripheral: didFailToConnect), error) 153 | } 154 | let event = ConnectionEvent.connectionFailed(didFailToConnect, error: lttlError) 155 | connectionEventPublisher.send(event) 156 | } 157 | 158 | func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { 159 | log("[LBT: CBCMD] WillRestoreState %{public}@", 160 | log: OSLog.LittleBT_Log_Restore, 161 | type: .debug, 162 | arg: [dict.description]) 163 | _willRestoreStatePublisher.send(CentralRestorer(centralManager: central, restoredInfo: dict)) 164 | } 165 | 166 | #if !os(macOS) 167 | func centralManager(_ central: CBCentralManager, connectionEventDidOccur event: CBConnectionEvent, for peripheral: CBPeripheral) {} 168 | #endif 169 | 170 | } 171 | 172 | extension CBCentralManagerDelegateProxy: Loggable {} 173 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/Classes/CustomOperator/ScanAndConnection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Connection.swift 3 | // LittleBlueTooth 4 | // 5 | // Created by Andrea Finollo on 26/08/2020. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import os.log 11 | #if TEST 12 | import CoreBluetoothMock 13 | #else 14 | import CoreBluetooth 15 | #endif 16 | 17 | // MARK: - Discover 18 | extension Publisher where Self.Failure == LittleBluetoothError { 19 | /// Starts scanning for `PeripheralDiscovery` 20 | /// - parameter littleBluetooth: the `LittleBlueTooth` instance 21 | /// - parameter services: Services for peripheral you are looking for 22 | /// - parameter options: Scanning options same as CoreBluetooth central manager option. 23 | /// - returns: A publisher with stream of disovered peripherals. 24 | public func startDiscovery(for littleBluetooth: LittleBlueTooth, withServices services: [CBUUID]?, options: [String : Any]? = nil) -> AnyPublisher { 25 | func startDiscovery(upstream: Upstream, 26 | for littleBluetooth: LittleBlueTooth, 27 | withServices services: [CBUUID]?, 28 | options: [String : Any]? = nil) -> AnyPublisher where Upstream.Failure == LittleBluetoothError { 29 | return upstream 30 | .flatMapLatest { _ in 31 | littleBluetooth.startDiscovery(withServices: services, options: options) 32 | } 33 | } 34 | return startDiscovery(upstream: self, 35 | for: littleBluetooth, 36 | withServices: services, 37 | options: options) 38 | } 39 | 40 | /// Stops peripheral discovery 41 | /// - parameter littleBluetooth: the `LittleBlueTooth` instance 42 | /// - returns: A publisher when discovery has been stopped 43 | public func stopDiscovery(for littleBluetooth: LittleBlueTooth) -> AnyPublisher { 44 | func stopDiscovery(upstream: Upstream, 45 | for littleBluetooth: LittleBlueTooth) -> AnyPublisherwhere Upstream.Failure == LittleBluetoothError { 46 | return upstream 47 | .flatMapLatest { _ in 48 | littleBluetooth.stopDiscovery() 49 | } 50 | } 51 | return stopDiscovery(upstream: self, 52 | for: littleBluetooth) 53 | } 54 | } 55 | 56 | // MARK: - Connect 57 | extension Publisher where Self.Output == PeripheralDiscovery, Self.Failure == LittleBluetoothError { 58 | 59 | /// Starts connection for `PeripheralDiscovery` 60 | /// - parameter littleBluetooth: the `LittleBlueTooth` instance 61 | /// - parameter options: Connecting options same as CoreBluetooth central manager option. 62 | /// - returns: A publisher with the just connected `Peripheral`. 63 | public func connect(for littleBluetooth: LittleBlueTooth, 64 | options: [String : Any]? = nil) -> AnyPublisher { 65 | 66 | func connect(upstream: Upstream, 67 | for littleBluetooth: LittleBlueTooth, 68 | options: [String : Any]? = nil) -> AnyPublisher where Upstream.Output == PeripheralDiscovery, Upstream.Failure == LittleBluetoothError { 69 | return upstream 70 | .flatMapLatest { (periph) in 71 | littleBluetooth.connect(to: periph, options: options) 72 | }.eraseToAnyPublisher() 73 | } 74 | 75 | return connect(upstream: self, 76 | for: littleBluetooth, 77 | options: options) 78 | } 79 | } 80 | 81 | extension Publisher where Self.Output == PeripheralIdentifier, Self.Failure == LittleBluetoothError { 82 | 83 | /// Starts connection for `PeripheralIdentifier` 84 | /// - parameter littleBluetooth: the `LittleBlueTooth` instance 85 | /// - parameter options: Connecting options same as CoreBluetooth central manager option. 86 | /// - returns: A publisher with the just connected `Peripheral`. 87 | public func connect(for littleBluetooth: LittleBlueTooth, 88 | options: [String : Any]? = nil) -> AnyPublisher { 89 | 90 | func connect(upstream: Upstream, 91 | for littleBluetooth: LittleBlueTooth, 92 | options: [String : Any]? = nil) -> AnyPublisher where Upstream.Output == PeripheralIdentifier, Upstream.Failure == LittleBluetoothError { 93 | return upstream 94 | .flatMapLatest { (periph) in 95 | littleBluetooth.connect(to: periph, options: options) 96 | }.eraseToAnyPublisher() 97 | } 98 | 99 | return connect(upstream: self, 100 | for: littleBluetooth, 101 | options: options) 102 | } 103 | } 104 | 105 | // MARK: - Disconnect 106 | extension Publisher where Self.Failure == LittleBluetoothError { 107 | 108 | /// Disconnect the connected `Peripheral` 109 | /// - returns: A publisher with the just disconnected `Peripheral` or a `LittleBluetoothError` 110 | @discardableResult 111 | public func disconnect(for littleBluetooth: LittleBlueTooth) -> AnyPublisher { 112 | func disconnect(upstream: Upstream, 113 | for littleBluetooth: LittleBlueTooth) -> AnyPublisher where Upstream.Failure == LittleBluetoothError { 114 | return upstream 115 | .flatMapLatest { _ in 116 | littleBluetooth.disconnect() 117 | } 118 | } 119 | return disconnect(upstream: self, 120 | for: littleBluetooth) 121 | } 122 | 123 | /// Specialized timeout function to return a `LittleBluetoothError` error type. By default it returns `.operationTimeout`, but you can specify a different error such as `.connectionTimeout`, `.scanTimeout` 124 | /// Terminates publishing if the upstream publisher exceeds the specified time interval without producing an element. 125 | /// - Parameters: 126 | /// - interval: The maximum time interval the publisher can go without emitting an element, expressed in the time system of the scheduler. 127 | /// - scheduler: The scheduler to deliver events on. 128 | /// - options: Scheduler options that customize the delivery of elements. 129 | /// - error: An error to be returned if the publisher times out, by default `LittleBluetoothError.connectionTimeout` 130 | /// - Returns: A publisher that terminates if the specified interval elapses with no events received from the upstream publisher. 131 | public func timeout(_ interval: S.SchedulerTimeType.Stride, scheduler: S, options: S.SchedulerOptions? = nil, error: LittleBluetoothError = .operationTimeout) -> AnyPublisher where S: Scheduler { 132 | func timeout(upsstream: Upstream,_ interval: S.SchedulerTimeType.Stride, scheduler: S, options: S.SchedulerOptions? = nil, error: LittleBluetoothError = .operationTimeout) -> AnyPublisher where S: Scheduler, Upstream.Failure == LittleBluetoothError { 133 | return upsstream 134 | .timeout(interval, scheduler: scheduler, options: options, customError: {error}) 135 | .eraseToAnyPublisher() 136 | } 137 | 138 | return timeout(upsstream: self, interval, scheduler: scheduler, options: options, error: error) 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /Sources/LittleBlueTooth/Classes/Proxies/CBPeripheralProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CBPeripheralProxy.swift 3 | // LittleBlueTooth 4 | // 5 | // Created by Andrea Finollo on 10/06/2020. 6 | // Copyright © 2020 Andrea Finollo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | import os.log 12 | #if TEST 13 | import CoreBluetoothMock 14 | #else 15 | import CoreBluetooth 16 | #endif 17 | 18 | final class CBPeripheralDelegateProxy: NSObject { 19 | 20 | let peripheralChangesPublisher = PassthroughSubject() 21 | let peripheralRSSIPublisher = PassthroughSubject<(Int, LittleBluetoothError?), Never>() 22 | 23 | lazy var peripheralDiscoveredServicesPublisher = { _peripheralDiscoveredServicesPublisher.share().eraseToAnyPublisher() 24 | }() 25 | let _peripheralDiscoveredServicesPublisher = PassthroughSubject<([CBService]?, LittleBluetoothError?), Never>() 26 | 27 | lazy var peripheralDiscoveredIncludedServicesPublisher = { _peripheralDiscoveredIncludedServicesPublisher.share().eraseToAnyPublisher() 28 | }() 29 | let _peripheralDiscoveredIncludedServicesPublisher = PassthroughSubject<(CBService, Error?), Never>() 30 | 31 | lazy var peripheralDiscoveredCharacteristicsForServicePublisher = { _peripheralDiscoveredCharacteristicsForServicePublisher.share().eraseToAnyPublisher() 32 | }() 33 | let _peripheralDiscoveredCharacteristicsForServicePublisher = PassthroughSubject<(CBService, LittleBluetoothError?), Never>() 34 | 35 | lazy var peripheralUpdatedNotificationStateForCharacteristicPublisher = { _peripheralUpdatedNotificationStateForCharacteristicPublisher.share().eraseToAnyPublisher() 36 | }() 37 | let _peripheralUpdatedNotificationStateForCharacteristicPublisher = 38 | PassthroughSubject<(CBCharacteristic, LittleBluetoothError?), Never>() 39 | 40 | let peripheralUpdatedValueForCharacteristicPublisher = PassthroughSubject<(CBCharacteristic, LittleBluetoothError?), Never>() 41 | let peripheralUpdatedValueForNotifyCharacteristicPublisher = PassthroughSubject<(CBCharacteristic, LittleBluetoothError?), Never>() 42 | let peripheralWrittenValueForCharacteristicPublisher = PassthroughSubject<(CBCharacteristic, LittleBluetoothError?), Never>() 43 | let peripheralIsReadyToSendWriteWithoutResponse = PassthroughSubject() 44 | 45 | let peripheralDiscoveredDescriptorsForCharacteristicPublisher = 46 | PassthroughSubject<(CBCharacteristic, LittleBluetoothError?), Never>() 47 | let peripheralUpdatedValueForDescriptor = PassthroughSubject<(CBDescriptor, LittleBluetoothError?), Never>() 48 | let peripheralWrittenValueForDescriptor = PassthroughSubject<(CBDescriptor, LittleBluetoothError?), Never>() 49 | 50 | var isLogEnabled: Bool = false 51 | 52 | } 53 | 54 | extension CBPeripheralDelegateProxy: CBPeripheralDelegate { 55 | 56 | func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral){ 57 | log("[LBT: CBPD] ReadyToSendWRiteWOResp", 58 | log: OSLog.LittleBT_Log_Peripheral, 59 | type: .debug, arg: []) 60 | peripheralIsReadyToSendWriteWithoutResponse.send() 61 | } 62 | 63 | func peripheralDidUpdateName(_ peripheral: CBPeripheral) { 64 | log("[LBT: CBPD] DidUpdateName %{public}@", 65 | log: OSLog.LittleBT_Log_Peripheral, 66 | type: .debug, 67 | arg: [peripheral.name ?? "na"]) 68 | peripheralChangesPublisher.send(.name(peripheral.name)) 69 | } 70 | 71 | func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]){ 72 | log("[LBT: CBPD] DidModifyServices %{public}@", 73 | log: OSLog.LittleBT_Log_Peripheral, 74 | type: .debug, 75 | arg: [invalidatedServices.description]) 76 | peripheralChangesPublisher.send(.invalidatedServices(invalidatedServices)) 77 | } 78 | 79 | func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) { 80 | if let error = error { 81 | peripheralRSSIPublisher.send((RSSI.intValue,.couldNotReadRSSI(error))) 82 | } else { 83 | peripheralRSSIPublisher.send((RSSI.intValue, nil)) 84 | } 85 | } 86 | 87 | func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?){ 88 | log("[LBT: CBPD] DidDiscoverServices, Error %{public}@", 89 | log: OSLog.LittleBT_Log_Peripheral, 90 | type: .debug, 91 | arg: [error?.localizedDescription ?? "None"]) 92 | if let error = error { 93 | _peripheralDiscoveredServicesPublisher.send((nil,.serviceNotFound(error))) 94 | } else { 95 | _peripheralDiscoveredServicesPublisher.send((peripheral.services, nil)) 96 | } 97 | } 98 | 99 | func peripheral(_ peripheral: CBPeripheral, didDiscoverIncludedServicesFor service: CBService, error: Error?) { 100 | log("[LBT: CBPD] DidDiscoverIncludedServices %{public}@, Error %{public}@", 101 | log: OSLog.LittleBT_Log_Peripheral, 102 | type: .debug, 103 | arg: [service.description, 104 | (error?.localizedDescription ?? "None")]) 105 | if let error = error { 106 | _peripheralDiscoveredIncludedServicesPublisher.send((service, error)) 107 | } else { 108 | _peripheralDiscoveredIncludedServicesPublisher.send((service, nil)) 109 | } 110 | } 111 | 112 | func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?){ 113 | log("[LBT: CBPD] DidDiscoverCharacteristic %{public}@, Error %{public}@", 114 | log: OSLog.LittleBT_Log_Peripheral, 115 | type: .debug, 116 | arg: [service.description, 117 | (error?.localizedDescription ?? "None")]) 118 | if let error = error { 119 | _peripheralDiscoveredCharacteristicsForServicePublisher.send((service, .characteristicNotFound(error))) 120 | } else { 121 | _peripheralDiscoveredCharacteristicsForServicePublisher.send((service, nil)) 122 | } 123 | } 124 | 125 | func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?){ 126 | log("[LBT: CBPD] DidUpdateValue %{public}@, Error %{public}@", 127 | log: OSLog.LittleBT_Log_Peripheral, 128 | type: .debug, 129 | arg: [characteristic.description, 130 | (error?.localizedDescription ?? "None")]) 131 | if let error = error { 132 | peripheralUpdatedValueForCharacteristicPublisher.send((characteristic, .couldNotReadFromCharacteristic(characteristic: characteristic.uuid, error: error))) 133 | } else { 134 | if !characteristic.isNotifying { 135 | peripheralUpdatedValueForCharacteristicPublisher.send((characteristic, nil)) 136 | } else { 137 | peripheralUpdatedValueForNotifyCharacteristicPublisher.send((characteristic, nil)) 138 | } 139 | } 140 | } 141 | 142 | func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { 143 | log("[LBT: CBPD] DidWriteValue %{public}@, Error %{public}@", 144 | log: OSLog.LittleBT_Log_Peripheral, 145 | type: .debug, 146 | arg: [characteristic.description, 147 | (error?.localizedDescription ?? "None")]) 148 | if let error = error { 149 | peripheralWrittenValueForCharacteristicPublisher.send((characteristic, .couldNotWriteFromCharacteristic(characteristic: characteristic.uuid, error: error))) 150 | } else { 151 | peripheralWrittenValueForCharacteristicPublisher.send((characteristic, nil)) 152 | } 153 | } 154 | 155 | func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?){ 156 | log("[LBT: CBPD] DidUpdateNotifState %{public}@, Error %{public}@", 157 | log: OSLog.LittleBT_Log_Peripheral, 158 | type: .debug, 159 | arg: [characteristic.description, 160 | (error?.localizedDescription ?? "None")]) 161 | if let error = error { 162 | _peripheralUpdatedNotificationStateForCharacteristicPublisher.send((characteristic, .couldNotUpdateListenState(characteristic: characteristic.uuid, error: error))) 163 | } else { 164 | _peripheralUpdatedNotificationStateForCharacteristicPublisher.send((characteristic, nil)) 165 | } 166 | } 167 | 168 | // MARK: - Descriptors 169 | // func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?){} 170 | // func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor descriptor: CBDescriptor, error: Error?){} 171 | // func peripheral(_ peripheral: CBPeripheral, didWriteValueFor descriptor: CBDescriptor, error: Error?){} 172 | } 173 | 174 | extension CBPeripheralDelegateProxy: Loggable {} 175 | -------------------------------------------------------------------------------- /Tests/LittleBlueToothTests/Mocks/MockPeripherals.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, Nordic Semiconductor 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, 6 | * are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, this 12 | * list of conditions and the following disclaimer in the documentation and/or 13 | * other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of the copyright holder nor the names of its contributors may 16 | * be used to endorse or promote products derived from this software without 17 | * specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 22 | * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 23 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 24 | * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 26 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 | * POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | import Foundation 32 | import CoreBluetoothMock 33 | 34 | // MARK: - Mock nRF Blinky 35 | 36 | extension CBMUUID { 37 | static let nordicBlinkyService = CBMUUID(string: "00001523-1212-EFDE-1523-785FEABCD123") 38 | static let buttonCharacteristic = CBMUUID(string: "00001524-1212-EFDE-1523-785FEABCD123") 39 | static let ledCharacteristic = CBMUUID(string: "00001525-1212-EFDE-1523-785FEABCD123") 40 | } 41 | 42 | extension CBMCharacteristicMock { 43 | 44 | static let buttonCharacteristic = CBMCharacteristicMock( 45 | type: .buttonCharacteristic, 46 | properties: [.notify, .read], 47 | descriptors: CBMClientCharacteristicConfigurationDescriptorMock() 48 | ) 49 | 50 | static let ledCharacteristic = CBMCharacteristicMock( 51 | type: .ledCharacteristic, 52 | properties: [.write, .read, .notify] 53 | ) 54 | 55 | } 56 | 57 | extension CBMServiceMock { 58 | 59 | static let blinkyService = CBMServiceMock( 60 | type: .nordicBlinkyService, primary: true, 61 | characteristics: 62 | .buttonCharacteristic, 63 | .ledCharacteristic 64 | ) 65 | 66 | } 67 | 68 | private class BlinkyCBMPeripheralSpecDelegate: CBMPeripheralSpecDelegate { 69 | private var ledEnabled: Bool = false 70 | private var buttonPressed: Bool = false 71 | 72 | private var ledData: Data { 73 | return ledEnabled ? Data([0x01]) : Data([0x00]) 74 | } 75 | 76 | private var buttonData: Data { 77 | return buttonPressed ? Data([0x01]) : Data([0x00]) 78 | } 79 | 80 | func reset() { 81 | ledEnabled = false 82 | buttonPressed = false 83 | } 84 | 85 | func peripheral(_ peripheral: CBMPeripheralSpec, 86 | didReceiveReadRequestFor characteristic: CBMCharacteristic) 87 | -> Result { 88 | if characteristic.uuid == .ledCharacteristic { 89 | return .success(ledData) 90 | } else { 91 | return .success(buttonData) 92 | } 93 | } 94 | 95 | func peripheral(_ peripheral: CBMPeripheralSpec, 96 | didReceiveWriteRequestFor characteristic: CBMCharacteristic, 97 | data: Data) -> Result { 98 | if characteristic.uuid == .ledCharacteristic { 99 | if !data.isEmpty { 100 | ledEnabled = data[0] != 0x00 101 | } 102 | } else { 103 | if !data.isEmpty { 104 | buttonPressed = data[0] != 0x00 105 | } 106 | } 107 | return .success(()) 108 | } 109 | } 110 | 111 | 112 | let blinky = CBMPeripheralSpec 113 | .simulatePeripheral(proximity: .outOfRange) 114 | .advertising( 115 | advertisementData: [ 116 | CBMAdvertisementDataLocalNameKey : "nRF Blinky", 117 | CBMAdvertisementDataServiceUUIDsKey : [CBMUUID.nordicBlinkyService], 118 | CBMAdvertisementDataIsConnectable : true as NSNumber 119 | ], 120 | withInterval: 0.250, 121 | alsoWhenConnected: false) 122 | .connectable( 123 | name: "nRF Blinky", 124 | services: [.blinkyService], 125 | delegate: BlinkyCBMPeripheralSpecDelegate(), 126 | connectionInterval: 0.150, 127 | mtu: 23) 128 | .build() 129 | 130 | let blinkyWOR = CBMPeripheralSpec 131 | .simulatePeripheral(proximity: .outOfRange) 132 | .advertising( 133 | advertisementData: [ 134 | CBMAdvertisementDataLocalNameKey : "nRF Blinky WO", 135 | CBMAdvertisementDataServiceUUIDsKey : [CBMUUID.nordicBlinkyService], 136 | CBMAdvertisementDataIsConnectable : true as NSNumber 137 | ], 138 | withInterval: 0.250, 139 | alsoWhenConnected: false) 140 | .connectable( 141 | name: "nRF Blinky", 142 | services: [.blinkyService], 143 | delegate: BlinkyCBMPeripheralSpecDelegate(), 144 | connectionInterval: 0.150, 145 | mtu: 3) 146 | .build() 147 | 148 | // MARK: - Mock Nordic HRM 149 | 150 | extension CBMServiceMock { 151 | 152 | static let hrmService = CBMServiceMock( 153 | type: CBMUUID(string: "180D"), primary: true, 154 | characteristics: 155 | CBMCharacteristicMock( 156 | type: CBMUUID(string: "2A37"), // Heart Rate Measurement 157 | properties: [.notify], 158 | descriptors: CBMClientCharacteristicConfigurationDescriptorMock() 159 | ), 160 | CBMCharacteristicMock( 161 | type: CBMUUID(string: "2A38"), // Body Sensor Location 162 | properties: [.read] 163 | ) 164 | ) 165 | 166 | } 167 | 168 | private struct DummyCBMPeripheralSpecDelegate: CBMPeripheralSpecDelegate { 169 | // Let's use default implementation. 170 | // The HRM will not show up in the scan result, as it 171 | // doesn't advertise with Nordic LED Button service. 172 | // If you uncomment the line below, and try to connect, 173 | // connection will fail on "Device not supported" error. 174 | } 175 | 176 | let hrm = CBMPeripheralSpec 177 | .simulatePeripheral(proximity: .outOfRange) 178 | .advertising( 179 | advertisementData: [ 180 | CBMAdvertisementDataLocalNameKey : "NordicHRM", 181 | CBMAdvertisementDataServiceUUIDsKey : [ 182 | CBMUUID(string: "180D"), // Heart Rate 183 | CBMUUID(string: "180A"), // Device Information 184 | // BlinkyPeripheral.nordicBlinkyServiceUUID // <- this line 185 | ], 186 | CBMAdvertisementDataIsConnectable : true as NSNumber 187 | ], 188 | withInterval: 0.100) 189 | .connectable( 190 | name: "NordicHRM", 191 | services: [.hrmService], 192 | delegate: DummyCBMPeripheralSpecDelegate(), 193 | connectionInterval: 0.250, 194 | mtu: 251) 195 | .build() 196 | 197 | // MARK: - Physical Web Beacon 198 | 199 | let thingy = CBMPeripheralSpec 200 | .simulatePeripheral(proximity: .outOfRange) 201 | .advertising( 202 | advertisementData: [ 203 | CBMAdvertisementDataServiceUUIDsKey : [ 204 | CBMUUID(string: "FEAA") // Eddystone 205 | ], 206 | CBMAdvertisementDataServiceDataKey : [ 207 | // Physical Web beacon: 10ee03676f2e676c2f7049576466972 208 | // type: URL 209 | // TX Power: -18 dBm 210 | // URL: https://goo.gl/pIWdir -> Thingy:52 211 | CBMUUID(string: "FEAA") : Data(base64Encoded: "EO4DZ28uZ2wvcElXZGaXIA==") 212 | ] 213 | ], 214 | withInterval: 0.100) 215 | .build() 216 | 217 | class FakePeriph: CBMPeripheral { 218 | var identifier: UUID = UUID() 219 | 220 | var delegate: CBMPeripheralDelegate? 221 | 222 | var name: String? = "SoFake" 223 | 224 | var state: CBMPeripheralState = .disconnected 225 | 226 | var services: [CBMService]? 227 | 228 | var canSendWriteWithoutResponse: Bool = false 229 | 230 | var ancsAuthorized: Bool = false 231 | 232 | func readRSSI() {} 233 | 234 | func discoverServices(_ serviceUUIDs: [CBMUUID]?) {} 235 | 236 | func discoverIncludedServices(_ includedServiceUUIDs: [CBMUUID]?, for service: CBMService) {} 237 | 238 | func discoverCharacteristics(_ characteristicUUIDs: [CBMUUID]?, for service: CBMService) {} 239 | 240 | func discoverDescriptors(for characteristic: CBMCharacteristic) {} 241 | 242 | func readValue(for characteristic: CBMCharacteristic) {} 243 | 244 | func readValue(for descriptor: CBMDescriptor) {} 245 | 246 | func maximumWriteValueLength(for type: CBMCharacteristicWriteType) -> Int { 247 | return Int.max 248 | } 249 | 250 | func writeValue(_ data: Data, for characteristic: CBMCharacteristic, type: CBMCharacteristicWriteType) {} 251 | 252 | func writeValue(_ data: Data, for descriptor: CBMDescriptor) {} 253 | 254 | func setNotifyValue(_ enabled: Bool, for characteristic: CBMCharacteristic) {} 255 | 256 | func openL2CAPChannel(_ PSM: CBML2CAPPSM) {} 257 | } 258 | -------------------------------------------------------------------------------- /Tests/LittleBlueToothTests/ConnectionTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionTest.swift 3 | // LittleBlueToothTests 4 | // 5 | // Created by Andrea Finollo on 29/06/2020. 6 | // Copyright © 2020 Andrea Finollo. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import CoreBluetoothMock 11 | import Combine 12 | @testable import LittleBlueToothForTest 13 | 14 | class ConnectionTest: LittleBlueToothTests { 15 | 16 | override func setUpWithError() throws { 17 | // Put setup code here. This method is called before the invocation of each test method in the class. 18 | try super.setUpWithError() 19 | var configuration = LittleBluetoothConfiguration() 20 | configuration.isLogEnabled = true 21 | littleBT = LittleBlueTooth(with: configuration) 22 | } 23 | 24 | override func tearDownWithError() throws { 25 | // Put teardown code here. This method is called after the invocation of each test method in the class. 26 | } 27 | 28 | func testPeripheralConnectionSuccess() { 29 | disposeBag.removeAll() 30 | 31 | blinky.simulateProximityChange(.immediate) 32 | let connectionExpectation = expectation(description: "Connection expectation") 33 | 34 | var connectedPeripheral: Peripheral? 35 | 36 | littleBT.startDiscovery(withServices: nil) 37 | .flatMap { discovery in 38 | self.littleBT.connect(to: discovery) 39 | } 40 | .sink(receiveCompletion: { completion in 41 | print("Completion \(completion)") 42 | }) { (connectedPeriph) in 43 | print("Discovery \(connectedPeriph)") 44 | connectedPeripheral = connectedPeriph 45 | self.littleBT.disconnect().sink(receiveCompletion: { _ in 46 | }) { _ in 47 | connectionExpectation.fulfill() 48 | } 49 | .store(in: &self.disposeBag) 50 | } 51 | .store(in: &disposeBag) 52 | 53 | waitForExpectations(timeout: 15) 54 | XCTAssertNotNil(connectedPeripheral) 55 | XCTAssertEqual(connectedPeripheral!.cbPeripheral.identifier, blinky.identifier) 56 | 57 | } 58 | 59 | func testPeripheralConnectionReadRSSI() { 60 | disposeBag.removeAll() 61 | 62 | blinky.simulateProximityChange(.near) 63 | let readRSSIExpectation = expectation(description: "Read RSSI expectation") 64 | 65 | var rssiRead: Int? 66 | 67 | littleBT.startDiscovery(withServices: nil) 68 | .flatMap { discovery in 69 | self.littleBT.connect(to: discovery) 70 | } 71 | .flatMap{ _ in 72 | self.littleBT.readRSSI() 73 | } 74 | .sink(receiveCompletion: { completion in 75 | print("Completion \(completion)") 76 | }) { (rssi) in 77 | print("RSSI \(rssi)") 78 | rssiRead = rssi 79 | self.littleBT.disconnect().sink(receiveCompletion: { _ in 80 | }) { _ in 81 | readRSSIExpectation.fulfill() 82 | } 83 | .store(in: &self.disposeBag) 84 | } 85 | .store(in: &disposeBag) 86 | 87 | waitForExpectations(timeout: 15) 88 | XCTAssertNotNil(rssiRead) 89 | XCTAssert(rssiRead! < 70) 90 | 91 | } 92 | 93 | 94 | func testMultipleConnection() { 95 | disposeBag.removeAll() 96 | 97 | blinky.simulateProximityChange(.immediate) 98 | let connectionExpectation = expectation(description: "Multiple connection") 99 | 100 | var isAlreadyConnected = false 101 | 102 | littleBT.startDiscovery(withServices: nil) 103 | .flatMap { discovery in 104 | self.littleBT.connect(to: discovery).map {_ in discovery} 105 | } 106 | .flatMap { discovery in 107 | self.littleBT.connect(to: discovery) 108 | } 109 | .sink(receiveCompletion: { completion in 110 | print("Completion \(completion)") 111 | switch completion { 112 | case .finished: 113 | break 114 | case let .failure(error): 115 | if case LittleBluetoothError.peripheralAlreadyConnectedOrConnecting(_) = error { 116 | isAlreadyConnected = true 117 | connectionExpectation.fulfill() 118 | self.littleBT.disconnect() 119 | } 120 | } 121 | }) { (connectedPeriph) in 122 | print("Discovery \(connectedPeriph)") 123 | } 124 | .store(in: &disposeBag) 125 | waitForExpectations(timeout: 10) 126 | XCTAssert(isAlreadyConnected) 127 | } 128 | 129 | func testConnectionDisconnectionObserving() { 130 | disposeBag.removeAll() 131 | 132 | blinky.simulateProximityChange(.immediate) 133 | var connectionEvent = [ConnectionEvent]() 134 | var peripheralState = [PeripheralState]() 135 | let connectionExpectation = expectation(description: "Connection test") 136 | 137 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(4)) { 138 | blinky.simulateDisconnection() 139 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { 140 | connectionExpectation.fulfill() 141 | } 142 | } 143 | 144 | littleBT.startDiscovery(withServices: nil) 145 | .flatMap { discovery in 146 | self.littleBT.connect(to: discovery) 147 | } 148 | .sink(receiveCompletion: { completion in 149 | print("Completion \(completion)") 150 | }) { (connectedPeriph) in 151 | print("Discovery \(connectedPeriph)") 152 | } 153 | .store(in: &disposeBag) 154 | 155 | littleBT.connectionEventPublisher 156 | .sink { (event) in 157 | print("ConnectionEvent \(event)") 158 | connectionEvent.append(event) 159 | } 160 | .store(in: &disposeBag) 161 | 162 | littleBT.peripheralStatePublisher 163 | .sink { (state) in 164 | print("Peripheral state: \(state)") 165 | peripheralState.append(state) 166 | } 167 | .store(in: &disposeBag) 168 | 169 | waitForExpectations(timeout: 30) 170 | print("Connection disconnection event \(connectionEvent.count)") 171 | XCTAssert(connectionEvent.count == 3) 172 | 173 | } 174 | 175 | 176 | func testChangeDeviceName() { 177 | disposeBag.removeAll() 178 | 179 | blinky.simulateProximityChange(.immediate) 180 | 181 | let changeNameAndServiceExpectation = expectation(description: "Change name test") 182 | 183 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(4)) { 184 | blinky.simulateServiceChange(newName: "pippo", 185 | newServices: [.blinkyService]) 186 | } 187 | let charateristic = LittleBlueToothCharacteristic(characteristic: CBMUUID.ledCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString, properties: [.notify, .read, .write]) 188 | 189 | 190 | littleBT.startDiscovery(withServices: nil) 191 | .flatMap { discovery in 192 | self.littleBT.connect(to: discovery) 193 | } 194 | .flatMap { _ -> AnyPublisher in 195 | self.littleBT.read(from: charateristic) 196 | } 197 | .sink(receiveCompletion: { completion in 198 | print("Completion \(completion)") 199 | }) { (connectedPeriph) in 200 | print("Discovery \(connectedPeriph)") 201 | } 202 | .store(in: &disposeBag) 203 | 204 | littleBT.changesStatePublisher 205 | .sink { (state) in 206 | print("Peripheral state: \(state)") 207 | switch state { 208 | case let .invalidatedServices(services): 209 | print("Invalidated services \(services)") 210 | case let .name(newName): 211 | print("New Name: \(String(describing: newName))") 212 | self.littleBT.disconnect() 213 | changeNameAndServiceExpectation.fulfill() 214 | } 215 | } 216 | .store(in: &disposeBag) 217 | 218 | waitForExpectations(timeout: 30) 219 | } 220 | 221 | 222 | func testDisconnection() { 223 | disposeBag.removeAll() 224 | 225 | blinky.simulateProximityChange(.immediate) 226 | 227 | let disconnectionExpectation = expectation(description: "Disconnection expectation") 228 | 229 | var isDisconnected = false 230 | 231 | 232 | littleBT.startDiscovery(withServices: nil) 233 | .flatMap { discovery in 234 | self.littleBT.connect(to: discovery) 235 | } 236 | .flatMap { _ in 237 | self.littleBT.disconnect() 238 | } 239 | .sink(receiveCompletion: { completion in 240 | print("Completion \(completion)") 241 | }) { (disconnectedPeriph) in 242 | print("Disconnection \(disconnectedPeriph)") 243 | isDisconnected = true 244 | disconnectionExpectation.fulfill() 245 | } 246 | .store(in: &disposeBag) 247 | 248 | waitForExpectations(timeout: 10) 249 | XCTAssert(isDisconnected) 250 | } 251 | 252 | 253 | func testAutoConnection() { 254 | disposeBag.removeAll() 255 | 256 | blinky.simulateProximityChange(.immediate) 257 | var connectionEvent = [ConnectionEvent]() 258 | var peripheralState = [PeripheralState]() 259 | let connectionExpectation = expectation(description: "Connection test") 260 | littleBT.autoconnectionHandler = { (_, _) -> Bool in 261 | return true 262 | } 263 | 264 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { 265 | blinky.simulateReset() 266 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(10)) { 267 | connectionExpectation.fulfill() 268 | } 269 | } 270 | 271 | littleBT.startDiscovery(withServices: nil) 272 | .flatMap { discovery in 273 | self.littleBT.connect(to: discovery) 274 | } 275 | .sink(receiveCompletion: { completion in 276 | print("Completion \(completion)") 277 | }) { (connectedPeriph) in 278 | print("Discovery \(connectedPeriph)") 279 | } 280 | .store(in: &disposeBag) 281 | 282 | littleBT.connectionEventPublisher 283 | .sink { (event) in 284 | print("ConnectionEvent \(event)") 285 | connectionEvent.append(event) 286 | } 287 | .store(in: &disposeBag) 288 | 289 | littleBT.peripheralStatePublisher 290 | .sink { (state) in 291 | print("Peripheral state: \(state)") 292 | peripheralState.append(state) 293 | } 294 | .store(in: &disposeBag) 295 | 296 | waitForExpectations(timeout: 100) 297 | self.littleBT.autoconnectionHandler = nil 298 | self.littleBT.disconnect() 299 | print("Autoconn event \(connectionEvent.count)") 300 | XCTAssert(connectionEvent.count == 5) 301 | } 302 | 303 | func testPeripheralConnectionInitializationSuccess() { 304 | disposeBag.removeAll() 305 | 306 | blinky.simulateProximityChange(.immediate) 307 | let connectionExpectation = expectation(description: "Connection expectation") 308 | let charateristic = LittleBlueToothCharacteristic(characteristic: CBMUUID.ledCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString, properties: [.notify, .read, .write]) 309 | 310 | var ledState: LedState? 311 | 312 | littleBT.connectionTasks = Just(()).setFailureType(to: LittleBluetoothError.self) 313 | .flatMap{ _ -> AnyPublisher in 314 | self.littleBT.read(from: charateristic) 315 | }.map { state in 316 | ledState = state 317 | return () 318 | }.eraseToAnyPublisher() 319 | 320 | 321 | var connectedPeripheral: Peripheral? 322 | 323 | littleBT.startDiscovery(withServices: nil) 324 | .flatMap { discovery in 325 | self.littleBT.connect(to: discovery) 326 | } 327 | .sink(receiveCompletion: { completion in 328 | print("Completion \(completion)") 329 | }) { (connectedPeriph) in 330 | print("Discovery \(connectedPeriph)") 331 | connectedPeripheral = connectedPeriph 332 | self.littleBT.disconnect().sink(receiveCompletion: { _ in 333 | }) { _ in 334 | connectionExpectation.fulfill() 335 | } 336 | .store(in: &self.disposeBag) 337 | } 338 | .store(in: &disposeBag) 339 | 340 | waitForExpectations(timeout: 15) 341 | littleBT.connectionTasks = nil 342 | XCTAssertNotNil(connectedPeripheral) 343 | XCTAssertNotNil(ledState) 344 | XCTAssert(!ledState!.isOn) 345 | XCTAssertEqual(connectedPeripheral!.cbPeripheral.identifier, blinky.identifier) 346 | } 347 | 348 | func testConnectionFailed() { 349 | disposeBag.removeAll() 350 | 351 | blinky.simulateProximityChange(.immediate) 352 | blinky.simulateReset() 353 | 354 | let foundExpectation = XCTestExpectation(description: "Device found expectation") 355 | 356 | var discovery: PeripheralDiscovery? 357 | 358 | littleBT.startDiscovery(withServices: nil) 359 | .sink(receiveCompletion: { completion in 360 | print("Completion \(completion)") 361 | }) { (disc) in 362 | print("Discovery \(disc)") 363 | discovery = disc 364 | foundExpectation.fulfill() 365 | } 366 | .store(in: &disposeBag) 367 | wait(for: [foundExpectation], timeout: 3) 368 | XCTAssertNotNil(discovery) 369 | 370 | blinky.simulateProximityChange(.outOfRange) 371 | // Should never happen 372 | let connected = XCTestExpectation(description: "Connected expectation") 373 | connected.isInverted = true 374 | 375 | littleBT.connect(to: discovery!) 376 | .sink(receiveCompletion: { (completion) in 377 | print("Completion \(completion)") 378 | }) { (periph) in 379 | print("Peripheral \(periph)") 380 | connected.fulfill() 381 | } 382 | .store(in: &disposeBag) 383 | 384 | wait(for: [connected], timeout: 3) 385 | littleBT.disconnect() 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /Tests/LittleBlueToothTests/WriteReadTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadTest.swift 3 | // LittleBlueToothTests 4 | // 5 | // Created by Andrea Finollo on 29/06/2020. 6 | // Copyright © 2020 Andrea Finollo. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Combine 11 | import CoreBluetoothMock 12 | @testable import LittleBlueToothForTest 13 | 14 | struct LedState: Readable { 15 | let isOn: Bool 16 | 17 | init(from data: Data) throws { 18 | let answer: Bool = try data.extract(start: 0, length: 1) 19 | self.isOn = answer 20 | } 21 | 22 | } 23 | 24 | class ReadWriteTest: LittleBlueToothTests { 25 | 26 | override func setUpWithError() throws { 27 | // Put setup code here. This method is called before the invocation of each test method in the class. 28 | try super.setUpWithError() 29 | var configuration = LittleBluetoothConfiguration() 30 | configuration.isLogEnabled = true 31 | littleBT = LittleBlueTooth(with: configuration) 32 | } 33 | 34 | override func tearDownWithError() throws { 35 | // Put teardown code here. This method is called after the invocation of each test method in the class. 36 | } 37 | 38 | func testWrongServiceError() { 39 | disposeBag.removeAll() 40 | 41 | blinky.simulateProximityChange(.immediate) 42 | let charateristic = LittleBlueToothCharacteristic(characteristic: CBMUUID.ledCharacteristic.uuidString, for: "10001523-1212-EFDE-1523-785FEABCD123", properties: [.notify, .read, .write]) 43 | 44 | let wrongServiceExpectation = expectation(description: "Wrong service expectation") 45 | 46 | var isWrong = false 47 | 48 | littleBT.startDiscovery(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : false]) 49 | .map { disc -> PeripheralDiscovery in 50 | print("Discovery discovery \(disc)") 51 | return disc 52 | } 53 | .flatMap { discovery in 54 | self.littleBT.connect(to: discovery) 55 | } 56 | .flatMap { _ -> AnyPublisher in 57 | self.littleBT.read(from: charateristic) 58 | } 59 | .sink(receiveCompletion: { (completion) in 60 | print("Completion \(completion)") 61 | switch completion { 62 | case .finished: 63 | break 64 | case let .failure(error): 65 | if case LittleBluetoothError.serviceNotFound(_) = error { 66 | isWrong = true 67 | self.littleBT.disconnect().sink(receiveCompletion: {_ in 68 | }) { (_) in 69 | wrongServiceExpectation.fulfill() 70 | } 71 | .store(in: &self.disposeBag) 72 | } else { 73 | isWrong = false 74 | } 75 | } 76 | }) { (answer) in 77 | print("Answer \(answer)") 78 | } 79 | .store(in: &disposeBag) 80 | 81 | waitForExpectations(timeout: 30) 82 | XCTAssert(isWrong) 83 | } 84 | 85 | func testDiscoverServices() { 86 | blinky.simulateReset() 87 | disposeBag.removeAll() 88 | 89 | blinky.simulateProximityChange(.immediate) 90 | 91 | let discoverExpectation = expectation(description: "Discover services expectation") 92 | var servicesCount: Int? 93 | littleBT.startDiscovery(withServices: nil, options: [CBMCentralManagerScanOptionAllowDuplicatesKey : false]) 94 | .map { disc -> PeripheralDiscovery in 95 | print("Discovery discovery \(disc)") 96 | return disc 97 | } 98 | .flatMap { discovery in 99 | self.littleBT.connect(to: discovery) 100 | } 101 | .flatMap { _ in 102 | self.littleBT.discover(nil) 103 | } 104 | .sink(receiveCompletion: { completion in 105 | print("Completion \(completion)") 106 | }) { (answer) in 107 | print("Answer \(answer)") 108 | servicesCount = answer?.count ?? 0 109 | self.littleBT.disconnect().sink(receiveCompletion: {_ in 110 | }) { (_) in 111 | discoverExpectation.fulfill() 112 | } 113 | .store(in: &self.disposeBag) 114 | } 115 | .store(in: &disposeBag) 116 | 117 | waitForExpectations(timeout: 100) 118 | XCTAssertNotNil(servicesCount) 119 | XCTAssert(servicesCount! == 1) 120 | } 121 | 122 | func testDiscoverCharacteristics() { 123 | blinky.simulateReset() 124 | disposeBag.removeAll() 125 | 126 | blinky.simulateProximityChange(.immediate) 127 | 128 | let discoverExpectation = expectation(description: "Discover characteristics expectation") 129 | var characteristicsCount: Int? 130 | littleBT.startDiscovery(withServices: nil, options: [CBMCentralManagerScanOptionAllowDuplicatesKey : false]) 131 | .map { disc -> PeripheralDiscovery in 132 | print("Discovery discovery \(disc)") 133 | return disc 134 | } 135 | .flatMap { discovery in 136 | self.littleBT.connect(to: discovery) 137 | } 138 | .flatMap { _ in 139 | self.littleBT.discover(nil, from: CBMServiceMock.blinkyService) 140 | } 141 | .sink(receiveCompletion: { completion in 142 | print("Completion \(completion)") 143 | }) { (answer) in 144 | print("Answer \(answer)") 145 | characteristicsCount = answer?.count ?? 0 146 | self.littleBT.disconnect().sink(receiveCompletion: {_ in 147 | }) { (_) in 148 | discoverExpectation.fulfill() 149 | } 150 | .store(in: &self.disposeBag) 151 | } 152 | .store(in: &disposeBag) 153 | 154 | waitForExpectations(timeout: 100) 155 | XCTAssertNotNil(characteristicsCount) 156 | XCTAssert(characteristicsCount! == 2) 157 | } 158 | 159 | func testReadLedOFF() { 160 | blinky.simulateReset() 161 | disposeBag.removeAll() 162 | 163 | blinky.simulateProximityChange(.immediate) 164 | let charateristic = LittleBlueToothCharacteristic(characteristic: CBMUUID.ledCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString, properties: [.notify, .read, .write]) 165 | let readExpectation = expectation(description: "Read expectation") 166 | 167 | var ledState: LedState? 168 | 169 | littleBT.startDiscovery(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : false]) 170 | .map { disc -> PeripheralDiscovery in 171 | print("Discovery discovery \(disc)") 172 | return disc 173 | } 174 | .flatMap { discovery in 175 | self.littleBT.connect(to: discovery) 176 | } 177 | .flatMap { _ -> AnyPublisher in 178 | self.littleBT.read(from: charateristic) 179 | } 180 | .sink(receiveCompletion: { completion in 181 | print("Completion \(completion)") 182 | }) { (answer) in 183 | print("Answer \(answer)") 184 | ledState = answer 185 | self.littleBT.disconnect().sink(receiveCompletion: {_ in 186 | }) { (_) in 187 | readExpectation.fulfill() 188 | } 189 | .store(in: &self.disposeBag) 190 | } 191 | .store(in: &disposeBag) 192 | 193 | waitForExpectations(timeout: 10) 194 | XCTAssertNotNil(ledState) 195 | XCTAssert(ledState!.isOn == false) 196 | } 197 | 198 | func testWriteLedOnReadLedON() { 199 | disposeBag.removeAll() 200 | 201 | blinky.simulateProximityChange(.immediate) 202 | let charateristic = LittleBlueToothCharacteristic(characteristic: CBMUUID.ledCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString, properties: [.notify, .read, .write]) 203 | let readExpectation = expectation(description: "Read expectation") 204 | 205 | var ledState: LedState? 206 | 207 | littleBT.startDiscovery(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : false]) 208 | .map { disc -> PeripheralDiscovery in 209 | print("Discovery discovery \(disc)") 210 | return disc 211 | } 212 | .flatMap { discovery in 213 | self.littleBT.connect(to: discovery) 214 | } 215 | .flatMap { _ in 216 | self.littleBT.write(to: charateristic, value: Data([0x01])) 217 | } 218 | .flatMap { _ -> AnyPublisher in 219 | self.littleBT.read(from: charateristic) 220 | } 221 | .sink(receiveCompletion: { completion in 222 | print("Completion \(completion)") 223 | }) { (answer) in 224 | print("Answer \(answer)") 225 | ledState = answer 226 | self.littleBT.disconnect().sink(receiveCompletion: {_ in 227 | }) { (_) in 228 | readExpectation.fulfill() 229 | } 230 | .store(in: &self.disposeBag) 231 | 232 | } 233 | .store(in: &disposeBag) 234 | waitForExpectations(timeout: 10) 235 | XCTAssertNotNil(ledState) 236 | XCTAssert(ledState!.isOn == true) 237 | } 238 | 239 | 240 | func testWriteAndListen() { 241 | disposeBag.removeAll() 242 | 243 | blinky.simulateProximityChange(.immediate) 244 | let charateristic = LittleBlueToothCharacteristic(characteristic: CBMUUID.ledCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString, properties: [.notify, .read, .write]) 245 | let writeAndListenExpectation = expectation(description: "Write and Listen") 246 | 247 | var ledState: LedState? 248 | 249 | Timer.publish(every: 0.5, on: .main, in: .common) 250 | .autoconnect() 251 | .map {_ in 252 | blinky.simulateValueUpdate(Data([0x01]), 253 | for: CBMCharacteristicMock.ledCharacteristic) 254 | }.sink { value in 255 | print("Led value:\(value)") 256 | } 257 | .store(in: &self.disposeBag) 258 | 259 | littleBT.startDiscovery(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : false]) 260 | .map { disc -> PeripheralDiscovery in 261 | print("Discovery discovery \(disc)") 262 | return disc 263 | } 264 | .flatMap { discovery in 265 | self.littleBT.connect(to: discovery) 266 | } 267 | .flatMap { _ in 268 | self.littleBT.writeAndListen(from: charateristic, value: Data([0x01])) 269 | } 270 | .sink(receiveCompletion: { completion in 271 | print("Completion \(completion)") 272 | }) { (answer: LedState) in 273 | print("Answer \(answer)") 274 | ledState = answer 275 | self.littleBT.disconnect().sink(receiveCompletion: {_ in 276 | }) { (_) in 277 | writeAndListenExpectation.fulfill() 278 | } 279 | .store(in: &self.disposeBag) 280 | 281 | } 282 | .store(in: &disposeBag) 283 | waitForExpectations(timeout: 10) 284 | XCTAssertNotNil(ledState) 285 | XCTAssert(ledState!.isOn == true) 286 | } 287 | 288 | func testMultipleRead() { 289 | blinky.simulateReset() 290 | disposeBag.removeAll() 291 | 292 | blinky.simulateProximityChange(.immediate) 293 | let ledCharateristic = LittleBlueToothCharacteristic(characteristic: CBUUID.ledCharacteristic.uuidString, for: CBUUID.nordicBlinkyService.uuidString, properties: [.read, .notify, .write]) 294 | let buttonCharateristic = LittleBlueToothCharacteristic(characteristic: CBUUID.buttonCharacteristic.uuidString, for: CBUUID.nordicBlinkyService.uuidString, properties: [.read, .notify]) 295 | let multipleReadExpectation = expectation(description: "Multiple read") 296 | 297 | var ledIsOff = false 298 | var buttonIsOff = false 299 | 300 | 301 | littleBT.startDiscovery(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : false]) 302 | .map { disc -> PeripheralDiscovery in 303 | print("Discovery discovery \(disc)") 304 | return disc 305 | } 306 | .flatMap { discovery in 307 | self.littleBT.connect(to: discovery) 308 | } 309 | .flatMap { _ -> AnyPublisher in 310 | self.littleBT.read(from: ledCharateristic) 311 | } 312 | .flatMap { led -> AnyPublisher in 313 | ledIsOff = !led.isOn 314 | return self.littleBT.read(from: buttonCharateristic) 315 | } 316 | .sink(receiveCompletion: { completion in 317 | print("Completion \(completion)") 318 | }) { (button) in 319 | print("Answer \(button)") 320 | buttonIsOff = !button.isOn 321 | self.littleBT.disconnect().sink(receiveCompletion: {_ in 322 | }) { (_) in 323 | multipleReadExpectation.fulfill() 324 | } 325 | .store(in: &self.disposeBag) 326 | } 327 | .store(in: &disposeBag) 328 | 329 | waitForExpectations(timeout: 10) 330 | XCTAssert(buttonIsOff) 331 | XCTAssert(ledIsOff) 332 | } 333 | 334 | func testDisconnectionBeforeRead() { 335 | disposeBag.removeAll() 336 | 337 | blinky.simulateProximityChange(.immediate) 338 | let charateristic = LittleBlueToothCharacteristic(characteristic: CBMUUID.ledCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString, properties: [.notify, .read, .write]) 339 | let disconnectionExpectation = expectation(description: "Disconnection before read") 340 | 341 | var isDisconnected = false 342 | 343 | littleBT.startDiscovery(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : false]) 344 | .map { disc -> PeripheralDiscovery in 345 | print("Discovery discovery \(disc)") 346 | return disc 347 | } 348 | .flatMap { discovery in 349 | self.littleBT.connect(to: discovery) 350 | } 351 | .flatMap { _ -> AnyPublisher in 352 | blinky.simulateDisconnection() 353 | return self.littleBT.read(from: charateristic) 354 | } 355 | .sink(receiveCompletion: { completion in 356 | print("Completion \(completion)") 357 | switch completion { 358 | case .finished: 359 | break 360 | case let .failure(error): 361 | if case LittleBluetoothError.peripheralDisconnected(_, _) = error { 362 | isDisconnected = true 363 | disconnectionExpectation.fulfill() 364 | } 365 | } 366 | }) { (answer) in 367 | print("Answer \(answer)") 368 | } 369 | .store(in: &disposeBag) 370 | 371 | waitForExpectations(timeout: 10) 372 | XCTAssert(isDisconnected) 373 | } 374 | 375 | } 376 | -------------------------------------------------------------------------------- /Tests/LittleBlueToothTests/ListenTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListenTest.swift 3 | // LittleBlueToothTests 4 | // 5 | // Created by Andrea Finollo on 04/07/2020. 6 | // Copyright © 2020 Andrea Finollo. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Combine 11 | import CoreBluetoothMock 12 | @testable import LittleBlueToothForTest 13 | 14 | struct ButtonState: Readable { 15 | let isOn: Bool 16 | 17 | init(from data: Data) throws { 18 | let answer: Bool = try data.extract(start: 0, length: 1) 19 | self.isOn = answer 20 | } 21 | 22 | } 23 | 24 | 25 | class ListenTest: LittleBlueToothTests { 26 | var cancellable: Cancellable? 27 | 28 | override func setUpWithError() throws { 29 | // Put setup code here. This method is called before the invocation of each test method in the class. 30 | try super.setUpWithError() 31 | var configuration = LittleBluetoothConfiguration() 32 | configuration.isLogEnabled = true 33 | littleBT = LittleBlueTooth(with: configuration) 34 | } 35 | 36 | override func tearDownWithError() throws { 37 | // Put teardown code here. This method is called after the invocation of each test method in the class. 38 | } 39 | 40 | func testListen() { 41 | disposeBag.removeAll() 42 | 43 | blinky.simulateProximityChange(.immediate) 44 | let charateristic = LittleBlueToothCharacteristic(characteristic: CBMUUID.buttonCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString, properties: [.notify, .read]) 45 | let listenExpectation = expectation(description: "Listen expectation") 46 | 47 | var listenCounter = 0 48 | var timerCounter = 0 49 | let timer = Timer.publish(every: 1, on: .main, in: .common) 50 | let scheduler: AnyCancellable = 51 | timer 52 | .map {_ in 53 | blinky.simulateValueUpdate(Data([0x01]), for: CBMCharacteristicMock.buttonCharacteristic) 54 | timerCounter += 1 55 | }.sink { value in 56 | print("Led value:\(value)") 57 | } 58 | 59 | littleBT.startDiscovery(withServices: nil) 60 | .map { disc -> PeripheralDiscovery in 61 | print("Discovery discovery \(disc)") 62 | return disc 63 | } 64 | .flatMap { discovery in 65 | self.littleBT.connect(to: discovery) 66 | } 67 | .flatMap { _ -> AnyPublisher in 68 | self.littleBT.startListen(from: charateristic) 69 | } 70 | .sink(receiveCompletion: { completion in 71 | print("Completion \(completion)") 72 | }) { (answer) in 73 | listenCounter += 1 74 | print("Answer \(answer)") 75 | if listenCounter > 10 { 76 | scheduler.cancel() 77 | self.littleBT.disconnect().sink(receiveCompletion: {_ in 78 | }) { (_) in 79 | listenExpectation.fulfill() 80 | } 81 | .store(in: &self.disposeBag) 82 | } 83 | } 84 | .store(in: &disposeBag) 85 | _ = timer.connect() 86 | 87 | waitForExpectations(timeout: 20) 88 | let contingencyRange = (timerCounter - 2)...timerCounter 89 | print("Timer counter: \(timerCounter) Listen counter \(listenCounter) ") 90 | XCTAssert(contingencyRange.contains(listenCounter)) 91 | } 92 | 93 | 94 | func testConnectableListen() { 95 | disposeBag.removeAll() 96 | 97 | blinky.simulateProximityChange(.immediate) 98 | let charateristic = LittleBlueToothCharacteristic(characteristic: CBMUUID.buttonCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString, properties: [.notify, .read]) 99 | 100 | // Expectation 101 | let firstListenExpectation = expectation(description: "First sub expectation") 102 | let secondListenExpectation = expectation(description: "Second sub expectation") 103 | var sub1Event = [Bool]() 104 | var sub2Event = [Bool]() 105 | var firstCounter = 0 106 | var secondCounter = 0 107 | // Simulate notification 108 | var timerCounter = 0 109 | let timer = Timer.publish(every: 1, on: .main, in: .common) 110 | let scheduler: AnyCancellable = timer 111 | .map {_ -> UInt8 in 112 | let data = UInt8.random(in: 0...1) 113 | blinky.simulateValueUpdate(Data([data]), for: CBMCharacteristicMock.buttonCharacteristic) 114 | timerCounter += 1 115 | return data 116 | }.sink { value in 117 | print("Led value:\(value)") 118 | } 119 | 120 | let connectable = littleBT.connectableListenPublisher(for: charateristic, valueType: ButtonState.self) 121 | 122 | // First subscriber 123 | connectable 124 | .sink(receiveCompletion: { completion in 125 | print("Completion \(completion)") 126 | }) { (answer) in 127 | firstCounter += 1 128 | print("Sub1 \(answer)") 129 | sub1Event.append(answer.isOn) 130 | if firstCounter == 10 { 131 | scheduler.cancel() 132 | self.littleBT.disconnect().sink(receiveCompletion: {_ in 133 | }) { (_) in 134 | firstListenExpectation.fulfill() 135 | } 136 | .store(in: &self.disposeBag) 137 | } 138 | } 139 | .store(in: &disposeBag) 140 | 141 | // Second subscriber 142 | connectable 143 | .sink(receiveCompletion: { completion in 144 | print("Completion \(completion)") 145 | }) { (answer) in 146 | print("Sub2: \(answer)") 147 | sub2Event.append(answer.isOn) 148 | secondCounter += 1 149 | if secondCounter == 10 { 150 | secondListenExpectation.fulfill() 151 | } 152 | } 153 | .store(in: &disposeBag) 154 | 155 | 156 | littleBT.startDiscovery(withServices: nil) 157 | .map { disc -> PeripheralDiscovery in 158 | print("Discovery discovery \(disc)") 159 | return disc 160 | } 161 | .flatMap { discovery in 162 | self.littleBT.connect(to: discovery) 163 | } 164 | .map { _ -> Void in 165 | self.cancellable = connectable.connect() 166 | return () 167 | } 168 | .sink(receiveCompletion: { completion in 169 | print("Completion \(completion)") 170 | }) { (answer) in 171 | print("Answer \(answer)") 172 | } 173 | .store(in: &disposeBag) 174 | _ = timer.connect() 175 | 176 | waitForExpectations(timeout: 20) 177 | XCTAssert(sub1Event.count == sub2Event.count) 178 | XCTAssert(sub1Event == sub2Event) 179 | let contingencyRange = (timerCounter - 2)...timerCounter 180 | print("Timer counter: \(timerCounter) Event counter \(sub2Event.count) ") 181 | 182 | XCTAssert(contingencyRange.contains(sub2Event.count)) 183 | 184 | } 185 | 186 | func testCombineLatest() { 187 | disposeBag.removeAll() 188 | blinky.simulateProximityChange(.immediate) 189 | let charateristicOne = LittleBlueToothCharacteristic(characteristic: CBMUUID.buttonCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString, properties: [.notify, .read]) 190 | let charateristicTwo = LittleBlueToothCharacteristic(characteristic: CBMUUID.ledCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString, properties: [.notify, .read, .write]) 191 | 192 | func getOne() -> AnyPublisher { 193 | littleBT.startListen(from: charateristicOne) 194 | .prepend(littleBT.read(from: charateristicOne)) 195 | .eraseToAnyPublisher() 196 | } 197 | 198 | func getTwo() -> AnyPublisher { 199 | littleBT.startListen(from: charateristicTwo) 200 | .prepend(littleBT.read(from: charateristicTwo)) 201 | .eraseToAnyPublisher() 202 | } 203 | let combineLatestListenExpectation = XCTestExpectation(description: "Combine latest expect") 204 | var counter = 0 205 | let first = Timer.publish(every: 1, on: .main, in: .common).autoconnect() 206 | 207 | let firstScheduler = first 208 | .map {_ -> UInt8 in 209 | let data = UInt8.random(in: 0...1) 210 | blinky.simulateValueUpdate(Data([data]), for: CBMCharacteristicMock.buttonCharacteristic) 211 | return data 212 | } 213 | .sink { value in 214 | print("Button value:\(value)") 215 | } 216 | 217 | let second = Timer.publish(every: 1, on: .main, in: .common).autoconnect() 218 | 219 | let secondScheduler = second 220 | .map {_ -> UInt8 in 221 | let data = UInt8.random(in: 0...1) 222 | blinky.simulateValueUpdate(Data([data]), for: CBMCharacteristicMock.ledCharacteristic) 223 | return data 224 | }.sink { value in 225 | print("Led value:\(value)") 226 | } 227 | 228 | 229 | StartLittleBlueTooth 230 | .startDiscovery(for: self.littleBT, withServices: nil) 231 | .connect(for: self.littleBT) 232 | .sink(receiveCompletion: { completion in 233 | print("Completion \(completion)") 234 | }) { (answer: Peripheral) in 235 | print("Answer \(answer)") 236 | Publishers.CombineLatest( 237 | getOne(), 238 | getTwo() 239 | ) 240 | .sink(receiveCompletion: { completion in 241 | print("Completion \(completion)") 242 | }) { (answer) in 243 | print("Answer \(answer)") 244 | counter += 1 245 | if counter >= 10 { 246 | self.littleBT.disconnect().sink(receiveCompletion: {_ in 247 | }) { (_) in 248 | combineLatestListenExpectation.fulfill() 249 | secondScheduler.cancel() 250 | firstScheduler.cancel() 251 | } 252 | .store(in: &self.disposeBag) 253 | } 254 | } 255 | .store(in: &self.disposeBag) 256 | } 257 | .store(in: &disposeBag) 258 | 259 | wait(for: [combineLatestListenExpectation], timeout: 30) 260 | 261 | } 262 | 263 | 264 | func testListenToMoreCharacteristic() { 265 | disposeBag.removeAll() 266 | 267 | blinky.simulateProximityChange(.immediate) 268 | let charateristicOne = LittleBlueToothCharacteristic(characteristic: CBMUUID.buttonCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString, properties: [.notify, .read]) 269 | let charateristicTwo = LittleBlueToothCharacteristic(characteristic: CBMUUID.ledCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString, properties: [.notify, .read, .write]) 270 | // Expectation 271 | let firstListenExpectation = XCTestExpectation(description: "First sub more expectation") 272 | let secondListenExpectation = XCTestExpectation(description: "Second sub more expectation") 273 | var sub1Event = [Bool]() 274 | var sub2Event = [Bool]() 275 | var firstCounter = 0 276 | var secondCounter = 0 277 | // Simulate notification 278 | var timerCounter = 0 279 | let timer = Timer.publish(every: 1, on: .main, in: .common) 280 | let scheduler: AnyCancellable = timer 281 | .map {_ -> UInt8 in 282 | var data = UInt8.random(in: 0...1) 283 | blinky.simulateValueUpdate(Data([data]), for: CBMCharacteristicMock.buttonCharacteristic) 284 | data = UInt8.random(in: 0...1) 285 | blinky.simulateValueUpdate(Data([data]), for: CBMCharacteristicMock.ledCharacteristic) 286 | timerCounter += 1 287 | return data 288 | }.sink { value in 289 | print("Sink from timer value:\(value)") 290 | } 291 | 292 | // First publisher 293 | littleBT.listenPublisher 294 | .filter { charact -> Bool in 295 | charact.id == charateristicOne.id 296 | } 297 | .tryMap { (characteristic) -> ButtonState in 298 | try characteristic.value() 299 | } 300 | .mapError { (error) -> LittleBluetoothError in 301 | if let er = error as? LittleBluetoothError { 302 | return er 303 | } 304 | return .emptyData 305 | } 306 | .sink(receiveCompletion: { completion in 307 | print("Completion \(completion)") 308 | }) { (answer) in 309 | print("Sub1: \(answer)") 310 | if firstCounter == 10 { 311 | scheduler.cancel() 312 | return 313 | } else { 314 | sub1Event.append(answer.isOn) 315 | firstCounter += 1 316 | } 317 | } 318 | .store(in: &self.disposeBag) 319 | 320 | // Second publisher 321 | littleBT.listenPublisher 322 | .filter { charact -> Bool in 323 | charact.id == charateristicTwo.id 324 | } 325 | .tryMap { (characteristic) -> LedState in 326 | try characteristic.value() 327 | }.mapError { (error) -> LittleBluetoothError in 328 | if let er = error as? LittleBluetoothError { 329 | return er 330 | } 331 | return .emptyData 332 | } 333 | .sink(receiveCompletion: { completion in 334 | print("Completion \(completion)") 335 | }) { (answer) in 336 | print("Sub2: \(answer)") 337 | if secondCounter == 10 { 338 | return 339 | } else { 340 | sub2Event.append(answer.isOn) 341 | secondCounter += 1 342 | } 343 | } 344 | .store(in: &self.disposeBag) 345 | 346 | littleBT.startDiscovery(withServices: nil) 347 | .map { disc -> PeripheralDiscovery in 348 | print("Discovery discovery \(disc)") 349 | return disc 350 | } 351 | .flatMap { discovery in 352 | self.littleBT.connect(to: discovery) 353 | } 354 | .flatMap { _ in 355 | self.littleBT.enableListen(from: charateristicOne) 356 | } 357 | .flatMap { _ in 358 | self.littleBT.enableListen(from: charateristicTwo) 359 | } 360 | .delay(for: .seconds(20), scheduler: DispatchQueue.global()) 361 | .flatMap { _ in 362 | self.littleBT.disableListen(from: charateristicOne) 363 | } 364 | .flatMap { _ in 365 | self.littleBT.disableListen(from: charateristicTwo) 366 | } 367 | .sink(receiveCompletion: { completion in 368 | print("Completion \(completion)") 369 | }) { (_) in 370 | secondListenExpectation.fulfill() 371 | firstListenExpectation.fulfill() 372 | } 373 | .store(in: &disposeBag) 374 | _ = timer.connect() 375 | 376 | wait(for: [firstListenExpectation, secondListenExpectation], timeout: 30) 377 | littleBT.disconnect() 378 | XCTAssert(sub1Event.count == sub2Event.count) 379 | let contingencyRange = (timerCounter - 2)...timerCounter 380 | print("Timer counter: \(timerCounter) Event counter \(sub2Event.count) ") 381 | XCTAssert(contingencyRange.contains(sub2Event.count)) 382 | 383 | } 384 | 385 | func testPowerOffWhileListen() { 386 | disposeBag.removeAll() 387 | 388 | blinky.simulateProximityChange(.immediate) 389 | let charateristic = LittleBlueToothCharacteristic(characteristic: CBMUUID.buttonCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString, properties: [.read, .notify]) 390 | let listenExpectation = expectation(description: "Listen while powering off expectation") 391 | var isPowerOff = false 392 | 393 | let scheduler: AnyCancellable = Timer.publish(every: 0.5, on: .main, in: .common) 394 | .autoconnect() 395 | .map {_ in 396 | let data = UInt8.random(in: 0...1) 397 | blinky.simulateValueUpdate(Data([data]), for: CBMCharacteristicMock.buttonCharacteristic) 398 | }.sink { value in 399 | print("Led value:\(value)") 400 | } 401 | 402 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { 403 | CBMCentralManagerMock.simulateInitialState(.poweredOff) 404 | } 405 | 406 | littleBT.startDiscovery(withServices: nil) 407 | .map { disc -> PeripheralDiscovery in 408 | print("Discovery discovery \(disc)") 409 | return disc 410 | } 411 | .flatMap { discovery in 412 | self.littleBT.connect(to: discovery) 413 | } 414 | .flatMap { _ -> AnyPublisher in 415 | self.littleBT.startListen(from: charateristic) 416 | } 417 | .sink(receiveCompletion: { completion in 418 | print("Completion \(completion)") 419 | switch completion { 420 | case let .failure(error): 421 | if case LittleBluetoothError.bluetoothPoweredOff = error { 422 | isPowerOff = true 423 | listenExpectation.fulfill() 424 | } 425 | default: 426 | break 427 | } 428 | }) { (answer) in 429 | print("Answer \(answer)") 430 | } 431 | .store(in: &disposeBag) 432 | 433 | waitForExpectations(timeout: 10) 434 | XCTAssert(isPowerOff) 435 | scheduler.cancel() 436 | } 437 | 438 | } 439 | -------------------------------------------------------------------------------- /Tests/LittleBlueToothTests/CustomOperator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomOperator.swift 3 | // LittleBlueToothTests 4 | // 5 | // Created by Andrea Finollo on 27/08/2020. 6 | // 7 | 8 | import XCTest 9 | import CoreBluetoothMock 10 | import Combine 11 | @testable import LittleBlueToothForTest 12 | 13 | class CustomOperator: LittleBlueToothTests { 14 | 15 | override func setUpWithError() throws { 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | try super.setUpWithError() 18 | var configuration = LittleBluetoothConfiguration() 19 | configuration.isLogEnabled = true 20 | littleBT = LittleBlueTooth(with: configuration) 21 | } 22 | 23 | override func tearDownWithError() throws { 24 | // Put teardown code here. This method is called after the invocation of each test method in the class. 25 | } 26 | 27 | /// Connection custom operator test 28 | func testConnectionOperatorFromScanDiscovery() { 29 | disposeBag.removeAll() 30 | 31 | blinky.simulateProximityChange(.immediate) 32 | let connectionExpectation = expectation(description: "Connection expectation") 33 | 34 | var connectedPeripheral: Peripheral? 35 | 36 | StartLittleBlueTooth 37 | .startDiscovery(for: self.littleBT, withServices: nil) 38 | .connect(for: self.littleBT) 39 | .sink(receiveCompletion: { completion in 40 | print("Completion \(completion)") 41 | }) { (connectedPeriph) in 42 | print("Discovery \(connectedPeriph)") 43 | connectedPeripheral = connectedPeriph 44 | self.littleBT.disconnect() 45 | .delay(for: .seconds(5), scheduler: DispatchQueue.global()) 46 | .sink(receiveCompletion: { _ in 47 | }) { _ in 48 | connectionExpectation.fulfill() 49 | } 50 | .store(in: &self.disposeBag) 51 | } 52 | .store(in: &disposeBag) 53 | 54 | waitForExpectations(timeout: 15) 55 | XCTAssertNotNil(connectedPeripheral) 56 | XCTAssertEqual(connectedPeripheral!.cbPeripheral.identifier, blinky.identifier) 57 | } 58 | 59 | /// Connection custom operator test 60 | func testConnectionOperatorFromPeriphIdentifier() { 61 | disposeBag.removeAll() 62 | blinkyWOR.simulateProximityChange(.outOfRange) 63 | blinky.simulateProximityChange(.immediate) 64 | let connectionExpectation = expectation(description: "Connection identifier expectation") 65 | 66 | var connectedPeripheral: Peripheral? 67 | 68 | StartLittleBlueTooth 69 | .startDiscovery(for: self.littleBT, withServices: nil) 70 | .prefix(1) 71 | .map{ PeripheralIdentifier(peripheral: $0.cbPeripheral)} 72 | .connect(for: self.littleBT) 73 | .sink(receiveCompletion: { completion in 74 | print("Completion \(completion)") 75 | }) { (connectedPeriph) in 76 | print("Discovery \(connectedPeriph)") 77 | connectedPeripheral = connectedPeriph 78 | self.littleBT.disconnect() 79 | .delay(for: .seconds(5), scheduler: DispatchQueue.global()) 80 | .sink(receiveCompletion: { _ in 81 | }) { _ in 82 | connectionExpectation.fulfill() 83 | } 84 | .store(in: &self.disposeBag) 85 | } 86 | .store(in: &disposeBag) 87 | 88 | waitForExpectations(timeout: 15) 89 | XCTAssertNotNil(connectedPeripheral) 90 | XCTAssertEqual(connectedPeripheral!.cbPeripheral.identifier, blinky.identifier) 91 | } 92 | 93 | /// Scan e Stop custom operator test 94 | func testScanStopOperatorFromScanDiscovery() { 95 | disposeBag.removeAll() 96 | blinkyWOR.simulateProximityChange(.immediate) 97 | blinky.simulateProximityChange(.immediate) 98 | let scanExpectation = expectation(description: "Scanning expectation") 99 | scanExpectation.expectedFulfillmentCount = 2 100 | var isStopped = false 101 | var periphCounter = 0 102 | 103 | StartLittleBlueTooth 104 | .startDiscovery(for: self.littleBT, withServices: nil) 105 | .map { _ in 106 | periphCounter += 1 107 | } 108 | .delay(for: .seconds(5), scheduler: DispatchQueue.global()) 109 | .stopDiscovery(for: self.littleBT) 110 | .map { 111 | isStopped = true 112 | } 113 | .sink(receiveCompletion: { completion in 114 | print("Completion \(completion)") 115 | }) { (_) in 116 | print("Stopped") 117 | blinkyWOR.simulateProximityChange(.outOfRange) 118 | scanExpectation.fulfill() 119 | } 120 | .store(in: &disposeBag) 121 | 122 | waitForExpectations(timeout: 15) 123 | XCTAssertTrue(isStopped) 124 | XCTAssertTrue(periphCounter == 2) 125 | } 126 | 127 | /// ReadRSSI custom operator test 128 | func testPeripheralConnectionReadRSSIOperator() { 129 | disposeBag.removeAll() 130 | 131 | blinky.simulateProximityChange(.near) 132 | let readRSSIExpectation = expectation(description: "Read RSSI expectation") 133 | 134 | var rssiRead: Int? 135 | 136 | StartLittleBlueTooth 137 | .startDiscovery(for: self.littleBT, withServices: nil) 138 | .connect(for: self.littleBT) 139 | .readRSSI(for: self.littleBT) 140 | .sink(receiveCompletion: { completion in 141 | print("Completion \(completion)") 142 | }) { (rssi) in 143 | print("RSSI \(rssi)") 144 | rssiRead = rssi 145 | self.littleBT.disconnect().sink(receiveCompletion: { _ in 146 | }) { _ in 147 | readRSSIExpectation.fulfill() 148 | } 149 | .store(in: &self.disposeBag) 150 | } 151 | .store(in: &disposeBag) 152 | 153 | waitForExpectations(timeout: 15) 154 | XCTAssertNotNil(rssiRead) 155 | XCTAssert(rssiRead! < 70) 156 | 157 | } 158 | 159 | /// Disconnection custom operator test 160 | func testDisconnectionOperator() { 161 | disposeBag.removeAll() 162 | 163 | blinky.simulateProximityChange(.immediate) 164 | 165 | let disconnectionExpectation = expectation(description: "Disconnection expectation") 166 | var isDisconnected = false 167 | 168 | StartLittleBlueTooth 169 | .startDiscovery(for: self.littleBT, withServices: nil) 170 | .connect(for: littleBT) 171 | .delay(for: .seconds(5), scheduler: DispatchQueue.global()) 172 | .disconnect(for: littleBT) 173 | .sink(receiveCompletion: { completion in 174 | print("Completion \(completion)") 175 | }) { (disconnectedPeriph) in 176 | print("Disconnection \(disconnectedPeriph)") 177 | isDisconnected = true 178 | disconnectionExpectation.fulfill() 179 | } 180 | .store(in: &disposeBag) 181 | 182 | waitForExpectations(timeout: 30) 183 | XCTAssert(isDisconnected) 184 | } 185 | 186 | 187 | /// Read custom operator test 188 | func testReadLedOFFOperator() { 189 | disposeBag.removeAll() 190 | 191 | blinky.simulateProximityChange(.immediate) 192 | let charateristic = LittleBlueToothCharacteristic(characteristic: CBUUID.ledCharacteristic.uuidString, for: CBUUID.nordicBlinkyService.uuidString, properties: [.read, .notify, .write]) 193 | let readExpectation = expectation(description: "Read expectation") 194 | 195 | var ledState: LedState? 196 | 197 | StartLittleBlueTooth 198 | .startDiscovery(for: self.littleBT, withServices: nil) 199 | .connect(for: self.littleBT) 200 | .read(for: self.littleBT, from: charateristic) 201 | .sink(receiveCompletion: { completion in 202 | print("Completion \(completion)") 203 | }) { (answer: LedState) in 204 | print("Answer \(answer)") 205 | ledState = answer 206 | self.littleBT.disconnect().sink(receiveCompletion: {_ in 207 | }) { (_) in 208 | readExpectation.fulfill() 209 | } 210 | .store(in: &self.disposeBag) 211 | } 212 | .store(in: &disposeBag) 213 | 214 | waitForExpectations(timeout: 10) 215 | XCTAssertNotNil(ledState) 216 | XCTAssert(ledState!.isOn == false) 217 | } 218 | 219 | /// Write custom operator test 220 | func testWriteLedOnReadLedONOperator() { 221 | disposeBag.removeAll() 222 | 223 | blinky.simulateProximityChange(.immediate) 224 | let charateristic = LittleBlueToothCharacteristic(characteristic: CBUUID.ledCharacteristic.uuidString, for: CBUUID.nordicBlinkyService.uuidString, properties: [.read, .notify, .write]) 225 | let readExpectation = expectation(description: "Read expectation") 226 | 227 | var ledState: LedState? 228 | 229 | StartLittleBlueTooth 230 | .startDiscovery(for: self.littleBT, withServices: nil) 231 | .connect(for: self.littleBT) 232 | .write(for: self.littleBT, to: charateristic, value: Data([0x01])) 233 | .read(for: self.littleBT, from: charateristic) 234 | .sink(receiveCompletion: { completion in 235 | print("Completion \(completion)") 236 | }) { (answer: LedState) in 237 | print("Answer \(answer)") 238 | ledState = answer 239 | self.littleBT.disconnect().sink(receiveCompletion: {_ in 240 | }) { (_) in 241 | readExpectation.fulfill() 242 | } 243 | .store(in: &self.disposeBag) 244 | 245 | } 246 | .store(in: &disposeBag) 247 | waitForExpectations(timeout: 10) 248 | XCTAssertNotNil(ledState) 249 | XCTAssert(ledState!.isOn == true) 250 | } 251 | 252 | /// Write and listen custom operator test 253 | func testWriteAndListenOperator() { 254 | disposeBag.removeAll() 255 | 256 | blinky.simulateProximityChange(.immediate) 257 | let charateristic = LittleBlueToothCharacteristic(characteristic: CBUUID.ledCharacteristic.uuidString, for: CBUUID.nordicBlinkyService.uuidString, properties: [.read, .notify, .write]) 258 | let writeAndListenExpectation = expectation(description: "Write and Listen") 259 | 260 | var ledState: LedState? 261 | 262 | Timer.publish(every: 0.5, on: .main, in: .common) 263 | .autoconnect() 264 | .map {_ in 265 | blinky.simulateValueUpdate(Data([0x01]), 266 | for: CBMCharacteristicMock.ledCharacteristic) 267 | }.sink { value in 268 | print("Led value:\(value)") 269 | } 270 | .store(in: &self.disposeBag) 271 | 272 | StartLittleBlueTooth 273 | .startDiscovery(for: self.littleBT, withServices: nil) 274 | .prefix(1) 275 | .connect(for: self.littleBT) 276 | .writeAndListen(for: self.littleBT, from: charateristic, value: Data([0x01])) 277 | .sink(receiveCompletion: { completion in 278 | print("Completion \(completion)") 279 | }) { (answer: LedState) in 280 | print("Answer \(answer)") 281 | ledState = answer 282 | self.littleBT.disconnect().sink(receiveCompletion: {_ in 283 | }) { (_) in 284 | writeAndListenExpectation.fulfill() 285 | } 286 | .store(in: &self.disposeBag) 287 | 288 | } 289 | .store(in: &disposeBag) 290 | waitForExpectations(timeout: 10) 291 | XCTAssertNotNil(ledState) 292 | XCTAssert(ledState!.isOn == true) 293 | } 294 | /// Listen custom operator test 295 | func testListenOperator() { 296 | disposeBag.removeAll() 297 | 298 | blinky.simulateProximityChange(.immediate) 299 | let charateristic = LittleBlueToothCharacteristic(characteristic: CBMUUID.buttonCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString, properties: [.read, .notify]) 300 | let listenExpectation = expectation(description: "Listen expectation") 301 | 302 | var listenCounter = 0 303 | var timerCounter = 0 304 | let timer = Timer.publish(every: 1, on: .main, in: .common) 305 | let scheduler: AnyCancellable = 306 | timer 307 | .map {_ in 308 | blinky.simulateValueUpdate(Data([0x01]), for: CBMCharacteristicMock.buttonCharacteristic) 309 | timerCounter += 1 310 | }.sink { value in 311 | print("Led value:\(value)") 312 | } 313 | 314 | StartLittleBlueTooth 315 | .startDiscovery(for: self.littleBT, withServices: nil) 316 | .connect(for: self.littleBT) 317 | .startListen(for: self.littleBT, from: charateristic) 318 | .sink(receiveCompletion: { completion in 319 | print("Completion \(completion)") 320 | }) { (answer: LedState) in 321 | listenCounter += 1 322 | print("Answer \(answer)") 323 | if listenCounter > 10 { 324 | scheduler.cancel() 325 | self.littleBT.disconnect().sink(receiveCompletion: {_ in 326 | }) { (_) in 327 | listenExpectation.fulfill() 328 | } 329 | .store(in: &self.disposeBag) 330 | } 331 | } 332 | .store(in: &disposeBag) 333 | _ = timer.connect() 334 | waitForExpectations(timeout: 20) 335 | let contingencyRange = (timerCounter - 2)...timerCounter 336 | print("Timer counter: \(timerCounter) Listen counter \(listenCounter) ") 337 | XCTAssert(contingencyRange.contains(listenCounter)) 338 | } 339 | /// Enable Listen custom operator test 340 | func testListenToMoreCharacteristicOperator() { 341 | disposeBag.removeAll() 342 | 343 | blinky.simulateProximityChange(.immediate) 344 | let charateristicOne = LittleBlueToothCharacteristic(characteristic: CBMUUID.buttonCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString, properties: [.read, .notify]) 345 | let charateristicTwo = LittleBlueToothCharacteristic(characteristic: CBMUUID.ledCharacteristic.uuidString, for: CBMUUID.nordicBlinkyService.uuidString, properties: [.read, .notify, .write]) 346 | // Expectation 347 | let firstListenExpectation = XCTestExpectation(description: "First sub more expectation") 348 | let secondListenExpectation = XCTestExpectation(description: "Second sub more expectation") 349 | var sub1Event = [Bool]() 350 | var sub2Event = [Bool]() 351 | var firstCounter = 0 352 | var secondCounter = 0 353 | // Simulate notification 354 | var timerCounter = 0 355 | let timer = Timer.publish(every: 1, on: .main, in: .common) 356 | let scheduler: AnyCancellable = timer 357 | .map {_ -> UInt8 in 358 | var data = UInt8.random(in: 0...1) 359 | blinky.simulateValueUpdate(Data([data]), for: CBMCharacteristicMock.buttonCharacteristic) 360 | data = UInt8.random(in: 0...1) 361 | blinky.simulateValueUpdate(Data([data]), for: CBMCharacteristicMock.ledCharacteristic) 362 | timerCounter += 1 363 | return data 364 | }.sink { value in 365 | print("Sink from timer value:\(value)") 366 | } 367 | 368 | // First publisher 369 | littleBT.listenPublisher 370 | .filter { charact -> Bool in 371 | charact.id == charateristicOne.id 372 | } 373 | .tryMap { (characteristic) -> ButtonState in 374 | try characteristic.value() 375 | } 376 | .mapError { (error) -> LittleBluetoothError in 377 | if let er = error as? LittleBluetoothError { 378 | return er 379 | } 380 | return .emptyData 381 | } 382 | .sink(receiveCompletion: { completion in 383 | print("Completion \(completion)") 384 | }) { (answer) in 385 | print("Sub1: \(answer)") 386 | if firstCounter == 10 { 387 | scheduler.cancel() 388 | return 389 | } else { 390 | sub1Event.append(answer.isOn) 391 | firstCounter += 1 392 | } 393 | } 394 | .store(in: &self.disposeBag) 395 | 396 | // Second publisher 397 | littleBT.listenPublisher 398 | .filter { charact -> Bool in 399 | charact.id == charateristicTwo.id 400 | } 401 | .tryMap { (characteristic) -> LedState in 402 | try characteristic.value() 403 | }.mapError { (error) -> LittleBluetoothError in 404 | if let er = error as? LittleBluetoothError { 405 | return er 406 | } 407 | return .emptyData 408 | } 409 | .sink(receiveCompletion: { completion in 410 | print("Completion \(completion)") 411 | }) { (answer) in 412 | print("Sub2: \(answer)") 413 | if secondCounter == 10 { 414 | return 415 | } else { 416 | sub2Event.append(answer.isOn) 417 | secondCounter += 1 418 | } 419 | } 420 | .store(in: &self.disposeBag) 421 | 422 | StartLittleBlueTooth 423 | .startDiscovery(for: self.littleBT, withServices: nil) 424 | .connect(for: self.littleBT) 425 | .enableListen(for: self.littleBT, from: charateristicOne) 426 | .enableListen(for: self.littleBT, from: charateristicTwo) 427 | .delay(for: .seconds(20), scheduler: DispatchQueue.global()) 428 | .disableListen(for: self.littleBT, from: charateristicOne) 429 | .disableListen(for: self.littleBT, from: charateristicTwo) 430 | .sink(receiveCompletion: { completion in 431 | print("Completion \(completion)") 432 | }) { (_) in 433 | secondListenExpectation.fulfill() 434 | firstListenExpectation.fulfill() 435 | } 436 | .store(in: &disposeBag) 437 | _ = timer.connect() 438 | 439 | wait(for: [firstListenExpectation, secondListenExpectation], timeout: 30) 440 | littleBT.disconnect() 441 | XCTAssert(sub1Event.count == sub2Event.count) 442 | let contingencyRange = (timerCounter - 2)...timerCounter 443 | print("Timer counter: \(timerCounter) Event counter \(sub2Event.count) ") 444 | XCTAssert(contingencyRange.contains(sub2Event.count)) 445 | } 446 | 447 | } 448 | --------------------------------------------------------------------------------