CyberSpy

Rantings from a guy with way too much free time

Baby it's Cold Outside -- How Cold? Use an SHT31 and See!

2019-01-29 programming Rob Baruch

ESP32 and SHT31 Temperature and Humidity Sensor Secure Web Server

In this blog post, I follow-up on the ESP32-IDF development and demonstrate how to incorporate an i2c device to measure the temperature and humidity. Specifically the SHT31. First, let’s do it plain-jane vanilla. We’ll incorporate the i2c device and simply display the measurements to the serial terminal through the uart. Next, we’ll make it more practical and send the data over the wifi network to a service that makes it convenient to visualize the time-series data.

We can do this the easy way, or the hard way. What’s it going to be?

In order to use our SHT31 sensor, we’ll need to read the datasheet, understand how the i2c protocol works, and then write the code that drives the chip. It’s not insurmountable but it’s beyond the scope of what I want to do in this post (maybe we’ll dive into that in the next one!). Fortunately, someone has already done this for us! All we need to do is drop this component into our build for our project and we can incorporate the code to drive the device. Just like the previous post, I’ve modified the code to accommodate our ESP32 SparkFun Thing device. You can find the repo here.

Steps to incorporate the component

Let’s start off with a blank project and follow the steps below to incorporate our component, modify the build configuration, and finally, flash the device.

STEP 1: Create a new Project

Let’s make a copy of the basic hello_world project in the examples directory.

$ cd $IDF_PATH
$ cp -r $IDF_PATH//examples/get-started/hello_world  sht-demo

Next let’s grab the repo for the SHT31 component and copy the component into our components directory:

$ git clone https://github.com/rabarar/ESP32_SHT31
$ cp -r ./ESP32_SHT31/sht31 $IDF_PATH/components/.

STEP 2: Modify our code to incorporate the SHT31 device

Open your editor and look at the basic code in the project. Specifically, the main/hello_world_main.c code.

/* Hello World Example

   This example code is in the Public Domain (or CC0 licensed, at your option.)

   Unless required by applicable law or agreed to in writing, this
   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
   CONDITIONS OF ANY KIND, either express or implied.
*/
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_spi_flash.h"


void app_main()
{
    printf("Hello world!\n");

    /* Print chip information */
    esp_chip_info_t chip_info;
    esp_chip_info(&chip_info);
    printf("This is ESP32 chip with %d CPU cores, WiFi%s%s, ",
            chip_info.cores,
            (chip_info.features & CHIP_FEATURE_BT) ? "/BT" : "",
            (chip_info.features & CHIP_FEATURE_BLE) ? "/BLE" : "");

    printf("silicon revision %d, ", chip_info.revision);

    printf("%dMB %s flash\n", spi_flash_get_chip_size() / (1024 * 1024),
            (chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" : "external");

    for (int i = 10; i >= 0; i--) {
        printf("Restarting in %d seconds...\n", i);
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
    printf("Restarting now.\n");
    fflush(stdout);
    esp_restart();
}

Let’s take this code and modify it to do the same thing, but instead of just counting down and rebooting, let’s report the temperature and humidity in the countdown.

In order to do that we first need to add a few header files. Before the declaration of the void app_main[ ] function, add the following code:

#include "esp_log.h"
#include "sht31.h"
#include "sdkconfig.h"

static char tag[] = "sht-demo";

Next, let’s add the code to initialize the SHT31. After we print Hello World, add the following:

    ESP_LOGI(tag, "sht31 initialize");
    sht31_init();

We initialize the logger with our tag, and then we initialize the SHT31 device. Now that it’s initialized, let’s add the following code to our for-loop to read the sensor:

    if (sht31_readTempHum()) {
        float h = sht31_readHumidity();
        float t = sht31_readTemperature();
        ESP_LOGI(tag, "Humidity, Temp(c) : %.f, %.f", h, t);
    } else {
        ESP_LOGI(tag, "sht31_readTempHum : failed");
    }

That’s it! We’re now ready to configure, build, and flash the code:

STEP 3: Configure, Build, and Flash

Let’s follow the steps from our previous post to configure our project - specifically, setting the USB device, setting the clock speeds for both the ESP32 and the UART. Then we make the project, make -j4 all, and flash the device make flash

If all works, bring up your serial terminal app and take a look. We should see output similar to the following:

...
I (18) cpu_start: Starting scheduler on PRO CPU.
I (0) cpu_start: Starting scheduler on APP CPU.
Hello world!
I (20) sht-demo: sht31 initialize
This is ESP32 chip with 2 CPU cores, WiFi/BT/BLE, silicon revision 0, 4MB external flash
I (730) sht-demo: Humidity, Temp(c) : 39, 22
Restarting in 10 seconds...
I (2230) sht-demo: Humidity, Temp(c) : 39, 22
Restarting in 9 seconds...
I (3730) sht-demo: Humidity, Temp(c) : 39, 22
Restarting in 8 seconds...
I (5230) sht-demo: Humidity, Temp(c) : 39, 21
Restarting in 7 seconds...
I (6730) sht-demo: Humidity, Temp(c) : 39, 21
Restarting in 6 seconds...
I (8230) sht-demo: Humidity, Temp(c) : 39, 21
Restarting in 5 seconds...
I (9730) sht-demo: Humidity, Temp(c) : 39, 21
Restarting in 4 seconds...
I (11230) sht-demo: Humidity, Temp(c) : 39, 21
Restarting in 3 seconds...
I (12730) sht-demo: Humidity, Temp(c) : 39, 21
Restarting in 2 seconds...
I (14230) sht-demo: Humidity, Temp(c) : 39, 21
Restarting in 1 seconds...
I (15730) sht-demo: Humidity, Temp(c) : 39, 21
Restarting in 0 seconds...
Restarting now.
...

Incorporating our Sensor with the Web

Okay. We’ve demonstrated that we can incorporate our sensor into our basic application, build and flash the program and see the values on the terminal display. Let’s move a step further and turn our device into a web server that publishes the values using a REST GET verb.

In order to do that, let’s take a look at the example program that shows us how to turn our ESP32 device into a web server. In the examples directory there is a https_server directory that demonstrates how to write a secure http server and serve a route. Let’s dive into the example and see what’s going on. Next, we’ll take our example above and and integrate the SHT31 sensor to serve up the data.

Below, here’s the code to turn our device into a secure web-server:

     1	/* Simple HTTP + SSL Server Example
      	
     2	   This example code is in the Public Domain (or CC0 licensed, at your option.)
      	
     3	   Unless required by applicable law or agreed to in writing, this
     4	   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
     5	   CONDITIONS OF ANY KIND, either express or implied.
     6	*/
      	
     7	#include <esp_wifi.h>
     8	#include <esp_event_loop.h>
     9	#include <esp_log.h>
    10	#include <esp_system.h>
    11	#include <nvs_flash.h>
    12	#include <sys/param.h>
      	
    13	#include <esp_https_server.h>
      	
    14	/* A simple example that demonstrates how to create GET and POST
    15	 * handlers for the web server.
    16	 * The examples use simple WiFi configuration that you can set via
    17	 * 'make menuconfig'.
    18	 * If you'd rather not, just change the below entries to strings
    19	 * with the config you want -
    20	 * ie. #define EXAMPLE_WIFI_SSID "mywifissid"
    21	*/
    22	#define EXAMPLE_WIFI_SSID CONFIG_WIFI_SSID
    23	#define EXAMPLE_WIFI_PASS CONFIG_WIFI_PASSWORD
      	
    24	static const char *TAG="APP";
      	
      	
    25	/* An HTTP GET handler */
    26	esp_err_t root_get_handler(httpd_req_t *req)
    27	{
    28	    httpd_resp_set_type(req, "text/html");
    29	    httpd_resp_send(req, "<h1>Hello Secure World!</h1>", -1); // -1 = use strlen()
      	
    30	    return ESP_OK;
    31	}
      	
    32	const httpd_uri_t root = {
    33	    .uri       = "/",
    34	    .method    = HTTP_GET,
    35	    .handler   = root_get_handler
    36	};
      	
      	
    37	httpd_handle_t start_webserver(void)
    38	{
    39	    httpd_handle_t server = NULL;
      	
    40	    // Start the httpd server
    41	    ESP_LOGI(TAG, "Starting server");
      	
    42	    httpd_ssl_config_t conf = HTTPD_SSL_CONFIG_DEFAULT();
      	
    43	    extern const unsigned char cacert_pem_start[] asm("_binary_cacert_pem_start");
    44	    extern const unsigned char cacert_pem_end[]   asm("_binary_cacert_pem_end");
    45	    conf.cacert_pem = cacert_pem_start;
    46	    conf.cacert_len = cacert_pem_end - cacert_pem_start;
      	
    47	    extern const unsigned char prvtkey_pem_start[] asm("_binary_prvtkey_pem_start");
    48	    extern const unsigned char prvtkey_pem_end[]   asm("_binary_prvtkey_pem_end");
    49	    conf.prvtkey_pem = prvtkey_pem_start;
    50	    conf.prvtkey_len = prvtkey_pem_end - prvtkey_pem_start;
      	
    51	    esp_err_t ret = httpd_ssl_start(&server, &conf);
    52	    if (ESP_OK != ret) {
    53	        ESP_LOGI(TAG, "Error starting server!");
    54	        return NULL;
    55	    }
      	
    56	    // Set URI handlers
    57	    ESP_LOGI(TAG, "Registering URI handlers");
    58	    httpd_register_uri_handler(server, &root);
    59	    return server;
    60	}
      	
    61	void stop_webserver(httpd_handle_t server)
    62	{
    63	    // Stop the httpd server
    64	    httpd_ssl_stop(server);
    65	}
      	
      	
      	
      	
    66	// ------------------------- application boilerplate ------------------------
      	
    67	static esp_err_t event_handler(void *ctx, system_event_t *event)
    68	{
    69	    httpd_handle_t *server = (httpd_handle_t *) ctx;
      	
    70	    switch(event->event_id) {
    71	    case SYSTEM_EVENT_STA_START:
    72	        ESP_LOGI(TAG, "SYSTEM_EVENT_STA_START");
    73	        ESP_ERROR_CHECK(esp_wifi_connect());
    74	        break;
    75	    case SYSTEM_EVENT_STA_GOT_IP:
    76	        ESP_LOGI(TAG, "SYSTEM_EVENT_STA_GOT_IP");
    77	        ESP_LOGI(TAG, "Got IP: '%s'",
    78	                ip4addr_ntoa(&event->event_info.got_ip.ip_info.ip));
      	
    79	        /* Start the web server */
    80	        if (*server == NULL) {
    81	            *server = start_webserver();
    82	        }
    83	        break;
    84	    case SYSTEM_EVENT_STA_DISCONNECTED:
    85	        ESP_LOGI(TAG, "SYSTEM_EVENT_STA_DISCONNECTED");
    86	        ESP_ERROR_CHECK(esp_wifi_connect());
      	
    87	        /* Stop the web server */
    88	        if (*server) {
    89	            stop_webserver(*server);
    90	            *server = NULL;
    91	        }
    92	        break;
    93	    default:
    94	        break;
    95	    }
    96	    return ESP_OK;
    97	}
      	
    98	static void initialise_wifi(void *arg)
    99	{
   100	    tcpip_adapter_init();
   101	    ESP_ERROR_CHECK(esp_event_loop_init(event_handler, arg));
   102	    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
   103	    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
   104	    ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
   105	    wifi_config_t wifi_config = {
   106	        .sta = {
   107	            .ssid = EXAMPLE_WIFI_SSID,
   108	            .password = EXAMPLE_WIFI_PASS,
   109	        },
   110	    };
   111	    ESP_LOGI(TAG, "Setting WiFi configuration SSID %s...", wifi_config.sta.ssid);
   112	    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
   113	    ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config));
   114	    ESP_ERROR_CHECK(esp_wifi_start());
   115	}
      	
   116	void app_main()
   117	{
   118	    static httpd_handle_t server = NULL;
   119	    ESP_ERROR_CHECK(nvs_flash_init());
   120	    initialise_wifi(&server);
   121	}

Let’s start from the bottom and work our way back up. The entry point for our application begins on line 116 with the void app_main() function. In our function we create an instance of httpd_handle_t and pass a reference of that to the initialise_wifi() function.

   116	void app_main()
   117	{
   118	    static httpd_handle_t server = NULL;
   119	    ESP_ERROR_CHECK(nvs_flash_init());
   120	    initialise_wifi(&server);
   121	}

Next, let’s see what’s going on in initialise_wifi() starting on line 98.

    98	static void initialise_wifi(void *arg)
    99	{
   100	    tcpip_adapter_init();
   101	    ESP_ERROR_CHECK(esp_event_loop_init(event_handler, arg));
   102	    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
   103	    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
   104	    ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
   105	    wifi_config_t wifi_config = {
   106	        .sta = {
   107	            .ssid = EXAMPLE_WIFI_SSID,
   108	            .password = EXAMPLE_WIFI_PASS,
   109	        },
   110	    };
   111	    ESP_LOGI(TAG, "Setting WiFi configuration SSID %s...", wifi_config.sta.ssid);
   112	    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
   113	    ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config));
   114	    ESP_ERROR_CHECK(esp_wifi_start());
   115	}

This function starts by initalizing the TCP/IP protocol on line 100. Next we start the event loop and check for any errors on line 101. On lines 102-114 we set the values for our SSID and PASSWORD extracted from our build configuration parameters and initialize the wifi in Station Mode.

The event hander that we set on line 101 will do all the work responding to events. Let’s look closer at this function on lines 67-97:

    67	static esp_err_t event_handler(void *ctx, system_event_t *event)
    68	{
    69	    httpd_handle_t *server = (httpd_handle_t *) ctx;
      	
    70	    switch(event->event_id) {
    71	    case SYSTEM_EVENT_STA_START:
    72	        ESP_LOGI(TAG, "SYSTEM_EVENT_STA_START");
    73	        ESP_ERROR_CHECK(esp_wifi_connect());
    74	        break;
    75	    case SYSTEM_EVENT_STA_GOT_IP:
    76	        ESP_LOGI(TAG, "SYSTEM_EVENT_STA_GOT_IP");
    77	        ESP_LOGI(TAG, "Got IP: '%s'",
    78	                ip4addr_ntoa(&event->event_info.got_ip.ip_info.ip));
      	
    79	        /* Start the web server */
    80	        if (*server == NULL) {
    81	            *server = start_webserver();
    82	        }
    83	        break;
    84	    case SYSTEM_EVENT_STA_DISCONNECTED:
    85	        ESP_LOGI(TAG, "SYSTEM_EVENT_STA_DISCONNECTED");
    86	        ESP_ERROR_CHECK(esp_wifi_connect());
      	
    87	        /* Stop the web server */
    88	        if (*server) {
    89	            stop_webserver(*server);
    90	            *server = NULL;
    91	        }
    92	        break;
    93	    default:
    94	        break;
    95	    }
    96	    return ESP_OK;
    97	}

The event processing in this section of code responds to wifi events and enumerates through starting the station mode, getting an IP address, and disconnecting.

Looking at the SYSTEM_EVENT_STA_GOT_IP case we see that we start the server if it’s not yet been started by calling the start_webserver(). Similarly, when we receive a SYSTEM_EVENT_STA_DISCONNECT event, we see if we’ve started a webserver (*server) and if so, we call the stop_server() function and reinistialize our server to NULL.

Next, let’s look at the web-server start and stop functions. On lines 37-60 we define our start_webserver() function:

    37	httpd_handle_t start_webserver(void)
    38	{
    39	    httpd_handle_t server = NULL;
      	
    40	    // Start the httpd server
    41	    ESP_LOGI(TAG, "Starting server");
      	
    42	    httpd_ssl_config_t conf = HTTPD_SSL_CONFIG_DEFAULT();
      	
    43	    extern const unsigned char cacert_pem_start[] asm("_binary_cacert_pem_start");
    44	    extern const unsigned char cacert_pem_end[]   asm("_binary_cacert_pem_end");
    45	    conf.cacert_pem = cacert_pem_start;
    46	    conf.cacert_len = cacert_pem_end - cacert_pem_start;
      	
    47	    extern const unsigned char prvtkey_pem_start[] asm("_binary_prvtkey_pem_start");
    48	    extern const unsigned char prvtkey_pem_end[]   asm("_binary_prvtkey_pem_end");
    49	    conf.prvtkey_pem = prvtkey_pem_start;
    50	    conf.prvtkey_len = prvtkey_pem_end - prvtkey_pem_start;
      	
    51	    esp_err_t ret = httpd_ssl_start(&server, &conf);
    52	    if (ESP_OK != ret) {
    53	        ESP_LOGI(TAG, "Error starting server!");
    54	        return NULL;
    55	    }
      	
    56	    // Set URI handlers
    57	    ESP_LOGI(TAG, "Registering URI handlers");
    58	    httpd_register_uri_handler(server, &root);
    59	    return server;
    60	}

This function will read the certificate and key required to start the secure server. We’ll talk more about how to generate cerificate and keys using openssl later. On line 51, we call the ESP-IDF function, httpd_ssl_start() to start our server, and on line 58, we set the routes that our server will respond to by calling the ESP-IDF function, httpd_register_uri_handlers() passing in the root defined on lines 32-36:

    32	const httpd_uri_t root = {
    33	    .uri       = "/",
    34	    .method    = HTTP_GET,
    35	    .handler   = root_get_handler
    36	};

When our server receives a REST GET request for /, it will call the root_get_handler() that we define on lines 25-31:

    25	/* An HTTP GET handler */
    26	esp_err_t root_get_handler(httpd_req_t *req)
    27	{
    28	    httpd_resp_set_type(req, "text/html");
    29	    httpd_resp_send(req, "<h1>Hello Secure World!</h1>", -1); // -1 = use strlen()
      	
    30	    return ESP_OK;
    31	}

This function sets the MIME type of the data being returned to the client from our server, and then sends the data of that type by calling the ESP-IDF function, httpd_resp_send().

And that’s it! With that we’ve successfully defined a web server that returns data to the client.

Putting it All Together - Temp/Humidity Web Server

So, let’s repeat what we did in our first example, but this time let’s copy the https_sever code above into a new project called sht31-server.

$ cp -r $IDF_PATH//examples/protocols/https_server sht31-demo
$ cd sht31-demo

Next, repeat the modifications that we made in our trivial example to incorporate the SHT31 sensor. Specifically, add the header files, and initialize the device.

#include "esp_log.h"
#include "sht31.h"
#include "sdkconfig.h"

static char tag[] = "sht31-server";

and the initialization in our app_main():

    ESP_LOGI(tag, "sht31 initialize");
    sht31_init();

Now, let’s modify the handler to grab the data and send it back to the client. Here’s the example handler and uri:

esp_err_t root_get_handler(httpd_req_t *req)
{
    httpd_resp_set_type(req, "text/html");
    httpd_resp_send(req, "<h1>Hello Secure World!</h1>", -1); // -1 = use strlen()

    return ESP_OK;
}

const httpd_uri_t root = {
    .uri       = "/",
    .method    = HTTP_GET,
    .handler   = root_get_handler
};

Let’s modify ours slightly. Instead of returning text/html, let’s return text/json. And instead of using the root path, /, let’s make it more readable. /sht31

/* An HTTP GET handler */
esp_err_t root_get_handler(httpd_req_t *req)
{
    char buffer[100];

    httpd_resp_set_type(req, "text/json");

    if (sht31_readTempHum()) {
        float h = sht31_readHumidity();
        float t = sht31_readTemperature();
        ESP_LOGI(TAG, "Humidity, Temp(c) : %.f, %.f", h, t);
        sprintf(buffer, "{\"sensor\": \"sht31\", \"values\": { \"humidity\": %f, \"temp\": %f, \"units\": \"celsius\" } }", h, t);
        httpd_resp_send(req, buffer, -1);
    } else {
        ESP_LOGI(TAG, "sht31_readTempHum : failed");
        httpd_resp_send(req, "{\"error\":\"failed to read sensor\"}", -1);
    }

    return ESP_OK;
}

const httpd_uri_t root = {
    .uri       = "/sht31",
    .method    = HTTP_GET,
    .handler   = root_get_handler
};

Now let’s build, flash, and run our code:

I (412) cpu_start: Pro cpu start user code
I (57) cpu_start: Starting scheduler on PRO CPU.
I (0) cpu_start: Starting scheduler on APP CPU.
I (59) sht31-server: sht31 initialize
I (319) wifi: wifi driver task: 3ffc066c, prio:23, stack:3584, core=0
I (319) wifi: wifi firmware version: 44ce2e2
I (319) wifi: config NVS flash: enabled
I (319) wifi: config nano formating: disabled
I (329) system_api: Base MAC address is not set, read default base MAC address from BLK0 of EFUSE
I (339) system_api: Base MAC address is not set, read default base MAC address from BLK0 of EFUSE
I (359) wifi: Init dynamic tx buffer num: 32
I (359) wifi: Init data frame dynamic rx buffer num: 32
I (359) wifi: Init management frame dynamic rx buffer num: 32
I (359) wifi: Init static rx buffer size: 1600
I (369) wifi: Init static rx buffer num: 10
I (369) wifi: Init dynamic rx buffer num: 32
I (379) sht31-server: Setting WiFi configuration SSID EWAPkids...
I (459) phy: phy_version: 4006, e540b8e, Dec 17 2018, 11:53:06, 1, 0
I (459) wifi: mode : sta (24:0a:c4:00:97:40)
I (459) sht31-server: SYSTEM_EVENT_STA_START
I (579) wifi: new:<1,0>, old:<1,0>, ap:<255,255>, sta:<1,0>, prof:1
I (1239) wifi: state: init -> auth (b0)
I (1239) wifi: state: auth -> assoc (0)
I (1249) wifi: state: assoc -> run (10)
I (1259) wifi: connected with EWAPkids, channel 1, bssid = f8:1e:df:fd:2a:29
I (1259) wifi: pm start, type: 1

I (5299) event: sta ip: 192.168.1.69, mask: 255.255.255.0, gw: 192.168.1.1
I (5299) sht31-server: SYSTEM_EVENT_STA_GOT_IP
I (5299) sht31-server: Got IP: '192.168.1.69'
I (5299) sht31-server: Starting server
I (5309) esp_https_server: Starting server
I (5389) esp_https_server: Server listening on port 443
I (5389) sht31-server: Registering URI handlers

From our output, we can see that our server is on 192.168.1.69 so if we visit the route, https://192.168.1.69/sht31 using curl, we’ll see the following output from our server:

$  curl -k https://192.168.1.69/sht31 | pp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    96  100    96    0     0     43      0  0:00:02  0:00:02 --:--:--    43
{
    "sensor": "sht31",
    "values": {
        "humidity": 39.549858,
        "temp": 21.192112,
        "units": "celsius"
    }
}

Note, pp is just a pretty json printer filter. The json we return is less pretty and indented than what we see, so pp, makes it easier to read. pp is just a simple alias that uses python: alias pp='cat ${1} | python -m json.tool'.

Next Steps

Although our server responds to the browser and returns the temperature and humidity values, a nicer solution would incorporate a gui component to visualize the data. I’ll leave that as an exercise for the reader. Lots of ways to do it! Regardless of the UI, hopefully the example above gives you enough insights to build bigger and better embedded server applications!

-- rob

Note: To generate certificats in the above example, use the openssl function as follows:

$ cd sht31-server/main/certs
$ openssl req -x509 -sha256 -newkey rsa:4096 -keyout prvkey.pem -out cacert.pem -days 365
comments powered by Disqus