chore(format): initial formatting

This commit is contained in:
HendrikRauh 2026-03-05 23:05:35 +01:00
parent fa08fcfe65
commit 008c79852b
21 changed files with 1021 additions and 1082 deletions

View file

@ -21,9 +21,9 @@
## 📱 Implemented microcontrollers ## 📱 Implemented microcontrollers
- [x] Lolin S2 mini - [x] Lolin S2 mini
- [ ] ESP 32 WROOM - [ ] ESP 32 WROOM
- [ ] ESP 32 C3 - [ ] ESP 32 C3
> For other microcontrollers you may need to adjust the `platformio.ini` > For other microcontrollers you may need to adjust the `platformio.ini`

View file

@ -1,8 +1,7 @@
#pragma once #pragma once
#ifdef __cplusplus #ifdef __cplusplus
extern "C" extern "C" {
{
#endif #endif
#ifdef __cplusplus #ifdef __cplusplus

View file

@ -4,23 +4,22 @@
#include "esp_err.h" #include "esp_err.h"
#ifdef __cplusplus #ifdef __cplusplus
extern "C" extern "C" {
{
#endif #endif
/** /**
* @brief Initialize and mount LittleFS filesystem * @brief Initialize and mount LittleFS filesystem
* *
* @return ESP_OK on success, error code otherwise * @return ESP_OK on success, error code otherwise
*/ */
esp_err_t storage_init(void); esp_err_t storage_init(void);
/** /**
* @brief Get the mount point for the LittleFS filesystem * @brief Get the mount point for the LittleFS filesystem
* *
* @return Pointer to the mount point string (e.g., "/data") * @return Pointer to the mount point string (e.g., "/data")
*/ */
const char *storage_get_mount_point(void); const char *storage_get_mount_point(void);
#ifdef __cplusplus #ifdef __cplusplus
} }

View file

@ -7,8 +7,7 @@
static const char *TAG = "STORAGE"; static const char *TAG = "STORAGE";
static const char *LITTLEFS_MOUNT_POINT = "/data"; static const char *LITTLEFS_MOUNT_POINT = "/data";
esp_err_t storage_init(void) esp_err_t storage_init(void) {
{
esp_vfs_littlefs_conf_t conf = { esp_vfs_littlefs_conf_t conf = {
.base_path = LITTLEFS_MOUNT_POINT, .base_path = LITTLEFS_MOUNT_POINT,
.partition_label = "storage", .partition_label = "storage",
@ -18,18 +17,12 @@ esp_err_t storage_init(void)
esp_err_t ret = esp_vfs_littlefs_register(&conf); esp_err_t ret = esp_vfs_littlefs_register(&conf);
if (ret != ESP_OK) if (ret != ESP_OK) {
{ if (ret == ESP_FAIL) {
if (ret == ESP_FAIL)
{
ESP_LOGE(TAG, "Failed to mount LittleFS or format filesystem"); ESP_LOGE(TAG, "Failed to mount LittleFS or format filesystem");
} } else if (ret == ESP_ERR_INVALID_STATE) {
else if (ret == ESP_ERR_INVALID_STATE)
{
ESP_LOGE(TAG, "ESP_ERR_INVALID_STATE"); ESP_LOGE(TAG, "ESP_ERR_INVALID_STATE");
} } else {
else
{
ESP_LOGE(TAG, "Failed to initialize LittleFS: %s", esp_err_to_name(ret)); ESP_LOGE(TAG, "Failed to initialize LittleFS: %s", esp_err_to_name(ret));
} }
return ret; return ret;
@ -37,20 +30,14 @@ esp_err_t storage_init(void)
size_t total = 0, used = 0; size_t total = 0, used = 0;
ret = esp_littlefs_info(conf.partition_label, &total, &used); ret = esp_littlefs_info(conf.partition_label, &total, &used);
if (ret == ESP_OK) if (ret == ESP_OK) {
{
ESP_LOGI(TAG, "LittleFS mounted at %s. Total: %d bytes, Used: %d bytes", ESP_LOGI(TAG, "LittleFS mounted at %s. Total: %d bytes, Used: %d bytes",
LITTLEFS_MOUNT_POINT, total, used); LITTLEFS_MOUNT_POINT, total, used);
} } else {
else
{
ESP_LOGE(TAG, "Failed to get LittleFS information"); ESP_LOGE(TAG, "Failed to get LittleFS information");
} }
return ESP_OK; return ESP_OK;
} }
const char *storage_get_mount_point(void) const char *storage_get_mount_point(void) { return LITTLEFS_MOUNT_POINT; }
{
return LITTLEFS_MOUNT_POINT;
}

View file

@ -1,6 +1,7 @@
/** /**
* @file web_server.h * @file web_server.h
* @brief Simple HTTP web server component for ESP32 with async FreeRTOS support. * @brief Simple HTTP web server component for ESP32 with async FreeRTOS
* support.
*/ */
#pragma once #pragma once
@ -8,50 +9,50 @@
#include "esp_http_server.h" #include "esp_http_server.h"
#ifdef __cplusplus #ifdef __cplusplus
extern "C" extern "C" {
{
#endif #endif
/** /**
* @brief Web server configuration structure. * @brief Web server configuration structure.
*/ */
typedef struct typedef struct {
{ uint16_t port; ///< HTTP server port (default: 80)
uint16_t port; ///< HTTP server port (default: 80) size_t max_uri_handlers; ///< Maximum number of URI handlers
size_t max_uri_handlers; ///< Maximum number of URI handlers size_t stack_size; ///< FreeRTOS task stack size
size_t stack_size; ///< FreeRTOS task stack size UBaseType_t task_priority; ///< FreeRTOS task priority
UBaseType_t task_priority; ///< FreeRTOS task priority } webserver_config_t;
} webserver_config_t;
/** /**
* @brief Initialize and start the HTTP web server. * @brief Initialize and start the HTTP web server.
* *
* This function creates a FreeRTOS task that manages the HTTP server. * This function creates a FreeRTOS task that manages the HTTP server.
* It serves static files from the data/ folder and supports dynamic handler registration. * It serves static files from the data/ folder and supports dynamic handler
* * registration.
* @param config Configuration structure. If NULL, default values are used. *
* @return HTTP server handle on success, NULL on failure. * @param config Configuration structure. If NULL, default values are used.
*/ * @return HTTP server handle on success, NULL on failure.
httpd_handle_t webserver_start(const webserver_config_t *config); */
httpd_handle_t webserver_start(const webserver_config_t *config);
/** /**
* @brief Stop the web server and cleanup resources. * @brief Stop the web server and cleanup resources.
* *
* @param server HTTP server handle returned by webserver_start(). * @param server HTTP server handle returned by webserver_start().
* Safe to pass NULL. * Safe to pass NULL.
*/ */
void webserver_stop(httpd_handle_t server); void webserver_stop(httpd_handle_t server);
/** /**
* @brief Register a custom URI handler. * @brief Register a custom URI handler.
* *
* This allows dynamic registration of API endpoints and other custom handlers. * This allows dynamic registration of API endpoints and other custom handlers.
* *
* @param server HTTP server handle. * @param server HTTP server handle.
* @param uri_handler Pointer to httpd_uri_t structure. * @param uri_handler Pointer to httpd_uri_t structure.
* @return ESP_OK on success, error code otherwise. * @return ESP_OK on success, error code otherwise.
*/ */
esp_err_t webserver_register_handler(httpd_handle_t server, const httpd_uri_t *uri_handler); esp_err_t webserver_register_handler(httpd_handle_t server,
const httpd_uri_t *uri_handler);
#ifdef __cplusplus #ifdef __cplusplus
} }

View file

@ -3,12 +3,12 @@
#include "esp_err.h" #include "esp_err.h"
#ifdef __cplusplus #ifdef __cplusplus
extern "C" extern "C" {
{
#endif #endif
esp_err_t wifi_start_ap(const char *ssid, const char *password, uint8_t channel, uint8_t max_connections); esp_err_t wifi_start_ap(const char *ssid, const char *password, uint8_t channel,
void wifi_stop_ap(void); uint8_t max_connections);
void wifi_stop_ap(void);
#ifdef __cplusplus #ifdef __cplusplus
} }

View file

@ -25,240 +25,219 @@ static TaskHandle_t s_server_task_handle = NULL;
/** /**
* @brief Get MIME type based on file extension * @brief Get MIME type based on file extension
*/ */
static const char *get_mime_type(const char *filename) static const char *get_mime_type(const char *filename) {
{ const char *dot = strrchr(filename, '.');
const char *dot = strrchr(filename, '.'); if (!dot)
if (!dot)
return "application/octet-stream";
if (strcmp(dot, ".html") == 0)
return "text/html";
if (strcmp(dot, ".css") == 0)
return "text/css";
if (strcmp(dot, ".js") == 0)
return "application/javascript";
if (strcmp(dot, ".json") == 0)
return "application/json";
if (strcmp(dot, ".png") == 0)
return "image/png";
if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0)
return "image/jpeg";
if (strcmp(dot, ".gif") == 0)
return "image/gif";
if (strcmp(dot, ".svg") == 0)
return "image/svg+xml";
if (strcmp(dot, ".ico") == 0)
return "image/x-icon";
if (strcmp(dot, ".txt") == 0)
return "text/plain";
if (strcmp(dot, ".xml") == 0)
return "application/xml";
if (strcmp(dot, ".wav") == 0)
return "audio/wav";
if (strcmp(dot, ".mp3") == 0)
return "audio/mpeg";
return "application/octet-stream"; return "application/octet-stream";
if (strcmp(dot, ".html") == 0)
return "text/html";
if (strcmp(dot, ".css") == 0)
return "text/css";
if (strcmp(dot, ".js") == 0)
return "application/javascript";
if (strcmp(dot, ".json") == 0)
return "application/json";
if (strcmp(dot, ".png") == 0)
return "image/png";
if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0)
return "image/jpeg";
if (strcmp(dot, ".gif") == 0)
return "image/gif";
if (strcmp(dot, ".svg") == 0)
return "image/svg+xml";
if (strcmp(dot, ".ico") == 0)
return "image/x-icon";
if (strcmp(dot, ".txt") == 0)
return "text/plain";
if (strcmp(dot, ".xml") == 0)
return "application/xml";
if (strcmp(dot, ".wav") == 0)
return "audio/wav";
if (strcmp(dot, ".mp3") == 0)
return "audio/mpeg";
return "application/octet-stream";
} }
/** /**
* @brief HTTP handler for static files from LittleFS * @brief HTTP handler for static files from LittleFS
*/ */
static esp_err_t static_file_handler(httpd_req_t *req) static esp_err_t static_file_handler(httpd_req_t *req) {
{ // Build the file path
// Build the file path char filepath[1024];
char filepath[1024]; snprintf(filepath, sizeof(filepath), "%s%s", storage_get_mount_point(),
snprintf(filepath, sizeof(filepath), "%s%s", storage_get_mount_point(), req->uri); req->uri);
// Handle root path // Handle root path
if (strcmp(req->uri, "/") == 0) if (strcmp(req->uri, "/") == 0) {
{ snprintf(filepath, sizeof(filepath), "%s/index.html",
snprintf(filepath, sizeof(filepath), "%s/index.html", storage_get_mount_point()); storage_get_mount_point());
} }
FILE *f = fopen(filepath, "r"); FILE *f = fopen(filepath, "r");
if (!f) if (!f) {
{ ESP_LOGW(TAG, "File not found: %s", filepath);
ESP_LOGW(TAG, "File not found: %s", filepath); httpd_resp_send_404(req);
httpd_resp_send_404(req);
return ESP_OK;
}
// Get MIME type and set content type
const char *mime_type = get_mime_type(filepath);
httpd_resp_set_type(req, mime_type);
// Send file in chunks
char buf[1024];
size_t read_len;
while ((read_len = fread(buf, 1, sizeof(buf), f)) > 0)
{
if (httpd_resp_send_chunk(req, buf, read_len) != ESP_OK)
{
ESP_LOGW(TAG, "Failed to send data chunk for %s", filepath);
break;
}
}
fclose(f);
httpd_resp_send_chunk(req, NULL, 0); // Send end marker
return ESP_OK; return ESP_OK;
}
// Get MIME type and set content type
const char *mime_type = get_mime_type(filepath);
httpd_resp_set_type(req, mime_type);
// Send file in chunks
char buf[1024];
size_t read_len;
while ((read_len = fread(buf, 1, sizeof(buf), f)) > 0) {
if (httpd_resp_send_chunk(req, buf, read_len) != ESP_OK) {
ESP_LOGW(TAG, "Failed to send data chunk for %s", filepath);
break;
}
}
fclose(f);
httpd_resp_send_chunk(req, NULL, 0); // Send end marker
return ESP_OK;
} }
/** /**
* @brief HTTP handler for API health check (GET /api/health) * @brief HTTP handler for API health check (GET /api/health)
*/ */
static esp_err_t health_check_handler(httpd_req_t *req) static esp_err_t health_check_handler(httpd_req_t *req) {
{ httpd_resp_set_type(req, "application/json");
httpd_resp_set_type(req, "application/json"); httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); return ESP_OK;
return ESP_OK;
} }
/** /**
* @brief FreeRTOS task function for the HTTP server. * @brief FreeRTOS task function for the HTTP server.
* Allows non-blocking server operation and future extensibility. * Allows non-blocking server operation and future extensibility.
*/ */
static void webserver_task(void *arg) static void webserver_task(void *arg) {
{ (void)arg; // Unused parameter
(void)arg; // Unused parameter ESP_LOGI(TAG, "Web server task started");
ESP_LOGI(TAG, "Web server task started");
// Keep task alive - the server runs in the background // Keep task alive - the server runs in the background
while (s_server_handle != NULL) while (s_server_handle != NULL) {
{ vTaskDelay(pdMS_TO_TICKS(10000)); // 10 second check interval
vTaskDelay(pdMS_TO_TICKS(10000)); // 10 second check interval }
}
ESP_LOGI(TAG, "Web server task ending"); ESP_LOGI(TAG, "Web server task ending");
vTaskDelete(NULL); vTaskDelete(NULL);
} }
httpd_handle_t webserver_start(const webserver_config_t *config) httpd_handle_t webserver_start(const webserver_config_t *config) {
{ if (s_server_handle != NULL) {
if (s_server_handle != NULL) ESP_LOGW(TAG, "Web server already running");
{
ESP_LOGW(TAG, "Web server already running");
return s_server_handle;
}
// Initialize LittleFS
esp_err_t ret = storage_init();
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "Failed to initialize storage");
return NULL;
}
// Use provided config or defaults
uint16_t port = WEBSERVER_DEFAULT_PORT;
size_t max_handlers = WEBSERVER_DEFAULT_MAX_HANDLERS;
size_t stack_size = WEBSERVER_DEFAULT_STACK_SIZE;
UBaseType_t task_priority = WEBSERVER_DEFAULT_TASK_PRIORITY;
if (config)
{
port = config->port;
max_handlers = config->max_uri_handlers;
stack_size = config->stack_size;
task_priority = config->task_priority;
}
// Create HTTP server configuration
httpd_config_t http_config = HTTPD_DEFAULT_CONFIG();
http_config.server_port = port;
http_config.max_uri_handlers = max_handlers;
http_config.stack_size = stack_size;
http_config.uri_match_fn = httpd_uri_match_wildcard;
// Start HTTP server
ret = httpd_start(&s_server_handle, &http_config);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "Failed to start HTTP server: %s", esp_err_to_name(ret));
s_server_handle = NULL;
return NULL;
}
ESP_LOGI(TAG, "HTTP server started on port %d", port);
// Register default handlers
// Health check endpoint
httpd_uri_t health_uri = {
.uri = "/api/health",
.method = HTTP_GET,
.handler = health_check_handler,
.user_ctx = NULL,
};
httpd_register_uri_handler(s_server_handle, &health_uri);
// Wildcard handler for static files from LittleFS (must be last)
httpd_uri_t file_uri = {
.uri = "/*",
.method = HTTP_GET,
.handler = static_file_handler,
.user_ctx = NULL,
};
httpd_register_uri_handler(s_server_handle, &file_uri);
// Create FreeRTOS task for the server
// This allows other tasks to continue running and makes the server async-ready
BaseType_t task_ret = xTaskCreate(
webserver_task,
"webserver",
stack_size,
(void *)s_server_handle,
task_priority,
&s_server_task_handle);
if (task_ret != pdPASS)
{
ESP_LOGE(TAG, "Failed to create web server task");
httpd_stop(s_server_handle);
s_server_handle = NULL;
return NULL;
}
ESP_LOGI(TAG, "Web server initialized successfully");
return s_server_handle; return s_server_handle;
} }
void webserver_stop(httpd_handle_t server) // Initialize LittleFS
{ esp_err_t ret = storage_init();
if (server == NULL) if (ret != ESP_OK) {
{ ESP_LOGE(TAG, "Failed to initialize storage");
return; return NULL;
} }
httpd_stop(server); // Use provided config or defaults
uint16_t port = WEBSERVER_DEFAULT_PORT;
size_t max_handlers = WEBSERVER_DEFAULT_MAX_HANDLERS;
size_t stack_size = WEBSERVER_DEFAULT_STACK_SIZE;
UBaseType_t task_priority = WEBSERVER_DEFAULT_TASK_PRIORITY;
if (config) {
port = config->port;
max_handlers = config->max_uri_handlers;
stack_size = config->stack_size;
task_priority = config->task_priority;
}
// Create HTTP server configuration
httpd_config_t http_config = HTTPD_DEFAULT_CONFIG();
http_config.server_port = port;
http_config.max_uri_handlers = max_handlers;
http_config.stack_size = stack_size;
http_config.uri_match_fn = httpd_uri_match_wildcard;
// Start HTTP server
ret = httpd_start(&s_server_handle, &http_config);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to start HTTP server: %s", esp_err_to_name(ret));
s_server_handle = NULL; s_server_handle = NULL;
return NULL;
}
// Wait for task to finish ESP_LOGI(TAG, "HTTP server started on port %d", port);
if (s_server_task_handle != NULL)
{
vTaskDelay(pdMS_TO_TICKS(100));
s_server_task_handle = NULL;
}
ESP_LOGI(TAG, "Web server stopped"); // Register default handlers
// Health check endpoint
httpd_uri_t health_uri = {
.uri = "/api/health",
.method = HTTP_GET,
.handler = health_check_handler,
.user_ctx = NULL,
};
httpd_register_uri_handler(s_server_handle, &health_uri);
// Wildcard handler for static files from LittleFS (must be last)
httpd_uri_t file_uri = {
.uri = "/*",
.method = HTTP_GET,
.handler = static_file_handler,
.user_ctx = NULL,
};
httpd_register_uri_handler(s_server_handle, &file_uri);
// Create FreeRTOS task for the server
// This allows other tasks to continue running and makes the server
// async-ready
BaseType_t task_ret = xTaskCreate(webserver_task, "webserver", stack_size,
(void *)s_server_handle, task_priority,
&s_server_task_handle);
if (task_ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create web server task");
httpd_stop(s_server_handle);
s_server_handle = NULL;
return NULL;
}
ESP_LOGI(TAG, "Web server initialized successfully");
return s_server_handle;
} }
esp_err_t webserver_register_handler(httpd_handle_t server, const httpd_uri_t *uri_handler) void webserver_stop(httpd_handle_t server) {
{ if (server == NULL) {
if (server == NULL || uri_handler == NULL) return;
{ }
return ESP_ERR_INVALID_ARG;
}
esp_err_t ret = httpd_register_uri_handler(server, uri_handler); httpd_stop(server);
if (ret == ESP_OK) s_server_handle = NULL;
{
ESP_LOGI(TAG, "Registered handler: %s [%d]", uri_handler->uri, uri_handler->method);
}
else
{
ESP_LOGE(TAG, "Failed to register handler %s: %s", uri_handler->uri, esp_err_to_name(ret));
}
return ret; // Wait for task to finish
if (s_server_task_handle != NULL) {
vTaskDelay(pdMS_TO_TICKS(100));
s_server_task_handle = NULL;
}
ESP_LOGI(TAG, "Web server stopped");
}
esp_err_t webserver_register_handler(httpd_handle_t server,
const httpd_uri_t *uri_handler) {
if (server == NULL || uri_handler == NULL) {
return ESP_ERR_INVALID_ARG;
}
esp_err_t ret = httpd_register_uri_handler(server, uri_handler);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Registered handler: %s [%d]", uri_handler->uri,
uri_handler->method);
} else {
ESP_LOGE(TAG, "Failed to register handler %s: %s", uri_handler->uri,
esp_err_to_name(ret));
}
return ret;
} }

View file

@ -11,32 +11,28 @@
static const char *TAG = "WIFI"; static const char *TAG = "WIFI";
static bool s_wifi_started = false; static bool s_wifi_started = false;
esp_err_t wifi_start_ap(const char *ssid, const char *password, uint8_t channel, uint8_t max_connections) esp_err_t wifi_start_ap(const char *ssid, const char *password, uint8_t channel,
{ uint8_t max_connections) {
if (s_wifi_started) if (s_wifi_started) {
{
return ESP_OK; return ESP_OK;
} }
if (!ssid || strlen(ssid) == 0 || strlen(ssid) > 32) if (!ssid || strlen(ssid) == 0 || strlen(ssid) > 32) {
{
return ESP_ERR_INVALID_ARG; return ESP_ERR_INVALID_ARG;
} }
const bool has_password = password && strlen(password) > 0; const bool has_password = password && strlen(password) > 0;
if (has_password && strlen(password) < 8) if (has_password && strlen(password) < 8) {
{
return ESP_ERR_INVALID_ARG; return ESP_ERR_INVALID_ARG;
} }
esp_err_t err = nvs_flash_init(); esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) if (err == ESP_ERR_NVS_NO_FREE_PAGES ||
{ err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase()); ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init(); err = nvs_flash_init();
} }
if (err != ESP_OK) if (err != ESP_OK) {
{
return err; return err;
} }
@ -48,22 +44,24 @@ esp_err_t wifi_start_ap(const char *ssid, const char *password, uint8_t channel,
ESP_ERROR_CHECK(esp_wifi_init(&cfg)); ESP_ERROR_CHECK(esp_wifi_init(&cfg));
wifi_config_t wifi_config = { wifi_config_t wifi_config = {
.ap = { .ap =
.channel = channel, {
.max_connection = max_connections, .channel = channel,
.authmode = has_password ? WIFI_AUTH_WPA2_PSK : WIFI_AUTH_OPEN, .max_connection = max_connections,
.pmf_cfg = { .authmode = has_password ? WIFI_AUTH_WPA2_PSK : WIFI_AUTH_OPEN,
.required = false, .pmf_cfg =
{
.required = false,
},
}, },
},
}; };
strlcpy((char *)wifi_config.ap.ssid, ssid, sizeof(wifi_config.ap.ssid)); strlcpy((char *)wifi_config.ap.ssid, ssid, sizeof(wifi_config.ap.ssid));
wifi_config.ap.ssid_len = strlen(ssid); wifi_config.ap.ssid_len = strlen(ssid);
if (has_password) if (has_password) {
{ strlcpy((char *)wifi_config.ap.password, password,
strlcpy((char *)wifi_config.ap.password, password, sizeof(wifi_config.ap.password)); sizeof(wifi_config.ap.password));
} }
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP)); ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
@ -75,10 +73,8 @@ esp_err_t wifi_start_ap(const char *ssid, const char *password, uint8_t channel,
return ESP_OK; return ESP_OK;
} }
void wifi_stop_ap(void) void wifi_stop_ap(void) {
{ if (!s_wifi_started) {
if (!s_wifi_started)
{
return; return;
} }

View file

@ -1,328 +1,313 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Konfiguration</title> <title>Konfiguration</title>
<link rel="stylesheet" href="/style.css" /> <link rel="stylesheet" href="/style.css" />
<script type="module" src="/input-visibility.js" defer></script> <script type="module" src="/input-visibility.js" defer></script>
<script type="module" src="/loading-screen.js" defer></script> <script type="module" src="/loading-screen.js" defer></script>
<script type="module" src="/load-data.js" defer></script> <script type="module" src="/load-data.js" defer></script>
<script type="module" src="/networks.js" defer></script> <script type="module" src="/networks.js" defer></script>
<script type="module" src="/submit.js" defer></script> <script type="module" src="/submit.js" defer></script>
<script type="module" src="/reset.js" defer></script> <script type="module" src="/reset.js" defer></script>
<script type="module" src="/range-input.js" defer></script> <script type="module" src="/range-input.js" defer></script>
<script type="module" src="/status.js" defer></script> <script type="module" src="/status.js" defer></script>
<script type="module" src="/websocket.js" defer></script> <script type="module" src="/websocket.js" defer></script>
</head> </head>
<body> <body>
<main> <main>
<section class="loading-screen"> <section class="loading-screen">
<div class="spinner-container"> <div class="spinner-container">
<!-- h2 is filled dynamically --> <!-- h2 is filled dynamically -->
<h2></h2> <h2></h2>
<div class="spinner"></div> <div class="spinner"></div>
<button <button
class="reload" class="reload"
type="button" type="button"
onclick="window.location.reload()" onclick="window.location.reload()"
> >
Seite neu laden Seite neu laden
</button> </button>
</div> </div>
</section> </section>
<section class="content hidden"> <section class="content hidden">
<div class="status"> <div class="status">
<!-- placeholder for wifi icon --> <!-- placeholder for wifi icon -->
<img class="connection-icon" src="" alt="" /> <img class="connection-icon" src="" alt="" />
<span>Temp.: <span class="cpu-temp"></span> °C</span> <span>Temp.: <span class="cpu-temp"></span> °C</span>
<span>Heap: <span class="heap-percentage"></span> %</span> <span>Heap: <span class="heap-percentage"></span> %</span>
<span>PSRAM: <span class="psram-percentage"></span> %</span> <span>PSRAM: <span class="psram-percentage"></span> %</span>
<span>Uptime: <span class="uptime"></span></span> <span>Uptime: <span class="uptime"></span></span>
<button type="button" class="expand-status icon-button"> <button type="button" class="expand-status icon-button">
<img src="/icons/open.svg" alt="Mehr" /> <img src="/icons/open.svg" alt="Mehr" />
</button> </button>
</div> </div>
<dialog class="dialog-status"> <dialog class="dialog-status">
<div class="dialog-header"> <div class="dialog-header">
<h2 class="model"></h2> <h2 class="model"></h2>
<form method="dialog"> <form method="dialog">
<button type="submit" class="icon-button"> <button type="submit" class="icon-button">
<img src="/icons/close.svg" alt="Schließen" /> <img src="/icons/close.svg" alt="Schließen" />
</button> </button>
</form> </form>
</div> </div>
<div class="dialog-status-content"> <div class="dialog-status-content">
<div class="card"> <div class="card">
<span>Signalstärke</span> <span>Signalstärke</span>
<span class="centered-vertical"> <span class="centered-vertical">
<img <img class="connection-icon small" src="" alt="" />
class="connection-icon small" <span><span class="rssi"></span> dBm</span>
src="" </span>
alt="" </div>
/>
<span><span class="rssi"></span> dBm</span>
</span>
</div>
<div class="card"> <div class="card">
<span>Uptime</span> <span>Uptime</span>
<span class="uptime"></span> <span class="uptime"></span>
</div> </div>
<div class="card"> <div class="card">
<span>CPU-Temperatur</span> <span>CPU-Temperatur</span>
<span><span class="cpu-temp"></span> °C</span> <span><span class="cpu-temp"></span> °C</span>
</div> </div>
<div class="card"> <div class="card">
<span>CPU Cycle Count</span> <span>CPU Cycle Count</span>
<span class="cpu-cycle-count"></span> <span class="cpu-cycle-count"></span>
</div> </div>
<div class="card"> <div class="card">
<span>Heap</span> <span>Heap</span>
<span><span class="heap-percentage"></span> %</span> <span><span class="heap-percentage"></span> %</span>
<span <span
><span class="heap-used"></span> / ><span class="heap-used"></span> /
<span class="heap-total"></span <span class="heap-total"></span
></span> ></span>
</div> </div>
<div class="card"> <div class="card">
<span>PSRAM</span> <span>PSRAM</span>
<span <span><span class="psram-percentage"></span> %</span>
><span class="psram-percentage"></span> %</span <span
> ><span class="psram-used"></span> /
<span <span class="psram-total"></span
><span class="psram-used"></span> / ></span>
<span class="psram-total"></span </div>
></span>
</div>
<div class="card"> <div class="card">
<span>CPU-Taktfrequenz</span> <span>CPU-Taktfrequenz</span>
<span><span class="cpu-freq"></span> MHz</span> <span><span class="cpu-freq"></span> MHz</span>
</div> </div>
<div class="card"> <div class="card">
<span>MAC-Adresse</span> <span>MAC-Adresse</span>
<span class="mac"></span> <span class="mac"></span>
</div> </div>
<div class="card"> <div class="card">
<span>Script-Hash</span> <span>Script-Hash</span>
<span class="hash"></span> <span class="hash"></span>
</div> </div>
<div class="card"> <div class="card">
<span>SDK-Version</span> <span>SDK-Version</span>
<span class="sdk-version"></span> <span class="sdk-version"></span>
</div> </div>
</div> </div>
</dialog> </dialog>
<form class="config"> <form class="config">
<h1>Konfiguration</h1> <h1>Konfiguration</h1>
<fieldset> <fieldset>
<legend>Verbindung</legend> <legend>Verbindung</legend>
<label> <label>
<span>IP-Zuweisung:</span> <span>IP-Zuweisung:</span>
<select <select
name="ip-method" name="ip-method"
id="input-ip-method" id="input-ip-method"
title="IP-" title="IP-"
required required
> >
<option value="0">Statisch</option> <option value="0">Statisch</option>
<option value="1">DHCP</option> <option value="1">DHCP</option>
</select> </select>
</label> </label>
<div data-field="input-ip-method" data-values="0"> <div data-field="input-ip-method" data-values="0">
<label> <label>
<span>IP-Adresse:</span> <span>IP-Adresse:</span>
<input <input
type="text" type="text"
name="ip" name="ip"
id="input-ip" id="input-ip"
placeholder="IP-Adresse" placeholder="IP-Adresse"
required required
/> />
</label> </label>
<label> <label>
<span>Subnetzmaske:</span> <span>Subnetzmaske:</span>
<input <input
type="text" type="text"
name="subnet" name="subnet"
id="input-subnet" id="input-subnet"
placeholder="Subnetzmaske" placeholder="Subnetzmaske"
required required
/> />
</label> </label>
<label> <label>
<span>Gateway:</span> <span>Gateway:</span>
<input <input
type="text" type="text"
name="gateway" name="gateway"
id="input-gateway" id="input-gateway"
placeholder="Gateway" placeholder="Gateway"
required required
/> />
</label> </label>
</div> </div>
<label> <label>
<span>Verbindungsmethode:</span> <span>Verbindungsmethode:</span>
<select <select
name="connection" name="connection"
id="input-connection" id="input-connection"
title="Verbindung" title="Verbindung"
required required
> >
<option value="0">WiFi-Station</option> <option value="0">WiFi-Station</option>
<option value="1">WiFi-AccessPoint</option> <option value="1">WiFi-AccessPoint</option>
<option value="2">Ethernet</option> <option value="2">Ethernet</option>
</select> </select>
</label> </label>
<div data-field="input-connection" data-values="1"> <div data-field="input-connection" data-values="1">
<label> <label>
<span>SSID:</span> <span>SSID:</span>
<input <input
type="text" type="text"
name="ssid" name="ssid"
id="input-ssid" id="input-ssid"
placeholder="SSID" placeholder="SSID"
required required
/> />
</label> </label>
</div> </div>
<div data-field="input-connection" data-values="0"> <div data-field="input-connection" data-values="0">
<label> <label>
<span>Netzwerk:</span> <span>Netzwerk:</span>
<select <select
name="ssid" name="ssid"
id="select-network" id="select-network"
title="Netzwerk" title="Netzwerk"
required required
></select> ></select>
<button <button type="button" id="refresh-networks" class="icon-button">
type="button" <img src="/icons/refresh.svg" alt="Neu laden" />
id="refresh-networks" </button>
class="icon-button" </label>
> </div>
<img <div data-field="input-connection" data-values="0|1">
src="/icons/refresh.svg" <label>
alt="Neu laden" <span>Password:</span>
/> <input
</button> type="password"
</label> name="password"
</div> id="input-password"
<div data-field="input-connection" data-values="0|1"> placeholder="Passwort"
<label> />
<span>Password:</span> </label>
<input </div>
type="password" </fieldset>
name="password" <fieldset>
id="input-password" <legend>Input/Output 1</legend>
placeholder="Passwort" <label class="switch">
/> <span>Output</span>
</label> <input
</div> type="checkbox"
</fieldset> name="direction-1"
<fieldset> id="input-direction-1"
<legend>Input/Output 1</legend> data-value-not-checked="0"
<label class="switch"> data-value-checked="1"
<span>Output</span> />
<input <span class="slider"></span>
type="checkbox" <span>Input</span>
name="direction-1" </label>
id="input-direction-1" <label>
data-value-not-checked="0" ArtNet-Universe:
data-value-checked="1" <input
/> type="number"
<span class="slider"></span> name="universe-1"
<span>Input</span> id="universe-1"
</label> placeholder="Universe"
<label> min="0"
ArtNet-Universe: max="15"
<input />
type="number" </label>
name="universe-1" </fieldset>
id="universe-1" <fieldset>
placeholder="Universe" <legend>Input/Output 2</legend>
min="0" <label class="switch">
max="15" <span>Output</span>
/> <input
</label> type="checkbox"
</fieldset> name="direction-2"
<fieldset> id="input-direction-2"
<legend>Input/Output 2</legend> data-value-not-checked="0"
<label class="switch"> data-value-checked="1"
<span>Output</span> />
<input <span class="slider"></span>
type="checkbox" <span>Input</span>
name="direction-2" </label>
id="input-direction-2" <label>
data-value-not-checked="0" ArtNet-Universe:
data-value-checked="1" <input
/> type="number"
<span class="slider"></span> name="universe-2"
<span>Input</span> id="universe-2"
</label> placeholder="Universe"
<label> min="0"
ArtNet-Universe: max="15"
<input />
type="number" </label>
name="universe-2" </fieldset>
id="universe-2" <fieldset>
placeholder="Universe" <legend>Sonstiges</legend>
min="0" <label>
max="15" LED-Helligkeit
/> <div>
</label> <input
</fieldset> type="range"
<fieldset> name="led-brightness"
<legend>Sonstiges</legend> id="led-brightness"
<label> min="0"
LED-Helligkeit max="255"
<div> class="range"
<input />
type="range" <span class="range-value"></span>
name="led-brightness" </div>
id="led-brightness" </label>
min="0" <label>
max="255" <span>Aktion bei Knopfdruck:</span>
class="range" <select
/> name="button-action"
<span class="range-value"></span> id="input-button-action"
</div> title="Aktion bei Knopfdruck"
</label> required
<label> >
<span>Aktion bei Knopfdruck:</span> <option value="0">Nichts</option>
<select <option value="1">Konfiguration zurücksetzen</option>
name="button-action" <option value="2">Neustart</option>
id="input-button-action" </select>
title="Aktion bei Knopfdruck" </label>
required </fieldset>
>
<option value="0">Nichts</option>
<option value="1">
Konfiguration zurücksetzen
</option>
<option value="2">Neustart</option>
</select>
</label>
</fieldset>
<div class="buttons"> <div class="buttons">
<button type="reset">Zurücksetzen</button> <button type="reset">Zurücksetzen</button>
<button type="submit">Speichern</button> <button type="submit">Speichern</button>
</div> </div>
</form> </form>
</section> </section>
</main> </main>
</body> </body>
</html> </html>

View file

@ -4,20 +4,20 @@ const dynamicInputs = form.querySelectorAll("[data-field][data-values]");
document.addEventListener("change", updateVisibility); document.addEventListener("change", updateVisibility);
function updateVisibility() { function updateVisibility() {
dynamicInputs.forEach((element) => { dynamicInputs.forEach(element => {
const input = form.querySelector(`#${element.dataset.field}`); const input = form.querySelector(`#${element.dataset.field}`);
if (element.dataset.values.split("|").includes(input.value)) { if (element.dataset.values.split("|").includes(input.value)) {
element.classList.remove("hidden"); element.classList.remove("hidden");
element element
.querySelectorAll("input, select, button, textarea") .querySelectorAll("input, select, button, textarea")
.forEach((childInput) => (childInput.disabled = false)); .forEach(childInput => (childInput.disabled = false));
} else { } else {
element.classList.add("hidden"); element.classList.add("hidden");
element element
.querySelectorAll("input, select, button, textarea") .querySelectorAll("input, select, button, textarea")
.forEach((childInput) => (childInput.disabled = true)); .forEach(childInput => (childInput.disabled = true));
} }
}); });
} }
updateVisibility(); updateVisibility();

View file

@ -1,7 +1,7 @@
import { import {
showLoadingScreen, showLoadingScreen,
showError, showError,
hideLoadingScreen, hideLoadingScreen,
} from "./loading-screen.js"; } from "./loading-screen.js";
const form = document.querySelector("form.config"); const form = document.querySelector("form.config");
@ -9,46 +9,46 @@ const form = document.querySelector("form.config");
export let data = {}; export let data = {};
export async function loadData(timeout = null) { export async function loadData(timeout = null) {
const req = await fetch("/config", { const req = await fetch("/config", {
method: "GET", method: "GET",
signal: timeout !== null ? AbortSignal.timeout(timeout) : undefined, signal: timeout !== null ? AbortSignal.timeout(timeout) : undefined,
}); });
if (!req.ok) { if (!req.ok) {
throw new Error(`Response status: ${req.status}`); throw new Error(`Response status: ${req.status}`);
} }
const json = await req.json(); const json = await req.json();
console.log(json); console.log(json);
return json; return json;
} }
export function writeDataToInput(data) { export function writeDataToInput(data) {
console.log("write data"); console.log("write data");
for (const [key, value] of Object.entries(data)) { for (const [key, value] of Object.entries(data)) {
const element = document.querySelector(`[name=${key}]`); const element = document.querySelector(`[name=${key}]`);
console.log(key, element); console.log(key, element);
if (element.type === "checkbox") { if (element.type === "checkbox") {
element.checked = value; element.checked = value;
} else { } else {
element.value = value; element.value = value;
}
if (element.type === "range") {
// update text next to the slider by sending an event
element.dispatchEvent(new Event("input", { bubbles: true }));
}
} }
// send "change" event
form.dispatchEvent(new Event("change", { bubbles: true })); if (element.type === "range") {
// update text next to the slider by sending an event
element.dispatchEvent(new Event("input", { bubbles: true }));
}
}
// send "change" event
form.dispatchEvent(new Event("change", { bubbles: true }));
} }
showLoadingScreen("Konfiguration wird geladen..."); showLoadingScreen("Konfiguration wird geladen...");
try { try {
data = await loadData(); data = await loadData();
hideLoadingScreen(); hideLoadingScreen();
writeDataToInput(data); writeDataToInput(data);
} catch (error) { } catch (error) {
console.log(error.message); console.log(error.message);
showError("Die Konfiguration konnte nicht geladen werden."); showError("Die Konfiguration konnte nicht geladen werden.");
} }

View file

@ -5,36 +5,36 @@ const spinner = loadingScreen.querySelector(".spinner");
const reloadBtn = loadingScreen.querySelector(".reload"); const reloadBtn = loadingScreen.querySelector(".reload");
export function showLoadingScreen(msg) { export function showLoadingScreen(msg) {
hide(content, reloadBtn); hide(content, reloadBtn);
show(loadingScreen, spinner); show(loadingScreen, spinner);
loadingMsg.classList.remove("error"); loadingMsg.classList.remove("error");
loadingMsg.textContent = msg; loadingMsg.textContent = msg;
} }
export function showError(msg) { export function showError(msg) {
showLoadingScreen(msg); showLoadingScreen(msg);
loadingMsg.innerHTML += loadingMsg.innerHTML +=
"<br/>Stelle sicher, dass du mit dem DMX-Interface verbunden bist und die IP-Adresse stimmt."; "<br/>Stelle sicher, dass du mit dem DMX-Interface verbunden bist und die IP-Adresse stimmt.";
show(reloadBtn); show(reloadBtn);
hide(spinner); hide(spinner);
loadingMsg.classList.add("error"); loadingMsg.classList.add("error");
} }
export function hideLoadingScreen() { export function hideLoadingScreen() {
hide(loadingScreen, reloadBtn); hide(loadingScreen, reloadBtn);
show(content); show(content);
loadingMsg.classList.remove("error"); loadingMsg.classList.remove("error");
loadingMsg.textContent = ""; loadingMsg.textContent = "";
} }
function show(...elements) { function show(...elements) {
for (const element of elements) { for (const element of elements) {
element.classList.remove("hidden"); element.classList.remove("hidden");
} }
} }
function hide(...elements) { function hide(...elements) {
for (const element of elements) { for (const element of elements) {
element.classList.add("hidden"); element.classList.add("hidden");
} }
} }

View file

@ -7,67 +7,67 @@ const refreshIcon = refreshButton.querySelector("img");
let isLoading = false; let isLoading = false;
refreshButton.addEventListener("click", async () => { refreshButton.addEventListener("click", async () => {
// check if interface is in WiFi-AccessPoint mode // check if interface is in WiFi-AccessPoint mode
if (data.connection == 1) { if (data.connection == 1) {
alert( alert(
"Beim WLAN-Scan wird die Verbindung hardwarebedingt kurzzeitig" + "Beim WLAN-Scan wird die Verbindung hardwarebedingt kurzzeitig" +
"unterbrochen.\n" + "unterbrochen.\n" +
"Möglicherweise muss das Interface neu verbunden werden." "Möglicherweise muss das Interface neu verbunden werden."
); );
} }
updateNetworks(); updateNetworks();
}); });
// check if connected via WiFi-Station // check if connected via WiFi-Station
if (data.connection === 0) { if (data.connection === 0) {
// show currently connected WiFi // show currently connected WiFi
insertNetworks([data.ssid]); insertNetworks([data.ssid]);
} }
function insertNetworks(networks) { function insertNetworks(networks) {
networkDropdown.textContent = ""; // clear dropdown networkDropdown.textContent = ""; // clear dropdown
for (const ssid of networks) { for (const ssid of networks) {
const option = document.createElement("option"); const option = document.createElement("option");
option.value = ssid; option.value = ssid;
option.innerText = ssid; option.innerText = ssid;
networkDropdown.appendChild(option); networkDropdown.appendChild(option);
} }
} }
async function loadNetworks() { async function loadNetworks() {
if (isLoading) return; if (isLoading) return;
isLoading = true; isLoading = true;
refreshButton.classList.remove("error-bg"); refreshButton.classList.remove("error-bg");
refreshIcon.classList.add("spinning"); refreshIcon.classList.add("spinning");
try { try {
const res = await fetch("/networks", { const res = await fetch("/networks", {
method: "GET", method: "GET",
}); });
if (!res.ok) { if (!res.ok) {
throw Error(`response status: ${res.status}`); throw Error(`response status: ${res.status}`);
}
const networks = await res.json();
refreshIcon.classList.remove("spinning");
isLoading = false;
// remove duplicate values
return Array.from(new Set(networks));
} catch (e) {
refreshIcon.classList.remove("spinning");
refreshButton.classList.add("error-bg");
isLoading = false;
return [];
} }
const networks = await res.json();
refreshIcon.classList.remove("spinning");
isLoading = false;
// remove duplicate values
return Array.from(new Set(networks));
} catch (e) {
refreshIcon.classList.remove("spinning");
refreshButton.classList.add("error-bg");
isLoading = false;
return [];
}
} }
async function updateNetworks() { async function updateNetworks() {
const networks = await loadNetworks(); const networks = await loadNetworks();
if (networks) { if (networks) {
insertNetworks(["", ...networks]); insertNetworks(["", ...networks]);
} }
} }

View file

@ -1,14 +1,14 @@
document.querySelector("form.config").addEventListener("input", (event) => { document.querySelector("form.config").addEventListener("input", event => {
if (event.target.classList.contains("range")) { if (event.target.classList.contains("range")) {
updateValue(event.target); updateValue(event.target);
} }
}); });
function updateValue(slider) { function updateValue(slider) {
const percentage = Math.round((slider.value / slider.max) * 100); const percentage = Math.round((slider.value / slider.max) * 100);
slider.nextElementSibling.innerText = `${percentage}%`; slider.nextElementSibling.innerText = `${percentage}%`;
} }
document.querySelectorAll("input[type='range'].range").forEach((element) => { document.querySelectorAll("input[type='range'].range").forEach(element => {
updateValue(element); updateValue(element);
}); });

View file

@ -2,15 +2,15 @@ import { updateConfig } from "/submit.js";
const form = document.querySelector("form.config"); const form = document.querySelector("form.config");
form.addEventListener("reset", async (event) => { form.addEventListener("reset", async event => {
event.preventDefault(); event.preventDefault();
const ok = confirm( const ok = confirm(
"Sicher, dass du alle Einstellungen zurücksetzen möchtest?" "Sicher, dass du alle Einstellungen zurücksetzen möchtest?"
); );
if (ok) { if (ok) {
updateConfig({ updateConfig({
method: "DELETE", method: "DELETE",
}); });
} }
}); });

View file

@ -5,108 +5,105 @@ const statusDialog = document.querySelector(".dialog-status");
const expandButton = document.querySelector(".expand-status"); const expandButton = document.querySelector(".expand-status");
expandButton.addEventListener("click", () => { expandButton.addEventListener("click", () => {
statusDialog.showModal(); statusDialog.showModal();
}); });
registerCallback("status", setStatus); registerCallback("status", setStatus);
initWebSocket(); initWebSocket();
function setStatus(status) { function setStatus(status) {
setValue("model", status.chip.model); setValue("model", status.chip.model);
setValue("mac", formatMac(status.chip.mac)); setValue("mac", formatMac(status.chip.mac));
setValue("sdk-version", status.sdkVersion); setValue("sdk-version", status.sdkVersion);
setValue("rssi", status.connection.signalStrength); setValue("rssi", status.connection.signalStrength);
const icon = selectConnectionIcon(status.connection.signalStrength); const icon = selectConnectionIcon(status.connection.signalStrength);
document.querySelectorAll(".connection-icon").forEach((img) => { document.querySelectorAll(".connection-icon").forEach(img => {
img.src = `/icons/${icon}`; img.src = `/icons/${icon}`;
}); });
setValue("cpu-freq", status.chip.cpuFreqMHz); setValue("cpu-freq", status.chip.cpuFreqMHz);
setValue("cpu-cycle-count", status.chip.cycleCount); setValue("cpu-cycle-count", status.chip.cycleCount);
setValue("cpu-temp", status.chip.tempC); setValue("cpu-temp", status.chip.tempC);
const usedHeap = status.heap.total - status.heap.free; const usedHeap = status.heap.total - status.heap.free;
setValue("heap-used", formatBytes(usedHeap)); setValue("heap-used", formatBytes(usedHeap));
setValue("heap-total", formatBytes(status.heap.total)); setValue("heap-total", formatBytes(status.heap.total));
setValue( setValue("heap-percentage", Math.round((usedHeap / status.heap.total) * 100));
"heap-percentage",
Math.round((usedHeap / status.heap.total) * 100)
);
const usedPsram = status.psram.total - status.psram.free; const usedPsram = status.psram.total - status.psram.free;
setValue("psram-used", formatBytes(usedPsram)); setValue("psram-used", formatBytes(usedPsram));
setValue("psram-total", formatBytes(status.psram.total)); setValue("psram-total", formatBytes(status.psram.total));
setValue( setValue(
"psram-percentage", "psram-percentage",
Math.round((usedPsram / status.psram.total) * 100) Math.round((usedPsram / status.psram.total) * 100)
); );
setValue("uptime", parseDuration(status.uptime)); setValue("uptime", parseDuration(status.uptime));
setValue("hash", parseHash(status.sketch.md5)); setValue("hash", parseHash(status.sketch.md5));
} }
function setValue(className, value) { function setValue(className, value) {
document.querySelectorAll("." + className).forEach((element) => { document.querySelectorAll("." + className).forEach(element => {
element.innerText = value; element.innerText = value;
}); });
} }
function parseDuration(ms) { function parseDuration(ms) {
const date = new Date(ms); const date = new Date(ms);
const time = const time =
date.getUTCHours().toString().padStart(2, "0") + date.getUTCHours().toString().padStart(2, "0") +
":" + ":" +
date.getUTCMinutes().toString().padStart(2, "0") + date.getUTCMinutes().toString().padStart(2, "0") +
" h"; " h";
const days = Math.floor(date.getTime() / (1000 * 60 * 60 * 24)); const days = Math.floor(date.getTime() / (1000 * 60 * 60 * 24));
return days !== 0 ? `${days} Tage, ${time}` : time; return days !== 0 ? `${days} Tage, ${time}` : time;
} }
function parseHash(hash) { function parseHash(hash) {
return hash.toUpperCase().substring(0, 16); return hash.toUpperCase().substring(0, 16);
} }
function formatBytes(bytes) { function formatBytes(bytes) {
const units = ["Bytes", "KB", "MB", "GB"]; const units = ["Bytes", "KB", "MB", "GB"];
let value = bytes; let value = bytes;
let index = 0; let index = 0;
while (value >= 1000) { while (value >= 1000) {
value /= 1000; value /= 1000;
index++; index++;
} }
return `${Math.round(value * 10) / 10} ${units[index]}`; return `${Math.round(value * 10) / 10} ${units[index]}`;
} }
function formatMac(decimalMac) { function formatMac(decimalMac) {
const octets = decimalMac.toString(16).toUpperCase().match(/../g) || []; const octets = decimalMac.toString(16).toUpperCase().match(/../g) || [];
return octets.reverse().join(":"); return octets.reverse().join(":");
} }
function selectConnectionIcon(signalStrength) { function selectConnectionIcon(signalStrength) {
// access point // access point
if (data.connection == 1) { if (data.connection == 1) {
return "hotspot.svg"; return "hotspot.svg";
} }
// ethernet // ethernet
if (data.connection == 2) { if (data.connection == 2) {
return "lan.svg"; return "lan.svg";
} }
// station // station
if (signalStrength >= -50) { if (signalStrength >= -50) {
return "signal4.svg"; return "signal4.svg";
} }
if (signalStrength >= -60) { if (signalStrength >= -60) {
return "signal3.svg"; return "signal3.svg";
} }
if (signalStrength >= -70) { if (signalStrength >= -70) {
return "signal2.svg"; return "signal2.svg";
} }
return "signal1.svg"; return "signal1.svg";
} }

View file

@ -1,330 +1,330 @@
:root { :root {
--color-primary: #087e8b; --color-primary: #087e8b;
--color-on-primary: white; --color-on-primary: white;
--color-background: #222; --color-background: #222;
--color-surface: #333; --color-surface: #333;
--color-danger: #fa2b58; --color-danger: #fa2b58;
--appended-item-size: 2.5rem; --appended-item-size: 2.5rem;
color-scheme: dark; color-scheme: dark;
} }
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
background: linear-gradient(to left, #065760, black, black, #065760); background: linear-gradient(to left, #065760, black, black, #065760);
color: white; color: white;
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
overflow: hidden; overflow: hidden;
} }
main { main {
background-color: var(--color-background); background-color: var(--color-background);
max-width: 700px; max-width: 700px;
padding: 8px max(5%, 8px); padding: 8px max(5%, 8px);
margin: 0 auto; margin: 0 auto;
height: 100vh; height: 100vh;
overflow: auto; overflow: auto;
} }
h1 { h1 {
text-align: center; text-align: center;
} }
form > * { form > * {
margin-bottom: 16px; margin-bottom: 16px;
} }
fieldset { fieldset {
border-radius: 8px; border-radius: 8px;
border-color: white; border-color: white;
} }
label { label {
display: block; display: block;
display: flex; display: flex;
gap: 8px; gap: 8px;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
} }
label span { label span {
flex-grow: 1; flex-grow: 1;
} }
input, input,
select, select,
label div { label div {
width: clamp(200px, 100%, 400px); width: clamp(200px, 100%, 400px);
} }
input, input,
select { select {
background-color: var(--color-background); background-color: var(--color-background);
color: white; color: white;
border: 1px solid white; border: 1px solid white;
border-radius: 8px; border-radius: 8px;
padding: 8px; padding: 8px;
box-sizing: border-box; box-sizing: border-box;
} }
input:focus, input:focus,
select:focus { select:focus {
outline: none; outline: none;
border-color: var(--color-primary); border-color: var(--color-primary);
} }
select:has(+ .icon-button), select:has(+ .icon-button),
label div input[type="range"] { label div input[type="range"] {
width: calc(clamp(200px, 100%, 400px) - var(--appended-item-size) - 8px); width: calc(clamp(200px, 100%, 400px) - var(--appended-item-size) - 8px);
} }
input[type="range"] { input[type="range"] {
accent-color: var(--color-primary); accent-color: var(--color-primary);
} }
button { button {
border: none; border: none;
inset: none; inset: none;
border-radius: 8px; border-radius: 8px;
background-color: var(--color-primary); background-color: var(--color-primary);
color: var(--color-on-primary); color: var(--color-on-primary);
padding: 8px 16px; padding: 8px 16px;
} }
button[type="reset"] { button[type="reset"] {
background-color: var(--color-danger); background-color: var(--color-danger);
} }
:is(div:has(:is(input, select)), input, select, label) :is(div:has(:is(input, select)), input, select, label)
+ :is(div:has(:is(input, select)), input, select, label) { + :is(div:has(:is(input, select)), input, select, label) {
margin-top: 8px; margin-top: 8px;
} }
.hidden { .hidden {
display: none !important; display: none !important;
} }
label.switch { label.switch {
display: inline-flex; display: inline-flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
-webkit-user-select: none; -webkit-user-select: none;
user-select: none; user-select: none;
} }
label.switch input { label.switch input {
display: none; display: none;
} }
label.switch .slider { label.switch .slider {
display: inline-block; display: inline-block;
position: relative; position: relative;
height: 1em; height: 1em;
width: 2em; width: 2em;
background-color: #444; background-color: #444;
border-radius: 1em; border-radius: 1em;
border: 4px solid #444; border: 4px solid #444;
} }
label.switch .slider::before { label.switch .slider::before {
content: ""; content: "";
position: absolute; position: absolute;
height: 100%; height: 100%;
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
border-radius: 50%; border-radius: 50%;
top: 50%; top: 50%;
background-color: white; background-color: white;
transition: all 0.1s linear; transition: all 0.1s linear;
} }
label.switch:active .slider::before { label.switch:active .slider::before {
transform: scale(1.3); transform: scale(1.3);
transform-origin: 50% 50%; transform-origin: 50% 50%;
} }
label.switch input:not(:checked) + .slider::before { label.switch input:not(:checked) + .slider::before {
left: 0%; left: 0%;
translate: 0 -50%; translate: 0 -50%;
} }
label.switch input:checked + .slider::before { label.switch input:checked + .slider::before {
left: 100%; left: 100%;
translate: -100% -50%; translate: -100% -50%;
} }
dialog { dialog {
width: 80%; width: 80%;
max-width: 500px; max-width: 500px;
max-height: 80%; max-height: 80%;
overflow: auto; overflow: auto;
background-color: var(--color-background); background-color: var(--color-background);
color: white; color: white;
border: none; border: none;
border-radius: 8px; border-radius: 8px;
padding: 16px; padding: 16px;
} }
dialog::backdrop { dialog::backdrop {
background-color: #000a; background-color: #000a;
} }
.dialog-header { .dialog-header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
& button { & button {
margin: 0; margin: 0;
} }
} }
.card { .card {
background-color: var(--color-surface); background-color: var(--color-surface);
padding: 8px; padding: 8px;
border-radius: 8px; border-radius: 8px;
} }
.card > * { .card > * {
display: block; display: block;
} }
.card > :first-child { .card > :first-child {
color: var(--color-primary); color: var(--color-primary);
margin-bottom: 8px; margin-bottom: 8px;
} }
.buttons { .buttons {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
gap: 8px; gap: 8px;
} }
.loading-screen { .loading-screen {
display: grid; display: grid;
justify-content: center; justify-content: center;
} }
.error { .error {
color: var(--color-danger); color: var(--color-danger);
} }
.error-bg { .error-bg {
background-color: var(--color-danger) !important; background-color: var(--color-danger) !important;
} }
button.reload { button.reload {
display: block; display: block;
margin: 0 auto; margin: 0 auto;
} }
.spinner-container { .spinner-container {
width: min(max-content, 100%); width: min(max-content, 100%);
} }
.spinner { .spinner {
position: relative; position: relative;
margin: 10px auto; margin: 10px auto;
background: conic-gradient(transparent 150deg, var(--color-primary)); background: conic-gradient(transparent 150deg, var(--color-primary));
--outer-diameter: 50px; --outer-diameter: 50px;
width: var(--outer-diameter); width: var(--outer-diameter);
height: var(--outer-diameter); height: var(--outer-diameter);
border-radius: 50%; border-radius: 50%;
animation-name: spin; animation-name: spin;
animation-duration: 1s; animation-duration: 1s;
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-timing-function: linear; animation-timing-function: linear;
} }
.spinner::after { .spinner::after {
position: absolute; position: absolute;
content: ""; content: "";
display: block; display: block;
--spinner-border: 5px; --spinner-border: 5px;
top: var(--spinner-border); top: var(--spinner-border);
left: var(--spinner-border); left: var(--spinner-border);
--inner-diameter: calc(var(--outer-diameter) - 2 * var(--spinner-border)); --inner-diameter: calc(var(--outer-diameter) - 2 * var(--spinner-border));
width: var(--inner-diameter); width: var(--inner-diameter);
height: var(--inner-diameter); height: var(--inner-diameter);
background-color: var(--color-background); background-color: var(--color-background);
border-radius: 50%; border-radius: 50%;
} }
@keyframes spin { @keyframes spin {
from { from {
transform: rotate(0deg); transform: rotate(0deg);
} }
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
.icon-button { .icon-button {
padding: 8px; padding: 8px;
font-size: 0; font-size: 0;
width: var(--appended-item-size); width: var(--appended-item-size);
height: var(--appended-item-size); height: var(--appended-item-size);
} }
.spinning { .spinning {
animation-name: spin; animation-name: spin;
animation-duration: 0.5s; animation-duration: 0.5s;
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-timing-function: linear; animation-timing-function: linear;
} }
div:has(.range-value) { div:has(.range-value) {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 8px; gap: 8px;
} }
.range-value { .range-value {
width: var(--appended-item-size); width: var(--appended-item-size);
height: var(--appended-item-size); height: var(--appended-item-size);
text-align: center; text-align: center;
line-height: var(--appended-item-size); line-height: var(--appended-item-size);
} }
.status { .status {
background-color: var(--color-surface); background-color: var(--color-surface);
padding: 8px; padding: 8px;
border-radius: 8px; border-radius: 8px;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 16px; gap: 16px;
} }
.dialog-status-content { .dialog-status-content {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 8px; gap: 8px;
} }
.connection-icon { .connection-icon {
width: 24px; width: 24px;
height: 24px; height: 24px;
padding: 8px; padding: 8px;
} }
.connection-icon.small { .connection-icon.small {
padding: 0; padding: 0;
height: 1em; height: 1em;
} }
.centered-vertical { .centered-vertical {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }

View file

@ -1,74 +1,74 @@
import { loadData, writeDataToInput } from "./load-data.js"; import { loadData, writeDataToInput } from "./load-data.js";
import { import {
showLoadingScreen, showLoadingScreen,
hideLoadingScreen, hideLoadingScreen,
showError, showError,
} from "./loading-screen.js"; } from "./loading-screen.js";
const form = document.querySelector("form.config"); const form = document.querySelector("form.config");
function parseValue(input) { function parseValue(input) {
if (input.type === "checkbox") { if (input.type === "checkbox") {
return input.checked return input.checked
? input.dataset.valueChecked ? input.dataset.valueChecked
: input.dataset.valueNotChecked; : input.dataset.valueNotChecked;
} }
if (input.value === "") { if (input.value === "") {
return null; return null;
} }
if (input.type === "number" || input.type === "range") { if (input.type === "number" || input.type === "range") {
const number = Number(input.value); const number = Number(input.value);
return Number.isNaN(number) ? null : number; return Number.isNaN(number) ? null : number;
} }
return input.value; return input.value;
} }
form.addEventListener("submit", (event) => { form.addEventListener("submit", event => {
event.preventDefault(); event.preventDefault();
const inputFields = document.querySelectorAll( const inputFields = document.querySelectorAll(
"form :is(input, select, textarea):not(:disabled)" "form :is(input, select, textarea):not(:disabled)"
); );
const data = Array.from(inputFields).reduce((data, input) => { const data = Array.from(inputFields).reduce((data, input) => {
data[input.name] = parseValue(input); data[input.name] = parseValue(input);
return data; return data;
}, {}); }, {});
console.log(data); console.log(data);
updateConfig({ updateConfig({
method: "PUT", method: "PUT",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
}); });
export async function updateConfig(fetchOptions) { export async function updateConfig(fetchOptions) {
showLoadingScreen("Konfiguration anwenden und ESP neustarten..."); showLoadingScreen("Konfiguration anwenden und ESP neustarten...");
try {
const res = await fetch("/config", fetchOptions);
if (!res.ok) {
throw new Error(`Response status: ${res.status}`);
}
} catch (error) {
console.error(error.message);
showError(error.message);
}
for (let i = 0; i < 10; i++) {
try { try {
const res = await fetch("/config", fetchOptions); const data = await loadData(5000);
if (!res.ok) { writeDataToInput(data);
throw new Error(`Response status: ${res.status}`); hideLoadingScreen();
}
break;
} catch (error) { } catch (error) {
console.error(error.message); // retry loading config until successful
showError(error.message);
}
for (let i = 0; i < 10; i++) {
try {
const data = await loadData(5000);
writeDataToInput(data);
hideLoadingScreen();
break;
} catch (error) {
// retry loading config until successful
}
} }
}
} }

View file

@ -4,34 +4,34 @@ let ws;
let callbacks = {}; let callbacks = {};
export function initWebSocket() { export function initWebSocket() {
if (ws) return; if (ws) return;
ws = new WebSocket(gateway); ws = new WebSocket(gateway);
ws.onopen = () => { ws.onopen = () => {
console.info("WebSocket connection opened"); console.info("WebSocket connection opened");
}; };
ws.onclose = (event) => { ws.onclose = event => {
console.info("WebSocket connection closed, reason:", event.reason); console.info("WebSocket connection closed, reason:", event.reason);
ws = null; ws = null;
}; };
ws.onerror = (event) => { ws.onerror = event => {
console.warn("WebSocket encountered error, closing socket.", event); console.warn("WebSocket encountered error, closing socket.", event);
ws.close(); ws.close();
ws = null; ws = null;
}; };
ws.onmessage = (event) => { ws.onmessage = event => {
const message = JSON.parse(event.data); const message = JSON.parse(event.data);
console.log("received websocket data", message); console.log("received websocket data", message);
if (message.type in callbacks) { if (message.type in callbacks) {
callbacks[message.type](message.data); callbacks[message.type](message.data);
} }
}; };
} }
export function registerCallback(type, callback) { export function registerCallback(type, callback) {
callbacks[type] = callback; callbacks[type] = callback;
} }

View file

@ -9,21 +9,18 @@
static const char *TAG = "MAIN"; static const char *TAG = "MAIN";
void app_main(void) void app_main(void) {
{
ESP_LOGI(TAG, "DMX Interface starting..."); ESP_LOGI(TAG, "DMX Interface starting...");
esp_err_t wifi_err = wifi_start_ap("DMX", "mbgmbgmbg", 1, 4); esp_err_t wifi_err = wifi_start_ap("DMX", "mbgmbgmbg", 1, 4);
if (wifi_err != ESP_OK) if (wifi_err != ESP_OK) {
{
ESP_LOGE(TAG, "Failed to start WiFi AP: %s", esp_err_to_name(wifi_err)); ESP_LOGE(TAG, "Failed to start WiFi AP: %s", esp_err_to_name(wifi_err));
return; return;
} }
// Start HTTP web server // Start HTTP web server
httpd_handle_t server = webserver_start(NULL); httpd_handle_t server = webserver_start(NULL);
if (server == NULL) if (server == NULL) {
{
ESP_LOGE(TAG, "Failed to start web server!"); ESP_LOGE(TAG, "Failed to start web server!");
return; return;
} }
@ -32,8 +29,7 @@ void app_main(void)
ESP_LOGI(TAG, "Open http://192.168.4.1 in your browser"); ESP_LOGI(TAG, "Open http://192.168.4.1 in your browser");
// Keep the app running // Keep the app running
while (1) while (1) {
{
vTaskDelay(pdMS_TO_TICKS(1000)); vTaskDelay(pdMS_TO_TICKS(1000));
} }
} }

View file

@ -1,7 +1,7 @@
## IDF Component Manager Manifest File ## IDF Component Manager Manifest File
dependencies: dependencies:
idf: idf:
version: '>=4.1.0' version: ">=4.1.0"
joltwallet/littlefs: ==1.20.2 joltwallet/littlefs: ==1.20.2
someweisguy/esp_dmx: someweisguy/esp_dmx:
git: https://github.com/davispolito/esp_dmx.git git: https://github.com/davispolito/esp_dmx.git