diff --git a/package-lock.json b/package-lock.json index 0afeeaafd8..c81072cfc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wled", - "version": "0.16.0-dev", + "version": "0.16.0-alpha", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wled", - "version": "0.16.0-dev", + "version": "0.16.0-alpha", "license": "ISC", "dependencies": { "clean-css": "^5.3.3", diff --git a/usermods/FSEQ/README.md b/usermods/FSEQ/README.md new file mode 100644 index 0000000000..0730143335 --- /dev/null +++ b/usermods/FSEQ/README.md @@ -0,0 +1,117 @@ +# ✨ Usermod FSEQ ✨ + +> **Created by: Andrej Chrcek** + +Welcome to the **Usermod FSEQ** project! This innovative module empowers your WLED setup by enabling FSEQ file playback from an SD card, complete with a sleek web UI and UDP remote control. Dive into a world where creativity meets functionality and transform your lighting experience. + +# SWEB UI http://yourIP/sd/ui + +# SD & FSEQ Usermod for WLED + +This usermod adds support for playing FSEQ files from an SD card and provides a web interface for managing SD files and controlling FSEQ playback via HTTP and UDP. It also supports configurable SPI pin settings for SD SPI. The usermod exposes several HTTP endpoints for file management and playback control. + +--- + +## Features + +- **FSEQ Playback:** Play FSEQ files from an SD card. +- **Web UI:** Manage SD files (list, upload, delete) and control FSEQ playback. +- **UDP Synchronization:** Remote control via UDP packets. +- **Configurable SPI Pins:** SPI pin assignments are configurable through WLED’s configuration JSON. + +--- + +## Installation + +### 1. Configure PlatformIO + +Add the following configuration to your `platformio_override.ini` (or `platformio.ini`) file: + +```ini +[env:esp32dev_V4] +build_flags = + -D WLED_USE_SD_SPI + -D USERMOD_FPP + -D USERMOD_FSEQ + -I wled00/src/dependencies/json + ; optional: + ; -D WLED_DEBUG + + +### 2. Update the WLED Web Interface (Optional) + +To integrate the new FSEQ functionality into the WLED UI, add a new button to the navigation area in your `wled00/data/index.htm` file. For example: + + + + +### 3. Modify usermods_list.cpp + +Register the FSEQ usermod by adding the following lines to `usermods_list.cpp`. Ensure no conflicting modules are included: + +```cpp +#ifdef USERMOD_FSEQ + #include "../usermods/FSEQ/usermod_fseq.h" +#endif + +#ifdef USERMOD_FSEQ + UsermodManager::add(new UsermodFseq()); +#endif + +// Remove or comment out any conflicting SD card usermod: +// //#include "../usermods/sd_card/usermod_sd_card.h" +// //#ifdef SD_ADAPTER +// //UsermodManager::add(new UsermodSdCard()); +// //#endif + +HTTP Endpoints + +The following endpoints are available: + • GET /sd/ui + Description: Returns an HTML page with the SD & FSEQ Manager interface. + Usage: Open this URL in a browser. + • GET /sd/list + Description: Displays an HTML page listing all files on the SD card, with options to delete files and a form to upload new files. + Usage: Access this URL to view the SD file list. + • POST /sd/upload + Description: Handles file uploads to the SD card using a multipart/form-data POST request. + Usage: Use a file upload form or an HTTP client. + • GET /sd/delete + Description: Deletes a specified file from the SD card. Requires a query parameter path indicating the file path. + Usage: Example: /sd/delete?path=/example.fseq. + • GET /fseq/list + Description: Returns an HTML page listing all FSEQ files (with .fseq or .FSEQ extensions) on the SD card. Each file includes a button to play it. + Usage: Open this URL in a browser to view and interact with the file list. + • GET /fseq/start + Description: Starts playback of a selected FSEQ file. Requires a file query parameter and optionally a t parameter (time offset in seconds). + Usage: Example: /fseq/start?file=/animation.fseq&t=10. + • GET /fseq/stop + Description: Stops the current FSEQ playback and clears the active playback session. + Usage: Send an HTTP GET request to stop playback. + • GET /fseqfilelist + Description: Returns a JSON list of all FSEQ files found on the SD card. + Usage: Open this URL or send an HTTP GET request to retrieve the file list. + + +Configurable SPI Pin Settings + +The default SPI pin assignments for SD SPI are defined as follows: + +#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 + +These values can be modified via WLED’s configuration JSON using the addToConfig() and readFromConfig() methods. This allows you to change the pin settings without recompiling the firmware. + +Summary + +The SD & FSEQ Usermod for WLED enables FSEQ file playback from an SD card with a full-featured web UI and UDP synchronization for remote control. With configurable SPI pin settings, this usermod integrates seamlessly into WLED, providing additional functionality without modifying core code. + +For further customization or support, please refer to the project documentation or open an issue on GitHub. + diff --git a/usermods/FSEQ/fseq_player.cpp b/usermods/FSEQ/fseq_player.cpp new file mode 100644 index 0000000000..70fe94dc58 --- /dev/null +++ b/usermods/FSEQ/fseq_player.cpp @@ -0,0 +1,233 @@ +#include "fseq_player.h" +#include +#include "wled.h" + +#ifdef WLED_USE_SD_SPI + #include + #include +#elif defined(WLED_USE_SD_MMC) + #include "SD_MMC.h" +#endif + +File FSEQPlayer::recordingFile; +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; + +inline uint32_t FSEQPlayer::readUInt32() { + char buffer[4]; + if (recordingFile.readBytes(buffer, 4) < 4) return 0; + return (uint32_t)buffer[0] | ((uint32_t)buffer[1] << 8) | ((uint32_t)buffer[2] << 16) | ((uint32_t)buffer[3] << 24); +} + +inline uint32_t FSEQPlayer::readUInt24() { + char buffer[3]; + if (recordingFile.readBytes(buffer, 3) < 3) return 0; + return (uint32_t)buffer[0] | ((uint32_t)buffer[1] << 8) | ((uint32_t)buffer[2] << 16); +} + +inline uint16_t FSEQPlayer::readUInt16() { + char buffer[2]; + if (recordingFile.readBytes(buffer, 2) < 2) return 0; + return (uint16_t)buffer[0] | ((uint16_t)buffer[1] << 8); +} + +inline uint8_t FSEQPlayer::readUInt8() { + char buffer[1]; + if (recordingFile.readBytes(buffer, 1) < 1) return 0; + return (uint8_t)buffer[0]; +} + +bool FSEQPlayer::fileOnSD(const char* filepath) { + uint8_t cardType = SD_ADAPTER.cardType(); + if(cardType == CARD_NONE) return false; + return SD_ADAPTER.exists(filepath); +} + +bool FSEQPlayer::fileOnFS(const char* filepath) { + return false; +} + +void FSEQPlayer::printHeaderInfo() { + DEBUG_PRINTLN("FSEQ file header:"); + DEBUG_PRINTF(" channel_data_offset = %d\n", file_header.channel_data_offset); + DEBUG_PRINTF(" minor_version = %d\n", file_header.minor_version); + DEBUG_PRINTF(" major_version = %d\n", file_header.major_version); + DEBUG_PRINTF(" header_length = %d\n", file_header.header_length); + DEBUG_PRINTF(" channel_count = %d\n", file_header.channel_count); + DEBUG_PRINTF(" frame_count = %d\n", file_header.frame_count); + DEBUG_PRINTF(" step_time = %d\n", file_header.step_time); + DEBUG_PRINTF(" flags = %d\n", file_header.flags); +} + +void FSEQPlayer::processFrameData() { + uint16_t packetLength = file_header.channel_count; + uint16_t lastLed = min(playbackLedStop, uint16_t(playbackLedStart + (packetLength / 3))); + char frame_data[buffer_size]; + CRGB* crgb = reinterpret_cast(frame_data); + uint16_t bytes_remaining = packetLength; + uint16_t index = playbackLedStart; + while (index < lastLed && bytes_remaining > 0) { + uint16_t length = min(bytes_remaining, buffer_size); + 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) break; + } + } + strip.show(); + realtimeLock(3000, REALTIME_MODE_FSEQ); + next_time = now + file_header.step_time; +} + +bool FSEQPlayer::stopBecauseAtTheEnd() { + if (!recordingFile.available()) { + if (recordingRepeats == RECORDING_REPEAT_LOOP) { + // Reset file pointer and frame counter for continuous loop + recordingFile.seek(0); + frame = 0; + } else if (recordingRepeats > 0) { + recordingFile.seek(0); + recordingRepeats--; + frame = 0; + DEBUG_PRINTF("Repeat recording again for: %d\n", recordingRepeats); + } else { + DEBUG_PRINTLN("Finished playing recording, disabling realtime mode"); + realtimeLock(10, REALTIME_MODE_INACTIVE); + recordingFile.close(); + clearLastPlayback(); + return true; + } + } + return false; +} + +void FSEQPlayer::playNextRecordingFrame() { + if (stopBecauseAtTheEnd()) return; + uint32_t offset = file_header.channel_count * frame++; + offset += file_header.channel_data_offset; + if (!recordingFile.seek(offset)) { + if (recordingFile.position() != offset) { + DEBUG_PRINTLN("Failed to seek to proper offset for channel data!"); + return; + } + } + processFrameData(); +} + +void FSEQPlayer::handlePlayRecording() { + now = millis(); + if (realtimeMode != REALTIME_MODE_FSEQ) return; + if (now < next_time) return; + playNextRecordingFrame(); +} + +void FSEQPlayer::loadRecording(const char* filepath, uint16_t startLed, uint16_t stopLed, float secondsElapsed) { + if (recordingFile.available()) { + clearLastPlayback(); + recordingFile.close(); + } + 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); + if (fileOnSD(filepath)) { + DEBUG_PRINTF("Read file from SD: %s\n", filepath); + recordingFile = SD_ADAPTER.open(filepath, "rb"); + } else if (fileOnFS(filepath)) { + DEBUG_PRINTF("Read file from FS: %s\n", filepath); + } else { + DEBUG_PRINTF("File %s not found (%s)\n", filepath, USED_STORAGE_FILESYSTEMS); + return; + } + if ((uint64_t)recordingFile.available() < sizeof(file_header)) { + DEBUG_PRINTF("Invalid file size: %d\n", recordingFile.available()); + recordingFile.close(); + return; + } + for (int i = 0; i < 4; i++) { + file_header.identifier[i] = readUInt8(); + } + file_header.channel_data_offset = readUInt16(); + file_header.minor_version = readUInt8(); + file_header.major_version = readUInt8(); + file_header.header_length = readUInt16(); + file_header.channel_count = readUInt32(); + file_header.frame_count = readUInt32(); + file_header.step_time = readUInt8(); + file_header.flags = readUInt8(); + printHeaderInfo(); + if (file_header.identifier[0] != 'P' || file_header.identifier[1] != 'S' || + file_header.identifier[2] != 'E' || file_header.identifier[3] != 'Q') { + DEBUG_PRINTF("Error reading FSEQ file %s header, invalid identifier\n", filepath); + recordingFile.close(); + return; + } + if (((uint64_t)file_header.channel_count * (uint64_t)file_header.frame_count) + file_header.header_length > UINT32_MAX) { + DEBUG_PRINTF("Error reading FSEQ file %s header, file too long (max 4gb)\n", filepath); + recordingFile.close(); + return; + } + if (file_header.step_time < 1) { + DEBUG_PRINTF("Invalid step time %d, using default %d instead\n", 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; + } + // Set loop mode if secondsElapsed is exactly 1.0f + if (secondsElapsed == 1.0f) { + recordingRepeats = RECORDING_REPEAT_LOOP; + } else { + recordingRepeats = RECORDING_REPEAT_DEFAULT; + } + playNextRecordingFrame(); +} + +void FSEQPlayer::clearLastPlayback() { + for (uint16_t i = playbackLedStart; i < playbackLedStop; i++) { + setRealtimePixel(i, 0, 0, 0, 0); + } + frame = 0; +} + +bool FSEQPlayer::isPlaying() { + return recordingFile && recordingFile.available(); +} + +void FSEQPlayer::syncPlayback(float secondsElapsed) { + if (!isPlaying()) { + DEBUG_PRINTLN("[FSEQ] Sync: Playback not active, cannot sync."); + return; + } + + uint32_t expectedFrame = (uint32_t)((secondsElapsed * 1000.0f) / file_header.step_time); + int32_t diff = (int32_t)expectedFrame - (int32_t)frame; + + if (abs(diff) > 2) { + frame = expectedFrame; + uint32_t offset = file_header.channel_data_offset + file_header.channel_count * frame; + if (recordingFile.seek(offset)) { + DEBUG_PRINTF("[FSEQ] Sync: Adjusted frame to %lu (diff=%ld)\n", expectedFrame, diff); + } else { + DEBUG_PRINTLN("[FSEQ] Sync: Failed to seek to new frame"); + } + } else { + DEBUG_PRINTF("[FSEQ] Sync: No adjustment needed (current frame: %lu, expected: %lu)\n", frame, expectedFrame); + } +} \ No newline at end of file diff --git a/usermods/FSEQ/fseq_player.h b/usermods/FSEQ/fseq_player.h new file mode 100644 index 0000000000..0585873654 --- /dev/null +++ b/usermods/FSEQ/fseq_player.h @@ -0,0 +1,71 @@ +#ifndef FSEQ_PLAYER_H +#define FSEQ_PLAYER_H + +#ifndef RECORDING_REPEAT_LOOP + #define RECORDING_REPEAT_LOOP -1 +#endif +#ifndef RECORDING_REPEAT_DEFAULT + #define RECORDING_REPEAT_DEFAULT 0 +#endif +#ifndef REALTIME_MODE_FSEQ + #define REALTIME_MODE_FSEQ 3 +#endif + +#include "wled.h" +#ifdef WLED_USE_SD_SPI + #include + #include +#elif defined(WLED_USE_SD_MMC) + #include "SD_MMC.h" +#endif + +class FSEQPlayer { +public: + struct FileHeader { + uint8_t identifier[4]; + uint16_t channel_data_offset; + uint8_t minor_version; + uint8_t major_version; + uint16_t header_length; + uint32_t channel_count; + uint32_t frame_count; + uint8_t step_time; + uint8_t flags; + }; + + static void loadRecording(const char* filepath, uint16_t startLed, uint16_t stopLed, float secondsElapsed = 0.0f); + static void handlePlayRecording(); + static void clearLastPlayback(); + static void syncPlayback(float secondsElapsed); + static bool isPlaying(); + +private: + FSEQPlayer() {} + + static const int FSEQ_DEFAULT_STEP_TIME = 50; + + static File recordingFile; + static uint8_t colorChannels; + 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; + + static inline uint32_t readUInt32(); + static inline uint32_t readUInt24(); + static inline uint16_t readUInt16(); + static inline uint8_t readUInt8(); + + static bool fileOnSD(const char* filepath); + static bool fileOnFS(const char* filepath); + static void printHeaderInfo(); + static void processFrameData(); + static bool stopBecauseAtTheEnd(); + static void playNextRecordingFrame(); +}; + +#endif // FSEQ_PLAYER_H \ No newline at end of file diff --git a/usermods/FSEQ/sd_manager.cpp b/usermods/FSEQ/sd_manager.cpp new file mode 100644 index 0000000000..74c056c82f --- /dev/null +++ b/usermods/FSEQ/sd_manager.cpp @@ -0,0 +1,45 @@ +#include "sd_manager.h" + +bool SDManager::begin() { +#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(); +} + +String SDManager::listFiles(const char* dirname) { + String result = ""; + File root = SD_ADAPTER.open(dirname); + if(!root) { + result += "
  • Failed to open directory: "; + result += dirname; + result += "
  • "; + return result; + } + if(!root.isDirectory()){ + result += "
  • Not a directory: "; + result += dirname; + result += "
  • "; + return result; + } + File file = root.openNextFile(); + while(file) { + result += "
  • "; + result += file.name(); + result += " (" + String(file.size()) + " bytes)
  • "; + file.close(); + file = root.openNextFile(); + } + root.close(); + return result; +} + +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 new file mode 100644 index 0000000000..c5ac79c67d --- /dev/null +++ b/usermods/FSEQ/sd_manager.h @@ -0,0 +1,22 @@ +#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(); + String listFiles(const char* dirname); + 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 new file mode 100644 index 0000000000..d3c6c61027 --- /dev/null +++ b/usermods/FSEQ/usermod_fpp.h @@ -0,0 +1,516 @@ +#pragma once + +#include "wled.h" +#include "usermod_fseq.h" // Contains FSEQ playback logic and getter methods for pins + +#ifdef WLED_USE_SD_SPI + #include + #include +#elif defined(WLED_USE_SD_MMC) + #include "SD_MMC.h" +#endif + +#include +#include +#include + +// ----- Minimal WriteBufferingStream Implementation ----- +// This class buffers data before writing it to an underlying Stream. +class WriteBufferingStream : public Stream { + public: + WriteBufferingStream(Stream &upstream, size_t capacity) : _upstream(upstream) { + _capacity = capacity; + _buffer = (uint8_t*)malloc(capacity); + _offset = 0; + if (!_buffer) { + DEBUG_PRINTLN(F("[WBS] ERROR: Buffer allocation failed")); + } + } + ~WriteBufferingStream() { + flush(); + if (_buffer) free(_buffer); + } + // Write a block of data to the buffer + size_t write(const uint8_t *buffer, size_t size) override { + size_t total = 0; + while (size > 0) { + size_t space = _capacity - _offset; + size_t toCopy = (size < space) ? size : space; + memcpy(_buffer + _offset, buffer, toCopy); + _offset += toCopy; + buffer += toCopy; + size -= toCopy; + total += toCopy; + if (_offset == _capacity) flush(); + } + return total; + } + // Write a single byte + size_t write(uint8_t b) override { + return write(&b, 1); + } + // Flush the buffer to the upstream stream + void flush() override { + if (_offset > 0) { + _upstream.write(_buffer, _offset); + _offset = 0; + } + _upstream.flush(); + } + int available() override { return _upstream.available(); } + int read() override { return _upstream.read(); } + int peek() override { return _upstream.peek(); } + private: + Stream &_upstream; + uint8_t *_buffer = nullptr; + size_t _capacity = 0; + size_t _offset = 0; +}; +// ----- End WriteBufferingStream ----- + +#define FILE_UPLOAD_BUFFER_SIZE (563 * 7) + +// Definitions for UDP (FPP) synchronization +#define CTRL_PKT_SYNC 1 +#define CTRL_PKT_PING 4 +#define CTRL_PKT_BLANK 3 + +// UDP port for FPP discovery/synchronization +const uint16_t UDP_SYNC_PORT = 32320; + +// Inline functions to write 16-bit and 32-bit values +static inline void write16(uint8_t* dest, uint16_t value) { + dest[0] = (value >> 8) & 0xff; + dest[1] = value & 0xff; +} + +static inline void write32(uint8_t* dest, uint32_t value) { + dest[0] = (value >> 24) & 0xff; + dest[1] = (value >> 16) & 0xff; + dest[2] = (value >> 8) & 0xff; + dest[3] = value & 0xff; +} + +// Structure for the synchronization packet +// Using pragma pack to avoid any padding issues +#pragma pack(push, 1) +struct FPPMultiSyncPacket { + uint8_t header[4]; // e.g. "FPPD" + uint8_t packet_type; // e.g. CTRL_PKT_SYNC + uint16_t data_len; // data length + uint8_t sync_action; // action: start, stop, sync, open, etc. + uint8_t sync_type; // sync type, e.g. 0 for FSEQ + uint32_t frame_number; // current frame number + float seconds_elapsed; // elapsed seconds + char filename[64]; // name of the file to play + uint8_t raw[128]; // raw packet data +}; +#pragma pack(pop) + +// UsermodFPP class: Implements FPP (FSEQ/UDP) functionality +class UsermodFPP : public Usermod { + private: + AsyncUDP udp; // UDP object for FPP discovery/sync + bool udpStarted = false; // Flag to indicate UDP listener status + const IPAddress multicastAddr = IPAddress(239, 70, 80, 80); // Multicast address + const uint16_t udpPort = UDP_SYNC_PORT; // UDP port + + // Variables for FSEQ file upload + File currentUploadFile; + String currentUploadFileName = ""; + unsigned long uploadStartTime = 0; + WriteBufferingStream* uploadStream = nullptr; + + // Returns device name from server description + String getDeviceName() { + return String(serverDescription); + } + + // Build JSON with system information + String buildSystemInfoJSON() { + DynamicJsonDocument doc(1024); + doc["fppd"] = "running"; + String devName = getDeviceName(); + doc["HostName"] = devName; + doc["HostDescription"] = devName; + doc["Platform"] = "ESPixelStick"; + doc["Variant"] = "ESPixelStick-ESP32"; + doc["Mode"] = 8; + doc["Version"] = "4.x-dev"; + doc["majorVersion"] = 4; + doc["minorVersion"] = 0; + doc["typeId"] = 195; + JsonObject utilization = doc.createNestedObject("Utilization"); + utilization["MemoryFree"] = ESP.getFreeHeap(); + utilization["Uptime"] = millis(); + doc["rssi"] = WiFi.RSSI(); + JsonArray ips = doc.createNestedArray("IPS"); + ips.add(WiFi.localIP().toString()); + String json; + serializeJson(doc, json); + return json; + } + + // Build JSON with system status + String buildSystemStatusJSON() { + DynamicJsonDocument doc(1024); + doc["fppd"] = "running"; + String devName = getDeviceName(); + doc["HostName"] = devName; + doc["HostDescription"] = devName; + doc["Platform"] = "ESPixelStick"; + doc["Variant"] = "ESPixelStick-ESP32"; + doc["Mode"] = 8; + doc["Version"] = "4.x-dev"; + doc["majorVersion"] = 4; + doc["minorVersion"] = 0; + doc["typeId"] = 195; + doc["current_sequence"] = ""; + doc["playlist"] = ""; + doc["seconds_elapsed"] = 0; + doc["seconds_played"] = 0; + doc["seconds_remaining"] = 0; + doc["time_elapsed"] = "00:00"; + doc["time_remaining"] = "00:00"; + doc["status"] = 0; + doc["status_name"] = "idle"; + String json; + serializeJson(doc, json); + return json; + } + + // Build JSON for FPP multi-sync systems + String buildFppdMultiSyncSystemsJSON() { + DynamicJsonDocument doc(512); + String devName = getDeviceName(); + doc["hostname"] = devName; + doc["id"] = devName; + doc["ip"] = WiFi.localIP().toString(); + doc["version"] = "4.x-dev"; + doc["hardwareType"] = "ESPixelStick-ESP32"; + doc["type"] = 195; + doc["num_chan"] = 2052; + doc["NumPixelPort"] = 5; + doc["NumSerialPort"] = 0; + String json; + serializeJson(doc, json); + return json; + } + + // UDP - send a ping packet + void sendPingPacket(IPAddress destination = IPAddress(255,255,255,255)) { + uint8_t buf[301]; + memset(buf, 0, sizeof(buf)); + buf[0] = 'F'; buf[1] = 'P'; buf[2] = 'P'; buf[3] = 'D'; + buf[4] = 0x04; // ping type + uint16_t dataLen = 294; + buf[5] = (dataLen >> 8) & 0xFF; + buf[6] = dataLen & 0xFF; + buf[7] = 0x03; + buf[8] = 0x00; + buf[9] = 0xC3; + uint16_t versionMajor = 0, versionMinor = 16; + buf[10] = (versionMajor >> 8) & 0xFF; + buf[11] = versionMajor & 0xFF; + buf[12] = (versionMinor >> 8) & 0xFF; + buf[13] = versionMinor & 0xFF; + buf[14] = 0x08; + IPAddress ip = WiFi.localIP(); + buf[15] = ip[0]; buf[16] = ip[1]; buf[17] = ip[2]; buf[18] = ip[3]; + String hostName = getDeviceName(); + if(hostName.length() > 32) hostName = hostName.substring(0,32); + for (int i = 0; i < 32; i++) { + buf[19 + i] = (i < hostName.length()) ? hostName[i] : 0; + } + String verStr = "4.x-dev"; + for (int i = 0; i < 16; i++) { + buf[51 + i] = (i < verStr.length()) ? verStr[i] : 0; + } + String hwType = "ESPixelStick-ESP32"; + for (int i = 0; i < 16; i++) { + buf[67 + i] = (i < hwType.length()) ? hwType[i] : 0; + } + udp.writeTo(buf, sizeof(buf), destination, udpPort); + } + + // UDP - send a sync message + void sendSyncMessage(uint8_t action, const String &fileName, uint32_t currentFrame, float secondsElapsed) { + FPPMultiSyncPacket syncPacket; + // Fill in header "FPPD" + syncPacket.header[0] = 'F'; + syncPacket.header[1] = 'P'; + syncPacket.header[2] = 'P'; + syncPacket.header[3] = 'D'; + syncPacket.packet_type = CTRL_PKT_SYNC; + write16((uint8_t*)&syncPacket.data_len, sizeof(syncPacket)); + syncPacket.sync_action = action; + syncPacket.sync_type = 0; // FSEQ synchronization + write32((uint8_t*)&syncPacket.frame_number, currentFrame); + syncPacket.seconds_elapsed = secondsElapsed; + strncpy(syncPacket.filename, fileName.c_str(), sizeof(syncPacket.filename)-1); + syncPacket.filename[sizeof(syncPacket.filename)-1] = 0x00; + // Send to both broadcast and multicast addresses + udp.writeTo((uint8_t*)&syncPacket, sizeof(syncPacket), IPAddress(255,255,255,255), udpPort); + udp.writeTo((uint8_t*)&syncPacket, sizeof(syncPacket), multicastAddr, udpPort); + } + + // UDP - process received packet + void processUdpPacket(AsyncUDPPacket packet) { + // Print the raw UDP packet in hex format for debugging + DEBUG_PRINTLN(F("[FPP] Raw UDP Packet:")); + for (size_t i = 0; i < packet.length(); i++) { + DEBUG_PRINTF("%02X ", packet.data()[i]); + } + DEBUG_PRINTLN(); + + if (packet.length() < 4) return; + if(packet.data()[0] != 'F' || packet.data()[1] != 'P' || + packet.data()[2] != 'P' || packet.data()[3] != 'D') + return; + uint8_t packetType = packet.data()[4]; + switch(packetType) { + case CTRL_PKT_SYNC: { + FPPMultiSyncPacket* syncPacket = reinterpret_cast(packet.data()); + DEBUG_PRINTLN(F("[FPP] Received UDP sync packet")); + // Print detailed sync packet information: + DEBUG_PRINTF("[FPP] Sync Packet - Action: %d\n", syncPacket->sync_action); + DEBUG_PRINT(F("[FPP] Filename: ")); + DEBUG_PRINTLN(syncPacket->filename); + DEBUG_PRINTF("[FPP] Frame Number: %lu\n", syncPacket->frame_number); + DEBUG_PRINTF("[FPP] Seconds Elapsed: %.2f\n", syncPacket->seconds_elapsed); + ProcessSyncPacket(syncPacket->sync_action, String(syncPacket->filename), syncPacket->seconds_elapsed); + break; + } + case CTRL_PKT_PING: + DEBUG_PRINTLN(F("[FPP] Received UDP ping packet")); + sendPingPacket(packet.remoteIP()); + break; + 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")); + break; + } + } + + // Process sync command with detailed debug output + void ProcessSyncPacket(uint8_t action, String fileName, float secondsElapsed) { + // Ensure the filename is absolute + if (!fileName.startsWith("/")) { + fileName = "/" + fileName; + } + + DEBUG_PRINTLN(F("[FPP] ProcessSyncPacket: Sync command received")); + DEBUG_PRINTF("[FPP] Action: %d\n", action); + DEBUG_PRINT(F("[FPP] FileName: ")); + DEBUG_PRINTLN(fileName); + DEBUG_PRINTF("[FPP] Seconds Elapsed: %.2f\n", secondsElapsed); + + switch (action) { + case 0: // SYNC_PKT_START + FSEQPlayer::loadRecording(fileName.c_str(), 0, strip.getLength(), 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); + } else { + FSEQPlayer::syncPlayback(secondsElapsed); + } + break; + case 3: // SYNC_PKT_OPEN + DEBUG_PRINTLN(F("[FPP] Open command received – metadata request (not implemented)")); + break; + default: + DEBUG_PRINTLN(F("[FPP] ProcessSyncPacket: Unknown sync action")); + break; + } + } + + public: + static const char _name[]; + + // Setup function called once at startup + void setup() { + DEBUG_PRINTF("[%s] FPP Usermod loaded\n", _name); +#ifdef WLED_USE_SD_SPI + int8_t csPin = UsermodFseq::getCsPin(); + int8_t sckPin = UsermodFseq::getSckPin(); + int8_t misoPin = UsermodFseq::getMisoPin(); + int8_t mosiPin = UsermodFseq::getMosiPin(); + if (!SD.begin(csPin)) { + DEBUG_PRINTF("[%s] ERROR: SD.begin() failed with CS pin %d!\n", _name, csPin); + } else { + DEBUG_PRINTF("[%s] SD card initialized (SPI) with CS pin %d\n", _name, csPin); + } +#elif defined(WLED_USE_SD_MMC) + if (!SD_MMC.begin()) { + DEBUG_PRINTF("[%s] ERROR: SD_MMC.begin() failed!\n", _name); + } else { + DEBUG_PRINTF("[%s] SD card initialized (MMC)\n", _name); + } +#endif + + // Register API endpoints + server.on("/api/system/info", HTTP_GET, [this](AsyncWebServerRequest *request) { + String json = buildSystemInfoJSON(); + request->send(200, "application/json", json); + }); + server.on("/api/system/status", HTTP_GET, [this](AsyncWebServerRequest *request) { + String json = buildSystemStatusJSON(); + request->send(200, "application/json", json); + }); + server.on("/api/fppd/multiSyncSystems", HTTP_GET, [this](AsyncWebServerRequest *request) { + String json = buildFppdMultiSyncSystemsJSON(); + request->send(200, "application/json", json); + }); + // Other API endpoints as needed... + + // Endpoint for file upload (raw, application/octet-stream) + server.on("/fpp", HTTP_POST, + [this](AsyncWebServerRequest *request) { + if (uploadStream != nullptr) { + uploadStream->flush(); + delete uploadStream; + uploadStream = nullptr; + } + if (currentUploadFile) { + currentUploadFile.close(); + } + currentUploadFileName = ""; + request->send(200, "text/plain", "Upload complete"); + }, + NULL, + [this](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + if (index == 0) { + DEBUG_PRINTLN("[FPP] Received upload parameters:"); + for (uint8_t i = 0; i < request->params(); i++) { + AsyncWebParameter* p = request->getParam(i); + DEBUG_PRINTF("[FPP] Param %s = %s\n", p->name().c_str(), p->value().c_str()); + } + String fileParam = ""; + if (request->hasParam("filename")) { + fileParam = request->arg("filename"); + } + DEBUG_PRINTF("[FPP] fileParam = %s\n", fileParam.c_str()); + currentUploadFileName = (fileParam != "") ? (fileParam.startsWith("/") ? fileParam : "/" + fileParam) : "/default.fseq"; + DEBUG_PRINTF("[FPP] Using filename: %s\n", currentUploadFileName.c_str()); + if (SD.exists(currentUploadFileName.c_str())) { + SD.remove(currentUploadFileName.c_str()); + } + currentUploadFile = SD.open(currentUploadFileName.c_str(), FILE_WRITE); + if (!currentUploadFile) { + DEBUG_PRINTLN(F("[FPP] ERROR: Failed to open file for writing")); + return; + } + uploadStream = new WriteBufferingStream(currentUploadFile, FILE_UPLOAD_BUFFER_SIZE); + uploadStartTime = millis(); + } + if (uploadStream != nullptr) { + uploadStream->write(data, len); + } + if (index + len >= total) { + if (uploadStream != nullptr) { + uploadStream->flush(); + delete uploadStream; + uploadStream = nullptr; + } + currentUploadFile.close(); + currentUploadFileName = ""; + } + } + ); + + // Endpoint to list FSEQ files on SD card + server.on("/fseqfilelist", HTTP_GET, [](AsyncWebServerRequest *request) { + DynamicJsonDocument doc(1024); + JsonArray files = doc.createNestedArray("files"); + + File root = SD_ADAPTER.open("/"); + if(root && root.isDirectory()){ + File file = root.openNextFile(); + while(file){ + String name = file.name(); + if(name.endsWith(".fseq") || name.endsWith(".FSEQ")){ + JsonObject fileObj = files.createNestedObject(); + fileObj["name"] = name; + fileObj["size"] = file.size(); + } + file.close(); + file = root.openNextFile(); + } + } + else { + doc["error"] = "Cannot open SD root directory"; + } + + String json; + serializeJson(doc, json); + 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)) { + if (udp.listenMulticast(multicastAddr, udpPort)) { + udpStarted = true; + udp.onPacket([this](AsyncUDPPacket packet) { + processUdpPacket(packet); + }); + DEBUG_PRINTLN(F("[FPP] UDP listener started on multicast")); + } + } + } + + // Main loop function + void loop() { + if (!udpStarted && (WiFi.status() == WL_CONNECTED)) { + if (udp.listenMulticast(multicastAddr, udpPort)) { + udpStarted = true; + udp.onPacket([this](AsyncUDPPacket packet) { + processUdpPacket(packet); + }); + DEBUG_PRINTLN(F("[FPP] UDP listener started on multicast")); + } + } + // Process FSEQ playback + FSEQPlayer::handlePlayRecording(); + } + + uint16_t getId() { + return USERMOD_ID_SD_CARD; + } + void addToConfig(JsonObject &root) { } + bool readFromConfig(JsonObject &root) { return true; } +}; + +const char UsermodFPP::_name[] PROGMEM = "FPP Connect"; \ No newline at end of file diff --git a/usermods/FSEQ/usermod_fseq.h b/usermods/FSEQ/usermod_fseq.h new file mode 100644 index 0000000000..b9b4bda9a6 --- /dev/null +++ b/usermods/FSEQ/usermod_fseq.h @@ -0,0 +1,181 @@ +#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 + +#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 + #define SD_ADAPTER SD + #else + #define SD_ADAPTER SD_MMC + #endif + #endif +#endif + +#ifdef WLED_USE_SD_SPI + #ifndef SPI_PORT_DEFINED + inline SPIClass spiPort = SPIClass(VSPI); + #define SPI_PORT_DEFINED + #endif +#endif + +#include "wled.h" +#include "../usermods/FSEQ/fseq_player.h" +#include "../usermods/FSEQ/fseq_player.cpp" +#include "../usermods/FSEQ/sd_manager.h" +#include "../usermods/FSEQ/sd_manager.cpp" +#include "../usermods/FSEQ/web_ui_manager.h" +#include "../usermods/FSEQ/web_ui_manager.cpp" + +// Usermod for FSEQ playback with UDP and web UI support +class UsermodFseq : public Usermod { +private: + WebUIManager webUI; // Web UI Manager module (handles endpoints) + static const char _name[]; // for storing usermod name in config + +public: + // 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 web endpoints defined in WebUIManager + webUI.registerEndpoints(); + } + + // Loop function called continuously + void loop() { + // Process FSEQ playback (includes UDP sync commands) + FSEQPlayer::handlePlayRecording(); + } + + // Unique ID for the usermod + uint16_t getId() override { + return USERMOD_ID_SD_CARD; + } + + // Add a link in the Info tab to your SD UI + void addToJsonInfo(JsonObject &root) override { + JsonObject user = root["u"]; + if (user.isNull()) { + user = root.createNestedObject("u"); + } + // Create an array with two items: label and value + JsonArray arr = user.createNestedArray("Usermod FSEQ UI"); + + String ip = WiFi.localIP().toString(); + arr.add("http://" + ip + "/fsequi"); // value + } + + // Save your SPI pins to WLED config JSON + 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; + + 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(); // 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() { + // Deinit SD if needed + SD_ADAPTER.end(); + // Reallocate pins + PinManager::deallocatePin(configPinSourceSelect, PinOwner::UM_SdCard); + PinManager::deallocatePin(configPinSourceClock, PinOwner::UM_SdCard); + PinManager::deallocatePin(configPinPoci, PinOwner::UM_SdCard); + PinManager::deallocatePin(configPinPico, 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 +}; + +// Provide a usermod name for config storage +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 \ No newline at end of file diff --git a/usermods/FSEQ/web_ui_manager.cpp b/usermods/FSEQ/web_ui_manager.cpp new file mode 100644 index 0000000000..39a40c1761 --- /dev/null +++ b/usermods/FSEQ/web_ui_manager.cpp @@ -0,0 +1,344 @@ +#include "web_ui_manager.h" +#include "sd_manager.h" +#include "fseq_player.h" + +static const char PAGE_HTML[] PROGMEM = R"rawliteral( + + + + + Unified UI + + + + + + + + + + +
    +

    SD Files

    +
      +

      Upload File

      +
      +

      + +
      +
      +
      + +
      +

      FSEQ Files

      +
        +
        + + +)rawliteral"; + +void WebUIManager::registerEndpoints() { + + // Main UI page (navigation, SD and FSEQ tabs) + server.on("/fsequi", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send_P(200, "text/html", PAGE_HTML); + }); + + // API - List SD files (size in KB) + server.on("/api/sd/list", HTTP_GET, [](AsyncWebServerRequest *request){ + File root = SD_ADAPTER.open("/"); + String json = "["; + if(root && root.isDirectory()){ + bool first = true; + File file = root.openNextFile(); + while(file){ + if(!first) json += ","; + first = false; + float sizeKB = file.size() / 1024.0; + json += "{"; + json += "\"name\":\"" + String(file.name()) + "\","; + json += "\"size\":" + String(sizeKB, 2); + json += "}"; + file.close(); + file = root.openNextFile(); + } + } + root.close(); + json += "]"; + request->send(200, "application/json", json); + }); + + // API - List FSEQ files + server.on("/api/fseq/list", HTTP_GET, [](AsyncWebServerRequest *request){ + File root = SD_ADAPTER.open("/"); + String json = "["; + if(root && root.isDirectory()){ + bool first = true; + File file = root.openNextFile(); + while(file){ + String name = file.name(); + if(name.endsWith(".fseq") || name.endsWith(".FSEQ")){ + if(!first) json += ","; + first = false; + json += "{"; + json += "\"name\":\"" + name + "\""; + json += "}"; + } + file.close(); + file = root.openNextFile(); + } + } + root.close(); + json += "]"; + request->send(200, "application/json", json); + }); + + // API - File Upload + server.on("/api/sd/upload", HTTP_POST, + [](AsyncWebServerRequest *request){ + request->send(200, "text/plain", "Upload complete"); + }, + [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final){ + static File uploadFile; + if(index == 0) { + if(!filename.startsWith("/")) filename = "/" + filename; + uploadFile = SD_ADAPTER.open(filename.c_str(), FILE_WRITE); + } + if(uploadFile) { + uploadFile.write(data, len); + if(final) uploadFile.close(); + } + } + ); + + // API - File Delete + server.on("/api/sd/delete", HTTP_GET, [](AsyncWebServerRequest *request){ + if(!request->hasArg("path")) { + request->send(400, "text/plain", "Missing path"); + return; + } + String path = request->arg("path"); + if (!path.startsWith("/")) path = "/" + path; + 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_GET, [](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); + request->send(200, "text/plain", "FSEQ started"); + }); + + // API - Start FSEQ in loop mode + server.on("/api/fseq/startloop", HTTP_GET, [](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; + // Passing 1.0f enables loop mode in loadRecording() + FSEQPlayer::loadRecording(filepath.c_str(), 0, uint16_t(-1), 1.0f); + request->send(200, "text/plain", "FSEQ loop started"); + }); + + // API - Stop FSEQ + server.on("/api/fseq/stop", HTTP_GET, [](AsyncWebServerRequest *request){ + FSEQPlayer::clearLastPlayback(); + realtimeLock(10, REALTIME_MODE_INACTIVE); + request->send(200, "text/plain", "FSEQ stopped"); + }); + + // API - FSEQ Status + server.on("/api/fseq/status", HTTP_GET, [](AsyncWebServerRequest *request){ + bool playing = FSEQPlayer::isPlaying(); + String json = "{\"playing\":"; + json += (playing ? "true" : "false"); + json += "}"; + request->send(200, "application/json", json); + }); +} \ No newline at end of file diff --git a/usermods/FSEQ/web_ui_manager.h b/usermods/FSEQ/web_ui_manager.h new file mode 100644 index 0000000000..7621341f6c --- /dev/null +++ b/usermods/FSEQ/web_ui_manager.h @@ -0,0 +1,13 @@ +#ifndef WEB_UI_MANAGER_H +#define WEB_UI_MANAGER_H + +#include "wled.h" + + +class WebUIManager { + public: + WebUIManager() {} + void registerEndpoints(); +}; + +#endif // WEB_UI_MANAGER_H \ No newline at end of file diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index df4715d148..3de7247e7a 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -11,6 +11,14 @@ */ //#include "../usermods/EXAMPLE_v2/usermod_v2_example.h" +#ifdef USERMOD_FSEQ + #include "../usermods/FSEQ/usermod_fseq.h" +#endif + +#ifdef USERMOD_FPP + #include "../usermods/FSEQ/usermod_fpp.h" +#endif + #ifdef USERMOD_BATTERY #include "../usermods/Battery/usermod_v2_Battery.h" #endif @@ -191,7 +199,7 @@ #include "SD.h" #include "SPI.h" #endif - #include "../usermods/sd_card/usermod_sd_card.h" + //#include "../usermods/sd_card/usermod_sd_card.h" #endif #ifdef USERMOD_PWM_OUTPUTS @@ -263,6 +271,14 @@ void registerUsermods() */ //UsermodManager::add(new MyExampleUsermod()); + #ifdef USERMOD_FSEQ + UsermodManager::add(new UsermodFseq()); + #endif + + #ifdef USERMOD_FPP + UsermodManager::add(new UsermodFPP()); + #endif + #ifdef USERMOD_BATTERY UsermodManager::add(new UsermodBattery()); #endif @@ -419,9 +435,9 @@ void registerUsermods() UsermodManager::add(new BobLightUsermod()); #endif - #ifdef SD_ADAPTER - UsermodManager::add(new UsermodSdCard()); - #endif + //#ifdef SD_ADAPTER + //UsermodManager::add(new UsermodSdCard()); + //#endif #ifdef USERMOD_PWM_OUTPUTS UsermodManager::add(new PwmOutputsUsermod());