Skip to content

Instantly share code, notes, and snippets.

@3dsf
Created October 2, 2025 22:55
Show Gist options
  • Select an option

  • Save 3dsf/e30af1d407815405f21c23d0984f9cd5 to your computer and use it in GitHub Desktop.

Select an option

Save 3dsf/e30af1d407815405f21c23d0984f9cd5 to your computer and use it in GitHub Desktop.
arduino_hr_kalman.ino
// ----------------------------------------------------------------------------
// Hybrid Heart Rate Monitor (ESP32-C5 + MAX30102)
// ----------------------------------------------------------------------------
// Features:
// * Hybrid beat detection (median baseline + adaptive thresholds)
// * Lightweight Kalman-like smoothing (baseline managed externally)
// * Output Modes:
// - BASELINE: baseline batch dump
// - PULSE: pulse amplitude + beat markers
// - BPM ONLY: adaptive BPM reporting (slow pre-cal, 1 Hz + on-change post-cal)
// - NORMAL: verbose diagnostic output
// * Optional display: disabled (stub only)
//
// This file has been cleaned and deduplicated. All legacy duplicate blocks were removed.
// ----------------------------------------------------------------------------
#include <Wire.h>
#include "MAX30105.h"
// ----------------------------------------------------------------------------
// Global Objects
// ----------------------------------------------------------------------------
MAX30105 particleSensor;
// ----------------------------------------------------------------------------
// Global Configuration & State (Single Source of Truth)
// ----------------------------------------------------------------------------
// Heart rate averaging
const byte RATE_SIZE = 4; // Number of samples for rolling average
byte rates[RATE_SIZE]; // Last N beat-to-beat BPM values
byte rateSpot = 0; // Rolling index into rates[]
long lastBeat = 0; // Timestamp of last detected beat (ms)
float beatsPerMinute = 0; // Latest instantaneous / Kalman BPM estimate
int beatAvg = 0; // Rolling average BPM
// Hybrid baseline / peak detection buffers
const int QUICK_BUFFER_SIZE = 8; // Small circular buffer for median baseline
long irBuffer[QUICK_BUFFER_SIZE];
int bufferIndex = 0;
long irBaseline = 0; // Slowly adapting IR baseline
bool isCalibrated = false; // True once initial baseline established
int calibrationCount = 0; // Number of samples accumulated for calibration
long calibrationSum = 0; // Sum of samples during calibration
// Compatibility / diagnostic helpers
float baseline = 0; // Mirrors irBaseline for legacy print logic
float signalAmplitude = 0; // Instant amplitude (|IR - baseline|)
// Kalman-like smoothing state
struct KalmanState {
float x[4]; // [baseline, pulse_amplitude, heart_rate(BPM), phase]
float P[4][4]; // Error covariance
float Q[4][4]; // Process noise
float R; // Measurement noise (scalar)
float K[4]; // Kalman gain (not fully exploited, reserved)
unsigned long lastUpdate;
};
KalmanState kalman;
bool kalmanInitialized = false;
float confidenceScore = 0.0; // Heuristic confidence (0–1)
// Finger placement gating (delay before calibration start)
unsigned long fingerContactTime = 0;
bool fingerJustPlaced = false; // True during initial contact delay
const unsigned long CALIBRATION_DELAY_MS = 2000; // 2s pre-calibration hold
// Sampling / output timing
const int SAMPLING_RATE_HZ = 100; // Fixed sensor polling rate
const int PRECAL_OUTPUT_HZ = 2; // Status print rate before calibration
const int POSTCAL_BASE_OUTPUT_HZ = 1; // Base periodic BPM print rate after calibration
const int BPM_CHANGE_THRESHOLD = 1; // Immediate print if BPM delta >= this
const unsigned long MIN_CHANGE_PRINT_SPACING_MS = 200; // Debounce rapid changes
const int LEGACY_OUTPUT_HZ = 25; // For legacy verbose / waveform modes
const int OUTPUT_INTERVAL = SAMPLING_RATE_HZ / LEGACY_OUTPUT_HZ; // Sample interval for legacy printing
// Adaptive BPM-only mode counters
int outputCounter = 0; // General purpose counter (legacy modes)
int adaptiveCounter = 0; // Counts samples for periodic BPM-only prints
int lastPrintedBPM = -1; // Last emitted BPM (BPM-only mode)
unsigned long lastPrintMillis = 0; // Timestamp of last BPM print
// Mode selection flags (ensure only one of the first three is true for clarity)
bool BASELINE_OUTPUT_MODE = false; // Dump baseline values batch-wise
bool PULSE_OUTPUT_MODE = false; // Output pulse waveform + beat markers
bool BPM_ONLY_MODE = true; // Adaptive BPM-only output
bool VERBOSE_KALMAN_UPDATES = false;// Enable detailed Kalman update logs
// Baseline batch buffer (baseline mode)
long baselineBuffer[100];
int baselineIndex = 0;
// ----------------------------------------------------------------------------
// Kalman Filter Helpers
// ----------------------------------------------------------------------------
void initializeKalmanFilter() {
// Initial reasonable estimates
kalman.x[0] = irBaseline > 0 ? irBaseline : 90000.0; // Baseline
kalman.x[1] = 200.0; // Pulse amplitude
kalman.x[2] = 70.0; // Heart rate BPM
kalman.x[3] = 0.0; // Phase
// Covariance (start uncertain)
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
kalman.P[i][j] = (i == j) ? 1000.0 : 0.0;
kalman.Q[i][j] = 0.0; // Zero init; set diagonals below
}
}
kalman.Q[0][0] = 10.0f; // Baseline slow drift
kalman.Q[1][1] = 50.0f; // Amplitude variability
kalman.Q[2][2] = 1.0f; // HR gradual change
kalman.Q[3][3] = 0.1f; // Phase progression noise
kalman.R = 100.0f; // Measurement variance (empirical)
kalman.lastUpdate = millis();
kalmanInitialized = true;
if (VERBOSE_KALMAN_UPDATES) Serial.println("[Kalman] Initialized");
}
void kalmanPredict(float dt) {
// Predict heart signal phase advancement from BPM; baseline managed externally
float heartRateHz = kalman.x[2] / 60.0f;
float phaseIncrement = 2.0f * PI * heartRateHz * dt;
// Propagate state (simple model)
kalman.x[3] += phaseIncrement;
while (kalman.x[3] > 2.0f * PI) kalman.x[3] -= 2.0f * PI;
// Inflate covariance by process noise
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
kalman.P[i][j] += kalman.Q[i][j] * dt;
}
}
}
void kalmanUpdate(float measurement) {
// We allow baseline to be maintained by hybrid algorithm; only amplitude/confidence here
float residual = measurement - kalman.x[0]; // Baseline-compensated
kalman.x[1] = fabs(residual); // Track amplitude as magnitude of deviation
confidenceScore = exp(-fabs(residual) / 1000.0f); // Heuristic confidence
}
float detectHeartRateKalman(long irValue) {
if (!kalmanInitialized) return 0.0f;
static unsigned long lastUpdateTime = 0;
unsigned long now = millis();
if (lastUpdateTime == 0) {
lastUpdateTime = now;
return kalman.x[2];
}
float dt = (now - lastUpdateTime) / 1000.0f;
lastUpdateTime = now;
// Sync baseline from hybrid mechanism
kalman.x[0] = irBaseline;
baseline = irBaseline;
kalmanPredict(dt);
kalmanUpdate(irValue);
signalAmplitude = fabs((float)irValue - kalman.x[0]);
// Clamp HR to physiological range
if (kalman.x[2] < 30.0f) kalman.x[2] = 30.0f;
if (kalman.x[2] > 220.0f) kalman.x[2] = 220.0f;
return kalman.x[2];
}
// ----------------------------------------------------------------------------
// Setup
// ----------------------------------------------------------------------------
void setup() {
Serial.begin(9600);
Serial.println();
Serial.println("Initializing Hybrid Heart Rate Monitor...");
// Explicit pin assignment for ESP32-C5 I2C
Wire.begin(8, 9); // SDA=GPIO8, SCL=GPIO9
// Initialize sensor
if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) {
Serial.println("✗ MAX30102 not found. Check wiring/power.");
while (true) delay(1000);
}
Serial.println("✓ MAX30102 sensor found");
particleSensor.setup();
particleSensor.setPulseAmplitudeRed(0xFF); // Max red LED current
particleSensor.setPulseAmplitudeGreen(0x00); // Disable green
Serial.println("Modes:");
Serial.println(" Normal (all flags false)");
Serial.println(" Baseline (BASELINE_OUTPUT_MODE)");
Serial.println(" Pulse Waveform (PULSE_OUTPUT_MODE)");
Serial.println(" BPM Only (BPM_ONLY_MODE)");
if (BASELINE_OUTPUT_MODE) Serial.println("*** BASELINE MODE ***");
else if (PULSE_OUTPUT_MODE) Serial.println("*** PULSE MODE ***");
else if (BPM_ONLY_MODE) Serial.println("*** BPM ONLY MODE ***");
else Serial.println("*** NORMAL MODE ***");
Serial.println();
}
// ----------------------------------------------------------------------------
void updateKalmanWithBeat(unsigned long beatTime) {
static unsigned long lastBeatTime = 0;
if (lastBeatTime != 0) {
float beatInterval = (beatTime - lastBeatTime) / 1000.0; // seconds
float instantBPM = 60.0 / beatInterval;
// Only update if BPM is reasonable
if (instantBPM >= 30 && instantBPM <= 220) {
// Update heart rate estimate in Kalman state
kalman.x[2] = kalman.x[2] * 0.8 + instantBPM * 0.2; // Weighted average
// Reset phase when beat detected
kalman.x[3] = 0.0;
if (VERBOSE_KALMAN_UPDATES) {
Serial.print(" [Kalman updated: ");
Serial.print(instantBPM, 1);
Serial.print(" BPM]");
}
}
}
lastBeatTime = beatTime;
}
// Function to update the 4-digit display with BPM
void updateDisplay(int bpm) {
// Display functionality disabled - function does nothing
/*
if (bpm == 0) {
// Show "----" when no reading
alpha4.writeDigitAscii(0, '-');
alpha4.writeDigitAscii(1, '-');
alpha4.writeDigitAscii(2, '-');
alpha4.writeDigitAscii(3, '-');
} else if (bpm < 0) {
// Show "WAIT" during calibration
alpha4.writeDigitAscii(0, 'W');
alpha4.writeDigitAscii(1, 'A');
alpha4.writeDigitAscii(2, 'I');
alpha4.writeDigitAscii(3, 'T');
} else {
// Show BPM value (e.g., " 75" for 75 BPM)
if (bpm >= 100) {
alpha4.writeDigitAscii(0, '0' + (bpm / 100));
alpha4.writeDigitAscii(1, '0' + ((bpm / 10) % 10));
alpha4.writeDigitAscii(2, '0' + (bpm % 10));
alpha4.writeDigitAscii(3, ' ');
} else {
alpha4.writeDigitAscii(0, ' ');
alpha4.writeDigitAscii(1, '0' + (bpm / 10));
alpha4.writeDigitAscii(2, '0' + (bpm % 10));
alpha4.writeDigitAscii(3, ' ');
}
}
alpha4.writeDisplay();
*/
}
bool detectBeatHybrid(long irValue) {
// Simple calibration - just get a baseline
if (!isCalibrated) {
calibrationSum += irValue;
calibrationCount++;
if (calibrationCount >= 10) { // Faster calibration
irBaseline = calibrationSum / calibrationCount;
isCalibrated = true;
Serial.print("✓ Calibration complete! Baseline: ");
Serial.println(irBaseline);
// Initialize Kalman filter with calibrated baseline
initializeKalmanFilter();
kalman.x[0] = irBaseline; // Set baseline from calibration
baseline = irBaseline; // Update compatibility variable
kalmanInitialized = true;
return false;
}
return false;
}
// Store current value in buffer
irBuffer[bufferIndex] = irValue;
bufferIndex = (bufferIndex + 1) % QUICK_BUFFER_SIZE;
// Calculate a stable baseline from buffer (use median for stability)
long sortedBuffer[QUICK_BUFFER_SIZE];
for (int i = 0; i < QUICK_BUFFER_SIZE; i++) {
sortedBuffer[i] = irBuffer[i];
}
// Simple bubble sort for small array
for (int i = 0; i < QUICK_BUFFER_SIZE - 1; i++) {
for (int j = 0; j < QUICK_BUFFER_SIZE - i - 1; j++) {
if (sortedBuffer[j] > sortedBuffer[j + 1]) {
long temp = sortedBuffer[j];
sortedBuffer[j] = sortedBuffer[j + 1];
sortedBuffer[j + 1] = temp;
}
}
}
// Use median as stable baseline (less affected by pulse peaks)
long medianBaseline = sortedBuffer[QUICK_BUFFER_SIZE / 2];
// Very slow adaptation to prevent drift
irBaseline = (irBaseline * 95 + medianBaseline * 5) / 100;
// Improved peak detection with adaptive threshold
static long lastValue = 0;
static bool risingEdge = false;
static int risingCount = 0;
static long peakValue = 0;
static long minValue = irBaseline;
// Calculate dynamic threshold based on signal strength
long signalRange = 0;
long maxVal = irBuffer[0], minVal = irBuffer[0];
for (int i = 0; i < QUICK_BUFFER_SIZE; i++) {
if (irBuffer[i] > maxVal) maxVal = irBuffer[i];
if (irBuffer[i] < minVal) minVal = irBuffer[i];
}
signalRange = maxVal - minVal;
// More sensitive adaptive threshold for strong signals
int risingThreshold = max(20, min(100, (int)(signalRange / 8))); // Smaller threshold for rising
int peakThreshold = max(100, min(800, (int)(signalRange / 3))); // Larger threshold for peak detection
// Track minimum value for better peak detection
if (irValue < minValue) {
minValue = (minValue * 9 + irValue) / 10; // Slow adaptation
}
// Look for rising edge with adaptive threshold
if (irValue > lastValue + risingThreshold) {
risingCount++;
if (risingCount >= 2) { // Need consecutive rises
risingEdge = true;
peakValue = max(peakValue, irValue);
}
} else if (irValue <= lastValue) {
risingCount = 0;
}
// Look for peak (value stops rising and starts falling significantly)
if (risingEdge && irValue < (peakValue - risingThreshold)) {
// Found a peak! Check if it's significant enough above minimum
if ((peakValue - minValue) > peakThreshold) {
risingEdge = false;
risingCount = 0;
peakValue = 0;
lastValue = irValue;
return true; // Beat detected!
}
risingEdge = false;
risingCount = 0;
peakValue = 0;
}
lastValue = irValue;
return false;
}
void loop() {
long irValue = particleSensor.getIR();
// Increment output counter
outputCounter++;
if (BASELINE_OUTPUT_MODE) {
// Baseline output mode - collect baselines and output them every second
if (irValue >= 50000) { // Only when finger is present
// Handle finger contact detection and calibration delay
if (!isCalibrated && !fingerJustPlaced && calibrationCount == 0) {
fingerContactTime = millis();
fingerJustPlaced = true;
Serial.println("✓ Finger detected! Waiting 2 seconds before calibration...");
} else if (!isCalibrated && fingerJustPlaced && calibrationCount == 0) {
if (millis() - fingerContactTime >= CALIBRATION_DELAY_MS) {
// Delay period complete, start calibration
fingerJustPlaced = false;
Serial.println("✓ Starting calibration now...");
}
}
// Only call detection if delay period is over or already calibrated
if (!fingerJustPlaced) {
detectBeatHybrid(irValue);
// Store baseline if calibrated
if (isCalibrated && baselineIndex < 100) {
baselineBuffer[baselineIndex] = irBaseline;
baselineIndex++;
}
}
} else {
// Reset when finger removed
isCalibrated = false;
calibrationCount = 0;
calibrationSum = 0;
bufferIndex = 0;
baselineIndex = 0;
fingerJustPlaced = false; // Reset finger contact detection
}
// Output baselines every second (100 samples)
if (outputCounter >= OUTPUT_INTERVAL) {
if (baselineIndex > 0) {
for (int i = 0; i < baselineIndex; i++) {
Serial.print(baselineBuffer[i]);
Serial.print(","); // Always print comma (including trailing)
}
Serial.println();
updateDisplay(888); // Show "888 " to indicate baseline mode
} else {
Serial.println("NO_FINGER");
updateDisplay(0); // Show "----" on display
}
outputCounter = 0;
baselineIndex = 0; // Reset for next batch
}
} else if (PULSE_OUTPUT_MODE) {
// Pulse output mode - detect and output pulse peaks only
if (irValue >= 50000) { // Only when finger is present
// Handle finger contact detection and calibration delay
if (!isCalibrated && !fingerJustPlaced && calibrationCount == 0) {
fingerContactTime = millis();
fingerJustPlaced = true;
Serial.println("✓ Finger detected! Waiting 2 seconds before calibration...");
} else if (!isCalibrated && fingerJustPlaced && calibrationCount == 0) {
if (millis() - fingerContactTime >= CALIBRATION_DELAY_MS) {
// Delay period complete, start calibration
fingerJustPlaced = false;
Serial.println("✓ Starting calibration now...");
}
}
// Only call detection if delay period is over or already calibrated
if (!fingerJustPlaced) {
bool beatDetected = detectBeatHybrid(irValue);
// Output pulse signal continuously for real-time visualization
if (isCalibrated) {
long pulseAmplitude = irValue - irBaseline;
// Output every sample for real-time pulse visualization
Serial.print(pulseAmplitude);
if (beatDetected) {
Serial.print(",BEAT"); // CSV format with beat marker
} else {
Serial.print(",0"); // No beat marker
}
Serial.println();
} else {
// During calibration, show progress
if (outputCounter % 10 == 0) {
Serial.print("CALIBRATING... (");
Serial.print(calibrationCount);
Serial.println("/10)");
}
}
}
} else {
// Reset when finger removed
isCalibrated = false;
calibrationCount = 0;
calibrationSum = 0;
bufferIndex = 0;
fingerJustPlaced = false;
if (outputCounter >= OUTPUT_INTERVAL) {
Serial.println("NO_FINGER");
updateDisplay(0); // Show "----" on display
outputCounter = 0;
}
}
} else if (BPM_ONLY_MODE) {
// BPM-only output mode - adaptive:
// * Before calibration: low-rate status (PRECAL_OUTPUT_HZ)
// * After calibration: 1 Hz periodic PLUS immediate prints on meaningful BPM change
if (irValue >= 50000) { // Only when finger is present
// Handle finger contact detection and calibration delay
if (!isCalibrated && !fingerJustPlaced && calibrationCount == 0) {
fingerContactTime = millis();
fingerJustPlaced = true;
Serial.println("✓ Finger detected! Waiting 2 seconds before calibration...");
} else if (!isCalibrated && fingerJustPlaced && calibrationCount == 0) {
if (millis() - fingerContactTime >= CALIBRATION_DELAY_MS) {
// Delay period complete, start calibration
fingerJustPlaced = false;
Serial.println("✓ Starting calibration now...");
}
}
// Only call detection if delay period is over or already calibrated
if (!fingerJustPlaced) {
bool beatDetected = detectBeatHybrid(irValue);
// Update Kalman filter continuously
float kalmanHeartRate = detectHeartRateKalman(irValue);
if (kalmanHeartRate > 0) {
beatsPerMinute = kalmanHeartRate;
}
// Update when beat is detected
if (beatDetected) {
updateKalmanWithBeat(millis());
beatsPerMinute = kalman.x[2]; // Use updated Kalman estimate
// Update rate averaging
rates[rateSpot++] = (byte)beatsPerMinute;
rateSpot %= RATE_SIZE;
beatAvg = 0;
for (byte x = 0; x < RATE_SIZE; x++)
beatAvg += rates[x];
beatAvg /= RATE_SIZE;
}
}
} else {
// Reset when finger removed
isCalibrated = false;
calibrationCount = 0;
calibrationSum = 0;
bufferIndex = 0;
fingerJustPlaced = false;
beatAvg = 0;
}
// Adaptive output decision
int targetHz = isCalibrated ? POSTCAL_BASE_OUTPUT_HZ : PRECAL_OUTPUT_HZ;
int samplesPerPeriodicPrint = SAMPLING_RATE_HZ / max(1, targetHz);
adaptiveCounter++;
bool periodicDue = adaptiveCounter >= samplesPerPeriodicPrint;
bool changeDue = false;
unsigned long nowMs = millis();
if (isCalibrated && beatAvg > 0) {
if (lastPrintedBPM < 0) {
changeDue = true; // First valid reading
} else if (abs(beatAvg - lastPrintedBPM) >= BPM_CHANGE_THRESHOLD && (nowMs - lastPrintMillis) >= MIN_CHANGE_PRINT_SPACING_MS) {
changeDue = true; // Significant change
}
}
if (periodicDue || changeDue) {
if (irValue >= 50000 && isCalibrated && beatAvg > 0) {
Serial.println(beatAvg);
updateDisplay(beatAvg);
lastPrintedBPM = beatAvg;
lastPrintMillis = nowMs;
} else if (irValue >= 50000 && !isCalibrated) {
Serial.println("CALIBRATING");
updateDisplay(-1);
} else {
Serial.println("NO_FINGER");
updateDisplay(0);
lastPrintedBPM = -1; // Invalidate last BPM
}
if (periodicDue) adaptiveCounter = 0; // Reset periodic counter
}
} else {
// Normal heart rate monitoring mode (original code)
// Always process the sensor data for beat detection
if (irValue < 50000) {
beatAvg = 0;
isCalibrated = false; // Reset calibration when finger removed
calibrationCount = 0;
calibrationSum = 0;
bufferIndex = 0; // Reset buffer index too
fingerJustPlaced = false; // Reset finger contact detection
// Only output every OUTPUT_INTERVAL samples
if (outputCounter >= OUTPUT_INTERVAL) {
Serial.print("IR: ");
Serial.print(irValue);
Serial.println(" - NO FINGER");
Serial.print("Output: ");
Serial.println(beatAvg);
Serial.println("---");
updateDisplay(0); // Show "----" on display
outputCounter = 0; // Reset counter
}
} else {
// Call the detection function first to update calibration count
bool beatDetected = false;
// Handle finger contact detection and calibration delay
if (!isCalibrated && !fingerJustPlaced && calibrationCount == 0) {
fingerContactTime = millis();
fingerJustPlaced = true;
Serial.println("✓ Finger detected! Waiting 2 seconds before calibration...");
} else if (!isCalibrated && fingerJustPlaced && calibrationCount == 0) {
if (millis() - fingerContactTime >= CALIBRATION_DELAY_MS) {
// Delay period complete, start calibration
fingerJustPlaced = false;
Serial.println("✓ Starting calibration now...");
}
}
// Only call detection if delay period is over or already calibrated
if (!fingerJustPlaced) {
// Run hybrid detection for beat timing events
beatDetected = detectBeatHybrid(irValue);
// Update Kalman filter continuously
float kalmanHeartRate = detectHeartRateKalman(irValue);
if (kalmanHeartRate > 0) {
beatsPerMinute = kalmanHeartRate;
}
// Update Kalman filter when beat is detected
if (beatDetected) {
updateKalmanWithBeat(millis());
beatsPerMinute = kalman.x[2]; // Use updated Kalman estimate
}
}
// Only output every OUTPUT_INTERVAL samples
if (outputCounter >= OUTPUT_INTERVAL) {
Serial.print("IR: ");
Serial.print(irValue);
if (!isCalibrated) {
Serial.print(" - CALIBRATING... (");
Serial.print(calibrationCount);
Serial.print("/10)");
Serial.println();
updateDisplay(-1); // Show "WAIT" on display
} else {
Serial.print(" - MONITORING ");
if (beatDetected) {
Serial.print("♥ HEARTBEAT! ");
long delta = millis() - lastBeat;
lastBeat = millis();
// Display Kalman filter estimates
Serial.print("Kalman BPM: ");
Serial.print(beatsPerMinute, 1);
Serial.print(" | Confidence: ");
Serial.print(confidenceScore * 100, 1);
Serial.print("%");
if (delta > 270 && delta < 1000) { // 270ms = 220 BPM, 1000ms = 60 BPM
float timingBPM = 60000.0 / delta;
Serial.print(" | Timing BPM: ");
Serial.print(timingBPM, 1);
// Always add to rate array for averaging, using Kalman estimate
rates[rateSpot++] = (byte)beatsPerMinute; // Use Kalman estimate
rateSpot %= RATE_SIZE;
beatAvg = 0;
for (byte x = 0; x < RATE_SIZE; x++)
beatAvg += rates[x];
beatAvg /= RATE_SIZE;
Serial.print(" → Kalman Avg: ");
Serial.print(beatAvg);
if (timingBPM < 100 && timingBPM > 60) { // Normal resting range: 60-100 BPM
Serial.print(" ♥♥♥ (good timing)");
} else {
Serial.print(" (timing out of range: 60-100)");
}
} else {
Serial.print(" (timing: ");
Serial.print(delta);
Serial.print("ms - out of range)");
}
Serial.println();
} else {
Serial.print("Kalman BPM: ");
Serial.print(beatsPerMinute, 1);
Serial.print(" | Baseline: ");
Serial.print(baseline, 0);
Serial.print(" | Above baseline: ");
Serial.println(irValue - baseline);
}
}
Serial.print("Kalman Output: ");
Serial.print(beatsPerMinute, 1);
Serial.print(" BPM | Avg: ");
Serial.println(beatAvg);
Serial.println("---");
updateDisplay(beatAvg); // Update display with current BPM average
outputCounter = 0; // Reset counter
}
}
}
delay(10); // 100 Hz sampling rate (10ms delay)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment