In this tutorial we will check how to send a HTTP/2 GET request from an ESP32 to a Node.js server running on the Raspberry Pi 3.
Introduction
In this tutorial we will check how to send a HTTP/2 GET request from an ESP32 to a Node.js server running on the Raspberry Pi 3.
This will be a slightly more complex tutorial that works on top of the knowledge built on other previous tutorials. So, I’ll leave the links for those below.
The ESP32 will be programmed using the Arduino core. You can check in detail how to perform an HTTP/2 GET request from the ESP32 on this previous tutorial.
In sum, we will be using a wrapper called sh2lib from IDF. This wrapper works on top of NGHTTP2, offering easier to use functions. The sh2lib will be used as an Arduino library. The installation procedure is really simple and it can be checked on this previous post.
On the Raspberry Pi side, we will need to host a HTTP/2 server. This was already covered on this previous tutorial. The tutorial explains how to generate the server certificate and private keys needed and contains a step by step analysis of all the code.
The Node.js code
The first thing we will do in our code is importing the HTTP/2 module. This module will expose the features we need to configure our HTTP/2 server.
We will also need the file system module, so we can read the content of the private key and certificate files.
const http2 = require('http2');
const fs = require('fs');
After the imports we will define a function that will be executed every time a HTTP/2 request is received. The function will be invoked with two arguments:
In our code, we will only make use of the second object, which will allow us to send a response back to the client.
To do it, we simply need to call the end method on the mentioned object, passing as input the content we want to return to the client.
Additionally, it’s important to take in consideration that this method also signals that all the content was sent by the server and that the message should be considered complete.
The full function implementation can be seen below. We will simply send a “Hello World” message back, that should be received by the client (in our case, the ESP32).
function onRequest (req, resp) {
resp.end("Hello World from the RPI3,via HTTP2");
}
After this, we need to create the server by calling the createSecureServer from the imported HTTP/2 module.
As the first input, this function receives an object containing two properties (key and cert), which contain the content of the private key and certificate files, respectively.
We will make use of the readFileSync function from the File System module to get the content of these files. Note that, as input, the function receives the path to the files. On the code below, I’m assuming that both the files and the Node.js script are running in the same directory, which is why only the file names is used.
As the second parameter, the createSecureServer function receives a function to be executed when a request is received. Naturally, we should pass the onRequest callback function we have defined earlier.
const server = http2.createSecureServer({
key: fs.readFileSync('localhost-privkey.pem'),
cert: fs.readFileSync('localhost-cert.pem')
}, onRequest);
To finalize, we simply need to call the listen method on our server object. As input, we pass the number of the port where it should listen to incoming HTTP/2 requests. We will be using port 8443.
server.listen(8443);
The final code can be seen below
const http2 = require('http2');
const fs = require('fs');
function onRequest (req, resp) {
resp.end("Hello World from the RPI3,via HTTP2");
}
const server = http2.createSecureServer({
key: fs.readFileSync('localhost-privkey.pem'),
cert: fs.readFileSync('localhost-cert.pem')
}, onRequest);
server.listen(8443);
The ESP32 code
We will also start the ESP32 Arduino code with the library includes. We will need to include the WiFi.h, so we can connect the device to a WiFi network, and the sh2lib.h, which is a wrapper built on top of the NGHTTP2 library, making it easier to use the HTTP/2 features.
#include "WiFi.h"
extern "C" {
#include "sh2lib.h"
}
In order to be able to connect the ESP32 to a WiFi network, we will also need its credentials, which we will store as two global variables. Note that we will need both the network name and password.
const char* ssid = "yourNetworkName";
const char* password = "yourNetworkPass";
To finalize the global variables declaration, we will also need a flag that will signal when the request sent to the server is complete. Naturally, we will set its initial value to false.
bool request_finished = false;
Moving on to the setup function, we will start by opening a serial connection and then connecting the ESP32 to the WiFi network, using the previously declared credentials.
After that, we will launch the FreeRTOS task that will take care of the HTTP/2 related function calls.
The whole 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);
}
Moving on to the FreeRTOS task implementation, we will start by declaring a sh2lib_handle struct. This struct will be needed for the HTTP/2 related function calls we will do below.
struct sh2lib_handle hd;
Then we will connect the ESP32 to the HTTP/2 server, with a call to the sh2lib_connect function.
As first parameter, we should pass the address of the sh2lib handle we have just created. As the second parameter we should pass the URL of the server.
In our case, the URL has the format shown below, where you should change #yourRaspberryIp# by the IP of your device. For an explanation o how to get the local IP of your Raspberry Pi, please check here.
https://#yourRaspberryIp#:8443
The code for establishing the connection to the server can be seen below, using the local IP address of my Raspberry Pi. We will also do an error check to confirm we could successfully connect to the server.
if (sh2lib_connect(&hd, "https://192.168.1.92:8443") != ESP_OK) {
Serial.println("Error connecting to HTTP2 server");
vTaskDelete(NULL);
}
Serial.println("Connected");
After establishing the connection to the server, we will setup the HTTP/2 GET request with a call to the sh2lib_do_get function. Recall from the previous tutorial that this function only does the setup of the request, and later we need to invoke an additional function to actually exchange the data with the server.
As first input, the sh2lib_do_get receives the address of our handle, as second input the relative path of the server that we want to reach, and as third parameter a callback function that will be called to handle the server response. We will take care of defining this function later.
Note that we are using an endpoint called “/test” but we could use any other value since the HTTP/2 server will always return a “Hello World” message back to the client.
sh2lib_do_get(&hd, "/test", handle_get_response);
As already mentioned, we need to call an additional function to do the actual exchange of data with the server. This function is called sh2lib_execute and it receives as only input our handle address.
Note that this function needs to be called periodically until the response from the server is received and the stream is closed. Thus, we will do it in an infinite loop that will break only when the request_finished flag is set to true.
It will be the responsibility of the server response handling function to set this flag to true as soon as the request is finished.
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 the request is finished, so we can disconnect from the server and then delete the FreeRTOS task, which will no longer be needed.
sh2lib_free(&hd);
Serial.println("Disconnected");
vTaskDelete(NULL);
The full FreeRTOS task can be seen below.
void http2_task(void *args)
{
struct sh2lib_handle hd;
if (sh2lib_connect(&hd, "https://192.168.1.92:8443") != ESP_OK) {
Serial.println("Error connecting to HTTP2 server");
vTaskDelete(NULL);
}
Serial.println("Connected");
sh2lib_do_get(&hd, "/test", 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);
}
Finally, we just need to check the implementation of the server response callback function. In its implementation we will imply check if there is data available to read and, in case some is available, print it to the serial port.
Additionally, we will check if we received the flag that indicates the HTTP/2 stream is closed, thus indicating the request is completed. In case it is, we set the request_finished flag to true, to break the loop of the FreeRTOS task.
The full callback function code can be seen below. You can read a more detailed explanation of all its parameters on this previous tutorial.
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 source code can be seen below.
#include "WiFi.h"
extern "C" {
#include "sh2lib.h"
}
const char* ssid = "yourNetworkName";
const char* password = "yourNetworkPass";
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://192.168.1.92:8443") != ESP_OK) {
Serial.println("Error connecting to HTTP2 server");
vTaskDelete(NULL);
}
Serial.println("Connected");
sh2lib_do_get(&hd, "/test", 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 whole system, the first thing we need to do is launching the HTTP/2 server on the Raspberry Pi.
To do it, simply run the Node.js code from this post from the command line. You can check here how to run a Node.js script from the command line.
After the server is running, you should do a quick test from the Raspberry Pi web browser to confirm it is correctly receiving the HTTP/2 requests and returning an answer. You can check how to test the server from a browser on this previous post.
After confirming the Node.js code is running correctly and you are being able to reach it locally, it is also a good ideia to check if you can reach it from another computer on the same WiFi network.
To do it, you just need to access it from a web browser on a computer. Nonetheless, this time you will need to use the local IP address of the Raspberry Pi on the WiFi network, which should be the one you have obtained to use on the ESP32 code.
If everything is working correctly, we can proceed to test the integration between the ESP32 and the server. To do it, we first need to compile and upload the code to the ESP32, using the Arduino IDE.
As soon as it finishes, open the IDE serial monitor tool. You should get an output similar to figure 1. As can be seen, the ESP32 was able to connect to the server and then it received the message we set on the Node.js code. After that, the connection was closed, as expected.
Figure 1 – Output of the program, with the response from the Raspberry Pi HTTP/2 server.