Hi all.
tested MAX30105 with T-Display S3 works well with little code modification:
//if (sensor.begin() && sensor.setSamplingRate(kSamplingRate)) {
// Serial.println("Sensor initialized");
//}
else {
Serial.println("Sensor not found");
while (1);
}
as:
sensor.begin() && sensor.setSamplingRate(kSamplingRate);
and added:
Wire.begin(43, 44);
inside setup.
code from: Oximeter Monitor with OLED screen and MAX30105 sensor on NodeMCU
Question is it doesn't work when removed OLED, and modified the code relatively
(Phenomenon is the S3 shown the first screen and stuck at "no_finger", actually the MAX30105 didn't on ):
void initDrawScreen(void) {
// display.clearDisplay();
tft.fillScreen(TFT_BLACK);
tft.setTextSize(1); // Normal 1:1 pixel scale
tft.setTextColor(TFT_WHITE); // Draw white text
tft.setCursor(0, 0); // Start at top-left corner
tft.println(F(" Taste The Code"));
tft.println(F(""));
tft.setCursor(5, tft.getCursorY());
tft.setTextSize(2);
tft.println(F("BPM %SpO2"));
// display.display();
}
bool display_reset = true;
void displayMeasuredValues(bool no_finger, int32_t beatAvg, int32_t spo2) {
tft.setCursor(5, 35);
tft.setTextColor(TFT_WHITE);
if (no_finger) {
tft.setTextSize(2);
tft.println(F("NO Finger "));
display_reset = true;
// display.display();
} else if (beatAvg < 30 && display_reset) {
tft.setTextSize(2);
tft.println(F("Pls. Wait "));
display_reset = false;
// display.display();
} else if (beatAvg >= 30) {
tft.setTextSize(2);
tft.println(F(" "));
tft.setCursor(5, 35);
tft.setTextSize(3);
tft.print(beatAvg);
tft.print(F(" "));
if (spo2 >= 20 && spo2 <= 100) {
tft.print(spo2);
} else {
tft.print(F("--"));
}
tft.println(F(" "));
// display.display();
}
}
the code for OLED:
#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
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
while (1);
}
sensor.begin() && 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);
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;
// Detect Finger using raw sensor value
if (sample.red > kFingerThreshold) {
if (millis() - finger_timestamp > kFingerCooldownMs) {
finger_detected = true;
}
}
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();
}
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);
// 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)) {
// 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("Heart Rate (avg, bpm): ");
Serial.println(average_bpm);
Serial.print("R-Value (avg): ");
Serial.println(average_r);
Serial.print("SpO2 (avg, %): ");
Serial.println(average_spo2);
displayMeasuredValues(false, average_bpm, average_spo2);
}
}
else {
Serial.print("Time (ms): ");
Serial.println(millis());
Serial.print("Heart Rate (current, bpm): ");
Serial.println(bpm);
Serial.print("R-Value (current): ");
Serial.println(r);
Serial.print("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;
display.display();
} else if (beatAvg < 30 && display_reset) {
display.setTextSize(2);
display.println(F("Pls. Wait "));
display_reset = false;
display.display();
} else if (beatAvg >= 30) {
display.setTextSize(2);
display.println(F(" "));
display.setCursor(5, 35);
display.setTextSize(3);
display.print(beatAvg);
display.print(F(" "));
if (spo2 >= 20 && spo2 <= 100) {
display.print(spo2);
} else {
display.print(F("--"));
}
display.println(F(" "));
display.display();
}
}
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),
max_(NAN),
sum_(0),
count_(0){}
/**
* @brief Add value to the statistic
*/
void process(float value) {
min_ = isnan(min_) ? value : min(min_, 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
output ERROR:
ESP32_MAX30102_OLED_gd_S3_2!