Skip to content

Commit 928f47c

Browse files
committed
add readrssi
You can read the Received Signal Strength Indicator (RSSI) of a peripheral to determine its signal strength: let rssiValue = try await peripheral.readRSSI()
1 parent c76f35f commit 928f47c

6 files changed

Lines changed: 311 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.3.2] - 2025-04-25
9+
10+
### Added
11+
- Add RSSI value to Peripheral `readRSSI()`
12+
813
## [0.3.1] - 2025-03-10
914

1015
### Fixed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,29 @@ for await value in characteristic.value.stream { // or .current or .observable
353353
try await peripheral.writeValueWithResponse(data, for: characteristic)
354354
```
355355

356+
## RSSI
357+
358+
You can read the Received Signal Strength Indicator (RSSI) of a peripheral to determine its signal strength:
359+
360+
```swift
361+
// Read RSSI once
362+
let rssiValue = try await peripheral.readRSSI()
363+
print("Signal strength: \(rssiValue) dBm") // Values typically range from -40 (near) to -100 dBm (far)
364+
365+
// Or observe RSSI changes over time
366+
for await rssi in peripheral.rssi.stream {
367+
print("RSSI updated: \(rssi) dBm")
368+
if rssi > -50 {
369+
print("Device is very close")
370+
} else if rssi > -70 {
371+
print("Device is at medium range")
372+
} else {
373+
print("Device is far away")
374+
}
375+
}
376+
377+
// note that the rssi is only updated when you call readRSSI, so you would need to call this for the loop to itterate
378+
```
356379

357380
## Running Tests
358381

Sources/AsyncCoreBluetooth/Peripheral.swift

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ public actor Peripheral {
138138
/// that is only available in the underlying `CBMPeripheral`.
139139
public private(set) var cbPeripheral: CBMPeripheral
140140

141+
141142
/// The unique identifier associated with the peripheral.
142143
///
143144
/// This UUID uniquely identifies the peripheral and can be used to retrieve the peripheral later
@@ -153,12 +154,44 @@ public actor Peripheral {
153154
/// This will not affect the async streams.
154155
public var delegate: CBMPeripheralDelegate?
155156

157+
/// The received signal strength indicator (RSSI) of the peripheral.
158+
///
159+
/// This property is an AsyncObservableUnwrapped that provides the signal strength
160+
/// of the connection with the peripheral, measured in decibels (dBm).
161+
/// The closer to 0, the stronger the signal (for example, -30 is a strong signal,
162+
/// while -90 is a weak signal).
163+
///
164+
/// Example Usage:
165+
/// ```swift
166+
/// Task {
167+
/// for await value in peripheral.rssi {
168+
/// print("RSSI updated: \(value) dBm")
169+
/// // You could use this to estimate proximity to the device
170+
/// if value > -50 {
171+
/// print("Very close proximity")
172+
/// } else if value > -70 {
173+
/// print("Medium proximity")
174+
/// } else {
175+
/// print("Far proximity")
176+
/// }
177+
/// }
178+
/// }
179+
/// ```
180+
///
181+
/// Note: The RSSI value updates when you call `readRSSI()` or when the system
182+
/// periodically updates the value for connected peripherals.
183+
@MainActor
184+
let _rssi: AsyncObservableUnwrapped<Int> = .init(nil)
185+
@MainActor
186+
public var rssi: some AsyncObservableUnwrappedStreamReadOnly<Int> { _rssi }
187+
156188
private var peripheralDelegate: PeripheralDelegate?
157189

158190
var discoverServicesContinuations = Deque<CheckedContinuation<[CBUUID /* service uuid */: Service], Error>>()
159191
var discoverCharacteristicsContinuations: [CBUUID /* service uuid */: Deque<CheckedContinuation<[CBUUID /* characteristic uuid */: Characteristic], Error>>] = [:]
160192

161193
var readCharacteristicValueContinuations: [CBUUID: Deque<CheckedContinuation<Data, Error>>] = [:]
194+
var readRSSIContinuations: Deque<CheckedContinuation<Int, Error>> = []
162195
var writeCharacteristicWithResponseContinuations: Deque<CheckedContinuation<Void, any Error>> = Deque<CheckedContinuation<Void, Error>>()
163196
var notifyCharacteristicValueContinuations: [CBUUID: Deque<CheckedContinuation<Bool, Error>>] = [:]
164197

@@ -427,6 +460,41 @@ extension Peripheral {
427460
}
428461
}
429462

463+
/// Reads the current RSSI value for the peripheral.
464+
///
465+
/// This method requests the current received signal strength indicator (RSSI) from the peripheral.
466+
/// RSSI is measured in decibels (dBm) and provides an indication of connection signal strength.
467+
/// The value is typically negative, with values closer to 0 indicating stronger signals.
468+
///
469+
/// Example Usage:
470+
/// ```swift
471+
/// do {
472+
/// let rssiValue = try await peripheral.readRSSI()
473+
/// print("Current RSSI: \(rssiValue) dBm")
474+
///
475+
/// // Use RSSI for approximate distance estimation
476+
/// if rssiValue > -50 {
477+
/// print("Device is nearby")
478+
/// } else if rssiValue > -70 {
479+
/// print("Device is at medium range")
480+
/// } else {
481+
/// print("Device is far away")
482+
/// }
483+
/// } catch {
484+
/// print("Failed to read RSSI: \(error)")
485+
/// }
486+
/// ```
487+
///
488+
/// - Returns: The current RSSI value in dBm (decibels relative to 1 milliwatt).
489+
/// - Throws: An error if the RSSI read operation fails or if the peripheral disconnects during the read.
490+
@discardableResult
491+
public func readRSSI() async throws -> Int {
492+
return try await withCheckedThrowingContinuation { continuation in
493+
readRSSIContinuations.append(continuation)
494+
cbPeripheral.readRSSI()
495+
}
496+
}
497+
430498
/// Writes a value to a characteristic and waits for a response.
431499
///
432500
/// This method writes data to the specified characteristic and waits for a confirmation

Sources/AsyncCoreBluetooth/PeripheralDelegate.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ extension Peripheral {
2525
func peripheral(_ cbPeripheral: CBMPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
2626
print("peripheral \(cbPeripheral) didReadRSSI \(RSSI) error \(String(describing: error))")
2727
delegate?.peripheral(cbPeripheral, didReadRSSI: RSSI, error: error)
28+
self._rssi.update(RSSI.intValue)
29+
guard let continuation = readRSSIContinuations.popFirst() else {
30+
return
31+
}
32+
continuation.resume(with: Result.success(RSSI.intValue))
2833
}
2934

3035
func peripheral(

Tests/AsyncCoreBluetoothTests/MockPeripheral.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ class MockPeripheral: @unchecked Sendable {
3333
return peripheralDidReceiveConnectionRequestResult
3434
}
3535

36+
37+
3638
// Read requests
3739
func peripheral(
3840
_ peripheral: CBMPeripheralSpec,
@@ -93,12 +95,12 @@ class MockPeripheral: @unchecked Sendable {
9395
}
9496
}
9597

96-
static func makeDevice(identifier: UUID = UUID(), delegate: CBMPeripheralSpecDelegate, isKnown: Bool = false)
98+
static func makeDevice(identifier: UUID = UUID(), delegate: CBMPeripheralSpecDelegate, isKnown: Bool = false, proximity: CBMProximity = .near)
9799
-> CBMPeripheralSpec
98100
{
99101
var spec =
100102
CBMPeripheralSpec
101-
.simulatePeripheral(identifier: identifier, proximity: .near)
103+
.simulatePeripheral(identifier: identifier, proximity: proximity)
102104
.advertising(
103105
advertisementData: [
104106
CBMAdvertisementDataLocalNameKey: "my device",
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
@preconcurrency import CoreBluetoothMock
2+
import Foundation
3+
import Testing
4+
5+
@testable import AsyncCoreBluetooth
6+
7+
@Suite(.serialized) struct ReadRSSITests {
8+
let mockPeripheralDelegate = MockPeripheral.Delegate()
9+
let rssiDeviation = 15
10+
11+
func setup(proximity: CBMProximity, connect: Bool = true) async throws -> (Peripheral, CBMPeripheralSpec, CentralManager) {
12+
CBMCentralManagerMock.simulateInitialState(.poweredOff)
13+
14+
// Create a mutable mockPeripheral
15+
let mockPeripheral = MockPeripheral.makeDevice(
16+
delegate: mockPeripheralDelegate,
17+
isKnown: true,
18+
proximity: proximity
19+
)
20+
21+
CBMCentralManagerMock.simulatePeripherals([mockPeripheral])
22+
CBMCentralManagerMock.simulateInitialState(.poweredOn)
23+
24+
let centralManager = CentralManager(forceMock: true)
25+
_ = await centralManager.start().first(where: { $0 == .poweredOn })
26+
27+
let peripheral = await centralManager.retrievePeripherals(withIdentifiers: [
28+
mockPeripheral.identifier
29+
])[0]
30+
if connect {
31+
_ = await centralManager.connect(peripheral).first(where: { $0 == .connected })
32+
}
33+
34+
return (peripheral, mockPeripheral, centralManager)
35+
}
36+
37+
@Test("Read RSSI returns near proximity value")
38+
func test_readRSSI_near() async throws {
39+
let (peripheral, _, _) = try await setup(proximity: .near)
40+
41+
try await peripheral.readRSSI()
42+
guard let rssiValue = await peripheral.rssi.stream.first(where: { _ in true }) else {
43+
Issue.record("No RSSI value received")
44+
return
45+
}
46+
// Near proximity base RSSI is -40, but can vary by ±15 (rssiDeviation)
47+
#expect(rssiValue >= -40 - rssiDeviation && rssiValue <= -40 + rssiDeviation)
48+
}
49+
50+
@Test("Read RSSI returns immediate proximity value")
51+
func test_readRSSI_immediate() async throws {
52+
let (peripheral, _, _) = try await setup(proximity: .immediate)
53+
54+
try await peripheral.readRSSI()
55+
guard let rssiValue = await peripheral.rssi.stream.first(where: { _ in true }) else {
56+
Issue.record("No RSSI value received")
57+
return
58+
}
59+
// Immediate proximity base RSSI is -70, but can vary by ±15 (rssiDeviation)
60+
#expect(rssiValue >= -70 - rssiDeviation && rssiValue <= -70 + rssiDeviation)
61+
}
62+
63+
@Test("Read RSSI returns far proximity value")
64+
func test_readRSSI_far() async throws {
65+
let (peripheral, _, _) = try await setup(proximity: .far)
66+
67+
try await peripheral.readRSSI()
68+
guard let rssiValue = await peripheral.rssi.stream.first(where: { _ in true }) else {
69+
Issue.record("No RSSI value received")
70+
return
71+
}
72+
// Far proximity base RSSI is -100, but can vary by ±15 (rssiDeviation)
73+
#expect(rssiValue >= -100 - rssiDeviation && rssiValue <= -100 + rssiDeviation)
74+
}
75+
76+
@Test("RSSI Observable is initially nil")
77+
func test_rssi_initially_nil() async throws {
78+
let (peripheral, _, _) = try await setup(proximity: .near)
79+
80+
// Before reading RSSI, the observable should be nil
81+
let initialValue = await peripheral._rssi.current
82+
#expect(initialValue == nil)
83+
}
84+
85+
@Test("Multiple sequential RSSI reads return consistent values for the same proximity")
86+
func test_sequential_rssi_reads_consistent() async throws {
87+
let (peripheral, _, _) = try await setup(proximity: .near)
88+
89+
// Read RSSI multiple times with the same proximity
90+
let rssi1 = try await peripheral.readRSSI()
91+
let rssi2 = try await peripheral.readRSSI()
92+
let rssi3 = try await peripheral.readRSSI()
93+
94+
// All values should be near proximity (-40 ± deviation)
95+
#expect(rssi1 >= -40 - rssiDeviation && rssi1 <= -40 + rssiDeviation)
96+
#expect(rssi2 >= -40 - rssiDeviation && rssi2 <= -40 + rssiDeviation)
97+
#expect(rssi3 >= -40 - rssiDeviation && rssi3 <= -40 + rssiDeviation)
98+
99+
// The observable should have the latest value
100+
let currentValue = await peripheral._rssi.current
101+
#expect(currentValue == rssi3)
102+
}
103+
104+
@Test("RSSI observable updates when proximity changes")
105+
func test_rssi_updates_with_proximity_changes() async throws {
106+
// Start with near proximity
107+
let (peripheral, mockPeripheral, _) = try await setup(proximity: .near)
108+
109+
// Read initial RSSI
110+
_ = try await peripheral.readRSSI()
111+
let initialRssi = await peripheral._rssi.current
112+
#expect(initialRssi != nil)
113+
#expect(initialRssi! >= -40 - rssiDeviation && initialRssi! <= -40 + rssiDeviation)
114+
115+
// Change proximity to far and read again
116+
mockPeripheral.simulateProximityChange(.far)
117+
_ = try await peripheral.readRSSI()
118+
119+
// RSSI should update to far proximity value
120+
let farRssi = await peripheral._rssi.current
121+
#expect(farRssi != nil)
122+
#expect(farRssi! >= -100 - rssiDeviation && farRssi! <= -100 + rssiDeviation)
123+
}
124+
125+
@Test("RSSI stream receives updates")
126+
func test_rssi_stream_receives_updates() async throws {
127+
let (peripheral, mockPeripheral, _) = try await setup(proximity: .near)
128+
129+
// Get initial RSSI value
130+
_ = try await peripheral.readRSSI()
131+
let firstValue = await peripheral._rssi.current
132+
#expect(firstValue != nil)
133+
#expect(firstValue! >= -40 - rssiDeviation && firstValue! <= -40 + rssiDeviation)
134+
135+
// Change to immediate proximity and read
136+
mockPeripheral.simulateProximityChange(.immediate)
137+
_ = try await peripheral.readRSSI()
138+
let secondValue = await peripheral._rssi.current
139+
#expect(secondValue != nil)
140+
#expect(secondValue! >= -70 - rssiDeviation && secondValue! <= -70 + rssiDeviation)
141+
142+
// Change to far proximity and read
143+
mockPeripheral.simulateProximityChange(.far)
144+
_ = try await peripheral.readRSSI()
145+
let thirdValue = await peripheral._rssi.current
146+
#expect(thirdValue != nil)
147+
#expect(thirdValue! >= -100 - rssiDeviation && thirdValue! <= -100 + rssiDeviation)
148+
}
149+
150+
@Test("RSSI values can be collected with AsyncStream")
151+
func test_rssi_values_collected_with_stream() async throws {
152+
let (peripheral, mockPeripheral, _) = try await setup(proximity: .near)
153+
154+
// Create a task to collect RSSI values from the stream
155+
let streamTask = Task {
156+
var values = [Int]()
157+
var count = 0
158+
for await value in await peripheral.rssi.stream {
159+
values.append(value)
160+
count += 1
161+
if count >= 3 {
162+
// We'll collect 3 values
163+
break
164+
}
165+
}
166+
return values
167+
}
168+
169+
// Now perform real RSSI reads with different proximities to generate values
170+
_ = try await peripheral.readRSSI() // Read with near proximity
171+
172+
// Change to immediate proximity and read again
173+
mockPeripheral.simulateProximityChange(.immediate)
174+
_ = try await peripheral.readRSSI()
175+
176+
// Change to far proximity and read again
177+
mockPeripheral.simulateProximityChange(.far)
178+
_ = try await peripheral.readRSSI()
179+
180+
// Get the collected values from our stream task
181+
let collectedValues = await streamTask.value
182+
183+
// Verify the stream collected all three values
184+
#expect(collectedValues.count == 3)
185+
186+
// Verify values match the expected proximity ranges
187+
#expect(collectedValues[0] >= -40 - rssiDeviation && collectedValues[0] <= -40 + rssiDeviation) // Near
188+
#expect(collectedValues[1] >= -70 - rssiDeviation && collectedValues[1] <= -70 + rssiDeviation) // Immediate
189+
#expect(collectedValues[2] >= -100 - rssiDeviation && collectedValues[2] <= -100 + rssiDeviation) // Far
190+
}
191+
192+
// @Test("Disconnected peripheral throws error on RSSI read")
193+
// func test_disconnected_peripheral_throws_error() async throws {
194+
// let (peripheral, _, centralManager) = try await setup(proximity: .near)
195+
// await centralManager.cancelPeripheralConnection(peripheral)
196+
197+
// // Attempt to read RSSI, should throw an error
198+
// do {
199+
// _ = try await peripheral.readRSSI()
200+
// Issue.record("Expected an error when reading RSSI from disconnected peripheral")
201+
// } catch {
202+
// // Expected to throw an error
203+
// #expect(error is PeripheralConnectionError)
204+
// }
205+
// }
206+
}

0 commit comments

Comments
 (0)