Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for oximeter MAX30100 #2623

Open
JavierAder opened this issue Oct 13, 2024 · 8 comments
Open

Support for oximeter MAX30100 #2623

JavierAder opened this issue Oct 13, 2024 · 8 comments
Labels
enhancement New feature or request

Comments

@JavierAder
Copy link

Description

I've been studying the possibility of giving support to the MAX30100 sensor; from what I've seen it should be very similar to the support given for DHT sensors; instead of temperature and humidity, oxygen percentage and pulses per minute should be reported (I wrote some code, I'll upload it later). My biggest doubt is which library to use. Of those I saw, the most suitable one seemed to me to be this one https://registry.platformio.org/libraries/oxullo/MAX30100lib
My doubt is if it can be used as it is under Espurna; this library directly uses Wire and I don't know if it generates any port problems or something similar (from what I've seen, Espurna doesn't use Wire for i2c connections).
Could this library be used directly? Or, would I have to use another one?
Thanks

Solution

No response

Alternatives

No response

Additional context

No response

@JavierAder JavierAder added the enhancement New feature or request label Oct 13, 2024
@mcspr
Copy link
Collaborator

mcspr commented Oct 14, 2024

Internally, I2C .h wraps Wire, since it is the only available I2C API provided by the Core.

I don't see any issues in c/p directly to the sensor class omitting the lib itself and extra code it introduces for sketch use-cases. No need to set up Wire, writers and readers are already implemented, big-endian / little-endian code as well. Don't forget the (c) info, obviously.

@JavierAder
Copy link
Author

Hello. Let's see if I understood your approach:

  • not using the Oxullo library directly, but a modified version of it (I suppose it would have to be put under code/espurna/libs)
  • the modified version that does not use Wire, instead using I2C.h
    Is this correct?
    If so, I suppose that you only have to modify MAX30100.cpp and implement the following functionalities using I2C.h:
    Wire.begin();
    Wire.setClock(
    Wire.beginTransmission(MAX30100_I2C_ADDRESS);
    Wire.write(address);
    Wire.endTransmission(false);
    Wire.requestFrom(MAX30100_I2C_ADDRESS, 1);
    Wire.read();
    Wire.available())

I don't think Wire is used anywhere else.

@mcspr
Copy link
Collaborator

mcspr commented Oct 17, 2024

not using the Oxullo library directly, but a modified version of it (I suppose it would have to be put under code/espurna/libs)

see lib_deps = ... in platformio .ini. so far I have not implemented any clever code / lib / dep detection, so once added any code can magically use this lib headers

the modified version that does not use Wire, instead using I2C.h

removing begin() and setClock() might be enough, unless you also declare i2c .h funcs manually
(since include paths from lib to project is ok, project to lib is not)
edit: ...since either way, everything goes through Wire. Lib just loses our abstraction on top of it, everything still should work just fine when reading / writing.

I would still suggest to take a look at a possibility of extracting some code into a single sensor class file, removing the lib dependency. Might be easier to debug in the future.

@JavierAder
Copy link
Author

I would still suggest to take a look at a possibility of extracting some code into a single sensor class file, removing the lib dependency. Might be easier to debug in the future.

Yes, it is probably better; by the way, it seems that the algorithms to calculate sp02 are not so trivial and there are variants (for example, this one seems to be quite a bit more complex than Oxullo's https://github.com/sparkfun/SparkFun_MAX3010x_Sensor_Library/tree/master/src ); a perzonalizaded version should be easier to maintain.
As soon as I have a bit more time I'll try something like this.

@mcspr
Copy link
Collaborator

mcspr commented Oct 18, 2024

https://github.com/sparkfun/SparkFun_MAX3010x_Sensor_Library/blob/72d5308df500ae1a64cc9d63e950c68c96dc78d5/examples/Example8_SPO2/Example8_SPO2.ino#L86 also mentions that it takes some time to gather all of the results provide a meaningful output
Another question, how do sensor filters fit here since we (sort-of) expect to aggregate them at an upper level to median / average / etc. them. Unless sp02 calc becomes a filter here, shared between these two magnitude readers

@JavierAder
Copy link
Author

https://github.com/sparkfun/SparkFun_MAX3010x_Sensor_Library/blob/72d5308df500ae1a64cc9d63e950c68c96dc78d5/examples/Example8_SPO2/Example8_SPO2.ino#L86 also mentions that it takes some time to gather all of the results provide a meaningful output

The first 100 samples are for the algorithm to work properly (it needs to remove the DC component from the signal among other things); then it recalculates the Spo2 every 25 samples. The Sparkfun example is blocking
while (particleSensor.available() == false)
although I don't see it necessary to do it this way; you should simply update the data buffer progressively and only recalculate the Spo2 every certain amount of new samples. Oxullo does it differently; every time the sensor is accessed, all the samples that it has stored in its internal buffer are read (this is not blocking) and it always recalculates Spo2.
https://github.com/oxullo/Arduino-MAX30100/blob/5754dfe2cfd882b91d5db748d78c4a6ad4cab0d1/src/MAX30100.cpp#L134

Another question, how do sensor filters fit here since we (sort-of) expect to aggregate them at an upper level to median / average / etc. them. Unless sp02 calc becomes a filter here, shared between these two magnitude readers

I don't see the problem with calculating the average, median, etc., in a standard way.

@JavierAder
Copy link
Author

Well, for the moment, I made the sensor code directly using the Sparkfun library, I included it in sensor.cpp and the build at least worked (I don't know if it is using a lot of memory; platformIO showed me

Checking size .pio\build\nodemcu-lolin\firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [===== ] 46.7% (used 38248 bytes from 81920 bytes)
Flash: [====== ] 58.9% (used 615093 bytes from 1044464 bytes)

I guess the next step should be to migrate the Sparkfun library code into MAX30100Sensor.h
MAX30100Sensor.h


#include <Wire.h>
#include "BaseSensor.h"
#include "SparkFun_MAX3010x/MAX30105.h"
#include "SparkFun_MAX3010x/spo2_algorithm.h"

class MAX30100Sensor : public BaseSensor
{

    // ---------------------------------------------------------------------
    // Sensor API
    // ---------------------------------------------------------------------

    unsigned char id() const override
    {
        return SENSOR_MAX30100_ID;
    }

    unsigned char count() const override
    {
        return 2;
    }

    // Type for slot # index
    unsigned char type(unsigned char index) const override
    {
        if (index == 0)
            return MAGNITUDE_HEART_RATE;
        if (index == 1)
            return MAGNITUDE_SPO2;
        return MAGNITUDE_NONE;
    }

    // Current value for slot # index
    double value(unsigned char index) override
    {
        if (index == 0)
            return _rate;
        if (index == 1)
            return _spO2;
        return 0;
    }

    // Initialization method, must be idempotent
    void begin() override
    {
        // TODO: here?
        // Based Sparkfun SPO2 example (setup())
        // Initialize sensor
        if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) // Use default I2C port, 400kHz speed
        {
            _error = SENSOR_ERROR_I2C;
            return;
        }

        //TODO: get that data from runtime config

        byte ledBrightness = 60; // Options: 0=Off to 255=50mA
        byte sampleAverage = 4;  // Options: 1, 2, 4, 8, 16, 32
        byte ledMode = 2;        // Options: 1 = Red only, 2 = Red + IR, 3 = Red + IR + Green
        byte sampleRate = 100;   // Options: 50, 100, 200, 400, 800, 1000, 1600, 3200
        int pulseWidth = 411;    // Options: 69, 118, 215, 411
        int adcRange = 4096;     // Options: 2048, 4096, 8192, 16384

        particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange); // Configure sensor with these settings
    }

    //TODO: pre or tick?
    // Pre-read hook (usually to populate registers with up-to-date data)
    void pre() override {
 
        uint8_t samples = particleSensor.available();
        if (samples == false)
        { 
            particleSensor.check(); //Check the sensor for new data
            return;
        }

        //TODO if (samples <25 ) return?

        if (samples > bufferLength)
            samples = bufferLength;
        
        //Make room in the end of buffers
        uint8_t samplesToMove = bufferLength - samples;
        for (byte i = 0; i<samplesToMove; i++)
        {
            redBuffer[i] = redBuffer[i+samples];
            irBuffer[i] = irBuffer[i+samples];
        }
        //update buffers with samples
        for (byte i = bufferLength-samples; i<bufferLength;i++)
        {
            redBuffer[i] = particleSensor.getRed();
            irBuffer[i] = particleSensor.getIR();
            particleSensor.nextSample(); //We're finished with this sample so move to next sample


        }

        int32_t spo2; //SPO2 value
        int8_t validSPO2; //indicator to show if the SPO2 calculation is valid
        int32_t heartRate; //heart rate value
        int8_t validHeartRate; //indicator to show if the heart rate calculation is valid
        //
        //calculate spo2 and heart rate
        maxim_heart_rate_and_oxygen_saturation(this->irBuffer, bufferLength, this->redBuffer, &spo2,
         &validSPO2, &heartRate, &validHeartRate);
        if (validSPO2)
            _spO2 = spo2;
        if (validHeartRate)
            _rate = heartRate; 


    }

protected:
    int32_t _rate;
    int32_t _spO2;

    // Based Sparkfun SPO2 example
    MAX30105 particleSensor;
    // Arduino Uno doesn't have enough SRAM to store 100 samples of IR led data and red led data in 32-bit format
    // To solve this problem, 16-bit MSB of the sampled data will be truncated. Samples become 16-bit data.
    // TODO: 8266 has memory problem?
    uint32_t irBuffer[100];  // infrared LED sensor data
    uint32_t redBuffer[100]; // red LED sensor data

    int32_t bufferLength = 100; // data length
    byte i = 0;
};

@mcspr
Copy link
Collaborator

mcspr commented Oct 22, 2024

I don't see the problem with calculating the average, median, etc., in a standard way.

Hm... I thought it would make some sense to re-use 'filtered' data storage to attempt to somewhat reduce number of buffers, but I have missed the part about the internal storage and the number of samples required.

//TODO: pre or tick?

pre() usually converts raw to actual values. tick() usually gathers the raw data, in this case it would read from the i2c (either all at once or split in batches). value() should preferably output already converted data
pre() is called before value() and both are on the same timer
tick() runs without timer constraints, just whenever loop() is scheduled by the system

// Arduino Uno doesn't have enough SRAM to store 100 samples of IR led data and red led data in 32-bit format
// To solve this problem, 16-bit MSB of the sampled data will be truncated. Samples become 16-bit data.
// TODO: 8266 has memory problem?

u32 is natively handled, no problem here. just watch out when allocating sensor class on stack and prefer heap (400bytes times two per instance)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants