knxdisplay/main/WebServer.cpp
2026-01-22 22:16:48 +01:00

375 lines
11 KiB
C++

#include "WebServer.hpp"
#include "SdCard.hpp"
#include "WidgetManager.hpp"
#include "KnxWorker.hpp"
#include "Gui.hpp"
#include "esp_log.h"
#include "esp_system.h"
#include <cstring>
#include <cstdio>
#include <sys/stat.h>
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 = "<!DOCTYPE html><html><body><h1>SD Card Error</h1>"
"<p>SD card not mounted. Please insert SD card with /webseite/index.html</p></body></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;
}