mirror of
https://github.com/arendst/Tasmota.git
synced 2025-04-19 12:27:16 +00:00
Expose esp_http_server to Berry (#23206)
* Expose esp_http_server to Berry * Fix conditional build defines * Fix Berry returns, dangling pointer * Use correct Berry returns * Remove debug code * cleanup * add BERRY to conditionals to avoid confusion
This commit is contained in:
parent
4ed48feaa2
commit
929582b1af
@ -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),
|
||||
|
843
lib/libesp32/berry_tasmota/src/be_httpserver_lib.c
Normal file
843
lib/libesp32/berry_tasmota/src/be_httpserver_lib.c
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifdef USE_BERRY_HTTPSERVER
|
||||
|
||||
#include <stddef.h> // For NULL, size_t
|
||||
#include <stdbool.h> // For bool, true, false
|
||||
#include <string.h> // For string functions
|
||||
#include <stdlib.h> // 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
|
392
lib/libesp32/berry_tasmota/src/be_webfiles_lib.c
Normal file
392
lib/libesp32/berry_tasmota/src/be_webfiles_lib.c
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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
|
1326
lib/libesp32/berry_tasmota/src/be_wsserver_lib.c
Normal file
1326
lib/libesp32/berry_tasmota/src/be_wsserver_lib.c
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user