Created
October 2, 2025 22:55
-
-
Save 3dsf/e30af1d407815405f21c23d0984f9cd5 to your computer and use it in GitHub Desktop.
arduino_hr_kalman.ino
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ---------------------------------------------------------------------------- | |
| // 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