From 12b326d9696347da5f396288bbe32c22f224eac6 Mon Sep 17 00:00:00 2001 From: Tuan PM Date: Mon, 12 Mar 2018 21:08:05 +0700 Subject: [PATCH] add support websocket --- examples/websocket/Makefile | 13 + examples/websocket/README.md | 1 + examples/websocket/main/Kconfig.projbuild | 15 + examples/websocket/main/app_main.c | 130 +++++++ examples/websocket/main/component.mk | 0 include/websocket_client.h | 70 ++++ websocket_client.c | 435 ++++++++++++++++++++++ 7 files changed, 664 insertions(+) create mode 100644 examples/websocket/Makefile create mode 100644 examples/websocket/README.md create mode 100644 examples/websocket/main/Kconfig.projbuild create mode 100755 examples/websocket/main/app_main.c create mode 100644 examples/websocket/main/component.mk create mode 100755 include/websocket_client.h create mode 100644 websocket_client.c diff --git a/examples/websocket/Makefile b/examples/websocket/Makefile new file mode 100644 index 0000000..a12f512 --- /dev/null +++ b/examples/websocket/Makefile @@ -0,0 +1,13 @@ +# +# This is a project Makefile. It is assumed the directory this Makefile resides in is a +# project subdirectory. +# +# +# This is a project Makefile. It is assumed the directory this Makefile resides in is a +# project subdirectory. +# +PROJECT_NAME := websocket +EXTRA_COMPONENT_DIRS += $(PROJECT_PATH)/../../../ + +include $(IDF_PATH)/make/project.mk + diff --git a/examples/websocket/README.md b/examples/websocket/README.md new file mode 100644 index 0000000..6362714 --- /dev/null +++ b/examples/websocket/README.md @@ -0,0 +1 @@ +# ESPMQTT Sample application diff --git a/examples/websocket/main/Kconfig.projbuild b/examples/websocket/main/Kconfig.projbuild new file mode 100644 index 0000000..06b8d8f --- /dev/null +++ b/examples/websocket/main/Kconfig.projbuild @@ -0,0 +1,15 @@ +menu "Websocket Application sample" + +config WIFI_SSID + string "WiFi SSID" + default "myssid" + help + SSID (network name) for the example to connect to. + +config WIFI_PASSWORD + string "WiFi Password" + default "mypassword" + help + WiFi password (WPA or WPA2) for the example to use. + +endmenu diff --git a/examples/websocket/main/app_main.c b/examples/websocket/main/app_main.c new file mode 100755 index 0000000..12432f6 --- /dev/null +++ b/examples/websocket/main/app_main.c @@ -0,0 +1,130 @@ +#include +#include +#include +#include +#include "esp_wifi.h" +#include "esp_system.h" +#include "nvs_flash.h" +#include "esp_event_loop.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "freertos/queue.h" +#include "freertos/event_groups.h" + +#include "lwip/sockets.h" +#include "lwip/dns.h" +#include "lwip/netdb.h" + +#include "esp_log.h" +#include "websocket_client.h" + +static const char *TAG = "WEBSOCKET"; + +static EventGroupHandle_t wifi_event_group; +const static int CONNECTED_BIT = BIT0; + + +static esp_err_t websocket_event_handler(esp_websocket_event_handle_t event) +{ + esp_websocket_client_handle_t client = event->client; + // your_context_t *context = event->context; + switch (event->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"); + printf("DATA=%.*s\r\n", event->data_len, event->data); + break; + case WEBSOCKET_EVENT_ERROR: + ESP_LOGI(TAG, "WEBSOCKET_EVENT_ERROR"); + break; + } + return ESP_OK; +} + +static esp_err_t wifi_event_handler(void *ctx, system_event_t *event) +{ + switch (event->event_id) { + case SYSTEM_EVENT_STA_START: + esp_wifi_connect(); + break; + case SYSTEM_EVENT_STA_GOT_IP: + xEventGroupSetBits(wifi_event_group, CONNECTED_BIT); + + break; + case SYSTEM_EVENT_STA_DISCONNECTED: + esp_wifi_connect(); + xEventGroupClearBits(wifi_event_group, CONNECTED_BIT); + break; + default: + break; + } + return ESP_OK; +} + +static void wifi_init(void) +{ + tcpip_adapter_init(); + wifi_event_group = xEventGroupCreate(); + ESP_ERROR_CHECK(esp_event_loop_init(wifi_event_handler, NULL)); + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); + wifi_config_t wifi_config = { + .sta = { + .ssid = CONFIG_WIFI_SSID, + .password = CONFIG_WIFI_PASSWORD, + }, + }; + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config)); + ESP_LOGI(TAG, "start the WIFI SSID:[%s] password:[%s]", CONFIG_WIFI_SSID, "******"); + ESP_ERROR_CHECK(esp_wifi_start()); + ESP_LOGI(TAG, "Waiting for wifi"); + xEventGroupWaitBits(wifi_event_group, CONNECTED_BIT, false, true, portMAX_DELAY); +} + +static void websocket_app_start(void) +{ + const esp_websocket_client_config_t websocket_cfg = { + .uri = "ws://echo.websocket.org", + .event_handle = websocket_event_handler, + // .user_context = (void *)your_context + }; + + esp_websocket_client_handle_t client = esp_websocket_client_init(&websocket_cfg); + esp_websocket_client_start(client); + while(1) { + if (esp_websocket_client_is_connected(client)) { + esp_websocket_client_send(client, "hello", 5); + } + vTaskDelay(500/portTICK_RATE_MS); + } +} + +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_VERBOSE); + esp_log_level_set("TRANSPORT_TCP", ESP_LOG_VERBOSE); + esp_log_level_set("TRANSPORT_SSL", ESP_LOG_VERBOSE); + esp_log_level_set("TRANSPORT", ESP_LOG_VERBOSE); + esp_log_level_set("OUTBOX", ESP_LOG_VERBOSE); + + nvs_flash_init(); + wifi_init(); + websocket_app_start(); +} diff --git a/examples/websocket/main/component.mk b/examples/websocket/main/component.mk new file mode 100644 index 0000000..e69de29 diff --git a/include/websocket_client.h b/include/websocket_client.h new file mode 100755 index 0000000..0cffe0e --- /dev/null +++ b/include/websocket_client.h @@ -0,0 +1,70 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE', which is part of this source code package. + * Tuan PM + */ + +#ifndef _WEBSOCKET_CLIENT_H_ +#define _WEBSOCKET_CLIENT_H_ + +#include +#include +#include + +#include "mqtt_config.h" + +typedef struct esp_websocket_client* esp_websocket_client_handle_t; + +typedef enum { + WEBSOCKET_EVENT_ERROR = 0, + WEBSOCKET_EVENT_CONNECTED, + WEBSOCKET_EVENT_DISCONNECTED, + WEBSOCKET_EVENT_DATA, +} esp_websocket_event_id_t; + +typedef enum { + WEBSOCKET_TRANSPORT_UNKNOWN = 0x0, + WEBSOCKET_TRANSPORT_OVER_TCP, + WEBSOCKET_TRANSPORT_OVER_SSL, +} esp_websocket_transport_t; + +typedef struct { + esp_websocket_event_id_t event_id; + esp_websocket_client_handle_t client; + void *user_context; + char *data; + int data_len; +} 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); + + +typedef struct { + websocket_event_callback_t event_handle; + const char *uri; + const char *scheme; + const char *host; + int port; + const char *username; + const char *password; + const char *path; + bool disable_auto_reconnect; + void *user_context; + int task_prio; + int task_stack; + int buffer_size; + const char *cert_pem; + esp_websocket_transport_t transport; +} esp_websocket_client_config_t; + +esp_websocket_client_handle_t esp_websocket_client_init(const esp_websocket_client_config_t *config); +esp_err_t esp_websocket_client_set_uri(esp_websocket_client_handle_t client, const char *uri); +esp_err_t esp_websocket_client_start(esp_websocket_client_handle_t client); +esp_err_t esp_websocket_client_stop(esp_websocket_client_handle_t client); +esp_err_t esp_websocket_client_destroy(esp_websocket_client_handle_t client); +int esp_websocket_client_send(esp_websocket_client_handle_t client, const char *data, int len); +bool esp_websocket_client_is_connected(esp_websocket_client_handle_t client); + +#endif diff --git a/websocket_client.c b/websocket_client.c new file mode 100644 index 0000000..a2cd810 --- /dev/null +++ b/websocket_client.c @@ -0,0 +1,435 @@ +#include +#include "platform.h" + +#include "websocket_client.h" +#include "transport.h" +#include "transport_tcp.h" +#include "transport_ssl.h" +#include "transport_ws.h" +#include "platform.h" + +/* using uri parser */ +#include "http_parser.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) + +const static int STOPPED_BIT = BIT0; + +typedef struct { + websocket_event_callback_t event_handle; + 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; +} websockeet_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 { + transport_list_handle_t transport_list; + transport_handle_t transport; + websockeet_config_storage_t *config; + websocket_client_state_t state; + long long keepalive_tick; + long long reconnect_tick; + int wait_timeout_ms; + int auto_reconnect; + esp_websocket_event_t event; + bool run; + EventGroupHandle_t status_bits; + char *buffer; + int buffer_size; +}; + +static char *create_string(const char *ptr, int len) +{ + char *ret; + if (len <= 0) { + return NULL; + } + ret = calloc(1, len + 1); + mem_assert(ret); + memcpy(ret, ptr, len); + return ret; +} + +static esp_err_t esp_websocket_client_dispatch_event(esp_websocket_client_handle_t client) +{ + client->event.user_context = client->config->user_context; + client->event.client = client; + + if (client->config->event_handle) { + return client->config->event_handle(&client->event); + } + return ESP_FAIL; +} + +static esp_err_t esp_websocket_client_abort_connection(esp_websocket_client_handle_t client) +{ + transport_close(client->transport); + client->wait_timeout_ms = WEBSOCKET_RECONNECT_TIMEOUT_MS; + client->reconnect_tick = platform_tick_get_ms(); + client->state = WEBSOCKET_STATE_WAIT_TIMEOUT; + ESP_LOGI(TAG, "Reconnect after %d ms", client->wait_timeout_ms); + client->event.event_id = WEBSOCKET_EVENT_DISCONNECTED; + esp_websocket_client_dispatch_event(client); + 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) +{ + //Copy user configurations to client context + websockeet_config_storage_t *cfg = calloc(1, sizeof(websockeet_config_storage_t)); + mem_assert(cfg); + + 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); + } + + cfg->port = config->port; + + if (config->username) { + cfg->username = strdup(config->username); + } + + if (config->password) { + cfg->password = strdup(config->password); + } + + if (config->uri) { + cfg->uri = strdup(config->uri); + } + + cfg->network_timeout_ms = WEBSOCKET_NETWORK_TIMEOUT_MS; + cfg->user_context = config->user_context; + cfg->event_handle = config->event_handle; + cfg->auto_reconnect = true; + if (config->disable_auto_reconnect) { + cfg->auto_reconnect = false; + } + + client->config = cfg; + return ESP_OK; +} + +static esp_err_t esp_websocket_client_destroy_config(esp_websocket_client_handle_t client) +{ + websockeet_config_storage_t *cfg = client->config; + if (cfg->host) { + free(cfg->host); + } + if (cfg->uri) { + free(cfg->uri); + } + if (cfg->path) { + free(cfg->path); + } + if (cfg->scheme) { + free(cfg->scheme); + } + if (cfg->username) { + free(cfg->username); + } + if (cfg->password) { + free(cfg->password); + } + + free(client->config); + 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)); + mem_assert(client); + + client->transport_list = transport_list_init(); + + transport_handle_t tcp = transport_tcp_init(); + transport_set_default_port(tcp, WEBSOCKET_TCP_DEFAULT_PORT); + transport_list_add(client->transport_list, tcp, "_tcp"); + + + transport_handle_t ws = transport_ws_init(tcp); + transport_set_default_port(ws, WEBSOCKET_TCP_DEFAULT_PORT); + transport_list_add(client->transport_list, ws, "ws"); + if (config->transport == WEBSOCKET_TRANSPORT_OVER_TCP) { + client->config->scheme = create_string("ws", 2); + } + + transport_handle_t ssl = transport_ssl_init(); + transport_set_default_port(ssl, WEBSOCKET_SSL_DEFAULT_PORT); + if (config->cert_pem) { + transport_ssl_set_cert_data(ssl, config->cert_pem, strlen(config->cert_pem)); + } + transport_list_add(client->transport_list, ssl, "_ssl"); + + transport_handle_t wss = transport_ws_init(ssl); + transport_set_default_port(wss, WEBSOCKET_SSL_DEFAULT_PORT); + transport_list_add(client->transport_list, wss, "wss"); + if (config->transport == WEBSOCKET_TRANSPORT_OVER_TCP) { + client->config->scheme = create_string("wss", 3); + } + + esp_websocket_client_set_config(client, config); + + if (client->config->uri) { + if (esp_websocket_client_set_uri(client, client->config->uri) != ESP_OK) { + return NULL; + } + } + + if (client->config->scheme == NULL) { + client->config->scheme = create_string("ws", 2); + } + + client->keepalive_tick = platform_tick_get_ms(); + client->reconnect_tick = platform_tick_get_ms(); + + int buffer_size = config->buffer_size; + if (buffer_size <= 0) { + buffer_size = WEBSOCKET_BUFFER_SIZE_BYTE; + } + + client->buffer = malloc(buffer_size); + assert(client->buffer); + client->buffer_size = buffer_size; + + esp_websocket_client_set_config(client, config); + if (config->uri) { + esp_websocket_client_set_uri(client, config->uri); + } + + client->status_bits = xEventGroupCreate(); + return client; +} + +esp_err_t esp_websocket_client_destroy(esp_websocket_client_handle_t client) +{ + esp_websocket_client_stop(client); + esp_websocket_client_destroy_config(client); + transport_list_destroy(client->transport_list); + free(client->buffer); + vEventGroupDelete(client->status_bits); + free(client); + return ESP_OK; +} + +esp_err_t esp_websocket_client_set_uri(esp_websocket_client_handle_t client, const char *uri) +{ + 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 (client->config->scheme == NULL) { + client->config->scheme = create_string(uri + puri.field_data[UF_SCHEMA].off, puri.field_data[UF_SCHEMA].len); + } + + if (client->config->host == NULL) { + client->config->host = create_string(uri + puri.field_data[UF_HOST].off, puri.field_data[UF_HOST].len); + } + + if (client->config->path == NULL) { + client->config->path = create_string(uri + puri.field_data[UF_PATH].off, puri.field_data[UF_PATH].len); + } + + if (client->config->path) { + transport_handle_t trans = transport_list_get_transport(client->transport_list, "ws"); + if (trans) { + transport_ws_set_path(trans, client->config->path); + } + trans = transport_list_get_transport(client->transport_list, "wss"); + if (trans) { + transport_ws_set_path(trans, client->config->path); + } + } + + char *port = create_string(uri + puri.field_data[UF_PORT].off, puri.field_data[UF_PORT].len); + if (port) { + client->config->port = atoi(port); + free(port); + } + + char *user_info = create_string(uri + puri.field_data[UF_USERINFO].off, puri.field_data[UF_USERINFO].len); + if (user_info) { + char *pass = strchr(user_info, ':'); + if (pass) { + pass[0] = 0; //terminal username + pass ++; + client->config->password = strdup(pass); + } + client->config->username = strdup(user_info); + + free(user_info); + } + + 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 = 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 = transport_get_default_port(client->transport); + } + + client->state = WEBSOCKET_STATE_INIT; + xEventGroupClearBits(client->status_bits, STOPPED_BIT); + + 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 (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->event.event_id = WEBSOCKET_EVENT_CONNECTED; + client->state = WEBSOCKET_STATE_CONNECTED; + esp_websocket_client_dispatch_event(client); + + break; + case WEBSOCKET_STATE_CONNECTED: + rlen = transport_read(client->transport, client->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) { + client->event.event_id = WEBSOCKET_EVENT_DATA; + client->event.data = client->buffer; + client->event.data_len = rlen; + esp_websocket_client_dispatch_event(client); + } + break; + case WEBSOCKET_STATE_WAIT_TIMEOUT: + + if (!client->config->auto_reconnect) { + client->run = false; + break; + } + if (platform_tick_get_ms() - client->reconnect_tick > client->wait_timeout_ms) { + client->state = WEBSOCKET_STATE_INIT; + client->reconnect_tick = platform_tick_get_ms(); + ESP_LOGD(TAG, "Reconnecting..."); + } + vTaskDelay(client->wait_timeout_ms / 2 / portTICK_RATE_MS); + break; + } + } + + transport_close(client->transport); + xEventGroupSetBits(client->status_bits, STOPPED_BIT); + vTaskDelete(NULL); +} + +esp_err_t esp_websocket_client_start(esp_websocket_client_handle_t client) +{ + if (client->state >= WEBSOCKET_STATE_INIT) { + ESP_LOGE(TAG, "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) +{ + 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) +{ + int need_write = len; + int wlen = 0, widx = 0; + while (widx < len) { + if (need_write > client->buffer_size) { + need_write = client->buffer_size; + } + memcpy(client->buffer, data + widx, need_write); + wlen = transport_write(client->transport, + (char *)client->buffer, + need_write, + client->config->network_timeout_ms); + if (wlen <= 0) { + return wlen; + } + widx += wlen; + need_write = len - widx; + } + return widx; +} + +bool esp_websocket_client_is_connected(esp_websocket_client_handle_t client) +{ + return client->state == WEBSOCKET_STATE_CONNECTED; +}