diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index fb67e578e0..efb818f87e 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -694,37 +694,71 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(macroCountdown, cntdwn["macro"]); setCountdown(); + // Load timers into new vector-based system + clearTimers(); + + // Load regular timers from "ins" array 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]++; + // 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) << 1; // shift weekdays to upper 7 bits + if (timer["en"] | false) weekdays |= 0x01; // set enabled bit if timer is enabled + + // Handle date range for regular timers + JsonObject start = timer["start"]; + JsonObject end = timer["end"]; + uint8_t monthStart = start["mon"] | 1; + uint8_t dayStart = start["day"] | 1; + uint8_t monthEnd = end["mon"] | 12; + uint8_t dayEnd = end["day"] | 31; + + // Add regular timer to vector system + addTimer(preset, hour, minute, weekdays, monthStart, monthEnd, dayStart, dayEnd); + } + + // Load sunrise/sunset timers from separate "sunrise_sunset" object + JsonObject sunriseSunset = tm["sunrise_sunset"]; + if (!sunriseSunset.isNull()) { + // Load sunrise timer + JsonObject sunrise = sunriseSunset["sunrise"]; + if (!sunrise.isNull()) { + uint8_t preset = sunrise["macro"] | 0; + int8_t offset = sunrise["offset"] | 0; + uint8_t weekdays = (sunrise[F("dow")] | 0) << 1; + if (sunrise["en"] | false) weekdays |= 0x01; + + JsonObject start = sunrise["start"]; + JsonObject end = sunrise["end"]; + uint8_t monthStart = start["mon"] | 1; + uint8_t dayStart = start["day"] | 1; + uint8_t monthEnd = end["mon"] | 12; + uint8_t dayEnd = end["day"] | 31; + + addTimer(preset, TIMER_HOUR_SUNRISE, offset, weekdays, monthStart, monthEnd, dayStart, dayEnd); } - if (it<8) { - 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 + + // Load sunset timer + JsonObject sunset = sunriseSunset["sunset"]; + if (!sunset.isNull()) { + uint8_t preset = sunset["macro"] | 0; + int8_t offset = sunset["offset"] | 0; + uint8_t weekdays = (sunset[F("dow")] | 0) << 1; + if (sunset["en"] | false) weekdays |= 0x01; + + JsonObject start = sunset["start"]; + JsonObject end = sunset["end"]; + uint8_t monthStart = start["mon"] | 1; + uint8_t dayStart = start["day"] | 1; + uint8_t monthEnd = end["mon"] | 12; + uint8_t dayEnd = end["day"] | 31; + + addTimer(preset, TIMER_HOUR_SUNSET, offset, weekdays, monthStart, monthEnd, dayStart, dayEnd); } - it++; } JsonObject ota = doc["ota"]; @@ -1192,25 +1226,70 @@ void serializeConfig(JsonObject root) { goal.add(countdownHour); goal.add(countdownMin); goal.add(countdownSec); cntdwn["macro"] = macroCountdown; + // Separate regular timers and sunrise/sunset timers 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) - 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) { + JsonObject sunrise_sunset = timers.createNestedObject("sunrise_sunset"); + + Timer sunriseTimer, sunsetTimer; + bool hasSunrise = false, hasSunset = false; + + // Access the global timers vector from ntp.cpp + for (const auto& timer : ::timers) { + if (timer.isSunrise()) { + sunriseTimer = timer; + hasSunrise = true; + } else if (timer.isSunset()) { + sunsetTimer = timer; + hasSunset = true; + } else if (timer.isRegular()) { + // Skip completely empty regular timers + if (timer.preset == 0 && timer.minute == 0) continue; + + JsonObject timers_ins0 = timers_ins.createNestedObject(); + 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 + 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; } } + + // Save sunrise timer if it exists + if (hasSunrise) { + JsonObject sunrise = sunrise_sunset.createNestedObject("sunrise"); + sunrise["en"] = sunriseTimer.isEnabled(); + sunrise["offset"] = sunriseTimer.minute; // offset in minutes + sunrise["macro"] = sunriseTimer.preset; + sunrise[F("dow")] = sunriseTimer.weekdays >> 1; // remove enabled bit + JsonObject start = sunrise.createNestedObject("start"); + start["mon"] = sunriseTimer.monthStart; + start["day"] = sunriseTimer.dayStart; + JsonObject end = sunrise.createNestedObject("end"); + end["mon"] = sunriseTimer.monthEnd; + end["day"] = sunriseTimer.dayEnd; + } + + // Save sunset timer if it exists + if (hasSunset) { + JsonObject sunset = sunrise_sunset.createNestedObject("sunset"); + sunset["en"] = sunsetTimer.isEnabled(); + sunset["offset"] = sunsetTimer.minute; // offset in minutes + sunset["macro"] = sunsetTimer.preset; + sunset[F("dow")] = sunsetTimer.weekdays >> 1; // remove enabled bit + JsonObject start = sunset.createNestedObject("start"); + start["mon"] = sunsetTimer.monthStart; + start["day"] = sunsetTimer.dayStart; + JsonObject end = sunset.createNestedObject("end"); + end["mon"] = sunsetTimer.monthEnd; + end["day"] = sunsetTimer.dayEnd; + } JsonObject ota = root.createNestedObject("ota"); ota[F("lock")] = otaLock; diff --git a/wled00/data/settings_time.htm b/wled00/data/settings_time.htm index ae29065ead..df7c855160 100644 --- a/wled00/data/settings_time.htm +++ b/wled00/data/settings_time.htm @@ -8,13 +8,25 @@ -
+

@@ -200,9 +311,132 @@

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 d19f89b27d..a1dff3627b 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -318,6 +318,46 @@ void setCountdown(); byte weekdayMondayFirst(); void checkTimers(); void calculateSunriseAndSunset(); + +// Timer management functions +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); +void clearTimers(); +void syncTimersToArrays(); +uint8_t getTimerCount(); +uint8_t getRegularTimerCount(); +bool hasSunriseTimer(); +bool hasSunsetTimer(); + +// Timer constants +const uint8_t maxTimePresets = 32; + +// Timer special hour values +const uint8_t TIMER_HOUR_SUNRISE = 255; // Special value for sunrise timer +const uint8_t TIMER_HOUR_SUNSET = 254; // Special value for sunset timer + +// External timer declarations +struct Timer { + uint8_t preset; + uint8_t hour; + int8_t minute; + uint8_t weekdays; + uint8_t monthStart, monthEnd; + uint8_t dayStart, dayEnd; + + // Constructor + Timer(uint8_t p = 0, uint8_t h = 0, int8_t m = 0, uint8_t w = 0, + uint8_t ms = 1, uint8_t me = 12, uint8_t ds = 1, uint8_t de = 31) + : preset(p), hour(h), minute(m), weekdays(w), + monthStart(ms), monthEnd(me), dayStart(ds), dayEnd(de) {} + + 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; } +}; + +extern std::vector timers; + void setTimeFromAPI(uint32_t timein); //overlay.cpp diff --git a/wled00/ntp.cpp b/wled00/ntp.cpp index 12b698f445..113ea4aa4a 100644 --- a/wled00/ntp.cpp +++ b/wled00/ntp.cpp @@ -1,10 +1,56 @@ #include "src/dependencies/timezone/Timezone.h" #include "wled.h" #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 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 */ @@ -371,6 +417,182 @@ bool isTodayInDateRange(byte monthStart, byte dayStart, byte monthEnd, byte dayE return (m == monthStart && d >= dayStart && d <= dayEnd); //just the designated days this month } +/* + * Timer Management Functions + */ + +// Add a timer to the vector with validation +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")); + return; + } + + // Validate hour (0-23 for regular timers, or special values for sunrise/sunset) + if (hour > 23 && hour != TIMER_HOUR_SUNSET && hour != TIMER_HOUR_SUNRISE) { + DEBUG_PRINTLN(F("Error: Invalid hour value")); + return; + } + + // Validate minute based on timer type + if (hour < TIMER_HOUR_SUNSET) { // Regular timer + if (minute < 0 || minute > 59) { + DEBUG_PRINTLN(F("Error: Invalid minute value for regular timer")); + return; + } + } else { // Sunrise/sunset offset + if (minute < -59 || minute > 59) { + DEBUG_PRINTLN(F("Error: Invalid minute offset for sunrise/sunset")); + return; + } + } + + // Validate weekdays (7-bit bitmask: 0-127) + weekdays &= 0x7F; // Ensure only valid bits are set + + // Validate month range + if (monthStart < 1 || monthStart > 12 || monthEnd < 1 || monthEnd > 12) { + DEBUG_PRINTLN(F("Error: Invalid month range")); + return; + } + + // Validate day range + 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); + syncTimersToArraysInternal(); +} + +// Clear all timers +void clearTimers() { + TimerCriticalSection lock; + timers.clear(); + syncTimersToArraysInternal(); +} + +// Legacy array size constants +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); + memset(timerMinutes, 0, LEGACY_TIMER_ARRAY_SIZE); + memset(timerWeekday, 0, LEGACY_TIMER_ARRAY_SIZE); + memset(timerMonth, 0, LEGACY_DATE_ARRAY_SIZE); + memset(timerDay, 0, LEGACY_DATE_ARRAY_SIZE); + memset(timerDayEnd, 0, LEGACY_DATE_ARRAY_SIZE); + + uint8_t regularTimerCount = 0; + bool sunriseTimerSynced = false; + bool sunsetTimerSynced = false; + bool regularTimersDropped = false; + + for (const auto& timer : timers) { + if (timer.isSunrise()) { + if (!sunriseTimerSynced) { + // Sunrise timer goes to reserved index + timerMacro[LEGACY_SUNRISE_INDEX] = timer.preset; + timerHours[LEGACY_SUNRISE_INDEX] = timer.hour; + timerMinutes[LEGACY_SUNRISE_INDEX] = timer.minute; + timerWeekday[LEGACY_SUNRISE_INDEX] = timer.weekdays; + sunriseTimerSynced = true; + } else { + DEBUG_PRINTLN(F("Warning: Multiple sunrise timers found, only first one synced to legacy arrays")); + } + } else if (timer.isSunset()) { + if (!sunsetTimerSynced) { + // Sunset timer goes to reserved index + timerMacro[LEGACY_SUNSET_INDEX] = timer.preset; + timerHours[LEGACY_SUNSET_INDEX] = timer.hour; + timerMinutes[LEGACY_SUNSET_INDEX] = timer.minute; + timerWeekday[LEGACY_SUNSET_INDEX] = timer.weekdays; + sunsetTimerSynced = true; + } else { + DEBUG_PRINTLN(F("Warning: Multiple sunset timers found, only first one synced to legacy arrays")); + } + } else if (timer.isRegular()) { + if (regularTimerCount < LEGACY_REGULAR_TIMER_MAX) { + // Regular timers go to indices 0-7 + timerMacro[regularTimerCount] = timer.preset; + timerHours[regularTimerCount] = timer.hour; + timerMinutes[regularTimerCount] = timer.minute; + 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; + + regularTimerCount++; + } else { + if (!regularTimersDropped) { + DEBUG_PRINTLN(F("Warning: Too many regular timers for legacy arrays, some timers not synced")); + regularTimersDropped = true; // Only print warning once + } + } + } + } + + if (regularTimerCount == LEGACY_REGULAR_TIMER_MAX && timers.size() > LEGACY_TIMER_ARRAY_SIZE) { + DEBUG_PRINTF("Timer sync: %d regular timers synced, %d total timers in vector\n", + regularTimerCount, timers.size()); + } +} + +// 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++; + } + return count; +} + +bool hasSunriseTimer() { + TimerCriticalSection lock; + for (const auto& timer : timers) { + if (timer.isSunrise()) return true; + } + return false; +} + +bool hasSunsetTimer() { + TimerCriticalSection lock; + for (const auto& timer : timers) { + if (timer.isSunset()) return true; + } + return false; +} + void checkTimers() { if (lastTimerMinute != minute(localTime)) //only check once a new minute begins @@ -381,45 +603,49 @@ void checkTimers() if (!hour(localTime) && minute(localTime)==1) calculateSunriseAndSunset(); DEBUG_PRINTF_P(PSTR("Local time: %02d:%02d\n"), hour(localTime), minute(localTime)); - for (unsigned i = 0; i < 8; i++) + + // Check all timers in the vector with thread safety { - if (timerMacro[i] != 0 - && (timerWeekday[i] & 0x01) //timer is enabled - && (timerHours[i] == hour(localTime) || timerHours[i] == 24) //if hour is set to 24, activate every hour - && timerMinutes[i] == minute(localTime) - && ((timerWeekday[i] >> weekdayMondayFirst()) & 0x01) //timer should activate at current day of week - && isTodayInDateRange(((timerMonth[i] >> 4) & 0x0F), timerDay[i], timerMonth[i] & 0x0F, timerDayEnd[i]) - ) - { - applyPreset(timerMacro[i]); + TimerCriticalSection lock; + for (const auto& timer : timers) { + if (!timer.isEnabled() || timer.preset == 0) continue; + + bool shouldTrigger = false; + + if (timer.isRegular()) { + // Regular timer logic + shouldTrigger = (timer.hour == hour(localTime) || timer.hour == 24) // 24 = every hour + && timer.minute == minute(localTime) + && ((timer.weekdays >> weekdayMondayFirst()) & 0x01) // weekday check + && isTodayInDateRange(timer.monthStart, timer.dayStart, timer.monthEnd, timer.dayEnd); } - } - // sunrise macro - if (sunrise) { - time_t tmp = sunrise + timerMinutes[8]*60; // NOTE: may not be ok - DEBUG_PRINTF_P(PSTR("Trigger time: %02d:%02d\n"), hour(tmp), minute(tmp)); - if (timerMacro[8] != 0 - && hour(tmp) == hour(localTime) - && minute(tmp) == minute(localTime) - && (timerWeekday[8] & 0x01) //timer is enabled - && ((timerWeekday[8] >> weekdayMondayFirst()) & 0x01)) //timer should activate at current day of week - { - applyPreset(timerMacro[8]); - DEBUG_PRINTF_P(PSTR("Sunrise macro %d triggered."),timerMacro[8]); + else if (timer.isSunrise() && sunrise) { + // Sunrise timer logic + time_t triggerTime = sunrise + timer.minute * 60; // minute is offset for sunrise/sunset + shouldTrigger = hour(triggerTime) == hour(localTime) + && minute(triggerTime) == minute(localTime) + && ((timer.weekdays >> weekdayMondayFirst()) & 0x01); + + if (shouldTrigger) { + DEBUG_PRINTF_P(PSTR("Sunrise timer triggered at %02d:%02d\n"), hour(triggerTime), minute(triggerTime)); + } + } + else if (timer.isSunset() && sunset) { + // Sunset timer logic + time_t triggerTime = sunset + timer.minute * 60; // minute is offset for sunrise/sunset + shouldTrigger = hour(triggerTime) == hour(localTime) + && minute(triggerTime) == minute(localTime) + && ((timer.weekdays >> weekdayMondayFirst()) & 0x01); + + if (shouldTrigger) { + DEBUG_PRINTF_P(PSTR("Sunset timer triggered at %02d:%02d\n"), hour(triggerTime), minute(triggerTime)); + } + } + + if (shouldTrigger) { + DEBUG_PRINTF_P(PSTR("Timer triggered: preset %d\n"), timer.preset); + applyPreset(timer.preset); } - } - // sunset macro - if (sunset) { - time_t tmp = sunset + timerMinutes[9]*60; // NOTE: may not be ok - DEBUG_PRINTF_P(PSTR("Trigger time: %02d:%02d\n"), hour(tmp), minute(tmp)); - if (timerMacro[9] != 0 - && hour(tmp) == hour(localTime) - && minute(tmp) == minute(localTime) - && (timerWeekday[9] & 0x01) //timer is enabled - && ((timerWeekday[9] >> weekdayMondayFirst()) & 0x01)) //timer should activate at current day of week - { - applyPreset(timerMacro[9]); - DEBUG_PRINTF_P(PSTR("Sunset macro %d triggered."),timerMacro[9]); } } } diff --git a/wled00/set.cpp b/wled00/set.cpp index 9fa84d7aac..63d90c9ff1 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -539,29 +539,98 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) macroDoublePress[i] = request->arg(md).toInt(); } - char k[3]; k[2] = 0; - for (int i = 0; i<10; i++) { - k[1] = i+48;//ascii 0,1,2,3,... - k[0] = 'H'; //timer hours - timerHours[i] = request->arg(k).toInt(); - k[0] = 'N'; //minutes - timerMinutes[i] = request->arg(k).toInt(); - k[0] = 'T'; //macros - timerMacro[i] = request->arg(k).toInt(); - k[0] = 'W'; //weekdays - timerWeekday[i] = request->arg(k).toInt(); - if (i<8) { - k[0] = 'M'; //start month - timerMonth[i] = request->arg(k).toInt() & 0x0F; - timerMonth[i] <<= 4; - k[0] = 'P'; //end month - timerMonth[i] += (request->arg(k).toInt() & 0x0F); - k[0] = 'D'; //start day - timerDay[i] = request->arg(k).toInt(); - k[0] = 'E'; //end day - timerDayEnd[i] = request->arg(k).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 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 + 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 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 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 + 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 and 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 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 = constrain(request->arg(k).toInt(), 0, 23); // Regular timers: 0-23 hours only + + k[0] = 'N'; // minutes + 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 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); + } } //SECURITY 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