Skip to content
Merged
Changes from 11 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f06bf51
Added Morse Code effect to the user_fx usermod
BobLoeffler68 Dec 22, 2025
c2a0a2e
Added a few comments
BobLoeffler68 Dec 22, 2025
580584c
Added punctuation and end-of-message codes, and changing them will fo…
BobLoeffler68 Dec 28, 2025
ab806ab
Fixed mode name in addEffect
BobLoeffler68 Dec 28, 2025
a2efb16
cosmetic changes
BobLoeffler68 Dec 31, 2025
c5cfbe7
* removed PROGMEM from letters and numbers arrays.
BobLoeffler68 Jan 1, 2026
c8a7dd9
More bounds checking added.
BobLoeffler68 Jan 1, 2026
bd7ef64
Added a lookup table for punctuation morse codes.
BobLoeffler68 Jan 1, 2026
85d8099
Removed PALETTE_MOVING_WRAP macro as it's not used in this effect.
BobLoeffler68 Jan 2, 2026
5e0d52a
Moved all static variables to SEGENV.data
BobLoeffler68 Jan 3, 2026
3c1c4be
Now using a bit array instead of a bool array
BobLoeffler68 Jan 4, 2026
d05db80
added a check to see if the pattern is empty
BobLoeffler68 Jan 4, 2026
9c8b2b2
Added "color by word" option, moved Color modes to a slider and added…
BobLoeffler68 Jan 31, 2026
61b1b75
Removed return FRAMETIME
BobLoeffler68 Feb 11, 2026
44df743
Merge branch 'main' of https://github.com/Aircoookie/WLED into pr-mor…
BobLoeffler68 Feb 11, 2026
b3b5297
A few changes suggested by coderabbit.
BobLoeffler68 Feb 11, 2026
a37ccd4
A few changes suggested by coderabbit
BobLoeffler68 Feb 12, 2026
7de0344
Merge branch 'main' of https://github.com/Aircoookie/WLED into pr-mor…
BobLoeffler68 Mar 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 213 additions & 0 deletions usermods/user_fx/user_fx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -89,6 +92,215 @@ 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
*/
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// 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<uint8_t*>(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)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// 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++;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// 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;
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));
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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 //
/////////////////////
Expand All @@ -98,6 +310,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 //
Expand Down