Author: Covor Alexandra, ACES
Project repository: https://github.com/Alexandra182/AirQuality-EdgeImpulse-ESP32
Demo video: https://www.youtube.com/watch?v=48sH_nrRz8E
The purpose of this project is to identify anomalies in air quality data through embedded Machine Learning, using the Edge Impulse platform to build and train a model to be deployed on an ESP32 development board. Firstly, the training data will be collected through the ESP32 and sent to the Edge Impulse platform. The ML model will be created and trained online, after which it will be deployed on the ESP32. The program will enable the user to set up the WiFi connection through a web page. The data from the air quality sensor, as well as the output of the inference, will be displayed on a webpage, through a web server running on the ESP32 development board.
A potential future application for this project is an artificial nose, such as the one implemented in this project:
The components used in this project are:
The ESP32 development board is not officially supported by Edge Impulse, but their Porting guide provides instructions on how to connect it to the platform. The first option to do this, and also the fastest and easiest to implement, is the Data Forwarder, which allows collecting data from the development board over a serial connection. The second option, and the one I chose to implement in this project, is sending data remotely, directly from the device, using the Ingestion API and (optionally) the Remote management protocol. After creating a project on Edge Impulse, using the Remote management protocol, I first opened a WebSocket connection from the ESP32 to ws://remote-mgmt.edgeimpulse.com and I sent a JSON-encoded Hello request:
{ "hello": { "version": 3, "apiKey": "<API_KEY>", "deviceId": "<DEVICE_ID>", "deviceType": "ESP32_DEV", "connection": "ip", "sensors": [{ "name": "Air Quality", "frequencies": [0], "maxSampleLengthS": 60000 }], "supportsSnapshotStreaming": false } }
The logic diagram for this program is presented below.
After this, the ESP32 appeared in the devices list on the platform:
The code uses the ArduinoWebsockets library and it can be found in the hello_request.ino file.
Next, using the Ingestion service, I collected data from the CCS811 air quality sensor and stored it on Edge Impulse. The code is based on Edge Impulse’s C SDK Usage Guide and Rafael Bidese’s repository. I created two Arduino libraries containing the header files I used from these repositories (EdgeImpulse and QCBOR). The ESP32 sends data in the Edge Impulse Data acquisition format (encoded using CBOR), and signs it with an HMAC key. The project details can be configured in the payload header:
sensor_aq_payload_info payload = { // Unique device ID (optional) "24:0A:C4:05:75:E0", // Device type (required) "ESP32_DEV", // How often new data is sampled 1 / SAMPLE_TIME, // The axes which you'll use { { "CO2", "ppm" }, { "TVOC", "ppb" } } };
The program reads 100 samples from the air quality sensor and stores them in the values vector. I used an LED connected to a PWM-capable pin of the ESP32 to get some visual feedback on this loop. The LED brightness intensifies as the buffer is filled with samples.
float values[SAMPLE_TIME * SAMPLE_RATE][2] = { 0 }; uint16_t values_ix = 0; while (values_ix < 100) { uint64_t next_tick = micros() + SAMPLE_RATE * 10; float co2, tvoc; if (mySensor.dataAvailable()) { mySensor.readAlgorithmResults(); values[values_ix][0] = mySensor.getCO2(); values[values_ix][1] = mySensor.getTVOC(); Serial.printf("co2: (%f), ", values[values_ix][0]); Serial.printf("tvoc (%f)\n", values[values_ix][1]); values_ix++; ledcWrite(ledChannel, values_ix * 2); while (micros() < next_tick) { /* blocking loop */ } } }
After all the samples are collected, the data is encoded and signed, then sent to Edge Impulse through an HTTP POST request to:
http://ingestion.edgeimpulse.com/api/training/data
The logic diagram for this program is presented below.
The collected data can be visualized on Edge Impulse in the Data Acquisition menu:
After collecting enough samples from the air quality sensor, I labelled all the samples with the normal or anomaly labels and then I performed a train/test split to balance the data. By default, the program sent all the samples to the Training data category.
Sample labelled as anomaly:
Sample labelled as normal:
Then I created an impulse and I extracted the features from the samples:
The following step was to train the NN Classifier. I created a model with 5 layers: the input layer, a dense layer with 10 neurons, a dense layer with 20 neurons, a dense layer with 30 neurons, and the output layer having 2 neurons, corresponding to the two classes I wanted to identify (normal and anomaly).
The last step was to deploy the trained model to the ESP32 development board. From the Deployment menu on Edge Impulse, I downloaded the Arduino library with the trained model and added it to my project. I used the instructions and the code sample provided here and here to feed data from the air quality sensor to the classifier network. Also, in order to visualise the sensor readings and the inference output, I used this tutorial to implement a webpage running on the ESP32. For implementing the webpage used for configuring the WiFi connection, I used this tutorial.
The full code can be found in the inference_web_server.ino file.
The user has to connect to the Access Point named ESP-WIFI-MANAGER, access the following IP from a web browser: 192.168.4.1, and fill in the SSID and the password of the network:
The next step is connecting to the WiFi network and accessing the previously assigned IP from a web browser, which will display the Air Quality dashboard:
Below is the logic diagram of the Arduino program:
The inference_web_server.ino file contains the getSensorReadings() function, which constructs a JSON object with the sensor readings and the inference result:
String getSensorReadings() { airQualitySensor.readAlgorithmResults(); readings["co2"] = String(airQualitySensor.getCO2()); readings["tvoc"] = String(airQualitySensor.getTVOC()); readings["result"] = String(anomalyScore); String jsonString = JSON.stringify(readings); Serial.println(jsonString); return jsonString; }
The program reads the CO2 and TVOC values from the Air Quality sensor and displays them on the webpage. When 100 samples are collected, it runs the ML model on them and outputs the prediction scores for each label (anomaly, normal). The anomaly score is also updated in the JSON String.
The chart displays 100 data points at once and it updates automatically every second using Sever-Sent Events (SSE).
The HTML, CSS, and JavaScript files necessary for the web page are stored on the board’s filesystem (SPIFFS) and can be found in the data folder of the inference_web_server Arduino sketch.
In the index.html file there is a <div> section with the id inference where the output from the ML model is displayed, and below it, there is another <div> section with the id chart-aq where the chart is rendered:
<div class="topnav"> <h1>ESP32 Air Quality</h1> </div> <div class="content"> <p class="card-title">Inference Result</p> <div class="inference">Anomaly score: <span id="inference"></span></div> <div class="card-grid"> <div class="card"> <p class="card-title">Air Quality Chart</p> <div id="chart-aq" class="chart-container"></div> </div> </div> </div> <script src="script.js"></script>
The index.html file calls script.js, where the inference and the chart data points are read:
const inferenceElement = document.getElementById('inference'); var chartT = new Highcharts.Chart({ chart:{ renderTo:'chart-aq' }, series: [ { name: 'CO2', type: 'line', color: '#101D42', marker: { symbol: 'circle', radius: 3, fillColor: '#101D42', } }, { name: 'TVOC', type: 'line', color: '#00A6A6', marker: { symbol: 'square', radius: 3, fillColor: '#00A6A6', } }, ], title: { text: undefined }, xAxis: { type: 'datetime', dateTimeLabelFormats: { second: '%H:%M:%S' } }, yAxis: { title: { text: 'ppm/ppb' } }, credits: { enabled: false } });
The getReadings() function performs a GET request to the server on the /readings URL and receives the JSON string from which it parses the values:
function getReadings(){ var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var myObj = JSON.parse(this.responseText); console.log(myObj); plotAirQuality(myObj); inferenceElement.innerHTML = myObj.result; } }; xhr.open("GET", "/readings", true); xhr.send(); }
Future improvements to the project include gathering more data in order to properly train the model, as the current implementation is more of a proof of concept, and implementing alerts when anomalies in the data are detected.