The CLEAN Mobile Monitor Prototype Firmware

The firmware of the mobile prototype was developed for the Microchip ATMega2560 microcontroller embedded on an Arduino Mega platform. The code was developed in the C/C++ programming language using the Arduino Framework, available at the PlatformIO IDE for the Microsoft Visual Studio Code editor (VSCode). For more details on programming the Arduino MEGA, please refer to The Arduino MEGA Programming Guide.

The code consists of two parts; one for setup and the other for the main loop. The current version of the firmware has four main functionalities which are in accordance with the hardware of the monitor, as follows:

  • Reading the gas samples from the serial interface of the SPEC sensors
  • Reading the geographical coordinates of the location where each reading takes place
  • Storing the gas concentration information into an SD card
  • Sending the gas concentration information to an HTTP server through the ESP8266 microcontroller

A detailed description of the hardware components of this prototype and the way they connect to the Arduino platform can be found at The CLEAN Mobile Monitor Prototype and The CLEAN Mobile Monitor Prototype Mounting Guide.

Figure 1. Flowchart of the firmware programmed for the Arduino MEGA microcontroller

Figure 1 shows a flowchart of the code programmed for the ATMega2560. As with every program of the Arduino Framework the code is executed in two main functions: setup() and loop(). In the current version of the firmware, the setup() prepares the communication between the external modules (i.e.: GPS, Wi-Fi, SD Card, and SPEC sensors) and the microcontroller. This function also sets up the digital output ports and TIMER 1, which are used for temporization and visualization of the status of certain functionalities. The loop() function first verifies if the ESP8266 hasn’t been responsive for some time, in which case the Arduino will send a RESET signal to the ESP8266. The rest of the function is divided into four sections that are executed periodically and control the above-mentioned functionalities. Next, we describe the different sections of the code.

The firmware includes two more functions that are separate from the main flow of the program. Those functions are the ISR(), that handles interrupt events from TIMER1, and serialEvent3(), that handles interrupt events from the UART3. The serialEvent3()interruption is executed whenever data is received at the serial buffer. The ISR() interruption, on the other hand, is executed every time the TIMER1 overflows.

Firstly, identify the device and its sensors

One crucial part of the firmware is the identification of the device and the sensors connected to it. These identifications must match the ones configured in the RENOVAr server, and will be used by the backend application to update the databases. First, you must set the Device id, as follows:

unsigned long Device::id = <THE NUMBER OF YOUR DEVICE>;

After that, you can define an enum to storage the IDs of the sensors, like the one we used in our code (see below). Each value of the enum represents a variable at the RENOVAr application.

enum iotId_e    {   CO_ID   = <The ID of the CO gas concentration variable>, 
                    NO2_ID  = <The ID of the NO2 gas concentration variable>, 
                    O3_ID   = <The ID of the O3 gas concentration variable>,
                    SO2_ID  = <The ID of the SO2 gas concentration variable>, 
                    TEMP_ID = <The ID of the temperature variable>,
                    RHUM_ID = <The ID of the relative humidity variable>,
                };

Setup

This function prepares the communication between the external modules and the microcontroller, as well as the digital output ports and the TIMER 1. The code of this function is shown below. Firstly, the program sets up the serial ports that will be used for communication with the GPS module (Serial1), the SPEC sensors (RS485_2), and the ESP8266 (Serial3). Each serial port is initialized at a baud rate previously defined in the code, as will be described later. The serial port UART0 (Serial) is used for debugging. The RS485_2 implements a serial interface to the RS485 bus that connects the sensors to the microcontroller. The digital pin D13 is used in this version as a voltage reference for the level shifter used to interfacing the ESP8266, but it could be ignored if using another source of 5 V. The setup() function also restarts the ESP8266 microcontroller through the object espIoT and initializes the interface for the SD card module. Finally, the function configures three digital pins as digital outputs that control the LEDs used for visual indication and configures TIMER1 to trigger an interruption every 1 second (frequency of 1 Hz). Table 1 describes the objects, constants, and functions that are used in this part of the code.

void setup()
{  
  Serial.begin(9600);
  Serial1.begin(GPSBaud);
  RS485_2.setPins(RS485_DEFAULT_TX1_PIN, (int)MASTERPIN, (int)MASTERPIN);
  RS485_2.begin(DEFAULT_BAUDRATE);
  Serial3.begin(9600UL);
  
  pinMode(13, OUTPUT);
  digitalWrite(13, HIGH);

  espIoT.restart();
  
  SD.begin(CHIPSEL_PIN);
  
  // Setup digital outputs for led indications
  pinMode     (GPSLedPin   , OUTPUT );
  pinMode     (SDLedPin    , OUTPUT );
  pinMode     (SensorLedPin, OUTPUT );
  digitalWrite(GPSLedPin   , HIGH   );
  digitalWrite(SDLedPin    , HIGH   );
  digitalWrite(SensorLedPin, HIGH   );
  
  // Set-up TIMER1
  noInterrupts();             // disable all interrupts

  TCCR1A  = 0;
  TCCR1B  = 0;
  TCNT1   = 0;  
  OCR1A   = 62500;            // compare match register 16MHz/256/1Hz
  TCCR1B |= (1 << WGM12);     // CTC mode
  TCCR1B |= (1 << CS12);      // 256 prescaler 
  TIMSK1 |= (1 << OCIE1A);    // enable timer compare interrupt

  interrupts();               // enable all interrupts
}

Table 1. Objects, constants, and functions used in the setup function

NameDescription
Serial
Serial1
Serial3
These objects, which are declared in the Arduino core, represent the UART Ports of the microcontroller. For more information, please refer to the Arduino Documentation. The objects are initialized by the begin() function, which receives the baud rate of the serial communication.
GPSBaudThis constant defines the baud rate of the serial communication between the ATMega microcontroller and the GPS module. The default value is 9600 bauds. It is defined in the library serial-geo-interface. For more information on this library refer to The Hardware Interfaces Package Documentation.
RS485_2This object establishes an interface for controlling the RS485 bus that connects the gas sensors. It is declared in the rs-485 library as an instance of RS485Class. The object is initialized with the method begin(), similar to the Serial objects. For more information on this library refer to The Hardware Interfaces Package Documentation.
RS485Class::setPins(int txPin, int dePin, int rePin)The function setPins() defines the digital pins of the microcontroller that will be used in the RS485 hardware interface. Those pins are the Tx pin, for data transmission, and the DE, RE pins that control the data flow (for reading or for writing). The pins used for the current version of the firmware are:
txPin - RS485_DEFAULT_TX1_PIN. Defined in the rs-485 library as D18
dePin, rePin - MASTERPIN. Defined as D10 within the enumerate en_rs485_pinout defined in the sensor library.
espIoTThis is an object of the class ESPSerialInterface defined in the serial-internet-interface library. This object controls the communication with the ESP8266 connected to the UART3 of the Arduino. The function restart() sends a RESET signal to the ESP8266.
The object espIoT is defined previously in the code as follows:
ESPSerialInterface espIoT(&Serial3);
SDThis is an object of SDClass declared in the Arduino core for interfacing SD Card modules. It is initialized with a begin() method that receives the digital pin that connects to the CS pin of the module. The digital pin used for the current version of the hardware and the firmware is defined in the file hardstorage.h as follows:
#define CHIPSEL_PIN 53
GPSLedPin
SDLedPin
SensorLedPin
These constants represent the digital pins where the LEDs are connected, previously defined in the code as follows:
const uint8_t GPSLedPin = 35, SDLedPin = 36, SensorLedPin = 41;
TIMER1For more information on how to use TIMER1 and the registers used for configuration refer to the ATMega2560 Datasheet.

TIMER1 Interruption

The code of the function that handles the TIMER1 interruption is shown below. Every time the function is triggered, the program checks the states of the variables blinkGPS, blinkSD and blinkSensor. Those variables are booleans which are modified within the main loop, depending on the state of the respective modules. In case a failure is found in any of the modules, the corresponding variable will be set to true and, once the interruption gets triggered, it will toggle the state of the corresponding digital pin. That way the program controls the blinking of the LEDs in relation to the status of the modules. The function also checks if the microcontroller has updated its time from an NTP server. In case it hasn’t, a request is sent to the ESP8266. For more information on the communication with the ESP8266, please refer to The IoT Package Documentation.

ISR(TIMER1_COMPA_vect)          // timer compare interrupt service routine
{
  if(blinkGPS)    digitalWrite(GPSLedPin, !digitalRead(GPSLedPin));
  else            digitalWrite(GPSLedPin, HIGH);

  if(blinkSD)     digitalWrite(SDLedPin, !digitalRead(SDLedPin));
  else            digitalWrite(SDLedPin, HIGH);
  
  if(blinkSensor) digitalWrite(SensorLedPin, !digitalRead(SensorLedPin));
  else            digitalWrite(SensorLedPin, HIGH);

  if((!TimeDriver::_already_up_to_date()))  espIoT.request_time();
}

Serial3 Interruption

The code of the function that handles the UART3 interruption is shown below. Every time data is available in the input buffer, the object espIoT will parse the String received. For more information on that, please refer to The IoT Package Documentation.

void serialEvent3()
{
  if(Serial3.available())
  {
    espIoT.parse_esp_string(Serial3.readStringUntil(';'));
  }
}

The main loop

The main loop is executed within the function loop()of the Arduino framework, as shown in the code below. This function is responsible for handling the four main functionalities described previously, following a sequence as illustrated in Figure 1.

void loop()
{
  static uint32_t mLastTime = 0, mLastTimeGPS = 0;
  static uint32_t mLastTimeuSD = 0,mLastTimeHTTP = 0;
  
  espIoT.watch_dog();

  if((millis() - mLastTimeuSD) >= uSD_TIME_MSEC)  // Counts for sample time
  {
    mLastTimeuSD = millis();
    static uint8_t data_index_uSD = 0;
    Vars[data_index_uSD]->sense(data[data_index_uSD]);
    
    char* filename = (char*)malloc(strlen_P(filenames[data_index_uSD])+1);
    strcpy_P(filename, filenames[data_index_uSD]);
    if(open_file(filename))
    {
      blinkSD = !save_to_file<sensorData<Variable>>(data[data_index_uSD], filename);
    }
    else  SD.begin(CHIPSEL_PIN);
    free(filename);

    data_index_uSD = (data_index_uSD >= numSensors-1) ? 0 : data_index_uSD + 1;
  }
  
  if((millis() - mLastTimeHTTP) >= HTTP_TIME_SEC*1000)  // Counts for sample time
  {
    mLastTimeHTTP = millis();

    static uint8_t data_index_iot = 0;
    if(!blinkSD)
    {
      if(!espIoT.send_http_post((DataContainer*)data[data_index_iot]))  print_debug("Couldn't post!");
      data_index_iot = (data_index_iot >= numSensors-1) ? 0 : data_index_iot + 1;
    }
  }
  
  if ((millis() - mLastTime) >= SAMPLE_TIME_SEC*1000)
  {
    mLastTime = millis();
    static uint8_t sensores_index = SPEC_ID;
    blinkSensor = !(specSensors[sensores_index]->readSensor(Vars[sensores_index]->getSmoother(),Vars[TEMP_ID]->getSmoother(), Vars[HUMD_ID]->getSmoother()) > 0);
    sensores_index = (sensores_index >= numSpec-1) ? 0 : sensores_index + 1;
  }
  
  if((millis() - mLastTimeGPS) >= MSECS_GPSOUTDATE)  // Counts for sample time
  {
    mLastTimeGPS = millis();
    
    blinkGPS = !gps.readGPS(MSECS_GPSOUTDATE/2);
  }
}

The code of loop()is divided into four sections that execute the actions related to each functionality. Each section is executed periodically controlled by the variables mLastTime, mLastTimeGPS, mLastTimeuSD, and mLastTimeHTTP, that store the timestamp when each functionality was last executed, and the constants uSD_TIME_MSEC, HTTP_TIME_SEC, SAMPLE_TIME_SEC, and MSECS_GPSOUTDATE, that represent the periods, as summarized in Table 2. In every loop cycle, the object espIoT calls its method watch_dog() in order to check if any request sent to the ESP8266 has experienced a timeout. In case it has, the ESP8266 will be restarted. For more information on the connection and communication between the Arduino MEGA and the ESP8266, please refer to The CLEAN Mobile Monitor Prototype, The ESP8266 Firmware, and The IoT Package Documentation.

Table 2. Constants and variables used for controlling the execution of each functionality in the firmware

FunctionalityPeriodPeriod constant in the codeDefinition of the period constant in the codeControl variable
Storing data to SD Card5 suSD_TIME_MSECconst uint32_t uSD_TIME_MSEC = 5000;
Defined in main.cpp
mLastTimeuSD
Sending data for HTTP POST100 sHTTP_TIME_SECconst uint32_t HTTP_TIME_SEC = 100;
Defined in main.cpp
mLastTimeHTTP
Reading gas sensors5 sSAMPLE_TIME_SECconst uint8_t SAMPLE_TIME_SEC = 5;
Defined in main.cpp
mLastTime
Reading GPS information7 sMSECS_GPSOUTDATE#define MSECS_GPSOUTDATE 7000UL
Defined in serial-geo-interface.h
mLastTimeGPS
Reading the gas sensors

The section of code that reads the gas sensors is shown below:

if ((millis() - mLastTime) >= SAMPLE_TIME_SEC*1000)
{
  mLastTime = millis();
  static uint8_t sensores_index = SPEC_ID;
  blinkSensor = !(specSensors[sensores_index]->readSensor(Vars[sensores_index]->getSmoother(),Vars[TEMP_ID]->getSmoother(), Vars[HUMD_ID]->getSmoother()) > 0);
  sensores_index = (sensores_index >= numSpec-1) ? 0 : sensores_index + 1;
}

The code first checks if the time for reading the SPEC sensors has elapsed and updates the variable mLastTime. This section of the code basically iterates in the array specSensors in order to get the gas concentration reading of each sensor, as well as the temperature and the relative humidity. For more information on the sensors used in this prototype please refer to The CLEAN Mobile Monitor Prototype.

The array specSensors is defined previously in the code as an array of pointers to the classspecDGS_sensor, as shown below. The class specDGS_sensor represents a digital sensor from SPEC Sensor and is defined in the sensor library. For more information refer to The Sensor Package Documentation.

const uint32_t CO_SERIAL  = 10730;
const uint32_t O3_SERIAL  = 21020;
const uint32_t SO2_SERIAL = 21220;
const uint32_t NO2_SERIAL = 20443;

const uint8_t numSpec = 4;
specDGS_sensor* specSensors[numSpec] = 
  { new specDGS_sensor(new specDGS_RS485(DE_S1PIN, RE_S1PIN), &RS485_2, CO_SERIAL ),
    new specDGS_sensor(new specDGS_RS485(DE_S2PIN, RE_S2PIN), &RS485_2, O3_SERIAL ),
    new specDGS_sensor(new specDGS_RS485(DE_S3PIN, RE_S3PIN), &RS485_2, SO2_SERIAL),
    new specDGS_sensor(new specDGS_RS485(DE_S4PIN, RE_S4PIN), &RS485_2, NO2_SERIAL)
  };

The code above creates an array of four pointers to the class specDGS_sensor called specSensors. Each instance of that class receives a pointer to an instance of the class serial_sensor, a pointer to an instance of the class Stream and the sensor ID. In this version of the firmware, the instance of serial_sensor is a pointer to the class specDGS_RS485, which inherits from serial_sensor. Refer to The Sensor Package Documentation for further details. The pointer to Stream is the serial interface; in this case, it’s a reference to the object RS485_2. The sensors IDs are the last five digits of the factory barcode.

The method readSensor() of the class specDGS_sensor reads the gas concentration, temperature, and relative humidity data from each SPEC sensor and stores them into a correspondent item of the array Vars. Each item in Vars is related to a sensor by the variable sensores_index. Vars is an array of pointers to the class Variable, declared previously in the code as shown below. sensores_index, on the other hand, is initialized by the constant SPEC_ID, which indicates the position of the first gas sensor in the array Vars. TEMP_ID and HUMD_ID are the positions of the temperature and relative humidity sensors respectively.

#define BUFFER_SIZE     6UL
const uint8_t numSensors = numSpec+2;
#define SPEC_ID 0
#define TEMP_ID 4
#define HUMD_ID 5

Variable* Vars[numSensors] = 
  { 
    new GasConcentration(CO_ID,   SI_CONC_ppb,      BUFFER_SIZE),
    new GasConcentration(O3_ID,   SI_CONC_ppb,      BUFFER_SIZE),
    new GasConcentration(SO2_ID,  SI_CONC_ppb,      BUFFER_SIZE),
    new GasConcentration(NO2_ID,  SI_CONC_ppb,      BUFFER_SIZE),
    new Temperature     (TEMP_ID, SI_TEMP_Celsius,  BUFFER_SIZE),
    new Humidity        (RHUM_ID, SI_HUMD_Relative, BUFFER_SIZE)
  };

Vars stores pointers to Variable, which can be GasConcentration, Temperature, Humidity and others defined in the variable library. More details on that subject can be found in The Data Package Documentation. For this prototype, the array Vars includes four variables that represent the concentration read by each gas sensor, and two variables that represent the temperature and the relative humidity. The laters store the mean value of the temperature and relative humidity sensors that are embedded into each digital sensor board. Finally, BUFFER_SIZE is the number of samples used for applying a moving average filter to each Variable.

Storing the data into an SD card

The section of code that stores the data into an SD card is shown below:

if((millis() - mLastTimeuSD) >= uSD_TIME_MSEC) 
{
  mLastTimeuSD = millis();
  static uint8_t data_index_uSD = 0;
  Vars[data_index_uSD]->sense(data[data_index_uSD]);
    
  char* filename = (char*)malloc(strlen_P(filenames[data_index_uSD])+1);
  strcpy_P(filename, filenames[data_index_uSD]);
  if(open_file(filename))
  {
    blinkSD = !save_to_file<sensorData<Variable>>(data[data_index_uSD], filename);
  }
  else  SD.begin(CHIPSEL_PIN);
  free(filename);

  data_index_uSD = (data_index_uSD >= numSensors-1) ? 0 : data_index_uSD + 1;
}

The code first checks if the time storing the data has elapsed and updates the variable mLastTimeuSD. This section of the code basically transfers the data of each Variable to a sensorData object that will subsequently be used to storing the information of each variable. The method used for transferring the information in Variable to sensorData is sense(), which receives a pointer to sensorData. In this case, the function receives an item of the array data, which is an array of pointers to sensorData<Variable>. This array is defined previously in the code as shown below. Once the data have been transferred to the instance of sensorData, it is stored into a file in the SD card. For more information, please refer to The Data Package Documentation.

sensorData<Variable>* data[numSensors] = 
  {
    new sensorData<Variable>(Vars[SPEC_ID]),
    new sensorData<Variable>(Vars[SPEC_ID+1]),
    new sensorData<Variable>(Vars[SPEC_ID+2]),
    new sensorData<Variable>(Vars[SPEC_ID+3]),
    new sensorData<Variable>(Vars[TEMP_ID]),
    new sensorData<Variable>(Vars[HUMD_ID])
  };

const char CO_log   [] PROGMEM = "CO_log.csv\0"  ;
const char O3_log   [] PROGMEM = "O3_log.csv\0"  ;
const char SO2_log  [] PROGMEM = "SO2_log.csv\0" ;
const char NO2_log  [] PROGMEM = "NO2_log.csv\0" ;
const char TEMP_log [] PROGMEM = "TMP_log.csv\0" ;
const char RH_log   [] PROGMEM = "RH_log.csv\0"  ;

const char* filenames[numSensors] = {
                                      CO_log, O3_log, SO2_log, NO2_log,
                                      TEMP_log, RH_log
                                    };
Sending data for an HTTP POST

The section of code that sends the data to the ESP8266 for posting into an HTTP server is shown below:

if((millis() - mLastTimeHTTP) >= HTTP_TIME_SEC*1000)
{
  mLastTimeHTTP = millis();

  static uint8_t data_index_iot = 0;
  if(!blinkSD)
  {
    if(!espIoT.send_http_post((DataContainer*)data[data_index_iot]))  print_debug("Couldn't post!");
    data_index_iot = (data_index_iot >= numSensors-1) ? 0 : data_index_iot + 1;
  }
}

Like the other sections of code, this section first checks if the time storing the data has elapsed and updates the variable mLastTimeHTTP. After that, it iterates in the array data in order to send the information acquired for each Variable. If the last sample was successfully written to the SD card, the espIoT object will send a JSON string containing the information to be posted by the ESP8266. The method send_http_post() receives a pointer to DataContainer. Since the class sensorData inherits from DataContainer, each item in data can be converted to a pointer to DataContainer. For more information refer to The Data Package Documentation.

Reading the GPS information

Finally, the section of code that updates the geolocation information from the GPS module is shown below:

if((millis() - mLastTimeGPS) >= MSECS_GPSOUTDATE)
{
  mLastTimeGPS = millis();
    
  blinkGPS = !gps.readGPS(MSECS_GPSOUTDATE/2);
}

Like the other sections of code, this section first checks if the time for updating the GPS information has elapsed and updates the variable mLastTimeGPS. For reading the GPS module, the object gps invokes the method readGPS(). This method receives as a parameter the maximum time the Arduino should wait for a response from the GPS module, in this case MSECS_GPSOUTDATE/2. The object gps is an instance of the class TinyGPSSerialInterface which is defined previously in the code as shown below. The constructor of this object receives a reference to the serial port used for communicating with the module, in this case Serial1. For more information refer to The Hardware Interfaces Package Documentation.

TinyGPSSerialInterface gps(&Serial1);

Translate