ESP32 Arduino: HTTP/2 GET Request

In this tutorial we will test how to perform a HTTP/2 GET request using the ESP32 and the Arduino core. The tests were performed using a DFRobot’s ESP32 module integrated in a ESP32 development board.

Introduction

In this tutorial we will test how to perform a HTTP/2 GET request using the ESP32 and the Arduino core.

We will be using the sh2lib wrapper working on top of NGHTT2. For a tutorial on how to get the sh2lib from the IDF project and adapt it to be used as an Arduino library, please check this previous tutorial.

Explaining in detail how HTTP/2 works is outside the scope of this tutorial. Nonetheless, having a basic understanding about HTTP/2 frames and streams is recommended. You can read more about these concepts here.

We will be contacting this testing endpoint, which returns as output of a GET request the IP of the client. You can test it with your browser.

The tests were performed using a DFRobot’s ESP32 module integrated in a ESP32 development board.

If you prefer a video tutorial, please check below my YouTube channel:

The code

Includes and global variables

As usual, we start our code by the library includes. We will need the WiFi.h library, so we can connect the ESP32 to a WiFi network, and the sh2lib.h, which we need to make the HTTP/2 GET request.

As covered in the previous tutorial, the sh2lib include needs to be enclosed in a extern C block. You can read more about this language linkage here.

#include "WiFi.h"

extern "C" {
#include "sh2lib.h"
}

Additionally, we will need to declare the credentials of our WiFi network, so we can connect the ESP32 to it. The needed credentials are the network name (SSID) and the password.

const char* ssid = "yourNetworkName";
const char* password = "yourNetworkPassword";

To finalize the global variables declaration, we will declare a Boolean value that will be used to indicate when the HTTP Request is finished. Its purpose will be explained in greater detail below. For now, we will initialize it to false.

bool request_finished = false;

The setup function

Moving on to the setup function, we will start by opening a serial connection, so we can output some results from out program. Then, we will connect the ESP32 to the WiFi network, making use of the previously declared global variables.

Serial.begin(115200);

WiFi.begin(ssid, password);

while (WiFi.status() != WL_CONNECTED) {
   delay(1000);
   Serial.println("Connecting to WiFi..");
}

Next, we will launch a dedicated FreeRTOS task that will handle the connection to the server and the execution of the HTTP/2 GET request, keeping the approach followed in the IDF example on which this tutorial is based. You can read more about launching FreeRTOS tasks using the Arduino core on this tutorial.

Note 1: Although launching a dedicated task to handle all the HTTP/2 related operations is a good approach since it isolates these functionalities, I’ve also tried to make the request directly from the Arduino setup function. The connection procedure works fine, but then the request is never executed. So far, I’ve not yet been able to identify the cause for the behavior.

Note 2: At the time of writing, I don’t have a clear notion of the size of the stack that the FreeRTOS task needs to have in order to be able to handle the HTTP/2 request. So, I’m using the same value from IDF’s example. For simplicity and coherence with the example, I’m also keeping the same priority value for the task.

So, in order to create a task, we call the xTaskCreate function. As first input, we pass our task function, which we will implement below. As second input, we pass a descriptive name for the task and as third input we specify the stack depth.

The fourth argument of the xTaskCreate function can be used to pass a parameter to our task function. In our case we don’t need it, so we can pass the value NULL.

As fifth argument we pass the task priority. The sixth argument can be used to obtain a handle to the task, so it can be later referenced. Nonetheless, we will also not need this handle, so we pass NULL for this argument.

xTaskCreate(http2_task, "http2_task", (1024 * 32), NULL, 5, NULL);

The full setup function can be seen below.

void setup() {
  Serial.begin(115200);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }

  xTaskCreate(http2_task, "http2_task", (1024 * 32), NULL, 5, NULL);

}

The task function

Now we need to define our FreeRTOS task function, which will contain the HTTP/2 related operations. Recall that FreeRTOS functions need to follow a pre-defined signature: they must return void and receive as input a pointer to void.

void http2_task(void *args){
// Task function implementation
}

The first thing we will do is declaring a sh2lib_handle struct, which will be used by the sh2lib function calls we will make below. Recall from the previous HTTP/2 tutorial that this struct will hold the TLS and HTTP/2 session handles.

struct sh2lib_handle hd;

Then we will establish a connection to the HTTP/2 server by calling the sh2lib_connect function. This function receives as first input the address of our handle and as second input the URL of the server, as a string.

Recall that this function returns as output the value ESP_OK in case the connection is successfully established. So, we will use this value for error checking and, if the connection fails, we don’t attempt to perform the GET request and finish the task.

if (sh2lib_connect(&hd, "https://nghttp2.org") != ESP_OK) {
   Serial.println("Error connecting to HTTP2 server");
   vTaskDelete(NULL);
}

After the connection is established, we will do the GET request setup by calling the sh2lib_do_get function. This function receives as first input the address of our sh2lib_handle and as second a string with the relative path of the server to which we want to make the request.

As third argument, this function receives a callback function that will be called for processing the response to the request. We will define this callback function later.

sh2lib_do_get(&hd, "/httpbin/ip", handle_get_response);

As mentioned, the previous sh2lib_do_get function call is only used to make the GET request setup. In order to perform the actual exchange of data with the HTTP/2 server, we need to call the sh2lib_execute function, once again passing as input the address of our handle.

We will need to call this function periodically until the request is fully processed and we have received the answer. So, we will call it in an infinite loop, which will break when our previously declared global Boolean variable is set to true (recall that it indicates when the request is finished).

Naturally, it will be the responsibility of the request callback function to set the value of this flag to true once the request is finished, as we will see below.

The sh2lib_execute returns ESP_OK if no problem occurs during the execution of the requests. So, we will use this value for error checking.

Naturally, we should introduce a small delay between each iteration of the infinite loop, so other tasks can execute.

while (1) {

   if (sh2lib_execute(&hd) != ESP_OK) {
      Serial.println("Error in send/receive");
      break;
   }

   if (request_finished) {
      break;
   }

   vTaskDelay(10);
}

After the loop breaks, we know that we have already received the answer to our request. So, after that, we will disconnect from the HTTP/2 server by calling the sh2lib_free function, passing as input the address of our handle. This function call should end the connection to the server and free the resources.

sh2lib_free(&hd);

Finally, we will delete our HTTP/2 handling task with a call to the vTaskDelete function, passing as input the value NULL, which indicates that the task is deleting itself. Note that, as indicated here, task functions cannot return (as in, using a “return” statement), but only delete themselves.

vTaskDelete(NULL);

The final task code can be seen below.

void http2_task(void *args)
{
  struct sh2lib_handle hd;

  if (sh2lib_connect(&hd, "https://nghttp2.org") != ESP_OK) {
    Serial.println("Error connecting to HTTP2 server");
    vTaskDelete(NULL);
  }

  Serial.println("Connected");

  sh2lib_do_get(&hd, "/httpbin/ip", handle_get_response);

  while (1) {

    if (sh2lib_execute(&hd) != ESP_OK) {
      Serial.println("Error in send/receive");
      break;
    }

    if (request_finished) {
      break;
    }

    vTaskDelay(10);
  }

  sh2lib_free(&hd);
  Serial.println("Disconnected");

  vTaskDelete(NULL);
}

The request handling callback function

As mentioned before, we need to specify a callback function that will be called for handling the reception of the request.

This function needs to follow a pre-defined signature. It must return an integer and it receives as input four parameters:

  • A pointer to a sh2lib_handle struct
  • A pointer to a buffer with the received data
  • The length of the data
  • An integer that corresponds to the flags indicating if the stream is closed or if a particular received frame is finished
int handle_get_response(struct sh2lib_handle *handle, const char *data, size_t len, int flags) {
// Implementation of the callback function
}

So, the first thing we will do is checking if we have received data. We can do this by checking if the parameter that contains the length of the received data is greater than zero. If it is, then we will print the data to the serial port.

We will use the printf method of the Serial object, so we can use format specifiers. We will use the %.*s format specifier, which allows to specify a string and a width. That way, we can use the length of the buffer and print it as a string, making sure we don’t read outside its boundaries.

When we use this format specifier, the width is also specified as an argument of the printf function, preceding the argument that will be formatted [1] (in our case, our buffer of data).

if (len > 0) {
    Serial.printf("%.*s\n", len, data);
}

Besides receiving the data, we need to check when the stream is closed, in order to set the global request_finished flag to true and signal the HTTP/2 task that the request is completed.

As mentioned, we can infer this by looking into the fourth argument of the callback function. Although it is an integer, the sh2lib.h has a define called DATA_RECV_RST_STREAM which allows us to check if the flag value corresponds to the stream closed event.

If it does, then we will print a message indicating the stream was closed and we set the value of the global Boolean variable request_finished to true. Note that here we don’t need to worry about synchronization primitives because this function is writing and the FreeRTOS task is reading the value of the flag, so there is no danger of concurrent updates.

if (flags == DATA_RECV_RST_STREAM) {
    request_finished = true;
    Serial.println("STREAM CLOSED");
}

The final callback function code can be seen below. Note that it should return zero, as indicated in the sh2lib.h file.

int handle_get_response(struct sh2lib_handle *handle, const char *data, size_t len, int flags)
{
    if (len > 0) {
        Serial.printf("%.*s\n", len, data);
    }

    if (flags == DATA_RECV_RST_STREAM) {
        request_finished = true;
        Serial.println("STREAM CLOSED");
    }
    return 0;
}

The final code

The final source code can be seen below. Note that since we don’t need our main loop to execute any computation, we can just make a call to the vTaskDelete function passing NULL as input, in order to delete the corresponding task.

#include "WiFi.h"

extern "C" {
#include "sh2lib.h"
}

const char* ssid = "yourNetworkName";
const char* password =  "yourNetworkPassword";

bool request_finished = false;

int handle_get_response(struct sh2lib_handle *handle, const char *data, size_t len, int flags)
{
    if (len > 0) {
        Serial.printf("%.*s\n", len, data);
    }

    if (flags == DATA_RECV_RST_STREAM) {
        request_finished = true;
        Serial.println("STREAM CLOSED");
    }
    return 0;
}

void http2_task(void *args)
{
  struct sh2lib_handle hd;

  if (sh2lib_connect(&hd, "https://nghttp2.org") != ESP_OK) {
    Serial.println("Error connecting to HTTP2 server");
    vTaskDelete(NULL);
  }

  Serial.println("Connected");

  sh2lib_do_get(&hd, "/httpbin/ip", handle_get_response);

  while (1) {

    if (sh2lib_execute(&hd) != ESP_OK) {
      Serial.println("Error in send/receive");
      break;
    }

    if (request_finished) {
      break;
    }

    vTaskDelay(10);
  }

  sh2lib_free(&hd);
  Serial.println("Disconnected");

  vTaskDelete(NULL);
}

void setup() {
  Serial.begin(115200);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }

  xTaskCreate(http2_task, "http2_task", (1024 * 32), NULL, 5, NULL);

}

void loop() {
  vTaskDelete(NULL);
}

Testing the code

To test the code, simply compile it and upload it to your ESP32. Once the procedure finishes, open the Arduino IDE serial monitor.

You should get an output similar to figure 1. As can be seen, after the connection is established, the ESP32 performs the HTTP/2 request and receives as output the answer with the IP address, in JSON format. After that, the stream closed event occurs and then the ESP32 successfully disconnects from the server.

ESP32 HTTP/2 request using the Arduino core

Figure 1 – Result of the HTTP/2 GET request.

Related Posts

References

[1] http://www.cplusplus.com/reference/cstdio/printf/

23 thoughts on “ESP32 Arduino: HTTP/2 GET Request”

  1. Jaume Miralles

    “sh2lib” works fine… with servers supporting HTTP/2.0… what are the options if the server only supports HTTP/1.0?

  2. Jaume Miralles

    “sh2lib” works fine… with servers supporting HTTP/2.0… what are the options if the server only supports HTTP/1.0?

Leave a Reply to Jaume MirallesCancel reply

Discover more from techtutorialsx

Subscribe now to keep reading and get access to the full archive.

Continue reading