Commit 7ad99de
Changed files (12)
app
lib
test
bluetooth
characteristics
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);
+ });
+}