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