diff --git a/README.md b/README.md index 60193e0..60072d4 100644 --- a/README.md +++ b/README.md @@ -5,24 +5,24 @@ Easily communicate between iOS devices using BLE. [![Cocoapods Compatible](https://img.shields.io/cocoapods/v/BluetoothKit.svg)](https://img.shields.io/cocoapods/v/BluetoothKit.svg) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) -##Background +## Background Apple mostly did a great job with the CoreBluetooth API, but because it encapsulated the entire Bluetooth 4.0 LE specification, it can be a lot of work to achieve simple tasks like sending data back and forth between iOS devices, without having to worry about the specification and the inner workings of the CoreBluetooth stack. BluetoothKit tries to address the challenges this may cause by providing a much simpler, modern, closure-based API all implemented in Swift. -##Features +## Features -####Common +#### Common - More concise Bluetooth LE availability definition with enums. - Bluetooth LE availability observation allowing multiple observers at once. -####Central +#### Central - Scan for remote peripherals for a given time interval. - Continuously scan for remote peripherals for a give time interval, with an in-between delay until interrupted. - Connect to remote peripherals with a given time interval as time out. - Receive any size of data without having to worry about chunking. -####Peripheral +#### Peripheral - Start broadcasting with only a single function call. - Send any size of data to connected remote centrals without having to worry about chunking. @@ -32,7 +32,7 @@ BluetoothKit tries to address the challenges this may cause by providing a much ## Installation -####CocoaPods +#### CocoaPods [CocoaPods](http://cocoapods.org) is a dependency manager for Cocoa projects. CocoaPods 0.38.2 is required to build BluetoothKit. It adds support for Xcode 7, Swift 2.0 and embedded frameworks. You can install it with the following command: @@ -57,7 +57,7 @@ Then, run the following command: $ pod install ``` -####Carthage +#### Carthage [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that automates the process of adding frameworks to your Cocoa application. You can install Carthage with [Homebrew](http://brew.sh/) using the following command: @@ -73,20 +73,20 @@ To integrate BluetoothKit into your Xcode project using Carthage, specify it in github "rasmusth/BluetoothKit" ~> 0.4.0 ``` -####Manual +#### Manual Add the BluetoothKit project to your existing project and add BluetoothKit as an embedded binary of your target(s). -##Usage +## Usage Below you find some examples of how the framework can be used. Accompanied in the repository you find an example project that demonstrates a usage of the framework in practice. The example project uses [SnapKit](https://github.com/SnapKit/SnapKit) and [CryptoSwift](https://github.com/krzyzanowskim/CryptoSwift) both of which are great projects. They're bundled in the project and it should all build without further ado. -####Common +#### Common Make sure to import the BluetoothKit framework in files that use it. ```swift import BluetoothKit ``` -####Peripheral +#### Peripheral Prepare and start a BKPeripheral object with a configuration holding UUIDs uniqueue to your app(s) and an optional local name that will be broadcasted. You can generate UUIDs in the OSX terminal using the command "uuidgen". ```swift @@ -104,6 +104,24 @@ do { } ``` +Pause advertising without disconnecting from connected centrals. +```swift +do { + try peripheral.pause() +} catch let error { + // Handle error. +} +``` + +Resume advertising using the same data given at startup time (only valid if the peripheral was previously paused and not completely stopped). +```swift +do { + try peripheral.resume() +} catch let error { + // Handle error. +} +``` + Send data to a connected remote central. ```swift let data = "Hello beloved central!".dataUsingEncoding(NSUTF8StringEncoding) @@ -114,7 +132,7 @@ peripheral.sendData(data, toRemoteCentral: remoteCentral) { data, remoteCentral, } ``` -####Central +#### Central Prepare and start a BKCentral object with a configuration holding the UUIDs you used to configure your BKPeripheral object. ```swift let central = BKCentral() @@ -163,5 +181,5 @@ central.connect(remotePeripheral: peripherals[indexPath.row]) { remotePeripheral } ``` -##License +## License BluetoothKit is released under the MIT License. diff --git a/Source/BKAvailability.swift b/Source/BKAvailability.swift index 953af09..f27d2fb 100644 --- a/Source/BKAvailability.swift +++ b/Source/BKAvailability.swift @@ -27,26 +27,26 @@ import CoreBluetooth public func == (lhs: BKAvailability, rhs: BKAvailability) -> Bool { switch (lhs, rhs) { - case (.available, .available): return true - case (.unavailable(cause: .any), .unavailable): return true - case (.unavailable, .unavailable(cause: .any)): return true - case (.unavailable(let lhsCause), .unavailable(let rhsCause)): return lhsCause == rhsCause - default: return false + case (.available, .available): return true + case (.unavailable(cause: .any), .unavailable): return true + case (.unavailable, .unavailable(cause: .any)): return true + case (.unavailable(let lhsCause), .unavailable(let rhsCause)): return lhsCause == rhsCause + default: return false } } /** - Bluetooth LE availability. - - Available: Bluetooth LE is available. - - Unavailable: Bluetooth LE is unavailable. - - The unavailable case can be accompanied by a cause. -*/ + Bluetooth LE availability. + - Available: Bluetooth LE is available. + - Unavailable: Bluetooth LE is unavailable. + + The unavailable case can be accompanied by a cause. + */ public enum BKAvailability: Equatable { - + case available case unavailable(cause: BKUnavailabilityCause) - + #if os(iOS) || os(tvOS) @available(iOS 10.0, tvOS 10.0, *) @available(OSX, unavailable) @@ -57,14 +57,14 @@ public enum BKAvailability: Equatable { } } #endif - + internal init(centralManagerState: CBCentralManagerState) { switch centralManagerState { case .poweredOn: self = .available default: self = .unavailable(cause: BKUnavailabilityCause(centralManagerState: centralManagerState)) } } - + internal init(peripheralManagerState: CBPeripheralManagerState) { switch peripheralManagerState { case .poweredOn: self = .available @@ -74,25 +74,25 @@ public enum BKAvailability: Equatable { } /** - Bluetooth LE unavailability cause. - - Any: When initialized with nil. - - Resetting: Bluetooth is resetting. - - Unsupported: Bluetooth LE is not supported on the device. - - Unauthorized: The app isn't allowed to use Bluetooth. - - PoweredOff: Bluetooth is turned off. -*/ + Bluetooth LE unavailability cause. + - Any: When initialized with nil. + - Resetting: Bluetooth is resetting. + - Unsupported: Bluetooth LE is not supported on the device. + - Unauthorized: The app isn't allowed to use Bluetooth. + - PoweredOff: Bluetooth is turned off. + */ public enum BKUnavailabilityCause: ExpressibleByNilLiteral { - + case any case resetting case unsupported case unauthorized case poweredOff - + public init(nilLiteral: Void) { self = .any } - + #if os(iOS) || os(tvOS) @available(iOS 10.0, tvOS 10.0, *) @available(OSX, unavailable) @@ -106,7 +106,7 @@ public enum BKUnavailabilityCause: ExpressibleByNilLiteral { } } #endif - + internal init(centralManagerState: CBCentralManagerState) { switch centralManagerState { case .poweredOff: self = .poweredOff @@ -116,7 +116,7 @@ public enum BKUnavailabilityCause: ExpressibleByNilLiteral { default: self = nil } } - + internal init(peripheralManagerState: CBPeripheralManagerState) { switch peripheralManagerState { case .poweredOff: self = .poweredOff @@ -126,12 +126,12 @@ public enum BKUnavailabilityCause: ExpressibleByNilLiteral { default: self = nil } } - + } /** - Classes that can be observed for Bluetooth LE availability implement this protocol. -*/ + Classes that can be observed for Bluetooth LE availability implement this protocol. + */ public protocol BKAvailabilityObservable: class { var availabilityObservers: [BKWeakAvailabilityObserver] { get set } func addAvailabilityObserver(_ availabilityObserver: BKAvailabilityObserver) @@ -139,8 +139,8 @@ public protocol BKAvailabilityObservable: class { } /** - Class used to hold a weak reference to an observer of Bluetooth LE availability. -*/ + Class used to hold a weak reference to an observer of Bluetooth LE availability. + */ public class BKWeakAvailabilityObserver { weak var availabilityObserver: BKAvailabilityObserver? init (availabilityObserver: BKAvailabilityObserver) { @@ -149,45 +149,45 @@ public class BKWeakAvailabilityObserver { } public extension BKAvailabilityObservable { - + /** - Add a new availability observer. The observer will be weakly stored. If the observer is already subscribed the call will be ignored. - - parameter availabilityObserver: The availability observer to add. - */ + Add a new availability observer. The observer will be weakly stored. If the observer is already subscribed the call will be ignored. + - parameter availabilityObserver: The availability observer to add. + */ func addAvailabilityObserver(_ availabilityObserver: BKAvailabilityObserver) { if !availabilityObservers.contains(where: { $0.availabilityObserver === availabilityObserver }) { availabilityObservers.append(BKWeakAvailabilityObserver(availabilityObserver: availabilityObserver)) } } - + /** - Remove an availability observer. If the observer isn't subscribed the call will be ignored. - - parameter availabilityObserver: The availability observer to remove. - */ + Remove an availability observer. If the observer isn't subscribed the call will be ignored. + - parameter availabilityObserver: The availability observer to remove. + */ func removeAvailabilityObserver(_ availabilityObserver: BKAvailabilityObserver) { if availabilityObservers.contains(where: { $0.availabilityObserver === availabilityObserver }) { availabilityObservers.remove(at: availabilityObservers.index(where: { $0 === availabilityObserver })!) } } - + } /** - Observers of Bluetooth LE availability should implement this protocol. -*/ + Observers of Bluetooth LE availability should implement this protocol. + */ public protocol BKAvailabilityObserver: class { - + /** - Informs the observer about a change in Bluetooth LE availability. - - parameter availabilityObservable: The object that registered the availability change. - - parameter availability: The new availability value. - */ + Informs the observer about a change in Bluetooth LE availability. + - parameter availabilityObservable: The object that registered the availability change. + - parameter availability: The new availability value. + */ func availabilityObserver(_ availabilityObservable: BKAvailabilityObservable, availabilityDidChange availability: BKAvailability) - + /** - Informs the observer that the cause of Bluetooth LE unavailability changed. - - parameter availabilityObservable: The object that registered the cause change. - - parameter unavailabilityCause: The new cause of unavailability. - */ + Informs the observer that the cause of Bluetooth LE unavailability changed. + - parameter availabilityObservable: The object that registered the cause change. + - parameter unavailabilityCause: The new cause of unavailability. + */ func availabilityObserver(_ availabilityObservable: BKAvailabilityObservable, unavailabilityCauseDidChange unavailabilityCause: BKUnavailabilityCause) } diff --git a/Source/BKCentral.swift b/Source/BKCentral.swift index 9581ba2..ed610a1 100644 --- a/Source/BKCentral.swift +++ b/Source/BKCentral.swift @@ -26,100 +26,109 @@ import Foundation import CoreBluetooth /** - The central's delegate is called when asynchronous events occur. -*/ + The central's delegate is called when asynchronous events occur. + */ public protocol BKCentralDelegate: class { /** - Called when a remote peripheral disconnects or is disconnected. - - parameter central: The central from which it disconnected. - - parameter remotePeripheral: The remote peripheral that disconnected. - */ - + Called when a remote peripheral disconnects or is disconnected. + - parameter central: The central from which it disconnected. + - parameter remotePeripheral: The remote peripheral that disconnected. + */ + func central(_ central: BKCentral, remotePeripheralDidDisconnect remotePeripheral: BKRemotePeripheral) } /** - The class used to take the Bluetooth LE central role. The central discovers remote peripherals by scanning - and connects to them. When a connection is established the central can receive data from the remote peripheral. -*/ + The class used to take the Bluetooth LE central role. The central discovers remote peripherals by scanning + and connects to them. When a connection is established the central can receive data from the remote peripheral. + */ public class BKCentral: BKPeer, BKCBCentralManagerStateDelegate, BKConnectionPoolDelegate, BKAvailabilityObservable { - + // MARK: Type Aliases - + public typealias ScanProgressHandler = ((_ newDiscoveries: [BKDiscovery]) -> Void) public typealias ScanCompletionHandler = ((_ result: [BKDiscovery]?, _ error: BKError?) -> Void) public typealias ContinuousScanChangeHandler = ((_ changes: [BKDiscoveriesChange], _ discoveries: [BKDiscovery]) -> Void) public typealias ContinuousScanStateHandler = ((_ newState: ContinuousScanState) -> Void) public typealias ContinuousScanErrorHandler = ((_ error: BKError) -> Void) public typealias ConnectCompletionHandler = ((_ remotePeripheral: BKRemotePeripheral, _ error: BKError?) -> Void) - + // MARK: Enums - + /** - Possible states returned by the ContinuousScanStateHandler. - - Stopped: The scan has come to a complete stop and won't start again by triggered manually. - - Scanning: The scan is currently active. - - Waiting: The scan is on hold due while waiting for the in-between delay to expire, after which it will start again. - */ + Possible states returned by the ContinuousScanStateHandler. + - Stopped: The scan has come to a complete stop and won't start again by triggered manually. + - Scanning: The scan is currently active. + - Waiting: The scan is on hold due while waiting for the in-between delay to expire, after which it will start again. + */ public enum ContinuousScanState { case stopped case scanning case waiting } - + // MARK: Properties - + /// Bluetooth LE availability, derived from the underlying CBCentralManager. public var availability: BKAvailability? { guard let centralManager = _centralManager else { return nil } - #if os(iOS) || os(tvOS) - if #available(tvOS 10.0, iOS 10.0, *) { - return BKAvailability(managerState: centralManager.state) - } else { - return BKAvailability(centralManagerState: centralManager.centralManagerState) - } - #else - return BKAvailability(centralManagerState: centralManager.state) - #endif - + #if os(iOS) || os(tvOS) + if #available(tvOS 10.0, iOS 10.0, *) { + return BKAvailability(managerState: centralManager.state) + } else { + return BKAvailability(centralManagerState: centralManager.centralManagerState) + } + #else + return BKAvailability(centralManagerState: centralManager.state) + #endif + } - + /// All currently connected remote peripherals. public var connectedRemotePeripherals: [BKRemotePeripheral] { return connectionPool.connectedRemotePeripherals } - + override public var configuration: BKConfiguration? { return _configuration } - + /// The delegate of the BKCentral object. public weak var delegate: BKCentralDelegate? - + /// Current availability observers. public var availabilityObservers = [BKWeakAvailabilityObserver]() - + internal override var connectedRemotePeers: [BKRemotePeer] { get { return connectedRemotePeripherals } set { - connectionPool.connectedRemotePeripherals = newValue.flatMap({ + #if swift(>=4.1) + connectionPool.connectedRemotePeripherals = newValue.compactMap({ guard let remotePeripheral = $0 as? BKRemotePeripheral else { return nil } return remotePeripheral }) + #else + connectionPool.connectedRemotePeripherals = newValue.flatMap({ + guard let remotePeripheral = $0 as? BKRemotePeripheral else { + return nil + } + return remotePeripheral + }) + #endif } } - + private var centralManager: CBCentralManager? { return _centralManager } - + private let scanner = BKScanner() private let connectionPool = BKConnectionPool() private var _configuration: BKConfiguration? @@ -127,9 +136,9 @@ public class BKCentral: BKPeer, BKCBCentralManagerStateDelegate, BKConnectionPoo private var centralManagerDelegate: BKCBCentralManagerDelegateProxy! private var stateMachine: BKCentralStateMachine! private var _centralManager: CBCentralManager! - + // MARK: Initialization - + public override init() { super.init() centralManagerDelegate = BKCBCentralManagerDelegateProxy(stateDelegate: self, discoveryDelegate: scanner, connectionDelegate: connectionPool) @@ -137,14 +146,14 @@ public class BKCentral: BKPeer, BKCBCentralManagerStateDelegate, BKConnectionPoo connectionPool.delegate = self continuousScanner = BKContinousScanner(scanner: scanner) } - + // MARK: Public Functions - + /** - Start the BKCentral object with a configuration. - - parameter configuration: The configuration defining which UUIDs to use when discovering peripherals. - - throws: Throws an InternalError if the BKCentral object is already started. - */ + Start the BKCentral object with a configuration. + - parameter configuration: The configuration defining which UUIDs to use when discovering peripherals. + - throws: Throws an InternalError if the BKCentral object is already started. + */ public func startWithConfiguration(_ configuration: BKConfiguration) throws { do { try stateMachine.handleEvent(.start) @@ -157,17 +166,18 @@ public class BKCentral: BKPeer, BKCBCentralManagerStateDelegate, BKConnectionPoo throw BKError.internalError(underlyingError: error) } } - + /** - Scan for peripherals for a limited duration of time. - - parameter duration: The number of seconds to scan for (defaults to 3). - - parameter progressHandler: A progress handler allowing you to react immediately when a peripheral is discovered during a scan. - - parameter completionHandler: A completion handler allowing you to react on the full result of discovered peripherals or an error if one occured. - */ - public func scanWithDuration(_ duration: TimeInterval = 3, progressHandler: ScanProgressHandler?, completionHandler: ScanCompletionHandler?) { + Scan for peripherals for a limited duration of time. + - parameter duration: The number of seconds to scan for (defaults to 3). A duration of 0 means endless + - parameter updateDuplicates: normally, discoveries for the same peripheral are coalesced by IOS. Setting this to true advises the OS to generate new discoveries anyway. This allows you to react to RSSI changes (defaults to false). + - parameter progressHandler: A progress handler allowing you to react immediately when a peripheral is discovered during a scan. + - parameter completionHandler: A completion handler allowing you to react on the full result of discovered peripherals or an error if one occured. + */ + public func scanWithDuration(_ duration: TimeInterval = 3, updateDuplicates: Bool = false, progressHandler: ScanProgressHandler?, completionHandler: ScanCompletionHandler?) { do { try stateMachine.handleEvent(.scan) - try scanner.scanWithDuration(duration, progressHandler: progressHandler) { result, error in + try scanner.scanWithDuration(duration, updateDuplicates: updateDuplicates, progressHandler: progressHandler) { result, error in var returnError: BKError? if error == nil { _ = try? self.stateMachine.handleEvent(.setAvailable) @@ -181,17 +191,18 @@ public class BKCentral: BKPeer, BKCBCentralManagerStateDelegate, BKConnectionPoo return } } - + /** - Scan for peripherals for a limited duration of time continuously with an in-between delay. - - parameter changeHandler: A change handler allowing you to react to changes in "maintained" discovered peripherals. - - parameter stateHandler: A state handler allowing you to react when the scanner is started, waiting and stopped. - - parameter duration: The number of seconds to scan for (defaults to 3). - - parameter inBetweenDelay: The number of seconds to wait for, in-between scans (defaults to 3). - - parameter errorHandler: An error handler allowing you to react when an error occurs. For now this is also called when the scan is manually interrupted. - */ - - public func scanContinuouslyWithChangeHandler(_ changeHandler: @escaping ContinuousScanChangeHandler, stateHandler: ContinuousScanStateHandler?, duration: TimeInterval = 3, inBetweenDelay: TimeInterval = 3, errorHandler: ContinuousScanErrorHandler?) { + Scan for peripherals for a limited duration of time continuously with an in-between delay. + - parameter changeHandler: A change handler allowing you to react to changes in "maintained" discovered peripherals. + - parameter stateHandler: A state handler allowing you to react when the scanner is started, waiting and stopped. + - parameter duration: The number of seconds to scan for (defaults to 3). A duration of 0 means endless and inBetweenDelay is pointless + - parameter inBetweenDelay: The number of seconds to wait for, in-between scans (defaults to 3). + - parameter updateDuplicates: normally, discoveries for the same peripheral are coalesced by IOS. Setting this to true advises the OS to generate new discoveries anyway. This allows you to react to RSSI changes (defaults to false). + - parameter errorHandler: An error handler allowing you to react when an error occurs. For now this is also called when the scan is manually interrupted. + */ + + public func scanContinuouslyWithChangeHandler(_ changeHandler: @escaping ContinuousScanChangeHandler, stateHandler: ContinuousScanStateHandler?, duration: TimeInterval = 3, inBetweenDelay: TimeInterval = 3, updateDuplicates: Bool = false, errorHandler: ContinuousScanErrorHandler?) { do { try stateMachine.handleEvent(.scan) continuousScanner.scanContinuouslyWithChangeHandler(changeHandler, stateHandler: { newState in @@ -199,28 +210,28 @@ public class BKCentral: BKPeer, BKCBCentralManagerStateDelegate, BKConnectionPoo _ = try? self.stateMachine.handleEvent(.setAvailable) } stateHandler?(newState) - }, duration: duration, inBetweenDelay: inBetweenDelay, errorHandler: { error in + }, duration: duration, inBetweenDelay: inBetweenDelay, updateDuplicates: updateDuplicates, errorHandler: { error in errorHandler?(.internalError(underlyingError: error)) }) } catch let error { errorHandler?(.internalError(underlyingError: error)) } } - + /** - Interrupts the active scan session if present. - */ + Interrupts the active scan session if present. + */ public func interruptScan() { continuousScanner.interruptScan() scanner.interruptScan() } - + /** - Connect to a remote peripheral. - - parameter timeout: The number of seconds the connection attempt should continue for before failing. - - parameter remotePeripheral: The remote peripheral to connect to. - - parameter completionHandler: A completion handler allowing you to react when the connection attempt succeeded or failed. - */ + Connect to a remote peripheral. + - parameter timeout: The number of seconds the connection attempt should continue for before failing. + - parameter remotePeripheral: The remote peripheral to connect to. + - parameter completionHandler: A completion handler allowing you to react when the connection attempt succeeded or failed. + */ public func connect(_ timeout: TimeInterval = 3, remotePeripheral: BKRemotePeripheral, completionHandler: @escaping ConnectCompletionHandler) { do { try stateMachine.handleEvent(.connect) @@ -238,12 +249,12 @@ public class BKCentral: BKPeer, BKCBCentralManagerStateDelegate, BKConnectionPoo return } } - + /** - Disconnects a connected peripheral. - - parameter remotePeripheral: The peripheral to disconnect. - - throws: Throws an InternalError if the remote peripheral is not currently connected. - */ + Disconnects a connected peripheral. + - parameter remotePeripheral: The peripheral to disconnect. + - throws: Throws an InternalError if the remote peripheral is not currently connected. + */ public func disconnectRemotePeripheral(_ remotePeripheral: BKRemotePeripheral) throws { do { try connectionPool.disconnectRemotePeripheral(remotePeripheral) @@ -251,27 +262,28 @@ public class BKCentral: BKPeer, BKCBCentralManagerStateDelegate, BKConnectionPoo throw BKError.internalError(underlyingError: error) } } - + /** - Stops the BKCentral object. - - throws: Throws an InternalError if the BKCentral object isn't already started. - */ - public func stop() throws { + Stops the BKCentral object. + - throws: Throws an InternalError if the BKCentral object isn't already started. + */ + public override func stop() throws { do { try stateMachine.handleEvent(.stop) interruptScan() connectionPool.reset() _configuration = nil _centralManager = nil + try super.stop() } catch let error { throw BKError.internalError(underlyingError: error) } } - + /** - Retrieves a previously-scanned peripheral for direct connection. - - parameter remoteUUID: The UUID of the remote peripheral to look for - - return: optional remote peripheral if found + Retrieves a previously-scanned peripheral for direct connection. + - parameter remoteUUID: The UUID of the remote peripheral to look for + - return: optional remote peripheral if found */ public func retrieveRemotePeripheralWithUUID (remoteUUID: UUID) -> BKRemotePeripheral? { guard let peripherals = retrieveRemotePeripheralsWithUUIDs(remoteUUIDs: [remoteUUID]) else { @@ -282,11 +294,11 @@ public class BKCentral: BKPeer, BKCBCentralManagerStateDelegate, BKConnectionPoo } return peripherals[0] } - + /** - Retrieves an array of previously-scanned peripherals for direct connection. - - parameter remoteUUIDs: An array of UUIDs of remote peripherals to look for - - return: optional array of found remote peripherals + Retrieves an array of previously-scanned peripherals for direct connection. + - parameter remoteUUIDs: An array of UUIDs of remote peripherals to look for + - return: optional array of found remote peripherals */ public func retrieveRemotePeripheralsWithUUIDs (remoteUUIDs: [UUID]) -> [BKRemotePeripheral]? { if let centralManager = _centralManager { @@ -294,9 +306,9 @@ public class BKCentral: BKPeer, BKCBCentralManagerStateDelegate, BKConnectionPoo guard peripherals.count > 0 else { return nil } - + var remotePeripherals: [BKRemotePeripheral] = [] - + for peripheral in peripherals { let remotePeripheral = BKRemotePeripheral(identifier: peripheral.identifier, peripheral: peripheral) remotePeripheral.configuration = configuration @@ -306,9 +318,9 @@ public class BKCentral: BKPeer, BKCBCentralManagerStateDelegate, BKConnectionPoo } return nil } - + // MARK: Internal Functions - + internal func setUnavailable(_ cause: BKUnavailabilityCause, oldCause: BKUnavailabilityCause?) { scanner.interruptScan() connectionPool.reset() @@ -322,63 +334,63 @@ public class BKCentral: BKPeer, BKCBCentralManagerStateDelegate, BKConnectionPoo } } } - - internal override func sendData(_ data: Data, toRemotePeer remotePeer: BKRemotePeer) -> Bool { + + internal override func sendData(_ data: Data, toRemotePeer remotePeer: BKRemotePeer, forUUID uuid: UUID) -> Bool { guard let remotePeripheral = remotePeer as? BKRemotePeripheral, - let peripheral = remotePeripheral.peripheral, - let characteristic = remotePeripheral.characteristicData else { - return false + let peripheral = remotePeripheral.peripheral, + let characteristic = remotePeripheral.characteristicsData.filter({ $0.uuid == CBUUID(nsuuid: uuid)}).first else { + return false } peripheral.writeValue(data, for: characteristic, type: .withoutResponse) return true } - + // MARK: BKCBCentralManagerStateDelegate - - + + internal func centralManagerDidUpdateState(_ central: CBCentralManager) { switch central.state { - case .unknown, .resetting: - break - case .unsupported, .unauthorized, .poweredOff: - let newCause: BKUnavailabilityCause - #if os(iOS) || os(tvOS) - if #available(iOS 10.0, tvOS 10.0, *) { - newCause = BKUnavailabilityCause(managerState: central.state) - } else { - newCause = BKUnavailabilityCause(centralManagerState: central.centralManagerState) - } - #else - newCause = BKUnavailabilityCause(centralManagerState: central.state) - #endif - switch stateMachine.state { - case let .unavailable(cause): - let oldCause = cause - _ = try? stateMachine.handleEvent(.setUnavailable(cause: newCause)) - setUnavailable(oldCause, oldCause: newCause) - default: - _ = try? stateMachine.handleEvent(.setUnavailable(cause: newCause)) - setUnavailable(newCause, oldCause: nil) - } - - case .poweredOn: - let state = stateMachine.state - _ = try? stateMachine.handleEvent(.setAvailable) - switch state { - case .starting, .unavailable: - for availabilityObserver in availabilityObservers { - availabilityObserver.availabilityObserver?.availabilityObserver(self, availabilityDidChange: .available) - } - default: - break + case .unknown, .resetting: + break + case .unsupported, .unauthorized, .poweredOff: + let newCause: BKUnavailabilityCause + #if os(iOS) || os(tvOS) + if #available(iOS 10.0, tvOS 10.0, *) { + newCause = BKUnavailabilityCause(managerState: central.state) + } else { + newCause = BKUnavailabilityCause(centralManagerState: central.centralManagerState) + } + #else + newCause = BKUnavailabilityCause(centralManagerState: central.state) + #endif + switch stateMachine.state { + case let .unavailable(cause): + let oldCause = cause + _ = try? stateMachine.handleEvent(.setUnavailable(cause: newCause)) + setUnavailable(oldCause, oldCause: newCause) + default: + _ = try? stateMachine.handleEvent(.setUnavailable(cause: newCause)) + setUnavailable(newCause, oldCause: nil) + } + + case .poweredOn: + let state = stateMachine.state + _ = try? stateMachine.handleEvent(.setAvailable) + switch state { + case .starting, .unavailable: + for availabilityObserver in availabilityObservers { + availabilityObserver.availabilityObserver?.availabilityObserver(self, availabilityDidChange: .available) } + default: + break + } } } - + // MARK: BKConnectionPoolDelegate - + internal func connectionPool(_ connectionPool: BKConnectionPool, remotePeripheralDidDisconnect remotePeripheral: BKRemotePeripheral) { delegate?.central(self, remotePeripheralDidDisconnect: remotePeripheral) } - + } diff --git a/Source/BKConfiguration.swift b/Source/BKConfiguration.swift index ad28f0b..2e4066a 100644 --- a/Source/BKConfiguration.swift +++ b/Source/BKConfiguration.swift @@ -26,45 +26,44 @@ import Foundation import CoreBluetooth /** - Class that represents a configuration used when starting a BKCentral object. -*/ + Class that represents a configuration used when starting a BKCentral object. + */ public class BKConfiguration { - + // MARK: Properties - + /// The UUID for the service used to send data. This should be unique to your applications. public let dataServiceUUID: CBUUID - - /// The UUID for the characteristic used to send data. This should be unique to your application. - public var dataServiceCharacteristicUUID: CBUUID - + + /// The UUIDs for the characteristics used to send data. This should be unique to your application. + public var dataServiceCharacteristicUUIDs: [CBUUID] + /// Data used to indicate that no more data is coming when communicating. public var endOfDataMark: Data - + /// Data used to indicate that a transfer was cancellen when communicating. public var dataCancelledMark: Data - + internal var serviceUUIDs: [CBUUID] { let serviceUUIDs = [ dataServiceUUID ] return serviceUUIDs } - + // MARK: Initialization - - public init(dataServiceUUID: UUID, dataServiceCharacteristicUUID: UUID) { + + public init(dataServiceUUID: UUID, dataServiceCharacteristicUUIDs: [UUID]) { self.dataServiceUUID = CBUUID(nsuuid: dataServiceUUID) - self.dataServiceCharacteristicUUID = CBUUID(nsuuid: dataServiceCharacteristicUUID) + self.dataServiceCharacteristicUUIDs = dataServiceCharacteristicUUIDs.map { CBUUID(nsuuid: $0) } endOfDataMark = "EOD".data(using: String.Encoding.utf8)! dataCancelledMark = "COD".data(using: String.Encoding.utf8)! } - + // MARK Functions - + internal func characteristicUUIDsForServiceUUID(_ serviceUUID: CBUUID) -> [CBUUID] { if serviceUUID == dataServiceUUID { - return [ dataServiceCharacteristicUUID ] + return self.dataServiceCharacteristicUUIDs } return [] } - } diff --git a/Source/BKContinuousScanner.swift b/Source/BKContinuousScanner.swift index 0bf2b75..086988a 100644 --- a/Source/BKContinuousScanner.swift +++ b/Source/BKContinuousScanner.swift @@ -52,6 +52,7 @@ internal class BKContinousScanner { private var changeHandler: ChangeHandler? private var duration: TimeInterval! private var inBetweenDelay: TimeInterval! + private var updateDuplicates: Bool! // MARK: Initialization @@ -62,7 +63,7 @@ internal class BKContinousScanner { // MARK Internal Functions - internal func scanContinuouslyWithChangeHandler(_ changeHandler: @escaping ChangeHandler, stateHandler: StateHandler? = nil, duration: TimeInterval = 3, inBetweenDelay: TimeInterval = 3, errorHandler: ErrorHandler?) { + internal func scanContinuouslyWithChangeHandler(_ changeHandler: @escaping ChangeHandler, stateHandler: StateHandler? = nil, duration: TimeInterval = 3, inBetweenDelay: TimeInterval = 3,updateDuplicates: Bool, errorHandler: ErrorHandler?) { guard !busy else { errorHandler?(.busy) return @@ -70,6 +71,7 @@ internal class BKContinousScanner { busy = true self.duration = duration self.inBetweenDelay = inBetweenDelay + self.updateDuplicates = updateDuplicates self.errorHandler = errorHandler self.stateHandler = stateHandler self.changeHandler = changeHandler @@ -87,11 +89,27 @@ internal class BKContinousScanner { do { state = .scanning stateHandler?(state) - try scanner.scanWithDuration(duration, progressHandler: { newDiscoveries in - let actualDiscoveries = newDiscoveries.filter({ !self.maintainedDiscoveries.contains($0) }) - if !actualDiscoveries.isEmpty { - self.maintainedDiscoveries += actualDiscoveries - let changes = actualDiscoveries.map({ BKDiscoveriesChange.insert(discovery: $0) }) + try scanner.scanWithDuration(duration, updateDuplicates: self.updateDuplicates, progressHandler: { newDiscoveries in + var changes: [BKDiscoveriesChange] = [] + + //find discoveries that have been updated and add a change for each + for newDiscovery in newDiscoveries { + if let index = self.maintainedDiscoveries.index(of: newDiscovery) { + let outdatedDiscovery = self.maintainedDiscoveries[index] + self.maintainedDiscoveries[index] = newDiscovery + + //TODO: probably need an update change + changes.append(.remove(discovery: outdatedDiscovery)) + changes.append(.insert(discovery: newDiscovery)) + } + else if !self.maintainedDiscoveries.contains(newDiscovery) { + + self.maintainedDiscoveries.append(newDiscovery) + changes.append(.insert(discovery: newDiscovery)) + } + } + + if !changes.isEmpty { self.changeHandler?(changes, self.maintainedDiscoveries) } }, completionHandler: { result, error in diff --git a/Source/BKPeer.swift b/Source/BKPeer.swift index 4ee3e66..ce75500 100644 --- a/Source/BKPeer.swift +++ b/Source/BKPeer.swift @@ -27,12 +27,12 @@ import Foundation public typealias BKSendDataCompletionHandler = ((_ data: Data, _ remotePeer: BKRemotePeer, _ error: BKError?) -> Void) public class BKPeer { - + /// The configuration the BKCentral object was started with. public var configuration: BKConfiguration? { return nil } - + internal var connectedRemotePeers: [BKRemotePeer] { get { return _connectedRemotePeers @@ -41,36 +41,37 @@ public class BKPeer { _connectedRemotePeers = newValue } } - + internal var sendDataTasks: [BKSendDataTask] = [] - + private var _connectedRemotePeers: [BKRemotePeer] = [] - + /** Sends data to a connected remote central. - parameter data: The data to send. - parameter remotePeer: The destination of the data payload. + - parameter forUUID: The UUID of the characteristic whose value needs to be written - parameter completionHandler: A completion handler allowing you to react in case the data failed to send or once it was sent succesfully. */ - public func sendData(_ data: Data, toRemotePeer remotePeer: BKRemotePeer, completionHandler: BKSendDataCompletionHandler?) { + public func sendData(_ data: Data, toRemotePeer remotePeer: BKRemotePeer, forUUID uuid: UUID, completionHandler: BKSendDataCompletionHandler?) { guard connectedRemotePeers.contains(remotePeer) else { completionHandler?(data, remotePeer, BKError.remotePeerNotConnected) return } - let sendDataTask = BKSendDataTask(data: data, destination: remotePeer, completionHandler: completionHandler) + let sendDataTask = BKSendDataTask(data: data, destination: remotePeer, uuid: uuid, completionHandler: completionHandler) sendDataTasks.append(sendDataTask) - if sendDataTasks.count == 1 { + if sendDataTasks.count >= 1 { processSendDataTasks() } } - + internal func processSendDataTasks() { guard sendDataTasks.count > 0 else { return } let nextTask = sendDataTasks.first! if nextTask.sentAllData { - let sentEndOfDataMark = sendData(configuration!.endOfDataMark, toRemotePeer: nextTask.destination) + let sentEndOfDataMark = sendData(configuration!.endOfDataMark, toRemotePeer: nextTask.destination, forUUID: nextTask.uuid) if sentEndOfDataMark { sendDataTasks.remove(at: sendDataTasks.index(of: nextTask)!) nextTask.completionHandler?(nextTask.data, nextTask.destination, nil) @@ -80,7 +81,7 @@ public class BKPeer { } } if let nextPayload = nextTask.nextPayload { - let sentNextPayload = sendData(nextPayload, toRemotePeer: nextTask.destination) + let sentNextPayload = sendData(nextPayload, toRemotePeer: nextTask.destination, forUUID: nextTask.uuid) if sentNextPayload { nextTask.offset += nextPayload.count processSendDataTasks() @@ -90,18 +91,22 @@ public class BKPeer { } else { return } - + } - + internal func failSendDataTasksForRemotePeer(_ remotePeer: BKRemotePeer) { for sendDataTask in sendDataTasks.filter({ $0.destination == remotePeer }) { sendDataTasks.remove(at: sendDataTasks.index(of: sendDataTask)!) sendDataTask.completionHandler?(sendDataTask.data, sendDataTask.destination, .remotePeerNotConnected) } } - - internal func sendData(_ data: Data, toRemotePeer remotePeer: BKRemotePeer) -> Bool { + + internal func stop() throws { + self.sendDataTasks = [] + } + + internal func sendData(_ data: Data, toRemotePeer remotePeer: BKRemotePeer, forUUID uuid: UUID) -> Bool { fatalError("Function must be overridden by subclass") } - + } diff --git a/Source/BKPeripheral.swift b/Source/BKPeripheral.swift index 50cc955..941257a 100644 --- a/Source/BKPeripheral.swift +++ b/Source/BKPeripheral.swift @@ -26,92 +26,105 @@ import Foundation import CoreBluetooth /** - The peripheral's delegate is called when asynchronous events occur. -*/ + The peripheral's delegate is called when asynchronous events occur. + */ public protocol BKPeripheralDelegate: class { /** - Called when a remote central connects and is ready to receive data. - - parameter peripheral: The peripheral object to which the remote central connected. - - parameter remoteCentral: The remote central that connected. - */ - func peripheral(_ peripheral: BKPeripheral, remoteCentralDidConnect remoteCentral: BKRemoteCentral) + Called when a remote central connects and is ready to receive data. + - parameter peripheral: The peripheral object to which the remote central connected. + - parameter remoteCentral: The remote central that connected. + */ + func peripheral(_ peripheral: BKPeripheral, remoteCentralDidConnect remoteCentral: BKRemoteCentral, toCharactersticUUID characteristicUUID: UUID) /** - Called when a remote central disconnects and can no longer receive data. - - parameter peripheral: The peripheral object from which the remote central disconnected. - - parameter remoteCentral: The remote central that disconnected. - */ - func peripheral(_ peripheral: BKPeripheral, remoteCentralDidDisconnect remoteCentral: BKRemoteCentral) + Called when a remote central disconnects and can no longer receive data. + - parameter peripheral: The peripheral object from which the remote central disconnected. + - parameter remoteCentral: The remote central that disconnected. If null, the disconnection happens for an unforseen issue. + */ + func peripheral(_ peripheral: BKPeripheral, remoteCentralDidDisconnect remoteCentral: BKRemoteCentral, fromCharacteristicUUID characteristicUUID: UUID?) } /** - The class used to take the Bluetooth LE peripheral role. Peripherals can be discovered and connected to by centrals. - One a central has connected, the peripheral can send data to it. -*/ + The class used to take the Bluetooth LE peripheral role. Peripherals can be discovered and connected to by centrals. + One a central has connected, the peripheral can send data to it. + */ public class BKPeripheral: BKPeer, BKCBPeripheralManagerDelegate, BKAvailabilityObservable { - + // MARK: Properies - + /// Bluetooth LE availability derived from the underlying CBPeripheralManager object. - + public var availability: BKAvailability { #if os(iOS) || os(tvOS) - if #available(iOS 10.0, tvOS 10.0, *) { - return BKAvailability(managerState: peripheralManager.state) - } else { - return BKAvailability(peripheralManagerState: peripheralManager.peripheralManagerState) - } + if #available(iOS 10.0, tvOS 10.0, *) { + return BKAvailability(managerState: peripheralManager.state) + } else { + return BKAvailability(peripheralManagerState: peripheralManager.peripheralManagerState) + } #else - return BKAvailability(peripheralManagerState: peripheralManager.state) + return BKAvailability(peripheralManagerState: peripheralManager.state) #endif } - - - + + + /// The configuration that the BKPeripheral object was started with. override public var configuration: BKPeripheralConfiguration? { return _configuration } - + /// The BKPeriheral object's delegate. public weak var delegate: BKPeripheralDelegate? - + /// Current availability observers public var availabilityObservers = [BKWeakAvailabilityObserver]() - + /// Currently connected remote centrals public var connectedRemoteCentrals: [BKRemoteCentral] { - return connectedRemotePeers.flatMap({ + #if swift(>=4.1) + return connectedRemotePeers.compactMap({ guard let remoteCentral = $0 as? BKRemoteCentral else { return nil + } return remoteCentral + }) + #else + return connectedRemotePeers.flatMap({ + guard let remoteCentral = $0 as? BKRemoteCentral else { + return nil + } + return remoteCentral + }) + #endif } - + private var _configuration: BKPeripheralConfiguration! private var peripheralManager: CBPeripheralManager! private let stateMachine = BKPeripheralStateMachine() private var peripheralManagerDelegate: BKCBPeripheralManagerDelegateProxy! - private var characteristicData: CBMutableCharacteristic! + private var characteristicsData: [CBMutableCharacteristic]! private var dataService: CBMutableService! - + + private var advertisementData: [String: Any]? + // MARK: Initialization - + public override init() { super.init() peripheralManagerDelegate = BKCBPeripheralManagerDelegateProxy(delegate: self) } - + // MARK: Public Functions - + /** - Starts the BKPeripheral object. Once started the peripheral will be discoverable and possible to connect to - by remote centrals, provided that Bluetooth LE is available. - - parameter configuration: A configuration defining the unique identifiers along with the name to be broadcasted. - - throws: An internal error if the BKPeripheral object was already started. - */ + Starts the BKPeripheral object. Once started the peripheral will be discoverable and possible to connect to + by remote centrals, provided that Bluetooth LE is available. + - parameter configuration: A configuration defining the unique identifiers along with the name to be broadcasted. + - throws: An internal error if the BKPeripheral object was already started. + */ public func startWithConfiguration(_ configuration: BKPeripheralConfiguration) throws { do { try stateMachine.handleEvent(event: .start) @@ -121,12 +134,28 @@ public class BKPeripheral: BKPeer, BKCBPeripheralManagerDelegate, BKAvailability throw BKError.internalError(underlyingError: error) } } - + /** - Stops the BKPeripheral object. - - throws: An internal error if the peripheral object wasn't started. - */ - public func stop() throws { + Resumes the BKPeripheral object. Advertisement data will be the same sa when it was started the first time. The BKPeripheral will now be discoverable by sourriding centrals again. + - throws: An internal error if the BKPeripheral object was already started. + */ + public func resume() throws { + print(self.peripheralManagerDelegate.delegate === self) + do { + try stateMachine.handleEvent(event: .resume) + if !peripheralManager.isAdvertising { + peripheralManager.startAdvertising(self.advertisementData) + } + } catch let error { + throw BKError.internalError(underlyingError: error) + } + } + + /** + Stops the BKPeripheral object. + - throws: An internal error if the peripheral object wasn't started. + */ + public override func stop() throws { do { try stateMachine.handleEvent(event: .stop) _configuration = nil @@ -135,13 +164,30 @@ public class BKPeripheral: BKPeer, BKCBPeripheralManagerDelegate, BKAvailability } peripheralManager.removeAllServices() peripheralManager = nil + self.advertisementData = nil + try super.stop() } catch let error { throw BKError.internalError(underlyingError: error) } } - + + /** + Pauses the BKPeripheral object. Connected centrals will not be disconnected. + - throws: An internal error if the peripheral obejct wasn't started. + */ + public func pause() throws { + do { + try stateMachine.handleEvent(event: .pause) + if peripheralManager.isAdvertising { + peripheralManager.stopAdvertising() + } + } catch let error { + throw BKError.internalError(underlyingError: error) + } + } + // MARK: Private Functions - + private func setUnavailable(_ cause: BKUnavailabilityCause, oldCause: BKUnavailabilityCause?) { if oldCause == nil { for remotePeer in connectedRemotePeers { @@ -158,7 +204,7 @@ public class BKPeripheral: BKPeer, BKCBPeripheralManagerDelegate, BKAvailability } } } - + private func setAvailable() { for availabilityObserver in availabilityObservers { availabilityObserver.availabilityObserver?.availabilityObserver(self, availabilityDidChange: .available) @@ -167,27 +213,32 @@ public class BKPeripheral: BKPeer, BKCBPeripheralManagerDelegate, BKAvailability dataService = CBMutableService(type: _configuration.dataServiceUUID, primary: true) let properties: CBCharacteristicProperties = [ .read, .notify, .writeWithoutResponse, .write ] let permissions: CBAttributePermissions = [ .readable, .writeable ] - characteristicData = CBMutableCharacteristic(type: _configuration.dataServiceCharacteristicUUID, properties: properties, value: nil, permissions: permissions) - dataService.characteristics = [ characteristicData ] + characteristicsData = configuration?.dataServiceCharacteristicUUIDs.map { CBMutableCharacteristic(type: $0, properties: properties, value: nil, permissions: permissions) } + dataService.characteristics = characteristicsData peripheralManager.add(dataService) } } - - internal override func sendData(_ data: Data, toRemotePeer remotePeer: BKRemotePeer) -> Bool { + + internal override func sendData(_ data: Data, toRemotePeer remotePeer: BKRemotePeer, forUUID uuid: UUID) -> Bool { guard let remoteCentral = remotePeer as? BKRemoteCentral else { return false } + let characteristicData = self.characteristicsData.first(where: { $0.uuid == CBUUID(nsuuid: uuid) })! return peripheralManager.updateValue(data, for: characteristicData, onSubscribedCentrals: [ remoteCentral.central ]) } - - private func handleDisconnectForRemoteCentral(_ remoteCentral: BKRemoteCentral) { + + private func handleDisconnectForRemoteCentral(_ remoteCentral: BKRemoteCentral, forCharacteristic characteristic: CBCharacteristic?=nil) { failSendDataTasksForRemotePeer(remoteCentral) connectedRemotePeers.remove(at: connectedRemotePeers.index(of: remoteCentral)!) - delegate?.peripheral(self, remoteCentralDidDisconnect: remoteCentral) + guard let characteristic = characteristic else { + delegate?.peripheral(self, remoteCentralDidDisconnect: remoteCentral, fromCharacteristicUUID: nil) + return + } + delegate?.peripheral(self, remoteCentralDidDisconnect: remoteCentral, fromCharacteristicUUID: UUID(uuidString: characteristic.uuid.uuidString)!) } - + // MARK: BKCBPeripheralManagerDelegate - + internal func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { switch peripheral.state { case .unknown, .resetting: @@ -195,79 +246,79 @@ public class BKPeripheral: BKPeer, BKCBPeripheralManagerDelegate, BKAvailability case .unsupported, .unauthorized, .poweredOff: let newCause: BKUnavailabilityCause #if os(iOS) || os(tvOS) - if #available(iOS 10.0, tvOS 10.0, *) { - newCause = BKUnavailabilityCause(managerState: peripheralManager.state) - } else { - newCause = BKUnavailabilityCause(peripheralManagerState: peripheralManager.peripheralManagerState) - } + if #available(iOS 10.0, tvOS 10.0, *) { + newCause = BKUnavailabilityCause(managerState: peripheralManager.state) + } else { + newCause = BKUnavailabilityCause(peripheralManagerState: peripheralManager.peripheralManagerState) + } #else - newCause = BKUnavailabilityCause(peripheralManagerState: peripheralManager.state) + newCause = BKUnavailabilityCause(peripheralManagerState: peripheralManager.state) #endif switch stateMachine.state { - case let .unavailable(cause): - let oldCause = cause - _ = try? stateMachine.handleEvent(event: .setUnavailable(cause: newCause)) - setUnavailable(oldCause, oldCause: newCause) - default: - _ = try? stateMachine.handleEvent(event: .setUnavailable(cause: newCause)) - setUnavailable(newCause, oldCause: nil) - } - case .poweredOn: - let state = stateMachine.state - _ = try? stateMachine.handleEvent(event: .setAvailable) - switch state { - case .starting, .unavailable: - setAvailable() - default: - break + case let .unavailable(cause): + let oldCause = cause + _ = try? stateMachine.handleEvent(event: .setUnavailable(cause: newCause)) + setUnavailable(oldCause, oldCause: newCause) + default: + _ = try? stateMachine.handleEvent(event: .setUnavailable(cause: newCause)) + setUnavailable(newCause, oldCause: nil) + } + case .poweredOn: + let state = stateMachine.state + _ = try? stateMachine.handleEvent(event: .setAvailable) + switch state { + case .starting, .unavailable: + setAvailable() + default: + break } } } - - + + internal func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) { - + } - + internal func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) { if !peripheralManager.isAdvertising { - var advertisementData: [String: Any] = [ CBAdvertisementDataServiceUUIDsKey: _configuration.serviceUUIDs ] + self.advertisementData = [ CBAdvertisementDataServiceUUIDsKey: _configuration.serviceUUIDs ] if let localName = _configuration.localName { - advertisementData[CBAdvertisementDataLocalNameKey] = localName + self.advertisementData![CBAdvertisementDataLocalNameKey] = localName } peripheralManager.startAdvertising(advertisementData) } } - + internal func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeToCharacteristic characteristic: CBCharacteristic) { let remoteCentral = BKRemoteCentral(central: central) remoteCentral.configuration = configuration connectedRemotePeers.append(remoteCentral) - delegate?.peripheral(self, remoteCentralDidConnect: remoteCentral) + delegate?.peripheral(self, remoteCentralDidConnect: remoteCentral, toCharactersticUUID: UUID(uuidString: characteristic.uuid.uuidString)!) } - + internal func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFromCharacteristic characteristic: CBCharacteristic) { if let remoteCentral = connectedRemotePeers.filter({ ($0.identifier == central.identifier) }).last as? BKRemoteCentral { - handleDisconnectForRemoteCentral(remoteCentral) + handleDisconnectForRemoteCentral(remoteCentral, forCharacteristic: characteristic) } } - + func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWriteRequests requests: [CBATTRequest]) { for writeRequest in requests { - guard writeRequest.characteristic.uuid == characteristicData.uuid else { + guard self.characteristicsData.contains(where: { $0.uuid == writeRequest.characteristic.uuid }) else { continue } guard let remotePeer = (connectedRemotePeers.filter { $0.identifier == writeRequest.central.identifier } .last), - let remoteCentral = remotePeer as? BKRemoteCentral, - let data = writeRequest.value else { - continue + let remoteCentral = remotePeer as? BKRemoteCentral, + let data = writeRequest.value else { + continue } remoteCentral.handleReceivedData(data) } } - + internal func peripheralManagerIsReadyToUpdateSubscribers(_ peripheral: CBPeripheralManager) { processSendDataTasks() } - + } diff --git a/Source/BKPeripheralConfiguration.swift b/Source/BKPeripheralConfiguration.swift index 0e81cd7..04e13c2 100644 --- a/Source/BKPeripheralConfiguration.swift +++ b/Source/BKPeripheralConfiguration.swift @@ -26,20 +26,20 @@ import Foundation import CoreBluetooth /** - A subclass of BKConfiguration for constructing configurations to use when starting BKPeripheral objects. -*/ + A subclass of BKConfiguration for constructing configurations to use when starting BKPeripheral objects. + */ public class BKPeripheralConfiguration: BKConfiguration { - + // MARK: Properties - + /// The local name to broadcast to remote centrals. public let localName: String? - + // MARK: Initialization - - public init(dataServiceUUID: UUID, dataServiceCharacteristicUUID: UUID, localName: String? = nil) { + + public init(dataServiceUUID: UUID, dataServiceCharacteristicUUIDs: [UUID], localName: String? = nil) { self.localName = localName - super.init(dataServiceUUID: dataServiceUUID, dataServiceCharacteristicUUID: dataServiceCharacteristicUUID) + super.init(dataServiceUUID: dataServiceUUID, dataServiceCharacteristicUUIDs: dataServiceCharacteristicUUIDs) } - + } diff --git a/Source/BKPeripheralStateMachine.swift b/Source/BKPeripheralStateMachine.swift index 3482cf4..52b8119 100644 --- a/Source/BKPeripheralStateMachine.swift +++ b/Source/BKPeripheralStateMachine.swift @@ -25,46 +25,50 @@ import Foundation internal class BKPeripheralStateMachine { - + // MARK: Enums - + internal enum BKError: Error { case transitioning(currentState: State, validStates: [State]) } - + internal enum State { - case initialized, starting, unavailable(cause: BKUnavailabilityCause), available + case initialized, starting, unavailable(cause: BKUnavailabilityCause), available, paused } - + internal enum Event { - case start, setUnavailable(cause: BKUnavailabilityCause), setAvailable, stop + case start, setUnavailable(cause: BKUnavailabilityCause), setAvailable, stop, pause, resume } - + // MARK: Properties - + internal var state: State - + // MARK: Initialization - + internal init() { self.state = .initialized } - + // MARK: Functions - + internal func handleEvent(event: Event) throws { switch event { case .start: try handleStartEvent(event: event) + case .resume: + try handleResumeEvent(event: event) case .setAvailable: try handleSetAvailableEvent(event: event) case let .setUnavailable(cause): try handleSetUnavailableEvent(event: event, withCause: cause) + case .pause: + try handlePauseEvent(event: event) case .stop: try handleStopEvent(event: event) } } - + private func handleStartEvent(event: Event) throws { switch state { case .initialized: @@ -73,32 +77,49 @@ internal class BKPeripheralStateMachine { throw BKError.transitioning(currentState: state, validStates: [ .initialized ]) } } - + private func handleResumeEvent(event: Event) throws { + switch state { + case .paused: + state = .available + default: + throw BKError.transitioning(currentState: state, validStates: [.paused]) + } + } + private func handleSetAvailableEvent(event: Event) throws { switch state { case .initialized: - throw BKError.transitioning(currentState: state, validStates: [ .starting, .available, .unavailable(cause: nil) ]) + throw BKError.transitioning(currentState: state, validStates: [ .starting, .available, .paused, .unavailable(cause: nil) ]) default: state = .available } } - + private func handleSetUnavailableEvent(event: Event, withCause cause: BKUnavailabilityCause) throws { switch state { case .initialized: - throw BKError.transitioning(currentState: state, validStates: [ .starting, .available, .unavailable(cause: nil) ]) + throw BKError.transitioning(currentState: state, validStates: [ .starting, .available, .paused, .unavailable(cause: nil) ]) default: state = .unavailable(cause: cause) } } - + + private func handlePauseEvent(event: Event) throws { + switch state { + case .available: + state = .paused + default: + throw BKError.transitioning(currentState: state, validStates: [.available]) + } + } + private func handleStopEvent(event: Event) throws { switch state { case .initialized: - throw BKError.transitioning(currentState: state, validStates: [ .starting, .available, .unavailable(cause: nil) ]) + throw BKError.transitioning(currentState: state, validStates: [ .starting, .available, .paused, .unavailable(cause: nil) ]) default: state = .initialized } } - + } diff --git a/Source/BKRemotePeripheral.swift b/Source/BKRemotePeripheral.swift index 2648b6f..4468ea6 100644 --- a/Source/BKRemotePeripheral.swift +++ b/Source/BKRemotePeripheral.swift @@ -26,46 +26,46 @@ import Foundation import CoreBluetooth /** - The delegate of a remote peripheral receives callbacks when asynchronous events occur. -*/ + The delegate of a remote peripheral receives callbacks when asynchronous events occur. + */ public protocol BKRemotePeripheralDelegate: class { - + /** - Called when the remote peripheral updated its name. - - parameter remotePeripheral: The remote peripheral that updated its name. - - parameter name: The new name. - */ + Called when the remote peripheral updated its name. + - parameter remotePeripheral: The remote peripheral that updated its name. + - parameter name: The new name. + */ func remotePeripheral(_ remotePeripheral: BKRemotePeripheral, didUpdateName name: String) - + /** Called when services and charateristic are discovered and the device is ready for send/receive - parameter remotePeripheral: The remote peripheral that is ready. */ func remotePeripheralIsReady(_ remotePeripheral: BKRemotePeripheral) - + } /** - Class to represent a remote peripheral that can be connected to by BKCentral objects. -*/ + Class to represent a remote peripheral that can be connected to by BKCentral objects. + */ public class BKRemotePeripheral: BKRemotePeer, BKCBPeripheralDelegate { - + // MARK: Enums - + /** - Possible states for BKRemotePeripheral objects. - - Shallow: The peripheral was initialized only with an identifier (used when one wants to connect to a peripheral for which the identifier is known in advance). - - Disconnected: The peripheral is disconnected. - - Connecting: The peripheral is currently connecting. - - Connected: The peripheral is already connected. - - Disconnecting: The peripheral is currently disconnecting. - */ + Possible states for BKRemotePeripheral objects. + - Shallow: The peripheral was initialized only with an identifier (used when one wants to connect to a peripheral for which the identifier is known in advance). + - Disconnected: The peripheral is disconnected. + - Connecting: The peripheral is currently connecting. + - Connected: The peripheral is already connected. + - Disconnecting: The peripheral is currently disconnecting. + */ public enum State { case shallow, disconnected, connecting, connected, disconnecting } - + // MARK: Properties - + /// The current state of the remote peripheral, either shallow or derived from an underlying CBPeripheral object. public var state: State { if peripheral == nil { @@ -73,58 +73,58 @@ public class BKRemotePeripheral: BKRemotePeer, BKCBPeripheralDelegate { } #if os(iOS) || os(tvOS) switch peripheral!.state { - case .disconnected: return .disconnected - case .connecting: return .connecting - case .connected: return .connected - case .disconnecting: return .disconnecting + case .disconnected: return .disconnected + case .connecting: return .connecting + case .connected: return .connected + case .disconnecting: return .disconnecting } #else switch peripheral!.state { - case .disconnected: return .disconnected - case .connecting: return .connecting - case .connected: return .connected + case .disconnected: return .disconnected + case .connecting: return .connecting + case .connected: return .connected } #endif } - + /// The name of the remote peripheral, derived from an underlying CBPeripheral object. public var name: String? { return peripheral?.name } - + /// The remote peripheral's delegate. public weak var peripheralDelegate: BKRemotePeripheralDelegate? - + override internal var maximumUpdateValueLength: Int { guard #available(iOS 9, *), let peripheral = peripheral else { return super.maximumUpdateValueLength } #if os(OSX) - return super.maximumUpdateValueLength + return super.maximumUpdateValueLength #else - return peripheral.maximumWriteValueLength(for: .withoutResponse) + return peripheral.maximumWriteValueLength(for: .withoutResponse) #endif } - - internal var characteristicData: CBCharacteristic? + + internal var characteristicsData: [CBCharacteristic] = [] internal var peripheral: CBPeripheral? - + private var peripheralDelegateProxy: BKCBPeripheralDelegateProxy! - + // MARK: Initialization - + public init(identifier: UUID, peripheral: CBPeripheral?) { super.init(identifier: identifier) self.peripheralDelegateProxy = BKCBPeripheralDelegateProxy(delegate: self) self.peripheral = peripheral } - + // MARK: Internal Functions - + internal func prepareForConnection() { peripheral?.delegate = peripheralDelegateProxy } - + internal func discoverServices() { if peripheral?.services != nil { peripheral(peripheral!, didDiscoverServices: nil) @@ -132,7 +132,7 @@ public class BKRemotePeripheral: BKRemotePeer, BKCBPeripheralDelegate { } peripheral?.discoverServices(configuration!.serviceUUIDs) } - + internal func unsubscribe() { guard peripheral?.services != nil else { return @@ -146,13 +146,13 @@ public class BKRemotePeripheral: BKRemotePeer, BKCBPeripheralDelegate { } } } - + // MARK: BKCBPeripheralDelegate - + internal func peripheralDidUpdateName(_ peripheral: CBPeripheral) { peripheralDelegate?.remotePeripheral(self, didUpdateName: name!) } - + internal func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { guard let services = peripheral.services else { return @@ -165,22 +165,22 @@ public class BKRemotePeripheral: BKRemotePeer, BKCBPeripheralDelegate { } } } - + internal func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { - guard service.uuid == configuration!.dataServiceUUID, let dataCharacteristic = service.characteristics?.filter({ $0.uuid == configuration!.dataServiceCharacteristicUUID }).last else { + guard service.uuid == configuration!.dataServiceUUID, let dataCharacteristics = service.characteristics?.filter({ serviceCharacteristic in + return configuration!.dataServiceCharacteristicUUIDs.contains(where: { return $0 == serviceCharacteristic.uuid }) + }) else { return } - characteristicData = dataCharacteristic - peripheral.setNotifyValue(true, for: dataCharacteristic) + characteristicsData = dataCharacteristics + characteristicsData.forEach { peripheral.setNotifyValue(true, for: $0) } peripheralDelegate?.remotePeripheralIsReady(self) } - + internal func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { - guard characteristic.uuid == configuration!.dataServiceCharacteristicUUID else { + guard configuration!.dataServiceCharacteristicUUIDs.contains(characteristic.uuid) else { return } handleReceivedData(characteristic.value!) } - - } diff --git a/Source/BKScanner.swift b/Source/BKScanner.swift index 96a87a4..b892d2a 100644 --- a/Source/BKScanner.swift +++ b/Source/BKScanner.swift @@ -26,51 +26,54 @@ import Foundation import CoreBluetooth internal class BKScanner: BKCBCentralManagerDiscoveryDelegate { - + // MARK: Type Aliases - + internal typealias ScanCompletionHandler = ((_ result: [BKDiscovery]?, _ error: BKError?) -> Void) - + // MARK: Enums - + internal enum BKError: Error { case noCentralManagerSet case busy case interrupted } - + // MARK: Properties - + internal var configuration: BKConfiguration! internal var centralManager: CBCentralManager! private var busy = false private var scanHandlers: (progressHandler: BKCentral.ScanProgressHandler?, completionHandler: ScanCompletionHandler )? private var discoveries = [BKDiscovery]() private var durationTimer: Timer? - + // MARK: Internal Functions - - internal func scanWithDuration(_ duration: TimeInterval, progressHandler: BKCentral.ScanProgressHandler? = nil, completionHandler: @escaping ScanCompletionHandler) throws { + + internal func scanWithDuration(_ duration: TimeInterval, updateDuplicates: Bool, progressHandler: BKCentral.ScanProgressHandler? = nil, completionHandler: @escaping ScanCompletionHandler) throws { do { try validateForActivity() busy = true scanHandlers = ( progressHandler: progressHandler, completionHandler: completionHandler) - centralManager.scanForPeripherals(withServices: configuration.serviceUUIDs, options: nil) - durationTimer = Timer.scheduledTimer(timeInterval: duration, target: self, selector: #selector(BKScanner.durationTimerElapsed), userInfo: nil, repeats: false) + let options = [CBCentralManagerScanOptionAllowDuplicatesKey: updateDuplicates] + centralManager.scanForPeripherals(withServices: configuration.serviceUUIDs, options: options) + if(duration > 0) { + durationTimer = Timer.scheduledTimer(timeInterval: duration, target: self, selector: #selector(BKScanner.durationTimerElapsed), userInfo: nil, repeats: false) + } } catch let error { throw error } } - + internal func interruptScan() { guard busy else { return } endScan(.interrupted) } - + // MARK: Private Functions - + private func validateForActivity() throws { guard !busy else { throw BKError.busy @@ -79,11 +82,11 @@ internal class BKScanner: BKCBCentralManagerDiscoveryDelegate { throw BKError.noCentralManagerSet } } - + @objc private func durationTimerElapsed() { endScan(nil) } - + private func endScan(_ error: BKError?) { invalidateTimer() centralManager.stopScan() @@ -94,28 +97,31 @@ internal class BKScanner: BKCBCentralManagerDiscoveryDelegate { busy = false completionHandler?(discoveries, error) } - + private func invalidateTimer() { if let durationTimer = self.durationTimer { durationTimer.invalidate() self.durationTimer = nil } } - + // MARK: BKCBCentralManagerDiscoveryDelegate - + internal func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { guard busy else { return } - let RSSI = Int(RSSI) + let RSSI = Int(truncating: RSSI) let remotePeripheral = BKRemotePeripheral(identifier: peripheral.identifier, peripheral: peripheral) remotePeripheral.configuration = configuration let discovery = BKDiscovery(advertisementData: advertisementData, remotePeripheral: remotePeripheral, RSSI: RSSI) - if !discoveries.contains(discovery) { + if let index = discoveries.index(of: discovery) { + discoveries[index] = discovery + } + else { discoveries.append(discovery) - scanHandlers?.progressHandler?([ discovery ]) } + scanHandlers?.progressHandler?([ discovery ]) } - + } diff --git a/Source/BKSendDataTask.swift b/Source/BKSendDataTask.swift index b016da5..ac616ab 100644 --- a/Source/BKSendDataTask.swift +++ b/Source/BKSendDataTask.swift @@ -29,46 +29,48 @@ internal func == (lhs: BKSendDataTask, rhs: BKSendDataTask) -> Bool { } internal class BKSendDataTask: Equatable { - + // MARK: Properties - + internal let data: Data internal let destination: BKRemotePeer + internal let uuid: UUID internal let completionHandler: BKSendDataCompletionHandler? internal var offset = 0 - + internal var maximumPayloadLength: Int { return destination.maximumUpdateValueLength } - + internal var lengthOfRemainingData: Int { return data.count - offset } - + internal var sentAllData: Bool { return lengthOfRemainingData == 0 } - + internal var rangeForNextPayload: Range? { let lenghtOfNextPayload = maximumPayloadLength <= lengthOfRemainingData ? maximumPayloadLength : lengthOfRemainingData let payLoadRange = NSRange(location: offset, length: lenghtOfNextPayload) - return payLoadRange.toRange() + return Range(payLoadRange) } - + internal var nextPayload: Data? { if let range = rangeForNextPayload { - return data.subdata(in: range) + return data.subdata(in: range) } else { return nil } } - + // MARK: Initialization - - internal init(data: Data, destination: BKRemotePeer, completionHandler: BKSendDataCompletionHandler?) { + + internal init(data: Data, destination: BKRemotePeer, uuid: UUID, completionHandler: BKSendDataCompletionHandler?) { self.data = data self.destination = destination + self.uuid = uuid self.completionHandler = completionHandler } - + }