main
1import 'dart:async';
2
3import 'package:blood_pressure_app/features/bluetooth/backend/bluetooth_backend.dart';
4import 'package:blood_pressure_app/features/bluetooth/logic/characteristics/ble_measurement_data.dart';
5import 'package:blood_pressure_app/logging.dart';
6import 'package:flutter/foundation.dart';
7import 'package:flutter_bloc/flutter_bloc.dart';
8
9part 'ble_read_state.dart';
10
11/// Logic for reading a characteristic from a device through a "indication".
12///
13/// For using this Cubit the flow is as follows:
14/// 1. Create a instance with the initial [BleReadInProgress] state
15/// 2. Wait for either a [BleReadFailure] or a [BleReadSuccess].
16///
17/// When a read failure is emitted, the only way to try again is to create a new
18/// cubit. This should be accompanied with reconnecting to the [_device].
19///
20/// Internally the class performs multiple steps to successfully read data, if
21/// one of them fails the entire cubit fails:
22/// 1. Discover services
23/// 2. If the searched service is found read characteristics
24/// 3. If the searched characteristic is found read its value
25/// 4. If binary data is read decode it to object
26/// 5. Emit decoded object
27class BleReadCubit extends Cubit<BleReadState> with TypeLogger {
28 /// Start reading a characteristic from a device.
29 BleReadCubit(this._device, {
30 required this.serviceUUID,
31 required this.characteristicUUID,
32 }) : super(BleReadInProgress())
33 {
34 takeMeasurement();
35
36 // start read timeout
37 _timeoutTimer = Timer(const Duration(minutes: 2), () {
38 if (state is BleReadInProgress) {
39 logger.finer('BleReadCubit timeout reached and still running');
40 emit(BleReadFailure('Timed out after 2 minutes'));
41 } else {
42 logger.finer('BleReadCubit timeout reached with state: $state, ${state is BleReadInProgress}');
43 }
44 });
45 }
46
47 /// Bluetooth device to connect to.
48 ///
49 /// Must have an active established connection and support the measurement characteristic.
50 final BluetoothDevice _device;
51
52 /// UUID of the service to read.
53 final String serviceUUID;
54
55 /// UUID of the characteristic to read.
56 final String characteristicUUID;
57
58 late final Timer _timeoutTimer;
59
60 int _retryCount = 0;
61 final int _maxRetries = 3;
62
63 /// Take a 'measurement', i.e. read the blood pressure values from the given characteristicUUID
64 /// TODO: make this generic by accepting a data decoder argument?
65 Future<void> takeMeasurement() async {
66 final success = await _device.connect(
67 onDisconnect: () {
68 if (_retryCount < _maxRetries) {
69 _retryCount++;
70 takeMeasurement();
71
72 logger.finer('BleReadCubit: retrying after device.onDisconnect called');
73 return true;
74 }
75
76 logger.finer('BleReadCubit: device.onDisconnect called');
77 emit(BleReadFailure('Device unexpectedly disconnected'));
78 return true;
79 },
80 onError: (Object err) => emit(BleReadFailure(err.toString()))
81 );
82 if (success) {
83 final uuidService = _device.manager.createUuidFromString(serviceUUID);
84 final service = await _device.getServiceByUuid(uuidService);
85 logger.finer('BleReadCubit: Found service: $service');
86 if (service == null) {
87 // TODO: add a BleReadUnsupported state
88 emit(BleReadFailure('Device does not provide the expected service with uuid $serviceUUID'));
89 return;
90 }
91
92 final uuidCharacteristic = _device.manager.createUuidFromString(characteristicUUID);
93 final characteristic = await service.getCharacteristicByUuid(uuidCharacteristic);
94 logger.finer('BleReadCubit: Found characteristic: $characteristic');
95 if (characteristic == null) {
96 emit(BleReadFailure('Device does not provide the expected characteristic with uuid $characteristicUUID'));
97 return;
98 }
99
100 final List<Uint8List> data = [];
101 final success = await _device.getCharacteristicValue(characteristic, (Uint8List value, [_]) => data.add(value));
102
103 logger.finer('BleReadCubit(success: $success): Got data: $data');
104 if (!success) {
105 emit(BleReadFailure('Could not retrieve characteristic value'));
106 return;
107 }
108
109 final List<BleMeasurementData> measurements = [];
110
111 for (final item in data) {
112 final decodedData = BleMeasurementData.decode(item, 0);
113 if (decodedData == null) {
114 logger.severe('BleReadCubit decoding failed', item);
115 emit(BleReadFailure('Could not decode data'));
116 return;
117 }
118
119 measurements.add(decodedData);
120 }
121
122 if (measurements.length > 1) {
123 logger.finer('BleReadMultiple decoded ${measurements.length} measurements');
124 emit(BleReadMultiple(measurements));
125 } else {
126 logger.finer('BleReadCubit decoded: ${measurements.first}');
127 emit(BleReadSuccess(measurements.first));
128 }
129 }
130 }
131
132 @override
133 Future<void> close() async {
134 logger.finer('BleReadCubit close');
135 _timeoutTimer.cancel();
136
137 if (_device.isConnected) {
138 await _device.disconnect();
139 }
140
141 await super.close();
142 }
143
144 /// Called after reading from a device returned multiple measurements and the
145 /// user chose which measurement they wanted to add.
146 Future<void> useMeasurement(BleMeasurementData data) async {
147 assert(state is! BleReadSuccess);
148 emit(BleReadSuccess(data));
149 }
150}