From e83fb012c1f78206862f108cef078cdcbca63cc6 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 14 Nov 2024 16:12:09 -0500 Subject: [PATCH] Add Embedded Swift support for `PeripheralManager` --- Sources/DarwinGATT/DarwinPeripheral.swift | 2 +- Sources/GATT/GATTPeripheral.swift | 329 +++++++++++----------- Sources/GATT/GATTServerConnection.swift | 34 ++- Sources/GATT/PeripheralProtocol.swift | 26 +- 4 files changed, 211 insertions(+), 180 deletions(-) diff --git a/Sources/DarwinGATT/DarwinPeripheral.swift b/Sources/DarwinGATT/DarwinPeripheral.swift index 397053e..010bdc2 100644 --- a/Sources/DarwinGATT/DarwinPeripheral.swift +++ b/Sources/DarwinGATT/DarwinPeripheral.swift @@ -15,7 +15,7 @@ import GATT @preconcurrency import CoreBluetooth import CoreLocation -public final class DarwinPeripheral: PeripheralManager, @unchecked Sendable { +public final class DarwinPeripheral: @unchecked Sendable { // MARK: - Properties diff --git a/Sources/GATT/GATTPeripheral.swift b/Sources/GATT/GATTPeripheral.swift index 5f55195..1a5b00a 100644 --- a/Sources/GATT/GATTPeripheral.swift +++ b/Sources/GATT/GATTPeripheral.swift @@ -5,14 +5,16 @@ // Created by Alsey Coleman Miller on 7/18/18. // -#if canImport(BluetoothGATT) && canImport(BluetoothHCI) +#if canImport(Foundation) import Foundation +#endif +#if canImport(BluetoothGATT) && canImport(BluetoothHCI) @_exported import Bluetooth @_exported import BluetoothGATT @_exported import BluetoothHCI /// GATT Peripheral Manager -public final class GATTPeripheral : PeripheralManager, @unchecked Sendable where Socket: Sendable, HostController: Sendable { +public final class GATTPeripheral : PeripheralManager, @unchecked Sendable where Socket.Error == Socket.Connection.Error { /// Central Peer public typealias Central = GATT.Central @@ -27,35 +29,70 @@ public final class GATTPeripheral ())? - public let hostController: HostController public let options: Options - public var willRead: ((GATTReadRequest) -> ATTError?)? + /// Logging + public var log: (@Sendable (String) -> ())? { + get { + storage.log + } + set { + storage.log = newValue + } + } - public var willWrite: ((GATTWriteRequest) -> ATTError?)? + public var willRead: ((GATTReadRequest) -> ATTError?)? { + get { + storage.willRead + } + set { + storage.willRead = newValue + } + } - public var didWrite: ((GATTWriteConfirmation) async -> ())? + public var willWrite: ((GATTWriteRequest) -> ATTError?)? { + get { + storage.willWrite + } + set { + storage.willWrite = newValue + } + } - public var connections: Set { - get async { - return await Set(storage.connections.values.lazy.map { $0.central }) + public var didWrite: ((GATTWriteConfirmation) -> ())? { + get { + storage.didWrite + } + set { + storage.didWrite = newValue } } + public var connections: Set { + Set(storage.connections.values.lazy.map { $0.central }) + } + public var isAdvertising: Bool { - get async { - return await storage.isAdvertising - } + storage.isAdvertising } - private let storage: Storage - - private var maximumUpdateValueLength = [Central: Int]() + private var _storage = Storage() + private var storage: Storage { + get { + lock.lock() + defer { lock.unlock() } + return _storage + } + set { + lock.lock() + defer { lock.unlock() } + _storage = newValue + } + } + private let lock = NSLock() // MARK: - Initialization @@ -67,29 +104,27 @@ public final class GATTPeripheral .Service) async throws -> (UInt16, [UInt16]) { - return await storage.add(service: service) + public func add(service: BluetoothGATT.GATTAttribute.Service) -> (UInt16, [UInt16]) { + return storage.add(service: service) } - public func remove(service handle: UInt16) async { - await storage.remove(service: handle) + public func remove(service handle: UInt16) { + storage.remove(service: handle) } - public func removeAllServices() async { - await storage.removeAllServices() + public func removeAllServices() { + storage.removeAllServices() } /// Modify the value of a characteristic, optionally emiting notifications if configured on active connections. - public func write(_ newValue: Data, forCharacteristic handle: UInt16) async { - await write(newValue, forCharacteristic: handle, ignore: .none) - } - - public func write(_ newValue: Data, forCharacteristic handle: UInt16, for central: Central) async throws { - guard let connection = await storage.connections[central] else { - throw CentralError.disconnected - } - await connection.write(newValue, forCharacteristic: handle) + public func write(_ newValue: Data, forCharacteristic handle: UInt16) { + write(newValue, forCharacteristic: handle, ignore: .none) } - /// Modify the value of a characteristic, optionally emiting notifications if configured on active connections. - private func write(_ newValue: Data, forCharacteristic handle: UInt16, ignore central: Central? = nil) async { - // write to master DB - await storage.write(newValue, forAttribute: handle) - // propagate changes to active connections - let connections = await storage.connections - .values - .lazy - .filter { $0.central != central } - // update the DB of each connection, and send notifications concurrently - await withTaskGroup(of: Void.self) { taskGroup in - for connection in connections { - taskGroup.addTask { - await connection.write(newValue, forCharacteristic: handle) - } - } + public func write(_ newValue: Data, forCharacteristic handle: UInt16, for central: Central) throws(Error) { + guard let connection = storage.connections[central] else { + throw .disconnected(central) } + connection.write(newValue, forCharacteristic: handle) } /// Read the value of the characteristic with specified handle. public subscript(characteristic handle: UInt16) -> Data { - get async { - return await storage.database[handle: handle].value - } + storage.database[handle: handle].value } - public subscript(characteristic handle: UInt16, central: Central) -> Data { - get async throws { - guard let connection = await storage.connections[central] else { - throw CentralError.disconnected - } - return await connection[handle] + public func value(for characteristicHandle: UInt16, central: Central) throws(Error) -> Data { + guard let connection = storage.connections[central] else { + throw .disconnected(central) } + return connection[characteristicHandle] } /// Return the handles of the characteristics matching the specified UUID. - public func characteristics(for uuid: BluetoothUUID) async -> [UInt16] { - return await storage.database + public func characteristics(for uuid: BluetoothUUID) -> [UInt16] { + return storage.database .lazy .filter { $0.uuid == uuid } .map { $0.handle } } +} + +internal extension GATTPeripheral { - private func log(_ central: Central, _ message: String) { + func log(_ central: Central, _ message: String) { log?("[\(central)]: " + message) } -} - -extension GATTPeripheral { + + /// Modify the value of a characteristic, optionally emiting notifications if configured on active connections. + func write(_ newValue: Data, forCharacteristic handle: UInt16, ignore central: Central? = nil) { + // write to master DB + storage.write(newValue, forAttribute: handle) + // propagate changes to active connections + let connections = storage.connections + .values + .lazy + .filter { $0.central != central } + // update the DB of each connection, and send notifications concurrently + for connection in connections { + connection.write(newValue, forCharacteristic: handle) + } + } func callback(for central: Central) -> GATTServer.Callback { var callback = GATTServer.Callback() @@ -227,23 +252,13 @@ extension GATTPeripheral { self?.willWrite(central: central, uuid: $0, handle: $1, value: $2, newValue: $3) } callback.didWrite = { [weak self] (uuid, handle, value) in - Task { - await self?.didWrite(central: central, uuid: uuid, handle: handle, value: value) - } + self?.didWrite(central: central, uuid: uuid, handle: handle, value: value) } return callback } - func updateMaximumUpdateValueLength(_ newValue: Int, for central: Central) { - lock.lock() - defer { lock.unlock() } - self.maximumUpdateValueLength[central] = newValue - } - func maximumUpdateValueLength(for central: Central) -> Int { - lock.lock() - defer { lock.unlock() } - guard let maximumUpdateValueLength = self.maximumUpdateValueLength[central] else { + guard let maximumUpdateValueLength = self.storage.connections[central]?.maximumUpdateValueLength else { assertionFailure() return Int(ATTMaximumTransmissionUnit.min.rawValue - 3) } @@ -276,7 +291,7 @@ extension GATTPeripheral { return willWrite?(request) } - func didWrite(central: Central, uuid: BluetoothUUID, handle: UInt16, value: Data) async { + func didWrite(central: Central, uuid: BluetoothUUID, handle: UInt16, value: Data) { let maximumUpdateValueLength = maximumUpdateValueLength(for: central) let confirmation = GATTWriteConfirmation( central: central, @@ -286,20 +301,25 @@ extension GATTPeripheral { value: value ) // update DB and inform other connections - await write(confirmation.value, forCharacteristic: confirmation.handle, ignore: confirmation.central) + write(confirmation.value, forCharacteristic: confirmation.handle, ignore: confirmation.central) // notify delegate - await didWrite?(confirmation) + didWrite?(confirmation) } func accept( _ socket: Socket - ) async { + ) { let log = self.log do { - try Task.checkCancellation() + if Thread.current.isCancelled { + return + } // wait for pending socket while socket.status.accept == false, socket.status.error == nil { - try await Task.sleep(nanoseconds: 1_000_000) + Thread.sleep(forTimeInterval: 0.1) + if Thread.current.isCancelled { + return + } } let newSocket = try socket.accept() log?("[\(newSocket.address)]: New connection") @@ -309,50 +329,46 @@ extension GATTPeripheral { socket: newSocket, maximumTransmissionUnit: options.maximumTransmissionUnit, maximumPreparedWrites: options.maximumPreparedWrites, - database: await storage.database, - delegate: callback(for: central), + database: storage.database, + callback: callback(for: central), log: { log?("[\(central)]: " + $0) } ) - await storage.newConnection(connection) - Task.detached { [weak connection, weak self] in + storage.newConnection(connection) + Thread.detachNewThread { [weak connection, weak self] in do { - while let connection, let self { - try await Task.sleep(nanoseconds: 10_000) - // update cached MTU - let maximumUpdateValueLength = await connection.maximumUpdateValueLength - self.updateMaximumUpdateValueLength(maximumUpdateValueLength, for: central) + while let connection, self != nil { + Thread.sleep(forTimeInterval: 0.01) // read and write - try await connection.run() + try connection.run() } } catch { log?("[\(central)]: " + error.localizedDescription) } - await self?.didDisconnect(central, log: log) + self?.didDisconnect(central, log: log) } } - catch _ as CancellationError { - return - } catch { log?("Error waiting for new connection: \(error)") - try? await Task.sleep(nanoseconds: 10_000_000) + Thread.sleep(forTimeInterval: 1.0) } } - private nonisolated func didDisconnect( + func didDisconnect( _ central: Central, log: (@Sendable (String) -> ())? - ) async { + ) { // try advertising again let hostController = self.hostController - do { try await hostController.enableLowEnergyAdvertising() } - catch HCIError.commandDisallowed { /* ignore */ } - catch { log?("Could not enable advertising. \(error)") } + Task { + do { try await hostController.enableLowEnergyAdvertising() } + catch HCIError.commandDisallowed { /* ignore */ } + catch { log?("Could not enable advertising. \(error)") } + } // remove connection cache - await storage.removeConnection(central) + storage.removeConnection(central) // log log?("[\(central)]: " + "Did disconnect.") } @@ -400,50 +416,49 @@ public struct GATTPeripheralAdvertisingOptions: Equatable, Hashable, Sendable { } } +public extension GATTPeripheral { + + enum Error: Swift.Error { + + case disconnected(Central) + case connection(ATTConnectionError) + } +} + internal extension GATTPeripheral { - actor Storage { + struct Storage { - let hostController: HostController + var database = GATTDatabase() - let options: Options + var willRead: ((GATTReadRequest) -> ATTError?)? - var database = GATTDatabase() + var willWrite: ((GATTWriteRequest) -> ATTError?)? + + var didWrite: ((GATTWriteConfirmation) -> ())? + + var log: (@Sendable (String) -> ())? var socket: Socket? - var task: Task<(), Never>? + var thread: Thread? var connections = [Central: GATTServerConnection](minimumCapacity: 2) - - fileprivate init( - hostController: HostController, - options: Options - ) { - self.hostController = hostController - self.options = options - } + + fileprivate init() { } var isAdvertising: Bool { socket != nil } - func stop() { + mutating func stop() { assert(socket != nil) socket = nil - task?.cancel() - task = nil - } - - func didStart( - socket: Socket, - task: Task<(), Never>? - ) { - self.socket = socket - self.task = task + thread?.cancel() + thread = nil } - func add(service: GATTAttribute.Service) -> (UInt16, [UInt16]) { + mutating func add(service: GATTAttribute.Service) -> (UInt16, [UInt16]) { var includedServicesHandles = [UInt16]() var characteristicDeclarationHandles = [UInt16]() var characteristicValueHandles = [UInt16]() @@ -458,30 +473,30 @@ internal extension GATTPeripheral { return (serviceHandle, characteristicValueHandles) } - func remove(service handle: UInt16) { + mutating func remove(service handle: UInt16) { database.remove(service: handle) } - func removeAllServices() { + mutating func removeAllServices() { database.removeAll() } - func write(_ value: Data, forAttribute handle: UInt16) { + mutating func write(_ value: Data, forAttribute handle: UInt16) { database.write(value, forAttribute: handle) } - func newConnection( + mutating func newConnection( _ connection: GATTServerConnection ) { connections[connection.central] = connection } - func removeConnection(_ central: Central) { - self.connections[central] = nil + mutating func removeConnection(_ central: Central) { + connections[central] = nil } - func maximumUpdateValueLength(for central: Central) async -> Int? { - await connections[central]?.maximumUpdateValueLength + mutating func maximumUpdateValueLength(for central: Central) -> Int? { + connections[central]?.maximumUpdateValueLength } } } diff --git a/Sources/GATT/GATTServerConnection.swift b/Sources/GATT/GATTServerConnection.swift index f1bb8e6..eed44cc 100644 --- a/Sources/GATT/GATTServerConnection.swift +++ b/Sources/GATT/GATTServerConnection.swift @@ -5,34 +5,41 @@ // Created by Alsey Coleman Miller on 7/17/18. // +#if canImport(Foundation) +import Foundation +#endif #if canImport(BluetoothGATT) import Bluetooth import BluetoothGATT -internal actor GATTServerConnection { +internal final class GATTServerConnection : @unchecked Sendable { typealias Data = Socket.Data + typealias Error = Socket.Error + // MARK: - Properties - let central: Central + public let central: Central - let server: GATTServer + private let server: GATTServer - var maximumUpdateValueLength: Int { + public var maximumUpdateValueLength: Int { // ATT_MTU-3 Int(server.maximumTransmissionUnit.rawValue) - 3 } + private let lock = NSLock() + // MARK: - Initialization - init( + internal init( central: Central, socket: Socket, maximumTransmissionUnit: ATTMaximumTransmissionUnit, maximumPreparedWrites: Int, database: GATTDatabase, - delegate: GATTServer.Callback, + callback: GATTServer.Callback, log: (@Sendable (String) -> ())? ) { self.central = central @@ -43,21 +50,28 @@ internal actor GATTServerConnection { database: database, log: log ) + self.server.callback = callback } // MARK: - Methods /// Modify the value of a characteristic, optionally emiting notifications if configured on active connections. - func write(_ value: Data, forCharacteristic handle: UInt16) { + public func write(_ value: Data, forCharacteristic handle: UInt16) { + lock.lock() + defer { lock.unlock() } server.writeValue(value, forCharacteristic: handle) } - func run() throws(ATTConnectionError) { + public func run() throws(ATTConnectionError) { + lock.lock() + defer { lock.unlock() } try self.server.run() } - subscript(handle: UInt16) -> Data { - server.database[handle: handle].value + public subscript(handle: UInt16) -> Data { + lock.lock() + defer { lock.unlock() } + return server.database[handle: handle].value } } diff --git a/Sources/GATT/PeripheralProtocol.swift b/Sources/GATT/PeripheralProtocol.swift index f455cb7..9caba7f 100644 --- a/Sources/GATT/PeripheralProtocol.swift +++ b/Sources/GATT/PeripheralProtocol.swift @@ -13,7 +13,7 @@ /// GATT Peripheral Manager /// /// Implementation varies by operating system. -public protocol PeripheralManager: AnyObject { +public protocol PeripheralManager { /// Central Peer /// @@ -22,25 +22,27 @@ public protocol PeripheralManager: AnyObject { associatedtype Data: DataContainer + associatedtype Error: Swift.Error + /// Start advertising the peripheral and listening for incoming connections. - func start() async throws + func start() throws(Error) /// Stop the peripheral. - func stop() async + func stop() /// A Boolean value that indicates whether the peripheral is advertising data. - var isAdvertising: Bool { get async } + var isAdvertising: Bool { get } /// Attempts to add the specified service to the GATT database. /// /// - Returns: Handle for service declaration and handles for characteristic value handles. - func add(service: BluetoothGATT.GATTAttribute.Service) async throws -> (UInt16, [UInt16]) + func add(service: BluetoothGATT.GATTAttribute.Service) throws(Error) -> (UInt16, [UInt16]) /// Removes the service with the specified handle. - func remove(service: UInt16) async + func remove(service: UInt16) /// Clears the local GATT database. - func removeAllServices() async + func removeAllServices() /// Callback to handle GATT read requests. var willRead: ((GATTReadRequest) -> ATTError?)? { get set } @@ -49,21 +51,21 @@ public protocol PeripheralManager: AnyObject { var willWrite: ((GATTWriteRequest) -> ATTError?)? { get set } /// Callback to handle post-write actions for GATT write requests. - var didWrite: ((GATTWriteConfirmation) async -> ())? { get set } + var didWrite: ((GATTWriteConfirmation) -> ())? { get set } /// Modify the value of a characteristic, optionally emiting notifications if configured on active connections. - func write(_ newValue: Data, forCharacteristic handle: UInt16) async + func write(_ newValue: Data, forCharacteristic handle: UInt16) /// Modify the value of a characteristic, optionally emiting notifications if configured on the specified connection. /// /// Throws error if central is unknown or disconnected. - func write(_ newValue: Data, forCharacteristic handle: UInt16, for central: Central) async throws + func write(_ newValue: Data, forCharacteristic handle: UInt16, for central: Central) throws(Error) /// Read the value of the characteristic with specified handle. - subscript(characteristic handle: UInt16) -> Data { get async } + subscript(characteristic handle: UInt16) -> Data { get } /// Read the value of the characteristic with specified handle for the specified connection. - subscript(characteristic handle: UInt16, central: Central) -> Data { get async throws } + func value(for characteristicHandle: UInt16, central: Central) throws(Error) -> Data } // MARK: - Supporting Types