From 929582b1aff0c6aa698a77efc1fd46a0624a10b9 Mon Sep 17 00:00:00 2001 From: jetpax Date: Sun, 6 Apr 2025 13:27:48 -0700 Subject: [PATCH] Expose esp_http_server to Berry (#23206) * Expose esp_http_server to Berry * Fix conditional build defines * Fix Berry returns, dangling pointer * Use correct Berry returns * Remove debug code * cleanup * add BERRY to conditionals to avoid confusion --- lib/libesp32/berry/default/be_modtab.c | 20 + .../berry_tasmota/src/be_httpserver_lib.c | 843 +++++++++++ .../berry_tasmota/src/be_webfiles_lib.c | 392 +++++ .../berry_tasmota/src/be_wsserver_lib.c | 1326 +++++++++++++++++ 4 files changed, 2581 insertions(+) create mode 100644 lib/libesp32/berry_tasmota/src/be_httpserver_lib.c create mode 100644 lib/libesp32/berry_tasmota/src/be_webfiles_lib.c create mode 100644 lib/libesp32/berry_tasmota/src/be_wsserver_lib.c diff --git a/lib/libesp32/berry/default/be_modtab.c b/lib/libesp32/berry/default/be_modtab.c index ba758793e..fe7ef5980 100644 --- a/lib/libesp32/berry/default/be_modtab.c +++ b/lib/libesp32/berry/default/be_modtab.c @@ -40,6 +40,17 @@ be_extern_native_module(gpio); be_extern_native_module(display); be_extern_native_module(energy); be_extern_native_module(webserver); +#ifdef USE_BERRY_HTTPSERVER +be_extern_native_module(httpserver); + +#ifdef USE_BERRY_WSSERVER +be_extern_native_module(wsserver); +#endif // USE_BERRY_WSSERVER + +#ifdef USE_BERRY_WEBFILES +be_extern_native_module(webfiles); +#endif // USE_BERRY_WEBFILES +#endif // USE_BERRY_HTTPSERVER be_extern_native_module(flash); be_extern_native_module(path); be_extern_native_module(unishox); @@ -170,6 +181,15 @@ BERRY_LOCAL const bntvmodule_t* const be_module_table[] = { #ifdef USE_WEBSERVER &be_native_module(webserver), #endif // USE_WEBSERVER +#ifdef USE_BERRY_HTTPSERVER + &be_native_module(httpserver), +#ifdef USE_BERRY_WSSERVER + &be_native_module(wsserver), +#endif // USE_BERRY_WSSERVER +#ifdef USE_BERRY_WEBFILES + &be_native_module(webfiles), +#endif // USE_BERRY_WEBFILES +#endif // USE_BERRY_HTTPSERVER #ifdef USE_ZIGBEE &be_native_module(zigbee), &be_native_module(matter_zigbee), diff --git a/lib/libesp32/berry_tasmota/src/be_httpserver_lib.c b/lib/libesp32/berry_tasmota/src/be_httpserver_lib.c new file mode 100644 index 000000000..ea5244a99 --- /dev/null +++ b/lib/libesp32/berry_tasmota/src/be_httpserver_lib.c @@ -0,0 +1,843 @@ +/* + be_httpserver_lib.c - HTTP server support for Berry using ESP-IDF HTTP server + + Copyright (C) 2025 Jonathan E. Peace + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifdef USE_BERRY_HTTPSERVER + +#include // For NULL, size_t +#include // For bool, true, false +#include // For string functions +#include // For malloc/free + +// ESP-IDF includes +#define LOG_LOCAL_LEVEL ESP_LOG_INFO +#include "esp_log.h" +#include "esp_http_server.h" +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" +#include "freertos/semphr.h" + +// Berry includes +#include "be_mapping.h" +#include "be_exec.h" +#include "be_vm.h" +#include "be_gc.h" + +// External function declarations +httpd_handle_t be_httpserver_get_handle(void); +void be_httpserver_set_disconnect_handler(httpd_close_func_t handler); +bool httpserver_has_queue(void); + +// Message types for queue +typedef enum { + HTTP_MSG_WEBSOCKET = 1, + HTTP_MSG_FILE = 2, + HTTP_MSG_WEB = 3 +} http_msg_type_t; + +// Message structure for queue +typedef struct { + http_msg_type_t type; + int client_id; + void *data; + size_t data_len; + void *user_data; + bvalue func; // Berry function value for web handlers + httpd_req_t *req; // HTTP request handle for async processing +} http_queue_msg_t; + +// Forward declarations for internal functions +void be_httpserver_process_websocket_msg(bvm *vm, int client_id, const char *data, size_t len); +bool httpserver_queue_message(http_msg_type_t type, int client_id, const void *data, size_t data_len, void *user_data); +bool httpserver_queue_web_request(int handler_id, httpd_req_t *req, bvalue func); + +// Logger tag +static const char *TAG = "HTTPSERVER"; + +// Global queue for handling messages in the main task context +static QueueHandle_t http_msg_queue = NULL; +static SemaphoreHandle_t http_queue_mutex = NULL; +static bool http_queue_initialized = false; + +// Maximum number of HTTP handlers +#define HTTP_HANDLER_MAX 5 + +// Handler storage +typedef struct { + bool active; // Whether this handler is in use + bvm *vm; // Berry VM instance + bvalue func; // Berry function to call for requests +} http_handler_t; + +static http_handler_t http_handlers[HTTP_HANDLER_MAX]; + +// Maximum concurrent HTTP server connections +#define HTTPD_MAX_CONNECTIONS 8 + +// Handle to HTTP server +static httpd_handle_t http_server = NULL; + +// Disconnect handler for WebSocket connections +static httpd_close_func_t http_server_disconn_handler = NULL; + +// Current HTTP request being processed (for Berry access) +static httpd_req_t *current_request = NULL; + +// Connection tracking +static struct { + int count; + SemaphoreHandle_t mutex; +} connection_tracking = {0, NULL}; + +// Connection cleanup function +// CONTEXT: ESP-IDF HTTP Server Task +// Called automatically by ESP-IDF when a client disconnects +static void http_connection_cleanup(void *arg) { + if (xSemaphoreTake(connection_tracking.mutex, pdMS_TO_TICKS(100)) == pdTRUE) { + connection_tracking.count--; + xSemaphoreGive(connection_tracking.mutex); + + // Call WebSocket disconnect handler if registered + if (http_server_disconn_handler) { + // arg is the socket file descriptor in this context + http_server_disconn_handler(NULL, (int)(intptr_t)arg); + } + } +} + +// WebSocket message processing function +// CONTEXT: Main Tasmota Task (Berry VM Context) +// This function runs in the main task when processing queued WebSocket messages +void be_httpserver_process_websocket_msg(bvm *vm, int client_id, const char *data, size_t data_len) { + // Log message details safely (handling NULL data for connect events) + if (data) { + ESP_LOGD(TAG, "Processing WebSocket message in main task context: client=%d, data='%s', len=%d", + client_id, data, (int)data_len); + } else { + ESP_LOGD(TAG, "Processing WebSocket event in main task context: client=%d", client_id); + } + + // Forward to the WebSocket handler in be_wsserver_lib.c + be_wsserver_handle_message(vm, client_id, data, data_len); +} + +// File request processing function +static void be_httpserver_process_file_request(bvm *vm, void *user_data) { + ESP_LOGI(TAG, "Processing file request (placeholder)"); + // Placeholder for file handling - to be extended as needed +} + + +// Initialize the message queue +static bool init_http_queue() { + if (!http_queue_initialized) { + http_msg_queue = xQueueCreate(10, sizeof(http_queue_msg_t)); + http_queue_mutex = xSemaphoreCreateMutex(); + + if (http_msg_queue != NULL && http_queue_mutex != NULL) { + http_queue_initialized = true; + ESP_LOGI(TAG, "HTTP queue initialized"); + return true; + } else { + ESP_LOGE(TAG, "Failed to create HTTP queue"); + return false; + } + } + return true; +} + +// Queue a message for processing in the main task +// CONTEXT: Any Task (typically ESP-IDF HTTP Server Task) +// This function is called to queue messages for processing in the main task +// TRANSITION: Current Task → Main Tasmota Task +bool httpserver_queue_message(http_msg_type_t type, int client_id, const void *data, size_t data_len, void *user_data) { + if (!http_queue_initialized) { + ESP_LOGE(TAG, "Queue not initialized"); + return false; + } + + if (!data && data_len > 0) { + ESP_LOGE(TAG, "Invalid data pointer with non-zero length"); + return false; + } + + // Take mutex to protect queue + if (xSemaphoreTake(http_queue_mutex, pdMS_TO_TICKS(100)) != pdTRUE) { + ESP_LOGE(TAG, "Failed to take mutex"); + return false; + } + + // Create a message to queue + http_queue_msg_t msg = {0};; + + msg.type = type; + msg.client_id = client_id; + msg.user_data = user_data; + + // Special case for HTTP_MSG_WEB is handled by httpserver_queue_web_request + if (type == HTTP_MSG_WEB) { + ESP_LOGE(TAG, "HTTP_MSG_WEB must use httpserver_queue_web_request"); + xSemaphoreGive(http_queue_mutex); + return false; + } + + // For other message types, copy the data if needed + if (data_len > 0) { + char *data_copy = malloc(data_len + 1); + if (!data_copy) { + ESP_LOGE(TAG, "Failed to allocate memory for data copy"); + xSemaphoreGive(http_queue_mutex); + return false; + } + + memcpy(data_copy, data, data_len); + data_copy[data_len] = '\0'; // Ensure null termination + + msg.data = data_copy; + msg.data_len = data_len; + } + + // Queue the message + if (xQueueSend(http_msg_queue, &msg, 0) != pdTRUE) { + // Queue is full, free the data if we allocated it + if (msg.data) { + free(msg.data); + } + ESP_LOGE(TAG, "Failed to queue message - queue is full"); + xSemaphoreGive(http_queue_mutex); + return false; + } + + // Message successfully queued + ESP_LOGD(TAG, "Message queued successfully (type %d, client %d)", type, client_id); + ESP_LOGD(TAG, "DIAGNOSTIC: Queue has %d messages waiting", uxQueueMessagesWaiting(http_msg_queue)); + + if (msg.data) { + ESP_LOGD(TAG, "QUEUE ITEM: type=%d, client=%d, data_len=%d, data_ptr=%p, user_data=%p", + msg.type, msg.client_id, (int)msg.data_len, msg.data, msg.user_data); + ESP_LOGD(TAG, "QUEUE DATA: '%s'", msg.data); + } + + xSemaphoreGive(http_queue_mutex); + return true; +} + +// Specialized function for queuing HTTP web requests with a Berry function +bool httpserver_queue_web_request(int handler_id, httpd_req_t *req, bvalue func) { + if (!http_queue_initialized) { + ESP_LOGE(TAG, "Queue not initialized"); + return false; + } + + if (!req) { + ESP_LOGE(TAG, "NULL request for web request"); + return false; + } + + // Create a copy of the request that we can process asynchronously + httpd_req_t *req_copy = NULL; + esp_err_t err = httpd_req_async_handler_begin(req, &req_copy); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to create async request: %d", err); + return false; + } + + // Take mutex to protect queue + if (xSemaphoreTake(http_queue_mutex, pdMS_TO_TICKS(100)) != pdTRUE) { + ESP_LOGE(TAG, "Failed to take mutex"); + // Release the request copy since we won't be using it + httpd_req_async_handler_complete(req_copy); + return false; + } + + // Create a message to queue + http_queue_msg_t msg = {0}; + + msg.type = HTTP_MSG_WEB; + msg.client_id = handler_id; + msg.user_data = NULL; + msg.func = func; // Store the function reference + msg.req = req_copy; // Store the COPY of the request handle + + // Queue the message + if (xQueueSend(http_msg_queue, &msg, 0) != pdTRUE) { + // Queue is full + ESP_LOGE(TAG, "Failed to queue web request - queue is full"); + // Release the request copy since we won't be using it + httpd_req_async_handler_complete(req_copy); + xSemaphoreGive(http_queue_mutex); + return false; + } + + // Message successfully queued + ESP_LOGD(TAG, "HTTP request queued successfully (type %d, handler %d)", msg.type, msg.client_id); + ESP_LOGD(TAG, "DIAGNOSTIC: Queue has %d messages waiting", uxQueueMessagesWaiting(http_msg_queue)); + ESP_LOGD(TAG, "QUEUE ITEM: type=%d, client=%d, data_len=%d, data_ptr=%p, user_data=%p, req=%p", + msg.type, msg.client_id, (int)msg.data_len, msg.data, msg.user_data, msg.req); + + xSemaphoreGive(http_queue_mutex); + return true; +} + + +// ------------------------------------------------------------------------ +// Web request processing function +// CONTEXT: Main Tasmota Task (Berry VM Context) +// This function processes queued web requests in the main task context +// ------------------------------------------------------------------------ + +void be_httpserver_process_web_request(bvm *vm, http_queue_msg_t *msg) { + ESP_LOGD(TAG, "Processing web request: msg=%p", msg); + + if (!msg) { + ESP_LOGE(TAG, "Web request has NULL message handle"); + return; + } + + ESP_LOGD(TAG, "Request details: req=%p, client_id=%d", msg->req, msg->client_id); + + if (!msg->req) { + ESP_LOGE(TAG, "Web request has NULL request handle"); + return; + } + + // Get handler ID (passed in client_id field) + int handler_id = msg->client_id; + if (handler_id < 0 || handler_id >= HTTP_HANDLER_MAX || !http_handlers[handler_id].active) { + ESP_LOGE(TAG, "Invalid handler ID from queue: %d", handler_id); + httpd_resp_set_status(msg->req, "500 Internal Server Error"); + httpd_resp_sendstr(msg->req, "Invalid handler ID"); + httpd_req_async_handler_complete(msg->req); + return; + } + + ESP_LOGI(TAG, "Processing web request for URI: %s with handler %d", msg->req->uri, handler_id); + + // Get the Berry VM and handler function + bvm *handler_vm = http_handlers[handler_id].vm; + + if (handler_vm == NULL) { + ESP_LOGE(TAG, "Berry VM is NULL for handler %d", handler_id); + httpd_resp_set_status(msg->req, "500 Internal Server Error"); + httpd_resp_sendstr(msg->req, "VM error"); + httpd_req_async_handler_complete(msg->req); + return; + } + + ESP_LOGI(TAG, "STACK: Before pushing function, stack top = %d", be_top(handler_vm)); + + // Push the function stored in the message + be_pushnil(handler_vm); + bvalue *top = be_indexof(handler_vm, -1); + *top = msg->func; + + current_request = msg->req; + + // Push URI as argument + be_pushstring(handler_vm, current_request->uri); + + // Call the Berry function + int result = be_pcall(handler_vm, 1); + + // Log stack state after call + ESP_LOGI(TAG, "STACK: After be_pcall, stack top = %d, result = %d", be_top(handler_vm), result); + + // Check for errors + if (result != 0) { + const char *err_msg = be_tostring(handler_vm, -1); + ESP_LOGE(TAG, "Berry handler error: %s", err_msg); + + // Send error response + httpd_resp_set_status(msg->req, "500 Internal Server Error"); + httpd_resp_sendstr(msg->req, (char*)err_msg); + + be_error_pop_all(handler_vm); // Clear entire stack on error + } else { + // Get return value + const char *response = be_tostring(handler_vm, -1); + ESP_LOGI(TAG, "Request processed. Response: %s", response ? response : "(null)"); + + // Send success response if httpserver.send() wasn't used + if (response != NULL) { + httpd_resp_set_type(msg->req, "text/html"); + httpd_resp_sendstr(msg->req, response); + } + + // Pop the argument (which has been replaced by the return value) + be_pop(handler_vm, 1); + } + + // Clear current_request AFTER all processing is done + current_request = NULL; + + // Complete the async request - ALWAYS call this to release the request + httpd_req_async_handler_complete(msg->req); + + // Pop the function if we didn't encounter an error + if (result == 0) { + // Pop the function + be_pop(handler_vm, 1); // Pop the function reference + } else { + ESP_LOGE(TAG, "Function parsing error: %d", result); + } + + // Log final stack state + ESP_LOGI(TAG, "STACK: Final state, stack top = %d", be_top(handler_vm)); +} + + +// ------------------------------------------------------------------------ +// Berry mapped C function to process queued messages +// CONTEXT: Main Tasmota Task (Berry VM Context) +// This function is registered to fast_loop() by the Berry app and is called +// periodically to process queued HTTP/WebSocket messages from the ESP-IDF HTTP server +// ------------------------------------------------------------------------ + +static int w_httpserver_process_queue(bvm *vm) { + if (!http_msg_queue) { + be_pushnil(vm); + be_return(vm); + } + + // Count of messages processed in this call + int processed = 0; + + // Process up to 5 messages in a single call to avoid blocking + for (int i = 0; i < 5; i++) { + // Take mutex before accessing queue + if (xSemaphoreTake(http_queue_mutex, 0) != pdTRUE) { + ESP_LOGW(TAG, "Failed to take mutex, will retry"); + break; + } + + // Process one message from the queue + http_queue_msg_t msg; + if (xQueueReceive(http_msg_queue, &msg, 0) == pdTRUE) { + // Release mutex while processing message + xSemaphoreGive(http_queue_mutex); + + // Count this message + processed++; + + // Diagnostic logging for queue state + ESP_LOGD(TAG, "QUEUE ITEM: type=%d, client=%d, data_len=%d, data_ptr=%p, user_data=%p, req=%p", + msg.type, msg.client_id, msg.data_len, msg.data, msg.user_data, msg.req); + + // Process message based on type + switch (msg.type) { + case HTTP_MSG_WEBSOCKET: + if (msg.data) { + ESP_LOGD(TAG, "QUEUE DATA: '%.*s'", msg.data_len, (char*)msg.data); + } else { + ESP_LOGD(TAG, "QUEUE DATA: '' (connect/disconnect event)"); + } + be_httpserver_process_websocket_msg(vm, msg.client_id, msg.data, msg.data_len); + // Free the data buffer we allocated + if (msg.data) { + free(msg.data); + } + break; + + case HTTP_MSG_FILE: + ESP_LOGI(TAG, "Processing file request"); + be_httpserver_process_file_request(vm, msg.user_data); + // user_data is not allocated by us, so don't free it + break; + + case HTTP_MSG_WEB: + ESP_LOGD(TAG, "Processing web request from queue"); + if (msg.req == NULL) { + ESP_LOGE(TAG, "CRITICAL ERROR: HTTP request pointer is NULL, skipping processing"); + break; + } + be_httpserver_process_web_request(vm, &msg); + break; + + default: + ESP_LOGW(TAG, "Unknown message type: %d", msg.type); + // Free data if it was allocated + if (msg.data) { + free(msg.data); + } + // If it's a request that wasn't processed, complete it + if (msg.req) { + httpd_req_async_handler_complete(msg.req); + } + break; + } + } else { + // No messages in queue + xSemaphoreGive(http_queue_mutex); + break; + } + } + + // Return the number of messages processed + be_pushint(vm, processed); + be_return(vm); +} + + +// ------------------------------------------------------------------------ +// HTTP Handler implementation +// ------------------------------------------------------------------------ + +// Forward declaration for handler implementation +static esp_err_t berry_http_handler_impl(httpd_req_t *req, int handler_id); + +// Macro to create handler functions for each supported endpoint +#define HTTP_HANDLER_FUNC(n) \ + static esp_err_t berry_http_handler_##n(httpd_req_t *req) { \ + return berry_http_handler_impl(req, n); \ + } + +// Generate handler functions +HTTP_HANDLER_FUNC(0) +HTTP_HANDLER_FUNC(1) +HTTP_HANDLER_FUNC(2) +HTTP_HANDLER_FUNC(3) +HTTP_HANDLER_FUNC(4) + +// Array of handler function pointers +typedef esp_err_t (*http_handler_func_t)(httpd_req_t *req); +static const http_handler_func_t berry_handlers[HTTP_HANDLER_MAX] = { + berry_http_handler_0, + berry_http_handler_1, + berry_http_handler_2, + berry_http_handler_3, + berry_http_handler_4 +}; + +// Implementation of HTTP handler dispatched by each numbered handler +static esp_err_t berry_http_handler_impl(httpd_req_t *req, int handler_id) { + if (handler_id < 0 || handler_id >= HTTP_HANDLER_MAX || !http_handlers[handler_id].active) { + ESP_LOGE(TAG, "Invalid or inactive handler ID: %d", handler_id); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Invalid handler"); + httpd_req_async_handler_complete(req); + return ESP_FAIL; + } + + // Store current request for access in Berry functions + current_request = req; + + // Get the Berry VM and handler function + bvm *vm = http_handlers[handler_id].vm; + + if (vm == NULL) { + ESP_LOGE(TAG, "Berry VM is NULL for handler %d", handler_id); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "VM error"); + current_request = NULL; + httpd_req_async_handler_complete(req); + return ESP_FAIL; + } + + // Log initial stack state + ESP_LOGI(TAG, "HANDLER: Initial stack top = %d", be_top(vm)); + + // Queue message for processing in main task if available + if (httpserver_has_queue()) { + ESP_LOGI(TAG, "Queueing request for %s", req->uri); + + // Queue the request with the stored function value + if (!httpserver_queue_web_request(handler_id, req, http_handlers[handler_id].func)) { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to queue message"); + current_request = NULL; + httpd_req_async_handler_complete(req); + return ESP_FAIL; + } + + // Note: We don't send a response here - that will be done asynchronously + current_request = NULL; + + // Log final stack state + ESP_LOGI(TAG, "HANDLER: Final stack top = %d", be_top(vm)); + + return ESP_OK; + } + + // If no queue, we'll process directly with caution + ESP_LOGW(TAG, "Processing request directly - this may be unsafe!"); + + // Start the async handler + httpd_req_t *async_req = NULL; + esp_err_t ret = httpd_req_async_handler_begin(req, &async_req); + if (ret != ESP_OK || async_req == NULL) { + ESP_LOGE(TAG, "Failed to start async handler"); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to start async handler"); + current_request = NULL; + httpd_req_async_handler_complete(req); + return ESP_FAIL; + } + + // Get the initial stack size + int top = be_top(vm); + + // Push the handler function directly onto the stack (copy from stored value) + be_pushnil(vm); // Push a temporary placeholder + bvalue *top_ptr = be_indexof(vm, -1); + *top_ptr = http_handlers[handler_id].func; // Replace placeholder with stored function + + // Push the URI string (argument) onto the stack + be_pushstring(vm, req->uri); + + // Call the handler function with the URI as single argument + if (be_pcall(vm, 1) != 0) { + const char *err_msg = be_tostring(vm, -1); + ESP_LOGE(TAG, "Berry error: %s", err_msg ? err_msg : "unknown error"); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Handler call failed"); + be_error_pop_all(vm); // Special case - clears entire stack on error + current_request = NULL; + httpd_req_async_handler_complete(req); + return ESP_FAIL; + } + + // Get the response string + const char *response = be_tostring(vm, -1); + if (response == NULL) { + ESP_LOGE(TAG, "Handler returned nil response"); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Handler returned nil response"); + be_error_pop_all(vm); // Special case - clears entire stack on error + current_request = NULL; + httpd_req_async_handler_complete(req); + return ESP_FAIL; + } + + // Send the response + httpd_resp_set_type(req, "text/html"); + httpd_resp_sendstr(req, response); + + // Clean up + be_pop(vm, 1); // Pop return value + be_pop(vm, 1); // Pop function + current_request = NULL; + + // Complete the async handler + httpd_req_async_handler_complete(async_req); + + return ESP_OK; +} + +// ------------------------------------------------------------------------ +// Berry API Implementation +// ------------------------------------------------------------------------ + +// Start the HTTP server +static int w_httpserver_start(bvm *vm) { + int top = be_top(vm); + + if (http_server != NULL) { + be_pushbool(vm, true); // Server already running + be_return (vm); + } + + // Initialize connection tracking + connection_tracking.mutex = xSemaphoreCreateMutex(); + if (!connection_tracking.mutex) { + ESP_LOGE(TAG, "Failed to create connection tracking mutex"); + be_pushbool(vm, false); + be_return (vm); + } + + // Configure the server + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.stack_size = 8192; + config.max_uri_handlers = HTTP_HANDLER_MAX; + config.max_open_sockets = HTTPD_MAX_CONNECTIONS; + config.lru_purge_enable = true; // Enable LRU purging of connections + config.uri_match_fn = httpd_uri_match_wildcard; // Enable wildcard URI matching + + // Handle port parameter if provided + if (top > 0 && be_isint(vm, 1)) { + config.server_port = be_toint(vm, 1); + } + + ESP_LOGI(TAG, "Starting HTTP server on port %d", config.server_port); + esp_err_t ret = httpd_start(&http_server, &config); + + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to start HTTP server: %d", ret); + vSemaphoreDelete(connection_tracking.mutex); + be_pushbool(vm, false); + be_return (vm); + } + + ESP_LOGI(TAG, "HTTP server started successfully"); + + // Initialize the queue for thread-safe message passing + init_http_queue(); + + be_pushbool(vm, true); + be_return (vm); +} + +// Register a URI handler +static int w_httpserver_on(bvm *vm) { + int top = be_top(vm); + + if (top < 2 || http_server == NULL) { + be_raise(vm, "value_error", top < 2 ? "Missing arguments" : "Server not started"); + return 0; + } + + if (!be_isstring(vm, 1) || !be_isfunction(vm, 2)) { + be_raise(vm, "type_error", "String and function required"); + return 0; + } + + const char *uri = be_tostring(vm, 1); + ESP_LOGI(TAG, "Registering handler for URI: %s", uri); + + // Find a free handler slot + int slot = -1; + for (int i = 0; i < HTTP_HANDLER_MAX; i++) { + if (!http_handlers[i].active) { + slot = i; + break; + } + } + + if (slot < 0) { + be_raise(vm, "runtime_error", "No more handler slots available"); + return 0; + } + + // Store handler info + http_handlers[slot].vm = vm; + http_handlers[slot].active = true; + + // Store the function reference + be_pushvalue(vm, 2); + bvalue *v = be_indexof(vm, -1); + http_handlers[slot].func = *v; + be_pop(vm, 1); + + // Register the handler with ESP-IDF HTTP server + httpd_uri_t http_uri = { + .uri = uri, + .method = HTTP_GET, + .handler = berry_handlers[slot], + .user_ctx = NULL + }; + + esp_err_t ret = httpd_register_uri_handler(http_server, &http_uri); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to register URI handler: %d", ret); + http_handlers[slot].active = false; + be_pushbool(vm, false); + be_return (vm); + } + + // Return the handler slot + be_pushint(vm, slot); + be_return (vm); +} + +// Stop the HTTP server +static int w_httpserver_stop(bvm *vm) { + if (http_server == NULL) { + be_pushbool(vm, false); // Server not running + be_return (vm); + } + + // Clean up handler registrations + for (int i = 0; i < HTTP_HANDLER_MAX; i++) { + if (http_handlers[i].active) { + http_handlers[i].active = false; + // Potentially unregister URI handlers here if needed + } + } + + // Stop the server + esp_err_t ret = httpd_stop(http_server); + http_server = NULL; + + // Clean up connection tracking + if (connection_tracking.mutex) { + vSemaphoreDelete(connection_tracking.mutex); + connection_tracking.mutex = NULL; + } + + be_pushbool(vm, ret == ESP_OK); + be_return (vm); +} + +// Get the server handle (for advanced usage) +static int w_httpserver_get_handle(bvm *vm) { + be_pushint(vm, (int)(intptr_t)http_server); + be_return (vm); +} + +// Simple wrapper around httpd_resp_sendstr +static int w_httpserver_send(bvm *vm) { + int argc = be_top(vm); + if (argc >= 1 && be_isstring(vm, 1)) { + const char* content = be_tostring(vm, 1); + + // Get the current request from the async message + httpd_req_t* req = current_request; + if (!req) { + be_raisef(vm, "request_error", "No active request"); + // Note: Don't call httpd_req_async_handler_complete here as there's no valid request + return 0; + } + + // Send the response + esp_err_t ret = httpd_resp_sendstr(req, content); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to send response: %d", ret); + // Don't complete the handler here - let the main handler do it + be_pushbool(vm, false); + be_return (vm); + } + + be_pushbool(vm, true); + be_return (vm); + } + + be_return_nil(vm); +} + +// Set WebSocket disconnect handler +void be_httpserver_set_disconnect_handler(httpd_close_func_t handler) { + http_server_disconn_handler = handler; +} + +// Get HTTP server handle +httpd_handle_t be_httpserver_get_handle(void) { + return http_server; +} + +// Function to check if message queue is available (referenced by wsserver) +bool httpserver_has_queue() { + return http_queue_initialized; +} + +/* @const_object_info_begin +module httpserver (scope: global) { + start, func(w_httpserver_start) + on, func(w_httpserver_on) + send, func(w_httpserver_send) + _handle, func(w_httpserver_get_handle) + stop, func(w_httpserver_stop) + process_queue, func(w_httpserver_process_queue) +} +@const_object_info_end */ + +#include "be_fixed_httpserver.h" + +#endif // USE_BERRY_HTTPSERVER \ No newline at end of file diff --git a/lib/libesp32/berry_tasmota/src/be_webfiles_lib.c b/lib/libesp32/berry_tasmota/src/be_webfiles_lib.c new file mode 100644 index 000000000..cb648ec67 --- /dev/null +++ b/lib/libesp32/berry_tasmota/src/be_webfiles_lib.c @@ -0,0 +1,392 @@ +/* + be_webfiles_lib.c - Static file server for Berry using ESP-IDF HTTP server + + Copyright (C) 2025 Jonathan E. Peace + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifdef USE_BERRY_WEBFILES + +#ifndef LOG_LOCAL_LEVEL +#define LOG_LOCAL_LEVEL ESP_LOG_INFO +#endif + +#include "be_constobj.h" +#include "be_mapping.h" + +// ESP-IDF includes +#include "esp_log.h" +#include "esp_http_server.h" +#include "esp_vfs.h" + +// External Berry/Tasmota includes +extern httpd_handle_t be_httpserver_get_handle(void); +extern bool httpserver_queue_message(int type, int client_id, + const char* data, size_t len, void* user_data); + +// Tag for logging +static const char *TAG = "WEBFILES"; + +// Default base path for files +static char base_path[64] = "/files"; + +// URI prefix for the file server +static char uri_prefix[32] = "/"; + +// Maximum file path length +#define FILE_PATH_MAX 128 + +// Scratch buffer size for file transfer +#define SCRATCH_BUFSIZE 4096 // 4KB scratch buffer for chunks + +// Static buffer for file sending - fixed allocation +static char scratch_buffer[SCRATCH_BUFSIZE]; + +// MIME Type Mapping +static const struct { + const char *extension; + const char *mimetype; +} mime_types[] = { + {".html", "text/html"}, + {".htm", "text/html"}, + {".js", "application/javascript"}, + {".mjs", "application/javascript"}, // ES modules + {".css", "text/css"}, + {".png", "image/png"}, + {".jpg", "image/jpeg"}, + {".jpeg", "image/jpeg"}, + {".gif", "image/gif"}, + {".ico", "image/x-icon"}, + {".svg", "image/svg+xml"}, + {".json", "application/json"}, + {".txt", "text/plain"}, + {".md", "text/markdown"}, + {".wasm", "application/wasm"}, // WebAssembly + {".map", "application/json"}, // Source maps + {".woff", "font/woff"}, + {".woff2", "font/woff2"}, + {".ttf", "font/ttf"}, + {".otf", "font/otf"}, + {".bin", "application/octet-stream"}, + {NULL, NULL} +}; + +// Get MIME type based on file extension +static const char* get_mime_type(const char *path) { + const char *ext = strrchr(path, '.'); + if (!ext) return "text/plain"; + + for (int i = 0; mime_types[i].extension; i++) { + if (strcasecmp(mime_types[i].extension, ext) == 0) { + return mime_types[i].mimetype; + } + } + return "text/plain"; +} + +// Build full path including base path +static const char* get_full_path(char *dest, const char *uri, size_t destsize) { + size_t base_len = strlen(base_path); + size_t prefix_len = strlen(uri_prefix); + size_t uri_len = strlen(uri); + + // Handle query parameters and fragments in URI + const char *query = strchr(uri, '?'); + if (query) { + uri_len = query - uri; + } + const char *fragment = strchr(uri, '#'); + if (fragment && (!query || fragment < query)) { + uri_len = fragment - uri; + } + + // Skip the URI prefix to get the relative path + const char *relative_path = uri; + if (prefix_len > 1 && strncmp(uri, uri_prefix, prefix_len) == 0) { + relative_path = uri + prefix_len - 1; // -1 because we want to keep the leading slash + uri_len -= (prefix_len - 1); + } + + // Check if path will fit in destination buffer + if (base_len + uri_len + 1 > destsize) { + ESP_LOGE(TAG, "Path too long"); + return NULL; + } + + // Construct full path + strcpy(dest, base_path); + if (base_len > 0 && base_path[base_len-1] == '/' && relative_path[0] == '/') { + // Avoid double slash + strlcpy(dest + base_len, relative_path + 1, uri_len); + } else { + strlcpy(dest + base_len, relative_path, uri_len + 1); + } + + return dest; +} + +// Set content type based on file extension +static void set_content_type_from_file(httpd_req_t *req, const char *filepath) { + const char* mime_type = get_mime_type(filepath); + httpd_resp_set_type(req, mime_type); + + // Set Cache-Control header for static assets + // Don't cache HTML, but cache other static assets + if (strstr(mime_type, "text/html") == NULL) { + // Cache for 1 hour (3600 seconds) + httpd_resp_set_hdr(req, "Cache-Control", "max-age=3600"); + } else { + // Don't cache HTML content + httpd_resp_set_hdr(req, "Cache-Control", "no-cache"); + } + + // Add CORS headers for development convenience + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); +} + +//checks if a .html, .css, or .js file exists with a .br suffix and serve that Brotli-compressed version instead + +static esp_err_t webfiles_handler(httpd_req_t *req) { + char filepath[FILE_PATH_MAX]; + char brotli_filepath[FILE_PATH_MAX]; + FILE *file = NULL; + struct stat file_stat; + bool use_brotli = false; + + // Process any URL query parameters if needed + char *query = strchr(req->uri, '?'); + if (query) { + ESP_LOGI(TAG, "Request has query params: %s", query); + *query = '\0'; // Temporarily terminate URI at the query string for path resolution + } + + // Get the full file path from the URI + if (get_full_path(filepath, req->uri, sizeof(filepath)) == NULL) { + ESP_LOGE(TAG, "Failed to get file path for URI: %s", req->uri); + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File not found"); + return ESP_FAIL; + } + + // Restore query string if we modified it + if (query) *query = '?'; + + ESP_LOGI(TAG, "Requested file: %s", filepath); + + // Check if file is .html, .css, or .js and if a .br version exists + const char *ext = strrchr(filepath, '.'); + if (ext && (strcasecmp(ext, ".html") == 0 || strcasecmp(ext, ".css") == 0 || + strcasecmp(ext, ".js") == 0 || strcasecmp(ext, ".svg") == 0)) { + // Check if client supports Brotli + char accept_encoding[64]; + if (httpd_req_get_hdr_value_str(req, "Accept-Encoding", accept_encoding, sizeof(accept_encoding)) == ESP_OK && + strstr(accept_encoding, "br") != NULL) { + // Construct Brotli filepath + snprintf(brotli_filepath, sizeof(brotli_filepath), "%s.br", filepath); + if (stat(brotli_filepath, &file_stat) == 0 && S_ISREG(file_stat.st_mode)) { + use_brotli = true; + strcpy(filepath, brotli_filepath); // Use the .br file + ESP_LOGI(TAG, "Found Brotli version: %s", filepath); + } + } + } + + // Check if file exists + if (stat(filepath, &file_stat) != 0) { + ESP_LOGE(TAG, "File does not exist: %s", filepath); + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File not found"); + return ESP_FAIL; + } + + // Check if it's a regular file + if (!S_ISREG(file_stat.st_mode)) { + ESP_LOGE(TAG, "Not a regular file: %s", filepath); + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Not a file"); + return ESP_FAIL; + } + + // Open the file for reading + file = fopen(filepath, "r"); + if (!file) { + ESP_LOGE(TAG, "Failed to open file: %s", filepath); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to open file"); + return ESP_FAIL; + } + + // Set content type based on file extension (use original path for MIME type if Brotli) + char original_filepath[FILE_PATH_MAX]; + if (use_brotli) { + // Strip .br for MIME type detection + strcpy(original_filepath, filepath); + original_filepath[strlen(original_filepath) - 3] = '\0'; // Remove ".br" + set_content_type_from_file(req, original_filepath); + } else { + set_content_type_from_file(req, filepath); + } + + // Set Brotli headers if applicable + if (use_brotli) { + httpd_resp_set_hdr(req, "Content-Encoding", "br"); + httpd_resp_set_hdr(req, "Vary", "Accept-Encoding"); + } + + // Send file in chunks for efficiency + size_t chunk_size; + size_t total_sent = 0; + + while ((chunk_size = fread(scratch_buffer, 1, SCRATCH_BUFSIZE, file)) > 0) { + if (httpd_resp_send_chunk(req, scratch_buffer, chunk_size) != ESP_OK) { + ESP_LOGE(TAG, "File send failed"); + fclose(file); + httpd_resp_send_chunk(req, NULL, 0); + return ESP_FAIL; + } + total_sent += chunk_size; + } + + // Close file + fclose(file); + + // Finish the HTTP response + httpd_resp_send_chunk(req, NULL, 0); + ESP_LOGI(TAG, "File sent successfully (%d bytes, %s)", (int)total_sent, use_brotli ? "Brotli" : "uncompressed"); + + return ESP_OK; +} + +/**************************************************************** + * Berry Interface Functions + ****************************************************************/ + +// webfiles.serve(base_path, uri_prefix) -> bool +// Serve files from base_path at uri_prefix +static int w_webfiles_serve(bvm *vm) { + int initial_top = be_top(vm); + + if (be_top(vm) >= 2 && be_isstring(vm, 1) && be_isstring(vm, 2)) { + const char* path = be_tostring(vm, 1); + const char* prefix = be_tostring(vm, 2); + + ESP_LOGI(TAG, "Setting up file server with base path: %s, uri prefix: %s", path, prefix); + + struct stat st; + if (stat(path, &st) != 0 || !S_ISDIR(st.st_mode)) { + ESP_LOGE(TAG, "Input path is not a valid directory: %s", path); + be_pushbool(vm, false); + // Clean up stack before returning + while (be_top(vm) > initial_top) { + be_pop(vm, 1); + } + be_return (vm); // Return directly + } + + httpd_handle_t server = be_httpserver_get_handle(); + if (!server) { + ESP_LOGE(TAG, "HTTP server not running"); + be_pushbool(vm, false); + while (be_top(vm) > initial_top) { + be_pop(vm, 1); + } + be_return (vm); + } + + strlcpy(base_path, path, sizeof(base_path)); + strlcpy(uri_prefix, prefix, sizeof(uri_prefix)); + + + // Use a static buffer for the URI pattern + static char registered_uri_pattern[64]; // ADD static keyword + // Ensure it's null-terminated even if snprintf truncates + registered_uri_pattern[sizeof(registered_uri_pattern) - 1] = '\0'; + snprintf(registered_uri_pattern, sizeof(registered_uri_pattern), "%s*", prefix); + + ESP_LOGI(TAG, "Registering URI handler with pattern: %s", registered_uri_pattern); + + httpd_uri_t uri_handler = { + // Point to the static buffer + .uri = registered_uri_pattern, // Use the static buffer + .method = HTTP_GET, + .handler = webfiles_handler, + .is_websocket = false, + .user_ctx = NULL + }; + + esp_err_t ret = httpd_register_uri_handler(server, &uri_handler); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to register URI handler: %d", ret); + be_pushbool(vm, false); + be_pop(vm, be_top(vm) - initial_top); + be_return (vm); + } + + be_pushbool(vm, true); + be_pop(vm, be_top(vm) - initial_top); + be_return (vm); + } + + be_pushbool(vm, false); + be_pop(vm, be_top(vm) - initial_top); + be_return (vm); +} + +// webfiles.serve_file(file_path, uri) -> bool +// Serve a specific file at a specific URI +static int w_webfiles_serve_file(bvm *vm) { + int initial_top = be_top(vm); + + if (be_top(vm) >= 2 && be_isstring(vm, 1) && be_isstring(vm, 2)) { + const char* file_path = be_tostring(vm, 1); + const char* uri = be_tostring(vm, 2); + + // Check if file exists + struct stat file_stat; + if (stat(file_path, &file_stat) == -1) { + ESP_LOGE(TAG, "File not found: %s", file_path); + be_pushbool(vm, false); + be_pop(vm, be_top(vm) - initial_top); + be_return (vm); + } + + // TODO: Implement custom handler for specific files + // This would require keeping track of file mappings + + ESP_LOGW(TAG, "serve_file not yet implemented"); + be_pushbool(vm, false); + be_pop(vm, be_top(vm) - initial_top); + be_return (vm); + } + + be_pushbool(vm, false); + be_pop(vm, be_top(vm) - initial_top); + be_return (vm); +} + +// Module definition +/* @const_object_info_begin +module webfiles (scope: global) { + serve, func(w_webfiles_serve) + serve_file, func(w_webfiles_serve_file) + + // MIME type constants + MIME_HTML, str("text/html") + MIME_JS, str("application/javascript") + MIME_CSS, str("text/css") + MIME_JSON, str("application/json") + MIME_TEXT, str("text/plain") + MIME_BINARY, str("application/octet-stream") +} +@const_object_info_end */ +#include "be_fixed_webfiles.h" + +#endif // USE_BERRY_WEBFILES diff --git a/lib/libesp32/berry_tasmota/src/be_wsserver_lib.c b/lib/libesp32/berry_tasmota/src/be_wsserver_lib.c new file mode 100644 index 000000000..9858939b8 --- /dev/null +++ b/lib/libesp32/berry_tasmota/src/be_wsserver_lib.c @@ -0,0 +1,1326 @@ +/* + be_wsserver_lib.c - WebSocket server support for Berry using ESP-IDF HTTP server + + Copyright (C) 2025 Jonathan E. Peace + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifdef USE_BERRY_WSSERVER + +#ifndef LOG_LOCAL_LEVEL +#define LOG_LOCAL_LEVEL ESP_LOG_INFO +#endif + +#include "be_constobj.h" +#include "be_mapping.h" +#include "be_exec.h" +#include "be_vm.h" + +// Standard C includes +#include +#include +#include + +// ESP-IDF includes +#include "esp_http_server.h" +#include "esp_event.h" +#include "esp_log.h" +#include "esp_timer.h" + +// Socket-related includes +#include +#include + +#include "be_object.h" +#include "be_string.h" +#include "be_gc.h" +#include "be_exec.h" +#include "be_debug.h" +#include "be_map.h" +#include "be_list.h" +#include "be_module.h" +#include "be_vm.h" + +static const char *TAG = "WSS"; + +// Max number of concurrent clients +#define MAX_WS_CLIENTS 5 + +// Define event types +#define WSSERVER_EVENT_CONNECT 0 +#define WSSERVER_EVENT_DISCONNECT 1 +#define WSSERVER_EVENT_MESSAGE 2 + +// Message types for queue - match those in be_httpserver_lib.c +#define HTTP_MSG_WEBSOCKET 1 +#define HTTP_MSG_FILE 2 +#define HTTP_MSG_WEB 3 + +// Forward declaration for the HTTP server handle getter function +extern httpd_handle_t be_httpserver_get_handle(void); + +// Declarations for functions used by httpserver_lib +extern bool httpserver_has_queue(void); +extern bool httpserver_queue_message(int msg_type, int client_id, + const void *data, size_t data_len, void *user_data); + +// Client tracking structure +typedef struct { + int sockfd; + bool active; + int64_t last_activity; // Timestamp of any client activity +} ws_client_t; + +// Callback structure to properly store Berry callbacks +typedef struct be_wsserver_callback_t { + bvm *vm; // VM instance + bvalue func; // Berry function value + bool active; // Whether this callback is active +} be_wsserver_callback_t; + +// Forward declarations for all functions to prevent compiler errors +static void init_clients(void); +static int find_free_client_slot(void); +static int add_client(int sockfd); +static int find_client_by_fd(int sockfd); +static void remove_client(int slot); +static bool is_client_valid(int slot); +static void log_ws_frame_info(httpd_ws_frame_t *ws_pkt); +static void handle_ws_message(int client_id, const char *message, size_t len); +static void handle_client_disconnect(int client_slot); +static void callBerryWsDispatcher(bvm *vm, int client_id, const char *event_name, const char *payload, int arg_count); +static void http_server_disconnect_handler(void* arg, int sockfd); +static void check_clients(void); + +// Globals +static httpd_handle_t ws_server = NULL; +static bool wsserver_running = false; +static uint32_t ping_interval_s; // Ping interval in seconds +static uint32_t ping_timeout_s; // Activity timeout in seconds + +// Client status and context +static ws_client_t ws_clients[MAX_WS_CLIENTS] = {0}; + +// Storage for Berry callback functions +static be_wsserver_callback_t wsserver_callbacks[3]; // CONNECT, DISCONNECT, MESSAGE + +// Forward declaration for processing WebSocket messages in main task context +void be_wsserver_handle_message(bvm *vm, int client_id, const char* data, size_t len); + +// Utility function to log WebSocket frame information +static void log_ws_frame_info(httpd_ws_frame_t *ws_pkt) { + if (!ws_pkt) return; + + const char* type_str = "UNKNOWN"; + switch (ws_pkt->type) { + case HTTPD_WS_TYPE_CONTINUE: type_str = "CONTINUE"; break; + case HTTPD_WS_TYPE_TEXT: type_str = "TEXT"; break; + case HTTPD_WS_TYPE_BINARY: type_str = "BINARY"; break; + case HTTPD_WS_TYPE_CLOSE: type_str = "CLOSE"; break; + case HTTPD_WS_TYPE_PING: type_str = "PING"; break; + case HTTPD_WS_TYPE_PONG: type_str = "PONG"; break; + } + + ESP_LOGI(TAG, "WS frame: type=%s, len=%d, final=%d", + type_str, ws_pkt->len, ws_pkt->final ? 1 : 0); +} + +// Call a Berry callback function registered by wsserver.on() +// CONTEXT: Main Tasmota Task (Berry VM Context) +// This function is only called from the main task when processing queued events +static void callBerryWsDispatcher(bvm *vm, int client_id, const char *event_name, const char *payload, int arg_count) { + if (!vm) { + ESP_LOGE(TAG, "Berry VM is NULL in callBerryWsDispatcher"); + return; + } + + // Ensure parameters are valid + if (client_id < 0 || client_id >= MAX_WS_CLIENTS || !event_name) { + ESP_LOGE(TAG, "Invalid parameters in callBerryWsDispatcher"); + return; + } + + ESP_LOGI(TAG, "Calling Berry callback for event '%s', client %d, payload: %s", + event_name, client_id, payload ? payload : "nil"); + + // Map event name to event type + int event_type = -1; + if (strcmp(event_name, "connect") == 0) { + event_type = WSSERVER_EVENT_CONNECT; + } else if (strcmp(event_name, "disconnect") == 0) { + event_type = WSSERVER_EVENT_DISCONNECT; + } else if (strcmp(event_name, "message") == 0) { + event_type = WSSERVER_EVENT_MESSAGE; + } else { + ESP_LOGE(TAG, "Unknown event type: %s", event_name); + return; + } + + // Check if we have a registered callback for this event type + if (!wsserver_callbacks[event_type].active) { + ESP_LOGI(TAG, "No callback registered for event type %d (%s)", event_type, event_name); + return; + } + + ESP_LOGI(TAG, "Using registered callback for event type %d (%s)", event_type, event_name); + + // Save initial stack position for diagnostic logging + int initial_top = be_top(vm); + ESP_LOGI(TAG, "Stack before function push: top=%d", initial_top); + + // Push the callback function onto the stack + bvalue *reg = vm->top; + var_setval(reg, &wsserver_callbacks[event_type].func); + be_incrtop(vm); + + ESP_LOGI(TAG, "Stack after function push: top=%d", be_top(vm)); + + // Push the client ID first, event name second, payload third (if applicable) + // This matches Berry function signature: function(client, event, message) + be_pushint(vm, client_id); // Push client ID argument + be_pushstring(vm, event_name); // Push event name argument + + // Push payload argument for message events (optional) + if (arg_count > 2 && payload) { + be_pushstring(vm, payload); + } + + // Log the arguments about to be passed + ESP_LOGI(TAG, "Stack after arg push: top=%d, with %d arguments", be_top(vm), arg_count); + + // Call the callback function with the appropriate number of arguments + int call_result = be_pcall(vm, arg_count); + + ESP_LOGI(TAG, "Stack after be_pcall: top=%d", be_top(vm)); + + // Handle the Berry call result + if (call_result != BE_OK) { + ESP_LOGE(TAG, "Berry callback error for event '%s': %s", + event_name, be_tostring(vm, -1)); + + be_error_pop_all(vm); + } else { + // Pop all arguments (including return value) + be_pop(vm, arg_count); + + ESP_LOGI(TAG, "Stack after arg pop: top=%d", be_top(vm)); + + // Pop the function separately + be_pop(vm, 1); + + ESP_LOGI(TAG, "Final stack after cleanup: top=%d", be_top(vm)); + } +} + +// Process a WebSocket message in the main task context +// CONTEXT: Main Tasmota Task (Berry VM Context) +// This function processes messages from the queue in the main task +void be_wsserver_handle_message(bvm *vm, int client_id, const char* data, size_t len) { + if (!data) { + // This is either a connect or disconnect event (no data) + // Determine event type based on client state + if (is_client_valid(client_id)) { + // Client exists and is active - this is a connect event + ESP_LOGI(TAG, "Handling WebSocket connect event in main task: client=%d", client_id); + callBerryWsDispatcher(vm, client_id, "connect", NULL, 2); + } else { + // Client no longer active - this is a disconnect event + ESP_LOGI(TAG, "Handling WebSocket disconnect event in main task: client=%d", client_id); + callBerryWsDispatcher(vm, client_id, "disconnect", NULL, 2); + + // Mark client as inactive after processing + if (client_id >= 0 && client_id < MAX_WS_CLIENTS) { + ws_clients[client_id].sockfd = -1; + ws_clients[client_id].active = false; + } + } + } else { + // Normal message event with data + ESP_LOGI(TAG, "Handling WebSocket message event in main task: client=%d, len=%d, data=%s", + client_id, (int)len, data); + callBerryWsDispatcher(vm, client_id, "message", data, 3); + } +} + +// WebSocket Event Handler +// CONTEXT: ESP-IDF HTTP Server Task +static esp_err_t ws_handler(httpd_req_t *req) { + if (req->method == HTTP_GET) { + // Handshake handling + ESP_LOGI(TAG, "WebSocket handshake received"); + int sockfd = httpd_req_to_sockfd(req); + int client_slot = add_client(sockfd); + + if (client_slot >= 0) { + ESP_LOGI(TAG, "Client %d connected, socket fd: %d", client_slot, sockfd); + + // Queue connect event for processing in main task + // TRANSITION: ESP-IDF HTTP Server Task → Main Tasmota Task + // Use NULL as data to indicate this is a connect event + if (httpserver_has_queue()) { + bool queue_result = httpserver_queue_message(HTTP_MSG_WEBSOCKET, client_slot, + NULL, 0, NULL); + + if (queue_result) { + ESP_LOGI(TAG, "WebSocket connect event successfully queued"); + } else { + ESP_LOGE(TAG, "Failed to queue WebSocket connect event!"); + } + } else { + ESP_LOGW(TAG, "No queue available for connect event - fallback to direct processing"); + // Fallback to direct processing (not recommended) + if (wsserver_callbacks[WSSERVER_EVENT_CONNECT].active) { + const char *event_name = "connect"; + callBerryWsDispatcher(wsserver_callbacks[WSSERVER_EVENT_CONNECT].vm, + client_slot, event_name, NULL, 2); + } + } + } + + // Return success for the handshake + return ESP_OK; + } + + ESP_LOGD(TAG, "WebSocket frame received"); + int sockfd = httpd_req_to_sockfd(req); + int client_slot = find_client_by_fd(sockfd); + + if (client_slot < 0) { + ESP_LOGE(TAG, "Received frame from unknown client (socket: %d)", sockfd); + return ESP_FAIL; + } + + // ----- PRE-PROCESSING: Frame Validation & Memory Allocation ----- + + // Update activity time for ANY frame + ws_clients[client_slot].last_activity = esp_timer_get_time() / 1000; + + // Standard ESP-IDF WebSocket frame reception + httpd_ws_frame_t ws_pkt = {0}; + uint8_t *buf = NULL; + + ws_pkt.type = HTTPD_WS_TYPE_TEXT; // Default type + + // Get the frame length + esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "httpd_ws_recv_frame failed with %d", ret); + return ret; + } + + ESP_LOGD(TAG, "Frame len is %d, type is %d", ws_pkt.len, ws_pkt.type); + + // Handle control frames immediately without involving Berry VM + if (ws_pkt.type == HTTPD_WS_TYPE_PONG) { + ESP_LOGI(TAG, "Received PONG from client %d", client_slot); + return ESP_OK; + } + + if (ws_pkt.type == HTTPD_WS_TYPE_CLOSE) { + ESP_LOGI(TAG, "Received CLOSE from client %d", client_slot); + handle_client_disconnect(client_slot); + return ESP_OK; + } + + if (ws_pkt.type == HTTPD_WS_TYPE_PING) { + ESP_LOGI(TAG, "Received PING from client %d", client_slot); + httpd_ws_frame_t pong = {0}; + pong.type = HTTPD_WS_TYPE_PONG; + pong.len = 0; + ret = httpd_ws_send_frame(req, &pong); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to send PONG: %d", ret); + } + return ESP_OK; + } + + // Not a control frame , so allocate memory for the message payload + if (ws_pkt.len) { + buf = calloc(1, ws_pkt.len + 1); + if (buf == NULL) { + ESP_LOGE(TAG, "Failed to allocate memory for WS message"); + return ESP_ERR_NO_MEM; + } + ws_pkt.payload = buf; + // Receive the actual message + ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "httpd_ws_recv_frame failed with %d", ret); + free(buf); + return ret; + } + + // Ensure null-termination for text frames + if (ws_pkt.type == HTTPD_WS_TYPE_TEXT) { + buf[ws_pkt.len] = 0; + } + } else { + // Empty message, nothing to process + return ESP_OK; + } + + // Process message based on type + if (ws_pkt.type == HTTPD_WS_TYPE_TEXT) { + ESP_LOGI(TAG, "Received TEXT WebSocket message: '%s'", buf); + handle_ws_message(client_slot, (char*)buf, ws_pkt.len); + } else if (ws_pkt.type == HTTPD_WS_TYPE_BINARY) { + ESP_LOGI(TAG, "Received BINARY WebSocket message, len=%d", ws_pkt.len); + handle_ws_message(client_slot, (char*)buf, ws_pkt.len); + } + + // Free the data copy regardless of send result + free(buf); + + return ESP_OK; +} + +// Handle a WebSocket message from a client +void handle_ws_message(int client_id, const char *message, size_t len) { + if (!message || len == 0) { + ESP_LOGE(TAG, "Received empty message from client %d", client_id); + return; + } + + // Update activity timestamp + if (client_id >= 0 && client_id < MAX_WS_CLIENTS && ws_clients[client_id].active) { + ws_clients[client_id].last_activity = esp_timer_get_time() / 1000; + } + + // If queue available, send to main task for processing + if (httpserver_has_queue()) { + ESP_LOGD(TAG, "Queueing WebSocket message from client %d: '%.*s'", client_id, (int)len, message); + + // Make a copy of the message data for the queue + char *data_copy = malloc(len); + if (!data_copy) { + ESP_LOGE(TAG, "Failed to allocate memory for message copy"); + return; + } + + memcpy(data_copy, message, len); + + // Queue the message + if (!httpserver_queue_message(HTTP_MSG_WEBSOCKET, client_id, data_copy, len, NULL)) { + ESP_LOGE(TAG, "Failed to queue WebSocket message"); + free(data_copy); + } + } else { + // No queue available, process directly (not recommended but fallback) + ESP_LOGW(TAG, "Processing WebSocket message directly without queue - this may be unsafe"); + callBerryWsDispatcher(NULL, client_id, "message", message, 2); + } +} + +// Handle client disconnection +// CONTEXT: ESP-IDF HTTP Server Task +// Called when the server detects a client disconnection +static void handle_client_disconnect(int client_slot) { + if (!is_client_valid(client_slot)) { + return; + } + + ESP_LOGI(TAG, "Client %d disconnected", client_slot); + + // Queue disconnect event for processing in main task + // Use NULL as data to indicate this is a disconnect event + if (httpserver_has_queue()) { + // Save sockfd for later removal + int sockfd = ws_clients[client_slot].sockfd; + + // Mark client as inactive BEFORE queuing the event + // This ensures the event processor knows it's a disconnect event + ws_clients[client_slot].active = false; + + bool queue_result = httpserver_queue_message(HTTP_MSG_WEBSOCKET, client_slot, + NULL, 0, NULL); + + if (queue_result) { + ESP_LOGD(TAG, "WebSocket disconnect event successfully queued"); + } else { + ESP_LOGE(TAG, "Failed to queue WebSocket disconnect event!"); + + // Fallback to direct processing + if (wsserver_callbacks[WSSERVER_EVENT_DISCONNECT].active) { + const char *event_name = "disconnect"; + callBerryWsDispatcher(wsserver_callbacks[WSSERVER_EVENT_DISCONNECT].vm, + client_slot, event_name, NULL, 2); + } + + // Ensure cleanup happens if queue fails + ws_clients[client_slot].sockfd = -1; + } + } else { + ESP_LOGW(TAG, "No queue available for disconnect event - fallback to direct processing"); + + // Fallback to direct processing (not recommended) + if (wsserver_callbacks[WSSERVER_EVENT_DISCONNECT].active) { + const char *event_name = "disconnect"; + callBerryWsDispatcher(wsserver_callbacks[WSSERVER_EVENT_DISCONNECT].vm, + client_slot, event_name, NULL, 2); + } + + // Always clean up client slot + ws_clients[client_slot].sockfd = -1; + ws_clients[client_slot].active = false; + } +} + +// Timer callback for pinging clients +static void ws_ping_timer_callback(void* arg) { + // Skip if server is not running or pings disabled + if (!wsserver_running || !ws_server || ping_interval_s == 0) return; + + int64_t now = esp_timer_get_time(); + + for (int i = 0; i < MAX_WS_CLIENTS; i++) { + if (ws_clients[i].active) { + // Check for inactivity timeout if enabled + if (ping_timeout_s > 0) { + // Calculate inactivity time based on milliseconds + int64_t inactivity_s = ((now / 1000) - ws_clients[i].last_activity) / 1000; // now/1000 for ms, then /1000 for seconds + + if (inactivity_s > ping_timeout_s) { + ESP_LOGI(TAG, "Client %d timed out (inactive for %d seconds)", + i, (int)inactivity_s); + + // Trigger session close + handle_client_disconnect(i); + continue; + } + } + + // Send ping + httpd_ws_frame_t ping = {0}; + ping.type = HTTPD_WS_TYPE_PING; + + ESP_LOGI(TAG, "Sending PING to client %d", i); + esp_err_t ret = httpd_ws_send_frame_async(ws_server, ws_clients[i].sockfd, &ping); + + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to send PING to client %d: %d", i, ret); + + // Trigger session close + handle_client_disconnect(i); + } + } + } +} + +// Client Management Functions +static void init_clients(void) { + for (int i = 0; i < MAX_WS_CLIENTS; i++) { + ws_clients[i].active = false; + ws_clients[i].sockfd = -1; + } +} + +static int find_free_client_slot(void) { + for (int i = 0; i < MAX_WS_CLIENTS; i++) { + if (!ws_clients[i].active) { + return i; + } + } + return -1; // No free slots +} + +static int add_client(int sockfd) { + int slot = find_free_client_slot(); + if (slot >= 0) { + ws_clients[slot].sockfd = sockfd; + ws_clients[slot].active = true; + ws_clients[slot].last_activity = esp_timer_get_time() / 1000; + ESP_LOGI(TAG, "Added client %d with socket %d", slot, sockfd); + return slot; + } + ESP_LOGE(TAG, "No free client slots available"); + return -1; +} + +static int find_client_by_fd(int sockfd) { + for (int i = 0; i < MAX_WS_CLIENTS; i++) { + if (ws_clients[i].active && ws_clients[i].sockfd == sockfd) { + return i; + } + } + return -1; +} + +static void remove_client(int slot) { + if (slot >= 0 && slot < MAX_WS_CLIENTS) { + ESP_LOGI(TAG, "Removing client %d with socket %d", slot, ws_clients[slot].sockfd); + ws_clients[slot].active = false; + ws_clients[slot].sockfd = -1; + } +} + +// Check if a client is valid and connected +static bool is_client_valid(int client_id) { + // Check for valid client ID range + if (client_id < 0 || client_id >= MAX_WS_CLIENTS) { + ESP_LOGE(TAG, "Invalid client ID: %d", client_id); + return false; + } + + // Check if client is active and has a valid socket + if (!ws_clients[client_id].active || ws_clients[client_id].sockfd < 0) { + ESP_LOGE(TAG, "Client %d is not active (active=%d, sockfd=%d)", + client_id, ws_clients[client_id].active, ws_clients[client_id].sockfd); + return false; + } + + return true; +} + +// Berry Interface Functions +static int w_wsserver_start(bvm *vm) { + if (wsserver_running) { + ESP_LOGI(TAG, "WebSocket server already running"); + // Don't just return true - we should still update the path or other parameters + // if they've changed, but don't re-register the handlers + if (be_top(vm) >= 1 && be_isstring(vm, 1)) { + const char* path = be_tostring(vm, 1); + + // Get optional ping parameters + if (be_top(vm) >= 3 && be_isint(vm, 3)) { + ping_interval_s = be_toint(vm, 3); + ESP_LOGD(TAG, "Updated ping interval to %d seconds", ping_interval_s); + } + + if (be_top(vm) >= 4 && be_isint(vm, 4)) { + ping_timeout_s = be_toint(vm, 4); + ESP_LOGD(TAG, "Updated ping timeout to %d seconds", ping_timeout_s); + } + } + + be_pushbool(vm, true); + be_return (vm); + } + + ESP_LOGI(TAG, "Init WebSocket server"); + if (be_top(vm) >= 1 && be_isstring(vm, 1)) { + const char* path = be_tostring(vm, 1); + ESP_LOGI(TAG, "Starting WebSocket server on path '%s'", path); + + // Get optional http_handle parameter + bool use_existing_handle = false; + if (be_top(vm) >= 2) { + // First try as comptr + if (be_iscomptr(vm, 2)) { + void* http_handle = be_tocomptr(vm, 2); + use_existing_handle = true; + if (http_handle == NULL) { + ESP_LOGE(TAG, "HTTP server handle is NULL"); + be_pushbool(vm, false); + be_return (vm); + } + + // Assign to global ws_server variable after validating it's not NULL + ws_server = http_handle; + ESP_LOGD(TAG, "Using existing HTTP server handle (comptr): %p", ws_server); + } + // If not comptr, try to get httpd_handle_t from Berry externally + else { + // First log that we're attempting to get the handle + ESP_LOGD(TAG, "Attempting to retrieve HTTP server handle via be_httpserver_get_handle()"); + + // Try to get the handle from the HTTP server + httpd_handle_t handle = be_httpserver_get_handle(); + ESP_LOGD(TAG, "Got HTTP server handle: %p", handle); + if (handle) { + use_existing_handle = true; + ws_server = handle; + ESP_LOGD(TAG, "Using HTTP server handle from httpserver module: %p", ws_server); + } else { + ESP_LOGE(TAG, "HTTP server module returned NULL handle"); + } + } + } + + // Get optional ping_interval parameter (0 = disabled) + ping_interval_s = 5; // Default: 5 seconds + ping_timeout_s = 10; // Default: 10 seconds + + if (be_top(vm) >= 3 && be_isint(vm, 3)) { + ping_interval_s = be_toint(vm, 3); + + // Optional ping_timeout parameter + if (be_top(vm) >= 4 && be_isint(vm, 4)) { + ping_timeout_s = be_toint(vm, 4); + } + + ESP_LOGI(TAG, "WebSocket ping configured: interval=%ds, timeout=%ds", + ping_interval_s, ping_timeout_s); + } + + // Initialize the client array + init_clients(); + + // Handle HTTP server setup + if (use_existing_handle) { + // If using an existing handle, just register our disconnect handler + ESP_LOGI(TAG, "Using existing HTTP server, skipping server creation"); + ESP_LOGI(TAG, "Registering disconnect handler with existing server"); + be_httpserver_set_disconnect_handler(http_server_disconnect_handler); + } else { + // Create a new HTTP server + ESP_LOGI(TAG, "No HTTP server provided, creating a new one"); + // Configure and start the HTTP server + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.server_port = 8080; // Use a different port from Tasmota web server + config.max_open_sockets = MAX_WS_CLIENTS + 2; // +2 for admin connections + config.max_uri_handlers = 2; // For WebSocket and potential health check + config.lru_purge_enable = true; // Enable LRU connection purging + config.recv_wait_timeout = 10; // 10 seconds timeout for receiving + config.send_wait_timeout = 10; // 10 seconds timeout for sending + config.core_id = 1; // Run on second core for better performance isolation + + // Set disconnect handler + config.close_fn = http_server_disconnect_handler; + ESP_LOGD(TAG, "Registering disconnect handler for new WebSocket server"); + + ESP_LOGI(TAG, "Starting new HTTP server on port %d", config.server_port); + esp_err_t ret = httpd_start(&ws_server, &config); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to start server: %d (0x%x)", ret, ret); + wsserver_running = false; + ws_server = NULL; + be_pushbool(vm, false); + be_return (vm); + } + ESP_LOGI(TAG, "New HTTP server started on port %d", config.server_port); + } + + // Register URI handler for WebSocket endpoint + httpd_uri_t ws_uri = { + .uri = path, + .method = HTTP_GET, + .handler = ws_handler, // Use the connect handler which properly registers clients + .user_ctx = NULL, + .is_websocket = true, + .handle_ws_control_frames = true // Allow ESP-IDF to properly handle control frames + }; + + ESP_LOGI(TAG, "Registering WebSocket handler for '%s'", path); + esp_err_t ret = httpd_register_uri_handler(ws_server, &ws_uri); + if (ret != ESP_OK) { + // Check if the error is just that the handler already exists + if (ret == ESP_ERR_HTTPD_HANDLER_EXISTS) { + ESP_LOGW(TAG, "WebSocket handler for '%s' already exists, continuing", path); + // This is actually OK, just continue + } else { + ESP_LOGE(TAG, "Failed to register URI handler: %d (0x%x)", ret, ret); + // Only stop the server if we created it + if (!use_existing_handle) { + httpd_stop(ws_server); + } else { + // When using an existing handle, we need to be careful not to nullify it + // since it's managed externally + ESP_LOGI(TAG, "Using existing HTTP server, not stopping it despite URI registration failure"); + } + wsserver_running = false; // Mark as not running + be_pushbool(vm, false); + be_return (vm); + } + } + + // Set up ping timer if enabled - direct conversion from seconds to microseconds + if (ping_interval_s > 0) { + ESP_LOGI(TAG, "Starting ping timer with interval %ds", ping_interval_s); + esp_timer_handle_t ping_timer = NULL; + esp_timer_create_args_t timer_args = { + .callback = ws_ping_timer_callback, + .name = "ws_ping" + }; + esp_timer_create(&timer_args, &ping_timer); + esp_timer_start_periodic(ping_timer, ping_interval_s * 1000000); // seconds to microseconds + } + + wsserver_running = true; + ESP_LOGI(TAG, "WebSocket server started successfully"); + be_pushbool(vm, true); + be_return (vm); + } + + ESP_LOGE(TAG, "Invalid path parameter"); + be_pushbool(vm, false); + be_return (vm); +} + +static int w_wsserver_client_info(bvm *vm) { + int initial_top = be_top(vm); // Save initial stack position for debugging + + ESP_LOGI(TAG, "client_info: Initial stack top: %d", initial_top); + + if (be_top(vm) >= 1 && be_isint(vm, 1)) { + int client_slot = be_toint(vm, 1); + + if (!is_client_valid(client_slot)) { + ESP_LOGE(TAG, "client_info: Invalid client %d, returning nil", client_slot); + be_pushnil(vm); + + // Check stack for consistency + int final_top = be_top(vm); + ESP_LOGI(TAG, "client_info: Invalid client path - Final stack: %d (expected %d)", + final_top, initial_top + 1); + be_return (vm); + } + + // Calculate time since last activity + int64_t now = esp_timer_get_time(); + int64_t inactive_time_ms = ((now / 1000) - ws_clients[client_slot].last_activity) / 1000; // convert to milliseconds + + // Create map with client info - this adds one item to the stack + be_newmap(vm); + int after_map_top = be_top(vm); + ESP_LOGI(TAG, "client_info: After map creation - Stack: %d", after_map_top); + + // Add client socket fd - Key + be_pushstring(vm, "socket"); + // Add client socket fd - Value + be_pushint(vm, ws_clients[client_slot].sockfd); + // Insert into map - consumes the key and value, leaving map on stack + be_data_insert(vm, -3); + // No need for explicit pop here as be_data_insert consumes key and value + + // Add last activity timestamp - Key + be_pushstring(vm, "last_activity"); + // Add last activity timestamp - Value + be_pushint(vm, (int)(ws_clients[client_slot].last_activity / 1000000)); // seconds + // Insert into map - consumes the key and value, leaving map on stack + be_data_insert(vm, -3); + // No need for explicit pop here as be_data_insert consumes key and value + + // Add inactivity duration - Key + be_pushstring(vm, "inactive_ms"); + // Add inactivity duration - Value + be_pushint(vm, (int)inactive_time_ms); + // Insert into map - consumes the key and value, leaving map on stack + be_data_insert(vm, -3); + // No need for explicit pop here as be_data_insert consumes key and value + + // At this point there should be exactly one item on the stack (the map) + // beyond what was there when we started + int final_top = be_top(vm); + ESP_LOGI(TAG, "client_info: Final stack: %d (expected %d)", final_top, initial_top + 1); + + // The map is already on the stack as our return value + be_return (vm); + } + + // If we reach here, either there were no parameters or the parameter was not an integer + ESP_LOGI(TAG, "client_info: Invalid parameters, returning nil"); + be_pushnil(vm); + + // Check stack for consistency + int final_top = be_top(vm); + ESP_LOGI(TAG, "client_info: Error path - Final stack: %d (expected %d)", + final_top, initial_top + 1); + be_return (vm); +} + +static int w_wsserver_count_clients(bvm *vm) { + int initial_top = be_top(vm); + ESP_LOGI(TAG, "wsserver_count_clients: Initial stack top: %d", initial_top); + + int count = 0; + for (int i = 0; i < MAX_WS_CLIENTS; i++) { + if (ws_clients[i].active) { + count++; + } + } + + be_pushint(vm, count); + + int final_top = be_top(vm); + ESP_LOGI(TAG, "wsserver_count_clients: Final stack: %d (expected %d), returning %d clients", + final_top, initial_top + 1, count); + be_return (vm); +} + +static int w_wsserver_send(bvm *vm) { + int initial_top = be_top(vm); + ESP_LOGI(TAG, "wsserver_send: Initial stack top: %d", initial_top); + + // First validate parameters + if (be_top(vm) < 2 || !be_isint(vm, 1)) { + ESP_LOGE(TAG, "Invalid parameters for send"); + be_pushbool(vm, false); + + int final_top = be_top(vm); + ESP_LOGI(TAG, "wsserver_send: Error path - Final stack: %d (expected %d)", + final_top, initial_top + 1); + be_return (vm); + } + + int client_slot = be_toint(vm, 1); + size_t len = 0; + const char* data = NULL; + uint8_t *data_copy = NULL; + + // First check if client is valid before processing the message + if (!is_client_valid(client_slot)) { + ESP_LOGE(TAG, "Invalid client ID: %d", client_slot); + be_pushbool(vm, false); + + int final_top = be_top(vm); + ESP_LOGI(TAG, "wsserver_send: Invalid client - Final stack: %d (expected %d)", + final_top, initial_top + 1); + be_return (vm); + } + + // Check if we're dealing with a string or bytes object + bool is_valid_data = false; + bool is_bytes = false; + + if (be_isstring(vm, 2)) { + is_valid_data = true; + } else if (be_isbytes(vm, 2)) { + is_valid_data = true; + is_bytes = true; + } + + if (!is_valid_data) { + ESP_LOGE(TAG, "Data must be a string or bytes object"); + be_pushbool(vm, false); + + int final_top = be_top(vm); + ESP_LOGI(TAG, "wsserver_send: Invalid data type - Final stack: %d (expected %d)", + final_top, initial_top + 1); + be_return (vm); + } + + // Get data and length based on type + if (is_bytes) { + // It's a bytes object - get raw length + data = be_tobytes(vm, 2, &len); + ESP_LOGI(TAG, "Got bytes object with length: %d", (int)len); + } else { + // For normal strings, get the length from Berry + len = be_strlen(vm, 2); + data = be_tostring(vm, 2); + ESP_LOGI(TAG, "Got string with length: %d", (int)len); + } + + if (len == 0 || data == NULL) { + ESP_LOGE(TAG, "Invalid data (empty or NULL)"); + be_pushbool(vm, false); + + int final_top = be_top(vm); + ESP_LOGI(TAG, "wsserver_send: Empty data - Final stack: %d (expected %d)", + final_top, initial_top + 1); + be_return (vm); + } + + // Check for optional binary flag parameter + bool is_binary = false; + if (be_top(vm) >= 3 && be_isbool(vm, 3)) { + is_binary = be_tobool(vm, 3); + } + + ESP_LOGI(TAG, "Sending %d bytes to client %d (type: %s)", + len, client_slot, is_binary ? "binary" : "text"); + + // Client was already checked at the start, but double check again for safety + if (!is_client_valid(client_slot)) { + ESP_LOGE(TAG, "Client ID %d became invalid during processing", client_slot); + be_pushbool(vm, false); + + int final_top = be_top(vm); + ESP_LOGI(TAG, "wsserver_send: Client became invalid - Final stack: %d (expected %d)", + final_top, initial_top + 1); + be_return (vm); + } + + httpd_ws_frame_t ws_pkt = {0}; + + // Create a copy of the data to ensure it remains valid during async operation + data_copy = (uint8_t *)malloc(len); + if (data_copy == NULL) { + ESP_LOGE(TAG, "Failed to allocate memory for data copy of size %d", len); + be_pushbool(vm, false); + + int final_top = be_top(vm); + ESP_LOGI(TAG, "wsserver_send: Memory allocation failed - Final stack: %d (expected %d)", + final_top, initial_top + 1); + be_return (vm); + } + + // Copy the data + memcpy(data_copy, data, len); + + ws_pkt.payload = data_copy; + ws_pkt.len = len; + ws_pkt.type = is_binary ? HTTPD_WS_TYPE_BINARY : HTTPD_WS_TYPE_TEXT; + ws_pkt.final = true; + ws_pkt.fragmented = false; + + // Get the client's socket + int sockfd = ws_clients[client_slot].sockfd; + + // Track last send attempt for this client + int64_t now = esp_timer_get_time(); + ws_clients[client_slot].last_activity = now; + + // Send the frame asynchronously + esp_err_t ret = httpd_ws_send_frame_async(ws_server, sockfd, &ws_pkt); + + // Free the data copy regardless of send result + free(data_copy); + + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to send message to client %d: %d (0x%x)", + client_slot, ret, ret); + + // Check if this is a fatal error indicating disconnection + if (ret == ESP_ERR_HTTPD_INVALID_REQ || ret == ESP_ERR_HTTPD_RESP_SEND) { + ESP_LOGE(TAG, "Fatal error sending to client %d, removing client", client_slot); + + // Trigger session close + handle_client_disconnect(client_slot); + } + + be_pushbool(vm, false); + + int final_top = be_top(vm); + ESP_LOGI(TAG, "wsserver_send: Send failed - Final stack: %d (expected %d)", + final_top, initial_top + 1); + be_return (vm); + } + + // Success! + ESP_LOGI(TAG, "Successfully sent message to client %d", client_slot); + be_pushbool(vm, true); + + int final_top = be_top(vm); + ESP_LOGI(TAG, "wsserver_send: Success - Final stack: %d (expected %d)", + final_top, initial_top + 1); + be_return (vm); +} + +static int w_wsserver_close(bvm *vm) { + if (be_top(vm) >= 1 && be_isint(vm, 1)) { + int client_slot = be_toint(vm, 1); + + ESP_LOGI(TAG, "Closing client %d", client_slot); + + if (!is_client_valid(client_slot)) { + ESP_LOGE(TAG, "Invalid client ID: %d", client_slot); + be_pushbool(vm, false); + be_return (vm); + } + + // Send a close frame + httpd_ws_frame_t ws_pkt = {0}; + ws_pkt.type = HTTPD_WS_TYPE_CLOSE; + ws_pkt.len = 0; + + int sockfd = ws_clients[client_slot].sockfd; + esp_err_t ret = httpd_ws_send_frame_async(ws_server, sockfd, &ws_pkt); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to send close frame: %d", ret); + } + + // Trigger session close + handle_client_disconnect(client_slot); + + be_pushbool(vm, (ret == ESP_OK)); + be_return (vm); + } + + ESP_LOGE(TAG, "Invalid parameters for close"); + be_pushbool(vm, false); + be_return (vm); +} + +static int w_wsserver_on(bvm *vm) { + // Save initial stack position for balance checking + int initial_top = be_top(vm); + + ESP_LOGI(TAG, "[ON-STACK] Initial stack position: %d", initial_top); + + if (be_top(vm) >= 2 && be_isint(vm, 1) && + (be_isfunction(vm, 2) || be_isclosure(vm, 2))) { // Accept both function and closure types + + // Extract event type value (we'll need this in both phases) + int event_type = be_toint(vm, 1); + + // Validate event type early + if (event_type < 0 || event_type >= 3) { + ESP_LOGE(TAG, "[ON-ERROR] Invalid event type: %d", event_type); + be_pushbool(vm, false); + be_return (vm); + } + + // Log function type for debugging + const char *type_str = be_isfunction(vm, 2) ? "function" : "closure"; + ESP_LOGI(TAG, "[ON-STACK] Registering %s callback (type %d)", type_str, event_type); + + // Make a safe copy of the function value - CRITICAL for stack stability + bvalue func_copy = *be_indexof(vm, 2); + bool is_gc_obj = be_isgcobj(&func_copy); + + // If there's already an active callback, unmark it for GC first + if (wsserver_callbacks[event_type].active) { + if (be_isgcobj(&wsserver_callbacks[event_type].func)) { + ESP_LOGI(TAG, "[ON-GC] Unmarking existing callback for event %d", event_type); + be_gc_fix_set(vm, wsserver_callbacks[event_type].func.v.gc, bfalse); + } + } + + // Store the callback information in our global registry + wsserver_callbacks[event_type].vm = vm; + wsserver_callbacks[event_type].active = true; + wsserver_callbacks[event_type].func = func_copy; // Store our copied function + + // Protect function from garbage collection if needed + if (is_gc_obj) { + ESP_LOGI(TAG, "[ON-GC] Marking callback as protected from GC for event %d", event_type); + be_gc_fix_set(vm, func_copy.v.gc, btrue); + } + + // Log event information + const char* event_name = + event_type == WSSERVER_EVENT_CONNECT ? "connect" : + event_type == WSSERVER_EVENT_DISCONNECT ? "disconnect" : "message"; + + ESP_LOGI(TAG, "[ON-EVENT] Registered %s callback (event %d)", event_name, event_type); + + // Return success + be_pushbool(vm, true); + be_return (vm); + } + + ESP_LOGE(TAG, "[ON-ERROR] Invalid parameters for on"); + be_pushbool(vm, false); + be_return (vm); +} + +static int w_wsserver_is_connected(bvm *vm) { + int initial_top = be_top(vm); + ESP_LOGI(TAG, "wsserver_is_connected: Initial stack top: %d", initial_top); + + if (be_top(vm) >= 1 && be_isint(vm, 1)) { + int client_slot = be_toint(vm, 1); + bool valid = is_client_valid(client_slot); + ESP_LOGI(TAG, "Checking if client %d is connected: %s", client_slot, valid ? "yes" : "no"); + be_pushbool(vm, valid); + + int final_top = be_top(vm); + ESP_LOGI(TAG, "wsserver_is_connected: Success - Final stack: %d (expected %d)", + final_top, initial_top + 1); + be_return (vm); + } + + ESP_LOGE(TAG, "Invalid parameters for is_connected"); + be_pushbool(vm, false); + + int final_top = be_top(vm); + ESP_LOGI(TAG, "wsserver_is_connected: Invalid parameters - Final stack: %d (expected %d)", + final_top, initial_top + 1); + be_return (vm); +} + +static int w_wsserver_stop(bvm *vm) { + if (!wsserver_running) { + ESP_LOGI(TAG, "WebSocket server not running"); + be_pushbool(vm, true); + + int final_top = be_top(vm); + ESP_LOGI(TAG, "wsserver_stop: Server not running - Final stack: %d (expected %d)", + final_top, 1); + be_return (vm); + } + + ESP_LOGI(TAG, "Stopping WebSocket server"); + + // ---- PHASE 1: Prepare data and handle Berry resources ---- + // First clear callbacks to release GC holds + for (int i = 0; i < 3; i++) { + if (wsserver_callbacks[i].active) { + if (be_isgcobj(&wsserver_callbacks[i].func)) { + ESP_LOGI(TAG, "Unmarking callback for event %d from GC protection", i); + be_gc_fix_set(vm, wsserver_callbacks[i].func.v.gc, bfalse); + } + } + } + + // ---- PHASE 2: External system operations ---- + // Close all client connections + int client_count = 0; + for (int i = 0; i < MAX_WS_CLIENTS; i++) { + if (ws_clients[i].active) { + client_count++; + + // Send close frame + httpd_ws_frame_t ws_pkt = {0}; + ws_pkt.type = HTTPD_WS_TYPE_CLOSE; + ws_pkt.len = 0; + + ESP_LOGI(TAG, "Sending close frame to client %d (socket: %d)", + i, ws_clients[i].sockfd); + + httpd_ws_send_frame_async(ws_server, ws_clients[i].sockfd, &ws_pkt); + + // Trigger session close + handle_client_disconnect(i); + } + } + + // Log how many clients were closed + ESP_LOGI(TAG, "Closed connections to %d client(s)", client_count); + + // Stop HTTP server + ESP_LOGI(TAG, "Stopping HTTP server"); + esp_err_t ret = httpd_stop(ws_server); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to stop server: %d (0x%x)", ret, ret); + } else { + ESP_LOGI(TAG, "HTTP server stopped successfully"); + } + + ws_server = NULL; + wsserver_running = false; + + be_pushbool(vm, (ret == ESP_OK)); + + int final_top = be_top(vm); + ESP_LOGI(TAG, "wsserver_stop: Final stack: %d (expected %d), success: %s", + final_top, 2, (ret == ESP_OK) ? "true" : "false"); + be_return (vm); +} + +// Deinitialize callbacks for VM shutdown +void be_wsserver_cb_deinit(bvm *vm) { + ESP_LOGI(TAG, "[DEINIT] Starting callback deinitialization for VM %p", vm); + + // Track all callbacks we're operating on + int count_active = 0; + int count_gc_protected = 0; + + // Clear all callbacks for this VM instance + for (int i = 0; i < 3; i++) { + if (wsserver_callbacks[i].vm == vm) { + const char* event_name = + i == WSSERVER_EVENT_CONNECT ? "connect" : + i == WSSERVER_EVENT_DISCONNECT ? "disconnect" : "message"; + + ESP_LOGI(TAG, "[DEINIT] Found active callback for event %d (%s)", i, event_name); + count_active++; + + // Check if it's GC protected + if (wsserver_callbacks[i].active && be_isgcobj(&wsserver_callbacks[i].func)) { + ESP_LOGI(TAG, "[DEINIT-GC] Callback for event %d is GC protected, unmarking", i); + count_gc_protected++; + + // Log the type of the function before unmarking + int func_type = wsserver_callbacks[i].func.type; + const char *type_str = + func_type == BE_FUNCTION ? "function" : + func_type == BE_CLOSURE ? "closure" : + func_type == BE_NTVCLOS ? "native_closure" : "other"; + ESP_LOGI(TAG, "[DEINIT-GC] Callback type: %s (%d)", type_str, func_type); + + // Unmark from garbage collection + be_gc_fix_set(vm, wsserver_callbacks[i].func.v.gc, bfalse); + ESP_LOGI(TAG, "[DEINIT-GC] Successfully unmarked callback for event %d", i); + } else if (wsserver_callbacks[i].active) { + ESP_LOGI(TAG, "[DEINIT-GC] Callback for event %d is not a GC object, no need to unmark", i); + } + + // Mark as inactive + wsserver_callbacks[i].active = false; + ESP_LOGI(TAG, "[DEINIT] Deactivated callback for event %d", i); + } + } + + ESP_LOGI(TAG, "[DEINIT] Completed callback deinitialization: %d active callbacks, %d GC protected", + count_active, count_gc_protected); +} + +// Handle disconnect event from the HTTP server +static void http_server_disconnect_handler(void* arg, int sockfd) { + ESP_LOGI(TAG, "HTTP server disconnect handler called for socket %d", sockfd); + + // Find the client slot for this socket + int client_slot = find_client_by_fd(sockfd); + if (client_slot < 0) { + ESP_LOGI(TAG, "No WebSocket client found for socket %d", sockfd); + return; + } + + ESP_LOGI(TAG, "Found WebSocket client %d for socket %d", client_slot, sockfd); + + // Queue disconnect event for processing in main task + if (httpserver_has_queue()) { + // Mark client as inactive BEFORE queuing the event + // This ensures the event processor knows it's a disconnect event + ws_clients[client_slot].active = false; + + bool queue_result = httpserver_queue_message(HTTP_MSG_WEBSOCKET, client_slot, + NULL, 0, NULL); + + if (queue_result) { + ESP_LOGI(TAG, "WebSocket disconnect event successfully queued from HTTP handler"); + } else { + ESP_LOGE(TAG, "Failed to queue WebSocket disconnect event from HTTP handler!"); + // Fallback to direct handling + handle_client_disconnect(client_slot); + } + } else { + ESP_LOGW(TAG, "No queue available for disconnect event - fallback to direct processing"); + handle_client_disconnect(client_slot); + } +} + +// Disconnect handler for httpd_register_uri_handler +esp_err_t websocket_disconnect_handler(httpd_handle_t hd, int sockfd) { + ESP_LOGI(TAG, "WebSocket disconnect handler called for socket %d", sockfd); + + int client_slot = find_client_by_fd(sockfd); + if (client_slot >= 0) { + // Call Berry disconnect callback directly + handle_client_disconnect(client_slot); + } + + return ESP_OK; +} + +// Module definition +/* @const_object_info_begin +module wsserver (scope: global) { + CONNECT, int(WSSERVER_EVENT_CONNECT) + DISCONNECT, int(WSSERVER_EVENT_DISCONNECT) + MESSAGE, int(WSSERVER_EVENT_MESSAGE) + + start, func(w_wsserver_start) + stop, func(w_wsserver_stop) + send, func(w_wsserver_send) + close, func(w_wsserver_close) + on, func(w_wsserver_on) + is_connected, func(w_wsserver_is_connected) + client_info, func(w_wsserver_client_info) + count_clients, func(w_wsserver_count_clients) + + // Constants for supported frame types + TEXT, int(HTTPD_WS_TYPE_TEXT) + BINARY, int(HTTPD_WS_TYPE_BINARY) + + // Constants for maximum clients + MAX_CLIENTS, int(MAX_WS_CLIENTS) +} +@const_object_info_end */ +#include "be_fixed_wsserver.h" + +#endif // USE_BERRY_WSSERVER