From b0fa4abc5948ab219bcdccd0e36fc845ce7c5af8 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 13 Nov 2021 17:45:38 -0500 Subject: [PATCH 01/44] Updated for Swift 5.5 --- Package.swift | 30 +++++++++++++----- Package@swift-5.3.swift | 67 ----------------------------------------- 2 files changed, 23 insertions(+), 74 deletions(-) delete mode 100644 Package@swift-5.3.swift diff --git a/Package.swift b/Package.swift index 08e7428dc..cb98f718f 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.1 +// swift-tools-version:5.5 import PackageDescription #if os(Linux) @@ -9,16 +9,32 @@ let libraryType: PackageDescription.Product.Library.LibraryType = .static let package = Package( name: "Bluetooth", + platforms: [ + .macOS(.v12), + .iOS(.v15), + .watchOS(.v8), + .tvOS(.v15), + ], products: [ .library( name: "Bluetooth", type: libraryType, - targets: [ - "Bluetooth", - "BluetoothGAP", - "BluetoothGATT", - "BluetoothHCI" - ] + targets: ["Bluetooth"] + ), + .library( + name: "BluetoothGAP", + type: libraryType, + targets: ["BluetoothGAP"] + ), + .library( + name: "BluetoothGATT", + type: libraryType, + targets: ["BluetoothGATT"] + ), + .library( + name: "BluetoothHCI", + type: libraryType, + targets: ["BluetoothHCI"] ) ], targets: [ diff --git a/Package@swift-5.3.swift b/Package@swift-5.3.swift deleted file mode 100644 index 1218d70e5..000000000 --- a/Package@swift-5.3.swift +++ /dev/null @@ -1,67 +0,0 @@ -// swift-tools-version:5.3 -import PackageDescription - -#if os(Linux) -let libraryType: PackageDescription.Product.Library.LibraryType = .dynamic -#else -let libraryType: PackageDescription.Product.Library.LibraryType = .static -#endif - -let package = Package( - name: "Bluetooth", - products: [ - .library( - name: "Bluetooth", - type: libraryType, - targets: ["Bluetooth"] - ), - .library( - name: "BluetoothGAP", - type: libraryType, - targets: ["BluetoothGAP"] - ), - .library( - name: "BluetoothGATT", - type: libraryType, - targets: ["BluetoothGATT"] - ), - .library( - name: "BluetoothHCI", - type: libraryType, - targets: ["BluetoothHCI"] - ) - ], - targets: [ - .target( - name: "Bluetooth" - ), - .target( - name: "BluetoothGAP", - dependencies: [ - "Bluetooth" - ] - ), - .target( - name: "BluetoothGATT", - dependencies: [ - "Bluetooth", - ] - ), - .target( - name: "BluetoothHCI", - dependencies: [ - "Bluetooth", - "BluetoothGAP" - ] - ), - .testTarget( - name: "BluetoothTests", - dependencies: [ - "Bluetooth", - "BluetoothGAP", - "BluetoothGATT", - "BluetoothHCI" - ] - ) - ] -) From 22d8d6e59e059cad1ef9f2e796786a2707c5bfa5 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 13 Nov 2021 17:45:53 -0500 Subject: [PATCH 02/44] Added `async` support --- Sources/Bluetooth/L2CAPSocket.swift | 21 +++++--- Sources/BluetoothGATT/ATTConnection.swift | 63 +++++++++++------------ Sources/BluetoothGATT/GATTClient.swift | 12 ++--- Sources/BluetoothGATT/GATTServer.swift | 28 +++++----- Tests/BluetoothTests/BluetoothTests.swift | 2 +- Tests/BluetoothTests/GATTTests.swift | 3 +- Tests/BluetoothTests/L2CAPSocket.swift | 11 ++-- 7 files changed, 73 insertions(+), 67 deletions(-) diff --git a/Sources/Bluetooth/L2CAPSocket.swift b/Sources/Bluetooth/L2CAPSocket.swift index 7a2e82139..625f67462 100644 --- a/Sources/Bluetooth/L2CAPSocket.swift +++ b/Sources/Bluetooth/L2CAPSocket.swift @@ -9,17 +9,24 @@ import Foundation /// L2CAP Socket protocol. -public protocol L2CAPSocketProtocol: AnyObject { - - /// Reads from the socket. - func recieve(_ bufferSize: Int) throws -> Data? +public protocol L2CAPSocket { /// Write to the socket. - func send(_ data: Data) throws + func send(_ data: Data) async throws - /// The socket's security level. - var securityLevel: SecurityLevel { get } + /// Reads from the socket. + func recieve(_ bufferSize: Int) async throws -> Data /// Attempts to change the socket's security level. func setSecurityLevel(_ securityLevel: SecurityLevel) throws + + /// Get security level + func securityLevel() throws -> SecurityLevel + + /// Attempt to accept an incomping connection. + func accept(sleep: UInt64) async throws -> Self + + func canWrite() throws -> Bool + + func canRead() throws -> Bool } diff --git a/Sources/BluetoothGATT/ATTConnection.swift b/Sources/BluetoothGATT/ATTConnection.swift index 5b28f6588..58665bd14 100755 --- a/Sources/BluetoothGATT/ATTConnection.swift +++ b/Sources/BluetoothGATT/ATTConnection.swift @@ -7,6 +7,7 @@ // import Foundation +import Bluetooth /// Manages a Bluetooth connection using the ATT protocol. internal final class ATTConnection { @@ -16,7 +17,7 @@ internal final class ATTConnection { /// Actual number of bytes for PDU ATT exchange. public var maximumTransmissionUnit: ATTMaximumTransmissionUnit = .default - public let socket: L2CAPSocketProtocol + public let socket: L2CAPSocket public var log: ((String) -> ())? @@ -57,24 +58,22 @@ internal final class ATTConnection { // MARK: - Initialization deinit { - unregisterAll() } - public init(socket: L2CAPSocketProtocol) { - + public init(socket: L2CAPSocket) { self.socket = socket } // MARK: - Methods /// Performs the actual IO for recieving data. - public func read() throws -> Bool { + public func read() async throws { //log?("Attempt read") - guard let recievedData = try socket.recieve(Int(maximumTransmissionUnit.rawValue)) - else { return false } // no data availible to read + let bytesToRead = Int(self.maximumTransmissionUnit.rawValue) + let recievedData = try await socket.recieve(bytesToRead) //log?("Recieved data (\(recievedData.count) bytes)") @@ -112,12 +111,10 @@ internal final class ATTConnection { // For all other opcodes notify the upper layer of the PDU and let them act on it. try handle(notify: recievedData, opcode: opcode) } - - return true } /// Performs the actual IO for sending data. - public func write() throws -> Bool { + public func write() async throws -> Bool { //log?("Attempt write") @@ -126,7 +123,7 @@ internal final class ATTConnection { //log?("Sending data... (\(sendOperation.data.count) bytes)") - try socket.send(sendOperation.data) + try await socket.send(sendOperation.data) let opcode = sendOperation.opcode @@ -137,24 +134,16 @@ internal final class ATTConnection { * no need to keep it around. */ switch opcode.type { - case .request: - pendingRequest = sendOperation - case .indication: - pendingRequest = sendOperation - case .response: - // Set `incomingRequest` to false to indicate that no request is pending incomingRequest = false - case .command, .notification, .confirmation: - break } @@ -476,26 +465,31 @@ internal final class ATTConnection { /// Attempts to change security level based on an error response. private func changeSecurity(for error: ATTError) -> Bool { + let securityLevel: Bluetooth.SecurityLevel + do { securityLevel = try self.socket.securityLevel() } + catch { + log?("Unable to get security level. \(error)") + return false + } + // only change if security is Auto - guard self.socket.securityLevel == .sdp + guard securityLevel == .sdp else { return false } // get security from IO - var security = self.socket.securityLevel + var newSecurityLevel: Bluetooth.SecurityLevel if error == .insufficientEncryption, - security < .medium { - - security = .medium - + securityLevel < .medium { + newSecurityLevel = .medium } else if error == .insufficientAuthentication { - if (security < .medium) { - security = .medium - } else if (security < .high) { - security = .high - } else if (security < .fips) { - security = .fips + if (securityLevel < .medium) { + newSecurityLevel = .medium + } else if (securityLevel < .high) { + newSecurityLevel = .high + } else if (securityLevel < .fips) { + newSecurityLevel = .fips } else { return false } @@ -504,8 +498,11 @@ internal final class ATTConnection { } // attempt to change security level on Socket IO - do { try self.socket.setSecurityLevel(security) } - catch { return false } + do { try self.socket.setSecurityLevel(newSecurityLevel) } + catch { + log?("Unable to set security level. \(error)") + return false + } return true } diff --git a/Sources/BluetoothGATT/GATTClient.swift b/Sources/BluetoothGATT/GATTClient.swift index 0b2a776d9..93e2d103a 100755 --- a/Sources/BluetoothGATT/GATTClient.swift +++ b/Sources/BluetoothGATT/GATTClient.swift @@ -41,7 +41,7 @@ public final class GATTClient { // MARK: - Initialization - public init(socket: L2CAPSocketProtocol, + public init(socket: L2CAPSocket, maximumTransmissionUnit: ATTMaximumTransmissionUnit = .default, log: ((String) -> ())? = nil, writePending: (() -> ())? = nil) { @@ -63,15 +63,13 @@ public final class GATTClient { // MARK: - Methods /// Performs the actual IO for recieving data. - public func read() throws -> Bool { - - return try connection.read() + public func read() async throws { + return try await connection.read() } /// Performs the actual IO for sending data. - public func write() throws -> Bool { - - return try connection.write() + public func write() async throws -> Bool { + return try await connection.write() } // MARK: Requests diff --git a/Sources/BluetoothGATT/GATTServer.swift b/Sources/BluetoothGATT/GATTServer.swift index 25cfc121c..07dd131a0 100644 --- a/Sources/BluetoothGATT/GATTServer.swift +++ b/Sources/BluetoothGATT/GATTServer.swift @@ -44,7 +44,7 @@ public final class GATTServer { // MARK: - Initialization - public init(socket: L2CAPSocketProtocol, + public init(socket: L2CAPSocket, maximumTransmissionUnit: ATTMaximumTransmissionUnit = .default, maximumPreparedWrites: Int = 50) { @@ -58,13 +58,13 @@ public final class GATTServer { // MARK: - Methods /// Performs the actual IO for sending data. - public func read() throws -> Bool { - return try connection.read() + public func read() async throws { + return try await connection.read() } /// Performs the actual IO for recieving data. - public func write() throws -> Bool { - return try connection.write() + public func write() async throws -> Bool { + return try await connection.write() } /// Update the value of a characteristic attribute. @@ -134,15 +134,13 @@ public final class GATTServer { else { fatalError("Could not add error PDU to queue: \(opcode) \(error) \(handle)") } } - @inline(__always) - private func fatalErrorResponse(_ message: String, _ opcode: ATTOpcode, _ handle: UInt16 = 0, line: UInt = #line) -> Never { + private func fatalErrorResponse(_ message: String, _ opcode: ATTOpcode, _ handle: UInt16 = 0, line: UInt = #line) async -> Never { errorResponse(opcode, .unlikelyError, handle) - - do { let _ = try connection.write() } - + do { let _ = try await connection.write() } catch { log?("Could not send .unlikelyError to client. (\(error))") } + // crash fatalError(message, line: line) } @@ -184,20 +182,22 @@ public final class GATTServer { guard attribute.permissions != permissions else { return nil } // check permissions - if permissions.contains(.read) && !attribute.permissions.contains(.read) { - return .readNotPermitted } if permissions.contains(.write) && !attribute.permissions.contains(.write) { - return .writeNotPermitted } // check security - let security = connection.socket.securityLevel + let security: SecurityLevel + do { security = try connection.socket.securityLevel() } + catch { + log?("Unable to get security level. \(error)") + security = .sdp + } if attribute.permissions.contains(.readAuthentication) || attribute.permissions.contains(.writeAuthentication) diff --git a/Tests/BluetoothTests/BluetoothTests.swift b/Tests/BluetoothTests/BluetoothTests.swift index 2ba107366..954419d66 100755 --- a/Tests/BluetoothTests/BluetoothTests.swift +++ b/Tests/BluetoothTests/BluetoothTests.swift @@ -58,7 +58,7 @@ final class BluetoothTests: XCTestCase { let level = SecurityLevel() XCTAssertTrue(level < .high) - XCTAssertTrue(.low < .high) + XCTAssertTrue(SecurityLevel.low < .high) } func testCompanyIdentifier() { diff --git a/Tests/BluetoothTests/GATTTests.swift b/Tests/BluetoothTests/GATTTests.swift index 48465ec55..6e77d3bec 100644 --- a/Tests/BluetoothTests/GATTTests.swift +++ b/Tests/BluetoothTests/GATTTests.swift @@ -10,7 +10,7 @@ import XCTest import Foundation import Bluetooth @testable import BluetoothGATT - +/* final class GATTTests: XCTestCase { static let allTests = [ @@ -1458,3 +1458,4 @@ struct ProximityProfile { ] ) } +*/ diff --git a/Tests/BluetoothTests/L2CAPSocket.swift b/Tests/BluetoothTests/L2CAPSocket.swift index f0f6a6e81..2ba0c66e4 100644 --- a/Tests/BluetoothTests/L2CAPSocket.swift +++ b/Tests/BluetoothTests/L2CAPSocket.swift @@ -11,7 +11,7 @@ import Foundation @testable import BluetoothGATT /// Test L2CAP socket -internal final class TestL2CAPSocket: L2CAPSocketProtocol { +internal final class TestL2CAPSocket { //}: L2CAPSocket { // MARK: - Properties @@ -22,12 +22,15 @@ internal final class TestL2CAPSocket: L2CAPSocketProtocol { let address: BluetoothAddress /// The socket's security level. - private(set) var securityLevel: SecurityLevel = .sdp + private var _securityLevel: SecurityLevel = .sdp /// Attempts to change the socket's security level. func setSecurityLevel(_ securityLevel: SecurityLevel) throws { - - self.securityLevel = securityLevel + _securityLevel = securityLevel + } + + func securityLevel() throws -> SecurityLevel { + return _securityLevel } /// Target socket. From 24c8f183f98ef203122b06d7d2d4d9797d8a6e02 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 13 Nov 2021 21:48:07 -0500 Subject: [PATCH 03/44] Working on `async` support --- Sources/Bluetooth/L2CAPSocket.swift | 10 +- Sources/BluetoothGATT/ATTConnection.swift | 199 ++++++++++------------ Sources/BluetoothGATT/GATTServer.swift | 40 +++-- 3 files changed, 116 insertions(+), 133 deletions(-) diff --git a/Sources/Bluetooth/L2CAPSocket.swift b/Sources/Bluetooth/L2CAPSocket.swift index 625f67462..b19954177 100644 --- a/Sources/Bluetooth/L2CAPSocket.swift +++ b/Sources/Bluetooth/L2CAPSocket.swift @@ -18,15 +18,15 @@ public protocol L2CAPSocket { func recieve(_ bufferSize: Int) async throws -> Data /// Attempts to change the socket's security level. - func setSecurityLevel(_ securityLevel: SecurityLevel) throws + func setSecurityLevel(_ securityLevel: SecurityLevel) async throws /// Get security level - func securityLevel() throws -> SecurityLevel + func securityLevel() async throws -> SecurityLevel - /// Attempt to accept an incomping connection. + /// Attempt to accept an incoming connection. func accept(sleep: UInt64) async throws -> Self - func canWrite() throws -> Bool + //func canWrite() throws -> Bool - func canRead() throws -> Bool + //func canRead() throws -> Bool } diff --git a/Sources/BluetoothGATT/ATTConnection.swift b/Sources/BluetoothGATT/ATTConnection.swift index 58665bd14..b8e007811 100755 --- a/Sources/BluetoothGATT/ATTConnection.swift +++ b/Sources/BluetoothGATT/ATTConnection.swift @@ -10,18 +10,18 @@ import Foundation import Bluetooth /// Manages a Bluetooth connection using the ATT protocol. -internal final class ATTConnection { +internal actor ATTConnection { // MARK: - Properties /// Actual number of bytes for PDU ATT exchange. - public var maximumTransmissionUnit: ATTMaximumTransmissionUnit = .default + public private(set) var maximumTransmissionUnit: ATTMaximumTransmissionUnit = .default public let socket: L2CAPSocket - public var log: ((String) -> ())? + public private(set) var log: ((String) -> ())? - public var writePending: (() -> ())? + public private(set) var writePending: (() -> ())? // MARK: - Private Properties @@ -52,9 +52,6 @@ internal final class ATTConnection { /// List of registered callbacks. private var notifyList = [ATTNotifyType]() - /// List of disconnect handlers. - private var disconnectList = [() -> ()]() - // MARK: - Initialization deinit { @@ -67,6 +64,10 @@ internal final class ATTConnection { // MARK: - Methods + public func setMaximumTransmissionUnit(_ newValue: ATTMaximumTransmissionUnit) { + self.maximumTransmissionUnit = newValue + } + /// Performs the actual IO for recieving data. public func read() async throws { @@ -93,27 +94,22 @@ internal final class ATTConnection { switch opcode.type { case .response: - - try handle(response: recievedData, opcode: opcode) - + try await handle(response: recievedData, opcode: opcode) case .confirmation: - try handle(confirmation: recievedData, opcode: opcode) - case .request: - - try handle(request: recievedData, opcode: opcode) - + try await handle(request: recievedData, opcode: opcode) case .command, .notification, .indication: // For all other opcodes notify the upper layer of the PDU and let them act on it. - try handle(notify: recievedData, opcode: opcode) + try await handle(notify: recievedData, opcode: opcode) } } /// Performs the actual IO for sending data. + @discardableResult public func write() async throws -> Bool { //log?("Attempt write") @@ -152,7 +148,7 @@ internal final class ATTConnection { /// Registers a callback for an opcode and returns the ID associated with that callback. @discardableResult - public func register (_ callback: @escaping (T) -> ()) -> UInt { + public func register (_ callback: @escaping (T) async -> ()) -> UInt { let identifier = nextRegisterID @@ -182,16 +178,18 @@ internal final class ATTConnection { /// Registers all callbacks. public func unregisterAll() { - notifyList.removeAll() - disconnectList.removeAll() } /// Sends an error. - public func send(error: ATTError, opcode: ATTOpcode, handle: UInt16 = 0, response: ((ATTErrorResponse) -> ())? = nil) -> UInt? { + public func send( + error: ATTError, + opcode: ATTOpcode, + handle: UInt16 = 0, + response: ((ATTErrorResponse) -> ())? = nil + ) -> UInt? { let error = ATTErrorResponse(request: opcode, attributeHandle: handle, error: error) - return self.send(error) // no callback for responses } @@ -199,15 +197,13 @@ internal final class ATTConnection { /// /// - Returns: Identifier of queued send operation or `nil` if the PDU cannot be sent. @discardableResult - public func send (_ pdu: PDU, response: (callback: (AnyATTResponse) -> (), ATTProtocolDataUnit.Type)? = nil) -> UInt? { - - let attributeOpcode = PDU.attributeOpcode + public func send (_ pdu: T, response: (callback: (ATTProtocolDataUnit) -> (), ATTProtocolDataUnit.Type)? = nil) -> UInt? { + let attributeOpcode = T.attributeOpcode let type = attributeOpcode.type // Only request and indication PDUs should have response callbacks. switch type { - case .request, .indication: // Indication handles confirmation @@ -224,41 +220,36 @@ internal final class ATTConnection { } /// unable to encode PDU - guard let encodedPDU = encode(PDU: pdu) + guard let encodedPDU = encode(pdu) else { return nil } - let identifier = nextSendOpcodeID + let id = nextSendOpcodeID - let sendOpcode = ATTSendOperation(identifier: identifier, - opcode: attributeOpcode, - data: encodedPDU, - response: response) + let sendOpcode = ATTSendOperation( + id: id, + opcode: attributeOpcode, + data: encodedPDU, + response: response + ) // increment ID nextSendOpcodeID += 1 // Add the op to the correct queue based on its type switch type { - case .request: - requestQueue.append(sendOpcode) - case .indication: - indicationQueue.append(sendOpcode) - case .response, .command, .confirmation, .notification: - writeQueue.append(sendOpcode) } writePending?() - - return sendOpcode.identifier + return sendOpcode.id } /* @@ -274,22 +265,23 @@ internal final class ATTConnection { // MARK: - Private Methods - private func encode (PDU: T) -> Data? { + private func encode (_ request: T) -> Data? { - let data = PDU.data + let data = request.data // actual PDU length let length = data.count /// MTU must be large enough to hold PDU. - guard length <= Int(maximumTransmissionUnit.rawValue) else { return nil } + guard length <= Int(maximumTransmissionUnit.rawValue) + else { return nil } // TODO: Sign (encrypt) data return data } - private func handle(response data: Data, opcode: ATTOpcode) throws { + private func handle(response data: Data, opcode: ATTOpcode) async throws { // If no request is pending, then the response is unexpected. Disconnect the bearer. guard let sendOperation = self.pendingRequest else { @@ -308,7 +300,7 @@ internal final class ATTConnection { guard let errorResponse = ATTErrorResponse(data: data) else { throw Error.garbageResponse(data) } - let (errorRequestOpcode, didRetry) = handle(errorResponse: errorResponse) + let (errorRequestOpcode, didRetry) = await handle(errorResponse: errorResponse) requestOpcode = errorRequestOpcode @@ -354,12 +346,11 @@ internal final class ATTConnection { // send the remaining indications if indicationQueue.isEmpty == false { - writePending?() } } - private func handle(request data: Data, opcode: ATTOpcode) throws { + private func handle(request data: Data, opcode: ATTOpcode) async throws { /* * If a request is currently pending, then the sequential @@ -374,14 +365,15 @@ internal final class ATTConnection { incomingRequest = true // notify - try handle(notify: data, opcode: opcode) + try await handle(notify: data, opcode: opcode) } - private func handle(notify data: Data, opcode: ATTOpcode) throws { + private func handle(notify data: Data, opcode: ATTOpcode) async throws { var foundPDU: ATTProtocolDataUnit? - for notify in notifyList { + let oldList = notifyList + for notify in oldList { // try next if type(of: notify).PDUType.attributeOpcode != opcode { continue } @@ -392,7 +384,7 @@ internal final class ATTConnection { foundPDU = PDU - notify.callback(PDU) + await notify.callback(PDU) // callback could remove all entries from notify list, if so, exit the loop if self.notifyList.isEmpty { break } @@ -412,7 +404,7 @@ internal final class ATTConnection { /// /// - Returns: The opcode of the request that errored /// and whether the request will be sent again. - private func handle(errorResponse: ATTErrorResponse) -> (opcode: ATTOpcode, didRetry: Bool) { + private func handle(errorResponse: ATTErrorResponse) async -> (opcode: ATTOpcode, didRetry: Bool) { let opcode = errorResponse.request @@ -420,7 +412,7 @@ internal final class ATTConnection { else { return (opcode, false) } // Attempt to change security - guard changeSecurity(for: errorResponse.error) + guard await changeSecurity(for: errorResponse.error) else { return (opcode, false) } //print("Retrying operation \(pendingRequest)") @@ -437,14 +429,12 @@ internal final class ATTConnection { // See if any operations are already in the write queue if let sendOpcode = writeQueue.popFirst() { - return sendOpcode } // If there is no pending request, pick an operation from the request queue. if pendingRequest == nil, let sendOpcode = requestQueue.popFirst() { - return sendOpcode } @@ -452,10 +442,8 @@ internal final class ATTConnection { // If there is no pending indication, pick an operation from the indication queue. if pendingIndication == nil, let sendOpcode = indicationQueue.popFirst() { - // can't send more indications until the last one is confirmed pendingIndication = sendOpcode - return sendOpcode } @@ -463,10 +451,10 @@ internal final class ATTConnection { } /// Attempts to change security level based on an error response. - private func changeSecurity(for error: ATTError) -> Bool { + private func changeSecurity(for error: ATTError) async -> Bool { let securityLevel: Bluetooth.SecurityLevel - do { securityLevel = try self.socket.securityLevel() } + do { securityLevel = try await self.socket.securityLevel() } catch { log?("Unable to get security level. \(error)") return false @@ -498,7 +486,7 @@ internal final class ATTConnection { } // attempt to change security level on Socket IO - do { try self.socket.setSecurityLevel(newSecurityLevel) } + do { try await self.socket.setSecurityLevel(newSecurityLevel) } catch { log?("Unable to set security level. \(error)") return false @@ -525,49 +513,34 @@ internal extension ATTConnection { typealias Error = ATTConnectionError } -/// Type-erased ATT Response -internal enum AnyATTResponse { - - case failure(ATTErrorResponse) - case success(ATTProtocolDataUnit) - - internal var rawValue: ATTProtocolDataUnit { - switch self { - case let .failure(pdu): return pdu - case let .success(pdu): return pdu - } - } -} +internal typealias AnyATTResponse = Result -internal enum ATTResponse { - - case failure(ATTErrorResponse) - case success(Value) +internal typealias ATTResponse = Result + +internal extension Result where Success: ATTProtocolDataUnit, Failure == ATTErrorResponse { - internal init(_ anyResponse: AnyATTResponse) { + init(_ response: ATTProtocolDataUnit) { - // validate types - assert(Value.self != ATTErrorResponse.self) - assert(Value.attributeOpcode.type == .response || Value.attributeOpcode.type == .confirmation) + assert(Success.self != ATTErrorResponse.self) + assert(Success.attributeOpcode.type == .response || + Success.attributeOpcode.type == .confirmation) - switch anyResponse { - case let .failure(error): + if let error = response as? Failure { + assert(type(of: response).attributeOpcode == .errorResponse) self = .failure(error) - case let .success(value): - // swiftlint:disable force_cast - let specializedValue = value as! Value - // swiftlint:enable all - self = .success(specializedValue) + } else if let value = response as? Success { + assert(type(of: response).attributeOpcode == Success.attributeOpcode) + self = .success(value) + } else { + fatalError("Invalid response \(type(of: response).attributeOpcode)") } } } -private final class ATTSendOperation { - - typealias Response = AnyATTResponse +internal final class ATTSendOperation { /// The operation identifier. - let identifier: UInt + let id: UInt /// The request data. let data: Data @@ -576,14 +549,16 @@ private final class ATTSendOperation { let opcode: ATTOpcode /// The response callback. - let response: (callback: (Response) -> (), responseType: ATTProtocolDataUnit.Type)? - - fileprivate init(identifier: UInt, - opcode: ATTOpcode, - data: Data, - response: (callback: (Response) -> (), responseType: ATTProtocolDataUnit.Type)? = nil) { - - self.identifier = identifier + let response: (callback: (ATTProtocolDataUnit) -> (), responseType: ATTProtocolDataUnit.Type)? + + fileprivate init( + id: UInt, + opcode: ATTOpcode, + data: Data, + response: (callback: (ATTProtocolDataUnit) -> (), + responseType: ATTProtocolDataUnit.Type)? = nil + ) { + self.id = id self.opcode = opcode self.data = data self.response = response @@ -594,46 +569,50 @@ private final class ATTSendOperation { guard let responseInfo = self.response else { throw ATTConnectionError.unexpectedResponse(data) } - guard let opcode = data.first + guard let opcode = data.first.flatMap(ATTOpcode.init(rawValue:)) else { throw ATTConnectionError.garbageResponse(data) } - if opcode == ATTOpcode.errorResponse.rawValue { + if opcode == .errorResponse { guard let errorResponse = ATTErrorResponse(data: data) else { throw ATTConnectionError.garbageResponse(data) } - responseInfo.callback(.failure(errorResponse)) + responseInfo.callback(errorResponse) - } else { + } else if opcode == responseInfo.responseType.attributeOpcode { guard let response = responseInfo.responseType.init(data: data) else { throw ATTConnectionError.garbageResponse(data) } - responseInfo.callback(.success(response)) + responseInfo.callback(response) + + } else { + // other ATT response + throw ATTConnectionError.garbageResponse(data) } } } -private protocol ATTNotifyType { +internal protocol ATTNotifyType { static var PDUType: ATTProtocolDataUnit.Type { get } var identifier: UInt { get } - var callback: (ATTProtocolDataUnit) -> () { get } + var callback: (ATTProtocolDataUnit) async -> () { get } } -private struct ATTNotify: ATTNotifyType { +internal struct ATTNotify: ATTNotifyType { static var PDUType: ATTProtocolDataUnit.Type { return PDU.self } let identifier: UInt - let notify: (PDU) -> () + let notify: (PDU) async -> () - var callback: (ATTProtocolDataUnit) -> () { return { self.notify($0 as! PDU) } } + var callback: (ATTProtocolDataUnit) async -> () { return { await self.notify($0 as! PDU) } } - init(identifier: UInt, notify: @escaping (PDU) -> ()) { + init(identifier: UInt, notify: @escaping (PDU) async -> ()) { self.identifier = identifier self.notify = notify diff --git a/Sources/BluetoothGATT/GATTServer.swift b/Sources/BluetoothGATT/GATTServer.swift index 07dd131a0..6a1c6df70 100644 --- a/Sources/BluetoothGATT/GATTServer.swift +++ b/Sources/BluetoothGATT/GATTServer.swift @@ -9,7 +9,7 @@ import Foundation import Bluetooth -public final class GATTServer { +public actor GATTServer { // MARK: - Properties @@ -52,7 +52,11 @@ public final class GATTServer { self.maximumPreparedWrites = maximumPreparedWrites self.preferredMaximumTransmissionUnit = maximumTransmissionUnit self.connection = ATTConnection(socket: socket) - self.registerATTHandlers() + + // async register handlers + Task { + await self.registerATTHandlers() + } } // MARK: - Methods @@ -86,43 +90,43 @@ public final class GATTServer { // MARK: - Private Methods @inline(__always) - private func registerATTHandlers() { + private func registerATTHandlers() async { // Exchange MTU - connection.register { [weak self] in self?.exchangeMTU($0) } + await connection.register { [weak self] in await self?.exchangeMTU($0) } // Read By Group Type - connection.register { [weak self] in self?.readByGroupType($0) } + await connection.register { [weak self] in await self?.readByGroupType($0) } // Read By Type - connection.register { [weak self] in self?.readByType($0) } + await connection.register { [weak self] in await self?.readByType($0) } // Find Information - connection.register { [weak self] in self?.findInformation($0) } + await connection.register { [weak self] in await self?.findInformation($0) } // Find By Type Value - connection.register { [weak self] in self?.findByTypeValue($0) } + await connection.register { [weak self] in await self?.findByTypeValue($0) } // Write Request - connection.register { [weak self] in self?.writeRequest($0) } + await connection.register { [weak self] in await self?.writeRequest($0) } // Write Command - connection.register { [weak self] in self?.writeCommand($0) } + await connection.register { [weak self] in await self?.writeCommand($0) } // Read Request - connection.register { [weak self] in self?.readRequest($0) } + await connection.register { [weak self] in await self?.readRequest($0) } // Read Blob Request - connection.register { [weak self] in self?.readBlobRequest($0) } + await connection.register { [weak self] in await self?.readBlobRequest($0) } // Read Multiple Request - connection.register { [weak self] in self?.readMultipleRequest($0) } + await connection.register { [weak self] in await self?.readMultipleRequest($0) } // Prepare Write Request - connection.register { [weak self] in self?.prepareWriteRequest($0) } + await connection.register { [weak self] in await self?.prepareWriteRequest($0) } // Execute Write Request - connection.register { [weak self] in self?.executeWriteRequest($0) } + await connection.register { [weak self] in await self?.executeWriteRequest($0) } } @inline(__always) @@ -177,7 +181,7 @@ public final class GATTServer { } private func checkPermissions(_ permissions: BitMaskOptionSet, - _ attribute: GATTDatabase.Attribute) -> ATTError? { + _ attribute: GATTDatabase.Attribute) async -> ATTError? { guard attribute.permissions != permissions else { return nil } @@ -193,7 +197,7 @@ public final class GATTServer { // check security let security: SecurityLevel - do { security = try connection.socket.securityLevel() } + do { security = try await connection.socket.securityLevel() } catch { log?("Unable to get security level. \(error)") security = .sdp @@ -240,7 +244,7 @@ public final class GATTServer { let attribute = database[handle: handle] // validate permissions - if let error = checkPermissions([.write, .writeAuthentication, .writeEncrypt], attribute) { + if let error = await checkPermissions([.write, .writeAuthentication, .writeEncrypt], attribute) { doResponse(errorResponse(opcode, error, handle)) return From ac4887ee79ed4c1693d2f63de715011250a11ced Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 13 Nov 2021 21:48:20 -0500 Subject: [PATCH 04/44] Working on `async` support for `GATTClient` --- Sources/BluetoothGATT/GATTClient.swift | 532 +++++++++++++------------ 1 file changed, 281 insertions(+), 251 deletions(-) diff --git a/Sources/BluetoothGATT/GATTClient.swift b/Sources/BluetoothGATT/GATTClient.swift index 93e2d103a..951deef31 100755 --- a/Sources/BluetoothGATT/GATTClient.swift +++ b/Sources/BluetoothGATT/GATTClient.swift @@ -10,25 +10,23 @@ import Foundation import Bluetooth /// GATT Client -public final class GATTClient { +public actor GATTClient { // MARK: - Properties public var log: ((String) -> ())? - public var writePending: (() -> ())? { - get { return connection.writePending } - set { connection.writePending = newValue } - } - - public private(set) var maximumTransmissionUnit: ATTMaximumTransmissionUnit { - get { return connection.maximumTransmissionUnit } - set { connection.maximumTransmissionUnit = newValue } + public var maximumTransmissionUnit: ATTMaximumTransmissionUnit { + get async { + return await self.connection.maximumTransmissionUnit + } } public let preferredMaximumTransmissionUnit: ATTMaximumTransmissionUnit - internal private(set) var connection: ATTConnection + internal private(set) var didExchangeMTU = false + + internal let connection: ATTConnection /// Whether the client is currently writing a long value. internal private(set) var inLongWrite: Bool = false @@ -41,37 +39,28 @@ public final class GATTClient { // MARK: - Initialization - public init(socket: L2CAPSocket, - maximumTransmissionUnit: ATTMaximumTransmissionUnit = .default, - log: ((String) -> ())? = nil, - writePending: (() -> ())? = nil) { - + public init( + socket: L2CAPSocket, + maximumTransmissionUnit: ATTMaximumTransmissionUnit = .default, + log: ((String) -> ())? = nil + ) { self.connection = ATTConnection(socket: socket) self.preferredMaximumTransmissionUnit = maximumTransmissionUnit self.log = log - self.writePending = writePending - // setup notifications and indications - self.registerATTHandlers() - - // queue MTU exchange if not default value - if maximumTransmissionUnit > .default { - self.exchangeMTU() + // async setup tasks + Task { [weak self] in + // setup notifications and indications + await self?.registerATTHandlers() + // queue MTU exchange if not default value + if maximumTransmissionUnit > .default { + await self?.exchangeMTU() + } } } // MARK: - Methods - /// Performs the actual IO for recieving data. - public func read() async throws { - return try await connection.read() - } - - /// Performs the actual IO for sending data. - public func write() async throws -> Bool { - return try await connection.write() - } - // MARK: Requests /// Discover All Primary Services @@ -80,12 +69,16 @@ public final class GATTClient { /// ![Image](https://github.com/PureSwift/Bluetooth/raw/master/Assets/DiscoverAllPrimaryServices.png) /// /// - Parameter completion: The completion closure. - public func discoverAllPrimaryServices(completion: @escaping (GATTClientResponse<[Service]>) -> ()) { + public func discoverAllPrimaryServices() async throws -> [Service] { /// The Attribute Protocol Read By Group Type Request shall be used with /// the Attribute Type parameter set to the UUID for «Primary Service». /// The Starting Handle shall be set to 0x0001 and the Ending Handle shall be set to 0xFFFF. - discoverServices(start: 0x0001, end: 0xFFFF, primary: true, completion: completion) + try await discoverServices( + start: 0x0001, + end: 0xFFFF, + primary: true + ) } /// Discover Primary Service by Service UUID @@ -97,14 +90,20 @@ public final class GATTClient { /// /// - Parameter uuid: The UUID of the service to find. /// - Parameter completion: The completion closure. - public func discoverPrimaryServices(by uuid: BluetoothUUID, - completion: @escaping (GATTClientResponse<[Service]>) -> ()) { + public func discoverPrimaryServices( + by uuid: BluetoothUUID + ) async throws -> [Service]{ // The Attribute Protocol Find By Type Value Request shall be used with the Attribute Type // parameter set to the UUID for «Primary Service» and the Attribute Value set to the 16-bit // Bluetooth UUID or 128-bit UUID for the specific primary service. // The Starting Handle shall be set to 0x0001 and the Ending Handle shall be set to 0xFFFF. - discoverServices(uuid: uuid, start: 0x0001, end: 0xFFFF, primary: true, completion: completion) + return try await discoverServices( + uuid: uuid, + start: 0x0001, + end: 0xFFFF, + primary: true + ) } /// Discover All Characteristics of a Service @@ -116,15 +115,14 @@ public final class GATTClient { /// /// - Parameter service: The service. /// - Parameter completion: The completion closure. - public func discoverAllCharacteristics(of service: Service, - completion: @escaping (GATTClientResponse<[Characteristic]>) -> ()) { + public func discoverAllCharacteristics(of service: Service) async throws -> [Characteristic] { // The Attribute Protocol Read By Type Request shall be used with the Attribute Type // parameter set to the UUID for «Characteristic» The Starting Handle shall be set to // starting handle of the specified service and the Ending Handle shall be set to the // ending handle of the specified service. - discoverCharacteristics(service: service, completion: completion) + return try await discoverCharacteristics(service: service) } /// Discover Characteristics by UUID @@ -138,15 +136,15 @@ public final class GATTClient { /// - Parameter service: The service of the characteristics to find. /// - Parameter uuid: The UUID of the characteristics to find. /// - Parameter completion: The completion closure. - public func discoverCharacteristics(of service: Service, - by uuid: BluetoothUUID, - completion: @escaping (GATTClientResponse<[Characteristic]>) -> ()) { + public func discoverCharacteristics( + of service: Service, + by uuid: BluetoothUUID + ) async throws -> [Characteristic] { // The Attribute Protocol Read By Type Request is used to perform the beginning of the sub-procedure. // The Attribute Type is set to the UUID for «Characteristic» and the Starting Handle and Ending Handle // parameters shall be set to the service handle range. - - discoverCharacteristics(uuid: uuid, service: service, completion: completion) + return try await discoverCharacteristics(uuid: uuid, service: service) } /// Read Characteristic Value @@ -155,8 +153,7 @@ public final class GATTClient { /// the Characteristic Value Handle. /// /// ![Image](https://github.com/PureSwift/Bluetooth/raw/master/Assets/ReadCharacteristicValue.png) - public func readCharacteristic(_ characteristic: Characteristic, - completion: @escaping (GATTClientResponse) -> ()) { + public func readCharacteristic(_ characteristic: Characteristic) async throws -> Data { // read value and try to read blob if too big readAttributeValue(characteristic.handle.value, completion: completion) @@ -168,9 +165,11 @@ public final class GATTClient { /// only knows the characteristic UUID and does not know the handle of the characteristic. /// /// ![Image](https://github.com/PureSwift/Bluetooth/raw/master/Assets/ReadUsingCharacteristicUUID.png) - public func readCharacteristics(using uuid: BluetoothUUID, - handleRange: (start: UInt16, end: UInt16) = (.min, .max), - completion: @escaping (GATTClientResponse<[UInt16: Data]>) -> ()) { + public func readCharacteristics( + using uuid: BluetoothUUID, + handleRange: (start: UInt16, end: UInt16) = (.min, .max), + completion: @escaping (GATTClientResponse<[UInt16: Data]>) -> () + ) { precondition(handleRange.start < handleRange.end) @@ -178,13 +177,17 @@ public final class GATTClient { // The Attribute Type is set to the known characteristic UUID and the Starting Handle and Ending Handle parameters // shall be set to the range over which this read is to be performed. This is typically the handle range for the service in which the characteristic belongs. - let pdu = ATTReadByTypeRequest(startHandle: handleRange.start, - endHandle: handleRange.end, - attributeType: uuid) + let pdu = ATTReadByTypeRequest( + startHandle: handleRange.start, + endHandle: handleRange.end, + attributeType: uuid + ) let operation = ReadUsingUUIDOperation(uuid: uuid, completion: completion) - send(pdu) { [unowned self] in self.readByTypeResponse($0, operation: operation) } + send(pdu) { [unowned self] in + self.readByTypeResponse($0, operation: operation) + } } /// Read Multiple Characteristic Values @@ -208,7 +211,9 @@ public final class GATTClient { let operation = ReadMultipleOperation(characteristics: characteristics, completion: completion) - send(pdu) { [unowned self] in self.readMultipleResponse($0, operation: operation) } + send(pdu) { [unowned self] in + self.readMultipleResponse($0, operation: operation) + } } /** @@ -295,11 +300,12 @@ public final class GATTClient { ![Image](https://github.com/PureSwift/Bluetooth/raw/master/Assets/Notifications.png) */ - public func clientCharacteristicConfiguration(notification: Notification?, - indication: Notification?, - for characteristic: Characteristic, - descriptors: [GATTClient.Descriptor], - completion: @escaping (GATTClientResponse<()>) -> ()) { + public func clientCharacteristicConfiguration( + notification: Notification?, + indication: Notification?, + for characteristic: Characteristic, + descriptors: [GATTClient.Descriptor] + ) async throws { guard let descriptor = descriptors.first(where: { $0.uuid == .clientCharacteristicConfiguration }) else { completion(.failure(GATTClientError.clientCharacteristicConfigurationNotAllowed(characteristic))); return } @@ -333,46 +339,67 @@ public final class GATTClient { // MARK: - Private Methods - @inline(__always) - private func registerATTHandlers() { + private func registerATTHandlers() async { // value notifications / indications - connection.register { [weak self] in self?.notification($0) } - connection.register { [weak self] in self?.indication($0) } + await connection.register { [weak self] in await self?.notification($0) } + await connection.register { [weak self] in await self?.indication($0) } } - @inline(__always) - private func send (_ request: Request, response: @escaping (ATTResponse) -> ()) { + private func send ( + _ request: Request, + response: Response.Type + ) async throws -> ATTResponse { let log = self.log - log?("Request: \(request)") - let callback: (AnyATTResponse) -> () = { - log?("Response: \($0.rawValue)") - response(ATTResponse($0)) + return try await withCheckedThrowingContinuation { [weak self] continuation in + guard let self = self else { return } + Task { + let responseType: ATTProtocolDataUnit.Type = Response.self + // callback if no I/O errors or disconnect + let callback: (ATTProtocolDataUnit) -> () = { + log?("Response: \($0)") + continuation.resume(returning: ATTResponse($0)) + } + guard let _ = await self.connection.send(request, response: (callback, responseType)) + else { fatalError("Could not add PDU to queue: \(request)") } + // do I/O + do { + // write pending + let didWrite = try await self.connection.write() + assert(didWrite, "Expected queued write operation") + try await self.connection.read() + } + catch { + // not ATTError + assert(type(of: error) != ATTError.self) + continuation.resume(throwing: error) + } + } } - - let responseType: ATTProtocolDataUnit.Type = Response.self - - guard let _ = connection.send(request, response: (callback, responseType)) - else { fatalError("Could not add PDU to queue: \(request)") } } @inline(__always) - private func send (_ request: Request) { + private func send(_ request: Request) async throws { log?("Request: \(request)") - guard let _ = connection.send(request) + guard let _ = await connection.send(request) else { fatalError("Could not add PDU to queue: \(request)") } + + // write pending + let didWrite = try await self.connection.write() + assert(didWrite, "Expected queued write operation") } - internal func endHandle(for characteristic: Characteristic, - service: (declaration: Service, characteristics: [Characteristic])) -> UInt16 { + internal func endHandle( + for characteristic: Characteristic, + service: (declaration: Service, characteristics: [Characteristic]) + ) -> UInt16 { // calculate ending handle of characteristic - let end: UInt16 guard let index = service.characteristics.firstIndex(where: { $0.handle.declaration == characteristic.handle.declaration }) @@ -382,61 +409,86 @@ public final class GATTClient { // get start handle of next characteristic if nextIndex < service.characteristics.count { - let nextCharacteristic = service.characteristics[nextIndex] - end = nextCharacteristic.handle.declaration - 1 - } else { - // use service end handle end = service.declaration.end } + // FIXME: Handle descriptors return end } // MARK: Requests - private func exchangeMTU() { - + /// Exchange MTU (should only be called once if not using default MTU) + internal func exchangeMTU() async { + assert(didExchangeMTU == false) let clientMTU = preferredMaximumTransmissionUnit - - let pdu = ATTMaximumTransmissionUnitRequest(clientMTU: clientMTU.rawValue) - - send(pdu) { [unowned self] in self.exchangeMTUResponse($0) } + let request = ATTMaximumTransmissionUnitRequest(clientMTU: clientMTU.rawValue) + do { + let response = try await send(request, response: ATTMaximumTransmissionUnitResponse.self).get() + await exchangeMTUResponse(response) + } catch { + log?("Could not exchange MTU: \(error)") + } } - private func discoverServices(uuid: BluetoothUUID? = nil, - start: UInt16 = 0x0001, - end: UInt16 = 0xffff, - primary: Bool = true, - completion: @escaping (GATTClientResponse<[Service]>) -> ()) { - - let serviceType = GATTUUID(primaryService: primary) - - let operation = DiscoveryOperation(uuid: uuid, - start: start, - end: end, - type: serviceType, - completion: completion) - - if let uuid = uuid { - - let pdu = ATTFindByTypeRequest(startHandle: start, - endHandle: end, - attributeType: serviceType.rawValue, - attributeValue: uuid.littleEndian.data) - - send(pdu) { [unowned self] in self.findByTypeResponse($0, operation: operation) } - - } else { - - let pdu = ATTReadByGroupTypeRequest(startHandle: start, - endHandle: end, - type: serviceType.uuid) - - send(pdu) { [unowned self] in self.readByGroupTypeResponse($0, operation: operation) } + internal func discoverServices( + uuid: BluetoothUUID? = nil, + start: UInt16 = 0x0001, + end: UInt16 = 0xffff, + primary: Bool = true + ) async throws -> [Service] { + return try await withCheckedThrowingContinuation() { continuation in + Task { [weak self] in + guard let self = self else { return } + let serviceType = GATTUUID(primaryService: primary) + let operation = DiscoveryOperation( + uuid: uuid, + start: start, + end: end, + type: serviceType + ) { result in + continuation.resume(with: result) + } + + do { + if let uuid = uuid { + let request = ATTFindByTypeRequest( + startHandle: start, + endHandle: end, + attributeType: serviceType.rawValue, + attributeValue: uuid.littleEndian.data + ) + // perform I/O + let response = try await self.send(request, response: ATTFindByTypeResponse.self) + switch response { + case let .success(response): + try await self.findByTypeResponse(response, operation: operation) + case let .failure(error): + operation.failure(error) + } + } else { + let request = ATTReadByGroupTypeRequest( + startHandle: start, + endHandle: end, + type: serviceType.uuid + ) + let response = try await self.send(request, response: ATTReadByGroupTypeResponse.self) + switch response { + case let .success(response): + try await self.readByGroupTypeResponse(response, operation: operation) + case let .failure(error): + operation.failure(error) + } + } + } catch { + assert(error as? ATTErrorResponse == nil) + continuation.resume(throwing: error) + } + } } } @@ -481,8 +533,7 @@ public final class GATTClient { // The Read Response returns the Characteristic Value in the Attribute Value parameter. // read value and try to read blob if too big - let pdu = ATTReadRequest(handle: handle) - + let request = ATTReadRequest(handle: handle) let operation = ReadOperation(handle: handle, completion: completion) send(pdu) { [unowned self] in self.readResponse($0, operation: operation) } @@ -606,7 +657,10 @@ public final class GATTClient { - Note: On BR/EDR, the ATT Bearer is always encrypted, due to the use of Security Mode 4, therefore this sub-procedure shall not be used. */ - private func writeSignedCharacteristicCommand(_ characteristic: Characteristic, data: Data) { + private func writeSignedCharacteristicCommand( + _ characteristic: Characteristic, + data: Data + ) async throws { // This sub-procedure only writes the first (ATT_MTU – 15) octets of an Attribute Value. // This sub-procedure cannot be used to write a long Attribute. @@ -619,39 +673,35 @@ public final class GATTClient { // Section 10.2 then, a Write Without Response as defined in Section 4.9.1 shall be used instead of // a Signed Write Without Response. - let data = Data(data.prefix(Int(maximumTransmissionUnit.rawValue) - 15)) + let dataLength = await Int(maximumTransmissionUnit.rawValue) - 15 + let data = Data(data.prefix(dataLength)) // TODO: Sign Data let pdu = ATTWriteCommand(handle: characteristic.handle.value, value: data) - - send(pdu) + try await send(pdu) } // MARK: - Callbacks - private func exchangeMTUResponse(_ response: ATTResponse) { - - switch response { - - case let .failure(error): - - log?("Could not exchange MTU: \(error)") - - case let .success(pdu): - - let clientMTU = preferredMaximumTransmissionUnit - - let finalMTU = ATTMaximumTransmissionUnit(server: pdu.serverMTU, client: clientMTU.rawValue) - - log?("MTU Exchange (\(clientMTU) -> \(finalMTU))") - - self.maximumTransmissionUnit = finalMTU - } + private func exchangeMTUResponse( + _ response: ATTMaximumTransmissionUnitResponse + ) async { + assert(didExchangeMTU == false) + let clientMTU = preferredMaximumTransmissionUnit + let finalMTU = ATTMaximumTransmissionUnit( + server: response.serverMTU, + client: clientMTU.rawValue + ) + log?("MTU Exchange (\(clientMTU) -> \(finalMTU))") + await self.connection.setMaximumTransmissionUnit(finalMTU) + self.didExchangeMTU = true } - private func readByGroupTypeResponse(_ response: ATTResponse, - operation: DiscoveryOperation) { + private func readByGroupTypeResponse( + _ response: ATTReadByGroupTypeResponse, + operation: DiscoveryOperation + ) async throws { // Read By Group Type Response returns a list of Attribute Handle, End Group Handle, and Attribute Value tuples // corresponding to the services supported by the server. Each Attribute Value contained in the response is the @@ -660,58 +710,56 @@ public final class GATTClient { // The Read By Group Type Request shall be called again with the Starting Handle set to one greater than the // last End Group Handle in the Read By Group Type Response. - switch response { + // store PDU values + for serviceData in response.attributeData { - case let .failure(errorResponse): - - operation.failure(errorResponse) - - case let .success(pdu): - - // store PDU values - for serviceData in pdu.attributeData { - - guard let littleEndianServiceUUID = BluetoothUUID(data: serviceData.value) - else { operation.completion(.failure(Error.invalidResponse(pdu))); return } - - let serviceUUID = BluetoothUUID(littleEndian: littleEndianServiceUUID) - - let service = Service(uuid: serviceUUID, - isPrimary: operation.type == .primaryService, - handle: serviceData.attributeHandle, - end: serviceData.endGroupHandle) - - operation.foundData.append(service) + guard let littleEndianServiceUUID = BluetoothUUID(data: serviceData.value) else { + operation.completion(.failure(Error.invalidResponse(response))) + return } - // get more if possible - let lastEnd = pdu.attributeData.last?.endGroupHandle ?? 0x00 - - // prevent infinite loop - guard lastEnd >= operation.start - else { operation.completion(.failure(Error.invalidResponse(pdu))); return } - - guard lastEnd < .max // End of database - else { operation.success(); return } - - operation.start = lastEnd + 1 + let serviceUUID = BluetoothUUID(littleEndian: littleEndianServiceUUID) + let service = Service( + uuid: serviceUUID, + isPrimary: operation.type == .primaryService, + handle: serviceData.attributeHandle, + end: serviceData.endGroupHandle + ) - if lastEnd < operation.end { - - let pdu = ATTReadByGroupTypeRequest(startHandle: operation.start, - endHandle: operation.end, - type: operation.type.uuid) - - send(pdu) { [unowned self] in self.readByGroupTypeResponse($0, operation: operation) } - - } else { - - operation.success() - } + operation.foundData.append(service) + } + + // get more if possible + let lastEnd = response.attributeData.last?.endGroupHandle ?? 0x00 + + // prevent infinite loop + guard lastEnd >= operation.start else { + operation.completion(.failure(Error.invalidResponse(response))) + return + } + + guard lastEnd < .max // End of database + else { operation.success(); return } + + operation.start = lastEnd + 1 + + if lastEnd < operation.end { + let request = ATTReadByGroupTypeRequest( + startHandle: operation.start, + endHandle: operation.end, + type: operation.type.uuid + ) + let response = try await send(request, response: ATTReadByGroupTypeResponse.self) + try await readByGroupTypeResponse(response, operation: operation) + } else { + operation.success() } } - private func findByTypeResponse(_ response: ATTResponse, operation: DiscoveryOperation) { + private func findByTypeResponse( + _ response: ATTFindByTypeResponse, + operation: DiscoveryOperation + ) async throws { // Find By Type Value Response returns a list of Attribute Handle ranges. // The Attribute Handle range is the starting handle and the ending handle of the service definition. @@ -719,53 +767,48 @@ public final class GATTClient { // is not 0xFFFF, the Find By Type Value Request may be called again with the Starting Handle set to one // greater than the last Attribute Handle range in the Find By Type Value Response. - switch response { - - case let .failure(errorResponse): - - operation.failure(errorResponse) - - case let .success(pdu): - - guard let serviceUUID = operation.uuid - else { fatalError("Should have UUID specified") } - - // pre-allocate array - operation.foundData.reserveCapacity(operation.foundData.count + pdu.handles.count) + guard let serviceUUID = operation.uuid + else { fatalError("Should have UUID specified") } + + // pre-allocate array + operation.foundData.reserveCapacity(operation.foundData.count + response.handles.count) + + // store PDU values + for serviceData in response.handles { - // store PDU values - for serviceData in pdu.handles { - - let service = Service(uuid: serviceUUID, - isPrimary: operation.type == .primaryService, - handle: serviceData.foundAttribute, - end: serviceData.groupEnd) - - operation.foundData.append(service) - } + let service = Service( + uuid: serviceUUID, + isPrimary: operation.type == .primaryService, + handle: serviceData.foundAttribute, + end: serviceData.groupEnd + ) - // get more if possible - let lastEnd = pdu.handles.last?.groupEnd ?? 0x00 + operation.foundData.append(service) + } + + // get more if possible + let lastEnd = response.handles.last?.groupEnd ?? 0x00 + + guard lastEnd < .max // End of database + else { operation.success(); return } + + operation.start = lastEnd + 1 + + // need to continue scanning + if lastEnd < operation.end { - guard lastEnd < .max // End of database - else { operation.success(); return } + let request = ATTFindByTypeRequest( + startHandle: operation.start, + endHandle: operation.end, + attributeType: operation.type.rawValue, + attributeValue: serviceUUID.littleEndian.data + ) + let response = try await send(request, response: ATTFindByTypeResponse.self) + try await findByTypeResponse(response, operation: operation) - operation.start = lastEnd + 1 + } else { - // need to continue scanning - if lastEnd < operation.end { - - let pdu = ATTFindByTypeRequest(startHandle: operation.start, - endHandle: operation.end, - attributeType: operation.type.rawValue, - attributeValue: serviceUUID.littleEndian.data) - - send(pdu) { [unowned self] in self.findByTypeResponse($0, operation: operation) } - - } else { - - operation.success() - } + operation.success() } } @@ -1105,17 +1148,14 @@ public final class GATTClient { } private func notification(_ notification: ATTHandleValueNotification) { - notifications[notification.handle]?(notification.value) } - private func indication(_ indication: ATTHandleValueIndication) { - + private func indication(_ indication: ATTHandleValueIndication) async { let confirmation = ATTHandleValueConfirmation() - // send acknowledgement - send(confirmation) - + do { try await send(confirmation) } + catch { log?("Unable to send indication confirmation. \(error)") } indications[indication.handle]?(indication.value) } } @@ -1125,9 +1165,7 @@ public final class GATTClient { public extension GATTClient { typealias Error = GATTClientError - - typealias Response = GATTClientResponse - + typealias Notification = (Data) -> () } @@ -1207,7 +1245,7 @@ extension GATTClientError: CustomNSError { #endif -public typealias GATTClientResponse = Result +internal typealias GATTClientResponse = Result public extension GATTClient { @@ -1278,19 +1316,14 @@ fileprivate final class DiscoveryOperation { @inline(__always) func success() { - completion(.success(foundData)) } @inline(__always) func failure(_ responseError: ATTErrorResponse) { - if responseError.error == .attributeNotFound { - success() - } else { - completion(.failure(GATTClientError.errorResponse(responseError))) } } @@ -1421,20 +1454,17 @@ private extension GATTClient { self.completion = completion } - @inline(__always) func success(_ attributes: [ATTReadByTypeResponse.AttributeData]) { var data = [UInt16: Data](minimumCapacity: attributes.count) for attribute in attributes { - data[attribute.handle] = Data(attribute.value) } completion(.success(data)) } - @inline(__always) func failure(_ responseError: ATTErrorResponse) { completion(.failure(GATTClientError.errorResponse(responseError))) From ceed8a5db6dd1c6fb5197e7ffc6f64931640232b Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 13 Nov 2021 22:45:25 -0500 Subject: [PATCH 05/44] Working on `async` support for `GATTClient` --- Sources/BluetoothGATT/GATTClient.swift | 307 +++++++++++-------------- 1 file changed, 137 insertions(+), 170 deletions(-) diff --git a/Sources/BluetoothGATT/GATTClient.swift b/Sources/BluetoothGATT/GATTClient.swift index 951deef31..1b6f13062 100755 --- a/Sources/BluetoothGATT/GATTClient.swift +++ b/Sources/BluetoothGATT/GATTClient.swift @@ -373,8 +373,8 @@ public actor GATTClient { try await self.connection.read() } catch { - // not ATTError - assert(type(of: error) != ATTError.self) + // I/O error + assert(type(of: error) != ATTErrorResponse.self) continuation.resume(throwing: error) } } @@ -389,7 +389,7 @@ public actor GATTClient { guard let _ = await connection.send(request) else { fatalError("Could not add PDU to queue: \(request)") } - // write pending + // write pending PDU let didWrite = try await self.connection.write() assert(didWrite, "Expected queued write operation") } @@ -428,9 +428,11 @@ public actor GATTClient { let clientMTU = preferredMaximumTransmissionUnit let request = ATTMaximumTransmissionUnitRequest(clientMTU: clientMTU.rawValue) do { - let response = try await send(request, response: ATTMaximumTransmissionUnitResponse.self).get() + let response = try await send(request, response: ATTMaximumTransmissionUnitResponse.self) await exchangeMTUResponse(response) } catch { + // I/O error + assert(error as? ATTErrorResponse == nil) log?("Could not exchange MTU: \(error)") } } @@ -464,12 +466,7 @@ public actor GATTClient { ) // perform I/O let response = try await self.send(request, response: ATTFindByTypeResponse.self) - switch response { - case let .success(response): - try await self.findByTypeResponse(response, operation: operation) - case let .failure(error): - operation.failure(error) - } + try await self.findByTypeResponse(response, operation: operation) } else { let request = ATTReadByGroupTypeRequest( startHandle: start, @@ -477,12 +474,7 @@ public actor GATTClient { type: serviceType.uuid ) let response = try await self.send(request, response: ATTReadByGroupTypeResponse.self) - switch response { - case let .success(response): - try await self.readByGroupTypeResponse(response, operation: operation) - case let .failure(error): - operation.failure(error) - } + try await self.readByGroupTypeResponse(response, operation: operation) } } catch { assert(error as? ATTErrorResponse == nil) @@ -684,22 +676,22 @@ public actor GATTClient { // MARK: - Callbacks - private func exchangeMTUResponse( - _ response: ATTMaximumTransmissionUnitResponse - ) async { + private func exchangeMTUResponse(_ response: ATTResponse) async { assert(didExchangeMTU == false) - let clientMTU = preferredMaximumTransmissionUnit - let finalMTU = ATTMaximumTransmissionUnit( - server: response.serverMTU, - client: clientMTU.rawValue - ) - log?("MTU Exchange (\(clientMTU) -> \(finalMTU))") - await self.connection.setMaximumTransmissionUnit(finalMTU) - self.didExchangeMTU = true + switch response { + case let .failure(errorResponse): + log?("Could not exchange MTU: \(errorResponse)") + case let .success(pdu): + let clientMTU = preferredMaximumTransmissionUnit + let finalMTU = ATTMaximumTransmissionUnit(server: pdu.serverMTU, client: clientMTU.rawValue) + log?("MTU Exchange (\(clientMTU) -> \(finalMTU))") + await self.connection.setMaximumTransmissionUnit(finalMTU) + self.didExchangeMTU = true + } } private func readByGroupTypeResponse( - _ response: ATTReadByGroupTypeResponse, + _ response: ATTResponse, operation: DiscoveryOperation ) async throws { @@ -710,54 +702,59 @@ public actor GATTClient { // The Read By Group Type Request shall be called again with the Starting Handle set to one greater than the // last End Group Handle in the Read By Group Type Response. - // store PDU values - for serviceData in response.attributeData { + switch response { + case let .failure(errorResponse): + operation.failure(errorResponse) + case let .success(response): + // store PDU values + for serviceData in response.attributeData { + + guard let littleEndianServiceUUID = BluetoothUUID(data: serviceData.value) else { + operation.completion(.failure(Error.invalidResponse(response))) + return + } + + let serviceUUID = BluetoothUUID(littleEndian: littleEndianServiceUUID) + let service = Service( + uuid: serviceUUID, + isPrimary: operation.type == .primaryService, + handle: serviceData.attributeHandle, + end: serviceData.endGroupHandle + ) + + operation.foundData.append(service) + } + + // get more if possible + let lastEnd = response.attributeData.last?.endGroupHandle ?? 0x00 - guard let littleEndianServiceUUID = BluetoothUUID(data: serviceData.value) else { + // prevent infinite loop + guard lastEnd >= operation.start else { operation.completion(.failure(Error.invalidResponse(response))) return } - let serviceUUID = BluetoothUUID(littleEndian: littleEndianServiceUUID) - let service = Service( - uuid: serviceUUID, - isPrimary: operation.type == .primaryService, - handle: serviceData.attributeHandle, - end: serviceData.endGroupHandle - ) + guard lastEnd < .max // End of database + else { operation.success(); return } - operation.foundData.append(service) - } - - // get more if possible - let lastEnd = response.attributeData.last?.endGroupHandle ?? 0x00 - - // prevent infinite loop - guard lastEnd >= operation.start else { - operation.completion(.failure(Error.invalidResponse(response))) - return - } - - guard lastEnd < .max // End of database - else { operation.success(); return } - - operation.start = lastEnd + 1 - - if lastEnd < operation.end { - let request = ATTReadByGroupTypeRequest( - startHandle: operation.start, - endHandle: operation.end, - type: operation.type.uuid - ) - let response = try await send(request, response: ATTReadByGroupTypeResponse.self) - try await readByGroupTypeResponse(response, operation: operation) - } else { - operation.success() + operation.start = lastEnd + 1 + + if lastEnd < operation.end { + let request = ATTReadByGroupTypeRequest( + startHandle: operation.start, + endHandle: operation.end, + type: operation.type.uuid + ) + let response = try await send(request, response: ATTReadByGroupTypeResponse.self) + try await readByGroupTypeResponse(response, operation: operation) + } else { + operation.success() + } } } private func findByTypeResponse( - _ response: ATTFindByTypeResponse, + _ response: ATTResponse, operation: DiscoveryOperation ) async throws { @@ -767,48 +764,49 @@ public actor GATTClient { // is not 0xFFFF, the Find By Type Value Request may be called again with the Starting Handle set to one // greater than the last Attribute Handle range in the Find By Type Value Response. - guard let serviceUUID = operation.uuid - else { fatalError("Should have UUID specified") } - - // pre-allocate array - operation.foundData.reserveCapacity(operation.foundData.count + response.handles.count) - - // store PDU values - for serviceData in response.handles { + switch response { + case let .failure(errorResponse): + operation.failure(errorResponse) + case let .success(response): + guard let serviceUUID = operation.uuid + else { fatalError("Should have UUID specified") } - let service = Service( - uuid: serviceUUID, - isPrimary: operation.type == .primaryService, - handle: serviceData.foundAttribute, - end: serviceData.groupEnd - ) + // pre-allocate array + operation.foundData.reserveCapacity(operation.foundData.count + response.handles.count) + + // store PDU values + for serviceData in response.handles { + let service = Service( + uuid: serviceUUID, + isPrimary: operation.type == .primaryService, + handle: serviceData.foundAttribute, + end: serviceData.groupEnd + ) + operation.foundData.append(service) + } - operation.foundData.append(service) - } - - // get more if possible - let lastEnd = response.handles.last?.groupEnd ?? 0x00 - - guard lastEnd < .max // End of database - else { operation.success(); return } - - operation.start = lastEnd + 1 - - // need to continue scanning - if lastEnd < operation.end { + // get more if possible + let lastEnd = response.handles.last?.groupEnd ?? 0x00 - let request = ATTFindByTypeRequest( - startHandle: operation.start, - endHandle: operation.end, - attributeType: operation.type.rawValue, - attributeValue: serviceUUID.littleEndian.data - ) - let response = try await send(request, response: ATTFindByTypeResponse.self) - try await findByTypeResponse(response, operation: operation) + guard lastEnd < .max // End of database + else { operation.success(); return } - } else { + operation.start = lastEnd + 1 - operation.success() + // need to continue scanning + if lastEnd < operation.end { + + let request = ATTFindByTypeRequest( + startHandle: operation.start, + endHandle: operation.end, + attributeType: operation.type.rawValue, + attributeValue: serviceUUID.littleEndian.data + ) + let response = try await send(request, response: ATTFindByTypeResponse.self) + try await findByTypeResponse(response, operation: operation) + } else { + operation.success() + } } } @@ -830,11 +828,8 @@ public actor GATTClient { */ switch response { - case let .failure(errorResponse): - operation.failure(errorResponse) - case let .success(pdu): // pre-allocate array @@ -882,11 +877,11 @@ public actor GATTClient { } } - private func readByTypeResponse(_ response: ATTResponse, - operation: DiscoveryOperation) { - + private func readByTypeResponse( + _ response: ATTResponse, + operation: DiscoveryOperation + ) async throws { typealias DeclarationAttribute = GATTDatabase.CharacteristicDeclarationAttribute - typealias Attribute = GATTDatabase.Attribute // Read By Type Response returns a list of Attribute Handle and Attribute Value pairs corresponding to the @@ -896,11 +891,8 @@ public actor GATTClient { // Attribute Handle in the Read By Type Response. switch response { - case let .failure(errorResponse): - operation.failure(errorResponse) - case let .success(pdu): // pre-allocate array @@ -945,15 +937,14 @@ public actor GATTClient { // need to continue discovery if lastEnd != 0, operation.start < operation.end { - - let pdu = ATTReadByTypeRequest(startHandle: operation.start, - endHandle: operation.end, - attributeType: operation.type.uuid) - - send(pdu) { [unowned self] in self.readByTypeResponse($0, operation: operation) } - + let request = ATTReadByTypeRequest( + startHandle: operation.start, + endHandle: operation.end, + attributeType: operation.type.uuid + ) + let response = try await send(request, response: ATTReadByTypeResponse.self) + try await readByTypeResponse(response, operation: operation) } else { - // end of service operation.success() } @@ -961,29 +952,25 @@ public actor GATTClient { } /// Read Characteristic (or Descriptor) Value Response - private func readResponse(_ response: ATTResponse, operation: ReadOperation) { + private func readResponse( + _ response: ATTResponse, + operation: ReadOperation + ) async { // The Read Response only contains a Characteristic Value that is less than or equal to (ATT_MTU – 1) octets in length. // If the Characteristic Value is greater than (ATT_MTU – 1) octets in length, the Read Long Characteristic Value procedure // may be used if the rest of the Characteristic Value is required. switch response { - case let .failure(error): - operation.failure(error) - case let .success(pdu): - operation.data = pdu.attributeValue - // short value - if pdu.attributeValue.count < (Int(maximumTransmissionUnit.rawValue) - 1) { - + let expectedLength = await Int(maximumTransmissionUnit.rawValue) - 1 + if pdu.attributeValue.count < expectedLength { operation.success() - } else { - // read blob readLongAttributeValue(operation) } @@ -991,27 +978,22 @@ public actor GATTClient { } /// Read Blob Response - private func readBlobResponse(_ response: ATTResponse, operation: ReadOperation) { + private func readBlobResponse( + _ response: ATTResponse, + operation: ReadOperation + ) async { // For each Read Blob Request a Read Blob Response is received with a portion of the Characteristic Value contained in the Part Attribute Value parameter. - switch response { - case let .failure(error): - operation.failure(error) - case let .success(pdu): - operation.data += pdu.partAttributeValue - // short value - if pdu.partAttributeValue.count < (Int(maximumTransmissionUnit.rawValue) - 1) { - + let expectedLength = await Int(maximumTransmissionUnit.rawValue) - 1 + if pdu.partAttributeValue.count < expectedLength { operation.success() - } else { - // read blob readLongAttributeValue(operation) } @@ -1019,15 +1001,10 @@ public actor GATTClient { } private func readMultipleResponse(_ response: ATTResponse, operation: ReadMultipleOperation) { - switch response { - case let .failure(error): - operation.failure(error) - case let .success(pdu): - operation.success(pdu.values) } } @@ -1038,13 +1015,9 @@ public actor GATTClient { // contained in the handle range provided. switch response { - case let .failure(error): - operation.failure(error) - case let .success(pdu): - operation.success(pdu.attributeData) } } @@ -1071,23 +1044,18 @@ public actor GATTClient { An Error Response shall be sent by the server in response to the Prepare Write Request if insufficient authentication, insufficient authorization, insufficient encryption key size is used by the client, or if a write operation is not permitted on the Characteristic Value. The Error Code parameter is set as specified in the Attribute Protocol. If the Attribute Value that is written is the wrong size, or has an invalid value as defined by the profile, then the write shall not succeed and an Error Response shall be sent with the Error Code set to Application Error by the server. */ - private func prepareWriteResponse(_ response: ATTResponse, operation: WriteOperation) { + private func prepareWriteResponse(_ response: ATTResponse, operation: WriteOperation) async throws { @inline(__always) func complete(_ completion: (WriteOperation) -> ()) { - inLongWrite = false completion(operation) } switch response { - case let .failure(error): - complete { $0.failure(error) } - case let .success(pdu): - operation.receivedData += pdu.partValue // verify data sent @@ -1104,18 +1072,21 @@ public actor GATTClient { if offset < operation.data.count { // write next part - let maxLength = Int(maximumTransmissionUnit.rawValue) - 5 + let maxLength = await Int(maximumTransmissionUnit.rawValue) - 5 let endIndex = min(offset + maxLength, operation.data.count) let attributeValuePart = operation.data.subdataNoCopy(in: offset ..< endIndex) - let pdu = ATTPrepareWriteRequest(handle: operation.lastRequest.handle, - offset: UInt16(offset), - partValue: attributeValuePart) + let request = ATTPrepareWriteRequest( + handle: operation.lastRequest.handle, + offset: UInt16(offset), + partValue: attributeValuePart + ) - operation.lastRequest = pdu + operation.lastRequest = request operation.sentData += attributeValuePart - send(pdu) { [unowned self] in self.prepareWriteResponse($0, operation: operation) } + let response = try await send(request, response: ATTPrepareWriteResponse.self) + try await self.prepareWriteResponse(response, operation: operation) } else { @@ -1123,18 +1094,16 @@ public actor GATTClient { assert(operation.receivedData == operation.sentData) // all data sent - let pdu = ATTExecuteWriteRequest.write - - send(pdu) { [unowned self] in self.executeWriteResponse($0, operation: operation) } + let request = ATTExecuteWriteRequest.write + let response = try await send(request, response: ATTExecuteWriteResponse.self) + self.executeWriteResponse(response, operation: operation) } } } private func executeWriteResponse(_ response: ATTResponse, operation: WriteOperation) { - @inline(__always) func complete(_ completion: (WriteOperation) -> ()) { - inLongWrite = false completion(operation) } @@ -1430,13 +1399,11 @@ private extension GATTClient { @inline(__always) func success(_ values: Data) { - completion(.success(values)) } @inline(__always) func failure(_ responseError: ATTErrorResponse) { - completion(.failure(GATTClientError.errorResponse(responseError))) } } From b4dd98145db86312ad64d23183c4a3585f49be09 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 13 Nov 2021 23:15:30 -0500 Subject: [PATCH 06/44] Working on `async` support for `GATTClient` --- Sources/BluetoothGATT/GATTClient.swift | 256 +++++++++++++------------ 1 file changed, 138 insertions(+), 118 deletions(-) diff --git a/Sources/BluetoothGATT/GATTClient.swift b/Sources/BluetoothGATT/GATTClient.swift index 1b6f13062..de841e9a5 100755 --- a/Sources/BluetoothGATT/GATTClient.swift +++ b/Sources/BluetoothGATT/GATTClient.swift @@ -154,9 +154,8 @@ public actor GATTClient { /// /// ![Image](https://github.com/PureSwift/Bluetooth/raw/master/Assets/ReadCharacteristicValue.png) public func readCharacteristic(_ characteristic: Characteristic) async throws -> Data { - // read value and try to read blob if too big - readAttributeValue(characteristic.handle.value, completion: completion) + return try await readAttributeValue(characteristic.handle.value) } /// Read Using Characteristic UUID @@ -167,26 +166,25 @@ public actor GATTClient { /// ![Image](https://github.com/PureSwift/Bluetooth/raw/master/Assets/ReadUsingCharacteristicUUID.png) public func readCharacteristics( using uuid: BluetoothUUID, - handleRange: (start: UInt16, end: UInt16) = (.min, .max), - completion: @escaping (GATTClientResponse<[UInt16: Data]>) -> () - ) { - + handleRange: (start: UInt16, end: UInt16) = (.min, .max) + ) async throws -> [UInt16: Data] { precondition(handleRange.start < handleRange.end) - - // The Attribute Protocol Read By Type Request is used to perform the sub-procedure. - // The Attribute Type is set to the known characteristic UUID and the Starting Handle and Ending Handle parameters - // shall be set to the range over which this read is to be performed. This is typically the handle range for the service in which the characteristic belongs. - - let pdu = ATTReadByTypeRequest( - startHandle: handleRange.start, - endHandle: handleRange.end, - attributeType: uuid - ) - - let operation = ReadUsingUUIDOperation(uuid: uuid, completion: completion) - - send(pdu) { [unowned self] in - self.readByTypeResponse($0, operation: operation) + return try await withCheckedThrowingContinuation() { continuation in + Task { + // The Attribute Protocol Read By Type Request is used to perform the sub-procedure. + // The Attribute Type is set to the known characteristic UUID and the Starting Handle and Ending Handle parameters + // shall be set to the range over which this read is to be performed. This is typically the handle range for the service in which the characteristic belongs. + let request = ATTReadByTypeRequest( + startHandle: handleRange.start, + endHandle: handleRange.end, + attributeType: uuid + ) + let operation = ReadUsingUUIDOperation(uuid: uuid) { result in + continuation.resume(with: result) + } + let response = try await send(request, response: ATTReadByTypeResponse.self) + readByTypeResponse(response, operation: operation) + } } } @@ -484,32 +482,43 @@ public actor GATTClient { } } - private func discoverCharacteristics(uuid: BluetoothUUID? = nil, - service: Service, - completion: @escaping (GATTClientResponse<[Characteristic]>) -> ()) { - - let attributeType = GATTUUID.characteristic - - let operation = DiscoveryOperation(uuid: uuid, - start: service.handle, - end: service.end, - type: attributeType, - completion: completion) - - let pdu = ATTReadByTypeRequest(startHandle: service.handle, - endHandle: service.end, - attributeType: attributeType.uuid) - - send(pdu) { [unowned self] in self.readByTypeResponse($0, operation: operation) } + private func discoverCharacteristics( + uuid: BluetoothUUID? = nil, + service: Service + ) async throws -> [Characteristic] { + return try await withCheckedThrowingContinuation() { continuation in + Task { + do { + let attributeType = GATTUUID.characteristic + let operation = DiscoveryOperation( + uuid: uuid, + start: service.handle, + end: service.end, + type: attributeType + ) { result in + continuation.resume(with: result) + } + let request = ATTReadByTypeRequest( + startHandle: service.handle, + endHandle: service.end, + attributeType: attributeType.uuid + ) + let response = try await send(request, response: ATTReadByTypeResponse.self) + try await readByTypeResponse(response, operation: operation) + } + catch { + assert(error as? ATTErrorResponse == nil) + continuation.resume(throwing: error) + } + } + } } - private func discoverDescriptors(operation: DescriptorDiscoveryOperation) { - + private func discoverDescriptors(operation: DescriptorDiscoveryOperation) async throws { assert(operation.start <= operation.end, "Invalid range") - - let pdu = ATTFindInformationRequest(startHandle: operation.start, endHandle: operation.end) - - send(pdu) { [unowned self] in self.findInformationResponse($0, operation: operation) } + let request = ATTFindInformationRequest(startHandle: operation.start, endHandle: operation.end) + let response = try await send(request, response: ATTFindInformationResponse.self) + try await findInformationResponse(response, operation: operation) } /// Read Characteristic Value @@ -518,17 +527,21 @@ public actor GATTClient { /// the Characteristic Value Handle. /// /// ![Image](https://github.com/PureSwift/Bluetooth/raw/master/Assets/ReadCharacteristicValue.png) - private func readAttributeValue(_ handle: UInt16, completion: @escaping (GATTClientResponse) -> ()) { - + private func readAttributeValue(_ handle: UInt16) async throws -> Data { // The Attribute Protocol Read Request is used with the // Attribute Handle parameter set to the Characteristic Value Handle. // The Read Response returns the Characteristic Value in the Attribute Value parameter. - - // read value and try to read blob if too big - let request = ATTReadRequest(handle: handle) - let operation = ReadOperation(handle: handle, completion: completion) - - send(pdu) { [unowned self] in self.readResponse($0, operation: operation) } + return try await withCheckedThrowingContinuation() { continuation in + Task { + // read value and try to read blob if too big + let operation = ReadOperation(handle: handle) { result in + continuation.resume(with: result) + } + let request = ATTReadRequest(handle: handle) + let response = try await send(request, response: ATTReadResponse.self) + try await readResponse(response, operation: operation) + } + } } /// Read Long Characteristic Value @@ -539,7 +552,7 @@ public actor GATTClient { /// /// ![Image](https://github.com/PureSwift/Bluetooth/raw/master/Assets/ReadLongCharacteristicValues.png) @inline(__always) - private func readLongAttributeValue(_ operation: ReadOperation) { + private func readLongAttributeValue(_ operation: ReadOperation) async throws { // The Attribute Protocol Read Blob Request is used to perform this sub-procedure. // The Attribute Handle shall be set to the Characteristic Value Handle of the Characteristic Value to be read. @@ -548,68 +561,71 @@ public actor GATTClient { // The offset for subsequent Read Blob Requests is the next octet that has yet to be read. // The Read Blob Request is repeated until the Read Blob Response’s Part Attribute Value parameter is shorter than (ATT_MTU-1). - let pdu = ATTReadBlobRequest(handle: operation.handle, - offset: operation.offset) + let request = ATTReadBlobRequest( + handle: operation.handle, + offset: operation.offset + ) - send(pdu) { [unowned self] in self.readBlobResponse($0, operation: operation) } + let response = try await send(request, response: ATTReadBlobResponse.self) + try await readBlobResponse(response, operation: operation) } - private func writeAttribute(_ handle: UInt16, - data: Data, - reliableWrites: Bool, - completion: ((GATTClientResponse<()>) -> ())?) { + private func writeAttribute( + _ handle: UInt16, + data: Data, + reliableWrites: Bool, + completion: ((GATTClientResponse<()>) -> ())? + ) async throws { // short value - if data.count <= Int(maximumTransmissionUnit.rawValue) - 3 { // ATT_MTU - 3 - + let shortValueLength = await Int(maximumTransmissionUnit.rawValue) - 3 // ATT_MTU - 3 + if data.count <= shortValueLength { if let completion = completion { - - writeAttributeValue(handle, - data: data, - completion: completion) - + try await writeAttributeValue( + handle, + data: data, + completion: completion + ) } else { - - writeAttributeCommand(handle, - data: data) + try await writeAttributeCommand(handle, data: data) } - } else { - let completion = completion ?? { _ in } - - writeLongAttributeValue(handle, - data: data, - reliableWrites: reliableWrites, - completion: completion) + try await writeLongAttributeValue( + handle, + data: data, + reliableWrites: reliableWrites, + completion: completion + ) } } - private func writeAttributeCommand(_ attribute: UInt16, data: Data) { - - let data = Data(data.prefix(Int(maximumTransmissionUnit.rawValue) - 3)) - - let pdu = ATTWriteCommand(handle: attribute, value: data) - - send(pdu) + private func writeAttributeCommand(_ attribute: UInt16, data: Data) async throws { + let length = await Int(maximumTransmissionUnit.rawValue) - 3 + let data = Data(data.prefix(length)) + let command = ATTWriteCommand(handle: attribute, value: data) + try await send(command) } /// Write attribute request. - private func writeAttributeValue(_ attribute: UInt16, - data: Data, - completion: @escaping (GATTClientResponse<()>) -> ()) { - - let data = Data(data.prefix(Int(maximumTransmissionUnit.rawValue) - 3)) - - let pdu = ATTWriteRequest(handle: attribute, value: data) - - send(pdu) { [unowned self] in self.writeResponse($0, completion: completion) } + private func writeAttributeValue( + _ attribute: UInt16, + data: Data, + completion: @escaping (GATTClientResponse<()>) -> () + ) async throws { + let length = await Int(maximumTransmissionUnit.rawValue) - 3 + let data = Data(data.prefix(length)) + let request = ATTWriteRequest(handle: attribute, value: data) + let response = try await send(request, response: ATTWriteResponse.self) + writeResponse(response, completion: completion) } - private func writeLongAttributeValue(_ attribute: UInt16, - data: Data, - reliableWrites: Bool = false, - completion: @escaping (GATTClientResponse<()>) -> ()) { + private func writeLongAttributeValue( + _ attribute: UInt16, + data: Data, + reliableWrites: Bool = false, + completion: @escaping (GATTClientResponse<()>) -> () + ) async throws { // The Attribute Protocol Prepare Write Request and Execute Write Request are used to perform this sub-procedure. // The Attribute Handle parameter shall be set to the Characteristic Value Handle of the Characteristic Value to be written. @@ -623,19 +639,25 @@ public actor GATTClient { guard inLongWrite == false else { completion(.failure(GATTClientError.inLongWrite)); return } - let firstValuePart = Data(data.prefix(Int(maximumTransmissionUnit.rawValue) - 5)) + let partLength = await Int(maximumTransmissionUnit.rawValue) - 5 + let firstValuePart = Data(data.prefix(partLength)) - let pdu = ATTPrepareWriteRequest(handle: attribute, - offset: 0x00, - partValue: firstValuePart) + let request = ATTPrepareWriteRequest( + handle: attribute, + offset: 0x00, + partValue: firstValuePart + ) - let operation = WriteOperation(handle: attribute, - data: data, - reliableWrites: reliableWrites, - lastRequest: pdu, - completion: completion) + let operation = WriteOperation( + handle: attribute, + data: data, + reliableWrites: reliableWrites, + lastRequest: request, + completion: completion + ) - send(pdu) { [unowned self] in self.prepareWriteResponse($0, operation: operation) } + let response = try await send(request, response: ATTPrepareWriteResponse.self) + try await prepareWriteResponse(response, operation: operation) } /** @@ -810,8 +832,10 @@ public actor GATTClient { } } - private func findInformationResponse(_ response: ATTResponse, - operation: DescriptorDiscoveryOperation) { + private func findInformationResponse( + _ response: ATTResponse, + operation: DescriptorDiscoveryOperation + ) async throws { /** Two possible responses can be sent from the server for the Find Information Request: Find Information Response and Error Response. @@ -864,13 +888,9 @@ public actor GATTClient { // need to continue discovery if lastHandle != 0, start < operation.end { - operation.start = start - - discoverDescriptors(operation: operation) - + try await discoverDescriptors(operation: operation) } else { - // end of service operation.success() } @@ -955,7 +975,7 @@ public actor GATTClient { private func readResponse( _ response: ATTResponse, operation: ReadOperation - ) async { + ) async throws { // The Read Response only contains a Characteristic Value that is less than or equal to (ATT_MTU – 1) octets in length. // If the Characteristic Value is greater than (ATT_MTU – 1) octets in length, the Read Long Characteristic Value procedure @@ -972,7 +992,7 @@ public actor GATTClient { operation.success() } else { // read blob - readLongAttributeValue(operation) + try await readLongAttributeValue(operation) } } } @@ -981,7 +1001,7 @@ public actor GATTClient { private func readBlobResponse( _ response: ATTResponse, operation: ReadOperation - ) async { + ) async throws { // For each Read Blob Request a Read Blob Response is received with a portion of the Characteristic Value contained in the Part Attribute Value parameter. switch response { @@ -995,7 +1015,7 @@ public actor GATTClient { operation.success() } else { // read blob - readLongAttributeValue(operation) + try await readLongAttributeValue(operation) } } } From 72137f1164ae7ecd91af32731f1630abd7bdfc0b Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 14 Nov 2021 00:06:36 -0500 Subject: [PATCH 07/44] Working on `async` support for `GATTClient` --- Sources/BluetoothGATT/GATTClient.swift | 108 ++++++++++++++----------- 1 file changed, 63 insertions(+), 45 deletions(-) diff --git a/Sources/BluetoothGATT/GATTClient.swift b/Sources/BluetoothGATT/GATTClient.swift index de841e9a5..008be41e7 100755 --- a/Sources/BluetoothGATT/GATTClient.swift +++ b/Sources/BluetoothGATT/GATTClient.swift @@ -174,16 +174,21 @@ public actor GATTClient { // The Attribute Protocol Read By Type Request is used to perform the sub-procedure. // The Attribute Type is set to the known characteristic UUID and the Starting Handle and Ending Handle parameters // shall be set to the range over which this read is to be performed. This is typically the handle range for the service in which the characteristic belongs. - let request = ATTReadByTypeRequest( - startHandle: handleRange.start, - endHandle: handleRange.end, - attributeType: uuid - ) - let operation = ReadUsingUUIDOperation(uuid: uuid) { result in - continuation.resume(with: result) + do { + let request = ATTReadByTypeRequest( + startHandle: handleRange.start, + endHandle: handleRange.end, + attributeType: uuid + ) + let operation = ReadUsingUUIDOperation(uuid: uuid) { result in + continuation.resume(with: result) + } + let response = try await send(request, response: ATTReadByTypeResponse.self) + readByTypeResponse(response, operation: operation) + } catch { + assert(error as? ATTErrorResponse == nil) + continuation.resume(throwing: error) } - let response = try await send(request, response: ATTReadByTypeResponse.self) - readByTypeResponse(response, operation: operation) } } } @@ -197,20 +202,30 @@ public actor GATTClient { /// /// - Note: A client should not use this request for attributes when the Set Of Values parameter could be `(ATT_MTU–1)` /// as it will not be possible to determine if the last attribute value is complete, or if it overflowed. - public func readCharacteristics(_ characteristics: [Characteristic], completion: @escaping (GATTClientResponse) -> ()) { + public func readCharacteristics( + _ characteristics: [Characteristic] + ) async throws -> Data { // The Attribute Protocol Read Multiple Request is used with the Set Of Handles parameter set to the Characteristic Value Handles. // The Read Multiple Response returns the Characteristic Values in the Set Of Values parameter. - let handles = characteristics.map { $0.handle.value } - - guard let pdu = ATTReadMultipleRequest(handles: handles) - else { fatalError("Must provide at least 2 characteristics") } - - let operation = ReadMultipleOperation(characteristics: characteristics, completion: completion) - - send(pdu) { [unowned self] in - self.readMultipleResponse($0, operation: operation) + assert(handles.count > 1) + return try await withCheckedThrowingContinuation() { continuation in + Task { + do { + guard let request = ATTReadMultipleRequest(handles: handles) + else { fatalError("Must provide at least 2 characteristics") } + let operation = ReadMultipleOperation(characteristics: characteristics) { result in + continuation.resume(with: result) + } + let response = try await send(request, response: ATTReadMultipleResponse.self) + readMultipleResponse(response, operation: operation) + } + catch { + assert(error as? ATTErrorResponse == nil) + continuation.resume(throwing: error) + } + } } } @@ -219,15 +234,17 @@ public actor GATTClient { Uses the appropriate procecedure to write the characteristic value. */ - public func writeCharacteristic(_ characteristic: Characteristic, - data: Data, - reliableWrites: Bool = true, - completion: ((GATTClientResponse<()>) -> ())?) { - - writeAttribute(characteristic.handle.value, - data: data, - reliableWrites: reliableWrites, - completion: completion) + public func writeCharacteristic( + _ characteristic: Characteristic, + data: Data, + reliableWrites: Bool = true + ) async throws { + try await writeAttribute( + characteristic.handle.value, + data: data, + reliableWrites: reliableWrites, + completion: completion + ) } /** @@ -237,23 +254,20 @@ public actor GATTClient { ![Image](https://github.com/PureSwift/Bluetooth/raw/master/Assets/DiscoverAllCharacteristicDescriptors.png) */ - public func discoverDescriptors(for characteristic: Characteristic, - service: (declaration: Service, characteristics: [Characteristic]), - completion: @escaping (GATTClientResponse<[Descriptor]>) -> ()) { + public func discoverDescriptors( + for characteristic: Characteristic, + service: (declaration: Service, characteristics: [Characteristic]) + ) async throws -> [Descriptor] { /** The Attribute Protocol Find Information Request shall be used with the Starting Handle set to the handle of the specified characteristic value + 1 and the Ending Handle set to the ending handle of the specified characteristic. */ - let start = characteristic.handle.value + 1 - let end = endHandle(for: characteristic, service: service) - let operation = DescriptorDiscoveryOperation(start: start, end: end, completion: completion) - - discoverDescriptors(operation: operation) + try await discoverDescriptors(operation: operation) } /// Read Characteristic Descriptor @@ -517,8 +531,18 @@ public actor GATTClient { private func discoverDescriptors(operation: DescriptorDiscoveryOperation) async throws { assert(operation.start <= operation.end, "Invalid range") let request = ATTFindInformationRequest(startHandle: operation.start, endHandle: operation.end) - let response = try await send(request, response: ATTFindInformationResponse.self) - try await findInformationResponse(response, operation: operation) + return try await withCheckedThrowingContinuation() { continuation in + Task { + do { + let response = try await send(request, response: ATTFindInformationResponse.self) + try await findInformationResponse(response, operation: operation) + } + catch { + assert(error as? ATTErrorResponse == nil) + continuation.resume(throwing: error) + } + } + } } /// Read Characteristic Value @@ -1323,7 +1347,6 @@ private extension GATTClient { final class DescriptorDiscoveryOperation { var start: UInt16 { - didSet { assert(start <= end, "Start Handle should always be less than or equal to End handle") } } @@ -1344,20 +1367,15 @@ private extension GATTClient { @inline(__always) func success() { - completion(.success(foundDescriptors)) } @inline(__always) func failure(_ responseError: ATTErrorResponse) { - if responseError.error == .attributeNotFound { - success() - } else { - - completion(.failure(GATTClientError.errorResponse(responseError))) + completion(.failure(.errorResponse(responseError))) } } } From 5fde909982a222486f01a2a9a55a228da4af2ac6 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 14 Nov 2021 00:57:35 -0500 Subject: [PATCH 08/44] Fixed `GATTClient.discoverServices()` --- Sources/BluetoothGATT/GATTClient.swift | 178 +++++++++++-------------- 1 file changed, 78 insertions(+), 100 deletions(-) diff --git a/Sources/BluetoothGATT/GATTClient.swift b/Sources/BluetoothGATT/GATTClient.swift index 008be41e7..ba4930488 100755 --- a/Sources/BluetoothGATT/GATTClient.swift +++ b/Sources/BluetoothGATT/GATTClient.swift @@ -70,8 +70,7 @@ public actor GATTClient { /// /// - Parameter completion: The completion closure. public func discoverAllPrimaryServices() async throws -> [Service] { - - /// The Attribute Protocol Read By Group Type Request shall be used with + /// The Attribute Protocol Read By Group Type Request shall be used with /// the Attribute Type parameter set to the UUID for «Primary Service». /// The Starting Handle shall be set to 0x0001 and the Ending Handle shall be set to 0xFFFF. try await discoverServices( @@ -93,7 +92,6 @@ public actor GATTClient { public func discoverPrimaryServices( by uuid: BluetoothUUID ) async throws -> [Service]{ - // The Attribute Protocol Find By Type Value Request shall be used with the Attribute Type // parameter set to the UUID for «Primary Service» and the Attribute Value set to the 16-bit // Bluetooth UUID or 128-bit UUID for the specific primary service. @@ -116,12 +114,10 @@ public actor GATTClient { /// - Parameter service: The service. /// - Parameter completion: The completion closure. public func discoverAllCharacteristics(of service: Service) async throws -> [Characteristic] { - // The Attribute Protocol Read By Type Request shall be used with the Attribute Type // parameter set to the UUID for «Characteristic» The Starting Handle shall be set to // starting handle of the specified service and the Ending Handle shall be set to the // ending handle of the specified service. - return try await discoverCharacteristics(service: service) } @@ -266,8 +262,14 @@ public actor GATTClient { */ let start = characteristic.handle.value + 1 let end = endHandle(for: characteristic, service: service) - let operation = DescriptorDiscoveryOperation(start: start, end: end, completion: completion) - try await discoverDescriptors(operation: operation) + return try await withCheckedThrowingContinuation() { + Task { + do { + let operation = DescriptorDiscoveryOperation(start: start, end: end, completion: completion) + try await discoverDescriptors(operation: operation) + } + } + } } /// Read Characteristic Descriptor @@ -276,8 +278,7 @@ public actor GATTClient { /// the characteristic descriptor declaration’s Attribute handle. /// /// ![Image](https://github.com/PureSwift/Bluetooth/raw/master/Assets/ReadCharacteristicValue.png) - public func readDescriptor(_ descriptor: Descriptor, - completion: @escaping (GATTClientResponse) -> ()) { + public func readDescriptor(_ descriptor: Descriptor) async throws -> Data { /** The Attribute Protocol Read Request is used for this sub-procedure. The Read Request is used with the Attribute Handle parameter set to the characteristic descriptor handle. The Read Response returns the characteristic descriptor value in the Attribute Value parameter. @@ -286,7 +287,7 @@ public actor GATTClient { */ // read value and try to read blob if too big - readAttributeValue(descriptor.handle, completion: completion) + return try await readAttributeValue(descriptor.handle) } /** @@ -455,45 +456,33 @@ public actor GATTClient { end: UInt16 = 0xffff, primary: Bool = true ) async throws -> [Service] { - return try await withCheckedThrowingContinuation() { continuation in - Task { [weak self] in - guard let self = self else { return } - let serviceType = GATTUUID(primaryService: primary) - let operation = DiscoveryOperation( - uuid: uuid, - start: start, - end: end, - type: serviceType - ) { result in - continuation.resume(with: result) - } - - do { - if let uuid = uuid { - let request = ATTFindByTypeRequest( - startHandle: start, - endHandle: end, - attributeType: serviceType.rawValue, - attributeValue: uuid.littleEndian.data - ) - // perform I/O - let response = try await self.send(request, response: ATTFindByTypeResponse.self) - try await self.findByTypeResponse(response, operation: operation) - } else { - let request = ATTReadByGroupTypeRequest( - startHandle: start, - endHandle: end, - type: serviceType.uuid - ) - let response = try await self.send(request, response: ATTReadByGroupTypeResponse.self) - try await self.readByGroupTypeResponse(response, operation: operation) - } - } catch { - assert(error as? ATTErrorResponse == nil) - continuation.resume(throwing: error) - } - } + let serviceType = GATTUUID(primaryService: primary) + var operation = GATTClientDiscoveryOperation( + uuid: uuid, + start: start, + end: end, + type: serviceType + ) + if let uuid = uuid { + let request = ATTFindByTypeRequest( + startHandle: start, + endHandle: end, + attributeType: serviceType.rawValue, + attributeValue: uuid.littleEndian.data + ) + // perform I/O + let response = try await send(request, response: ATTFindByTypeResponse.self) + try await findByTypeResponse(response, operation: &operation) + } else { + let request = ATTReadByGroupTypeRequest( + startHandle: start, + endHandle: end, + type: serviceType.uuid + ) + let response = try await send(request, response: ATTReadByGroupTypeResponse.self) + try await readByGroupTypeResponse(response, operation: &operation) } + return operation.foundData } private func discoverCharacteristics( @@ -531,18 +520,8 @@ public actor GATTClient { private func discoverDescriptors(operation: DescriptorDiscoveryOperation) async throws { assert(operation.start <= operation.end, "Invalid range") let request = ATTFindInformationRequest(startHandle: operation.start, endHandle: operation.end) - return try await withCheckedThrowingContinuation() { continuation in - Task { - do { - let response = try await send(request, response: ATTFindInformationResponse.self) - try await findInformationResponse(response, operation: operation) - } - catch { - assert(error as? ATTErrorResponse == nil) - continuation.resume(throwing: error) - } - } - } + let response = try await send(request, response: ATTFindInformationResponse.self) + try await findInformationResponse(response, operation: operation) } /// Read Characteristic Value @@ -597,29 +576,21 @@ public actor GATTClient { private func writeAttribute( _ handle: UInt16, data: Data, - reliableWrites: Bool, - completion: ((GATTClientResponse<()>) -> ())? + withResponse: Bool ) async throws { - // short value let shortValueLength = await Int(maximumTransmissionUnit.rawValue) - 3 // ATT_MTU - 3 if data.count <= shortValueLength { - if let completion = completion { - try await writeAttributeValue( - handle, - data: data, - completion: completion - ) + if withResponse { + try await writeAttributeValue(handle, data: data) } else { try await writeAttributeCommand(handle, data: data) } } else { - let completion = completion ?? { _ in } try await writeLongAttributeValue( handle, data: data, - reliableWrites: reliableWrites, - completion: completion + reliableWrites: reliableWrites ) } } @@ -634,21 +605,19 @@ public actor GATTClient { /// Write attribute request. private func writeAttributeValue( _ attribute: UInt16, - data: Data, - completion: @escaping (GATTClientResponse<()>) -> () + data: Data ) async throws { let length = await Int(maximumTransmissionUnit.rawValue) - 3 let data = Data(data.prefix(length)) let request = ATTWriteRequest(handle: attribute, value: data) let response = try await send(request, response: ATTWriteResponse.self) - writeResponse(response, completion: completion) + writeResponse(response) } private func writeLongAttributeValue( _ attribute: UInt16, data: Data, - reliableWrites: Bool = false, - completion: @escaping (GATTClientResponse<()>) -> () + reliableWrites: Bool = false ) async throws { // The Attribute Protocol Prepare Write Request and Execute Write Request are used to perform this sub-procedure. @@ -738,7 +707,7 @@ public actor GATTClient { private func readByGroupTypeResponse( _ response: ATTResponse, - operation: DiscoveryOperation + operation: inout GATTClientDiscoveryOperation ) async throws { // Read By Group Type Response returns a list of Attribute Handle, End Group Handle, and Attribute Value tuples @@ -750,16 +719,16 @@ public actor GATTClient { switch response { case let .failure(errorResponse): - operation.failure(errorResponse) + guard errorResponse.error != .attributeNotFound else { + return + } + throw Error.errorResponse(errorResponse) case let .success(response): // store PDU values for serviceData in response.attributeData { - guard let littleEndianServiceUUID = BluetoothUUID(data: serviceData.value) else { - operation.completion(.failure(Error.invalidResponse(response))) - return + throw Error.invalidResponse(response) } - let serviceUUID = BluetoothUUID(littleEndian: littleEndianServiceUUID) let service = Service( uuid: serviceUUID, @@ -767,7 +736,6 @@ public actor GATTClient { handle: serviceData.attributeHandle, end: serviceData.endGroupHandle ) - operation.foundData.append(service) } @@ -776,12 +744,11 @@ public actor GATTClient { // prevent infinite loop guard lastEnd >= operation.start else { - operation.completion(.failure(Error.invalidResponse(response))) - return + throw Error.invalidResponse(response) } guard lastEnd < .max // End of database - else { operation.success(); return } + else { return } operation.start = lastEnd + 1 @@ -792,27 +759,26 @@ public actor GATTClient { type: operation.type.uuid ) let response = try await send(request, response: ATTReadByGroupTypeResponse.self) - try await readByGroupTypeResponse(response, operation: operation) - } else { - operation.success() + try await readByGroupTypeResponse(response, operation: &operation) } } } private func findByTypeResponse( _ response: ATTResponse, - operation: DiscoveryOperation + operation: inout GATTClientDiscoveryOperation ) async throws { - - // Find By Type Value Response returns a list of Attribute Handle ranges. + // Find By Type Value Response returns a list of Attribute Handle ranges. // The Attribute Handle range is the starting handle and the ending handle of the service definition. // If the Attribute Handle range for the Service UUID being searched is returned and the End Found Handle // is not 0xFFFF, the Find By Type Value Request may be called again with the Starting Handle set to one // greater than the last Attribute Handle range in the Find By Type Value Response. - switch response { case let .failure(errorResponse): - operation.failure(errorResponse) + guard errorResponse.error != .attributeNotFound else { + return + } + throw GATTClientError.errorResponse(errorResponse) case let .success(response): guard let serviceUUID = operation.uuid else { fatalError("Should have UUID specified") } @@ -835,13 +801,12 @@ public actor GATTClient { let lastEnd = response.handles.last?.groupEnd ?? 0x00 guard lastEnd < .max // End of database - else { operation.success(); return } + else { return } operation.start = lastEnd + 1 // need to continue scanning if lastEnd < operation.end { - let request = ATTFindByTypeRequest( startHandle: operation.start, endHandle: operation.end, @@ -849,9 +814,7 @@ public actor GATTClient { attributeValue: serviceUUID.littleEndian.data ) let response = try await send(request, response: ATTFindByTypeResponse.self) - try await findByTypeResponse(response, operation: operation) - } else { - operation.success() + try await findByTypeResponse(response, operation: &operation) } } } @@ -1297,6 +1260,21 @@ public extension GATTClient { // MARK: - Private Supporting Types +internal struct GATTClientDiscoveryOperation { + + let uuid: BluetoothUUID? + + var start: UInt16 { + didSet { assert(start <= end, "Start Handle should always be less than or equal to End handle") } + } + + let end: UInt16 + + let type: GATTUUID + + var foundData = [T]() +} + fileprivate final class DiscoveryOperation { let uuid: BluetoothUUID? From 7dbcdba2800717628325b7c2e22af2a941eee28e Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 14 Nov 2021 01:40:42 -0500 Subject: [PATCH 09/44] Fixed `GATTClient.discoverCharacteristics()` --- Sources/BluetoothGATT/ATTConnection.swift | 3 - Sources/BluetoothGATT/GATTClient.swift | 309 ++++++---------------- 2 files changed, 85 insertions(+), 227 deletions(-) diff --git a/Sources/BluetoothGATT/ATTConnection.swift b/Sources/BluetoothGATT/ATTConnection.swift index b8e007811..110127dae 100755 --- a/Sources/BluetoothGATT/ATTConnection.swift +++ b/Sources/BluetoothGATT/ATTConnection.swift @@ -92,7 +92,6 @@ internal actor ATTConnection { // Act on the received PDU based on the opcode type switch opcode.type { - case .response: try await handle(response: recievedData, opcode: opcode) case .confirmation: @@ -102,7 +101,6 @@ internal actor ATTConnection { case .command, .notification, .indication: - // For all other opcodes notify the upper layer of the PDU and let them act on it. try await handle(notify: recievedData, opcode: opcode) } @@ -285,7 +283,6 @@ internal actor ATTConnection { // If no request is pending, then the response is unexpected. Disconnect the bearer. guard let sendOperation = self.pendingRequest else { - throw Error.unexpectedResponse(data) } diff --git a/Sources/BluetoothGATT/GATTClient.swift b/Sources/BluetoothGATT/GATTClient.swift index ba4930488..986a49cdf 100755 --- a/Sources/BluetoothGATT/GATTClient.swift +++ b/Sources/BluetoothGATT/GATTClient.swift @@ -363,14 +363,13 @@ public actor GATTClient { _ request: Request, response: Response.Type ) async throws -> ATTResponse { - + assert(Response.attributeOpcode != .errorResponse) let log = self.log log?("Request: \(request)") - return try await withCheckedThrowingContinuation { [weak self] continuation in guard let self = self else { return } Task { - let responseType: ATTProtocolDataUnit.Type = Response.self + let responseType: ATTProtocolDataUnit.Type = response // callback if no I/O errors or disconnect let callback: (ATTProtocolDataUnit) -> () = { log?("Response: \($0)") @@ -384,6 +383,7 @@ public actor GATTClient { let didWrite = try await self.connection.write() assert(didWrite, "Expected queued write operation") try await self.connection.read() + // FIXME: Handle notifications } catch { // I/O error @@ -456,72 +456,60 @@ public actor GATTClient { end: UInt16 = 0xffff, primary: Bool = true ) async throws -> [Service] { - let serviceType = GATTUUID(primaryService: primary) - var operation = GATTClientDiscoveryOperation( + let attributeType = GATTUUID(primaryService: primary) + var operation = DiscoveryOperation( uuid: uuid, start: start, end: end, - type: serviceType + type: attributeType ) if let uuid = uuid { let request = ATTFindByTypeRequest( startHandle: start, endHandle: end, - attributeType: serviceType.rawValue, + attributeType: attributeType.rawValue, attributeValue: uuid.littleEndian.data ) - // perform I/O let response = try await send(request, response: ATTFindByTypeResponse.self) - try await findByTypeResponse(response, operation: &operation) + try await findByTypeResponse(response, &operation) } else { let request = ATTReadByGroupTypeRequest( startHandle: start, endHandle: end, - type: serviceType.uuid + type: attributeType.uuid ) let response = try await send(request, response: ATTReadByGroupTypeResponse.self) - try await readByGroupTypeResponse(response, operation: &operation) + try await readByGroupTypeResponse(response, &operation) } return operation.foundData } - private func discoverCharacteristics( + internal func discoverCharacteristics( uuid: BluetoothUUID? = nil, service: Service ) async throws -> [Characteristic] { - return try await withCheckedThrowingContinuation() { continuation in - Task { - do { - let attributeType = GATTUUID.characteristic - let operation = DiscoveryOperation( - uuid: uuid, - start: service.handle, - end: service.end, - type: attributeType - ) { result in - continuation.resume(with: result) - } - let request = ATTReadByTypeRequest( - startHandle: service.handle, - endHandle: service.end, - attributeType: attributeType.uuid - ) - let response = try await send(request, response: ATTReadByTypeResponse.self) - try await readByTypeResponse(response, operation: operation) - } - catch { - assert(error as? ATTErrorResponse == nil) - continuation.resume(throwing: error) - } - } - } + let attributeType = GATTUUID.characteristic + var operation = DiscoveryOperation( + uuid: uuid, + start: service.handle, + end: service.end, + type: attributeType + ) + let request = ATTReadByTypeRequest( + startHandle: service.handle, + endHandle: service.end, + attributeType: attributeType.uuid + ) + let response = try await send(request, response: ATTReadByTypeResponse.self) + try await readByTypeResponse(response, &operation) + return operation.foundData } - private func discoverDescriptors(operation: DescriptorDiscoveryOperation) async throws { + private func discoverDescriptors(_ operation: inout DiscoveryOperation) async throws { assert(operation.start <= operation.end, "Invalid range") let request = ATTFindInformationRequest(startHandle: operation.start, endHandle: operation.end) let response = try await send(request, response: ATTFindInformationResponse.self) - try await findInformationResponse(response, operation: operation) + try await findInformationResponse(response, &operation) } /// Read Characteristic Value @@ -707,7 +695,7 @@ public actor GATTClient { private func readByGroupTypeResponse( _ response: ATTResponse, - operation: inout GATTClientDiscoveryOperation + _ operation: inout DiscoveryOperation ) async throws { // Read By Group Type Response returns a list of Attribute Handle, End Group Handle, and Attribute Value tuples @@ -759,14 +747,14 @@ public actor GATTClient { type: operation.type.uuid ) let response = try await send(request, response: ATTReadByGroupTypeResponse.self) - try await readByGroupTypeResponse(response, operation: &operation) + try await readByGroupTypeResponse(response, &operation) } } } private func findByTypeResponse( _ response: ATTResponse, - operation: inout GATTClientDiscoveryOperation + _ operation: inout DiscoveryOperation ) async throws { // Find By Type Value Response returns a list of Attribute Handle ranges. // The Attribute Handle range is the starting handle and the ending handle of the service definition. @@ -814,14 +802,14 @@ public actor GATTClient { attributeValue: serviceUUID.littleEndian.data ) let response = try await send(request, response: ATTFindByTypeResponse.self) - try await findByTypeResponse(response, operation: &operation) + try await findByTypeResponse(response, &operation) } } } private func findInformationResponse( _ response: ATTResponse, - operation: DescriptorDiscoveryOperation + _ operation: inout DescriptorDiscoveryOperation ) async throws { /** @@ -840,7 +828,10 @@ public actor GATTClient { switch response { case let .failure(errorResponse): - operation.failure(errorResponse) + guard errorResponse.error != .attributeNotFound else { + return + } + throw GATTClientError.errorResponse(errorResponse) case let .success(pdu): // pre-allocate array @@ -849,13 +840,9 @@ public actor GATTClient { let foundData: [Descriptor] switch pdu.attributeData { - case let .bit16(values): - foundData = values.map { Descriptor(uuid: .bit16($0.uuid), handle: $0.handle) } - case let .bit128(values): - foundData = values.map { Descriptor(uuid: .bit128($0.uuid), handle: $0.handle) } } @@ -866,28 +853,26 @@ public actor GATTClient { // prevent infinite loop guard lastHandle >= operation.start - else { operation.completion(.failure(Error.invalidResponse(pdu))); return } + else { throw Error.invalidResponse(pdu) } guard lastHandle < .max // End of database - else { operation.success(); return } + else { return } let start = lastHandle + 1 // need to continue discovery if lastHandle != 0, start < operation.end { operation.start = start - try await discoverDescriptors(operation: operation) - } else { - // end of service - operation.success() + try await discoverDescriptors(&operation) } } } private func readByTypeResponse( _ response: ATTResponse, - operation: DiscoveryOperation + _ operation: inout DiscoveryOperation ) async throws { + typealias DeclarationAttribute = GATTDatabase.CharacteristicDeclarationAttribute typealias Attribute = GATTDatabase.Attribute @@ -899,7 +884,10 @@ public actor GATTClient { switch response { case let .failure(errorResponse): - operation.failure(errorResponse) + guard errorResponse.error != .attributeNotFound else { + return + } + throw GATTClientError.errorResponse(errorResponse) case let .success(pdu): // pre-allocate array @@ -910,13 +898,15 @@ public actor GATTClient { let handle = characteristicData.handle - let attribute = Attribute(handle: handle, - uuid: .characteristic, - value: Data(characteristicData.value), - permissions: [.read]) + let attribute = Attribute( + handle: handle, + uuid: .characteristic, + value: Data(characteristicData.value), + permissions: [.read] + ) guard let declaration = DeclarationAttribute(attribute: attribute) - else { operation.completion(.failure(Error.invalidResponse(pdu))); return } + else { throw Error.invalidResponse(pdu) } let characteristic = Characteristic(uuid: declaration.uuid, properties: declaration.properties, @@ -927,9 +917,8 @@ public actor GATTClient { // if we specified a UUID to be searched, // then call completion if it matches if let operationUUID = operation.uuid { - guard characteristic.uuid != operationUUID - else { operation.success(); return } + else { return } } } @@ -938,7 +927,7 @@ public actor GATTClient { // prevent infinite loop guard lastEnd >= operation.start - else { operation.completion(.failure(Error.invalidResponse(pdu))); return } + else { throw Error.invalidResponse(pdu) } operation.start = lastEnd + 1 @@ -950,10 +939,7 @@ public actor GATTClient { attributeType: operation.type.uuid ) let response = try await send(request, response: ATTReadByTypeResponse.self) - try await readByTypeResponse(response, operation: operation) - } else { - // end of service - operation.success() + try await readByTypeResponse(response, &operation) } } } @@ -1258,71 +1244,12 @@ public extension GATTClient { } } -// MARK: - Private Supporting Types - -internal struct GATTClientDiscoveryOperation { - - let uuid: BluetoothUUID? - - var start: UInt16 { - didSet { assert(start <= end, "Start Handle should always be less than or equal to End handle") } - } - - let end: UInt16 - - let type: GATTUUID - - var foundData = [T]() -} -fileprivate final class DiscoveryOperation { +internal extension GATTClient { - let uuid: BluetoothUUID? - - var start: UInt16 { - - didSet { assert(start <= end, "Start Handle should always be less than or equal to End handle") } - } - - let end: UInt16 - - let type: GATTUUID - - var foundData = [T]() - - let completion: (GATTClientResponse<[T]>) -> () - - init(uuid: BluetoothUUID?, - start: UInt16, - end: UInt16, - type: GATTUUID, - completion: @escaping (GATTClientResponse<[T]>) -> ()) { + struct DiscoveryOperation { - self.uuid = uuid - self.start = start - self.end = end - self.type = type - self.completion = completion - } - - @inline(__always) - func success() { - completion(.success(foundData)) - } - - @inline(__always) - func failure(_ responseError: ATTErrorResponse) { - if responseError.error == .attributeNotFound { - success() - } else { - completion(.failure(GATTClientError.errorResponse(responseError))) - } - } -} - -private extension GATTClient { - - final class DescriptorDiscoveryOperation { + let uuid: BluetoothUUID? var start: UInt16 { didSet { assert(start <= end, "Start Handle should always be less than or equal to End handle") } @@ -1330,61 +1257,36 @@ private extension GATTClient { let end: UInt16 - var foundDescriptors = [Descriptor]() + let type: GATTUUID - let completion: (GATTClientResponse<[Descriptor]>) -> () + var foundData = [T]() + } + + struct DescriptorDiscoveryOperation { - init(start: UInt16, - end: UInt16, - completion: @escaping (GATTClientResponse<[Descriptor]>) -> ()) { - - self.start = start - self.end = end - self.completion = completion + var start: UInt16 { + didSet { assert(start <= end, "Start Handle should always be less than or equal to End handle") } } - @inline(__always) - func success() { - completion(.success(foundDescriptors)) - } + let end: UInt16 - @inline(__always) - func failure(_ responseError: ATTErrorResponse) { - if responseError.error == .attributeNotFound { - success() - } else { - completion(.failure(.errorResponse(responseError))) - } - } + var foundDescriptors = [GATTClient.Descriptor]() } - final class ReadOperation { + struct ReadOperation { let handle: UInt16 var data = Data() - - let completion: (GATTClientResponse) -> () - + var offset: UInt16 { - - @inline(__always) - get { return UInt16(data.count) } + return UInt16(data.count) } - init(handle: UInt16, - completion: @escaping (GATTClientResponse) -> ()) { - + init(handle: UInt16) { self.handle = handle - self.completion = completion } - - @inline(__always) - func success() { - - completion(.success(data)) - } - + /* @inline(__always) func failure(_ responseError: ATTErrorResponse) { @@ -1397,46 +1299,26 @@ private extension GATTClient { completion(.failure(GATTClientError.errorResponse(responseError))) } - } + }*/ } - final class ReadMultipleOperation { + struct ReadMultipleOperation { let characteristics: [Characteristic] - - let completion: (GATTClientResponse) -> () - - init(characteristics: [Characteristic], - completion: @escaping (GATTClientResponse) -> ()) { - + + init(characteristics: [Characteristic]) { self.characteristics = characteristics - self.completion = completion - } - - @inline(__always) - func success(_ values: Data) { - completion(.success(values)) - } - - @inline(__always) - func failure(_ responseError: ATTErrorResponse) { - completion(.failure(GATTClientError.errorResponse(responseError))) } } - final class ReadUsingUUIDOperation { + struct ReadUsingUUIDOperation { let uuid: BluetoothUUID - let completion: (GATTClientResponse<[UInt16: Data]>) -> () - - init(uuid: BluetoothUUID, - completion: @escaping (GATTClientResponse<[UInt16: Data]>) -> ()) { - + init(uuid: BluetoothUUID) { self.uuid = uuid - self.completion = completion } - + /* func success(_ attributes: [ATTReadByTypeResponse.AttributeData]) { var data = [UInt16: Data](minimumCapacity: attributes.count) @@ -1446,22 +1328,13 @@ private extension GATTClient { } completion(.success(data)) - } - - func failure(_ responseError: ATTErrorResponse) { - - completion(.failure(GATTClientError.errorResponse(responseError))) - } + }*/ } - final class WriteOperation { - - typealias Completion = (GATTClientResponse) -> () - + struct WriteOperation { + let handle: UInt16 - - let completion: Completion - + let reliableWrites: Bool let data: Data @@ -1475,28 +1348,16 @@ private extension GATTClient { init(handle: UInt16, data: Data, reliableWrites: Bool, - lastRequest: ATTPrepareWriteRequest, - completion: @escaping Completion) { + lastRequest: ATTPrepareWriteRequest) { precondition(data.isEmpty == false) self.handle = handle - self.completion = completion self.data = data self.reliableWrites = reliableWrites self.lastRequest = lastRequest self.sentData = lastRequest.partValue self.receivedData = Data() } - - @inline(__always) - func success() { - completion(.success(())) - } - - @inline(__always) - func failure(_ responseError: ATTErrorResponse) { - completion(.failure(GATTClientError.errorResponse(responseError))) - } } } From 8928e1be38ab347ba671f60fd54afe49b92ba947 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 14 Nov 2021 07:54:37 -0500 Subject: [PATCH 10/44] Working on `async` support for `GATTClient` --- Sources/BluetoothGATT/GATTClient.swift | 356 +++++++++---------------- 1 file changed, 126 insertions(+), 230 deletions(-) diff --git a/Sources/BluetoothGATT/GATTClient.swift b/Sources/BluetoothGATT/GATTClient.swift index 986a49cdf..3c16059b0 100755 --- a/Sources/BluetoothGATT/GATTClient.swift +++ b/Sources/BluetoothGATT/GATTClient.swift @@ -165,28 +165,22 @@ public actor GATTClient { handleRange: (start: UInt16, end: UInt16) = (.min, .max) ) async throws -> [UInt16: Data] { precondition(handleRange.start < handleRange.end) - return try await withCheckedThrowingContinuation() { continuation in - Task { - // The Attribute Protocol Read By Type Request is used to perform the sub-procedure. - // The Attribute Type is set to the known characteristic UUID and the Starting Handle and Ending Handle parameters - // shall be set to the range over which this read is to be performed. This is typically the handle range for the service in which the characteristic belongs. - do { - let request = ATTReadByTypeRequest( - startHandle: handleRange.start, - endHandle: handleRange.end, - attributeType: uuid - ) - let operation = ReadUsingUUIDOperation(uuid: uuid) { result in - continuation.resume(with: result) - } - let response = try await send(request, response: ATTReadByTypeResponse.self) - readByTypeResponse(response, operation: operation) - } catch { - assert(error as? ATTErrorResponse == nil) - continuation.resume(throwing: error) - } - } + // The Attribute Protocol Read By Type Request is used to perform the sub-procedure. + // The Attribute Type is set to the known characteristic UUID and the Starting Handle and Ending Handle parameters + // shall be set to the range over which this read is to be performed. This is typically the handle range for the service in which the characteristic belongs. + + let request = ATTReadByTypeRequest( + startHandle: handleRange.start, + endHandle: handleRange.end, + attributeType: uuid + ) + let response = try await send(request, response: ATTReadByTypeResponse.self).get() + // parse response + var attributeData = [UInt16: Data](minimumCapacity: response.attributeData.count) + for attribute in response.attributeData { + attributeData[attribute.handle] = attribute.value } + return attributeData } /// Read Multiple Characteristic Values @@ -206,23 +200,10 @@ public actor GATTClient { // The Read Multiple Response returns the Characteristic Values in the Set Of Values parameter. let handles = characteristics.map { $0.handle.value } assert(handles.count > 1) - return try await withCheckedThrowingContinuation() { continuation in - Task { - do { - guard let request = ATTReadMultipleRequest(handles: handles) - else { fatalError("Must provide at least 2 characteristics") } - let operation = ReadMultipleOperation(characteristics: characteristics) { result in - continuation.resume(with: result) - } - let response = try await send(request, response: ATTReadMultipleResponse.self) - readMultipleResponse(response, operation: operation) - } - catch { - assert(error as? ATTErrorResponse == nil) - continuation.resume(throwing: error) - } - } - } + guard let request = ATTReadMultipleRequest(handles: handles) + else { fatalError("Must provide at least 2 characteristics") } + let response = try await send(request, response: ATTReadMultipleResponse.self).get() + return response.values } /** @@ -233,13 +214,12 @@ public actor GATTClient { public func writeCharacteristic( _ characteristic: Characteristic, data: Data, - reliableWrites: Bool = true + withResponse: Bool = true ) async throws { try await writeAttribute( characteristic.handle.value, data: data, - reliableWrites: reliableWrites, - completion: completion + withResponse: withResponse ) } @@ -251,7 +231,7 @@ public actor GATTClient { ![Image](https://github.com/PureSwift/Bluetooth/raw/master/Assets/DiscoverAllCharacteristicDescriptors.png) */ public func discoverDescriptors( - for characteristic: Characteristic, + of characteristic: Characteristic, service: (declaration: Service, characteristics: [Characteristic]) ) async throws -> [Descriptor] { @@ -262,14 +242,12 @@ public actor GATTClient { */ let start = characteristic.handle.value + 1 let end = endHandle(for: characteristic, service: service) - return try await withCheckedThrowingContinuation() { - Task { - do { - let operation = DescriptorDiscoveryOperation(start: start, end: end, completion: completion) - try await discoverDescriptors(operation: operation) - } - } - } + var operation = DescriptorDiscoveryOperation( + start: start, + end: end + ) + try await discoverDescriptors(&operation) + return operation.foundDescriptors } /// Read Characteristic Descriptor @@ -295,15 +273,16 @@ public actor GATTClient { Uses the appropriate procecedure to write the characteristic descriptor value. */ - public func writeDescriptor(_ descriptor: Descriptor, - data: Data, - reliableWrites: Bool = true, - completion: ((GATTClientResponse<()>) -> ())?) { - - writeAttribute(descriptor.handle, - data: data, - reliableWrites: reliableWrites, - completion: completion) + public func writeDescriptor( + _ descriptor: Descriptor, + data: Data, + withResponse: Bool = true + ) async throws { + try await writeAttribute( + descriptor.handle, + data: data, + withResponse: withResponse + ) } /** @@ -314,40 +293,29 @@ public actor GATTClient { ![Image](https://github.com/PureSwift/Bluetooth/raw/master/Assets/Notifications.png) */ public func clientCharacteristicConfiguration( + _ characteristic: Characteristic, notification: Notification?, indication: Notification?, - for characteristic: Characteristic, - descriptors: [GATTClient.Descriptor] + descriptors: [Descriptor] ) async throws { guard let descriptor = descriptors.first(where: { $0.uuid == .clientCharacteristicConfiguration }) - else { completion(.failure(GATTClientError.clientCharacteristicConfigurationNotAllowed(characteristic))); return } + else { throw GATTClientError.clientCharacteristicConfigurationNotAllowed(characteristic) } var clientConfiguration = GATTClientCharacteristicConfiguration() - if notification != nil { clientConfiguration.configuration.insert(.notify) } - if indication != nil { clientConfiguration.configuration.insert(.indicate) } - writeDescriptor(descriptor, data: clientConfiguration.data) { [unowned self] (response) in - - switch response { - - case .failure: - break - - case .success: - - self.notifications[characteristic.handle.value] = notification - self.indications[characteristic.handle.value] = indication - } - - completion(response) - } + // write client configuration descriptor + try await writeDescriptor(descriptor, data: clientConfiguration.data) + + // store callbacks + self.notifications[characteristic.handle.value] = notification + self.indications[characteristic.handle.value] = indication } // MARK: - Private Methods @@ -407,7 +375,7 @@ public actor GATTClient { assert(didWrite, "Expected queued write operation") } - internal func endHandle( + private func endHandle( for characteristic: Characteristic, service: (declaration: Service, characteristics: [Characteristic]) ) -> UInt16 { @@ -436,7 +404,7 @@ public actor GATTClient { // MARK: Requests /// Exchange MTU (should only be called once if not using default MTU) - internal func exchangeMTU() async { + private func exchangeMTU() async { assert(didExchangeMTU == false) let clientMTU = preferredMaximumTransmissionUnit let request = ATTMaximumTransmissionUnitRequest(clientMTU: clientMTU.rawValue) @@ -450,7 +418,7 @@ public actor GATTClient { } } - internal func discoverServices( + private func discoverServices( uuid: BluetoothUUID? = nil, start: UInt16 = 0x0001, end: UInt16 = 0xffff, @@ -484,7 +452,7 @@ public actor GATTClient { return operation.foundData } - internal func discoverCharacteristics( + private func discoverCharacteristics( uuid: BluetoothUUID? = nil, service: Service ) async throws -> [Characteristic] { @@ -505,7 +473,7 @@ public actor GATTClient { return operation.foundData } - private func discoverDescriptors(_ operation: inout DiscoveryOperation) async throws { + private func discoverDescriptors(_ operation: inout DescriptorDiscoveryOperation) async throws { assert(operation.start <= operation.end, "Invalid range") let request = ATTFindInformationRequest(startHandle: operation.start, endHandle: operation.end) let response = try await send(request, response: ATTFindInformationResponse.self) @@ -522,17 +490,12 @@ public actor GATTClient { // The Attribute Protocol Read Request is used with the // Attribute Handle parameter set to the Characteristic Value Handle. // The Read Response returns the Characteristic Value in the Attribute Value parameter. - return try await withCheckedThrowingContinuation() { continuation in - Task { - // read value and try to read blob if too big - let operation = ReadOperation(handle: handle) { result in - continuation.resume(with: result) - } - let request = ATTReadRequest(handle: handle) - let response = try await send(request, response: ATTReadResponse.self) - try await readResponse(response, operation: operation) - } - } + // read value and try to read blob if too big + var operation = ReadOperation(handle: handle) + let request = ATTReadRequest(handle: handle) + let response = try await send(request, response: ATTReadResponse.self) + try await readResponse(response, &operation) + return operation.data } /// Read Long Characteristic Value @@ -543,7 +506,7 @@ public actor GATTClient { /// /// ![Image](https://github.com/PureSwift/Bluetooth/raw/master/Assets/ReadLongCharacteristicValues.png) @inline(__always) - private func readLongAttributeValue(_ operation: ReadOperation) async throws { + private func readLongAttributeValue(_ operation: inout ReadOperation) async throws { // The Attribute Protocol Read Blob Request is used to perform this sub-procedure. // The Attribute Handle shall be set to the Characteristic Value Handle of the Characteristic Value to be read. @@ -556,9 +519,8 @@ public actor GATTClient { handle: operation.handle, offset: operation.offset ) - let response = try await send(request, response: ATTReadBlobResponse.self) - try await readBlobResponse(response, operation: operation) + try await readBlobResponse(response, &operation) } private func writeAttribute( @@ -578,7 +540,7 @@ public actor GATTClient { try await writeLongAttributeValue( handle, data: data, - reliableWrites: reliableWrites + reliableWrites: withResponse ) } } @@ -599,7 +561,14 @@ public actor GATTClient { let data = Data(data.prefix(length)) let request = ATTWriteRequest(handle: attribute, value: data) let response = try await send(request, response: ATTWriteResponse.self) - writeResponse(response) + /** + Write Response + + A Write Response shall be sent by the server if the write of the Characteristic Value succeeded. + + An Error Response shall be sent by the server in response to the Write Request if insufficient authentication, insufficient authorization, insufficient encryption key size is used by the client, or if a write operation is not permitted on the Characteristic Value. The Error Code parameter is set as specified in the Attribute Protocol. If the Characteristic Value that is written is the wrong size, or has an invalid value as defined by the profile, then the value shall not be written and an Error Response shall be sent with the Error Code set to Application Error by the server. + */ + let _ = try response.get() } private func writeLongAttributeValue( @@ -618,7 +587,7 @@ public actor GATTClient { // after which an Executive Write Request is used to write the complete value. guard inLongWrite == false - else { completion(.failure(GATTClientError.inLongWrite)); return } + else { throw GATTClientError.inLongWrite } let partLength = await Int(maximumTransmissionUnit.rawValue) - 5 let firstValuePart = Data(data.prefix(partLength)) @@ -629,16 +598,15 @@ public actor GATTClient { partValue: firstValuePart ) - let operation = WriteOperation( + var operation = WriteOperation( handle: attribute, data: data, reliableWrites: reliableWrites, - lastRequest: request, - completion: completion + lastRequest: request ) let response = try await send(request, response: ATTPrepareWriteResponse.self) - try await prepareWriteResponse(response, operation: operation) + try await prepareWriteResponse(response, &operation) } /** @@ -947,7 +915,7 @@ public actor GATTClient { /// Read Characteristic (or Descriptor) Value Response private func readResponse( _ response: ATTResponse, - operation: ReadOperation + _ operation: inout ReadOperation ) async throws { // The Read Response only contains a Characteristic Value that is less than or equal to (ATT_MTU – 1) octets in length. @@ -955,80 +923,50 @@ public actor GATTClient { // may be used if the rest of the Characteristic Value is required. switch response { - case let .failure(error): - operation.failure(error) - case let .success(pdu): - operation.data = pdu.attributeValue + case let .failure(responseError): + guard responseError.error != .invalidOffset || operation.data.isEmpty else { + assert(responseError.error == .invalidOffset && operation.data.isEmpty == false) + return + } + throw GATTClientError.errorResponse(responseError) + case let .success(response): + operation.data = response.attributeValue // short value let expectedLength = await Int(maximumTransmissionUnit.rawValue) - 1 - if pdu.attributeValue.count < expectedLength { - operation.success() - } else { - // read blob - try await readLongAttributeValue(operation) + guard response.attributeValue.count >= expectedLength else { + assert(response.attributeValue.count < expectedLength) + return } + // read blob + try await readLongAttributeValue(&operation) } } /// Read Blob Response private func readBlobResponse( _ response: ATTResponse, - operation: ReadOperation + _ operation: inout ReadOperation ) async throws { // For each Read Blob Request a Read Blob Response is received with a portion of the Characteristic Value contained in the Part Attribute Value parameter. switch response { - case let .failure(error): - operation.failure(error) - case let .success(pdu): - operation.data += pdu.partAttributeValue - // short value + case let .failure(responseError): + guard responseError.error != .invalidOffset || operation.data.isEmpty else { + assert(responseError.error == .invalidOffset && operation.data.isEmpty == false) + return + } + throw GATTClientError.errorResponse(responseError) + case let .success(response): + operation.data += response.partAttributeValue + // keep on reading long value let expectedLength = await Int(maximumTransmissionUnit.rawValue) - 1 - if pdu.partAttributeValue.count < expectedLength { - operation.success() - } else { - // read blob - try await readLongAttributeValue(operation) + guard response.partAttributeValue.count >= expectedLength else { + // no more value + assert(response.partAttributeValue.count < expectedLength) + return } - } - } - - private func readMultipleResponse(_ response: ATTResponse, operation: ReadMultipleOperation) { - switch response { - case let .failure(error): - operation.failure(error) - case let .success(pdu): - operation.success(pdu.values) - } - } - - private func readByTypeResponse(_ response: ATTResponse, operation: ReadUsingUUIDOperation) { - - // Read By Type Response returns a list of Attribute Handle and Attribute Value pairs corresponding to the characteristics - // contained in the handle range provided. - - switch response { - case let .failure(error): - operation.failure(error) - case let .success(pdu): - operation.success(pdu.attributeData) - } - } - - /** - Write Response - - A Write Response shall be sent by the server if the write of the Characteristic Value succeeded. - - An Error Response shall be sent by the server in response to the Write Request if insufficient authentication, insufficient authorization, insufficient encryption key size is used by the client, or if a write operation is not permitted on the Characteristic Value. The Error Code parameter is set as specified in the Attribute Protocol. If the Characteristic Value that is written is the wrong size, or has an invalid value as defined by the profile, then the value shall not be written and an Error Response shall be sent with the Error Code set to Application Error by the server. - */ - private func writeResponse(_ response: ATTResponse, completion: (GATTClientResponse<()>) -> ()) { - - switch response { - case let .failure(error): - completion(.failure(.errorResponse(error))) - case .success: // PDU contains no data - completion(.success(())) + // read blob + try await readLongAttributeValue(&operation) } } @@ -1037,18 +975,17 @@ public actor GATTClient { An Error Response shall be sent by the server in response to the Prepare Write Request if insufficient authentication, insufficient authorization, insufficient encryption key size is used by the client, or if a write operation is not permitted on the Characteristic Value. The Error Code parameter is set as specified in the Attribute Protocol. If the Attribute Value that is written is the wrong size, or has an invalid value as defined by the profile, then the write shall not succeed and an Error Response shall be sent with the Error Code set to Application Error by the server. */ - private func prepareWriteResponse(_ response: ATTResponse, operation: WriteOperation) async throws { - - @inline(__always) - func complete(_ completion: (WriteOperation) -> ()) { - inLongWrite = false - completion(operation) - } + private func prepareWriteResponse( + _ response: ATTResponse, + _ operation: inout WriteOperation + ) async throws { switch response { - case let .failure(error): - complete { $0.failure(error) } + case let .failure(errorResponse): + inLongWrite = false + throw GATTClientError.errorResponse(errorResponse) case let .success(pdu): + // append recieved data operation.receivedData += pdu.partValue // verify data sent @@ -1056,12 +993,15 @@ public actor GATTClient { guard pdu.handle == operation.lastRequest.handle, pdu.offset == operation.lastRequest.offset, - pdu.partValue == operation.lastRequest.partValue - else { complete { $0.completion(.failure(GATTClientError.invalidResponse(pdu))) }; return } + pdu.partValue == operation.lastRequest.partValue else { + inLongWrite = false + throw GATTClientError.invalidResponse(pdu) + } } let offset = Int(operation.lastRequest.offset) + operation.lastRequest.partValue.count + // prepare write responses if offset < operation.data.count { // write next part @@ -1079,7 +1019,7 @@ public actor GATTClient { operation.sentData += attributeValuePart let response = try await send(request, response: ATTPrepareWriteResponse.self) - try await self.prepareWriteResponse(response, operation: operation) + try await prepareWriteResponse(response, &operation) } else { @@ -1089,23 +1029,22 @@ public actor GATTClient { // all data sent let request = ATTExecuteWriteRequest.write let response = try await send(request, response: ATTExecuteWriteResponse.self) - self.executeWriteResponse(response, operation: operation) + try executeWriteResponse(response, &operation) } } } - private func executeWriteResponse(_ response: ATTResponse, operation: WriteOperation) { - - func complete(_ completion: (WriteOperation) -> ()) { - inLongWrite = false - completion(operation) - } + private func executeWriteResponse( + _ response: ATTResponse, + _ operation: inout WriteOperation + ) throws { + inLongWrite = false switch response { - case let .failure(error): - complete { $0.failure(error) } + case let .failure(errorResponse): + throw GATTClientError.errorResponse(errorResponse) case .success: - complete { $0.success() } + return } } @@ -1286,49 +1225,6 @@ internal extension GATTClient { init(handle: UInt16) { self.handle = handle } - /* - @inline(__always) - func failure(_ responseError: ATTErrorResponse) { - - if responseError.error == .invalidOffset, - data.isEmpty == false { - - success() - - } else { - - completion(.failure(GATTClientError.errorResponse(responseError))) - } - }*/ - } - - struct ReadMultipleOperation { - - let characteristics: [Characteristic] - - init(characteristics: [Characteristic]) { - self.characteristics = characteristics - } - } - - struct ReadUsingUUIDOperation { - - let uuid: BluetoothUUID - - init(uuid: BluetoothUUID) { - self.uuid = uuid - } - /* - func success(_ attributes: [ATTReadByTypeResponse.AttributeData]) { - - var data = [UInt16: Data](minimumCapacity: attributes.count) - - for attribute in attributes { - data[attribute.handle] = Data(attribute.value) - } - - completion(.success(data)) - }*/ } struct WriteOperation { From a6fe975730eb027dab5e03d3e71526f68f6af4d8 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 14 Nov 2021 08:02:47 -0500 Subject: [PATCH 11/44] Working on `async` support for `GATTClient` --- Sources/BluetoothGATT/GATTClient.swift | 61 +++++++++++++++----------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/Sources/BluetoothGATT/GATTClient.swift b/Sources/BluetoothGATT/GATTClient.swift index 3c16059b0..3f9a2b5b4 100755 --- a/Sources/BluetoothGATT/GATTClient.swift +++ b/Sources/BluetoothGATT/GATTClient.swift @@ -43,19 +43,15 @@ public actor GATTClient { socket: L2CAPSocket, maximumTransmissionUnit: ATTMaximumTransmissionUnit = .default, log: ((String) -> ())? = nil - ) { + ) async { self.connection = ATTConnection(socket: socket) self.preferredMaximumTransmissionUnit = maximumTransmissionUnit self.log = log - - // async setup tasks - Task { [weak self] in - // setup notifications and indications - await self?.registerATTHandlers() - // queue MTU exchange if not default value - if maximumTransmissionUnit > .default { - await self?.exchangeMTU() - } + // setup notifications and indications + await self.registerATTHandlers() + // queue MTU exchange if not default value + if maximumTransmissionUnit > .default { + await self.exchangeMTU() } } @@ -425,7 +421,7 @@ public actor GATTClient { primary: Bool = true ) async throws -> [Service] { let attributeType = GATTUUID(primaryService: primary) - var operation = DiscoveryOperation( + var operation = ServiceDiscoveryOperation( uuid: uuid, start: start, end: end, @@ -449,7 +445,7 @@ public actor GATTClient { let response = try await send(request, response: ATTReadByGroupTypeResponse.self) try await readByGroupTypeResponse(response, &operation) } - return operation.foundData + return operation.foundServices } private func discoverCharacteristics( @@ -457,7 +453,7 @@ public actor GATTClient { service: Service ) async throws -> [Characteristic] { let attributeType = GATTUUID.characteristic - var operation = DiscoveryOperation( + var operation = CharacteristicDiscoveryOperation( uuid: uuid, start: service.handle, end: service.end, @@ -470,7 +466,7 @@ public actor GATTClient { ) let response = try await send(request, response: ATTReadByTypeResponse.self) try await readByTypeResponse(response, &operation) - return operation.foundData + return operation.foundCharacteristics } private func discoverDescriptors(_ operation: inout DescriptorDiscoveryOperation) async throws { @@ -663,7 +659,7 @@ public actor GATTClient { private func readByGroupTypeResponse( _ response: ATTResponse, - _ operation: inout DiscoveryOperation + _ operation: inout ServiceDiscoveryOperation ) async throws { // Read By Group Type Response returns a list of Attribute Handle, End Group Handle, and Attribute Value tuples @@ -692,7 +688,7 @@ public actor GATTClient { handle: serviceData.attributeHandle, end: serviceData.endGroupHandle ) - operation.foundData.append(service) + operation.foundServices.append(service) } // get more if possible @@ -722,7 +718,7 @@ public actor GATTClient { private func findByTypeResponse( _ response: ATTResponse, - _ operation: inout DiscoveryOperation + _ operation: inout ServiceDiscoveryOperation ) async throws { // Find By Type Value Response returns a list of Attribute Handle ranges. // The Attribute Handle range is the starting handle and the ending handle of the service definition. @@ -740,7 +736,7 @@ public actor GATTClient { else { fatalError("Should have UUID specified") } // pre-allocate array - operation.foundData.reserveCapacity(operation.foundData.count + response.handles.count) + operation.foundServices.reserveCapacity(operation.foundServices.count + response.handles.count) // store PDU values for serviceData in response.handles { @@ -750,7 +746,7 @@ public actor GATTClient { handle: serviceData.foundAttribute, end: serviceData.groupEnd ) - operation.foundData.append(service) + operation.foundServices.append(service) } // get more if possible @@ -803,7 +799,7 @@ public actor GATTClient { case let .success(pdu): // pre-allocate array - operation.foundDescriptors.reserveCapacity(operation.foundDescriptors.count + pdu.data.count) + //operation.foundDescriptors.reserveCapacity(operation.foundDescriptors.count + pdu.data.count) let foundData: [Descriptor] @@ -838,7 +834,7 @@ public actor GATTClient { private func readByTypeResponse( _ response: ATTResponse, - _ operation: inout DiscoveryOperation + _ operation: inout CharacteristicDiscoveryOperation ) async throws { typealias DeclarationAttribute = GATTDatabase.CharacteristicDeclarationAttribute @@ -859,7 +855,7 @@ public actor GATTClient { case let .success(pdu): // pre-allocate array - operation.foundData.reserveCapacity(operation.foundData.count + pdu.data.count) + //operation.foundCharacteristics.reserveCapacity(operation.foundCharacteristics.count + pdu.data.count) // parse pdu data for characteristicData in pdu.attributeData { @@ -880,7 +876,7 @@ public actor GATTClient { properties: declaration.properties, handle: (handle, declaration.valueHandle)) - operation.foundData.append(characteristic) + operation.foundCharacteristics.append(characteristic) // if we specified a UUID to be searched, // then call completion if it matches @@ -1186,7 +1182,22 @@ public extension GATTClient { internal extension GATTClient { - struct DiscoveryOperation { + struct ServiceDiscoveryOperation { + + let uuid: BluetoothUUID? + + var start: UInt16 { + didSet { assert(start <= end, "Start Handle should always be less than or equal to End handle") } + } + + let end: UInt16 + + let type: GATTUUID + + var foundServices = [Service]() + } + + struct CharacteristicDiscoveryOperation { let uuid: BluetoothUUID? @@ -1198,7 +1209,7 @@ internal extension GATTClient { let type: GATTUUID - var foundData = [T]() + var foundCharacteristics = [Characteristic]() } struct DescriptorDiscoveryOperation { From e612355217b56d00993f682aae6728be761f2aa9 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 14 Nov 2021 08:19:11 -0500 Subject: [PATCH 12/44] Working on `async` support for `GATTClient` --- Sources/BluetoothGATT/GATTClient.swift | 44 +++++++++++++++++--------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/Sources/BluetoothGATT/GATTClient.swift b/Sources/BluetoothGATT/GATTClient.swift index 3f9a2b5b4..70bb3382c 100755 --- a/Sources/BluetoothGATT/GATTClient.swift +++ b/Sources/BluetoothGATT/GATTClient.swift @@ -25,9 +25,7 @@ public actor GATTClient { public let preferredMaximumTransmissionUnit: ATTMaximumTransmissionUnit internal private(set) var didExchangeMTU = false - - internal let connection: ATTConnection - + /// Whether the client is currently writing a long value. internal private(set) var inLongWrite: Bool = false @@ -37,6 +35,8 @@ public actor GATTClient { /// Indications internal private(set) var indications = [UInt16: Notification]() + internal let connection: ATTConnection + // MARK: - Initialization public init( @@ -164,13 +164,14 @@ public actor GATTClient { // The Attribute Protocol Read By Type Request is used to perform the sub-procedure. // The Attribute Type is set to the known characteristic UUID and the Starting Handle and Ending Handle parameters // shall be set to the range over which this read is to be performed. This is typically the handle range for the service in which the characteristic belongs. - let request = ATTReadByTypeRequest( startHandle: handleRange.start, endHandle: handleRange.end, attributeType: uuid ) - let response = try await send(request, response: ATTReadByTypeResponse.self).get() + let response = try await send(request, response: ATTReadByTypeResponse.self) + .mapError({ GATTClientError.errorResponse($0) }) + .get() // parse response var attributeData = [UInt16: Data](minimumCapacity: response.attributeData.count) for attribute in response.attributeData { @@ -198,7 +199,9 @@ public actor GATTClient { assert(handles.count > 1) guard let request = ATTReadMultipleRequest(handles: handles) else { fatalError("Must provide at least 2 characteristics") } - let response = try await send(request, response: ATTReadMultipleResponse.self).get() + let response = try await send(request, response: ATTReadMultipleResponse.self) + .mapError({ GATTClientError.errorResponse($0) }) + .get() return response.values } @@ -328,6 +331,8 @@ public actor GATTClient { response: Response.Type ) async throws -> ATTResponse { assert(Response.attributeOpcode != .errorResponse) + assert(Response.attributeOpcode.type == .response) + assert(Request.attributeOpcode.type != .response) let log = self.log log?("Request: \(request)") return try await withCheckedThrowingContinuation { [weak self] continuation in @@ -360,12 +365,10 @@ public actor GATTClient { @inline(__always) private func send(_ request: Request) async throws { - log?("Request: \(request)") - + assert(Request.attributeOpcode.type != .response) guard let _ = await connection.send(request) else { fatalError("Could not add PDU to queue: \(request)") } - // write pending PDU let didWrite = try await self.connection.write() assert(didWrite, "Expected queued write operation") @@ -564,7 +567,9 @@ public actor GATTClient { An Error Response shall be sent by the server in response to the Write Request if insufficient authentication, insufficient authorization, insufficient encryption key size is used by the client, or if a write operation is not permitted on the Characteristic Value. The Error Code parameter is set as specified in the Attribute Protocol. If the Characteristic Value that is written is the wrong size, or has an invalid value as defined by the profile, then the value shall not be written and an Error Response shall be sent with the Error Code set to Application Error by the server. */ - let _ = try response.get() + let _ = try response + .mapError({ GATTClientError.errorResponse($0) }) + .get() } private func writeLongAttributeValue( @@ -1025,14 +1030,13 @@ public actor GATTClient { // all data sent let request = ATTExecuteWriteRequest.write let response = try await send(request, response: ATTExecuteWriteResponse.self) - try executeWriteResponse(response, &operation) + try executeWriteResponse(response) } } } private func executeWriteResponse( - _ response: ATTResponse, - _ operation: inout WriteOperation + _ response: ATTResponse ) throws { inLongWrite = false @@ -1045,7 +1049,12 @@ public actor GATTClient { } private func notification(_ notification: ATTHandleValueNotification) { - notifications[notification.handle]?(notification.value) + guard let handler = notifications[notification.handle] else { + log?("Recieved notification for unregistered handle \(notification.handle)") + return + } + // callback + handler(notification.value) } private func indication(_ indication: ATTHandleValueIndication) async { @@ -1053,7 +1062,12 @@ public actor GATTClient { // send acknowledgement do { try await send(confirmation) } catch { log?("Unable to send indication confirmation. \(error)") } - indications[indication.handle]?(indication.value) + // callback + guard let handler = indications[indication.handle] else { + log?("Recieved indication for unregistered handle \(indication.handle)") + return + } + handler(indication.value) } } From 6e5e469d43205aaca95bdb777a5c79cb411ba3a7 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 14 Nov 2021 08:44:00 -0500 Subject: [PATCH 13/44] Working on `async` support for `GATTClient` --- Sources/BluetoothGATT/ATTConnection.swift | 33 +++++++++++------------ Sources/BluetoothGATT/GATTClient.swift | 23 +++++++++++++--- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/Sources/BluetoothGATT/ATTConnection.swift b/Sources/BluetoothGATT/ATTConnection.swift index 110127dae..65cc97923 100755 --- a/Sources/BluetoothGATT/ATTConnection.swift +++ b/Sources/BluetoothGATT/ATTConnection.swift @@ -58,8 +58,14 @@ internal actor ATTConnection { unregisterAll() } - public init(socket: L2CAPSocket) { + public init( + socket: L2CAPSocket, + log: ((String) -> ())? = nil, + writePending: (() -> ())? = nil + ) { self.socket = socket + self.log = log + self.writePending = writePending } // MARK: - Methods @@ -68,6 +74,10 @@ internal actor ATTConnection { self.maximumTransmissionUnit = newValue } + public func setWritePending(_ newValue: (() -> ())?) { + self.writePending = newValue + } + /// Performs the actual IO for recieving data. public func read() async throws { @@ -179,23 +189,14 @@ internal actor ATTConnection { notifyList.removeAll() } - /// Sends an error. - public func send( - error: ATTError, - opcode: ATTOpcode, - handle: UInt16 = 0, - response: ((ATTErrorResponse) -> ())? = nil - ) -> UInt? { - - let error = ATTErrorResponse(request: opcode, attributeHandle: handle, error: error) - return self.send(error) // no callback for responses - } - /// Adds a PDU to the queue to send. /// /// - Returns: Identifier of queued send operation or `nil` if the PDU cannot be sent. @discardableResult - public func send (_ pdu: T, response: (callback: (ATTProtocolDataUnit) -> (), ATTProtocolDataUnit.Type)? = nil) -> UInt? { + public func queue ( + _ pdu: T, + response: (callback: (ATTProtocolDataUnit) -> (), ATTProtocolDataUnit.Type)? = nil + ) -> UInt? { let attributeOpcode = T.attributeOpcode let type = attributeOpcode.type @@ -389,10 +390,8 @@ internal actor ATTConnection { // If this was a request and no handler was registered for it, respond with "Not Supported" if foundPDU == nil && opcode.type == .request { - let errorResponse = ATTErrorResponse(request: opcode, attributeHandle: 0x00, error: .requestNotSupported) - - let _ = send(errorResponse) + let _ = queue(errorResponse) } } diff --git a/Sources/BluetoothGATT/GATTClient.swift b/Sources/BluetoothGATT/GATTClient.swift index 70bb3382c..b23c24a06 100755 --- a/Sources/BluetoothGATT/GATTClient.swift +++ b/Sources/BluetoothGATT/GATTClient.swift @@ -37,14 +37,23 @@ public actor GATTClient { internal let connection: ATTConnection + internal var backgroundTask: Task<(), Swift.Error>? + // MARK: - Initialization + deinit { + backgroundTask?.cancel() + } + public init( socket: L2CAPSocket, maximumTransmissionUnit: ATTMaximumTransmissionUnit = .default, log: ((String) -> ())? = nil ) async { - self.connection = ATTConnection(socket: socket) + self.connection = ATTConnection( + socket: socket, + log: log + ) self.preferredMaximumTransmissionUnit = maximumTransmissionUnit self.log = log // setup notifications and indications @@ -53,6 +62,14 @@ public actor GATTClient { if maximumTransmissionUnit > .default { await self.exchangeMTU() } + // read in background + backgroundTask = Task.detached(priority: .userInitiated) { + + } + // write in background + await self.connection.setWritePending { + + } } // MARK: - Methods @@ -344,7 +361,7 @@ public actor GATTClient { log?("Response: \($0)") continuation.resume(returning: ATTResponse($0)) } - guard let _ = await self.connection.send(request, response: (callback, responseType)) + guard let _ = await self.connection.queue(request, response: (callback, responseType)) else { fatalError("Could not add PDU to queue: \(request)") } // do I/O do { @@ -367,7 +384,7 @@ public actor GATTClient { private func send(_ request: Request) async throws { log?("Request: \(request)") assert(Request.attributeOpcode.type != .response) - guard let _ = await connection.send(request) + guard let _ = await connection.queue(request) else { fatalError("Could not add PDU to queue: \(request)") } // write pending PDU let didWrite = try await self.connection.write() From 883dc17ff9663e1ad7e5a1c617842a6a4652a91a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 14 Nov 2021 10:53:56 -0500 Subject: [PATCH 14/44] Working on `async` support for `GATTServer` --- Sources/BluetoothGATT/GATTServer.swift | 404 ++++++++++--------------- 1 file changed, 167 insertions(+), 237 deletions(-) diff --git a/Sources/BluetoothGATT/GATTServer.swift b/Sources/BluetoothGATT/GATTServer.swift index 6a1c6df70..f1a57d9c4 100644 --- a/Sources/BluetoothGATT/GATTServer.swift +++ b/Sources/BluetoothGATT/GATTServer.swift @@ -15,14 +15,10 @@ public actor GATTServer { public var log: ((String) -> ())? - public var writePending: (() -> ())? { - get { return connection.writePending } - set { connection.writePending = newValue } - } - - public private(set) var maximumTransmissionUnit: ATTMaximumTransmissionUnit { - get { return connection.maximumTransmissionUnit } - set { connection.maximumTransmissionUnit = newValue } + public var maximumTransmissionUnit: ATTMaximumTransmissionUnit { + get async { + return await self.connection.maximumTransmissionUnit + } } public let preferredMaximumTransmissionUnit: ATTMaximumTransmissionUnit @@ -44,19 +40,20 @@ public actor GATTServer { // MARK: - Initialization - public init(socket: L2CAPSocket, - maximumTransmissionUnit: ATTMaximumTransmissionUnit = .default, - maximumPreparedWrites: Int = 50) { + public init( + socket: L2CAPSocket, + maximumTransmissionUnit: ATTMaximumTransmissionUnit = .default, + maximumPreparedWrites: Int = 50, + log: ((String) -> ())? = nil + ) async { // set initial MTU and register handlers self.maximumPreparedWrites = maximumPreparedWrites self.preferredMaximumTransmissionUnit = maximumTransmissionUnit - self.connection = ATTConnection(socket: socket) + self.connection = ATTConnection(socket: socket, log: log) // async register handlers - Task { - await self.registerATTHandlers() - } + await self.registerATTHandlers() } // MARK: - Methods @@ -72,24 +69,20 @@ public actor GATTServer { } /// Update the value of a characteristic attribute. - public func writeValue(_ value: Data, forCharacteristic handle: UInt16) { - + public func writeValue(_ value: Data, forCharacteristic handle: UInt16) async { database.write(value, forAttribute: handle) - didWriteAttribute(handle, isLocalWrite: true) + await didWriteAttribute(handle, isLocalWrite: true) } /// Update the value of a characteristic attribute. - public func writeValue(_ value: Data, forCharacteristic uuid: BluetoothUUID) { - + public func writeValue(_ value: Data, forCharacteristic uuid: BluetoothUUID) async { guard let attribute = database.first(where: { $0.uuid == uuid }) else { fatalError("Invalid uuid \(uuid)") } - - writeValue(value, forCharacteristic: attribute.handle) + await writeValue(value, forCharacteristic: attribute.handle) } // MARK: - Private Methods - @inline(__always) private func registerATTHandlers() async { // Exchange MTU @@ -129,54 +122,51 @@ public actor GATTServer { await connection.register { [weak self] in await self?.executeWriteRequest($0) } } - @inline(__always) - private func errorResponse(_ opcode: ATTOpcode, _ error: ATTError, _ handle: UInt16 = 0) { - + private func errorResponse(_ opcode: ATTOpcode, _ error: ATTError, _ handle: UInt16 = 0) async { log?("Error \(error) - \(opcode) (\(handle))") - - guard let _ = connection.send(error: error, opcode: opcode, handle: handle) + let response = ATTErrorResponse(request: opcode, attributeHandle: handle, error: error) + guard let _ = await connection.queue(response) else { fatalError("Could not add error PDU to queue: \(opcode) \(error) \(handle)") } } private func fatalErrorResponse(_ message: String, _ opcode: ATTOpcode, _ handle: UInt16 = 0, line: UInt = #line) async -> Never { - errorResponse(opcode, .unlikelyError, handle) + await errorResponse(opcode, .unlikelyError, handle) do { let _ = try await connection.write() } - catch { log?("Could not send .unlikelyError to client. (\(error))") } + catch { log?("Could not send .unlikelyError to client. \(error)") } // crash fatalError(message, line: line) } /// Respond to a client-initiated PDU message. - @inline(__always) - private func respond (_ response: T) { - + private func respond (_ response: T) async { log?("Response: \(response)") - - guard let _ = connection.send(response) + guard let _ = await connection.queue(response) else { fatalError("Could not add PDU to queue: \(response)") } } /// Send a server-initiated PDU message. - @inline(__always) - private func send (_ indication: ATTHandleValueIndication, response: @escaping (ATTResponse) -> ()) { - + private func send(_ indication: ATTHandleValueIndication) async -> ATTResponse { log?("Indication: \(indication)") - - let callback: (AnyATTResponse) -> () = { response(ATTResponse($0)) } - - guard let _ = connection.send(indication, response: (callback, ATTHandleValueConfirmation.self)) - else { fatalError("Could not add PDU to queue: \(indication)") } + return await withCheckedContinuation { [weak self] continuation in + guard let self = self else { return } + Task { + let responseType: ATTProtocolDataUnit.Type = ATTHandleValueConfirmation.self + // callback if no I/O errors or disconnect + let callback: (ATTProtocolDataUnit) -> () = { + continuation.resume(returning: ATTResponse($0)) + } + guard let _ = await self.connection.queue(indication, response: (callback, responseType)) + else { fatalError("Could not add PDU to queue: \(indication)") } + } + } } /// Send a server-initiated PDU message. - @inline(__always) - private func send (_ notification: ATTHandleValueNotification) { - + private func send(_ notification: ATTHandleValueNotification) async { log?("Notification: \(notification)") - - guard let _ = connection.send(notification) + guard let _ = await connection.queue(notification) else { fatalError("Could not add PDU to queue: \(notification)") } } @@ -221,50 +211,50 @@ public actor GATTServer { } /// Handler for Write Request and Command - private func handleWriteRequest(opcode: ATTOpcode, handle: UInt16, value: Data, shouldRespond: Bool) { + private func handleWriteRequest(opcode: ATTOpcode, handle: UInt16, value: Data, shouldRespond: Bool) async { /// Conditionally respond - @inline(__always) - func doResponse( _ block: @autoclosure() -> ()) { - - if shouldRespond { block() } + func doResponse( _ block: @autoclosure () async -> ()) async { + if shouldRespond { + await block() + } } log?("Write \(shouldRespond ? "Request" : "Command") (\(handle)) \(value)") // no attributes, impossible to write - guard database.attributes.isEmpty == false - else { doResponse(errorResponse(opcode, .invalidHandle, handle)); return } + guard database.attributes.isEmpty == false else { + await doResponse(await errorResponse(opcode, .invalidHandle, handle)) + return + } // validate handle - guard database.contains(handle: handle) - else { errorResponse(opcode, .invalidHandle, handle); return } + guard database.contains(handle: handle) else { + await errorResponse(opcode, .invalidHandle, handle) + return + } // get attribute let attribute = database[handle: handle] // validate permissions if let error = await checkPermissions([.write, .writeAuthentication, .writeEncrypt], attribute) { - - doResponse(errorResponse(opcode, error, handle)) + await doResponse(await errorResponse(opcode, error, handle)) return } // validate application errors with write callback if let error = willWrite?(attribute.uuid, handle, attribute.value, value) { - - doResponse(errorResponse(opcode, error, handle)) + await doResponse(await errorResponse(opcode, error, handle)) return } database.write(value, forAttribute: handle) - - doResponse(respond(ATTWriteResponse())) - - didWriteAttribute(handle) + await doResponse(await respond(ATTWriteResponse())) + await didWriteAttribute(handle) } - private func didWriteAttribute(_ attributeHandle: UInt16, isLocalWrite: Bool = false) { + private func didWriteAttribute(_ attributeHandle: UInt16, isLocalWrite: Bool = false) async { let (group, attribute) = database.attributeGroup(for: attributeHandle) assert(attribute.handle == attributeHandle) @@ -284,21 +274,21 @@ public actor GATTServer { // notify if descriptor.configuration.contains(.notify) { - - let notification = ATTHandleValueNotification(attribute: attribute, maximumTransmissionUnit: connection.maximumTransmissionUnit) - - send(notification) + let notification = ATTHandleValueNotification( + attribute: attribute, + maximumTransmissionUnit: await connection.maximumTransmissionUnit + ) + await send(notification) } // indicate if descriptor.configuration.contains(.indicate) { - - let indication = ATTHandleValueIndication(attribute: attribute, maximumTransmissionUnit: connection.maximumTransmissionUnit) - - send(indication) { [unowned self] (confirmation) in - - self.log?("Confirmation: \(confirmation)") - } + let indication = ATTHandleValueIndication( + attribute: attribute, + maximumTransmissionUnit: await connection.maximumTransmissionUnit + ) + let confirmation = await send(indication) + self.log?("Confirmation: \(confirmation)") } } @@ -309,26 +299,33 @@ public actor GATTServer { } } - private func handleReadRequest(opcode: ATTOpcode, - handle: UInt16, - offset: UInt16 = 0, - isBlob: Bool = false) -> Data? { + private func handleReadRequest( + opcode: ATTOpcode, + handle: UInt16, + offset: UInt16 = 0, + isBlob: Bool = false + ) async -> Data? { + + let maximumTransmissionUnit = await self.connection.maximumTransmissionUnit // no attributes - guard database.attributes.isEmpty == false - else { errorResponse(opcode, .invalidHandle, handle); return nil } + guard database.attributes.isEmpty == false else { + await errorResponse(opcode, .invalidHandle, handle) + return nil + } // validate handle - guard database.contains(handle: handle) - else { errorResponse(opcode, .invalidHandle, handle); return nil } + guard database.contains(handle: handle) else { + await errorResponse(opcode, .invalidHandle, handle) + return nil + } // get attribute let attribute = database[handle: handle] // validate permissions - if let error = checkPermissions([.read, .readAuthentication, .readEncrypt], attribute) { - - errorResponse(opcode, error, handle) + if let error = await checkPermissions([.read, .readAuthentication, .readEncrypt], attribute) { + await errorResponse(opcode, error, handle) return nil } @@ -336,12 +333,15 @@ public actor GATTServer { // // If the Characteristic Value is not longer than (ATT_MTU – 1) an Error Response with // the Error Code set to Attribute Not Long shall be received on the first Read Blob Request. - guard isBlob == false || attribute.value.count > (Int(connection.maximumTransmissionUnit.rawValue) - 1) - else { errorResponse(opcode, .attributeNotLong, handle); return nil } + guard isBlob == false || attribute.value.count > (Int(maximumTransmissionUnit.rawValue) - 1) else { await errorResponse(opcode, .attributeNotLong, handle) + return nil + } // check boundary - guard offset <= UInt16(attribute.value.count) - else { errorResponse(opcode, .invalidOffset, handle); return nil } + guard offset <= UInt16(attribute.value.count) else { + await errorResponse(opcode, .invalidOffset, handle) + return nil + } var value: Data @@ -360,12 +360,11 @@ public actor GATTServer { } // adjust value for MTU - value = Data(value.prefix(Int(connection.maximumTransmissionUnit.rawValue) - 1)) + value = Data(value.prefix(Int(maximumTransmissionUnit.rawValue) - 1)) // validate application errors with read callback if let error = willRead?(attribute.uuid, handle, value, Int(offset)) { - - errorResponse(opcode, error, handle) + await errorResponse(opcode, error, handle) return nil } @@ -374,112 +373,93 @@ public actor GATTServer { // MARK: Callbacks - private func exchangeMTU(_ pdu: ATTMaximumTransmissionUnitRequest) { - + private func exchangeMTU(_ pdu: ATTMaximumTransmissionUnitRequest) async { let serverMTU = preferredMaximumTransmissionUnit.rawValue - let finalMTU = ATTMaximumTransmissionUnit(server: serverMTU, client: pdu.clientMTU) - // Respond with the server MTU (not final MTU) - connection.send(ATTMaximumTransmissionUnitResponse(serverMTU: serverMTU)) - + await connection.queue(ATTMaximumTransmissionUnitResponse(serverMTU: serverMTU)) // Set MTU - maximumTransmissionUnit = finalMTU - + await connection.setMaximumTransmissionUnit(finalMTU) log?("MTU Exchange (\(pdu.clientMTU) -> \(serverMTU))") } - private func readByGroupType(_ pdu: ATTReadByGroupTypeRequest) { - + private func readByGroupType(_ pdu: ATTReadByGroupTypeRequest) async { typealias AttributeData = ATTReadByGroupTypeResponse.AttributeData - log?("Read by Group Type (\(pdu.startHandle) - \(pdu.endHandle))") - // validate handles - guard pdu.startHandle != 0 && pdu.endHandle != 0 - else { errorResponse(type(of: pdu).attributeOpcode, .invalidHandle); return } - - guard pdu.startHandle <= pdu.endHandle - else { errorResponse(type(of: pdu).attributeOpcode, .invalidHandle, pdu.startHandle); return } - - // GATT defines that only the Primary Service and Secondary Service group types + guard pdu.startHandle != 0 && pdu.endHandle != 0 else { + await errorResponse(type(of: pdu).attributeOpcode, .invalidHandle) + return + } + guard pdu.startHandle <= pdu.endHandle else { + await errorResponse(type(of: pdu).attributeOpcode, .invalidHandle, pdu.startHandle) + return + } + // GATT defines that only the Primary Service and Secondary Service group types // can be used for the "Read By Group Type" request. Return an error if any other group type is given. - guard pdu.type == .primaryService - || pdu.type == .secondaryService - else { errorResponse(type(of: pdu).attributeOpcode, .unsupportedGroupType, pdu.startHandle); return } - + guard pdu.type == .primaryService || pdu.type == .secondaryService else { + await errorResponse(type(of: pdu).attributeOpcode, .unsupportedGroupType, pdu.startHandle) + return + } let attributeData = database.readByGroupType(handle: (pdu.startHandle, pdu.endHandle), type: pdu.type) + guard let firstAttribute = attributeData.first else { + await errorResponse(type(of: pdu).attributeOpcode, .attributeNotFound, pdu.startHandle) + return + } - guard let firstAttribute = attributeData.first - else { errorResponse(type(of: pdu).attributeOpcode, .attributeNotFound, pdu.startHandle); return } - - let mtu = Int(connection.maximumTransmissionUnit.rawValue) - + let mtu = Int(await connection.maximumTransmissionUnit.rawValue) let valueLength = firstAttribute.value.count - let response: ATTReadByGroupTypeResponse // truncate for MTU if first handle is too large if ATTReadByGroupTypeResponse([firstAttribute]).dataLength > mtu { - let maxLength = min(min(mtu - 6, 251), valueLength) - - let truncatedAttribute = AttributeData(attributeHandle: firstAttribute.attributeHandle, - endGroupHandle: firstAttribute.endGroupHandle, - value: Data(firstAttribute.value.prefix(maxLength))) - + let truncatedAttribute = AttributeData( + attributeHandle: firstAttribute.attributeHandle, + endGroupHandle: firstAttribute.endGroupHandle, + value: Data(firstAttribute.value.prefix(maxLength)) + ) response = ATTReadByGroupTypeResponse([truncatedAttribute]) - } else { - var count = 1 - // respond with results that are the same length if attributeData.count > 1 { - for (index, attribute) in attributeData.suffix(from: 1).enumerated() { - let newCount = index + 1 - guard attribute.value.count == valueLength, ATTReadByGroupTypeResponse.dataLength(for: attributeData.prefix(newCount)) <= mtu else { break } - count = newCount } } let limitedAttributes = Array(attributeData.prefix(count)) - response = ATTReadByGroupTypeResponse(limitedAttributes) } assert(response.dataLength <= mtu, "Response \(response.dataLength) bytes > MTU (\(mtu))") - respond(response) + await respond(response) } - private func readByType(_ pdu: ATTReadByTypeRequest) { - + private func readByType(_ pdu: ATTReadByTypeRequest) async { typealias AttributeData = ATTReadByTypeResponse.AttributeData - log?("Read by Type (\(pdu.attributeType)) (\(pdu.startHandle) - \(pdu.endHandle))") - guard pdu.startHandle != 0 && pdu.endHandle != 0 - else { errorResponse(type(of: pdu).attributeOpcode, .invalidHandle); return } + else { await errorResponse(type(of: pdu).attributeOpcode, .invalidHandle); return } guard pdu.startHandle <= pdu.endHandle - else { errorResponse(type(of: pdu).attributeOpcode, .invalidHandle, pdu.startHandle); return } + else { await errorResponse(type(of: pdu).attributeOpcode, .invalidHandle, pdu.startHandle); return } let attributeData = database .readByType(handle: (pdu.startHandle, pdu.endHandle), type: pdu.attributeType) .map { AttributeData(handle: $0.handle, value: $0.value) } guard let firstAttribute = attributeData.first - else { errorResponse(type(of: pdu).attributeOpcode, .attributeNotFound, pdu.startHandle); return } + else { await errorResponse(type(of: pdu).attributeOpcode, .attributeNotFound, pdu.startHandle); return } - let mtu = Int(connection.maximumTransmissionUnit.rawValue) + let mtu = Int(await connection.maximumTransmissionUnit.rawValue) let valueLength = firstAttribute.value.count @@ -522,10 +502,10 @@ public actor GATTServer { assert(response.dataLength <= mtu, "Response \(response.dataLength) bytes > MTU (\(mtu))") - respond(response) + await respond(response) } - private func findInformation(_ pdu: ATTFindInformationRequest) { + private func findInformation(_ pdu: ATTFindInformationRequest) async { typealias AttributeData = ATTFindInformationResponse.AttributeData @@ -536,18 +516,18 @@ public actor GATTServer { log?("Find Information (\(pdu.startHandle) - \(pdu.endHandle))") guard pdu.startHandle != 0 && pdu.endHandle != 0 - else { errorResponse(opcode, .invalidHandle); return } + else { await errorResponse(opcode, .invalidHandle); return } guard pdu.startHandle <= pdu.endHandle - else { errorResponse(opcode, .invalidHandle, pdu.startHandle); return } + else { await errorResponse(opcode, .invalidHandle, pdu.startHandle); return } let attributes = database.findInformation(handle: (pdu.startHandle, pdu.endHandle)) guard attributes.isEmpty == false - else { errorResponse(opcode, .attributeNotFound, pdu.startHandle); return } + else { await errorResponse(opcode, .attributeNotFound, pdu.startHandle); return } guard let format = Format(uuid: attributes[0].uuid) - else { errorResponse(opcode, .unlikelyError, pdu.startHandle); return } + else { await errorResponse(opcode, .unlikelyError, pdu.startHandle); return } var bit16Pairs = [ATTFindInformationResponse.Attribute16Bit]() var bit128Pairs = [ATTFindInformationResponse.Attribute128Bit]() @@ -557,24 +537,18 @@ public actor GATTServer { // truncate if bigger than MTU let encodedLength = 2 + ((index + 1) * format.length) - guard encodedLength <= Int(connection.maximumTransmissionUnit.rawValue) + guard encodedLength <= Int(await connection.maximumTransmissionUnit.rawValue) else { break } var mismatchedType = false // encode attribute switch (attribute.uuid, format) { - case let (.bit16(type), .bit16): - bit16Pairs.append(ATTFindInformationResponse.Attribute16Bit(handle: attribute.handle, uuid: type)) - case let (.bit128(type), .bit128): - bit128Pairs.append(ATTFindInformationResponse.Attribute128Bit(handle: attribute.handle, uuid: type)) - default: - mismatchedType = true // mismatching types } @@ -591,81 +565,65 @@ public actor GATTServer { } let response = ATTFindInformationResponse(attributeData: attributeData) - - respond(response) + await respond(response) } - private func findByTypeValue(_ pdu: ATTFindByTypeRequest) { + private func findByTypeValue(_ pdu: ATTFindByTypeRequest) async { typealias Handle = ATTFindByTypeResponse.HandlesInformation log?("Find By Type Value (\(pdu.startHandle) - \(pdu.endHandle)) (\(pdu.attributeType))") guard pdu.startHandle != 0 && pdu.endHandle != 0 - else { errorResponse(type(of: pdu).attributeOpcode, .invalidHandle); return } + else { await errorResponse(type(of: pdu).attributeOpcode, .invalidHandle); return } guard pdu.startHandle <= pdu.endHandle - else { errorResponse(type(of: pdu).attributeOpcode, .invalidHandle, pdu.startHandle); return } + else { await errorResponse(type(of: pdu).attributeOpcode, .invalidHandle, pdu.startHandle); return } let handles = database.findByTypeValue(handle: (pdu.startHandle, pdu.endHandle), type: pdu.attributeType, value: pdu.attributeValue) guard handles.isEmpty == false - else { errorResponse(type(of: pdu).attributeOpcode, .attributeNotFound, pdu.startHandle); return } + else { await errorResponse(type(of: pdu).attributeOpcode, .attributeNotFound, pdu.startHandle); return } let response = ATTFindByTypeResponse(handles) - - respond(response) + await respond(response) } - private func writeRequest(_ pdu: ATTWriteRequest) { - + private func writeRequest(_ pdu: ATTWriteRequest) async { let opcode = type(of: pdu).attributeOpcode - - handleWriteRequest(opcode: opcode, handle: pdu.handle, value: pdu.value, shouldRespond: true) + await handleWriteRequest(opcode: opcode, handle: pdu.handle, value: pdu.value, shouldRespond: true) } - private func writeCommand(_ pdu: ATTWriteCommand) { - + private func writeCommand(_ pdu: ATTWriteCommand) async { let opcode = type(of: pdu).attributeOpcode - - handleWriteRequest(opcode: opcode, handle: pdu.handle, value: pdu.value, shouldRespond: false) + await handleWriteRequest(opcode: opcode, handle: pdu.handle, value: pdu.value, shouldRespond: false) } - private func readRequest(_ pdu: ATTReadRequest) { - + private func readRequest(_ pdu: ATTReadRequest) async { let opcode = type(of: pdu).attributeOpcode - log?("Read (\(pdu.handle))") - - if let value = handleReadRequest(opcode: opcode, handle: pdu.handle) { - - respond(ATTReadResponse(attributeValue: value)) + if let value = await handleReadRequest(opcode: opcode, handle: pdu.handle) { + await respond(ATTReadResponse(attributeValue: value)) } } - private func readBlobRequest(_ pdu: ATTReadBlobRequest) { - + private func readBlobRequest(_ pdu: ATTReadBlobRequest) async { let opcode = type(of: pdu).attributeOpcode - log?("Read Blob (\(pdu.handle))") - - if let value = handleReadRequest(opcode: opcode, handle: pdu.handle, offset: pdu.offset, isBlob: true) { - - respond(ATTReadBlobResponse(partAttributeValue: value)) + if let value = await handleReadRequest(opcode: opcode, handle: pdu.handle, offset: pdu.offset, isBlob: true) { + await respond(ATTReadBlobResponse(partAttributeValue: value)) } } - private func readMultipleRequest(_ pdu: ATTReadMultipleRequest) { - + private func readMultipleRequest(_ pdu: ATTReadMultipleRequest) async { let opcode = type(of: pdu).attributeOpcode - log?("Read Multiple Request \(pdu.handles)") // no attributes, impossible to read guard database.attributes.isEmpty == false - else { errorResponse(opcode, .invalidHandle, pdu.handles[0]); return } + else { await errorResponse(opcode, .invalidHandle, pdu.handles[0]); return } var values = Data() @@ -673,15 +631,14 @@ public actor GATTServer { // validate handle guard database.contains(handle: handle) - else { errorResponse(opcode, .invalidHandle, handle); return } + else { await errorResponse(opcode, .invalidHandle, handle); return } // get attribute let attribute = database[handle: handle] // validate application errors with read callback if let error = willRead?(attribute.uuid, handle, attribute.value, 0) { - - errorResponse(opcode, error, handle) + await errorResponse(opcode, error, handle) return } @@ -689,11 +646,10 @@ public actor GATTServer { } let response = ATTReadMultipleResponse(values: values) - - respond(response) + await respond(response) } - private func prepareWriteRequest(_ pdu: ATTPrepareWriteRequest) { + private func prepareWriteRequest(_ pdu: ATTPrepareWriteRequest) async { let opcode = type(of: pdu).attributeOpcode @@ -701,23 +657,22 @@ public actor GATTServer { // no attributes, impossible to write guard database.attributes.isEmpty == false - else { errorResponse(opcode, .invalidHandle, pdu.handle); return } + else { await errorResponse(opcode, .invalidHandle, pdu.handle); return } // validate handle guard database.contains(handle: pdu.handle) - else { errorResponse(opcode, .invalidHandle, pdu.handle); return } + else { await errorResponse(opcode, .invalidHandle, pdu.handle); return } // validate that the prepared writes queue is not full guard preparedWrites.count <= maximumPreparedWrites - else { errorResponse(opcode, .prepareQueueFull); return } + else { await errorResponse(opcode, .prepareQueueFull); return } // get attribute let attribute = database[handle: pdu.handle] // validate permissions - if let error = checkPermissions([.write, .writeAuthentication, .writeEncrypt], attribute) { - - errorResponse(opcode, error, pdu.handle) + if let error = await checkPermissions([.write, .writeAuthentication, .writeEncrypt], attribute) { + await errorResponse(opcode, error, pdu.handle) return } @@ -727,76 +682,53 @@ public actor GATTServer { // add queued write let preparedWrite = PreparedWrite(handle: pdu.handle, value: pdu.partValue, offset: pdu.offset) - preparedWrites.append(preparedWrite) - let response = ATTPrepareWriteResponse(handle: pdu.handle, offset: pdu.offset, partValue: pdu.partValue) - - respond(response) + await respond(response) } - private func executeWriteRequest(_ pdu: ATTExecuteWriteRequest) { - + private func executeWriteRequest(_ pdu: ATTExecuteWriteRequest) async { let opcode = type(of: pdu).attributeOpcode - log?("Execute Write Request (\(pdu))") - let preparedWrites = self.preparedWrites self.preparedWrites = [] - var newValues = [UInt16: Data]() - switch pdu { - case .cancel: - break // queue always cleared - case .write: - // validate for write in preparedWrites { - let previousValue = newValues[write.handle] ?? Data() - let newValue = previousValue + write.value - // validate offset? newValues[write.handle] = newValue } - // validate new values for (handle, newValue) in newValues { - let attribute = database[handle: handle] - // validate application errors with write callback if let error = willWrite?(attribute.uuid, handle, attribute.value, newValue) { - - errorResponse(opcode, error, handle) + await errorResponse(opcode, error, handle) return } } - // write new values for (handle, newValue) in newValues { - database.write(newValue, forAttribute: handle) } } - respond(ATTExecuteWriteResponse()) - + await respond(ATTExecuteWriteResponse()) for handle in newValues.keys { - - didWriteAttribute(handle) + await didWriteAttribute(handle) } } } // MARK: - Supporting Types -private extension GATTServer { +internal extension GATTServer { struct PreparedWrite { @@ -808,8 +740,6 @@ private extension GATTServer { } } -// MARK: - GATTDatabase Extensions - internal struct HandleRange { public let start: UInt16 From c7f85dd5178d487d6a698a2e63788199e0c56ed5 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 14 Nov 2021 10:54:21 -0500 Subject: [PATCH 15/44] Working on `async` support for `GATTClient` --- Sources/BluetoothGATT/GATTClient.swift | 45 +++++++++----------------- 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/Sources/BluetoothGATT/GATTClient.swift b/Sources/BluetoothGATT/GATTClient.swift index b23c24a06..22ba98d58 100755 --- a/Sources/BluetoothGATT/GATTClient.swift +++ b/Sources/BluetoothGATT/GATTClient.swift @@ -36,7 +36,7 @@ public actor GATTClient { internal private(set) var indications = [UInt16: Notification]() internal let connection: ATTConnection - + internal var backgroundTask: Task<(), Swift.Error>? // MARK: - Initialization @@ -62,18 +62,20 @@ public actor GATTClient { if maximumTransmissionUnit > .default { await self.exchangeMTU() } - // read in background - backgroundTask = Task.detached(priority: .userInitiated) { - - } - // write in background - await self.connection.setWritePending { - - } } // MARK: - Methods + /// Performs the actual IO for sending data. + public func read() async throws { + return try await connection.read() + } + + /// Performs the actual IO for recieving data. + public func write() async throws -> Bool { + return try await connection.write() + } + // MARK: Requests /// Discover All Primary Services @@ -346,13 +348,13 @@ public actor GATTClient { private func send ( _ request: Request, response: Response.Type - ) async throws -> ATTResponse { + ) async -> ATTResponse { assert(Response.attributeOpcode != .errorResponse) assert(Response.attributeOpcode.type == .response) assert(Request.attributeOpcode.type != .response) let log = self.log log?("Request: \(request)") - return try await withCheckedThrowingContinuation { [weak self] continuation in + return await withCheckedContinuation { [weak self] continuation in guard let self = self else { return } Task { let responseType: ATTProtocolDataUnit.Type = response @@ -363,32 +365,15 @@ public actor GATTClient { } guard let _ = await self.connection.queue(request, response: (callback, responseType)) else { fatalError("Could not add PDU to queue: \(request)") } - // do I/O - do { - // write pending - let didWrite = try await self.connection.write() - assert(didWrite, "Expected queued write operation") - try await self.connection.read() - // FIXME: Handle notifications - } - catch { - // I/O error - assert(type(of: error) != ATTErrorResponse.self) - continuation.resume(throwing: error) - } } } } - @inline(__always) - private func send(_ request: Request) async throws { + private func send(_ request: Request) async { log?("Request: \(request)") assert(Request.attributeOpcode.type != .response) guard let _ = await connection.queue(request) else { fatalError("Could not add PDU to queue: \(request)") } - // write pending PDU - let didWrite = try await self.connection.write() - assert(didWrite, "Expected queued write operation") } private func endHandle( @@ -425,7 +410,7 @@ public actor GATTClient { let clientMTU = preferredMaximumTransmissionUnit let request = ATTMaximumTransmissionUnitRequest(clientMTU: clientMTU.rawValue) do { - let response = try await send(request, response: ATTMaximumTransmissionUnitResponse.self) + let response = await send(request, response: ATTMaximumTransmissionUnitResponse.self) await exchangeMTUResponse(response) } catch { // I/O error From cff7eb690002f79eacaf4d50316f50e725212e3f Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 14 Nov 2021 11:05:17 -0500 Subject: [PATCH 16/44] Working on `async` support for `GATTClient` --- Sources/BluetoothGATT/GATTClient.swift | 43 +++++++++++--------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/Sources/BluetoothGATT/GATTClient.swift b/Sources/BluetoothGATT/GATTClient.swift index 22ba98d58..c9cb58916 100755 --- a/Sources/BluetoothGATT/GATTClient.swift +++ b/Sources/BluetoothGATT/GATTClient.swift @@ -409,14 +409,8 @@ public actor GATTClient { assert(didExchangeMTU == false) let clientMTU = preferredMaximumTransmissionUnit let request = ATTMaximumTransmissionUnitRequest(clientMTU: clientMTU.rawValue) - do { - let response = await send(request, response: ATTMaximumTransmissionUnitResponse.self) - await exchangeMTUResponse(response) - } catch { - // I/O error - assert(error as? ATTErrorResponse == nil) - log?("Could not exchange MTU: \(error)") - } + let response = await send(request, response: ATTMaximumTransmissionUnitResponse.self) + await exchangeMTUResponse(response) } private func discoverServices( @@ -439,7 +433,7 @@ public actor GATTClient { attributeType: attributeType.rawValue, attributeValue: uuid.littleEndian.data ) - let response = try await send(request, response: ATTFindByTypeResponse.self) + let response = await send(request, response: ATTFindByTypeResponse.self) try await findByTypeResponse(response, &operation) } else { let request = ATTReadByGroupTypeRequest( @@ -447,7 +441,7 @@ public actor GATTClient { endHandle: end, type: attributeType.uuid ) - let response = try await send(request, response: ATTReadByGroupTypeResponse.self) + let response = await send(request, response: ATTReadByGroupTypeResponse.self) try await readByGroupTypeResponse(response, &operation) } return operation.foundServices @@ -469,7 +463,7 @@ public actor GATTClient { endHandle: service.end, attributeType: attributeType.uuid ) - let response = try await send(request, response: ATTReadByTypeResponse.self) + let response = await send(request, response: ATTReadByTypeResponse.self) try await readByTypeResponse(response, &operation) return operation.foundCharacteristics } @@ -477,7 +471,7 @@ public actor GATTClient { private func discoverDescriptors(_ operation: inout DescriptorDiscoveryOperation) async throws { assert(operation.start <= operation.end, "Invalid range") let request = ATTFindInformationRequest(startHandle: operation.start, endHandle: operation.end) - let response = try await send(request, response: ATTFindInformationResponse.self) + let response = await send(request, response: ATTFindInformationResponse.self) try await findInformationResponse(response, &operation) } @@ -494,7 +488,7 @@ public actor GATTClient { // read value and try to read blob if too big var operation = ReadOperation(handle: handle) let request = ATTReadRequest(handle: handle) - let response = try await send(request, response: ATTReadResponse.self) + let response = await send(request, response: ATTReadResponse.self) try await readResponse(response, &operation) return operation.data } @@ -520,7 +514,7 @@ public actor GATTClient { handle: operation.handle, offset: operation.offset ) - let response = try await send(request, response: ATTReadBlobResponse.self) + let response = await send(request, response: ATTReadBlobResponse.self) try await readBlobResponse(response, &operation) } @@ -550,7 +544,7 @@ public actor GATTClient { let length = await Int(maximumTransmissionUnit.rawValue) - 3 let data = Data(data.prefix(length)) let command = ATTWriteCommand(handle: attribute, value: data) - try await send(command) + await send(command) } /// Write attribute request. @@ -561,7 +555,7 @@ public actor GATTClient { let length = await Int(maximumTransmissionUnit.rawValue) - 3 let data = Data(data.prefix(length)) let request = ATTWriteRequest(handle: attribute, value: data) - let response = try await send(request, response: ATTWriteResponse.self) + let response = await send(request, response: ATTWriteResponse.self) /** Write Response @@ -608,7 +602,7 @@ public actor GATTClient { lastRequest: request ) - let response = try await send(request, response: ATTPrepareWriteResponse.self) + let response = await send(request, response: ATTPrepareWriteResponse.self) try await prepareWriteResponse(response, &operation) } @@ -645,7 +639,7 @@ public actor GATTClient { // TODO: Sign Data let pdu = ATTWriteCommand(handle: characteristic.handle.value, value: data) - try await send(pdu) + await send(pdu) } // MARK: - Callbacks @@ -717,7 +711,7 @@ public actor GATTClient { endHandle: operation.end, type: operation.type.uuid ) - let response = try await send(request, response: ATTReadByGroupTypeResponse.self) + let response = await send(request, response: ATTReadByGroupTypeResponse.self) try await readByGroupTypeResponse(response, &operation) } } @@ -772,7 +766,7 @@ public actor GATTClient { attributeType: operation.type.rawValue, attributeValue: serviceUUID.littleEndian.data ) - let response = try await send(request, response: ATTFindByTypeResponse.self) + let response = await send(request, response: ATTFindByTypeResponse.self) try await findByTypeResponse(response, &operation) } } @@ -909,7 +903,7 @@ public actor GATTClient { endHandle: operation.end, attributeType: operation.type.uuid ) - let response = try await send(request, response: ATTReadByTypeResponse.self) + let response = await send(request, response: ATTReadByTypeResponse.self) try await readByTypeResponse(response, &operation) } } @@ -1021,7 +1015,7 @@ public actor GATTClient { operation.lastRequest = request operation.sentData += attributeValuePart - let response = try await send(request, response: ATTPrepareWriteResponse.self) + let response = await send(request, response: ATTPrepareWriteResponse.self) try await prepareWriteResponse(response, &operation) } else { @@ -1031,7 +1025,7 @@ public actor GATTClient { // all data sent let request = ATTExecuteWriteRequest.write - let response = try await send(request, response: ATTExecuteWriteResponse.self) + let response = await send(request, response: ATTExecuteWriteResponse.self) try executeWriteResponse(response) } } @@ -1062,8 +1056,7 @@ public actor GATTClient { private func indication(_ indication: ATTHandleValueIndication) async { let confirmation = ATTHandleValueConfirmation() // send acknowledgement - do { try await send(confirmation) } - catch { log?("Unable to send indication confirmation. \(error)") } + await send(confirmation) // callback guard let handler = indications[indication.handle] else { log?("Recieved indication for unregistered handle \(indication.handle)") From 802baee23cc1e79de2e233b8dff51926943538b7 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 14 Nov 2021 14:20:34 -0500 Subject: [PATCH 17/44] Working on `async` support for `GATTClient` --- Sources/BluetoothGATT/ATTConnection.swift | 2 - Sources/BluetoothGATT/GATTClient.swift | 68 +++++++++++------------ Sources/BluetoothGATT/GATTServer.swift | 7 --- 3 files changed, 34 insertions(+), 43 deletions(-) diff --git a/Sources/BluetoothGATT/ATTConnection.swift b/Sources/BluetoothGATT/ATTConnection.swift index 65cc97923..c7dc2b48e 100755 --- a/Sources/BluetoothGATT/ATTConnection.swift +++ b/Sources/BluetoothGATT/ATTConnection.swift @@ -128,7 +128,6 @@ internal actor ATTConnection { //log?("Sending data... (\(sendOperation.data.count) bytes)") try await socket.send(sendOperation.data) - let opcode = sendOperation.opcode //log?("Did write \(opcode)") @@ -321,7 +320,6 @@ internal actor ATTConnection { /// Verify the recieved response belongs to the pending request guard sendOperation.opcode == requestOpcode else { - throw Error.unexpectedResponse(data) } diff --git a/Sources/BluetoothGATT/GATTClient.swift b/Sources/BluetoothGATT/GATTClient.swift index c9cb58916..096a70eb6 100755 --- a/Sources/BluetoothGATT/GATTClient.swift +++ b/Sources/BluetoothGATT/GATTClient.swift @@ -14,8 +14,8 @@ public actor GATTClient { // MARK: - Properties - public var log: ((String) -> ())? - + public let log: ((String) -> ())? + public var maximumTransmissionUnit: ATTMaximumTransmissionUnit { get async { return await self.connection.maximumTransmissionUnit @@ -25,7 +25,7 @@ public actor GATTClient { public let preferredMaximumTransmissionUnit: ATTMaximumTransmissionUnit internal private(set) var didExchangeMTU = false - + /// Whether the client is currently writing a long value. internal private(set) var inLongWrite: Bool = false @@ -36,14 +36,10 @@ public actor GATTClient { internal private(set) var indications = [UInt16: Notification]() internal let connection: ATTConnection - - internal var backgroundTask: Task<(), Swift.Error>? - // MARK: - Initialization + private var sendContinuations = [UInt: (Swift.Error) -> ()]() - deinit { - backgroundTask?.cancel() - } + // MARK: - Initialization public init( socket: L2CAPSocket, @@ -60,19 +56,25 @@ public actor GATTClient { await self.registerATTHandlers() // queue MTU exchange if not default value if maximumTransmissionUnit > .default { - await self.exchangeMTU() + // run in background, actor isolation will make sure its sequential + Task { + do { try await self.exchangeMTU() } + catch { + log?("Could not exchange MTU: \(error)") + } + } } } // MARK: - Methods /// Performs the actual IO for sending data. - public func read() async throws { + internal func read() async throws { return try await connection.read() } /// Performs the actual IO for recieving data. - public func write() async throws -> Bool { + internal func write() async throws -> Bool { return try await connection.write() } @@ -85,9 +87,9 @@ public actor GATTClient { /// /// - Parameter completion: The completion closure. public func discoverAllPrimaryServices() async throws -> [Service] { - /// The Attribute Protocol Read By Group Type Request shall be used with - /// the Attribute Type parameter set to the UUID for «Primary Service». - /// The Starting Handle shall be set to 0x0001 and the Ending Handle shall be set to 0xFFFF. + // The Attribute Protocol Read By Group Type Request shall be used with + // the Attribute Type parameter set to the UUID for «Primary Service». + // The Starting Handle shall be set to 0x0001 and the Ending Handle shall be set to 0xFFFF. try await discoverServices( start: 0x0001, end: 0xFFFF, @@ -151,7 +153,6 @@ public actor GATTClient { of service: Service, by uuid: BluetoothUUID ) async throws -> [Characteristic] { - // The Attribute Protocol Read By Type Request is used to perform the beginning of the sub-procedure. // The Attribute Type is set to the UUID for «Characteristic» and the Starting Handle and Ending Handle // parameters shall be set to the service handle range. @@ -211,7 +212,6 @@ public actor GATTClient { public func readCharacteristics( _ characteristics: [Characteristic] ) async throws -> Data { - // The Attribute Protocol Read Multiple Request is used with the Set Of Handles parameter set to the Characteristic Value Handles. // The Read Multiple Response returns the Characteristic Values in the Set Of Values parameter. let handles = characteristics.map { $0.handle.value } @@ -348,13 +348,13 @@ public actor GATTClient { private func send ( _ request: Request, response: Response.Type - ) async -> ATTResponse { + ) async throws -> ATTResponse { assert(Response.attributeOpcode != .errorResponse) assert(Response.attributeOpcode.type == .response) assert(Request.attributeOpcode.type != .response) let log = self.log log?("Request: \(request)") - return await withCheckedContinuation { [weak self] continuation in + return try await withCheckedThrowingContinuation { [weak self] continuation in guard let self = self else { return } Task { let responseType: ATTProtocolDataUnit.Type = response @@ -405,11 +405,11 @@ public actor GATTClient { // MARK: Requests /// Exchange MTU (should only be called once if not using default MTU) - private func exchangeMTU() async { + private func exchangeMTU() async throws { assert(didExchangeMTU == false) let clientMTU = preferredMaximumTransmissionUnit let request = ATTMaximumTransmissionUnitRequest(clientMTU: clientMTU.rawValue) - let response = await send(request, response: ATTMaximumTransmissionUnitResponse.self) + let response = try await send(request, response: ATTMaximumTransmissionUnitResponse.self) await exchangeMTUResponse(response) } @@ -433,7 +433,7 @@ public actor GATTClient { attributeType: attributeType.rawValue, attributeValue: uuid.littleEndian.data ) - let response = await send(request, response: ATTFindByTypeResponse.self) + let response = try await send(request, response: ATTFindByTypeResponse.self) try await findByTypeResponse(response, &operation) } else { let request = ATTReadByGroupTypeRequest( @@ -441,7 +441,7 @@ public actor GATTClient { endHandle: end, type: attributeType.uuid ) - let response = await send(request, response: ATTReadByGroupTypeResponse.self) + let response = try await send(request, response: ATTReadByGroupTypeResponse.self) try await readByGroupTypeResponse(response, &operation) } return operation.foundServices @@ -463,7 +463,7 @@ public actor GATTClient { endHandle: service.end, attributeType: attributeType.uuid ) - let response = await send(request, response: ATTReadByTypeResponse.self) + let response = try await send(request, response: ATTReadByTypeResponse.self) try await readByTypeResponse(response, &operation) return operation.foundCharacteristics } @@ -471,7 +471,7 @@ public actor GATTClient { private func discoverDescriptors(_ operation: inout DescriptorDiscoveryOperation) async throws { assert(operation.start <= operation.end, "Invalid range") let request = ATTFindInformationRequest(startHandle: operation.start, endHandle: operation.end) - let response = await send(request, response: ATTFindInformationResponse.self) + let response = try await send(request, response: ATTFindInformationResponse.self) try await findInformationResponse(response, &operation) } @@ -488,7 +488,7 @@ public actor GATTClient { // read value and try to read blob if too big var operation = ReadOperation(handle: handle) let request = ATTReadRequest(handle: handle) - let response = await send(request, response: ATTReadResponse.self) + let response = try await send(request, response: ATTReadResponse.self) try await readResponse(response, &operation) return operation.data } @@ -514,7 +514,7 @@ public actor GATTClient { handle: operation.handle, offset: operation.offset ) - let response = await send(request, response: ATTReadBlobResponse.self) + let response = try await send(request, response: ATTReadBlobResponse.self) try await readBlobResponse(response, &operation) } @@ -555,7 +555,7 @@ public actor GATTClient { let length = await Int(maximumTransmissionUnit.rawValue) - 3 let data = Data(data.prefix(length)) let request = ATTWriteRequest(handle: attribute, value: data) - let response = await send(request, response: ATTWriteResponse.self) + let response = try await send(request, response: ATTWriteResponse.self) /** Write Response @@ -602,7 +602,7 @@ public actor GATTClient { lastRequest: request ) - let response = await send(request, response: ATTPrepareWriteResponse.self) + let response = try await send(request, response: ATTPrepareWriteResponse.self) try await prepareWriteResponse(response, &operation) } @@ -711,7 +711,7 @@ public actor GATTClient { endHandle: operation.end, type: operation.type.uuid ) - let response = await send(request, response: ATTReadByGroupTypeResponse.self) + let response = try await send(request, response: ATTReadByGroupTypeResponse.self) try await readByGroupTypeResponse(response, &operation) } } @@ -766,7 +766,7 @@ public actor GATTClient { attributeType: operation.type.rawValue, attributeValue: serviceUUID.littleEndian.data ) - let response = await send(request, response: ATTFindByTypeResponse.self) + let response = try await send(request, response: ATTFindByTypeResponse.self) try await findByTypeResponse(response, &operation) } } @@ -903,7 +903,7 @@ public actor GATTClient { endHandle: operation.end, attributeType: operation.type.uuid ) - let response = await send(request, response: ATTReadByTypeResponse.self) + let response = try await send(request, response: ATTReadByTypeResponse.self) try await readByTypeResponse(response, &operation) } } @@ -1015,7 +1015,7 @@ public actor GATTClient { operation.lastRequest = request operation.sentData += attributeValuePart - let response = await send(request, response: ATTPrepareWriteResponse.self) + let response = try await send(request, response: ATTPrepareWriteResponse.self) try await prepareWriteResponse(response, &operation) } else { @@ -1025,7 +1025,7 @@ public actor GATTClient { // all data sent let request = ATTExecuteWriteRequest.write - let response = await send(request, response: ATTExecuteWriteResponse.self) + let response = try await send(request, response: ATTExecuteWriteResponse.self) try executeWriteResponse(response) } } diff --git a/Sources/BluetoothGATT/GATTServer.swift b/Sources/BluetoothGATT/GATTServer.swift index f1a57d9c4..104379016 100644 --- a/Sources/BluetoothGATT/GATTServer.swift +++ b/Sources/BluetoothGATT/GATTServer.swift @@ -46,12 +46,10 @@ public actor GATTServer { maximumPreparedWrites: Int = 50, log: ((String) -> ())? = nil ) async { - // set initial MTU and register handlers self.maximumPreparedWrites = maximumPreparedWrites self.preferredMaximumTransmissionUnit = maximumTransmissionUnit self.connection = ATTConnection(socket: socket, log: log) - // async register handlers await self.registerATTHandlers() } @@ -347,15 +345,10 @@ public actor GATTServer { // Guard against invalid access if offset equals to value length if offset == UInt16(attribute.value.count) { - value = Data() - } else if offset > 0 { - value = Data(attribute.value.suffix(from: Int(offset))) - } else { - value = attribute.value } From 95d03cc66f5a7cb4f020ae7181b6ab24d31adb0d Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 18 Dec 2021 10:41:23 -0500 Subject: [PATCH 18/44] Updated deployment target --- Package.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index cb98f718f..43957e0bd 100644 --- a/Package.swift +++ b/Package.swift @@ -10,10 +10,10 @@ let libraryType: PackageDescription.Product.Library.LibraryType = .static let package = Package( name: "Bluetooth", platforms: [ - .macOS(.v12), - .iOS(.v15), - .watchOS(.v8), - .tvOS(.v15), + .macOS(.v10_15), + .iOS(.v13), + .watchOS(.v6), + .tvOS(.v13), ], products: [ .library( From 89b27313bc8ea44109da39cebc3a275c57558bcd Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 18 Dec 2021 10:56:42 -0500 Subject: [PATCH 19/44] [BluetoothHCI] Working on async support --- .../BluetoothHostController.swift | 37 ++++------- .../HCIAcceptConnectionRequest.swift | 4 +- .../HCIAuthenticationRequested.swift | 4 +- .../HCIChangeConnectionPacketType.swift | 4 +- Sources/BluetoothHCI/HCICommand.swift | 9 ++- .../BluetoothHCI/HCICreateConnection.swift | 4 +- .../HCICreateConnectionCancel.swift | 4 +- .../BluetoothHCI/HCIDeleteStoredLinkKey.swift | 4 +- Sources/BluetoothHCI/HCIDisconnect.swift | 4 +- .../HCIExitPeriodicInquiryMode.swift | 4 +- .../HCIIOCapabilityRequestReply.swift | 4 +- Sources/BluetoothHCI/HCIInquiry.swift | 5 +- Sources/BluetoothHCI/HCIInquiryCancel.swift | 4 +- ...ILEAddDeviceToPeriodicAdvertiserList.swift | 4 +- .../HCILEAddDeviceToResolvingList.swift | 4 +- .../HCILEAddDeviceToWhiteList.swift | 4 +- .../BluetoothHCI/HCILECreateConnection.swift | 16 ++--- Sources/BluetoothHCI/HCILEEncrypt.swift | 4 +- .../HCILEEnhancedReceiverTest.swift | 4 +- .../HCILEEnhancedTransmitterTest.swift | 4 +- .../HCILEExtendedCreateConnection.swift | 4 +- Sources/BluetoothHCI/HCILEGenerateDHKey.swift | 4 +- ...HCILELongTermKeyRequestNegativeReply.swift | 4 +- .../HCILELongTermKeyRequestReply.swift | 4 +- .../HCILEPeriodicAdvertisingCreateSync.swift | 4 +- ...CILEPeriodicAdvertisingTerminateSync.swift | 4 +- Sources/BluetoothHCI/HCILERandom.swift | 4 +- .../HCILEReadAdvertisingChannelTxPower.swift | 4 +- .../BluetoothHCI/HCILEReadBufferSize.swift | 4 +- .../BluetoothHCI/HCILEReadChannelMap.swift | 4 +- ...CILEReadLocalResolvableAddressReturn.swift | 4 +- .../HCILEReadLocalSupportedFeatures.swift | 4 +- ...CILEReadMaximumAdvertisingDataLength.swift | 4 +- .../HCILEReadMaximumDataLength.swift | 4 +- ...ReadNumberOfSupportedAdvertisingSets.swift | 4 +- ...HCILEReadPeerResolvableAddressReturn.swift | 4 +- ...HCILEReadPeriodicAdvertisingListSize.swift | 4 +- Sources/BluetoothHCI/HCILEReadPhy.swift | 4 +- .../HCILEReadRemoteUsedFeatures.swift | 4 +- .../HCILEReadResolvingListSize.swift | 4 +- .../HCILEReadRfPathCompensation.swift | 4 +- .../HCILEReadSuggestedDefaultDataLength.swift | 4 +- .../HCILEReadSupportedStates.swift | 4 +- .../BluetoothHCI/HCILEReadTransmitPower.swift | 4 +- .../BluetoothHCI/HCILEReadWhiteListSize.swift | 4 +- Sources/BluetoothHCI/HCILEReceiverTest.swift | 4 +- ...nectionParameterRequestNegativeReply.swift | 4 +- ...emoteConnectionParameterRequestReply.swift | 4 +- .../HCILERemoveAdvertisingSet.swift | 4 +- .../HCILERemoveDeviceFromResolvingList.swift | 4 +- .../HCILERemoveDeviceFromWhiteList.swift | 4 +- ...RemoveDeviceToPeriodicAdvertiserList.swift | 4 +- .../HCILESetAddressResolutionEnable.swift | 4 +- .../HCILESetAdvertiseEnable.swift | 5 +- .../HCILESetAdvertisingData.swift | 4 +- .../HCILESetAdvertisingParameters.swift | 4 +- .../HCILESetAdvertisingSetRandomAddress.swift | 4 +- Sources/BluetoothHCI/HCILESetDataLength.swift | 4 +- Sources/BluetoothHCI/HCILESetDefaultPhy.swift | 4 +- Sources/BluetoothHCI/HCILESetEventMask.swift | 4 +- .../HCILESetExtendedAdvertisingData.swift | 4 +- ...CILESetExtendedAdvertisingParameters.swift | 4 +- .../HCILESetExtendedScanEnable.swift | 4 +- .../HCILESetExtendedScanParameters.swift | 4 +- .../HCILESetExtendedScanResponseData.swift | 4 +- .../HCILESetPeriodicAdvertisingData.swift | 4 +- .../HCILESetPeriodicAdvertisingEnable.swift | 4 +- ...CILESetPeriodicAdvertisingParameters.swift | 4 +- Sources/BluetoothHCI/HCILESetPhy.swift | 4 +- .../BluetoothHCI/HCILESetPrivacyMode.swift | 4 +- .../BluetoothHCI/HCILESetRandomAddress.swift | 6 +- ...LESetResolvablePrivateAddressTimeout.swift | 4 +- Sources/BluetoothHCI/HCILESetScanEnable.swift | 61 ++++++++++--------- .../HCILESetScanResponseData.swift | 4 +- .../BluetoothHCI/HCILEStartEncryption.swift | 4 +- Sources/BluetoothHCI/HCILETestEnd.swift | 4 +- .../BluetoothHCI/HCILETransmitterTest.swift | 4 +- .../BluetoothHCI/HCILEUpdateConnection.swift | 4 +- .../HCILEWriteRfPathCompensation.swift | 4 +- ...HCILEWriteSuggestedDefaultDataLength.swift | 4 +- .../HCILinkKeyRequestNegativeReply.swift | 4 +- .../BluetoothHCI/HCILinkKeyRequestReply.swift | 4 +- .../BluetoothHCI/HCIPINCodeRequestReply.swift | 4 +- .../BluetoothHCI/HCIPeriodicInquiryMode.swift | 4 +- Sources/BluetoothHCI/HCIQoSSetup.swift | 4 +- .../BluetoothHCI/HCIReadClassOfDevice.swift | 4 +- Sources/BluetoothHCI/HCIReadClockOffset.swift | 4 +- .../BluetoothHCI/HCIReadDataBlockSize.swift | 4 +- .../BluetoothHCI/HCIReadDeviceAddress.swift | 4 +- Sources/BluetoothHCI/HCIReadLMPHandle.swift | 4 +- Sources/BluetoothHCI/HCIReadLocalName.swift | 4 +- .../HCIReadLocalSupportedFeatures.swift | 4 +- .../HCIReadLocalVersionInformation.swift | 4 +- Sources/BluetoothHCI/HCIReadPageTimeout.swift | 4 +- .../HCIReadRemoteExtendedFeatures.swift | 4 +- .../HCIReadRemoteSupportedFeatures.swift | 4 +- .../HCIReadRemoteVersionInformation.swift | 4 +- .../BluetoothHCI/HCIReadStoredLinkKey.swift | 4 +- .../HCIRejectConnectionRequest.swift | 4 +- .../BluetoothHCI/HCIRemoteNameRequest.swift | 4 +- Sources/BluetoothHCI/HCIReset.swift | 4 +- .../HCISetConnectionEncryption.swift | 4 +- .../HCIUserConfirmationRequestReply.swift | 16 +++-- .../BluetoothHCI/HCIWriteClassOfDevice.swift | 4 +- .../HCIWriteLinkPolicySettings.swift | 4 +- .../HCIWriteLinkSupervisionTimeout.swift | 4 +- Sources/BluetoothHCI/HCIWriteLocalName.swift | 4 +- .../HCIWritePageScanActivity.swift | 4 +- .../BluetoothHCI/HCIWritePageScanType.swift | 4 +- .../BluetoothHCI/HCIWritePageTimeout.swift | 4 +- Sources/BluetoothHCI/HCIWriteScanEnable.swift | 4 +- .../BluetoothHCI/LowEnergyAdvertising.swift | 12 ++-- .../BluetoothHCI/LowEnergyConnection.swift | 8 +-- .../BluetoothHCI/LowEnergyResolvingList.swift | 6 +- Sources/BluetoothHCI/LowEnergyWhiteList.swift | 4 +- Sources/BluetoothHCI/iBeacon.swift | 10 +-- 116 files changed, 297 insertions(+), 310 deletions(-) diff --git a/Sources/BluetoothHCI/BluetoothHostController.swift b/Sources/BluetoothHCI/BluetoothHostController.swift index c78cd9dba..0b06e0c6e 100644 --- a/Sources/BluetoothHCI/BluetoothHostController.swift +++ b/Sources/BluetoothHCI/BluetoothHostController.swift @@ -15,33 +15,35 @@ public protocol BluetoothHostControllerInterface: AnyObject { static var controllers: [Self] { get } /// Send an HCI command to the controller. - func deviceCommand (_ command: C) throws + func deviceCommand (_ command: C) async throws /// Send an HCI command with parameters to the controller. - func deviceCommand (_ commandParameter: CP) throws + func deviceCommand (_ commandParameter: CP) async throws /// Send a command to the controller and wait for response. - func deviceRequest(_ command: C, timeout: HCICommandTimeout) throws + func deviceRequest(_ command: C, timeout: HCICommandTimeout) async throws /// Send a command to the controller and wait for response. - func deviceRequest(_ command: C, _ eventParameterType: EP.Type, timeout: HCICommandTimeout) throws -> EP + func deviceRequest(_ command: C, _ eventParameterType: EP.Type, timeout: HCICommandTimeout) async throws -> EP /// Send a command to the controller and wait for response. - func deviceRequest(_ commandParameter: CP, timeout: HCICommandTimeout) throws + func deviceRequest(_ commandParameter: CP, timeout: HCICommandTimeout) async throws /// Sends a command to the device and waits for a response. - func deviceRequest (_ commandParameter: CP, _ eventParameterType: EP.Type, timeout: HCICommandTimeout) throws -> EP + func deviceRequest (_ commandParameter: CP, _ eventParameterType: EP.Type, timeout: HCICommandTimeout) async throws -> EP /// Sends a command to the device and waits for a response with return parameter values. - func deviceRequest (_ commandReturnType: Return.Type, timeout: HCICommandTimeout) throws -> Return + func deviceRequest (_ commandReturnType: Return.Type, timeout: HCICommandTimeout) async throws -> Return /// Sends a command to the device and waits for a response with return parameter values. - func deviceRequest (_ commandParameter: CP, _ commandReturnType: Return.Type, timeout: HCICommandTimeout) throws -> Return + func deviceRequest (_ commandParameter: CP, _ commandReturnType: Return.Type, timeout: HCICommandTimeout) async throws -> Return /// Polls and waits for events. - func pollEvent (_ eventParameterType: EP.Type, - shouldContinue: () -> (Bool), - event: (EP) throws -> ()) throws + func pollEvent ( + _ eventParameterType: EP.Type, + shouldContinue: () -> (Bool), + event: (EP) async throws -> () + ) async throws } /// Bluetooth HCI errors @@ -65,19 +67,6 @@ public enum BluetoothHostControllerError: Error { public extension BluetoothHostControllerInterface { static var `default`: Self? { - return controllers.first } } - -public extension BluetoothHostControllerInterface { - - @available(*, deprecated, message: "Use readDeviceAddress() instead") - var address: BluetoothAddress { - - guard let address = try? self.readDeviceAddress() - else { return .zero } - - return address - } -} diff --git a/Sources/BluetoothHCI/HCIAcceptConnectionRequest.swift b/Sources/BluetoothHCI/HCIAcceptConnectionRequest.swift index e57d649ec..6d8b64ffd 100644 --- a/Sources/BluetoothHCI/HCIAcceptConnectionRequest.swift +++ b/Sources/BluetoothHCI/HCIAcceptConnectionRequest.swift @@ -15,11 +15,11 @@ public extension BluetoothHostControllerInterface { /// The Accept_Connection_Request command is used to accept a new incoming connection request. The Accept_Connection_Request command shall only be issued after a Connection Request event has occurred. The Connection Request event will return the BD_ADDR of the device which is requesting the connection. This command will cause the Link Manager to create a connection to the BR/EDR Controller, with the BD_ADDR specified by the command parameters. The Link Manager will determine how the new connection will be established. This will be determined by the current state of the device, its piconet, and the state of the device to be connected. The Role command parameter allows the Host to specify if the Link Manager shall request a role switch and become the Master for this connection. This is a preference and not a requirement. If the Role Switch fails then the connection will still be accepted, and the Role Discovery Command will reflect the current role. func acceptConnection(address: BluetoothAddress, role: HCIAcceptConnectionRequest.Role, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let acceptConnection = HCIAcceptConnectionRequest(address: address, role: role) - let commandStatus = try deviceRequest(acceptConnection, HCICommandStatus.self, timeout: timeout) + let commandStatus = try await deviceRequest(acceptConnection, HCICommandStatus.self, timeout: timeout) switch commandStatus.status { case let .error(error): diff --git a/Sources/BluetoothHCI/HCIAuthenticationRequested.swift b/Sources/BluetoothHCI/HCIAuthenticationRequested.swift index a53b6448c..c7cac02b0 100644 --- a/Sources/BluetoothHCI/HCIAuthenticationRequested.swift +++ b/Sources/BluetoothHCI/HCIAuthenticationRequested.swift @@ -18,11 +18,11 @@ public extension BluetoothHostControllerInterface { /// /// - Note: The Connection_Handle command parameter is used to identify the other BR/EDR Controller, which forms the connection. The Connection_Handle should be a Connection_Handle for an ACL connection. func authenticationRequested(handle: UInt16, - timeout: HCICommandTimeout = .default) throws -> HCIStatus { + timeout: HCICommandTimeout = .default) async throws -> HCIStatus { let command = HCIAuthenticationRequested(handle: handle) - return try deviceRequest(command, + return try await deviceRequest(command, HCIAuthenticationComplete.self, timeout: timeout).status } diff --git a/Sources/BluetoothHCI/HCIChangeConnectionPacketType.swift b/Sources/BluetoothHCI/HCIChangeConnectionPacketType.swift index 1112b6065..0ba8bfcd5 100644 --- a/Sources/BluetoothHCI/HCIChangeConnectionPacketType.swift +++ b/Sources/BluetoothHCI/HCIChangeConnectionPacketType.swift @@ -17,11 +17,11 @@ public extension BluetoothHostControllerInterface { /// The Change_Connection_Packet_Type command is used to change which packet types can be used for a connection that is currently established. This allows current connections to be dynamically modified to support different types of user data. The Packet_Type command parameter specifies which packet types the Link Manager can use for the connection. When sending HCI ACL Data Packets the Link Manager shall only use the packet type(s) specified by the Packet_Type command parameter or the always-allowed DM1 packet type. The interpretation of the value for the Packet_Type command parameter will depend on the Link_Type command parameter returned in the Connection Complete event at the connection setup. Multiple packet types may be speci- fied for the Packet_Type command parameter by bitwise OR operation of the different packet types. For a definition of the different packet types see the Part B, Baseband Specification on page 59. func changeConnectionPacketType(handle: UInt16, packetType: PacketType, - timeout: HCICommandTimeout = .default) throws -> HCIStatus { + timeout: HCICommandTimeout = .default) async throws -> HCIStatus { let command = HCIChangeConnectionPacketType(handle: handle, packetType: packetType) - return try deviceRequest(command, + return try await deviceRequest(command, HCIAuthenticationComplete.self, timeout: timeout).status } diff --git a/Sources/BluetoothHCI/HCICommand.swift b/Sources/BluetoothHCI/HCICommand.swift index 016cb8522..240bba2c1 100644 --- a/Sources/BluetoothHCI/HCICommand.swift +++ b/Sources/BluetoothHCI/HCICommand.swift @@ -25,7 +25,6 @@ public protocol HCICommand: RawRepresentable, Hashable, CustomStringConvertible public extension HCICommand { var description: String { - return name } } @@ -46,9 +45,9 @@ public typealias HCIOpcodeCommandField = UInt16 /// HCI Command Parameter. public protocol HCICommandParameter { - associatedtype HCICommandType: HCICommand + associatedtype Command: HCICommand - static var command: HCICommandType { get } + static var command: Command { get } /// Converts command parameter to raw bytes. var data: Data { get } @@ -57,9 +56,9 @@ public protocol HCICommandParameter { /// The return value (not event) returned by an HCI command. public protocol HCICommandReturnParameter { - associatedtype HCICommandType: HCICommand + associatedtype Command: HCICommand - static var command: HCICommandType { get } + static var command: Command { get } /// Length of the command return parameter when encoded to data. static var length: Int { get } diff --git a/Sources/BluetoothHCI/HCICreateConnection.swift b/Sources/BluetoothHCI/HCICreateConnection.swift index 610df1227..b456feabe 100644 --- a/Sources/BluetoothHCI/HCICreateConnection.swift +++ b/Sources/BluetoothHCI/HCICreateConnection.swift @@ -20,7 +20,7 @@ public extension BluetoothHostControllerInterface { pageScanRepetitionMode: PageScanRepetitionMode, clockOffset: BitMaskOptionSet, allowRoleSwitch: HCICreateConnection.AllowRoleSwitch, - timeout: HCICommandTimeout = .default) throws -> HCIConnectionComplete { + timeout: HCICommandTimeout = .default) async throws -> HCIConnectionComplete { let createConnection = HCICreateConnection(address: address, packetType: packetType, @@ -28,7 +28,7 @@ public extension BluetoothHostControllerInterface { clockOffset: clockOffset, allowRoleSwitch: allowRoleSwitch) - return try deviceRequest(createConnection, HCIConnectionComplete.self, timeout: timeout) + return try await deviceRequest(createConnection, HCIConnectionComplete.self, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCICreateConnectionCancel.swift b/Sources/BluetoothHCI/HCICreateConnectionCancel.swift index dab83c0f9..045fe37e1 100644 --- a/Sources/BluetoothHCI/HCICreateConnectionCancel.swift +++ b/Sources/BluetoothHCI/HCICreateConnectionCancel.swift @@ -16,11 +16,11 @@ public extension BluetoothHostControllerInterface { /// /// This command is used to request cancellation of the ongoing connection creation process, which was started by a Create_Connection command of the local BR/EDR Controller. func cancelConnection(address: BluetoothAddress, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let createConnectionCancel = HCICreateConnectionCancel(address: address) - try deviceRequest(createConnectionCancel, timeout: timeout) + try await deviceRequest(createConnectionCancel, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCIDeleteStoredLinkKey.swift b/Sources/BluetoothHCI/HCIDeleteStoredLinkKey.swift index 742bf94d2..ba3f99958 100644 --- a/Sources/BluetoothHCI/HCIDeleteStoredLinkKey.swift +++ b/Sources/BluetoothHCI/HCIDeleteStoredLinkKey.swift @@ -17,11 +17,11 @@ public extension BluetoothHostControllerInterface { /// The Delete_Stored_Link_Key command provides the ability to remove one or more of the link keys stored in the BR/EDR Controller. The BR/EDR Controller can store a limited number of link keys for other BR/EDR devices. Link keys are shared between two BR/EDR devices and are used for all security transac- tions between the two devices. The Delete_All_Flag parameter is used to indicate if all of the stored Link Keys should be deleted. If the Delete_All_Flag indicates that all Link Keys are to be deleted, then the BD_ADDR command parameter must be ignored This command provides the ability to negate all security agreements between two devices. The BD_ADDR command parameter is used to identify which link key to delete. If a link key is currently in use for a connection, then the link key will be deleted when all of the connections are disconnected. func deleteStoredLinkKey(address: BluetoothAddress, deleteFlag: HCIDeleteStoredLinkKey.DeleteFlag, - timeout: HCICommandTimeout = .default) throws -> HCIDeleteStoredLinkKeyReturn { + timeout: HCICommandTimeout = .default) async throws -> HCIDeleteStoredLinkKeyReturn { let command = HCIDeleteStoredLinkKey(address: address, deleteFlag: deleteFlag) - return try deviceRequest(command, HCIDeleteStoredLinkKeyReturn.self, timeout: timeout) + return try await deviceRequest(command, HCIDeleteStoredLinkKeyReturn.self, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCIDisconnect.swift b/Sources/BluetoothHCI/HCIDisconnect.swift index ca20e19f2..37ac41a47 100644 --- a/Sources/BluetoothHCI/HCIDisconnect.swift +++ b/Sources/BluetoothHCI/HCIDisconnect.swift @@ -17,11 +17,11 @@ public extension BluetoothHostControllerInterface { /// The Disconnection command is used to terminate an existing connection. The Connection_Handle command parameter indicates which connection is to be disconnected. The Reason command parameter indicates the reason for ending the connection. The remote Controller will receive the Reason command parameter in the Disconnection Complete event. All synchronous connections on a physical link should be disconnected before the ACL connection on the same physical connection is disconnected. func disconnect(connectionHandle: UInt16, error: HCIError, - timeout: HCICommandTimeout = .default) throws -> HCIDisconnectionComplete { + timeout: HCICommandTimeout = .default) async throws -> HCIDisconnectionComplete { let disconnect = HCIDisconnect(connectionHandle: connectionHandle, error: error) - return try deviceRequest(disconnect, HCIDisconnectionComplete.self, timeout: timeout) + return try await deviceRequest(disconnect, HCIDisconnectionComplete.self, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCIExitPeriodicInquiryMode.swift b/Sources/BluetoothHCI/HCIExitPeriodicInquiryMode.swift index 125fab85f..255555da2 100644 --- a/Sources/BluetoothHCI/HCIExitPeriodicInquiryMode.swift +++ b/Sources/BluetoothHCI/HCIExitPeriodicInquiryMode.swift @@ -15,8 +15,8 @@ public extension BluetoothHostControllerInterface { /// Exit Periodic Inquiry Mode Command /// /// The Exit_Periodic_Inquiry_Mode command is used to end the Periodic Inquiry mode when the local device is in Periodic Inquiry Mode. If the BR/EDR Control- ler is currently in an Inquiry process, the Inquiry process shall be stopped directly and the BR/EDR Controller shall no longer perform periodic inquiries until the Periodic Inquiry Mode command is reissued. - func exitPeriodicInquiry(timeout: HCICommandTimeout = .default) throws { + func exitPeriodicInquiry(timeout: HCICommandTimeout = .default) async throws { - try deviceRequest(LinkControlCommand.exitPeriodicInquiry, timeout: timeout) + try await deviceRequest(LinkControlCommand.exitPeriodicInquiry, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCIIOCapabilityRequestReply.swift b/Sources/BluetoothHCI/HCIIOCapabilityRequestReply.swift index 360aa120f..14b89ea63 100644 --- a/Sources/BluetoothHCI/HCIIOCapabilityRequestReply.swift +++ b/Sources/BluetoothHCI/HCIIOCapabilityRequestReply.swift @@ -22,14 +22,14 @@ public extension BluetoothHostControllerInterface { ioCapability: HCIIOCapabilityRequestReply.IOCapability, obbDataPresent: HCIIOCapabilityRequestReply.OBBDataPresent, authenticationRequirements: HCIIOCapabilityRequestReply.AuthenticationRequirements, - timeout: HCICommandTimeout = .default) throws -> BluetoothAddress { + timeout: HCICommandTimeout = .default) async throws -> BluetoothAddress { let command = HCIIOCapabilityRequestReply(address: address, ioCapability: ioCapability, obbDataPresent: obbDataPresent, authenticationRequirements: authenticationRequirements) - return try deviceRequest(command, HCIIOCapabilityRequestReplyReturn.self, timeout: timeout).address + return try await deviceRequest(command, HCIIOCapabilityRequestReplyReturn.self, timeout: timeout).address } } diff --git a/Sources/BluetoothHCI/HCIInquiry.swift b/Sources/BluetoothHCI/HCIInquiry.swift index e491d981e..c71e18215 100644 --- a/Sources/BluetoothHCI/HCIInquiry.swift +++ b/Sources/BluetoothHCI/HCIInquiry.swift @@ -19,11 +19,10 @@ public extension BluetoothHostControllerInterface { duration: HCIInquiry.Duration = .max, responses: HCIInquiry.Responses, timeout: HCICommandTimeout = .default, - foundDevice: (HCIInquiryResult.Report) -> ()) throws { + foundDevice: (HCIInquiryResult.Report) -> ()) async throws { let inquiry = HCIInquiry(lap: lap, duration: duration, responses: responses) - - let commandStatus = try deviceRequest(inquiry, HCICommandStatus.self, timeout: timeout) + let commandStatus = try await deviceRequest(inquiry, HCICommandStatus.self, timeout: timeout) switch commandStatus.status { case let .error(error): diff --git a/Sources/BluetoothHCI/HCIInquiryCancel.swift b/Sources/BluetoothHCI/HCIInquiryCancel.swift index 7f6a45f0d..aa47b2723 100644 --- a/Sources/BluetoothHCI/HCIInquiryCancel.swift +++ b/Sources/BluetoothHCI/HCIInquiryCancel.swift @@ -16,8 +16,8 @@ public extension BluetoothHostControllerInterface { /// /// This command is used to start a test where the DUT receives test reference packets at a fixed interval. /// The tester generates the test reference packets. - func inquiryCancel(timeout: HCICommandTimeout = .default) throws { + func inquiryCancel(timeout: HCICommandTimeout = .default) async throws { - try deviceRequest(LinkControlCommand.inquiryCancel, timeout: timeout) + try await deviceRequest(LinkControlCommand.inquiryCancel, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILEAddDeviceToPeriodicAdvertiserList.swift b/Sources/BluetoothHCI/HCILEAddDeviceToPeriodicAdvertiserList.swift index 320e7cabd..4c696fe80 100644 --- a/Sources/BluetoothHCI/HCILEAddDeviceToPeriodicAdvertiserList.swift +++ b/Sources/BluetoothHCI/HCILEAddDeviceToPeriodicAdvertiserList.swift @@ -18,13 +18,13 @@ public extension BluetoothHostControllerInterface { func addDeviceToPeriodicAdvertiserList(advertiserAddressType: LowEnergyAdvertiserAddressType, address: BluetoothAddress, advertisingSid: UInt8, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameters = HCILEAddDeviceToPeriodicAdvertiserList(advertiserAddressType: advertiserAddressType, address: address, advertisingSid: advertisingSid) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILEAddDeviceToResolvingList.swift b/Sources/BluetoothHCI/HCILEAddDeviceToResolvingList.swift index bcc4bf71a..fb0ca47d5 100644 --- a/Sources/BluetoothHCI/HCILEAddDeviceToResolvingList.swift +++ b/Sources/BluetoothHCI/HCILEAddDeviceToResolvingList.swift @@ -16,11 +16,11 @@ public extension BluetoothHostControllerInterface { /// /// The command is used to add one device to the list of address translations /// used to resolve Resolvable Private Addresses in the Controller. - func lowEnergyAddDeviceToResolvingList(peerIdentifyAddressType: LowEnergyPeerIdentifyAddressType, peerIdentifyAddress: UInt64, peerIrk: UInt128, localIrk: UInt128, timeout: HCICommandTimeout = .default) throws { + func lowEnergyAddDeviceToResolvingList(peerIdentifyAddressType: LowEnergyPeerIdentifyAddressType, peerIdentifyAddress: UInt64, peerIrk: UInt128, localIrk: UInt128, timeout: HCICommandTimeout = .default) async throws { let parameters = HCILEAddDeviceToResolvingList(peerIdentifyAddressType: peerIdentifyAddressType, peerIdentifyAddress: peerIdentifyAddress, peerIrk: peerIrk, localIrk: localIrk) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILEAddDeviceToWhiteList.swift b/Sources/BluetoothHCI/HCILEAddDeviceToWhiteList.swift index f64ad2877..84ea2b299 100644 --- a/Sources/BluetoothHCI/HCILEAddDeviceToWhiteList.swift +++ b/Sources/BluetoothHCI/HCILEAddDeviceToWhiteList.swift @@ -15,9 +15,9 @@ public extension BluetoothHostControllerInterface { /// LE Add Device To White List Command /// /// Used to add a single device to the White List stored in the Controller. - func lowEnergyAddDeviceToWhiteList(_ whiteListDevice: LowEnergyWhiteListDevice, timeout: HCICommandTimeout = .default) throws { + func lowEnergyAddDeviceToWhiteList(_ whiteListDevice: LowEnergyWhiteListDevice, timeout: HCICommandTimeout = .default) async throws { - try deviceRequest(HCILEAddDeviceToWhiteList(device: whiteListDevice), timeout: timeout) + try await deviceRequest(HCILEAddDeviceToWhiteList(device: whiteListDevice), timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILECreateConnection.swift b/Sources/BluetoothHCI/HCILECreateConnection.swift index 78e222b0f..295e1c709 100644 --- a/Sources/BluetoothHCI/HCILECreateConnection.swift +++ b/Sources/BluetoothHCI/HCILECreateConnection.swift @@ -15,20 +15,22 @@ public extension BluetoothHostControllerInterface { func lowEnergyCreateConnection(address peerAddress: BluetoothAddress, type peerAddressType: LowEnergyAddressType = .public, ownAddressType: LowEnergyAddressType = .public, - timeout: HCICommandTimeout = .default) throws -> UInt16 { + timeout: HCICommandTimeout = .default) async throws -> UInt16 { - let parameters = HCILECreateConnection(peerAddressType: peerAddressType, - peerAddress: peerAddress, - ownAddressType: ownAddressType) + let parameters = HCILECreateConnection( + peerAddressType: peerAddressType, + peerAddress: peerAddress, + ownAddressType: ownAddressType + ) - return try lowEnergyCreateConnection(parameters: parameters, timeout: timeout).handle + return try await lowEnergyCreateConnection(parameters: parameters, timeout: timeout).handle } func lowEnergyCreateConnection(parameters: HCILECreateConnection, - timeout: HCICommandTimeout = .default) throws -> HCILEConnectionComplete { + timeout: HCICommandTimeout = .default) async throws -> HCILEConnectionComplete { // connect with specified parameters - let event = try deviceRequest(parameters, + let event = try await deviceRequest(parameters, HCILEConnectionComplete.self, timeout: timeout) diff --git a/Sources/BluetoothHCI/HCILEEncrypt.swift b/Sources/BluetoothHCI/HCILEEncrypt.swift index 6f702862f..b0a56ea93 100644 --- a/Sources/BluetoothHCI/HCILEEncrypt.swift +++ b/Sources/BluetoothHCI/HCILEEncrypt.swift @@ -18,11 +18,11 @@ public extension BluetoothHostControllerInterface { /// and returns the Encrypted_Data to the Host. func lowEnergyEncrypt(key: UInt128, data: UInt128, - timeout: HCICommandTimeout = .default) throws -> UInt128 { + timeout: HCICommandTimeout = .default) async throws -> UInt128 { let parameters = HCILEEncrypt(key: key, plainText: data) - let returnParameters = try deviceRequest(parameters, HCILEEncryptReturn.self, timeout: timeout) + let returnParameters = try await deviceRequest(parameters, HCILEEncryptReturn.self, timeout: timeout) return returnParameters.encryptedData } diff --git a/Sources/BluetoothHCI/HCILEEnhancedReceiverTest.swift b/Sources/BluetoothHCI/HCILEEnhancedReceiverTest.swift index 07637a3e5..70214427e 100644 --- a/Sources/BluetoothHCI/HCILEEnhancedReceiverTest.swift +++ b/Sources/BluetoothHCI/HCILEEnhancedReceiverTest.swift @@ -17,11 +17,11 @@ public extension BluetoothHostControllerInterface { /// This command is used to start a test where the DUT receives test /// reference packets at a fixed interval. The tester generates the test /// reference packets. - func lowEnergyEnhancedReceiverTest(rxChannel: LowEnergyRxChannel, phy: HCILEEnhancedReceiverTest.Phy, modulationIndex: HCILEEnhancedReceiverTest.ModulationIndex, timeout: HCICommandTimeout = .default) throws { + func lowEnergyEnhancedReceiverTest(rxChannel: LowEnergyRxChannel, phy: HCILEEnhancedReceiverTest.Phy, modulationIndex: HCILEEnhancedReceiverTest.ModulationIndex, timeout: HCICommandTimeout = .default) async throws { let parameters = HCILEEnhancedReceiverTest(rxChannel: rxChannel, phy: phy, modulationIndex: modulationIndex) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILEEnhancedTransmitterTest.swift b/Sources/BluetoothHCI/HCILEEnhancedTransmitterTest.swift index 6b022d73d..ac4f995bd 100644 --- a/Sources/BluetoothHCI/HCILEEnhancedTransmitterTest.swift +++ b/Sources/BluetoothHCI/HCILEEnhancedTransmitterTest.swift @@ -16,11 +16,11 @@ public extension BluetoothHostControllerInterface { /// /// This command is used to start a test where the DUT generates test reference packets /// at a fixed interval. The Controller shall transmit at maximum power. - func lowEnergyEnhancedTransmitterTest(txChannel: LowEnergyTxChannel, lengthOfTestData: UInt8, packetPayload: LowEnergyPacketPayload, phy: HCILEEnhancedTransmitterTest.Phy, timeout: HCICommandTimeout = .default) throws { + func lowEnergyEnhancedTransmitterTest(txChannel: LowEnergyTxChannel, lengthOfTestData: UInt8, packetPayload: LowEnergyPacketPayload, phy: HCILEEnhancedTransmitterTest.Phy, timeout: HCICommandTimeout = .default) async throws { let parameters = HCILEEnhancedTransmitterTest(txChannel: txChannel, lengthOfTestData: lengthOfTestData, packetPayload: packetPayload, phy: phy) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILEExtendedCreateConnection.swift b/Sources/BluetoothHCI/HCILEExtendedCreateConnection.swift index be3934efc..48b224c70 100644 --- a/Sources/BluetoothHCI/HCILEExtendedCreateConnection.swift +++ b/Sources/BluetoothHCI/HCILEExtendedCreateConnection.swift @@ -20,7 +20,7 @@ public extension BluetoothHostControllerInterface { peerAddressType: LowEnergyPeerIdentifyAddressType, peerAddress: BluetoothAddress, initialingPHY: HCILEExtendedCreateConnection.InitialingPHY, - timeout: HCICommandTimeout = .default) throws -> HCILEEnhancedConnectionComplete { + timeout: HCICommandTimeout = .default) async throws -> HCILEEnhancedConnectionComplete { let parameters = HCILEExtendedCreateConnection(initialingFilterPolicy: initialingFilterPolicy, ownAddressType: ownAddressType, @@ -28,7 +28,7 @@ public extension BluetoothHostControllerInterface { peerAddress: peerAddress, initialingPHY: initialingPHY) - let event = try deviceRequest(parameters, + let event = try await deviceRequest(parameters, HCILEEnhancedConnectionComplete.self, timeout: timeout) diff --git a/Sources/BluetoothHCI/HCILEGenerateDHKey.swift b/Sources/BluetoothHCI/HCILEGenerateDHKey.swift index 528cdb4fb..6c66164b6 100644 --- a/Sources/BluetoothHCI/HCILEGenerateDHKey.swift +++ b/Sources/BluetoothHCI/HCILEGenerateDHKey.swift @@ -18,11 +18,11 @@ public extension BluetoothHostControllerInterface { /// This command takes the remote P-256 public key as input. /// The Diffie-Hellman key generation uses the private key generated by LE_Read_Local_P256_Public_Key command. func lowEnergyGenerateDHKey( remoteP256PublicKey: UInt512, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameters = HCILEGenerateDHKey(remoteP256PublicKey: remoteP256PublicKey) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILELongTermKeyRequestNegativeReply.swift b/Sources/BluetoothHCI/HCILELongTermKeyRequestNegativeReply.swift index b11127cac..50834792e 100644 --- a/Sources/BluetoothHCI/HCILELongTermKeyRequestNegativeReply.swift +++ b/Sources/BluetoothHCI/HCILELongTermKeyRequestNegativeReply.swift @@ -16,11 +16,11 @@ public extension BluetoothHostControllerInterface { /// /// The command is used to reply to an LE Long Term Key Request event from /// the Controller if the Host cannot provide a Long Term Key for this Connection_Handle. - func lowEnergyLongTermKeyRequestNegativeReply(handle: UInt16, timeout: HCICommandTimeout = .default) throws -> UInt16 { + func lowEnergyLongTermKeyRequestNegativeReply(handle: UInt16, timeout: HCICommandTimeout = .default) async throws -> UInt16 { let parameters = HCILELongTermKeyRequestNegativeReply(connectionHandle: handle) - let returnParameters = try deviceRequest(parameters, HCILELongTermKeyRequestNegativeReplyReturn.self, timeout: timeout) + let returnParameters = try await deviceRequest(parameters, HCILELongTermKeyRequestNegativeReplyReturn.self, timeout: timeout) return returnParameters.connectionHandle } diff --git a/Sources/BluetoothHCI/HCILELongTermKeyRequestReply.swift b/Sources/BluetoothHCI/HCILELongTermKeyRequestReply.swift index 5b5a0b70d..d89f774ce 100644 --- a/Sources/BluetoothHCI/HCILELongTermKeyRequestReply.swift +++ b/Sources/BluetoothHCI/HCILELongTermKeyRequestReply.swift @@ -17,11 +17,11 @@ public extension BluetoothHostControllerInterface { /// The LE_Long_Term_Key_Request Reply command is used to reply to an LE Long Term Key Request event /// from the Controller, and specifies the Long_Term_Key parameter that shall be used for /// this Connection_Handle. - func lowEnergyLongTermKeyRequestReply(handle: UInt16, longTermKey: UInt128, timeout: HCICommandTimeout = .default) throws -> UInt16 { + func lowEnergyLongTermKeyRequestReply(handle: UInt16, longTermKey: UInt128, timeout: HCICommandTimeout = .default) async throws -> UInt16 { let parameters = HCILELongTermKeyRequestReply(connectionHandle: handle, longTermKey: longTermKey) - let returnParameters = try deviceRequest(parameters, HCILELongTermKeyRequestReplyReturn.self, timeout: timeout) + let returnParameters = try await deviceRequest(parameters, HCILELongTermKeyRequestReplyReturn.self, timeout: timeout) return returnParameters.connectionHandle } diff --git a/Sources/BluetoothHCI/HCILEPeriodicAdvertisingCreateSync.swift b/Sources/BluetoothHCI/HCILEPeriodicAdvertisingCreateSync.swift index 6c948f252..93cb10f26 100644 --- a/Sources/BluetoothHCI/HCILEPeriodicAdvertisingCreateSync.swift +++ b/Sources/BluetoothHCI/HCILEPeriodicAdvertisingCreateSync.swift @@ -17,9 +17,9 @@ public extension BluetoothHostControllerInterface { /// The command is used to synchronize with periodic advertising from an advertiser /// and begin receiving periodic advertising packets. func setPeriodicAdvertisingCreateSyncParameters(_ parameters: HCILEPeriodicAdvertisingCreateSync, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILEPeriodicAdvertisingTerminateSync.swift b/Sources/BluetoothHCI/HCILEPeriodicAdvertisingTerminateSync.swift index 74aff39aa..4ac9c3f56 100644 --- a/Sources/BluetoothHCI/HCILEPeriodicAdvertisingTerminateSync.swift +++ b/Sources/BluetoothHCI/HCILEPeriodicAdvertisingTerminateSync.swift @@ -16,11 +16,11 @@ public extension BluetoothHostControllerInterface { /// /// The command is used to stop reception of the periodic advertising identified by the Sync_Handle parameter. func setPeriodicAdvertisingTerminateSync(syncHandle: UInt16, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameters = HCILEPeriodicAdvertisingTerminateSync(syncHandle: syncHandle) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILERandom.swift b/Sources/BluetoothHCI/HCILERandom.swift index 48118b495..5067649d8 100644 --- a/Sources/BluetoothHCI/HCILERandom.swift +++ b/Sources/BluetoothHCI/HCILERandom.swift @@ -15,9 +15,9 @@ public extension BluetoothHostControllerInterface { /// LE Rand Command /// /// The command is used to request the Controller to generate 8 octets of random data to be sent to the Host. - func lowEnergyRandom(timeout: HCICommandTimeout = .default) throws -> UInt64 { + func lowEnergyRandom(timeout: HCICommandTimeout = .default) async throws -> UInt64 { - let returnParameters = try deviceRequest(HCILERandom.self, timeout: timeout) + let returnParameters = try await deviceRequest(HCILERandom.self, timeout: timeout) return returnParameters.randomNumber } diff --git a/Sources/BluetoothHCI/HCILEReadAdvertisingChannelTxPower.swift b/Sources/BluetoothHCI/HCILEReadAdvertisingChannelTxPower.swift index 1977ce484..19b1aa51e 100644 --- a/Sources/BluetoothHCI/HCILEReadAdvertisingChannelTxPower.swift +++ b/Sources/BluetoothHCI/HCILEReadAdvertisingChannelTxPower.swift @@ -15,9 +15,9 @@ public extension BluetoothHostControllerInterface { /// LE Read Advertising Channel Tx Power Command /// /// The command is used by the Host to read the transmit power level used for LE advertising channel packets. - func readAdvertisingChannelTxPower(timeout: HCICommandTimeout = .default) throws -> HCILEReadAdvertisingChannelTxPower.TransmitPowerLevel { + func readAdvertisingChannelTxPower(timeout: HCICommandTimeout = .default) async throws -> HCILEReadAdvertisingChannelTxPower.TransmitPowerLevel { - let value = try deviceRequest(HCILEReadAdvertisingChannelTxPower.self, + let value = try await deviceRequest(HCILEReadAdvertisingChannelTxPower.self, timeout: timeout) return value.transmitPowerLevel diff --git a/Sources/BluetoothHCI/HCILEReadBufferSize.swift b/Sources/BluetoothHCI/HCILEReadBufferSize.swift index 5b9f0ffdb..a9f3deb5c 100644 --- a/Sources/BluetoothHCI/HCILEReadBufferSize.swift +++ b/Sources/BluetoothHCI/HCILEReadBufferSize.swift @@ -15,9 +15,9 @@ public extension BluetoothHostControllerInterface { /// LE Read Buffer Size Command /// /// The command is used to read the maximum size of the data portion of HCI LE ACL Data Packets sent from the Host to the Controller. - func readBufferSize(timeout: HCICommandTimeout = .default) throws -> HCILEReadBufferSize { + func readBufferSize(timeout: HCICommandTimeout = .default) async throws -> HCILEReadBufferSize { - let bufferSizeReturnParameter = try deviceRequest(HCILEReadBufferSize.self, timeout: timeout) + let bufferSizeReturnParameter = try await deviceRequest(HCILEReadBufferSize.self, timeout: timeout) return bufferSizeReturnParameter } diff --git a/Sources/BluetoothHCI/HCILEReadChannelMap.swift b/Sources/BluetoothHCI/HCILEReadChannelMap.swift index 495dd63bb..92b777482 100644 --- a/Sources/BluetoothHCI/HCILEReadChannelMap.swift +++ b/Sources/BluetoothHCI/HCILEReadChannelMap.swift @@ -17,11 +17,11 @@ public extension BluetoothHostControllerInterface { /// Returns the current Channel_Map for the specified Connection_Handle. The returned value indicates the state /// of the Channel_Map specified by the last transmitted or received Channel_Map (in a CONNECT_IND or LL_CHANNEL_MAP_IND message) /// for the specified Connection_Handle, regardless of whether the Master has received an acknowledgment. - func lowEnergyReadChannelMap(handle: UInt16, timeout: HCICommandTimeout = .default) throws -> LowEnergyChannelMap { + func lowEnergyReadChannelMap(handle: UInt16, timeout: HCICommandTimeout = .default) async throws -> LowEnergyChannelMap { let parameters = HCILEReadChannelMap(connectionHandle: handle) - let returnParameters = try deviceRequest(parameters, HCILEReadChannelMap.ReturnParameter.self, timeout: timeout) + let returnParameters = try await deviceRequest(parameters, HCILEReadChannelMap.ReturnParameter.self, timeout: timeout) return returnParameters.channelMap } diff --git a/Sources/BluetoothHCI/HCILEReadLocalResolvableAddressReturn.swift b/Sources/BluetoothHCI/HCILEReadLocalResolvableAddressReturn.swift index 4250dbad8..608c4db06 100644 --- a/Sources/BluetoothHCI/HCILEReadLocalResolvableAddressReturn.swift +++ b/Sources/BluetoothHCI/HCILEReadLocalResolvableAddressReturn.swift @@ -19,11 +19,11 @@ public extension BluetoothHostControllerInterface { /// being used may change after the command is called. func lowEnergyReadLocalResolvableAddress(peerIdentifyAddressType: LowEnergyPeerIdentifyAddressType, peerIdentifyAddress: UInt64, - timeout: HCICommandTimeout = .default) throws -> UInt64 { + timeout: HCICommandTimeout = .default) async throws -> UInt64 { let parameters = HCILEReadLocalResolvableAddress(peerIdentifyAddressType: peerIdentifyAddressType, peerIdentifyAddress: peerIdentifyAddress) - let returnParameters = try deviceRequest(parameters, HCILEReadLocalResolvableAddressReturn.self, timeout: timeout) + let returnParameters = try await deviceRequest(parameters, HCILEReadLocalResolvableAddressReturn.self, timeout: timeout) return returnParameters.localResolvableAddress } diff --git a/Sources/BluetoothHCI/HCILEReadLocalSupportedFeatures.swift b/Sources/BluetoothHCI/HCILEReadLocalSupportedFeatures.swift index d9a55254b..a00a285af 100644 --- a/Sources/BluetoothHCI/HCILEReadLocalSupportedFeatures.swift +++ b/Sources/BluetoothHCI/HCILEReadLocalSupportedFeatures.swift @@ -15,9 +15,9 @@ public extension BluetoothHostControllerInterface { /// LE Read Local Supported Features Command /// /// This command requests the list of the supported LE features for the Controller. - func lowEnergyReadLocalSupportedFeatures(timeout: HCICommandTimeout = .default) throws -> LowEnergyFeatureSet { + func lowEnergyReadLocalSupportedFeatures(timeout: HCICommandTimeout = .default) async throws -> LowEnergyFeatureSet { - let returValue = try deviceRequest(HCILEReadLocalSupportedFeatures.self, timeout: timeout) + let returValue = try await deviceRequest(HCILEReadLocalSupportedFeatures.self, timeout: timeout) return returValue.features } diff --git a/Sources/BluetoothHCI/HCILEReadMaximumAdvertisingDataLength.swift b/Sources/BluetoothHCI/HCILEReadMaximumAdvertisingDataLength.swift index f30aed8d4..293fc3e4c 100644 --- a/Sources/BluetoothHCI/HCILEReadMaximumAdvertisingDataLength.swift +++ b/Sources/BluetoothHCI/HCILEReadMaximumAdvertisingDataLength.swift @@ -16,9 +16,9 @@ public extension BluetoothHostControllerInterface { /// /// The ommand is used to read the maximum length of data supported by the Controller for use /// as advertisement data or scan response data in an advertising event or as periodic advertisement data. - func setReadMaximumAdvertisingDataLength(timeout: HCICommandTimeout = .default) throws -> UInt16 { + func setReadMaximumAdvertisingDataLength(timeout: HCICommandTimeout = .default) async throws -> UInt16 { - let value = try deviceRequest(HCILEReadMaximumAdvertisingDataLength.self, + let value = try await deviceRequest(HCILEReadMaximumAdvertisingDataLength.self, timeout: timeout) return value.maximumAdvertisingDataLength diff --git a/Sources/BluetoothHCI/HCILEReadMaximumDataLength.swift b/Sources/BluetoothHCI/HCILEReadMaximumDataLength.swift index d48188ea6..392e27760 100644 --- a/Sources/BluetoothHCI/HCILEReadMaximumDataLength.swift +++ b/Sources/BluetoothHCI/HCILEReadMaximumDataLength.swift @@ -18,9 +18,9 @@ public extension BluetoothHostControllerInterface { /// /// This ommand allows the Host to read the Controller’s maximum supported payload octets /// and packet duration times for transmission and reception - func lowEnergyReadMaximumDataLengthReturn(timeout: HCICommandTimeout = .default) throws -> HCILEReadMaximumDataLength { + func lowEnergyReadMaximumDataLengthReturn(timeout: HCICommandTimeout = .default) async throws -> HCILEReadMaximumDataLength { - let value = try deviceRequest(HCILEReadMaximumDataLength.self, timeout: timeout) + let value = try await deviceRequest(HCILEReadMaximumDataLength.self, timeout: timeout) return value } diff --git a/Sources/BluetoothHCI/HCILEReadNumberOfSupportedAdvertisingSets.swift b/Sources/BluetoothHCI/HCILEReadNumberOfSupportedAdvertisingSets.swift index 0007c8e5c..bff9f3437 100644 --- a/Sources/BluetoothHCI/HCILEReadNumberOfSupportedAdvertisingSets.swift +++ b/Sources/BluetoothHCI/HCILEReadNumberOfSupportedAdvertisingSets.swift @@ -18,9 +18,9 @@ public extension BluetoothHostControllerInterface { /// the advertising Controller at the same time. Note: The number of advertising sets that /// can be supported is not fixed and the Controller can change it at any time because the memory /// used to store advertising sets can also be used for other purposes. - func readNumberOfSupportedAdvertisingSets(timeout: HCICommandTimeout = .default) throws -> UInt8 { + func readNumberOfSupportedAdvertisingSets(timeout: HCICommandTimeout = .default) async throws -> UInt8 { - let value = try deviceRequest(HCILEReadNumberOfSupportedAdvertisingSets.self, + let value = try await deviceRequest(HCILEReadNumberOfSupportedAdvertisingSets.self, timeout: timeout) return value.numSupportedAdvertisingSets diff --git a/Sources/BluetoothHCI/HCILEReadPeerResolvableAddressReturn.swift b/Sources/BluetoothHCI/HCILEReadPeerResolvableAddressReturn.swift index f1a3c9705..17ca0fa10 100644 --- a/Sources/BluetoothHCI/HCILEReadPeerResolvableAddressReturn.swift +++ b/Sources/BluetoothHCI/HCILEReadPeerResolvableAddressReturn.swift @@ -17,11 +17,11 @@ public extension BluetoothHostControllerInterface { /// The command is used to get the current peer Resolvable Private Address being /// used for the corresponding peer Public and Random (static) Identity Address. /// The peer’s resolvable address being used may change after the command is called. - func lowEnergyReadPeerResolvableAddress(peerIdentifyAddressType: LowEnergyPeerIdentifyAddressType, peerIdentifyAddress: UInt64, timeout: HCICommandTimeout = .default) throws -> UInt64 { + func lowEnergyReadPeerResolvableAddress(peerIdentifyAddressType: LowEnergyPeerIdentifyAddressType, peerIdentifyAddress: UInt64, timeout: HCICommandTimeout = .default) async throws -> UInt64 { let parameters = HCILEReadPeerResolvableAddress(peerIdentifyAddressType: peerIdentifyAddressType, peerIdentifyAddress: peerIdentifyAddress) - let returnParameters = try deviceRequest(parameters, HCILEReadPeerResolvableAddressReturn.self, timeout: timeout) + let returnParameters = try await deviceRequest(parameters, HCILEReadPeerResolvableAddressReturn.self, timeout: timeout) return returnParameters.peerResolvableAddress } diff --git a/Sources/BluetoothHCI/HCILEReadPeriodicAdvertisingListSize.swift b/Sources/BluetoothHCI/HCILEReadPeriodicAdvertisingListSize.swift index c5c322ef6..dd252473c 100644 --- a/Sources/BluetoothHCI/HCILEReadPeriodicAdvertisingListSize.swift +++ b/Sources/BluetoothHCI/HCILEReadPeriodicAdvertisingListSize.swift @@ -15,9 +15,9 @@ public extension BluetoothHostControllerInterface { /// LE Read Periodic Advertiser List Size Command /// /// The command is used to read the total number of Periodic Advertiser list entries that can be stored in the Controller. - func lowEnergyReadPeriodicAdvertisingListSize(timeout: HCICommandTimeout = .default) throws -> UInt8 { + func lowEnergyReadPeriodicAdvertisingListSize(timeout: HCICommandTimeout = .default) async throws -> UInt8 { - let value = try deviceRequest(HCILEReadPeriodicAdvertisingListSize.self, + let value = try await deviceRequest(HCILEReadPeriodicAdvertisingListSize.self, timeout: timeout) return value.periodicAdvertiserListSize diff --git a/Sources/BluetoothHCI/HCILEReadPhy.swift b/Sources/BluetoothHCI/HCILEReadPhy.swift index 570d5e98c..122745f11 100644 --- a/Sources/BluetoothHCI/HCILEReadPhy.swift +++ b/Sources/BluetoothHCI/HCILEReadPhy.swift @@ -16,11 +16,11 @@ public extension BluetoothHostControllerInterface { /// /// This ommand is used to read the current transmitter PHY and receiver PHY /// on the connection identified by the Connection_Handle. - func lowEnergyReadPhy(connectionHandle: UInt16, timeout: HCICommandTimeout = .default) throws -> HCILEReadPHYReturn { + func lowEnergyReadPhy(connectionHandle: UInt16, timeout: HCICommandTimeout = .default) async throws -> HCILEReadPHYReturn { let parameters = HCILEReadPHY(connectionHandle: connectionHandle) - let value = try deviceRequest(parameters, HCILEReadPHYReturn.self, timeout: timeout) + let value = try await deviceRequest(parameters, HCILEReadPHYReturn.self, timeout: timeout) return value } diff --git a/Sources/BluetoothHCI/HCILEReadRemoteUsedFeatures.swift b/Sources/BluetoothHCI/HCILEReadRemoteUsedFeatures.swift index 73aea0e95..806b30260 100644 --- a/Sources/BluetoothHCI/HCILEReadRemoteUsedFeatures.swift +++ b/Sources/BluetoothHCI/HCILEReadRemoteUsedFeatures.swift @@ -16,11 +16,11 @@ public extension BluetoothHostControllerInterface { /// /// The command requests, from the remote device identified by the connection handle, /// the features used on the connection and the features supported by the remote device. - func lowEnergyReadRemoteUsedFeatures(connectionHandle: UInt16, timeout: HCICommandTimeout = .default) throws -> LowEnergyFeatureSet { + func lowEnergyReadRemoteUsedFeatures(connectionHandle: UInt16, timeout: HCICommandTimeout = .default) async throws -> LowEnergyFeatureSet { let parameters = HCILEReadRemoteUsedFeatures(connectionHandle: connectionHandle) - let event = try deviceRequest(parameters, + let event = try await deviceRequest(parameters, HCILEReadRemoteUsedFeaturesComplete.self, timeout: timeout) diff --git a/Sources/BluetoothHCI/HCILEReadResolvingListSize.swift b/Sources/BluetoothHCI/HCILEReadResolvingListSize.swift index a6ba63eb6..397859a64 100644 --- a/Sources/BluetoothHCI/HCILEReadResolvingListSize.swift +++ b/Sources/BluetoothHCI/HCILEReadResolvingListSize.swift @@ -16,9 +16,9 @@ public extension BluetoothHostControllerInterface { /// /// This command is used to read the total number of address translation /// entries in the resolving list that can be stored in the Controller. - func lowEnergyReadResolvingListSize(timeout: HCICommandTimeout = .default) throws -> UInt8 { + func lowEnergyReadResolvingListSize(timeout: HCICommandTimeout = .default) async throws -> UInt8 { - let value = try deviceRequest(HCILEReadResolvingListSize.self, + let value = try await deviceRequest(HCILEReadResolvingListSize.self, timeout: timeout) return value.resolvingListSize diff --git a/Sources/BluetoothHCI/HCILEReadRfPathCompensation.swift b/Sources/BluetoothHCI/HCILEReadRfPathCompensation.swift index d9b9889d4..80d0cd2b1 100644 --- a/Sources/BluetoothHCI/HCILEReadRfPathCompensation.swift +++ b/Sources/BluetoothHCI/HCILEReadRfPathCompensation.swift @@ -15,9 +15,9 @@ public extension BluetoothHostControllerInterface { /// LE Read RF Path Compensation Command /// /// The command is used to read the RF Path Compensation Values parameter used in the Tx Power Level and RSSI calculation. - func lowEnergyReadRfPathCompensation(timeout: HCICommandTimeout = .default) throws -> HCILEReadRfPathCompensation { + func lowEnergyReadRfPathCompensation(timeout: HCICommandTimeout = .default) async throws -> HCILEReadRfPathCompensation { - return try deviceRequest(HCILEReadRfPathCompensation.self, timeout: timeout) + return try await deviceRequest(HCILEReadRfPathCompensation.self, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILEReadSuggestedDefaultDataLength.swift b/Sources/BluetoothHCI/HCILEReadSuggestedDefaultDataLength.swift index 77ca68a13..a50b6aaca 100644 --- a/Sources/BluetoothHCI/HCILEReadSuggestedDefaultDataLength.swift +++ b/Sources/BluetoothHCI/HCILEReadSuggestedDefaultDataLength.swift @@ -16,9 +16,9 @@ public extension BluetoothHostControllerInterface { /// /// This command allows the Host to read the Host's suggested values (SuggestedMaxTxOctets and SuggestedMaxTxTime) /// for the Controller's maximum transmitted number of payload octets and maximum packet transmission time to be used for new connections. - func lowEnergyReadSuggestedDefaultDataLength(timeout: HCICommandTimeout = .default) throws -> HCILEReadSuggestedDefaultDataLength { + func lowEnergyReadSuggestedDefaultDataLength(timeout: HCICommandTimeout = .default) async throws -> HCILEReadSuggestedDefaultDataLength { - return try deviceRequest(HCILEReadSuggestedDefaultDataLength.self, timeout: timeout) + return try await deviceRequest(HCILEReadSuggestedDefaultDataLength.self, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILEReadSupportedStates.swift b/Sources/BluetoothHCI/HCILEReadSupportedStates.swift index 6740ddb99..902abf951 100644 --- a/Sources/BluetoothHCI/HCILEReadSupportedStates.swift +++ b/Sources/BluetoothHCI/HCILEReadSupportedStates.swift @@ -15,9 +15,9 @@ public extension BluetoothHostControllerInterface { /// LE Read Supported States /// /// The LE_Read_Supported_States command reads the states and state combinations that the link layer supports. - func readSupportedStates(timeout: HCICommandTimeout = .default) throws -> LowEnergyStateSet { + func readSupportedStates(timeout: HCICommandTimeout = .default) async throws -> LowEnergyStateSet { - let returValue = try deviceRequest(HCILEReadSupportedStates.self, timeout: timeout) + let returValue = try await deviceRequest(HCILEReadSupportedStates.self, timeout: timeout) return returValue.state } diff --git a/Sources/BluetoothHCI/HCILEReadTransmitPower.swift b/Sources/BluetoothHCI/HCILEReadTransmitPower.swift index 3294a80d3..35fee898a 100644 --- a/Sources/BluetoothHCI/HCILEReadTransmitPower.swift +++ b/Sources/BluetoothHCI/HCILEReadTransmitPower.swift @@ -15,9 +15,9 @@ public extension BluetoothHostControllerInterface { /// LE Read Transmit Power Command /// /// The command is used to read the minimum and maximum transmit powers supported by the Controller.ReadTransmitPowerReturnParameter - func lowEnergyReadTransmitPower(timeout: HCICommandTimeout = .default) throws -> HCILEReadTransmitPower { + func lowEnergyReadTransmitPower(timeout: HCICommandTimeout = .default) async throws -> HCILEReadTransmitPower { - return try deviceRequest(HCILEReadTransmitPower.self, timeout: timeout) + return try await deviceRequest(HCILEReadTransmitPower.self, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILEReadWhiteListSize.swift b/Sources/BluetoothHCI/HCILEReadWhiteListSize.swift index 13874156e..01ad4b0a7 100644 --- a/Sources/BluetoothHCI/HCILEReadWhiteListSize.swift +++ b/Sources/BluetoothHCI/HCILEReadWhiteListSize.swift @@ -15,9 +15,9 @@ public extension BluetoothHostControllerInterface { /// LE Read White List Size Command /// /// Used to read the total number of white list entries that can be stored in the Controller. - func lowEnergyReadWhiteListSize(timeout: HCICommandTimeout = .default) throws -> Int { + func lowEnergyReadWhiteListSize(timeout: HCICommandTimeout = .default) async throws -> Int { - let sizeReturnParameter = try deviceRequest(HCILEReadWhiteListSize.self, timeout: timeout) + let sizeReturnParameter = try await deviceRequest(HCILEReadWhiteListSize.self, timeout: timeout) return Int(sizeReturnParameter.size) } diff --git a/Sources/BluetoothHCI/HCILEReceiverTest.swift b/Sources/BluetoothHCI/HCILEReceiverTest.swift index 0a29af119..ed935fbbb 100644 --- a/Sources/BluetoothHCI/HCILEReceiverTest.swift +++ b/Sources/BluetoothHCI/HCILEReceiverTest.swift @@ -16,11 +16,11 @@ public extension BluetoothHostControllerInterface { /// /// This command is used to start a test where the DUT receives test reference packets at a fixed interval. /// The tester generates the test reference packets. - func lowEnergyReceiverTest(rxChannel: LowEnergyRxChannel, timeout: HCICommandTimeout = .default) throws { + func lowEnergyReceiverTest(rxChannel: LowEnergyRxChannel, timeout: HCICommandTimeout = .default) async throws { let parameters = HCILEReceiverTest(rxChannel: rxChannel) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILERemoteConnectionParameterRequestNegativeReply.swift b/Sources/BluetoothHCI/HCILERemoteConnectionParameterRequestNegativeReply.swift index bf4d1bed7..36f55fd9b 100644 --- a/Sources/BluetoothHCI/HCILERemoteConnectionParameterRequestNegativeReply.swift +++ b/Sources/BluetoothHCI/HCILERemoteConnectionParameterRequestNegativeReply.swift @@ -20,12 +20,12 @@ public extension BluetoothHostControllerInterface { /// The reason for the rejection is given in the Reason parameter. func lowEnergyRemoteConnectionParameterRequestNegativeReply(connectionHandle: UInt16, reason: UInt8, - timeout: HCICommandTimeout = .default) throws -> UInt16 { + timeout: HCICommandTimeout = .default) async throws -> UInt16 { let parameters = HCILERemoteConnectionParameterRequestNegativeReply(connectionHandle: connectionHandle, reason: reason) - let returnParameters = try deviceRequest(parameters, HCILERemoteConnectionParameterRequestNegativeReplyReturn.self, timeout: timeout) + let returnParameters = try await deviceRequest(parameters, HCILERemoteConnectionParameterRequestNegativeReplyReturn.self, timeout: timeout) return returnParameters.connectionHandle } diff --git a/Sources/BluetoothHCI/HCILERemoteConnectionParameterRequestReply.swift b/Sources/BluetoothHCI/HCILERemoteConnectionParameterRequestReply.swift index 8366d5590..defeba1e5 100644 --- a/Sources/BluetoothHCI/HCILERemoteConnectionParameterRequestReply.swift +++ b/Sources/BluetoothHCI/HCILERemoteConnectionParameterRequestReply.swift @@ -22,7 +22,7 @@ public extension BluetoothHostControllerInterface { latency: LowEnergyConnectionLatency, timeOut: LowEnergySupervisionTimeout, length: LowEnergyConnectionLength, - timeout: HCICommandTimeout = .default) throws -> UInt16 { + timeout: HCICommandTimeout = .default) async throws -> UInt16 { let parameters = HCILERemoteConnectionParameterRequestReply(connectionHandle: connectionHandle, interval: interval, @@ -30,7 +30,7 @@ public extension BluetoothHostControllerInterface { timeOut: timeOut, length: length) - let returnParameters = try deviceRequest(parameters, HCILERemoteConnectionParameterRequestReplyReturn.self, timeout: timeout) + let returnParameters = try await deviceRequest(parameters, HCILERemoteConnectionParameterRequestReplyReturn.self, timeout: timeout) return returnParameters.connectionHandle } diff --git a/Sources/BluetoothHCI/HCILERemoveAdvertisingSet.swift b/Sources/BluetoothHCI/HCILERemoveAdvertisingSet.swift index dacee15e5..051c6a2e5 100644 --- a/Sources/BluetoothHCI/HCILERemoveAdvertisingSet.swift +++ b/Sources/BluetoothHCI/HCILERemoveAdvertisingSet.swift @@ -15,11 +15,11 @@ public extension BluetoothHostControllerInterface { /// LE Remove Advertising Set Command /// /// The command is used to remove an advertising set from the Controller. - func setLowEnergyRemoveAdvertisingSet(advertisingHandle: UInt8, timeout: HCICommandTimeout = .default) throws { + func setLowEnergyRemoveAdvertisingSet(advertisingHandle: UInt8, timeout: HCICommandTimeout = .default) async throws { let parameters = HCILERemoveAdvertisingSet(advertisingHandle: advertisingHandle) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILERemoveDeviceFromResolvingList.swift b/Sources/BluetoothHCI/HCILERemoveDeviceFromResolvingList.swift index b59dd27bd..b3b834884 100644 --- a/Sources/BluetoothHCI/HCILERemoveDeviceFromResolvingList.swift +++ b/Sources/BluetoothHCI/HCILERemoveDeviceFromResolvingList.swift @@ -18,11 +18,11 @@ public extension BluetoothHostControllerInterface { /// Resolvable Private Addresses in the Controller. func lowEnergyRemoveDeviceFromResolvingList(peerIdentifyAddressType: LowEnergyPeerIdentifyAddressType, peerIdentifyAddress: UInt64, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameters = HCILERemoveDeviceFromResolvingList(peerIdentifyAddressType: peerIdentifyAddressType, peerIdentifyAddress: peerIdentifyAddress) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILERemoveDeviceFromWhiteList.swift b/Sources/BluetoothHCI/HCILERemoveDeviceFromWhiteList.swift index 1b70a9c16..c5966fa08 100644 --- a/Sources/BluetoothHCI/HCILERemoveDeviceFromWhiteList.swift +++ b/Sources/BluetoothHCI/HCILERemoveDeviceFromWhiteList.swift @@ -15,9 +15,9 @@ public extension BluetoothHostControllerInterface { /// LE Remove Device From White List Command /// /// Used to remove a single device from the White List stored in the Controller. - func lowEnergyRemoveDeviceFromWhiteList(_ whiteListDevice: LowEnergyWhiteListDevice, timeout: HCICommandTimeout = .default) throws { + func lowEnergyRemoveDeviceFromWhiteList(_ whiteListDevice: LowEnergyWhiteListDevice, timeout: HCICommandTimeout = .default) async throws { - try deviceRequest(HCILERemoveDeviceFromWhiteList(device: whiteListDevice), timeout: timeout) + try await deviceRequest(HCILERemoveDeviceFromWhiteList(device: whiteListDevice), timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILERemoveDeviceToPeriodicAdvertiserList.swift b/Sources/BluetoothHCI/HCILERemoveDeviceToPeriodicAdvertiserList.swift index cc9dbdbd0..21f080688 100644 --- a/Sources/BluetoothHCI/HCILERemoveDeviceToPeriodicAdvertiserList.swift +++ b/Sources/BluetoothHCI/HCILERemoveDeviceToPeriodicAdvertiserList.swift @@ -19,13 +19,13 @@ public extension BluetoothHostControllerInterface { func lowEnergyRemoveDeviceToPeriodicAdvertiserList(advertiserAddressType: LowEnergyAdvertiserAddressType, address: BluetoothAddress, advertisingSID: UInt8, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameters = HCILERemoveDeviceToPeriodicAdvertiserList(advertiserAddressType: advertiserAddressType, address: address, advertisingSID: advertisingSID) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILESetAddressResolutionEnable.swift b/Sources/BluetoothHCI/HCILESetAddressResolutionEnable.swift index 91d92eddd..fa7f5d47b 100644 --- a/Sources/BluetoothHCI/HCILESetAddressResolutionEnable.swift +++ b/Sources/BluetoothHCI/HCILESetAddressResolutionEnable.swift @@ -15,11 +15,11 @@ public extension BluetoothHostControllerInterface { /// LE Set Address Resolution Enable Command /// /// The command is used to enable resolution of Resolvable Private Addresses in the Controller. - func lowEnergySetAddressResolutionEnable(addressResolutionEnable: HCILESetAddressResolutionEnable.AddressResolutionEnable, timeout: HCICommandTimeout = .default) throws { + func lowEnergySetAddressResolutionEnable(addressResolutionEnable: HCILESetAddressResolutionEnable.AddressResolutionEnable, timeout: HCICommandTimeout = .default) async throws { let parameters = HCILESetAddressResolutionEnable(addressResolutionEnable: addressResolutionEnable) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILESetAdvertiseEnable.swift b/Sources/BluetoothHCI/HCILESetAdvertiseEnable.swift index 7a410584f..a41775d77 100644 --- a/Sources/BluetoothHCI/HCILESetAdvertiseEnable.swift +++ b/Sources/BluetoothHCI/HCILESetAdvertiseEnable.swift @@ -18,11 +18,10 @@ public extension BluetoothHostControllerInterface { /// The Controller manages the timing of advertisements as per the advertising parameters given in the /// LE Set Advertising Parameters command. func enableLowEnergyAdvertising(_ isEnabled: Bool = true, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameter = HCILESetAdvertiseEnable(isEnabled: isEnabled) - - try deviceRequest(parameter, timeout: timeout) + try await deviceRequest(parameter, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILESetAdvertisingData.swift b/Sources/BluetoothHCI/HCILESetAdvertisingData.swift index a8b5f903e..85bfd36bd 100644 --- a/Sources/BluetoothHCI/HCILESetAdvertisingData.swift +++ b/Sources/BluetoothHCI/HCILESetAdvertisingData.swift @@ -16,11 +16,11 @@ public extension BluetoothHostControllerInterface { /// /// Used to set the data used in advertising packets that have a data field. func setLowEnergyAdvertisingData(_ data: LowEnergyAdvertisingData, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameter = HCILESetAdvertisingData(advertisingData: data) - try deviceRequest(parameter, timeout: timeout) + try await deviceRequest(parameter, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILESetAdvertisingParameters.swift b/Sources/BluetoothHCI/HCILESetAdvertisingParameters.swift index ff727f9c4..c1b33a7ac 100644 --- a/Sources/BluetoothHCI/HCILESetAdvertisingParameters.swift +++ b/Sources/BluetoothHCI/HCILESetAdvertisingParameters.swift @@ -16,9 +16,9 @@ public extension BluetoothHostControllerInterface { /// /// Used by the Host to set the advertising parameters. func setLowEnergyAdvertisingParameters(_ parameters: HCILESetAdvertisingParameters, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILESetAdvertisingSetRandomAddress.swift b/Sources/BluetoothHCI/HCILESetAdvertisingSetRandomAddress.swift index d9d49e264..c2def4512 100644 --- a/Sources/BluetoothHCI/HCILESetAdvertisingSetRandomAddress.swift +++ b/Sources/BluetoothHCI/HCILESetAdvertisingSetRandomAddress.swift @@ -17,11 +17,11 @@ public extension BluetoothHostControllerInterface { /// The command is used by the Host to set the random device address specified by the Random_Address parameter. func setAdvertisingSetRandomAddress(advertisingHandle: UInt8, advertisingRandomAddress: BluetoothAddress, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameters = HCILESetAdvertisingSetRandomAddress(advertisingHandle: advertisingHandle, advertisingRandomAddress: advertisingRandomAddress) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILESetDataLength.swift b/Sources/BluetoothHCI/HCILESetDataLength.swift index 1d35bb723..53f6d70fc 100644 --- a/Sources/BluetoothHCI/HCILESetDataLength.swift +++ b/Sources/BluetoothHCI/HCILESetDataLength.swift @@ -19,13 +19,13 @@ public extension BluetoothHostControllerInterface { func lowEnergySetDataLength(connectionHandle: UInt16, txOctets: LowEnergyMaxTxOctets, txTime: LowEnergyMaxTxTime, - timeout: HCICommandTimeout = .default) throws -> UInt16 { + timeout: HCICommandTimeout = .default) async throws -> UInt16 { let parameters = HCILESetDataLength(connectionHandle: connectionHandle, txOctets: txOctets, txTime: txTime) - let returnParameters = try deviceRequest(parameters, HCILESetDataLengthReturn.self, timeout: timeout) + let returnParameters = try await deviceRequest(parameters, HCILESetDataLengthReturn.self, timeout: timeout) return returnParameters.connectionHandle } diff --git a/Sources/BluetoothHCI/HCILESetDefaultPhy.swift b/Sources/BluetoothHCI/HCILESetDefaultPhy.swift index 760fed3ef..9d8f30b53 100644 --- a/Sources/BluetoothHCI/HCILESetDefaultPhy.swift +++ b/Sources/BluetoothHCI/HCILESetDefaultPhy.swift @@ -19,11 +19,11 @@ public extension BluetoothHostControllerInterface { func lowEnergySetDefaultPhy(allPhys: LowEnergyAllPhys, txPhys: LowEnergyTxPhys, rxPhys: LowEnergyRxPhys, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameters = HCILESetDefaultPhy(allPhys: allPhys, txPhys: txPhys, rxPhys: rxPhys) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILESetEventMask.swift b/Sources/BluetoothHCI/HCILESetEventMask.swift index 420fae49e..6b97a3482 100644 --- a/Sources/BluetoothHCI/HCILESetEventMask.swift +++ b/Sources/BluetoothHCI/HCILESetEventMask.swift @@ -16,11 +16,11 @@ public extension BluetoothHostControllerInterface { /// /// The command is used to control which LE events are generated by the HCI for the Host. func setLowEnergyEventMask(_ eventMask: HCILESetEventMask.EventMask, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameter = HCILESetEventMask(eventMask: eventMask) - try deviceRequest(parameter, timeout: timeout) + try await deviceRequest(parameter, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILESetExtendedAdvertisingData.swift b/Sources/BluetoothHCI/HCILESetExtendedAdvertisingData.swift index 7f3a70dee..ca29423aa 100644 --- a/Sources/BluetoothHCI/HCILESetExtendedAdvertisingData.swift +++ b/Sources/BluetoothHCI/HCILESetExtendedAdvertisingData.swift @@ -19,11 +19,11 @@ public extension BluetoothHostControllerInterface { operation: HCILESetExtendedAdvertisingData.Operation, fragmentPreference: LowEnergyFragmentPreference, advertisingData: [UInt8], - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameters = HCILESetExtendedAdvertisingData(advertisingHandle: advertisingHandle, operation: operation, fragmentPreference: fragmentPreference, advertisingData: advertisingData) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILESetExtendedAdvertisingParameters.swift b/Sources/BluetoothHCI/HCILESetExtendedAdvertisingParameters.swift index 70d556b85..7b8080cc4 100644 --- a/Sources/BluetoothHCI/HCILESetExtendedAdvertisingParameters.swift +++ b/Sources/BluetoothHCI/HCILESetExtendedAdvertisingParameters.swift @@ -16,9 +16,9 @@ public extension BluetoothHostControllerInterface { /// /// The command is used by the Host to set the advertising parameters. func setExtendedAdvertisingParameters(_ parameters: HCILESetExtendedAdvertisingParameters, - timeout: HCICommandTimeout = .default) throws -> LowEnergyTxPower { + timeout: HCICommandTimeout = .default) async throws -> LowEnergyTxPower { - let returnParameter = try deviceRequest(parameters, HCILESetExtendedAdvertisingParametersReturn.self, timeout: timeout) + let returnParameter = try await deviceRequest(parameters, HCILESetExtendedAdvertisingParametersReturn.self, timeout: timeout) return returnParameter.selectedTxPower } diff --git a/Sources/BluetoothHCI/HCILESetExtendedScanEnable.swift b/Sources/BluetoothHCI/HCILESetExtendedScanEnable.swift index 475a087f4..c2f10b0ca 100644 --- a/Sources/BluetoothHCI/HCILESetExtendedScanEnable.swift +++ b/Sources/BluetoothHCI/HCILESetExtendedScanEnable.swift @@ -19,14 +19,14 @@ public extension BluetoothHostControllerInterface { filterDuplicates: HCILESetExtendedScanEnable.FilterDuplicates, duration: HCILESetExtendedScanEnable.Duration, period: HCILESetExtendedScanEnable.Period, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameters = HCILESetExtendedScanEnable(enable: enable, filterDuplicates: filterDuplicates, duration: duration, period: period) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILESetExtendedScanParameters.swift b/Sources/BluetoothHCI/HCILESetExtendedScanParameters.swift index b4feb6f37..d85cd54e8 100644 --- a/Sources/BluetoothHCI/HCILESetExtendedScanParameters.swift +++ b/Sources/BluetoothHCI/HCILESetExtendedScanParameters.swift @@ -18,13 +18,13 @@ public extension BluetoothHostControllerInterface { func setLowEnergyExtendedScanParameters(ownAddressType: LowEnergyAddressType, scanningFilterPolicy: HCILESetExtendedScanParameters.ScanningFilterPolicy, scanningPHY: HCILESetExtendedScanParameters.ScanningPHY, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameters = HCILESetExtendedScanParameters(ownAddressType: ownAddressType, scanningFilterPolicy: scanningFilterPolicy, scanningPHY: scanningPHY) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILESetExtendedScanResponseData.swift b/Sources/BluetoothHCI/HCILESetExtendedScanResponseData.swift index 62067fe82..b2a87d328 100644 --- a/Sources/BluetoothHCI/HCILESetExtendedScanResponseData.swift +++ b/Sources/BluetoothHCI/HCILESetExtendedScanResponseData.swift @@ -22,11 +22,11 @@ public extension BluetoothHostControllerInterface { operation: HCILESetExtendedScanResponseData.Operation, fragmentPreference: LowEnergyFragmentPreference, scanResponseData: [UInt8], - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameters = HCILESetExtendedScanResponseData(advertisingHandle: advertisingHandle, operation: operation, fragmentPreference: fragmentPreference, scanResponseData: scanResponseData) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILESetPeriodicAdvertisingData.swift b/Sources/BluetoothHCI/HCILESetPeriodicAdvertisingData.swift index 32813f0f0..1af1ffaba 100644 --- a/Sources/BluetoothHCI/HCILESetPeriodicAdvertisingData.swift +++ b/Sources/BluetoothHCI/HCILESetPeriodicAdvertisingData.swift @@ -18,11 +18,11 @@ public extension BluetoothHostControllerInterface { func setSetPeriodicAdvertisingData(advertisingHandle: UInt8, operation: HCILESetPeriodicAdvertisingData.Operation, advertisingData: [UInt8], - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameters = HCILESetPeriodicAdvertisingData(advertisingHandle: advertisingHandle, operation: operation, advertisingData: advertisingData) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILESetPeriodicAdvertisingEnable.swift b/Sources/BluetoothHCI/HCILESetPeriodicAdvertisingEnable.swift index 633ad0713..87a3c5bbe 100644 --- a/Sources/BluetoothHCI/HCILESetPeriodicAdvertisingEnable.swift +++ b/Sources/BluetoothHCI/HCILESetPeriodicAdvertisingEnable.swift @@ -18,11 +18,11 @@ public extension BluetoothHostControllerInterface { /// for the advertising set specified by the Advertising_Handle parameter (ordinary advertising is not affected). func setPeriodicAdvertisingEnable(enable: HCILESetPeriodicAdvertisingEnable.Enable, advertisingHandle: UInt8, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameters = HCILESetPeriodicAdvertisingEnable(enable: enable, advertisingHandle: advertisingHandle) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILESetPeriodicAdvertisingParameters.swift b/Sources/BluetoothHCI/HCILESetPeriodicAdvertisingParameters.swift index a8b8408fe..4dac0c956 100644 --- a/Sources/BluetoothHCI/HCILESetPeriodicAdvertisingParameters.swift +++ b/Sources/BluetoothHCI/HCILESetPeriodicAdvertisingParameters.swift @@ -18,11 +18,11 @@ public extension BluetoothHostControllerInterface { func setSetPeriodicAdvertisingParameters(advertisingHandle: UInt8, periodicAdvertisingInterval: HCILESetPeriodicAdvertisingParameters.PeriodicAdvertisingInterval, advertisingEventProperties: HCILESetPeriodicAdvertisingParameters.AdvertisingEventProperties, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameters = HCILESetPeriodicAdvertisingParameters(advertisingHandle: advertisingHandle, periodicAdvertisingInterval: periodicAdvertisingInterval, advertisingEventProperties: advertisingEventProperties) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILESetPhy.swift b/Sources/BluetoothHCI/HCILESetPhy.swift index 197dc9828..c973d124e 100644 --- a/Sources/BluetoothHCI/HCILESetPhy.swift +++ b/Sources/BluetoothHCI/HCILESetPhy.swift @@ -23,7 +23,7 @@ public extension BluetoothHostControllerInterface { txPhys: LowEnergyTxPhys, rxPhys: LowEnergyRxPhys, phyOptions: LowEnergyPhyOptions, - timeout: HCICommandTimeout = .default) throws -> HCILEPhyUpdateComplete { + timeout: HCICommandTimeout = .default) async throws -> HCILEPhyUpdateComplete { let parameters = HCILESetPhy(connectionHandle: connectionHandle, allPhys: allPhys, @@ -31,7 +31,7 @@ public extension BluetoothHostControllerInterface { rxPhys: rxPhys, phyOptions: phyOptions) - let event = try deviceRequest(parameters, + let event = try await deviceRequest(parameters, HCILEPhyUpdateComplete.self, timeout: timeout) diff --git a/Sources/BluetoothHCI/HCILESetPrivacyMode.swift b/Sources/BluetoothHCI/HCILESetPrivacyMode.swift index 7f98c7d2b..793fa27cf 100644 --- a/Sources/BluetoothHCI/HCILESetPrivacyMode.swift +++ b/Sources/BluetoothHCI/HCILESetPrivacyMode.swift @@ -18,13 +18,13 @@ public extension BluetoothHostControllerInterface { func lowEnergySetPrivacyMode(peerIdentityAddressType: LowEnergyPeerIdentifyAddressType, peerIdentityAddress: BluetoothAddress, privacyMode: HCILESetPrivacyMode.PrivacyMode = HCILESetPrivacyMode.PrivacyMode.networkPrivacy, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameters = HCILESetPrivacyMode(peerIdentityAddressType: peerIdentityAddressType, peerIdentityAddress: peerIdentityAddress, privacyMode: privacyMode) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILESetRandomAddress.swift b/Sources/BluetoothHCI/HCILESetRandomAddress.swift index 46b3e80d8..8c040c355 100644 --- a/Sources/BluetoothHCI/HCILESetRandomAddress.swift +++ b/Sources/BluetoothHCI/HCILESetRandomAddress.swift @@ -13,12 +13,10 @@ import Foundation public extension BluetoothHostControllerInterface { /// Used by the Host to set the LE Random Device Address in the Controller. - func lowEnergySetRandomAddress(_ address: BluetoothAddress, timeout: HCICommandTimeout = .default) throws { - + func lowEnergySetRandomAddress(_ address: BluetoothAddress, timeout: HCICommandTimeout = .default) async throws { // When the LE_Set_Random_Address command has completed, a Command Complete event shall be generated. let commandParameter = HCILESetRandomAddress(address: address) - - try self.deviceRequest(commandParameter, timeout: timeout) + try await deviceRequest(commandParameter, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILESetResolvablePrivateAddressTimeout.swift b/Sources/BluetoothHCI/HCILESetResolvablePrivateAddressTimeout.swift index 43d685cd7..ac3c0a644 100644 --- a/Sources/BluetoothHCI/HCILESetResolvablePrivateAddressTimeout.swift +++ b/Sources/BluetoothHCI/HCILESetResolvablePrivateAddressTimeout.swift @@ -15,11 +15,11 @@ public extension BluetoothHostControllerInterface { /// LE Set Resolvable Private Address Timeout Command /// /// The command set the length of time the Controller uses a Resolvable Private Address before a new resolvable private address is generated and starts being used. - func lowEnergySetResolvablePrivateAddressTimeout(rpaTimeout: HCILESetResolvablePrivateAddressTimeout.RPATimeout, timeout: HCICommandTimeout = .default) throws { + func lowEnergySetResolvablePrivateAddressTimeout(rpaTimeout: HCILESetResolvablePrivateAddressTimeout.RPATimeout, timeout: HCICommandTimeout = .default) async throws { let parameters = HCILESetResolvablePrivateAddressTimeout(rpaTimeout: rpaTimeout) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILESetScanEnable.swift b/Sources/BluetoothHCI/HCILESetScanEnable.swift index 3e5fb1b4a..726ee92d1 100644 --- a/Sources/BluetoothHCI/HCILESetScanEnable.swift +++ b/Sources/BluetoothHCI/HCILESetScanEnable.swift @@ -16,7 +16,7 @@ public extension BluetoothHostControllerInterface { func lowEnergyScan(duration: TimeInterval, filterDuplicates: Bool = true, parameters: HCILESetScanParameters = .init(), - timeout: HCICommandTimeout = .default) throws -> [HCILEAdvertisingReport.Report] { + timeout: HCICommandTimeout = .default) async throws -> [HCILEAdvertisingReport.Report] { let startDate = Date() let endDate = startDate + duration @@ -24,11 +24,13 @@ public extension BluetoothHostControllerInterface { var foundDevices: [HCILEAdvertisingReport.Report] = [] foundDevices.reserveCapacity(1) - try lowEnergyScan(filterDuplicates: filterDuplicates, - parameters: parameters, - timeout: timeout, - shouldContinue: { Date() < endDate }, - foundDevice: { foundDevices.append($0) }) + try await lowEnergyScan( + filterDuplicates: filterDuplicates, + parameters: parameters, + timeout: timeout, + shouldContinue: { Date() < endDate }, + foundDevice: { foundDevices.append($0) } + ) return foundDevices } @@ -38,46 +40,49 @@ public extension BluetoothHostControllerInterface { parameters: HCILESetScanParameters = .init(), timeout: HCICommandTimeout = .default, shouldContinue: () -> (Bool), - foundDevice: (HCILEAdvertisingReport.Report) -> ()) throws { + foundDevice: (HCILEAdvertisingReport.Report) -> ()) async throws { // macro for enabling / disabling scan - func enableScan(_ isEnabled: Bool = true) throws { + func enableScan(_ isEnabled: Bool = true) async throws { let scanEnableCommand = HCILESetScanEnable(isEnabled: isEnabled, filterDuplicates: filterDuplicates) - do { try deviceRequest(scanEnableCommand, timeout: timeout) } + do { try await deviceRequest(scanEnableCommand, timeout: timeout) } catch HCIError.commandDisallowed { /* ignore, means already turned on or off */ } } // disable scanning first - try enableScan(false) + try await enableScan(false) // set parameters - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) // enable scanning - try enableScan() - - // disable scanning after completion - defer { do { try enableScan(false) } catch { /* ignore all errors disabling scanning */ } } + try await enableScan() // poll for scanned devices - try pollEvent(HCILowEnergyMetaEvent.self, shouldContinue: shouldContinue) { (metaEvent) in - - // only want advertising report - guard metaEvent.subevent == .advertisingReport - else { return } - - // parse LE advertising report - guard let advertisingReport = HCILEAdvertisingReport(data: metaEvent.eventData) - else { throw BluetoothHostControllerError.garbageResponse(Data(metaEvent.eventData)) } - - // call closure on each device found - advertisingReport.reports.forEach { foundDevice($0) } + do { + try await pollEvent(HCILowEnergyMetaEvent.self, shouldContinue: shouldContinue) { (metaEvent) in + + // only want advertising report + guard metaEvent.subevent == .advertisingReport + else { return } + + // parse LE advertising report + guard let advertisingReport = HCILEAdvertisingReport(data: metaEvent.eventData) + else { throw BluetoothHostControllerError.garbageResponse(Data(metaEvent.eventData)) } + + // call closure on each device found + advertisingReport.reports.forEach { foundDevice($0) } + } + } catch { + // disable scanning + do { try await enableScan(false) } catch { /* ignore all errors disabling scanning */ } + throw error } + do { try await enableScan(false) } catch { /* ignore all errors disabling scanning */ } } - } // MARK: - Command diff --git a/Sources/BluetoothHCI/HCILESetScanResponseData.swift b/Sources/BluetoothHCI/HCILESetScanResponseData.swift index 6da3f0ed8..03ac372c2 100644 --- a/Sources/BluetoothHCI/HCILESetScanResponseData.swift +++ b/Sources/BluetoothHCI/HCILESetScanResponseData.swift @@ -23,12 +23,12 @@ public extension BluetoothHostControllerInterface { /// /// - Precondition: The provided length must be less than or equal to 31. func setLowEnergyScanResponse(_ data: LowEnergyAdvertisingData, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { // set scan response parameters let command = HCILESetScanResponseData(advertisingData: data) - try deviceRequest(command, timeout: timeout) + try await deviceRequest(command, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILEStartEncryption.swift b/Sources/BluetoothHCI/HCILEStartEncryption.swift index c54ba6cad..858ead4ab 100644 --- a/Sources/BluetoothHCI/HCILEStartEncryption.swift +++ b/Sources/BluetoothHCI/HCILEStartEncryption.swift @@ -20,7 +20,7 @@ public extension BluetoothHostControllerInterface { randomNumber: UInt64, encryptedDiversifier: UInt16, longTermKey: UInt128, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { /** When the Controller receives the LE_Start_Encryption command it shall send the Command Status event to the Host. If the connection is not encrypted when this command is issued, an Encryption Change event shall occur when encryption has been started for the connection. If the connection is encrypted when this command is issued, an Encryption Key Refresh Complete event shall occur when encryption has been resumed. @@ -34,7 +34,7 @@ public extension BluetoothHostControllerInterface { longTermKey: longTermKey) /// expect Command Status - LE Start Encryption - let commandStatus = try deviceRequest(command, HCICommandStatus.self, timeout: timeout) + let commandStatus = try await deviceRequest(command, HCICommandStatus.self, timeout: timeout) if let error = commandStatus.status.error { throw error diff --git a/Sources/BluetoothHCI/HCILETestEnd.swift b/Sources/BluetoothHCI/HCILETestEnd.swift index 0a1b2833b..28e9440d2 100644 --- a/Sources/BluetoothHCI/HCILETestEnd.swift +++ b/Sources/BluetoothHCI/HCILETestEnd.swift @@ -15,9 +15,9 @@ public extension BluetoothHostControllerInterface { /// LE Test End Command /// /// This command is used to stop any test which is in progress. - func lowEnergyTestEnd(timeout: HCICommandTimeout = .default) throws -> UInt16 { + func lowEnergyTestEnd(timeout: HCICommandTimeout = .default) async throws -> UInt16 { - let value = try deviceRequest(HCILETestEnd.self, + let value = try await deviceRequest(HCILETestEnd.self, timeout: timeout) return value.numberOfPackets diff --git a/Sources/BluetoothHCI/HCILETransmitterTest.swift b/Sources/BluetoothHCI/HCILETransmitterTest.swift index e41245a22..f56f27f76 100644 --- a/Sources/BluetoothHCI/HCILETransmitterTest.swift +++ b/Sources/BluetoothHCI/HCILETransmitterTest.swift @@ -19,11 +19,11 @@ public extension BluetoothHostControllerInterface { func lowEnergyTransmitterTest(txChannel: LowEnergyTxChannel, lengthOfTestData: UInt8, packetPayload: LowEnergyPacketPayload, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameters = HCILETransmitterTest(txChannel: txChannel, lengthOfTestData: lengthOfTestData, packetPayload: packetPayload) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILEUpdateConnection.swift b/Sources/BluetoothHCI/HCILEUpdateConnection.swift index edbe8c096..982eeded3 100644 --- a/Sources/BluetoothHCI/HCILEUpdateConnection.swift +++ b/Sources/BluetoothHCI/HCILEUpdateConnection.swift @@ -21,7 +21,7 @@ public extension BluetoothHostControllerInterface { connectionLatency: LowEnergyConnectionLatency = .zero, supervisionTimeout: LowEnergySupervisionTimeout = .max, connectionLength: LowEnergyConnectionLength = .full, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameters = HCILEUpdateConnection(connectionHandle: handle, connectionInterval: connectionInterval, @@ -29,7 +29,7 @@ public extension BluetoothHostControllerInterface { supervisionTimeout: supervisionTimeout, connectionLength: connectionLength) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILEWriteRfPathCompensation.swift b/Sources/BluetoothHCI/HCILEWriteRfPathCompensation.swift index c5fdb5f88..2fcbcf049 100644 --- a/Sources/BluetoothHCI/HCILEWriteRfPathCompensation.swift +++ b/Sources/BluetoothHCI/HCILEWriteRfPathCompensation.swift @@ -18,12 +18,12 @@ public extension BluetoothHostControllerInterface { /// the antenna contributed by intermediate components. func lowEnergyWriteRfPathCompensation(rfTxPathCompensationValue: LowEnergyRfTxPathCompensationValue, rfRxPathCompensationValue: LowEnergyRfTxPathCompensationValue, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameters = HCILEWriteRfPathCompensation(rfTxPathCompensationValue: rfTxPathCompensationValue, rfRxPathCompensationValue: rfRxPathCompensationValue) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILEWriteSuggestedDefaultDataLength.swift b/Sources/BluetoothHCI/HCILEWriteSuggestedDefaultDataLength.swift index 3877c1f3e..47b4d1c7a 100644 --- a/Sources/BluetoothHCI/HCILEWriteSuggestedDefaultDataLength.swift +++ b/Sources/BluetoothHCI/HCILEWriteSuggestedDefaultDataLength.swift @@ -18,12 +18,12 @@ public extension BluetoothHostControllerInterface { /// of payload octets and maximum packet transmission time to be used for new connections. func lowEnergyWriteSuggestedDefaultDataLength(suggestedMaxTxOctets: LowEnergyMaxTxOctets, suggestedMaxTxTime: LowEnergyMaxTxTime, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let parameters = HCILEWriteSuggestedDefaultDataLength(suggestedMaxTxOctets: suggestedMaxTxOctets, suggestedMaxTxTime: suggestedMaxTxTime) - try deviceRequest(parameters, timeout: timeout) + try await deviceRequest(parameters, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCILinkKeyRequestNegativeReply.swift b/Sources/BluetoothHCI/HCILinkKeyRequestNegativeReply.swift index 1f786642f..e0a7e53e0 100644 --- a/Sources/BluetoothHCI/HCILinkKeyRequestNegativeReply.swift +++ b/Sources/BluetoothHCI/HCILinkKeyRequestNegativeReply.swift @@ -17,11 +17,11 @@ public extension BluetoothHostControllerInterface { /// The Link_Key_Request_Reply command is used to reply to a Link Key Request event from the Controller, and specifies the Link Key stored on the Host to be used as the link key for the connection with the other BR/EDR Controller specified by BD_ADDR. The Link Key Request event will be generated when the BR/EDR Controller needs a Link Key for a connection. /// When the BR/EDR Controller generates a Link Key Request event in order for the local Link Manager to respond to the request from the remote Link Manager (as a result of a Create_Connection or Authentication_Requested com- mand from the remote Host), the local Host must respond with either a Link_Key_Request_Reply or Link_Key_Request_Negative_Reply command before the remote Link Manager detects LMP response timeout. func linkKeyRequestNegativeReply(address: BluetoothAddress, - timeout: HCICommandTimeout = .default) throws -> BluetoothAddress { + timeout: HCICommandTimeout = .default) async throws -> BluetoothAddress { let command = HCILinkKeyRequestNegativeReply(address: address) - return try deviceRequest(command, HCILinkKeyRequestNegativeReplyReturn.self, timeout: timeout).address + return try await deviceRequest(command, HCILinkKeyRequestNegativeReplyReturn.self, timeout: timeout).address } } diff --git a/Sources/BluetoothHCI/HCILinkKeyRequestReply.swift b/Sources/BluetoothHCI/HCILinkKeyRequestReply.swift index 5270b37cb..dc516b5a3 100644 --- a/Sources/BluetoothHCI/HCILinkKeyRequestReply.swift +++ b/Sources/BluetoothHCI/HCILinkKeyRequestReply.swift @@ -18,11 +18,11 @@ public extension BluetoothHostControllerInterface { /// When the BR/EDR Controller generates a Link Key Request event in order for the local Link Manager to respond to the request from the remote Link Manager (as a result of a Create_Connection or Authentication_Requested com- mand from the remote Host), the local Host must respond with either a Link_Key_Request_Reply or Link_Key_Request_Negative_Reply command before the remote Link Manager detects LMP response timeout. func linkKeyRequestReply(address: BluetoothAddress, linkKey: UInt128, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let linkKeyRequestReply = HCILinkKeyRequestReply(address: address, linkKey: linkKey) - try deviceRequest(linkKeyRequestReply, timeout: timeout) + try await deviceRequest(linkKeyRequestReply, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCIPINCodeRequestReply.swift b/Sources/BluetoothHCI/HCIPINCodeRequestReply.swift index 2b58bb64a..213c8c5ef 100644 --- a/Sources/BluetoothHCI/HCIPINCodeRequestReply.swift +++ b/Sources/BluetoothHCI/HCIPINCodeRequestReply.swift @@ -19,13 +19,13 @@ public extension BluetoothHostControllerInterface { func pinCodeRequestReply(address: BluetoothAddress, pinCodeLength: HCIPINCodeRequestReply.PINCodeLength, pinCode: UInt128, - timeout: HCICommandTimeout = .default) throws -> BluetoothAddress { + timeout: HCICommandTimeout = .default) async throws -> BluetoothAddress { let command = HCIPINCodeRequestReply(address: address, pinCodeLength: pinCodeLength, pinCode: pinCode) - return try deviceRequest(command, HCIPINCodeRequestReplyReturn.self, timeout: timeout).address + return try await deviceRequest(command, HCIPINCodeRequestReplyReturn.self, timeout: timeout).address } } diff --git a/Sources/BluetoothHCI/HCIPeriodicInquiryMode.swift b/Sources/BluetoothHCI/HCIPeriodicInquiryMode.swift index 144287d49..346e1ce35 100644 --- a/Sources/BluetoothHCI/HCIPeriodicInquiryMode.swift +++ b/Sources/BluetoothHCI/HCIPeriodicInquiryMode.swift @@ -20,7 +20,7 @@ public extension BluetoothHostControllerInterface { lap: HCIPeriodicInquiryMode.LAP, length: HCIPeriodicInquiryMode.Duration, responses: HCIPeriodicInquiryMode.Responses, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let periodicInquiryMode = HCIPeriodicInquiryMode(maxDuration: maxDuration, minDuration: minDuration, @@ -28,7 +28,7 @@ public extension BluetoothHostControllerInterface { length: length, responses: responses) - try deviceRequest(periodicInquiryMode, timeout: timeout) + try await deviceRequest(periodicInquiryMode, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCIQoSSetup.swift b/Sources/BluetoothHCI/HCIQoSSetup.swift index 4f40c08a0..f537563cd 100644 --- a/Sources/BluetoothHCI/HCIQoSSetup.swift +++ b/Sources/BluetoothHCI/HCIQoSSetup.swift @@ -23,7 +23,7 @@ public extension BluetoothHostControllerInterface { peakBandWidth: UInt32, latency: UInt32, delayVariation: UInt32, - timeout: HCICommandTimeout = .default) throws -> HCIQoSSetupComplete { + timeout: HCICommandTimeout = .default) async throws -> HCIQoSSetupComplete { let command = HCIQoSSetup(connectionHandle: connectionHandle, serviceType: serviceType, @@ -32,7 +32,7 @@ public extension BluetoothHostControllerInterface { latency: latency, delayVariation: delayVariation) - return try deviceRequest(command, HCIQoSSetupComplete.self, timeout: timeout) + return try await deviceRequest(command, HCIQoSSetupComplete.self, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCIReadClassOfDevice.swift b/Sources/BluetoothHCI/HCIReadClassOfDevice.swift index 3c61c5914..6cd1e91a4 100644 --- a/Sources/BluetoothHCI/HCIReadClassOfDevice.swift +++ b/Sources/BluetoothHCI/HCIReadClassOfDevice.swift @@ -15,9 +15,9 @@ public extension BluetoothHostControllerInterface { /// Read Class of Device Command /// /// This command writes the value for the Class_of_Device parameter. - func readClassOfDevice(timeout: HCICommandTimeout = .default) throws -> ClassOfDevice { + func readClassOfDevice(timeout: HCICommandTimeout = .default) async throws -> ClassOfDevice { - return try deviceRequest(HCIReadClassOfDeviceReturn.self, timeout: timeout).classOfDevice + return try await deviceRequest(HCIReadClassOfDeviceReturn.self, timeout: timeout).classOfDevice } } diff --git a/Sources/BluetoothHCI/HCIReadClockOffset.swift b/Sources/BluetoothHCI/HCIReadClockOffset.swift index e1a55045c..7bff3dfd6 100644 --- a/Sources/BluetoothHCI/HCIReadClockOffset.swift +++ b/Sources/BluetoothHCI/HCIReadClockOffset.swift @@ -16,11 +16,11 @@ public extension BluetoothHostControllerInterface { /// /// Both the System Clock and the clock offset to a remote device are used to determine what hopping frequency is used by a remote device for page scan. This command allows the Host to read clock offset to remote devices. The clock offset can be used to speed up the paging procedure when the local device tries to establish a connection to a remote device, for example, when the local Host has issued Create_Connection or Remote_Name_Request. The Connection_Handle must be a Connection_Handle for an ACL connection. func readClockOffset(handle: UInt16, - timeout: HCICommandTimeout = .default) throws -> HCIReadClockOffsetComplete.ClockOffset { + timeout: HCICommandTimeout = .default) async throws -> HCIReadClockOffsetComplete.ClockOffset { let completeEvent = HCIReadClockOffset(handle: handle) - return try deviceRequest(completeEvent, + return try await deviceRequest(completeEvent, HCIReadClockOffsetComplete.self, timeout: timeout).clockOffset } diff --git a/Sources/BluetoothHCI/HCIReadDataBlockSize.swift b/Sources/BluetoothHCI/HCIReadDataBlockSize.swift index f12a08e02..3bd854c78 100644 --- a/Sources/BluetoothHCI/HCIReadDataBlockSize.swift +++ b/Sources/BluetoothHCI/HCIReadDataBlockSize.swift @@ -16,9 +16,9 @@ public extension BluetoothHostControllerInterface { /// /// The Read_Data_Block_Size command is used to read values regarding the maximum permitted data transfers over the Controller and the data buffering available in the Controller. /// The Host uses this information when fragmenting data for transmission, and when performing block-based flow control, based on the Number Of Completed Data Blocks event. The Read_Data_Block_Size command shall be issued by the Host before it sends any data to the Controller. - func readDataBlockSize(timeout: HCICommandTimeout = .default) throws -> HCIReadDataBlockSizeReturn { + func readDataBlockSize(timeout: HCICommandTimeout = .default) async throws -> HCIReadDataBlockSizeReturn { - return try deviceRequest(HCIReadDataBlockSizeReturn.self, timeout: timeout) + return try await deviceRequest(HCIReadDataBlockSizeReturn.self, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCIReadDeviceAddress.swift b/Sources/BluetoothHCI/HCIReadDeviceAddress.swift index 5c86353da..facb2178c 100644 --- a/Sources/BluetoothHCI/HCIReadDeviceAddress.swift +++ b/Sources/BluetoothHCI/HCIReadDeviceAddress.swift @@ -13,9 +13,9 @@ import Foundation public extension BluetoothHostControllerInterface { /// Read Device Address - func readDeviceAddress(timeout: HCICommandTimeout = .default) throws -> BluetoothAddress { + func readDeviceAddress(timeout: HCICommandTimeout = .default) async throws -> BluetoothAddress { - return try deviceRequest(HCIReadDeviceAddress.self, timeout: timeout).address + return try await deviceRequest(HCIReadDeviceAddress.self, timeout: timeout).address } } diff --git a/Sources/BluetoothHCI/HCIReadLMPHandle.swift b/Sources/BluetoothHCI/HCIReadLMPHandle.swift index 14469c69e..e36820bee 100644 --- a/Sources/BluetoothHCI/HCIReadLMPHandle.swift +++ b/Sources/BluetoothHCI/HCIReadLMPHandle.swift @@ -16,11 +16,11 @@ public extension BluetoothHostControllerInterface { /// /// This command reads the current LMP Handle associated with the Connection_Handle. The Connection_Handle shall be a SCO or eSCO Handle. If the Connection_Handle is a SCO Connection_Handle, then this command shall read the LMP SCO Handle for this connection. If the Connection_Handle is an eSCO Connection_Handle, then this command shall read the LMP eSCO Handle for this connection. func readLMPHandle(handle: UInt16, - timeout: HCICommandTimeout = .default) throws -> UInt8 { + timeout: HCICommandTimeout = .default) async throws -> UInt8 { let lmpHandle = HCIReadLMPHandle(handle: handle) - return try deviceRequest(lmpHandle, + return try await deviceRequest(lmpHandle, HCIReadLMPHandleReturn.self, timeout: timeout).lmpHandle } diff --git a/Sources/BluetoothHCI/HCIReadLocalName.swift b/Sources/BluetoothHCI/HCIReadLocalName.swift index e3d3c538f..42916782c 100644 --- a/Sources/BluetoothHCI/HCIReadLocalName.swift +++ b/Sources/BluetoothHCI/HCIReadLocalName.swift @@ -15,9 +15,9 @@ public extension BluetoothHostControllerInterface { /// Write Local Name Command /// /// Provides the ability to modify the user- friendly name for the BR/EDR Controller. - func readLocalName(timeout: HCICommandTimeout = .default) throws -> String { + func readLocalName(timeout: HCICommandTimeout = .default) async throws -> String { - let value = try deviceRequest(HCIReadLocalName.self, + let value = try await deviceRequest(HCIReadLocalName.self, timeout: timeout) return value.localName diff --git a/Sources/BluetoothHCI/HCIReadLocalSupportedFeatures.swift b/Sources/BluetoothHCI/HCIReadLocalSupportedFeatures.swift index 850ba512f..e2ba96b5e 100644 --- a/Sources/BluetoothHCI/HCIReadLocalSupportedFeatures.swift +++ b/Sources/BluetoothHCI/HCIReadLocalSupportedFeatures.swift @@ -15,9 +15,9 @@ public extension BluetoothHostControllerInterface { /// LE Read Local Supported Features Command /// /// This command requests the list of the supported LE features for the Controller. - func readLocalSupportedFeatures(timeout: HCICommandTimeout = .default) throws -> BitMaskOptionSet { + func readLocalSupportedFeatures(timeout: HCICommandTimeout = .default) async throws -> BitMaskOptionSet { - let returValue = try deviceRequest(HCIReadLocalSupportedFeaturesReturn.self, timeout: timeout) + let returValue = try await deviceRequest(HCIReadLocalSupportedFeaturesReturn.self, timeout: timeout) return returValue.features } diff --git a/Sources/BluetoothHCI/HCIReadLocalVersionInformation.swift b/Sources/BluetoothHCI/HCIReadLocalVersionInformation.swift index db531f02f..98adf72da 100644 --- a/Sources/BluetoothHCI/HCIReadLocalVersionInformation.swift +++ b/Sources/BluetoothHCI/HCIReadLocalVersionInformation.swift @@ -13,9 +13,9 @@ import Foundation public extension BluetoothHostControllerInterface { /// This command reads the values for the version information for the local Controller. - func readLocalVersionInformation(timeout: HCICommandTimeout = .default) throws -> HCILocalVersionInformation { + func readLocalVersionInformation(timeout: HCICommandTimeout = .default) async throws -> HCILocalVersionInformation { - return try deviceRequest(HCILocalVersionInformation.self, timeout: timeout) + return try await deviceRequest(HCILocalVersionInformation.self, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCIReadPageTimeout.swift b/Sources/BluetoothHCI/HCIReadPageTimeout.swift index cf15d0411..0527b61a7 100644 --- a/Sources/BluetoothHCI/HCIReadPageTimeout.swift +++ b/Sources/BluetoothHCI/HCIReadPageTimeout.swift @@ -15,9 +15,9 @@ public extension BluetoothHostControllerInterface { /// Read Page Timeout Command /// /// This command reads the value for the Page_Timeout configuration parameter. - func readPageTimeout(timeout: HCICommandTimeout = .default) throws -> HCIReadPageTimeoutReturn { + func readPageTimeout(timeout: HCICommandTimeout = .default) async throws -> HCIReadPageTimeoutReturn { - return try deviceRequest(HCIReadPageTimeoutReturn.self, timeout: timeout) + return try await deviceRequest(HCIReadPageTimeoutReturn.self, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCIReadRemoteExtendedFeatures.swift b/Sources/BluetoothHCI/HCIReadRemoteExtendedFeatures.swift index e629da181..6e0affd9c 100644 --- a/Sources/BluetoothHCI/HCIReadRemoteExtendedFeatures.swift +++ b/Sources/BluetoothHCI/HCIReadRemoteExtendedFeatures.swift @@ -17,11 +17,11 @@ public extension BluetoothHostControllerInterface { /// The Read_Remote_Extended_Features command returns the requested page of the extended LMP features for the remote device identified by the specified Connection_Handle. The Connection_Handle must be the Connection_Handle for an ACL connection. This command is only available if the extended features feature is implemented by the remote device. The Read Remote Extended Features Complete event will return the requested information. func readRemoteExtendedFeatures(handle: UInt16, pageNumber: UInt8, - timeout: HCICommandTimeout = .default) throws -> BitMaskOptionSet { + timeout: HCICommandTimeout = .default) async throws -> BitMaskOptionSet { let readRemoteExtendedFeatures = HCIReadRemoteExtendedFeatures(handle: handle, pageNumber: pageNumber) - return try deviceRequest(readRemoteExtendedFeatures, + return try await deviceRequest(readRemoteExtendedFeatures, HCIReadRemoteExtendedFeaturesComplete.self, timeout: timeout).features } diff --git a/Sources/BluetoothHCI/HCIReadRemoteSupportedFeatures.swift b/Sources/BluetoothHCI/HCIReadRemoteSupportedFeatures.swift index ee1bb31a0..ca13966b2 100644 --- a/Sources/BluetoothHCI/HCIReadRemoteSupportedFeatures.swift +++ b/Sources/BluetoothHCI/HCIReadRemoteSupportedFeatures.swift @@ -16,11 +16,11 @@ public extension BluetoothHostControllerInterface { /// /// This command requests a list of the supported features for the remote device identified by the Connection_Handle parameter. The Connection_Handle must be a Connection_Handle for an ACL connection. The Read Remote Supported Features Complete event will return a list of the LMP features. func readRemoteSupportedFeatures(handle: UInt16, - timeout: HCICommandTimeout = .default) throws -> BitMaskOptionSet { + timeout: HCICommandTimeout = .default) async throws -> BitMaskOptionSet { let readRemoteSupportedFeatures = HCIReadRemoteSupportedFeatures(handle: handle) - return try deviceRequest(readRemoteSupportedFeatures, + return try await deviceRequest(readRemoteSupportedFeatures, HCIReadRemoteSupportedFeaturesComplete.self, timeout: timeout).features } diff --git a/Sources/BluetoothHCI/HCIReadRemoteVersionInformation.swift b/Sources/BluetoothHCI/HCIReadRemoteVersionInformation.swift index a18b8b6b5..5f30139d1 100644 --- a/Sources/BluetoothHCI/HCIReadRemoteVersionInformation.swift +++ b/Sources/BluetoothHCI/HCIReadRemoteVersionInformation.swift @@ -16,11 +16,11 @@ public extension BluetoothHostControllerInterface { /// /// This command will obtain the values for the version information for the remote device identified by the Connection_Handle parameter. The Connection_Handle must be a Connection_Handle for an ACL or LE connection. func readRemoteVersionInformation(handle: UInt16, - timeout: HCICommandTimeout = .default) throws -> HCIReadRemoteVersionInformationComplete { + timeout: HCICommandTimeout = .default) async throws -> HCIReadRemoteVersionInformationComplete { let readRemoteVersionInformation = HCIReadRemoteVersionInformation(handle: handle) - return try deviceRequest(readRemoteVersionInformation, + return try await deviceRequest(readRemoteVersionInformation, HCIReadRemoteVersionInformationComplete.self, timeout: timeout) } diff --git a/Sources/BluetoothHCI/HCIReadStoredLinkKey.swift b/Sources/BluetoothHCI/HCIReadStoredLinkKey.swift index 1bc611bbe..c7cf6747d 100644 --- a/Sources/BluetoothHCI/HCIReadStoredLinkKey.swift +++ b/Sources/BluetoothHCI/HCIReadStoredLinkKey.swift @@ -17,11 +17,11 @@ public extension BluetoothHostControllerInterface { /// The Read_Stored_Link_Key command provides the ability to read whether one or more link keys are stored in the BR/EDR Controller. The BR/EDR Controller can store a limited number of link keys for other BR/EDR Controllers. Link keys are shared between two BR/EDR Controllers, and are used for all security transactions between the two devices. The read link key command shall not return the link keys value. A Host device may have additional storage capabilities, which can be used to save additional link keys to be reloaded to the BR/ EDR Controller when needed. The Read_All_Flag parameter is used to indicate if all of the stored Link Keys should be returned. If Read_All_Flag indicates that all Link Keys are to be returned, then the BD_ADDR command parameter must be ignored. The BD_ADDR command parameter is used to identify which link key to read. The stored Link Keys are returned by one or more Return Link Keys events. func readStoredLinkKey(address: BluetoothAddress, readFlag: HCIReadStoredLinkKey.ReadFlag, - timeout: HCICommandTimeout = .default) throws -> HCIReadStoredLinkKeyReturn { + timeout: HCICommandTimeout = .default) async throws -> HCIReadStoredLinkKeyReturn { let command = HCIReadStoredLinkKey(address: address, readFlag: readFlag) - return try deviceRequest(command, HCIReadStoredLinkKeyReturn.self, timeout: timeout) + return try await deviceRequest(command, HCIReadStoredLinkKeyReturn.self, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCIRejectConnectionRequest.swift b/Sources/BluetoothHCI/HCIRejectConnectionRequest.swift index adf7e79f8..da40dacb4 100644 --- a/Sources/BluetoothHCI/HCIRejectConnectionRequest.swift +++ b/Sources/BluetoothHCI/HCIRejectConnectionRequest.swift @@ -17,11 +17,11 @@ public extension BluetoothHostControllerInterface { /// The Reject_Connection_Request command is used to decline a new incoming connection request. The Reject_Connection_Request command shall only be called after a Connection Request event has occurred. The Connection Request event will return the BD_ADDR of the device that is requesting the connection. The Reason command parameter will be returned to the connecting device in the Status parameter of the Connection Complete event returned to the Host of the connection device, to indicate why the connection was declined. func rejectConnection(address: BluetoothAddress, error: HCIError, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let rejectConnectionCancel = HCIRejectConnectionRequest(address: address, error: error) - try deviceRequest(rejectConnectionCancel, timeout: timeout) + try await deviceRequest(rejectConnectionCancel, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCIRemoteNameRequest.swift b/Sources/BluetoothHCI/HCIRemoteNameRequest.swift index 076a7914f..56c70d5d8 100644 --- a/Sources/BluetoothHCI/HCIRemoteNameRequest.swift +++ b/Sources/BluetoothHCI/HCIRemoteNameRequest.swift @@ -22,13 +22,13 @@ public extension BluetoothHostControllerInterface { func remoteNameRequest(address: BluetoothAddress, pscanRepMode: PageScanRepetitionMode, clockOffset: HCIRemoteNameRequest.ClockOffset, - timeout: HCICommandTimeout = .default) throws -> HCIRemoteNameRequestComplete { + timeout: HCICommandTimeout = .default) async throws -> HCIRemoteNameRequestComplete { let remoteNameRequest = HCIRemoteNameRequest(address: address, pscanRepMode: pscanRepMode, clockOffset: clockOffset) - return try deviceRequest(remoteNameRequest, HCIRemoteNameRequestComplete.self, timeout: timeout) + return try await deviceRequest(remoteNameRequest, HCIRemoteNameRequestComplete.self, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCIReset.swift b/Sources/BluetoothHCI/HCIReset.swift index 0d6dbafe7..f7b6b894d 100644 --- a/Sources/BluetoothHCI/HCIReset.swift +++ b/Sources/BluetoothHCI/HCIReset.swift @@ -17,8 +17,8 @@ public extension BluetoothHostControllerInterface { /// The Reset command will reset the Controller and the Link Manager on the BR/ EDR Controller, the PAL on an AMP Controller, or the Link Layer on an LE Controller. If the Controller supports both BR/EDR and LE then the Reset command shall reset the Link Manager, Baseband and Link Layer. The Reset command shall not affect the used HCI transport layer since the HCI transport layers may have reset mechanisms of their own. After the reset is completed, the current operational state will be lost, the Controller will enter standby mode and the Controller will automatically revert to the default values for the parameters for which default values are defined in the specification. /// - Note: The Reset command will not necessarily perform a hardware reset. This is implementation defined. On an AMP Controller, the Reset command shall reset the service provided at the logical HCI to its initial state , but beyond this the exact effect on the Controller device is implementation defined and should not interrupt the service provided to other protocol stacks. /// The Host shall not send additional HCI commands before the Command Com- plete event related to the Reset command has been received. - func reset(timeout: HCICommandTimeout = .default) throws { + func reset(timeout: HCICommandTimeout = .default) async throws { - return try deviceRequest(HostControllerBasebandCommand.reset, timeout: timeout) + return try await deviceRequest(HostControllerBasebandCommand.reset, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCISetConnectionEncryption.swift b/Sources/BluetoothHCI/HCISetConnectionEncryption.swift index aa39888e9..6f527ad63 100644 --- a/Sources/BluetoothHCI/HCISetConnectionEncryption.swift +++ b/Sources/BluetoothHCI/HCISetConnectionEncryption.swift @@ -17,11 +17,11 @@ public extension BluetoothHostControllerInterface { /// The Set_Connection_Encryption command is used to enable and disable the link level encryption. Note: the Connection_Handle command parameter is used to identify the other BR/EDR Controller which forms the connection. The Connection_Handle should be a Connection_Handle for an ACL connection. While the encryption is being changed, all ACL traffic must be turned off for all Connection_Handles associated with the remote device. func setConnectionEncryption(handle: UInt16, encryption: HCISetConnectionEncryption.Encryption, - timeout: HCICommandTimeout = .default) throws -> HCIEncryptionChange { + timeout: HCICommandTimeout = .default) async throws -> HCIEncryptionChange { let connectionEncryption = HCISetConnectionEncryption(handle: handle, encryption: encryption) - return try deviceRequest(connectionEncryption, HCIEncryptionChange.self, timeout: timeout) + return try await deviceRequest(connectionEncryption, HCIEncryptionChange.self, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCIUserConfirmationRequestReply.swift b/Sources/BluetoothHCI/HCIUserConfirmationRequestReply.swift index 3d36c1346..8989a8ebd 100644 --- a/Sources/BluetoothHCI/HCIUserConfirmationRequestReply.swift +++ b/Sources/BluetoothHCI/HCIUserConfirmationRequestReply.swift @@ -14,13 +14,14 @@ public extension BluetoothHostControllerInterface { /// User Confirmation Request Reply Command /// - /// The User_Confirmation_Request_Reply command is used to reply to a User Confirmation Request event and indicates that the user selected "yes". It is also used when the host has no input and no output capabilities. - func userConfirmationRequestReply(address: BluetoothAddress, - timeout: HCICommandTimeout = .default) throws -> BluetoothAddress { - + /// The User_Confirmation_Request_Reply command is used to reply to a User Confirmation Request event and indicates that the user selected "yes". + /// It is also used when the host has no input and no output capabilities. + func userConfirmationRequestReply( + address: BluetoothAddress, + timeout: HCICommandTimeout = .default + ) async throws -> BluetoothAddress { let command = HCIUserConfirmationRequestReply(address: address) - - return try deviceRequest(command, HCIUserConfirmationRequestReplyReturn.self, timeout: timeout).address + return try await deviceRequest(command, HCIUserConfirmationRequestReplyReturn.self, timeout: timeout).address } } @@ -65,12 +66,9 @@ public struct HCIUserConfirmationRequestReplyReturn: HCICommandReturnParameter { public let address: BluetoothAddress public init?(data: Data) { - guard data.count == HCIUserConfirmationRequestReplyReturn.length else { return nil } - let address = BluetoothAddress(littleEndian: BluetoothAddress(bytes: (data[0], data[1], data[2], data[3], data[4], data[5]))) - self.address = address } } diff --git a/Sources/BluetoothHCI/HCIWriteClassOfDevice.swift b/Sources/BluetoothHCI/HCIWriteClassOfDevice.swift index cd0a35863..0f7279317 100644 --- a/Sources/BluetoothHCI/HCIWriteClassOfDevice.swift +++ b/Sources/BluetoothHCI/HCIWriteClassOfDevice.swift @@ -16,11 +16,11 @@ public extension BluetoothHostControllerInterface { /// /// This command writes the value for the Class_of_Device parameter. func writeClassOfDevice(classOfDevice: ClassOfDevice, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let command = HCIWriteClassOfDevice(classOfDevice: classOfDevice) - return try deviceRequest(command, timeout: timeout) + return try await deviceRequest(command, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCIWriteLinkPolicySettings.swift b/Sources/BluetoothHCI/HCIWriteLinkPolicySettings.swift index 349021127..ad771a4e7 100644 --- a/Sources/BluetoothHCI/HCIWriteLinkPolicySettings.swift +++ b/Sources/BluetoothHCI/HCIWriteLinkPolicySettings.swift @@ -18,12 +18,12 @@ public extension BluetoothHostControllerInterface { /// The default value is the value set by the Write Default Link Policy Settings Command. func writeLinkPolicySettings(connectionHandle: UInt16, settings: BitMaskOptionSet, - timeout: HCICommandTimeout = .default) throws -> HCIWriteLinkPolicySettingsReturn { + timeout: HCICommandTimeout = .default) async throws -> HCIWriteLinkPolicySettingsReturn { let command = HCIWriteLinkPolicySettings(connectionHandle: connectionHandle, settings: settings) - return try deviceRequest(command, HCIWriteLinkPolicySettingsReturn.self, timeout: timeout) + return try await deviceRequest(command, HCIWriteLinkPolicySettingsReturn.self, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCIWriteLinkSupervisionTimeout.swift b/Sources/BluetoothHCI/HCIWriteLinkSupervisionTimeout.swift index 40c909b84..d92e1b78c 100644 --- a/Sources/BluetoothHCI/HCIWriteLinkSupervisionTimeout.swift +++ b/Sources/BluetoothHCI/HCIWriteLinkSupervisionTimeout.swift @@ -20,12 +20,12 @@ public extension BluetoothHostControllerInterface { /// Timeout values for other Synchronous Handles to that device. func writeLinkSupervisionTimeout(handle: UInt16, linkSupervisionTimeout: HCIWriteLinkSupervisionTimeout.LinkSupervisionTimeout, - timeout: HCICommandTimeout = .default) throws -> HCIWriteLinkSupervisionTimeoutReturn { + timeout: HCICommandTimeout = .default) async throws -> HCIWriteLinkSupervisionTimeoutReturn { let command = HCIWriteLinkSupervisionTimeout(handle: handle, linkSupervisionTimeout: linkSupervisionTimeout) - return try deviceRequest(command, HCIWriteLinkSupervisionTimeoutReturn.self, timeout: timeout) + return try await deviceRequest(command, HCIWriteLinkSupervisionTimeoutReturn.self, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCIWriteLocalName.swift b/Sources/BluetoothHCI/HCIWriteLocalName.swift index ddefa79bf..10254feb9 100644 --- a/Sources/BluetoothHCI/HCIWriteLocalName.swift +++ b/Sources/BluetoothHCI/HCIWriteLocalName.swift @@ -15,12 +15,12 @@ public extension BluetoothHostControllerInterface { /// Read Local Name Command /// /// Provides the ability to read the stored user-friendly name for the BR/EDR Controller. - func writeLocalName(_ newValue: String, timeout: HCICommandTimeout = .default) throws { + func writeLocalName(_ newValue: String, timeout: HCICommandTimeout = .default) async throws { guard let command = HCIWriteLocalName(localName: newValue) else { fatalError("Invalid string length \(newValue)") } - try deviceRequest(command, timeout: timeout) + try await deviceRequest(command, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCIWritePageScanActivity.swift b/Sources/BluetoothHCI/HCIWritePageScanActivity.swift index d8aab0150..7d4ec629d 100644 --- a/Sources/BluetoothHCI/HCIWritePageScanActivity.swift +++ b/Sources/BluetoothHCI/HCIWritePageScanActivity.swift @@ -17,12 +17,12 @@ public extension BluetoothHostControllerInterface { /// This command writes the values for the Page_Scan_Interval and Page_Scan_Window configuration parameters. The Page_Scan_Window shall be less than or equal to the Page_Scan_Interval. func writePageScanActivity(scanInterval: HCIWritePageScanActivity.PageScanInterval, scanWindow: HCIWritePageScanActivity.PageScanWindow, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let command = HCIWritePageScanActivity(scanInterval: scanInterval, scanWindow: scanWindow) - return try deviceRequest(command, timeout: timeout) + return try await deviceRequest(command, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCIWritePageScanType.swift b/Sources/BluetoothHCI/HCIWritePageScanType.swift index 86d3aa98a..23a919c89 100644 --- a/Sources/BluetoothHCI/HCIWritePageScanType.swift +++ b/Sources/BluetoothHCI/HCIWritePageScanType.swift @@ -16,11 +16,11 @@ public extension BluetoothHostControllerInterface { /// /// This command writes the Page Scan Type configuration parameter of the local BR/EDR Controller. func writePageScanType(pageScanType: HCIWritePageScanType.PageScanType, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let command = HCIWritePageScanType(pageScanType: pageScanType) - return try deviceRequest(command, timeout: timeout) + return try await deviceRequest(command, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCIWritePageTimeout.swift b/Sources/BluetoothHCI/HCIWritePageTimeout.swift index 27c9ebf59..cf8612402 100644 --- a/Sources/BluetoothHCI/HCIWritePageTimeout.swift +++ b/Sources/BluetoothHCI/HCIWritePageTimeout.swift @@ -16,11 +16,11 @@ public extension BluetoothHostControllerInterface { /// /// This command writes the value for the Page_Timeout configuration parameter. The Page_Timeout configuration parameter defines the maximum time the local Link Manager shall wait for a baseband page response from the remote device at a locally initiated connection attempt. If this time expires and the remote device has not responded to the page at baseband level, the connec- tion attempt will be considered to have failed. func writePageTimeout(pageTimeout: HCIWritePageTimeout.PageTimeout, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let command = HCIWritePageTimeout(pageTimeout: pageTimeout) - return try deviceRequest(command, timeout: timeout) + return try await deviceRequest(command, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/HCIWriteScanEnable.swift b/Sources/BluetoothHCI/HCIWriteScanEnable.swift index 8e644ab92..1667083de 100644 --- a/Sources/BluetoothHCI/HCIWriteScanEnable.swift +++ b/Sources/BluetoothHCI/HCIWriteScanEnable.swift @@ -16,11 +16,11 @@ public extension BluetoothHostControllerInterface { /// /// This command writes the value for the Scan_Enable configuration parameter. func writeScanEnable(scanEnable: HCIWriteScanEnable.ScanEnable, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { let command = HCIWriteScanEnable(scanEnable: scanEnable) - return try deviceRequest(command, timeout: timeout) + return try await deviceRequest(command, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/LowEnergyAdvertising.swift b/Sources/BluetoothHCI/LowEnergyAdvertising.swift index e148d12bd..082237e00 100644 --- a/Sources/BluetoothHCI/LowEnergyAdvertising.swift +++ b/Sources/BluetoothHCI/LowEnergyAdvertising.swift @@ -20,9 +20,9 @@ public extension BluetoothHostControllerInterface { /// then the Controller shall return the error code Command Disallowed (0x0C). /// /// Note: All advertising sets are cleared on HCI reset. - func lowEnergyClearAdvertisingSets(timeout: HCICommandTimeout = .default) throws { + func lowEnergyClearAdvertisingSets(timeout: HCICommandTimeout = .default) async throws { - try deviceRequest(HCILowEnergyCommand.clearAdvertisingSets, timeout: timeout) + try await deviceRequest(HCILowEnergyCommand.clearAdvertisingSets, timeout: timeout) } /// LE Periodic Advertising Create Sync Cancel Command @@ -31,9 +31,9 @@ public extension BluetoothHostControllerInterface { /// /// If the Host issues this command while no LE_Periodic_Advertising_Create_Sync command is pending, /// the Controller shall return the error code Command Disallowed (0x0C). - func lowEnergyPeriodicAdvertisingCreateSyncCancel(timeout: HCICommandTimeout = .default) throws { + func lowEnergyPeriodicAdvertisingCreateSyncCancel(timeout: HCICommandTimeout = .default) async throws { - try deviceRequest(HCILowEnergyCommand.periodicAdvertisingCreateSyncCancel, timeout: timeout) + try await deviceRequest(HCILowEnergyCommand.periodicAdvertisingCreateSyncCancel, timeout: timeout) } /// LE Clear Periodic Advertiser List Command @@ -43,9 +43,9 @@ public extension BluetoothHostControllerInterface { /// /// If this command is used when an LE_Periodic_Advertising_Create_Sync command is pending, /// the Controller shall return the error code Command Disallowed (0x0C). - func lowEnergyClearPeriodicAdvertiserList(timeout: HCICommandTimeout = .default) throws { + func lowEnergyClearPeriodicAdvertiserList(timeout: HCICommandTimeout = .default) async throws { - try deviceRequest(HCILowEnergyCommand.clearPeriodicAdvertiserList, timeout: timeout) //0x0049 + try await deviceRequest(HCILowEnergyCommand.clearPeriodicAdvertiserList, timeout: timeout) //0x0049 } } diff --git a/Sources/BluetoothHCI/LowEnergyConnection.swift b/Sources/BluetoothHCI/LowEnergyConnection.swift index a98902b07..99d56855c 100644 --- a/Sources/BluetoothHCI/LowEnergyConnection.swift +++ b/Sources/BluetoothHCI/LowEnergyConnection.swift @@ -14,18 +14,18 @@ public extension BluetoothHostControllerInterface { /// This command shall only be issued after the LE_Create_Connection command has been issued, /// a Command Status event has been received for the LE Create Connection command and before /// the LE Connection Complete event. - func lowEnergyCreateConnectionCancel(timeout: HCICommandTimeout = .default) throws { + func lowEnergyCreateConnectionCancel(timeout: HCICommandTimeout = .default) async throws { // cancel connection - try deviceRequest(HCILowEnergyCommand.createConnectionCancel, timeout: timeout) + try await deviceRequest(HCILowEnergyCommand.createConnectionCancel, timeout: timeout) } /// LE Read Local P-256 Public Key Command /// /// This command is used to return the local P-256 public key from the Controller. - func lowEnergyReadLocalP256PublicKey(timeout: HCICommandTimeout = .default) throws -> UInt512 { + func lowEnergyReadLocalP256PublicKey(timeout: HCICommandTimeout = .default) async throws -> UInt512 { - let event = try deviceRequest(HCILowEnergyCommand.readLocalP256PublicKeyCommand, + let event = try await deviceRequest(HCILowEnergyCommand.readLocalP256PublicKeyCommand, HCILEReadLocalP256PublicKeyComplete.self, timeout: timeout) diff --git a/Sources/BluetoothHCI/LowEnergyResolvingList.swift b/Sources/BluetoothHCI/LowEnergyResolvingList.swift index 3a28bd57d..1f48b8a97 100644 --- a/Sources/BluetoothHCI/LowEnergyResolvingList.swift +++ b/Sources/BluetoothHCI/LowEnergyResolvingList.swift @@ -12,9 +12,7 @@ public extension BluetoothHostControllerInterface { /// /// The command is used to remove all devices from the list of address translations /// used to resolve Resolvable Private Addresses in the Controller. - func lowEnergyClearResolvingList(timeout: HCICommandTimeout = .default) throws { - - try deviceRequest(HCILowEnergyCommand.clearResolvedList, timeout: timeout) + func lowEnergyClearResolvingList(timeout: HCICommandTimeout = .default) async throws { + try await deviceRequest(HCILowEnergyCommand.clearResolvedList, timeout: timeout) } - } diff --git a/Sources/BluetoothHCI/LowEnergyWhiteList.swift b/Sources/BluetoothHCI/LowEnergyWhiteList.swift index cc0a983c0..a1a6ef873 100644 --- a/Sources/BluetoothHCI/LowEnergyWhiteList.swift +++ b/Sources/BluetoothHCI/LowEnergyWhiteList.swift @@ -11,10 +11,10 @@ public extension BluetoothHostControllerInterface { /// LE Clear White List Command /// /// Used to clear the White List stored in the Controller. - func lowEnergyClearWhiteList(timeout: HCICommandTimeout = .default) throws { + func lowEnergyClearWhiteList(timeout: HCICommandTimeout = .default) async throws { // clear white list - try deviceRequest(HCILowEnergyCommand.clearWhiteList, timeout: timeout) + try await deviceRequest(HCILowEnergyCommand.clearWhiteList, timeout: timeout) } } diff --git a/Sources/BluetoothHCI/iBeacon.swift b/Sources/BluetoothHCI/iBeacon.swift index dfb7804aa..d66aa3015 100644 --- a/Sources/BluetoothHCI/iBeacon.swift +++ b/Sources/BluetoothHCI/iBeacon.swift @@ -15,24 +15,24 @@ public extension BluetoothHostControllerInterface { func iBeacon(_ beacon: AppleBeacon, flags: GAPFlags = [.lowEnergyGeneralDiscoverableMode, .notSupportedBREDR], interval: AdvertisingInterval = .default, - timeout: HCICommandTimeout = .default) throws { + timeout: HCICommandTimeout = .default) async throws { // stop advertising - do { try enableLowEnergyAdvertising(false, timeout: timeout) } + do { try await enableLowEnergyAdvertising(false, timeout: timeout) } catch HCIError.commandDisallowed { /* ignore, means already turned off */ } // set advertising parameters let advertisingParameters = HCILESetAdvertisingParameters(interval: (min: interval, max: interval)) - try deviceRequest(advertisingParameters, timeout: timeout) + try await deviceRequest(advertisingParameters, timeout: timeout) // start advertising - do { try enableLowEnergyAdvertising(timeout: timeout) } + do { try await enableLowEnergyAdvertising(timeout: timeout) } catch HCIError.commandDisallowed { /* ignore, means already turned on */ } // set iBeacon data let advertisingDataCommand = HCILESetAdvertisingData(advertisingData: LowEnergyAdvertisingData(beacon: beacon, flags: flags)) - try deviceRequest(advertisingDataCommand, timeout: timeout) + try await deviceRequest(advertisingDataCommand, timeout: timeout) } } From 59acc93d1f2212b3b85fe616a0bb463bd5ba06db Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 18 Dec 2021 10:57:08 -0500 Subject: [PATCH 20/44] [BluetoothGATT] Working on async support --- Sources/Bluetooth/L2CAPSocket.swift | 21 ++++- Sources/BluetoothGATT/ATTConnection.swift | 94 +++++++++++++++++------ Sources/BluetoothGATT/GATTClient.swift | 4 +- Sources/BluetoothGATT/GATTServer.swift | 6 +- 4 files changed, 91 insertions(+), 34 deletions(-) diff --git a/Sources/Bluetooth/L2CAPSocket.swift b/Sources/Bluetooth/L2CAPSocket.swift index b19954177..ebcbd6ef6 100644 --- a/Sources/Bluetooth/L2CAPSocket.swift +++ b/Sources/Bluetooth/L2CAPSocket.swift @@ -17,16 +17,29 @@ public protocol L2CAPSocket { /// Reads from the socket. func recieve(_ bufferSize: Int) async throws -> Data + /// Attempt to accept an incoming connection. + func accept(sleep: UInt64) async throws -> Self + /// Attempts to change the socket's security level. func setSecurityLevel(_ securityLevel: SecurityLevel) async throws /// Get security level func securityLevel() async throws -> SecurityLevel - /// Attempt to accept an incoming connection. - func accept(sleep: UInt64) async throws -> Self + /// + func hasPendingEvents() async throws -> (canWrite: Bool, canRead: Bool) - //func canWrite() throws -> Bool + /// Creates a new socket connected to the remote address specified. + static func lowEnergyClient( + address: BluetoothAddress, + destination: BluetoothAddress, + isRandom: Bool + ) async throws -> Self - //func canRead() throws -> Bool + /// Creates a new server, + static func lowEnergyServer( + address: BluetoothAddress, + isRandom: Bool, + backlog: Int + ) throws -> Self } diff --git a/Sources/BluetoothGATT/ATTConnection.swift b/Sources/BluetoothGATT/ATTConnection.swift index c7dc2b48e..fc6bd27df 100755 --- a/Sources/BluetoothGATT/ATTConnection.swift +++ b/Sources/BluetoothGATT/ATTConnection.swift @@ -17,11 +17,15 @@ internal actor ATTConnection { /// Actual number of bytes for PDU ATT exchange. public private(set) var maximumTransmissionUnit: ATTMaximumTransmissionUnit = .default - public let socket: L2CAPSocket + internal private(set) var socket: L2CAPSocket? public private(set) var log: ((String) -> ())? - public private(set) var writePending: (() -> ())? + public var isConnected: Bool { + get async { + return socket != nil + } + } // MARK: - Private Properties @@ -56,26 +60,55 @@ internal actor ATTConnection { deinit { unregisterAll() + disconnect() } public init( socket: L2CAPSocket, - log: ((String) -> ())? = nil, - writePending: (() -> ())? = nil - ) { + log: ((String) -> ())? = nil + ) async { self.socket = socket self.log = log - self.writePending = writePending + run() } // MARK: - Methods - public func setMaximumTransmissionUnit(_ newValue: ATTMaximumTransmissionUnit) { - self.maximumTransmissionUnit = newValue + public func disconnect(_ error: Swift.Error? = nil) { + guard socket != nil else { + return + } + // log error + if let error = error { + log?("Disconnected (\(error.localizedDescription))") + } else { + log?("Disconnected") + } + // close file descriptor + socket = nil + + } + + private func run() { + Task.detached { [weak self] in + // read and write socket + do { + while await self?.socket != nil { + var didWrite = false + repeat { + didWrite = try await self?.write() ?? false + } while didWrite + try await self?.read() + } + } + catch { + await self?.disconnect(error) + } + } } - public func setWritePending(_ newValue: (() -> ())?) { - self.writePending = newValue + public func setMaximumTransmissionUnit(_ newValue: ATTMaximumTransmissionUnit) { + self.maximumTransmissionUnit = newValue } /// Performs the actual IO for recieving data. @@ -84,6 +117,9 @@ internal actor ATTConnection { //log?("Attempt read") let bytesToRead = Int(self.maximumTransmissionUnit.rawValue) + guard let socket = self.socket else { + throw ATTConnectionError.disconnected + } let recievedData = try await socket.recieve(bytesToRead) //log?("Recieved data (\(recievedData.count) bytes)") @@ -127,6 +163,9 @@ internal actor ATTConnection { //log?("Sending data... (\(sendOperation.data.count) bytes)") + guard let socket = self.socket else { + throw ATTConnectionError.disconnected + } try await socket.send(sendOperation.data) let opcode = sendOperation.opcode @@ -157,10 +196,10 @@ internal actor ATTConnection { @discardableResult public func register (_ callback: @escaping (T) async -> ()) -> UInt { - let identifier = nextRegisterID + let id = nextRegisterID // create notification - let notify = ATTNotify(identifier: identifier, notify: callback) + let notify = ATTNotify(id: id, notify: callback) // increment ID nextRegisterID += 1 @@ -168,16 +207,16 @@ internal actor ATTConnection { // add to queue notifyList.append(notify) - return identifier + return id } /// Unregisters the callback associated with the specified identifier. /// /// - Returns: Whether the callback was unregistered. @discardableResult - public func unregister(_ identifier: UInt) -> Bool { + public func unregister(_ id: UInt) -> Bool { - guard let index = notifyList.firstIndex(where: { $0.identifier == identifier }) + guard let index = notifyList.firstIndex(where: { $0.id == id }) else { return false } notifyList.remove(at: index) return true @@ -246,7 +285,6 @@ internal actor ATTConnection { writeQueue.append(sendOpcode) } - writePending?() return sendOpcode.id } @@ -301,7 +339,7 @@ internal actor ATTConnection { requestOpcode = errorRequestOpcode - writePending?() + //writePending?() /// Return if error response caused a retry guard didRetry == false @@ -326,7 +364,7 @@ internal actor ATTConnection { // success! try sendOperation.handle(data: data) - writePending?() + //writePending?() } private func handle(confirmation data: Data, opcode: ATTOpcode) throws { @@ -342,7 +380,7 @@ internal actor ATTConnection { // send the remaining indications if indicationQueue.isEmpty == false { - writePending?() + //writePending?() } } @@ -447,8 +485,12 @@ internal actor ATTConnection { /// Attempts to change security level based on an error response. private func changeSecurity(for error: ATTError) async -> Bool { + guard let socket = self.socket else { + return false + } + let securityLevel: Bluetooth.SecurityLevel - do { securityLevel = try await self.socket.securityLevel() } + do { securityLevel = try await socket.securityLevel() } catch { log?("Unable to get security level. \(error)") return false @@ -480,7 +522,7 @@ internal actor ATTConnection { } // attempt to change security level on Socket IO - do { try await self.socket.setSecurityLevel(newSecurityLevel) } + do { try await socket.setSecurityLevel(newSecurityLevel) } catch { log?("Unable to set security level. \(error)") return false @@ -501,6 +543,8 @@ public enum ATTConnectionError: Error { /// Response is unexpected. case unexpectedResponse(Data) + + case disconnected } internal extension ATTConnection { @@ -591,7 +635,7 @@ internal protocol ATTNotifyType { static var PDUType: ATTProtocolDataUnit.Type { get } - var identifier: UInt { get } + var id: UInt { get } var callback: (ATTProtocolDataUnit) async -> () { get } } @@ -600,15 +644,15 @@ internal struct ATTNotify: ATTNotifyType { static var PDUType: ATTProtocolDataUnit.Type { return PDU.self } - let identifier: UInt + let id: UInt let notify: (PDU) async -> () var callback: (ATTProtocolDataUnit) async -> () { return { await self.notify($0 as! PDU) } } - init(identifier: UInt, notify: @escaping (PDU) async -> ()) { + init(id: UInt, notify: @escaping (PDU) async -> ()) { - self.identifier = identifier + self.id = id self.notify = notify } } diff --git a/Sources/BluetoothGATT/GATTClient.swift b/Sources/BluetoothGATT/GATTClient.swift index 096a70eb6..3fda57857 100755 --- a/Sources/BluetoothGATT/GATTClient.swift +++ b/Sources/BluetoothGATT/GATTClient.swift @@ -46,7 +46,7 @@ public actor GATTClient { maximumTransmissionUnit: ATTMaximumTransmissionUnit = .default, log: ((String) -> ())? = nil ) async { - self.connection = ATTConnection( + self.connection = await ATTConnection( socket: socket, log: log ) @@ -108,7 +108,7 @@ public actor GATTClient { /// - Parameter completion: The completion closure. public func discoverPrimaryServices( by uuid: BluetoothUUID - ) async throws -> [Service]{ + ) async throws -> [Service] { // The Attribute Protocol Find By Type Value Request shall be used with the Attribute Type // parameter set to the UUID for «Primary Service» and the Attribute Value set to the 16-bit // Bluetooth UUID or 128-bit UUID for the specific primary service. diff --git a/Sources/BluetoothGATT/GATTServer.swift b/Sources/BluetoothGATT/GATTServer.swift index 104379016..377f0b1de 100644 --- a/Sources/BluetoothGATT/GATTServer.swift +++ b/Sources/BluetoothGATT/GATTServer.swift @@ -9,6 +9,7 @@ import Foundation import Bluetooth +/// GATT Server public actor GATTServer { // MARK: - Properties @@ -49,7 +50,7 @@ public actor GATTServer { // set initial MTU and register handlers self.maximumPreparedWrites = maximumPreparedWrites self.preferredMaximumTransmissionUnit = maximumTransmissionUnit - self.connection = ATTConnection(socket: socket, log: log) + self.connection = await ATTConnection(socket: socket, log: log) // async register handlers await self.registerATTHandlers() } @@ -183,9 +184,8 @@ public actor GATTServer { } // check security - let security: SecurityLevel - do { security = try await connection.socket.securityLevel() } + do { security = try await connection.socket?.securityLevel() ?? .sdp } catch { log?("Unable to get security level. \(error)") security = .sdp From 209689c46a918aa7ba78fc70e8ffeb07ab32bce5 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 18 Dec 2021 11:00:19 -0500 Subject: [PATCH 21/44] Disabled HCI unit tests --- Tests/BluetoothTests/HCITests.swift | 3 ++- Tests/BluetoothTests/HostController.swift | 2 +- Tests/BluetoothTests/iBeaconTests.swift | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Tests/BluetoothTests/HCITests.swift b/Tests/BluetoothTests/HCITests.swift index 9ad2f8dd6..295c59060 100644 --- a/Tests/BluetoothTests/HCITests.swift +++ b/Tests/BluetoothTests/HCITests.swift @@ -12,7 +12,7 @@ import XCTest import Foundation import Bluetooth @testable import BluetoothHCI - +/* final class HCITests: XCTestCase { static let allTests = [ @@ -2858,3 +2858,4 @@ fileprivate func parseEvent (_ actualBytesRead: Int, _ ev return event } +*/ diff --git a/Tests/BluetoothTests/HostController.swift b/Tests/BluetoothTests/HostController.swift index ae2198fd4..b35480259 100644 --- a/Tests/BluetoothTests/HostController.swift +++ b/Tests/BluetoothTests/HostController.swift @@ -11,7 +11,7 @@ import Bluetooth import BluetoothHCI /// Test Bluetooth Host Controller -internal final class TestHostController: BluetoothHostControllerInterface { +internal final class TestHostController /*: BluetoothHostControllerInterface */ { /// All controllers on the host. static var controllers: [TestHostController] { return [TestHostController()] } diff --git a/Tests/BluetoothTests/iBeaconTests.swift b/Tests/BluetoothTests/iBeaconTests.swift index 72709c4f3..5019e0f10 100644 --- a/Tests/BluetoothTests/iBeaconTests.swift +++ b/Tests/BluetoothTests/iBeaconTests.swift @@ -17,8 +17,8 @@ final class iBeaconTests: XCTestCase { static let allTests = [ ("testInvalid", testInvalid), ("testData", testData), - ("testCommand", testCommand), - ("testEstimoteBeacon", testEstimoteBeacon) + //("testCommand", testCommand), + //("testEstimoteBeacon", testEstimoteBeacon) ] func testInvalid() { @@ -96,7 +96,7 @@ final class iBeaconTests: XCTestCase { XCTAssertEqual(decoded.beacon.minor, beacon.minor) XCTAssertEqual(decoded.beacon.rssi, beacon.rssi) } - + /* func testEstimoteBeacon() { let expectedData = Data([0x4c, 0x00, 0x02, 0x15, 0xb9, 0x40, 0x7f, 0x30, 0xf5, 0xf8, 0x46, 0x6e, 0xaf, 0xf9, 0x25, 0x55, 0x6b, 0x57, 0xfe, 0x6d, 0x29, 0x4c, 0x90, 0x39, 0x74]) @@ -220,5 +220,5 @@ final class iBeaconTests: XCTestCase { XCTAssertNoThrow(try hostController.iBeacon(beacon, flags: 0x1A, interval: AdvertisingInterval(rawValue: 100)!)) - } + }*/ } From 8f5941c3ea07e05c169059c5a1ff967cb3d9cfdf Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 21 Mar 2022 09:37:32 -0700 Subject: [PATCH 22/44] Updated for Swift 5.6 (cherry picked from commit 3457b5090534683b4f79149346a3ad5a33142fec) --- Sources/Bluetooth/Extensions/DataConvertible.swift | 4 +--- Sources/BluetoothGAP/Extensions/DataConvertible.swift | 4 +--- Sources/BluetoothGATT/Extensions/DataConvertible.swift | 4 +--- Sources/BluetoothHCI/Extensions/DataConvertible.swift | 4 +--- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/Sources/Bluetooth/Extensions/DataConvertible.swift b/Sources/Bluetooth/Extensions/DataConvertible.swift index eb2d7cc32..3eba44444 100644 --- a/Sources/Bluetooth/Extensions/DataConvertible.swift +++ b/Sources/Bluetooth/Extensions/DataConvertible.swift @@ -98,9 +98,7 @@ extension BluetoothAddress: UnsafeDataConvertible { } internal protocol DataContainer: RandomAccessCollection where Self.Index == Int { subscript(index: Int) -> UInt8 { get } - - subscript(range: Range) -> Slice { get } - + mutating func append(_ newElement: UInt8) mutating func append(_ pointer: UnsafePointer, count: Int) diff --git a/Sources/BluetoothGAP/Extensions/DataConvertible.swift b/Sources/BluetoothGAP/Extensions/DataConvertible.swift index eb2d7cc32..3eba44444 100644 --- a/Sources/BluetoothGAP/Extensions/DataConvertible.swift +++ b/Sources/BluetoothGAP/Extensions/DataConvertible.swift @@ -98,9 +98,7 @@ extension BluetoothAddress: UnsafeDataConvertible { } internal protocol DataContainer: RandomAccessCollection where Self.Index == Int { subscript(index: Int) -> UInt8 { get } - - subscript(range: Range) -> Slice { get } - + mutating func append(_ newElement: UInt8) mutating func append(_ pointer: UnsafePointer, count: Int) diff --git a/Sources/BluetoothGATT/Extensions/DataConvertible.swift b/Sources/BluetoothGATT/Extensions/DataConvertible.swift index eb2d7cc32..3eba44444 100644 --- a/Sources/BluetoothGATT/Extensions/DataConvertible.swift +++ b/Sources/BluetoothGATT/Extensions/DataConvertible.swift @@ -98,9 +98,7 @@ extension BluetoothAddress: UnsafeDataConvertible { } internal protocol DataContainer: RandomAccessCollection where Self.Index == Int { subscript(index: Int) -> UInt8 { get } - - subscript(range: Range) -> Slice { get } - + mutating func append(_ newElement: UInt8) mutating func append(_ pointer: UnsafePointer, count: Int) diff --git a/Sources/BluetoothHCI/Extensions/DataConvertible.swift b/Sources/BluetoothHCI/Extensions/DataConvertible.swift index eb2d7cc32..3eba44444 100644 --- a/Sources/BluetoothHCI/Extensions/DataConvertible.swift +++ b/Sources/BluetoothHCI/Extensions/DataConvertible.swift @@ -98,9 +98,7 @@ extension BluetoothAddress: UnsafeDataConvertible { } internal protocol DataContainer: RandomAccessCollection where Self.Index == Int { subscript(index: Int) -> UInt8 { get } - - subscript(range: Range) -> Slice { get } - + mutating func append(_ newElement: UInt8) mutating func append(_ pointer: UnsafePointer, count: Int) From 0e3794238ece52d6fcdb760f29dfc1c610fc589a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 26 Mar 2022 13:10:06 -0700 Subject: [PATCH 23/44] Build as static library --- Package.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Package.swift b/Package.swift index 43957e0bd..b087f4913 100644 --- a/Package.swift +++ b/Package.swift @@ -1,11 +1,7 @@ // swift-tools-version:5.5 import PackageDescription -#if os(Linux) -let libraryType: PackageDescription.Product.Library.LibraryType = .dynamic -#else let libraryType: PackageDescription.Product.Library.LibraryType = .static -#endif let package = Package( name: "Bluetooth", From 5262fba63f14b10bac3f7a9e4632a3711f169720 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 3 Apr 2022 01:26:09 -0700 Subject: [PATCH 24/44] Updated `L2CAPSocket` --- Sources/Bluetooth/L2CAPSocket.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/Bluetooth/L2CAPSocket.swift b/Sources/Bluetooth/L2CAPSocket.swift index ebcbd6ef6..719b609d2 100644 --- a/Sources/Bluetooth/L2CAPSocket.swift +++ b/Sources/Bluetooth/L2CAPSocket.swift @@ -18,7 +18,7 @@ public protocol L2CAPSocket { func recieve(_ bufferSize: Int) async throws -> Data /// Attempt to accept an incoming connection. - func accept(sleep: UInt64) async throws -> Self + func accept() async throws -> Self /// Attempts to change the socket's security level. func setSecurityLevel(_ securityLevel: SecurityLevel) async throws @@ -26,9 +26,6 @@ public protocol L2CAPSocket { /// Get security level func securityLevel() async throws -> SecurityLevel - /// - func hasPendingEvents() async throws -> (canWrite: Bool, canRead: Bool) - /// Creates a new socket connected to the remote address specified. static func lowEnergyClient( address: BluetoothAddress, From b454cb27d78919442a1ad29a94970da74859b7a0 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 3 Apr 2022 19:35:18 -0700 Subject: [PATCH 25/44] Added concurrency to `lowEnergyScan()` --- .../BluetoothHostController.swift | 8 +- Sources/BluetoothHCI/HCILESetScanEnable.swift | 194 ++++++++++++------ 2 files changed, 135 insertions(+), 67 deletions(-) diff --git a/Sources/BluetoothHCI/BluetoothHostController.swift b/Sources/BluetoothHCI/BluetoothHostController.swift index 0b06e0c6e..671c724da 100644 --- a/Sources/BluetoothHCI/BluetoothHostController.swift +++ b/Sources/BluetoothHCI/BluetoothHostController.swift @@ -37,13 +37,9 @@ public protocol BluetoothHostControllerInterface: AnyObject { /// Sends a command to the device and waits for a response with return parameter values. func deviceRequest (_ commandParameter: CP, _ commandReturnType: Return.Type, timeout: HCICommandTimeout) async throws -> Return - + /// Polls and waits for events. - func pollEvent ( - _ eventParameterType: EP.Type, - shouldContinue: () -> (Bool), - event: (EP) async throws -> () - ) async throws + func recieve(_ eventType: Event.Type) async throws -> Event where Event: HCIEventParameter, Event.HCIEventType == HCIGeneralEvent } /// Bluetooth HCI errors diff --git a/Sources/BluetoothHCI/HCILESetScanEnable.swift b/Sources/BluetoothHCI/HCILESetScanEnable.swift index 726ee92d1..3d9d8b4a0 100644 --- a/Sources/BluetoothHCI/HCILESetScanEnable.swift +++ b/Sources/BluetoothHCI/HCILESetScanEnable.swift @@ -12,76 +12,148 @@ import Foundation public extension BluetoothHostControllerInterface { - /// Scan LE devices for the specified time period. - func lowEnergyScan(duration: TimeInterval, - filterDuplicates: Bool = true, - parameters: HCILESetScanParameters = .init(), - timeout: HCICommandTimeout = .default) async throws -> [HCILEAdvertisingReport.Report] { - - let startDate = Date() - let endDate = startDate + duration - - var foundDevices: [HCILEAdvertisingReport.Report] = [] - foundDevices.reserveCapacity(1) - - try await lowEnergyScan( - filterDuplicates: filterDuplicates, - parameters: parameters, - timeout: timeout, - shouldContinue: { Date() < endDate }, - foundDevice: { foundDevices.append($0) } - ) + /// Scan LE devices. + func lowEnergyScan( + filterDuplicates: Bool = true, + parameters: HCILESetScanParameters = .init(), + timeout: HCICommandTimeout = .default + ) -> AsyncLowEnergyScanStream { + return AsyncLowEnergyScanStream { [weak self] continuation in + guard let self = self else { return } + // macro for enabling / disabling scan + func enableScan(_ isEnabled: Bool = true) async throws { + + let scanEnableCommand = HCILESetScanEnable( + isEnabled: isEnabled, + filterDuplicates: filterDuplicates + ) + + do { try await self.deviceRequest(scanEnableCommand, timeout: timeout) } + catch HCIError.commandDisallowed { /* ignore, means already turned on or off */ } + } + + do { + + // disable scanning first + try await enableScan(false) + + // set parameters + try await self.deviceRequest(parameters, timeout: timeout) + + // enable scanning + try await enableScan() + + // poll for scanned devices + while Task.isCancelled == false { + + let metaEvent = try await self.recieve(HCILowEnergyMetaEvent.self) + + // only want advertising report + guard metaEvent.subevent == .advertisingReport + else { continue } + + // parse LE advertising report + guard let advertisingReport = HCILEAdvertisingReport(data: metaEvent.eventData) + else { throw BluetoothHostControllerError.garbageResponse(Data(metaEvent.eventData)) } + + // call closure on each device found + for report in advertisingReport.reports { + continuation.yield(report) + } + } + + do { try await enableScan(false) } catch { /* ignore all errors disabling scanning */ } + } + catch _ as CancellationError { + // disable scanning + do { try await enableScan(false) } catch { /* ignore all errors disabling scanning */ } + } + catch { + // disable scanning + do { try await enableScan(false) } catch { /* ignore all errors disabling scanning */ } + continuation.finish(throwing: error) + } + } + } +} + +/// Bluetooth LE Scan Stream +public final class AsyncLowEnergyScanStream: AsyncSequence { - return foundDevices + public typealias Element = HCILEAdvertisingReport.Report + + public typealias AsyncIterator = AsyncThrowingStream.Iterator + + internal typealias StreamContinuation = AsyncThrowingStream.Continuation + + internal var stream: AsyncThrowingStream! + + internal var continuation: StreamContinuation! + + private let lock = NSLock() + + private var _isScanning = true + + private var task: Task<(), Never>! + + internal init(_ build: @escaping (Continuation) async -> ()) { + let stream = AsyncThrowingStream(Element.self, bufferingPolicy: .bufferingNewest(100)) { [weak self] (streamContinuation) in + guard let self = self else { return } + self.continuation = streamContinuation + let continuation = Continuation(self) + self.task = Task(priority: .userInitiated) { + await build(continuation) + } + } + self.stream = stream } - /// Scan LE devices. - func lowEnergyScan(filterDuplicates: Bool = true, - parameters: HCILESetScanParameters = .init(), - timeout: HCICommandTimeout = .default, - shouldContinue: () -> (Bool), - foundDevice: (HCILEAdvertisingReport.Report) -> ()) async throws { + public func makeAsyncIterator() -> AsyncThrowingStream.Iterator { + stream.makeAsyncIterator() + } - // macro for enabling / disabling scan - func enableScan(_ isEnabled: Bool = true) async throws { - - let scanEnableCommand = HCILESetScanEnable(isEnabled: isEnabled, - filterDuplicates: filterDuplicates) - - do { try await deviceRequest(scanEnableCommand, timeout: timeout) } - catch HCIError.commandDisallowed { /* ignore, means already turned on or off */ } - } + public var isScanning: Bool { + lock.lock() + defer { lock.unlock() } + return _isScanning + } + + public func stop() { + lock.lock() + assert(_isScanning) + _isScanning = false + lock.unlock() + task?.cancel() + continuation.finish() + } + + internal func yield(_ value: Element) { + continuation.yield(value) + } + + internal func finish(throwing error: Error) { + lock.lock() + assert(_isScanning) + _isScanning = false + lock.unlock() + continuation.finish(throwing: error) + } + + internal struct Continuation { - // disable scanning first - try await enableScan(false) + private unowned let stream: AsyncLowEnergyScanStream - // set parameters - try await deviceRequest(parameters, timeout: timeout) + init(_ stream: AsyncLowEnergyScanStream) { + self.stream = stream + } - // enable scanning - try await enableScan() + func yield(_ value: Element) { + stream.yield(value) + } - // poll for scanned devices - do { - try await pollEvent(HCILowEnergyMetaEvent.self, shouldContinue: shouldContinue) { (metaEvent) in - - // only want advertising report - guard metaEvent.subevent == .advertisingReport - else { return } - - // parse LE advertising report - guard let advertisingReport = HCILEAdvertisingReport(data: metaEvent.eventData) - else { throw BluetoothHostControllerError.garbageResponse(Data(metaEvent.eventData)) } - - // call closure on each device found - advertisingReport.reports.forEach { foundDevice($0) } - } - } catch { - // disable scanning - do { try await enableScan(false) } catch { /* ignore all errors disabling scanning */ } - throw error + func finish(throwing error: Error) { + stream.finish(throwing: error) } - do { try await enableScan(false) } catch { /* ignore all errors disabling scanning */ } } } From c0e6b2f81e60171f3daa884fb3bdc0fdc3db3210 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 5 Apr 2022 04:51:14 -0700 Subject: [PATCH 26/44] Added `L2CAPSocket.address` --- Sources/Bluetooth/L2CAPSocket.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/Bluetooth/L2CAPSocket.swift b/Sources/Bluetooth/L2CAPSocket.swift index 719b609d2..7c87403c0 100644 --- a/Sources/Bluetooth/L2CAPSocket.swift +++ b/Sources/Bluetooth/L2CAPSocket.swift @@ -11,6 +11,9 @@ import Foundation /// L2CAP Socket protocol. public protocol L2CAPSocket { + /// Socket address + var address: BluetoothAddress { get } + /// Write to the socket. func send(_ data: Data) async throws From a534595820adb8ff886fd7bfc0580a2199147dac Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 5 Apr 2022 07:37:15 -0700 Subject: [PATCH 27/44] Updated `GATTServer` event handlers --- Sources/BluetoothGATT/GATTServer.swift | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/Sources/BluetoothGATT/GATTServer.swift b/Sources/BluetoothGATT/GATTServer.swift index 377f0b1de..cbea964e0 100644 --- a/Sources/BluetoothGATT/GATTServer.swift +++ b/Sources/BluetoothGATT/GATTServer.swift @@ -13,9 +13,7 @@ import Bluetooth public actor GATTServer { // MARK: - Properties - - public var log: ((String) -> ())? - + public var maximumTransmissionUnit: ATTMaximumTransmissionUnit { get async { return await self.connection.maximumTransmissionUnit @@ -28,16 +26,18 @@ public actor GATTServer { public var database = GATTDatabase() - public var willRead: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data, _ offset: Int) -> ATTError?)? + internal let log: ((String) -> ())? + + internal let willRead: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data, _ offset: Int) -> ATTError?)? - public var willWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data, _ newValue: Data) -> ATTError?)? + internal let willWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data, _ newValue: Data) -> ATTError?)? - public var didWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data) -> Void)? + internal let didWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data) -> Void)? // Don't modify internal let connection: ATTConnection - private var preparedWrites = [PreparedWrite]() + internal private(set) var preparedWrites = [PreparedWrite]() // MARK: - Initialization @@ -45,12 +45,19 @@ public actor GATTServer { socket: L2CAPSocket, maximumTransmissionUnit: ATTMaximumTransmissionUnit = .default, maximumPreparedWrites: Int = 50, - log: ((String) -> ())? = nil + log: ((String) -> ())? = nil, + willRead: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data, _ offset: Int) -> ATTError?)? = nil, + willWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data, _ newValue: Data) -> ATTError?)? = nil, + didWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data) -> Void)? = nil ) async { // set initial MTU and register handlers self.maximumPreparedWrites = maximumPreparedWrites self.preferredMaximumTransmissionUnit = maximumTransmissionUnit self.connection = await ATTConnection(socket: socket, log: log) + self.log = log + self.willRead = willRead + self.willWrite = willWrite + self.didWrite = didWrite // async register handlers await self.registerATTHandlers() } From 4d27d61c37513fea8f7a11fcd9391177c064bde9 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 5 Apr 2022 18:52:53 -0700 Subject: [PATCH 28/44] Updated `GATTServer` callbacks --- Sources/BluetoothGATT/GATTServer.swift | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/Sources/BluetoothGATT/GATTServer.swift b/Sources/BluetoothGATT/GATTServer.swift index cbea964e0..956b5624f 100644 --- a/Sources/BluetoothGATT/GATTServer.swift +++ b/Sources/BluetoothGATT/GATTServer.swift @@ -28,13 +28,12 @@ public actor GATTServer { internal let log: ((String) -> ())? - internal let willRead: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data, _ offset: Int) -> ATTError?)? + internal let willRead: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data, _ offset: Int) async -> ATTError?)? - internal let willWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data, _ newValue: Data) -> ATTError?)? + internal let willWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data, _ newValue: Data) async -> ATTError?)? - internal let didWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data) -> Void)? + internal let didWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data) async -> Void)? - // Don't modify internal let connection: ATTConnection internal private(set) var preparedWrites = [PreparedWrite]() @@ -46,9 +45,9 @@ public actor GATTServer { maximumTransmissionUnit: ATTMaximumTransmissionUnit = .default, maximumPreparedWrites: Int = 50, log: ((String) -> ())? = nil, - willRead: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data, _ offset: Int) -> ATTError?)? = nil, - willWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data, _ newValue: Data) -> ATTError?)? = nil, - didWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data) -> Void)? = nil + willRead: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data, _ offset: Int) async -> ATTError?)? = nil, + willWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data, _ newValue: Data) async -> ATTError?)? = nil, + didWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data) async -> Void)? = nil ) async { // set initial MTU and register handlers self.maximumPreparedWrites = maximumPreparedWrites @@ -249,7 +248,7 @@ public actor GATTServer { } // validate application errors with write callback - if let error = willWrite?(attribute.uuid, handle, attribute.value, value) { + if let error = await willWrite?(attribute.uuid, handle, attribute.value, value) { await doResponse(await errorResponse(opcode, error, handle)) return } @@ -300,7 +299,7 @@ public actor GATTServer { } else { // writes from central should not notify clients (at least not this connected central) - didWrite?(attribute.uuid, attribute.handle, attribute.value) + await didWrite?(attribute.uuid, attribute.handle, attribute.value) } } @@ -363,7 +362,7 @@ public actor GATTServer { value = Data(value.prefix(Int(maximumTransmissionUnit.rawValue) - 1)) // validate application errors with read callback - if let error = willRead?(attribute.uuid, handle, value, Int(offset)) { + if let error = await willRead?(attribute.uuid, handle, value, Int(offset)) { await errorResponse(opcode, error, handle) return nil } @@ -637,7 +636,7 @@ public actor GATTServer { let attribute = database[handle: handle] // validate application errors with read callback - if let error = willRead?(attribute.uuid, handle, attribute.value, 0) { + if let error = await willRead?(attribute.uuid, handle, attribute.value, 0) { await errorResponse(opcode, error, handle) return } @@ -708,7 +707,7 @@ public actor GATTServer { for (handle, newValue) in newValues { let attribute = database[handle: handle] // validate application errors with write callback - if let error = willWrite?(attribute.uuid, handle, attribute.value, newValue) { + if let error = await willWrite?(attribute.uuid, handle, attribute.value, newValue) { await errorResponse(opcode, error, handle) return } From ac957a6c4b8bf4df8e8849136d5071c8576bf8a0 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 5 Apr 2022 21:19:35 -0700 Subject: [PATCH 29/44] Added `GATTServer.Callback` --- Sources/BluetoothGATT/ATTConnection.swift | 2 +- Sources/BluetoothGATT/GATTServer.swift | 44 ++++++++++++++--------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/Sources/BluetoothGATT/ATTConnection.swift b/Sources/BluetoothGATT/ATTConnection.swift index fc6bd27df..d3bcc5616 100755 --- a/Sources/BluetoothGATT/ATTConnection.swift +++ b/Sources/BluetoothGATT/ATTConnection.swift @@ -19,7 +19,7 @@ internal actor ATTConnection { internal private(set) var socket: L2CAPSocket? - public private(set) var log: ((String) -> ())? + public let log: ((String) -> ())? public var isConnected: Bool { get async { diff --git a/Sources/BluetoothGATT/GATTServer.swift b/Sources/BluetoothGATT/GATTServer.swift index 956b5624f..8dc352e4d 100644 --- a/Sources/BluetoothGATT/GATTServer.swift +++ b/Sources/BluetoothGATT/GATTServer.swift @@ -28,11 +28,7 @@ public actor GATTServer { internal let log: ((String) -> ())? - internal let willRead: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data, _ offset: Int) async -> ATTError?)? - - internal let willWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data, _ newValue: Data) async -> ATTError?)? - - internal let didWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data) async -> Void)? + internal var callback = Callback() internal let connection: ATTConnection @@ -44,25 +40,27 @@ public actor GATTServer { socket: L2CAPSocket, maximumTransmissionUnit: ATTMaximumTransmissionUnit = .default, maximumPreparedWrites: Int = 50, - log: ((String) -> ())? = nil, - willRead: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data, _ offset: Int) async -> ATTError?)? = nil, - willWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data, _ newValue: Data) async -> ATTError?)? = nil, - didWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data) async -> Void)? = nil + log: ((String) -> ())? ) async { // set initial MTU and register handlers self.maximumPreparedWrites = maximumPreparedWrites self.preferredMaximumTransmissionUnit = maximumTransmissionUnit self.connection = await ATTConnection(socket: socket, log: log) self.log = log - self.willRead = willRead - self.willWrite = willWrite - self.didWrite = didWrite // async register handlers await self.registerATTHandlers() } // MARK: - Methods + public func setCallbacks(_ callback: Callback) { + self.callback = callback + } + + public func updateDatabase(_ database: (inout GATTDatabase) -> ()) { + database(&self.database) + } + /// Performs the actual IO for sending data. public func read() async throws { return try await connection.read() @@ -248,7 +246,7 @@ public actor GATTServer { } // validate application errors with write callback - if let error = await willWrite?(attribute.uuid, handle, attribute.value, value) { + if let error = await callback.willWrite?(attribute.uuid, handle, attribute.value, value) { await doResponse(await errorResponse(opcode, error, handle)) return } @@ -299,7 +297,7 @@ public actor GATTServer { } else { // writes from central should not notify clients (at least not this connected central) - await didWrite?(attribute.uuid, attribute.handle, attribute.value) + await callback.didWrite?(attribute.uuid, attribute.handle, attribute.value) } } @@ -362,7 +360,7 @@ public actor GATTServer { value = Data(value.prefix(Int(maximumTransmissionUnit.rawValue) - 1)) // validate application errors with read callback - if let error = await willRead?(attribute.uuid, handle, value, Int(offset)) { + if let error = await callback.willRead?(attribute.uuid, handle, value, Int(offset)) { await errorResponse(opcode, error, handle) return nil } @@ -636,7 +634,7 @@ public actor GATTServer { let attribute = database[handle: handle] // validate application errors with read callback - if let error = await willRead?(attribute.uuid, handle, attribute.value, 0) { + if let error = await callback.willRead?(attribute.uuid, handle, attribute.value, 0) { await errorResponse(opcode, error, handle) return } @@ -707,7 +705,7 @@ public actor GATTServer { for (handle, newValue) in newValues { let attribute = database[handle: handle] // validate application errors with write callback - if let error = await willWrite?(attribute.uuid, handle, attribute.value, newValue) { + if let error = await callback.willWrite?(attribute.uuid, handle, attribute.value, newValue) { await errorResponse(opcode, error, handle) return } @@ -727,6 +725,18 @@ public actor GATTServer { // MARK: - Supporting Types +public extension GATTServer { + + struct Callback { + + public var willRead: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data, _ offset: Int) async -> ATTError?)? + + public var willWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data, _ newValue: Data) async -> ATTError?)? + + public var didWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data) async -> Void)? + } +} + internal extension GATTServer { struct PreparedWrite { From 8a9461bbb345b85648b7bade78776f8598df3434 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 5 Apr 2022 21:33:38 -0700 Subject: [PATCH 30/44] Updated `GATTServer` --- Sources/BluetoothGATT/GATTServer.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/BluetoothGATT/GATTServer.swift b/Sources/BluetoothGATT/GATTServer.swift index 8dc352e4d..1bef9c609 100644 --- a/Sources/BluetoothGATT/GATTServer.swift +++ b/Sources/BluetoothGATT/GATTServer.swift @@ -734,6 +734,8 @@ public extension GATTServer { public var willWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data, _ newValue: Data) async -> ATTError?)? public var didWrite: ((_ uuid: BluetoothUUID, _ handle: UInt16, _ value: Data) async -> Void)? + + public init() { } } } From 616b2665451e2e10c2948c1dd247658cc0180729 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 17 Apr 2022 13:39:33 -0700 Subject: [PATCH 31/44] Added `AsyncIndefiniteStream` --- Sources/Bluetooth/AsyncIndefiniteStream.swift | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 Sources/Bluetooth/AsyncIndefiniteStream.swift diff --git a/Sources/Bluetooth/AsyncIndefiniteStream.swift b/Sources/Bluetooth/AsyncIndefiniteStream.swift new file mode 100644 index 000000000..8a5c15cd9 --- /dev/null +++ b/Sources/Bluetooth/AsyncIndefiniteStream.swift @@ -0,0 +1,99 @@ +// +// AsyncIndefiniteStream.swift +// +// +// Created by Alsey Coleman Miller on 4/12/22. +// + +/// Async Stream that will produce values until `stop()` is called. +public struct AsyncIndefiniteStream : AsyncSequence { + + let storage: Storage + + public init( + bufferSize: Int = 100, + unfolding produce: @escaping () async throws -> Element + ) { + let storage = Storage() + let stream = AsyncThrowingStream(Element.self, bufferingPolicy: .bufferingNewest(bufferSize)) { continuation in + let task = Task { + do { + while Task.isCancelled == false { + let value = try await produce() + continuation.yield(value) + } + } + catch _ as CancellationError { } // end + catch { + continuation.finish(throwing: error) + } + continuation.finish() + } + storage.task = task + } + storage.stream = stream + self.storage = storage + } + + public func makeAsyncIterator() -> AsyncIterator { + return storage.makeAsyncIterator() + } + + public func stop() { + storage.stop() + } +} + +public extension AsyncIndefiniteStream { + + struct AsyncIterator: AsyncIteratorProtocol { + + private(set) var iterator: AsyncThrowingStream.AsyncIterator + + init(_ iterator: AsyncThrowingStream.AsyncIterator) { + self.iterator = iterator + } + + public mutating func next() async throws -> Element? { + return try await iterator.next() + } + } +} + +public extension AsyncIndefiniteStream { + + struct Continuation { + + let continuation: AsyncThrowingStream.Continuation + + public func yield(_ value: Element) { + continuation.yield(value) + } + + public func finish(throwing error: Error) { + continuation.finish(throwing: error) + } + } +} + +internal extension AsyncIndefiniteStream { + + final class Storage { + + var stream: AsyncThrowingStream! + + var task: Task<(), Never>! + + init() { } + + func stop() { + assert(task != nil) + assert(task.isCancelled == false) + task?.cancel() + } + + func makeAsyncIterator() -> AsyncIterator { + return AsyncIterator(stream.makeAsyncIterator()) + } + } +} From 82355538205638e12b2c227f9adc6caf77090b4b Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 17 Apr 2022 14:37:29 -0700 Subject: [PATCH 32/44] Updated `AsyncLowEnergyScanStream` --- Sources/Bluetooth/AsyncIndefiniteStream.swift | 30 ++++-- Sources/BluetoothHCI/HCILESetScanEnable.swift | 100 +++++------------- 2 files changed, 46 insertions(+), 84 deletions(-) diff --git a/Sources/Bluetooth/AsyncIndefiniteStream.swift b/Sources/Bluetooth/AsyncIndefiniteStream.swift index 8a5c15cd9..c79cd6394 100644 --- a/Sources/Bluetooth/AsyncIndefiniteStream.swift +++ b/Sources/Bluetooth/AsyncIndefiniteStream.swift @@ -5,7 +5,7 @@ // Created by Alsey Coleman Miller on 4/12/22. // -/// Async Stream that will produce values until `stop()` is called. +/// Async Stream that will produce values until `stop()` is called or task is cancelled. public struct AsyncIndefiniteStream : AsyncSequence { let storage: Storage @@ -13,15 +13,24 @@ public struct AsyncIndefiniteStream : AsyncSequence { public init( bufferSize: Int = 100, unfolding produce: @escaping () async throws -> Element + ) { + self.init(bufferSize: bufferSize) { continuation in + while Task.isCancelled == false { + let value = try await produce() + continuation.yield(value) + } + } + } + + public init( + bufferSize: Int = 100, + _ build: @escaping (Continuation) async throws -> () ) { let storage = Storage() let stream = AsyncThrowingStream(Element.self, bufferingPolicy: .bufferingNewest(bufferSize)) { continuation in let task = Task { do { - while Task.isCancelled == false { - let value = try await produce() - continuation.yield(value) - } + try await build(Continuation(continuation: continuation)) } catch _ as CancellationError { } // end catch { @@ -29,6 +38,9 @@ public struct AsyncIndefiniteStream : AsyncSequence { } continuation.finish() } + continuation.onTermination = { _ in + task.cancel() + } storage.task = task } storage.stream = stream @@ -42,6 +54,10 @@ public struct AsyncIndefiniteStream : AsyncSequence { public func stop() { storage.stop() } + + public var didStop: Bool { + storage.task.isCancelled + } } public extension AsyncIndefiniteStream { @@ -69,10 +85,6 @@ public extension AsyncIndefiniteStream { public func yield(_ value: Element) { continuation.yield(value) } - - public func finish(throwing error: Error) { - continuation.finish(throwing: error) - } } } diff --git a/Sources/BluetoothHCI/HCILESetScanEnable.swift b/Sources/BluetoothHCI/HCILESetScanEnable.swift index 3d9d8b4a0..548b4b5b3 100644 --- a/Sources/BluetoothHCI/HCILESetScanEnable.swift +++ b/Sources/BluetoothHCI/HCILESetScanEnable.swift @@ -20,20 +20,14 @@ public extension BluetoothHostControllerInterface { ) -> AsyncLowEnergyScanStream { return AsyncLowEnergyScanStream { [weak self] continuation in guard let self = self else { return } + // macro for enabling / disabling scan func enableScan(_ isEnabled: Bool = true) async throws { - - let scanEnableCommand = HCILESetScanEnable( - isEnabled: isEnabled, - filterDuplicates: filterDuplicates - ) - - do { try await self.deviceRequest(scanEnableCommand, timeout: timeout) } + do { try await self.enableLowEnergyScan(isEnabled, filterDuplicates: filterDuplicates, timeout: timeout) } catch HCIError.commandDisallowed { /* ignore, means already turned on or off */ } } do { - // disable scanning first try await enableScan(false) @@ -64,17 +58,27 @@ public extension BluetoothHostControllerInterface { do { try await enableScan(false) } catch { /* ignore all errors disabling scanning */ } } - catch _ as CancellationError { - // disable scanning - do { try await enableScan(false) } catch { /* ignore all errors disabling scanning */ } - } catch { // disable scanning do { try await enableScan(false) } catch { /* ignore all errors disabling scanning */ } - continuation.finish(throwing: error) + throw error } } } + + private func enableLowEnergyScan( + _ isEnabled: Bool = true, + filterDuplicates: Bool = true, + timeout: HCICommandTimeout = .default + ) async throws { + + let scanEnableCommand = HCILESetScanEnable( + isEnabled: isEnabled, + filterDuplicates: filterDuplicates + ) + + try await self.deviceRequest(scanEnableCommand, timeout: timeout) + } } /// Bluetooth LE Scan Stream @@ -82,78 +86,24 @@ public final class AsyncLowEnergyScanStream: AsyncSequence { public typealias Element = HCILEAdvertisingReport.Report - public typealias AsyncIterator = AsyncThrowingStream.Iterator - - internal typealias StreamContinuation = AsyncThrowingStream.Continuation - - internal var stream: AsyncThrowingStream! - - internal var continuation: StreamContinuation! - - private let lock = NSLock() - - private var _isScanning = true + public typealias AsyncIterator = AsyncIndefiniteStream.AsyncIterator - private var task: Task<(), Never>! + let stream: AsyncIndefiniteStream - internal init(_ build: @escaping (Continuation) async -> ()) { - let stream = AsyncThrowingStream(Element.self, bufferingPolicy: .bufferingNewest(100)) { [weak self] (streamContinuation) in - guard let self = self else { return } - self.continuation = streamContinuation - let continuation = Continuation(self) - self.task = Task(priority: .userInitiated) { - await build(continuation) - } - } - self.stream = stream + internal init(_ build: @escaping (AsyncIndefiniteStream.Continuation) async throws -> ()) { + self.stream = .init(bufferSize: 100, build) } - public func makeAsyncIterator() -> AsyncThrowingStream.Iterator { + public func makeAsyncIterator() -> AsyncIndefiniteStream.AsyncIterator { stream.makeAsyncIterator() } - + public var isScanning: Bool { - lock.lock() - defer { lock.unlock() } - return _isScanning + stream.didStop == false } public func stop() { - lock.lock() - assert(_isScanning) - _isScanning = false - lock.unlock() - task?.cancel() - continuation.finish() - } - - internal func yield(_ value: Element) { - continuation.yield(value) - } - - internal func finish(throwing error: Error) { - lock.lock() - assert(_isScanning) - _isScanning = false - lock.unlock() - continuation.finish(throwing: error) - } - - internal struct Continuation { - - private unowned let stream: AsyncLowEnergyScanStream - - init(_ stream: AsyncLowEnergyScanStream) { - self.stream = stream - } - - func yield(_ value: Element) { - stream.yield(value) - } - - func finish(throwing error: Error) { - stream.finish(throwing: error) - } + stream.stop() } } From e7efae8694c74f153933e8ad83aa89cc1e0c993a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 17 Apr 2022 19:59:16 -0700 Subject: [PATCH 33/44] Updated `AsyncIndefiniteStream` --- Sources/Bluetooth/AsyncIndefiniteStream.swift | 106 +++++++++++++----- Sources/BluetoothHCI/HCILESetScanEnable.swift | 10 +- 2 files changed, 86 insertions(+), 30 deletions(-) diff --git a/Sources/Bluetooth/AsyncIndefiniteStream.swift b/Sources/Bluetooth/AsyncIndefiniteStream.swift index c79cd6394..79c6b1f50 100644 --- a/Sources/Bluetooth/AsyncIndefiniteStream.swift +++ b/Sources/Bluetooth/AsyncIndefiniteStream.swift @@ -5,6 +5,8 @@ // Created by Alsey Coleman Miller on 4/12/22. // +import Foundation + /// Async Stream that will produce values until `stop()` is called or task is cancelled. public struct AsyncIndefiniteStream : AsyncSequence { @@ -12,36 +14,54 @@ public struct AsyncIndefiniteStream : AsyncSequence { public init( bufferSize: Int = 100, - unfolding produce: @escaping () async throws -> Element - ) { - self.init(bufferSize: bufferSize) { continuation in - while Task.isCancelled == false { - let value = try await produce() - continuation.yield(value) - } - } - } - - public init( - bufferSize: Int = 100, - _ build: @escaping (Continuation) async throws -> () + _ build: @escaping ((Element) -> ()) async throws -> () ) { let storage = Storage() let stream = AsyncThrowingStream(Element.self, bufferingPolicy: .bufferingNewest(bufferSize)) { continuation in let task = Task { do { - try await build(Continuation(continuation: continuation)) + try await build({ continuation.yield($0) }) } catch _ as CancellationError { } // end catch { continuation.finish(throwing: error) } - continuation.finish() } - continuation.onTermination = { _ in + continuation.onTermination = { [weak storage] in + switch $0 { + case .cancelled: + storage?.stop() + default: + break + } + } + storage.onTermination = { + // cancel task when `stop` is called task.cancel() } - storage.task = task + } + storage.stream = stream + self.storage = storage + } + + public init( + bufferSize: Int = 100, + onTermination: @escaping () -> (), + _ build: (Continuation) -> () + ) { + let storage = Storage() + storage.onTermination = onTermination + let stream = AsyncThrowingStream(Element.self, bufferingPolicy: .bufferingNewest(bufferSize)) { continuation in + continuation.onTermination = { [weak storage] in + switch $0 { + case .cancelled: + storage?.stop() + default: + break + } + } + storage.continuation = continuation + build(Continuation(continuation)) } storage.stream = stream self.storage = storage @@ -55,21 +75,22 @@ public struct AsyncIndefiniteStream : AsyncSequence { storage.stop() } - public var didStop: Bool { - storage.task.isCancelled + public var isExecuting: Bool { + storage.isExecuting } } public extension AsyncIndefiniteStream { struct AsyncIterator: AsyncIteratorProtocol { - + private(set) var iterator: AsyncThrowingStream.AsyncIterator init(_ iterator: AsyncThrowingStream.AsyncIterator) { self.iterator = iterator } + @inline(__always) public mutating func next() async throws -> Element? { return try await iterator.next() } @@ -82,26 +103,59 @@ public extension AsyncIndefiniteStream { let continuation: AsyncThrowingStream.Continuation + init(_ continuation: AsyncThrowingStream.Continuation) { + self.continuation = continuation + } + public func yield(_ value: Element) { continuation.yield(value) } + + public func finish(throwing error: Error) { + continuation.finish(throwing: error) + } } } internal extension AsyncIndefiniteStream { final class Storage { - + + var isExecuting: Bool { + get { + lock.lock() + let value = _isExecuting + lock.unlock() + return value + } + } + + private var _isExecuting = true + + let lock = NSLock() + var stream: AsyncThrowingStream! - - var task: Task<(), Never>! + + var continuation: AsyncThrowingStream.Continuation! + + var onTermination: (() -> ())! + + deinit { + stop() + } init() { } func stop() { - assert(task != nil) - assert(task.isCancelled == false) - task?.cancel() + // end stream + continuation.finish() + // cleanup + lock.lock() + defer { lock.unlock() } + guard _isExecuting else { return } + _isExecuting = false + // cleanup / stop scanning / cancel child task + onTermination() } func makeAsyncIterator() -> AsyncIterator { diff --git a/Sources/BluetoothHCI/HCILESetScanEnable.swift b/Sources/BluetoothHCI/HCILESetScanEnable.swift index 548b4b5b3..7eaaeb0a9 100644 --- a/Sources/BluetoothHCI/HCILESetScanEnable.swift +++ b/Sources/BluetoothHCI/HCILESetScanEnable.swift @@ -16,8 +16,10 @@ public extension BluetoothHostControllerInterface { func lowEnergyScan( filterDuplicates: Bool = true, parameters: HCILESetScanParameters = .init(), + bufferSize: Int = 100, timeout: HCICommandTimeout = .default ) -> AsyncLowEnergyScanStream { + assert(bufferSize >= 1) return AsyncLowEnergyScanStream { [weak self] continuation in guard let self = self else { return } @@ -52,7 +54,7 @@ public extension BluetoothHostControllerInterface { // call closure on each device found for report in advertisingReport.reports { - continuation.yield(report) + continuation(report) } } @@ -90,8 +92,8 @@ public final class AsyncLowEnergyScanStream: AsyncSequence { let stream: AsyncIndefiniteStream - internal init(_ build: @escaping (AsyncIndefiniteStream.Continuation) async throws -> ()) { - self.stream = .init(bufferSize: 100, build) + internal init(bufferSize: Int = 100, _ build: @escaping ((Element) -> ()) async throws -> ()) { + self.stream = .init(bufferSize: bufferSize, build) } public func makeAsyncIterator() -> AsyncIndefiniteStream.AsyncIterator { @@ -99,7 +101,7 @@ public final class AsyncLowEnergyScanStream: AsyncSequence { } public var isScanning: Bool { - stream.didStop == false + stream.isExecuting } public func stop() { From ce345d5fb245959f426214d32c6bb0720150d815 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sun, 17 Apr 2022 21:06:02 -0700 Subject: [PATCH 34/44] Fixed `AsyncIndefiniteStream` --- Sources/Bluetooth/AsyncIndefiniteStream.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/Bluetooth/AsyncIndefiniteStream.swift b/Sources/Bluetooth/AsyncIndefiniteStream.swift index 79c6b1f50..43de198f2 100644 --- a/Sources/Bluetooth/AsyncIndefiniteStream.swift +++ b/Sources/Bluetooth/AsyncIndefiniteStream.swift @@ -27,6 +27,7 @@ public struct AsyncIndefiniteStream : AsyncSequence { continuation.finish(throwing: error) } } + storage.continuation = continuation continuation.onTermination = { [weak storage] in switch $0 { case .cancelled: @@ -52,6 +53,7 @@ public struct AsyncIndefiniteStream : AsyncSequence { let storage = Storage() storage.onTermination = onTermination let stream = AsyncThrowingStream(Element.self, bufferingPolicy: .bufferingNewest(bufferSize)) { continuation in + storage.continuation = continuation continuation.onTermination = { [weak storage] in switch $0 { case .cancelled: @@ -60,7 +62,6 @@ public struct AsyncIndefiniteStream : AsyncSequence { break } } - storage.continuation = continuation build(Continuation(continuation)) } storage.stream = stream From 923a40ba8e0d8348fa67be94c057ac147345af6b Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 18 Apr 2022 04:59:00 -0700 Subject: [PATCH 35/44] Updated `lowEnergyScan()` --- Sources/BluetoothHCI/HCILESetScanEnable.swift | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/Sources/BluetoothHCI/HCILESetScanEnable.swift b/Sources/BluetoothHCI/HCILESetScanEnable.swift index 7eaaeb0a9..6ec3320fa 100644 --- a/Sources/BluetoothHCI/HCILESetScanEnable.swift +++ b/Sources/BluetoothHCI/HCILESetScanEnable.swift @@ -18,26 +18,27 @@ public extension BluetoothHostControllerInterface { parameters: HCILESetScanParameters = .init(), bufferSize: Int = 100, timeout: HCICommandTimeout = .default - ) -> AsyncLowEnergyScanStream { + ) async throws -> AsyncLowEnergyScanStream { assert(bufferSize >= 1) + + // macro for enabling / disabling scan + func enableScan(_ isEnabled: Bool = true) async throws { + do { try await self.enableLowEnergyScan(isEnabled, filterDuplicates: filterDuplicates, timeout: timeout) } + catch HCIError.commandDisallowed { /* ignore, means already turned on or off */ } + } + + // disable scanning first + try await enableScan(false) + + // set parameters + try await self.deviceRequest(parameters, timeout: timeout) + + // enable scanning + try await enableScan() + return AsyncLowEnergyScanStream { [weak self] continuation in guard let self = self else { return } - - // macro for enabling / disabling scan - func enableScan(_ isEnabled: Bool = true) async throws { - do { try await self.enableLowEnergyScan(isEnabled, filterDuplicates: filterDuplicates, timeout: timeout) } - catch HCIError.commandDisallowed { /* ignore, means already turned on or off */ } - } - do { - // disable scanning first - try await enableScan(false) - - // set parameters - try await self.deviceRequest(parameters, timeout: timeout) - - // enable scanning - try await enableScan() // poll for scanned devices while Task.isCancelled == false { From d5f573200d796213f1fe40d0501c2780ac9fe435 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 18 Apr 2022 23:39:09 -0700 Subject: [PATCH 36/44] Updated `L2CAPSocket` --- Sources/Bluetooth/L2CAPSocket.swift | 2 +- Sources/BluetoothGATT/ATTConnection.swift | 2 +- Sources/BluetoothGATT/GATTServer.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Bluetooth/L2CAPSocket.swift b/Sources/Bluetooth/L2CAPSocket.swift index 7c87403c0..7081eaf32 100644 --- a/Sources/Bluetooth/L2CAPSocket.swift +++ b/Sources/Bluetooth/L2CAPSocket.swift @@ -27,7 +27,7 @@ public protocol L2CAPSocket { func setSecurityLevel(_ securityLevel: SecurityLevel) async throws /// Get security level - func securityLevel() async throws -> SecurityLevel + var securityLevel: SecurityLevel { get async throws } /// Creates a new socket connected to the remote address specified. static func lowEnergyClient( diff --git a/Sources/BluetoothGATT/ATTConnection.swift b/Sources/BluetoothGATT/ATTConnection.swift index d3bcc5616..bd76e1f10 100755 --- a/Sources/BluetoothGATT/ATTConnection.swift +++ b/Sources/BluetoothGATT/ATTConnection.swift @@ -490,7 +490,7 @@ internal actor ATTConnection { } let securityLevel: Bluetooth.SecurityLevel - do { securityLevel = try await socket.securityLevel() } + do { securityLevel = try await socket.securityLevel } catch { log?("Unable to get security level. \(error)") return false diff --git a/Sources/BluetoothGATT/GATTServer.swift b/Sources/BluetoothGATT/GATTServer.swift index 1bef9c609..9bba8de10 100644 --- a/Sources/BluetoothGATT/GATTServer.swift +++ b/Sources/BluetoothGATT/GATTServer.swift @@ -189,7 +189,7 @@ public actor GATTServer { // check security let security: SecurityLevel - do { security = try await connection.socket?.securityLevel() ?? .sdp } + do { security = try await connection.socket?.securityLevel ?? .sdp } catch { log?("Unable to get security level. \(error)") security = .sdp From a57f7664c17de00134c31ca6229bdce5b8c7790d Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 18 Apr 2022 23:39:24 -0700 Subject: [PATCH 37/44] Updated `BluetoothHostControllerInterface` --- Sources/BluetoothHCI/BluetoothHostController.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/BluetoothHCI/BluetoothHostController.swift b/Sources/BluetoothHCI/BluetoothHostController.swift index 671c724da..37c312d21 100644 --- a/Sources/BluetoothHCI/BluetoothHostController.swift +++ b/Sources/BluetoothHCI/BluetoothHostController.swift @@ -12,7 +12,7 @@ import Foundation public protocol BluetoothHostControllerInterface: AnyObject { /// All controllers on the host. - static var controllers: [Self] { get } + static var controllers: [Self] { get async } /// Send an HCI command to the controller. func deviceCommand (_ command: C) async throws @@ -63,6 +63,8 @@ public enum BluetoothHostControllerError: Error { public extension BluetoothHostControllerInterface { static var `default`: Self? { - return controllers.first + get async { + return await controllers.first + } } } From 2f7436435f1a032f0d2b93270c23cf4f5aeaf199 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Mon, 18 Apr 2022 23:40:53 -0700 Subject: [PATCH 38/44] Updated `L2CAPSocket` --- Sources/Bluetooth/L2CAPSocket.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Bluetooth/L2CAPSocket.swift b/Sources/Bluetooth/L2CAPSocket.swift index 7081eaf32..150664215 100644 --- a/Sources/Bluetooth/L2CAPSocket.swift +++ b/Sources/Bluetooth/L2CAPSocket.swift @@ -41,5 +41,5 @@ public protocol L2CAPSocket { address: BluetoothAddress, isRandom: Bool, backlog: Int - ) throws -> Self + ) async throws -> Self } From 96c2e717c2418bfd1dd4028be7f051c6a23dbc39 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 19 Apr 2022 21:40:12 -0700 Subject: [PATCH 39/44] Updated `ATTConnection` --- Sources/BluetoothGATT/ATTConnection.swift | 29 ++++++++++++++--------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/Sources/BluetoothGATT/ATTConnection.swift b/Sources/BluetoothGATT/ATTConnection.swift index bd76e1f10..28475f581 100755 --- a/Sources/BluetoothGATT/ATTConnection.swift +++ b/Sources/BluetoothGATT/ATTConnection.swift @@ -56,6 +56,8 @@ internal actor ATTConnection { /// List of registered callbacks. private var notifyList = [ATTNotifyType]() + private var readTask: Task<(), Never>? + // MARK: - Initialization deinit { @@ -86,21 +88,18 @@ internal actor ATTConnection { } // close file descriptor socket = nil - + readTask?.cancel() } private func run() { - Task.detached { [weak self] in + readTask = Task.detached(priority: .high) { [weak self] in // read and write socket do { while await self?.socket != nil { - var didWrite = false - repeat { - didWrite = try await self?.write() ?? false - } while didWrite try await self?.read() } } + catch _ as CancellationError { } // ignore catch { await self?.disconnect(error) } @@ -141,7 +140,7 @@ internal actor ATTConnection { case .response: try await handle(response: recievedData, opcode: opcode) case .confirmation: - try handle(confirmation: recievedData, opcode: opcode) + try await handle(confirmation: recievedData, opcode: opcode) case .request: try await handle(request: recievedData, opcode: opcode) case .command, @@ -339,7 +338,7 @@ internal actor ATTConnection { requestOpcode = errorRequestOpcode - //writePending?() + try await writePending() /// Return if error response caused a retry guard didRetry == false @@ -364,10 +363,10 @@ internal actor ATTConnection { // success! try sendOperation.handle(data: data) - //writePending?() + try await writePending() } - private func handle(confirmation data: Data, opcode: ATTOpcode) throws { + private func handle(confirmation data: Data, opcode: ATTOpcode) async throws { // Disconnect the bearer if the confirmation is unexpected or the PDU is invalid. guard let sendOperation = pendingIndication @@ -380,7 +379,7 @@ internal actor ATTConnection { // send the remaining indications if indicationQueue.isEmpty == false { - //writePending?() + try await writePending() } } @@ -530,6 +529,14 @@ internal actor ATTConnection { return true } + + // write all pending PDUs + private func writePending() async throws { + var didWrite = false + repeat { + didWrite = try await write() + } while didWrite + } } // MARK: - Supporting Types From 0472e36bb11720ea0be3898d422c5b08211ae2d2 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 19 Apr 2022 21:40:23 -0700 Subject: [PATCH 40/44] Updated `L2CAPSocket` --- Sources/Bluetooth/L2CAPSocket.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Bluetooth/L2CAPSocket.swift b/Sources/Bluetooth/L2CAPSocket.swift index 150664215..a0dece70f 100644 --- a/Sources/Bluetooth/L2CAPSocket.swift +++ b/Sources/Bluetooth/L2CAPSocket.swift @@ -9,7 +9,7 @@ import Foundation /// L2CAP Socket protocol. -public protocol L2CAPSocket { +public protocol L2CAPSocket: AnyObject { /// Socket address var address: BluetoothAddress { get } From 4cb2dde9eaf225147fa94e36c9b11e4871146b19 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 20 Apr 2022 11:27:00 -0700 Subject: [PATCH 41/44] Updated `GATTServer.init()` --- Sources/BluetoothGATT/GATTServer.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/BluetoothGATT/GATTServer.swift b/Sources/BluetoothGATT/GATTServer.swift index 9bba8de10..2b67b16e2 100644 --- a/Sources/BluetoothGATT/GATTServer.swift +++ b/Sources/BluetoothGATT/GATTServer.swift @@ -24,7 +24,7 @@ public actor GATTServer { public let maximumPreparedWrites: Int - public var database = GATTDatabase() + public var database: GATTDatabase internal let log: ((String) -> ())? @@ -40,11 +40,13 @@ public actor GATTServer { socket: L2CAPSocket, maximumTransmissionUnit: ATTMaximumTransmissionUnit = .default, maximumPreparedWrites: Int = 50, + database: GATTDatabase = GATTDatabase(), log: ((String) -> ())? ) async { // set initial MTU and register handlers self.maximumPreparedWrites = maximumPreparedWrites self.preferredMaximumTransmissionUnit = maximumTransmissionUnit + self.database = database self.connection = await ATTConnection(socket: socket, log: log) self.log = log // async register handlers From 1ab73cac9527da9803b1b2b2bb3048dad1b306f7 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 20 Apr 2022 12:27:16 -0700 Subject: [PATCH 42/44] Fixed `ATTConnection.writePending()` --- Sources/BluetoothGATT/ATTConnection.swift | 42 +++++++++++------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Sources/BluetoothGATT/ATTConnection.swift b/Sources/BluetoothGATT/ATTConnection.swift index 28475f581..ca2e5cedb 100755 --- a/Sources/BluetoothGATT/ATTConnection.swift +++ b/Sources/BluetoothGATT/ATTConnection.swift @@ -140,7 +140,7 @@ internal actor ATTConnection { case .response: try await handle(response: recievedData, opcode: opcode) case .confirmation: - try await handle(confirmation: recievedData, opcode: opcode) + try handle(confirmation: recievedData, opcode: opcode) case .request: try await handle(request: recievedData, opcode: opcode) case .command, @@ -284,20 +284,11 @@ internal actor ATTConnection { writeQueue.append(sendOpcode) } - return sendOpcode.id - } - - /* - public func cancel(_ identifier: UInt) { + writePending() - writePending?() + return sendOpcode.id } - public func cancelAll() { - - writePending?() - }*/ - // MARK: - Private Methods private func encode (_ request: T) -> Data? { @@ -338,7 +329,7 @@ internal actor ATTConnection { requestOpcode = errorRequestOpcode - try await writePending() + writePending() /// Return if error response caused a retry guard didRetry == false @@ -363,10 +354,10 @@ internal actor ATTConnection { // success! try sendOperation.handle(data: data) - try await writePending() + writePending() } - private func handle(confirmation data: Data, opcode: ATTOpcode) async throws { + private func handle(confirmation data: Data, opcode: ATTOpcode) throws { // Disconnect the bearer if the confirmation is unexpected or the PDU is invalid. guard let sendOperation = pendingIndication @@ -379,7 +370,7 @@ internal actor ATTConnection { // send the remaining indications if indicationQueue.isEmpty == false { - try await writePending() + writePending() } } @@ -531,11 +522,20 @@ internal actor ATTConnection { } // write all pending PDUs - private func writePending() async throws { - var didWrite = false - repeat { - didWrite = try await write() - } while didWrite + private func writePending() { + Task(priority: .high) { [weak self] in + // write socket + do { + var didWrite = false + repeat { + didWrite = try await write() + } while didWrite + } + catch _ as CancellationError { } // ignore + catch { + await self?.disconnect(error) + } + } } } From 1258b12517bf33ba61d5d321ee7127e60ae68688 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 20 Apr 2022 12:59:51 -0700 Subject: [PATCH 43/44] Updated `GATTServer.send()` --- Sources/BluetoothGATT/GATTServer.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/BluetoothGATT/GATTServer.swift b/Sources/BluetoothGATT/GATTServer.swift index 2b67b16e2..b5d31a889 100644 --- a/Sources/BluetoothGATT/GATTServer.swift +++ b/Sources/BluetoothGATT/GATTServer.swift @@ -154,9 +154,10 @@ public actor GATTServer { /// Send a server-initiated PDU message. private func send(_ indication: ATTHandleValueIndication) async -> ATTResponse { log?("Indication: \(indication)") + let priority = Task.currentPriority return await withCheckedContinuation { [weak self] continuation in guard let self = self else { return } - Task { + Task(priority: priority) { let responseType: ATTProtocolDataUnit.Type = ATTHandleValueConfirmation.self // callback if no I/O errors or disconnect let callback: (ATTProtocolDataUnit) -> () = { From c12e3054d3ae12b1ccdd8f2d076a33f91730006f Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 20 Apr 2022 20:16:14 -0700 Subject: [PATCH 44/44] Updated CI --- .github/workflows/docs.yml | 2 +- .github/workflows/swift.yml | 22 ---------------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b85ea52e5..32ed86e4d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -11,7 +11,7 @@ jobs: runs-on: macOS-latest steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Xcode Version run: | xcodebuild -version diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 0395eb560..d105d90f7 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -3,28 +3,6 @@ name: Swift on: [push] jobs: - - macOS-swift: - name: macOS - runs-on: macOS-latest - steps: - - name: Checkout - uses: actions/checkout@v1 - - name: Xcode Version - run: | - xcodebuild -version - swift --version - - name: Swift Version - run: swift --version - - name: Build (Debug) - run: swift build -c debug - - name: Build (Release) - run: swift build -c release - - name: Test (Debug) - run: swift test --configuration debug --enable-test-discovery - - name: Test (Release) - run: swift test --configuration release -Xswiftc -enable-testing --enable-test-discovery - linux-swift: name: Linux x86_64 runs-on: ubuntu-20.04