1import 'dart:async';
  2import 'dart:typed_data';
  3
  4import 'package:blood_pressure_app/features/bluetooth/backend/bluetooth_connection.dart';
  5import 'package:blood_pressure_app/features/bluetooth/backend/bluetooth_device.dart';
  6import 'package:blood_pressure_app/features/bluetooth/backend/bluetooth_low_energy/ble_manager.dart';
  7import 'package:blood_pressure_app/features/bluetooth/backend/bluetooth_low_energy/ble_service.dart';
  8import 'package:bluetooth_low_energy/bluetooth_low_energy.dart' show CentralManager, ConnectionState, DiscoveredEventArgs, PeripheralConnectionStateChangedEventArgs;
  9
 10/// BluetoothDevice implementation for the 'bluetooth_low_energy' package
 11final class BluetoothLowEnergyDevice
 12  extends BluetoothDevice<
 13    BluetoothLowEnergyManager,
 14    BluetoothLowEnergyService,
 15    BluetoothLowEnergyCharacteristic,
 16    DiscoveredEventArgs
 17  > with CharacteristicValueListener<
 18    BluetoothLowEnergyManager,
 19    BluetoothLowEnergyService,
 20    BluetoothLowEnergyCharacteristic,
 21    DiscoveredEventArgs
 22  >
 23{
 24  /// Init BluetoothDevice implementation for the 'bluetooth_low_energy' package
 25  BluetoothLowEnergyDevice(super.manager, super.source);
 26
 27  @override
 28  String get deviceId => source.peripheral.uuid.toString();
 29
 30  @override
 31  String get name => source.advertisement.name ?? deviceId;
 32
 33  CentralManager get _cm => manager.backend;
 34
 35  @override
 36  Stream<BluetoothConnectionState> get connectionStream => _cm.connectionStateChanged
 37    .map((PeripheralConnectionStateChangedEventArgs rawState) => switch (rawState.state) {
 38      ConnectionState.connected => BluetoothConnectionState.connected,
 39      ConnectionState.disconnected => BluetoothConnectionState.disconnected,
 40    });
 41
 42  @override
 43  Future<void> backendConnect() => _cm.connect(source.peripheral);
 44
 45  @override
 46  Future<void> backendDisconnect() => _cm.disconnect(source.peripheral);
 47
 48  @override
 49  Future<void> dispose() => disposeCharacteristics();
 50
 51  @override
 52  Future<List<BluetoothLowEnergyService>?> discoverServices() async {
 53    if (!isConnected) {
 54      logger.finer('discoverServices: device not connect. Call device.connect() first');
 55      return null;
 56    }
 57
 58    // Query actual services supported by the device. While they must be
 59    // rediscovered when a disconnect happens, this object is also recreated.
 60    try {
 61      final rawServices = await _cm.discoverGATT(source.peripheral);
 62      logger.finer('discoverServices: $rawServices');
 63      return rawServices.map(BluetoothLowEnergyService.fromSource).toList();
 64    } catch (e) {
 65      logger.shout('discoverServices: error:', [source.peripheral, e]);
 66      return null;
 67    }
 68  }
 69  
 70  @override
 71  Future<bool> triggerCharacteristicValue(BluetoothLowEnergyCharacteristic characteristic, [bool state = true]) async {
 72    await _cm.setCharacteristicNotifyState(source.peripheral, characteristic.source, state: state);
 73    return true;
 74  }
 75
 76  @override
 77  Future<bool> getCharacteristicValue(
 78    BluetoothLowEnergyCharacteristic characteristic,
 79    void Function(Uint8List value, [void Function(bool success)? complete]) onValue,
 80  ) async {
 81    if (!isConnected) {
 82      assert(false, 'getCharacteristicValue: device not connected. Call device.connect() first');
 83      logger.finer('getCharacteristicValue: device not connected.');
 84      return false;
 85    }
 86
 87    if (characteristic.canRead) { // Read characteristic value if supported
 88      try {
 89        final data = await _cm.readCharacteristic(
 90          source.peripheral,
 91          characteristic.source,
 92        );
 93
 94        onValue(data);
 95        return true;
 96      } catch (err) {
 97        logger.warning('getCharacteristicValue: ble readCharacteristic failed', err);
 98        return false;
 99      }
100    }
101
102    else if (characteristic.canIndicate) { // Listen for characteristic value and trigger the device to send it
103      return listenCharacteristicValue(
104        characteristic,
105        _cm.characteristicNotified.map((eventArgs) {
106          if (characteristic.source != eventArgs.characteristic) {
107            /// characteristicNotified is a generic stream which ble does not
108            /// pre-filter for just the current requested characteristic
109            logger.finer('    data is for a different characteristic');
110            logger.finest('    ${eventArgs.characteristic.uuid} == ${characteristic.source.uuid} => ${eventArgs.characteristic == characteristic.source}');
111            logger.finest('    ${eventArgs.characteristic} == ${characteristic.source} => ${eventArgs.characteristic == characteristic.source}');
112            return null;
113          }
114
115          return eventArgs.value;
116        }),
117        onValue
118      );
119    }
120
121    logger.severe("Can't read or indicate characteristic: $characteristic");
122    return false;
123  }
124}