Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
43 changes: 42 additions & 1 deletion Sources/DarwinGATT/DarwinPeripheral.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Central> {
Expand Down Expand Up @@ -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 {
Expand Down
23 changes: 23 additions & 0 deletions Sources/GATT/GATTPeripheral.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ public final class GATTPeripheral <HostController, Socket: L2CAPServer>: 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<Central> {
Set(storage.connections.values.lazy.map { $0.central })
}
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -579,6 +600,8 @@ internal extension GATTPeripheral {

var didDisconnect: ((Central) -> ())?

var didConfirm: ((Central, UInt16) -> ())?

var log: (@Sendable (String) -> ())?

var socket: Socket?
Expand Down
12 changes: 11 additions & 1 deletion Sources/GATT/PeripheralProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,22 @@ public protocol PeripheralManager {
/// Callback to handle post-write actions for GATT write requests.
var didWrite: ((GATTWriteConfirmation<Central, Data>) -> ())? { 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)

Expand Down
Loading