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
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
91 changes: 91 additions & 0 deletions Sources/ActorCoreBluetooth/ConnectedPeripheral.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public final class ConnectedPeripheral {
private var characteristicReadOperations: [String: TimedOperation<Data?>] = [:]
private var characteristicWriteOperations: [String: TimedOperation<Void>] = [:]
private var notificationStateOperations: [String: TimedOperation<Void>] = [:]
private var rssiReadOperation: TimedOperation<Int>?

// Stream management for peripheral-level events
private var serviceDiscoveryStreams: [UUID: AsyncStream<[BluetoothService]>.Continuation] = [:]
Expand Down Expand Up @@ -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<Int>(
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
Expand Down Expand Up @@ -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: [
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}