Hi all.
The code attached run on ESP32S3 + SSD1306_OLED works well.
Added some Serial.print()
; to analysis code, some printed good, some not, why?
the all Serial.print();
inside void displayMeasuredValues(bool no_finger, int32_t beatAvg, int32_t spo2)
doesn't printed, however, the OLED shown the MAX30105 readings on screen, that means the code did call and run void displayMeasuredValues(bool no_finger, int32_t beatAvg, int32_t spo2)
.
Thanks.
Adam
SKETCH:
#include <MAX3010x.h>
#include <Adafruit_SSD1306.h>
#include "filters.h"
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Sensor (adjust to your sensor type)
MAX30105 sensor;
const auto kSamplingRate = sensor.SAMPLING_RATE_400SPS;
const float kSamplingFrequency = 400.0;
// Finger Detection Threshold and Cooldown
const unsigned long kFingerThreshold = 10000;
const unsigned int kFingerCooldownMs = 500;
// Edge Detection Threshold (decrease for MAX30100)
const float kEdgeThreshold = -2000.0;
// Filters
const float kLowPassCutoff = 5.0;
const float kHighPassCutoff = 0.5;
// Averaging
const bool kEnableAveraging = true;
const int kAveragingSamples = 5;
const int kSampleThreshold = 5;
void setup() {
Serial.begin(115200);
delay(5000);
Serial.println("ESP32_MAX30102_OLED_gd_S3_2!");
Wire.begin(43, 44); //SDA, SCL
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
//display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
while (1);
}
sensor.begin() && sensor.setSamplingRate(kSamplingRate);
Serial.print("71 sensor.begin()=");
Serial.println(sensor.begin());
Serial.print("73 sensor.setSamplingRate(kSamplingRate)=");
Serial.println(sensor.setSamplingRate(kSamplingRate));
/* //if (sensor.begin() && sensor.setSamplingRate(kSamplingRate)) {
// Serial.println("Sensor initialized");
//}
else {
Serial.println("Sensor not found");
while (1);
} */
display.clearDisplay();
initDrawScreen();
}
// Filter Instances
LowPassFilter low_pass_filter_red(kLowPassCutoff, kSamplingFrequency); // used: filters.h
LowPassFilter low_pass_filter_ir(kLowPassCutoff, kSamplingFrequency);
HighPassFilter high_pass_filter(kHighPassCutoff, kSamplingFrequency);
Differentiator differentiator(kSamplingFrequency);
MovingAverageFilter<kAveragingSamples> averager_bpm;
MovingAverageFilter<kAveragingSamples> averager_r;
MovingAverageFilter<kAveragingSamples> averager_spo2;
// Statistic for pulse oximetry
MinMaxAvgStatistic stat_red;
MinMaxAvgStatistic stat_ir;
// R value to SpO2 calibration factors
// See https://www.maximintegrated.com/en/design/technical-documents/app-notes/6/6845.html
float kSpO2_A = 1.5958422;
float kSpO2_B = -34.6596622;
float kSpO2_C = 112.6898759;
// Timestamp of the last heartbeat
long last_heartbeat = 0;
// Timestamp for finger detection
long finger_timestamp = 0;
bool finger_detected = false;
// Last diff to detect zero crossing
float last_diff = NAN;
bool crossed = false;
long crossed_time = 0;
void loop() {
auto sample = sensor.readSample(1000);
float current_value_red = sample.red;
float current_value_ir = sample.ir;
Serial.print("154! current_value_red=");
Serial.println(current_value_red);
// Detect Finger using raw sensor value
if (sample.red > kFingerThreshold) { /// kFingerThreshold = 10000;
Serial.print("160! sample.red=");
Serial.println(sample.red);
if (millis() - finger_timestamp > kFingerCooldownMs) { /// kFingerCooldownMs = 500;
finger_detected = true;
Serial.print("166! millis() - finger_timestamp=");
Serial.println(millis() - finger_timestamp);
}
}
else {
// Reset values if the finger is removed
differentiator.reset();
averager_bpm.reset();
averager_r.reset();
averager_spo2.reset();
low_pass_filter_red.reset();
low_pass_filter_ir.reset();
high_pass_filter.reset();
stat_red.reset();
stat_ir.reset();
finger_detected = false;
finger_timestamp = millis();
Serial.print("185! finger_timestamp=");
Serial.println(finger_timestamp);
}
if (finger_detected) {
displayMeasuredValues(false, 0, 0);
current_value_red = low_pass_filter_red.process(current_value_red);
current_value_ir = low_pass_filter_ir.process(current_value_ir);
Serial.print("195! current_value_red =");
Serial.println(current_value_red);
// Statistics for pulse oximetry
stat_red.process(current_value_red);
stat_ir.process(current_value_ir);
// Heart beat detection using value for red LED
float current_value = high_pass_filter.process(current_value_red);
float current_diff = differentiator.process(current_value);
// Valid values?
if (!isnan(current_diff) && !isnan(last_diff)) {
Serial.print("210! current_diff =");
Serial.println(current_diff);
Serial.print("212! last_diff =");
Serial.println(last_diff);
// Detect Heartbeat - Zero-Crossing
if (last_diff > 0 && current_diff < 0) {
crossed = true;
crossed_time = millis();
}
if (current_diff > 0) {
crossed = false;
}
// Detect Heartbeat - Falling Edge Threshold
if (crossed && current_diff < kEdgeThreshold) {
if (last_heartbeat != 0 && crossed_time - last_heartbeat > 300) {
// Show Results
int bpm = 60000 / (crossed_time - last_heartbeat);
float rred = (stat_red.maximum() - stat_red.minimum()) / stat_red.average();
float rir = (stat_ir.maximum() - stat_ir.minimum()) / stat_ir.average();
float r = rred / rir;
float spo2 = kSpO2_A * r * r + kSpO2_B * r + kSpO2_C;
if (bpm > 50 && bpm < 250) {
// Average?
if (kEnableAveraging) {
int average_bpm = averager_bpm.process(bpm);
int average_r = averager_r.process(r);
int average_spo2 = averager_spo2.process(spo2);
// Show if enough samples have been collected
if (averager_bpm.count() >= kSampleThreshold) {
Serial.print("Time (ms): ");
Serial.println(millis());
Serial.print("246! Heart Rate (avg, bpm): ");
Serial.println(average_bpm);
Serial.print("248! R-Value (avg): ");
Serial.println(average_r);
Serial.print("250! SpO2 (avg, %): ");
Serial.println(average_spo2);
displayMeasuredValues(false, average_bpm, average_spo2);
}
}
else {
Serial.print("Time (ms): ");
Serial.println(millis());
Serial.print("258! Heart Rate (current, bpm): ");
Serial.println(bpm);
Serial.print("R-Value (current): ");
Serial.println(r);
Serial.print("262! SpO2 (current, %): ");
Serial.println(spo2);
displayMeasuredValues(false, bpm, spo2);
}
}
// Reset statistic
stat_red.reset();
stat_ir.reset();
}
crossed = false;
last_heartbeat = crossed_time;
}
}
last_diff = current_diff;
} else {
displayMeasuredValues(true, 0, 0);
}
}
void initDrawScreen(void) {
display.clearDisplay();
display.setTextSize(1); // Normal 1:1 pixel scale
display.setTextColor(WHITE); // Draw white text
display.setCursor(0, 0); // Start at top-left corner
display.println(F(" Taste The Code"));
display.println(F(""));
display.setCursor(5, display.getCursorY());
display.setTextSize(2);
display.println(F("BPM %SpO2"));
display.display();
}
bool display_reset = true;
void displayMeasuredValues(bool no_finger, int32_t beatAvg, int32_t spo2) {
display.setCursor(5, 35);
display.setTextColor(WHITE, BLACK);
if (no_finger) {
display.setTextSize(2);
display.println(F("NO Finger "));
display_reset = true;
//Serial.print("299 beatAvg =");
Serial.print("309NO Finger???"); //shown on OLED, and output;
display.display();
} else if (beatAvg < 30 && display_reset) {
display.setTextSize(2);
display.println(F("Pls. Wait "));
display_reset = false;
display.display();
Serial.print("318! beatAvg ="); /// 318 (shown on OLED, and output;) beatAvg < 30???165 current_value_red =869889.00
Serial.println(beatAvg);
} else if (beatAvg >= 30) { /// never happen?
Serial.print("323! beatAvg =");
Serial.println(beatAvg);
display.setTextSize(2);
display.println(F(" "));
//.............
display.setCursor(5, 35);
display.setTextSize(1);
display.print("beatAvg=");
//..................
display.setCursor(5, 45); //5,35
display.setTextSize(2);
display.print(beatAvg);
Serial.print("340 beatAvg ="); //shown on OLED, doesn't output, whp?
Serial.print(beatAvg);
display.print(F(" "));
if (spo2 >= 20 && spo2 <= 100) {
display.print(spo2);
Serial.print("347! spo2 ="); //shown on OLED, doesn't output, whp?
Serial.print(spo2);
} else {
display.print(F("--"));
Serial.print("353! spo2 ="); //shown on OLED, doesn't output, whp?
Serial.print(spo2);
}
display.println(F(" "));
display.display();
}
}
filters.h:
///filters.h
#ifndef FILTERS_H
#define FILTERS_H
/**
* @brief Statistic block for min/nax/avg
*/
class MinMaxAvgStatistic {
float min_;
float max_;
float sum_;
int count_;
public:
/**
* @brief Initialize the Statistic block
*/
MinMaxAvgStatistic() :
min_(NAN), /// nan-Not-A-Number-Returns a quiet NaN (Not-A-Number) value of type double.
max_(NAN),
sum_(0),
count_(0){}
/**
* @brief Add value to the statistic
*/
void process(float value) {
min_ = isnan(min_) ? value : min(min_, value); // isnan-Is Not-A-Number-Returns whether x is a NaN (Not-A-Number) value.
max_ = isnan(max_) ? value : max(max_, value);
sum_ += value;
count_++;
}
/**
* @brief Resets the stored values
*/
void reset() {
min_ = NAN;
max_ = NAN;
sum_ = 0;
count_ = 0;
}
/**
* @brief Get Minimum
* @return Minimum Value
*/
float minimum() const {
return min_;
}
/**
* @brief Get Maximum
* @return Maximum Value
*/
float maximum() const {
return max_;
}
/**
* @brief Get Average
* @return Average Value
*/
float average() const {
return sum_/count_;
}
};
/**
* @brief High Pass Filter
*/
class HighPassFilter {
const float kX;
const float kA0;
const float kA1;
const float kB1;
float last_filter_value_;
float last_raw_value_;
public:
/**
* @brief Initialize the High Pass Filter
* @param samples Number of samples until decay to 36.8 %
* @remark Sample number is an RC time-constant equivalent
*/
HighPassFilter(float samples) :
kX(exp(-1/samples)),
kA0((1+kX)/2),
kA1(-kA0),
kB1(kX),
last_filter_value_(NAN),
last_raw_value_(NAN){}
/**
* @brief Initialize the High Pass Filter
* @param cutoff Cutoff frequency
* @pram sampling_frequency Sampling frequency
*/
HighPassFilter(float cutoff, float sampling_frequency) :
HighPassFilter(sampling_frequency/(cutoff*2*PI)){}
/**
* @brief Applies the high pass filter
*/
float process(float value) {
if(isnan(last_filter_value_) || isnan(last_raw_value_)) {
last_filter_value_ = 0.0;
}
else {
last_filter_value_ =
kA0 * value
+ kA1 * last_raw_value_
+ kB1 * last_filter_value_;
}
last_raw_value_ = value;
return last_filter_value_;
}
/**
* @brief Resets the stored values
*/
void reset() {
last_raw_value_ = NAN;
last_filter_value_ = NAN;
}
};
/**
* @brief Low Pass Filter
*/
class LowPassFilter {
const float kX;
const float kA0;
const float kB1;
float last_value_;
public:
/**
* @brief Initialize the Low Pass Filter
* @param samples Number of samples until decay to 36.8 %
* @remark Sample number is an RC time-constant equivalent
*/
LowPassFilter(float samples) :
kX(exp(-1/samples)),
kA0(1-kX),
kB1(kX),
last_value_(NAN){}
/**
* @brief Initialize the Low Pass Filter
* @param cutoff Cutoff frequency
* @pram sampling_frequency Sampling frequency
*/
LowPassFilter(float cutoff, float sampling_frequency) :
LowPassFilter(sampling_frequency/(cutoff*2*PI)){}
/**
* @brief Applies the low pass filter
*/
float process(float value) {
if(isnan(last_value_)) {
last_value_ = value;
}
else {
last_value_ = kA0 * value + kB1 * last_value_;
}
return last_value_;
}
/**
* @brief Resets the stored values
*/
void reset() {
last_value_ = NAN;
}
};
/**
* @brief Differentiator
*/
class Differentiator {
const float kSamplingFrequency;
float last_value_;
public:
/**
* @brief Initializes the differentiator
*/
Differentiator(float sampling_frequency) :
kSamplingFrequency(sampling_frequency),
last_value_(NAN){}
/**
* @brief Applies the differentiator
*/
float process(float value) {
float diff = (value-last_value_)*kSamplingFrequency;
last_value_ = value;
return diff;
}
/**
* @brief Resets the stored values
*/
void reset() {
last_value_ = NAN;
}
};
/**
* @brief MovingAverageFilter
* @tparam buffer_size Number of samples to average over
*/
template<int kBufferSize> class MovingAverageFilter {
int index_;
int count_;
float values_[kBufferSize];
public:
/**
* @brief Initalize moving average filter
*/
MovingAverageFilter() :
index_(0),
count_(0){}
/**
* @brief Applies the moving average filter
*/
float process(float value) {
// Add value
values_[index_] = value;
// Increase index and count
index_ = (index_ + 1) % kBufferSize;
if(count_ < kBufferSize) {
count_++;
}
// Calculate sum
float sum = 0.0;
for(int i = 0; i < count_; i++) {
sum += values_[i];
}
// Calculate average
return sum/count_;
}
/**
* @brief Resets the stored values
*/
void reset() {
index_ = 0;
count_ = 0;
}
/**
* @brief Get number of samples
* @return Number of stored samples
*/
int count() const {
return count_;
}
};
#endif // FILTERS_H