From ccaa55f606b04eea7f5b9f9066813ba8af1a1974 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 10 Oct 2026 19:44:23 +0200 Subject: [PATCH 1/3] optimise morse code FX and move it into core --- usermods/user_fx/user_fx.cpp | 253 ----------------------------------- wled00/FX.cpp | 145 ++++++++++++++++++++ wled00/FX.h | 2 +- 3 files changed, 146 insertions(+), 254 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 0878672a4a..d9acb2fda9 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -1005,259 +1005,6 @@ static void mode_ants(void) { } static const char _data_FX_MODE_ANTS[] PROGMEM = "Ants@Ant speed,# of ants,Ant size,Blur,,Gathering food,Smear,Pass by;!,!,!;!;1;sx=192,ix=255,c1=32,c2=0,o1=1,o3=1"; - -/* -/ 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 saves settings: check2 (1 bit), check3 (1 bit), text hash (4 bits) and pattern length (10 bits) -* The first slider (sx) selects the scrolling speed -* The second slider selects the color mode (lower half selects color wheel, upper half selects color palettes) -* Checkbox1 displays all letters in a word with the same color -* 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: -* - a dot is 1 pixel/LED; a dash is 3 pixels/LEDs -* - there is 1 space between each dot or dash that make up a letter/number/punctuation -* - there are 3 spaces between each letter/number/punctuation -* - there are 7 spaces between each word -*/ - -// 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 -static void build_morsecode_pattern(const char *morse_code, uint8_t *pattern, uint8_t *wordIndex, uint16_t &index, uint8_t currentWord, 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; - SET_BIT8(pattern, index); - wordIndex[index] = currentWord; - index++; - } - else { // Must be a dash which is 3 pixels - if (index >= maxSize - 3) return; - SET_BIT8(pattern, index); - wordIndex[index] = currentWord; - index++; - SET_BIT8(pattern, index); - wordIndex[index] = currentWord; - index++; - SET_BIT8(pattern, index); - wordIndex[index] = currentWord; - index++; - } - - c++; - - // 1 space between parts of a letter/number/punctuation (but not after the last one) - if (*c != '\0') { - if (index >= maxSize) return; - wordIndex[index] = currentWord; - index++; - } - } - - // 3 spaces between two letters/numbers/punctuation - if (index >= maxSize - 2) return; - wordIndex[index] = currentWord; - index++; - if (index >= maxSize - 1) return; - wordIndex[index] = currentWord; - index++; - if (index >= maxSize) return; - wordIndex[index] = currentWord; - index++; -} - -static void mode_morsecode(void) { - if (SEGLEN < 1) FX_FALLBACK_STATIC; - - // A-Z in Morse Code - static const char * letters[] = {".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---", "-.-", ".-..", "--", - "-.", "---", ".--.", "--.-", ".-.", "...", "-", "..-", "...-", ".--", "-..-", "-.--", "--.."}; - // 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; - - if (SEGMENT.name) len = strlen(SEGMENT.name); - if (len == 0) { - 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); - } - - // Allocate per-segment storage for pattern (1023 bits = 127 bytes) + word index array (1024 bytes) + word count (1 byte) - constexpr size_t MORSECODE_MAX_PATTERN_SIZE = 1023; - constexpr size_t MORSECODE_PATTERN_BYTES = (MORSECODE_MAX_PATTERN_SIZE + 7) / 8; // 128 bytes - constexpr size_t MORSECODE_WORD_INDEX_BYTES = MORSECODE_MAX_PATTERN_SIZE; // 1 byte per bit position - constexpr size_t MORSECODE_WORD_COUNT_BYTES = 1; // 1 byte for word count - if (!SEGENV.allocateData(MORSECODE_PATTERN_BYTES + MORSECODE_WORD_INDEX_BYTES + MORSECODE_WORD_COUNT_BYTES)) FX_FALLBACK_STATIC; - uint8_t* morsecodePattern = reinterpret_cast(SEGENV.data); - uint8_t* wordIndexArray = reinterpret_cast(SEGENV.data + MORSECODE_PATTERN_BYTES); - uint8_t* wordCountPtr = reinterpret_cast(SEGENV.data + MORSECODE_PATTERN_BYTES + MORSECODE_WORD_INDEX_BYTES); - - // 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 lastHashBits = (SEGENV.aux1 >> 10) & 0xF; // 4 bits of hash - uint16_t patternLength = SEGENV.aux1 & 0x3FF; // Lower 10 bits for length (up to 1023) - - // Compute text hash - uint16_t textHash = 0; - for (char *p = text; *p; p++) { - textHash = ((textHash << 5) + textHash) + *p; - } - 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 (needsRebuild) { - patternLength = 0; - - // Clear the bit array and word index array first - memset(morsecodePattern, 0, MORSECODE_PATTERN_BYTES); - memset(wordIndexArray, 0, MORSECODE_WORD_INDEX_BYTES); - - // Track current word index - uint8_t currentWordIndex = 0; - - // 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, wordIndexArray, patternLength, currentWordIndex, MORSECODE_MAX_PATTERN_SIZE); - } - else if (*c >= '0' && *c <= '9') { - build_morsecode_pattern(numbers[*c - '0'], morsecodePattern, wordIndexArray, patternLength, currentWordIndex, MORSECODE_MAX_PATTERN_SIZE); - } - else if (*c == ' ') { - // Space between words - increment word index for next word - currentWordIndex++; - // Add 4 additional spaces (7 total with the 3 after each letter) - for (int x = 0; x < 4; x++) { - if (patternLength >= MORSECODE_MAX_PATTERN_SIZE) break; - wordIndexArray[patternLength] = currentWordIndex; - patternLength++; - } - } - else if (SEGMENT.check2) { - const char *punctuationCode = nullptr; - for (const auto& p : punctuation) { - if (*c == p.character) { - punctuationCode = p.code; - break; - } - } - if (punctuationCode) { - build_morsecode_pattern(punctuationCode, morsecodePattern, wordIndexArray, patternLength, currentWordIndex, MORSECODE_MAX_PATTERN_SIZE); - } - } - } - - if (SEGMENT.check3) { - build_morsecode_pattern(".-.-.", morsecodePattern, wordIndexArray, patternLength, currentWordIndex, MORSECODE_MAX_PATTERN_SIZE); - } - - for (int x = 0; x < 7; x++) { - if (patternLength >= MORSECODE_MAX_PATTERN_SIZE) break; - wordIndexArray[patternLength] = currentWordIndex; - patternLength++; - } - - // Store the total number of words (currentWordIndex + 1 because it's 0-indexed) - *wordCountPtr = currentWordIndex + 1; - - // 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; - } - - // if pattern is empty for some reason, display black background only - if (patternLength == 0) { - SEGMENT.fill(BLACK); - return; - } - - // 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; - if (SEGENV.step != it) { - SEGENV.aux0++; - SEGENV.step = it; - } - - // Clear background - SEGMENT.fill(BLACK); - - // Draw the scrolling pattern - int offset = SEGENV.aux0 % patternLength; - - // Get the word count and calculate color spacing - uint8_t wordCount = *wordCountPtr; - if (wordCount == 0) wordCount = 1; - uint8_t colorSpacing = 255 / wordCount; // Distribute colors evenly across color wheel/palette - - for (int i = 0; i < SEGLEN; i++) { - int patternIndex = (offset + i) % patternLength; - if (GET_BIT8(morsecodePattern, patternIndex)) { - uint8_t wordIdx = wordIndexArray[patternIndex]; - if (SEGMENT.check1) { // make each word a separate color - if (SEGMENT.custom3 < 16) - // use word index to select base color, add slight offset for animation - SEGMENT.setPixelColor(i, SEGMENT.color_wheel((wordIdx * colorSpacing) + (SEGENV.aux0 / 4))); - else - SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(wordIdx * colorSpacing, true, PALETTE_SOLID_WRAP, 0)); - } - else { - if (SEGMENT.custom3 < 16) - SEGMENT.setPixelColor(i, SEGMENT.color_wheel(SEGENV.aux0 + i)); - else - SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); - } - } - } -} -static const char _data_FX_MODE_MORSECODE[] PROGMEM = "Morse Code@Speed,,,,Color mode,Color by Word,Punctuation,EndOfMessage;;!;1;sx=192,c3=8,o1=1,o2=1"; - - /* * Dissolve Plus * Modifications to original Dissolve effect by Bob Loeffler diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 205f784a7e..13ebcd709c 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -5081,6 +5081,150 @@ void mode_FlowStripe(void) { } // mode_FlowStripe() static const char _data_FX_MODE_FLOWSTRIPE[] PROGMEM = "Flow Stripe@Hue speed,Effect speed;;!;pal=11"; + +/* +* Morse Code by Bob Loeffler (#5252) - size optimized and extended by @dedehai +* Adapted from code by automaticaddison.com and optimized using claude.ai +*/ + +// 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) + +// Morse lookup as a binary tree: the index in the char array is the morse code (0 = dot, 1 = dash) with a leading "1" +// for example the second char is 'E' at index 2 (binary 10), discard the "1" -> 0 (dot) +// Finding the Morse code for a character can be done by finding the index using strchr() +// Tree structure: the char at index i has its "dot" child at 2i and its "dash" child at 2i+1 +static const char morseTree[] = + " ETIANMSURWDKGO" "HVF L PJBXCYZQ " "54 3 2& + 1" "6=/ ( 7 8 90" // note: 16-char block strings are concatenated at compile time + " ? " " . @ ' " " - ! ) " " , :"; + +#define MORSE_MAX_LEN 1023 +#define MORSE_WORD_GAP 7 // 7 empty pixels in a row mark the end of a word + +struct MorseData { + uint16_t hash; // detects text / option changes + uint16_t length; // pattern length in pixels + uint8_t words; // word count (for color spacing) + uint8_t bits[(MORSE_MAX_LEN + 7) / 8]; // 1 bit per pixel +}; + +// Append 'count' pixels (lit or empty) to the pattern +static void morse_put(MorseData &m, bool on, uint8_t count) { + while (count-- && m.length < MORSE_MAX_LEN) { + if (on) SET_BIT8(m.bits, m.length); + m.length++; + } +} + +// Append one character; idx = index of the character in morseTree +static void morse_addchar(MorseData &m, uint8_t idx) { + uint8_t bit = 0x40; + while (!(idx & bit)) bit >>= 1; // skip to the leading 1 (marker bit) + while (bit >>= 1) { // one pass per dot/dash + morse_put(m, true, (idx & bit) ? 3 : 1); // dot = 1 pixel, dash = 3 pixels + morse_put(m, false, 1); // 1 gap between elements + } + morse_put(m, false, 2); // +2 = 3 gaps between letters +} + +static void mode_morsecode(void) { + if (SEGLEN < 1) FX_FALLBACK_STATIC; + if (!SEGENV.allocateData(sizeof(MorseData))) FX_FALLBACK_STATIC; + MorseData *m = reinterpret_cast(SEGENV.data); + + // Get the text to display + char text[WLED_MAX_SEGNAME_LEN+1]; + if (SEGMENT.name && *SEGMENT.name) strlcpy(text, SEGMENT.name, sizeof(text)); + else strcpy_P(text, PSTR("I Love WLED!")); + + // 16-bit hash of text + option flags decides whether to rebuild the pattern + uint16_t hash = SEGMENT.check2 | (SEGMENT.check3 << 1); + for (const char *c = text; *c; c++) hash = hash * 33 + *c; + + // ---- Build the pattern (first call, or text/options changed) ---- + if (SEGENV.call == 0 || hash != m->hash) { + m->hash = hash; + m->length = 0; + memset(m->bits, 0, sizeof(m->bits)); + uint8_t words = 1; + + for (const char *c = text; *c && m->length <= MORSE_MAX_LEN - 24; c++) { + char ch = toupper(*c); + if (ch == ' ') { // 3 gaps after the letter + 4 = 7 between words + words++; + morse_put(*m, false, 4); + continue; + } + if (!isalnum(ch) && !SEGMENT.check2) continue; // punctuation is optional + const char *p = strchr(morseTree, ch); + uint8_t idx = p - morseTree; // index of the character in morseTree = char morse code in binary + if (p) morse_addchar(*m, idx); + } + + if (SEGMENT.check3) morse_addchar(*m, 42); // end of message ".-.-." + morse_put(*m, false, MORSE_WORD_GAP+4); // double word gap at end of message + m->words = words; + SEGENV.aux0 = 0; // restart + } + + // Timing: advance one step per tick + uint32_t it = strip.now / (50 + (255 - SEGMENT.speed) * 3); + if (SEGENV.step != it) { SEGENV.aux0++; SEGENV.step = it; } + + const uint16_t length = m->length; // length of the Morse code pattern in pixels + const uint16_t scroll = SEGENV.aux0 % length; // pattern index currently shown on pixel 0 + const uint8_t wordSpacing = 255 / m->words; // spread word colors evenly + const uint8_t hueStep = SEGMENT.custom3; // hue change per pixel, 0 = color by word + const bool useWheel = SEGMENT.palette > 0; // otherwise use the selected palette + + // Flash mode: whole strip blinks the message + if (SEGMENT.check1) { + for (int i = 0; i < SEGLEN; i++) { + uint8_t colorPos; + colorPos = i * hueStep; + bool lightUp = GET_BIT8(m->bits, SEGENV.aux0 % length); + SEGMENT.setPixelColor(i, lightUp ? SEGMENT.color_wheel(colorPos) : BLACK); + } + return; + } + // Scroll mode: paint the message onto the strip with scrolling + SEGMENT.fill(BLACK); + int word = 0; + int emptyRun = 0; + + for (uint16_t index = 0; index < length; index++) { + if (!GET_BIT8(m->bits, index)) { + emptyRun++; + if (emptyRun == MORSE_WORD_GAP) word++; // word gap detected, count word (for coloring below) + continue; // skip non-lit pixels + } + emptyRun = 0; + + // Scrolling means pattern index N is shown on pixel (N - scroll), wrapped around. + // If the strip is longer than the pattern, the pattern repeats every 'length' pixels. + int firstPixel = (index + length - scroll) % length; + + for (int pixel = firstPixel; pixel < SEGLEN; pixel += length) { + uint8_t colorPos; + + if (hueStep == 0) { // color by word: each word gets its own color + colorPos = word * wordSpacing; + if (useWheel) colorPos += SEGENV.aux0 / 4; // slowly cycle the hue + } else { // gradient along the strip, hueStep per pixel + colorPos = pixel * hueStep; + if (useWheel) colorPos += SEGENV.aux0; // cycle the hue as it scrolls + } + + SEGMENT.setPixelColor(pixel, SEGMENT.color_wheel(colorPos)); // uses HSV color if pal=0, use palette otherwise + } + } +} +static const char _data_FX_MODE_MORSECODE[] PROGMEM = "Morse Code@!,,,,Color,Flash,Punctuation,EndOfMessage;;!;1;sx=192,c3=0,o2=1"; + +#undef SET_BIT8 +#undef GET_BIT8 + /* Shimmer effect: moves a gradient with optional modulators across the strip at a given interval, up to 60 seconds It can be used as an overlay to other effects or standalone @@ -11186,6 +11330,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_WAVESINS, &mode_wavesins, _data_FX_MODE_WAVESINS); addEffect(FX_MODE_ROCKTAVES, &mode_rocktaves, _data_FX_MODE_ROCKTAVES); addEffect(FX_MODE_SHIMMER, &mode_shimmer, _data_FX_MODE_SHIMMER); + addEffect(FX_MODE_MORSECODE, &mode_morsecode, _data_FX_MODE_MORSECODE); // --- 2D effects --- #ifndef WLED_DISABLE_2D diff --git a/wled00/FX.h b/wled00/FX.h index 3568ba5523..c2ba7d4dba 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -313,7 +313,7 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_GRAVFREQ 158 #define FX_MODE_DJLIGHT 159 #define FX_MODE_2DFUNKYPLANK 160 -//#define FX_MODE_2DCENTERBARS 161 +#define FX_MODE_MORSECODE 161 // gap fill #define FX_MODE_SHIMMER 161 // gap fill, non SR 1D effect #define FX_MODE_2DPULSER 162 #define FX_MODE_BLURZ 163 From 97afd72fbeaedfdddd1cb9c94caec9eb48c339cc Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 10 Oct 2026 20:05:09 +0200 Subject: [PATCH 2/3] fix issues --- usermods/user_fx/user_fx.cpp | 1 - wled00/FX.h | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index d9acb2fda9..8fdb0d4783 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -1223,7 +1223,6 @@ class UserFxUsermod : public Usermod { strip.addEffect(255, &mode_2D_lavalamp, _data_FX_MODE_2D_LAVALAMP); strip.addEffect(255, &mode_2D_magma, _data_FX_MODE_2D_MAGMA); strip.addEffect(255, &mode_ants, _data_FX_MODE_ANTS); - strip.addEffect(255, &mode_morsecode, _data_FX_MODE_MORSECODE); strip.addEffect(255, &mode_dissolveplus, _data_FX_MODE_DISSOLVEPLUS); //////////////////////////////////////// diff --git a/wled00/FX.h b/wled00/FX.h index c2ba7d4dba..643d0c035f 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -313,7 +313,6 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_GRAVFREQ 158 #define FX_MODE_DJLIGHT 159 #define FX_MODE_2DFUNKYPLANK 160 -#define FX_MODE_MORSECODE 161 // gap fill #define FX_MODE_SHIMMER 161 // gap fill, non SR 1D effect #define FX_MODE_2DPULSER 162 #define FX_MODE_BLURZ 163 @@ -375,8 +374,9 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_COLORCLOUDS 218 #define FX_MODE_SLOW_TRANSITION 219 #define FX_MODE_2DANALOGCLOCK 220 // new in 16.1 +#define FX_MODE_MORSECODE 221 -#define MODE_COUNT 221 +#define MODE_COUNT 222 #define TRANSITION_FADE 0x00 // universal From 61b563ed95b07c46befb988aa262c68f92a4e67b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 10 Oct 2026 20:21:02 +0200 Subject: [PATCH 3/3] fix null check --- wled00/FX.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 13ebcd709c..3ae67cf955 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -5158,8 +5158,9 @@ static void mode_morsecode(void) { } if (!isalnum(ch) && !SEGMENT.check2) continue; // punctuation is optional const char *p = strchr(morseTree, ch); + if (!p) continue; // skip characters not in the morseTree uint8_t idx = p - morseTree; // index of the character in morseTree = char morse code in binary - if (p) morse_addchar(*m, idx); + morse_addchar(*m, idx); } if (SEGMENT.check3) morse_addchar(*m, 42); // end of message ".-.-."