ESPIDF development ESP32 learning notes [HTTP client implementation]

Posted by JCScoobyRS on Mon, 24 Jan 2022 16:38:13 +0100

TCP protocol stack

ESP uses lwIP as embedded TCP/IP protocol stack support

lwIP is a set of IP protocol stack implemented in C at MCU level, which can run on bare metal / RTOS / Embedded Linux. Lexin provides relevant porting packages for ESP32

For relevant contents, please refer to LwIP library functions, which are supported in LwIP and ESP-NETIF components

esp_err_t esp_netif_init(void);
esp_err_t esp_netif_deinit(void);
esp_netif_t *esp_netif_new(const esp_netif_config_t *esp_netif_config);
void esp_netif_destroy(esp_netif_t *esp_netif);
esp_err_t esp_netif_set_driver_config(esp_netif_t *esp_netif, const esp_netif_driver_ifconfig_t *driver_config);
esp_err_t esp_netif_attach(esp_netif_t *esp_netif, esp_netif_iodriver_handle driver_handle);
esp_err_t esp_netif_receive(esp_netif_t *esp_netif, void *buffer, size_t len, void *eb);
void esp_netif_action_start(void *esp_netif, esp_event_base_t base, int32_t event_id, void *data);
void esp_netif_action_stop(void *esp_netif, esp_event_base_t base, int32_t event_id, void *data);
void esp_netif_action_connected(void *esp_netif, esp_event_base_t base, int32_t event_id, void *data);
void esp_netif_action_disconnected(void *esp_netif, esp_event_base_t base, int32_t event_id, void *data);
void esp_netif_action_got_ip(void *esp_netif, esp_event_base_t base, int32_t event_id, void *data);
esp_err_t esp_netif_set_mac(esp_netif_t *esp_netif, uint8_t mac[]);
esp_err_t esp_netif_set_hostname(esp_netif_t *esp_netif, const char *hostname);
esp_err_t esp_netif_get_hostname(esp_netif_t *esp_netif, const char **hostname);
bool esp_netif_is_netif_up(esp_netif_t *esp_netif);
esp_err_t esp_netif_get_ip_info(esp_netif_t *esp_netif, esp_netif_ip_info_t *ip_info);
esp_err_t esp_netif_get_old_ip_info(esp_netif_t *esp_netif, esp_netif_ip_info_t *ip_info);
esp_err_t esp_netif_set_ip_info(esp_netif_t *esp_netif, const esp_netif_ip_info_t *ip_info);
esp_err_t esp_netif_set_old_ip_info(esp_netif_t *esp_netif, const esp_netif_ip_info_t *ip_info);
int esp_netif_get_netif_impl_index(esp_netif_t *esp_netif);
esp_err_t esp_netif_dhcps_option(esp_netif_t *esp_netif, esp_netif_dhcp_option_mode_t opt_op, esp_netif_dhcp_option_id_t opt_id, void *opt_val, uint32_t opt_len);
esp_err_t esp_netif_dhcpc_option(esp_netif_t *esp_netif, esp_netif_dhcp_option_mode_t opt_op, esp_netif_dhcp_option_id_t opt_id, void *opt_val, uint32_t opt_len);
esp_err_t esp_netif_dhcpc_start(esp_netif_t *esp_netif);
esp_err_t esp_netif_dhcpc_stop(esp_netif_t *esp_netif);
esp_err_t esp_netif_dhcpc_get_status(esp_netif_t *esp_netif, esp_netif_dhcp_status_t *status);
esp_err_t esp_netif_dhcps_get_status(esp_netif_t *esp_netif, esp_netif_dhcp_status_t *status);
esp_err_t esp_netif_dhcps_start(esp_netif_t *esp_netif);
esp_err_t esp_netif_dhcps_stop(esp_netif_t *esp_netif);

esp_ The netif component is based on lwip, as shown in the API above, and implements

  • TCP/IP protocol initialization and memory allocation
  • Establish communication based on IP protocol
  • Control the local IP address and find the target IP
  • DHCP function
  • Bottom layer implementation of sending and receiving TCP messages

Note: this component does not implement the DNS function. You need to use a separate DNS component to implement the DNS server / DNS resolution function

HTTP client

ESP-IDF provides HTTP client components that can realize stable links < ESP_ http_ Client >, which implements the API for sending HTTP/S requests from ESP-IDF applications

HTTP client can be understood as a "browser" without a screen - it establishes a TCP/IP connection with the server and sends and receives TCP messages conforming to the HTTP protocol standard, including message headers and data packets, which will be transmitted in json format

To sum up, we can know that if you want to establish a stable connection between the ESP-IDF device and the HTTP website (server), you need five components:

  • wifi or ethernet components, providing underlying networking functions
  • lwip component provides MCU implementation of IP protocol
  • netif component, which provides MCU implementation of TCP protocol
  • esp_ http_ The client component provides the implementation of HTTP client / server data parsing and connection processing. The HTTP server component has been introduced in the previous blog post
  • The cJSON component is used to parse the json data returned by the server / process the local data in json format and POST it to the server

If necessary, you also need to use the freertos component to facilitate multitasking

The steps of using HTTP client related API s are as follows:

Before starting, you need to establish NVS storage, connect WiFi and initialize netif network interface

esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
	ESP_ERROR_CHECK(nvs_flash_erase());
	ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(internet_connect()); //Connect wifi or ethernet
  1. esp_http_client_init()

    Create an ESP_ http_ client_ config_ Instance of T (instantiate object) and configure HTTP client handle

    esp_http_client_config_t config = {
    	.host = WEB_SERVER,
    	.path = WEB_PATH_GET_TIME,
        .query = WEB_QUERY,
        .transport_type = HTTP_TRANSPORT_OVER_TCP,
        .event_handler = _http_event_handler,
        .user_data = local_response_buffer
    };
    
  2. Perform various operations on the HTTP client

    This includes opening links, exchanging data, or closing links

    All these operations can be combined with the event specified in the above steps through the encapsulated function_ Handler callback function

    esp_http_client_handle_t client = esp_http_client_init(&config);
    

    Where event_handler is based on the state machine, as shown below

    esp_err_t _http_event_handler(esp_http_client_event_t* evt)
    {
        static char* output_buffer; // Buffer to store response of http request from event handler
        static int output_len; // Stores number of bytes read
    
        switch (evt->event_id)
        {
        case HTTP_EVENT_ERROR:
            ESP_LOGD(TAG, "HTTP_EVENT_ERROR");
            break;
        case HTTP_EVENT_ON_CONNECTED:
            ESP_LOGD(TAG, "HTTP_EVENT_ON_CONNECTED");
            break;
        case HTTP_EVENT_HEADER_SENT:
            ESP_LOGD(TAG, "HTTP_EVENT_HEADER_SENT");
            break;
        case HTTP_EVENT_ON_HEADER:
            ESP_LOGD(TAG, "HTTP_EVENT_ON_HEADER, key=%s, value=%s", evt->header_key, evt->header_value);
            break;
        case HTTP_EVENT_ON_DATA:
            ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
            /*
             *  Check for chunked encoding is added as the URL for chunked encoding used in this example returns binary data.
             *  However, event handler can also be used in case chunked encoding is used.
             */
            if (!esp_http_client_is_chunked_response(evt->client))
            {
                // If user_data buffer is configured, copy the response into the buffer
                if (evt->user_data)
                {
                    memcpy(evt->user_data + output_len, evt->data, evt->data_len);
                }
                else
                {
                    if (output_buffer == NULL)
                    {
                        output_buffer = (char*)malloc(esp_http_client_get_content_length(evt->client));
                        output_len = 0;
                        if (output_buffer == NULL)
                        {
                            ESP_LOGE(TAG, "Failed to allocate memory for output buffer");
                            return ESP_FAIL;
                        }
                    }
                    memcpy(output_buffer + output_len, evt->data, evt->data_len);
                }
                output_len += evt->data_len;
            }
            break;
        case HTTP_EVENT_ON_FINISH:
            ESP_LOGD(TAG, "HTTP_EVENT_ON_FINISH");
            if (output_buffer != NULL)
            {
                // Response is accumulated in output_buffer. Uncomment the below line to print the accumulated response
                // ESP_LOG_BUFFER_HEX(TAG, output_buffer, output_len);
                free(output_buffer);
                output_buffer = NULL;
                output_len = 0;
            }
            break;
        case HTTP_EVENT_DISCONNECTED:
            ESP_LOGI(TAG, "HTTP_EVENT_DISCONNECTED");
            int mbedtls_err = 0;
            esp_err_t err = esp_tls_get_and_clear_last_error(evt->data, &mbedtls_err, NULL);
            if (err != 0)
            {
                if (output_buffer != NULL)
                {
                    free(output_buffer);
                    output_buffer = NULL;
                    output_len = 0;
                }
                ESP_LOGI(TAG, "Last esp error code: 0x%x", err);
                ESP_LOGI(TAG, "Last mbedtls failure: 0x%x", mbedtls_err);
            }
            break;
        }
        return ESP_OK;
    }
    
  3. Through esp_http_client_cleanup() closes the link and releases system resources

    Note: this function must be the last function invoked after the operation is completed.

One thing to note: ESP_ http_ client_ The connection established by init () is persistent, so the HTTP client can reuse the same connection in multiple exchanges, as long as the server does not use the header connection: close to forcibly close, or does not use esp_http_client_cleanup() * when the link is closed, the HTTP link of the device will remain open

Common HTTP client operations

HTTP: GET

// GET
esp_err_t err = esp_http_client_perform(client);
if (err == ESP_OK) 
{
	ESP_LOGI(TAG, "HTTP GET Status = %d, content_length = %d",
    esp_http_client_get_status_code(client),
    esp_http_client_get_content_length(client));
} 
else 
{
	ESP_LOGE(TAG, "HTTP GET request failed: %s", esp_err_to_name(err));
}
ESP_LOG_BUFFER_HEX(TAG, local_response_buffer, strlen(local_response_buffer)); 
//The data is saved to the data cache previously registered in the HTTP client object

Here are two common API s

esp_http_client_get_status_code(client) //Get HTTP return status code
esp_http_client_get_content_length(client) //Gets the length of the HTTP return data

esp_http_client_get_content_length() is special and can only be used when the length of the returned data is fixed, that is, there can be no chunked in the HTTP header. If you want to receive non fixed length data, you need to use a special API: esp_http_client_is_chunked_response(esp_http_client_handle_t client) first obtains the message length, and then receives the data in the callback function to the HTTP message buffer for this length

Enthusiastic netizens in ESP IDF repo on GitHub gave the following code to implement a simple and fast request

int http_request(char *http_response, int *http_response_length, int range_start, int range_end, int client_id)
{
	int err = -1;
	char range[64];
	int len_received = 0;

    if (range_end > range_start) 
    {
            sprintf(range, "bytes=%d-%d", range_start, range_end);
            esp_http_client_set_header(ota_client[client_id], "Range", range);
    }

    err = esp_http_client_open(ota_client[client_id], 0);
    if (err != ESP_OK) 
    {
            ESP_LOGE(LOG_TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
    } 
    else 
    {
		int content_length = esp_http_client_fetch_headers(ota_client[client_id]);
		if (content_length < 0) 
        {
        	ESP_LOGE(LOG_TAG, "HTTP client fetch headers failed");
		} 
        else 
        {
        	len_received = esp_http_client_read_response(ota_client[client_id], http_response, *http_response_length);
			if (len_received >= 0) 
            {
                ESP_LOGI(LOG_TAG, "HTTP Status = %d, content_length = %lld",
                                esp_http_client_get_status_code(ota_client[client_id]),
                                esp_http_client_get_content_length(ota_client[client_id]));
			} 
            else 
            {
            	ESP_LOGE(LOG_TAG, "Failed to read response");    
            }
		}
    }

    esp_http_client_close(ota_client[client_id]);
    *http_response_length = len_received;
    return (err);
}

This code is based on esp_http_client_open, relatively fast, recommended

HTTP: POST

As an example:

// POST
const char *post_data = "{\"field1\":\"value1\"}";
esp_http_client_set_url(client, "http://httpbin.org/post");
esp_http_client_set_method(client, HTTP_METHOD_POST); //Set the current method to POST
esp_http_client_set_header(client, "Content-Type", "application/json"); //Set request header
esp_http_client_set_post_field(client, post_data, strlen(post_data)); //Packet used when setting POST
//This packet is usually a string formatted with cJSON
err = esp_http_client_perform(client);
if (err == ESP_OK) 
{
	ESP_LOGI(TAG, "HTTP POST Status = %d, content_length = %d",
                esp_http_client_get_status_code(client),
                esp_http_client_get_content_length(client));
} 
else 
{
	ESP_LOGE(TAG, "HTTP POST request failed: %s", esp_err_to_name(err));
}

Note: the data package must be set to the string with correct format. It is recommended to use cJSON component instead of manual formatting

Other operations

You can view the official sample code < ESP IDF Directory > / example / protocols / esp_ http_ Client to get the instruction format

Space constraints, no more introduction

cJSON component

ESP32 provides the migration of cJSON (if it is not provided, it is easy to migrate by itself)

cJSON is a set of C libraries for formatting and processing JSON data, which can be divided into two types of API s for use scenarios:

  • Processing strings as JSON objects
  • Processing JSON objects as strings

The string in C is described by char type array, while the JSON object is defined as a structure by cJSON, as shown below

typedef struct cJSON
{
    /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
    struct cJSON *next;
    struct cJSON *prev;
    /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
    struct cJSON *child;

    /* The type of the item, as above. */
    int type;

    /* The item's string, if type==cJSON_String  and type == cJSON_Raw */
    char *valuestring;
    /* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */
    int valueint;
    /* The item's number, if type==cJSON_Number */
    double valuedouble;

    /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
    char *string;
} cJSON;

Its "base class" is a two-way linked list and supports the expansion of more subclasses

The type attribute represents the type of JSON

valuestring, valueint and valuedouble represent three possible data types in JSON: string, integer and floating point

The string attribute represents the name of the subclass or the name of the JSON instantiated object

Use in code

#include "cJSON.h"

You can call the cJSON component

use

CJSON_PUBLIC(const char*) cJSON_Version(void)
    
#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY)
#define CJSON_PUBLIC(type)   __attribute__((visibility("default"))) type
#else
#define CJSON_PUBLIC(type) type
#endif
#endif

You can output the version number of cJSON to a specified string. As long as you output the string again, you can check whether the current version of cJSON meets the requirements

Processing strings as JSON objects

It is often used for parsing when the data is saved locally after obtaining the data from the server

json exists as an ordered list of nested key value pairs or values, usually as follows

{
    "code":0,
    "message":null,
    "data":1642959073193
}
//or
{
    "employees":
    {
		{ 
        	"firstName":"Bill",
        	"lastName":"Gates"
    	},
		{
    		"firstName":"George",
    		"lastName":"Bush"
		},
		{
            "firstName":"Thomas",
    		"lastName":"Carter"
        },
	},
	"message": 12345678
}

It adopts a text format completely independent of the language, but it also uses habits similar to the C language family (including C/C++/C#, Java, JavaScript, Perl, Python, etc.), and many languages have their own JSON library implementation

Nested key value pairs have their own understanding in different languages: Object, Record, struct, dictionary, hash table, key value, associative array, etc., but they all have the form of one-to-one or one to many

An ordered list of values, which is understood by most languages as an array

These common data structures can be exchanged between programming languages based on these structures, which is why JSON format is popular on the Internet (even many embedded devices are considering using JSON in applications with low real-time performance)

In json's own implementation, each json string is regarded as a json object, which is a collection of unordered name value pairs (nested unordered key value pairs). An object starts with an open parenthesis {and ends with a close parenthesis}. Each name is required to be followed by a colon:, and the paired combinations are separated by commas

For the basic use of cJSON parsing API, please refer to the following example code (the received json data is the first json format example given above):

/* Confirm from local_ response_ Whether the data obtained from buffer (HTTP message buffer) is in JSON format */
cJSON* response_json = cJSON_Parse(local_response_buffer);
if (response_json != NULL)
{
    /* Get the value corresponding to the data key, where the data transmission is actually a timestamp */
	cJSON* timestamp_json = cJSON_GetObjectItem(response_json, "data");
    /* Output corresponding value to string */
	char* timestamp_temp = cJSON_Print(timestamp_json);
    /* Printout string */
	ESP_LOGI(TAG, "recv number:%s", timestamp_temp);

    /* Get the value corresponding to the message key, where the value is fixed to null */
	cJSON* message_json = cJSON_GetObjectItem(response_json, "message");
    /* Output corresponding value to string */
	char* message = cJSON_Print(message_json);
    /* Printout string */
	ESP_LOGI(TAG, "recv msg:%s", message);
    if (!strcmp(message, "null")) //Check whether the data is correct
    {
    	ESP_LOGI(TAG, "senting Queue msg:%s", timestamp); //Send timestamp data with message queue
        xQueueSend(trans_timestamp_Queue, timestamp, 0);
	}
}

Processing JSON objects as strings

As mentioned above, JSON objects are based on linked lists

When creating a JSON object, first create a "root node" and then "grow" more data from this root node. Each additional node means that there are one more layer of braces. For example, in the two JSON examples given above, the first has a root node; the second has a root node and three message child nodes

You can refer to the following example code:

cJSON* cjson_root = cJSON_CreateObject(); //Create JSON root node

/* Add the data corresponding to the key and value respectively */
/* The first parameter of the function is the corresponding root node */
/* The second argument to the function is the key */
/* The third argument to the function is the value */
cJSON_AddStringToObject(cjson_root, "value", "8399d88e3293cc89cacc1d735af12810");
cJSON_AddStringToObject(cjson_root, "location", "classroom");
cJSON_AddNumberToObject(cjson_root, "timestamp", timestamp);

/* The output JSON data is in string format */
char* post_data = cJSON_Print(cjson_root);
/* Delete the root node previously created for the JSON object */
cJSON_Delete(cjson_root);
/* Print JSON data in string format */
ESP_LOGI(TAG, "generate:%s", post_data);

Note here: you must remember to delete JSON objects after they are created. You must remember to delete the corresponding data every time you create one. C has no memory management mechanism and needs to allocate and recycle manually!

If there are multiple child nodes and a root node, you should first delete the child node at the top level, and then delete the root node

Topics: Linux network http ESP32