Wemos D1 mini + BME680 + BSEC/IAQ + MQTT

Hello folks,
past couple of days I’ve been tweaking my sketch for my personal setup with bunch of Wemos D1 minis around the house and BME680 sensors attached to them.

As I wanted fully “official” BSEC and IAQ data to be gathered and calculated, I’ve been reusing some bosh codes and some other codes around. Which none of them was complete package.
I found quite harsh to get BSEC working from various sources over the internet, and mine is now working reliable so I wanted to do some share :wink:

needed libraries:

  • ArduinoJson (5.x) - it’s not compatible with 6.x
  • PubSubClient
  • Uptime-Library
  • bsec
    (from bosh generic or bosh-arduino) , bosh is currently down, I can provide my own if wanted

note: there is a need to copy bsec_serialized_configurations_iaq.c and bsec_serialized_configurations_iaq.h and bsec_iaq.config to the /Arduino/libraries/bsec root as for some reason BOSH haven’t put them there … and therefore their examples aren’t working :wink:

note2: it will not run on devices with 1MB Flash, as bsec is pretty big, so something like wemos is needed, but basically any esp8266 based units with enough flash would work.

note3: sensor is connected to 0x77 (SECONDARY, eg. without SDO to GND) if you use 0x76 change it in main program

Sketch folder contains main file and settings.h file which is basically ontly thing you need to adjust for your needs. If you are going to use more than one (as me) unit, this is very handy as you are changing just one line to make another unit in another room :wink:

Following program reads and adjust sensor and IAQ accurancy itself and after reaching 3 (calibrated) state it will save itself to EEPROM so when you restart or whatever your Wemos, it will recalibrate after reboot quite quickly.
As well BSEC is recalibrating itself while running so each 6 hours (configurable) it will save new state etc.
Each 2 mins (configurable) it will report to the MQTT broker to the topic home/your-device-config-name/climate/BME680 data from sensor in JSON format, which then can be easily integrated to OH (which I’m not going to explain in here).

MQTT output looks like this:

home/kitchen/climate/BME680 {"Uptime":"1T4:43:59","Temperature":25.6,"Humidity":57.7,"Pressure":978.8,"Gas":185.03,"IAQ":62.7,"sIAQ":82.3,"IAQ_Accuracy":3,"CO2e":822.671,"bVOC":1.188,"rawTemperature":25.7,"rawHumidity":57.3}

Progam maintain wifi and mqtt connection, reconnects as needed etc. So it’s fully autonomous and quite reliable in terms of IAQ.
It’s meant to be indoor, but I guess it can be used as outdoor (not yet tested) as well.

From observation of running BME680 and BME250 2cm from each other, it’s likely needed to add 1.3f temp offset to BME680 as build in heat compensation is not enough.
Your results might vary, but I’ve ran 5 BME680 and 3 BME280 at same place at same time without any heat from other chips and resuls are almost exactly the same 1.3-1.5 degree :wink:

Feel free to comment / update / hint following sketch

#include <EEPROM.h>
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include "uptime.h"
#include "bsec.h"
#include "bsec_serialized_configurations_iaq.h"
#include "settings.h"

WiFiClient wifiClient;
PubSubClient client(wifiClient);

#define STATE_SAVE_PERIOD  UINT32_C(iaq_save_period * 60 * 60 * 1000)
#define MQTT_REPORT UINT32_C(mqtt_publish_period * 60 * 1000)
char wifi_hostname[50];
char mqtt_topic[100];

// Helper functions declarations
void checkIaqSensorStatus(void);
void errLeds(void);
void loadState(void);
void updateState(void);

// Create an object of the class Bsec
Bsec iaqSensor;
uint8_t bsecState[BSEC_MAX_STATE_BLOB_SIZE] = {0};
uint16_t stateUpdateCounter = 0;
uint16_t stateUpdateMqtt = 0;

String output;

// Entry point for the example
void setup(void)
{
  // uniq name, topic, etc
  sprintf(wifi_hostname, "iot-wemos-%s",device);
  sprintf(mqtt_topic, "home/%s/climate/BME680",device);

  EEPROM.begin(BSEC_MAX_STATE_BLOB_SIZE + 1); // 1st address for the length
  Serial.begin(115200);

  setup_wifi();
  client.setServer(mqtt_server, 1883);
  Wire.begin();

  // set address
  iaqSensor.begin(BME680_I2C_ADDR_SECONDARY, Wire);
  // set offset
  iaqSensor.setTemperatureOffset(temp_offset);

  // useless info
  output = "\nBSEC library version " + String(iaqSensor.version.major) + "." + String(iaqSensor.version.minor) + "." + String(iaqSensor.version.major_bugfix) + "." + String(iaqSensor.version.minor_bugfix);
  Serial.println(output);
  Serial.println("");
  
  checkIaqSensorStatus();

  // set proprietary BOSH config
  iaqSensor.setConfig(bsec_config_iaq);
  checkIaqSensorStatus();

  // check EEPROM
  loadState();

  // subscribe to data from sensor
  bsec_virtual_sensor_t sensorList[10] = {
    BSEC_OUTPUT_RAW_TEMPERATURE,
    BSEC_OUTPUT_RAW_PRESSURE,
    BSEC_OUTPUT_RAW_HUMIDITY,
    BSEC_OUTPUT_RAW_GAS,
    BSEC_OUTPUT_IAQ,
    BSEC_OUTPUT_STATIC_IAQ,
    BSEC_OUTPUT_CO2_EQUIVALENT,
    BSEC_OUTPUT_BREATH_VOC_EQUIVALENT,
    BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE,
    BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY,
  };

  iaqSensor.updateSubscription(sensorList, 10, BSEC_SAMPLE_RATE_LP);
  checkIaqSensorStatus();
}

void setup_wifi() 
{
  delay(50);
  Serial.println();
  Serial.print("Connecting to SSID: ");
  Serial.print(ssid);
  WiFi.hostname(wifi_hostname);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  // disable AP mode which is for some unknown reason ON by default
  WiFi.softAPdisconnect (true);

  Serial.println("connected!");
  Serial.print("IP: ");
  Serial.println(WiFi.localIP());
}

void reconnect()
{
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("MQTT connection...");
    // Attempt to connect
    if (client.connect(wifi_hostname))
    {
      Serial.println("connected!"); \
    }
    else
    {
      Serial.print("failed");
      Serial.print(client.state());
      Serial.println(", try again in 5 seconds");
    }
    // Wait 5 seconds before retrying
    delay(5000);
  }
}

// Function that is looped forever
void loop(void)
{
  uptime::calculateUptime();
  
  // wifi disconnected
  if (WiFi.status() != WL_CONNECTED){
    setup_wifi();
  }
  // MQTT disconnected
  if (!client.connected()){
    reconnect();
  }
  client.loop();
  {
    unsigned long time_trigger = millis();
    if (iaqSensor.run()) { // If new data is available

      output = "";
      output += " Temperature: " + String(iaqSensor.rawTemperature) + " (" + String(iaqSensor.temperature) + ")";
      output += ", Pressure: " + String(iaqSensor.pressure / 1e2);
      output += ", Humidity: " + String(iaqSensor.rawHumidity) + " (" + String(iaqSensor.humidity) + ")";
      output += ", Gas:" + String(iaqSensor.gasResistance / 1e3);
      output += ", IAQ:" + String(iaqSensor.iaqEstimate) + " (" + String(iaqSensor.staticIaq) + ")";
      output += " (" + String(iaqSensor.iaqAccuracy);
      output += "), CO2e:" + String(iaqSensor.co2Equivalent);
      output += ", bVOC:" + String(iaqSensor.breathVocEquivalent);
      
      Serial.println(output);
     
      updateState();

      // MQTT publish every (n) minutes
      if (stateUpdateMqtt * MQTT_REPORT < millis()) {

        StaticJsonBuffer<300> jsonBuffer;
        JsonObject& root = jsonBuffer.createObject();
        
        root["Uptime"] = String(uptime::getDays()) + "T" + String(uptime::getHours()) + ":" + String(uptime::getMinutes()) + ":" + String(uptime::getSeconds());
        root["Temperature"] = decimals(iaqSensor.temperature,1);
        root["Humidity"] = decimals(iaqSensor.humidity,1);
        root["Pressure"] = decimals((iaqSensor.pressure / 1e2),1);
        root["Gas"] = decimals((iaqSensor.gasResistance / 1e3),2);
        root["IAQ"] = decimals(iaqSensor.iaqEstimate,1);
        root["sIAQ"] = decimals(iaqSensor.staticIaq,1);
        root["IAQ_Accuracy"] = iaqSensor.iaqAccuracy;
        root["CO2e"] = decimals(iaqSensor.co2Equivalent,3);
        root["bVOC"] = decimals(iaqSensor.breathVocEquivalent,3);
        root["rawTemperature"] = decimals(iaqSensor.rawTemperature,1);
        root["rawHumidity"] = decimals(iaqSensor.rawHumidity,1);
        
        // use it as JSON
        char data[300];
        root.printTo(data, root.measureLength() + 1);

        // mqtt report
        client.publish(mqtt_topic, data);
        Serial.println("MQTT Publish");
        stateUpdateMqtt++;
      }
    } else {
      checkIaqSensorStatus();
    }
  }

}

// Helper function definitions
void checkIaqSensorStatus(void)
{
  if (iaqSensor.status != BSEC_OK) {
    if (iaqSensor.status < BSEC_OK) {
      output = "BSEC error code : " + String(iaqSensor.status);
      Serial.println(output);
      for (;;)
        errLeds(); /* Halt in case of failure */
    } else {
      output = "BSEC warning code : " + String(iaqSensor.status);
      Serial.println(output);
    }
  }

  if (iaqSensor.bme680Status != BME680_OK) {
    if (iaqSensor.bme680Status < BME680_OK) {
      output = "BME680 error code : " + String(iaqSensor.bme680Status);
      Serial.println(output);
      for (;;)
        errLeds(); /* Halt in case of failure */
    } else {
      output = "BME680 warning code : " + String(iaqSensor.bme680Status);
      Serial.println(output);
    }
  }
}

void errLeds(void)
{
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);
  delay(100);
  digitalWrite(LED_BUILTIN, LOW);
  delay(100);
}

void loadState(void)
{
  if (EEPROM.read(0) == BSEC_MAX_STATE_BLOB_SIZE) {
    // Existing state in EEPROM
    Serial.println("Reading state from EEPROM");

    for (uint8_t i = 0; i < BSEC_MAX_STATE_BLOB_SIZE; i++) {
      bsecState[i] = EEPROM.read(i + 1);
      //Serial.println(bsecState[i], HEX);
    }

    iaqSensor.setState(bsecState);
    checkIaqSensorStatus();
  } else {
    // Erase the EEPROM with zeroes
    Serial.println("Erasing EEPROM");

    for (uint8_t i = 0; i < BSEC_MAX_STATE_BLOB_SIZE + 1; i++)
      EEPROM.write(i, 0);

    EEPROM.commit();
  }
}

void updateState(void)
{
  bool update = false;
  if (stateUpdateCounter == 0) {
    /* First state update when IAQ accuracy is >= 3 */
    if (iaqSensor.iaqAccuracy >= 3) {
      update = true;
      stateUpdateCounter++;
    }
  } else {
    /* Update every STATE_SAVE_PERIOD minutes */
    if ((stateUpdateCounter * STATE_SAVE_PERIOD) < millis()) {
      update = true;
      stateUpdateCounter++;
    }
  }

  if (update) {
    iaqSensor.getState(bsecState);
    checkIaqSensorStatus();

    Serial.println("Writing state to EEPROM");

    for (uint8_t i = 0; i < BSEC_MAX_STATE_BLOB_SIZE ; i++) {
      EEPROM.write(i + 1, bsecState[i]);
      //Serial.println(bsecState[i], HEX);
    }

    EEPROM.write(0, BSEC_MAX_STATE_BLOB_SIZE);
    EEPROM.commit();
  }
}


float decimals(float input, int decimals)
{
  float scale=pow(10,decimals);
  return round(input*scale)/scale;
}

and settings.h

/*
* Device Setting
*/
const char* device = "short-and-uniq-device-name"; // lowercase

// reporting periods
const int iaq_save_period     = 6; // hours
const int mqtt_publish_period = 2; // minutes

// temperature offset
const float temp_offset(0.0f);

// wifi & mqtt
const char* ssid        = "YOUR WIFI SSID";
const char* password    = "YOUR WIFI PSWD";
const char* mqtt_server = "YOUR MQTT BROKER IP";

if you find it usefull, cheers! :slight_smile:
K.

1 Like

That’s really cool. I’m currently running on a raspberry pi, with some mix results.

Does it need to change the configurations on the Arduino IDE to compile with the libalgobsec.a like they show on the bosh github?
I was successful to get the files from bosh to work using plataformio, it’s easier I think.

I had completely different code for RPI which honesly was not working neither closely as good as this one. I recon having a lot of issues with libalgobsec … and that’s why I switched this to the wemos.

for these type of application (where you do not need much power/IO/full linux etc.) I find much more usefull arduino code + esp chip . It’s pretty easy to follow, nothing really needed for compiling and can be remotely reflashed at will…