Skip to content
Open
Changes from all commits
Commits
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
219 changes: 219 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,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<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)

// 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 //
/////////////////////
Expand All @@ -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 //
Expand Down