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 stopButton;
|
||||||
private Button saveRawButton;
|
private Button saveRawButton;
|
||||||
private Button saveSpectrumButton;
|
private Button saveSpectrumButton;
|
||||||
|
private Button startHrButton;
|
||||||
|
private Button stopHrButton;
|
||||||
|
private Button saveHrButton;
|
||||||
private TextView statusTv;
|
private TextView statusTv;
|
||||||
private TextView progressTv;
|
private TextView progressTv;
|
||||||
private TextView resultTv;
|
private TextView resultTv;
|
||||||
|
private TextView hrProgressTv;
|
||||||
|
private TextView hrResultTv;
|
||||||
private BarChart chart;
|
private BarChart chart;
|
||||||
|
|
||||||
private boolean collecting = false;
|
private boolean collecting = false;
|
||||||
@@ -77,6 +82,14 @@ public class SensorValidationActivity extends AppCompatActivity {
|
|||||||
private final ArrayList<Double> sampleBuffer = new ArrayList<>();
|
private final ArrayList<Double> sampleBuffer = new ArrayList<>();
|
||||||
private FrequencyValidationAnalyzer.Result lastResult = null;
|
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
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -95,9 +108,14 @@ public class SensorValidationActivity extends AppCompatActivity {
|
|||||||
stopButton = (Button) findViewById(R.id.validationStopButton);
|
stopButton = (Button) findViewById(R.id.validationStopButton);
|
||||||
saveRawButton = (Button) findViewById(R.id.validationSaveRawButton);
|
saveRawButton = (Button) findViewById(R.id.validationSaveRawButton);
|
||||||
saveSpectrumButton = (Button) findViewById(R.id.validationSaveSpectrumButton);
|
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);
|
statusTv = (TextView) findViewById(R.id.validationStatusTv);
|
||||||
progressTv = (TextView) findViewById(R.id.validationProgressTv);
|
progressTv = (TextView) findViewById(R.id.validationProgressTv);
|
||||||
resultTv = (TextView) findViewById(R.id.validationResultTv);
|
resultTv = (TextView) findViewById(R.id.validationResultTv);
|
||||||
|
hrProgressTv = (TextView) findViewById(R.id.validationHrProgressTv);
|
||||||
|
hrResultTv = (TextView) findViewById(R.id.validationHrResultTv);
|
||||||
chart = (BarChart) findViewById(R.id.validationChart);
|
chart = (BarChart) findViewById(R.id.validationChart);
|
||||||
|
|
||||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
|
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
|
||||||
@@ -142,6 +160,24 @@ public class SensorValidationActivity extends AppCompatActivity {
|
|||||||
saveSpectrumCsv();
|
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();
|
configureChart();
|
||||||
updateButtons();
|
updateButtons();
|
||||||
@@ -167,6 +203,7 @@ public class SensorValidationActivity extends AppCompatActivity {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
pollLatestDataBlock();
|
pollLatestDataBlock();
|
||||||
|
pollHeartRateSample();
|
||||||
updateStatusText();
|
updateStatusText();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -191,6 +228,10 @@ public class SensorValidationActivity extends AppCompatActivity {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (collectingHr) {
|
||||||
|
stopHeartRateTest("HR test stopped because accelerometer validation started.");
|
||||||
|
}
|
||||||
|
|
||||||
sampleBuffer.clear();
|
sampleBuffer.clear();
|
||||||
lastResult = null;
|
lastResult = null;
|
||||||
lastAnalysisDurationSeconds = 0.0;
|
lastAnalysisDurationSeconds = 0.0;
|
||||||
@@ -407,10 +448,204 @@ public class SensorValidationActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateButtons() {
|
private void updateButtons() {
|
||||||
startButton.setEnabled(!collecting);
|
startButton.setEnabled(!collecting && !collectingHr);
|
||||||
stopButton.setEnabled(collecting);
|
stopButton.setEnabled(collecting);
|
||||||
saveRawButton.setEnabled(lastResult != null);
|
saveRawButton.setEnabled(lastResult != null);
|
||||||
saveSpectrumButton.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() {
|
private File getOutputDir() {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Sensor Frequency Validation"
|
android:text="Sensor Validation"
|
||||||
android:textColor="#111827"
|
android:textColor="#111827"
|
||||||
android:textSize="22sp"
|
android:textSize="22sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
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:textColor="#4b5563"
|
||||||
android:textSize="14sp" />
|
android:textSize="14sp" />
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Test setup"
|
android:text="Accelerometer frequency test"
|
||||||
android:textColor="#111827"
|
android:textColor="#111827"
|
||||||
android:textSize="18sp"
|
android:textSize="18sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="Start" />
|
android:text="Start Accel" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/validationStopButton"
|
android:id="@+id/validationStopButton"
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="Stop" />
|
android:text="Stop Accel" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@@ -122,6 +122,78 @@
|
|||||||
android:textColor="#374151" />
|
android:textColor="#374151" />
|
||||||
</LinearLayout>
|
</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
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|||||||
Reference in New Issue
Block a user