Add sensor validation for heart rate monitor
This commit is contained in:
@@ -63,9 +63,14 @@ public class SensorValidationActivity extends AppCompatActivity {
|
||||
private Button stopButton;
|
||||
private Button saveRawButton;
|
||||
private Button saveSpectrumButton;
|
||||
private Button startHrButton;
|
||||
private Button stopHrButton;
|
||||
private Button saveHrButton;
|
||||
private TextView statusTv;
|
||||
private TextView progressTv;
|
||||
private TextView resultTv;
|
||||
private TextView hrProgressTv;
|
||||
private TextView hrResultTv;
|
||||
private BarChart chart;
|
||||
|
||||
private boolean collecting = false;
|
||||
@@ -77,6 +82,14 @@ public class SensorValidationActivity extends AppCompatActivity {
|
||||
private final ArrayList<Double> sampleBuffer = new ArrayList<>();
|
||||
private FrequencyValidationAnalyzer.Result lastResult = null;
|
||||
|
||||
private boolean collectingHr = false;
|
||||
private long hrStartMillis = -1;
|
||||
private int currentHrSecond = -1;
|
||||
private double currentHrSum = 0.0;
|
||||
private int currentHrValidCount = 0;
|
||||
private int currentHrReadCount = 0;
|
||||
private final ArrayList<HeartRatePoint> hrPoints = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -95,9 +108,14 @@ public class SensorValidationActivity extends AppCompatActivity {
|
||||
stopButton = (Button) findViewById(R.id.validationStopButton);
|
||||
saveRawButton = (Button) findViewById(R.id.validationSaveRawButton);
|
||||
saveSpectrumButton = (Button) findViewById(R.id.validationSaveSpectrumButton);
|
||||
startHrButton = (Button) findViewById(R.id.validationStartHrButton);
|
||||
stopHrButton = (Button) findViewById(R.id.validationStopHrButton);
|
||||
saveHrButton = (Button) findViewById(R.id.validationSaveHrButton);
|
||||
statusTv = (TextView) findViewById(R.id.validationStatusTv);
|
||||
progressTv = (TextView) findViewById(R.id.validationProgressTv);
|
||||
resultTv = (TextView) findViewById(R.id.validationResultTv);
|
||||
hrProgressTv = (TextView) findViewById(R.id.validationHrProgressTv);
|
||||
hrResultTv = (TextView) findViewById(R.id.validationHrResultTv);
|
||||
chart = (BarChart) findViewById(R.id.validationChart);
|
||||
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
|
||||
@@ -142,6 +160,24 @@ public class SensorValidationActivity extends AppCompatActivity {
|
||||
saveSpectrumCsv();
|
||||
}
|
||||
});
|
||||
startHrButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
startHeartRateTest();
|
||||
}
|
||||
});
|
||||
stopHrButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
stopHeartRateTest("HR test stopped by user.");
|
||||
}
|
||||
});
|
||||
saveHrButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
saveHeartRateCsv();
|
||||
}
|
||||
});
|
||||
|
||||
configureChart();
|
||||
updateButtons();
|
||||
@@ -167,6 +203,7 @@ public class SensorValidationActivity extends AppCompatActivity {
|
||||
@Override
|
||||
public void run() {
|
||||
pollLatestDataBlock();
|
||||
pollHeartRateSample();
|
||||
updateStatusText();
|
||||
}
|
||||
});
|
||||
@@ -191,6 +228,10 @@ public class SensorValidationActivity extends AppCompatActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
if (collectingHr) {
|
||||
stopHeartRateTest("HR test stopped because accelerometer validation started.");
|
||||
}
|
||||
|
||||
sampleBuffer.clear();
|
||||
lastResult = null;
|
||||
lastAnalysisDurationSeconds = 0.0;
|
||||
@@ -407,10 +448,204 @@ public class SensorValidationActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void updateButtons() {
|
||||
startButton.setEnabled(!collecting);
|
||||
startButton.setEnabled(!collecting && !collectingHr);
|
||||
stopButton.setEnabled(collecting);
|
||||
saveRawButton.setEnabled(lastResult != null);
|
||||
saveSpectrumButton.setEnabled(lastResult != null);
|
||||
startHrButton.setEnabled(!collecting && !collectingHr);
|
||||
stopHrButton.setEnabled(collectingHr);
|
||||
saveHrButton.setEnabled(!hrPoints.isEmpty() && !collectingHr);
|
||||
}
|
||||
|
||||
|
||||
private void startHeartRateTest() {
|
||||
if (!mConnection.mBound || mConnection.mSdServer == null) {
|
||||
mUtil.showToast("Cannot start HR test: background service is not connected.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (collecting) {
|
||||
stopValidationTest("Accelerometer validation stopped because HR test started.");
|
||||
}
|
||||
|
||||
hrPoints.clear();
|
||||
currentHrSecond = -1;
|
||||
currentHrSum = 0.0;
|
||||
currentHrValidCount = 0;
|
||||
currentHrReadCount = 0;
|
||||
hrStartMillis = System.currentTimeMillis();
|
||||
collectingHr = true;
|
||||
|
||||
hrResultTv.setText("Collecting heart-rate data...");
|
||||
updateHeartRateProgressText();
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
private void stopHeartRateTest(String message) {
|
||||
if (collectingHr) {
|
||||
flushCurrentHeartRateSecond();
|
||||
}
|
||||
collectingHr = false;
|
||||
updateHeartRateProgressText();
|
||||
showHeartRateSummary(message);
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
private void pollHeartRateSample() {
|
||||
if (!collectingHr) return;
|
||||
if (!mConnection.mBound || mConnection.mSdServer == null || mConnection.mSdServer.mSdData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
double elapsedSeconds = (now - hrStartMillis) / 1000.0;
|
||||
int secondIndex = (int) Math.floor(elapsedSeconds);
|
||||
|
||||
if (secondIndex >= targetSeconds) {
|
||||
stopHeartRateTest(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentHrSecond != secondIndex) {
|
||||
flushCurrentHeartRateSecond();
|
||||
currentHrSecond = secondIndex;
|
||||
currentHrSum = 0.0;
|
||||
currentHrValidCount = 0;
|
||||
currentHrReadCount = 0;
|
||||
}
|
||||
|
||||
double hr = mConnection.mSdServer.mSdData.mHR;
|
||||
currentHrReadCount++;
|
||||
if (hr >= 0.0) {
|
||||
currentHrSum += hr;
|
||||
currentHrValidCount++;
|
||||
}
|
||||
|
||||
updateHeartRateProgressText();
|
||||
}
|
||||
|
||||
private void flushCurrentHeartRateSecond() {
|
||||
if (currentHrSecond < 0) return;
|
||||
|
||||
double averageHr = currentHrValidCount > 0 ? currentHrSum / currentHrValidCount : -1.0;
|
||||
long timestampMillis = hrStartMillis + currentHrSecond * 1000L;
|
||||
String timestamp = new SimpleDateFormat("HH:mm:ss", Locale.UK).format(new Date(timestampMillis));
|
||||
|
||||
hrPoints.add(new HeartRatePoint(
|
||||
hrPoints.size(),
|
||||
timestamp,
|
||||
currentHrSecond,
|
||||
averageHr,
|
||||
currentHrValidCount,
|
||||
currentHrReadCount));
|
||||
}
|
||||
|
||||
private void updateHeartRateProgressText() {
|
||||
double elapsedSeconds = 0.0;
|
||||
if (collectingHr && hrStartMillis > 0) {
|
||||
elapsedSeconds = (System.currentTimeMillis() - hrStartMillis) / 1000.0;
|
||||
} else if (!hrPoints.isEmpty()) {
|
||||
elapsedSeconds = hrPoints.size();
|
||||
}
|
||||
|
||||
hrProgressTv.setText("HR progress: "
|
||||
+ String.format(Locale.UK, "%.1f", elapsedSeconds)
|
||||
+ " / " + targetSeconds
|
||||
+ " sec, " + hrPoints.size()
|
||||
+ " one-second points saved");
|
||||
}
|
||||
|
||||
private void showHeartRateSummary(String message) {
|
||||
if (hrPoints.isEmpty()) {
|
||||
hrResultTv.setText(message != null ? message : "No HR data collected.");
|
||||
return;
|
||||
}
|
||||
|
||||
double sum = 0.0;
|
||||
double min = Double.MAX_VALUE;
|
||||
double max = -Double.MAX_VALUE;
|
||||
int validSeconds = 0;
|
||||
int totalReadings = 0;
|
||||
int validReadings = 0;
|
||||
|
||||
for (HeartRatePoint point : hrPoints) {
|
||||
totalReadings += point.readingsInSecond;
|
||||
validReadings += point.validReadingsInSecond;
|
||||
if (point.valid) {
|
||||
sum += point.averageHr;
|
||||
min = Math.min(min, point.averageHr);
|
||||
max = Math.max(max, point.averageHr);
|
||||
validSeconds++;
|
||||
}
|
||||
}
|
||||
|
||||
DecimalFormat df2 = new DecimalFormat("0.00");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (message != null) {
|
||||
sb.append(message).append("\n\n");
|
||||
}
|
||||
sb.append("Heart-rate data collected.\n\n");
|
||||
sb.append("Window: ").append(targetSeconds).append(" seconds\n");
|
||||
sb.append("One-second points: ").append(hrPoints.size()).append("\n");
|
||||
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");
|
||||
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");
|
||||
sb.append("Max HR: ").append(df2.format(max)).append(" bpm");
|
||||
} else {
|
||||
sb.append("Average HR: N/A");
|
||||
}
|
||||
hrResultTv.setText(sb.toString());
|
||||
}
|
||||
|
||||
private void saveHeartRateCsv() {
|
||||
if (hrPoints.isEmpty()) return;
|
||||
File file = new File(getOutputDir(), "validation_hr_" + timestampForFilename() + ".csv");
|
||||
try {
|
||||
OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file));
|
||||
writer.write("point_index,time_hhmmss,elapsed_seconds,avg_hr_bpm,valid,valid_readings_in_second,total_readings_in_second\n");
|
||||
for (HeartRatePoint point : hrPoints) {
|
||||
writer.write(point.index + ","
|
||||
+ point.timestampHhMmSs + ","
|
||||
+ point.elapsedSecond + ","
|
||||
+ (point.valid ? String.format(Locale.UK, "%.3f", point.averageHr) : "") + ","
|
||||
+ point.valid + ","
|
||||
+ point.validReadingsInSecond + ","
|
||||
+ point.readingsInSecond + "\n");
|
||||
}
|
||||
writer.close();
|
||||
mUtil.showToast("Saved HR CSV: " + file.getAbsolutePath());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error saving HR CSV: " + e.toString());
|
||||
mUtil.showToast("Error saving HR CSV: " + e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class HeartRatePoint {
|
||||
final int index;
|
||||
final String timestampHhMmSs;
|
||||
final int elapsedSecond;
|
||||
final double averageHr;
|
||||
final boolean valid;
|
||||
final int validReadingsInSecond;
|
||||
final int readingsInSecond;
|
||||
|
||||
HeartRatePoint(int index,
|
||||
String timestampHhMmSs,
|
||||
int elapsedSecond,
|
||||
double averageHr,
|
||||
int validReadingsInSecond,
|
||||
int readingsInSecond) {
|
||||
this.index = index;
|
||||
this.timestampHhMmSs = timestampHhMmSs;
|
||||
this.elapsedSecond = elapsedSecond;
|
||||
this.averageHr = averageHr;
|
||||
this.valid = validReadingsInSecond > 0;
|
||||
this.validReadingsInSecond = validReadingsInSecond;
|
||||
this.readingsInSecond = readingsInSecond;
|
||||
}
|
||||
}
|
||||
|
||||
private File getOutputDir() {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Sensor Frequency Validation"
|
||||
android:text="Sensor Validation"
|
||||
android:textColor="#111827"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold" />
|
||||
@@ -23,7 +23,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="Use a known simulated motion frequency and measure the long-window FFT output from the watch data. This does not affect seizure detection."
|
||||
android:text="Measure long-window accelerometer frequency output or collect timestamped heart-rate data. This does not affect seizure detection."
|
||||
android:textColor="#4b5563"
|
||||
android:textSize="14sp" />
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Test setup"
|
||||
android:text="Accelerometer frequency test"
|
||||
android:textColor="#111827"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
@@ -102,7 +102,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Start" />
|
||||
android:text="Start Accel" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/validationStopButton"
|
||||
@@ -110,7 +110,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:text="Stop" />
|
||||
android:text="Stop Accel" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
@@ -122,6 +122,78 @@
|
||||
android:textColor="#374151" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
android:background="#ffffff"
|
||||
android:orientation="vertical"
|
||||
android:padding="12dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Heart-rate monitor test"
|
||||
android:textColor="#111827"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="Uses the same 30/60 second window. Saves one timestamped heart-rate datapoint per second using the average of the HR reads seen during that second."
|
||||
android:textColor="#4b5563"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/validationStartHrButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Start HR" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/validationStopHrButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:text="Stop HR" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/validationHrProgressTv"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="HR progress: 0.0 / 30 sec, 0 one-second points saved"
|
||||
android:textColor="#374151" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/validationHrResultTv"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="No HR test run yet."
|
||||
android:textColor="#111827"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/validationSaveHrButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="Save HR CSV" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
Reference in New Issue
Block a user