diff --git a/usermods/audioreactive/audio_reactive.cpp b/usermods/audioreactive/audio_reactive.cpp index 757ad35482..afcaac877c 100644 --- a/usermods/audioreactive/audio_reactive.cpp +++ b/usermods/audioreactive/audio_reactive.cpp @@ -879,6 +879,9 @@ class AudioReactive : public Usermod { #endif static const char _digitalmic[]; static const char _addPalettes[]; + static const char _palName0[]; + static const char _palName1[]; + static const char _palName2[]; static const char UDP_SYNC_HEADER[]; static const char UDP_SYNC_HEADER_v1[]; @@ -1951,14 +1954,10 @@ class AudioReactive : public Usermod { } #endif } - if (palettes > 0 && root.containsKey(F("rmcpal"))) { - // handle removal of custom palettes from JSON call so we don't break things - removeAudioPalettes(); - } } void onStateChange(uint8_t callMode) override { - if (initDone && enabled && addPalettes && palettes==0 && customPalettes.size()0) { - customPalettes.pop_back(); - DEBUG_PRINTLN(palettes); - palettes--; - } - DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(customPalettes.size()); + palettes -= (int8_t)removeUsermodPalettes(_name); + if (palettes < 0) palettes = 0; // safeguard } void AudioReactive::createAudioPalettes(void) { - DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(customPalettes.size()); if (palettes) return; DEBUG_PRINTLN(F("Adding audio palettes.")); - for (int i=0; i= palettes) lastCustPalette -= palettes; - for (int pal=0; pal= FIXED_PALETTE_COUNT && pal <= 255-customPalettes.size()) pal = 0; // out of bounds palette - //default palette. Differs depending on effect - if (pal == 0) pal = _default_palette; // _default_palette is set in setMode() + if (pal == 0) pal = _default_palette; // _default_palette is set in setMode(), differs depending on effect + const int umCount = usermodPalettes.size(); + const int custCount = customPalettes.size(); + if (pal >= FIXED_PALETTE_COUNT) { + if (pal > WLED_CUSTOM_PALETTE_ID_BASE) { // usermod range (IDs 201-255) + if ((WLED_USERMOD_PALETTE_ID_BASE - pal) >= umCount) pal = 0; + } else { // custom range + if ((WLED_CUSTOM_PALETTE_ID_BASE - pal) >= custCount) pal = 0; + } + } switch (pal) { case 0: //default palette. Exceptions for specific effects above targetPalette = PartyColors_gc22; @@ -267,8 +274,10 @@ void Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { } break;} default: //progmem palettes - if (pal > 255 - customPalettes.size()) { - targetPalette = customPalettes[255-pal]; // we checked bounds above + if (pal > WLED_CUSTOM_PALETTE_ID_BASE) { // usermod palette + targetPalette = usermodPalettes[WLED_USERMOD_PALETTE_ID_BASE - pal].palette; + } else if (pal >= FIXED_PALETTE_COUNT) { // user custom palette + targetPalette = customPalettes[WLED_CUSTOM_PALETTE_ID_BASE - pal]; } else if (pal < DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT) { // palette 6 - 12, fastled palettes targetPalette = *fastledPalettes[pal - DYNAMIC_PALETTE_COUNT]; } else { @@ -585,7 +594,13 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) { } Segment &Segment::setPalette(uint8_t pal) { - if (pal <= 255-customPalettes.size() && pal > FIXED_PALETTE_COUNT) pal = 0; // not built in palette or custom palette + if (pal >= FIXED_PALETTE_COUNT) { + if (pal > WLED_CUSTOM_PALETTE_ID_BASE) { // usermod range + if ((WLED_USERMOD_PALETTE_ID_BASE - pal) >= (int)usermodPalettes.size()) pal = 0; + } else { // custom range + if ((WLED_CUSTOM_PALETTE_ID_BASE - pal) >= (int)customPalettes.size()) pal = 0; + } + } if (pal != palette) { //DEBUG_PRINTF_P(PSTR("- Starting palette transition: %d\n"), pal); startTransition(strip.getTransition(), blendingStyle != TRANSITION_FADE); // start transition prior to change (no need to copy segment) diff --git a/wled00/colors.cpp b/wled00/colors.cpp index 32fc521045..6ddc4ec892 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -312,6 +312,15 @@ void loadCustomPalettes() { } } +size_t removeUsermodPalettes(const char *name) { + size_t before = usermodPalettes.size(); + for (int i = usermodPalettes.size() - 1; i >= 0; i--) { + if (usermodPalettes[i].name == name) + usermodPalettes.erase(usermodPalettes.begin() + i); + } + return before - usermodPalettes.size(); +} + // convert HSV (16bit hue) to RGB (32bit with white = 0), optimized for speed WLED_O2_ATTR void hsv2rgb_spectrum(const CHSV32& hsv, CRGBW& rgb) { unsigned p, q, t; diff --git a/wled00/colors.h b/wled00/colors.h index 7b8c791c47..105048d35d 100644 --- a/wled00/colors.h +++ b/wled00/colors.h @@ -61,9 +61,20 @@ void adjust_color(CRGBW& rgb, int32_t hueShift, int32_t satChange,int32_t valueC [[gnu::hot, gnu::pure]] uint32_t ColorFromPalette(const CRGBPalette16 &pal, unsigned index, uint8_t brightness = (uint8_t)255U, TBlendType blendType = LINEARBLEND); CRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette); CRGBPalette16 generateRandomPalette(); +// Palette registered by a usermod at fixed IDs (255, 254, 253... 201). +// Display name is "name: palName" (if palName non-null) or falls back to "name index" (e.g. "AudioReactive 1"), see util.cpp +struct UsermodPalette { + CRGBPalette16 palette; + const char *name; // PROGMEM base name string (must not be nullptr), this name is used in removeUsermodPalettes() + uint8_t palIndex; // index of the palette for a usermod + const char *palName; // optional PROGMEM display name; if set, shown as "name: palName" (e.g. "AudioReactive: Audio Responsive Hue"), otherwise falls back to "name index" +}; + void loadCustomPalettes(); +size_t removeUsermodPalettes(const char *name); // remove all entries from usermodPalettes whose name pointer matches 'name' extern std::vector customPalettes; -inline size_t getPaletteCount() { return FIXED_PALETTE_COUNT + customPalettes.size(); } +extern std::vector usermodPalettes; +inline size_t getPaletteCount() { return FIXED_PALETTE_COUNT + usermodPalettes.size() + customPalettes.size(); } void hsv2rgb_spectrum(const CHSV32& hsv, CRGBW& rgb); void hsv2rgb_spectrum(const CHSV& hsv, CRGB& rgb); diff --git a/wled00/const.h b/wled00/const.h index e49dd2900a..5808d4ebc9 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -10,8 +10,16 @@ constexpr size_t FASTLED_PALETTE_COUNT = 7; // 6-12 = sizeof(fastledPalettes) constexpr size_t GRADIENT_PALETTE_COUNT = 59; // 13-72 = sizeof(gGradientPalettes) / sizeof(gGradientPalettes[0]); constexpr size_t DYNAMIC_PALETTE_COUNT = 6; // 0- 5 = dynamic palettes (0=default(virtual),1=random,2=primary,3=primary+secondary,4=primary+secondary+tertiary,5=primary+secondary(+tertiary if not black) constexpr size_t FIXED_PALETTE_COUNT = DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT + GRADIENT_PALETTE_COUNT; // total number of fixed palettes + +// Palette ID space layout (palette IDs are uint8_t, 0-255): +// 0 .. FIXED_PALETTE_COUNT-1 : fixed built-in palettes +// 72 .. WLED_CUSTOM_PALETTE_ID_BASE(200) : user custom palettes (index 0 = ID 200, growing downward) +// 201.. WLED_USERMOD_PALETTE_ID_BASE(255): usermod-registered palettes (index 0 = ID 255, growing downward) +constexpr uint8_t WLED_USERMOD_PALETTE_ID_BASE = 255; // highest ID for usermod palettes +constexpr uint8_t WLED_CUSTOM_PALETTE_ID_BASE = 200; // highest ID for user custom palettes +constexpr size_t WLED_MAX_USERMOD_PALETTES = WLED_USERMOD_PALETTE_ID_BASE - WLED_CUSTOM_PALETTE_ID_BASE; // 55 slots (IDs 201-255) #ifndef ESP8266 - #define WLED_MAX_CUSTOM_PALETTES (255 - FIXED_PALETTE_COUNT) // allow up to 255 total palettes, user is warned about stability issues when adding more than 10 + #define WLED_MAX_CUSTOM_PALETTES (WLED_CUSTOM_PALETTE_ID_BASE - FIXED_PALETTE_COUNT + 1) // 129 slots (IDs 72-200) #else #define WLED_MAX_CUSTOM_PALETTES 10 // ESP8266: limit custom palettes to 10 #endif diff --git a/wled00/data/cpal/cpal.htm b/wled00/data/cpal/cpal.htm index b5abad054e..841c95be32 100644 --- a/wled00/data/cpal/cpal.htm +++ b/wled00/data/cpal/cpal.htm @@ -460,7 +460,11 @@ rm.className = 'sml'; rm.title = 'Delete palette'; rm.innerHTML = '✖'; - rm.onclick = () => { requestJson({rmcpal:i}); setTimeout(refr, 500); }; + rm.onclick = () => { + requestJson({rmcpal:i}); // send remove command + setTimeout(refr, 500); // slight delay to allow ESP to process deletion before fetching updated list + localStorage.removeItem('wledPalx'); // invalidate main UI cache + }; const name = isEmpty(p.palette) ? 'Empty slot' : 'Custom' + i; const css = isEmpty(p.palette) ? '#666' : cssArr(p.palette); diff --git a/wled00/data/index.js b/wled00/data/index.js index 023c2f00ab..ee5126973c 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -989,23 +989,39 @@ function populatePalettes() ); } gId('pallist').innerHTML=html; - // append custom palettes (when loading for the 1st time) + // append usermod palettes (fixed ID space: 255 down to 201) let li = lastinfo; - if (!isEmpty(li) && li.cpalcount) { - for (let j = 0; j e[1] === 128 && e[2] === 128 && e[3] === 128)) continue; // skip all gray gap-placeholder entries + if (!isEmpty(li) && li.umpalcount && li.umpalnames) { + for (let j = 0; j < li.umpalcount; j++) { let div = d.createElement("div"); gId('pallist').appendChild(div); div.outerHTML = generateListItemHtml( 'palette', 255-j, - '~ Custom '+j+' ~', + li.umpalnames[j], 'setPalette', `
` ); } } + // append user custom palettes (fixed ID space: 200 down to FIXED_PALETTE_COUNT+1) + if (!isEmpty(li) && li.cpalcount) { + for (let j = 0; j < li.cpalcount; j++) { + const id = 200 - j; + const pd = palettesData[id]; + if (pd && pd.length === 16 && pd.every(e => e[1] === 128 && e[2] === 128 && e[3] === 128)) continue; // skip gray gap-placeholder entries + let div = d.createElement("div"); + gId('pallist').appendChild(div); + div.outerHTML = generateListItemHtml( + 'palette', + id, + '~ Custom '+j+' ~', + 'setPalette', + `
` + ); + } + } + updateSelectedPalette(selectedPal); // update selection after adding usermod and custom palettes } function redrawPalPrev() @@ -2819,7 +2835,7 @@ function loadPalettesData() { if (lsPalData) { try { var d = JSON.parse(lsPalData); - if (d && d.vid == lastinfo.vid) { + if (d && d.vid == lastinfo.vid && d.pcount == lastinfo.palcount) { palettesData = d.p; redrawPalPrev(); return resolve(); @@ -2831,7 +2847,8 @@ function loadPalettesData() { getPalettesData(0, () => { localStorage.setItem("wledPalx", JSON.stringify({ p: palettesData, - vid: lastinfo.vid + vid: lastinfo.vid, + pcount: lastinfo.palcount // total palette count, refresh cache if it changes })); redrawPalPrev(); setTimeout(resolve, 99); // delay optional diff --git a/wled00/json.cpp b/wled00/json.cpp index b8f29e08d6..74e12b5470 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -774,8 +774,18 @@ void serializeInfo(JsonObject root) root[F("fxcount")] = strip.getModeCount(); root[F("palcount")] = getPaletteCount(); - root[F("cpalcount")] = customPalettes.size(); // number of custom palettes (includes gray placeholders) + root[F("cpalcount")] = customPalettes.size(); // number of user custom palettes (includes gray placeholders) + root[F("umpalcount")] = usermodPalettes.size(); // number of usermod-registered palettes root[F("cpalmax")] = WLED_MAX_CUSTOM_PALETTES; // maximum number of custom palettes + // send usermod palette names so the UI can label them correctly + if (usermodPalettes.size() > 0) { + JsonArray umpalnames = root.createNestedArray(F("umpalnames")); + for (size_t j = 0; j < usermodPalettes.size(); j++) { + char buf[34]; + extractModeName(WLED_USERMOD_PALETTE_ID_BASE - j, JSON_palette_names, buf, sizeof(buf) - 1); + umpalnames.add(buf); + } + } JsonArray ledmaps = root.createNestedArray(F("maps")); for (size_t i=0; i maxPage) page = maxPage; const int start = itemPerPage * page; - int end = min(start + itemPerPage, palettesCount + customPalettesCount); + int end = min(start + itemPerPage, palettesCount + umPalettesCount + customPalettesCount); root[F("m")] = maxPage; // inform caller how many pages there are JsonObject palettes = root.createNestedObject("p"); for (int i = start; i < end; i++) { - JsonArray curPalette = palettes.createNestedArray(String(i >= palettesCount ? 255 - i + palettesCount : i)); + // compute the palette ID for this sequential index + int paletteId; + if (i >= palettesCount + umPalettesCount) // user custom palette (IDs 200, 199, ...) + paletteId = WLED_CUSTOM_PALETTE_ID_BASE - (i - palettesCount - umPalettesCount); + else if (i >= palettesCount) // usermod palette (IDs 255, 254, ...) + paletteId = WLED_USERMOD_PALETTE_ID_BASE - (i - palettesCount); + else + paletteId = i; // fixed palette + JsonArray curPalette = palettes.createNestedArray(String(paletteId)); switch (i) { case 0: //default palette setPaletteColors(curPalette, PartyColors_gc22); @@ -987,9 +1006,13 @@ void serializePalettes(JsonObject root, int page) curPalette.add("c1"); break; default: - if (i >= palettesCount) // custom palettes - setPaletteColors(curPalette, customPalettes[i - palettesCount]); - else if (i < DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT) // palette 6 - 12, fastled palettes + if (i >= palettesCount + umPalettesCount) { // user custom palettes (lowest IDs in the custom range) + int custIdx = i - palettesCount - umPalettesCount; + setPaletteColors(curPalette, customPalettes[custIdx]); + } else if (i >= palettesCount) { // usermod palettes (IDs 255, 254, ...) + int umIdx = i - palettesCount; + setPaletteColors(curPalette, usermodPalettes[umIdx].palette); + } else if (i < DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT) // palette 6 - 12, fastled palettes setPaletteColors(curPalette, *fastledPalettes[i - DYNAMIC_PALETTE_COUNT]); else { memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[i - (DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT)])), sizeof(tcp)); diff --git a/wled00/util.cpp b/wled00/util.cpp index 9830d5b6f6..66aaea97fc 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -317,10 +317,36 @@ uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLe } else return 0; } - if (src == JSON_palette_names && mode > 255-customPalettes.size()) { - snprintf_P(dest, maxLen, PSTR("~ Custom %d ~"), 255-mode); - dest[maxLen] = '\0'; - return strlen(dest); + if (src == JSON_palette_names) { + if (mode > WLED_CUSTOM_PALETTE_ID_BASE) { + // usermod palette (IDs 201-255) + uint8_t umIdx = WLED_USERMOD_PALETTE_ID_BASE - mode; + if (umIdx >= usermodPalettes.size()) { + dest[0] = '\0'; // empty string if requested index is out of bounds + return 0; + } + const UsermodPalette &ump = usermodPalettes[umIdx]; + char base[33]; + strncpy_P(base, ump.name, sizeof(base) - 1); + base[sizeof(base) - 1] = '\0'; + if (ump.palName) { + // usermod supplied a specific display name — prefix with the usermod name (e.g. "AudioReactive: Hue") + char palName[33]; + strncpy_P(palName, ump.palName, sizeof(palName) - 1); + palName[sizeof(palName) - 1] = '\0'; + snprintf(dest, maxLen + 1, "%s: %s", base, palName); + } else { + // fallback: "UMName index" (e.g. "AudioReactive 1") + snprintf(dest, maxLen + 1, "%s %u", base, (unsigned)ump.palIndex); + } + return strlen(dest); + } + if (mode >= FIXED_PALETTE_COUNT && mode <= WLED_CUSTOM_PALETTE_ID_BASE) { + // user custom palette (IDs FIXED_PALETTE_COUNT up to WLED_CUSTOM_PALETTE_ID_BASE=200) + snprintf_P(dest, maxLen, PSTR("~ Custom %d ~"), WLED_CUSTOM_PALETTE_ID_BASE - mode); + dest[maxLen] = '\0'; + return strlen(dest); + } } unsigned qComma = 0; diff --git a/wled00/wled.h b/wled00/wled.h index b96264c25c..c1887116b2 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -599,7 +599,8 @@ WLED_GLOBAL bool wasConnected _INIT(false); // color WLED_GLOBAL byte lastRandomIndex _INIT(0); // used to save last random color so the new one is not the same -WLED_GLOBAL std::vector customPalettes; // custom palettes +WLED_GLOBAL std::vector customPalettes; // custom palettes (file-based, IDs grow downwards starting at 200) +WLED_GLOBAL std::vector usermodPalettes; // usermod-registered palettes (IDs 255, 254, 253...) WLED_GLOBAL uint8_t paletteBlend _INIT(0); // determines blending and wrapping of palette: 0: blend, wrap if moving (SEGMENT.speed>0); 1: blend, always wrap; 2: blend, never wrap; 3: don't blend or wrap // transitions