RAK12047 unexpected behavior

In the example - “The voc index can directly indicate the quality of the air. The higher the value, the worse the air quality.” But for me, the values change inverted, when I ventilate the index grows, when I tested spray it falls. Checked on two sensors - the same.

I have two RAK12047 running in my office and a Milesight Air Quality device as reference and the behaviour of the devices is as expected.
Forced ventilation could lead to more volatile organic components being blown to the sensor. Not sure what you want to achieve with “spraying”.

Keep in mind that Sensirions algorithm says it takes up to 24 hours learning time.

Maybe What is Sensirion’s VOC Index? helps.

Both sensors have been installed and working for over a week and behave the same. Generally VOC index is 235-238. I spray with a deodorant (I also tried a solvent) nearby - the index value drops to 170-180 and then slowly rises to 235. Is that how it should be? I think not, but I may be wrong.

What is the code you are using?
I guess you are using the Sensirion VOC algorithm?
What is you reading interval from the sensor?

RAK12047_SGP40_GetVOCIndex Example

120 sec

whole code

#include <Arduino.h>
#include "LoRaWan-Arduino.h" //http://librarymanager/All#SX126x
#include <SPI.h>
#include "SparkFun_SCD30_Arduino_Library.h" // Click here to get the library: http://librarymanager/All#SparkFun_SCD30
#include <SensirionI2CSgp40.h> // Click here to get the library: http://librarymanager/All#SensirionI2CSgp40
#include <stdio.h>
#include "mbed.h"
#include "rtos.h"
#include <Wire.h>
#include "SparkFun_SHTC3.h"     //Click here to get the library: http://librarymanager/All#SparkFun_SHTC3
#define BUZZER_CONTROL  WB_IO1

using namespace std::chrono_literals;
using namespace std::chrono;

SCD30 airSensor;
SensirionI2CSgp40 sgp40;
SHTC3 g_shtc3;                  // Declare an instance of the SHTC3 class

bool doOTAA = true;   // OTAA is used by default.
#define SCHED_MAX_EVENT_DATA_SIZE APP_TIMER_SCHED_EVENT_DATA_SIZE /**< Maximum size of scheduler events. */
#define SCHED_QUEUE_SIZE 60                      /**< Maximum number of events in the scheduler queue. */
#define LORAWAN_DATERATE DR_3                   /*LoRaMac datarates definition, from DR_0 to DR_5*/
#define LORAWAN_TX_POWER TX_POWER_3             /*LoRaMac tx power definition, from TX_POWER_0 to TX_POWER_15*/
#define JOINREQ_NBTRIALS 3                      /**< Number of trials for the join request. */
DeviceClass_t g_CurrentClass = CLASS_C;         /* class definition*/
LoRaMacRegion_t g_CurrentRegion = LORAMAC_REGION_RU864;    /* Region:EU868*/
lmh_confirm g_CurrentConfirm = LMH_CONFIRMED_MSG;         /* confirm/unconfirm packet definition*/
uint8_t gAppPort = LORAWAN_APP_PORT;                      /* data port*/

/**@brief Structure containing LoRaWan parameters, needed for lmh_init()
*/
static lmh_param_t g_lora_param_init = {LORAWAN_ADR_ON, LORAWAN_DATERATE, LORAWAN_PUBLIC_NETWORK, JOINREQ_NBTRIALS, LORAWAN_TX_POWER, LORAWAN_DUTYCYCLE_OFF};

// Foward declaration
static void lorawan_has_joined_handler(void);
static void lorawan_join_failed_handler(void);
static void lorawan_rx_handler(lmh_app_data_t *app_data);
static void lorawan_confirm_class_handler(DeviceClass_t Class);
static void send_lora_frame(void);
void lorawan_unconf_finished(void);
void lorawan_conf_finished(bool result);

/**@brief Structure containing LoRaWan callback functions, needed for lmh_init()
*/
static lmh_callback_t g_lora_callbacks = {BoardGetBatteryLevel, BoardGetUniqueId, BoardGetRandomSeed,
                                          lorawan_rx_handler, lorawan_has_joined_handler,
                                          lorawan_confirm_class_handler, lorawan_join_failed_handler,
                                          lorawan_unconf_finished, lorawan_conf_finished
                                         };
//OTAA keys !!!! KEYS ARE MSB !!!!
uint8_t nodeDeviceEUI[8] = {0000000000000000};
uint8_t nodeAppEUI[8] = {0000000000000000};
uint8_t nodeAppKey[16] = {0000000000000000};

// ABP keys
uint32_t nodeDevAddr = 0x260116F8;
uint8_t nodeNwsKey[16] = {0x7E, 0xAC, 0xE2, 0x55, 0xB8, 0xA5, 0xE2, 0x69, 0x91, 0x51, 0x96, 0x06, 0x47, 0x56, 0x9D, 0x23};
uint8_t nodeAppsKey[16] = {0xFB, 0xAC, 0xB6, 0x47, 0xF3, 0x58, 0x45, 0xC7, 0x50, 0x7D, 0xBF, 0x16, 0x8B, 0xA8, 0xC1, 0x7C};

// Private defination
#define LORAWAN_APP_DATA_BUFF_SIZE 64                     /**< buffer size of the data to be transmitted. */
#define LORAWAN_APP_INTERVAL 120000                        /**< Defines for user timer, the application data transmission interval. 20s, value in [ms]. */
static uint8_t m_lora_app_data_buffer[LORAWAN_APP_DATA_BUFF_SIZE];            //< Lora user application data buffer.
static lmh_app_data_t m_lora_app_data = {m_lora_app_data_buffer, 0, 0, 0, 0}; //< Lora user application data structure.

mbed::Ticker appTimer;
void tx_lora_periodic_handler(void);

static uint32_t count = 0;
static uint32_t count_fail = 0;

bool send_now = false;

void errorDecoder(SHTC3_Status_TypeDef message)   // The errorDecoder function prints "SHTC3_Status_TypeDef" resultsin a human-friendly way
{
  switch (message)
  {
    case SHTC3_Status_Nominal:
      Serial.print("Nominal");
      break;
    case SHTC3_Status_Error:
      Serial.print("Error");
      break;
    case SHTC3_Status_CRC_Fail:
      Serial.print("CRC Fail");
      break;
    default:
      Serial.print("Unknown return code");
      break;
  }
}


void setup()
{
  // Initialize Serial for debug output
  time_t timeout = millis();
  Serial.begin(115200);
  while (!Serial)
  {
    if ((millis() - timeout) < 5000)
    {
      delay(100);
    }
    else
    {
      break;
    }
  }

  // Initialize LoRa chip.
  lora_rak11300_init();

  Serial.println("=====================================");
  Serial.println("Welcome to RAK11300 LoRaWan!!!");
  if (doOTAA)
  {
    Serial.println("Type: OTAA");
  }
  else
  {
    Serial.println("Type: ABP");
  }

  switch (g_CurrentRegion)
  {
    case LORAMAC_REGION_AS923:
      Serial.println("Region: AS923");
      break;
    case LORAMAC_REGION_AU915:
      Serial.println("Region: AU915");
      break;
    case LORAMAC_REGION_CN470:
      Serial.println("Region: CN470");
      break;
    case LORAMAC_REGION_CN779:
      Serial.println("Region: CN779");
      break;
    case LORAMAC_REGION_EU433:
      Serial.println("Region: EU433");
      break;
    case LORAMAC_REGION_IN865:
      Serial.println("Region: IN865");
      break;
    case LORAMAC_REGION_EU868:
      Serial.println("Region: EU868");
      break;
    case LORAMAC_REGION_KR920:
      Serial.println("Region: KR920");
      break;
    case LORAMAC_REGION_US915:
      Serial.println("Region: US915");
      break;
    case LORAMAC_REGION_RU864:
      Serial.println("Region: RU864");
      break;
    case LORAMAC_REGION_AS923_2:
      Serial.println("Region: AS923-2");
      break;
    case LORAMAC_REGION_AS923_3:
      Serial.println("Region: AS923-3");
      break;
    case LORAMAC_REGION_AS923_4:
      Serial.println("Region: AS923-4");
      break;
  }
  Serial.println("=====================================");

  // Setup the EUIs and Keys
  if (doOTAA)
  {
    lmh_setDevEui(nodeDeviceEUI);
    lmh_setAppEui(nodeAppEUI);
    lmh_setAppKey(nodeAppKey);
  }
  else
  {
    lmh_setNwkSKey(nodeNwsKey);
    lmh_setAppSKey(nodeAppsKey);
    lmh_setDevAddr(nodeDevAddr);
  }

  // Initialize LoRaWan
  uint32_t err_code = lmh_init(&g_lora_callbacks, g_lora_param_init, doOTAA, g_CurrentClass, g_CurrentRegion);
  if (err_code != 0)
  {
    Serial.printf("lmh_init failed - %d\n", err_code);
    return;
  }

  // Start Join procedure
  lmh_join();

  uint16_t error;
  char errorMessage[256];
  uint16_t serialNumber[3];
  uint8_t serialNumberSize = 3;

  // pinMode(WB_IO2, OUTPUT);
  // digitalWrite(WB_IO2, HIGH);
  pinMode(BUZZER_CONTROL,OUTPUT);
 
  delay(1000);

  Wire.begin();

  sgp40.begin(Wire);

  error = sgp40.getSerialNumber(serialNumber, serialNumberSize);
  if (error) 
  {
    Serial.print("Error trying to execute getSerialNumber(): ");
    errorToString(error, errorMessage, 256);
    Serial.println(errorMessage);
  } 
  else 
  {
    Serial.print("Serial Number:");
    Serial.print("0x");
    for (size_t i = 0; i < serialNumberSize; i++) 
    {
      uint16_t value = serialNumber[i];
      Serial.print(value < 4096 ? "0" : "");
      Serial.print(value < 256 ? "0" : "");
      Serial.print(value < 16 ? "0" : "");
      Serial.print(value, HEX);
    }
    Serial.println();
  }

  uint16_t testResult;
  error = sgp40.executeSelfTest(testResult);
  if (error) 
  {
    Serial.print("Error trying to execute executeSelfTest(): ");
    errorToString(error, errorMessage, 256);
    Serial.println(errorMessage);
  } 
  else if (testResult != 0xD400) 
  {
    Serial.print("executeSelfTest failed with error: ");
    Serial.println(testResult);
  }

  //Start sensor using the Wire port and enable the auto-calibration (ASC)
  if (airSensor.begin(Wire, true) == false)
  {
    Serial.println("Air sensor not detected. Please check wiring. Freezing...");
    while (1)
    {
      delay(10);
    }
  }

  Serial.print("Automatic self-calibration set to:");
  if (airSensor.getAutoSelfCalibration() == true)
    Serial.println("true");
  else
    Serial.println("false");

  Serial.println("shtc3 init");
  Serial.print("Beginning sensor. Result = "); // Most SHTC3 functions return a variable of the type "SHTC3_Status_TypeDef" to indicate the status of their execution
  errorDecoder(g_shtc3.begin());              // To start the sensor you must call "begin()", the default settings use Wire (default Arduino I2C port)
  Wire.setClock(400000);                      // The sensor is listed to work up to 1 MHz I2C speed, but the I2C clock speed is global for all sensors on that bus so using 400kHz or 100kHz is recommended
  Serial.println();

  if (g_shtc3.passIDcrc)                      // Whenever data is received the associated checksum is calculated and verified so you can be sure the data is true
  {                                           // The checksum pass indicators are: passIDcrc, passRHcrc, and passTcrc for the ID, RH, and T readings respectively
    Serial.print("ID Passed Checksum. ");
    Serial.print("Device ID: 0b");
    Serial.println(g_shtc3.ID, BIN);          // The 16-bit device ID can be accessed as a member variable of the object
  }
  else
  {
    Serial.println("ID Checksum Failed. ");
  }    

}

void loop()
{
  // Every LORAWAN_APP_INTERVAL milliseconds send_now will be set
  // true by the application timer and collects and sends the data
  if (send_now)
  {
    Serial.println("Sending frame now...");
    send_now = false;
    send_lora_frame();
  }
}

/**@brief LoRa function for handling HasJoined event.
*/
void lorawan_has_joined_handler(void)
{
  if (doOTAA == true)
  {
    Serial.println("OTAA Mode, Network Joined!");
  }
  else
  {
    Serial.println("ABP Mode");
  }

  lmh_error_status ret = lmh_class_request(g_CurrentClass);
  if (ret == LMH_SUCCESS)
  {
    delay(1000);
    // Start the application timer. Time has to be in microseconds
    appTimer.attach(tx_lora_periodic_handler, (std::chrono::microseconds)(LORAWAN_APP_INTERVAL * 1000));
  }
}
/**@brief LoRa function for handling OTAA join failed
*/
static void lorawan_join_failed_handler(void)
{
  Serial.println("OTAA join failed!");
  Serial.println("Check your EUI's and Keys's!");
  Serial.println("Check if a Gateway is in range!");
}
/**@brief Function for handling LoRaWan received data from Gateway

   @param[in] app_data  Pointer to rx data
*/
void lorawan_rx_handler(lmh_app_data_t *app_data)
{
  Serial.printf("LoRa Packet received on port %d, size:%d, rssi:%d, snr:%d, data:%s\n",
                app_data->port, app_data->buffsize, app_data->rssi, app_data->snr, app_data->buffer);

  uint8_t siren_command = *app_data->buffer;
  
  if(siren_command==0)
  {
    Serial.println("0 Siren OFF");  
    noTone (BUZZER_CONTROL);
  }
    
  if(siren_command==1)
  {
    Serial.println("1 Siren ON");  
    for (int x = 0; x < 100; x++)
    {
      tone(BUZZER_CONTROL, 1000);
      delay(300);
      noTone (BUZZER_CONTROL);
      tone(BUZZER_CONTROL, 200);
      delay(300);
      noTone (BUZZER_CONTROL);
    }
  }

}

void lorawan_confirm_class_handler(DeviceClass_t Class)
{
  Serial.printf("switch to class %c done\n", "ABC"[Class]);
  // Informs the server that switch has occurred ASAP
  m_lora_app_data.buffsize = 0;
  m_lora_app_data.port = gAppPort;
  lmh_send(&m_lora_app_data, g_CurrentConfirm);
}

void lorawan_unconf_finished(void)
{
  Serial.println("TX finished");
}

void lorawan_conf_finished(bool result)
{
  Serial.printf("Confirmed TX %s\n", result ? "success" : "failed");
}

void send_lora_frame(void)
{
  if (lmh_join_status_get() != LMH_SET)
  {
    //Not joined, try again later
    return;
  }

  collect_data();

  lmh_error_status error = lmh_send(&m_lora_app_data, g_CurrentConfirm);
  if (error == LMH_SUCCESS)
  {
    count++;
    Serial.printf("lmh_send ok count %d\n", count);
  }
  else
  {
    count_fail++;
    Serial.printf("lmh_send fail count %d\n", count_fail);
  }
}

String data = "";

void collect_data()
{
  float Temperature = 0;
  float Humidity = 0;
  
  g_shtc3.update();
  if (g_shtc3.lastStatus == SHTC3_Status_Nominal) // You can also assess the status of the last command by checking the ".lastStatus" member of the object
  {

    Temperature = g_shtc3.toDegC();               // Packing LoRa data
    Humidity = g_shtc3.toPercent();
    
    Serial.print("RH = ");
    Serial.print(g_shtc3.toPercent());            // "toPercent" returns the percent humidity as a floating point number
    Serial.print("% (checksum: ");
    
    if (g_shtc3.passRHcrc)                        // Like "passIDcrc" this is true when the RH value is valid from the sensor (but not necessarily up-to-date in terms of time)
    {
      Serial.print("pass");
    }
    else
    {
      Serial.print("fail");
    }
    
    Serial.print("), T = ");
    Serial.print(g_shtc3.toDegC());               // "toDegF" and "toDegC" return the temperature as a flaoting point number in deg F and deg C respectively
    Serial.print(" deg C (checksum: ");
    
    if (g_shtc3.passTcrc)                         // Like "passIDcrc" this is true when the T value is valid from the sensor (but not necessarily up-to-date in terms of time)
    {
      Serial.print("pass");
    }
    else
    {
      Serial.print("fail");
    }
    Serial.println(")");
  }
  else
  {
    Serial.print("Update failed, error: ");
    errorDecoder(g_shtc3.lastStatus);
    Serial.println();
  }

  uint16_t  error;
  char      errorMessage[256];
  uint16_t  srawVoc   = 0;
  float     vocIndex  = 0;
  /* 
   * @brief Set the relative humidity and temperature in the current environment.
   *        Temperature and humidity calibration has been performed inside the sensor.
   *        RH/ticks=RH/%×65535/100
   *        T/ticks=(T/°C + 45)×65535/175
   */
  uint16_t  defaultRh = 0x8000;  // 50 %RH
  uint16_t  defaultT  = 0x6666;  // 25 ℃
  
  delay(1000);
  
  error = sgp40.measureRawSignal(defaultRh, defaultT, srawVoc);
  if (error) 
  {
    Serial.print("Error trying to execute measureRawSignal(): ");
    errorToString(error, errorMessage, 256);
    Serial.println(errorMessage);
  } 
  else 
  {
    Serial.print("SRAW_VOC:");
    Serial.print(srawVoc);
    vocIndex = (float)srawVoc/131.07 ;

   /* VOC index.
    * The voc index can directly indicate the quality of the air. The higher the value, the worse the air quality.
    *   0-100,no need to ventilate,purify.
    *   100-200,no need to ventilate,purify.
    *   200-400,ventilate,purify.
    *   400-500,ventilate,purify intensely.
    */
    Serial.print("  VOC Index:");
    Serial.println(vocIndex);
  }

  uint16_t co2;
  double temp;
  double hum;
  uint16_t voci = vocIndex;
  
  if (airSensor.dataAvailable())
  {
    co2 = airSensor.getCO2();
    temp = airSensor.getTemperature();
    hum = airSensor.getHumidity();
  }
  else
    Serial.println("Waiting for new data");

  data = "Tem:" + String(temp) + "C " + "Hum:" + String(hum) + "% " + "CO2: " + String(co2) + "ppm" + "VOC Index " + String(voci);
  Serial.println(data);

///////////////////////////////////////////////////////////////////

  uint32_t i = 0;
  memset(m_lora_app_data.buffer, 0, LORAWAN_APP_DATA_BUFF_SIZE);
  m_lora_app_data.port = gAppPort;

  uint16_t t = Temperature * 100;
  uint16_t h = Humidity * 100;
  uint32_t c = co2 * 100;
  uint32_t v = voci * 100;

  //result: T=28.25C, RH=50.00%, P=958.57hPa, G=100406 Ohms
  m_lora_app_data.buffer[i++] = 0x01;
  m_lora_app_data.buffer[i++] = (uint8_t)(t >> 8);
  m_lora_app_data.buffer[i++] = (uint8_t)t;
  m_lora_app_data.buffer[i++] = (uint8_t)(h >> 8);
  m_lora_app_data.buffer[i++] = (uint8_t)h;
  m_lora_app_data.buffer[i++] = (uint8_t)((c & 0xFF000000) >> 24);
  m_lora_app_data.buffer[i++] = (uint8_t)((c & 0x00FF0000) >> 16);
  m_lora_app_data.buffer[i++] = (uint8_t)((c & 0x0000FF00) >> 8);
  m_lora_app_data.buffer[i++] = (uint8_t)(c & 0x000000FF);
  m_lora_app_data.buffer[i++] = (uint8_t)((v & 0xFF000000) >> 24);
  m_lora_app_data.buffer[i++] = (uint8_t)((v & 0x00FF0000) >> 16);
  m_lora_app_data.buffer[i++] = (uint8_t)((v & 0x0000FF00) >> 8);
  m_lora_app_data.buffer[i++] = (uint8_t)(v & 0x000000FF);
  m_lora_app_data.buffsize = i;
}

/**@brief Function for handling user timerout event.
*/
void tx_lora_periodic_handler(void)
{
  appTimer.attach(tx_lora_periodic_handler, (std::chrono::microseconds)(LORAWAN_APP_INTERVAL * 1000));
  // This is a timer interrupt, do not do lengthy things here. Signal the loop() instead
  send_now = true;
}

That is not the best way to use the VOC sensor. It is just a very simple code to show how to read from the sensor.

Have a look at this code for RAK12047

Using the Sensirion VOC Gas Index Algorithm will get you better results.

thanks, I’ll try
for the CO2 sensor, maybe there are also more correct methods?

The CO2 sensor RAk12037 is much simpler, but you can find code I use in the same repo

Another remark for the RAk12047, the first 100 values coming out of the Sensirion algorithm should not be trusted.
I actually throw them away in my code.

Hi Bernd.
I don’t have RUI3 and unfortunately my experience is not enough to apply this:

for RAK11310. Do you have the same example code for RAK11310?
thanks in advance

This RAK12047_voc.cpp code example is written for RAK4631, RAK11200 and RAK11310, but no 100% guarantee that it works without problems on the RAK11310.
I don’t like Mbed and because of that I am not using the RAK11310 much.