Skip to content

Commit ed213dc

Browse files
NickKibishgithub-actions[bot]
authored andcommitted
Copied files from native CoreBluetooth version to CoreBluetoothMock
1 parent a5bc8c8 commit ed213dc

22 files changed

+2372
-0
lines changed
+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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 CoreBluetoothMock
32+
33+
// Copy this file to your project to start using CoreBluetoothMock classes
34+
// without having to refactor any of your code. You will just have to remove
35+
// the imports to CoreBluetooth to fix conflicts and initiate the manager
36+
// using CBCentralManagerFactory, instad of just creating a CBCentralManager.
37+
38+
// disabled for Xcode 12.5 beta
39+
//typealias CBPeer = CBMPeer
40+
//typealias CBAttribute = CBMAttribute
41+
public typealias CBCentralManagerFactory = CBMCentralManagerFactory
42+
public typealias CBUUID = CBMUUID
43+
public typealias CBError = CBMError
44+
public typealias CBATTError = CBMATTError
45+
public typealias CBManagerState = CBMManagerState
46+
public typealias CBPeripheralState = CBMPeripheralState
47+
public typealias CBCentralManager = CBMCentralManager
48+
public typealias CBCentralManagerDelegate = CBMCentralManagerDelegate
49+
public typealias CBPeripheral = CBMPeripheral
50+
public typealias CBPeripheralDelegate = CBMPeripheralDelegate
51+
public typealias CBService = CBMService
52+
public typealias CBCharacteristic = CBMCharacteristic
53+
public typealias CBCharacteristicWriteType = CBMCharacteristicWriteType
54+
public typealias CBCharacteristicProperties = CBMCharacteristicProperties
55+
public typealias CBDescriptor = CBMDescriptor
56+
public typealias CBConnectionEvent = CBMConnectionEvent
57+
public typealias CBConnectionEventMatchingOption = CBMConnectionEventMatchingOption
58+
@available(iOS 11.0, tvOS 11.0, watchOS 4.0, *)
59+
public typealias CBL2CAPPSM = CBML2CAPPSM
60+
@available(iOS 11.0, tvOS 11.0, watchOS 4.0, *)
61+
public typealias CBL2CAPChannel = CBML2CAPChannel
62+
63+
public let CBCentralManagerScanOptionAllowDuplicatesKey =
64+
CBMCentralManagerScanOptionAllowDuplicatesKey
65+
public let CBCentralManagerOptionShowPowerAlertKey = CBMCentralManagerOptionShowPowerAlertKey
66+
public let CBCentralManagerOptionRestoreIdentifierKey = CBMCentralManagerOptionRestoreIdentifierKey
67+
public let CBCentralManagerScanOptionSolicitedServiceUUIDsKey =
68+
CBMCentralManagerScanOptionSolicitedServiceUUIDsKey
69+
public let CBConnectPeripheralOptionStartDelayKey = CBMConnectPeripheralOptionStartDelayKey
70+
#if !os(macOS)
71+
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
72+
public let CBConnectPeripheralOptionRequiresANCS = CBMConnectPeripheralOptionRequiresANCS
73+
#endif
74+
public let CBCentralManagerRestoredStatePeripheralsKey =
75+
CBMCentralManagerRestoredStatePeripheralsKey
76+
public let CBCentralManagerRestoredStateScanServicesKey =
77+
CBMCentralManagerRestoredStateScanServicesKey
78+
public let CBCentralManagerRestoredStateScanOptionsKey =
79+
CBMCentralManagerRestoredStateScanOptionsKey
80+
81+
public let CBAdvertisementDataLocalNameKey = CBMAdvertisementDataLocalNameKey
82+
public let CBAdvertisementDataServiceUUIDsKey = CBMAdvertisementDataServiceUUIDsKey
83+
public let CBAdvertisementDataIsConnectable = CBMAdvertisementDataIsConnectable
84+
public let CBAdvertisementDataTxPowerLevelKey = CBMAdvertisementDataTxPowerLevelKey
85+
public let CBAdvertisementDataServiceDataKey = CBMAdvertisementDataServiceDataKey
86+
public let CBAdvertisementDataManufacturerDataKey = CBMAdvertisementDataManufacturerDataKey
87+
public let CBAdvertisementDataOverflowServiceUUIDsKey = CBMAdvertisementDataOverflowServiceUUIDsKey
88+
public let CBAdvertisementDataSolicitedServiceUUIDsKey =
89+
CBMAdvertisementDataSolicitedServiceUUIDsKey
90+
91+
public let CBConnectPeripheralOptionNotifyOnConnectionKey =
92+
CBMConnectPeripheralOptionNotifyOnConnectionKey
93+
public let CBConnectPeripheralOptionNotifyOnDisconnectionKey =
94+
CBMConnectPeripheralOptionNotifyOnDisconnectionKey
95+
public let CBConnectPeripheralOptionNotifyOnNotificationKey =
96+
CBMConnectPeripheralOptionNotifyOnNotificationKey
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by Nick Kibysh on 18/04/2023.
6+
//
7+
8+
import Combine
9+
import CoreBluetoothMock
10+
import Foundation
11+
12+
extension CentralManager {
13+
public enum Err: Error {
14+
case wrongManager
15+
case badState(CBManagerState)
16+
case unknownError
17+
18+
public var localizedDescription: String {
19+
switch self {
20+
case .wrongManager:
21+
return "Incorrect manager instance provided."
22+
case .badState(let state):
23+
return "Bad state: \(state)"
24+
case .unknownError:
25+
return "An unknown error occurred."
26+
}
27+
}
28+
}
29+
}
30+
31+
private class Observer: NSObject {
32+
@objc dynamic private weak var cm: CBCentralManager?
33+
private weak var publisher: CurrentValueSubject<Bool, Never>?
34+
private var observation: NSKeyValueObservation?
35+
36+
init(cm: CBCentralManager, publisher: CurrentValueSubject<Bool, Never>) {
37+
self.cm = cm
38+
self.publisher = publisher
39+
super.init()
40+
}
41+
42+
func setup() {
43+
observation = observe(
44+
\.cm?.isScanning,
45+
options: [.old, .new],
46+
changeHandler: { _, change in
47+
48+
change.newValue?.flatMap { [weak self] new in
49+
self?.publisher?.send(new)
50+
}
51+
}
52+
)
53+
}
54+
}
55+
56+
/// A custom Central Manager class that extends the functionality of the standard CBCentralManager.
57+
/// This class brings a reactive approach and is based on the Swift Combine framework.
58+
public class CentralManager {
59+
private let isScanningSubject = CurrentValueSubject<Bool, Never>(false)
60+
private let killSwitchSubject = PassthroughSubject<Void, Never>()
61+
private lazy var observer = Observer(cm: centralManager, publisher: isScanningSubject)
62+
63+
public let centralManager: CBCentralManager
64+
public let centralManagerDelegate: ReactiveCentralManagerDelegate
65+
66+
/// Initializes a new instance of `CentralManager`.
67+
/// - Parameters:
68+
/// - centralManagerDelegate: The delegate for the reactive central manager. Default is `ReactiveCentralManagerDelegate()`.
69+
/// - queue: The queue to perform operations on. Default is the main queue.
70+
public init(
71+
centralManagerDelegate: ReactiveCentralManagerDelegate =
72+
ReactiveCentralManagerDelegate(), queue: DispatchQueue = .main
73+
) {
74+
self.centralManagerDelegate = centralManagerDelegate
75+
self.centralManager = CBMCentralManagerFactory.instance(
76+
delegate: centralManagerDelegate, queue: queue)
77+
observer.setup()
78+
}
79+
80+
/// Initializes a new instance of `CentralManager` with an existing CBCentralManager instance.
81+
/// - Parameter centralManager: An existing CBCentralManager instance.
82+
/// - Throws: An error if the provided manager's delegate is not of type `ReactiveCentralManagerDelegate`.
83+
public init(centralManager: CBCentralManager) throws {
84+
guard
85+
let reactiveDelegate = centralManager.delegate
86+
as? ReactiveCentralManagerDelegate
87+
else {
88+
throw Err.wrongManager
89+
}
90+
91+
self.centralManager = centralManager
92+
self.centralManagerDelegate = reactiveDelegate
93+
94+
observer.setup()
95+
}
96+
}
97+
98+
// MARK: Establishing or Canceling Connections with Peripherals
99+
extension CentralManager {
100+
/// Establishes a connection with the specified peripheral.
101+
/// - Parameters:
102+
/// - peripheral: The peripheral to connect to.
103+
/// - options: Optional connection options.
104+
/// - Returns: A publisher that emits the connected peripheral on successful connection.
105+
/// The publisher does not finish until the peripheral is successfully connected.
106+
/// If the peripheral was disconnected successfully, the publisher finishes without error.
107+
/// If the connection was unsuccessful or disconnection returns an error (e.g., peripheral disconnected unexpectedly),
108+
/// the publisher finishes with an error.
109+
public func connect(_ peripheral: CBPeripheral, options: [String: Any]? = nil)
110+
-> Publishers.BluetoothPublisher<CBPeripheral, Error>
111+
{
112+
let killSwitch = self.disconnectedPeripheralsChannel.tryFirst(where: { p in
113+
if let e = p.1 {
114+
throw e
115+
}
116+
return p.0.identifier == peripheral.identifier
117+
})
118+
119+
return self.connectedPeripheralChannel
120+
.filter { $0.0.identifier == peripheral.identifier }
121+
.tryMap { p in
122+
if let e = p.1 {
123+
throw e
124+
}
125+
126+
return p.0
127+
}
128+
.prefix(untilUntilOutputOrCompletion: killSwitch)
129+
.bluetooth {
130+
self.centralManager.connect(peripheral, options: options)
131+
}
132+
}
133+
134+
/// Cancels the connection with the specified peripheral.
135+
/// - Parameter peripheral: The peripheral to disconnect from.
136+
/// - Returns: A publisher that emits the disconnected peripheral.
137+
public func cancelPeripheralConnection(_ peripheral: CBPeripheral) -> Publishers.Peripheral
138+
{
139+
return self.disconnectedPeripheralsChannel
140+
.tryFilter { r in
141+
guard r.0.identifier == peripheral.identifier else {
142+
return false
143+
}
144+
145+
if let e = r.1 {
146+
throw e
147+
} else {
148+
return true
149+
}
150+
}
151+
.map { $0.0 }
152+
.first()
153+
.peripheral {
154+
self.centralManager.cancelPeripheralConnection(peripheral)
155+
}
156+
}
157+
}
158+
159+
// MARK: Retrieving Lists of Peripherals
160+
extension CentralManager {
161+
/// Returns a list of the peripherals connected to the system whose
162+
/// services match a given set of criteria.
163+
///
164+
/// The list of connected peripherals can include those that other apps
165+
/// have connected. You need to connect these peripherals locally using
166+
/// the `connect(_:options:)` method before using them.
167+
/// - Parameter serviceUUIDs: A list of service UUIDs, represented by
168+
/// `CBUUID` objects.
169+
/// - Returns: A list of the peripherals that are currently connected
170+
/// to the system and that contain any of the services
171+
/// specified in the `serviceUUID` parameter.
172+
public func retrieveConnectedPeripherals(withServices identifiers: [CBUUID])
173+
-> [CBPeripheral]
174+
{
175+
centralManager.retrieveConnectedPeripherals(withServices: identifiers)
176+
}
177+
178+
/// Returns a list of known peripherals by their identifiers.
179+
/// - Parameter identifiers: A list of peripheral identifiers
180+
/// (represented by `NSUUID` objects) from which
181+
/// ``CBPeripheral`` objects can be retrieved.
182+
/// - Returns: A list of peripherals that the central manager is able
183+
/// to match to the provided identifiers.
184+
public func retrievePeripherals(withIdentifiers identifiers: [UUID]) -> [CBPeripheral] {
185+
centralManager.retrievePeripherals(withIdentifiers: identifiers)
186+
}
187+
}
188+
189+
// MARK: Scanning or Stopping Scans of Peripherals
190+
extension CentralManager {
191+
/// Initiates a scan for peripherals with the specified services.
192+
/// - Parameter services: The services to scan for.
193+
/// - Returns: A publisher that emits scan results or errors.
194+
public func scanForPeripherals(withServices services: [CBUUID]?)
195+
-> Publishers.BluetoothPublisher<ScanResult, Error>
196+
{
197+
stopScan()
198+
// TODO: Change to BluetoothPublisher
199+
return centralManagerDelegate.stateSubject
200+
.tryFirst { state in
201+
guard let determined = state.ready else { return false }
202+
203+
guard determined else { throw Err.badState(state) }
204+
return true
205+
}
206+
.flatMap { _ in
207+
// TODO: Check for mmemory leaks
208+
return self.centralManagerDelegate.scanResultSubject
209+
.setFailureType(to: Error.self)
210+
}
211+
.map { a in
212+
return a
213+
}
214+
.prefix(untilOutputFrom: killSwitchSubject)
215+
.mapError { [weak self] e in
216+
self?.stopScan()
217+
return e
218+
}
219+
.bluetooth {
220+
self.centralManager.scanForPeripherals(withServices: services)
221+
}
222+
}
223+
224+
/// Stops an ongoing scan for peripherals.
225+
/// Calling this method finishes the publisher returned by ``scanForPeripherals(withServices:)``.
226+
public func stopScan() {
227+
centralManager.stopScan()
228+
killSwitchSubject.send(())
229+
}
230+
}
231+
232+
// MARK: Channels
233+
extension CentralManager {
234+
/// A publisher that emits the state of the central manager.
235+
public var stateChannel: AnyPublisher<CBManagerState, Never> {
236+
centralManagerDelegate
237+
.stateSubject
238+
.eraseToAnyPublisher()
239+
}
240+
241+
/// A publisher that emits the scanning state.
242+
public var isScanningChannel: AnyPublisher<Bool, Never> {
243+
isScanningSubject
244+
.eraseToAnyPublisher()
245+
}
246+
247+
/// A publisher that emits scan results.
248+
public var scanResultsChannel: AnyPublisher<ScanResult, Never> {
249+
centralManagerDelegate.scanResultSubject
250+
.eraseToAnyPublisher()
251+
}
252+
253+
/// A publisher that emits connected peripherals along with errors.
254+
public var connectedPeripheralChannel: AnyPublisher<(CBPeripheral, Error?), Never> {
255+
centralManagerDelegate.connectedPeripheralSubject
256+
.eraseToAnyPublisher()
257+
}
258+
259+
/// A publisher that emits disconnected peripherals along with errors.
260+
public var disconnectedPeripheralsChannel: AnyPublisher<(CBPeripheral, Error?), Never> {
261+
centralManagerDelegate.disconnectedPeripheralsSubject
262+
.eraseToAnyPublisher()
263+
}
264+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by Nick Kibysh on 19/04/2023.
6+
//
7+
8+
import CoreBluetoothMock
9+
import Foundation
10+
11+
public struct ScanResult {
12+
public let peripheral: CBPeripheral
13+
public let rssi: RSSI
14+
public let advertisementData: AdvertisementData
15+
16+
init(peripheral: CBPeripheral, rssi: NSNumber, advertisementData: [String: Any]) {
17+
self.peripheral = peripheral
18+
self.rssi = RSSI(integerLiteral: rssi.intValue)
19+
self.advertisementData = AdvertisementData(advertisementData)
20+
}
21+
22+
public var name: String? {
23+
peripheral.name ?? advertisementData.localName
24+
}
25+
}

0 commit comments

Comments
 (0)