Now that we can acquire data with MAX3010x modules, it's time to apply our heart rate measurement algorithm to it.
In the last part of this tutorial, we connected a MAX3010x module to the Arduino and learned how to acquire data with it. Even if one can already see the heart beats, when showing the data in the serial plotter, it would be better if we could directly print out the heart rate. For that we are going to adapt the algorithm we used while experimenting with the simple heart rate modules.
What is needed to use the code with the MAX3010x module? Well, not much. In theory, we just need to adjust the part, where we acquire the data from the sensor.
Instead of analogRead
we now call the readSample
method of the MAX3010x sensor and initialize it in the setup
procedure.
The adjusted code looks like this:
#include <MAX3010x.h>
// Sensor (adjust to your sensor type)
MAX30100 sensor;
// Constants
const float rThreshold = 0.7;
const float decayRate = 0.02;
const float thrRate = 0.05;
const int minDiff = 50;
// Current values
float maxValue = 0;
float minValue = 0;
float threshold = 0;
// Timestamp of the last heartbeat
long lastHeartbeat = 0;
// Last value to detect crossing the threshold
float lastValue = 0;
void setup() {
Serial.begin(9600);
if(sensor.begin()) {
Serial.println("Sensor initialized");
}
else {
Serial.println("Sensor not found");
while(1);
}
}
void loop() {
auto sample = sensor.readSample(1000);
float currentValue = sample.red;
maxValue = max(maxValue, currentValue);
minValue = min(minValue, currentValue);
// Detect Heartbeat
float nthreshold = (maxValue - minValue) * rThreshold + minValue;
threshold = threshold * (1-thrRate) + nthreshold * thrRate;
threshold = min(maxValue, max(minValue, threshold));
if(currentValue >= threshold
&& lastValue < threshold
&& (maxValue-minValue) > minDiff
&& millis() - lastHeartbeat > 300) {
if(lastHeartbeat != 0) {
// Show Results
int bpm = 60000/(millis() - lastHeartbeat);
if(bpm > 50 && bpm < 250) {
Serial.print("Heart Rate (bpm): ");
Serial.println(bpm);
}
}
lastHeartbeat = millis();
}
// Decay for max/min
maxValue -= (maxValue-currentValue)*decayRate;
minValue += (currentValue-minValue)*decayRate;
lastValue = currentValue;
}
If you compare the new code with the old one, you can see, that some constants have been adjusted as well. As the value range is a lot bigger for the MAX3010x sensor, I increased the minimum difference needed to detect a heart beat from 5
to 50
. When using the MAX30101, MAX30102 or MAX30105 an even higher value might be needed to prevent random sensor noise from getting detected as heart beats. The reason is that these three sensors feature a higher resolution then the MAX30100 and thus the value range is even bigger. A change of 50
indicates only a very small change for them.
Additionally, the initial values for minValue
, maxValue
and threshold
have been changed to 0
. In the old code the minimum value was set to the highest possible value and the maximum to the lowest, so that they would immediately be replaced by the first measured value. The threshold was previously set to the mid of the expected value range. For the MAX3010x modules the value range differs depending on the sensor type. As said before, it is much smaller for the MAX30100. This means it is no longer possible to work with that tactic and neither does it make sense to do so. Due to the ambient light cancellation in the MAX3010x sensors, the measurement value without a finger on the sensor is close to zero. In consequence, setting the initial values to zero is a more sensible default for the MAX3010x sensors.
Does the code work? Yes, it does. In the graph below you can see some test data recorded with a MAX30100. You can see, that the heart beats are successfully detected by the algorithm.
However, there is an issue that this graph doesn't show - it takes quite some time until the first heart rate measurements are shown in the serial monitor. You will have to wait for around 20 seconds after putting a finger on the sensor. Why does it take that long?
The graph below shows the recorded data right after the finger is put on to the sensor. It makes the issue visible: minimum and maximum are not adjusted fast enough. Especially the maximum does not decrease fast enough. The interesting part is, that it is only an issue in the beginning, because the DC offset of the sensors signal decreases continuously. One reason for it is, that it is impossible to keep touching the sensor with exactly the same pressure. However, another issue is that the sensor heats up when you put your finger on it. This causes the measurements to change as for example the LED wavelength varies slightly with temperature.
Can we fix this issue? Well, we can try to by increasing the decay and threshold rate. A higher decay rate causes a faster adjustment of maximum and minimum and an earlier heart rate detection. However, if the values a too high, this causes issues with the heart beat detection as well, as the values now decays too fast in between the individual heart beats.
We'll, leave this issue aside for now. There are other issues that are a bit easier to fix. Namely, the one that the code does not prevent the detection of heart beats in the data even if no finger is on the sensor. Due to the ambient light cancellation it is now much easier for us to check for that. Thus, let's do it and adjust the code once more.
How can we detect that a finger is on the sensor? Well, we can't really tell if it is really a finger, however, it is easy to detect if something is in front the sensor using a simple threshold.
If nothing is there the value is close to zero. If a finger is on the sensor the value is well above 10000
for the MAX30100. For the MAX30101, MAX30102 and MAX30105 you can use a much higher value, however, a threshold of 10000
does work for them, as well.
Let's think about what we need to do if no finger is on the sensor. Obviously, we don't want to execute the heart rate detection algorithm in this case. Additionally, we should reset the stored values. The next time we put the finger on the sensor, it is likely positioned a bit different, and we would need to determine a fresh minimum, maximum and threshold anyway.
Here is how the code looks like with all that being implemented:
#include <MAX3010x.h>
// Sensor (adjust to your sensor type)
MAX30100 sensor;
// Constants
const float rThreshold = 0.7;
const float decayRate = 0.02;
const float thrRate = 0.05;
const int minDiff = 50;
// Current values
float maxValue = NAN;
float minValue = NAN;
float threshold = NAN;
// Timestamp of the last heartbeat
long lastHeartbeat = 0;
// Last value to detect crossing the threshold
float lastValue = NAN;
void setup() {
Serial.begin(9600);
if(sensor.begin()) {
Serial.println("Sensor initialized");
}
else {
Serial.println("Sensor not found");
while(1);
}
}
void loop() {
auto sample = sensor.readSample(1000);
float currentValue = sample.red;
if(currentValue > 10000) {
maxValue = max(maxValue, currentValue);
minValue = min(minValue, currentValue);
// Detect Heartbeat
float nthreshold = (maxValue - minValue) * rThreshold + minValue;
if(isnan(threshold)) {
threshold = nthreshold;
}
else {
threshold = threshold * (1-thrRate) + nthreshold * thrRate;
threshold = min(maxValue, max(minValue, threshold));
}
if(currentValue >= threshold
&& lastValue < threshold
&& (maxValue-minValue) > minDiff
&& millis() - lastHeartbeat > 300) {
if(lastHeartbeat != 0) {
// Show Results
int bpm = 60000/(millis() - lastHeartbeat);
if(bpm > 50 && bpm < 250) {
Serial.print("Heart Rate (bpm): ");
Serial.println(bpm);
}
}
lastHeartbeat = millis();
}
// Decay for max/min
maxValue -= (maxValue-currentValue)*decayRate;
minValue += (currentValue-minValue)*decayRate;
lastValue = currentValue;
}
else {
// No finger -> Reset Values
lastHeartbeat = 0;
lastValue = NAN;
maxValue = NAN;
minValue = NAN;
threshold = NAN;
}
}
In the code the value NAN
, which stands for 'not a number', is used to indicate, that no valid value has been set. The algorithm then uses the first valid data to initialize the variables again.
During normal operation with a finger on the sensor, the changes have no effect as you can see in the graph below. However, if the measured value falls below the threshold the heart rate detection is suspended. You can try this out by increasing the threshold for the graph below.
Let's have a look at the result:
To be honest, the result is kind of disappointing. Yes, we can now reliably detect if no finger is on the sensor. However, the heart beat detection is unstable and whether it works depends highly on the positioning of and the pressure on the finger. If it works, then we have a high variability between individual measurements. Which is expected as discussed in part 3 of the series on simple heart rate modules. However, not all the variation is caused by the natural heart rate variability. What is the problem?
Well, the first issue is that the sampling rate is too low. This is an issue we also had with simpler heart rate modules. The easy solution for this is to increase the sampling rate. After the adjustment the steps between the individual values should become less apparent.
However, there is also the more fundamental issue: the signal recorded with the MAX3010x modules has a different shape, due to using a red LED instead of a green one. We don't have a clear peak to trigger on anymore. When looking at the signal shape it seems much more reasonable to trigger on the falling edge after each heart beat. Additionally, we still have the issue with the shifting DC offset right after putting the finger on the sensor. Both issues cause the heart beat detection to be unreliable. In the next part of this tutorial, we are going to solve both issues by using a different more scientific heart rate measurement method.