Commit 7ad99de

derdilla <82763757+NobodyForNothing@users.noreply.github.com>
2024-06-16 09:51:37
implement proper characteristic decoding
Signed-off-by: derdilla <82763757+NobodyForNothing@users.noreply.github.com>
1 parent b96cf65
app/lib/bluetooth/characteristics/ble_date_time.dart
@@ -0,0 +1,32 @@
+import 'package:blood_pressure_app/bluetooth/characteristics/decoding_util.dart';
+
+extension BleDateTimeParser on DateTime {
+  static DateTime? parseBle(List<int> bytes, int offset) {
+    if (bytes.length < offset + 7) return null;
+
+    final int? year = readUInt16Le(bytes, offset);
+    offset += 2;
+    final int? month = readUInt8(bytes, offset);
+    offset += 1;
+    final int? day = readUInt8(bytes, offset);
+    offset += 1;
+    final int? hourOfDay = readUInt8(bytes, offset);
+    offset += 1;
+    final int? minute = readUInt8(bytes, offset);
+    offset += 1;
+    final int? second = readUInt8(bytes, offset);
+
+    if (year == null
+      || month == null
+      || day == null
+      || hourOfDay == null
+      || minute == null
+      || second == null) return null;
+
+    if (year <= 0
+      || month <= 0
+      || day <= 0) return null;
+
+    return DateTime(year, month, day, hourOfDay, minute, second);
+  }
+}
app/lib/bluetooth/characteristics/ble_measurement_data.dart
@@ -1,23 +1,110 @@
+import 'package:blood_pressure_app/logging.dart';
+
+import 'ble_date_time.dart';
 import 'ble_measurement_status.dart';
+import 'decoding_util.dart';
 
+/// https://developer.nordicsemi.com/nRF51_SDK/nRF51_SDK_v4.x.x/doc/html/structble__bps__meas__s.html
+/// https://github.com/NordicSemiconductor/Kotlin-BLE-Library/blob/6b565e59de21dfa53ef80ff8351ac4a4550e8d58/profile/src/main/java/no/nordicsemi/android/kotlin/ble/profile/bps/BloodPressureMeasurementParser.kt
 class BleMeasurementData {
-  BleMeasurementData({
+  BleMeasurementData._({
     required this.systolic,
     required this.diastolic,
     required this.meanArterialPressure,
     required this.isMMHG,
-    required this.pulseRate,
+    required this.pulse,
     required this.userID,
     required this.status,
     required this.timestamp,
   });
 
+  static BleMeasurementData? decode(List<int> data, int offset) {
+    // https://github.com/NordicSemiconductor/Kotlin-BLE-Library/blob/6b565e59de21dfa53ef80ff8351ac4a4550e8d58/profile/src/main/java/no/nordicsemi/android/kotlin/ble/profile/bps/BloodPressureMeasurementParser.kt
+
+    // Reading specific bits: `(byte & (1 << bitIdx))`
+
+    if (data.length < 7) {
+      Log.trace('BleMeasurementData decodeMeasurement: Not enough data, $data has less than 7 bytes.');
+      return null;
+    }
+
+    int offset = 0;
+
+    final int flagsByte = data[offset];
+    offset += 1;
+
+    final bool isMMHG = !isBitIntByteSet(flagsByte, 0); // 0 => mmHg 1 =>kPA
+    final bool timestampPresent = isBitIntByteSet(flagsByte, 1);
+    final bool pulseRatePresent = isBitIntByteSet(flagsByte, 2);
+    final bool userIdPresent = isBitIntByteSet(flagsByte, 3);
+    final bool measurementStatusPresent = isBitIntByteSet(flagsByte, 4);
+
+    if (data.length < (7
+      + (timestampPresent ? 7 : 0)
+      + (pulseRatePresent ? 2 : 0)
+      + (userIdPresent ? 1 : 0)
+      + (measurementStatusPresent ? 2 : 0)
+    )) {
+      Log.trace("BleMeasurementData decodeMeasurement: Flags don't match, $data has less bytes than expected.");
+      return null;
+    }
+
+    final double? systolic = readSFloat(data, offset);
+    offset += 2;
+    final double? diastolic = readSFloat(data, offset);
+    offset += 2;
+    final double? meanArterialPressure = readSFloat(data, offset);
+    offset += 2;
+
+    if (systolic == null || diastolic == null || meanArterialPressure == null) {
+      Log.trace('BleMeasurementData decodeMeasurement: Unable to decode required values sys, dia, and meanArterialPressure, $data.');
+      return null;
+    }
+
+    DateTime? timestamp;
+    if (timestampPresent) {
+      timestamp = BleDateTimeParser.parseBle(data, offset);
+      offset += 7;
+    }
+
+    double? pulse;
+    if (pulseRatePresent) {
+      pulse = readSFloat(data, offset);
+      offset += 2;
+    }
+
+    int? userId;
+    if (userIdPresent) {
+      userId = data[offset];
+      offset += 1;
+    }
+
+    BleMeasurementStatus? status;
+    if (measurementStatusPresent) {
+      status = BleMeasurementStatus.decode(data[offset]);
+    }
+
+    return BleMeasurementData._(
+      systolic: systolic,
+      diastolic: diastolic,
+      meanArterialPressure: meanArterialPressure,
+      isMMHG: isMMHG, // TODO: use
+      pulse: pulse,
+      userID: userId,
+      status: status,
+      timestamp: timestamp,
+    );
+  }
+
   final double systolic;
   final double diastolic;
   final double meanArterialPressure;
   final bool isMMHG; // mmhg or kpa
-  final double? pulseRate;
+  final double? pulse;
   final int? userID;
-  final BleMeasurementStatus status;
+  final BleMeasurementStatus? status;
   final DateTime? timestamp;
+
+  @override
+  String toString() => 'BleMeasurementData{systolic: $systolic, diastolic: $diastolic, meanArterialPressure: $meanArterialPressure, isMMHG: $isMMHG, pulse: $pulse, userID: $userID, status: $status, timestamp: $timestamp}';
 }
app/lib/bluetooth/characteristics/ble_measurement_status.dart
@@ -1,6 +1,7 @@
+import 'package:blood_pressure_app/bluetooth/characteristics/decoding_util.dart';
 
 class BleMeasurementStatus {
-  BleMeasurementStatus({
+  BleMeasurementStatus._({
     required this.bodyMovementDetected,
     required this.cuffTooLose,
     required this.irregularPulseDetected,
@@ -10,6 +11,15 @@ class BleMeasurementStatus {
     required this.improperMeasurementPosition,
   });
 
+  factory BleMeasurementStatus.decode(int byte) => BleMeasurementStatus._(
+    bodyMovementDetected: isBitIntByteSet(byte, 1),
+    cuffTooLose: isBitIntByteSet(byte, 2),
+    irregularPulseDetected: isBitIntByteSet(byte, 3),
+    pulseRateInRange: (byte & (1 << 4) >> 3) == 0,
+    pulseRateExceedsUpperLimit: (byte & (1 << 4) >> 3) == 1,
+    pulseRateIsLessThenLowerLimit: (byte & (1 << 4) >> 3) == 2,
+    improperMeasurementPosition: isBitIntByteSet(byte, 5),
+  );
 
   final bool bodyMovementDetected;
   final bool cuffTooLose;
@@ -18,4 +28,7 @@ class BleMeasurementStatus {
   final bool pulseRateExceedsUpperLimit;
   final bool pulseRateIsLessThenLowerLimit;
   final bool improperMeasurementPosition;
+
+  @override
+  String toString() => 'BleMeasurementStatus{bodyMovementDetected: $bodyMovementDetected, cuffTooLose: $cuffTooLose, irregularPulseDetected: $irregularPulseDetected, pulseRateInRange: $pulseRateInRange, pulseRateExceedsUpperLimit: $pulseRateExceedsUpperLimit, pulseRateIsLessThenLowerLimit: $pulseRateIsLessThenLowerLimit, improperMeasurementPosition: $improperMeasurementPosition}';
 }
app/lib/bluetooth/characteristics/decoding_util.dart
@@ -0,0 +1,32 @@
+import 'dart:math';
+
+/// Whether the bit at offset (0-7) is set.
+///
+/// Masks the byte with a 1 that has [offset] to the right and moves the
+/// remaining bit to the first position and checks if it's equal to 1.
+bool isBitIntByteSet(int byte, int offset) =>
+    (((byte & (1 << offset)) >> offset) == 1);
+
+/// Attempts to read an IEEE-11073 16bit SFloat starting at data[offset].
+double? readSFloat(List<int> data, int offset) {
+  if (data.length < offset + 2) return null;
+  // TODO: special values (NaN, Infinity)
+  final mantissa = data[offset] + ((data[offset + 1] & 0x0F) << 8); // TODO: https://github.com/NordicSemiconductor/Kotlin-BLE-Library/blob/6b565e59de21dfa53ef80ff8351ac4a4550e8d58/core/src/main/java/no/nordicsemi/android/kotlin/ble/core/data/util/DataByteArray.kt#L392
+  final exponent = data[offset + 1] >> 4;
+  return (mantissa * (pow(10, exponent))).toDouble();
+}
+
+int? readUInt8(List<int> data, int offset) {
+  if (data.length < offset + 1) return null;
+  return data[offset];
+}
+
+int? readUInt16Le(List<int> data, int offset) {
+  if (data.length < offset + 2) return null;
+  return data[offset] + (data[offset+1] << 8);
+}
+
+int? readUInt16Be(List<int> data, int offset) {
+  if (data.length < offset + 2) return null;
+  return (data[offset] << 8) + data[offset + 1];
+}
app/lib/bluetooth/characteristics/measurement.dart
@@ -1,72 +0,0 @@
-/// Blood pressure measurement according to default GATT.
-///
-/// https://developer.nordicsemi.com/nRF51_SDK/nRF51_SDK_v4.x.x/doc/html/structble__bps__meas__s.html
-/// https://github.com/NordicSemiconductor/Kotlin-BLE-Library/blob/6b565e59de21dfa53ef80ff8351ac4a4550e8d58/profile/src/main/java/no/nordicsemi/android/kotlin/ble/profile/bps/BloodPressureMeasurementParser.kt
-class MeasurementCharacteristic {
-  /// Create a blood pressure measurement with default GATT fields.
-  const MeasurementCharacteristic({
-    required this.bloodPressureUnitsInKPa,
-    required this.sys,
-    required this.dia,
-    required this.meanArterialPressure,
-    required this.time,
-    required this.pul,
-    required this.userid,
-    required this.bodyMoved,
-    required this.cuffLoose,
-    required this.irregularPulse,
-    required this.measurementStatus,
-    required this.improperMeasurementPosition,
-  });
-
-  /// Whether the unit is kPa (true) or mmHg (false).
-  final bool bloodPressureUnitsInKPa;
-
-  /// Systolic value in [unit].
-  final double sys;
-
-  /// Diatolic value in [unit].
-  final double dia;
-
-  /// Mean arterial pressure in [unit].
-  final double meanArterialPressure;
-
-  /// Time stamp of the measurement.
-  final DateTime? time;
-
-  /// Pulse rate in beats per minute.
-  final 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/
-  final int? userid;
-
-  /// Whether body movement was detected during measurement.
-  final bool? bodyMoved;
-
-  /// Whether the cuff was too loose during measurement.
-  final bool? cuffLoose;
-
-  /// Whether irregular pulse was detected.
-  final bool? irregularPulse;
-
-  /// The range the pulse rate was in.
-  final MeasurementStatus? measurementStatus;
-
-  /// Whether the measurement was taken at an improper position.
-  final bool? improperMeasurementPosition;
-}
-
-/// 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,
-}
app/lib/bluetooth/characteristics/measurement_characteristic.dart
@@ -1,230 +0,0 @@
-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,
-}
app/lib/bluetooth/ble_read_cubit.dart
@@ -1,8 +1,7 @@
 import 'dart:async';
 
-import 'package:blood_pressure_app/bluetooth/characteristic_decoder.dart';
+import 'package:blood_pressure_app/bluetooth/characteristics/ble_measurement_data.dart';
 import 'package:blood_pressure_app/logging.dart';
-import 'package:blood_pressure_app/model/blood_pressure/record.dart';
 import 'package:collection/collection.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -57,6 +56,7 @@ class BleReadCubit extends Cubit<BleReadState> {
   
   late final StreamSubscription<BluetoothConnectionState> _subscription;
   late final Timer _timeoutTimer;
+  StreamSubscription<List<int>>? _indicationListener;
 
   @override
   Future<void> close() async {
@@ -157,27 +157,22 @@ class BleReadCubit extends Cubit<BleReadState> {
     }
 
     // This characteristic only supports indication so we need to listen to values.
-    final indicationListener = characteristic
-      .onValueReceived.listen((data) {
-        Log.trace('BleReadCubit data indicated: $data');
-        final record = CharacteristicDecoder.decodeMeasurementV2(data);
-        Log.trace('BleReadCubit decoded $record');
-        emit(BleReadSuccess(record!));
+    _indicationListener = characteristic
+      .onValueReceived.listen((rawData) {
+        Log.trace('BleReadCubit data received: $rawData');
+        final decodedData = BleMeasurementData.decode(rawData, 0);
+        if (decodedData == null) {
+          Log.err('BleReadCubit decoding failed', [ rawData ]);
+          emit(BleReadFailure());
+        } else {
+          Log.trace('BleReadCubit decoded: $decodedData');
+          emit(BleReadSuccess(decodedData));
+        }
+        _indicationListener?.cancel();
+        _indicationListener = null;
       });
-    await characteristic.setNotifyValue(true);
 
-    /*late final List<int> data;
-    try {
-      data = await characteristic.read();
-    } catch (e) {
-      Log.err('read error', [e, _device, allServices, allCharacteristics, characteristic,]);
-      emit(BleReadFailure());
-      return;
-    }
-
-    Log.trace('BleReadCubit received $data');
-    final record = CharacteristicDecoder.decodeMeasurement(data);
-    Log.trace('BleReadCubit decoded $record');
-    emit(BleReadSuccess(record));*/
+    final bool indicationsSet = await characteristic.setNotifyValue(true);
+    Log.trace('BleReadCubit indicationsSet: $indicationsSet');
   }
 }
app/lib/bluetooth/ble_read_state.dart
@@ -16,5 +16,5 @@ class BleReadSuccess extends BleReadState {
   BleReadSuccess(this.data);
 
   /// Measurement decoded from the device.
-  final BloodPressureRecord data;
+  final BleMeasurementData data;
 }
app/lib/bluetooth/characteristic_decoder.dart
@@ -1,90 +0,0 @@
-import 'dart:math';
-
-import 'package:blood_pressure_app/logging.dart';
-import 'package:blood_pressure_app/model/blood_pressure/record.dart';
-
-/// Decoder for blood pressure values.
-class CharacteristicDecoder {
-  /// Parse a measurement from binary data.
-  static BloodPressureRecord decodeMeasurement(List<int> data) {
-    // This is valid parsing according to: https://github.com/NobodyForNothing/blood-pressure-monitor-fl/issues/80#issuecomment-2067212894
-    // TODO: check if this really works and remove old characteristics decodes if it does.
-    print(data);
-    return BloodPressureRecord(DateTime.now(), data[1], data[3], data[14], '');
-  }
-
-  static BloodPressureRecord? decodeMeasurementV2(List<int> data) {
-    // https://github.com/NordicSemiconductor/Kotlin-BLE-Library/blob/6b565e59de21dfa53ef80ff8351ac4a4550e8d58/profile/src/main/java/no/nordicsemi/android/kotlin/ble/profile/bps/BloodPressureMeasurementParser.kt
-
-    // Reading specific bits: `(byte & (1 < bitIdx))`
-
-    if (data.length < 7) {
-      Log.trace('BleMeasurementData decodeMeasurement: Not enough data, $data has less than 7 bytes.');
-      return null;
-    }
-
-    int offset = 0;
-
-    final int flagsByte = data[offset];
-    offset += 1;
-
-    final bool isMMHG = ((flagsByte & (1 << 0)) == 0);
-    final bool timestampPresent = ((flagsByte & (1 << 1)) == 0);
-    final bool pulseRatePresent = ((flagsByte & (1 << 2)) == 0);
-    final bool userIdPresent = ((flagsByte & (1 << 3)) == 0);
-    final bool measurementStatusPresent = ((flagsByte & (1 << 4)) == 0);
-
-    if (data.length < (7
-        + (timestampPresent ? 7 : 0)
-        + (pulseRatePresent ? 2 : 0)
-        + (userIdPresent ? 1 : 0)
-        + (measurementStatusPresent ? 2 : 0)
-    )) {
-      Log.trace("BleMeasurementData decodeMeasurement: Flags don't match, $data has less bytes than expected.");
-      return null;
-    }
-
-    final double? systolic = _readSFloat(data, offset);
-    offset += 2;
-    final double? diastolic = _readSFloat(data, offset);
-    offset += 2;
-    final double? meanArterialPressure = _readSFloat(data, offset);
-    offset += 2;
-
-    if (systolic == null || diastolic == null || meanArterialPressure == null) {
-      Log.trace('BleMeasurementData decodeMeasurement: Unable to decode required values sys, dia, and meanArterialPressure, $data.');
-      return null;
-    }
-
-    if (timestampPresent) {
-      // TODO: decode timestamp
-      offset += 7;
-    }
-
-    double? pulse;
-    if (pulseRatePresent) {
-      pulse = _readSFloat(data, offset);
-      offset += 2;
-    }
-
-    return BloodPressureRecord(DateTime.now(), systolic.toInt(), diastolic.toInt(), pulse?.toInt(), '');
-    /*return BleMeasurementData(
-      systolic: systolic,
-      diastolic: diastolic,
-      meanArterialPressure: meanArterialPressure,
-      isMMHG: isMMHG,
-      pulseRate: -1,
-      userID: -1,
-      status: BleaM
-    );*/
-  }
-}
-
-/// Attempts to read an IEEE-11073 16bit SFloat starting at data[offset].
-double? _readSFloat(List<int> data, int offset) {
-  if (data.length < offset + 2) return null;
-  // TODO: special values (NaN, Infinity)
-  final mantissa = data[offset] + ((data[offset + 1] & 0x0F) << 8); // TODO: https://github.com/NordicSemiconductor/Kotlin-BLE-Library/blob/6b565e59de21dfa53ef80ff8351ac4a4550e8d58/core/src/main/java/no/nordicsemi/android/kotlin/ble/core/data/util/DataByteArray.kt#L392
-  final exponent = data[offset + 1] >> 4;
-  return (mantissa * (pow(10, exponent))).toDouble();
-}
app/lib/components/bluetooth_input/measurement_success.dart
@@ -1,3 +1,4 @@
+import 'package:blood_pressure_app/bluetooth/characteristics/ble_measurement_data.dart';
 import 'package:blood_pressure_app/components/bluetooth_input/input_card.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@@ -5,7 +6,13 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 /// Indication of a successful bluetooth measurement.
 class MeasurementSuccess extends StatelessWidget {
   /// Indicate a successful while taking a bluetooth measurement.
-  const MeasurementSuccess({super.key, required this.onTap});
+  const MeasurementSuccess({super.key,
+    required this.onTap,
+    required this.data,
+  });
+
+  /// Data decoded from bluetooth.
+  final BleMeasurementData data;
 
   /// Called when the user requests closing.
   final void Function() onTap;
@@ -23,6 +30,34 @@ class MeasurementSuccess extends StatelessWidget {
             const SizedBox(height: 8,),
             Text(AppLocalizations.of(context)!.measurementSuccess),
             const SizedBox(height: 8,),
+            Row(
+              crossAxisAlignment: CrossAxisAlignment.baseline,
+              children: [
+                Expanded(child: Text('Mean arterial pressure')), // TODO: localizations and testing
+                Text(data.meanArterialPressure.toString()),
+              ],
+            ),
+            if (data.userID != null)
+              Row(
+                crossAxisAlignment: CrossAxisAlignment.baseline,
+                children: [
+                  Expanded(child: Text('userID')),
+                  Text(data.userID!.toString()),
+                ],
+              ),
+            if (data.status?.bodyMovementDetected ?? false)
+              ListTile(title: Text('bodyMovementDetected')),
+            if (data.status?.cuffTooLose ?? false)
+              ListTile(title: Text('cuffTooLose')),
+            if (data.status?.improperMeasurementPosition ?? false)
+              ListTile(title: Text('improperMeasurementPosition')),
+            if (data.status?.irregularPulseDetected ?? false)
+              ListTile(title: Text('irregularPulseDetected')),
+            if (data.status?.pulseRateExceedsUpperLimit ?? false)
+              ListTile(title: Text('pulseRateExceedsUpperLimit')),
+            if (data.status?.pulseRateIsLessThenLowerLimit ?? false)
+              ListTile(title: Text('pulseRateIsLessThenLowerLimit')),
+            const SizedBox(height: 8,),
           ],
         ),
       ),
app/lib/components/bluetooth_input.dart
@@ -73,7 +73,7 @@ class _BluetoothInputState extends State<BluetoothInput> {
         _returnToIdle();
       }
     });
-    _deviceScanCubit = DeviceScanCubit(
+    _deviceScanCubit ??= DeviceScanCubit(
       service: Guid('1810'), // TODO one source of truth (w read cubit)
       settings: widget.settings,
     );
@@ -96,9 +96,21 @@ class _BluetoothInputState extends State<BluetoothInput> {
           ),
             // distinction
           DeviceSelected() => BlocConsumer<BleReadCubit, BleReadState>(
-            bloc: () { _deviceReadCubit = BleReadCubit(state.device); return _deviceReadCubit; }(),
+            bloc: (){
+              _deviceReadCubit = BleReadCubit(state.device);
+              return _deviceReadCubit;
+            }(),
             listener: (BuildContext context, BleReadState state) {
-              if (state is BleReadSuccess) widget.onMeasurement(state.data);
+              if (state is BleReadSuccess) {
+                final BloodPressureRecord record = BloodPressureRecord(
+                  state.data.timestamp ?? DateTime.now(),
+                  state.data.systolic.toInt(), // TODO: use pressure info in data
+                  state.data.diastolic.toInt(),
+                  state.data.pulse?.toInt(),
+                  'notes',
+                );
+                widget.onMeasurement(record);
+              }
             },
             builder: (BuildContext context, BleReadState state) {
               Log.trace('_BluetoothInputState BleReadCubit: $state');
@@ -112,6 +124,7 @@ class _BluetoothInputState extends State<BluetoothInput> {
                 ),
                 BleReadSuccess() => MeasurementSuccess(
                   onTap: _returnToIdle,
+                  data: state.data,
                 ),
               };
             },
app/test/bluetooth/characteristics/ble_measurement_data_test.dart
@@ -0,0 +1,26 @@
+import 'package:blood_pressure_app/bluetooth/characteristics/ble_measurement_data.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  test('decodes sample data', () {
+    // 22 => 0001 0110
+    final result = BleMeasurementData.decode([22, 124, 0, 86, 0, 97, 0, 232, 7, 6, 15, 17, 17, 27, 51, 0, 0, 0], 0);
+
+    expect(result, isNotNull);
+    expect(result!.systolic, 124.0);
+    expect(result.diastolic, 86.0);
+    expect(result.meanArterialPressure, 97.0);
+    expect(result.isMMHG, true);
+
+    expect(result.pulse, 51.0);
+    expect(result.timestamp, DateTime(2024, 06, 15, 17, 17, 27));
+    expect(result.userID, null);
+    expect(result.status?.bodyMovementDetected, false);
+    expect(result.status?.cuffTooLose, false);
+    expect(result.status?.irregularPulseDetected, false);
+    expect(result.status?.pulseRateInRange, true);
+    expect(result.status?.pulseRateExceedsUpperLimit, false);
+    expect(result.status?.pulseRateIsLessThenLowerLimit, false);
+    expect(result.status?.improperMeasurementPosition, false);
+  });
+}