Merge branch 'feature/ws_client' into 'master'

esp_websocket_client

See merge request idf/esp-idf!3420
This commit is contained in:
Angus Gratton 2019-06-20 15:37:40 +08:00
commit 4397627b5b
21 changed files with 1147 additions and 18 deletions

View File

@ -0,0 +1,4 @@
set(COMPONENT_SRCS "esp_websocket_client.c")
set(COMPONENT_ADD_INCLUDEDIRS "include")
set(COMPONENT_REQUIRES lwip esp-tls tcp_transport nghttp)
register_component()

View File

@ -0,0 +1,606 @@
// Copyright 2015-2018 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <stdio.h>
#include "esp_websocket_client.h"
#include "esp_transport.h"
#include "esp_transport_tcp.h"
#include "esp_transport_ssl.h"
#include "esp_transport_ws.h"
#include "esp_transport_utils.h"
/* using uri parser */
#include "http_parser.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "freertos/queue.h"
#include "freertos/event_groups.h"
#include "esp_log.h"
#include "esp_timer.h"
static const char *TAG = "WEBSOCKET_CLIENT";
#define WEBSOCKET_TCP_DEFAULT_PORT (80)
#define WEBSOCKET_SSL_DEFAULT_PORT (443)
#define WEBSOCKET_BUFFER_SIZE_BYTE (1024)
#define WEBSOCKET_RECONNECT_TIMEOUT_MS (10*1000)
#define WEBSOCKET_TASK_PRIORITY (5)
#define WEBSOCKET_TASK_STACK (4*1024)
#define WEBSOCKET_NETWORK_TIMEOUT_MS (10*1000)
#define WEBSOCKET_PING_TIMEOUT_MS (10*1000)
#define WEBSOCKET_EVENT_QUEUE_SIZE (1)
#define WEBSOCKET_SEND_EVENT_TIMEOUT_MS (1000/portTICK_RATE_MS)
const static int STOPPED_BIT = BIT0;
ESP_EVENT_DEFINE_BASE(WEBSOCKET_EVENTS);
typedef struct {
int task_stack;
int task_prio;
char *uri;
char *host;
char *path;
char *scheme;
char *username;
char *password;
int port;
bool auto_reconnect;
void *user_context;
int network_timeout_ms;
} websocket_config_storage_t;
typedef enum {
WEBSOCKET_STATE_ERROR = -1,
WEBSOCKET_STATE_UNKNOW = 0,
WEBSOCKET_STATE_INIT,
WEBSOCKET_STATE_CONNECTED,
WEBSOCKET_STATE_WAIT_TIMEOUT,
} websocket_client_state_t;
struct esp_websocket_client {
esp_event_loop_handle_t event_handle;
esp_transport_list_handle_t transport_list;
esp_transport_handle_t transport;
websocket_config_storage_t *config;
websocket_client_state_t state;
uint64_t keepalive_tick_ms;
uint64_t reconnect_tick_ms;
uint64_t ping_tick_ms;
int wait_timeout_ms;
int auto_reconnect;
bool run;
EventGroupHandle_t status_bits;
xSemaphoreHandle lock;
char *rx_buffer;
char *tx_buffer;
int buffer_size;
};
static uint64_t _tick_get_ms()
{
return esp_timer_get_time()/1000;
}
static esp_err_t esp_websocket_client_dispatch_event(esp_websocket_client_handle_t client,
esp_websocket_event_id_t event,
const char *data,
int data_len)
{
esp_err_t err;
esp_websocket_event_data_t event_data;
event_data.data_ptr = data;
event_data.data_len = data_len;
if ((err = esp_event_post_to(client->event_handle,
WEBSOCKET_EVENTS, event,
&event_data,
sizeof(esp_websocket_event_data_t),
WEBSOCKET_SEND_EVENT_TIMEOUT_MS)) != ESP_OK) {
return err;
}
return esp_event_loop_run(client->event_handle, WEBSOCKET_SEND_EVENT_TIMEOUT_MS);
}
static esp_err_t esp_websocket_client_abort_connection(esp_websocket_client_handle_t client)
{
esp_transport_close(client->transport);
client->wait_timeout_ms = WEBSOCKET_RECONNECT_TIMEOUT_MS;
client->reconnect_tick_ms = _tick_get_ms();
client->state = WEBSOCKET_STATE_WAIT_TIMEOUT;
ESP_LOGI(TAG, "Reconnect after %d ms", client->wait_timeout_ms);
esp_websocket_client_dispatch_event(client, WEBSOCKET_EVENT_DISCONNECTED, NULL, 0);
return ESP_OK;
}
static esp_err_t esp_websocket_client_set_config(esp_websocket_client_handle_t client, const esp_websocket_client_config_t *config)
{
websocket_config_storage_t *cfg = client->config;
cfg->task_prio = config->task_prio;
if (cfg->task_prio <= 0) {
cfg->task_prio = WEBSOCKET_TASK_PRIORITY;
}
cfg->task_stack = config->task_stack;
if (cfg->task_stack == 0) {
cfg->task_stack = WEBSOCKET_TASK_STACK;
}
if (config->host) {
cfg->host = strdup(config->host);
ESP_TRANSPORT_MEM_CHECK(TAG, cfg->host, return ESP_ERR_NO_MEM);
}
if (config->port) {
cfg->port = config->port;
}
if (config->username) {
free(cfg->username);
cfg->username = strdup(config->username);
ESP_TRANSPORT_MEM_CHECK(TAG, cfg->username, {
free(cfg->host);
return ESP_ERR_NO_MEM;
});
}
if (config->password) {
free(cfg->password);
cfg->password = strdup(config->password);
ESP_TRANSPORT_MEM_CHECK(TAG, cfg->password, {
free(cfg->host);
free(cfg->username);
return ESP_ERR_NO_MEM;
});
}
if (config->uri) {
free(cfg->uri);
cfg->uri = strdup(config->uri);
ESP_TRANSPORT_MEM_CHECK(TAG, cfg->uri, {
free(cfg->host);
free(cfg->username);
free(cfg->password);
return ESP_ERR_NO_MEM;
});
}
if (config->path) {
free(cfg->path);
cfg->path = strdup(config->path);
ESP_TRANSPORT_MEM_CHECK(TAG, cfg->path, {
free(cfg->uri);
free(cfg->host);
free(cfg->username);
free(cfg->password);
return ESP_ERR_NO_MEM;
});
}
cfg->network_timeout_ms = WEBSOCKET_NETWORK_TIMEOUT_MS;
cfg->user_context = config->user_context;
cfg->auto_reconnect = true;
if (config->disable_auto_reconnect) {
cfg->auto_reconnect = false;
}
return ESP_OK;
}
static esp_err_t esp_websocket_client_destroy_config(esp_websocket_client_handle_t client)
{
if (client == NULL) {
return ESP_ERR_INVALID_ARG;
}
websocket_config_storage_t *cfg = client->config;
if (client->config == NULL) {
return ESP_ERR_INVALID_ARG;
}
free(cfg->host);
free(cfg->uri);
free(cfg->path);
free(cfg->scheme);
free(cfg->username);
free(cfg->password);
memset(cfg, 0, sizeof(websocket_config_storage_t));
free(client->config);
client->config = NULL;
return ESP_OK;
}
esp_websocket_client_handle_t esp_websocket_client_init(const esp_websocket_client_config_t *config)
{
esp_websocket_client_handle_t client = calloc(1, sizeof(struct esp_websocket_client));
ESP_TRANSPORT_MEM_CHECK(TAG, client, return NULL);
esp_event_loop_args_t event_args = {
.queue_size = WEBSOCKET_EVENT_QUEUE_SIZE,
.task_name = NULL // no task will be created
};
if (esp_event_loop_create(&event_args, &client->event_handle) != ESP_OK) {
ESP_LOGE(TAG, "Error create event handler for websocket client");
free(client);
return NULL;
}
client->lock = xSemaphoreCreateMutex();
ESP_TRANSPORT_MEM_CHECK(TAG, client->lock, goto _websocket_init_fail);
client->transport_list = esp_transport_list_init();
ESP_TRANSPORT_MEM_CHECK(TAG, client->transport_list, goto _websocket_init_fail);
esp_transport_handle_t tcp = esp_transport_tcp_init();
ESP_TRANSPORT_MEM_CHECK(TAG, tcp, goto _websocket_init_fail);
esp_transport_set_default_port(tcp, WEBSOCKET_TCP_DEFAULT_PORT);
esp_transport_list_add(client->transport_list, tcp, "_tcp"); // need to save to transport list, for cleanup
esp_transport_handle_t ws = esp_transport_ws_init(tcp);
ESP_TRANSPORT_MEM_CHECK(TAG, ws, goto _websocket_init_fail);
esp_transport_set_default_port(ws, WEBSOCKET_TCP_DEFAULT_PORT);
esp_transport_list_add(client->transport_list, ws, "ws");
if (config->transport == WEBSOCKET_TRANSPORT_OVER_TCP) {
asprintf(&client->config->scheme, "ws");
ESP_TRANSPORT_MEM_CHECK(TAG, client->config->scheme, goto _websocket_init_fail);
}
esp_transport_handle_t ssl = esp_transport_ssl_init();
ESP_TRANSPORT_MEM_CHECK(TAG, ssl, goto _websocket_init_fail);
esp_transport_set_default_port(ssl, WEBSOCKET_SSL_DEFAULT_PORT);
if (config->cert_pem) {
esp_transport_ssl_set_cert_data(ssl, config->cert_pem, strlen(config->cert_pem));
}
esp_transport_list_add(client->transport_list, ssl, "_ssl"); // need to save to transport list, for cleanup
esp_transport_handle_t wss = esp_transport_ws_init(ssl);
ESP_TRANSPORT_MEM_CHECK(TAG, wss, goto _websocket_init_fail);
esp_transport_set_default_port(wss, WEBSOCKET_SSL_DEFAULT_PORT);
esp_transport_list_add(client->transport_list, wss, "wss");
if (config->transport == WEBSOCKET_TRANSPORT_OVER_TCP) {
asprintf(&client->config->scheme, "wss");
ESP_TRANSPORT_MEM_CHECK(TAG, client->config->scheme, goto _websocket_init_fail);
}
client->config = calloc(1, sizeof(websocket_config_storage_t));
ESP_TRANSPORT_MEM_CHECK(TAG, client->config, goto _websocket_init_fail);
if (config->uri) {
if (esp_websocket_client_set_uri(client, config->uri) != ESP_OK) {
ESP_LOGE(TAG, "Invalid uri");
goto _websocket_init_fail;
}
}
if (esp_websocket_client_set_config(client, config) != ESP_OK) {
ESP_LOGE(TAG, "Failed to set the configuration");
goto _websocket_init_fail;
}
if (client->config->scheme == NULL) {
asprintf(&client->config->scheme, "ws");
ESP_TRANSPORT_MEM_CHECK(TAG, client->config->scheme, goto _websocket_init_fail);
}
client->keepalive_tick_ms = _tick_get_ms();
client->reconnect_tick_ms = _tick_get_ms();
client->ping_tick_ms = _tick_get_ms();
int buffer_size = config->buffer_size;
if (buffer_size <= 0) {
buffer_size = WEBSOCKET_BUFFER_SIZE_BYTE;
}
client->rx_buffer = malloc(buffer_size);
ESP_TRANSPORT_MEM_CHECK(TAG, client->rx_buffer, {
goto _websocket_init_fail;
});
client->tx_buffer = malloc(buffer_size);
ESP_TRANSPORT_MEM_CHECK(TAG, client->tx_buffer, {
goto _websocket_init_fail;
});
client->status_bits = xEventGroupCreate();
ESP_TRANSPORT_MEM_CHECK(TAG, client->status_bits, {
goto _websocket_init_fail;
});
client->buffer_size = buffer_size;
return client;
_websocket_init_fail:
esp_websocket_client_destroy(client);
return NULL;
}
esp_err_t esp_websocket_client_destroy(esp_websocket_client_handle_t client)
{
if (client == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (client->run) {
esp_websocket_client_stop(client);
}
if (client->event_handle) {
esp_event_loop_delete(client->event_handle);
}
esp_websocket_client_destroy_config(client);
esp_transport_list_destroy(client->transport_list);
vQueueDelete(client->lock);
free(client->tx_buffer);
free(client->rx_buffer);
if (client->status_bits) {
vEventGroupDelete(client->status_bits);
}
free(client);
client = NULL;
return ESP_OK;
}
esp_err_t esp_websocket_client_set_uri(esp_websocket_client_handle_t client, const char *uri)
{
if (client == NULL || uri == NULL) {
return ESP_ERR_INVALID_ARG;
}
struct http_parser_url puri;
http_parser_url_init(&puri);
int parser_status = http_parser_parse_url(uri, strlen(uri), 0, &puri);
if (parser_status != 0) {
ESP_LOGE(TAG, "Error parse uri = %s", uri);
return ESP_FAIL;
}
if (puri.field_data[UF_SCHEMA].len) {
free(client->config->scheme);
asprintf(&client->config->scheme, "%.*s", puri.field_data[UF_SCHEMA].len, uri + puri.field_data[UF_SCHEMA].off);
ESP_TRANSPORT_MEM_CHECK(TAG, client->config->scheme, return ESP_ERR_NO_MEM);
}
if (puri.field_data[UF_HOST].len) {
free(client->config->host);
asprintf(&client->config->host, "%.*s", puri.field_data[UF_HOST].len, uri + puri.field_data[UF_HOST].off);
ESP_TRANSPORT_MEM_CHECK(TAG, client->config->host, return ESP_ERR_NO_MEM);
}
if (puri.field_data[UF_PATH].len) {
free(client->config->path);
asprintf(&client->config->path, "%.*s", puri.field_data[UF_PATH].len, uri + puri.field_data[UF_PATH].off);
ESP_TRANSPORT_MEM_CHECK(TAG, client->config->path, return ESP_ERR_NO_MEM);
esp_transport_handle_t trans = esp_transport_list_get_transport(client->transport_list, "ws");
if (trans) {
esp_transport_ws_set_path(trans, client->config->path);
}
trans = esp_transport_list_get_transport(client->transport_list, "wss");
if (trans) {
esp_transport_ws_set_path(trans, client->config->path);
}
}
if (puri.field_data[UF_PORT].off) {
client->config->port = strtol((const char*)(uri + puri.field_data[UF_PORT].off), NULL, 10);
}
if (puri.field_data[UF_USERINFO].len) {
char *user_info = NULL;
asprintf(&user_info, "%.*s", puri.field_data[UF_USERINFO].len, uri + puri.field_data[UF_USERINFO].off);
if (user_info) {
char *pass = strchr(user_info, ':');
if (pass) {
pass[0] = 0; //terminal username
pass ++;
free(client->config->password);
client->config->password = strdup(pass);
ESP_TRANSPORT_MEM_CHECK(TAG, client->config->password, return ESP_ERR_NO_MEM);
}
free(client->config->username);
client->config->username = strdup(user_info);
ESP_TRANSPORT_MEM_CHECK(TAG, client->config->username, return ESP_ERR_NO_MEM);
free(user_info);
} else {
return ESP_ERR_NO_MEM;
}
}
return ESP_OK;
}
static void esp_websocket_client_task(void *pv)
{
int rlen;
esp_websocket_client_handle_t client = (esp_websocket_client_handle_t) pv;
client->run = true;
//get transport by scheme
client->transport = esp_transport_list_get_transport(client->transport_list, client->config->scheme);
if (client->transport == NULL) {
ESP_LOGE(TAG, "There are no transports valid, stop websocket client");
client->run = false;
}
//default port
if (client->config->port == 0) {
client->config->port = esp_transport_get_default_port(client->transport);
}
client->state = WEBSOCKET_STATE_INIT;
xEventGroupClearBits(client->status_bits, STOPPED_BIT);
int read_select;
while (client->run) {
switch ((int)client->state) {
case WEBSOCKET_STATE_INIT:
if (client->transport == NULL) {
ESP_LOGE(TAG, "There are no transport");
client->run = false;
break;
}
if (esp_transport_connect(client->transport,
client->config->host,
client->config->port,
client->config->network_timeout_ms) < 0) {
ESP_LOGE(TAG, "Error transport connect");
esp_websocket_client_abort_connection(client);
break;
}
ESP_LOGD(TAG, "Transport connected to %s://%s:%d", client->config->scheme, client->config->host, client->config->port);
client->state = WEBSOCKET_STATE_CONNECTED;
esp_websocket_client_dispatch_event(client, WEBSOCKET_EVENT_CONNECTED, NULL, 0);
break;
case WEBSOCKET_STATE_CONNECTED:
read_select = esp_transport_poll_read(client->transport, 1000); //Poll every 1000ms
if (read_select < 0) {
ESP_LOGE(TAG, "Network error, errorno");
esp_websocket_client_abort_connection(client);
break;
}
if (_tick_get_ms() - client->ping_tick_ms > WEBSOCKET_PING_TIMEOUT_MS) {
client->ping_tick_ms = _tick_get_ms();
// Send PING
esp_transport_write(client->transport, NULL, 0, client->config->network_timeout_ms);
}
if (read_select == 0) {
ESP_LOGD(TAG, "Timeout...");
continue;
}
client->ping_tick_ms = _tick_get_ms();
rlen = esp_transport_read(client->transport, client->rx_buffer, client->buffer_size, client->config->network_timeout_ms);
if (rlen < 0) {
ESP_LOGE(TAG, "Error read data");
esp_websocket_client_abort_connection(client);
break;
}
if (rlen > 0) {
esp_websocket_client_dispatch_event(client, WEBSOCKET_EVENT_DATA, client->rx_buffer, rlen);
}
break;
case WEBSOCKET_STATE_WAIT_TIMEOUT:
if (!client->config->auto_reconnect) {
client->run = false;
break;
}
if (_tick_get_ms() - client->reconnect_tick_ms > client->wait_timeout_ms) {
client->state = WEBSOCKET_STATE_INIT;
client->reconnect_tick_ms = _tick_get_ms();
ESP_LOGD(TAG, "Reconnecting...");
}
vTaskDelay(client->wait_timeout_ms / 2 / portTICK_RATE_MS);
break;
}
}
esp_transport_close(client->transport);
xEventGroupSetBits(client->status_bits, STOPPED_BIT);
client->state = WEBSOCKET_STATE_UNKNOW;
vTaskDelete(NULL);
}
esp_err_t esp_websocket_client_start(esp_websocket_client_handle_t client)
{
if (client == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (client->state >= WEBSOCKET_STATE_INIT) {
ESP_LOGE(TAG, "The client has started");
return ESP_FAIL;
}
if (xTaskCreate(esp_websocket_client_task, "websocket_task", client->config->task_stack, client, client->config->task_prio, NULL) != pdTRUE) {
ESP_LOGE(TAG, "Error create websocket task");
return ESP_FAIL;
}
xEventGroupClearBits(client->status_bits, STOPPED_BIT);
return ESP_OK;
}
esp_err_t esp_websocket_client_stop(esp_websocket_client_handle_t client)
{
if (client == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (!client->run) {
ESP_LOGW(TAG, "Client was not started");
return ESP_FAIL;
}
client->run = false;
xEventGroupWaitBits(client->status_bits, STOPPED_BIT, false, true, portMAX_DELAY);
client->state = WEBSOCKET_STATE_UNKNOW;
return ESP_OK;
}
int esp_websocket_client_send(esp_websocket_client_handle_t client, const char *data, int len, TickType_t timeout)
{
int need_write = len;
int wlen = 0, widx = 0;
if (client == NULL || data == NULL || len <= 0) {
ESP_LOGE(TAG, "Invalid arguments");
return ESP_FAIL;
}
if (!esp_websocket_client_is_connected(client)) {
ESP_LOGE(TAG, "Websocket client is not connected");
return ESP_FAIL;
}
if (client->transport == NULL) {
ESP_LOGE(TAG, "Invalid transport");
return ESP_FAIL;
}
if (xSemaphoreTake(client->lock, timeout) != pdPASS) {
return ESP_FAIL;
}
while (widx < len) {
if (need_write > client->buffer_size) {
need_write = client->buffer_size;
}
memcpy(client->tx_buffer, data + widx, need_write);
wlen = esp_transport_write(client->transport,
(char *)client->tx_buffer,
need_write,
client->config->network_timeout_ms);
if (wlen <= 0) {
xSemaphoreGive(client->lock);
return wlen;
}
widx += wlen;
need_write = len - widx;
}
xSemaphoreGive(client->lock);
return widx;
}
bool esp_websocket_client_is_connected(esp_websocket_client_handle_t client)
{
if (client == NULL) {
return false;
}
return client->state == WEBSOCKET_STATE_CONNECTED;
}
esp_err_t esp_websocket_register_events(esp_websocket_client_handle_t client,
esp_websocket_event_id_t event,
esp_event_handler_t event_handler,
void* event_handler_arg) {
if (client == NULL) {
return ESP_ERR_INVALID_ARG;
}
return esp_event_handler_register_with(client->event_handle, WEBSOCKET_EVENTS, event, event_handler, event_handler_arg);
}

View File

@ -0,0 +1,195 @@
// Copyright 2015-2018 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef _ESP_WEBSOCKET_CLIENT_H_
#define _ESP_WEBSOCKET_CLIENT_H_
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "esp_err.h"
#include "esp_event.h"
#include "esp_event_loop.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct esp_websocket_client* esp_websocket_client_handle_t;
ESP_EVENT_DECLARE_BASE(WEBSOCKET_EVENTS); // declaration of the task events family
/**
* @brief Websocket Client events id
*/
typedef enum {
WEBSOCKET_EVENT_ANY = -1,
WEBSOCKET_EVENT_ERROR = 0, /*!< This event occurs when there are any errors during execution */
WEBSOCKET_EVENT_CONNECTED, /*!< Once the Websocket has been connected to the server, no data exchange has been performed */
WEBSOCKET_EVENT_DISCONNECTED, /*!< The connection has been disconnected */
WEBSOCKET_EVENT_DATA, /*!< When receiving data from the server, possibly multiple portions of the packet */
WEBSOCKET_EVENT_MAX
} esp_websocket_event_id_t;
/**
* @brief Websocket event data
*/
typedef struct {
const char *data_ptr; /*!< Data pointer */
int data_len; /*!< Data length */
} esp_websocket_event_data_t;
/**
* @brief Websocket Client transport
*/
typedef enum {
WEBSOCKET_TRANSPORT_UNKNOWN = 0x0, /*!< Transport unknown */
WEBSOCKET_TRANSPORT_OVER_TCP, /*!< Transport over tcp */
WEBSOCKET_TRANSPORT_OVER_SSL, /*!< Transport over ssl */
} esp_websocket_transport_t;
/**
* @brief Websocket Client events data
*/
typedef struct {
esp_websocket_event_id_t event_id; /*!< event_id, to know the cause of the event */
esp_websocket_client_handle_t client; /*!< esp_websocket_client_handle_t context */
void *user_context;/*!< user_data context, from esp_websocket_client_config_t user_data */
char *data; /*!< data of the event */
int data_len; /*!< length of data */
} esp_websocket_event_t;
typedef esp_websocket_event_t* esp_websocket_event_handle_t;
typedef esp_err_t (* websocket_event_callback_t)(esp_websocket_event_handle_t event);
/**
* @brief Websocket client setup configuration
*/
typedef struct {
const char *uri; /*!< Websocket URI, the information on the URI can be overrides the other fields below, if any */
const char *host; /*!< Domain or IP as string */
int port; /*!< Port to connect, default depend on esp_websocket_transport_t (80 or 443) */
const char *username; /*!< Using for Http authentication - Not supported for now */
const char *password; /*!< Using for Http authentication - Not supported for now */
const char *path; /*!< HTTP Path, if not set, default is `/` */
bool disable_auto_reconnect; /*!< Disable the automatic reconnect function when disconnected */
void *user_context; /*!< HTTP user data context */
int task_prio; /*!< Websocket task priority */
int task_stack; /*!< Websocket task stack */
int buffer_size; /*!< Websocket buffer size */
const char *cert_pem; /*!< SSL Certification, PEM format as string, if the client requires to verify server */
esp_websocket_transport_t transport; /*!< Websocket transport type, see `esp_websocket_transport_t */
} esp_websocket_client_config_t;
/**
* @brief Start a Websocket session
* This function must be the first function to call,
* and it returns a esp_websocket_client_handle_t that you must use as input to other functions in the interface.
* This call MUST have a corresponding call to esp_websocket_client_destroy when the operation is complete.
*
* @param[in] config The configuration
*
* @return
* - `esp_websocket_client_handle_t`
* - NULL if any errors
*/
esp_websocket_client_handle_t esp_websocket_client_init(const esp_websocket_client_config_t *config);
/**
* @brief Set URL for client, when performing this behavior, the options in the URL will replace the old ones
* Must stop the WebSocket client before set URI if the client has been connected
*
* @param[in] client The client
* @param[in] uri The uri
*
* @return esp_err_t
*/
esp_err_t esp_websocket_client_set_uri(esp_websocket_client_handle_t client, const char *uri);
/**
* @brief Open the WebSocket connection
*
* @param[in] client The client
*
* @return esp_err_t
*/
esp_err_t esp_websocket_client_start(esp_websocket_client_handle_t client);
/**
* @brief Close the WebSocket connection
*
* @param[in] client The client
*
* @return esp_err_t
*/
esp_err_t esp_websocket_client_stop(esp_websocket_client_handle_t client);
/**
* @brief Destroy the WebSocket connection and free all resources.
* This function must be the last function to call for an session.
* It is the opposite of the esp_websocket_client_init function and must be called with the same handle as input that a esp_websocket_client_init call returned.
* This might close all connections this handle has used.
*
* @param[in] client The client
*
* @return esp_err_t
*/
esp_err_t esp_websocket_client_destroy(esp_websocket_client_handle_t client);
/**
* @brief Write data to the WebSocket connection
*
* @param[in] client The client
* @param[in] data The data
* @param[in] len The length
* @param[in] timeout Write data timeout
*
* @return
* - Number of data was sent
* - (-1) if any errors
*/
int esp_websocket_client_send(esp_websocket_client_handle_t client, const char *data, int len, TickType_t timeout);
/**
* @brief Check the WebSocket connection status
*
* @param[in] client The client handle
*
* @return
* - true
* - false
*/
bool esp_websocket_client_is_connected(esp_websocket_client_handle_t client);
/**
* @brief Register the Websocket Events
*
* @param client The client handle
* @param event The event id
* @param event_handler The callback function
* @param event_handler_arg User context
* @return esp_err_t
*/
esp_err_t esp_websocket_register_events(esp_websocket_client_handle_t client,
esp_websocket_event_id_t event,
esp_event_handler_t event_handler,
void* event_handler_arg);
#ifdef __cplusplus
}
#endif
#endif

@ -1 +1 @@
Subproject commit e205913b2cf3eff20b1f8ced7c76cf03533f130b
Subproject commit 11f884623bd32cb4269f24f47847f5d046da93f5

View File

@ -23,8 +23,25 @@ extern "C" {
*/
esp_transport_handle_t esp_transport_ws_init(esp_transport_handle_t parent_handle);
/**
* @brief Set HTTP path to update protocol to websocket
*
* @param t websocket transport handle
* @param path The HTTP Path
*/
void esp_transport_ws_set_path(esp_transport_handle_t t, const char *path);
/**
* @brief Set websocket sub protocol header
*
* @param t websocket transport handle
* @param sub_protocol Sub protocol string
*
* @return
* - ESP_OK on success
* - One of the error codes
*/
esp_err_t esp_transport_ws_set_subprotocol(esp_transport_handle_t t, const char *sub_protocol);
#ifdef __cplusplus

View File

@ -112,6 +112,7 @@ static int ssl_write(esp_transport_handle_t t, const char *buffer, int len, int
ret = esp_tls_conn_write(ssl->tls, (const unsigned char *) buffer, len);
if (ret < 0) {
ESP_LOGE(TAG, "esp_tls_conn_write error, errno=%s", strerror(errno));
return -1;
}
return ret;
}
@ -129,6 +130,7 @@ static int ssl_read(esp_transport_handle_t t, char *buffer, int len, int timeout
ret = esp_tls_conn_read(ssl->tls, (unsigned char *)buffer, len);
if (ret < 0) {
ESP_LOGE(TAG, "esp_tls_conn_read error, errno=%s", strerror(errno));
return -1;
}
if (ret == 0) {
ret = -1;

View File

@ -25,12 +25,13 @@ static const char *TAG = "TRANSPORT_WS";
#define WS_MASK 0x80
#define WS_SIZE16 126
#define WS_SIZE64 127
#define MAX_WEBSOCKET_HEADER_SIZE 10
#define MAX_WEBSOCKET_HEADER_SIZE 16
#define WS_RESPONSE_OK 101
typedef struct {
char *path;
char *buffer;
char *sub_protocol;
esp_transport_handle_t parent;
} transport_ws_t;
@ -80,7 +81,7 @@ static int ws_connect(esp_transport_handle_t t, const char *host, int port, int
{
transport_ws_t *ws = esp_transport_get_context_data(t);
if (esp_transport_connect(ws->parent, host, port, timeout_ms) < 0) {
ESP_LOGE(TAG, "Error connect to the server");
ESP_LOGE(TAG, "Error connecting to host %s:%d", host, port);
return -1;
}
@ -98,12 +99,15 @@ static int ws_connect(esp_transport_handle_t t, const char *host, int port, int
"Host: %s:%d\r\n"
"Upgrade: websocket\r\n"
"Sec-WebSocket-Version: 13\r\n"
"Sec-WebSocket-Protocol: mqtt\r\n"
"Sec-WebSocket-Key: %s\r\n"
"User-Agent: ESP32 Websocket Client\r\n\r\n",
"User-Agent: ESP32 Websocket Client\r\n",
ws->path,
host, port,
client_key);
if (ws->sub_protocol) {
len += snprintf(ws->buffer + len, DEFAULT_WS_BUFFER - len, "Sec-WebSocket-Protocol: %s\r\n", ws->sub_protocol);
}
len += snprintf(ws->buffer + len, DEFAULT_WS_BUFFER - len, "\r\n");
if (len <= 0 || len >= DEFAULT_WS_BUFFER) {
ESP_LOGE(TAG, "Error in request generation, %d", len);
return -1;
@ -152,42 +156,70 @@ static int ws_connect(esp_transport_handle_t t, const char *host, int port, int
return 0;
}
static int ws_write(esp_transport_handle_t t, const char *buff, int len, int timeout_ms)
static int _ws_write(esp_transport_handle_t t, int opcode, int mask_flag, const char *b, int len, int timeout_ms)
{
transport_ws_t *ws = esp_transport_get_context_data(t);
char *buffer = (char *)b;
char ws_header[MAX_WEBSOCKET_HEADER_SIZE];
char *mask;
int header_len = 0, i;
char *buffer = (char *)buff;
int poll_write;
if ((poll_write = esp_transport_poll_write(ws->parent, timeout_ms)) <= 0) {
ESP_LOGE(TAG, "Error transport_poll_write");
return poll_write;
}
ws_header[header_len++] = opcode;
ws_header[header_len++] = WS_OPCODE_BINARY | WS_FIN;
// NOTE: no support for > 16-bit sized messages
if (len > 125) {
ws_header[header_len++] = WS_SIZE16 | WS_MASK;
if (len <= 125) {
ws_header[header_len++] = (uint8_t)(len | mask_flag);
} else if (len < 65536) {
ws_header[header_len++] = WS_SIZE16 | mask_flag;
ws_header[header_len++] = (uint8_t)(len >> 8);
ws_header[header_len++] = (uint8_t)(len & 0xFF);
} else {
ws_header[header_len++] = (uint8_t)(len | WS_MASK);
ws_header[header_len++] = WS_SIZE64 | mask_flag;
/* Support maximum 4 bytes length */
ws_header[header_len++] = 0; //(uint8_t)((len >> 56) & 0xFF);
ws_header[header_len++] = 0; //(uint8_t)((len >> 48) & 0xFF);
ws_header[header_len++] = 0; //(uint8_t)((len >> 40) & 0xFF);
ws_header[header_len++] = 0; //(uint8_t)((len >> 32) & 0xFF);
ws_header[header_len++] = (uint8_t)((len >> 24) & 0xFF);
ws_header[header_len++] = (uint8_t)((len >> 16) & 0xFF);
ws_header[header_len++] = (uint8_t)((len >> 8) & 0xFF);
ws_header[header_len++] = (uint8_t)((len >> 0) & 0xFF);
}
mask = &ws_header[header_len];
getrandom(ws_header + header_len, 4, 0);
header_len += 4;
if (len) {
if (mask_flag) {
mask = &ws_header[header_len];
getrandom(ws_header + header_len, 4, 0);
header_len += 4;
for (i = 0; i < len; ++i) {
buffer[i] = (buffer[i] ^ mask[i % 4]);
}
}
for (i = 0; i < len; ++i) {
buffer[i] = (buffer[i] ^ mask[i % 4]);
}
if (esp_transport_write(ws->parent, ws_header, header_len, timeout_ms) != header_len) {
ESP_LOGE(TAG, "Error write header");
return -1;
}
if (len == 0) {
return 0;
}
return esp_transport_write(ws->parent, buffer, len, timeout_ms);
}
static int ws_write(esp_transport_handle_t t, const char *b, int len, int timeout_ms)
{
if (len == 0) {
ESP_LOGD(TAG, "Write PING message");
return _ws_write(t, WS_OPCODE_PING | WS_FIN, 0, NULL, 0, timeout_ms);
}
return _ws_write(t, WS_OPCODE_BINARY | WS_FIN, WS_MASK, b, len, timeout_ms);
}
static int ws_read(esp_transport_handle_t t, char *buffer, int len, int timeout_ms)
{
transport_ws_t *ws = esp_transport_get_context_data(t);
@ -279,6 +311,7 @@ static esp_err_t ws_destroy(esp_transport_handle_t t)
transport_ws_t *ws = esp_transport_get_context_data(t);
free(ws->buffer);
free(ws->path);
free(ws->sub_protocol);
free(ws);
return 0;
}
@ -288,6 +321,7 @@ void esp_transport_ws_set_path(esp_transport_handle_t t, const char *path)
ws->path = realloc(ws->path, strlen(path) + 1);
strcpy(ws->path, path);
}
esp_transport_handle_t esp_transport_ws_init(esp_transport_handle_t parent_handle)
{
esp_transport_handle_t t = esp_transport_init();
@ -315,3 +349,22 @@ esp_transport_handle_t esp_transport_ws_init(esp_transport_handle_t parent_handl
return t;
}
esp_err_t esp_transport_ws_set_subprotocol(esp_transport_handle_t t, const char *sub_protocol)
{
if (t == NULL) {
return ESP_ERR_INVALID_ARG;
}
transport_ws_t *ws = esp_transport_get_context_data(t);
if (ws->sub_protocol) {
free(ws->sub_protocol);
}
if (sub_protocol == NULL) {
ws->sub_protocol = NULL;
return ESP_OK;
}
ws->sub_protocol = strdup(sub_protocol);
if (ws->sub_protocol == NULL) {
return ESP_ERR_NO_MEM;
}
return ESP_OK;
}

View File

@ -104,6 +104,7 @@ INPUT = \
## mDNS
../../components/mdns/include/mdns.h \
../../components/esp_http_client/include/esp_http_client.h \
../../components/esp_websocket_client/include/esp_websocket_client.h \
../../components/esp_http_server/include/esp_http_server.h \
../../components/esp_https_server/include/esp_https_server.h \
##

View File

@ -0,0 +1,70 @@
ESP WebSocket Client
====================
Overview
--------
The ESP WebSocket client is an implementation of `WebSocket protocol client <https://tools.ietf.org/html/rfc6455>`_ for ESP32
Features
--------
* supports WebSocket over TCP, SSL with mbedtls
* Easy to setup with URI
* Multiple instances (Multiple clients in one application)
Configuration
-------------
URI
^^^
- Supports ``ws``, ``wss`` schemes
- WebSocket samples:
- ``ws://websocket.org``: WebSocket over TCP, default port 80
- ``wss://websocket.org``: WebSocket over SSL, default port 443
- Minimal configurations:
.. code:: c
const esp_websocket_client_config_t ws_cfg = {
.uri = "ws://websocket.org",
};
- If there are any options related to the URI in
``esp_websocket_client_config_t``, the option defined by the URI will be
overridden. Sample:
.. code:: c
const esp_websocket_client_config_t ws_cfg = {
.uri = "ws://websocket.org:123",
.port = 4567,
};
//WebSocket client will connect to websocket.org using port 4567
SSL
^^^
- Get certificate from server, example: ``websocket.org``
``openssl s_client -showcerts -connect websocket.org:443 </dev/null 2>/dev/null|openssl x509 -outform PEM >websocket_org.pem``
- Configuration:
.. code:: cpp
const esp_websocket_client_config_t ws_cfg = {
.uri = "wss://websocket.org",
.cert_pem = (const char *)websocket_org_pem_start,
};
For more options on ``esp_websocket_client_config_t``, please refer to API reference below
Application Example
-------------------
Simple WebSocket example that uses esp_websocket_client to establish a websocket connection and send/receive data with the `websocket.org <https://websocket.org>`_ Server: :example:`protocols/websocket`.
API Reference
-------------
.. include:: /_build/inc/esp_websocket_client.inc

View File

@ -8,6 +8,7 @@ Application Protocols
mDNS <mdns>
ESP-TLS <esp_tls>
HTTP Client <esp_http_client>
Websocket Client <esp_websocket_client>
HTTP Server <esp_http_server>
HTTPS Server <esp_https_server>
ASIO <asio>

View File

@ -0,0 +1 @@
.. include:: ../../../en/api-reference/protocols/esp_websocket_client.rst

View File

@ -8,6 +8,7 @@
mDNS <mdns>
ESP-TLS <esp_tls>
HTTP Client <esp_http_client>
Websocket Client <esp_websocket_client>
HTTP 服务器 <esp_http_server>
HTTPS Server <esp_https_server>
ASIO <asio>

View File

@ -0,0 +1,10 @@
# The following four lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)
# (Not part of the boilerplate)
# This example uses an extra component for common functions such as Wi-Fi and Ethernet connection.
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(websocket-example)

View File

@ -0,0 +1,10 @@
#
# This is a project Makefile. It is assumed the directory this Makefile resides in is a
# project subdirectory.
#
PROJECT_NAME := websocket-example
EXTRA_COMPONENT_DIRS = $(IDF_PATH)/examples/common_components/protocol_examples_common
include $(IDF_PATH)/make/project.mk

View File

@ -0,0 +1 @@
# Websocket Sample application

View File

@ -0,0 +1,41 @@
import re
import os
import sys
import IDF
# this is a test case write with tiny-test-fw.
# to run test cases outside tiny-test-fw,
# we need to set environment variable `TEST_FW_PATH`,
# then get and insert `TEST_FW_PATH` to sys path before import FW module
test_fw_path = os.getenv("TEST_FW_PATH")
if test_fw_path and test_fw_path not in sys.path:
sys.path.insert(0, test_fw_path)
@IDF.idf_example_test(env_tag="Example_WIFI", ignore=True)
def test_examples_protocol_websocket(env, extra_data):
"""
steps: |
1. join AP
2. connect to ws://echo.websocket.org
3. send and receive data
"""
dut1 = env.get_dut("websocket", "examples/protocols/websocket")
# check and log bin size
binary_file = os.path.join(dut1.app.binary_path, "websocket-example.bin")
bin_size = os.path.getsize(binary_file)
IDF.log_performance("websocket_bin_size", "{}KB".format(bin_size // 1024))
IDF.check_performance("websocket_bin_size", bin_size // 1024)
# start test
dut1.start_app()
dut1.expect("Waiting for wifi ...")
dut1.expect("Connection established...", timeout=30)
dut1.expect("WEBSOCKET_EVENT_CONNECTED")
for i in range(0, 10):
dut1.expect(re.compile(r"Sending hello (\d)"))
dut1.expect(re.compile(r"Received=hello (\d)"))
dut1.expect("Websocket Stopped")
if __name__ == '__main__':
test_examples_protocol_websocket()

View File

@ -0,0 +1,4 @@
set(COMPONENT_SRCS "websocket_example.c")
set(COMPONENT_ADD_INCLUDEDIRS ".")
register_component()

View File

@ -0,0 +1,9 @@
menu "Example Configuration"
config WEBSOCKET_URI
string "Websocket endpoint URI"
default "ws://echo.websocket.org";
help
URL of websocket endpoint this example connects to and sends echo
endmenu

View File

@ -0,0 +1,103 @@
/* ESP HTTP Client 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 "esp_wifi.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "esp_event_loop.h"
#include "protocol_examples_common.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_log.h"
#include "esp_websocket_client.h"
#include "esp_event.h"
#include "esp_event_loop.h"
static const char *TAG = "WEBSOCKET";
static const char *WEBSOCKET_ECHO_ENDPOINT = CONFIG_WEBSOCKET_URI;
static void websocket_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
{
// esp_websocket_client_handle_t client = (esp_websocket_client_handle_t)handler_args;
esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data;
switch (event_id) {
case WEBSOCKET_EVENT_CONNECTED:
ESP_LOGI(TAG, "WEBSOCKET_EVENT_CONNECTED");
break;
case WEBSOCKET_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "WEBSOCKET_EVENT_DISCONNECTED");
break;
case WEBSOCKET_EVENT_DATA:
ESP_LOGI(TAG, "WEBSOCKET_EVENT_DATA");
ESP_LOGW(TAG, "Received=%.*s\r\n", data->data_len, (char*)data->data_ptr);
break;
case WEBSOCKET_EVENT_ERROR:
ESP_LOGI(TAG, "WEBSOCKET_EVENT_ERROR");
break;
}
}
static void websocket_app_start(void)
{
ESP_LOGI(TAG, "Connectiong to %s...", WEBSOCKET_ECHO_ENDPOINT);
const esp_websocket_client_config_t websocket_cfg = {
.uri = WEBSOCKET_ECHO_ENDPOINT, // or wss://echo.websocket.org for websocket secure
};
esp_websocket_client_handle_t client = esp_websocket_client_init(&websocket_cfg);
esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, websocket_event_handler, (void *)client);
esp_websocket_client_start(client);
char data[32];
int i = 0;
while (i < 10) {
if (esp_websocket_client_is_connected(client)) {
int len = sprintf(data, "hello %04d", i++);
ESP_LOGI(TAG, "Sending %s", data);
esp_websocket_client_send(client, data, len, portMAX_DELAY);
}
vTaskDelay(1000 / portTICK_RATE_MS);
}
esp_websocket_client_stop(client);
ESP_LOGI(TAG, "Websocket Stopped");
esp_websocket_client_destroy(client);
}
void app_main()
{
ESP_LOGI(TAG, "[APP] Startup..");
ESP_LOGI(TAG, "[APP] Free memory: %d bytes", esp_get_free_heap_size());
ESP_LOGI(TAG, "[APP] IDF version: %s", esp_get_idf_version());
esp_log_level_set("*", ESP_LOG_INFO);
esp_log_level_set("WEBSOCKET_CLIENT", ESP_LOG_DEBUG);
esp_log_level_set("TRANS_TCP", ESP_LOG_DEBUG);
ESP_ERROR_CHECK(nvs_flash_init());
tcpip_adapter_init();
ESP_ERROR_CHECK(esp_event_loop_create_default());
/* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig.
* Read "Establishing Wi-Fi or Ethernet Connection" section in
* examples/protocols/README.md for more information about this function.
*/
ESP_ERROR_CHECK(example_connect());
websocket_app_start();
}