fix: smooth scan RSSI readings

This commit is contained in:
2026-04-28 20:38:33 +02:00
parent 96416a2f73
commit 76b7195e5e
2 changed files with 102 additions and 1 deletions

View File

@ -45,6 +45,7 @@ class BluetoothController {
BluetoothController(this._ble);
static const int defaultMtu = 64;
static const Duration _rssiAverageWindow = Duration(milliseconds: 500);
final FlutterReactiveBle _ble;
@ -52,6 +53,7 @@ class BluetoothController {
StreamSubscription<DiscoveredDevice>? _scanResultsSubscription;
Timer? _scanTimeout;
final Map<String, DiscoveredDevice> _scanResultsById = {};
final RssiAverager _rssiAverager = RssiAverager(window: _rssiAverageWindow);
final _scanResultsSubject =
BehaviorSubject<List<DiscoveredDevice>>.seeded(const []);
final _isScanningSubject = BehaviorSubject<bool>.seeded(false);
@ -102,6 +104,7 @@ class BluetoothController {
_scanTimeout?.cancel();
_scanResultsById.clear();
_rssiAverager.clear();
_scanResultsSubject.add(const []);
_isScanningSubject.add(true);
@ -112,7 +115,12 @@ class BluetoothController {
requireLocationServicesEnabled: requireLocationServicesEnabled,
)
.listen((device) {
_scanResultsById[device.id] = device;
final smoothedRssi = _rssiAverager.addSample(
device.id,
device.rssi,
DateTime.now(),
);
_scanResultsById[device.id] = device.copyWith(rssi: smoothedRssi);
_scanResultsSubject
.add(_scanResultsById.values.toList(growable: false));
}, onError: (Object error, StackTrace st) {
@ -394,3 +402,26 @@ class BluetoothController {
return Ok(null);
}
}
class RssiAverager {
RssiAverager({required this.window});
final Duration window;
final Map<String, List<(DateTime, int)>> _samplesByDeviceId = {};
int addSample(String deviceId, int rssi, DateTime timestamp) {
final cutoff = timestamp.subtract(window);
final samples = _samplesByDeviceId.putIfAbsent(deviceId, () => []);
samples
..removeWhere((sample) => sample.$1.isBefore(cutoff))
..add((timestamp, rssi));
final total = samples.fold<int>(0, (sum, sample) => sum + sample.$2);
return (total / samples.length).round();
}
void clear() {
_samplesByDeviceId.clear();
}
}

View File

@ -0,0 +1,70 @@
import 'package:abawo_bt_app/controller/bluetooth.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('RssiAverager', () {
test('averages samples within the configured window', () {
final averager = RssiAverager(window: const Duration(milliseconds: 500));
final startedAt = DateTime(2026);
expect(averager.addSample('trainer', -80, startedAt), -80);
expect(
averager.addSample(
'trainer',
-70,
startedAt.add(const Duration(milliseconds: 100)),
),
-75,
);
expect(
averager.addSample(
'trainer',
-60,
startedAt.add(const Duration(milliseconds: 400)),
),
-70,
);
});
test('drops samples older than the configured window', () {
final averager = RssiAverager(window: const Duration(milliseconds: 500));
final startedAt = DateTime(2026);
averager.addSample('trainer', -80, startedAt);
averager.addSample(
'trainer',
-60,
startedAt.add(const Duration(milliseconds: 250)),
);
expect(
averager.addSample(
'trainer',
-40,
startedAt.add(const Duration(milliseconds: 501)),
),
-50,
);
});
test('tracks devices independently', () {
final averager = RssiAverager(window: const Duration(milliseconds: 500));
final startedAt = DateTime(2026);
averager.addSample('trainer-a', -80, startedAt);
averager.addSample('trainer-a', -60, startedAt);
expect(averager.addSample('trainer-b', -40, startedAt), -40);
});
test('clear removes previous samples', () {
final averager = RssiAverager(window: const Duration(milliseconds: 500));
final startedAt = DateTime(2026);
averager.addSample('trainer', -80, startedAt);
averager.clear();
expect(averager.addSample('trainer', -40, startedAt), -40);
});
});
}