From 71df7b86590ddda9132a68126e6c7bedd20507bd Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Thu, 26 Feb 2026 14:13:55 +0000 Subject: [PATCH] Add FSEQ Player effect Remove HTTP api for playback control in favour of segment name --- platformio.ini | 4 +- usermods/FSEQ/fseq_effect.h | 59 ++++ usermods/FSEQ/fseq_player.cpp | 68 ++--- usermods/FSEQ/fseq_player.h | 10 +- usermods/FSEQ/sd_manager.cpp | 21 -- usermods/FSEQ/sd_manager.h | 21 -- usermods/FSEQ/usermod_fpp.h | 52 ++-- usermods/FSEQ/usermod_fseq.h | 153 +--------- usermods/FSEQ/web_ui_manager.cpp | 489 ++++++++----------------------- 9 files changed, 254 insertions(+), 623 deletions(-) create mode 100644 usermods/FSEQ/fseq_effect.h delete mode 100644 usermods/FSEQ/sd_manager.cpp delete mode 100644 usermods/FSEQ/sd_manager.h diff --git a/platformio.ini b/platformio.ini index 60dedd473b..b2f9282681 100644 --- a/platformio.ini +++ b/platformio.ini @@ -659,7 +659,9 @@ board = lolin_s3_mini ;; -S3 mini, 4MB flash 2MB PSRAM platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} upload_speed = 921600 -custom_usermods = audioreactive +custom_usermods = + audioreactive + FSEQ build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_4M_qspi\" -DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") diff --git a/usermods/FSEQ/fseq_effect.h b/usermods/FSEQ/fseq_effect.h new file mode 100644 index 0000000000..cba48790a6 --- /dev/null +++ b/usermods/FSEQ/fseq_effect.h @@ -0,0 +1,59 @@ +#pragma once + +#include "wled.h" +#include "fseq_player.h" + +// +// FSEQ Player effect – renders FSEQ frame data into the active SEGMENT. +// +// File selection follows the same pattern as the built-in Image effect: +// set the **segment name** to the .fseq filename on the SD card +// (e.g. "show.fseq"). The effect prepends "/" and opens the file. +// +// Check1 enables loop mode (repeat the sequence forever). +// + +// Stored per-effect so we can detect when the segment name changes. +static char _fseq_lastName[WLED_MAX_SEGNAME_LEN + 2] = ""; + +static void mode_fseq_player(void) { + // Build the target filename from the segment name + const char *segName = SEGMENT.name; + + // No name set – show static colour + if (!segName || segName[0] == '\0') { + if (FSEQPlayer::isPlaying()) FSEQPlayer::clearLastPlayback(); + SEGMENT.fill(SEGCOLOR(0)); + return; + } + + // Detect segment-name change → (re)load the file + if (strncmp(_fseq_lastName, segName, WLED_MAX_SEGNAME_LEN) != 0) { + strncpy(_fseq_lastName, segName, WLED_MAX_SEGNAME_LEN); + _fseq_lastName[WLED_MAX_SEGNAME_LEN] = '\0'; + + // Build "/" path + char path[WLED_MAX_SEGNAME_LEN + 2]; + path[0] = '/'; + strncpy(path + 1, segName, WLED_MAX_SEGNAME_LEN); + path[WLED_MAX_SEGNAME_LEN + 1] = '\0'; + + bool loop = SEGMENT.check1; + FSEQPlayer::loadRecording(path, 0.0f, loop); + } + + if (!FSEQPlayer::isPlaying()) { + SEGMENT.fill(SEGCOLOR(0)); + return; + } + + // Keep loop state in sync with the UI checkbox + FSEQPlayer::setLooping(SEGMENT.check1); + + // Render the current frame into the segment + FSEQPlayer::renderFrameToSegment(); +} + +static const char _data_FX_MODE_FSEQ_PLAYER[] PROGMEM = + "FSEQ Player@,,,,,Loop;;!;1"; + diff --git a/usermods/FSEQ/fseq_player.cpp b/usermods/FSEQ/fseq_player.cpp index 12278b405a..d4fe9c92a5 100644 --- a/usermods/FSEQ/fseq_player.cpp +++ b/usermods/FSEQ/fseq_player.cpp @@ -10,16 +10,9 @@ #include "SD_MMC.h" #endif -// Static member definitions moved from header to avoid multiple definition -// errors -const char UsermodFseq::_name[] PROGMEM = "usermod FSEQ sd card"; - -#ifdef WLED_USE_SD_SPI -int8_t UsermodFseq::configPinSourceSelect = 5; -int8_t UsermodFseq::configPinSourceClock = 18; -int8_t UsermodFseq::configPinPoci = 19; -int8_t UsermodFseq::configPinPico = 23; -#endif +// Static member definitions +const char UsermodFseq::_name[] PROGMEM = "FSEQ"; +uint8_t UsermodFseq::fseqEffectId = 0; File FSEQPlayer::recordingFile; String FSEQPlayer::currentFileName = ""; @@ -29,8 +22,6 @@ uint8_t FSEQPlayer::colorChannels = 3; int32_t FSEQPlayer::recordingRepeats = RECORDING_REPEAT_DEFAULT; uint32_t FSEQPlayer::now = 0; uint32_t FSEQPlayer::next_time = 0; -uint16_t FSEQPlayer::playbackLedStart = 0; -uint16_t FSEQPlayer::playbackLedStop = uint16_t(-1); uint32_t FSEQPlayer::frame = 0; uint16_t FSEQPlayer::buffer_size = 48; FSEQPlayer::FileHeader FSEQPlayer::file_header; @@ -86,25 +77,26 @@ void FSEQPlayer::printHeaderInfo() { void FSEQPlayer::processFrameData() { uint32_t packetLength = file_header.channel_count; - uint16_t lastLed = - min((uint32_t)playbackLedStop, (uint32_t)playbackLedStart + (packetLength / 3)); - char frame_data[48]; // fixed size; buffer_size is always 48 + uint16_t segLen = SEGLEN; // number of virtual pixels in the current segment + uint16_t maxLeds = min((uint32_t)segLen, packetLength / 3); + char frame_data[48]; // fixed size; buffer_size is always 48 CRGB *crgb = reinterpret_cast(frame_data); uint32_t bytes_remaining = packetLength; - uint16_t index = playbackLedStart; - while (index < lastLed && bytes_remaining > 0) { + uint16_t index = 0; + while (index < maxLeds && bytes_remaining > 0) { uint16_t length = (uint16_t)min(bytes_remaining, (uint32_t)sizeof(frame_data)); recordingFile.readBytes(frame_data, length); bytes_remaining -= length; for (uint16_t offset = 0; offset < length / 3; offset++) { - setRealtimePixel(index, crgb[offset].r, crgb[offset].g, crgb[offset].b, - 0); - if (++index > lastLed) + SEGMENT.setPixelColor(index, RGBW32(crgb[offset].r, crgb[offset].g, crgb[offset].b, 0)); + if (++index >= maxLeds) break; } } - strip.show(); - realtimeLock(3000, REALTIME_MODE_FSEQ); + // Fill remaining segment pixels with black if the file has fewer channels + for (uint16_t i = index; i < segLen; i++) { + SEGMENT.setPixelColor(i, BLACK); + } next_time = now + file_header.step_time; } @@ -127,8 +119,7 @@ bool FSEQPlayer::stopBecauseAtTheEnd() { return false; } - DEBUG_PRINTLN("Finished playing recording, disabling realtime mode"); - realtimeLock(10, REALTIME_MODE_INACTIVE); + DEBUG_PRINTLN("Finished playing recording"); clearLastPlayback(); return true; } @@ -150,9 +141,9 @@ void FSEQPlayer::playNextRecordingFrame() { processFrameData(); } -void FSEQPlayer::handlePlayRecording() { +void FSEQPlayer::renderFrameToSegment() { now = millis(); - if (realtimeMode != REALTIME_MODE_FSEQ) + if (!isPlaying()) return; if (now < next_time) return; @@ -160,23 +151,13 @@ void FSEQPlayer::handlePlayRecording() { } void FSEQPlayer::loadRecording(const char *filepath, - uint16_t startLed, - uint16_t stopLed, float secondsElapsed, bool loop) { if (recordingFile.available()) { clearLastPlayback(); } - playbackLedStart = startLed; - playbackLedStop = stopLed; - if (playbackLedStart == uint16_t(-1) || playbackLedStop == uint16_t(-1)) { - Segment sg = strip.getSegment(-1); - playbackLedStart = sg.start; - playbackLedStop = sg.stop; - } - DEBUG_PRINTF("FSEQ load animation on LED %d to %d\n", playbackLedStart, - playbackLedStop); + DEBUG_PRINTF("FSEQ load animation: %s\n", filepath); if (fileOnSD(filepath)) { DEBUG_PRINTF("Read file from SD: %s\n", filepath); recordingFile = SD_ADAPTER.open(filepath, "rb"); @@ -190,8 +171,7 @@ void FSEQPlayer::loadRecording(const char *filepath, if (currentFileName.startsWith("/")) currentFileName = currentFileName.substring(1); } else { - DEBUG_PRINTF("File %s not found (%s)\n", filepath, - USED_STORAGE_FILESYSTEMS); + DEBUG_PRINTF("File %s not found on SD or FS\n", filepath); return; } if ((uint64_t)recordingFile.available() < sizeof(file_header)) { @@ -232,9 +212,6 @@ void FSEQPlayer::loadRecording(const char *filepath, file_header.step_time, FSEQ_DEFAULT_STEP_TIME); file_header.step_time = FSEQ_DEFAULT_STEP_TIME; } - if (realtimeOverride == REALTIME_OVERRIDE_ONCE) { - realtimeOverride = REALTIME_OVERRIDE_NONE; - } frame = (uint32_t)((secondsElapsed * 1000.0f) / file_header.step_time); if (frame >= file_header.frame_count) { frame = file_header.frame_count - 1; @@ -249,9 +226,6 @@ void FSEQPlayer::loadRecording(const char *filepath, } void FSEQPlayer::clearLastPlayback() { - for (uint16_t i = playbackLedStart; i < playbackLedStop; i++) { - setRealtimePixel(i, 0, 0, 0, 0); - } frame = 0; recordingFile.close(); currentFileName = ""; @@ -261,6 +235,10 @@ bool FSEQPlayer::isPlaying() { return recordingFile && frame < file_header.frame_count; } +void FSEQPlayer::setLooping(bool loop) { + recordingRepeats = loop ? RECORDING_REPEAT_LOOP : RECORDING_REPEAT_DEFAULT; +} + String FSEQPlayer::getFileName() { return currentFileName; } float FSEQPlayer::getElapsedSeconds() { diff --git a/usermods/FSEQ/fseq_player.h b/usermods/FSEQ/fseq_player.h index eb05b98ce1..74812fce98 100644 --- a/usermods/FSEQ/fseq_player.h +++ b/usermods/FSEQ/fseq_player.h @@ -31,17 +31,19 @@ class FSEQPlayer { }; static void loadRecording(const char *filepath, - uint16_t startLed, - uint16_t stopLed, float secondsElapsed = 0.0f, bool loop = false); - static void handlePlayRecording(); static void clearLastPlayback(); static void syncPlayback(float secondsElapsed); static bool isPlaying(); + static void setLooping(bool loop); static String getFileName(); static float getElapsedSeconds(); + // Called from the WLED effect function – renders the current frame into + // the active SEGMENT using SEGMENT.setPixelColor(). + static void renderFrameToSegment(); + private: FSEQPlayer() {} @@ -54,8 +56,6 @@ class FSEQPlayer { static int32_t recordingRepeats; static uint32_t now; static uint32_t next_time; - static uint16_t playbackLedStart; - static uint16_t playbackLedStop; static uint32_t frame; static uint16_t buffer_size; static FileHeader file_header; diff --git a/usermods/FSEQ/sd_manager.cpp b/usermods/FSEQ/sd_manager.cpp deleted file mode 100644 index 27c560323d..0000000000 --- a/usermods/FSEQ/sd_manager.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "sd_manager.h" -#include "usermod_fseq.h" - -bool SDManager::begin() { -#if !defined(WLED_USE_SD_SPI) && !defined(WLED_USE_SD_MMC) -#error "FSEQ requires SD backend (WLED_USE_SD_SPI or WLED_USE_SD_MMC)" -#endif - -#ifdef WLED_USE_SD_SPI - if (!SD_ADAPTER.begin(WLED_PIN_SS, spiPort)) - return false; -#elif defined(WLED_USE_SD_MMC) - if (!SD_ADAPTER.begin()) - return false; -#endif - return true; -} - -void SDManager::end() { SD_ADAPTER.end(); } - -bool SDManager::deleteFile(const char *path) { return SD_ADAPTER.remove(path); } \ No newline at end of file diff --git a/usermods/FSEQ/sd_manager.h b/usermods/FSEQ/sd_manager.h deleted file mode 100644 index e40a2e2040..0000000000 --- a/usermods/FSEQ/sd_manager.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef SD_MANAGER_H -#define SD_MANAGER_H - -#include "wled.h" - -#ifdef WLED_USE_SD_SPI - #include - #include -#elif defined(WLED_USE_SD_MMC) - #include "SD_MMC.h" -#endif - -class SDManager { - public: - SDManager() {} - bool begin(); - void end(); - bool deleteFile(const char* path); -}; - -#endif // SD_MANAGER_H \ No newline at end of file diff --git a/usermods/FSEQ/usermod_fpp.h b/usermods/FSEQ/usermod_fpp.h index 288280134e..c41a324965 100644 --- a/usermods/FSEQ/usermod_fpp.h +++ b/usermods/FSEQ/usermod_fpp.h @@ -486,7 +486,6 @@ void sendPingPacket(IPAddress destination = IPAddress(255, 255, 255, 255)) { case CTRL_PKT_BLANK: DEBUG_PRINTLN(F("[FPP] Received UDP blank packet")); FSEQPlayer::clearLastPlayback(); - realtimeLock(10, REALTIME_MODE_INACTIVE); break; default: DEBUG_PRINTLN(F("[FPP] Unknown UDP packet type")); @@ -494,6 +493,25 @@ void sendPingPacket(IPAddress destination = IPAddress(255, 255, 255, 255)) { } } + // Switch the main segment to the FSEQ Player effect and set its name + // to the given filename so the effect knows which file to play. + void activateFseqEffect(const String &fileName) { + uint8_t fxId = UsermodFseq::fseqEffectId; + if (fxId == 0) return; // effect not registered + + Segment &seg = strip.getMainSegment(); + // Strip leading '/' for the segment name (effect prepends it) + const char *nameStr = fileName.c_str(); + if (nameStr[0] == '/') nameStr++; + seg.setName(nameStr); + + if (seg.mode != fxId) { + seg.setMode(fxId); + seg.check1 = true; // enable looping for sync playback + stateChanged = true; + } + } + // Process sync command with detailed debug output void ProcessSyncPacket(uint8_t action, String fileName, float secondsElapsed) { @@ -510,21 +528,17 @@ void sendPingPacket(IPAddress destination = IPAddress(255, 255, 255, 255)) { switch (action) { case 0: // SYNC_PKT_START - FSEQPlayer::loadRecording(fileName.c_str(), 0, strip.getLength(), - secondsElapsed); + activateFseqEffect(fileName); + FSEQPlayer::loadRecording(fileName.c_str(), secondsElapsed); break; case 1: // SYNC_PKT_STOP FSEQPlayer::clearLastPlayback(); - realtimeLock(10, REALTIME_MODE_INACTIVE); break; case 2: // SYNC_PKT_SYNC - DEBUG_PRINTLN(F("[FPP] ProcessSyncPacket: Sync command received")); - DEBUG_PRINTF("[FPP] Sync Packet - FileName: %s, Seconds Elapsed: %.2f\n", - fileName.c_str(), secondsElapsed); if (!FSEQPlayer::isPlaying()) { DEBUG_PRINTLN(F("[FPP] Sync: Playback not active, starting playback.")); - FSEQPlayer::loadRecording(fileName.c_str(), 0, strip.getLength(), - secondsElapsed); + activateFseqEffect(fileName); + FSEQPlayer::loadRecording(fileName.c_str(), secondsElapsed); } else { FSEQPlayer::syncPlayback(secondsElapsed); } @@ -682,26 +696,6 @@ void sendPingPacket(IPAddress destination = IPAddress(255, 255, 255, 255)) { request->send(200, "application/json", json); }); - // Endpoint to start FSEQ playback - server.on("/fpp/connect", HTTP_GET, [this](AsyncWebServerRequest *request) { - if (!request->hasArg("file")) { - request->send(400, "text/plain", "Missing 'file' parameter"); - return; - } - String filepath = request->arg("file"); - if (!filepath.startsWith("/")) { - filepath = "/" + filepath; - } - // Use FSEQPlayer to start playback - FSEQPlayer::loadRecording(filepath.c_str(), 0, strip.getLength()); - request->send(200, "text/plain", "FPP connect started: " + filepath); - }); - // Endpoint to stop FSEQ playback - server.on("/fpp/stop", HTTP_GET, [this](AsyncWebServerRequest *request) { - FSEQPlayer::clearLastPlayback(); - realtimeLock(10, REALTIME_MODE_INACTIVE); - request->send(200, "text/plain", "FPP connect stopped"); - }); // Initialize UDP listener for synchronization and ping if (!udpStarted && (WiFi.status() == WL_CONNECTED)) { diff --git a/usermods/FSEQ/usermod_fseq.h b/usermods/FSEQ/usermod_fseq.h index 0f09a65e2e..82cb2bd9dd 100644 --- a/usermods/FSEQ/usermod_fseq.h +++ b/usermods/FSEQ/usermod_fseq.h @@ -1,79 +1,41 @@ #pragma once -#ifndef USED_STORAGE_FILESYSTEMS -#ifdef WLED_USE_SD_SPI -#define USED_STORAGE_FILESYSTEMS "SD SPI, LittleFS" -#else -#define USED_STORAGE_FILESYSTEMS "SD MMC, LittleFS" -#endif -#endif - #include "wled.h" + #ifdef WLED_USE_SD_SPI #include #include #endif +// Define SD_ADAPTER macro if not already defined (used by FSEQ file operations) #ifndef SD_ADAPTER -#if defined(WLED_USE_SD) || defined(WLED_USE_SD_SPI) -#ifdef WLED_USE_SD_SPI -#ifndef WLED_USE_SD -#define WLED_USE_SD -#endif -#ifndef WLED_PIN_SCK -#define WLED_PIN_SCK SCK -#endif -#ifndef WLED_PIN_MISO -#define WLED_PIN_MISO MISO -#endif -#ifndef WLED_PIN_MOSI -#define WLED_PIN_MOSI MOSI -#endif -#ifndef WLED_PIN_SS -#define WLED_PIN_SS SS -#endif +#if defined(WLED_USE_SD_SPI) #define SD_ADAPTER SD -#else +#elif defined(WLED_USE_SD_MMC) #define SD_ADAPTER SD_MMC #endif #endif -#endif - -#ifdef WLED_USE_SD_SPI -#ifndef SPI_PORT_DEFINED -#if CONFIG_IDF_TARGET_ESP32 -inline SPIClass spiPort = SPIClass(VSPI); -#elif CONFIG_IDF_TARGET_ESP32S3 -inline SPIClass spiPort = SPI; -#else -inline SPIClass spiPort = SPI; -#endif -#define SPI_PORT_DEFINED -#endif -#endif #include "fseq_player.h" -#include "sd_manager.h" +#include "fseq_effect.h" #include "web_ui_manager.h" -// Usermod for FSEQ playback with UDP and web UI support +// Usermod for FSEQ playback with UDP and web UI support. +// SD card initialisation is handled by the standard sd_card usermod. class UsermodFseq : public Usermod { private: WebUIManager webUI; // Web UI Manager module (handles endpoints) static const char _name[]; // for storing usermod name in config public: + static uint8_t fseqEffectId; // effect ID assigned by strip.addEffect() + // Setup function called once at startup void setup() { DEBUG_PRINTF("[%s] Usermod loaded\n", FPSTR(_name)); - // Initialize SD card using SDManager - SDManager sd; - if (!sd.begin()) { - DEBUG_PRINTF("[%s] SD initialization FAILED.\n", FPSTR(_name)); - } else { - DEBUG_PRINTF("[%s] SD initialization successful.\n", FPSTR(_name)); - } + // Register the FSEQ Player as a WLED effect and store its ID + fseqEffectId = strip.addEffect(255, &mode_fseq_player, _data_FX_MODE_FSEQ_PLAYER); // Register web endpoints defined in WebUIManager webUI.registerEndpoints(); @@ -81,8 +43,8 @@ class UsermodFseq : public Usermod { // Loop function called continuously void loop() { - // Process FSEQ playback (includes UDP sync commands) - FSEQPlayer::handlePlayRecording(); + // FSEQ playback is now driven by the WLED effect engine via mode_fseq_player. + // No work needed here. } // Unique ID for the usermod @@ -103,91 +65,6 @@ class UsermodFseq : public Usermod { arr.add(button); } - // Save your SPI pins to WLED config - void addToConfig(JsonObject &root) override { - - #ifdef WLED_USE_SD_SPI - - JsonObject top = root.createNestedObject(FPSTR(_name)); - - top["csPin"] = configPinSourceSelect; - top["sckPin"] = configPinSourceClock; - top["misoPin"] = configPinPoci; - top["mosiPin"] = configPinPico; - - #endif - } - - // Read your SPI pins from WLED config JSON - bool readFromConfig(JsonObject &root) override { -#ifdef WLED_USE_SD_SPI - JsonObject top = root[FPSTR(_name)]; - if (top.isNull()) - return false; - - int8_t oldCs = configPinSourceSelect; - int8_t oldSck = configPinSourceClock; - int8_t oldMiso = configPinPoci; - int8_t oldMosi = configPinPico; - - if (top["csPin"].is()) - configPinSourceSelect = top["csPin"].as(); - if (top["sckPin"].is()) - configPinSourceClock = top["sckPin"].as(); - if (top["misoPin"].is()) - configPinPoci = top["misoPin"].as(); - if (top["mosiPin"].is()) - configPinPico = top["mosiPin"].as(); - - reinit_SD_SPI(oldCs, oldSck, oldMiso, oldMosi); // reinitialize SD with new pins - return true; -#else - return false; -#endif - } - -#ifdef WLED_USE_SD_SPI - // Reinitialize SD SPI with updated pins - void reinit_SD_SPI(int8_t oldCs, int8_t oldSck, int8_t oldMiso, int8_t oldMosi) { - // Deinit SD if needed - SD_ADAPTER.end(); - // Reallocate pins - PinManager::deallocatePin(oldCs, PinOwner::UM_SdCard); - PinManager::deallocatePin(oldSck, PinOwner::UM_SdCard); - PinManager::deallocatePin(oldMiso, PinOwner::UM_SdCard); - PinManager::deallocatePin(oldMosi, PinOwner::UM_SdCard); - - PinManagerPinType pins[4] = {{configPinSourceSelect, true}, - {configPinSourceClock, true}, - {configPinPoci, false}, - { configPinPico, - true }}; - if (!PinManager::allocateMultiplePins(pins, 4, PinOwner::UM_SdCard)) { - DEBUG_PRINTF("[%s] SPI pin allocation failed!\n", FPSTR(_name)); - return; - } - - // Reinit SPI with new pins - spiPort.begin(configPinSourceClock, configPinPoci, configPinPico, - configPinSourceSelect); - - // Try to begin SD again - if (!SD_ADAPTER.begin(configPinSourceSelect, spiPort)) { - DEBUG_PRINTF("[%s] SPI begin failed!\n", FPSTR(_name)); - } else { - DEBUG_PRINTF("[%s] SD SPI reinitialized with new pins\n", FPSTR(_name)); - } - } - - // Getter methods and static variables for SD pins - static int8_t getCsPin() { return configPinSourceSelect; } - static int8_t getSckPin() { return configPinSourceClock; } - static int8_t getMisoPin() { return configPinPoci; } - static int8_t getMosiPin() { return configPinPico; } - - static int8_t configPinSourceSelect; - static int8_t configPinSourceClock; - static int8_t configPinPoci; - static int8_t configPinPico; -#endif + void addToConfig(JsonObject &root) override {} + bool readFromConfig(JsonObject &root) override { return true; } }; \ No newline at end of file diff --git a/usermods/FSEQ/web_ui_manager.cpp b/usermods/FSEQ/web_ui_manager.cpp index b0604a3f66..9842a42fe9 100644 --- a/usermods/FSEQ/web_ui_manager.cpp +++ b/usermods/FSEQ/web_ui_manager.cpp @@ -1,6 +1,4 @@ #include "web_ui_manager.h" -#include "fseq_player.h" -#include "sd_manager.h" #include "usermod_fseq.h" struct UploadContext { @@ -13,7 +11,7 @@ static const char PAGE_HTML[] PROGMEM = R"rawliteral( -WLED FSEQ UI +WLED FSEQ SD Manager @@ -48,24 +46,9 @@ header h1 { .back-btn:hover { background:var(--accent); color:#000; } -nav { display:flex; background:#000; border-bottom:1px solid #222; } - -nav button { - flex:1; padding:12px; background:none; border:none; - color:var(--text-dim); font-size:14px; cursor:pointer; -} - -nav button.active { - color:var(--accent); - border-bottom:2px solid var(--accent); -} - -.tab-content { display:none; padding:15px; } -.tab-content.active { display:block; } - .card { background:var(--card); padding:15px; - margin-bottom:15px; border-radius:var(--radius); + margin:15px; border-radius:var(--radius); box-shadow:0 0 10px #000; } @@ -86,13 +69,6 @@ li { .btn:hover { background:var(--accent); color:#000; } -.btn-stop { - border-color:var(--danger); - color:var(--danger); -} - -.btn-stop:hover { background:var(--danger); color:#000; } - .progress-container { width:100%; background:#222; border-radius:var(--radius); @@ -114,23 +90,16 @@ li { font-size:13px; margin-top:8px; color:var(--text-dim); } + +.info { + font-size:13px; color:var(--text-dim); + margin-top:4px; +} @@ -344,48 +180,36 @@ document.addEventListener("DOMContentLoaded",()=>{
- -

FSEQ UI

+ +

FSEQ SD Manager

- - -
- -
-

SD Storage

-
-
-
-
+
+

SD Storage

+
+
+
+
-
-

SD Files

-
    -
    +
    +

    SD Files

    +
      +
      -
      -

      Upload File

      -

      - -
      -
      -
      -
      +
      +

      Upload File

      +

      + +
      +
      - +
      -
      -
      -

      FSEQ Files

      -
        -
        +
        +

        FSEQ playback is controlled via the WLED effects interface. Select the FSEQ Player effect to play back sequences.

        @@ -395,152 +219,145 @@ document.addEventListener("DOMContentLoaded",()=>{ void WebUIManager::registerEndpoints() { - // Main UI page (navigation, SD and FSEQ tabs) + // Main UI page server.on("/fsequi", HTTP_GET, [](AsyncWebServerRequest *request) { request->send_P(200, "text/html", PAGE_HTML); }); - // API - List SD files (size in KB + storage info) - server.on("/api/sd/list", HTTP_GET, [](AsyncWebServerRequest *request) { + // API - List SD files (size in KB + storage info) + server.on("/api/sd/list", HTTP_GET, [](AsyncWebServerRequest *request) { - File root = SD_ADAPTER.open("/"); + File root = SD_ADAPTER.open("/"); - uint64_t totalBytes = SD_ADAPTER.totalBytes(); - uint64_t usedBytes = SD_ADAPTER.usedBytes(); + uint64_t totalBytes = SD_ADAPTER.totalBytes(); + uint64_t usedBytes = SD_ADAPTER.usedBytes(); - // Adjust size if needed (depends on max file count) - DynamicJsonDocument doc(8192); + DynamicJsonDocument doc(8192); - JsonObject rootObj = doc.to(); - JsonArray files = rootObj.createNestedArray("files"); + JsonObject rootObj = doc.to(); + JsonArray files = rootObj.createNestedArray("files"); - if (root && root.isDirectory()) { + if (root && root.isDirectory()) { + File file = root.openNextFile(); + while (file) { + String name = file.name(); - File file = root.openNextFile(); - while (file) { - - String name = file.name(); + JsonObject obj = files.createNestedObject(); + obj["name"] = name; + obj["size"] = (float)file.size() / 1024.0; - JsonObject obj = files.createNestedObject(); - obj["name"] = name; - obj["size"] = (float)file.size() / 1024.0; + file.close(); + file = root.openNextFile(); + } + } - file.close(); - file = root.openNextFile(); - } - } + root.close(); - root.close(); + rootObj["usedKB"] = (float)usedBytes / 1024.0; + rootObj["totalKB"] = (float)totalBytes / 1024.0; - rootObj["usedKB"] = (float)usedBytes / 1024.0; - rootObj["totalKB"] = (float)totalBytes / 1024.0; + String output; + serializeJson(doc, output); - String output; - serializeJson(doc, output); - - if (doc.overflowed()) { + if (doc.overflowed()) { request->send(507, "text/plain", "JSON buffer too small; file list may be truncated"); return; - } - - request->send(200, "application/json", output); - }); + } + request->send(200, "application/json", output); + }); // API - List FSEQ files server.on("/api/fseq/list", HTTP_GET, [](AsyncWebServerRequest *request) { - File root = SD_ADAPTER.open("/"); + File root = SD_ADAPTER.open("/"); - DynamicJsonDocument doc(4096); - JsonArray files = doc.to(); + DynamicJsonDocument doc(4096); + JsonArray files = doc.to(); - if (root && root.isDirectory()) { + if (root && root.isDirectory()) { + File file = root.openNextFile(); + while (file) { + String name = file.name(); - File file = root.openNextFile(); - while (file) { - - String name = file.name(); + if (name.endsWith(".fseq") || name.endsWith(".FSEQ")) { + JsonObject obj = files.createNestedObject(); + obj["name"] = name; + } - if (name.endsWith(".fseq") || name.endsWith(".FSEQ")) { - JsonObject obj = files.createNestedObject(); - obj["name"] = name; - } + file.close(); + file = root.openNextFile(); + } + } - file.close(); - file = root.openNextFile(); - } - } + root.close(); - root.close(); + String output; + serializeJson(doc, output); - String output; - serializeJson(doc, output); - - if (doc.overflowed()) { - request->send(507, "text/plain", "JSON buffer too small; file list may be truncated"); - return; - } + if (doc.overflowed()) { + request->send(507, "text/plain", "JSON buffer too small; file list may be truncated"); + return; + } - request->send(200, "application/json", output); - }); + request->send(200, "application/json", output); + }); // API - File Upload - server.on( - "/api/sd/upload", HTTP_POST, + server.on( + "/api/sd/upload", HTTP_POST, - // MAIN HANDLER - [](AsyncWebServerRequest *request) { + // MAIN HANDLER + [](AsyncWebServerRequest *request) { - UploadContext* ctx = static_cast(request->_tempObject); + UploadContext* ctx = static_cast(request->_tempObject); - if (!ctx || ctx->error || !ctx->file || !*(ctx->file)) { - request->send(500, "text/plain", "Failed to open file for writing"); - } else { - request->send(200, "text/plain", "Upload complete"); - } + if (!ctx || ctx->error || !ctx->file || !*(ctx->file)) { + request->send(500, "text/plain", "Failed to open file for writing"); + } else { + request->send(200, "text/plain", "Upload complete"); + } - // Cleanup - if (ctx) { - if (ctx->file) { - if (*(ctx->file)) ctx->file->close(); - delete ctx->file; - } - delete ctx; - request->_tempObject = nullptr; - } - }, + // Cleanup + if (ctx) { + if (ctx->file) { + if (*(ctx->file)) ctx->file->close(); + delete ctx->file; + } + delete ctx; + request->_tempObject = nullptr; + } + }, - // UPLOAD CALLBACK - [](AsyncWebServerRequest *request, String filename, size_t index, - uint8_t *data, size_t len, bool final) { + // UPLOAD CALLBACK + [](AsyncWebServerRequest *request, String filename, size_t index, + uint8_t *data, size_t len, bool final) { - UploadContext* ctx; + UploadContext* ctx; - if (index == 0) { - if (!filename.startsWith("/")) - filename = "/" + filename; + if (index == 0) { + if (!filename.startsWith("/")) + filename = "/" + filename; - ctx = new UploadContext(); - ctx->error = false; - ctx->file = new File(SD_ADAPTER.open(filename.c_str(), FILE_WRITE)); + ctx = new UploadContext(); + ctx->error = false; + ctx->file = new File(SD_ADAPTER.open(filename.c_str(), FILE_WRITE)); - if (!*(ctx->file)) { - ctx->error = true; - } + if (!*(ctx->file)) { + ctx->error = true; + } - request->_tempObject = ctx; - } + request->_tempObject = ctx; + } - ctx = static_cast(request->_tempObject); + ctx = static_cast(request->_tempObject); - if (!ctx || ctx->error || !ctx->file || !*(ctx->file)) - return; + if (!ctx || ctx->error || !ctx->file || !*(ctx->file)) + return; - ctx->file->write(data, len); - - } - ); + ctx->file->write(data, len); + } + ); // API - File Delete server.on("/api/sd/delete", HTTP_POST, [](AsyncWebServerRequest *request) { @@ -554,59 +371,5 @@ void WebUIManager::registerEndpoints() { bool res = SD_ADAPTER.remove(path.c_str()); request->send(200, "text/plain", res ? "File deleted" : "Delete failed"); }); +} - // API - Start FSEQ (normal playback) - server.on("/api/fseq/start", HTTP_POST, [](AsyncWebServerRequest *request) { - if (!request->hasArg("file")) { - request->send(400, "text/plain", "Missing file param"); - return; - } - String filepath = request->arg("file"); - if (!filepath.startsWith("/")) - filepath = "/" + filepath; - FSEQPlayer::loadRecording(filepath.c_str(), 0, uint16_t(-1), 0.0f, false); - request->send(200, "text/plain", "FSEQ started"); - }); - - // API - Start FSEQ in loop mode - server.on( - "/api/fseq/startloop", HTTP_POST, [](AsyncWebServerRequest *request) { - if (!request->hasArg("file")) { - request->send(400, "text/plain", "Missing file param"); - return; - } - String filepath = request->arg("file"); - if (!filepath.startsWith("/")) - filepath = "/" + filepath; - FSEQPlayer::loadRecording(filepath.c_str(), 0, uint16_t(-1), 0.0f, true); - request->send(200, "text/plain", "FSEQ loop started"); - }); - - // API - Stop FSEQ - server.on("/api/fseq/stop", HTTP_POST, [](AsyncWebServerRequest *request) { - FSEQPlayer::clearLastPlayback(); - if (realtimeOverride == REALTIME_OVERRIDE_ONCE) - realtimeOverride = REALTIME_OVERRIDE_NONE; - if (realtimeMode) - exitRealtime(); - else { - realtimeMode = REALTIME_MODE_INACTIVE; - strip.trigger(); - } - request->send(200, "text/plain", "FSEQ stopped"); - }); - - // API - FSEQ Status - server.on("/api/fseq/status", HTTP_GET, [](AsyncWebServerRequest *request) { - - DynamicJsonDocument doc(512); - - doc["playing"] = FSEQPlayer::isPlaying(); - doc["file"] = FSEQPlayer::getFileName(); - - String output; - serializeJson(doc, output); - - request->send(200, "application/json", output); - }); -} \ No newline at end of file