Commit de34245
Changed files (3)
app
lib
bluetooth
test
bluetooth
app/lib/bluetooth/ble_read_cubit.dart
@@ -1,6 +1,5 @@
import 'dart:async';
-import 'package:blood_pressure_app/bluetooth/flutter_blue_plus_mockable.dart';
import 'package:blood_pressure_app/bluetooth/logging.dart';
import 'package:collection/collection.dart';
import 'package:flutter/cupertino.dart';
@@ -13,15 +12,28 @@ part 'ble_read_state.dart';
/// Logic for reading a characteristic from a device.
///
/// May only be used on devices that are fully connected.
+///
+/// For using this Cubit the flow is as follows:
+/// 1. Create a instance with the initial [BleReadInProgress] state
+/// 2. Wait for either a [BleReadFailure] or a [BleReadSuccess].
+///
+/// When a read failure is emitted, the only way to try again is to create a new
+/// cubit. This should be accompanied with reconnecting to the [_device].
+///
+/// Internally the class performs multiple steps to successfully read data, if
+/// one of them fails the entire cubit fails:
+/// 1. Discover services
+/// 2. If the searched service is found read characteristics
+/// 3. If the searched characteristic is found read its value
+/// 4. If binary data is read decode it to object
+/// 5. Emit decoded object
class BleReadCubit extends Cubit<BleRead> {
/// Start reading a characteristic from a device.
- BleReadCubit(this._flutterBluePlus, this._device)
+ BleReadCubit(this._device)
: super(BleReadInProgress()) {
unawaited(_startRead());
}
- final FlutterBluePlusMockable _flutterBluePlus;
-
/// Bluetooth device to connect to.
///
/// Must have an active established connection and support the measurement
@@ -35,8 +47,9 @@ class BleReadCubit extends Cubit<BleRead> {
try {
allServices = await _device.discoverServices();
} catch (e) {
- emit(BleReadFailure());
Log.err('service discovery', [_device, e]);
+ emit(BleReadFailure());
+ return;
}
@@ -52,10 +65,10 @@ class BleReadCubit extends Cubit<BleRead> {
// https://developer.nordicsemi.com/nRF51_SDK/nRF51_SDK_v4.x.x/doc/html/group___u_u_i_d___c_h_a_r_a_c_t_e_r_i_s_t_i_c_s.html#ga95fc99c7a99cf9d991c81027e4866936
final allCharacteristics = service.characteristics;
final characteristic = allCharacteristics.firstWhereOrNull(
- (c) => c.uuid.str == '2A35');
+ (c) => c.uuid.str == '2a35');
if (characteristic == null) {
- emit(BleReadFailure());
Log.err('no characteristic', [_device, allServices, allCharacteristics]);
+ emit(BleReadFailure());
return;
}
@@ -63,8 +76,9 @@ class BleReadCubit extends Cubit<BleRead> {
try {
data = await characteristic.read();
} catch (e) {
- emit(BleReadFailure());
Log.err('read error', [_device, allServices, characteristic, e]);
+ emit(BleReadFailure());
+ return;
}
// TODO: decode data before emitting success.
app/lib/bluetooth/logging.dart
@@ -2,14 +2,17 @@ import 'package:flutter/foundation.dart';
/// Simple class for manually logging in debug builds.
class Log {
+ /// Disable error logging during testing.
+ static bool testExpectError = false;
+
/// Log an error with stack trace in debug builds.
static void err(String message, [List<Object>? dumps]) {
- if (kDebugMode) {
+ if (kDebugMode && !testExpectError) {
debugPrint('-----------------------------');
- debugPrintStack();
debugPrint('ERROR $message:');
+ debugPrintStack();
for (final e in dumps ?? []) {
- debugPrint(e);
+ debugPrint(e.toString());
}
}
}
app/test/bluetooth/ble_read_cubit_test.dart
@@ -0,0 +1,94 @@
+import 'package:blood_pressure_app/bluetooth/ble_read_cubit.dart';
+import 'package:blood_pressure_app/bluetooth/logging.dart';
+import 'package:flutter_blue_plus/flutter_blue_plus.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+
+@GenerateNiceMocks([
+ MockSpec<BluetoothDevice>(),
+ MockSpec<BluetoothService>(),
+ MockSpec<BluetoothCharacteristic>()
+])
+import 'ble_read_cubit_test.mocks.dart';
+
+void main() {
+ test('success path works', () async {
+ final characteristic = MockBluetoothCharacteristic();
+ when(characteristic.uuid).thenReturn(Guid('2A35'));
+ when(characteristic.read()).thenAnswer((_) async => [1,2,3]); // TODO
+
+ final service = MockBluetoothService();
+ when(service.uuid).thenReturn(Guid('1810'));
+ when(service.characteristics).thenReturn([characteristic]);
+
+ final BluetoothDevice device = MockBluetoothDevice();
+ when(device.discoverServices()).thenAnswer((_) async => [service]);
+
+ final cubit = BleReadCubit(device);
+ expect(cubit.state, isA<BleReadInProgress>());
+ await expectLater(cubit.stream, emits(isA<BleReadSuccess>()));
+ });
+ test('should fail when device not connected', () async {
+ final BluetoothDevice device = MockBluetoothDevice();
+ when(device.discoverServices()).thenThrow(FlutterBluePlusException(
+ ErrorPlatform.fbp, 'discoverServices', FbpErrorCode.deviceIsDisconnected.index, 'device is not connected'
+ ));
+
+ Log.testExpectError = true;
+ final cubit = BleReadCubit(device);
+ expect(cubit.state, isA<BleReadFailure>(), reason: 'fails fast. Having a '
+ 'BleReadInProgress first would also be fine.');
+ Log.testExpectError = false;
+ });
+ test('should fail without matching service', () async {
+ final service = MockBluetoothService();
+ when(service.uuid).thenReturn(Guid('1811'));
+
+ final BluetoothDevice device = MockBluetoothDevice();
+ when(device.discoverServices()).thenAnswer((_) async => [service]);
+
+ Log.testExpectError = true;
+ final cubit = BleReadCubit(device);
+ expect(cubit.state, isA<BleReadInProgress>());
+ await expectLater(cubit.stream, emits(isA<BleReadFailure>()));
+ Log.testExpectError = false;
+ });
+ test('fails without matching characteristic', () async {
+ final characteristic = MockBluetoothCharacteristic();
+ when(characteristic.uuid).thenReturn(Guid('2A34'));
+
+ final service = MockBluetoothService();
+ when(service.uuid).thenReturn(Guid('1810'));
+ when(service.characteristics).thenReturn([characteristic]);
+
+ final BluetoothDevice device = MockBluetoothDevice();
+ when(device.discoverServices()).thenAnswer((_) async => [service]);
+
+ Log.testExpectError = true;
+ final cubit = BleReadCubit(device);
+ expect(cubit.state, isA<BleReadInProgress>());
+ await expectLater(cubit.stream, emits(isA<BleReadFailure>()));
+ Log.testExpectError = false;
+ });
+ test('fails when not able to read data', () async {
+ final characteristic = MockBluetoothCharacteristic();
+ when(characteristic.uuid).thenReturn(Guid('2A35'));
+ when(characteristic.read()).thenThrow(FlutterBluePlusException(
+ ErrorPlatform.fbp, 'discoverServices', FbpErrorCode.deviceIsDisconnected.index, 'device is not connected'
+ ));
+
+ final service = MockBluetoothService();
+ when(service.uuid).thenReturn(Guid('1810'));
+ when(service.characteristics).thenReturn([characteristic]);
+
+ final BluetoothDevice device = MockBluetoothDevice();
+ when(device.discoverServices()).thenAnswer((_) async => [service]);
+
+ Log.testExpectError = true;
+ final cubit = BleReadCubit(device);
+ expect(cubit.state, isA<BleReadInProgress>());
+ await expectLater(cubit.stream, emits(isA<BleReadFailure>()));
+ Log.testExpectError = false;
+ });
+}