Tasmota/lib/libesp32/berry_tasmota/src/be_wsserver_lib.c

1327 lines
49 KiB
C

/*
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 <http://www.gnu.org/licenses/>.
*/
#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 <stdlib.h>
#include <stdbool.h>
#include <string.h>
// ESP-IDF includes
#include "esp_http_server.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_timer.h"
// Socket-related includes
#include <sys/socket.h>
#include <netinet/in.h>
#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, strings: weak) {
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