fix: smooth scan RSSI readings
This commit is contained in:
@ -45,6 +45,7 @@ class BluetoothController {
|
|||||||
BluetoothController(this._ble);
|
BluetoothController(this._ble);
|
||||||
|
|
||||||
static const int defaultMtu = 64;
|
static const int defaultMtu = 64;
|
||||||
|
static const Duration _rssiAverageWindow = Duration(milliseconds: 500);
|
||||||
|
|
||||||
final FlutterReactiveBle _ble;
|
final FlutterReactiveBle _ble;
|
||||||
|
|
||||||
@ -52,6 +53,7 @@ class BluetoothController {
|
|||||||
StreamSubscription<DiscoveredDevice>? _scanResultsSubscription;
|
StreamSubscription<DiscoveredDevice>? _scanResultsSubscription;
|
||||||
Timer? _scanTimeout;
|
Timer? _scanTimeout;
|
||||||
final Map<String, DiscoveredDevice> _scanResultsById = {};
|
final Map<String, DiscoveredDevice> _scanResultsById = {};
|
||||||
|
final RssiAverager _rssiAverager = RssiAverager(window: _rssiAverageWindow);
|
||||||
final _scanResultsSubject =
|
final _scanResultsSubject =
|
||||||
BehaviorSubject<List<DiscoveredDevice>>.seeded(const []);
|
BehaviorSubject<List<DiscoveredDevice>>.seeded(const []);
|
||||||
final _isScanningSubject = BehaviorSubject<bool>.seeded(false);
|
final _isScanningSubject = BehaviorSubject<bool>.seeded(false);
|
||||||
@ -102,6 +104,7 @@ class BluetoothController {
|
|||||||
|
|
||||||
_scanTimeout?.cancel();
|
_scanTimeout?.cancel();
|
||||||
_scanResultsById.clear();
|
_scanResultsById.clear();
|
||||||
|
_rssiAverager.clear();
|
||||||
_scanResultsSubject.add(const []);
|
_scanResultsSubject.add(const []);
|
||||||
_isScanningSubject.add(true);
|
_isScanningSubject.add(true);
|
||||||
|
|
||||||
@ -112,7 +115,12 @@ class BluetoothController {
|
|||||||
requireLocationServicesEnabled: requireLocationServicesEnabled,
|
requireLocationServicesEnabled: requireLocationServicesEnabled,
|
||||||
)
|
)
|
||||||
.listen((device) {
|
.listen((device) {
|
||||||
_scanResultsById[device.id] = device;
|
final smoothedRssi = _rssiAverager.addSample(
|
||||||
|
device.id,
|
||||||
|
device.rssi,
|
||||||
|
DateTime.now(),
|
||||||
|
);
|
||||||
|
_scanResultsById[device.id] = device.copyWith(rssi: smoothedRssi);
|
||||||
_scanResultsSubject
|
_scanResultsSubject
|
||||||
.add(_scanResultsById.values.toList(growable: false));
|
.add(_scanResultsById.values.toList(growable: false));
|
||||||
}, onError: (Object error, StackTrace st) {
|
}, onError: (Object error, StackTrace st) {
|
||||||
@ -394,3 +402,26 @@ class BluetoothController {
|
|||||||
return Ok(null);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
70
test/controller/bluetooth_test.dart
Normal file
70
test/controller/bluetooth_test.dart
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user