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}