diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index da6937c87d..d94336d529 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,221 @@ 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 +* 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: +* - 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 +*/ + +// 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; + SET_BIT8(pattern, index); + index++; + } + else { // Must be a dash which is 3 pixels + if (index >= maxSize - 3) return; + SET_BIT8(pattern, index); + index++; + SET_BIT8(pattern, index); + index++; + SET_BIT8(pattern, index); + index++; + } + + 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/numbers/punctuation + if (index >= maxSize - 2) return; + index++; + if (index >= maxSize - 1) return; + index++; + if (index >= maxSize) return; + 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) + constexpr size_t MORSECODE_MAX_PATTERN_SIZE = 1024; + 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); + + // 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 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); + } + else if (*c >= '0' && *c <= '9') { + build_morsecode_pattern(numbers[*c - '0'], morsecodePattern, patternLength, MORSECODE_MAX_PATTERN_SIZE); + } + else if (*c == ' ') { + for (int x = 0; x < 4; x++) { + if (patternLength >= MORSECODE_MAX_PATTERN_SIZE) break; + 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, patternLength, MORSECODE_MAX_PATTERN_SIZE); + } + } + } + + if (SEGMENT.check3) { + build_morsecode_pattern(".-.-.", morsecodePattern, patternLength, MORSECODE_MAX_PATTERN_SIZE); + } + + for (int x = 0; x < 7; x++) { + if (patternLength >= MORSECODE_MAX_PATTERN_SIZE) break; + patternLength++; + } + + // 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; + + for (int i = 0; i < SEGLEN; i++) { + int patternIndex = (offset + i) % patternLength; + if (GET_BIT8(morsecodePattern, patternIndex)) { + if (SEGMENT.check1) + 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,Punctuation,EndOfMessage;;!;1;sx=128,o1=1,o2=1"; + + + ///////////////////// // UserMod Class // ///////////////////// @@ -98,6 +316,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 //