Trying to resolve heart rate issue
This commit is contained in:
@@ -133,6 +133,20 @@ public class SdData implements Parcelable {
|
||||
public boolean mAverageHrAlarmStanding = false;
|
||||
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 mO2SatFaultStanding = false;
|
||||
public double mO2Sat = 0;
|
||||
|
||||
@@ -363,16 +363,38 @@ public class SdDataSourceBLE2 extends SdDataSource {
|
||||
Log.v(TAG,"onCharacteristicUpdate() - Characteristic "+charUuidStr+" updated");
|
||||
|
||||
if (charUuidStr.equals(CHAR_HEART_RATE_MEASUREMENT)) {
|
||||
Log.v(TAG, String.format("%s", "HR Measurement"));
|
||||
// Parse the flags
|
||||
Log.v(TAG, "HR Measurement");
|
||||
|
||||
int flags = parser.getUInt8();
|
||||
|
||||
// Bit 0: 0 = UINT8 HR, 1 = UINT16 HR
|
||||
final int unit = flags & 0x01;
|
||||
|
||||
// Bits 1-2: sensor contact status, if provided by the device
|
||||
final int sensorContactStatus = (flags & 0x06) >> 1;
|
||||
|
||||
final boolean energyExpenditurePresent = (flags & 0x08) > 0;
|
||||
final boolean rrIntervalPresent = (flags & 0x10) > 0;
|
||||
// Parse heart rate
|
||||
mSdData.mHR = (unit == 0) ? parser.getUInt8() : parser.getUInt16();
|
||||
Log.d(TAG,"Received HR="+mSdData.mHR);
|
||||
|
||||
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)
|
||||
|| charUuidStr.equals(CHAR_INFINITIME_ACC_DATA)) {
|
||||
|
||||
@@ -88,8 +88,18 @@ public class SensorValidationActivity extends AppCompatActivity {
|
||||
private double currentHrSum = 0.0;
|
||||
private int currentHrValidCount = 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
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -120,13 +130,32 @@ public class SensorValidationActivity extends AppCompatActivity {
|
||||
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
|
||||
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);
|
||||
durationSpinner.setAdapter(adapter);
|
||||
durationSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -473,6 +502,16 @@ public class SensorValidationActivity extends AppCompatActivity {
|
||||
currentHrSum = 0.0;
|
||||
currentHrValidCount = 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();
|
||||
collectingHr = true;
|
||||
|
||||
@@ -512,14 +551,35 @@ public class SensorValidationActivity extends AppCompatActivity {
|
||||
currentHrSum = 0.0;
|
||||
currentHrValidCount = 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++;
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -532,6 +592,12 @@ public class SensorValidationActivity extends AppCompatActivity {
|
||||
String timestampIso = timestampIsoLocal(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.size(),
|
||||
timestampIso,
|
||||
@@ -540,7 +606,11 @@ public class SensorValidationActivity extends AppCompatActivity {
|
||||
currentHrSecond,
|
||||
averageHr,
|
||||
currentHrValidCount,
|
||||
currentHrReadCount));
|
||||
currentHrReadCount,
|
||||
currentHrFreshNotificationCount,
|
||||
lastHrAgeMs,
|
||||
currentHrRawValue,
|
||||
currentHrSensorContactStatus));
|
||||
}
|
||||
|
||||
private void updateHeartRateProgressText() {
|
||||
@@ -570,10 +640,16 @@ public class SensorValidationActivity extends AppCompatActivity {
|
||||
int validSeconds = 0;
|
||||
int totalReadings = 0;
|
||||
int validReadings = 0;
|
||||
int freshNotificationSeconds = 0;
|
||||
int totalFreshNotifications = 0;
|
||||
|
||||
for (HeartRatePoint point : hrPoints) {
|
||||
totalReadings += point.readingsInSecond;
|
||||
validReadings += point.validReadingsInSecond;
|
||||
totalFreshNotifications += point.freshNotificationsInSecond;
|
||||
if (point.freshNotificationsInSecond > 0) {
|
||||
freshNotificationSeconds++;
|
||||
}
|
||||
if (point.valid) {
|
||||
sum += 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("Total reads: ").append(totalReadings).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) {
|
||||
sb.append("Average HR: ").append(df2.format(sum / validSeconds)).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");
|
||||
try {
|
||||
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) {
|
||||
writer.write(point.index + ","
|
||||
+ point.timestampIso + ","
|
||||
@@ -618,7 +696,11 @@ public class SensorValidationActivity extends AppCompatActivity {
|
||||
+ (point.valid ? String.format(Locale.UK, "%.3f", point.averageHr) : "") + ","
|
||||
+ point.valid + ","
|
||||
+ point.validReadingsInSecond + ","
|
||||
+ point.readingsInSecond + "\n");
|
||||
+ point.readingsInSecond + ","
|
||||
+ point.freshNotificationsInSecond + ","
|
||||
+ point.lastHrAgeMs + ","
|
||||
+ point.rawHrValue + ","
|
||||
+ point.sensorContactStatus + "\n");
|
||||
}
|
||||
writer.close();
|
||||
mUtil.showToast("Saved HR CSV: " + file.getAbsolutePath());
|
||||
@@ -639,6 +721,11 @@ public class SensorValidationActivity extends AppCompatActivity {
|
||||
final int validReadingsInSecond;
|
||||
final int readingsInSecond;
|
||||
|
||||
final int freshNotificationsInSecond;
|
||||
final long lastHrAgeMs;
|
||||
final int rawHrValue;
|
||||
final int sensorContactStatus;
|
||||
|
||||
HeartRatePoint(int index,
|
||||
String timestampIso,
|
||||
long epochMillis,
|
||||
@@ -646,7 +733,11 @@ public class SensorValidationActivity extends AppCompatActivity {
|
||||
int elapsedSecond,
|
||||
double averageHr,
|
||||
int validReadingsInSecond,
|
||||
int readingsInSecond) {
|
||||
int readingsInSecond,
|
||||
int freshNotificationsInSecond,
|
||||
long lastHrAgeMs,
|
||||
int rawHrValue,
|
||||
int sensorContactStatus) {
|
||||
this.index = index;
|
||||
this.timestampIso = timestampIso;
|
||||
this.epochMillis = epochMillis;
|
||||
@@ -656,6 +747,10 @@ public class SensorValidationActivity extends AppCompatActivity {
|
||||
this.valid = validReadingsInSecond > 0;
|
||||
this.validReadingsInSecond = validReadingsInSecond;
|
||||
this.readingsInSecond = readingsInSecond;
|
||||
this.freshNotificationsInSecond = freshNotificationsInSecond;
|
||||
this.lastHrAgeMs = lastHrAgeMs;
|
||||
this.rawHrValue = rawHrValue;
|
||||
this.sensorContactStatus = sensorContactStatus;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user