From 9d1d4fed65e5bc91dcfd7e7d12f891356934b9f7 Mon Sep 17 00:00:00 2001 From: pagedpenguin251 Date: Mon, 14 Jul 2025 11:15:10 -0600 Subject: [PATCH 1/8] add scheduling --- wled00/data/settings_time.htm | 5 ++ wled00/schedule.cpp | 147 ++++++++++++++++++++++++++++++++++ wled00/schedule.h | 27 +++++++ wled00/wled.cpp | 4 + wled00/wled_server.cpp | 109 ++++++++++++++++++++----- 5 files changed, 273 insertions(+), 19 deletions(-) create mode 100644 wled00/schedule.cpp create mode 100644 wled00/schedule.h diff --git a/wled00/data/settings_time.htm b/wled00/data/settings_time.htm index ae29065ead..8622b79c79 100644 --- a/wled00/data/settings_time.htm +++ b/wled00/data/settings_time.htm @@ -178,6 +178,11 @@

Clock

Countdown Goal:
Date: 20--
Time: ::
+

Upload Schedule JSON

+ + +
+ Backup schedule

Macro presets

Macros have moved!
Presets now also can be used as macros to save both JSON and HTTP API commands.
diff --git a/wled00/schedule.cpp b/wled00/schedule.cpp new file mode 100644 index 0000000000..41b1fe47d1 --- /dev/null +++ b/wled00/schedule.cpp @@ -0,0 +1,147 @@ +// schedule.cpp +// Handles reading, parsing, and checking the preset schedule from schedule.json + +#include "schedule.h" +#include +#include + +#define SCHEDULE_FILE "/schedule.json" + +// Array to hold scheduled events, max size defined in schedule.h +ScheduleEvent scheduleEvents[MAX_SCHEDULE_EVENTS]; +uint8_t numScheduleEvents = 0; // Current count of loaded schedule entries + +// Helper function to check if current date (cm, cd) is within the event's start and end date range +bool isTodayInRange(uint8_t sm, uint8_t sd, uint8_t em, uint8_t ed, uint8_t cm, uint8_t cd) +{ + // Handles ranges that wrap over the year end, e.g., Dec to Jan + if (sm < em || (sm == em && sd <= ed)) + { + // Normal range within a year + return (cm > sm || (cm == sm && cd >= sd)) && + (cm < em || (cm == em && cd <= ed)); + } + else + { + // Range wraps year-end (e.g., Nov 20 to Feb 10) + return (cm > sm || (cm == sm && cd >= sd)) || + (cm < em || (cm == em && cd <= ed)); + } +} + +// Checks current time against schedule entries and applies matching presets +void checkSchedule() { + static int lastMinute = -1; // To avoid multiple triggers within the same minute + + time_t now = localTime; + if (now < 100000) return; // Invalid or uninitialized time guard + + struct tm* timeinfo = localtime(&now); + + int thisMinute = timeinfo->tm_min + timeinfo->tm_hour * 60; + if (thisMinute == lastMinute) return; // Already checked this minute + lastMinute = thisMinute; + + // Extract date/time components for easier use + uint8_t cm = timeinfo->tm_mon + 1; // Month [1-12] + uint8_t cd = timeinfo->tm_mday; // Day of month [1-31] + uint8_t wday = timeinfo->tm_wday; // Weekday [0-6], Sunday=0 + uint8_t hr = timeinfo->tm_hour; // Hour [0-23] + uint8_t min = timeinfo->tm_min; // Minute [0-59] + + DEBUG_PRINTF_P(PSTR("[Schedule] Checking schedule at %02u:%02u\n"), hr, min); + + // Iterate through all scheduled events + for (uint8_t i = 0; i < numScheduleEvents; i++) + { + const ScheduleEvent &e = scheduleEvents[i]; + + // Skip if hour or minute doesn't match current time + if (e.hour != hr || e.minute != min) + continue; + + bool match = false; + + // Check if repeat mask matches current weekday (bitmask with Sunday=LSB) + if (e.repeatMask && ((e.repeatMask >> wday) & 0x01)) + match = true; + + // Otherwise check if current date is within start and end date range + if (e.startMonth) + { + if (isTodayInRange(e.startMonth, e.startDay, e.endMonth, e.endDay, cm, cd)) + match = true; + } + + // If match, apply preset and print debug + if (match) + { + applyPreset(e.presetId); + DEBUG_PRINTF_P(PSTR("[Schedule] Applying preset %u at %02u:%02u\n"), e.presetId, hr, min); + } + } +} + +// Loads schedule events from the schedule JSON file +// Returns true if successful, false on error or missing file +bool loadSchedule() { + if (!WLED_FS.exists(SCHEDULE_FILE)) return false; + + // Acquire JSON buffer lock to prevent concurrent access + if (!requestJSONBufferLock(7)) return false; + + File file = WLED_FS.open(SCHEDULE_FILE, "r"); + if (!file) { + releaseJSONBufferLock(); + return false; + } + + DynamicJsonDocument doc(4096); + DeserializationError error = deserializeJson(doc, file); + file.close(); // Always close file before releasing lock + + if (error) { + DEBUG_PRINTF_P(PSTR("[Schedule] JSON parse failed: %s\n"), error.c_str()); + releaseJSONBufferLock(); + return false; + } + + numScheduleEvents = 0; + for (JsonObject e : doc.as()) { + if (numScheduleEvents >= MAX_SCHEDULE_EVENTS) break; + + // Read and validate fields with type safety + int sm = e["sm"].as(); + int sd = e["sd"].as(); + int em = e["em"].as(); + int ed = e["ed"].as(); + int r = e["r"].as(); + int h = e["h"].as(); + int m = e["m"].as(); + int p = e["p"].as(); + + // Validate ranges to prevent bad data + if (sm < 1 || sm > 12 || em < 1 || em > 12 || + sd < 1 || sd > 31 || ed < 1 || ed > 31 || + h < 0 || h > 23 || m < 0 || m > 59 || + r < 0 || r > 127|| p < 1 || p > 250) { + DEBUG_PRINTF_P(PSTR("[Schedule] Invalid values in event %u, skipping\n"), numScheduleEvents); + continue; + } + + // Store event in the array + scheduleEvents[numScheduleEvents++] = { + (uint8_t)sm, (uint8_t)sd, + (uint8_t)em, (uint8_t)ed, + (uint8_t)r, (uint8_t)h, + (uint8_t)m, (uint8_t)p + }; + } + + DEBUG_PRINTF_P(PSTR("[Schedule] Loaded %u schedule entries from schedule.json\n"), numScheduleEvents); + + // Release JSON buffer lock after finishing + releaseJSONBufferLock(); + + return true; +} diff --git a/wled00/schedule.h b/wled00/schedule.h new file mode 100644 index 0000000000..d7d66c876a --- /dev/null +++ b/wled00/schedule.h @@ -0,0 +1,27 @@ +// schedule.h +// Defines the schedule event data structure and declares schedule-related functions + +#pragma once + +#include + +// Maximum number of schedule events supported +#define MAX_SCHEDULE_EVENTS 32 + +// Structure representing one scheduled event +struct ScheduleEvent { + uint8_t startMonth; // Starting month of the schedule event (1-12) + uint8_t startDay; // Starting day of the schedule event (1-31) + uint8_t endMonth; // Ending month of the schedule event (1-12) + uint8_t endDay; // Ending day of the schedule event (1-31) + uint8_t repeatMask; // Bitmask indicating repeat days of the week (bit0=Sunday ... bit6=Saturday) + uint8_t hour; // Hour of day when event triggers (0-23) + uint8_t minute; // Minute of hour when event triggers (0-59) + uint8_t presetId; // Preset number to apply when event triggers (1-250) +}; + +// Loads the schedule from the JSON file; returns true if successful +bool loadSchedule(); + +// Checks current time against schedule and applies any matching preset +void checkSchedule(); \ No newline at end of file diff --git a/wled00/wled.cpp b/wled00/wled.cpp index c372d22abd..a6067a552f 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -2,6 +2,7 @@ #include "wled.h" #include "wled_ethernet.h" #include +#include "schedule.h" #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DISABLE_BROWNOUT_DET) #include "soc/soc.h" @@ -54,6 +55,7 @@ void WLED::loop() #endif handleTime(); + checkSchedule(); #ifndef WLED_DISABLE_INFRARED handleIR(); // 2nd call to function needed for ESP32 to return valid results -- should be good for ESP8266, too #endif @@ -414,6 +416,8 @@ void WLED::setup() #endif updateFSInfo(); + loadSchedule(); // Load schedule from file on startup + // generate module IDs must be done before AP setup escapedMac = WiFi.macAddress(); escapedMac.replace(":", ""); diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 4434a2f3e5..a84ed461dc 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -15,6 +15,7 @@ #include "html_pxmagic.h" #endif #include "html_cpal.h" +#include "schedule.h" // define flash strings once (saves flash memory) static const char s_redirecting[] PROGMEM = "Redirecting..."; @@ -174,33 +175,103 @@ static String msgProcessor(const String& var) return String(); } -static void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool isFinal) { - if (!correctPIN) { - if (isFinal) request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_unlock_cfg)); +static void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool isFinal) +{ + if (!correctPIN) + { + if (isFinal) + request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_unlock_cfg)); return; } - if (!index) { - String finalname = filename; - if (finalname.charAt(0) != '/') { + + String finalname = filename; + if (!index) + { + if (finalname.charAt(0) != '/') + { finalname = '/' + finalname; // prepend slash if missing } - request->_tempFile = WLED_FS.open(finalname, "w"); - DEBUG_PRINTF_P(PSTR("Uploading %s\n"), finalname.c_str()); - if (finalname.equals(FPSTR(getPresetsFileName()))) presetsModifiedTime = toki.second(); + // Special case: schedule.json upload uses temp file + if (finalname.equals(F("/schedule.json"))) + { + request->_tempFile = WLED_FS.open("/schedule.json.tmp", "w"); + DEBUG_PRINTLN(F("Uploading to /schedule.json.tmp")); + } + else + { + request->_tempFile = WLED_FS.open(finalname, "w"); + DEBUG_PRINTF_P(PSTR("Uploading %s\n"), finalname.c_str()); + + if (finalname.equals(FPSTR(getPresetsFileName()))) + { + presetsModifiedTime = toki.second(); + } + } } - if (len) { - request->_tempFile.write(data,len); + + // Write chunk + if (len && request->_tempFile) + { + size_t written = request->_tempFile.write(data, len); + if (written != len) + { + DEBUG_PRINTLN(F("File write error during upload")); + request->_tempFile.close(); + request->_tempFile = File(); // invalidate file handle + // Consider sending error response early + } } - if (isFinal) { - request->_tempFile.close(); - if (filename.indexOf(F("cfg.json")) >= 0) { // check for filename with or without slash - doReboot = true; - request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Configuration restore successful.\nRebooting...")); - } else { - if (filename.indexOf(F("palette")) >= 0 && filename.indexOf(F(".json")) >= 0) loadCustomPalettes(); - request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File Uploaded!")); + + // Finalize upload + if (isFinal) + { + if (request->_tempFile) + request->_tempFile.close(); + + if (finalname.equals(F("/schedule.json"))) + { + // Atomically replace old file + // First try rename (which overwrites on most filesystems) + if (!WLED_FS.rename("/schedule.json.tmp", "/schedule.json")) + { + // If rename failed, try remove then rename + WLED_FS.remove("/schedule.json"); + if (!WLED_FS.rename("/schedule.json.tmp", "/schedule.json")) + { + DEBUG_PRINTLN(F("[Schedule] Failed to replace schedule file")); + request->send(500, FPSTR(CONTENT_TYPE_PLAIN), F("Failed to save schedule file.")); + return; + } + } + request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Schedule uploaded and applied.")); + DEBUG_PRINTLN(F("[Schedule] Upload complete and applied.")); + + // Apply new schedule immediately + if (!loadSchedule()) + { + DEBUG_PRINTLN(F("[Schedule] Failed to load new schedule")); + request->send(500, FPSTR(CONTENT_TYPE_PLAIN), F("Schedule uploaded but failed to load.")); + return; + } } + else + { + if (filename.indexOf(F("cfg.json")) >= 0) + { + doReboot = true; + request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Configuration restore successful.\nRebooting...")); + } + else + { + if (filename.indexOf(F("palette")) >= 0 && filename.indexOf(F(".json")) >= 0) + { + loadCustomPalettes(); + } + request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File Uploaded!")); + } + } + cacheInvalidate++; } } From 958a34a8efa850caa9644b935ce46e29e2d105a4 Mon Sep 17 00:00:00 2001 From: pagedpenguin251 Date: Mon, 14 Jul 2025 12:30:29 -0600 Subject: [PATCH 2/8] Fix the wled.h include statement --- wled00/schedule.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/schedule.cpp b/wled00/schedule.cpp index 41b1fe47d1..0823231d09 100644 --- a/wled00/schedule.cpp +++ b/wled00/schedule.cpp @@ -2,7 +2,7 @@ // Handles reading, parsing, and checking the preset schedule from schedule.json #include "schedule.h" -#include +#include "wled.h" #include #define SCHEDULE_FILE "/schedule.json" From e95c2b71fa80b5a842780e77876e7dbfab1ba3ce Mon Sep 17 00:00:00 2001 From: pagedpenguin251 Date: Mon, 14 Jul 2025 12:32:03 -0600 Subject: [PATCH 3/8] Add configurable JSON buffer size --- wled00/schedule.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wled00/schedule.cpp b/wled00/schedule.cpp index 0823231d09..655389d41c 100644 --- a/wled00/schedule.cpp +++ b/wled00/schedule.cpp @@ -6,6 +6,7 @@ #include #define SCHEDULE_FILE "/schedule.json" +#define SCHEDULE_JSON_BUFFER_SIZE 4096 // Array to hold scheduled events, max size defined in schedule.h ScheduleEvent scheduleEvents[MAX_SCHEDULE_EVENTS]; @@ -96,7 +97,7 @@ bool loadSchedule() { return false; } - DynamicJsonDocument doc(4096); + DynamicJsonDocument doc(SCHEDULE_JSON_BUFFER_SIZE); DeserializationError error = deserializeJson(doc, file); file.close(); // Always close file before releasing lock From 01ac8b0cf786994897b2fdf5027df9c9c616d671 Mon Sep 17 00:00:00 2001 From: pagedpenguin251 Date: Mon, 14 Jul 2025 13:43:54 -0600 Subject: [PATCH 4/8] fix bracket formating --- wled00/schedule.cpp | 15 +++++-------- wled00/wled_server.cpp | 51 ++++++++++++++---------------------------- 2 files changed, 22 insertions(+), 44 deletions(-) diff --git a/wled00/schedule.cpp b/wled00/schedule.cpp index 655389d41c..d6c3d84c7c 100644 --- a/wled00/schedule.cpp +++ b/wled00/schedule.cpp @@ -16,14 +16,12 @@ uint8_t numScheduleEvents = 0; // Current count of loaded schedule entries bool isTodayInRange(uint8_t sm, uint8_t sd, uint8_t em, uint8_t ed, uint8_t cm, uint8_t cd) { // Handles ranges that wrap over the year end, e.g., Dec to Jan - if (sm < em || (sm == em && sd <= ed)) - { + if (sm < em || (sm == em && sd <= ed)) { // Normal range within a year return (cm > sm || (cm == sm && cd >= sd)) && (cm < em || (cm == em && cd <= ed)); } - else - { + else { // Range wraps year-end (e.g., Nov 20 to Feb 10) return (cm > sm || (cm == sm && cd >= sd)) || (cm < em || (cm == em && cd <= ed)); @@ -53,8 +51,7 @@ void checkSchedule() { DEBUG_PRINTF_P(PSTR("[Schedule] Checking schedule at %02u:%02u\n"), hr, min); // Iterate through all scheduled events - for (uint8_t i = 0; i < numScheduleEvents; i++) - { + for (uint8_t i = 0; i < numScheduleEvents; i++) { const ScheduleEvent &e = scheduleEvents[i]; // Skip if hour or minute doesn't match current time @@ -68,15 +65,13 @@ void checkSchedule() { match = true; // Otherwise check if current date is within start and end date range - if (e.startMonth) - { + if (e.startMonth) { if (isTodayInRange(e.startMonth, e.startDay, e.endMonth, e.endDay, cm, cd)) match = true; } // If match, apply preset and print debug - if (match) - { + if (match) { applyPreset(e.presetId); DEBUG_PRINTF_P(PSTR("[Schedule] Applying preset %u at %02u:%02u\n"), e.presetId, hr, min); } diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index a84ed461dc..132ec3c23f 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -175,47 +175,38 @@ static String msgProcessor(const String& var) return String(); } -static void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool isFinal) -{ - if (!correctPIN) - { +static void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool isFinal){ + if (!correctPIN) { if (isFinal) request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_unlock_cfg)); return; } String finalname = filename; - if (!index) - { - if (finalname.charAt(0) != '/') - { + if (!index) { + if (finalname.charAt(0) != '/') { finalname = '/' + finalname; // prepend slash if missing } // Special case: schedule.json upload uses temp file - if (finalname.equals(F("/schedule.json"))) - { + if (finalname.equals(F("/schedule.json"))) { request->_tempFile = WLED_FS.open("/schedule.json.tmp", "w"); DEBUG_PRINTLN(F("Uploading to /schedule.json.tmp")); } - else - { + else { request->_tempFile = WLED_FS.open(finalname, "w"); DEBUG_PRINTF_P(PSTR("Uploading %s\n"), finalname.c_str()); - if (finalname.equals(FPSTR(getPresetsFileName()))) - { + if (finalname.equals(FPSTR(getPresetsFileName()))) { presetsModifiedTime = toki.second(); } } } // Write chunk - if (len && request->_tempFile) - { + if (len && request->_tempFile) { size_t written = request->_tempFile.write(data, len); - if (written != len) - { + if (written != len) { DEBUG_PRINTLN(F("File write error during upload")); request->_tempFile.close(); request->_tempFile = File(); // invalidate file handle @@ -224,21 +215,17 @@ static void handleUpload(AsyncWebServerRequest *request, const String &filename, } // Finalize upload - if (isFinal) - { + if (isFinal) { if (request->_tempFile) request->_tempFile.close(); - if (finalname.equals(F("/schedule.json"))) - { + if (finalname.equals(F("/schedule.json"))) { // Atomically replace old file // First try rename (which overwrites on most filesystems) - if (!WLED_FS.rename("/schedule.json.tmp", "/schedule.json")) - { + if (!WLED_FS.rename("/schedule.json.tmp", "/schedule.json")) { // If rename failed, try remove then rename WLED_FS.remove("/schedule.json"); - if (!WLED_FS.rename("/schedule.json.tmp", "/schedule.json")) - { + if (!WLED_FS.rename("/schedule.json.tmp", "/schedule.json")) { DEBUG_PRINTLN(F("[Schedule] Failed to replace schedule file")); request->send(500, FPSTR(CONTENT_TYPE_PLAIN), F("Failed to save schedule file.")); return; @@ -248,8 +235,7 @@ static void handleUpload(AsyncWebServerRequest *request, const String &filename, DEBUG_PRINTLN(F("[Schedule] Upload complete and applied.")); // Apply new schedule immediately - if (!loadSchedule()) - { + if (!loadSchedule()) { DEBUG_PRINTLN(F("[Schedule] Failed to load new schedule")); request->send(500, FPSTR(CONTENT_TYPE_PLAIN), F("Schedule uploaded but failed to load.")); return; @@ -257,15 +243,12 @@ static void handleUpload(AsyncWebServerRequest *request, const String &filename, } else { - if (filename.indexOf(F("cfg.json")) >= 0) - { + if (filename.indexOf(F("cfg.json")) >= 0) { doReboot = true; request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Configuration restore successful.\nRebooting...")); } - else - { - if (filename.indexOf(F("palette")) >= 0 && filename.indexOf(F(".json")) >= 0) - { + else { + if (filename.indexOf(F("palette")) >= 0 && filename.indexOf(F(".json")) >= 0) { loadCustomPalettes(); } request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File Uploaded!")); From c892931b4510e810616712ca5854480ccc363050 Mon Sep 17 00:00:00 2001 From: pagedpenguin251 Date: Mon, 14 Jul 2025 13:58:04 -0600 Subject: [PATCH 5/8] Using single instance of "/schedule.json" string --- wled00/wled_server.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 132ec3c23f..aa0e2205ed 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -26,6 +26,8 @@ static const char s_rebooting [] PROGMEM = "Rebooting now..."; static const char s_notimplemented[] PROGMEM = "Not implemented"; static const char s_accessdenied[] PROGMEM = "Access Denied"; static const char _common_js[] PROGMEM = "/common.js"; +static const char SCHEDULE_JSON_PATH[] PROGMEM = "/schedule.json"; +static const char SCHEDULE_JSON_TMP_PATH[] PROGMEM = "/schedule.json.tmp"; //Is this an IP? static bool isIp(const String &str) { @@ -189,8 +191,8 @@ static void handleUpload(AsyncWebServerRequest *request, const String &filename, } // Special case: schedule.json upload uses temp file - if (finalname.equals(F("/schedule.json"))) { - request->_tempFile = WLED_FS.open("/schedule.json.tmp", "w"); + if (finalname.equals(FPSTR(SCHEDULE_JSON_PATH))) { + request->_tempFile = WLED_FS.open(FPSTR(SCHEDULE_JSON_TMP_PATH), "w"); DEBUG_PRINTLN(F("Uploading to /schedule.json.tmp")); } else { @@ -219,13 +221,13 @@ static void handleUpload(AsyncWebServerRequest *request, const String &filename, if (request->_tempFile) request->_tempFile.close(); - if (finalname.equals(F("/schedule.json"))) { + if (finalname.equals(FPSTR(SCHEDULE_JSON_PATH))) { // Atomically replace old file // First try rename (which overwrites on most filesystems) - if (!WLED_FS.rename("/schedule.json.tmp", "/schedule.json")) { + if (!WLED_FS.rename(FPSTR(SCHEDULE_JSON_TMP_PATH), FPSTR(SCHEDULE_JSON_PATH))) { // If rename failed, try remove then rename - WLED_FS.remove("/schedule.json"); - if (!WLED_FS.rename("/schedule.json.tmp", "/schedule.json")) { + WLED_FS.remove(FPSTR(SCHEDULE_JSON_PATH)); + if (!WLED_FS.rename(FPSTR(SCHEDULE_JSON_TMP_PATH), FPSTR(SCHEDULE_JSON_PATH))) { DEBUG_PRINTLN(F("[Schedule] Failed to replace schedule file")); request->send(500, FPSTR(CONTENT_TYPE_PLAIN), F("Failed to save schedule file.")); return; From e130bb9886d39c05ad95dc54ab8c773089875fcd Mon Sep 17 00:00:00 2001 From: pagedpenguin251 Date: Mon, 14 Jul 2025 14:30:08 -0600 Subject: [PATCH 6/8] Refactor schedule event storage to use std::vector --- wled00/schedule.cpp | 20 +++++++++----------- wled00/schedule.h | 26 ++++++++++++++++---------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/wled00/schedule.cpp b/wled00/schedule.cpp index d6c3d84c7c..b5f01e9641 100644 --- a/wled00/schedule.cpp +++ b/wled00/schedule.cpp @@ -4,13 +4,13 @@ #include "schedule.h" #include "wled.h" #include +#include #define SCHEDULE_FILE "/schedule.json" #define SCHEDULE_JSON_BUFFER_SIZE 4096 // Array to hold scheduled events, max size defined in schedule.h -ScheduleEvent scheduleEvents[MAX_SCHEDULE_EVENTS]; -uint8_t numScheduleEvents = 0; // Current count of loaded schedule entries +std::vector scheduleEvents; // Helper function to check if current date (cm, cd) is within the event's start and end date range bool isTodayInRange(uint8_t sm, uint8_t sd, uint8_t em, uint8_t ed, uint8_t cm, uint8_t cd) @@ -51,7 +51,7 @@ void checkSchedule() { DEBUG_PRINTF_P(PSTR("[Schedule] Checking schedule at %02u:%02u\n"), hr, min); // Iterate through all scheduled events - for (uint8_t i = 0; i < numScheduleEvents; i++) { + for (size_t i = 0; i < scheduleEvents.size(); i++) { const ScheduleEvent &e = scheduleEvents[i]; // Skip if hour or minute doesn't match current time @@ -102,9 +102,8 @@ bool loadSchedule() { return false; } - numScheduleEvents = 0; + scheduleEvents.clear(); for (JsonObject e : doc.as()) { - if (numScheduleEvents >= MAX_SCHEDULE_EVENTS) break; // Read and validate fields with type safety int sm = e["sm"].as(); @@ -117,24 +116,23 @@ bool loadSchedule() { int p = e["p"].as(); // Validate ranges to prevent bad data - if (sm < 1 || sm > 12 || em < 1 || em > 12 || - sd < 1 || sd > 31 || ed < 1 || ed > 31 || + if (sm < 0 || sm > 12 || em < 0 || em > 12 || + sd < 0 || sd > 31 || ed < 0 || ed > 31 || h < 0 || h > 23 || m < 0 || m > 59 || r < 0 || r > 127|| p < 1 || p > 250) { DEBUG_PRINTF_P(PSTR("[Schedule] Invalid values in event %u, skipping\n"), numScheduleEvents); continue; } - // Store event in the array - scheduleEvents[numScheduleEvents++] = { + scheduleEvents.push_back({ (uint8_t)sm, (uint8_t)sd, (uint8_t)em, (uint8_t)ed, (uint8_t)r, (uint8_t)h, (uint8_t)m, (uint8_t)p - }; + }); } - DEBUG_PRINTF_P(PSTR("[Schedule] Loaded %u schedule entries from schedule.json\n"), numScheduleEvents); + DEBUG_PRINTF_P(PSTR("[Schedule] Loaded %u schedule entries from schedule.json\n"), (uint16_t)scheduleEvents.size()); // Release JSON buffer lock after finishing releaseJSONBufferLock(); diff --git a/wled00/schedule.h b/wled00/schedule.h index d7d66c876a..c94764f96f 100644 --- a/wled00/schedule.h +++ b/wled00/schedule.h @@ -8,20 +8,26 @@ // Maximum number of schedule events supported #define MAX_SCHEDULE_EVENTS 32 -// Structure representing one scheduled event struct ScheduleEvent { - uint8_t startMonth; // Starting month of the schedule event (1-12) - uint8_t startDay; // Starting day of the schedule event (1-31) - uint8_t endMonth; // Ending month of the schedule event (1-12) - uint8_t endDay; // Ending day of the schedule event (1-31) - uint8_t repeatMask; // Bitmask indicating repeat days of the week (bit0=Sunday ... bit6=Saturday) - uint8_t hour; // Hour of day when event triggers (0-23) - uint8_t minute; // Minute of hour when event triggers (0-59) - uint8_t presetId; // Preset number to apply when event triggers (1-250) + uint8_t startMonth : 4; // 0–12 (0 means no date range) + uint8_t startDay : 5; // 0–31 + + uint8_t endMonth : 4; // 0–12 + uint8_t endDay : 5; // 0–31 + + uint8_t repeatMask : 7; // bitmask for days of week (Sun=bit0 .. Sat=bit6) + + uint8_t hour : 5; // 0–23 + uint8_t minute : 6; // 0–59 + + uint8_t presetId; // 1–250 }; // Loads the schedule from the JSON file; returns true if successful bool loadSchedule(); // Checks current time against schedule and applies any matching preset -void checkSchedule(); \ No newline at end of file +void checkSchedule(); + +// Checks if the current date is within the specified range +bool isTodayInRange(uint8_t sm, uint8_t sd, uint8_t em, uint8_t ed, uint8_t cm, uint8_t cd); \ No newline at end of file From de9630eca68330b95847153bd80e6d01120495b3 Mon Sep 17 00:00:00 2001 From: pagedpenguin251 Date: Mon, 14 Jul 2025 14:47:08 -0600 Subject: [PATCH 7/8] Fix undefined variable and improve validation. --- wled00/schedule.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wled00/schedule.cpp b/wled00/schedule.cpp index b5f01e9641..e805547d4e 100644 --- a/wled00/schedule.cpp +++ b/wled00/schedule.cpp @@ -116,11 +116,11 @@ bool loadSchedule() { int p = e["p"].as(); // Validate ranges to prevent bad data - if (sm < 0 || sm > 12 || em < 0 || em > 12 || - sd < 0 || sd > 31 || ed < 0 || ed > 31 || + if (sm < 1 || sm > 12 || em < 1 || em > 12 || + sd < 1 || sd > 31 || ed < 1 || ed > 31 || h < 0 || h > 23 || m < 0 || m > 59 || r < 0 || r > 127|| p < 1 || p > 250) { - DEBUG_PRINTF_P(PSTR("[Schedule] Invalid values in event %u, skipping\n"), numScheduleEvents); + DEBUG_PRINTF_P(PSTR("[Schedule] Invalid values in event %u, skipping\n"), (uint16_t)scheduleEvents.size()); continue; } From 255ba9b6e309342baeaba1c91b1b6d96a44373ab Mon Sep 17 00:00:00 2001 From: PagedPenguin Date: Wed, 23 Jul 2025 14:58:55 -0600 Subject: [PATCH 8/8] dynamic schedule ui adding --- wled00/data/settings_time.htm | 208 +++++++++++++++++++++++++++------- 1 file changed, 168 insertions(+), 40 deletions(-) diff --git a/wled00/data/settings_time.htm b/wled00/data/settings_time.htm index 8622b79c79..2fa5c09bb9 100644 --- a/wled00/data/settings_time.htm +++ b/wled00/data/settings_time.htm @@ -8,6 +8,8 @@