The firmware of the devices was developed on the Arduino Framework. This Framework is an abstraction of source codes and libraries that are common to various hardware platforms. The framework makes it possible to write programs to control a wide range of devices embedded into Arduino and other manufacturers’ boards. It provides code libraries written in C/C++ for programming microcontrollers and for interfacing with peripheral devices.
For programming all the firmware’s functionalities, the code was structured in a set of classes. This structure was conceived aiming at its reuse in other microcontrollers platforms and other hardware components supported in the Arduino Framework (such as ESP8266 from Espressif) and also to facilitate code review and maintenance. The classes developed for the project are distributed in four main library packages, as shown in Figure 1: the the Hardware Interfaces package, the System Drivers package, the Sensors package and the Data package.
The Hardware Interfaces package groups all the classes and structures used for interfacing the peripheral hardware, such as sensors, timing modules, geolocation modules, and storage modules. The Drivers implement functionalities that can be used by the main program independently of the hardware used in each device. The Sensors package is at the same level as the Drivers and could be interpreted as a set of special drivers for the sensors, but with the peculiarity of being specific for each manufacturer. Finally, the Data package encompasses all the functionalities that are related to the preparation of sensor data for storage and transmission. This package abstracts the concentration information acquired by the gas sensors from specific details about the functioning and operation of their hardware.
The main program of each device is executed in the loop() function of the Arduino Framework. This function is executed as an infinite loop that contains the main sequence of instructions. To make the code easier to understand, review and reuse, we have separated the main loop into five sequences that run periodically. Figure 2 illustrates the flowchart of the main loop with its five sequence.
As mentioned before, the sequences are executed periodically, at time intervals which are defined by the constants uSD_TIME, HTTP_TIME, IoT_TIME, SAMPLE_TIME, GPS_TIME. In the flowchart, the variables that appear with the ‘dt_’ prefix in the conditional comparison blocks represent the time that has elapsed since the last corresponding sequence was executed. The geolocation sequence was programmed only in the mobile version firmware, that’s why it appears in blue color in the diagram.
The Hardware Interfaces package
The Hardware Interfaces encompass the functionalities related to communication and interfacing of gas sensors, geopositioning modules (GPS), and real-time clock (RTC) that were used in the prototypes (refer to the Hardware Documentation for details). The operating mode and the output of the sensors and the other devices will determine their connection to the microcontroller and the way their reading is implemented in the firmware.
Figure 3 shows a diagram of the classes that were implemented for the current version of the firmware. The SerialSensorInterface
and the AnalogSensorInterface
implement interfaces for digital and analog sensors respectively. The SerialSensorInterface
, in particular, can implement the interface for a digital sensor connected via a UART bus or an RS-485 bus, by means of the child classes UARTSensorInterface
and RS485SensorInterface
. Each class implements its own sense()
method. This method receives as parameters a pointer to a Stream
object (generally a serial port of the microcontroller), and a pointer to a SerialParser
, which parses the strings of commands/data sent by the digital sensor. The SerialParser
is implemented as a higher-level layer by the classes in the Sensor package.
The interface with a serial device for an internet connection was implemented through the classes SerialInternetInterface
and ESPSerialInterface
, the latter representing the connection with the ESP8266 microcontroller. Two more interfaces were created for GPS and RTC modules. In the current version of the firmware, we used the libraries TinyGPSPlus
and RtcDS3221
for each module respectively. However, any other library or module could be used as well, as long as it is created as a child of GPSSerialInterface
and RTCInterface
and implement their own virtual methods: readGPS()
, set_time()
and get_time()
repectively.
The System Drivers package
The Drivers act as an intermediate layer between the Hardware Interfaces and the main program. They abstract the hardware of the devices from the main code, allowing its reuse independent from the modules and libraries used at a lower level. Some drivers implemented for the firmware were the HardStorage
driver, for data storage in an SD Card; the RTCDriver
for the RTC Interface; the GPSDriver
for the GPS Interface; and the TimeDriver
for managing the time sources in the device, which can be provided by an RTC module, a GPS module, or an NTP server. Those four drivers use statics methods, which means that they can be used without an object. The other two drivers implemented are related to the handling of the data, these are the DataContainer
and the Smoother
. Figure 4 shows the class diagram of this package and Table 1 summarizes some of the main methods and attributes of each class.
Table 1. Properties of the classes related to the Drivers Package
Class description | Attributes and methods |
TimeDriver: this class keeps track of the system’s internal date and time and provides methods to return date and time information in different formats | {static} set_time(time_t): void This method sets the system date and time. Internally it invokes the setTime() method from the Time.h library of the Arduino framework. It receives as a parameter a 32-bit integer containing the date and time provided by some external clock source (a GPS module, an RTC module, or an NTP server). |
GPSDriver: This class controls the interface with a GPS module. It stores the information of the geographical coordinates of the system and provides methods to access them. | {static} get_latitude(): double {static} get_longitude(): double {static} get_altitude(): double {static} get_gps_st(): GPSSt_e These methods provide the geolocation information stored in the GPSDriver as well as the state of that information. The geolocation information could be OK or Outdated. Those two values are returned as an enumeration of type GPSSt_e .{static} set_coordinates(): void {static} set_gps_state(): void These two methods set the geographical coordinates of the system and the state of that information. These methods are called by an instance of a GPSInterface . |
RTCDriver: this class controls the interface with an RTC module | {static} update_rtc(RTCInterface*, time_t): void This method is called any time the RTC module needs to be updated. Receives as a parameter a pointer to the instance of RTCInterface which will be updated and the timestamp.{static} sync_time_from_RTC(RTCInterface*): time_t This method returns the timestamp from the pointer to the RTCInterface passed as parameter |
DataContainer: this is an abstract class that contains information about a reading of a variable. Such information is the id of the variable being measured and its value, the coordinates, and the timestamp where the variable was measured. Objects of this class are used for storing data into the SD card and for sending HTTP posts. | {virtual} toCSV(Print*): size_t = 0 This is a pure virtual method for formatting the data of a variable reading and storing it into a CSV file. Since this is a virtual method, it should be implemented by children of DataContainer in higher layers. This way each application can have its own format for storing the information. |
HardStorage : this library holds the methods for reading and writing from and into an SD Card. | {static} open_file(const char*): void This method opens the file into which the read/write operations will be executed. The method receives the file name as a parameter. {static} save_to_file<T>(DataContainer*, const char*): double This method writes data into a file in the SD Card. The file name is passed as a parameter, together with the data to be saved. The function expects a pointer to a DataContainer , which in the current version of the firmware are objects of type SensorData . The SensorData object implements the method toCSV(Print*) , which receives a pointer to the File and stores the data into it. |
The Sensors Package
The classes in this package encapsulate the logic for reading each sensor, considering the specifications of each manufacturer. They make use of the sensors interfaces implemented in the Hardware Interface package. Two sensor manufacturers were used in the hardware of the prototypes: Alphasense and SPEC Sensors. The interfaces for Alphasense and SPEC sensors differ in the way they were implemented. The outputs of Alphasense sensors are two analog voltage signals. SPEC sensors, on the other hand, provide the gas concentration, temperature, and humidity value in a string that is sent through a UART interface. Figure 5 shows a diagram of the classes implemented for this package.
The basis of the interface with Alphasense sensors is the reading of an analog input from the microcontroller using the analogRead()
function of the Arduino framework. Therefore, the base class for modeling the Alphasense sensors is the class AnalogSensorInterface
. The latter class represents an analog input identified by the _inputPin
attribute and whose read_mv()
method converts the digital value acquired by the analog-digital converter of the Arduino, into a voltage value between 0 – 5000 mV. This method can receive as a parameter a reference to an object of the type Smoother
associated with a Variable
object, thus linking the physical variables modeled in the firmware with the respective analog hardware interface.
The HeatSensor
class represents a sensor that needs a warm-up time to work. The logic that determines the validity of the sensor readings is implemented within this class, taking into account a warm-up period for each sensor. From HeatSensor
derive the classes that represent both Alphasense and SPEC sensors, since both are amperometric electrochemical sensors that require a heating interval to ensure that the readings are valid. Table 2 summarizes the main properties of the classes related to Alphasense sensors.
Table 2. Properties of the classes related to the Alphasense sensors
Class description | Attributes and methods |
AlphaSenseISB : represents an Alphasense sensor with an ISB conditioning circuit. The suffix ISB indicates that the conditioning circuit used is the individual sensing board from the sensor manufacturer (See Alphasense Sensors Conditioning Interface). This kind class doesn’t incorporate any compensation algorithm. | _we: AnalogSensorInterface This is a private attribute representing the analog input connected to the working electrode (WE) of the sensor readConc(): double These are public methods that convert the voltage value read by the _we attribute into a concentration value, taking into account the sensor sensitivity reported by the manufacturer. The reference to aSmoother object associates the sensor with the corresponding physical variable and returns a smooth value of the variable readings. |
AlphaSenseCompensator : derived from AlphaSenseISB , represents an Alphasense sensor with a compensation algorithm. The Alphasense B4 series sensors can use different compensation algorithms depending on the gas they are sensitive to. For that reason, each algorithm is inherent to each object and not to the class | _ae: AnalogSensorInterface This is a private attribute that represents the output of the auxiliary electrode (AE) of the electrochemical sensor. The output value of this electrode is used in the compensation algorithms (*comp_algorithm) This is a pointer to the function that implements the compensation algorithm. The functions receive the variables required in the computation of the algorithm as parameters, among them the temperature. See the Alphasense application note for more details. {virtual} readConc_Comp( These are public methods that read the voltage values stored in the attributes _we (inherited from AlphaSenseISB ) and _ae attributes. They apply the corresponding compensation algorithm and return a concentration value. Both methods receive as parameters the environmental temperature and a reference to a Smoother object, as in AlphaSenseISB . |
AlphaOXCompensator : this is a special case for ozone sensors that use a compensation algorithm. Ozone sensors actually measure the sum of the concentrations of ozone and nitrogen dioxide, therefore the value of the concentration of nitrogen dioxide is required by the compensation algorithm. | _no2: AlphaSenseCompensator* For accessing the nitrogen dioxide sensor, the AlphaOXCompensator class uses a pointer to an AlphaSenseCompensator object that represents the nitrogen dioxide sensorreadConc_Comp( This method reads the concentration value of the ozone sensor and applies a compensation algorithm considering also the concentration of nitrogen dioxide. To link these readings with an object of type Variable , class AlphaOXCompensator uses the same readConc_Comp() method inherited from class AlphaSenseCompensator , which receives a reference to an object of type Smoother . |
The interface with the SPEC sensors is performed through the abstract class SerialSensorInterface
. This class provides methods for reading the sensors through the microcontroller’s serial port. The communication between the sensors and the Arduino can be implemented via a UART interface or via an RS-485 bus (refer to the Hardware Documentation for details). Both communication interfaces are modeled in the classes UARTSensorInterface
and RS485SensorInterface
, that derive from SerialSensorInterface
.
The class specDGS_sensor
functions as an intermediary layer between the hardware interface and the classes in the Data package. As it represents an electrochemical sensor that needs a warm-up period, it is also a child of the HeatSensor
class. The instances of specDGS_sensor
have the purpose of reading and analyzing the strings sent by the SPEC sensors, with the temperature, humidity, and gas concentration readings, as well as validating these readings taking into account the heating time of the sensors and possible errors in serial communication. The method readSensor()
reads these concentration, temperature, and humidity values and makes them available to corresponding Variable
objects through the Smoother
references it receives as parameters.
The _serial
attribute of the specDGS_sensor
class is a pointer to an object of the type SerialSensorInterface
. The reference of this pointer is assigned during the construction of each specDGS_sensor
instance, and it can be either an object of type UARTSensorInterface
or RS485SensorInterface
. It only depends on the communication interface implemented in the hardware. The specDGS_sensor
objects represent the SPEC sensors. The instances derived from the abstract class SerialSensorInterface
represent the interface with these sensors, which in the actual hardware is a single serial port.
The Data Package
Figure 6 shows the class diagram of the Data Package. As already mentioned, this package works as an intermediate layer that prepares and formats the measurements obtained from the sensor hardware for storage and transmission. It is formed by two main classes: Variable
and SensorData
.
Each object of SensorData
is associated with a single object of type Variable
, which represents a physical variable with a unique identifier. It is noteworthy that, although in firmware each physical variable is represented by a single identifier, in hardware one or more of these variables can be linked to the same transducer. The identification number that represents each physical variable is what associates each object of class Variable
with the object of the corresponding SensorData
type. This number is stored in each class the 32-bit integer attributes _sensorID
and _id
.
Objects of type SensorData
contain the quantities of the physical variables to which they are associated, together with information about the timestamp and location where the measurements were taken. The quantity of each variable is stored in the_value
attribute. The value stored in _value
can be the raw data measured in an instant or an average of values acquired over a period of time. The method toCSV()
consolidates and prepares the information of the value measured by the sensor, to CSV format, its geolocation, and the date and time when the measurement was taken. This method prepares the data for remote transmission and storage.
The class Variable
acts as an intermediate layer between the sensor hardware layer and the SensorData
class. Objects of this class represent the physical variables being monitored, however, they do not contain their quantities, as these values are stored in objects of SensorData
. As already mentioned, the _id
attribute contains the identifier of the physical variable it represents. The _unit
attribute represents the unit of measurement of the physical variable being monitored. The attribute _var
, of type Smoother
, works as a memory buffer in which objects that implement the hardware interface of the sensors can place the samples of the measured variable, and from which the associated SensorData
object can extract the average value of these samples. The number of samples depends on the programmed buffer capacity. The diagrams in Figures 7a and b illustrate this process.
Figure 7. Read-write process on the class Smoother
(left). Sequence diagram of the sense()
method (right).
The objects that represent the sensors write the readings of the physical variable, into the sampling buffer, via the smooth()
method of the Smoother
class (Figure 7a). The Variable
class, on the other hand, accesses the average of the samples by invoking the getAve()
method of the _var
attribute (Smoother
type), and transfers this value to the SensorData
object assigned to it, invoking the setValue()
method. The process of reading and transferring the mean value of the samples to the SensorData
object happens within the sense()
method of the class Variable
. Figure 7b shows the sequence diagram for this method.
The sense()
function is a pure virtual method, therefore, the instances that derive from the abstract class Variable
must implement it. However, the main sequence of actions performed by the method is common to all the derived classes, and is shown in Figure 4b. When the sense()
method is invoked, the Variable-derived class accesses, through the method getAve()
of the class Smoother
class, the mean value of the samples. This value is then passed to the object of type SensorData
associated with this variable. This is done through the method setValue()
. Likewise, the object of type SensorData
stores the date, time, and the location where the measurement was taken, as well as the status of the reading (method getReadSt
).
The types of physical variables implemented in the current version of the firmware were: Temperature, Humidity, Gas concentration, and Analog variable. The latter representing a generic analog variable voltage that could be read as a voltage signal between 0 – 5 V. These variables were modeled in classes derived from Variable
, i.e.: Humidity
, AnalogInput
, GasConcentration
and Temperature
. Since they are child classes, they all have the same attributes as Variable
, however, each implements its own sense()
method.