From 671a6d6fb452e103f33551b6259e28c02257599d Mon Sep 17 00:00:00 2001 From: gudbjartur22 Date: Tue, 5 May 2026 10:30:46 +0000 Subject: [PATCH] Add sensor validation for heart rate monitor --- .../SensorValidationActivity.java | 237 +++++++++++++++++- .../res/layout/activity_sensor_validation.xml | 82 +++++- 2 files changed, 313 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/uk/org/openseizuredetector/SensorValidationActivity.java b/app/src/main/java/uk/org/openseizuredetector/SensorValidationActivity.java index ab3ebef..e80e8df 100644 --- a/app/src/main/java/uk/org/openseizuredetector/SensorValidationActivity.java +++ b/app/src/main/java/uk/org/openseizuredetector/SensorValidationActivity.java @@ -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 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 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 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() { diff --git a/app/src/main/res/layout/activity_sensor_validation.xml b/app/src/main/res/layout/activity_sensor_validation.xml index 6afbba3..49e9c34 100644 --- a/app/src/main/res/layout/activity_sensor_validation.xml +++ b/app/src/main/res/layout/activity_sensor_validation.xml @@ -14,7 +14,7 @@ @@ -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 @@ @@ -102,7 +102,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" - android:text="Start" /> + android:text="Start Accel" />