Commit 80c9b8d

derdilla <82763757+NobodyForNothing@users.noreply.github.com>
2024-03-24 11:14:24
implement bluetooth blood pressure measurement characteristic
Signed-off-by: derdilla <82763757+NobodyForNothing@users.noreply.github.com>
1 parent ab52b8d
Changed files (2)
app/lib/components/ble_input/ble_input_bloc.dart
@@ -2,6 +2,7 @@ import 'dart:async';
 
 import 'package:blood_pressure_app/components/ble_input/ble_input_events.dart';
 import 'package:blood_pressure_app/components/ble_input/ble_input_state.dart';
+import 'package:blood_pressure_app/components/ble_input/measurement_characteristic.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
 
@@ -56,8 +57,9 @@ class BleInputBloc extends Bloc<BleInputEvent, BleInputState> {
                 serviceId: Uuid.parse('1810'),
                 deviceId: event.device.id,
               );
-              _ble.subscribeToCharacteristic(characteristic).listen((event) {
-                // TODO: decode byte array and create measurement
+              _ble.subscribeToCharacteristic(characteristic).listen((List<int> data) {
+                final decoded = BPMeasurementCharacteristic.parse(data);
+                // TODO: display gathered information and allow saving gathered measurement.
               });
 
               // TODO: move reading code
app/lib/components/ble_input/measurement_characteristic.dart
@@ -0,0 +1,230 @@
+import 'dart:math';
+
+// TODO: test all parts of the conversion and parsing.
+
+/// Dart representation of org.bluetooth.characteristic.blood_pressure_measurement.
+///
+/// Reference: https://bitbucket.org/bluetooth-SIG/public/src/main/gss/org.bluetooth.characteristic.blood_pressure_measurement.yaml
+class BPMeasurementCharacteristic {
+  BPMeasurementCharacteristic._create(
+    this.unit,
+    this.sys,
+    this.dia,
+    this.meanArterialPressure,
+    this.time,
+    this.pul,
+    this.userid,
+    this.bodyMoved,
+    this.cuffLoose,
+    this.irregularPulse,
+    this.measurementStatus,
+    this.improperMeasurementPosition,
+  );
+
+  /// Parse from binary data in byte array format as specified in protocol.
+  factory BPMeasurementCharacteristic.parse(List<int> bin) {
+    // - bool flags(8 bit) bool:
+    //    - 0   blood_pressure_units (mmHg or kPa)
+    //    - 1   contains time stamp
+    //    - 2   contains pulse
+    //    - 3   contains userid
+    //    - 4   contains measurement status
+    // - [sys (mmHg) (16 bit) medfloat16 - IEEE 11073-20601 16-bit SFLOAT] (flags[0] == 0)
+    // - [dia (mmHg) (16 bit) medfloat16] (flags[0] == 0)
+    // - [Mean Arterial Pressure (mmHg) (16 bit) medfloat16] (flags[0] == 0)
+    // - [sys (kPa) (16 bit) medfloat16] (flags[0] == 1)
+    // - [dia (kPa) (16 bit) medfloat16] (flags[0] == 1)
+    // - [Mean Arterial Pressure (kPa) (16 bit) medfloat16] (flags[0] == 1)
+    // - [Timestamp (7byte) sec:org.bluetooth.characteristic.date_time] (flags[1] == 1)
+    // - [pul (bpm) (16 bit) medfloat16] (flags[2] == 1)
+    // - [userid (8 bit) uint8] (flags[3] == 1)
+    // - [Measurement status (16 bit) bool] (flags[4] == 1)
+    //    - 0   Body Movement (==1)
+    //    - 1   Cuff loose (==1)
+    //    - 2   Irregular pulse (==1)
+    //    - 3,4 00=ok 01=toHigh 10=toLow
+    //    - 5   Improper measurement position (==1)
+
+    int currByte = 0;
+
+    if (bin.length < 1+3*2) {
+      throw ArgumentError('BloodPressureMeasurementCharacteristic has not enough bytes.');
+    }
+    final BPUnit unit = _checkBit(bin[currByte], 0) ? BPUnit.kPa : BPUnit.mmHg;
+    final bool containsTimestamp = _checkBit(bin[currByte], 1);
+    final bool containsPulse = _checkBit(bin[currByte], 2);
+    final bool containsUserid = _checkBit(bin[currByte], 3);
+    final bool containsMeasurementStatus = _checkBit(bin[currByte], 4);
+
+    final int expectedByteCount = 1
+        + 3*2
+        + (containsTimestamp ? 7 : 0)
+        + (containsPulse ? 2 : 0)
+        + (containsUserid ? 1 : 0)
+        + (containsMeasurementStatus ? 2 : 0);
+    if (bin.length != expectedByteCount) {
+      throw ArgumentError('Unexpected byte count. Flags indicate '
+          '$expectedByteCount but got ${bin.length}');
+    }
+    currByte += 1;
+
+    final sys = _parseMedfloat(bin[currByte], bin[currByte+1]);
+    currByte += 2;
+    final dia = _parseMedfloat(bin[currByte], bin[currByte+1]);
+    currByte += 2;
+    final meanArterialPressure = _parseMedfloat(bin[currByte], bin[currByte+1]);
+    currByte += 2;
+    DateTime? time;
+    if (containsTimestamp) {
+      time = _parseDateTime(bin.sublist(currByte, currByte + 7));
+      currByte += 7;
+    }
+    double? pul;
+    if (containsPulse) {
+      pul = _parseMedfloat(bin[currByte], bin[currByte+1]);
+      currByte += 2;
+    }
+    int? userid;
+    if (containsUserid) {
+      userid = bin[currByte];
+      currByte += 1;
+    }
+    bool? bodyMoved;
+    bool? cuffLoose;
+    bool? irregularPulse;
+    MeasurementStatus? measurementStatus;
+    bool? improperMeasurementPosition;
+    if (containsMeasurementStatus) {
+      bodyMoved = _checkBit(bin[currByte], 0);
+      cuffLoose = _checkBit(bin[currByte], 1);
+      irregularPulse = _checkBit(bin[currByte], 2);
+      if (!_checkBit(bin[currByte], 3) && !_checkBit(bin[currByte], 4)) {
+        measurementStatus = MeasurementStatus.ok;
+      } else if (!_checkBit(bin[currByte], 3) && _checkBit(bin[currByte], 4)) {
+        measurementStatus = MeasurementStatus.toHigh;
+      } else if (_checkBit(bin[currByte], 3) && !_checkBit(bin[currByte], 4)) {
+        measurementStatus = MeasurementStatus.toLow;
+      } else {
+        assert(false);
+        measurementStatus = MeasurementStatus.ok;
+      }
+      improperMeasurementPosition = _checkBit(bin[currByte], 5);
+      currByte += 2;
+    }
+
+    return BPMeasurementCharacteristic._create(
+      unit,
+      sys,
+      dia,
+      meanArterialPressure,
+      time,
+      pul,
+      userid,
+      bodyMoved,
+      cuffLoose,
+      irregularPulse,
+      measurementStatus,
+      improperMeasurementPosition,
+    );
+  }
+
+  /// Pressure unit for [sys], [dia] and [meanArterialPressure].
+  BPUnit unit;
+
+  /// Systolic value in [unit].
+  double sys;
+
+  /// Diatolic value in [unit].
+  double dia;
+
+  /// Mean arterial pressure in [unit].
+  double meanArterialPressure;
+
+  /// Time stamp of the measurement.
+  DateTime? time;
+
+  /// Pulse rate in beats per minute.
+  double? pul;
+
+  /// User id of the person that took the measurement.
+  ///
+  /// This could be used to get more information about that person. Refer to:
+  /// https://www.bluetooth.com/specifications/blp-1-1-1/
+  int? userid;
+
+  /// Whether body movement was detected during measurement.
+  bool? bodyMoved;
+
+  /// Whether the cuff was too loose during measurement.
+  bool? cuffLoose;
+
+  /// Whether irregular pulse was detected.
+  bool? irregularPulse;
+
+  /// The range the pulse rate was in.
+  MeasurementStatus? measurementStatus;
+
+  /// Whether the measurement was taken at an improper position.
+  bool? improperMeasurementPosition;
+
+  /// Whether a the bit at [bit] in [value] is 1.
+  static bool _checkBit(int value, int bit) => (value & (1 << bit)) != 0;
+
+  /// Parse a medfloat to double.
+  ///
+  /// Format: 4 bits exponent, 12 bits mantissa
+  /// https://www.bluetooth.com/wp-content/uploads/2019/03/PHD_Transcoding_WP_v16.pdf
+  static double _parseMedfloat(int firstByte, int secondByte) {
+    final exponent = firstByte & 0xF0;
+    final mantissa = ((firstByte & 0x0F) << 4) + secondByte;
+    if (exponent == 0) {
+      if (mantissa == 0x07FF) {
+        return double.nan;
+      } else if (mantissa == 0x0800) {
+        return double.nan;
+      } else if (mantissa == 0x07FE) {
+        return double.infinity;
+      } else if (mantissa == 0x0802) {
+        return double.negativeInfinity;
+      } else if (mantissa == 0x0801) {
+        assert(false, 'unimplemented, reserved');
+        return double.nan;
+      }
+    }
+
+    return (mantissa * pow(10, exponent)).toDouble();
+  }
+
+  static DateTime _parseDateTime(List<int> data) {
+    assert(data.length == 7);
+    return DateTime(
+      (data[0] << 8) + data[1], // year
+      data[2], // month
+      data[3], // day
+      data[4], // hour
+      data[5], // minute
+      data[6], // seconds
+    );
+  }
+
+}
+
+/// Pressure unit for blood pressure values.
+enum BPUnit {
+  /// Millimeters of mercury.
+  mmHg,
+  /// Kilopascal.
+  kPa
+}
+
+/// Whether pulse rate was in an measurable range.
+///
+/// https://bitbucket.org/bluetooth-SIG/public/src/995423d6e1136111c1759a3d7270c15213ee5b9a/gss/org.bluetooth.characteristic.blood_pressure_measurement.yaml#lines-166:172
+enum MeasurementStatus {
+  /// Pulse rate is within the range.
+  ok,
+  /// Pulse rate exceeds upper limit.
+  toHigh,
+  /// Pulse rate is less than lower limit.
+  toLow,
+}