#include "WebServer.hpp" #include "SdCard.hpp" #include "WidgetManager.hpp" #include "KnxWorker.hpp" #include "Gui.hpp" #include "esp_log.h" #include "esp_system.h" #include #include #include static const char* TAG = "WebServer"; WebServer& WebServer::instance() { static WebServer instance; return instance; } WebServer::~WebServer() { stop(); } void WebServer::start() { if (server_ != nullptr) { ESP_LOGW(TAG, "Server already running"); return; } httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.uri_match_fn = httpd_uri_match_wildcard; config.stack_size = 8192; config.max_uri_handlers = 8; ESP_LOGI(TAG, "Starting HTTP server on port %d", config.server_port); if (httpd_start(&server_, &config) != ESP_OK) { ESP_LOGE(TAG, "Failed to start HTTP server"); return; } // GET / - Web page (loads index.html from SD card) httpd_uri_t root = { .uri = "/", .method = HTTP_GET, .handler = rootHandler, .user_ctx = nullptr }; httpd_register_uri_handler(server_, &root); // GET /webseite/* - Static files from SD card httpd_uri_t webseite = { .uri = "/webseite/*", .method = HTTP_GET, .handler = staticFileHandler, .user_ctx = nullptr }; httpd_register_uri_handler(server_, &webseite); // GET /images/* - Images from SD card httpd_uri_t images = { .uri = "/images/*", .method = HTTP_GET, .handler = imagesHandler, .user_ctx = nullptr }; httpd_register_uri_handler(server_, &images); // GET /api/config - Get full configuration httpd_uri_t getConfig = { .uri = "/api/config", .method = HTTP_GET, .handler = getConfigHandler, .user_ctx = nullptr }; httpd_register_uri_handler(server_, &getConfig); // POST /api/config - Update configuration (from editor) httpd_uri_t postConfig = { .uri = "/api/config", .method = HTTP_POST, .handler = postConfigHandler, .user_ctx = nullptr }; httpd_register_uri_handler(server_, &postConfig); // POST /api/save - Save and apply configuration httpd_uri_t postSave = { .uri = "/api/save", .method = HTTP_POST, .handler = postSaveHandler, .user_ctx = nullptr }; httpd_register_uri_handler(server_, &postSave); // POST /api/reset - Reset to defaults httpd_uri_t postReset = { .uri = "/api/reset", .method = HTTP_POST, .handler = postResetHandler, .user_ctx = nullptr }; httpd_register_uri_handler(server_, &postReset); // GET /api/knx/addresses - Get KNX group addresses httpd_uri_t getKnxAddresses = { .uri = "/api/knx/addresses", .method = HTTP_GET, .handler = getKnxAddressesHandler, .user_ctx = nullptr }; httpd_register_uri_handler(server_, &getKnxAddresses); // POST /api/usb-mode - Enable USB Mass Storage mode httpd_uri_t postUsbMode = { .uri = "/api/usb-mode", .method = HTTP_POST, .handler = postUsbModeHandler, .user_ctx = nullptr }; httpd_register_uri_handler(server_, &postUsbMode); // GET /api/status - Get system status httpd_uri_t getStatus = { .uri = "/api/status", .method = HTTP_GET, .handler = getStatusHandler, .user_ctx = nullptr }; httpd_register_uri_handler(server_, &getStatus); ESP_LOGI(TAG, "HTTP server started successfully"); } void WebServer::stop() { if (server_ != nullptr) { httpd_stop(server_); server_ = nullptr; ESP_LOGI(TAG, "HTTP server stopped"); } } const char* WebServer::getContentType(const char* filepath) { const char* ext = strrchr(filepath, '.'); if (!ext) return "application/octet-stream"; if (strcasecmp(ext, ".html") == 0 || strcasecmp(ext, ".htm") == 0) return "text/html; charset=utf-8"; if (strcasecmp(ext, ".css") == 0) return "text/css"; if (strcasecmp(ext, ".js") == 0) return "application/javascript"; if (strcasecmp(ext, ".json") == 0) return "application/json"; if (strcasecmp(ext, ".png") == 0) return "image/png"; if (strcasecmp(ext, ".jpg") == 0 || strcasecmp(ext, ".jpeg") == 0) return "image/jpeg"; if (strcasecmp(ext, ".gif") == 0) return "image/gif"; if (strcasecmp(ext, ".svg") == 0) return "image/svg+xml"; if (strcasecmp(ext, ".ico") == 0) return "image/x-icon"; return "application/octet-stream"; } esp_err_t WebServer::sendFile(httpd_req_t* req, const char* filepath) { struct stat st; if (stat(filepath, &st) != 0) { ESP_LOGE(TAG, "File not found: %s", filepath); httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File not found"); return ESP_FAIL; } FILE* f = fopen(filepath, "r"); if (!f) { 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; } httpd_resp_set_type(req, getContentType(filepath)); // Send file in chunks char* chunk = new char[1024]; if (!chunk) { fclose(f); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Out of memory"); return ESP_FAIL; } size_t read; while ((read = fread(chunk, 1, 1024, f)) > 0) { if (httpd_resp_send_chunk(req, chunk, read) != ESP_OK) { delete[] chunk; fclose(f); ESP_LOGE(TAG, "Failed to send chunk"); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Send failed"); return ESP_FAIL; } } delete[] chunk; fclose(f); // End chunked response httpd_resp_send_chunk(req, nullptr, 0); return ESP_OK; } esp_err_t WebServer::rootHandler(httpd_req_t* req) { if (!SdCard::instance().isMounted()) { httpd_resp_set_type(req, "text/html; charset=utf-8"); const char* errorPage = "

SD Card Error

" "

SD card not mounted. Please insert SD card with /webseite/index.html

"; httpd_resp_send(req, errorPage, strlen(errorPage)); return ESP_OK; } return sendFile(req, "/sdcard/webseite/index.html"); } esp_err_t WebServer::staticFileHandler(httpd_req_t* req) { // URI: /webseite/filename -> /sdcard/webseite/filename char filepath[CONFIG_HTTPD_MAX_URI_LEN + 8]; snprintf(filepath, sizeof(filepath), "/sdcard%.*s", (int)(sizeof(filepath) - 8), req->uri); return sendFile(req, filepath); } esp_err_t WebServer::imagesHandler(httpd_req_t* req) { // URI: /images/filename -> /sdcard/images/filename char filepath[CONFIG_HTTPD_MAX_URI_LEN + 8]; snprintf(filepath, sizeof(filepath), "/sdcard%.*s", (int)(sizeof(filepath) - 8), req->uri); return sendFile(req, filepath); } esp_err_t WebServer::getConfigHandler(httpd_req_t* req) { // Allocate buffer for JSON (widgets can be large) char* buf = new char[8192]; if (buf == nullptr) { httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Out of memory"); return ESP_FAIL; } WidgetManager::instance().getConfigJson(buf, 8192); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, buf, strlen(buf)); delete[] buf; return ESP_OK; } esp_err_t WebServer::postConfigHandler(httpd_req_t* req) { // Read request body int total_len = req->content_len; if (total_len > 8192) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Content too large"); return ESP_FAIL; } char* buf = new char[total_len + 1]; if (buf == nullptr) { httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Out of memory"); return ESP_FAIL; } int received = 0; while (received < total_len) { int ret = httpd_req_recv(req, buf + received, total_len - received); if (ret <= 0) { delete[] buf; httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Receive failed"); return ESP_FAIL; } received += ret; } buf[received] = '\0'; // Update config (does NOT apply yet) bool success = WidgetManager::instance().updateConfigFromJson(buf); delete[] buf; if (success) { httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, "{\"status\":\"ok\"}", 15); } else { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); } return success ? ESP_OK : ESP_FAIL; } esp_err_t WebServer::postSaveHandler(httpd_req_t* req) { ESP_LOGI(TAG, "Saving and applying configuration"); WidgetManager::instance().saveAndApply(); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, "{\"status\":\"ok\"}", 15); return ESP_OK; } esp_err_t WebServer::postResetHandler(httpd_req_t* req) { ESP_LOGI(TAG, "Resetting to defaults"); WidgetManager::instance().resetToDefaults(); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, "{\"status\":\"ok\"}", 15); return ESP_OK; } esp_err_t WebServer::getKnxAddressesHandler(httpd_req_t* req) { char buf[2048]; int pos = 0; pos += snprintf(buf + pos, sizeof(buf) - pos, "["); // Get group objects from KnxWorker KnxWorker& knxWorker = Gui::knxWorker; size_t count = knxWorker.getGroupObjectCount(); for (size_t i = 1; i <= count && pos < (int)sizeof(buf) - 100; i++) { KnxGroupObjectInfo info; if (knxWorker.getGroupObjectInfo(i, info)) { char addrStr[16]; KnxWorker::formatGroupAddress(info.groupAddress, addrStr, sizeof(addrStr)); if (i > 1) pos += snprintf(buf + pos, sizeof(buf) - pos, ","); pos += snprintf(buf + pos, sizeof(buf) - pos, "{\"index\":%d,\"addr\":%d,\"addrStr\":\"%s\",\"comm\":%s,\"read\":%s,\"write\":%s}", info.goIndex, info.groupAddress, addrStr, info.commFlag ? "true" : "false", info.readFlag ? "true" : "false", info.writeFlag ? "true" : "false" ); } } snprintf(buf + pos, sizeof(buf) - pos, "]"); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, buf, strlen(buf)); return ESP_OK; } esp_err_t WebServer::postUsbModeHandler(httpd_req_t* req) { ESP_LOGI(TAG, "Enabling USB Mass Storage mode"); bool success = SdCard::instance().enableUsbMsc(); if (success) { httpd_resp_set_type(req, "application/json"); const char* response = "{\"status\":\"ok\",\"message\":\"USB MSC enabled. Connect USB cable to access SD card. Reboot to return to normal mode.\"}"; httpd_resp_send(req, response, strlen(response)); } else { httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to enable USB MSC"); } return success ? ESP_OK : ESP_FAIL; } esp_err_t WebServer::getStatusHandler(httpd_req_t* req) { char buf[256]; snprintf(buf, sizeof(buf), "{\"sdMounted\":%s,\"usbMscActive\":%s}", SdCard::instance().isMounted() ? "true" : "false", SdCard::instance().isUsbMscActive() ? "true" : "false" ); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, buf, strlen(buf)); return ESP_OK; }