main
1import 'dart:async';
2
3import 'package:blood_pressure_app/features/old_bluetooth/logic/characteristics/ble_measurement_data.dart';
4import 'package:blood_pressure_app/logging.dart';
5import 'package:collection/collection.dart';
6import 'package:flutter/foundation.dart';
7import 'package:flutter_bloc/flutter_bloc.dart';
8import 'package:flutter_blue_plus/flutter_blue_plus.dart';
9
10part 'ble_read_state.dart';
11
12/// Logic for reading a characteristic from a device through a "indication".
13///
14/// For using this Cubit the flow is as follows:
15/// 1. Create a instance with the initial [BleReadInProgress] state
16/// 2. Wait for either a [BleReadFailure] or a [BleReadSuccess].
17///
18/// When a read failure is emitted, the only way to try again is to create a new
19/// cubit. This should be accompanied with reconnecting to the [_device].
20///
21/// Internally the class performs multiple steps to successfully read data, if
22/// one of them fails the entire cubit fails:
23/// 1. Discover services
24/// 2. If the searched service is found read characteristics
25/// 3. If the searched characteristic is found read its value
26/// 4. If binary data is read decode it to object
27/// 5. Emit decoded object
28class BleReadCubit extends Cubit<BleReadState> with TypeLogger {
29 /// Start reading a characteristic from a device.
30 BleReadCubit(this._device, {
31 required this.serviceUUID,
32 required this.characteristicUUID,
33 }) : super(BleReadInProgress())
34 {
35 _subscription = _device.connectionState
36 .listen(_onConnectionStateChanged);
37 // timeout
38 _timeoutTimer = Timer(const Duration(minutes: 2), () {
39 if (state is BleReadInProgress) {
40 logger.finest('BleReadCubit timeout reached and still running');
41 emit(BleReadFailure());
42 } else {
43 logger.finest('BleReadCubit timeout reached with state: $state, ${state is BleReadInProgress}');
44 }
45 });
46 }
47
48 /// UUID of the service to read.
49 final Guid serviceUUID;
50
51 /// UUID of the characteristic to read.
52 final Guid characteristicUUID;
53
54 /// Bluetooth device to connect to.
55 ///
56 /// Must have an active established connection and support the measurement
57 /// characteristic.
58 final BluetoothDevice _device;
59
60 late final StreamSubscription<BluetoothConnectionState> _subscription;
61 late final Timer _timeoutTimer;
62 StreamSubscription<List<int>>? _indicationListener;
63
64 @override
65 Future<void> close() async {
66 logger.finest('BleReadCubit close');
67 await _subscription.cancel();
68 _timeoutTimer.cancel();
69
70 if (_device.isConnected) {
71 try {
72 logger.finest('BleReadCubit close: Attempting disconnect from ${_device.advName}');
73 await _device.disconnect();
74 assert(_device.isDisconnected);
75 } catch (e) {
76 logger.severe('unable to disconnect', [e, _device]);
77 }
78 }
79
80 await super.close();
81 }
82
83 bool _ensureConnectionInProgress = false;
84 Future<void> _ensureConnection([int attemptCount = 0]) async {
85 logger.finest('BleReadCubit _ensureConnection');
86 if (_ensureConnectionInProgress) return;
87 _ensureConnectionInProgress = true;
88
89 if (_device.isAutoConnectEnabled) {
90 logger.finest('BleReadCubit Waiting for auto connect...');
91 _ensureConnectionInProgress = false;
92 return;
93 }
94
95 if (_device.isDisconnected) {
96 logger.finest('BleReadCubit _ensureConnection: Attempting to connect with ${_device.advName}');
97 try {
98 await _device.connect();
99 } on FlutterBluePlusException catch (e) {
100 logger.severe('BleReadCubit _device.connect failed:', [_device, e]);
101 }
102
103 if (_device.isDisconnected) {
104 logger.finest('BleReadCubit _ensureConnection: Device not connected');
105 _ensureConnectionInProgress = false;
106 if (attemptCount >= 5) {
107 emit(BleReadFailure());
108 return;
109 } else {
110 return _ensureConnection(attemptCount + 1);
111 }
112 } else {
113 logger.finest('BleReadCubit Connection successful');
114 }
115 }
116 assert(_device.isConnected);
117 _ensureConnectionInProgress = false;
118 }
119
120 Future<void> _onConnectionStateChanged(BluetoothConnectionState state) async {
121 logger.finest('BleReadCubit _onConnectionStateChanged: $state');
122 if (super.state is BleReadSuccess) return;
123 if (state == BluetoothConnectionState.disconnected) {
124 logger.finest('BleReadCubit _onConnectionStateChanged disconnected: '
125 '${_device.disconnectReason} Attempting reconnect');
126 await _ensureConnection();
127 return;
128 }
129 assert(state == BluetoothConnectionState.connected, 'state should be '
130 'connected as connecting and disconnecting are not streamed by android');
131 assert(_device.isConnected);
132
133 // Query actual services supported by the device. While they must be
134 // rediscovered when a disconnect happens, this object is also recreated.
135 late final List<BluetoothService> allServices;
136 try {
137 allServices = await _device.discoverServices();
138 logger.finest('BleReadCubit allServices: $allServices');
139 } catch (e) {
140 logger.severe('service discovery', [_device, e]);
141 emit(BleReadFailure());
142 return;
143 }
144
145 // [Guid.str] trims standard parts from the uuid. 0x1810 is the blood
146 // pressure uuid. https://developer.nordicsemi.com/nRF51_SDK/nRF51_SDK_v4.x.x/doc/html/group___u_u_i_d___s_e_r_v_i_c_e_s.html
147 final BluetoothService? service = allServices
148 .firstWhereOrNull((BluetoothService s) => s.uuid == serviceUUID);
149 if (service == null) {
150 logger.severe('unsupported service', [_device, allServices]);
151 emit(BleReadFailure());
152 return;
153 }
154
155 // 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
156 final List<BluetoothCharacteristic> allCharacteristics = service.characteristics;
157 logger.finest('BleReadCubit allCharacteristics: $allCharacteristics');
158 final BluetoothCharacteristic? characteristic = allCharacteristics
159 .firstWhereOrNull((c) => c.uuid == characteristicUUID,);
160 if (characteristic == null) {
161 logger.severe('no characteristic', [_device, allServices, allCharacteristics]);
162 emit(BleReadFailure());
163 return;
164 }
165
166 // This characteristic only supports indication so we need to listen to values.
167 await _indicationListener?.cancel();
168 _indicationListener = characteristic
169 .onValueReceived.listen((rawData) {
170 logger.finest('BleReadCubit data received: $rawData');
171 final decodedData = BleMeasurementData.decode(rawData, 0);
172 if (decodedData == null) {
173 logger.severe('BleReadCubit decoding failed', [ rawData ]);
174 emit(BleReadFailure());
175 } else {
176 logger.finest('BleReadCubit decoded: $decodedData');
177 emit(BleReadSuccess(decodedData));
178 }
179 _indicationListener?.cancel();
180 _indicationListener = null;
181 });
182
183 final bool indicationsSet = await characteristic.setNotifyValue(true);
184 logger.finest('BleReadCubit indicationsSet: $indicationsSet');
185 }
186}