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();
final int unit = flags & 0x01;
final int sensorContactStatus = (flags & 0x06) >> 1; // Bit 0: 0 = UINT8 HR, 1 = UINT16 HR
final boolean energyExpenditurePresent = (flags & 0x08) > 0; final int unit = flags & 0x01;
final boolean rrIntervalPresent = (flags & 0x10) > 0;
// Parse heart rate // Bits 1-2: sensor contact status, if provided by the device
mSdData.mHR = (unit == 0) ? parser.getUInt8() : parser.getUInt16(); final int sensorContactStatus = (flags & 0x06) >> 1;
Log.d(TAG,"Received HR="+mSdData.mHR);
final boolean energyExpenditurePresent = (flags & 0x08) > 0;
final boolean rrIntervalPresent = (flags & 0x10) > 0;
int heartRate = (unit == 0) ? parser.getUInt8() : parser.getUInt16();
// 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();
} }
@@ -471,10 +500,20 @@ public class SensorValidationActivity extends AppCompatActivity {
hrPoints.clear(); hrPoints.clear();
currentHrSecond = -1; currentHrSecond = -1;
currentHrSum = 0.0; currentHrSum = 0.0;
currentHrValidCount = 0; currentHrValidCount = 0;
currentHrReadCount = 0; currentHrReadCount = 0;
hrStartMillis = System.currentTimeMillis(); currentHrFreshNotificationCount = 0;
collectingHr = true; currentHrLastUpdateMillisInSecond = 0;
currentHrRawValue = -1;
currentHrSensorContactStatus = -1;
SdData sdData = mConnection.mSdServer.mSdData;
hrStartNotificationCount = sdData != null ? sdData.mHrNotificationCount : 0;
hrLastSeenNotificationCount = hrStartNotificationCount;
currentHrLastSeenNotificationCount = hrStartNotificationCount;
hrStartMillis = System.currentTimeMillis();
collectingHr = true;
hrResultTv.setText("Collecting heart-rate data..."); hrResultTv.setText("Collecting heart-rate data...");
updateHeartRateProgressText(); updateHeartRateProgressText();
@@ -492,56 +531,87 @@ public class SensorValidationActivity extends AppCompatActivity {
} }
private void pollHeartRateSample() { private void pollHeartRateSample() {
if (!collectingHr) return; if (!collectingHr) return;
if (!mConnection.mBound || mConnection.mSdServer == null || mConnection.mSdServer.mSdData == null) { if (!mConnection.mBound || mConnection.mSdServer == null || mConnection.mSdServer.mSdData == null) {
return; return;
} }
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
double elapsedSeconds = (now - hrStartMillis) / 1000.0; double elapsedSeconds = (now - hrStartMillis) / 1000.0;
int secondIndex = (int) Math.floor(elapsedSeconds); int secondIndex = (int) Math.floor(elapsedSeconds);
if (secondIndex >= targetSeconds) { if (secondIndex >= targetSeconds) {
stopHeartRateTest(null); stopHeartRateTest(null);
return; return;
} }
if (currentHrSecond != secondIndex) { if (currentHrSecond != secondIndex) {
flushCurrentHeartRateSecond(); flushCurrentHeartRateSecond();
currentHrSecond = secondIndex; currentHrSecond = secondIndex;
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++;
if (hr >= 0.0) {
currentHrSum += hr;
currentHrValidCount++;
}
updateHeartRateProgressText(); 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) {
currentHrSum += hr;
currentHrValidCount++;
}
}
updateHeartRateProgressText();
}
private void flushCurrentHeartRateSecond() { private void flushCurrentHeartRateSecond() {
if (currentHrSecond < 0) return; if (currentHrSecond < 0) return;
double averageHr = currentHrValidCount > 0 ? currentHrSum / currentHrValidCount : -1.0; double averageHr = currentHrValidCount > 0 ? currentHrSum / currentHrValidCount : -1.0;
long timestampMillis = hrStartMillis + currentHrSecond * 1000L; long timestampMillis = hrStartMillis + currentHrSecond * 1000L;
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));
hrPoints.add(new HeartRatePoint( long lastHrAgeMs = -1;
hrPoints.size(), if (currentHrLastUpdateMillisInSecond > 0) {
timestampIso, lastHrAgeMs = timestampMillis - currentHrLastUpdateMillisInSecond;
timestampMillis, if (lastHrAgeMs < 0) lastHrAgeMs = 0;
timestamp, }
currentHrSecond,
averageHr, hrPoints.add(new HeartRatePoint(
currentHrValidCount, hrPoints.size(),
currentHrReadCount)); timestampIso,
} timestampMillis,
timestamp,
currentHrSecond,
averageHr,
currentHrValidCount,
currentHrReadCount,
currentHrFreshNotificationCount,
lastHrAgeMs,
currentHrRawValue,
currentHrSensorContactStatus));
}
private void updateHeartRateProgressText() { private void updateHeartRateProgressText() {
double elapsedSeconds = 0.0; double elapsedSeconds = 0.0;
@@ -568,12 +638,18 @@ public class SensorValidationActivity extends AppCompatActivity {
double min = Double.MAX_VALUE; double min = Double.MAX_VALUE;
double max = -Double.MAX_VALUE; double max = -Double.MAX_VALUE;
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,17 +686,21 @@ 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 + ","
+ point.epochMillis + "," + point.epochMillis + ","
+ point.timestampHhMmSs + "," + point.timestampHhMmSs + ","
+ point.elapsedSecond + "," + point.elapsedSecond + ","
+ (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());
@@ -629,35 +711,48 @@ public class SensorValidationActivity extends AppCompatActivity {
} }
private static class HeartRatePoint { private static class HeartRatePoint {
final int index; final int index;
final String timestampIso; final String timestampIso;
final long epochMillis; final long epochMillis;
final String timestampHhMmSs; final String timestampHhMmSs;
final int elapsedSecond; final int elapsedSecond;
final double averageHr; final double averageHr;
final boolean valid; final boolean valid;
final int validReadingsInSecond; final int validReadingsInSecond;
final int readingsInSecond; final int readingsInSecond;
HeartRatePoint(int index, final int freshNotificationsInSecond;
String timestampIso, final long lastHrAgeMs;
long epochMillis, final int rawHrValue;
String timestampHhMmSs, final int sensorContactStatus;
int elapsedSecond,
double averageHr, HeartRatePoint(int index,
int validReadingsInSecond, String timestampIso,
int readingsInSecond) { long epochMillis,
this.index = index; String timestampHhMmSs,
this.timestampIso = timestampIso; int elapsedSecond,
this.epochMillis = epochMillis; double averageHr,
this.timestampHhMmSs = timestampHhMmSs; int validReadingsInSecond,
this.elapsedSecond = elapsedSecond; int readingsInSecond,
this.averageHr = averageHr; int freshNotificationsInSecond,
this.valid = validReadingsInSecond > 0; long lastHrAgeMs,
this.validReadingsInSecond = validReadingsInSecond; int rawHrValue,
this.readingsInSecond = readingsInSecond; int sensorContactStatus) {
} this.index = index;
} this.timestampIso = timestampIso;
this.epochMillis = epochMillis;
this.timestampHhMmSs = timestampHhMmSs;
this.elapsedSecond = elapsedSecond;
this.averageHr = averageHr;
this.valid = validReadingsInSecond > 0;
this.validReadingsInSecond = validReadingsInSecond;
this.readingsInSecond = readingsInSecond;
this.freshNotificationsInSecond = freshNotificationsInSecond;
this.lastHrAgeMs = lastHrAgeMs;
this.rawHrValue = rawHrValue;
this.sensorContactStatus = sensorContactStatus;
}
}
private File getOutputDir() { private File getOutputDir() {
File dir = new File(getExternalFilesDir(null), "sensor_validation"); File dir = new File(getExternalFilesDir(null), "sensor_validation");