From f06bf513535171c49745399a7cb65a046d5c9381 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Sun, 21 Dec 2025 17:06:28 -0700 Subject: [PATCH 01/12] Added Morse Code effect to the user_fx usermod --- usermods/user_fx/user_fx.cpp | 132 +++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index da6937c87d..372e6a8104 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -89,6 +89,137 @@ unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vW static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spark rate,Diffusion Speed,Turbulence,,Use palette;;Color;;2;pal=35"; +/* +/ Scrolling Morse Code by Bob Loeffler +* With help from code by automaticaddison.com and then a pass through claude.ai +* aux0 is the pattern offset for scrolling +* aux1 is the total pattern length +*/ + +// Build morse pattern into a buffer +void build_morsecode_pattern(const char *morse_code, bool *pattern, int &index) { +// unsigned int i = 0; + const char *c = morse_code; + + // Build the dots and dashes into pattern array + while (*c != '\0') { + // it's a dot which is 1 pixel + if (*c == '.') { + pattern[index++] = true; + } + else { // Must be a dash which is 3 pixels + pattern[index++] = true; + pattern[index++] = true; + pattern[index++] = true; + } + + // 1 space between parts of a letter or number + pattern[index++] = false; + c++; + } + + // 3 spaces between two letters + pattern[index++] = false; + pattern[index++] = false; + pattern[index++] = false; +} + +static uint16_t mode_morsecode(void) { + if (SEGLEN < 1) return mode_static(); + + // A-Z in Morse Code + static const char * letters[] PROGMEM = {".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---", "-.-", ".-..", "--", + "-.", "---", ".--.", "--.-", ".-.", "...", "-", "..-", "...-", ".--", "-..-", "-.--", "--.."}; + // 0-9 in Morse Code + static const char * numbers[] PROGMEM = {"-----", ".----", "..---", "...--", "....-", ".....", "-....", "--...", "---..", "----."}; + + // Get the text to display + char text[WLED_MAX_SEGNAME_LEN+1] = {'\0'}; + size_t len = 0; + + if (SEGMENT.name) len = strlen(SEGMENT.name); + if (len == 0) { // fallback if empty segment name + strcpy_P(text, PSTR("I Love WLED")); + } else { + strcpy(text, SEGMENT.name); + } + + // Convert to uppercase in place + for (char *p = text; *p; p++) { + *p = toupper(*p); + } + + // Build the complete morse pattern (estimate max size generously) + static bool morsecodePattern[1024]; // Static to avoid stack overflow + + static char lastText[WLED_MAX_SEGNAME_LEN+1] = {'\0'}; // Track last text + bool textChanged = (strcmp(text, lastText) != 0); // Check if the text has changed since the last frame + + // Initialize on first call or rebuild pattern + if (SEGENV.call == 0 || textChanged) { + SEGENV.aux0 = 0; + strcpy(lastText, text); // Save current text + + int patternLength = 0; + + // Build complete morse code pattern + for (char *c = text; *c; c++) { + // Check for letters + if (*c >= 'A' && *c <= 'Z') { + build_morsecode_pattern(letters[*c - 'A'], morsecodePattern, patternLength); + } + // Check for numbers + else if (*c >= '0' && *c <= '9') { + build_morsecode_pattern(numbers[*c - '0'], morsecodePattern, patternLength); + } + // Check for space between words + else if (*c == ' ') { + for (int x = 0; x < 7; x++) { + morsecodePattern[patternLength++] = false; + } + } + } + + // End of message + build_morsecode_pattern(".-.-.", morsecodePattern, patternLength); + for (int x = 0; x < 7; x++) { + morsecodePattern[patternLength++] = false; + } + + SEGENV.aux1 = patternLength; // Store pattern length + } + + // Update offset to make the morse code scroll + uint32_t cycleTime = 50 + (255 - SEGMENT.speed)*3; + uint32_t it = strip.now / cycleTime; + if (SEGENV.step != it) { + SEGENV.aux0++; // Increment scroll offset + SEGENV.step = it; + } + + int patternLength = SEGENV.aux1; + + // Clear background + SEGMENT.fill(BLACK); + + // Draw the scrolling pattern + int offset = SEGENV.aux0 % patternLength; + + for (int i = 0; i < SEGLEN; i++) { + int patternIndex = (offset + i) % patternLength; + if (morsecodePattern[patternIndex]) { + if (SEGMENT.check1) + SEGMENT.setPixelColor(i, SEGMENT.color_wheel(SEGENV.aux0 + i)); + else + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, (strip.paletteBlend == 1 || strip.paletteBlend == 3), 0)); + } + } + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_MORSECODE[] PROGMEM = "Morse Code@Speed,,,,,Colorful,,;;!;1;sx=128,o1=1"; + + ///////////////////// // UserMod Class // ///////////////////// @@ -98,6 +229,7 @@ class UserFxUsermod : public Usermod { public: void setup() override { strip.addEffect(255, &mode_diffusionfire, _data_FX_MODE_DIFFUSIONFIRE); + strip.addEffect(255, &mode_morsecode, _data_FX_MODE_PS_MORSECODE); //////////////////////////////////////// // add your effect function(s) here // From c2a0a2e21181d5d563ec7c488206907aedecebc9 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Sun, 21 Dec 2025 23:53:33 -0700 Subject: [PATCH 02/12] Added a few comments --- usermods/user_fx/user_fx.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 372e6a8104..a02fe83f46 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -91,9 +91,12 @@ static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spar /* / Scrolling Morse Code by Bob Loeffler -* With help from code by automaticaddison.com and then a pass through claude.ai +* Adapted from code by automaticaddison.com and then optimized by claude.ai * aux0 is the pattern offset for scrolling * aux1 is the total pattern length +* the sx slider selects the scrolling speed +* check1 selects the color mode +* we get the text from the SEGMENT.name and convert it to morse code */ // Build morse pattern into a buffer @@ -129,7 +132,7 @@ static uint16_t mode_morsecode(void) { // A-Z in Morse Code static const char * letters[] PROGMEM = {".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---", "-.-", ".-..", "--", - "-.", "---", ".--.", "--.-", ".-.", "...", "-", "..-", "...-", ".--", "-..-", "-.--", "--.."}; + "-.", "---", ".--.", "--.-", ".-.", "...", "-", "..-", "...-", ".--", "-..-", "-.--", "--.."}; // 0-9 in Morse Code static const char * numbers[] PROGMEM = {"-----", ".----", "..---", "...--", "....-", ".....", "-....", "--...", "---..", "----."}; From 580584c8743f882e2489ac16c7a6b0ddd4d74b99 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Sun, 28 Dec 2025 00:36:37 -0700 Subject: [PATCH 03/12] Added punctuation and end-of-message codes, and changing them will force a re-draw. --- usermods/user_fx/user_fx.cpp | 76 ++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 20 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index a02fe83f46..3b17edc2fd 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -2,6 +2,10 @@ // for information how FX metadata strings work see https://kno.wled.ge/interfaces/json-api/#effect-metadata +// paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) +#define PALETTE_SOLID_WRAP (strip.paletteBlend == 1 || strip.paletteBlend == 3) +#define PALETTE_MOVING_WRAP !(strip.paletteBlend == 2 || (strip.paletteBlend == 0 && SEGMENT.speed == 0)) + // static effect, used if an effect fails to initialize static uint16_t mode_static(void) { SEGMENT.fill(SEGCOLOR(0)); @@ -94,14 +98,18 @@ static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spar * Adapted from code by automaticaddison.com and then optimized by claude.ai * aux0 is the pattern offset for scrolling * aux1 is the total pattern length -* the sx slider selects the scrolling speed -* check1 selects the color mode -* we get the text from the SEGMENT.name and convert it to morse code +* The sx slider selects the scrolling speed +* Checkbox1 selects the color mode +* Checkbox2 displays punctuation or not +* Checkbox3 displays the End-of-message code or not +* We get the text from the SEGMENT.name and convert it to morse code +* Morse Code rules: +* - there is one space between each part of a letter or number +* - there are 3 spaces between each letter or number +* - there are 7 spaces between each word */ - // Build morse pattern into a buffer void build_morsecode_pattern(const char *morse_code, bool *pattern, int &index) { -// unsigned int i = 0; const char *c = morse_code; // Build the dots and dashes into pattern array @@ -116,12 +124,12 @@ void build_morsecode_pattern(const char *morse_code, bool *pattern, int &index) pattern[index++] = true; } - // 1 space between parts of a letter or number + // 1 space between parts of a character (letter/number/punctuation) pattern[index++] = false; c++; } - // 3 spaces between two letters + // 3 spaces between each character pattern[index++] = false; pattern[index++] = false; pattern[index++] = false; @@ -132,7 +140,7 @@ static uint16_t mode_morsecode(void) { // A-Z in Morse Code static const char * letters[] PROGMEM = {".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---", "-.-", ".-..", "--", - "-.", "---", ".--.", "--.-", ".-.", "...", "-", "..-", "...-", ".--", "-..-", "-.--", "--.."}; + "-.", "---", ".--.", "--.-", ".-.", "...", "-", "..-", "...-", ".--", "-..-", "-.--", "--.."}; // 0-9 in Morse Code static const char * numbers[] PROGMEM = {"-----", ".----", "..---", "...--", "....-", ".....", "-....", "--...", "---..", "----."}; @@ -142,7 +150,7 @@ static uint16_t mode_morsecode(void) { if (SEGMENT.name) len = strlen(SEGMENT.name); if (len == 0) { // fallback if empty segment name - strcpy_P(text, PSTR("I Love WLED")); + strcpy_P(text, PSTR("I Love WLED!")); } else { strcpy(text, SEGMENT.name); } @@ -155,14 +163,18 @@ static uint16_t mode_morsecode(void) { // Build the complete morse pattern (estimate max size generously) static bool morsecodePattern[1024]; // Static to avoid stack overflow + static bool lastCheck2 = false; + static bool lastCheck3 = false; static char lastText[WLED_MAX_SEGNAME_LEN+1] = {'\0'}; // Track last text - bool textChanged = (strcmp(text, lastText) != 0); // Check if the text has changed since the last frame + + bool settingsChanged = (SEGMENT.check2 != lastCheck2) || (SEGMENT.check3 != lastCheck3); // check if any checkbox settings were changed since last frame + bool textChanged = (strcmp(text, lastText) != 0); // check if the text has changed since the last frame // Initialize on first call or rebuild pattern - if (SEGENV.call == 0 || textChanged) { - SEGENV.aux0 = 0; + if (SEGENV.call == 0 || textChanged || settingsChanged) { strcpy(lastText, text); // Save current text - + lastCheck2 = SEGMENT.check2; // Save current state + lastCheck3 = SEGMENT.check3; // Save current state int patternLength = 0; // Build complete morse code pattern @@ -175,17 +187,41 @@ static uint16_t mode_morsecode(void) { else if (*c >= '0' && *c <= '9') { build_morsecode_pattern(numbers[*c - '0'], morsecodePattern, patternLength); } - // Check for space between words + // Check for a space between words else if (*c == ' ') { - for (int x = 0; x < 7; x++) { + for (int x = 0; x < 4; x++) { // 7 spaces after the morse code pattern (3 after the last character and now 4 more) morsecodePattern[patternLength++] = false; } } + // Check for punctuation + else if (SEGMENT.check2) { + const char *punctuationCode = nullptr; + switch (*c) { + case '.': punctuationCode = ".-.-.-"; break; + case ',': punctuationCode = "--..--"; break; + case '?': punctuationCode = "..--.."; break; + case ':': punctuationCode = "---..."; break; + case '-': punctuationCode = "-....-"; break; + case '!': punctuationCode = "-.-.--"; break; + case '&': punctuationCode = ".-..."; break; + case '@': punctuationCode = ".--.-."; break; + case ')': punctuationCode = "-.--.-"; break; + case '(': punctuationCode = "-.--."; break; + case '/': punctuationCode = "-..-."; break; + case '\'': punctuationCode = ".----."; break; // apostrophe character must be escaped with a \ character + } + if (punctuationCode) { + build_morsecode_pattern(punctuationCode, morsecodePattern, patternLength); + } + } + } + + // Build the End-of-message pattern + if (SEGMENT.check3) { + build_morsecode_pattern(".-.-.", morsecodePattern, patternLength); } - // End of message - build_morsecode_pattern(".-.-.", morsecodePattern, patternLength); - for (int x = 0; x < 7; x++) { + for (int x = 0; x < 7; x++) { // 10 spaces after the last pattern (3 after the last character and now 7 more) morsecodePattern[patternLength++] = false; } @@ -214,13 +250,13 @@ static uint16_t mode_morsecode(void) { if (SEGMENT.check1) SEGMENT.setPixelColor(i, SEGMENT.color_wheel(SEGENV.aux0 + i)); else - SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, (strip.paletteBlend == 1 || strip.paletteBlend == 3), 0)); + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } } return FRAMETIME; } -static const char _data_FX_MODE_PS_MORSECODE[] PROGMEM = "Morse Code@Speed,,,,,Colorful,,;;!;1;sx=128,o1=1"; +static const char _data_FX_MODE_MORSECODE[] PROGMEM = "Morse Code@Speed,,,,,Color mode,Punctuation,EndOfMessage;;!;1;sx=128,o1=1,o2=1"; ///////////////////// From ab806abfb17d10e7dc52ac06deb16ebcf6dc11f6 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Sun, 28 Dec 2025 01:08:47 -0700 Subject: [PATCH 04/12] Fixed mode name in addEffect --- usermods/user_fx/user_fx.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 3b17edc2fd..d87090eba7 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -268,7 +268,7 @@ class UserFxUsermod : public Usermod { public: void setup() override { strip.addEffect(255, &mode_diffusionfire, _data_FX_MODE_DIFFUSIONFIRE); - strip.addEffect(255, &mode_morsecode, _data_FX_MODE_PS_MORSECODE); + strip.addEffect(255, &mode_morsecode, _data_FX_MODE_MORSECODE); //////////////////////////////////////// // add your effect function(s) here // From a2efb16d74393e4edede2dd7b6cb9266a99db93e Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Wed, 31 Dec 2025 01:05:56 -0700 Subject: [PATCH 05/12] cosmetic changes --- usermods/user_fx/user_fx.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index d87090eba7..ae16c0e0d7 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -108,6 +108,7 @@ static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spar * - there are 3 spaces between each letter or number * - there are 7 spaces between each word */ + // Build morse pattern into a buffer void build_morsecode_pattern(const char *morse_code, bool *pattern, int &index) { const char *c = morse_code; @@ -259,6 +260,7 @@ static uint16_t mode_morsecode(void) { static const char _data_FX_MODE_MORSECODE[] PROGMEM = "Morse Code@Speed,,,,,Color mode,Punctuation,EndOfMessage;;!;1;sx=128,o1=1,o2=1"; + ///////////////////// // UserMod Class // ///////////////////// From c5cfbe74ad604541ec76d07a080bbe0a3498666d Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Thu, 1 Jan 2026 10:27:22 -0700 Subject: [PATCH 06/12] * removed PROGMEM from letters and numbers arrays. * changed 1024 to a constexpr named MORSECODE_MAX_PATTERN_SIZE. * added MORSECODE_MAX_PATTERN_SIZE to build_morsecode_pattern(). * added boundary checked when adding patterns to the array. * changed code to allocate per-segment storage for pattern. --- usermods/user_fx/user_fx.cpp | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index ae16c0e0d7..37f0bb13d5 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -110,27 +110,30 @@ static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spar */ // Build morse pattern into a buffer -void build_morsecode_pattern(const char *morse_code, bool *pattern, int &index) { +void build_morsecode_pattern(const char *morse_code, bool *pattern, int &index, int maxSize) { const char *c = morse_code; // Build the dots and dashes into pattern array while (*c != '\0') { + if (index >= maxSize - 4) return; // Reserve space for spacing // it's a dot which is 1 pixel if (*c == '.') { pattern[index++] = true; } else { // Must be a dash which is 3 pixels + if (index >= maxSize - 3) return; pattern[index++] = true; pattern[index++] = true; pattern[index++] = true; } - // 1 space between parts of a character (letter/number/punctuation) + // 1 space between parts of a letter/number pattern[index++] = false; c++; } - // 3 spaces between each character + // 3 spaces between two letters + if (index >= maxSize - 3) return; pattern[index++] = false; pattern[index++] = false; pattern[index++] = false; @@ -138,17 +141,17 @@ void build_morsecode_pattern(const char *morse_code, bool *pattern, int &index) static uint16_t mode_morsecode(void) { if (SEGLEN < 1) return mode_static(); - + // A-Z in Morse Code - static const char * letters[] PROGMEM = {".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---", "-.-", ".-..", "--", + static const char * letters[] = {".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---", "-.-", ".-..", "--", "-.", "---", ".--.", "--.-", ".-.", "...", "-", "..-", "...-", ".--", "-..-", "-.--", "--.."}; // 0-9 in Morse Code - static const char * numbers[] PROGMEM = {"-----", ".----", "..---", "...--", "....-", ".....", "-....", "--...", "---..", "----."}; + static const char * numbers[] = {"-----", ".----", "..---", "...--", "....-", ".....", "-....", "--...", "---..", "----."}; // Get the text to display char text[WLED_MAX_SEGNAME_LEN+1] = {'\0'}; size_t len = 0; - + if (SEGMENT.name) len = strlen(SEGMENT.name); if (len == 0) { // fallback if empty segment name strcpy_P(text, PSTR("I Love WLED!")); @@ -161,8 +164,10 @@ static uint16_t mode_morsecode(void) { *p = toupper(*p); } - // Build the complete morse pattern (estimate max size generously) - static bool morsecodePattern[1024]; // Static to avoid stack overflow + // Allocate per-segment storage for pattern + constexpr size_t MORSECODE_MAX_PATTERN_SIZE = 1024; + if (!SEGENV.allocateData(MORSECODE_MAX_PATTERN_SIZE)) return mode_static(); + bool* morsecodePattern = reinterpret_cast(SEGENV.data); static bool lastCheck2 = false; static bool lastCheck3 = false; @@ -182,11 +187,11 @@ static uint16_t mode_morsecode(void) { for (char *c = text; *c; c++) { // Check for letters if (*c >= 'A' && *c <= 'Z') { - build_morsecode_pattern(letters[*c - 'A'], morsecodePattern, patternLength); - } + build_morsecode_pattern(letters[*c - 'A'], morsecodePattern, patternLength, MORSECODE_MAX_PATTERN_SIZE); + } // Check for numbers else if (*c >= '0' && *c <= '9') { - build_morsecode_pattern(numbers[*c - '0'], morsecodePattern, patternLength); + build_morsecode_pattern(numbers[*c - '0'], morsecodePattern, patternLength, MORSECODE_MAX_PATTERN_SIZE); } // Check for a space between words else if (*c == ' ') { @@ -212,14 +217,14 @@ static uint16_t mode_morsecode(void) { case '\'': punctuationCode = ".----."; break; // apostrophe character must be escaped with a \ character } if (punctuationCode) { - build_morsecode_pattern(punctuationCode, morsecodePattern, patternLength); + build_morsecode_pattern(punctuationCode, morsecodePattern, patternLength, MORSECODE_MAX_PATTERN_SIZE); } } } // Build the End-of-message pattern if (SEGMENT.check3) { - build_morsecode_pattern(".-.-.", morsecodePattern, patternLength); + build_morsecode_pattern(".-.-.", morsecodePattern, patternLength, MORSECODE_MAX_PATTERN_SIZE); } for (int x = 0; x < 7; x++) { // 10 spaces after the last pattern (3 after the last character and now 7 more) From c8a7dd98dd5e0b7c866afc2cc6ec25901488a759 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Thu, 1 Jan 2026 11:20:50 -0700 Subject: [PATCH 07/12] More bounds checking added. --- usermods/user_fx/user_fx.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 37f0bb13d5..cec1d148cd 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -115,9 +115,9 @@ void build_morsecode_pattern(const char *morse_code, bool *pattern, int &index, // Build the dots and dashes into pattern array while (*c != '\0') { - if (index >= maxSize - 4) return; // Reserve space for spacing // it's a dot which is 1 pixel if (*c == '.') { + if (index >= maxSize - 1) return; // Reserve space for spacing pattern[index++] = true; } else { // Must be a dash which is 3 pixels @@ -128,14 +128,17 @@ void build_morsecode_pattern(const char *morse_code, bool *pattern, int &index, } // 1 space between parts of a letter/number + if (index >= maxSize) return; pattern[index++] = false; c++; } // 3 spaces between two letters - if (index >= maxSize - 3) return; + if (index >= maxSize - 2) return; pattern[index++] = false; + if (index >= maxSize - 1) return; pattern[index++] = false; + if (index >= maxSize) return; pattern[index++] = false; } @@ -185,6 +188,7 @@ static uint16_t mode_morsecode(void) { // Build complete morse code pattern for (char *c = text; *c; c++) { + if (patternLength >= MORSECODE_MAX_PATTERN_SIZE - 10) break; // Reserve space for trailing pattern // Check for letters if (*c >= 'A' && *c <= 'Z') { build_morsecode_pattern(letters[*c - 'A'], morsecodePattern, patternLength, MORSECODE_MAX_PATTERN_SIZE); @@ -196,6 +200,7 @@ static uint16_t mode_morsecode(void) { // Check for a space between words else if (*c == ' ') { for (int x = 0; x < 4; x++) { // 7 spaces after the morse code pattern (3 after the last character and now 4 more) + if (patternLength >= MORSECODE_MAX_PATTERN_SIZE) break; morsecodePattern[patternLength++] = false; } } @@ -228,6 +233,7 @@ static uint16_t mode_morsecode(void) { } for (int x = 0; x < 7; x++) { // 10 spaces after the last pattern (3 after the last character and now 7 more) + if (patternLength >= MORSECODE_MAX_PATTERN_SIZE) break; morsecodePattern[patternLength++] = false; } From bd7ef64c9fa4ca37e04f6b5cd687a6305e6875cb Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Thu, 1 Jan 2026 13:57:09 -0700 Subject: [PATCH 08/12] Added a lookup table for punctuation morse codes. --- usermods/user_fx/user_fx.cpp | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index cec1d148cd..be91a2703e 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -151,6 +151,19 @@ static uint16_t mode_morsecode(void) { // 0-9 in Morse Code static const char * numbers[] = {"-----", ".----", "..---", "...--", "....-", ".....", "-....", "--...", "---..", "----."}; + // Punctuation in Morse Code + struct PunctuationMapping { + char character; + const char* code; + }; + + static const PunctuationMapping punctuation[] = { + {'.', ".-.-.-"}, {',', "--..--"}, {'?', "..--.."}, + {':', "---..."}, {'-', "-....-"}, {'!', "-.-.--"}, + {'&', ".-..."}, {'@', ".--.-."}, {')', "-.--.-"}, + {'(', "-.--."}, {'/', "-..-."}, {'\'', ".----."} + }; + // Get the text to display char text[WLED_MAX_SEGNAME_LEN+1] = {'\0'}; size_t len = 0; @@ -207,19 +220,11 @@ static uint16_t mode_morsecode(void) { // Check for punctuation else if (SEGMENT.check2) { const char *punctuationCode = nullptr; - switch (*c) { - case '.': punctuationCode = ".-.-.-"; break; - case ',': punctuationCode = "--..--"; break; - case '?': punctuationCode = "..--.."; break; - case ':': punctuationCode = "---..."; break; - case '-': punctuationCode = "-....-"; break; - case '!': punctuationCode = "-.-.--"; break; - case '&': punctuationCode = ".-..."; break; - case '@': punctuationCode = ".--.-."; break; - case ')': punctuationCode = "-.--.-"; break; - case '(': punctuationCode = "-.--."; break; - case '/': punctuationCode = "-..-."; break; - case '\'': punctuationCode = ".----."; break; // apostrophe character must be escaped with a \ character + for (const auto& p : punctuation) { + if (*c == p.character) { + punctuationCode = p.code; + break; + } } if (punctuationCode) { build_morsecode_pattern(punctuationCode, morsecodePattern, patternLength, MORSECODE_MAX_PATTERN_SIZE); From 85d809911ee206c925f694821a142edf5f99683c Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Fri, 2 Jan 2026 08:33:52 -0700 Subject: [PATCH 09/12] Removed PALETTE_MOVING_WRAP macro as it's not used in this effect. --- usermods/user_fx/user_fx.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index be91a2703e..45aa742c11 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -4,7 +4,6 @@ // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) #define PALETTE_SOLID_WRAP (strip.paletteBlend == 1 || strip.paletteBlend == 3) -#define PALETTE_MOVING_WRAP !(strip.paletteBlend == 2 || (strip.paletteBlend == 0 && SEGMENT.speed == 0)) // static effect, used if an effect fails to initialize static uint16_t mode_static(void) { From 5e0d52a6e01546cb5c827298c9d8f5f514643962 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Sat, 3 Jan 2026 16:00:44 -0700 Subject: [PATCH 10/12] Moved all static variables to SEGENV.data --- usermods/user_fx/user_fx.cpp | 64 +++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 45aa742c11..4e3ac61236 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -109,7 +109,7 @@ static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spar */ // Build morse pattern into a buffer -void build_morsecode_pattern(const char *morse_code, bool *pattern, int &index, int maxSize) { +void build_morsecode_pattern(const char *morse_code, bool *pattern, uint16_t &index, int maxSize) { const char *c = morse_code; // Build the dots and dashes into pattern array @@ -163,12 +163,12 @@ static uint16_t mode_morsecode(void) { {'(', "-.--."}, {'/', "-..-."}, {'\'', ".----."} }; - // Get the text to display + // Get the text to display char text[WLED_MAX_SEGNAME_LEN+1] = {'\0'}; size_t len = 0; if (SEGMENT.name) len = strlen(SEGMENT.name); - if (len == 0) { // fallback if empty segment name + if (len == 0) { strcpy_P(text, PSTR("I Love WLED!")); } else { strcpy(text, SEGMENT.name); @@ -184,39 +184,43 @@ static uint16_t mode_morsecode(void) { if (!SEGENV.allocateData(MORSECODE_MAX_PATTERN_SIZE)) return mode_static(); bool* morsecodePattern = reinterpret_cast(SEGENV.data); - static bool lastCheck2 = false; - static bool lastCheck3 = false; - static char lastText[WLED_MAX_SEGNAME_LEN+1] = {'\0'}; // Track last text + // Use bits in aux1 to store the checkbox states (upper bits since pattern length won't exceed 1024) + // Bit 15: lastCheck2, Bit 14: lastCheck3, Bit 13: textChanged flag, Bits 0-12: pattern length + bool lastCheck2 = (SEGENV.aux1 & 0x8000) != 0; + bool lastCheck3 = (SEGENV.aux1 & 0x4000) != 0; + uint16_t patternLength = SEGENV.aux1 & 0x1FFF; // Lower 13 bits for length (up to 8191) - bool settingsChanged = (SEGMENT.check2 != lastCheck2) || (SEGMENT.check3 != lastCheck3); // check if any checkbox settings were changed since last frame - bool textChanged = (strcmp(text, lastText) != 0); // check if the text has changed since the last frame + bool settingsChanged = (SEGMENT.check2 != lastCheck2) || (SEGMENT.check3 != lastCheck3); + + // For text comparison, we need to store a hash or checksum since we can't store the full text + // Use step for text hash storage when not scrolling + uint16_t textHash = 0; + for (char *p = text; *p; p++) { + textHash = ((textHash << 5) + textHash) + *p; // djb2 hash + } + + bool textChanged = (SEGENV.step != textHash && SEGENV.call != 0); // Initialize on first call or rebuild pattern if (SEGENV.call == 0 || textChanged || settingsChanged) { - strcpy(lastText, text); // Save current text - lastCheck2 = SEGMENT.check2; // Save current state - lastCheck3 = SEGMENT.check3; // Save current state - int patternLength = 0; + patternLength = 0; // Build complete morse code pattern for (char *c = text; *c; c++) { - if (patternLength >= MORSECODE_MAX_PATTERN_SIZE - 10) break; // Reserve space for trailing pattern - // Check for letters + if (patternLength >= MORSECODE_MAX_PATTERN_SIZE - 10) break; + if (*c >= 'A' && *c <= 'Z') { build_morsecode_pattern(letters[*c - 'A'], morsecodePattern, patternLength, MORSECODE_MAX_PATTERN_SIZE); } - // Check for numbers else if (*c >= '0' && *c <= '9') { build_morsecode_pattern(numbers[*c - '0'], morsecodePattern, patternLength, MORSECODE_MAX_PATTERN_SIZE); } - // Check for a space between words else if (*c == ' ') { - for (int x = 0; x < 4; x++) { // 7 spaces after the morse code pattern (3 after the last character and now 4 more) + for (int x = 0; x < 4; x++) { if (patternLength >= MORSECODE_MAX_PATTERN_SIZE) break; morsecodePattern[patternLength++] = false; } } - // Check for punctuation else if (SEGMENT.check2) { const char *punctuationCode = nullptr; for (const auto& p : punctuation) { @@ -231,40 +235,46 @@ static uint16_t mode_morsecode(void) { } } - // Build the End-of-message pattern if (SEGMENT.check3) { build_morsecode_pattern(".-.-.", morsecodePattern, patternLength, MORSECODE_MAX_PATTERN_SIZE); } - for (int x = 0; x < 7; x++) { // 10 spaces after the last pattern (3 after the last character and now 7 more) + for (int x = 0; x < 7; x++) { if (patternLength >= MORSECODE_MAX_PATTERN_SIZE) break; morsecodePattern[patternLength++] = false; } - SEGENV.aux1 = patternLength; // Store pattern length + // Store pattern length and checkbox states in aux1 + SEGENV.aux1 = patternLength | (SEGMENT.check2 ? 0x8000 : 0) | (SEGMENT.check3 ? 0x4000 : 0); + + // Store text hash in step + SEGENV.step = textHash; + + // Reset scroll offset + SEGENV.aux0 = 0; } // Update offset to make the morse code scroll uint32_t cycleTime = 50 + (255 - SEGMENT.speed)*3; uint32_t it = strip.now / cycleTime; - if (SEGENV.step != it) { - SEGENV.aux0++; // Increment scroll offset - SEGENV.step = it; + uint16_t scrollCounter = SEGENV.aux0 >> 8; // Use upper byte for scroll timing + if (scrollCounter != (it & 0xFF)) { + SEGENV.aux0 = (SEGENV.aux0 & 0xFF) + 1 + ((it & 0xFF) << 8); // Increment scroll offset (lower byte), update counter (upper byte) } - int patternLength = SEGENV.aux1; + uint16_t scrollOffset = SEGENV.aux0 & 0xFF; // Clear background SEGMENT.fill(BLACK); // Draw the scrolling pattern - int offset = SEGENV.aux0 % patternLength; + int offset = scrollOffset % patternLength; for (int i = 0; i < SEGLEN; i++) { int patternIndex = (offset + i) % patternLength; if (morsecodePattern[patternIndex]) { if (SEGMENT.check1) - SEGMENT.setPixelColor(i, SEGMENT.color_wheel(SEGENV.aux0 + i)); + SEGMENT.setPixelColor(i, SEGMENT.color_wheel(scrollOffset + i)); else SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } From 3c1c4bef2507971b492ae40155800fa2847dd7f4 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Sun, 4 Jan 2026 13:02:48 -0700 Subject: [PATCH 11/12] Now using a bit array instead of a bool array --- usermods/user_fx/user_fx.cpp | 121 ++++++++++++++++++++--------------- 1 file changed, 68 insertions(+), 53 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 4e3ac61236..930329609b 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -96,49 +96,63 @@ static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spar / Scrolling Morse Code by Bob Loeffler * Adapted from code by automaticaddison.com and then optimized by claude.ai * aux0 is the pattern offset for scrolling -* aux1 is the total pattern length -* The sx slider selects the scrolling speed +* aux1 saves settings: check3 (1 bit), check3 (1 bit), text hash (4 bits) and pattern length (10 bits) +* The first slider (sx) selects the scrolling speed * Checkbox1 selects the color mode * Checkbox2 displays punctuation or not * Checkbox3 displays the End-of-message code or not * We get the text from the SEGMENT.name and convert it to morse code +* This effect uses a bit array, instead of bool array, for efficient storage - 8x memory reduction (128 bytes vs 1024 bytes) +* * Morse Code rules: -* - there is one space between each part of a letter or number -* - there are 3 spaces between each letter or number +* - a dot is 1 pixel/LED; a dash is 3 pixels/LEDs +* - there is 1 space between each part (dot or dash) of a letter/number/punctuation +* - there are 3 spaces between each letter/number/punctuation * - there are 7 spaces between each word */ -// Build morse pattern into a buffer -void build_morsecode_pattern(const char *morse_code, bool *pattern, uint16_t &index, int maxSize) { +// Bit manipulation macros +#define SET_BIT8(arr, i) ((arr)[(i) >> 3] |= (1 << ((i) & 7))) +#define GET_BIT8(arr, i) (((arr)[(i) >> 3] & (1 << ((i) & 7))) != 0) + +// Build morse code pattern into a buffer +void build_morsecode_pattern(const char *morse_code, uint8_t *pattern, uint16_t &index, int maxSize) { const char *c = morse_code; // Build the dots and dashes into pattern array while (*c != '\0') { // it's a dot which is 1 pixel if (*c == '.') { - if (index >= maxSize - 1) return; // Reserve space for spacing - pattern[index++] = true; + if (index >= maxSize - 1) return; + SET_BIT8(pattern, index); + index++; } else { // Must be a dash which is 3 pixels if (index >= maxSize - 3) return; - pattern[index++] = true; - pattern[index++] = true; - pattern[index++] = true; + SET_BIT8(pattern, index); + index++; + SET_BIT8(pattern, index); + index++; + SET_BIT8(pattern, index); + index++; } - - // 1 space between parts of a letter/number - if (index >= maxSize) return; - pattern[index++] = false; + c++; + + // 1 space between parts of a letter/number/punctuation (but not after the last one) + if (*c != '\0') { + if (index >= maxSize) return; + index++; + } } - - // 3 spaces between two letters + + // 3 spaces between two letters/numbers/punctuation if (index >= maxSize - 2) return; - pattern[index++] = false; + index++; if (index >= maxSize - 1) return; - pattern[index++] = false; + index++; if (index >= maxSize) return; - pattern[index++] = false; + index++; } static uint16_t mode_morsecode(void) { @@ -163,7 +177,7 @@ static uint16_t mode_morsecode(void) { {'(', "-.--."}, {'/', "-..-."}, {'\'', ".----."} }; - // Get the text to display + // Get the text to display char text[WLED_MAX_SEGNAME_LEN+1] = {'\0'}; size_t len = 0; @@ -179,36 +193,41 @@ static uint16_t mode_morsecode(void) { *p = toupper(*p); } - // Allocate per-segment storage for pattern + // Allocate per-segment storage for pattern (1024 bits = 128 bytes) constexpr size_t MORSECODE_MAX_PATTERN_SIZE = 1024; - if (!SEGENV.allocateData(MORSECODE_MAX_PATTERN_SIZE)) return mode_static(); - bool* morsecodePattern = reinterpret_cast(SEGENV.data); + constexpr size_t MORSECODE_PATTERN_BYTES = MORSECODE_MAX_PATTERN_SIZE / 8; // 128 bytes + if (!SEGENV.allocateData(MORSECODE_PATTERN_BYTES)) return mode_static(); + uint8_t* morsecodePattern = reinterpret_cast(SEGENV.data); - // Use bits in aux1 to store the checkbox states (upper bits since pattern length won't exceed 1024) - // Bit 15: lastCheck2, Bit 14: lastCheck3, Bit 13: textChanged flag, Bits 0-12: pattern length + // SEGENV.aux1 stores: [bit 15: check2] [bit 14: check3] [bits 10-13: text hash (4 bits)] [bits 0-9: pattern length] bool lastCheck2 = (SEGENV.aux1 & 0x8000) != 0; bool lastCheck3 = (SEGENV.aux1 & 0x4000) != 0; - uint16_t patternLength = SEGENV.aux1 & 0x1FFF; // Lower 13 bits for length (up to 8191) + uint16_t lastHashBits = (SEGENV.aux1 >> 10) & 0xF; // 4 bits of hash + uint16_t patternLength = SEGENV.aux1 & 0x3FF; // Lower 10 bits for length (up to 1023) - bool settingsChanged = (SEGMENT.check2 != lastCheck2) || (SEGMENT.check3 != lastCheck3); - - // For text comparison, we need to store a hash or checksum since we can't store the full text - // Use step for text hash storage when not scrolling + // Compute text hash uint16_t textHash = 0; for (char *p = text; *p; p++) { - textHash = ((textHash << 5) + textHash) + *p; // djb2 hash + textHash = ((textHash << 5) + textHash) + *p; } - - bool textChanged = (SEGENV.step != textHash && SEGENV.call != 0); + uint16_t currentHashBits = (textHash >> 12) & 0xF; // Use upper 4 bits of hash + + bool textChanged = (currentHashBits != lastHashBits) && (SEGENV.call > 0); + + // Check if we need to rebuild the pattern + bool needsRebuild = (SEGENV.call == 0) || textChanged || (SEGMENT.check2 != lastCheck2) || (SEGMENT.check3 != lastCheck3); // Initialize on first call or rebuild pattern - if (SEGENV.call == 0 || textChanged || settingsChanged) { + if (needsRebuild) { patternLength = 0; + // Clear the bit array first + memset(morsecodePattern, 0, MORSECODE_PATTERN_BYTES); + // Build complete morse code pattern for (char *c = text; *c; c++) { if (patternLength >= MORSECODE_MAX_PATTERN_SIZE - 10) break; - + if (*c >= 'A' && *c <= 'Z') { build_morsecode_pattern(letters[*c - 'A'], morsecodePattern, patternLength, MORSECODE_MAX_PATTERN_SIZE); } @@ -218,7 +237,7 @@ static uint16_t mode_morsecode(void) { else if (*c == ' ') { for (int x = 0; x < 4; x++) { if (patternLength >= MORSECODE_MAX_PATTERN_SIZE) break; - morsecodePattern[patternLength++] = false; + patternLength++; } } else if (SEGMENT.check2) { @@ -241,40 +260,36 @@ static uint16_t mode_morsecode(void) { for (int x = 0; x < 7; x++) { if (patternLength >= MORSECODE_MAX_PATTERN_SIZE) break; - morsecodePattern[patternLength++] = false; + patternLength++; } - // Store pattern length and checkbox states in aux1 - SEGENV.aux1 = patternLength | (SEGMENT.check2 ? 0x8000 : 0) | (SEGMENT.check3 ? 0x4000 : 0); - - // Store text hash in step - SEGENV.step = textHash; - - // Reset scroll offset + // Store pattern length, checkbox states, and hash bits in aux1 + SEGENV.aux1 = patternLength | (currentHashBits << 10) | (SEGMENT.check2 ? 0x8000 : 0) | (SEGMENT.check3 ? 0x4000 : 0); + + // Reset the scroll offset SEGENV.aux0 = 0; } // Update offset to make the morse code scroll + // Use step for scroll timing only uint32_t cycleTime = 50 + (255 - SEGMENT.speed)*3; uint32_t it = strip.now / cycleTime; - uint16_t scrollCounter = SEGENV.aux0 >> 8; // Use upper byte for scroll timing - if (scrollCounter != (it & 0xFF)) { - SEGENV.aux0 = (SEGENV.aux0 & 0xFF) + 1 + ((it & 0xFF) << 8); // Increment scroll offset (lower byte), update counter (upper byte) + if (SEGENV.step != it) { + SEGENV.aux0++; + SEGENV.step = it; } - uint16_t scrollOffset = SEGENV.aux0 & 0xFF; - // Clear background SEGMENT.fill(BLACK); // Draw the scrolling pattern - int offset = scrollOffset % patternLength; + int offset = SEGENV.aux0 % patternLength; for (int i = 0; i < SEGLEN; i++) { int patternIndex = (offset + i) % patternLength; - if (morsecodePattern[patternIndex]) { + if (GET_BIT8(morsecodePattern, patternIndex)) { if (SEGMENT.check1) - SEGMENT.setPixelColor(i, SEGMENT.color_wheel(scrollOffset + i)); + SEGMENT.setPixelColor(i, SEGMENT.color_wheel(SEGENV.aux0 + i)); else SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } From d05db8070bc482a191efa7a1f920aed253c5305f Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Sun, 4 Jan 2026 13:27:23 -0700 Subject: [PATCH 12/12] added a check to see if the pattern is empty --- usermods/user_fx/user_fx.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 930329609b..d94336d529 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -270,6 +270,12 @@ static uint16_t mode_morsecode(void) { SEGENV.aux0 = 0; } + // if pattern is empty for some reason, display black background only + if (patternLength == 0) { + SEGMENT.fill(BLACK); + return FRAMETIME; + } + // Update offset to make the morse code scroll // Use step for scroll timing only uint32_t cycleTime = 50 + (255 - SEGMENT.speed)*3;