diff --git a/Package.swift b/Package.swift index db6ccd8..f2929ce 100644 --- a/Package.swift +++ b/Package.swift @@ -35,7 +35,7 @@ var package = Package( dependencies: [ .package( url: "https://github.com/PureSwift/Bluetooth.git", - from: "7.5.0" + from: "8.0.0" ) ], targets: [ diff --git a/Sources/DarwinGATT/DarwinPeripheral.swift b/Sources/DarwinGATT/DarwinPeripheral.swift index 931843a..3e76f24 100644 --- a/Sources/DarwinGATT/DarwinPeripheral.swift +++ b/Sources/DarwinGATT/DarwinPeripheral.swift @@ -44,6 +44,27 @@ public final class DarwinPeripheral: PeripheralManager, @unchecked Sendable { public var didDisconnect: ((Central) -> ())? + /// Callback invoked when a central acknowledges (confirms) an indication for the + /// specified characteristic handle. + /// + /// - Note: CoreBluetooth does not report ATT-level indication confirmations + /// directly; `CBPeripheralManagerDelegate` has no `didConfirm`-style method. + /// The only related signal is `peripheralManagerIsReady(toUpdateSubscribers:)`, + /// which fires when the shared transmit queue (used for both notifications and + /// indications, across all subscribed centrals) has space again after a prior + /// `updateValue(_:for:onSubscribedCentrals:)` call returned `false`. Because + /// CoreBluetooth will not accept another update for a central while an + /// indication to that central is unconfirmed, this callback is invoked as a + /// best-effort proxy: only for calls to `write(_:forCharacteristic:for:)` (a + /// single, explicit central) that initially failed to enqueue and only + /// succeeded after waiting for `peripheralManagerIsReady`. This is a heuristic, + /// not a verified per-characteristic confirmation — the queue can also free up + /// for reasons unrelated to that specific central or characteristic (e.g. a + /// plain notification draining, or another central's indication being + /// confirmed), and it is never invoked for the broadcast `write(_:forCharacteristic:)` + /// since no single central can be attributed there. + public var didConfirm: ((Central, UInt16) -> ())? + public var stateChanged: ((DarwinBluetoothState) -> ())? public var connections: Set { @@ -173,7 +194,27 @@ public final class DarwinPeripheral: PeripheralManager, @unchecked Sendable { } public func write(_ newValue: Data, forCharacteristic handle: UInt16, for central: Central) { - write(newValue, forCharacteristic: handle) // per-connection database not supported on Darwin + // update GATT DB (shared; per-connection database not supported on Darwin) + database[characteristic: handle] = newValue + // send notification/indication to only the specified central + if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { + Task { + var didNotify = updateValue(newValue, forCharacteristic: handle, centrals: [central]) + var didWaitForTransmitQueue = false + while didNotify == false { + didWaitForTransmitQueue = true + await waitPeripheralReadyUpdateSubcribers() + didNotify = updateValue(newValue, forCharacteristic: handle, centrals: [central]) + } + // Best-effort indication confirmation heuristic. + // See the `didConfirm` documentation for its limitations. + if didWaitForTransmitQueue { + didConfirm?(central, handle) + } + } + } else { + updateValue(newValue, forCharacteristic: handle, centrals: [central]) + } } public func value(for characteristicHandle: UInt16, central: Central) -> Data { diff --git a/Sources/GATT/GATTPeripheral.swift b/Sources/GATT/GATTPeripheral.swift index dcd24f5..c6e24dc 100644 --- a/Sources/GATT/GATTPeripheral.swift +++ b/Sources/GATT/GATTPeripheral.swift @@ -94,6 +94,17 @@ public final class GATTPeripheral : Periphe } } + /// Callback invoked when a central acknowledges (confirms) an indication for the + /// specified characteristic handle. + public var didConfirm: ((Central, UInt16) -> ())? { + get { + storage.didConfirm + } + set { + storage.didConfirm = newValue + } + } + public var connections: Set { Set(storage.connections.values.lazy.map { $0.central }) } @@ -358,6 +369,9 @@ internal extension GATTPeripheral { callback.didWrite = { (uuid, handle, value) in self.didWrite(central: central, uuid: uuid, handle: handle, value: value) } + callback.didConfirm = { (uuid, handle) in + self.didConfirm(central: central, uuid: uuid, handle: handle) + } #else callback.willRead = { [weak self] in self?.willRead(central: central, uuid: $0, handle: $1, value: $2, offset: $3) @@ -368,6 +382,9 @@ internal extension GATTPeripheral { callback.didWrite = { [weak self] (uuid, handle, value) in self?.didWrite(central: central, uuid: uuid, handle: handle, value: value) } + callback.didConfirm = { [weak self] (uuid, handle) in + self?.didConfirm(central: central, uuid: uuid, handle: handle) + } #endif return callback } @@ -430,6 +447,10 @@ internal extension GATTPeripheral { // notify delegate didWrite?(confirmation) } + + func didConfirm(central: Central, uuid: BluetoothUUID, handle: UInt16) { + didConfirm?(central, handle) + } /// Accept a pending connection, without blocking. @discardableResult @@ -579,6 +600,8 @@ internal extension GATTPeripheral { var didDisconnect: ((Central) -> ())? + var didConfirm: ((Central, UInt16) -> ())? + var log: (@Sendable (String) -> ())? var socket: Socket? diff --git a/Sources/GATT/PeripheralProtocol.swift b/Sources/GATT/PeripheralProtocol.swift index ad5919b..ebd9f3e 100644 --- a/Sources/GATT/PeripheralProtocol.swift +++ b/Sources/GATT/PeripheralProtocol.swift @@ -59,12 +59,22 @@ public protocol PeripheralManager { /// Callback to handle post-write actions for GATT write requests. var didWrite: ((GATTWriteConfirmation) -> ())? { get set } - /// Callback to handle when a central connects. +/// Callback to handle when a central connects. var didConnect: ((Central) -> ())? { get set } /// Callback to handle when a central disconnects. var didDisconnect: ((Central) -> ())? { get set } + /// Callback invoked when a central acknowledges (confirms) an indication for the + /// specified characteristic handle. + /// + /// Indications differ from notifications in that the central sends an ATT + /// confirmation once it receives the value, letting the peripheral know delivery + /// succeeded. Not every backend is able to observe that confirmation with + /// central / characteristic granularity; see the documentation on each + /// conforming type for what, if anything, triggers this callback. + var didConfirm: ((Central, UInt16) -> ())? { get set } + /// Modify the value of a characteristic, optionally emiting notifications if configured on active connections. func write(_ newValue: Data, forCharacteristic handle: UInt16)