From 64a2ddac50d469c7bb9872258875e41b6f7e4e75 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 15 Nov 2025 17:38:38 -0500 Subject: [PATCH 1/6] Set up Effect as a struct Define an "Effect" struct and move the global list out of WS2812FX. The important change is that the id code is now a property of the Effect instead of an index in to a vector. We require retrieving an ID code for an effect to call through a function. This allows us to change the back end implementation of the numeric ID map in the future. Segment::mode is preserved, but marked as deprecated. Usermods not updated yet. --- wled00/Effects.cpp | 107 +++++++++++++++++++++++++++++ wled00/FX.cpp | 39 ++--------- wled00/FX.h | 95 +++++++++++++++++-------- wled00/FX_fcn.cpp | 88 +++++++++++++++--------- wled00/e131.cpp | 2 +- wled00/fcn_declare.h | 1 - wled00/json.cpp | 60 +++++++++------- wled00/led.cpp | 4 +- wled00/overlay.cpp | 2 +- wled00/set.cpp | 10 +-- wled00/udp.cpp | 4 +- wled00/util.cpp | 160 +++++++++++++++++-------------------------- 12 files changed, 343 insertions(+), 229 deletions(-) create mode 100644 wled00/Effects.cpp diff --git a/wled00/Effects.cpp b/wled00/Effects.cpp new file mode 100644 index 0000000000..a8e61d8ca9 --- /dev/null +++ b/wled00/Effects.cpp @@ -0,0 +1,107 @@ +#include "wled.h" +#include "FX.h" +#include + +// Namespace Effects: Global effects list +// Construct-on-first-use idiom to avoid static initialization fiasco with WS2812FX +static std::vector& _globalEffectsList() { + static std::vector* _g = new std::vector{}; + return *_g; +}; + +// Highest used effect ID +static uint8_t _highestId = 0; + +void Effects::reserve(size_t s) { + _globalEffectsList().reserve(s); +} + +void Effects::addEffect(const Effect* new_effect) { + if (new_effect) { + _globalEffectsList().push_back(new_effect); + auto id = getIdForEffect(new_effect); + if (id > _highestId) { + _highestId = id; + } + } +} + +uint8_t Effects::addEffect(const char *mode_name, effect_function mode_fn, uint8_t id) { + if (id == 255) { // find next available id + // Build a quick bitmap of the used IDs + uint32_t used_ids[8] = {0}; + for(auto& e: _globalEffectsList()) { + auto id = getIdForEffect(e); + if (id != 255) { + // Do math together so the compiler identifies the division+remainder + auto chunk = id/32; + auto bit = id%32; + used_ids[chunk] |= (1u << bit); + } + } + // Now find the lowest value + for(auto chunk = 0U; chunk < 8; ++chunk) { + auto inverted = ~used_ids[chunk]; + if (inverted) { + // found one! + id = (chunk*32) + __builtin_ctz(inverted); // Find lowest set bit with compiler intrinsic + break; + } + } + } + + addEffect(new(std::nothrow) Effect { mode_name, mode_fn, id }); + return id; +} + +// const Effect* Effects::getEffectByName(const char* name) {} TODO + +const Effect* Effects::getEffectById(uint8_t id) { + auto effect_iter = std::find_if(_globalEffectsList().begin(), _globalEffectsList().end(), [=](const Effect* e) { return getIdForEffect(e) == id; }); + if (effect_iter == _globalEffectsList().end()) effect_iter = _globalEffectsList().begin(); // set solid mode, always the first element of the list + return *effect_iter; +} + +uint8_t Effects::getHighestId() { return _highestId; }; + +size_t Effects::getCount() { return _globalEffectsList().size(); } +size_t Effects::getCapacity() { return _globalEffectsList().capacity(); } + +// Range interface: for(auto& effect: Effects.all()) +std::vector::iterator Effects::asRange::begin() { return _globalEffectsList().begin(); } +std::vector::iterator Effects::asRange::end() { return _globalEffectsList().end(); } + + +// Effect parsing utilities +String Effect::getName() const { +#ifdef ESP8266 + char lineBuffer[256]; + strncpy_P(lineBuffer, data, sizeof(lineBuffer)/sizeof(char)-1); + lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\0'; // terminate string + char* p = strchr(lineBuffer, '@'); + if (p) { + *p = '\0'; // terminate there + } + return String(lineBuffer); +#else + char* p = strchr(data, '@'); + if (p) { + return String(data, p - data); + } else { + return String(data); + } +#endif +} + +size_t Effect::getName(char* dest, size_t dest_size) const { + char lineBuffer[256]; + strncpy_P(lineBuffer, data, sizeof(lineBuffer)/sizeof(char)-1); + lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\0'; // terminate string + size_t j = 0; + for (; j < dest_size; j++) { + if (lineBuffer[j] == '\0' || lineBuffer[j] == '@') break; + dest[j] = lineBuffer[j]; + } + dest[j] = 0; // terminate string + return strlen(dest); +} diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 036e97a75c..94601cf4f7 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -902,7 +902,7 @@ static uint16_t chase(uint32_t color1, uint32_t color2, uint32_t color3, bool do uint16_t counter = strip.now * ((SEGMENT.speed >> 2) + 1); uint16_t a = (counter * SEGLEN) >> 16; - bool chase_random = (SEGMENT.mode == FX_MODE_CHASE_RANDOM); + bool chase_random = (Effects::getIdForEffect(SEGMENT.effect) == FX_MODE_CHASE_RANDOM); if (chase_random) { if (a < SEGENV.step) //we hit the start again, choose new color for Chase random { @@ -10750,41 +10750,12 @@ static const char _data_FX_MODE_PS_SPRINGY[] PROGMEM = "PS Springy@Stiffness,Dam ////////////////////////////////////////////////////////////////////////////////////////// // mode data -static const char _data_RESERVED[] PROGMEM = "RSVD"; - -// add (or replace reserved) effect mode and data into vector -// use id==255 to find unallocated gaps (with "Reserved" data string) -// if vector size() is smaller than id (single) data is appended at the end (regardless of id) -// return the actual id used for the effect or 255 if the add failed. -uint8_t WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) { - if (id == 255) { // find empty slot - for (size_t i=1; i<_mode.size(); i++) if (_modeData[i] == _data_RESERVED) { id = i; break; } - } - if (id < _mode.size()) { - if (_modeData[id] != _data_RESERVED) return 255; // do not overwrite an already added effect - _mode[id] = mode_fn; - _modeData[id] = mode_name; - return id; - } else if (_mode.size() < 255) { // 255 is reserved for indicating the effect wasn't added - _mode.push_back(mode_fn); - _modeData.push_back(mode_name); - if (_modeCount < _mode.size()) _modeCount++; - return _mode.size() - 1; - } else { - return 255; // The vector is full so return 255 - } -} void WS2812FX::setupEffectData() { - // Solid must be first! (assuming vector is empty upon call to setup) - _mode.push_back(&mode_static); - _modeData.push_back(_data_FX_MODE_STATIC); - // fill reserved word in case there will be any gaps in the array - for (size_t i=1; i<_modeCount; i++) { - _mode.push_back(&mode_static); - _modeData.push_back(_data_RESERVED); - } - // now replace all pre-allocated effects + // TODO: make this a static initializer for _effects + addEffect(FX_MODE_STATIC, mode_static, _data_FX_MODE_STATIC); + + // now add all pre-allocated effects addEffect(FX_MODE_COPY, &mode_copy_segment, _data_FX_MODE_COPY); // --- 1D non-audio effects --- addEffect(FX_MODE_BLINK, &mode_blink, _data_FX_MODE_BLINK); diff --git a/wled00/FX.h b/wled00/FX.h index 250df2646d..3a9b703fca 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -420,6 +420,56 @@ typedef enum mapping1D2D { class WS2812FX; +// Forward declarations +struct Effect; +namespace Effects { uint8_t getIdForEffect(const Effect*); }; + +// Effect information structure +typedef uint16_t (*effect_function)(); // pointer to mode function +struct Effect { + const char *data; // mode (effect) name and its UI control data + effect_function fcn; // mode (effect) function + /* Future: per-effect constructor function pointer goes here */ + + // Parsed data accessors + String getName() const; + size_t getName(char* dest, size_t dest_size) const; // no-alloc retrieval + + // ID being kept here right now is an implementation detail - use getIdForEffect() to retrieve + private: + friend uint8_t Effects::getIdForEffect(const Effect*); + uint8_t id; // mode (effect) id for legacy table (this may later be moved elsewhere) + + public: + Effect(const char* data_, effect_function fcn_, uint8_t id_ = 255) : data(data_), fcn(fcn_), id(id_) {}; +}; + +// Global effect list +// FUTURE: the effect list could potentially be statically assembled at link time, like the usermod list; addEffect may become a macro +namespace Effects { + void reserve(size_t); // preallocate at least this much space for effect list + void addEffect(const Effect* effect); // add an effect to the global list + uint8_t addEffect(const char *mode_name, effect_function mode_fn, uint8_t id); // Add a non-static effect to the list; returns id + + const Effect* getEffectByName(const char* name); + const Effect* getEffectById(uint8_t id); // Returns an effect pointer by id number; if not found, returns first effect + inline uint8_t getIdForEffect(const Effect* effect) { return effect->id; } // Returns the ID number assigned to an effect, or 255 if none available + uint8_t getHighestId(); // Returns the highest assigned ID value; often used by older code to iterate through fx by id + + inline const char *getModeData(unsigned id = 0) { return getEffectById(id)->data; } + + size_t getCount(); + size_t getCapacity(); + + // Range interface: for(auto& effect: Effects.all()) + struct asRange { + std::vector::iterator begin(); + std::vector::iterator end(); + }; + inline asRange all() { return asRange {}; } +} + + // segment, 76 bytes class Segment { public: @@ -449,7 +499,8 @@ class Segment { uint8_t grouping, spacing; uint8_t opacity, cct; // 0==1900K, 255==10091K // effect data - uint8_t mode; + const Effect* effect; + [[deprecated("Use seg.effect for effect metadata, or Effects::getIdForEffect(Segment.effect) if you require a legacy numeric ID")]] uint8_t mode; uint8_t palette; uint8_t speed; uint8_t intensity; @@ -567,6 +618,8 @@ class Segment { public: +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" Segment(uint16_t sStart=0, uint16_t sStop=30, uint16_t sStartY = 0, uint16_t sStopY = 1) : colors{DEFAULT_COLOR,BLACK,BLACK} , start(sStart) @@ -579,7 +632,8 @@ class Segment { , spacing(0) , opacity(255) , cct(127) - , mode(DEFAULT_MODE) + , effect(Effects::getEffectById(0)) + , mode(0) , palette(0) , speed(DEFAULT_SPEED) , intensity(DEFAULT_INTENSITY) @@ -612,6 +666,7 @@ class Segment { stop = 0; // mark segment as inactive/invalid } } +#pragma GCC diagnostic pop Segment(const Segment &orig); // copy constructor Segment(Segment &&orig) noexcept; // move constructor @@ -822,14 +877,7 @@ class Segment { // main "strip" class (108 bytes) class WS2812FX { - typedef uint16_t (*mode_ptr)(); // pointer to mode function typedef void (*show_callback)(); // pre show callback - typedef struct ModeData { - uint8_t _id; // mode (effect) id - mode_ptr _fcn; // mode (effect) function - const char *_data; // mode (effect) name and its UI control data - ModeData(uint8_t id, uint16_t (*fcn)(void), const char *data) : _id(id), _fcn(fcn), _data(data) {} - } mode_data_t; public: @@ -861,29 +909,20 @@ class WS2812FX { _triggered(false), _segment_index(0), _mainSegment(0), - _modeCount(MODE_COUNT), _callback(nullptr), customMappingTable(nullptr), customMappingSize(0), _lastShow(0), _lastServiceShow(0) { - _mode.reserve(_modeCount); // allocate memory to prevent initial fragmentation (does not increase size()) - _modeData.reserve(_modeCount); // allocate memory to prevent initial fragmentation (does not increase size()) - if (_mode.capacity() <= 1 || _modeData.capacity() <= 1) _modeCount = 1; // memory allocation failed only show Solid - else setupEffectData(); + Effects::reserve(MODE_COUNT); // allocate memory to prevent initial fragmentation (does not increase size()) + setupEffectData(); } ~WS2812FX() { p_free(_pixels); p_free(_pixelCCT); // just in case d_free(customMappingTable); - _mode.clear(); - _modeData.clear(); - _segments.clear(); -#ifndef WLED_DISABLE_2D - panel.clear(); -#endif } void @@ -941,15 +980,13 @@ class WS2812FX { uint8_t getFirstSelectedSegId() const; uint8_t getLastActiveSegmentId() const; uint8_t getActiveSegsLightCapabilities(bool selectedOnly = false) const; - uint8_t addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name); // add effect to the list; defined in FX.cpp; - + inline uint8_t getBrightness() const { return _brightness; } // returns current strip brightness inline static constexpr unsigned getMaxSegments() { return MAX_NUM_SEGMENTS; } // returns maximum number of supported segments (fixed value) inline uint8_t getSegmentsNum() const { return _segments.size(); } // returns currently present segments inline uint8_t getCurrSegmentId() const { return _segment_index; } // returns current segment index (only valid while strip.isServicing()) inline uint8_t getMainSegmentId() const { return _mainSegment; } // returns main segment index inline uint8_t getTargetFps() const { return _targetFps; } // returns rough FPS value for las 2s interval - inline uint8_t getModeCount() const { return _modeCount; } // returns number of registered modes/effects uint16_t getLengthPhysical() const; uint16_t getLengthTotal() const; // will include virtual/nonexistent pixels in matrix @@ -968,8 +1005,10 @@ class WS2812FX { inline uint32_t getPixelColor(unsigned n) const { return (n < getLengthTotal()) ? _pixels[n] : 0; } // returns color of pixel n inline uint32_t getLastShow() const { return _lastShow; } // returns millis() timestamp of last strip.show() call - const char *getModeData(unsigned id = 0) const { return (id && id < _modeCount) ? _modeData[id] : PSTR("Solid"); } - inline const char **getModeDataSrc() { return &(_modeData[0]); } // vectors use arrays for underlying data + // Shim interface to library of Effects for compatibility + inline uint8_t addEffect(uint8_t id, effect_function mode_fn, const char *mode_name) { return Effects::addEffect(mode_name, mode_fn, id); } + inline const char *getModeData(unsigned id = 0) const { return Effects::getEffectById(id)->data; } + inline uint8_t getModeCount() const { return Effects::getHighestId() + 1; } // For iterating through Effects by ID - may be larger or smaller than the actual valid Effect count Segment& getSegment(unsigned id); inline Segment& getFirstSelectedSeg() { return _segments[getFirstSelectedSegId()]; } // returns reference to first segment that is "selected" @@ -1048,10 +1087,6 @@ class WS2812FX { uint8_t _segment_index; uint8_t _mainSegment; - uint8_t _modeCount; - std::vector _mode; // SRAM footprint: 4 bytes per element - std::vector _modeData; // mode (effect) name and its slider control data array - show_callback _callback; uint16_t* customMappingTable; @@ -1063,7 +1098,7 @@ class WS2812FX { friend class Segment; }; -extern const char JSON_mode_names[]; +constexpr const char* JSON_mode_names = nullptr; extern const char JSON_palette_names[]; #endif diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 465f1dcfb7..6cc9730ae2 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -55,6 +55,10 @@ uint16_t Segment::_clipStop = 0; uint8_t Segment::_clipStartY = 0; uint8_t Segment::_clipStopY = 1; +// Ignore warnings about "mode" for now +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + // copy constructor Segment::Segment(const Segment &orig) { //DEBUG_PRINTF_P(PSTR("-- Copy segment constructor: %p -> %p\n"), &orig, this); @@ -144,6 +148,8 @@ Segment& Segment::operator= (Segment &&orig) noexcept { return *this; } +#pragma GCC diagnostic pop + // allocates effect data buffer on heap and initialises (erases) it bool Segment::allocateData(size_t len) { if (len == 0) return false; // nothing to do @@ -548,33 +554,54 @@ Segment &Segment::setOption(uint8_t n, bool val) { return *this; } +// extracts parameter defaults from last section of effect data (e.g. "Juggle@!,Trail;!,!,;!;012;sx=16,ix=240") +static int16_t extractEffectDefault(const char* data, const char *segVar) +{ + if (!data) return -1; + + char* stopPtr = strstr(data, segVar); + if (!stopPtr) return -1; + + stopPtr += strlen(segVar) +1; // skip "=" + return atoi(stopPtr); +} + Segment &Segment::setMode(uint8_t fx, bool loadDefaults) { - // skip reserved - while (fx < strip.getModeCount() && strncmp_P("RSVD", strip.getModeData(fx), 4) == 0) fx++; - if (fx >= strip.getModeCount()) fx = 0; // set solid mode - // if we have a valid mode & is not reserved - if (fx != mode) { + const Effect* new_effect = Effects::getEffectById(fx); + if (effect != new_effect) { startTransition(strip.getTransition(), true); // set effect transitions (must create segment copy) - mode = fx; + effect = new_effect; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + mode = Effects::getIdForEffect(effect); +#pragma GCC diagnostic pop int sOpt; + + // Cache string on the stack to avoid PROGMEM issues + char lineBuffer[256]; + strncpy_P(lineBuffer, effect->data, sizeof(lineBuffer)/sizeof(char)-1); + lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\0'; // terminate string + // Find start of defaults section + char* defPtr = strrchr(lineBuffer, ';'); // last ";" in FX data + // load default values from effect string if (loadDefaults) { - sOpt = extractModeDefaults(fx, "sx"); speed = (sOpt >= 0) ? sOpt : DEFAULT_SPEED; - sOpt = extractModeDefaults(fx, "ix"); intensity = (sOpt >= 0) ? sOpt : DEFAULT_INTENSITY; - sOpt = extractModeDefaults(fx, "c1"); custom1 = (sOpt >= 0) ? sOpt : DEFAULT_C1; - sOpt = extractModeDefaults(fx, "c2"); custom2 = (sOpt >= 0) ? sOpt : DEFAULT_C2; - sOpt = extractModeDefaults(fx, "c3"); custom3 = (sOpt >= 0) ? sOpt : DEFAULT_C3; - sOpt = extractModeDefaults(fx, "o1"); check1 = (sOpt >= 0) ? (bool)sOpt : false; - sOpt = extractModeDefaults(fx, "o2"); check2 = (sOpt >= 0) ? (bool)sOpt : false; - sOpt = extractModeDefaults(fx, "o3"); check3 = (sOpt >= 0) ? (bool)sOpt : false; - sOpt = extractModeDefaults(fx, "m12"); if (sOpt >= 0) map1D2D = constrain(sOpt, 0, 7); else map1D2D = M12_Pixels; // reset mapping if not defined (2D FX may not work) - sOpt = extractModeDefaults(fx, "si"); if (sOpt >= 0) soundSim = constrain(sOpt, 0, 3); - sOpt = extractModeDefaults(fx, "rev"); if (sOpt >= 0) reverse = (bool)sOpt; - sOpt = extractModeDefaults(fx, "mi"); if (sOpt >= 0) mirror = (bool)sOpt; // NOTE: setting this option is a risky business - sOpt = extractModeDefaults(fx, "rY"); if (sOpt >= 0) reverse_y = (bool)sOpt; - sOpt = extractModeDefaults(fx, "mY"); if (sOpt >= 0) mirror_y = (bool)sOpt; // NOTE: setting this option is a risky business + sOpt = extractEffectDefault(defPtr, "sx"); speed = (sOpt >= 0) ? sOpt : DEFAULT_SPEED; + sOpt = extractEffectDefault(defPtr, "ix"); intensity = (sOpt >= 0) ? sOpt : DEFAULT_INTENSITY; + sOpt = extractEffectDefault(defPtr, "c1"); custom1 = (sOpt >= 0) ? sOpt : DEFAULT_C1; + sOpt = extractEffectDefault(defPtr, "c2"); custom2 = (sOpt >= 0) ? sOpt : DEFAULT_C2; + sOpt = extractEffectDefault(defPtr, "c3"); custom3 = (sOpt >= 0) ? sOpt : DEFAULT_C3; + sOpt = extractEffectDefault(defPtr, "o1"); check1 = (sOpt >= 0) ? (bool)sOpt : false; + sOpt = extractEffectDefault(defPtr, "o2"); check2 = (sOpt >= 0) ? (bool)sOpt : false; + sOpt = extractEffectDefault(defPtr, "o3"); check3 = (sOpt >= 0) ? (bool)sOpt : false; + sOpt = extractEffectDefault(defPtr, "m12"); if (sOpt >= 0) map1D2D = constrain(sOpt, 0, 7); else map1D2D = M12_Pixels; // reset mapping if not defined (2D FX may not work) + sOpt = extractEffectDefault(defPtr, "si"); if (sOpt >= 0) soundSim = constrain(sOpt, 0, 3); + sOpt = extractEffectDefault(defPtr, "rev"); if (sOpt >= 0) reverse = (bool)sOpt; + sOpt = extractEffectDefault(defPtr, "mi"); if (sOpt >= 0) mirror = (bool)sOpt; // NOTE: setting this option is a risky business + sOpt = extractEffectDefault(defPtr, "rY"); if (sOpt >= 0) reverse_y = (bool)sOpt; + sOpt = extractEffectDefault(defPtr, "mY"); if (sOpt >= 0) mirror_y = (bool)sOpt; // NOTE: setting this option is a risky business } - sOpt = extractModeDefaults(fx, "pal"); // always extract 'pal' to set _default_palette + sOpt = extractEffectDefault(defPtr, "pal"); // always extract 'pal' to set _default_palette if (sOpt >= 0 && loadDefaults) setPalette(sOpt); if (sOpt <= 0) sOpt = 6; // partycolors if zero or not set _default_palette = sOpt; // _deault_palette is loaded into pal0 in loadPalette() (if selected) @@ -601,7 +628,7 @@ Segment &Segment::setName(const char *newName) { if (newLen) { if (name) p_free(name); // free old name name = static_cast(allocate_buffer(newLen+1, BFRALLOC_PREFER_PSRAM)); - if (mode == FX_MODE_2DSCROLLTEXT) startTransition(strip.getTransition(), true); // if the name changes in scrolling text mode, we need to copy the segment for blending + if (Effects::getIdForEffect(effect) == FX_MODE_2DSCROLLTEXT) startTransition(strip.getTransition(), true); // if the name changes in scrolling text mode, we need to copy the segment for blending if (name) strlcpy(name, newName, newLen+1); return *this; } @@ -1281,7 +1308,7 @@ void WS2812FX::service() { if (!seg.isActive()) continue; // last condition ensures all solid segments are updated at the same time - if (nowUp > seg.next_time || _triggered || (doShow && seg.mode == FX_MODE_STATIC)) + if (nowUp > seg.next_time || _triggered || (doShow && Effects::getIdForEffect(seg.effect) == FX_MODE_STATIC)) { doShow = true; unsigned frameDelay = FRAMETIME; @@ -1292,18 +1319,18 @@ void WS2812FX::service() { seg.beginDraw(prog); // set up parameters for get/setPixelColor() (will also blend colors and palette if blend style is FADE) _currentSegment = &seg; // set current segment for effect functions (SEGMENT & SEGENV) // workaround for on/off transition to respect blending style - frameDelay = (*_mode[seg.mode])(); // run new/current mode (needed for bri workaround) + frameDelay = seg.effect->fcn(); // run new/current mode (needed for bri workaround) seg.call++; // if segment is in transition and no old segment exists we don't need to run the old mode // (blendSegments() takes care of On/Off transitions and clipping) Segment *segO = seg.getOldSegment(); - if (segO && segO->isActive() && (seg.mode != segO->mode || blendingStyle != BLEND_STYLE_FADE || + if (segO && segO->isActive() && (seg.effect != segO->effect || blendingStyle != BLEND_STYLE_FADE || (segO->name != seg.name && segO->name && seg.name && strncmp(segO->name, seg.name, WLED_MAX_SEGNAME_LEN) != 0))) { Segment::modeBlend(true); // set semaphore for beginDraw() to blend colors and palette segO->beginDraw(prog); // set up palette & colors (also sets draw dimensions), parent segment has transition progress _currentSegment = segO; // set current segment // workaround for on/off transition to respect blending style - frameDelay = min(frameDelay, (unsigned)(*_mode[segO->mode])()); // run old mode (needed for bri workaround; semaphore!!) + frameDelay = min(frameDelay, (unsigned)(segO->effect->fcn())); // run old mode (needed for bri workaround; semaphore!!) segO->call++; // increment old mode run counter Segment::modeBlend(false); // unset semaphore } @@ -1497,7 +1524,7 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { uint32_t c_a = BLACK; if (x < vCols && y < vRows) c_a = seg->getPixelColorRaw(x + y*vCols); // will get clipped pixel from old segment or unclipped pixel from new segment if (segO && blendingStyle == BLEND_STYLE_FADE - && (topSegment.mode != segO->mode || (segO->name != topSegment.name && segO->name && topSegment.name && strncmp(segO->name, topSegment.name, WLED_MAX_SEGNAME_LEN) != 0)) + && (topSegment.effect != segO->effect || (segO->name != topSegment.name && segO->name && topSegment.name && strncmp(segO->name, topSegment.name, WLED_MAX_SEGNAME_LEN) != 0)) && x < oCols && y < oRows) { // we need to blend old segment using fade as pixels are not clipped c_a = color_blend16(c_a, segO->getPixelColorRaw(x + y*oCols), progInv); @@ -1565,10 +1592,11 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { switch (blendingStyle) { case BLEND_STYLE_PUSH_RIGHT: i = (i + offsetI) % nLen; break; case BLEND_STYLE_PUSH_LEFT: i = (i - offsetI + nLen) % nLen; break; + default: ; } uint32_t c_a = BLACK; if (i < vLen) c_a = seg->getPixelColorRaw(i); // will get clipped pixel from old segment or unclipped pixel from new segment - if (segO && blendingStyle == BLEND_STYLE_FADE && topSegment.mode != segO->mode && i < oLen) { + if (segO && blendingStyle == BLEND_STYLE_FADE && topSegment.effect != segO->effect && i < oLen) { // we need to blend old segment using fade as pixels are not clipped c_a = color_blend16(c_a, segO->getPixelColorRaw(i), progInv); } else if (blendingStyle != BLEND_STYLE_FADE) { @@ -1953,8 +1981,7 @@ void WS2812FX::printSize() { for (const Segment &seg : _segments) size += seg.getSize(); DEBUG_PRINTF_P(PSTR("Segments: %d -> %u/%dB\n"), _segments.size(), size, Segment::getUsedSegmentData()); for (const Segment &seg : _segments) DEBUG_PRINTF_P(PSTR(" Seg: %d,%d [A=%d, 2D=%d, RGB=%d, W=%d, CCT=%d]\n"), seg.width(), seg.height(), seg.isActive(), seg.is2D(), seg.hasRGB(), seg.hasWhite(), seg.isCCT()); - DEBUG_PRINTF_P(PSTR("Modes: %d*%d=%uB\n"), sizeof(mode_ptr), _mode.size(), (_mode.capacity()*sizeof(mode_ptr))); - DEBUG_PRINTF_P(PSTR("Data: %d*%d=%uB\n"), sizeof(const char *), _modeData.size(), (_modeData.capacity()*sizeof(const char *))); + DEBUG_PRINTF_P(PSTR("Modes: %d*(%d of %d)=%uB\n"), sizeof(Effect), Effects::getCount(), Effects::getCapacity(), (Effects::getCapacity()*sizeof(Effect))); DEBUG_PRINTF_P(PSTR("Map: %d*%d=%uB\n"), sizeof(uint16_t), (int)customMappingSize, customMappingSize*sizeof(uint16_t)); } #endif @@ -2054,7 +2081,6 @@ bool WS2812FX::deserializeMap(unsigned n) { } -const char JSON_mode_names[] PROGMEM = R"=====(["FX names moved"])====="; const char JSON_palette_names[] PROGMEM = R"=====([ "Default","* Random Cycle","* Color 1","* Colors 1&2","* Color Gradient","* Colors Only","Party","Cloud","Lava","Ocean", "Forest","Rainbow","Rainbow Bands","Sunset","Rivendell","Breeze","Red & Blue","Yellowout","Analogous","Splash", diff --git a/wled00/e131.cpp b/wled00/e131.cpp index 357e7841fe..a1deb78243 100644 --- a/wled00/e131.cpp +++ b/wled00/e131.cpp @@ -228,7 +228,7 @@ void handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8 return; if (e131_data[dataOffset+1] < strip.getModeCount()) - if (e131_data[dataOffset+1] != seg.mode) seg.setMode( e131_data[dataOffset+1]); + if (e131_data[dataOffset+1] != Effects::getIdForEffect(seg.effect)) seg.setMode( e131_data[dataOffset+1]); if (e131_data[dataOffset+2] != seg.speed) seg.speed = e131_data[dataOffset+2]; if (e131_data[dataOffset+3] != seg.intensity) seg.intensity = e131_data[dataOffset+3]; if (e131_data[dataOffset+4] != seg.palette) seg.setPalette(e131_data[dataOffset+4]); diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 01c2c2ec92..0523aebbb9 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -398,7 +398,6 @@ bool requestJSONBufferLock(uint8_t moduleID=255); void releaseJSONBufferLock(); uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLen); uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxLen, uint8_t *var = nullptr); -int16_t extractModeDefaults(uint8_t mode, const char *segVar); void checkSettingsPIN(const char *pin); uint16_t crc16(const unsigned char* data_p, size_t length); uint16_t beatsin88_t(accum88 beats_per_minute_88, uint16_t lowest = 0, uint16_t highest = 65535, uint32_t timebase = 0, uint16_t phase_offset = 0); diff --git a/wled00/json.cpp b/wled00/json.cpp index a5ef74757d..6acf50397c 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -23,7 +23,7 @@ namespace { uint16_t startY; uint16_t stopY; uint16_t options; - uint8_t mode; + const Effect* effect; uint8_t palette; uint8_t opacity; uint8_t speed; @@ -44,7 +44,7 @@ namespace { if (a.grouping != b.grouping) d |= SEG_DIFFERS_GSO; if (a.spacing != b.spacing) d |= SEG_DIFFERS_GSO; if (a.opacity != b.opacity) d |= SEG_DIFFERS_BRI; - if (a.mode != b.mode) d |= SEG_DIFFERS_FX; + if (a.effect != b.effect) d |= SEG_DIFFERS_FX; if (a.speed != b.speed) d |= SEG_DIFFERS_FX; if (a.intensity != b.intensity) d |= SEG_DIFFERS_FX; if (a.palette != b.palette) d |= SEG_DIFFERS_FX; @@ -97,7 +97,7 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0) seg.startY, seg.stopY, seg.options, - seg.mode, + seg.effect, seg.palette, seg.opacity, seg.speed, @@ -247,7 +247,7 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0) if (!colValid) continue; seg.setColor(i, RGBW32(rgbw[0],rgbw[1],rgbw[2],rgbw[3])); // use transition - if (seg.mode == FX_MODE_STATIC) strip.trigger(); //instant refresh + if (Effects::getIdForEffect(seg.effect) == FX_MODE_STATIC) strip.trigger(); //instant refresh } } else { // non RGB & non White segment (usually On/Off bus) @@ -279,10 +279,10 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0) seg.transpose = transpose; #endif - byte fx = seg.mode; - if (getVal(elem["fx"], fx, 0, strip.getModeCount())) { + byte fx = Effects::getIdForEffect(seg.effect); + if (getVal(elem["fx"], fx, 0, 255)) { if (!presetId && currentPlaylist>=0) unloadPlaylist(); - if (fx != seg.mode) seg.setMode(fx, elem[F("fxdef")]); // use transition (WARNING: may change map1D2D causing geometry change) + if (fx != seg.effect->id) seg.setMode(fx, elem[F("fxdef")]); // use transition (WARNING: may change map1D2D causing geometry change) } getVal(elem["sx"], seg.speed); @@ -607,7 +607,7 @@ static void serializeSegment(JsonObject& root, const Segment& seg, byte id, bool strcat(colstr, "]"); root["col"] = serialized(colstr); - root["fx"] = seg.mode; + root["fx"] = Effects::getIdForEffect(seg.effect); root["sx"] = seg.speed; root["ix"] = seg.intensity; root["pal"] = seg.palette; @@ -770,7 +770,7 @@ void serializeInfo(JsonObject root) root[F("ws")] = -1; #endif - root[F("fxcount")] = strip.getModeCount(); + root[F("fxcount")] = Effects::getCount(); root[F("palcount")] = getPaletteCount(); root[F("cpalcount")] = customPalettes.size(); // number of custom palettes root[F("cpalmax")] = WLED_MAX_CUSTOM_PALETTES; // maximum number of custom palettes @@ -1042,37 +1042,45 @@ void serializeNodes(JsonObject root) } } -// deserializes mode data string into JsonArray +// deserializes mode data string into JsonArray, omitting names void serializeModeData(JsonArray fxdata) { - char lineBuffer[256]; - for (size_t i = 0; i < strip.getModeCount(); i++) { - strncpy_P(lineBuffer, strip.getModeData(i), sizeof(lineBuffer)/sizeof(char)-1); - lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\0'; // terminate string - if (lineBuffer[0] != 0) { + for (auto& effect: Effects::all()) { + uint8_t id = Effects::getIdForEffect(effect); + if (id < 255) { + // Copy data to stack for analysis + char lineBuffer[256]; + strncpy_P(lineBuffer, effect->data, sizeof(lineBuffer)/sizeof(char)-1); + lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\0'; // terminate string char* dataPtr = strchr(lineBuffer,'@'); - if (dataPtr) fxdata.add(dataPtr+1); - else fxdata.add(""); + if (dataPtr) { + fxdata[id] = (dataPtr+1); + } } } -} + // Fill out empty elements + for(unsigned i = 0; i < fxdata.size(); ++i) { + if (fxdata[i].isNull()) fxdata[i] = ""; + } +} // deserializes mode names string into JsonArray // also removes effect data extensions (@...) from deserialised names void serializeModeNames(JsonArray arr) { - char lineBuffer[256]; - for (size_t i = 0; i < strip.getModeCount(); i++) { - strncpy_P(lineBuffer, strip.getModeData(i), sizeof(lineBuffer)/sizeof(char)-1); - lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\0'; // terminate string - if (lineBuffer[0] != 0) { - char* dataPtr = strchr(lineBuffer,'@'); - if (dataPtr) *dataPtr = 0; // terminate mode data after name - arr.add(lineBuffer); + for (auto& effect: Effects::all()) { + uint8_t id = Effects::getIdForEffect(effect); + if (id < 255) { + arr[id] = effect->getName(); } } + // Fill out empty elements + for(unsigned i = 0; i < arr.size(); ++i) { + if (arr[i].isNull()) arr[i] = FPSTR("RSVD"); + } } + // Global buffer locking response helper class (to make sure lock is released when AsyncJsonResponse is destroyed) class LockedJsonResponse: public AsyncJsonResponse { bool _holding_lock; diff --git a/wled00/led.cpp b/wled00/led.cpp index 35f5003679..b8c01f3777 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -15,7 +15,7 @@ void setValuesFromSegment(uint8_t s) { colSec[1] = G(seg.colors[1]); colSec[2] = B(seg.colors[1]); colSec[3] = W(seg.colors[1]); - effectCurrent = seg.mode; + effectCurrent = Effects::getIdForEffect(seg.effect); effectSpeed = seg.speed; effectIntensity = seg.intensity; effectPalette = seg.palette; @@ -30,7 +30,7 @@ void applyValuesToSelectedSegs() { if (effectSpeed != seg.speed) {seg.speed = effectSpeed; stateChanged = true;} if (effectIntensity != seg.intensity) {seg.intensity = effectIntensity; stateChanged = true;} if (effectPalette != seg.palette) {seg.setPalette(effectPalette);} - if (effectCurrent != seg.mode) {seg.setMode(effectCurrent);} + if (effectCurrent != Effects::getIdForEffect(seg.effect)) {seg.setMode(effectCurrent);} uint32_t col0 = RGBW32(colPri[0], colPri[1], colPri[2], colPri[3]); uint32_t col1 = RGBW32(colSec[0], colSec[1], colSec[2], colSec[3]); if (col0 != seg.colors[0]) {seg.setColor(0, col0);} diff --git a/wled00/overlay.cpp b/wled00/overlay.cpp index 3f6e631214..7023f23960 100644 --- a/wled00/overlay.cpp +++ b/wled00/overlay.cpp @@ -93,7 +93,7 @@ void handleOverlayDraw() { for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { const Segment& segment = strip.getSegment(i); if (!segment.isActive()) continue; - if (segment.mode > 0 || segment.colors[0] > 0) { + if (Effects::getIdForEffect(segment.effect) > 0 || segment.colors[0] > 0) { return; } } diff --git a/wled00/set.cpp b/wled00/set.cpp index 9c05803e40..ce8b39751f 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -875,7 +875,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) uint32_t col2 = selseg.colors[2]; byte colIn[4] = {R(col0), G(col0), B(col0), W(col0)}; byte colInSec[4] = {R(col1), G(col1), B(col1), W(col1)}; - byte effectIn = selseg.mode; + byte effectIn = 0; byte speedIn = selseg.speed; byte intensityIn = selseg.intensity; byte paletteIn = selseg.palette; @@ -1060,9 +1060,11 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) bool fxModeChanged = false, speedChanged = false, intensityChanged = false, paletteChanged = false; bool custom1Changed = false, custom2Changed = false, custom3Changed = false, check1Changed = false, check2Changed = false, check3Changed = false; // set effect parameters - if (updateVal(req.c_str(), "FX=", effectIn, 0, strip.getModeCount()-1)) { - if (request != nullptr) unloadPlaylist(); // unload playlist if changing FX using web request - fxModeChanged = true; + if (updateVal(req.c_str(), "FX=", effectIn, 0, 255)) { + if (Effects::getEffectById(effectIn) != selseg.effect) { + if (request != nullptr) unloadPlaylist(); // unload playlist if changing FX using web request + fxModeChanged = true; + } } speedChanged = updateVal(req.c_str(), "SX=", speedIn); intensityChanged = updateVal(req.c_str(), "IX=", intensityIn); diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 26a80ac349..05371696af 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -48,7 +48,7 @@ void notify(byte callMode, bool followUp) udpOut[5] = B(col); udpOut[6] = nightlightActive; udpOut[7] = nightlightDelayMins; - udpOut[8] = mainseg.mode; + udpOut[8] = Effects::getIdForEffect(mainseg.effect); udpOut[9] = mainseg.speed; udpOut[10] = W(col); //compatibilityVersionByte: @@ -118,7 +118,7 @@ void notify(byte callMode, bool followUp) udpOut[8 +ofs] = selseg.offset & 0xFF; udpOut[9 +ofs] = selseg.options & 0x8F; //only take into account selected, mirrored, on, reversed, reverse_y (for 2D); ignore freeze, reset, transitional udpOut[10+ofs] = selseg.opacity; - udpOut[11+ofs] = selseg.mode; + udpOut[11+ofs] = Effects::getIdForEffect(selseg.effect); udpOut[12+ofs] = selseg.speed; udpOut[13+ofs] = selseg.intensity; udpOut[14+ofs] = selseg.palette; diff --git a/wled00/util.cpp b/wled00/util.cpp index e9811cf29d..5bd0988fa5 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -213,21 +213,13 @@ void releaseJSONBufferLock() // caller must provide large enough buffer for name (including SR extensions)! uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLen) { - if (src == JSON_mode_names || src == nullptr) { - if (mode < strip.getModeCount()) { - char lineBuffer[256]; - //strcpy_P(lineBuffer, (const char*)pgm_read_dword(&(WS2812FX::_modeData[mode]))); - strncpy_P(lineBuffer, strip.getModeData(mode), sizeof(lineBuffer)/sizeof(char)-1); - lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\0'; // terminate string - size_t len = strlen(lineBuffer); - size_t j = 0; - for (; j < maxLen && j < len; j++) { - if (lineBuffer[j] == '\0' || lineBuffer[j] == '@') break; - dest[j] = lineBuffer[j]; - } - dest[j] = 0; // terminate string - return strlen(dest); - } else return 0; + if (src == nullptr) { + const Effect* effect = Effects::getEffectById(mode); + if (effect) { + return effect->getName(dest, maxLen); + } else { + return 0; + } } if (src == JSON_palette_names && mode > 255-customPalettes.size()) { @@ -272,98 +264,72 @@ uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxL { dest[0] = '\0'; // start by clearing buffer - if (mode < strip.getModeCount()) { - String lineBuffer = FPSTR(strip.getModeData(mode)); - if (lineBuffer.length() > 0) { - int start = lineBuffer.indexOf('@'); // String::indexOf() returns an int, not an unsigned; -1 means "not found" - int stop = lineBuffer.indexOf(';', start); - if (start>0 && stop>0) { - String names = lineBuffer.substring(start, stop); // include @ - int nameBegin = 1, nameEnd, nameDefault; - if (slider < 10) { - for (size_t i=0; i<=slider; i++) { - const char *tmpstr; - dest[0] = '\0'; //clear dest buffer - if (nameBegin <= 0) break; // there are no more names - nameEnd = names.indexOf(',', nameBegin); - if (i == slider) { - nameDefault = names.indexOf('=', nameBegin); // find default value - if (nameDefault > 0 && var && ((nameEnd>0 && nameDefault 0) { + int start = lineBuffer.indexOf('@'); // String::indexOf() returns an int, not an unsigned; -1 means "not found" + int stop = lineBuffer.indexOf(';', start); + if (start>0 && stop>0) { + String names = lineBuffer.substring(start, stop); // include @ + int nameBegin = 1, nameEnd, nameDefault; + if (slider < 10) { + for (size_t i=0; i<=slider; i++) { + const char *tmpstr; + dest[0] = '\0'; //clear dest buffer + if (nameBegin <= 0) break; // there are no more names + nameEnd = names.indexOf(',', nameBegin); + if (i == slider) { + nameDefault = names.indexOf('=', nameBegin); // find default value + if (nameDefault > 0 && var && ((nameEnd>0 && nameDefault= 0) { - nameEnd = names.indexOf(';', nameBegin+1); - if (!isdigit(names[nameBegin+1])) nameBegin = names.indexOf('=', nameBegin+1); // look for default value - if (nameEnd >= 0 && nameBegin > nameEnd) nameBegin = -1; - if (nameBegin >= 0 && var) { - *var = (uint8_t)atoi(names.substring(nameBegin+1).c_str()); + if (names.charAt(nameBegin) == '!') { + switch (slider) { + case 0: tmpstr = PSTR("FX Speed"); break; + case 1: tmpstr = PSTR("FX Intensity"); break; + case 2: tmpstr = PSTR("FX Custom 1"); break; + case 3: tmpstr = PSTR("FX Custom 2"); break; + case 4: tmpstr = PSTR("FX Custom 3"); break; + default: tmpstr = PSTR("FX Custom"); break; + } + strncpy_P(dest, tmpstr, maxLen); // copy the name into buffer (replacing previous) + dest[maxLen-1] = '\0'; + } else { + if (nameEnd<0) tmpstr = names.substring(nameBegin).c_str(); // did not find ",", last name? + else tmpstr = names.substring(nameBegin, nameEnd).c_str(); + strlcpy(dest, tmpstr, maxLen); // copy the name into buffer (replacing previous) } } + nameBegin = nameEnd+1; // next name (if "," is not found it will be 0) + } // next slider + } else if (slider == 255) { + // palette + strlcpy(dest, "pal", maxLen); + names = lineBuffer.substring(stop+1); // stop has index of color slot names + nameBegin = names.indexOf(';'); // look for palette + if (nameBegin >= 0) { + nameEnd = names.indexOf(';', nameBegin+1); + if (!isdigit(names[nameBegin+1])) nameBegin = names.indexOf('=', nameBegin+1); // look for default value + if (nameEnd >= 0 && nameBegin > nameEnd) nameBegin = -1; + if (nameBegin >= 0 && var) { + *var = (uint8_t)atoi(names.substring(nameBegin+1).c_str()); + } } - // we have slider name (including default value) in the dest buffer - for (size_t i=0; i Date: Sun, 16 Nov 2025 23:27:29 -0500 Subject: [PATCH 2/6] Fix usermod that proves the rule getModeDataSrc() was the API we lost. --- .../usermod_v2_rotary_encoder_ui_ALT.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp index 02bb08c9b9..2d42697782 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp @@ -178,9 +178,6 @@ class RotaryEncoderUIUsermod : public Usermod { void* display; #endif - // Pointers the start of the mode names within JSON_mode_names - const char **modes_qstrings; - // Array of mode indexes in alphabetical order. byte *modes_alpha_indexes; @@ -265,7 +262,6 @@ class RotaryEncoderUIUsermod : public Usermod { , currentSat1(255) , currentCCT(128) , display(nullptr) - , modes_qstrings(nullptr) , modes_alpha_indexes(nullptr) , palettes_qstrings(nullptr) , palettes_alpha_indexes(nullptr) @@ -395,10 +391,12 @@ byte RotaryEncoderUIUsermod::readPin(uint8_t pin) { */ void RotaryEncoderUIUsermod::sortModesAndPalettes() { DEBUG_PRINT(F("Sorting modes: ")); DEBUG_PRINTLN(strip.getModeCount()); - //modes_qstrings = re_findModeStrings(JSON_mode_names, strip.getModeCount()); - modes_qstrings = strip.getModeDataSrc(); + std::vector modes_qstrings { strip.getModeCount(), "RSVD" }; + for (auto& effect_ptr: Effects::all()) { + modes_qstrings[Effects::getIdForEffect(effect_ptr)] = effect_ptr->data; + } modes_alpha_indexes = re_initIndexArray(strip.getModeCount()); - re_sortModes(modes_qstrings, modes_alpha_indexes, strip.getModeCount(), MODE_SORT_SKIP_COUNT); + re_sortModes(modes_qstrings.data(), modes_alpha_indexes, strip.getModeCount(), MODE_SORT_SKIP_COUNT); DEBUG_PRINT(F("Sorting palettes: ")); DEBUG_PRINT(getPaletteCount()); DEBUG_PRINT('/'); DEBUG_PRINTLN(customPalettes.size()); palettes_qstrings = re_findModeStrings(JSON_palette_names, getPaletteCount()); From 041547152658f8784a2b55804189ea7f6bc56c69 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sun, 16 Nov 2025 16:10:49 -0500 Subject: [PATCH 3/6] Add effect name to JSON API --- wled00/Effects.cpp | 9 ++++++++- wled00/FX.h | 10 +++++++--- wled00/FX_fcn.cpp | 3 +-- wled00/json.cpp | 20 ++++++++++++++++---- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/wled00/Effects.cpp b/wled00/Effects.cpp index a8e61d8ca9..4b4694442e 100644 --- a/wled00/Effects.cpp +++ b/wled00/Effects.cpp @@ -54,7 +54,14 @@ uint8_t Effects::addEffect(const char *mode_name, effect_function mode_fn, uint8 return id; } -// const Effect* Effects::getEffectByName(const char* name) {} TODO +const Effect* Effects::getEffectByName(const char* name, size_t len) { + auto effect_iter = std::find_if(_globalEffectsList().begin(), _globalEffectsList().end(), [=](const Effect* e) { + auto match = strncmp_P(e->data, name, len); + return (match == 0) && ((e->data[len] == '@') || (e->data[len] == 0)); + }); + if (effect_iter == _globalEffectsList().end()) effect_iter = _globalEffectsList().begin(); // set solid mode, always the first element of the list + return *effect_iter; +} const Effect* Effects::getEffectById(uint8_t id) { auto effect_iter = std::find_if(_globalEffectsList().begin(), _globalEffectsList().end(), [=](const Effect* e) { return getIdForEffect(e) == id; }); diff --git a/wled00/FX.h b/wled00/FX.h index 3a9b703fca..2997e418b0 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -427,7 +427,7 @@ namespace Effects { uint8_t getIdForEffect(const Effect*); }; // Effect information structure typedef uint16_t (*effect_function)(); // pointer to mode function struct Effect { - const char *data; // mode (effect) name and its UI control data + const char *data; // mode (effect) name and its UI control data effect_function fcn; // mode (effect) function /* Future: per-effect constructor function pointer goes here */ @@ -451,7 +451,10 @@ namespace Effects { void addEffect(const Effect* effect); // add an effect to the global list uint8_t addEffect(const char *mode_name, effect_function mode_fn, uint8_t id); // Add a non-static effect to the list; returns id - const Effect* getEffectByName(const char* name); + const Effect* getEffectByName(const char* name, size_t name_len); + inline const Effect* getEffectByName(const char* name) { return getEffectByName(name, strlen(name)); } + inline const Effect* getEffectByName(const String& name) { return getEffectByName(name.c_str(), name.length()); } + const Effect* getEffectById(uint8_t id); // Returns an effect pointer by id number; if not found, returns first effect inline uint8_t getIdForEffect(const Effect* effect) { return effect->id; } // Returns the ID number assigned to an effect, or 255 if none available uint8_t getHighestId(); // Returns the highest assigned ID value; often used by older code to iterate through fx by id @@ -724,7 +727,8 @@ class Segment { Segment &setCCT(uint16_t k); Segment &setOpacity(uint8_t o); Segment &setOption(uint8_t n, bool val); - Segment &setMode(uint8_t fx, bool loadDefaults = false); + Segment &setEffect(const Effect* effect, bool loadDefaults = false); + inline Segment &setMode(uint8_t fx, bool loadDefaults = false) { return setEffect(Effects::getEffectById(fx), loadDefaults); } Segment &setPalette(uint8_t pal); Segment &setName(const char* name); void refreshLightCapabilities() const; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 6cc9730ae2..524aba6776 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -566,8 +566,7 @@ static int16_t extractEffectDefault(const char* data, const char *segVar) return atoi(stopPtr); } -Segment &Segment::setMode(uint8_t fx, bool loadDefaults) { - const Effect* new_effect = Effects::getEffectById(fx); +Segment &Segment::setEffect(const Effect* new_effect, bool loadDefaults) { if (effect != new_effect) { startTransition(strip.getTransition(), true); // set effect transitions (must create segment copy) effect = new_effect; diff --git a/wled00/json.cpp b/wled00/json.cpp index 6acf50397c..7dda147b2e 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -279,10 +279,21 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0) seg.transpose = transpose; #endif - byte fx = Effects::getIdForEffect(seg.effect); - if (getVal(elem["fx"], fx, 0, 255)) { - if (!presetId && currentPlaylist>=0) unloadPlaylist(); - if (fx != seg.effect->id) seg.setMode(fx, elem[F("fxdef")]); // use transition (WARNING: may change map1D2D causing geometry change) + { + const Effect* new_effect = nullptr; + JsonVariant ef = elem["ef"]; + if (ef.is()) { + new_effect = Effects::getEffectByName(ef.as()); + } else { + byte fx = Effects::getIdForEffect(seg.effect); + if (getVal(elem["fx"], fx, 0, 255) && (fx < 255)) { + new_effect = Effects::getEffectById(fx); + } + } + if (new_effect) { + if (!presetId && currentPlaylist>=0) unloadPlaylist(); + if (new_effect != seg.effect) seg.setEffect(new_effect, elem[F("fxdef")]); // use transition (WARNING: may change map1D2D causing geometry change) + } } getVal(elem["sx"], seg.speed); @@ -607,6 +618,7 @@ static void serializeSegment(JsonObject& root, const Segment& seg, byte id, bool strcat(colstr, "]"); root["col"] = serialized(colstr); + root["ef"] = seg.effect->getName(); root["fx"] = Effects::getIdForEffect(seg.effect); root["sx"] = seg.speed; root["ix"] = seg.intensity; From 16b06904a2161f11ddd62bd496047f32073d5591 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 15 Nov 2025 23:17:49 -0500 Subject: [PATCH 4/6] Add "json/fxmap" endpoint Add an endpoint that lists all FX as objects, without any gaps --- wled00/json.cpp | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/wled00/json.cpp b/wled00/json.cpp index 7dda147b2e..9406984f81 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -1092,6 +1092,67 @@ void serializeModeNames(JsonArray arr) } } +void serveFxMap(AsyncWebServerRequest* request) +{ + // sendChunked allows us to stream the data out without needing to buffer it all up front. + // We use a mutable lambda as the function type to preserve state across calls. + // This isn't the most packet-efficient -- we'd need to preserve some partially serialized traffic + // for that -- but it's low memory, no matter how big the data gets. + auto effect = Effects::all().begin(); + bool done = false; + request->sendChunked( + FPSTR(CONTENT_TYPE_JSON), + [effect, done](uint8_t* buf, size_t len, size_t filledLength) mutable { + size_t buf_len = len;; + //DEBUG_PRINTF_P(PSTR("SFM: eff %08X, done %d, buf %08X, len %d, fL %d\n"), (intptr_t)*effect, done, (intptr_t)buf, len, filledLength); + if (done) return 0U; + + if (filledLength == 0) { + *buf = '{'; + ++buf, --len; + } + + while(1) { + if (effect == Effects::all().end()) { + *buf = '}'; --len; + done = true; + //DEBUG_PRINTF_P(PSTR("SFM: done\n")); + return (buf_len - len); // Done + } + if (len < (strlen_P((*effect)->data) + 24)) { // "":{"info":"","id":nnn}, + //DEBUG_PRINTF_P(PSTR("SFM: exiting: wanted %d, have %d\n"), (strlen_P((*effect)->data) + 24), len); + return (buf_len - len); // not enough space + } + + // Copy data to stack for analysis + char lineBuffer[256]; + strncpy_P(lineBuffer, (*effect)->data, sizeof(lineBuffer)/sizeof(char)-1); + lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\0'; // terminate string + char* dataPtr = strchr(lineBuffer,'@'); + if (dataPtr) { + *dataPtr = '\0'; + } else { + // point to null terminator + dataPtr = lineBuffer + strlen(lineBuffer); + } + //DEBUG_PRINTF_P(PSTR("SFM: sending eff %08X, buf %08X, len %d\n"), (intptr_t)*effect, (intptr_t)buf, len); + auto l_used = snprintf_P((char*) buf, len, PSTR("\"%s\":{\"info\":\"%s\""), lineBuffer, dataPtr+1); + buf += l_used; len -= l_used; + + uint8_t id = Effects::getIdForEffect(*effect); + if (id < 255) { + l_used = snprintf_P((char*) buf, len, PSTR(",\"id\":%d"), id); + buf += l_used; len -= l_used; + } + *buf = '}'; ++buf, --len; + ++effect; + if (effect != Effects::all().end()) { + *buf = ','; ++buf, --len; + } + } + } + ); +} // Global buffer locking response helper class (to make sure lock is released when AsyncJsonResponse is destroyed) class LockedJsonResponse: public AsyncJsonResponse { @@ -1134,6 +1195,10 @@ void serveJson(AsyncWebServerRequest* request) else if (url.indexOf(F("fxda")) > 0) subJson = json_target::fxdata; else if (url.indexOf(F("net")) > 0) subJson = json_target::networks; else if (url.indexOf(F("cfg")) > 0) subJson = json_target::config; + else if (url.indexOf(F("fxmap")) > 0) { + serveFxMap(request); + return; + } #ifdef WLED_ENABLE_JSONLIVE else if (url.indexOf("live") > 0) { serveLiveLeds(request); From 2d4d1e54f94408ef0aeb9c4509e26730418deccc Mon Sep 17 00:00:00 2001 From: Will Miles Date: Tue, 18 Nov 2025 23:11:29 -0500 Subject: [PATCH 5/6] Use fx names instead of ids in the UI --- wled00/data/index.js | 143 +++++++++++++++---------------------------- 1 file changed, 50 insertions(+), 93 deletions(-) diff --git a/wled00/data/index.js b/wled00/data/index.js index 2d49a26400..595a389150 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -5,7 +5,7 @@ var hasWhite = false, hasRGB = false, hasCCT = false, has2D = false; var nlDur = 60, nlTar = 0; var nlMode = false; var segLmax = 0; // size (in pixels) of largest selected segment -var selectedFx = 0; +var selectedFx = "Solid"; var selectedPal = 0; var csel = 0; // selected color slot (0-2) var currentPreset = -1; @@ -18,7 +18,6 @@ var d = document; const ranges = RangeTouch.setup('input[type="range"]', {}); var retry = false; var palettesData; -var fxdata = []; var pJson = {}, eJson = {}, lJson = {}; var plJson = {}; // array of playlists var pN = "", pI = 0, pNum = 0; @@ -280,16 +279,13 @@ function onLoad() // Load initial data loadPalettes(()=>{ - // fill effect extra data array - loadFXData(()=>{ - // load and populate effects - setTimeout(()=>{loadFX(()=>{ - loadPalettesData(()=>{ - requestJson();// will load presets and create WS - if (cfg.comp.css) setTimeout(()=>{loadSkinCSS('skinCss')},50); - }); - })},50); - }); + // load and populate effects + setTimeout(()=>{loadFX(()=>{ + loadPalettesData(()=>{ + requestJson();// will load presets and create WS + if (cfg.comp.css) setTimeout(()=>{loadSkinCSS('skinCss')},50); + }); + })},50); }); resetUtil(); @@ -546,7 +542,7 @@ function loadPalettes(callback = null) function loadFX(callback = null) { - fetch(getURL('/json/effects'), { + fetch(getURL('/json/fxmap'), { method: 'get' }) .then((res)=>{ @@ -554,7 +550,7 @@ function loadFX(callback = null) return res.json(); }) .then((json)=>{ - eJson = Object.entries(json); + eJson = json; populateEffects(); retry = false; }) @@ -571,36 +567,6 @@ function loadFX(callback = null) }); } -function loadFXData(callback = null) -{ - fetch(getURL('/json/fxdata'), { - method: 'get' - }) - .then((res)=>{ - if (!res.ok) showErrorToast(); - return res.json(); - }) - .then((json)=>{ - fxdata = json||[]; - // add default value for Solid - fxdata.shift() - fxdata.unshift(";!;"); - retry = false; - }) - .catch((e)=>{ - fxdata = []; - if (!retry) { - retry = true; - setTimeout(()=>{loadFXData(loadFX);}, 500); // retry - } - showToast(e, true); - }) - .finally(()=>{ - if (callback) callback(); - updateUI(); - }); -} - var pQL = []; function populateQL() { @@ -933,46 +899,34 @@ function populateSegments(s) function populateEffects() { - var effects = eJson; + var effects = {}; var html = ""; - effects.shift(); // temporary remove solid - for (let i = 0; i < effects.length; i++) { - effects[i] = { - id: effects[i][0], - name:effects[i][1] - }; + // Created sorted dictionary of effects + effects["Solid"] = eJson["Solid"] + for (effect of Object.keys(eJson).sort((a,b) => (a).localeCompare(b))) { + effects[effect] = eJson[effect] + } - effects.sort((a,b) => (a.name).localeCompare(b.name)); - effects.unshift({ - "id": 0, - "name": "Solid" - }); - for (let ef of effects) { + for (const [name, ef] of Object.entries(effects)) { // add slider and color control to setFX (used by requestjson) - let id = ef.id; - let nm = ef.name+" "; - let fd = ""; - if (ef.name.indexOf("RSVD") < 0) { - if (Array.isArray(fxdata) && fxdata.length>id) { - if (fxdata[id].length==0) fd = ";;!;1" - else fd = fxdata[id]; - let eP = (fd == '')?[]:fd.split(";"); // effect parameters - let p = (eP.length<3 || eP[2]==='')?[]:eP[2].split(","); // palette data - if (p.length>0 && (p[0] !== "" && !isNumeric(p[0]))) nm += "🎨"; // effects using palette - let m = (eP.length<4 || eP[3]==='')?'1':eP[3]; // flags - if (id == 0) m = ''; // solid has no flags - if (m.length>0) { - if (m.includes('0')) nm += "•"; // 0D effects (PWM & On/Off) - if (m.includes('1')) nm += "⋮"; // 1D effects - if (m.includes('2')) nm += "▦"; // 2D effects - if (m.includes('v')) nm += "♪"; // volume effects - if (m.includes('f')) nm += "♫"; // frequency effects - } - } - html += generateListItemHtml('fx',id,nm,'setFX','',fd); + let nm = name+" "; + let fd = ef["info"]; + if (fd.length==0) fd = ";;!;1" + let eP = (fd == '')?[]:fd.split(";"); // effect parameters + let p = (eP.length<3 || eP[2]==='')?[]:eP[2].split(","); // palette data + if (p.length>0 && (p[0] !== "" && !isNumeric(p[0]))) nm += "🎨"; // effects using palette + let m = (eP.length<4 || eP[3]==='')?'1':eP[3]; // flags + if (ef["id"] == 0) m = ''; // solid has no flags + if (m.length>0) { + if (m.includes('0')) nm += "•"; // 0D effects (PWM & On/Off) + if (m.includes('1')) nm += "⋮"; // 1D effects + if (m.includes('2')) nm += "▦"; // 2D effects + if (m.includes('v')) nm += "♪"; // volume effects + if (m.includes('f')) nm += "♫"; // frequency effects } + html += generateListItemHtml('fx',name,nm,'setFX','',fd); } gId('fxlist').innerHTML=html; @@ -1071,7 +1025,7 @@ function genPalPrevCss(id) function generateListItemHtml(listName, id, name, clickAction, extraHtml = '', effectPar = '') { - return `
`+ + return `
`+ `