diff --git a/Web_socket_patch.zip b/Web_socket_patch.zip new file mode 100644 index 0000000000..ca062036ac Binary files /dev/null and b/Web_socket_patch.zip differ diff --git a/components/esp_websocket_client/README.md b/components/esp_websocket_client/README.md index bffb1aa663..22ea01b169 100644 --- a/components/esp_websocket_client/README.md +++ b/components/esp_websocket_client/README.md @@ -2,12 +2,22 @@ [![Component Registry](https://components.espressif.com/components/espressif/esp_websocket_client/badge.svg)](https://components.espressif.com/components/espressif/esp_websocket_client) -The `esp-websocket_client` component is a managed component for `esp-idf` that contains implementation of [WebSocket protocol client](https://datatracker.ietf.org/doc/html/rfc6455) for ESP32 +The `esp-websocket_client` component is a managed component for `esp-idf` that contains an implementation of the [WebSocket protocol client](https://datatracker.ietf.org/doc/html/rfc6455) for ESP targets. + +## Highlights + +- WebSocket over TCP (`ws`) and TLS (`wss`) +- Optional header callback event (`WEBSOCKET_EVENT_HEADER_RECEIVED`, IDF 6+) +- Fragmented message send helpers for text and binary payloads +- Runtime ping/reconnect tuning APIs +- Pause/resume support without destroying the websocket task +- Automatic HTTP redirect handling during websocket upgrade ## Examples -Get started with example test [example](https://github.com/espressif/esp-protocols/tree/master/components/esp_websocket_client/examples): +- Component examples: +- Feature showcase example (comprehensive walkthrough): ## Documentation -* View the full [html documentation](https://docs.espressif.com/projects/esp-protocols/esp_websocket_client/docs/latest/index.html) +- Full HTML documentation: diff --git a/components/esp_websocket_client/esp_websocket_client.c b/components/esp_websocket_client/esp_websocket_client.c index 6616ac6e87..96f5955e80 100644 --- a/components/esp_websocket_client/esp_websocket_client.c +++ b/components/esp_websocket_client/esp_websocket_client.c @@ -23,6 +23,31 @@ #include #include +/* + * ESP-IDF 4.4.x compatibility shims. + * These functions / struct fields were added in later esp-protocols releases + * and are not present in the bundled IDF 4.4.7 tcp_transport library. + */ +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + +/* esp_transport_ws_get_fin_flag() is unavailable in IDF 4.4. + * The old transport always delivers complete frames, so default to true. */ +static inline bool esp_transport_ws_get_fin_flag(esp_transport_handle_t t) +{ + (void)t; + return true; +} + +/* esp_transport_ws_get_upgrade_request_status() is unavailable in IDF 4.4. + * Return 0 (unknown) when the API does not exist. */ +static inline int esp_transport_ws_get_upgrade_request_status(esp_transport_handle_t t) +{ + (void)t; + return 0; +} + +#endif /* ESP_IDF_VERSION < 5.0.0 */ + static const char *TAG = "websocket_client"; #define WEBSOCKET_TCP_DEFAULT_PORT (80) @@ -75,6 +100,8 @@ const static int STOPPED_BIT = BIT0; const static int CLOSE_FRAME_SENT_BIT = BIT1; // Indicates that a close frame was sent by the client // and we are waiting for the server to continue with clean close const static int REQUESTED_STOP_BIT = BIT2; // Indicates that a client stop has been requested +const static int RESUME_BIT = BIT3; // Signal to resume from PAUSED state +const static int WAKE_BIT = BIT4; // Generic: wake task from any blocking event wait to re-check state ESP_EVENT_DEFINE_BASE(WEBSOCKET_EVENTS); @@ -122,6 +149,7 @@ typedef enum { WEBSOCKET_STATE_CONNECTED, WEBSOCKET_STATE_WAIT_TIMEOUT, WEBSOCKET_STATE_CLOSING, + WEBSOCKET_STATE_PAUSED, // Task alive but blocked; waiting for RESUME_BIT } websocket_client_state_t; struct esp_websocket_client { @@ -259,8 +287,8 @@ static esp_err_t esp_websocket_client_abort_connection(esp_websocket_client_hand if (client->state == WEBSOCKET_STATE_CLOSING || client->state == WEBSOCKET_STATE_UNKNOW || - client->state == WEBSOCKET_STATE_WAIT_TIMEOUT) { - ESP_LOGW(TAG, "Connection already closing/closed, skipping abort"); + client->state == WEBSOCKET_STATE_WAIT_TIMEOUT || client->state == WEBSOCKET_STATE_PAUSED) { + ESP_LOGW(TAG, "Connection already closing/closed/paused, skipping abort"); goto cleanup; } @@ -536,6 +564,82 @@ static esp_err_t stop_wait_task(esp_websocket_client_handle_t client) return ESP_OK; } +esp_err_t esp_websocket_client_pause(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; + } + + /* Cannot pause from within the websocket task */ + TaskHandle_t running_task = xTaskGetCurrentTaskHandle(); + if (running_task == client->task_handle) { + ESP_LOGE(TAG, "Client cannot be paused from websocket task"); + return ESP_FAIL; + } + + xSemaphoreTakeRecursive(client->lock, portMAX_DELAY); + + if (client->state == WEBSOCKET_STATE_PAUSED) { + xSemaphoreGiveRecursive(client->lock); + return ESP_OK; /* already paused */ + } + + bool was_connected = (client->state == WEBSOCKET_STATE_CONNECTED); + + /* Close the TCP/TLS transport (does NOT destroy the transport object) */ + if (client->transport) { + esp_transport_close(client->transport); + } + + client->state = WEBSOCKET_STATE_PAUSED; + xEventGroupClearBits(client->status_bits, CLOSE_FRAME_SENT_BIT | RESUME_BIT); + + xSemaphoreGiveRecursive(client->lock); + + /* Wake the task if it's blocked in any event-group wait (e.g. WAIT_TIMEOUT) */ + xEventGroupSetBits(client->status_bits, WAKE_BIT); + + if (was_connected) { + esp_websocket_client_dispatch_event(client, WEBSOCKET_EVENT_DISCONNECTED, NULL, 0); + } + + ESP_LOGI(TAG, "Client paused (task kept alive)"); + return ESP_OK; +} + +esp_err_t esp_websocket_client_resume(esp_websocket_client_handle_t client, const char *headers) +{ + if (client == NULL) { + return ESP_ERR_INVALID_ARG; + } + if (!client->run) { + ESP_LOGW(TAG, "Client was not started"); + return ESP_FAIL; + } + if (client->state != WEBSOCKET_STATE_PAUSED) { + ESP_LOGW(TAG, "Client is not paused (state=%d)", (int)client->state); + return ESP_FAIL; + } + + /* Update config headers while the task is blocked (safe, no lock needed). + * The task will call set_websocket_transport_optional_settings() when it + * picks up RESUME_BIT to push these into the ws transport layer. */ + if (headers) { + xSemaphoreTakeRecursive(client->lock, portMAX_DELAY); + free(client->config->headers); + client->config->headers = strdup(headers); + xSemaphoreGiveRecursive(client->lock); + } + + xEventGroupSetBits(client->status_bits, RESUME_BIT); + ESP_LOGI(TAG, "Resume requested"); + return ESP_OK; +} + #if WS_TRANSPORT_HEADER_CALLBACK_SUPPORT static void websocket_header_hook(void * client, const char * line, int line_len) { @@ -557,7 +661,6 @@ static esp_err_t set_websocket_transport_optional_settings(esp_websocket_client_ .header_hook = websocket_header_hook, .header_user_context = client, #endif - .auth = client->config->auth, .propagate_control_frames = true }; return esp_transport_ws_set_config(trans, &config); @@ -1333,6 +1436,9 @@ static void esp_websocket_client_task(void *pv) xEventGroupSetBits(client->status_bits, CLOSE_FRAME_SENT_BIT); } break; + case WEBSOCKET_STATE_PAUSED: + // Nothing to do — task will block on event bits below + break; default: ESP_LOGD(TAG, "Client run iteration in a default state: %d", client->state); break; @@ -1365,7 +1471,27 @@ static void esp_websocket_client_task(void *pv) } } else if (WEBSOCKET_STATE_WAIT_TIMEOUT == client->state) { // waiting for reconnection or a request to stop the client... - xEventGroupWaitBits(client->status_bits, REQUESTED_STOP_BIT, false, true, client->wait_timeout_ms / 2 / portTICK_PERIOD_MS); + xEventGroupWaitBits(client->status_bits, REQUESTED_STOP_BIT | WAKE_BIT, false, false, client->wait_timeout_ms / 2 / portTICK_PERIOD_MS); + xEventGroupClearBits(client->status_bits, WAKE_BIT); // consume wake signal + } else if (WEBSOCKET_STATE_PAUSED == client->state) { + // Block until resume or stop requested — zero CPU while parked + EventBits_t bits = xEventGroupWaitBits(client->status_bits, + RESUME_BIT | REQUESTED_STOP_BIT, + true, /* clear on exit */ + false, /* any bit */ + portMAX_DELAY); + if (bits & REQUESTED_STOP_BIT) { + client->run = false; + } + if (bits & RESUME_BIT) { + xSemaphoreTakeRecursive(client->lock, portMAX_DELAY); + // Refresh transport WS settings (path, headers) from config + set_websocket_transport_optional_settings(client, client->config->scheme); + client->state = WEBSOCKET_STATE_INIT; + xEventGroupClearBits(client->status_bits, CLOSE_FRAME_SENT_BIT | STOPPED_BIT); + xSemaphoreGiveRecursive(client->lock); + ESP_LOGI(TAG, "Resumed from paused state"); + } } else if (WEBSOCKET_STATE_CLOSING == client->state && (CLOSE_FRAME_SENT_BIT & xEventGroupGetBits(client->status_bits))) { ESP_LOGD(TAG, " Waiting for TCP connection to be closed by the server"); @@ -1402,6 +1528,7 @@ static void esp_websocket_client_task(void *pv) } else { xEventGroupSetBits(client->status_bits, STOPPED_BIT); } + ESP_LOGI(TAG, "[DIAG] websocket_task calling vTaskDelete(NULL) tick=%lu", (unsigned long)xTaskGetTickCount()); vTaskDelete(NULL); } @@ -1533,6 +1660,13 @@ int esp_websocket_client_send_bin(esp_websocket_client_handle_t client, const ch return esp_websocket_client_send_with_opcode(client, WS_TRANSPORT_OPCODES_BINARY, (const uint8_t *)data, len, timeout); } +/* Backward-compat: generic send (defaults to binary). Removed upstream but + * still present in the IDF 4.4 pre-compiled archive API. */ +int esp_websocket_client_send(esp_websocket_client_handle_t client, const char *data, int len, TickType_t timeout) +{ + return esp_websocket_client_send_bin(client, data, len, timeout); +} + int esp_websocket_client_send_bin_partial(esp_websocket_client_handle_t client, const char *data, int len, TickType_t timeout) { return esp_websocket_client_send_with_exact_opcode(client, WS_TRANSPORT_OPCODES_BINARY, (const uint8_t *)data, len, timeout); diff --git a/components/esp_websocket_client/include/esp_websocket_client.h b/components/esp_websocket_client/include/esp_websocket_client.h index d01540da42..a14b4c7121 100644 --- a/components/esp_websocket_client/include/esp_websocket_client.h +++ b/components/esp_websocket_client/include/esp_websocket_client.h @@ -228,6 +228,36 @@ 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); +/** + * @brief Pause the WebSocket client without destroying the task. + * + * Closes the TCP/TLS transport but keeps the internal FreeRTOS task alive + * (blocked at zero CPU cost). Call esp_websocket_client_resume() to reconnect + * using the same task, avoiding vTaskDelete/xTaskCreate overhead. + * + * Notes: + * - Cannot be called from the websocket event handler / websocket task + * - After pause, call set_uri() if the endpoint changed, then resume() + * + * @param[in] client The client + * + * @return esp_err_t + */ +esp_err_t esp_websocket_client_pause(esp_websocket_client_handle_t client); + +/** + * @brief Resume a paused WebSocket client. + * + * Wakes the internal task which will re-apply transport settings from config + * (updated path, headers) and establish a fresh connection. + * + * @param[in] client The client (must be in paused state) + * @param[in] headers Optional new headers to set before reconnecting (NULL to keep current) + * + * @return esp_err_t + */ +esp_err_t esp_websocket_client_resume(esp_websocket_client_handle_t client, const char *headers); + /** * @brief Destroy the WebSocket connection and free all resources. * This function must be the last function to call for an session. @@ -271,6 +301,21 @@ esp_err_t esp_websocket_client_destroy_on_exit(esp_websocket_client_handle_t cli */ int esp_websocket_client_send_bin(esp_websocket_client_handle_t client, const char *data, int len, TickType_t timeout); +/** + * @brief Generic write data to the WebSocket connection; defaults to binary send + * (backward-compat shim kept for IDF 4.4 API parity) + * + * @param[in] client The client + * @param[in] data The data + * @param[in] len The length + * @param[in] timeout Write data timeout in RTOS ticks + * + * @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 Write binary data to the WebSocket connection and sends it without setting the FIN flag(data send with WS OPCODE=02, i.e. binary) * diff --git a/components/esp_websocket_client/tests/unit/main/test_websocket_client.c b/components/esp_websocket_client/tests/unit/main/test_websocket_client.c index ae75a693d6..3f9f6c5c15 100644 --- a/components/esp_websocket_client/tests/unit/main/test_websocket_client.c +++ b/components/esp_websocket_client/tests/unit/main/test_websocket_client.c @@ -70,11 +70,34 @@ TEST(websocket, websocket_set_invalid_url) esp_websocket_client_destroy(client); } +TEST(websocket, websocket_pause_resume_argument_validation) +{ + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_websocket_client_pause(NULL)); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_websocket_client_resume(NULL, NULL)); +} + +TEST(websocket, websocket_pause_resume_when_not_started) +{ + const esp_websocket_client_config_t websocket_cfg = { + .uri = "ws://echo.websocket.org", + }; + + esp_websocket_client_handle_t client = esp_websocket_client_init(&websocket_cfg); + TEST_ASSERT_NOT_NULL(client); + + TEST_ASSERT_EQUAL(ESP_FAIL, esp_websocket_client_pause(client)); + TEST_ASSERT_EQUAL(ESP_FAIL, esp_websocket_client_resume(client, NULL)); + + esp_websocket_client_destroy(client); +} + TEST_GROUP_RUNNER(websocket) { RUN_TEST_CASE(websocket, websocket_init_deinit) RUN_TEST_CASE(websocket, websocket_init_invalid_url) RUN_TEST_CASE(websocket, websocket_set_invalid_url) + RUN_TEST_CASE(websocket, websocket_pause_resume_argument_validation) + RUN_TEST_CASE(websocket, websocket_pause_resume_when_not_started) } void app_main(void) diff --git a/docs/esp_websocket_client/en/index.rst b/docs/esp_websocket_client/en/index.rst index e904256a08..61098e1edb 100644 --- a/docs/esp_websocket_client/en/index.rst +++ b/docs/esp_websocket_client/en/index.rst @@ -7,9 +7,13 @@ The ESP WebSocket client is an implementation of `WebSocket protocol client `_ server can be found here: `example `_ +A simple WebSocket example that uses esp_websocket_client to establish a websocket connection and send/receive data can be found in the component example directory: `examples `_. + +For a broader feature walkthrough (headers callback, fragmented frames, pause/resume, runtime ping/reconnect tuning), see: `websocket_features example `_. Sending Text Data ^^^^^^^^^^^^^^^^^ diff --git a/examples/README.md b/examples/README.md index 6e198bb2e6..73cfd7d8d5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -15,6 +15,9 @@ This directory showcases a variety of examples, illustrating the use of differen 3. **MQTT Demo on Linux**: A comprehensive demonstration of an MQTT (Message Queuing Telemetry Transport) application designed to run on a Linux environment. Location: [mqtt](mqtt) +4. **WebSocket Feature Showcase**: Demonstrates modern `esp_websocket_client` features such as header callbacks, custom headers, fragmented frames, pause/resume, and runtime ping/reconnect tuning. + Location: [websocket_features](websocket_features) + ## Additional Resources and Examples For an extensive collection of additional examples, especially those related to specific components, please visit the upper layer directory and navigate to the respective component's example directory. diff --git a/examples/websocket_features/CMakeLists.txt b/examples/websocket_features/CMakeLists.txt new file mode 100644 index 0000000000..1820858d06 --- /dev/null +++ b/examples/websocket_features/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.16) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(websocket_features) diff --git a/examples/websocket_features/README.md b/examples/websocket_features/README.md new file mode 100644 index 0000000000..a3c762bdb8 --- /dev/null +++ b/examples/websocket_features/README.md @@ -0,0 +1,86 @@ +# WebSocket Feature Showcase Example + +## Overview + +This example demonstrates the newest `esp_websocket_client` capabilities in one place, with verbose comments in code and runtime logs. + +It is designed to be a **reference app** when you want to copy/paste a production-ready websocket control flow and then trim it for your use case. + +### Features Demonstrated + +1. Event-driven lifecycle handling (`BEGIN`, `BEFORE_CONNECT`, `CONNECTED`, `DATA`, `ERROR`, `DISCONNECTED`, `CLOSED`, `FINISH`) +2. Header inspection via `WEBSOCKET_EVENT_HEADER_RECEIVED` (IDF 6+) +3. Custom headers using both: + - `esp_websocket_client_set_headers()` + - `esp_websocket_client_append_header()` +4. Fragmented transfer APIs: + - `esp_websocket_client_send_text_partial()` + - `esp_websocket_client_send_bin_partial()` + - `esp_websocket_client_send_cont_msg()` + - `esp_websocket_client_send_fin()` +5. Runtime reconnect tuning: + - `esp_websocket_client_get_reconnect_timeout()` + - `esp_websocket_client_set_reconnect_timeout()` +6. Runtime ping tuning: + - `esp_websocket_client_get_ping_interval_sec()` + - `esp_websocket_client_set_ping_interval_sec()` +7. Task-preserving reconnect with: + - `esp_websocket_client_pause()` + - `esp_websocket_client_set_uri()` + - `esp_websocket_client_resume()` +8. Close handshake and clean teardown: + - `esp_websocket_client_close()` + - unregister events + - `esp_websocket_client_destroy()` +9. Heap/stability observability: + - heap snapshots before init and after destroy + - lifecycle summary logging for connect/error states + - cleanup path that avoids hard-failing during recovery paths + +## Project Layout + +- `main/app_main.c` — fully commented example source. +- `main/Kconfig.projbuild` — URI configuration in menuconfig. +- `main/idf_component.yml` — dependencies. + +## Configure + +Set your endpoint in menuconfig: + +```bash +idf.py menuconfig +``` + +Path: + +```text +WebSocket Feature Showcase Configuration ---> + WebSocket endpoint URI +``` + +Default endpoint: + +```text +wss://echo.websocket.events +``` + +## Build & Flash + +```bash +idf.py set-target esp32 +idf.py build +idf.py -p /dev/ttyUSB0 flash monitor +``` + +## What to look for in logs + +- Upgrade headers printed by `WEBSOCKET_EVENT_HEADER_RECEIVED`. +- Text + binary + fragmented send sequences. +- Ping/reconnect values before and after runtime update. +- Pause/resume flow reconnecting without destroying the task. +- Heap snapshots and a final "Heap recovery" message after client destroy. + +## Notes + +- If your server uses TLS with a private CA, configure certificates as needed for your deployment. +- Redirect handling is managed by the websocket transport layer; this example focuses on application-side controls and observability. diff --git a/examples/websocket_features/main/CMakeLists.txt b/examples/websocket_features/main/CMakeLists.txt new file mode 100644 index 0000000000..61fac40e63 --- /dev/null +++ b/examples/websocket_features/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "app_main.c" + INCLUDE_DIRS ".") diff --git a/examples/websocket_features/main/Kconfig.projbuild b/examples/websocket_features/main/Kconfig.projbuild new file mode 100644 index 0000000000..0409286f9e --- /dev/null +++ b/examples/websocket_features/main/Kconfig.projbuild @@ -0,0 +1,9 @@ +menu "WebSocket Feature Showcase Configuration" + +config WEBSOCKET_FEATURES_URI + string "WebSocket endpoint URI" + default "wss://echo.websocket.events" + help + URI used by the websocket_features example. + +endmenu diff --git a/examples/websocket_features/main/app_main.c b/examples/websocket_features/main/app_main.c new file mode 100644 index 0000000000..3f70fe4a34 --- /dev/null +++ b/examples/websocket_features/main/app_main.c @@ -0,0 +1,239 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include +#include + +#include "esp_event.h" +#include "esp_netif.h" +#include "esp_log.h" +#include "esp_system.h" +#include "esp_websocket_client.h" +#include "nvs_flash.h" +#include "protocol_examples_common.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +static const char *TAG = "ws_features"; + +typedef struct { + bool saw_connected; + bool saw_error; +} ws_runtime_state_t; + +static size_t log_heap_snapshot(const char *stage) +{ + size_t free_heap = esp_get_free_heap_size(); + ESP_LOGI(TAG, "Heap snapshot (%s): free=%u bytes", stage, (unsigned)free_heap); + return free_heap; +} + +static esp_err_t ws_send_checked(int sent_len, const char *api_name) +{ + if (sent_len < 0) { + ESP_LOGE(TAG, "%s failed", api_name); + return ESP_FAIL; + } + return ESP_OK; +} + +static void ws_event_handler(void *arg, esp_event_base_t base, int32_t event_id, void *event_data) +{ + (void)arg; + (void)base; + esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data; + ws_runtime_state_t *state = (ws_runtime_state_t *)arg; + + switch (event_id) { + case WEBSOCKET_EVENT_BEGIN: + ESP_LOGI(TAG, "WEBSOCKET_EVENT_BEGIN"); + break; +#if WS_TRANSPORT_HEADER_CALLBACK_SUPPORT + case WEBSOCKET_EVENT_HEADER_RECEIVED: + /* New feature: inspect HTTP headers during the upgrade response. */ + ESP_LOGI(TAG, "WEBSOCKET_EVENT_HEADER_RECEIVED: %.*s", data->data_len, data->data_ptr); + break; +#endif + case WEBSOCKET_EVENT_BEFORE_CONNECT: + ESP_LOGI(TAG, "WEBSOCKET_EVENT_BEFORE_CONNECT"); + break; + case WEBSOCKET_EVENT_CONNECTED: + if (state != NULL) { + state->saw_connected = true; + } + ESP_LOGI(TAG, "WEBSOCKET_EVENT_CONNECTED"); + break; + case WEBSOCKET_EVENT_DATA: + ESP_LOGI(TAG, "WEBSOCKET_EVENT_DATA opcode=%d fin=%d payload_len=%d payload_offset=%d", + data->op_code, data->fin, data->payload_len, data->payload_offset); + ESP_LOGI(TAG, "Payload chunk: %.*s", data->data_len, data->data_ptr); + break; + case WEBSOCKET_EVENT_ERROR: + if (state != NULL) { + state->saw_error = true; + } + ESP_LOGW(TAG, "WEBSOCKET_EVENT_ERROR type=%d status=%d tls_esp_err=0x%x sock_errno=%d", + data->error_handle.error_type, + data->error_handle.esp_ws_handshake_status_code, + data->error_handle.esp_tls_last_esp_err, + data->error_handle.esp_transport_sock_errno); + break; + case WEBSOCKET_EVENT_DISCONNECTED: + ESP_LOGI(TAG, "WEBSOCKET_EVENT_DISCONNECTED"); + break; + case WEBSOCKET_EVENT_CLOSED: + ESP_LOGI(TAG, "WEBSOCKET_EVENT_CLOSED"); + break; + case WEBSOCKET_EVENT_FINISH: + ESP_LOGI(TAG, "WEBSOCKET_EVENT_FINISH"); + break; + default: + break; + } +} + +static void websocket_feature_showcase(void) +{ + /* + * This config intentionally enables and documents several modern features: + * - close-reconnect behavior + * - runtime ping and reconnect tuning + * - custom handshake headers + * - fragmented send helpers + * - pause/resume without destroying the worker task + */ + esp_websocket_client_config_t ws_cfg = { + .uri = CONFIG_WEBSOCKET_FEATURES_URI, + .enable_close_reconnect = true, + .disable_auto_reconnect = false, + .reconnect_timeout_ms = 4000, + .network_timeout_ms = 8000, + .ping_interval_sec = 8, + .pingpong_timeout_sec = 20, + }; + + ws_runtime_state_t runtime_state = { 0 }; + bool started = false; + + const size_t heap_before = log_heap_snapshot("before init"); + + esp_websocket_client_handle_t client = esp_websocket_client_init(&ws_cfg); + if (client == NULL) { + ESP_LOGE(TAG, "Failed to initialize websocket client"); + return; + } + + ESP_ERROR_CHECK(esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, ws_event_handler, &runtime_state)); + + /* Add headers in two ways: batch set + key/value append API. */ + ESP_ERROR_CHECK(esp_websocket_client_set_headers(client, + "X-Example-Profile: feature-showcase\r\n" + "X-Trace-Id: boot-phase\r\n")); + ESP_ERROR_CHECK(esp_websocket_client_append_header(client, "X-Client", "esp-protocols-example")); + + ESP_LOGI(TAG, "Connecting to %s", ws_cfg.uri); + ESP_ERROR_CHECK(esp_websocket_client_start(client)); + started = true; + + for (int retry = 0; retry < 30 && !esp_websocket_client_is_connected(client); ++retry) { + vTaskDelay(pdMS_TO_TICKS(100)); + } + + if (esp_websocket_client_is_connected(client)) { + const char *text_msg = "Feature showcase: single text frame"; + ESP_LOGI(TAG, "Sending text frame"); + ESP_ERROR_CHECK_WITHOUT_ABORT(ws_send_checked( + esp_websocket_client_send_text(client, text_msg, strlen(text_msg), pdMS_TO_TICKS(1000)), + "esp_websocket_client_send_text")); + + /* Send a fragmented text message in 3 pieces. */ + ESP_LOGI(TAG, "Sending fragmented text frame"); + ESP_ERROR_CHECK_WITHOUT_ABORT(ws_send_checked( + esp_websocket_client_send_text_partial(client, "fragment-1/", 11, pdMS_TO_TICKS(1000)), + "esp_websocket_client_send_text_partial")); + ESP_ERROR_CHECK_WITHOUT_ABORT(ws_send_checked( + esp_websocket_client_send_cont_msg(client, "fragment-2/", 11, pdMS_TO_TICKS(1000)), + "esp_websocket_client_send_cont_msg")); + ESP_ERROR_CHECK_WITHOUT_ABORT(ws_send_checked( + esp_websocket_client_send_cont_msg(client, "fragment-3", 10, pdMS_TO_TICKS(1000)), + "esp_websocket_client_send_cont_msg")); + ESP_ERROR_CHECK_WITHOUT_ABORT(ws_send_checked( + esp_websocket_client_send_fin(client, pdMS_TO_TICKS(1000)), + "esp_websocket_client_send_fin")); + + /* Send a fragmented binary message. */ + const char bin_a[] = {0x01, 0x02, 0x03, 0x04}; + const char bin_b[] = {0x05, 0x06, 0x07, 0x08}; + ESP_LOGI(TAG, "Sending fragmented binary frame"); + ESP_ERROR_CHECK_WITHOUT_ABORT(ws_send_checked( + esp_websocket_client_send_bin_partial(client, bin_a, sizeof(bin_a), pdMS_TO_TICKS(1000)), + "esp_websocket_client_send_bin_partial")); + ESP_ERROR_CHECK_WITHOUT_ABORT(ws_send_checked( + esp_websocket_client_send_cont_msg(client, bin_b, sizeof(bin_b), pdMS_TO_TICKS(1000)), + "esp_websocket_client_send_cont_msg")); + ESP_ERROR_CHECK_WITHOUT_ABORT(ws_send_checked( + esp_websocket_client_send_fin(client, pdMS_TO_TICKS(1000)), + "esp_websocket_client_send_fin")); + + /* Demonstrate runtime tuning for new ping/reconnect controls. */ + ESP_LOGI(TAG, "Current ping interval: %u sec", (unsigned)esp_websocket_client_get_ping_interval_sec(client)); + ESP_ERROR_CHECK(esp_websocket_client_set_ping_interval_sec(client, 5)); + ESP_LOGI(TAG, "Updated ping interval: %u sec", (unsigned)esp_websocket_client_get_ping_interval_sec(client)); + + ESP_LOGI(TAG, "Current reconnect timeout: %d ms", esp_websocket_client_get_reconnect_timeout(client)); + ESP_ERROR_CHECK(esp_websocket_client_set_reconnect_timeout(client, 3000)); + ESP_LOGI(TAG, "Updated reconnect timeout: %d ms", esp_websocket_client_get_reconnect_timeout(client)); + + /* + * Pause/resume keeps the websocket task alive but tears down the transport. + * It is useful when your app changes endpoint/headers at runtime. + */ + ESP_LOGI(TAG, "Pausing client"); + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_websocket_client_pause(client)); + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_websocket_client_set_uri(client, CONFIG_WEBSOCKET_FEATURES_URI)); + ESP_LOGI(TAG, "Resuming client with new temporary header"); + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_websocket_client_resume(client, "X-Resume-Reason: config-refresh\r\n")); + } + + vTaskDelay(pdMS_TO_TICKS(1000)); + + ESP_LOGI(TAG, "Stopping client"); + if (started && esp_websocket_client_is_connected(client)) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_websocket_client_close(client, pdMS_TO_TICKS(1000))); + } + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_websocket_unregister_events(client, WEBSOCKET_EVENT_ANY, ws_event_handler)); + ESP_ERROR_CHECK(esp_websocket_client_destroy(client)); + + const size_t heap_after = log_heap_snapshot("after destroy"); + if (heap_after >= heap_before) { + ESP_LOGI(TAG, "Heap recovery OK: +%d bytes after lifecycle", (int)(heap_after - heap_before)); + } else { + ESP_LOGW(TAG, "Heap recovery check: -%d bytes after lifecycle (may vary by system allocators)", + (int)(heap_before - heap_after)); + } + + ESP_LOGI(TAG, "Session summary: connected=%s error_seen=%s", + runtime_state.saw_connected ? "yes" : "no", + runtime_state.saw_error ? "yes" : "no"); +} + +void app_main(void) +{ + ESP_LOGI(TAG, "[APP] Startup"); + ESP_LOGI(TAG, "[APP] Free memory: %" PRIu32 " bytes", esp_get_free_heap_size()); + ESP_LOGI(TAG, "[APP] IDF version: %s", esp_get_idf_version()); + + ESP_ERROR_CHECK(nvs_flash_init()); + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + /* Bring up network using helper from protocol_examples_common. */ + ESP_ERROR_CHECK(example_connect()); + + websocket_feature_showcase(); +} diff --git a/examples/websocket_features/main/idf_component.yml b/examples/websocket_features/main/idf_component.yml new file mode 100644 index 0000000000..4d15b0b474 --- /dev/null +++ b/examples/websocket_features/main/idf_component.yml @@ -0,0 +1,6 @@ +dependencies: + idf: ">=5.0" + espressif/esp_websocket_client: + version: "^1.0.0" + protocol_examples_common: + path: ${IDF_PATH}/examples/common_components/protocol_examples_common