diff --git a/lib/controller/bluetooth.dart b/lib/controller/bluetooth.dart index c54844b..65c4306 100644 --- a/lib/controller/bluetooth.dart +++ b/lib/controller/bluetooth.dart @@ -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? _scanResultsSubscription; Timer? _scanTimeout; final Map _scanResultsById = {}; + final RssiAverager _rssiAverager = RssiAverager(window: _rssiAverageWindow); final _scanResultsSubject = BehaviorSubject>.seeded(const []); final _isScanningSubject = BehaviorSubject.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> _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(0, (sum, sample) => sum + sample.$2); + return (total / samples.length).round(); + } + + void clear() { + _samplesByDeviceId.clear(); + } +} diff --git a/test/controller/bluetooth_test.dart b/test/controller/bluetooth_test.dart new file mode 100644 index 0000000..0c78614 --- /dev/null +++ b/test/controller/bluetooth_test.dart @@ -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); + }); + }); +}