From 93197027397210d399f2eaee5b2454d739cb44fe Mon Sep 17 00:00:00 2001 From: PagedPenguin Date: Thu, 24 Jul 2025 20:45:07 -0600 Subject: [PATCH 1/4] Make time-controlled presets scalable and partially integrated into the UI. --- wled00/cfg.cpp | 79 ++++++------ wled00/data/settings_time.htm | 154 +++++++++++++--------- wled00/fcn_declare.h | 40 ++++++ wled00/ntp.cpp | 237 ++++++++++++++++++++++++++++------ wled00/set.cpp | 68 +++++++--- 5 files changed, 425 insertions(+), 153 deletions(-) diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index fb67e578e0..3b775f620b 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -694,37 +694,35 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(macroCountdown, cntdwn["macro"]); setCountdown(); + // Load timers into new vector-based system + clearTimers(); + JsonArray timers = tm["ins"]; - uint8_t it = 0; for (JsonObject timer : timers) { - if (it > 9) break; - if (it<8 && timer[F("hour")]==255) it=8; // hour==255 -> sunrise/sunset - CJSON(timerHours[it], timer[F("hour")]); - CJSON(timerMinutes[it], timer["min"]); - CJSON(timerMacro[it], timer["macro"]); - - byte dowPrev = timerWeekday[it]; - //note: act is currently only 0 or 1. - //the reason we are not using bool is that the on-disk type in 0.11.0 was already int - int actPrev = timerWeekday[it] & 0x01; - CJSON(timerWeekday[it], timer[F("dow")]); - if (timerWeekday[it] != dowPrev) { //present in JSON - timerWeekday[it] <<= 1; //add active bit - int act = timer["en"] | actPrev; - if (act) timerWeekday[it]++; - } - if (it<8) { + // Extract timer data from JSON + uint8_t hour = timer[F("hour")] | 0; + int8_t minute = timer["min"] | 0; + uint8_t preset = timer["macro"] | 0; + + // Handle weekdays and enabled state + uint8_t weekdays = timer[F("dow")] | 0; + weekdays <<= 1; // shift to make room for enabled bit + bool enabled = timer["en"] | false; + if (enabled) weekdays |= 0x01; // set enabled bit + + // Handle date range (only for regular timers) + uint8_t monthStart = 1, monthEnd = 12, dayStart = 1, dayEnd = 31; + if (hour < 254) { // regular timer JsonObject start = timer["start"]; - byte startm = start["mon"]; - if (startm) timerMonth[it] = (startm << 4); - CJSON(timerDay[it], start["day"]); JsonObject end = timer["end"]; - CJSON(timerDayEnd[it], end["day"]); - byte endm = end["mon"]; - if (startm) timerMonth[it] += endm & 0x0F; - if (!(timerMonth[it] & 0x0F)) timerMonth[it] += 12; //default end month to 12 + monthStart = start["mon"] | 1; + dayStart = start["day"] | 1; + monthEnd = end["mon"] | 12; + dayEnd = end["day"] | 31; } - it++; + + // Add timer to vector system + addTimer(preset, hour, minute, weekdays, monthStart, monthEnd, dayStart, dayEnd); } JsonObject ota = doc["ota"]; @@ -1194,21 +1192,26 @@ void serializeConfig(JsonObject root) { JsonArray timers_ins = timers.createNestedArray("ins"); - for (unsigned i = 0; i < 10; i++) { - if (timerMacro[i] == 0 && timerHours[i] == 0 && timerMinutes[i] == 0) continue; // sunrise/sunset get saved always (timerHours=255) + // Access the global timers vector from ntp.cpp + for (const auto& timer : ::timers) { + // Skip completely empty timers (but save sunrise/sunset even if preset=0) + if (timer.preset == 0 && timer.hour < 254 && timer.minute == 0) continue; + JsonObject timers_ins0 = timers_ins.createNestedObject(); - timers_ins0["en"] = (timerWeekday[i] & 0x01); - timers_ins0[F("hour")] = timerHours[i]; - timers_ins0["min"] = timerMinutes[i]; - timers_ins0["macro"] = timerMacro[i]; - timers_ins0[F("dow")] = timerWeekday[i] >> 1; - if (i<8) { + timers_ins0["en"] = timer.isEnabled(); + timers_ins0[F("hour")] = timer.hour; + timers_ins0["min"] = timer.minute; + timers_ins0["macro"] = timer.preset; + timers_ins0[F("dow")] = timer.weekdays >> 1; // remove enabled bit + + // Add date range for regular timers only + if (timer.isRegular()) { JsonObject start = timers_ins0.createNestedObject("start"); - start["mon"] = (timerMonth[i] >> 4) & 0xF; - start["day"] = timerDay[i]; + start["mon"] = timer.monthStart; + start["day"] = timer.dayStart; JsonObject end = timers_ins0.createNestedObject("end"); - end["mon"] = timerMonth[i] & 0xF; - end["day"] = timerDayEnd[i]; + end["mon"] = timer.monthEnd; + end["day"] = timer.dayEnd; } } diff --git a/wled00/data/settings_time.htm b/wled00/data/settings_time.htm index ae29065ead..0b2340bed9 100644 --- a/wled00/data/settings_time.htm +++ b/wled00/data/settings_time.htm @@ -8,13 +8,17 @@ -
+

@@ -234,12 +311,131 @@

Button actions

Analog Button setup +

Sunrise/Sunset Presets

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
En.TypeOffsetPreset
Sunrise
📅
+ +
Sunset
📅
+ +
+
+

Time-controlled presets

- - + +

diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index cefc320a0a..a1dff3627b 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -329,7 +329,7 @@ bool hasSunriseTimer(); bool hasSunsetTimer(); // Timer constants -const uint8_t maxTimePresets = 16; +const uint8_t maxTimePresets = 32; // Timer special hour values const uint8_t TIMER_HOUR_SUNRISE = 255; // Special value for sunrise timer @@ -350,7 +350,7 @@ struct Timer { : preset(p), hour(h), minute(m), weekdays(w), monthStart(ms), monthEnd(me), dayStart(ds), dayEnd(de) {} - bool isEnabled() const { return weekdays != 0; } // Timer is enabled if any weekday bit is set + bool isEnabled() const { return weekdays & 0x01; } // Timer is enabled if LSB (bit 0) is set bool isSunrise() const { return hour == TIMER_HOUR_SUNRISE; } bool isSunset() const { return hour == TIMER_HOUR_SUNSET; } bool isRegular() const { return hour < TIMER_HOUR_SUNSET; } diff --git a/wled00/ntp.cpp b/wled00/ntp.cpp index 165edd56b6..113ea4aa4a 100644 --- a/wled00/ntp.cpp +++ b/wled00/ntp.cpp @@ -3,12 +3,54 @@ #include "fcn_declare.h" #include +#ifdef ESP32 +#include "freertos/semphr.h" +#endif + // WARNING: may cause errors in sunset calculations on ESP8266, see #3400 // building with `-D WLED_USE_REAL_MATH` will prevent those errors at the expense of flash and RAM -// Dynamic timer storage +// Dynamic timer storage with thread safety std::vector timers; +#ifdef ESP32 +// FreeRTOS mutex for ESP32 dual-core thread safety +static SemaphoreHandle_t timerMutex = nullptr; + +// Initialize mutex (called once at startup) +static void initTimerMutex() { + if (timerMutex == nullptr) { + timerMutex = xSemaphoreCreateMutex(); + } +} + +// Critical section helper for ESP32 dual-core environments +class TimerCriticalSection { +public: + TimerCriticalSection() { + if (timerMutex == nullptr) initTimerMutex(); + if (timerMutex != nullptr) { + xSemaphoreTake(timerMutex, portMAX_DELAY); + } + } + ~TimerCriticalSection() { + if (timerMutex != nullptr) { + xSemaphoreGive(timerMutex); + } + } +}; +#else +// Simple critical section helper for single-core Arduino/ESP8266 +class TimerCriticalSection { +public: + TimerCriticalSection() { noInterrupts(); } + ~TimerCriticalSection() { interrupts(); } +}; +#endif + +// Forward declaration +static void syncTimersToArraysInternal(); + /* * Acquires time from NTP server */ @@ -383,6 +425,8 @@ bool isTodayInDateRange(byte monthStart, byte dayStart, byte monthEnd, byte dayE void addTimer(uint8_t preset, uint8_t hour, int8_t minute, uint8_t weekdays, uint8_t monthStart, uint8_t monthEnd, uint8_t dayStart, uint8_t dayEnd) { + TimerCriticalSection lock; + // Prevent unbounded memory growth by enforcing timer limit if (timers.size() >= maxTimePresets) { DEBUG_PRINTLN(F("Error: Maximum number of timers reached")); @@ -412,34 +456,39 @@ void addTimer(uint8_t preset, uint8_t hour, int8_t minute, uint8_t weekdays, weekdays &= 0x7F; // Ensure only valid bits are set // Validate month range - monthStart = constrain(monthStart, 1, 12); - monthEnd = constrain(monthEnd, 1, 12); + if (monthStart < 1 || monthStart > 12 || monthEnd < 1 || monthEnd > 12) { + DEBUG_PRINTLN(F("Error: Invalid month range")); + return; + } // Validate day range - dayStart = constrain(dayStart, 1, 31); - dayEnd = constrain(dayEnd, 1, 31); + if (dayStart < 1 || dayStart > 31 || dayEnd < 1 || dayEnd > 31) { + DEBUG_PRINTLN(F("Error: Invalid day range")); + return; + } // All validation passed, add the timer Timer newTimer(preset, hour, minute, weekdays, monthStart, monthEnd, dayStart, dayEnd); timers.push_back(newTimer); - syncTimersToArrays(); + syncTimersToArraysInternal(); } // Clear all timers void clearTimers() { + TimerCriticalSection lock; timers.clear(); - syncTimersToArrays(); + syncTimersToArraysInternal(); } // Legacy array size constants -const uint8_t LEGACY_TIMER_ARRAY_SIZE = 10; // Size of main timer arrays -const uint8_t LEGACY_DATE_ARRAY_SIZE = 8; // Size of date-related arrays -const uint8_t LEGACY_REGULAR_TIMER_MAX = 8; // Maximum number of regular timers (indices 0-7) -const uint8_t LEGACY_SUNRISE_INDEX = 8; // Reserved index for sunrise timer -const uint8_t LEGACY_SUNSET_INDEX = 9; // Reserved index for sunset timer - -// Sync vector timers to legacy arrays for backward compatibility -void syncTimersToArrays() { +const uint8_t LEGACY_TIMER_ARRAY_SIZE = 34; // Size of main timer arrays (32 regular + 2 sunrise/sunset) +const uint8_t LEGACY_DATE_ARRAY_SIZE = 32; // Size of date-related arrays +const uint8_t LEGACY_REGULAR_TIMER_MAX = 32; // Maximum number of regular timers (indices 0-31) +const uint8_t LEGACY_SUNRISE_INDEX = 32; // Reserved index for sunrise timer +const uint8_t LEGACY_SUNSET_INDEX = 33; // Reserved index for sunset timer + +// Internal function to sync timers without locking (assumes mutex is already held) +static void syncTimersToArraysInternal() { // Clear legacy arrays memset(timerMacro, 0, LEGACY_TIMER_ARRAY_SIZE); memset(timerHours, 0, LEGACY_TIMER_ARRAY_SIZE); @@ -486,6 +535,7 @@ void syncTimersToArrays() { timerWeekday[regularTimerCount] = timer.weekdays; // Date range info (only for regular timers) + // Encode monthStart in upper 4 bits, monthEnd in lower 4 bits timerMonth[regularTimerCount] = (timer.monthStart << 4) | (timer.monthEnd & 0x0F); timerDay[regularTimerCount] = timer.dayStart; timerDayEnd[regularTimerCount] = timer.dayEnd; @@ -506,12 +556,20 @@ void syncTimersToArrays() { } } +// Public function to sync timers with thread safety +void syncTimersToArrays() { + TimerCriticalSection lock; + syncTimersToArraysInternal(); +} + // Get timer count for different types uint8_t getTimerCount() { + TimerCriticalSection lock; return timers.size(); } uint8_t getRegularTimerCount() { + TimerCriticalSection lock; uint8_t count = 0; for (const auto& timer : timers) { if (timer.isRegular()) count++; @@ -520,6 +578,7 @@ uint8_t getRegularTimerCount() { } bool hasSunriseTimer() { + TimerCriticalSection lock; for (const auto& timer : timers) { if (timer.isSunrise()) return true; } @@ -527,6 +586,7 @@ bool hasSunriseTimer() { } bool hasSunsetTimer() { + TimerCriticalSection lock; for (const auto& timer : timers) { if (timer.isSunset()) return true; } @@ -544,8 +604,10 @@ void checkTimers() DEBUG_PRINTF_P(PSTR("Local time: %02d:%02d\n"), hour(localTime), minute(localTime)); - // Check all timers in the vector - for (const auto& timer : timers) { + // Check all timers in the vector with thread safety + { + TimerCriticalSection lock; + for (const auto& timer : timers) { if (!timer.isEnabled() || timer.preset == 0) continue; bool shouldTrigger = false; @@ -584,6 +646,7 @@ void checkTimers() DEBUG_PRINTF_P(PSTR("Timer triggered: preset %d\n"), timer.preset); applyPreset(timer.preset); } + } } } } diff --git a/wled00/set.cpp b/wled00/set.cpp index 29b7a07b5b..c179c883d1 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -542,50 +542,139 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) // Clear existing timers and rebuild from form data clearTimers(); + // Process sunrise timer (SR_* fields) + if (request->hasArg("SR_T")) { + // Extract and validate preset + int presetRaw = request->arg("SR_T").toInt(); + if (presetRaw < 0 || presetRaw > 250) { + DEBUG_PRINTF("Sunrise timer: Invalid preset %d, constraining to range 0-250\n", presetRaw); + } + uint8_t preset = constrain(presetRaw, 0, 250); + + // Extract and validate offset + int offsetRaw = request->arg("SR_N").toInt(); + if (offsetRaw < -59 || offsetRaw > 59) { + DEBUG_PRINTF("Sunrise timer: Invalid offset %d, constraining to range -59 to +59\n", offsetRaw); + } + int8_t offset = constrain(offsetRaw, -59, 59); + + // Extract weekdays (no validation needed as we mask to 7 bits) + uint8_t weekdays = request->arg("SR_W").toInt() & 0x7F; + + // Extract and validate date range + int monthStartRaw = request->arg("SR_M").toInt(); + if (monthStartRaw < 1 || monthStartRaw > 12) { + DEBUG_PRINTF("Sunrise timer: Invalid start month %d, constraining to range 1-12\n", monthStartRaw); + } + uint8_t monthStart = constrain(monthStartRaw, 1, 12); + + int dayStartRaw = request->arg("SR_D").toInt(); + if (dayStartRaw < 1 || dayStartRaw > 31) { + DEBUG_PRINTF("Sunrise timer: Invalid start day %d, constraining to range 1-31\n", dayStartRaw); + } + uint8_t dayStart = constrain(dayStartRaw, 1, 31); + + int monthEndRaw = request->arg("SR_P").toInt(); + if (monthEndRaw < 1 || monthEndRaw > 12) { + DEBUG_PRINTF("Sunrise timer: Invalid end month %d, constraining to range 1-12\n", monthEndRaw); + } + uint8_t monthEnd = constrain(monthEndRaw, 1, 12); + + int dayEndRaw = request->arg("SR_E").toInt(); + if (dayEndRaw < 1 || dayEndRaw > 31) { + DEBUG_PRINTF("Sunrise timer: Invalid end day %d, constraining to range 1-31\n", dayEndRaw); + } + uint8_t dayEnd = constrain(dayEndRaw, 1, 31); + + // Add sunrise timer if preset is valid or enabled + if (preset > 0 || (weekdays & 0x01)) { + addTimer(preset, TIMER_HOUR_SUNRISE, offset, weekdays, monthStart, monthEnd, dayStart, dayEnd); + } + } + + // Process sunset timer (SS_* fields) + if (request->hasArg("SS_T")) { + // Extract and validate preset + int presetRaw = request->arg("SS_T").toInt(); + if (presetRaw < 0 || presetRaw > 250) { + DEBUG_PRINTF("Sunset timer: Invalid preset %d, constraining to range 0-250\n", presetRaw); + } + uint8_t preset = constrain(presetRaw, 0, 250); + + // Extract and validate offset + int offsetRaw = request->arg("SS_N").toInt(); + if (offsetRaw < -59 || offsetRaw > 59) { + DEBUG_PRINTF("Sunset timer: Invalid offset %d, constraining to range -59 to +59\n", offsetRaw); + } + int8_t offset = constrain(offsetRaw, -59, 59); + + // Extract weekdays (no validation needed as we mask to 7 bits) + uint8_t weekdays = request->arg("SS_W").toInt() & 0x7F; + + // Extract and validate date range + int monthStartRaw = request->arg("SS_M").toInt(); + if (monthStartRaw < 1 || monthStartRaw > 12) { + DEBUG_PRINTF("Sunset timer: Invalid start month %d, constraining to range 1-12\n", monthStartRaw); + } + uint8_t monthStart = constrain(monthStartRaw, 1, 12); + + int dayStartRaw = request->arg("SS_D").toInt(); + if (dayStartRaw < 1 || dayStartRaw > 31) { + DEBUG_PRINTF("Sunset timer: Invalid start day %d, constraining to range 1-31\n", dayStartRaw); + } + uint8_t dayStart = constrain(dayStartRaw, 1, 31); + + int monthEndRaw = request->arg("SS_P").toInt(); + if (monthEndRaw < 1 || monthEndRaw > 12) { + DEBUG_PRINTF("Sunset timer: Invalid end month %d, constraining to range 1-12\n", monthEndRaw); + } + uint8_t monthEnd = constrain(monthEndRaw, 1, 12); + + int dayEndRaw = request->arg("SS_E").toInt(); + if (dayEndRaw < 1 || dayEndRaw > 31) { + DEBUG_PRINTF("Sunset timer: Invalid end day %d, constraining to range 1-31\n", dayEndRaw); + } + uint8_t dayEnd = constrain(dayEndRaw, 1, 31); + + // Add sunset timer if preset is valid or enabled + if (preset > 0 || (weekdays & 0x01)) { + addTimer(preset, TIMER_HOUR_SUNSET, offset, weekdays, monthStart, monthEnd, dayStart, dayEnd); + } + } + + // Process regular timers (0-9, A-F fields) char k[3]; k[2] = 0; for (int i = 0; i < maxTimePresets; i++) { k[1] = (i < 10) ? (i + 48) : (i + 55); // ascii 0-9, then A-F for indices 10-15 - // Check if this timer entry exists in the form + // Check if this timer entry exists and has valid content k[0] = 'T'; // preset/macro field if (!request->hasArg(k)) continue; + String presetValue = request->arg(k); + if (presetValue.isEmpty() || presetValue.toInt() == 0) continue; // Skip empty or zero preset values // Extract timer data from form with validation k[0] = 'T'; // preset/macro field uint8_t preset = constrain(request->arg(k).toInt(), 0, 250); // Limit preset to valid range k[0] = 'H'; // hours - uint8_t hour = request->arg(k).toInt(); - // Validate hour: must be 0-23 for regular timers or special values (254, 255) for sunrise/sunset - if (hour > 23 && hour != TIMER_HOUR_SUNSET && hour != TIMER_HOUR_SUNRISE) { - hour = 0; // Reset to safe value if invalid - } + uint8_t hour = constrain(request->arg(k).toInt(), 0, 23); // Regular timers: 0-23 hours only k[0] = 'N'; // minutes - int8_t minute = request->arg(k).toInt(); - // For regular timers, minute must be 0-59 - // For sunrise/sunset, minute is an offset that should be within reasonable bounds (-59 to 59) - if (hour < TIMER_HOUR_SUNSET) { // Regular timer - minute = constrain(minute, 0, 59); - } else { // Sunrise/sunset offset - minute = constrain(minute, -59, 59); - } + int8_t minute = constrain(request->arg(k).toInt(), 0, 59); // Regular timers: 0-59 minutes k[0] = 'W'; // weekdays uint8_t weekdays = request->arg(k).toInt() & 0x7F; // Ensure only 7 bits used (0-127) - // Date range (only for regular timers, not sunrise/sunset) - uint8_t monthStart = 1, monthEnd = 12, dayStart = 1, dayEnd = 31; - if (hour < 254) { // regular timer - k[0] = 'M'; // start month - monthStart = request->arg(k).toInt(); - k[0] = 'P'; // end month - monthEnd = request->arg(k).toInt(); - k[0] = 'D'; // start day - dayStart = request->arg(k).toInt(); - k[0] = 'E'; // end day - dayEnd = request->arg(k).toInt(); - } + // Date range for regular timers + k[0] = 'M'; // start month + uint8_t monthStart = constrain(request->arg(k).toInt(), 1, 12); + k[0] = 'P'; // end month + uint8_t monthEnd = constrain(request->arg(k).toInt(), 1, 12); + k[0] = 'D'; // start day + uint8_t dayStart = constrain(request->arg(k).toInt(), 1, 31); + k[0] = 'E'; // end day + uint8_t dayEnd = constrain(request->arg(k).toInt(), 1, 31); // Add timer to the new system addTimer(preset, hour, minute, weekdays, monthStart, monthEnd, dayStart, dayEnd); diff --git a/wled00/wled.h b/wled00/wled.h index 52bb2f9366..361d4da7cf 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -807,17 +807,17 @@ WLED_GLOBAL bool hueStoreAllowed _INIT(false), hueNewKey _INIT(false); WLED_GLOBAL unsigned long countdownTime _INIT(1514764800L); WLED_GLOBAL bool countdownOverTriggered _INIT(true); -//timer +//timer - expanded to support 32 regular timers + 2 sunrise/sunset (34 total) WLED_GLOBAL byte lastTimerMinute _INIT(0); -WLED_GLOBAL byte timerHours[] _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 })); -WLED_GLOBAL int8_t timerMinutes[] _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 })); -WLED_GLOBAL byte timerMacro[] _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 })); +WLED_GLOBAL byte timerHours[34] _INIT_N(({ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 })); +WLED_GLOBAL int8_t timerMinutes[34] _INIT_N(({ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 })); +WLED_GLOBAL byte timerMacro[34] _INIT_N(({ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 })); //weekdays to activate on, bit pattern of arr elem: 0b11111111: sun,sat,fri,thu,wed,tue,mon,validity -WLED_GLOBAL byte timerWeekday[] _INIT_N(({ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 })); -//upper 4 bits start, lower 4 bits end month (default 28: start month 1 and end month 12) -WLED_GLOBAL byte timerMonth[] _INIT_N(({28,28,28,28,28,28,28,28})); -WLED_GLOBAL byte timerDay[] _INIT_N(({1,1,1,1,1,1,1,1})); -WLED_GLOBAL byte timerDayEnd[] _INIT_N(({31,31,31,31,31,31,31,31})); +WLED_GLOBAL byte timerWeekday[34] _INIT_N(({ 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255 })); +//upper 4 bits start, lower 4 bits end month (default 28: start month 1 and end month 12) - expanded to 32 elements +WLED_GLOBAL byte timerMonth[32] _INIT_N(({28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28})); +WLED_GLOBAL byte timerDay[32] _INIT_N(({1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1})); +WLED_GLOBAL byte timerDayEnd[32] _INIT_N(({31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31})); WLED_GLOBAL bool doAdvancePlaylist _INIT(false); //improv From 32a2d672aef7ecd5fdf54ffc7d1c01f8ea859037 Mon Sep 17 00:00:00 2001 From: PagedPenguin Date: Sat, 26 Jul 2025 13:44:25 -0600 Subject: [PATCH 3/4] Fix Sunday bit loss in sunrise/sunset timer parsing --- wled00/set.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wled00/set.cpp b/wled00/set.cpp index c179c883d1..f91f047cee 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -558,8 +558,8 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) } int8_t offset = constrain(offsetRaw, -59, 59); - // Extract weekdays (no validation needed as we mask to 7 bits) - uint8_t weekdays = request->arg("SR_W").toInt() & 0x7F; + // Extract weekdays (preserve all 8 bits: enabled bit + 7 weekdays) + uint8_t weekdays = request->arg("SR_W").toInt() & 0xFF; // Extract and validate date range int monthStartRaw = request->arg("SR_M").toInt(); @@ -608,8 +608,8 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) } int8_t offset = constrain(offsetRaw, -59, 59); - // Extract weekdays (no validation needed as we mask to 7 bits) - uint8_t weekdays = request->arg("SS_W").toInt() & 0x7F; + // Extract weekdays (preserve all 8 bits: enabled bit + 7 weekdays) + uint8_t weekdays = request->arg("SS_W").toInt() & 0xFF; // Extract and validate date range int monthStartRaw = request->arg("SS_M").toInt(); From 81848f736f2bf32a44da299eb490b13e1f25ad3f Mon Sep 17 00:00:00 2001 From: PagedPenguin Date: Sat, 26 Jul 2025 14:07:26 -0600 Subject: [PATCH 4/4] =?UTF-8?q?refactoring=20sunrise=20timer=20processing?= =?UTF-8?q?=20and=20Fix=20timer-add=20condition=20to=20honor=20the=20UI=20?= =?UTF-8?q?=E2=80=9Cenabled=E2=80=9D=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wled00/set.cpp | 104 +++++++++++++------------------------------------ 1 file changed, 28 insertions(+), 76 deletions(-) diff --git a/wled00/set.cpp b/wled00/set.cpp index f91f047cee..63d90c9ff1 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -539,105 +539,57 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) macroDoublePress[i] = request->arg(md).toInt(); } + // Helper function for parameter validation with debug logging + auto validateTimerParam = [](const String& value, int minVal, int maxVal, const char* paramName, const char* timerType) -> int { + int rawValue = value.toInt(); + if (rawValue < minVal || rawValue > maxVal) { + DEBUG_PRINTF("%s timer: Invalid %s %d, constraining to range %d-%d\n", + timerType, paramName, rawValue, minVal, maxVal); + } + return constrain(rawValue, minVal, maxVal); + }; + // Clear existing timers and rebuild from form data clearTimers(); // Process sunrise timer (SR_* fields) if (request->hasArg("SR_T")) { - // Extract and validate preset - int presetRaw = request->arg("SR_T").toInt(); - if (presetRaw < 0 || presetRaw > 250) { - DEBUG_PRINTF("Sunrise timer: Invalid preset %d, constraining to range 0-250\n", presetRaw); - } - uint8_t preset = constrain(presetRaw, 0, 250); - - // Extract and validate offset - int offsetRaw = request->arg("SR_N").toInt(); - if (offsetRaw < -59 || offsetRaw > 59) { - DEBUG_PRINTF("Sunrise timer: Invalid offset %d, constraining to range -59 to +59\n", offsetRaw); - } - int8_t offset = constrain(offsetRaw, -59, 59); + // Extract and validate parameters + uint8_t preset = validateTimerParam(request->arg("SR_T"), 0, 250, "preset", "Sunrise"); + int8_t offset = validateTimerParam(request->arg("SR_N"), -59, 59, "offset", "Sunrise"); // Extract weekdays (preserve all 8 bits: enabled bit + 7 weekdays) uint8_t weekdays = request->arg("SR_W").toInt() & 0xFF; // Extract and validate date range - int monthStartRaw = request->arg("SR_M").toInt(); - if (monthStartRaw < 1 || monthStartRaw > 12) { - DEBUG_PRINTF("Sunrise timer: Invalid start month %d, constraining to range 1-12\n", monthStartRaw); - } - uint8_t monthStart = constrain(monthStartRaw, 1, 12); - - int dayStartRaw = request->arg("SR_D").toInt(); - if (dayStartRaw < 1 || dayStartRaw > 31) { - DEBUG_PRINTF("Sunrise timer: Invalid start day %d, constraining to range 1-31\n", dayStartRaw); - } - uint8_t dayStart = constrain(dayStartRaw, 1, 31); - - int monthEndRaw = request->arg("SR_P").toInt(); - if (monthEndRaw < 1 || monthEndRaw > 12) { - DEBUG_PRINTF("Sunrise timer: Invalid end month %d, constraining to range 1-12\n", monthEndRaw); - } - uint8_t monthEnd = constrain(monthEndRaw, 1, 12); - - int dayEndRaw = request->arg("SR_E").toInt(); - if (dayEndRaw < 1 || dayEndRaw > 31) { - DEBUG_PRINTF("Sunrise timer: Invalid end day %d, constraining to range 1-31\n", dayEndRaw); - } - uint8_t dayEnd = constrain(dayEndRaw, 1, 31); + uint8_t monthStart = validateTimerParam(request->arg("SR_M"), 1, 12, "start month", "Sunrise"); + uint8_t dayStart = validateTimerParam(request->arg("SR_D"), 1, 31, "start day", "Sunrise"); + uint8_t monthEnd = validateTimerParam(request->arg("SR_P"), 1, 12, "end month", "Sunrise"); + uint8_t dayEnd = validateTimerParam(request->arg("SR_E"), 1, 31, "end day", "Sunrise"); - // Add sunrise timer if preset is valid or enabled - if (preset > 0 || (weekdays & 0x01)) { + // Add sunrise timer if preset is valid and enabled + if (preset > 0 && (weekdays & 0x01)) { addTimer(preset, TIMER_HOUR_SUNRISE, offset, weekdays, monthStart, monthEnd, dayStart, dayEnd); } } // Process sunset timer (SS_* fields) if (request->hasArg("SS_T")) { - // Extract and validate preset - int presetRaw = request->arg("SS_T").toInt(); - if (presetRaw < 0 || presetRaw > 250) { - DEBUG_PRINTF("Sunset timer: Invalid preset %d, constraining to range 0-250\n", presetRaw); - } - uint8_t preset = constrain(presetRaw, 0, 250); - - // Extract and validate offset - int offsetRaw = request->arg("SS_N").toInt(); - if (offsetRaw < -59 || offsetRaw > 59) { - DEBUG_PRINTF("Sunset timer: Invalid offset %d, constraining to range -59 to +59\n", offsetRaw); - } - int8_t offset = constrain(offsetRaw, -59, 59); + // Extract and validate parameters + uint8_t preset = validateTimerParam(request->arg("SS_T"), 0, 250, "preset", "Sunset"); + int8_t offset = validateTimerParam(request->arg("SS_N"), -59, 59, "offset", "Sunset"); // Extract weekdays (preserve all 8 bits: enabled bit + 7 weekdays) uint8_t weekdays = request->arg("SS_W").toInt() & 0xFF; // Extract and validate date range - int monthStartRaw = request->arg("SS_M").toInt(); - if (monthStartRaw < 1 || monthStartRaw > 12) { - DEBUG_PRINTF("Sunset timer: Invalid start month %d, constraining to range 1-12\n", monthStartRaw); - } - uint8_t monthStart = constrain(monthStartRaw, 1, 12); - - int dayStartRaw = request->arg("SS_D").toInt(); - if (dayStartRaw < 1 || dayStartRaw > 31) { - DEBUG_PRINTF("Sunset timer: Invalid start day %d, constraining to range 1-31\n", dayStartRaw); - } - uint8_t dayStart = constrain(dayStartRaw, 1, 31); - - int monthEndRaw = request->arg("SS_P").toInt(); - if (monthEndRaw < 1 || monthEndRaw > 12) { - DEBUG_PRINTF("Sunset timer: Invalid end month %d, constraining to range 1-12\n", monthEndRaw); - } - uint8_t monthEnd = constrain(monthEndRaw, 1, 12); - - int dayEndRaw = request->arg("SS_E").toInt(); - if (dayEndRaw < 1 || dayEndRaw > 31) { - DEBUG_PRINTF("Sunset timer: Invalid end day %d, constraining to range 1-31\n", dayEndRaw); - } - uint8_t dayEnd = constrain(dayEndRaw, 1, 31); + uint8_t monthStart = validateTimerParam(request->arg("SS_M"), 1, 12, "start month", "Sunset"); + uint8_t dayStart = validateTimerParam(request->arg("SS_D"), 1, 31, "start day", "Sunset"); + uint8_t monthEnd = validateTimerParam(request->arg("SS_P"), 1, 12, "end month", "Sunset"); + uint8_t dayEnd = validateTimerParam(request->arg("SS_E"), 1, 31, "end day", "Sunset"); - // Add sunset timer if preset is valid or enabled - if (preset > 0 || (weekdays & 0x01)) { + // Add sunset timer if preset is valid and enabled + if (preset > 0 && (weekdays & 0x01)) { addTimer(preset, TIMER_HOUR_SUNSET, offset, weekdays, monthStart, monthEnd, dayStart, dayEnd); } }