-
Notifications
You must be signed in to change notification settings - Fork 189
feat(websocket): add task-preserving pause/resume lifecycle and websocket feature showcase docs/example (IDFGH-17329) #1023
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
6e3c814
cc232cd
ec1ee50
23e467f
1bd758e
50f680c
91892bd
9dac13f
38ef5b6
ba2f186
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,31 @@ | |
| #include <errno.h> | ||
| #include <arpa/inet.h> | ||
|
|
||
| /* | ||
| * 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()); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Diagnostic debug log accidentally left in production codeLow Severity The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bugbot Autofix determined this is a false positive. No This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard. |
||
| 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); | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Re-adds previously removed deprecated API functionMedium Severity
Additional Locations (1)There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bugbot Autofix determined this is a false positive. The deprecated This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard. |
||
|
|
||
| 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); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| cmake_minimum_required(VERSION 3.16) | ||
| include($ENV{IDF_PATH}/tools/cmake/project.cmake) | ||
| project(websocket_features) |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed
.authfield breaks HTTP Basic AuthenticationHigh Severity
The
.auth = client->config->authline was removed from theesp_transport_ws_config_tinitialization inset_websocket_transport_optional_settings(). Theauthfield is computed fromusername/passwordviahttp_auth_basic()and stored inclient->config->auth, but it's no longer passed to the transport layer. This silently breaks HTTP Basic Authentication for all websocket connections that use username/password credentials.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bugbot Autofix determined this is a false positive.
The websocket transport configuration already includes
.auth = client->config->auth, so Basic Auth credentials are still propagated correctly.This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.