Web-based Data Monitor?
The ATOM Lite ESP32 IoT Development Kit from M5Stack is a very small development board that can be used out of the box and can be powered via the USB-C interface. External sensors can be easily connected via the Grove connector. Sounds perfect, but one thing is missing: A display! For example, if I connect an environmental sensor to measure the temperature, air pressure and humidity, then I want to see the values. Otherwise, it wouldn't make sense, would it?
And let's also say that I want to use the sensor in different places in my house. And I want to read the values when I sit at my PC. That sounds exactly like a job for a wireless web-based solution.
Screenshot of the website with the data from my cozy warm office room.
Sensor
If you want to display measured values of a sensor, then of course you need a sensor. The ENV III unit from M5Stack has two sensors in one unit:
- SHT30: temperature and humidity sensor (I2C: 0x44)
- QMP6988: absolute air pressure sensor (I2C: 0x70)
The sensor unit can be connected directly to the ATOM device via the Grove connector. The values can then be read via I2C with the help of the corresponding libraries.
1 / 3 • Left: The ENV III Sensor Unit / Right: The ATOM Lite
2 / 3 • Both can be easily connected via I2C
3 / 3 • The USB-C port of the ATOM Lite is still Accessible (as Power Supply)
The M5ATOM lite device can be powered with a power bank via the USB-C port. But beware: Many power banks turn themselves off when not enough power is drawn. And this setup consumes very little power!
Simple and Clean Approach
To keep things as simple as possible, I don't want to use an external IOT platform to send the data to, I want to pull the data directly from the device via a web browser. This is not as difficult as it sounds, because there is a good web server library available for the ESP32. The basic framework for the program looks like this:
Copy Code
// Start TCP/IP-Server
server.begin();
}
void loop() {
// check for incoming clients
WiFiClient client = server.available();
if (client) {
// loop while the client's connected
while (client.connected()) {
// if there's bytes to read from the client,
if (client.available()) {
// MAGIC HAPPENs HERE
// The client sends a request
// an empty line is indicating the end
// of the client HTTP request
// I should then send the content...
}
}
}
}
The WiFi Server is configured to port 80, which is the port for HTTP. After the connection to my home WiFi is established in the setup() function, the server is started. Now, the server is listening on port 80 for Incoming requests. And this is where the fun begins, because when I connect myself to the IP address of the ATOM device via a web browser, an HTTP request is received:
Copy Code
New Client.
GET / HTTP/1.1
Host: 192.168.178.49
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7
That's more information than I need. In fact, I only need the line with the "GET", because it says what the client wants to retrieve from the server. Therefore, the code first consists of going through the client's request line by line and searching for the line that starts with a "GET".
HTTP-Requests
If you look at the requests in the serial monitor, you can see that the client is not only requesting a request for the home page "/", but also wants to load the favicon "/favicon.ico":
- GET / HTTP/1.1
- GET /favicon.ico HTTP/1.1
The first GET request signals that the client, in this case my web browser, would like to see the web server's home page. I requested this with: http://192.168.178.49/
The second GET request is something the web browser always does automatically: it asks for the favicon. This is an icon that is displayed in the web browser tab for the web page. If that is not there, then the default icon is used. With my own favicon the tab of the web browser then looks like this:
Browser tab with my own favicon for the M5ATOM web page
A favicon can be created online from an image file. For example, here. You will get a file with the name "favicon.ico". This file must then be sent to the client when it asks for it. So, we have to put this file on the ATOM device to be able to send it to the client. One method is to store the file as a char array in the PROGMEM. The PROGMEM is the Flash memory (program space), where the compiled program itself is stored. The only thing you have to do is to convert the binary image file into a char array header file.
Binary Files to char Arrays conversion
Fortunately, there's an app for that. For Windows, there is a simple command line tool. And James Swineson has written a Python script that can be used to convert binary data into char arrays. Both are well documented, so it should not be a problem to generate the arrays. Simply create a header file with the definition for the variable:
Copy Code
#include <pgmspace.h> // PROGMEM support header
// Image is stored in this array
PROGMEM const char electric_favicon[] = {
... paste converted binary data here ...
};
Then all you need to do is to paste the binary file converted as hex values in between:
Copy Code
#include <pgmspace.h> // PROGMEM support header
// Image is stored in this array
PROGMEM const char electric_favicon[] = {
0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x30, 0x30, 0x00, 0x00, 0x01, 0x00, 0x08,
0x00, 0xa8, 0x0e, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x20, 0x20, 0x00, 0x00,
.....
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
Now this new header file only needs to be included and the favicon is available as a char array. If the client then requests the favicon, it can be sent to the client via this command:
Copy Code
case GET_favicon: {
client.println("HTTP/1.1 200 OK");
client.println("Content-type:image/x-icon");
client.println();
client.write_P(electric_favicon, sizeof(electric_favicon));
break;
}
HTTP response headers always start with a response code (e.g. HTTP/1.1 200 OK) and a content-type so the client knows what's coming, then a blank line, followed by the content. At the end, the HTTP response ends with another blank line. How easy is that?
Client Connection Timeout
This method for a web server follows the classic Arduino example. It is simple and slim but has a few disadvantages. One drawback is that the "while (client.connected())"-loop loops until one of the two (the client or the server) closes the connection or gives up. If something doesn't go according to plan in the transfer, then the program hangs at that point. That's why I added a timeout that hits after 1 second:
Copy Code
// force a disconnect after 1 second
unsigned long timeout_millis = millis()+1000;
...
while (client.connected()) {
// if the client is still connected after 1 second,
// something is wrong. So kill the connection
if(millis() > timeout_millis){
Serial.println("Force Client stop!");
client.stop();
}
On a call where nothing goes wrong, the client terminates the request with an empty line (two newline characters in a row). Having received that, I send the content back and signal the end with an empty line as well. This is super fast, so I can assume that if the loop is active for more than a second, that something is wrong.
Dynamic Web Page
I created a simple web page for the presentation of the data from the environmental sensor. The page should look nice and therefore I also included a logo. The logo must be included as a char array, as described above for the favicon, so that it can be sent to the client. It will ask for it with a separate GET request. The website is programmed in plain HTML. Since it is just text, you could send it to the client directly from the code, but then the code becomes cluttered. One way to include the HTML file as a char array is to convert it just like the images with the tool. Then the code stays clean and readable.
But on the web page the current values of the sensor should be displayed. To realize this without having to change the source code of the web page (index.html), I have outsourced this to a JavaScript. Each column of the table, in which one of the values should be placed, has its own ID:
Copy Code
<table style="float: center;" cellspacing="10">
<tbody id="DataFont">
<tr>
<td style="text-align: right;">Temperature:</td>
<td id="temperatureOutput" style="letter-spacing: 0px;"></td>
</tr>
<tr>
<td style="text-align: right;">Humidity:</td>
<td id="humidityOutput"></td>
</tr>
<tr>
<td style="text-align: right;">Air Pressure:</td>
<td id="pressureOutput"></td>
</tr>
</tbody>
</table>
In the HTML code there is nothing in the field yet. This is done by a JavaScript after the pages is loaded:
Copy Code
<script>
window.onload = function(){
document.getElementById('temperatureOutput').innerHTML = temperatureValue+"°C";
document.getElementById('humidityOutput').innerHTML = humidityValue+"%";
document.getElementById('pressureOutput').innerHTML = pressureValue+"hPa";
};
</script>
The only missing things are the values that should be inserted. In detail: The three variables whose values the JavaScript wants to insert. I have outsourced them to an external JavaScript:
Copy Code
<script type="text/javascript" src="data.js"></script>
And you may already guess: the client will request this with its own GET request. So, in the program code I only need to return the JavaScript code that contains the variable definitions:
Copy Code
case GET_script: {
client.println("HTTP/1.1 200 OK");
client.println("Content-type:application/javascript");
client.println();
client.printf("var temperatureValue = %3.2f;\n", sht30_Temperature);
client.printf("var humidityValue = %3.2f;", sht30_Humidity);
client.printf("var pressureValue = %3.2f;", qmp_Pressure/100.0F);
break;
}
And that's it. A web-based data monitor without bells and whistles.
I hope you like this short example and this code can prove to be useful to some of you.
Enjoy!
Code
main.cpp C/C++
main code for the M5ATOM Lite