xoseperez / espurna

Home automation firmware for ESP8266-based devices
http://tinkerman.cat
GNU General Public License v3.0
3k stars 638 forks source link

Support for oximeter MAX30100 #2623

Open JavierAder opened 1 week ago

JavierAder commented 1 week ago

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

mcspr commented 1 week ago

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 commented 6 days ago

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

I don't think Wire is used anywhere else.

mcspr commented 4 days ago

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 commented 4 days ago

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 commented 3 days ago

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 commented 2 days ago

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 commented 1 day ago

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;
};