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}