xoseperez / espurna

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

Add Sensor CCS811 support (I2C VOC/eCO2 sensor) #2345

Closed FLXMM closed 3 years ago

FLXMM commented 4 years ago

Hi all,

since I dont know how to compile espurna with external sensor libraries: is it possible to integrate the Sensor CCS811? Its an enviromental sensor measuring TVOCs. Air quality.

https://github.com/sciosense/CCS811_driver https://github.com/adafruit/Adafruit_CCS811 https://github.com/sparkfun/SparkFun_CCS811_Arduino_Library

or can someone point me in the right direction of a tutorial? how i can integrate it my self? I couldnt find anything. Thank you

mcspr commented 3 years ago

Take a look at https://github.com/xoseperez/espurna/commit/b500273029d1c8e8bb14d88751960c7f76900268#diff-55ad6784b09d44d0f6e35b8d8914794a (https://github.com/xoseperez/espurna/pull/2216)

You can follow the existing library example and integrate them into the reading loop. Sensor class announces what values it holds via _count member var and type(index), return the value types you want under the specific index. After that value(index) will be used to read them

FLXMM commented 3 years ago

Thanks for the feedback. But tbh my expertice in coding is sooo little the looking at your answer and investigating the files I still dont know what to do. My "coding skills" consist of copy and pasting things to customize what i want :P

mcspr commented 3 years ago

This is nearly what I proposed :)

Example code lists 2 possible readings: https://github.com/xoseperez/espurna/blob/f6ec2422a49f303658cd2d96fb6c3d14cab861a1/code/espurna/config/types.h#L382 https://github.com/xoseperez/espurna/blob/f6ec2422a49f303658cd2d96fb6c3d14cab861a1/code/espurna/config/types.h#L364

For example, we will use Adafruit lib, so we would need to copy example's behaviour: https://github.com/adafruit/Adafruit_CCS811/blob/master/examples/CCS811_test/CCS811_test.ino

pre() routine would adapt to what example loop() does:

void pre() override {
    _error = SENSOR_ERROR_WARM_UP;
    if (!_ccs.available()) return;
    if (!_ccs.readData()) return;
    _error = SENSOR_ERROR_OK;
    _eco2 = _ccs.geteCO2();
    _tvoc = _ccs.getTVOC();
}

Reading will use cached values:

double value(unsigned char index) override {
    switch (index) {
    case 0: return _eco2;
    case 1: return _tvoc;
    default: return 0.0;
    }
}

As I mentioned earlier, we need specific indexes mapped correctly so the above works:

unsigned char type(unsigned char index) override {
    switch (index) {
    case 0: return MAGNITUDE_CO2;
    case 1: return MAGNITUDE_VOC;
    default: return MAGNITUDE_NONE;
    }
}

Implementation minutiae is that we also need _count to be correctly set right when class is constructed. For example, we created sensors/CCS811Sensor.h and have these:

#include "BaseSensor.h"

#include <Adafruit_CCS811.h>

class CCS811Sensor : public BaseSensor {
public:

CCS811Sensor() {
    _count = 2;
}

void begin() override {
    _ccs.begin();
    _ready = true;
}

// the rest of the sensor which we implemented above

private:   

Adafruit_CCS811 _ccs;
uint16_t _eco2 { 0u );
uint16_t _tvoc { 0u };

};

At the end, we include this sensor header in the sensor.cpp and add _sensors.push_back(new CCS811Sensor()); and the end of the sensor loading routine (just search for _sensors.push_back(). We don't need any complex setup, so we just create the object and that's it.

(I have not tested if this works though, but I am sure mistakes are easy to spot after trying to build this)

FLXMM commented 3 years ago

first of all thank you very much @mcspr . for the more detailed description. It made me look deeper into stuff like BaseSensor etc. But unfortanetly it didnt solve my issue. Everything compiles fine. But i dont see any sensor values in the webinterface and no info in the debug log of the webinterface.

So I have no clue where to start or identifying what to change.

I did the following: 1st I added : https://github.com/adafruit/Adafruit_CCS811 to the platform.ini and

in sensor.cpp

    _sensors.push_back(sensor);
    _sensors.push_back(new CCS811Sensor());
}
#endif`

and I created this file sensors/CCS811Sensor.h as mentioned in your description:

#include "BaseSensor.h"
#include <Adafruit_CCS811.h>

class CCS811Sensor : public BaseSensor {
public:

CCS811Sensor() {
    _count = 2;
}

void begin() override {
    _ccs.begin();
    _ready = true;
}
unsigned char type(unsigned char index) override {
    switch (index) {
    case 0: return MAGNITUDE_CO2;
    case 1: return MAGNITUDE_VOC;
    default: return MAGNITUDE_NONE;
    }
}
void pre() override {
    _error = SENSOR_ERROR_WARM_UP;
    if (!_ccs.available()) return;
    if (!_ccs.readData()) return;
    _error = SENSOR_ERROR_OK;
    _eco2 = _ccs.geteCO2();
    _tvoc = _ccs.getTVOC();
}

double value(unsigned char index) override {
    switch (index) {
    case 0: return _eco2;
    case 1: return _tvoc;
    default: return 0.0;
    }
}

// the rest of the sensor which we implemented above

private:   

Adafruit_CCS811 _ccs;
uint16_t _eco2 { 0u };
uint16_t _tvoc { 0u };

};

does this make at all sense?? Or am I missing something very important? You are using BaseSensor can it be problem that the CCS811 is a I2CSensor ? I am not sure if this important or not but I want to run 2 I2C sensors (different address) on an Sonoff S20 (itead 20)

mcspr commented 3 years ago

Note of #if SOMETHING ... #endif blocks, it seems you included the push_back(...) code inside of PZEM004V3_SUPPORT block which is not enabled, so while it builds it never gets to include the CCS811Sensor code. So it is something like:

diff --git a/code/espurna/sensor.cpp b/code/espurna/sensor.cpp
index 7162cf5e..7201e768 100644
--- a/code/espurna/sensor.cpp
+++ b/code/espurna/sensor.cpp
@@ -2190,6 +2192,8 @@ void _sensorLoad() {
     }
     #endif

+    _sensors.push_back(new CCS811Sensor());
+
 }

 String _magnitudeTopicIndex(const sensor_magnitude_t& magnitude) {

I missed some required override methods btw (what compiler will describe as pure virtual functions that are missing in the CCS811Sensor class):

String description() override {
    return String(F("CCS811"));
}
String description(unsigned char) override {
    return description();
}
String address(unsigned char) override {
    return String(F("0x5a")); // TODO: this does not do anything useful atm
}

Perhaps, it would be also nice to do

void begin() override {
    _ready = _ccs.begin();
}

to notify about any initial issues with the connection and to re-do the .begin() call until it is successful.

I2CSensor is indented for the things we implement ourselves (like BMX280, for example) and to ensure we don't have conflicting addresses with multiple sensors, but Adafruit lib already manages the i2c connection after _ccs.begin() and we don't need this atm.

Another thing I have not mentioned before... since this is an ad-hoc sensor class, we also need to manually specify -DSENSOR_SUPPORT=1 so the sensor.cpp actually builds. e.g. env ESPURNA_FLAGS='-DNODEMCU_LOLIN -DSENSOR_SUPPORT=1' pio run -e esp8266-4m-base

FLXMM commented 3 years ago

@mcspr THX Max.. this helps a lot ..now it seems there is a connection :) I am getting [012016] [SENSOR] Error reading data from CCS811 (error: 2) Displaying warmup in the webinterface which as far as I see is from the sensor.cpp. (I abortet after 30minutes)

But from what i found out error 2 refers to:

2 MEASMODE_INVALID The CCS811 received an I²C request to write an unsupported mode to
MEAS_MODE

(https://cdn.sparkfun.com/assets/learn_tutorials/1/4/3/CCS811_Datasheet-DS000459.pdf) But i have been trying to get a work that out but i dont know how to solve this. I check with nodemcu the test.ino for adafruit_CCS811 library and the sensor is working (also i had to delete the while(1) from the test.ino

Do you have any idea what else I could try?

mcspr commented 3 years ago

(error: 2) refers to this line in pre():

    _error = SENSOR_ERROR_WARM_UP;
    if (!_ccs.available()) return;
    if (!_ccs.readData()) return;
    _error = SENSOR_ERROR_OK;

So it is entirely from sw side, readData() does not have any data at the time we want it. But it should have something after 30 minutes though? I see that adafruit sets CCS811_DRIVE_MODE_1SEC aka every 1 second we should have available() -> true

edit: does removing the first _error = ... show anything?

FLXMM commented 3 years ago

well that just gives me 0ppmin webinterface and no info in debug ..so i dont know but when i thought ok whatever.. i looked at dummySensor.h again and just formatted your code to look like the DummySensor.

#include "BaseSensor.h"

#include <Adafruit_CCS811.h>

class CCS811Sensor : public BaseSensor {
public:

CCS811Sensor() {
    _count = 2;
}

/*void begin() override {
    _ccs.begin();
    _ready = true;
}
*/
void begin() override {
    _ready = _ccs.begin();
    _error = SENSOR_ERROR_OK;
}

String description() override {
    static String CCS811 (F("CCS811"));
    return CCS811;
}
String description(unsigned char) override {
    return description();
}
String address(unsigned char) override {
    static String CCS811(F("0x5a")); // TODO: this does not do anything useful atm
    return CCS811;
}
unsigned char type(unsigned char index) override {
    switch (index) {
    case 0: return MAGNITUDE_CO2;
    case 1: return MAGNITUDE_VOC;
    }
    return MAGNITUDE_NONE;
}

double value(unsigned char index) override {
    switch (index) {
    case 0: return _eco2;
    case 1: return _tvoc;
    }
    return 0.0;
    }

    void pre() override {
    if (_ccs.available()){
    if (!_ccs.readData()){
    _eco2 = _ccs.geteCO2();
    _tvoc = _ccs.getTVOC();
    }
    }
}

// the rest of the sensor which we implemented above

private:   

Adafruit_CCS811 _ccs;
uint16_t _eco2 { 0u };
uint16_t _tvoc { 0u };

};

Doing this and at the same time looking at the working tasmota code (https://github.com/arendst/Tasmota/blob/7dafeb280e08d64b7603d7204aebb816fb17714d/tasmota/xsns_31_ccs811.ino) I recognized you where using !_ccs.available() but in tasmota they use _ccs.available() this might have been it. Because know i get values :))) SO THX again @mcspr and gn8

mcspr commented 3 years ago

Reading the readData code again I see the thing they were trying to do and return false kind of blindsided me when skimming the code previously. Reading the comment would've helped :) It really spells it out

returns 0 if no error, error code otherwise.