From 2d85f560bbedb6798d0f99b85c7afd97cfe46533 Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Tue, 16 Sep 2025 12:12:27 +0200 Subject: [PATCH] feat: serve static files, relay example --- data/{ => public}/index.html | 0 data/public/relay.html | 305 +++++++++++++++++++++ examples/base/data/public/index.html | 165 +++++++++++ examples/relay/README.md | 26 +- examples/relay/data/public/index.html | 305 +++++++++++++++++++++ examples/relay/main.cpp | 1 + include/spore/core/ApiServer.h | 3 + include/spore/services/StaticFileService.h | 6 - platformio.ini | 26 +- src/spore/core/ApiServer.cpp | 5 + src/spore/services/StaticFileService.cpp | 92 +------ 11 files changed, 834 insertions(+), 100 deletions(-) rename data/{ => public}/index.html (100%) create mode 100644 data/public/relay.html create mode 100644 examples/base/data/public/index.html create mode 100644 examples/relay/data/public/index.html diff --git a/data/index.html b/data/public/index.html similarity index 100% rename from data/index.html rename to data/public/index.html diff --git a/data/public/relay.html b/data/public/relay.html new file mode 100644 index 0000000..b418789 --- /dev/null +++ b/data/public/relay.html @@ -0,0 +1,305 @@ + + + + + + Relay Control - Spore + + + +
+

🔌 Relay Control

+ +
+
+ OFF +
+
Relay is OFF
+
Pin: Loading...
+
+ +
+ + + +
+ +
+
+
+ + + + diff --git a/examples/base/data/public/index.html b/examples/base/data/public/index.html new file mode 100644 index 0000000..d52d7bc --- /dev/null +++ b/examples/base/data/public/index.html @@ -0,0 +1,165 @@ + + + + + + Spore + + + +
+

🍄 Spore Node

+ +
+
+

Node Status

+
Loading...
+
+
+

Network

+
Loading...
+
+
+

Tasks

+
Loading...
+
+
+

Cluster

+
Loading...
+
+
+ + +
+ + + + diff --git a/examples/relay/README.md b/examples/relay/README.md index 3ef3421..6fc8ace 100644 --- a/examples/relay/README.md +++ b/examples/relay/README.md @@ -1,6 +1,13 @@ # Relay Service Example -A minimal example that demonstrates the Spore framework with a custom RelayService. The Spore framework automatically handles all core functionality (WiFi, clustering, API server, task management) while allowing easy registration of custom services. +A minimal example that demonstrates the Spore framework with a custom RelayService and web interface. The Spore framework automatically handles all core functionality (WiFi, clustering, API server, task management) while allowing easy registration of custom services. + +## Features + +- **API Control**: RESTful API endpoints for programmatic control +- **Web Interface**: Beautiful web UI for manual control at `http:///relay.html` +- **Real-time Status**: Live status updates and visual feedback +- **Toggle Functionality**: One-click toggle between ON/OFF states - Default relay pin: `GPIO0` (ESP-01). Override with `-DRELAY_PIN=`. - WiFi and API port are configured in `src/Config.cpp`. @@ -25,7 +32,7 @@ RelayService* relayService = nullptr; void setup() { spore.setup(); - relayService = new RelayService(spore.getTaskManager(), RELAY_PIN); + relayService = new RelayService(spore.getContext(), spore.getTaskManager(), RELAY_PIN); spore.addService(relayService); spore.begin(); @@ -42,6 +49,7 @@ The Spore framework automatically provides: - REST API server with core endpoints - Task scheduling and execution - Node status monitoring +- Static file serving for web interfaces (core service) ## Build & Upload @@ -61,6 +69,20 @@ pio device monitor -b 115200 Assume the device IP is 192.168.1.50 below (replace with your device's IP shown in serial output). +## Web Interface + +The web interface is located in the `data/` folder and will be served by the core StaticFileService. + +Access the web interface at: `http://192.168.1.50/relay.html` + +The web interface provides: +- Visual status indicator (red for OFF, green for ON) +- Turn ON/OFF buttons +- Toggle button for quick switching +- Real-time status updates every 2 seconds +- Uptime display +- Error handling and user feedback + ## Relay API - Get relay status diff --git a/examples/relay/data/public/index.html b/examples/relay/data/public/index.html new file mode 100644 index 0000000..b418789 --- /dev/null +++ b/examples/relay/data/public/index.html @@ -0,0 +1,305 @@ + + + + + + Relay Control - Spore + + + +
+

🔌 Relay Control

+ +
+
+ OFF +
+
Relay is OFF
+
Pin: Loading...
+
+ +
+ + + +
+ +
+
+
+ + + + diff --git a/examples/relay/main.cpp b/examples/relay/main.cpp index abb9991..50735fd 100644 --- a/examples/relay/main.cpp +++ b/examples/relay/main.cpp @@ -30,6 +30,7 @@ void setup() { spore.begin(); LOG_INFO(spore.getContext(), "Main", "Relay service registered and ready!"); + LOG_INFO(spore.getContext(), "Main", "Web interface available at http:///relay.html"); } void loop() { diff --git a/include/spore/core/ApiServer.h b/include/spore/core/ApiServer.h index bbea44c..dcb59dd 100644 --- a/include/spore/core/ApiServer.h +++ b/include/spore/core/ApiServer.h @@ -29,6 +29,9 @@ public: std::function uploadHandler, const std::vector& params); + // Static file serving + void serveStatic(const String& uri, fs::FS& fs, const String& path, const String& cache_header = ""); + static const char* methodToStr(int method); // Access to endpoints for endpoints endpoint diff --git a/include/spore/services/StaticFileService.h b/include/spore/services/StaticFileService.h index 543dcdd..ba77128 100644 --- a/include/spore/services/StaticFileService.h +++ b/include/spore/services/StaticFileService.h @@ -15,10 +15,4 @@ private: static const String name; NodeContext& ctx; ApiServer& apiServer; - - void handleRootRequest(AsyncWebServerRequest* request); - void handleStaticFileRequest(AsyncWebServerRequest* request); - void handleNotFound(AsyncWebServerRequest* request); - String getContentType(const String& filename); - bool fileExists(const String& path); }; diff --git a/platformio.ini b/platformio.ini index ce386f1..a0e0f8c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -11,6 +11,7 @@ [platformio] ;default_envs = esp01_1m src_dir = . +data_dir = ${PROJECT_DIR}/examples/${PIOENV}/data [common] monitor_speed = 115200 @@ -18,7 +19,7 @@ lib_deps = esp32async/ESPAsyncWebServer@^3.8.0 bblanchon/ArduinoJson@^7.4.2 -[env:esp01_1m] +[env:base] platform = platformio/espressif8266@^4.2.1 board = esp01_1m framework = arduino @@ -57,7 +58,7 @@ build_src_filter = + + -[env:esp01_1m_relay] +[env:relay] platform = platformio/espressif8266@^4.2.1 board = esp01_1m framework = arduino @@ -67,6 +68,27 @@ board_build.filesystem = littlefs board_build.flash_mode = dout board_build.ldscript = eagle.flash.1m64.ld lib_deps = ${common.lib_deps} +;data_dir = examples/relay/data +build_src_filter = + + + + + + + + + + + + + +[env:d1_mini_relay] +platform = platformio/espressif8266@^4.2.1 +board = d1_mini +framework = arduino +upload_speed = 115200 +monitor_speed = 115200 +board_build.filesystem = littlefs +board_build.flash_mode = dio +board_build.flash_size = 4M +board_build.ldscript = eagle.flash.4m1m.ld +lib_deps = ${common.lib_deps} +data_dir = examples/relay/data build_src_filter = + + diff --git a/src/spore/core/ApiServer.cpp b/src/spore/core/ApiServer.cpp index 9e27352..119fcdd 100644 --- a/src/spore/core/ApiServer.cpp +++ b/src/spore/core/ApiServer.cpp @@ -81,6 +81,11 @@ void ApiServer::addService(Service& service) { LOG_INFO(ctx, "API", "Added service: " + String(service.getName())); } +void ApiServer::serveStatic(const String& uri, fs::FS& fs, const String& path, const String& cache_header) { + server.serveStatic(uri.c_str(), fs, path.c_str(), cache_header.c_str()).setDefaultFile("index.html"); + LOG_INFO(ctx, "API", "Registered static file serving: " + uri + " -> " + path); +} + void ApiServer::begin() { // Register all service endpoints for (auto& service : services) { diff --git a/src/spore/services/StaticFileService.cpp b/src/spore/services/StaticFileService.cpp index 415d40b..bd681de 100644 --- a/src/spore/services/StaticFileService.cpp +++ b/src/spore/services/StaticFileService.cpp @@ -16,95 +16,7 @@ void StaticFileService::registerEndpoints(ApiServer& api) { } LOG_INFO(ctx, "StaticFileService", "LittleFS mounted successfully"); - // Root endpoint - serve index.html - api.addEndpoint("/", HTTP_GET, - [this](AsyncWebServerRequest* request) { handleRootRequest(request); }, - std::vector{}); - - // Static file serving for any path - api.addEndpoint("/*", HTTP_GET, - [this](AsyncWebServerRequest* request) { handleStaticFileRequest(request); }, - std::vector{}); + // Use the built-in static file serving from ESPAsyncWebServer + api.serveStatic("/", LittleFS, "/public", "max-age=3600"); } -void StaticFileService::handleRootRequest(AsyncWebServerRequest* request) { - // Serve index.html from root - String path = "/index.html"; - - if (!fileExists(path)) { - request->send(404, "text/plain", "File not found"); - return; - } - - File file = LittleFS.open(path, "r"); - if (!file) { - request->send(500, "text/plain", "Failed to open file"); - return; - } - - String contentType = getContentType(path); - request->send(LittleFS, path, contentType); - file.close(); -} - -void StaticFileService::handleStaticFileRequest(AsyncWebServerRequest* request) { - String path = request->url(); - - // Remove leading slash for LittleFS path - if (path.startsWith("/")) { - path = path.substring(1); - } - - // If path is empty or just "/", serve index.html - if (path.isEmpty() || path == "/") { - path = "index.html"; - } - - // Check if file exists - if (!fileExists("/" + path)) { - request->send(404, "text/plain", "File not found: " + path); - return; - } - - String contentType = getContentType(path); - request->send(LittleFS, "/" + path, contentType); -} - -void StaticFileService::handleNotFound(AsyncWebServerRequest* request) { - // Try to serve index.html as fallback - if (fileExists("/index.html")) { - request->send(LittleFS, "/index.html", "text/html"); - } else { - request->send(404, "text/plain", "Not found"); - } -} - -String StaticFileService::getContentType(const String& filename) { - if (filename.endsWith(".html") || filename.endsWith(".htm")) { - return "text/html"; - } else if (filename.endsWith(".css")) { - return "text/css"; - } else if (filename.endsWith(".js")) { - return "application/javascript"; - } else if (filename.endsWith(".json")) { - return "application/json"; - } else if (filename.endsWith(".png")) { - return "image/png"; - } else if (filename.endsWith(".jpg") || filename.endsWith(".jpeg")) { - return "image/jpeg"; - } else if (filename.endsWith(".gif")) { - return "image/gif"; - } else if (filename.endsWith(".svg")) { - return "image/svg+xml"; - } else if (filename.endsWith(".ico")) { - return "image/x-icon"; - } else if (filename.endsWith(".txt")) { - return "text/plain"; - } else { - return "application/octet-stream"; - } -} - -bool StaticFileService::fileExists(const String& path) { - return LittleFS.exists(path); -}