diff --git a/README.md b/README.md index 5cff176..aae2bf4 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 Reading**: RSSI value reading with timeout support - **Real-time Monitoring**: Live streams for connection states and characteristic updates ## Platform Support @@ -332,6 +333,40 @@ func monitorConnectionState(central: BluetoothCentral, peripheralID: UUID) async } ``` +### RSSI Reading + +Read signal strength (RSSI) values to check connection quality: + +```swift +@MainActor +func checkSignalStrength(peripheral: ConnectedPeripheral) async throws { + // 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: Read RSSI periodically +@MainActor +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)) + } +} +``` + ### 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..b8596e9 100644 --- a/Sources/ActorCoreBluetooth/ConnectedPeripheral.swift +++ b/Sources/ActorCoreBluetooth/ConnectedPeripheral.swift @@ -25,6 +25,7 @@ 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] = [:] @@ -479,6 +480,59 @@ public final class ConnectedPeripheral { notificationStreams.removeValue(forKey: monitorID) } + // MARK: - RSSI Operations + + /// 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 + ]) + 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, + "timeout": timeout as Any + ]) + + 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 /// Full service and characteristic discovery @@ -684,6 +738,30 @@ public final class ConnectedPeripheral { } } + // Called by delegate proxy when RSSI is read + internal func handleRSSIUpdate(rssi: NSNumber, error: Error?) { + logger?.internalDebug("Processing RSSI read response") + + if let read = rssiReadOperation { + rssiReadOperation = nil + + 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)) + } + } + } + /// Cancel all pending operations - called during disconnection internal func cancelAllPendingOperations() { logger?.peripheralInfo("Cancelling all pending operations due to disconnection", context: [ @@ -722,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 @@ -781,4 +865,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) + } }