diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index da6937c87d..18e088845e 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -2,6 +2,9 @@ // 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) + // static effect, used if an effect fails to initialize static uint16_t mode_static(void) { SEGMENT.fill(SEGCOLOR(0)); @@ -89,6 +92,260 @@ 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 +* Adapted from code by automaticaddison.com and then optimized by claude.ai +* aux0 is the pattern offset for scrolling +* 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 +* 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 +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 uint16_t mode_morsecode(void) { + if (SEGLEN < 1) return mode_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 (1024 bits = 128 bytes) + word index array (1024 bytes) + word count (1 byte) + constexpr size_t MORSECODE_MAX_PATTERN_SIZE = 1024; + constexpr size_t MORSECODE_PATTERN_BYTES = MORSECODE_MAX_PATTERN_SIZE / 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)) return mode_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 FRAMETIME; + } + + // 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)); + } + } + } + return FRAMETIME; +} +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"; + + + ///////////////////// // UserMod Class // ///////////////////// @@ -98,6 +355,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_MORSECODE); //////////////////////////////////////// // add your effect function(s) here //