Trying to resolve heart rate issue

This commit is contained in:
2026-05-07 09:21:06 +00:00
parent e9c2b7c15d
commit bc5a5a3a14
3 changed files with 228 additions and 97 deletions
@@ -133,6 +133,20 @@ public class SdData implements Parcelable {
public boolean mAverageHrAlarmStanding = false; public boolean mAverageHrAlarmStanding = false;
public double mHR = 0; public double mHR = 0;
// Heart-rate diagnostics.
// These are used to distinguish fresh BLE HR measurements from stale values.
public long mHrLastUpdateMillis = 0;
public int mHrNotificationCount = 0;
public int mHrRawValue = -1;
// BLE Heart Rate Measurement sensor contact status.
// -1 = unknown / not provided
// 0 = contact feature not supported or not detected depending on flags
// 1 = contact not detected
// 2 = contact detected
// 3 = reserved/unknown
public int mHrSensorContactStatus = -1;
public boolean mO2SatAlarmStanding = false; public boolean mO2SatAlarmStanding = false;
public boolean mO2SatFaultStanding = false; public boolean mO2SatFaultStanding = false;
public double mO2Sat = 0; public double mO2Sat = 0;
@@ -363,16 +363,38 @@ public class SdDataSourceBLE2 extends SdDataSource {
Log.v(TAG,"onCharacteristicUpdate() - Characteristic "+charUuidStr+" updated"); Log.v(TAG,"onCharacteristicUpdate() - Characteristic "+charUuidStr+" updated");
if (charUuidStr.equals(CHAR_HEART_RATE_MEASUREMENT)) { if (charUuidStr.equals(CHAR_HEART_RATE_MEASUREMENT)) {
Log.v(TAG, String.format("%s", "HR Measurement")); Log.v(TAG, "HR Measurement");
// Parse the flags
int flags = parser.getUInt8(); int flags = parser.getUInt8();
// Bit 0: 0 = UINT8 HR, 1 = UINT16 HR
final int unit = flags & 0x01; final int unit = flags & 0x01;
// Bits 1-2: sensor contact status, if provided by the device
final int sensorContactStatus = (flags & 0x06) >> 1; final int sensorContactStatus = (flags & 0x06) >> 1;
final boolean energyExpenditurePresent = (flags & 0x08) > 0; final boolean energyExpenditurePresent = (flags & 0x08) > 0;
final boolean rrIntervalPresent = (flags & 0x10) > 0; final boolean rrIntervalPresent = (flags & 0x10) > 0;
// Parse heart rate
mSdData.mHR = (unit == 0) ? parser.getUInt8() : parser.getUInt16(); int heartRate = (unit == 0) ? parser.getUInt8() : parser.getUInt16();
Log.d(TAG,"Received HR="+mSdData.mHR);
// Timestamp every actual HR BLE notification, even if the value is invalid.
mSdData.mHrLastUpdateMillis = System.currentTimeMillis();
mSdData.mHrNotificationCount++;
mSdData.mHrRawValue = heartRate;
mSdData.mHrSensorContactStatus = sensorContactStatus;
// Treat 0 and 255 as invalid/fault values, matching the older BLE datasource behavior.
if (heartRate == 0 || heartRate == 255) {
mSdData.mHR = -1;
} else {
mSdData.mHR = (double) heartRate;
}
Log.d(TAG, "Received HR raw=" + heartRate
+ ", stored=" + mSdData.mHR
+ ", contactStatus=" + sensorContactStatus
+ ", notificationCount=" + mSdData.mHrNotificationCount);
} else if (charUuidStr.equals(CHAR_OSD_ACC_DATA) } else if (charUuidStr.equals(CHAR_OSD_ACC_DATA)
|| charUuidStr.equals(CHAR_INFINITIME_ACC_DATA)) { || charUuidStr.equals(CHAR_INFINITIME_ACC_DATA)) {
@@ -88,8 +88,18 @@ public class SensorValidationActivity extends AppCompatActivity {
private double currentHrSum = 0.0; private double currentHrSum = 0.0;
private int currentHrValidCount = 0; private int currentHrValidCount = 0;
private int currentHrReadCount = 0; private int currentHrReadCount = 0;
private final ArrayList<HeartRatePoint> hrPoints = new ArrayList<>();
// Counts actual fresh BLE HR notifications per one-second bucket.
private int currentHrFreshNotificationCount = 0;
private long currentHrLastSeenNotificationCount = -1;
private long currentHrLastUpdateMillisInSecond = 0;
private int currentHrRawValue = -1;
private int currentHrSensorContactStatus = -1;
private long hrStartNotificationCount = 0;
private long hrLastSeenNotificationCount = -1;
private final ArrayList<HeartRatePoint> hrPoints = new ArrayList<>();
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -120,13 +130,32 @@ public class SensorValidationActivity extends AppCompatActivity {
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
android.R.layout.simple_spinner_item, android.R.layout.simple_spinner_item,
new String[]{"30 seconds", "60 seconds"}); new String[]{"30 seconds", "60 seconds", "2 minutes", "3 minutes", "5 minutes"});
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
durationSpinner.setAdapter(adapter); durationSpinner.setAdapter(adapter);
durationSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { durationSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override @Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
targetSeconds = position == 0 ? 30 : 60; switch (position) {
case 0:
targetSeconds = 30;
break;
case 1:
targetSeconds = 60;
break;
case 2:
targetSeconds = 120;
break;
case 3:
targetSeconds = 180;
break;
case 4:
targetSeconds = 300;
break;
default:
targetSeconds = 30;
break;
}
updateProgressText(); updateProgressText();
} }
@@ -473,6 +502,16 @@ public class SensorValidationActivity extends AppCompatActivity {
currentHrSum = 0.0; currentHrSum = 0.0;
currentHrValidCount = 0; currentHrValidCount = 0;
currentHrReadCount = 0; currentHrReadCount = 0;
currentHrFreshNotificationCount = 0;
currentHrLastUpdateMillisInSecond = 0;
currentHrRawValue = -1;
currentHrSensorContactStatus = -1;
SdData sdData = mConnection.mSdServer.mSdData;
hrStartNotificationCount = sdData != null ? sdData.mHrNotificationCount : 0;
hrLastSeenNotificationCount = hrStartNotificationCount;
currentHrLastSeenNotificationCount = hrStartNotificationCount;
hrStartMillis = System.currentTimeMillis(); hrStartMillis = System.currentTimeMillis();
collectingHr = true; collectingHr = true;
@@ -512,14 +551,35 @@ public class SensorValidationActivity extends AppCompatActivity {
currentHrSum = 0.0; currentHrSum = 0.0;
currentHrValidCount = 0; currentHrValidCount = 0;
currentHrReadCount = 0; currentHrReadCount = 0;
currentHrFreshNotificationCount = 0;
currentHrLastUpdateMillisInSecond = 0;
currentHrRawValue = -1;
currentHrSensorContactStatus = -1;
currentHrLastSeenNotificationCount = hrLastSeenNotificationCount;
} }
double hr = mConnection.mSdServer.mSdData.mHR; SdData sdData = mConnection.mSdServer.mSdData;
currentHrReadCount++; currentHrReadCount++;
boolean freshNotification = sdData.mHrNotificationCount != hrLastSeenNotificationCount;
if (freshNotification) {
int newNotifications = (int) (sdData.mHrNotificationCount - hrLastSeenNotificationCount);
if (newNotifications < 0) newNotifications = 0;
currentHrFreshNotificationCount += newNotifications;
hrLastSeenNotificationCount = sdData.mHrNotificationCount;
currentHrLastUpdateMillisInSecond = sdData.mHrLastUpdateMillis;
currentHrRawValue = sdData.mHrRawValue;
currentHrSensorContactStatus = sdData.mHrSensorContactStatus;
double hr = sdData.mHR;
if (hr >= 0.0) { if (hr >= 0.0) {
currentHrSum += hr; currentHrSum += hr;
currentHrValidCount++; currentHrValidCount++;
} }
}
updateHeartRateProgressText(); updateHeartRateProgressText();
} }
@@ -532,6 +592,12 @@ public class SensorValidationActivity extends AppCompatActivity {
String timestampIso = timestampIsoLocal(timestampMillis); String timestampIso = timestampIsoLocal(timestampMillis);
String timestamp = new SimpleDateFormat("HH:mm:ss", Locale.UK).format(new Date(timestampMillis)); String timestamp = new SimpleDateFormat("HH:mm:ss", Locale.UK).format(new Date(timestampMillis));
long lastHrAgeMs = -1;
if (currentHrLastUpdateMillisInSecond > 0) {
lastHrAgeMs = timestampMillis - currentHrLastUpdateMillisInSecond;
if (lastHrAgeMs < 0) lastHrAgeMs = 0;
}
hrPoints.add(new HeartRatePoint( hrPoints.add(new HeartRatePoint(
hrPoints.size(), hrPoints.size(),
timestampIso, timestampIso,
@@ -540,7 +606,11 @@ public class SensorValidationActivity extends AppCompatActivity {
currentHrSecond, currentHrSecond,
averageHr, averageHr,
currentHrValidCount, currentHrValidCount,
currentHrReadCount)); currentHrReadCount,
currentHrFreshNotificationCount,
lastHrAgeMs,
currentHrRawValue,
currentHrSensorContactStatus));
} }
private void updateHeartRateProgressText() { private void updateHeartRateProgressText() {
@@ -570,10 +640,16 @@ public class SensorValidationActivity extends AppCompatActivity {
int validSeconds = 0; int validSeconds = 0;
int totalReadings = 0; int totalReadings = 0;
int validReadings = 0; int validReadings = 0;
int freshNotificationSeconds = 0;
int totalFreshNotifications = 0;
for (HeartRatePoint point : hrPoints) { for (HeartRatePoint point : hrPoints) {
totalReadings += point.readingsInSecond; totalReadings += point.readingsInSecond;
validReadings += point.validReadingsInSecond; validReadings += point.validReadingsInSecond;
totalFreshNotifications += point.freshNotificationsInSecond;
if (point.freshNotificationsInSecond > 0) {
freshNotificationSeconds++;
}
if (point.valid) { if (point.valid) {
sum += point.averageHr; sum += point.averageHr;
min = Math.min(min, point.averageHr); min = Math.min(min, point.averageHr);
@@ -593,6 +669,8 @@ public class SensorValidationActivity extends AppCompatActivity {
sb.append("Seconds with valid HR: ").append(validSeconds).append("\n"); sb.append("Seconds with valid HR: ").append(validSeconds).append("\n");
sb.append("Total reads: ").append(totalReadings).append("\n"); sb.append("Total reads: ").append(totalReadings).append("\n");
sb.append("Valid reads: ").append(validReadings).append("\n"); sb.append("Valid reads: ").append(validReadings).append("\n");
sb.append("Seconds with fresh HR notifications: ").append(freshNotificationSeconds).append("\n");
sb.append("Fresh HR notifications: ").append(totalFreshNotifications).append("\n");
if (validSeconds > 0) { if (validSeconds > 0) {
sb.append("Average HR: ").append(df2.format(sum / validSeconds)).append(" bpm\n"); sb.append("Average HR: ").append(df2.format(sum / validSeconds)).append(" bpm\n");
sb.append("Min HR: ").append(df2.format(min)).append(" bpm\n"); sb.append("Min HR: ").append(df2.format(min)).append(" bpm\n");
@@ -608,7 +686,7 @@ public class SensorValidationActivity extends AppCompatActivity {
File file = new File(getOutputDir(), "validation_hr_" + timestampForFilename() + ".csv"); File file = new File(getOutputDir(), "validation_hr_" + timestampForFilename() + ".csv");
try { try {
OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file)); OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file));
writer.write("point_index,timestamp_iso,epoch_ms,time_hhmmss,elapsed_seconds,avg_hr_bpm,valid,valid_readings_in_second,total_readings_in_second\n"); writer.write("point_index,timestamp_iso,epoch_ms,time_hhmmss,elapsed_seconds,avg_hr_bpm,valid,valid_readings_in_second,total_reads_in_second,fresh_notifications_in_second,last_hr_age_ms,raw_hr_value,sensor_contact_status\n");
for (HeartRatePoint point : hrPoints) { for (HeartRatePoint point : hrPoints) {
writer.write(point.index + "," writer.write(point.index + ","
+ point.timestampIso + "," + point.timestampIso + ","
@@ -618,7 +696,11 @@ public class SensorValidationActivity extends AppCompatActivity {
+ (point.valid ? String.format(Locale.UK, "%.3f", point.averageHr) : "") + "," + (point.valid ? String.format(Locale.UK, "%.3f", point.averageHr) : "") + ","
+ point.valid + "," + point.valid + ","
+ point.validReadingsInSecond + "," + point.validReadingsInSecond + ","
+ point.readingsInSecond + "\n"); + point.readingsInSecond + ","
+ point.freshNotificationsInSecond + ","
+ point.lastHrAgeMs + ","
+ point.rawHrValue + ","
+ point.sensorContactStatus + "\n");
} }
writer.close(); writer.close();
mUtil.showToast("Saved HR CSV: " + file.getAbsolutePath()); mUtil.showToast("Saved HR CSV: " + file.getAbsolutePath());
@@ -639,6 +721,11 @@ public class SensorValidationActivity extends AppCompatActivity {
final int validReadingsInSecond; final int validReadingsInSecond;
final int readingsInSecond; final int readingsInSecond;
final int freshNotificationsInSecond;
final long lastHrAgeMs;
final int rawHrValue;
final int sensorContactStatus;
HeartRatePoint(int index, HeartRatePoint(int index,
String timestampIso, String timestampIso,
long epochMillis, long epochMillis,
@@ -646,7 +733,11 @@ public class SensorValidationActivity extends AppCompatActivity {
int elapsedSecond, int elapsedSecond,
double averageHr, double averageHr,
int validReadingsInSecond, int validReadingsInSecond,
int readingsInSecond) { int readingsInSecond,
int freshNotificationsInSecond,
long lastHrAgeMs,
int rawHrValue,
int sensorContactStatus) {
this.index = index; this.index = index;
this.timestampIso = timestampIso; this.timestampIso = timestampIso;
this.epochMillis = epochMillis; this.epochMillis = epochMillis;
@@ -656,6 +747,10 @@ public class SensorValidationActivity extends AppCompatActivity {
this.valid = validReadingsInSecond > 0; this.valid = validReadingsInSecond > 0;
this.validReadingsInSecond = validReadingsInSecond; this.validReadingsInSecond = validReadingsInSecond;
this.readingsInSecond = readingsInSecond; this.readingsInSecond = readingsInSecond;
this.freshNotificationsInSecond = freshNotificationsInSecond;
this.lastHrAgeMs = lastHrAgeMs;
this.rawHrValue = rawHrValue;
this.sensorContactStatus = sensorContactStatus;
} }
} }