From 76d5598e3d3085dcbf216271deedb35f2befeef4 Mon Sep 17 00:00:00 2001 From: Konstantin Polin Date: Sat, 27 Dec 2025 18:52:28 -0800 Subject: [PATCH 1/3] RSSI monitoring --- README.md | 98 +++++++++++++++++++ .../ConnectedPeripheral.swift | 70 +++++++++++++ 2 files changed, 168 insertions(+) diff --git a/README.md b/README.md index 5cff176..8d1ce73 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ A modern Swift Bluetooth library providing async/await APIs for CoreBluetooth us - **Data Reading**: Characteristic value reading with timeout support - **Data Writing**: Both response-required and fire-and-forget writing - **Notifications**: Real-time characteristic value notifications +- **RSSI Monitoring**: Real-time signal strength monitoring with streaming support - **Real-time Monitoring**: Live streams for connection states and characteristic updates ## Platform Support @@ -332,6 +333,103 @@ func monitorConnectionState(central: BluetoothCentral, peripheralID: UUID) async } ``` +### RSSI Monitoring + +Monitor signal strength (RSSI) in real-time to track connection quality: + +```swift +@MainActor +func monitorRSSI(peripheral: ConnectedPeripheral) async throws { + // Create RSSI monitoring stream + let (rssiStream, monitorID) = peripheral.createRSSIMonitor() + + // Important: Ensure proper cleanup + defer { + peripheral.stopRSSIMonitoring(monitorID) + } + + // Start monitoring in a task + Task { + for await rssi in rssiStream { + print("RSSI: \(rssi) dBm") + + // Interpret signal strength + if rssi > -50 { + print("Excellent signal") + } else if rssi > -70 { + print("Good signal") + } else if rssi > -85 { + print("Fair signal") + } else { + print("Weak signal") + } + } + } + + // Trigger RSSI reads periodically + while true { + try peripheral.readRSSI() + try await Task.sleep(for: .seconds(1)) + } +} + +// Example: One-time RSSI read +@MainActor +func checkSignalStrength(peripheral: ConnectedPeripheral) async throws { + let (rssiStream, monitorID) = peripheral.createRSSIMonitor() + + defer { + peripheral.stopRSSIMonitoring(monitorID) + } + + // Trigger a single read + try peripheral.readRSSI() + + // Wait for the result + if let rssi = await rssiStream.first(where: { _ in true }) { + print("Current signal strength: \(rssi) dBm") + } +} + +// Example: Track connection quality over time +@MainActor +func trackConnectionQuality(peripheral: ConnectedPeripheral) async throws { + let (rssiStream, monitorID) = peripheral.createRSSIMonitor() + + defer { + peripheral.stopRSSIMonitoring(monitorID) + } + + var rssiHistory: [Int] = [] + let maxHistorySize = 10 + + // Monitor RSSI updates + Task { + for await rssi in rssiStream { + rssiHistory.append(rssi) + if rssiHistory.count > maxHistorySize { + rssiHistory.removeFirst() + } + + // Calculate average signal strength + let average = rssiHistory.reduce(0, +) / rssiHistory.count + print("Current: \(rssi) dBm, Average: \(average) dBm") + + // Warn if signal is deteriorating + if average < -85 { + print("⚠️ Warning: Connection quality is poor") + } + } + } + + // Read RSSI every 2 seconds + while true { + try peripheral.readRSSI() + try await Task.sleep(for: .seconds(2)) + } +} +``` + ### Comprehensive Logging Built-in logging system with multiple categories and levels: diff --git a/Sources/ActorCoreBluetooth/ConnectedPeripheral.swift b/Sources/ActorCoreBluetooth/ConnectedPeripheral.swift index bc91f79..b963e75 100644 --- a/Sources/ActorCoreBluetooth/ConnectedPeripheral.swift +++ b/Sources/ActorCoreBluetooth/ConnectedPeripheral.swift @@ -30,6 +30,7 @@ public final class ConnectedPeripheral { private var serviceDiscoveryStreams: [UUID: AsyncStream<[BluetoothService]>.Continuation] = [:] private var characteristicValueStreams: [UUID: AsyncStream<(BluetoothCharacteristic, Data?)>.Continuation] = [:] private var notificationStreams: [UUID: AsyncStream<(BluetoothCharacteristic, Data?)>.Continuation] = [:] + private var rssiStreams: [UUID: AsyncStream.Continuation] = [:] internal init(cbPeripheral: CBPeripheral, logger: BluetoothLogger?) { self.identifier = cbPeripheral.identifier @@ -479,6 +480,47 @@ public final class ConnectedPeripheral { notificationStreams.removeValue(forKey: monitorID) } + /// Create monitor for RSSI updates + public func createRSSIMonitor() -> (stream: AsyncStream, monitorID: UUID) { + let monitorID = UUID() + + logger?.streamInfo("Creating RSSI monitor", context: [ + "peripheralID": identifier.uuidString, + "monitorID": monitorID.uuidString + ]) + + let stream = AsyncStream { continuation in + rssiStreams[monitorID] = continuation + } + + return (stream, monitorID) + } + + /// Stop monitoring RSSI updates + public func stopRSSIMonitoring(_ monitorID: UUID) { + logger?.streamInfo("Stopping RSSI monitor", context: [ + "monitorID": monitorID.uuidString + ]) + rssiStreams[monitorID]?.finish() + rssiStreams.removeValue(forKey: monitorID) + } + + /// Read RSSI value once + public func readRSSI() throws { + guard cbPeripheral.state == .connected else { + logger?.errorError("Cannot read RSSI: peripheral not connected", context: [ + "peripheralID": identifier.uuidString + ]) + throw BluetoothError.peripheralNotConnected + } + + logger?.peripheralInfo("Reading RSSI", context: [ + "peripheralID": identifier.uuidString + ]) + + cbPeripheral.readRSSI() + } + // MARK: - Convenience Methods /// Full service and characteristic discovery @@ -684,6 +726,27 @@ public final class ConnectedPeripheral { } } + // Called by delegate proxy when RSSI is read + internal func handleRSSIUpdate(rssi: NSNumber, error: Error?) { + if let error = error { + logger?.errorError("RSSI read failed", context: [ + "peripheralID": identifier.uuidString, + "error": error.localizedDescription + ]) + } else { + let rssiValue = rssi.intValue + logger?.peripheralInfo("RSSI updated", context: [ + "peripheralID": identifier.uuidString, + "rssi": rssiValue + ]) + + // Notify all RSSI streams + for continuation in rssiStreams.values { + continuation.yield(rssiValue) + } + } + } + /// Cancel all pending operations - called during disconnection internal func cancelAllPendingOperations() { logger?.peripheralInfo("Cancelling all pending operations due to disconnection", context: [ @@ -781,4 +844,11 @@ private final class ConnectedPeripheralDelegateProxy: NSObject, @preconcurrency ]) self.peripheral?.handleNotificationStateUpdate(for: characteristic, error: error) } + + func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) { + logger?.internalDebug("CBPeripheralDelegate.didReadRSSI called", context: [ + "rssi": RSSI.intValue + ]) + self.peripheral?.handleRSSIUpdate(rssi: RSSI, error: error) + } } From 196fd01713c169c155f47a7d4387cf856c3197f4 Mon Sep 17 00:00:00 2001 From: Konstantin Polin Date: Sat, 27 Dec 2025 20:03:49 -0800 Subject: [PATCH 2/3] Remove monitors and use simple async function --- README.md | 101 ++++------------- .../ConnectedPeripheral.swift | 107 +++++++++++------- 2 files changed, 83 insertions(+), 125 deletions(-) diff --git a/README.md b/README.md index 8d1ce73..6a7b881 100644 --- a/README.md +++ b/README.md @@ -335,96 +335,33 @@ func monitorConnectionState(central: BluetoothCentral, peripheralID: UUID) async ### RSSI Monitoring -Monitor signal strength (RSSI) in real-time to track connection quality: +Read signal strength (RSSI) values to check connection quality: ```swift @MainActor -func monitorRSSI(peripheral: ConnectedPeripheral) async throws { - // Create RSSI monitoring stream - let (rssiStream, monitorID) = peripheral.createRSSIMonitor() - - // Important: Ensure proper cleanup - defer { - peripheral.stopRSSIMonitoring(monitorID) - } - - // Start monitoring in a task - Task { - for await rssi in rssiStream { - print("RSSI: \(rssi) dBm") - - // Interpret signal strength - if rssi > -50 { - print("Excellent signal") - } else if rssi > -70 { - print("Good signal") - } else if rssi > -85 { - print("Fair signal") - } else { - print("Weak signal") - } - } - } - - // Trigger RSSI reads periodically - while true { - try peripheral.readRSSI() - try await Task.sleep(for: .seconds(1)) - } -} - -// Example: One-time RSSI read -@MainActor func checkSignalStrength(peripheral: ConnectedPeripheral) async throws { - let (rssiStream, monitorID) = peripheral.createRSSIMonitor() - - defer { - peripheral.stopRSSIMonitoring(monitorID) - } - - // Trigger a single read - try peripheral.readRSSI() - - // Wait for the result - if let rssi = await rssiStream.first(where: { _ in true }) { - print("Current signal strength: \(rssi) dBm") + // Read RSSI value with timeout + let rssi = try await peripheral.readRSSI(timeout: 3.0) + print("Current signal strength: \(rssi) dBm") + + // Interpret signal strength + if rssi > -50 { + print("Excellent signal") + } else if rssi > -70 { + print("Good signal") + } else if rssi > -85 { + print("Fair signal") + } else { + print("Weak signal") } } -// Example: Track connection quality over time +// Example: Read RSSI periodically @MainActor -func trackConnectionQuality(peripheral: ConnectedPeripheral) async throws { - let (rssiStream, monitorID) = peripheral.createRSSIMonitor() - - defer { - peripheral.stopRSSIMonitoring(monitorID) - } - - var rssiHistory: [Int] = [] - let maxHistorySize = 10 - - // Monitor RSSI updates - Task { - for await rssi in rssiStream { - rssiHistory.append(rssi) - if rssiHistory.count > maxHistorySize { - rssiHistory.removeFirst() - } - - // Calculate average signal strength - let average = rssiHistory.reduce(0, +) / rssiHistory.count - print("Current: \(rssi) dBm, Average: \(average) dBm") - - // Warn if signal is deteriorating - if average < -85 { - print("⚠️ Warning: Connection quality is poor") - } - } - } - - // Read RSSI every 2 seconds - while true { - try peripheral.readRSSI() +func monitorRSSI(peripheral: ConnectedPeripheral) async throws { + for _ in 0..<10 { + let rssi = try await peripheral.readRSSI(timeout: 3.0) + print("RSSI: \(rssi) dBm") try await Task.sleep(for: .seconds(2)) } } diff --git a/Sources/ActorCoreBluetooth/ConnectedPeripheral.swift b/Sources/ActorCoreBluetooth/ConnectedPeripheral.swift index b963e75..b8596e9 100644 --- a/Sources/ActorCoreBluetooth/ConnectedPeripheral.swift +++ b/Sources/ActorCoreBluetooth/ConnectedPeripheral.swift @@ -25,12 +25,12 @@ public final class ConnectedPeripheral { private var characteristicReadOperations: [String: TimedOperation] = [:] private var characteristicWriteOperations: [String: TimedOperation] = [:] private var notificationStateOperations: [String: TimedOperation] = [:] + private var rssiReadOperation: TimedOperation? // Stream management for peripheral-level events private var serviceDiscoveryStreams: [UUID: AsyncStream<[BluetoothService]>.Continuation] = [:] private var characteristicValueStreams: [UUID: AsyncStream<(BluetoothCharacteristic, Data?)>.Continuation] = [:] private var notificationStreams: [UUID: AsyncStream<(BluetoothCharacteristic, Data?)>.Continuation] = [:] - private var rssiStreams: [UUID: AsyncStream.Continuation] = [:] internal init(cbPeripheral: CBPeripheral, logger: BluetoothLogger?) { self.identifier = cbPeripheral.identifier @@ -480,33 +480,10 @@ public final class ConnectedPeripheral { notificationStreams.removeValue(forKey: monitorID) } - /// Create monitor for RSSI updates - public func createRSSIMonitor() -> (stream: AsyncStream, monitorID: UUID) { - let monitorID = UUID() - - logger?.streamInfo("Creating RSSI monitor", context: [ - "peripheralID": identifier.uuidString, - "monitorID": monitorID.uuidString - ]) - - let stream = AsyncStream { continuation in - rssiStreams[monitorID] = continuation - } - - return (stream, monitorID) - } - - /// Stop monitoring RSSI updates - public func stopRSSIMonitoring(_ monitorID: UUID) { - logger?.streamInfo("Stopping RSSI monitor", context: [ - "monitorID": monitorID.uuidString - ]) - rssiStreams[monitorID]?.finish() - rssiStreams.removeValue(forKey: monitorID) - } + // MARK: - RSSI Operations - /// Read RSSI value once - public func readRSSI() throws { + /// Read RSSI (signal strength) value from the peripheral + public func readRSSI(timeout: TimeInterval? = nil) async throws -> Int { guard cbPeripheral.state == .connected else { logger?.errorError("Cannot read RSSI: peripheral not connected", context: [ "peripheralID": identifier.uuidString @@ -514,11 +491,46 @@ public final class ConnectedPeripheral { throw BluetoothError.peripheralNotConnected } + // Cancel any existing RSSI read operation + if let existingRead = rssiReadOperation { + logger?.peripheralInfo("Canceling previous RSSI read to start new one", context: [ + "peripheralID": identifier.uuidString + ]) + existingRead.resumeOnce(with: .failure(BluetoothError.operationCancelled)) + rssiReadOperation = nil + } + logger?.peripheralInfo("Reading RSSI", context: [ - "peripheralID": identifier.uuidString + "peripheralID": identifier.uuidString, + "timeout": timeout as Any ]) - cbPeripheral.readRSSI() + return try await withCheckedThrowingContinuation { continuation in + let read = TimedOperation( + operationName: "RSSI read", + logger: logger + ) + read.setup(continuation) + rssiReadOperation = read + + if let timeout { + logger?.internalDebug("Setting RSSI read timeout", context: [ + "timeout": timeout + ]) + read.setTimeoutTask(timeout: timeout, onTimeout: { [weak self] in + guard let self else { return } + self.logger?.logTimeout( + operation: "RSSI read", + timeout: timeout, + context: ["peripheralID": self.identifier.uuidString] + ) + self.rssiReadOperation = nil + }) + } + + logger?.internalDebug("Calling CBPeripheral.readRSSI") + cbPeripheral.readRSSI() + } } // MARK: - Convenience Methods @@ -728,21 +740,24 @@ public final class ConnectedPeripheral { // Called by delegate proxy when RSSI is read internal func handleRSSIUpdate(rssi: NSNumber, error: Error?) { - if let error = error { - logger?.errorError("RSSI read failed", context: [ - "peripheralID": identifier.uuidString, - "error": error.localizedDescription - ]) - } else { - let rssiValue = rssi.intValue - logger?.peripheralInfo("RSSI updated", context: [ - "peripheralID": identifier.uuidString, - "rssi": rssiValue - ]) + logger?.internalDebug("Processing RSSI read response") + + if let read = rssiReadOperation { + rssiReadOperation = nil - // Notify all RSSI streams - for continuation in rssiStreams.values { - continuation.yield(rssiValue) + if let error = error { + logger?.errorError("RSSI read failed", context: [ + "peripheralID": identifier.uuidString, + "error": error.localizedDescription + ]) + read.resumeOnce(with: .failure(error)) + } else { + let rssiValue = rssi.intValue + logger?.peripheralInfo("RSSI read completed", context: [ + "peripheralID": identifier.uuidString, + "rssi": rssiValue + ]) + read.resumeOnce(with: .success(rssiValue)) } } } @@ -785,6 +800,12 @@ public final class ConnectedPeripheral { } notificationStateOperations.removeAll() + if rssiReadOperation != nil { + rssiReadOperation?.cancel() + rssiReadOperation = nil + cancelledCount += 1 + } + logger?.internalInfo("All pending operations cancelled", context: [ "peripheralID": identifier.uuidString, "cancelledOperations": cancelledCount From 6bfbadb3f34da15c260122b1f9ef81e67e867de7 Mon Sep 17 00:00:00 2001 From: Konstantin Polin Date: Sat, 27 Dec 2025 20:25:22 -0800 Subject: [PATCH 3/3] Fixed readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6a7b881..aae2bf4 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ A modern Swift Bluetooth library providing async/await APIs for CoreBluetooth us - **Data Reading**: Characteristic value reading with timeout support - **Data Writing**: Both response-required and fire-and-forget writing - **Notifications**: Real-time characteristic value notifications -- **RSSI Monitoring**: Real-time signal strength monitoring with streaming support +- **RSSI Reading**: RSSI value reading with timeout support - **Real-time Monitoring**: Live streams for connection states and characteristic updates ## Platform Support @@ -333,7 +333,7 @@ func monitorConnectionState(central: BluetoothCentral, peripheralID: UUID) async } ``` -### RSSI Monitoring +### RSSI Reading Read signal strength (RSSI) values to check connection quality: