From a5a5729542f8f469b35018b065d5d98a36c0de6b Mon Sep 17 00:00:00 2001 From: zekageri Date: Tue, 10 Mar 2026 13:11:37 +0100 Subject: [PATCH] chore: align formatter baseline with esptoolkit-template --- .clang-format | 11 + .editorconfig | 11 + .gitignore | 1 - .vscode/bin/clang-format | 19 + .vscode/extensions.json | 9 + .vscode/settings.json | 30 + .vscode/tasks.json | 12 + README.md | 7 + examples/custom_fields/custom_fields.ino | 18 +- .../inline_astronomical.ino | 32 +- .../inline_every_15_minutes_work_hours.ino | 12 +- .../inline_every_hour/inline_every_hour.ino | 12 +- .../inline_every_hour_selected_days.ino | 10 +- .../inline_every_minute.ino | 12 +- .../inline_every_minute_selected_days.ino | 10 +- .../inline_pause_resume.ino | 12 +- examples/worker_weekly/worker_weekly.ino | 12 +- scripts/format_cpp.sh | 24 + src/esp_scheduler/scheduler.cpp | 1825 +++++++++-------- src/esp_scheduler/scheduler.h | 403 ++-- src/esp_scheduler/scheduler_allocator.h | 109 +- .../test_esp_scheduler/test_esp_scheduler.cpp | 528 +++-- 22 files changed, 1688 insertions(+), 1431 deletions(-) create mode 100644 .clang-format create mode 100644 .editorconfig create mode 100755 .vscode/bin/clang-format create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100755 scripts/format_cpp.sh diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..8450693 --- /dev/null +++ b/.clang-format @@ -0,0 +1,11 @@ +BasedOnStyle: LLVM +ColumnLimit: 100 +BinPackArguments: false +BinPackParameters: false +AllowAllArgumentsOnNextLine: false +AlignAfterOpenBracket: BlockIndent +UseTab: ForIndentation +IndentWidth: 4 +TabWidth: 4 +ContinuationIndentWidth: 4 +AllowShortFunctionsOnASingleLine: None diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d89c76d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +[*.{c,cc,cpp,h,hpp,ino}] +indent_style = tab +indent_size = tab +tab_width = 4 diff --git a/.gitignore b/.gitignore index 78f49b6..6346d5c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ .venv build/ build_prev_runner/ -.vscode \ No newline at end of file diff --git a/.vscode/bin/clang-format b/.vscode/bin/clang-format new file mode 100755 index 0000000..0df371f --- /dev/null +++ b/.vscode/bin/clang-format @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if command -v clang-format >/dev/null 2>&1; then + exec clang-format "$@" +fi + +_home_dir="${HOME:-}" +if [ -n "$_home_dir" ]; then + _candidate="$(ls -1d "$_home_dir"/.vscode/extensions/ms-vscode.cpptools-*-linux-x64/LLVM/bin/clang-format 2>/dev/null | tail -n 1 || true)" + if [ -n "$_candidate" ] && [ -x "$_candidate" ]; then + exec "$_candidate" "$@" + fi +fi + +echo "clang-format executable not found." >&2 +echo "Install clang-format system-wide or install/update ms-vscode.cpptools." >&2 +exit 127 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..f814711 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "pioarduino.pioarduino-ide", + "xaver.clang-format" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..24368c8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,30 @@ +{ + "files.associations": { + "*.ino": "cpp" + }, + "editor.defaultFormatter": "xaver.clang-format", + "C_Cpp.formatting": "Disabled", + "clang-format.style": "file", + "clang-format.executable": "${workspaceRoot}/.vscode/bin/clang-format", + "[cpp]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + }, + "[c]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + }, + "[arduino]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..20e66d5 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,12 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Format Firmware Sources", + "type": "shell", + "command": "bash ${workspaceFolder}/scripts/format_cpp.sh", + "group": "build", + "problemMatcher": [] + } + ] +} diff --git a/README.md b/README.md index 241c6da..3fe3063 100644 --- a/README.md +++ b/README.md @@ -339,6 +339,13 @@ Example sketches in this repo: - Host-side CTest is intentionally skipped because the scheduler relies on ESP32 FreeRTOS and ESPDate wall-clock helpers. - CI also compiles all examples through PlatformIO and Arduino CLI across ESP32, S3, C3, and P4 boards. +## Formatting Baseline + +This repository follows the firmware formatting baseline from `esptoolkit-template`: +- `.clang-format` is the source of truth for C/C++/INO layout. +- `.editorconfig` enforces tabs (`tab_width = 4`), LF endings, and final newline. +- Format all tracked firmware sources with `bash scripts/format_cpp.sh`. + ## License MIT — see `LICENSE.md`. diff --git a/examples/custom_fields/custom_fields.ino b/examples/custom_fields/custom_fields.ino index fe506cb..67c1093 100644 --- a/examples/custom_fields/custom_fields.ino +++ b/examples/custom_fields/custom_fields.ino @@ -18,19 +18,15 @@ void setup() { int weekdays[] = {1, 3, 5}; // Mon, Wed, Fri (0 = Sun) Schedule custom = Schedule::custom( - ScheduleField::every(5), // every 5 minutes - ScheduleField::range(9, 17), // during hours 9..17 - ScheduleField::any(), // any day of month - ScheduleField::any(), // any month - ScheduleField::list(weekdays, 3) // selected weekdays + ScheduleField::every(5), // every 5 minutes + ScheduleField::range(9, 17), // during hours 9..17 + ScheduleField::any(), // any day of month + ScheduleField::any(), // any month + ScheduleField::list(weekdays, 3) // selected weekdays ); - scheduler.addJob( - custom, - SchedulerJobMode::Inline, - &customInline, - (void *)"MWF 9-17 every 5min" - ); + scheduler + .addJob(custom, SchedulerJobMode::Inline, &customInline, (void *)"MWF 9-17 every 5min"); } void loop() { diff --git a/examples/inline_astronomical/inline_astronomical.ino b/examples/inline_astronomical/inline_astronomical.ino index 1ebe254..5313c1f 100644 --- a/examples/inline_astronomical/inline_astronomical.ino +++ b/examples/inline_astronomical/inline_astronomical.ino @@ -28,29 +28,25 @@ void setup() { scheduler.setMinValidUtc(date.fromUtc(2020, 1, 1, 0, 0, 0)); + scheduler + .addJob(Schedule::sunrise(), SchedulerJobMode::Inline, &astroCallback, (void *)"sunrise"); scheduler.addJob( - Schedule::sunrise(), - SchedulerJobMode::Inline, - &astroCallback, - (void *)"sunrise" + Schedule::sunset(15), + SchedulerJobMode::Inline, + &astroCallback, + (void *)"sunset +15m" ); scheduler.addJob( - Schedule::sunset(15), - SchedulerJobMode::Inline, - &astroCallback, - (void *)"sunset +15m" + Schedule::moonPhase(MoonPhaseName::LastQuarter, 2), + SchedulerJobMode::Inline, + &astroCallback, + (void *)"last quarter" ); scheduler.addJob( - Schedule::moonPhase(MoonPhaseName::LastQuarter, 2), - SchedulerJobMode::Inline, - &astroCallback, - (void *)"last quarter" - ); - scheduler.addJob( - Schedule::moonIlluminationPercent(75.0, 0.5), - SchedulerJobMode::Inline, - &astroCallback, - (void *)"illumination 75%" + Schedule::moonIlluminationPercent(75.0, 0.5), + SchedulerJobMode::Inline, + &astroCallback, + (void *)"illumination 75%" ); } diff --git a/examples/inline_every_15_minutes_work_hours/inline_every_15_minutes_work_hours.ino b/examples/inline_every_15_minutes_work_hours/inline_every_15_minutes_work_hours.ino index 60959bc..a12dd87 100644 --- a/examples/inline_every_15_minutes_work_hours/inline_every_15_minutes_work_hours.ino +++ b/examples/inline_every_15_minutes_work_hours/inline_every_15_minutes_work_hours.ino @@ -17,12 +17,12 @@ void setup() { // Useful for periodic checks while staff is active. Schedule schedule = Schedule::custom( - ScheduleField::every(15), // minute: 0,15,30,45 - ScheduleField::range(9, 17), - ScheduleField::any(), - ScheduleField::any(), - ScheduleField::any() - ); + ScheduleField::every(15), // minute: 0,15,30,45 + ScheduleField::range(9, 17), + ScheduleField::any(), + ScheduleField::any(), + ScheduleField::any() + ); scheduler.addJob(schedule, SchedulerJobMode::Inline, &workHoursPulse, nullptr); } diff --git a/examples/inline_every_hour/inline_every_hour.ino b/examples/inline_every_hour/inline_every_hour.ino index 306bb34..88dd75d 100644 --- a/examples/inline_every_hour/inline_every_hour.ino +++ b/examples/inline_every_hour/inline_every_hour.ino @@ -17,12 +17,12 @@ void setup() { // Fires at HH:00 every day. Schedule schedule = Schedule::custom( - ScheduleField::only(0), // minute - ScheduleField::any(), // hour - ScheduleField::any(), // day of month - ScheduleField::any(), // month - ScheduleField::any() // day of week - ); + ScheduleField::only(0), // minute + ScheduleField::any(), // hour + ScheduleField::any(), // day of month + ScheduleField::any(), // month + ScheduleField::any() // day of week + ); scheduler.addJob(schedule, SchedulerJobMode::Inline, &everyHour, nullptr); } diff --git a/examples/inline_every_hour_selected_days/inline_every_hour_selected_days.ino b/examples/inline_every_hour_selected_days/inline_every_hour_selected_days.ino index eedc45b..c502e2b 100644 --- a/examples/inline_every_hour_selected_days/inline_every_hour_selected_days.ino +++ b/examples/inline_every_hour_selected_days/inline_every_hour_selected_days.ino @@ -17,11 +17,11 @@ void setup() { int selectedDays[] = {2, 4, 6}; // Tue, Thu, Sat Schedule schedule = Schedule::custom( - ScheduleField::only(0), // minute - ScheduleField::any(), // hour - ScheduleField::any(), // day of month - ScheduleField::any(), // month - ScheduleField::list(selectedDays, 3) // day of week + ScheduleField::only(0), // minute + ScheduleField::any(), // hour + ScheduleField::any(), // day of month + ScheduleField::any(), // month + ScheduleField::list(selectedDays, 3) // day of week ); scheduler.addJob(schedule, SchedulerJobMode::Inline, &hourOnSelectedDays, nullptr); diff --git a/examples/inline_every_minute/inline_every_minute.ino b/examples/inline_every_minute/inline_every_minute.ino index d44ae4e..f7e4316 100644 --- a/examples/inline_every_minute/inline_every_minute.ino +++ b/examples/inline_every_minute/inline_every_minute.ino @@ -16,12 +16,12 @@ void setup() { Serial.println("ESPScheduler inline every-minute example"); Schedule schedule = Schedule::custom( - ScheduleField::every(1), // minute - ScheduleField::any(), // hour - ScheduleField::any(), // day of month - ScheduleField::any(), // month - ScheduleField::any() // day of week - ); + ScheduleField::every(1), // minute + ScheduleField::any(), // hour + ScheduleField::any(), // day of month + ScheduleField::any(), // month + ScheduleField::any() // day of week + ); scheduler.addJob(schedule, SchedulerJobMode::Inline, &everyMinute, nullptr); } diff --git a/examples/inline_every_minute_selected_days/inline_every_minute_selected_days.ino b/examples/inline_every_minute_selected_days/inline_every_minute_selected_days.ino index 1abb9a2..02d530b 100644 --- a/examples/inline_every_minute_selected_days/inline_every_minute_selected_days.ino +++ b/examples/inline_every_minute_selected_days/inline_every_minute_selected_days.ino @@ -17,11 +17,11 @@ void setup() { int selectedDays[] = {1, 3, 5}; // 0=Sun, 1=Mon, ... 6=Sat Schedule schedule = Schedule::custom( - ScheduleField::every(1), // minute - ScheduleField::any(), // hour - ScheduleField::any(), // day of month - ScheduleField::any(), // month - ScheduleField::list(selectedDays, 3) // day of week + ScheduleField::every(1), // minute + ScheduleField::any(), // hour + ScheduleField::any(), // day of month + ScheduleField::any(), // month + ScheduleField::list(selectedDays, 3) // day of week ); scheduler.addJob(schedule, SchedulerJobMode::Inline, &minuteOnSelectedDays, nullptr); diff --git a/examples/inline_pause_resume/inline_pause_resume.ino b/examples/inline_pause_resume/inline_pause_resume.ino index 734b218..a8d00d6 100644 --- a/examples/inline_pause_resume/inline_pause_resume.ino +++ b/examples/inline_pause_resume/inline_pause_resume.ino @@ -19,12 +19,12 @@ void setup() { // Every minute (all hours/days) using custom cron fields Schedule everyMinute = Schedule::custom( - ScheduleField::every(1), // minute - ScheduleField::any(), // hour - ScheduleField::any(), // day of month - ScheduleField::any(), // month - ScheduleField::any() // day of week - ); + ScheduleField::every(1), // minute + ScheduleField::any(), // hour + ScheduleField::any(), // day of month + ScheduleField::any(), // month + ScheduleField::any() // day of week + ); jobId = scheduler.addJob(everyMinute, SchedulerJobMode::Inline, &recurringInline, nullptr); } diff --git a/examples/worker_weekly/worker_weekly.ino b/examples/worker_weekly/worker_weekly.ino index c9ed81c..960264c 100644 --- a/examples/worker_weekly/worker_weekly.ino +++ b/examples/worker_weekly/worker_weekly.ino @@ -26,12 +26,12 @@ void setup() { cfg.usePsramStack = true; scheduler.addJob( - Schedule::weeklyAtLocal(weekdaysMask, 18, 30), - SchedulerJobMode::WorkerTask, - &weeklyReport, - nullptr, - &cfg - ); + Schedule::weeklyAtLocal(weekdaysMask, 18, 30), + SchedulerJobMode::WorkerTask, + &weeklyReport, + nullptr, + &cfg + ); } void loop() { diff --git a/scripts/format_cpp.sh b/scripts/format_cpp.sh new file mode 100755 index 0000000..7d17b04 --- /dev/null +++ b/scripts/format_cpp.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -euo pipefail + +_repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +_clang_format="${_repo_root}/.vscode/bin/clang-format" + +if [ ! -x "${_clang_format}" ]; then + echo "clang-format wrapper not found: ${_clang_format}" >&2 + exit 1 +fi + +mapfile -d '' _format_files < <( + git -C "${_repo_root}" ls-files -z -- '*.c' '*.cc' '*.cpp' '*.h' '*.hpp' '*.ino' +) + +if [ "${#_format_files[@]}" -eq 0 ]; then + echo "No tracked C/C++/INO files found to format." + exit 0 +fi + +"${_clang_format}" -i --style=file "${_format_files[@]}" + +echo "Formatted ${#_format_files[@]} files." diff --git a/src/esp_scheduler/scheduler.cpp b/src/esp_scheduler/scheduler.cpp index bf152b4..ca3a26a 100644 --- a/src/esp_scheduler/scheduler.cpp +++ b/src/esp_scheduler/scheduler.cpp @@ -26,1007 +26,1064 @@ constexpr double kMaxIlluminationTolerancePercent = 50.0; constexpr double kFullCircleDegrees = 360.0; constexpr double kComparisonEpsilon = 1e-9; -bool clockValidForMin(const DateTime& nowUtc, int64_t minValidEpochSeconds) { - return nowUtc.epochSeconds >= minValidEpochSeconds; +bool clockValidForMin(const DateTime &nowUtc, int64_t minValidEpochSeconds) { + return nowUtc.epochSeconds >= minValidEpochSeconds; } -ScheduleKind resolvedScheduleKind(const Schedule& schedule) { - if (schedule.kind == ScheduleKind::Cron && schedule.isOneShot) { - return ScheduleKind::OneShotUtc; - } - return schedule.kind; +ScheduleKind resolvedScheduleKind(const Schedule &schedule) { + if (schedule.kind == ScheduleKind::Cron && schedule.isOneShot) { + return ScheduleKind::OneShotUtc; + } + return schedule.kind; } -bool isOneShotSchedule(const Schedule& schedule) { - return resolvedScheduleKind(schedule) == ScheduleKind::OneShotUtc; +bool isOneShotSchedule(const Schedule &schedule) { + return resolvedScheduleKind(schedule) == ScheduleKind::OneShotUtc; } -DateTime roundToNextMinute(const ESPDate& date, const DateTime& fromUtc) { - DateTime rounded = fromUtc; - if (fromUtc.secondUtc() > 0) { - rounded = date.addMinutes(rounded, 1); - } - return date.setTimeOfDayUtc(rounded, rounded.hourUtc(), rounded.minuteUtc(), 0); +DateTime roundToNextMinute(const ESPDate &date, const DateTime &fromUtc) { + DateTime rounded = fromUtc; + if (fromUtc.secondUtc() > 0) { + rounded = date.addMinutes(rounded, 1); + } + return date.setTimeOfDayUtc(rounded, rounded.hourUtc(), rounded.minuteUtc(), 0); } double normalizeAngle360(double angle) { - double normalized = std::fmod(angle, kFullCircleDegrees); - if (normalized < 0.0) { - normalized += kFullCircleDegrees; - } - return normalized; + double normalized = std::fmod(angle, kFullCircleDegrees); + if (normalized < 0.0) { + normalized += kFullCircleDegrees; + } + return normalized; } double unwrapAngle(double previousUnwrapped, double currentWrapped) { - const double previousWrapped = normalizeAngle360(previousUnwrapped); - double delta = currentWrapped - previousWrapped; - if (delta > 180.0) { - delta -= kFullCircleDegrees; - } else if (delta < -180.0) { - delta += kFullCircleDegrees; - } - return previousUnwrapped + delta; + const double previousWrapped = normalizeAngle360(previousUnwrapped); + double delta = currentWrapped - previousWrapped; + if (delta > 180.0) { + delta -= kFullCircleDegrees; + } else if (delta < -180.0) { + delta += kFullCircleDegrees; + } + return previousUnwrapped + delta; } bool valueWithinPeriodicWindow(double value, double center, double tolerance) { - const double distance = std::fabs(value - center); - double wrapped = std::fmod(distance, kFullCircleDegrees); - if (wrapped < 0.0) { - wrapped += kFullCircleDegrees; - } - const double minimumDistance = std::min(wrapped, kFullCircleDegrees - wrapped); - return minimumDistance <= tolerance + kComparisonEpsilon; + const double distance = std::fabs(value - center); + double wrapped = std::fmod(distance, kFullCircleDegrees); + if (wrapped < 0.0) { + wrapped += kFullCircleDegrees; + } + const double minimumDistance = std::min(wrapped, kFullCircleDegrees - wrapped); + return minimumDistance <= tolerance + kComparisonEpsilon; } bool segmentIntersectsRange(double a, double b, double minValue, double maxValue) { - const double lo = std::min(a, b); - const double hi = std::max(a, b); - return hi >= minValue - kComparisonEpsilon && lo <= maxValue + kComparisonEpsilon; + const double lo = std::min(a, b); + const double hi = std::max(a, b); + return hi >= minValue - kComparisonEpsilon && lo <= maxValue + kComparisonEpsilon; } bool segmentIntersectsPeriodicWindow(double a, double b, double center, double tolerance) { - const double lo = std::min(a, b); - const double hi = std::max(a, b); - const int64_t firstPeriod = - static_cast(std::floor((lo - (center + tolerance)) / kFullCircleDegrees)) - 1; - const int64_t lastPeriod = - static_cast(std::ceil((hi - (center - tolerance)) / kFullCircleDegrees)) + 1; - for (int64_t period = firstPeriod; period <= lastPeriod; ++period) { - const double periodShift = static_cast(period) * kFullCircleDegrees; - const double windowMin = center - tolerance + periodShift; - const double windowMax = center + tolerance + periodShift; - if (segmentIntersectsRange(lo, hi, windowMin, windowMax)) { - return true; - } - } - return false; -} - -bool computeNextCronOccurrenceForDate(const ESPDate& date, - const Schedule& schedule, - const DateTime& fromUtc, - DateTime& outNextUtc) { - const DateTime rounded = roundToNextMinute(date, fromUtc); - DateTime cursor = rounded; - for (int64_t i = 0; i < kMaxSearchMinutes; ++i) { - const int month = date.getMonthLocal(cursor); - const int day = date.getDayLocal(cursor); - const int dow = date.getWeekdayLocal(cursor); - - const DateTime startOfDay = date.startOfDayLocal(cursor); - const int64_t minutesIntoDay = date.differenceInMinutes(cursor, startOfDay); - if (minutesIntoDay < 0) { - cursor = date.addMinutes(cursor, 1); - continue; - } - const int hour = static_cast(minutesIntoDay / 60); - const int minute = static_cast(minutesIntoDay % 60); - - const bool monthOk = schedule.month.matches(month); - const bool hourOk = schedule.hour.matches(hour); - const bool minuteOk = schedule.minute.matches(minute); - - const bool domAny = schedule.dayOfMonth.isAny(); - const bool dowAny = schedule.dayOfWeek.isAny(); - const bool domOk = schedule.dayOfMonth.matches(day); - const bool dowOk = schedule.dayOfWeek.matches(dow); - - bool dayOk = false; - if (domAny && dowAny) { - dayOk = true; - } else if (domAny && !dowAny) { - dayOk = dowOk; - } else if (!domAny && dowAny) { - dayOk = domOk; - } else { - dayOk = domOk || dowOk; - } - - if (monthOk && hourOk && minuteOk && dayOk) { - outNextUtc = date.setTimeOfDayLocal(cursor, hour, minute, 0); - return true; - } - cursor = date.addMinutes(cursor, 1); - } - return false; -} - -bool computeNextSunOccurrenceForDate(const ESPDate& date, - const Schedule& schedule, - const DateTime& fromUtc, - DateTime& outNextUtc, - bool sunrise) { - const DateTime rounded = roundToNextMinute(date, fromUtc); - const DateTime startDay = date.startOfDayLocal(rounded); - for (int64_t dayOffset = 0; dayOffset < kMaxSunSearchDays; ++dayOffset) { - const DateTime dayCursor = date.addDays(startDay, static_cast(dayOffset)); - const SunCycleResult cycle = sunrise ? date.sunrise(dayCursor) : date.sunset(dayCursor); - if (!cycle.ok) { - continue; - } - const DateTime candidate = date.addMinutes(cycle.value, schedule.sunOffsetMinutes); - if (date.isBefore(candidate, rounded)) { - continue; - } - outNextUtc = candidate; - return true; - } - return false; -} - -bool moonPhaseCrossed(double previousUnwrapped, - double currentUnwrapped, - double targetAngleDegrees, - double toleranceDegrees) { - if (valueWithinPeriodicWindow(previousUnwrapped, targetAngleDegrees, toleranceDegrees)) { - return false; - } - return segmentIntersectsPeriodicWindow(previousUnwrapped, currentUnwrapped, targetAngleDegrees, toleranceDegrees); -} - -bool moonIlluminationCrossed(double previousValue, double currentValue, double targetPercent, double tolerancePercent) { - const double minWindow = targetPercent - tolerancePercent; - const double maxWindow = targetPercent + tolerancePercent; - const bool wasInside = previousValue >= minWindow - kComparisonEpsilon && previousValue <= maxWindow + kComparisonEpsilon; - if (wasInside) { - return false; - } - return segmentIntersectsRange(previousValue, currentValue, minWindow, maxWindow); -} - -bool computeNextMoonPhaseOccurrenceForDate(const ESPDate& date, - const Schedule& schedule, - const DateTime& fromUtc, - DateTime& outNextUtc) { - const DateTime rounded = roundToNextMinute(date, fromUtc); - DateTime previous = date.addMinutes(rounded, -1); - DateTime current = rounded; - - MoonPhaseResult previousPhase = date.moonPhase(previous); - if (!previousPhase.ok) { - return false; - } - double previousUnwrapped = static_cast(previousPhase.angleDegrees); - - for (int64_t i = 0; i < kMaxMoonSearchMinutes; ++i) { - MoonPhaseResult currentPhase = date.moonPhase(current); - if (!currentPhase.ok) { - return false; - } - const double currentUnwrapped = unwrapAngle(previousUnwrapped, static_cast(currentPhase.angleDegrees)); - if (moonPhaseCrossed(previousUnwrapped, - currentUnwrapped, - static_cast(schedule.moonPhaseAngleDegrees), - static_cast(schedule.moonPhaseToleranceDegrees))) { - outNextUtc = current; - return true; - } - previousUnwrapped = currentUnwrapped; - current = date.addMinutes(current, 1); - } - return false; -} - -bool computeNextMoonIlluminationOccurrenceForDate(const ESPDate& date, - const Schedule& schedule, - const DateTime& fromUtc, - DateTime& outNextUtc) { - const DateTime rounded = roundToNextMinute(date, fromUtc); - DateTime previous = date.addMinutes(rounded, -1); - DateTime current = rounded; - - MoonPhaseResult previousPhase = date.moonPhase(previous); - if (!previousPhase.ok) { - return false; - } - double previousIllumination = previousPhase.illumination * kMaxIlluminationPercent; - - for (int64_t i = 0; i < kMaxMoonSearchMinutes; ++i) { - MoonPhaseResult currentPhase = date.moonPhase(current); - if (!currentPhase.ok) { - return false; - } - const double currentIllumination = currentPhase.illumination * kMaxIlluminationPercent; - if (moonIlluminationCrossed(previousIllumination, - currentIllumination, - schedule.moonIlluminationTargetPercent, - schedule.moonIlluminationTolerancePercent)) { - outNextUtc = current; - return true; - } - previousIllumination = currentIllumination; - current = date.addMinutes(current, 1); - } - return false; + const double lo = std::min(a, b); + const double hi = std::max(a, b); + const int64_t firstPeriod = + static_cast(std::floor((lo - (center + tolerance)) / kFullCircleDegrees)) - 1; + const int64_t lastPeriod = + static_cast(std::ceil((hi - (center - tolerance)) / kFullCircleDegrees)) + 1; + for (int64_t period = firstPeriod; period <= lastPeriod; ++period) { + const double periodShift = static_cast(period) * kFullCircleDegrees; + const double windowMin = center - tolerance + periodShift; + const double windowMax = center + tolerance + periodShift; + if (segmentIntersectsRange(lo, hi, windowMin, windowMax)) { + return true; + } + } + return false; +} + +bool computeNextCronOccurrenceForDate( + const ESPDate &date, const Schedule &schedule, const DateTime &fromUtc, DateTime &outNextUtc +) { + const DateTime rounded = roundToNextMinute(date, fromUtc); + DateTime cursor = rounded; + for (int64_t i = 0; i < kMaxSearchMinutes; ++i) { + const int month = date.getMonthLocal(cursor); + const int day = date.getDayLocal(cursor); + const int dow = date.getWeekdayLocal(cursor); + + const DateTime startOfDay = date.startOfDayLocal(cursor); + const int64_t minutesIntoDay = date.differenceInMinutes(cursor, startOfDay); + if (minutesIntoDay < 0) { + cursor = date.addMinutes(cursor, 1); + continue; + } + const int hour = static_cast(minutesIntoDay / 60); + const int minute = static_cast(minutesIntoDay % 60); + + const bool monthOk = schedule.month.matches(month); + const bool hourOk = schedule.hour.matches(hour); + const bool minuteOk = schedule.minute.matches(minute); + + const bool domAny = schedule.dayOfMonth.isAny(); + const bool dowAny = schedule.dayOfWeek.isAny(); + const bool domOk = schedule.dayOfMonth.matches(day); + const bool dowOk = schedule.dayOfWeek.matches(dow); + + bool dayOk = false; + if (domAny && dowAny) { + dayOk = true; + } else if (domAny && !dowAny) { + dayOk = dowOk; + } else if (!domAny && dowAny) { + dayOk = domOk; + } else { + dayOk = domOk || dowOk; + } + + if (monthOk && hourOk && minuteOk && dayOk) { + outNextUtc = date.setTimeOfDayLocal(cursor, hour, minute, 0); + return true; + } + cursor = date.addMinutes(cursor, 1); + } + return false; +} + +bool computeNextSunOccurrenceForDate( + const ESPDate &date, + const Schedule &schedule, + const DateTime &fromUtc, + DateTime &outNextUtc, + bool sunrise +) { + const DateTime rounded = roundToNextMinute(date, fromUtc); + const DateTime startDay = date.startOfDayLocal(rounded); + for (int64_t dayOffset = 0; dayOffset < kMaxSunSearchDays; ++dayOffset) { + const DateTime dayCursor = date.addDays(startDay, static_cast(dayOffset)); + const SunCycleResult cycle = sunrise ? date.sunrise(dayCursor) : date.sunset(dayCursor); + if (!cycle.ok) { + continue; + } + const DateTime candidate = date.addMinutes(cycle.value, schedule.sunOffsetMinutes); + if (date.isBefore(candidate, rounded)) { + continue; + } + outNextUtc = candidate; + return true; + } + return false; +} + +bool moonPhaseCrossed( + double previousUnwrapped, + double currentUnwrapped, + double targetAngleDegrees, + double toleranceDegrees +) { + if (valueWithinPeriodicWindow(previousUnwrapped, targetAngleDegrees, toleranceDegrees)) { + return false; + } + return segmentIntersectsPeriodicWindow( + previousUnwrapped, + currentUnwrapped, + targetAngleDegrees, + toleranceDegrees + ); +} + +bool moonIlluminationCrossed( + double previousValue, double currentValue, double targetPercent, double tolerancePercent +) { + const double minWindow = targetPercent - tolerancePercent; + const double maxWindow = targetPercent + tolerancePercent; + const bool wasInside = previousValue >= minWindow - kComparisonEpsilon && + previousValue <= maxWindow + kComparisonEpsilon; + if (wasInside) { + return false; + } + return segmentIntersectsRange(previousValue, currentValue, minWindow, maxWindow); +} + +bool computeNextMoonPhaseOccurrenceForDate( + const ESPDate &date, const Schedule &schedule, const DateTime &fromUtc, DateTime &outNextUtc +) { + const DateTime rounded = roundToNextMinute(date, fromUtc); + DateTime previous = date.addMinutes(rounded, -1); + DateTime current = rounded; + + MoonPhaseResult previousPhase = date.moonPhase(previous); + if (!previousPhase.ok) { + return false; + } + double previousUnwrapped = static_cast(previousPhase.angleDegrees); + + for (int64_t i = 0; i < kMaxMoonSearchMinutes; ++i) { + MoonPhaseResult currentPhase = date.moonPhase(current); + if (!currentPhase.ok) { + return false; + } + const double currentUnwrapped = + unwrapAngle(previousUnwrapped, static_cast(currentPhase.angleDegrees)); + if (moonPhaseCrossed( + previousUnwrapped, + currentUnwrapped, + static_cast(schedule.moonPhaseAngleDegrees), + static_cast(schedule.moonPhaseToleranceDegrees) + )) { + outNextUtc = current; + return true; + } + previousUnwrapped = currentUnwrapped; + current = date.addMinutes(current, 1); + } + return false; +} + +bool computeNextMoonIlluminationOccurrenceForDate( + const ESPDate &date, const Schedule &schedule, const DateTime &fromUtc, DateTime &outNextUtc +) { + const DateTime rounded = roundToNextMinute(date, fromUtc); + DateTime previous = date.addMinutes(rounded, -1); + DateTime current = rounded; + + MoonPhaseResult previousPhase = date.moonPhase(previous); + if (!previousPhase.ok) { + return false; + } + double previousIllumination = previousPhase.illumination * kMaxIlluminationPercent; + + for (int64_t i = 0; i < kMaxMoonSearchMinutes; ++i) { + MoonPhaseResult currentPhase = date.moonPhase(current); + if (!currentPhase.ok) { + return false; + } + const double currentIllumination = currentPhase.illumination * kMaxIlluminationPercent; + if (moonIlluminationCrossed( + previousIllumination, + currentIllumination, + schedule.moonIlluminationTargetPercent, + schedule.moonIlluminationTolerancePercent + )) { + outNextUtc = current; + return true; + } + previousIllumination = currentIllumination; + current = date.addMinutes(current, 1); + } + return false; } int moonPhaseAngleForName(MoonPhaseName name) { - switch (name) { - case MoonPhaseName::NewMoon: - return 0; - case MoonPhaseName::WaxingCrescent: - return 45; - case MoonPhaseName::FirstQuarter: - return 90; - case MoonPhaseName::WaxingGibbous: - return 135; - case MoonPhaseName::FullMoon: - return 180; - case MoonPhaseName::WaningGibbous: - return 225; - case MoonPhaseName::LastQuarter: - return 270; - case MoonPhaseName::WaningCrescent: - return 315; - default: - return 0; - } -} - -bool computeNextOccurrenceForDate(const ESPDate& date, - const Schedule& schedule, - const DateTime& fromUtc, - DateTime& outNextUtc) { - switch (resolvedScheduleKind(schedule)) { - case ScheduleKind::OneShotUtc: - outNextUtc = schedule.onceAtUtc; - return true; - case ScheduleKind::Cron: - return computeNextCronOccurrenceForDate(date, schedule, fromUtc, outNextUtc); - case ScheduleKind::Sunrise: - return computeNextSunOccurrenceForDate(date, schedule, fromUtc, outNextUtc, true); - case ScheduleKind::Sunset: - return computeNextSunOccurrenceForDate(date, schedule, fromUtc, outNextUtc, false); - case ScheduleKind::MoonPhaseAngle: - return computeNextMoonPhaseOccurrenceForDate(date, schedule, fromUtc, outNextUtc); - case ScheduleKind::MoonIlluminationPercent: - return computeNextMoonIlluminationOccurrenceForDate(date, schedule, fromUtc, outNextUtc); - default: - return false; - } -} -} // namespace + switch (name) { + case MoonPhaseName::NewMoon: + return 0; + case MoonPhaseName::WaxingCrescent: + return 45; + case MoonPhaseName::FirstQuarter: + return 90; + case MoonPhaseName::WaxingGibbous: + return 135; + case MoonPhaseName::FullMoon: + return 180; + case MoonPhaseName::WaningGibbous: + return 225; + case MoonPhaseName::LastQuarter: + return 270; + case MoonPhaseName::WaningCrescent: + return 315; + default: + return 0; + } +} + +bool computeNextOccurrenceForDate( + const ESPDate &date, const Schedule &schedule, const DateTime &fromUtc, DateTime &outNextUtc +) { + switch (resolvedScheduleKind(schedule)) { + case ScheduleKind::OneShotUtc: + outNextUtc = schedule.onceAtUtc; + return true; + case ScheduleKind::Cron: + return computeNextCronOccurrenceForDate(date, schedule, fromUtc, outNextUtc); + case ScheduleKind::Sunrise: + return computeNextSunOccurrenceForDate(date, schedule, fromUtc, outNextUtc, true); + case ScheduleKind::Sunset: + return computeNextSunOccurrenceForDate(date, schedule, fromUtc, outNextUtc, false); + case ScheduleKind::MoonPhaseAngle: + return computeNextMoonPhaseOccurrenceForDate(date, schedule, fromUtc, outNextUtc); + case ScheduleKind::MoonIlluminationPercent: + return computeNextMoonIlluminationOccurrenceForDate(date, schedule, fromUtc, outNextUtc); + default: + return false; + } +} +} // namespace ScheduleField ScheduleField::any() { - ScheduleField f; - f.m_isAny = true; - f.m_mask = 0; - return f; + ScheduleField f; + f.m_isAny = true; + f.m_mask = 0; + return f; } ScheduleField ScheduleField::only(int value) { - ScheduleField f; - if (value < 0 || value > 63) { - return f; - } - f.m_mask = 1ULL << value; - return f; + ScheduleField f; + if (value < 0 || value > 63) { + return f; + } + f.m_mask = 1ULL << value; + return f; } ScheduleField ScheduleField::range(int from, int to) { - ScheduleField f; - if (from < 0 || to < 0 || from > to || to > 63) { - return f; - } - for (int i = from; i <= to; ++i) { - f.m_mask |= 1ULL << i; - } - return f; + ScheduleField f; + if (from < 0 || to < 0 || from > to || to > 63) { + return f; + } + for (int i = from; i <= to; ++i) { + f.m_mask |= 1ULL << i; + } + return f; } ScheduleField ScheduleField::every(int step) { - ScheduleField f; - if (step <= 0) { - return f; - } - for (int i = 0; i <= 63; i += step) { - f.m_mask |= 1ULL << i; - } - return f; + ScheduleField f; + if (step <= 0) { + return f; + } + for (int i = 0; i <= 63; i += step) { + f.m_mask |= 1ULL << i; + } + return f; } ScheduleField ScheduleField::rangeEvery(int from, int to, int step) { - ScheduleField f; - if (step <= 0 || from < 0 || to < 0 || from > to || to > 63) { - return f; - } - for (int i = from; i <= to; i += step) { - f.m_mask |= 1ULL << i; - } - return f; -} - -ScheduleField ScheduleField::list(const int* values, size_t count) { - ScheduleField f; - if (!values || count == 0) { - return f; - } - for (size_t i = 0; i < count; ++i) { - const int v = values[i]; - if (v < 0 || v > 63) { - f.m_mask = 0; - return f; - } - f.m_mask |= 1ULL << v; - } - return f; + ScheduleField f; + if (step <= 0 || from < 0 || to < 0 || from > to || to > 63) { + return f; + } + for (int i = from; i <= to; i += step) { + f.m_mask |= 1ULL << i; + } + return f; +} + +ScheduleField ScheduleField::list(const int *values, size_t count) { + ScheduleField f; + if (!values || count == 0) { + return f; + } + for (size_t i = 0; i < count; ++i) { + const int v = values[i]; + if (v < 0 || v > 63) { + f.m_mask = 0; + return f; + } + f.m_mask |= 1ULL << v; + } + return f; } bool ScheduleField::matches(int value) const { - if (m_isAny) { - return true; - } - if (value < 0 || value > 63) { - return false; - } - return (m_mask & (1ULL << value)) != 0; + if (m_isAny) { + return true; + } + if (value < 0 || value > 63) { + return false; + } + return (m_mask & (1ULL << value)) != 0; } -Schedule Schedule::onceUtc(const DateTime& whenUtc) { - Schedule s; - s.kind = ScheduleKind::OneShotUtc; - s.isOneShot = true; - s.onceAtUtc = whenUtc; - return s; +Schedule Schedule::onceUtc(const DateTime &whenUtc) { + Schedule s; + s.kind = ScheduleKind::OneShotUtc; + s.isOneShot = true; + s.onceAtUtc = whenUtc; + return s; } Schedule Schedule::dailyAtLocal(int hour, int minute) { - Schedule s; - s.kind = ScheduleKind::Cron; - s.isOneShot = false; - s.hour = ScheduleField::only(hour); - s.minute = ScheduleField::only(minute); - s.dayOfMonth = ScheduleField::any(); - s.month = ScheduleField::any(); - s.dayOfWeek = ScheduleField::any(); - return s; + Schedule s; + s.kind = ScheduleKind::Cron; + s.isOneShot = false; + s.hour = ScheduleField::only(hour); + s.minute = ScheduleField::only(minute); + s.dayOfMonth = ScheduleField::any(); + s.month = ScheduleField::any(); + s.dayOfWeek = ScheduleField::any(); + return s; } Schedule Schedule::weeklyAtLocal(uint8_t dowMask, int hour, int minute) { - int days[7]; - size_t count = 0; - for (int i = 0; i < 7; ++i) { - if (dowMask & (1 << i)) { - days[count++] = i; - } - } - Schedule s; - s.kind = ScheduleKind::Cron; - s.isOneShot = false; - s.hour = ScheduleField::only(hour); - s.minute = ScheduleField::only(minute); - s.dayOfMonth = ScheduleField::any(); - s.month = ScheduleField::any(); - if (count == 0) { - s.dayOfWeek = ScheduleField::any(); - } else { - s.dayOfWeek = ScheduleField::list(days, count); - } - return s; + int days[7]; + size_t count = 0; + for (int i = 0; i < 7; ++i) { + if (dowMask & (1 << i)) { + days[count++] = i; + } + } + Schedule s; + s.kind = ScheduleKind::Cron; + s.isOneShot = false; + s.hour = ScheduleField::only(hour); + s.minute = ScheduleField::only(minute); + s.dayOfMonth = ScheduleField::any(); + s.month = ScheduleField::any(); + if (count == 0) { + s.dayOfWeek = ScheduleField::any(); + } else { + s.dayOfWeek = ScheduleField::list(days, count); + } + return s; } Schedule Schedule::monthlyOnDayLocal(int dayOfMonth, int hour, int minute) { - Schedule s; - s.kind = ScheduleKind::Cron; - s.isOneShot = false; - int clamped = dayOfMonth; - if (clamped < 1) { - clamped = 1; - } else if (clamped > 31) { - clamped = 31; - } - s.dayOfMonth = ScheduleField::only(clamped); - s.hour = ScheduleField::only(hour); - s.minute = ScheduleField::only(minute); - s.month = ScheduleField::any(); - s.dayOfWeek = ScheduleField::any(); - return s; + Schedule s; + s.kind = ScheduleKind::Cron; + s.isOneShot = false; + int clamped = dayOfMonth; + if (clamped < 1) { + clamped = 1; + } else if (clamped > 31) { + clamped = 31; + } + s.dayOfMonth = ScheduleField::only(clamped); + s.hour = ScheduleField::only(hour); + s.minute = ScheduleField::only(minute); + s.month = ScheduleField::any(); + s.dayOfWeek = ScheduleField::any(); + return s; } Schedule Schedule::sunrise(int offsetMinutes) { - Schedule s; - s.kind = ScheduleKind::Sunrise; - s.isOneShot = false; - s.sunOffsetMinutes = offsetMinutes; - return s; + Schedule s; + s.kind = ScheduleKind::Sunrise; + s.isOneShot = false; + s.sunOffsetMinutes = offsetMinutes; + return s; } Schedule Schedule::sunset(int offsetMinutes) { - Schedule s; - s.kind = ScheduleKind::Sunset; - s.isOneShot = false; - s.sunOffsetMinutes = offsetMinutes; - return s; + Schedule s; + s.kind = ScheduleKind::Sunset; + s.isOneShot = false; + s.sunOffsetMinutes = offsetMinutes; + return s; } Schedule Schedule::moonPhaseAngle(int angleDegrees, int toleranceDegrees) { - Schedule s; - s.kind = ScheduleKind::MoonPhaseAngle; - s.isOneShot = false; - s.moonPhaseAngleDegrees = angleDegrees; - s.moonPhaseToleranceDegrees = toleranceDegrees; - return s; + Schedule s; + s.kind = ScheduleKind::MoonPhaseAngle; + s.isOneShot = false; + s.moonPhaseAngleDegrees = angleDegrees; + s.moonPhaseToleranceDegrees = toleranceDegrees; + return s; } Schedule Schedule::moonPhase(MoonPhaseName name, int toleranceDegrees) { - return moonPhaseAngle(moonPhaseAngleForName(name), toleranceDegrees); + return moonPhaseAngle(moonPhaseAngleForName(name), toleranceDegrees); } Schedule Schedule::moonIlluminationPercent(double percent, double tolerancePercent) { - Schedule s; - s.kind = ScheduleKind::MoonIlluminationPercent; - s.isOneShot = false; - s.moonIlluminationTargetPercent = percent; - s.moonIlluminationTolerancePercent = tolerancePercent; - return s; -} - -Schedule Schedule::custom(const ScheduleField& minute, - const ScheduleField& hour, - const ScheduleField& dom, - const ScheduleField& month, - const ScheduleField& dow) { - Schedule s; - s.kind = ScheduleKind::Cron; - s.isOneShot = false; - s.minute = minute; - s.hour = hour; - s.dayOfMonth = dom; - s.month = month; - s.dayOfWeek = dow; - return s; -} - -ESPScheduler::ESPScheduler(ESPDate& date, ESPWorker* worker) - : ESPScheduler(date, worker, ESPSchedulerConfig{}) {} - -ESPScheduler::ESPScheduler(ESPDate& date, const ESPSchedulerConfig& config) - : ESPScheduler(date, nullptr, config) {} - -ESPScheduler::ESPScheduler(ESPDate& date, ESPWorker* worker, const ESPSchedulerConfig& config) - : m_date(date), - m_minValidEpochSecondsRef(std::make_shared>(kDefaultMinValidEpochSeconds)), + Schedule s; + s.kind = ScheduleKind::MoonIlluminationPercent; + s.isOneShot = false; + s.moonIlluminationTargetPercent = percent; + s.moonIlluminationTolerancePercent = tolerancePercent; + return s; +} + +Schedule Schedule::custom( + const ScheduleField &minute, + const ScheduleField &hour, + const ScheduleField &dom, + const ScheduleField &month, + const ScheduleField &dow +) { + Schedule s; + s.kind = ScheduleKind::Cron; + s.isOneShot = false; + s.minute = minute; + s.hour = hour; + s.dayOfMonth = dom; + s.month = month; + s.dayOfWeek = dow; + return s; +} + +ESPScheduler::ESPScheduler(ESPDate &date, ESPWorker *worker) + : ESPScheduler(date, worker, ESPSchedulerConfig{}) { +} + +ESPScheduler::ESPScheduler(ESPDate &date, const ESPSchedulerConfig &config) + : ESPScheduler(date, nullptr, config) { +} + +ESPScheduler::ESPScheduler(ESPDate &date, ESPWorker *worker, const ESPSchedulerConfig &config) + : m_date(date), m_minValidEpochSecondsRef( + std::make_shared>(kDefaultMinValidEpochSeconds) + ), usePSRAMBuffers_(config.usePSRAMBuffers), m_inlineJobs(SchedulerAllocator(usePSRAMBuffers_)), m_workerJobs(SchedulerAllocator(usePSRAMBuffers_)) { - (void)worker; + (void)worker; } ESPScheduler::~ESPScheduler() { - deinit(); + deinit(); } void ESPScheduler::deinit() { - if (!m_initialized.exchange(false, std::memory_order_relaxed)) { - return; - } - - for (auto& job : m_inlineJobs) { - job.finished = true; - } - for (auto& job : m_workerJobs) { - if (job.context) { - job.context->cancelRequested.store(true); - } - } - cleanupInline(); - m_workerJobs.clear(); - - SchedulerVector(SchedulerAllocator(usePSRAMBuffers_)).swap(m_inlineJobs); - SchedulerVector(SchedulerAllocator(usePSRAMBuffers_)).swap(m_workerJobs); - m_nextId = 1; + if (!m_initialized.exchange(false, std::memory_order_relaxed)) { + return; + } + + for (auto &job : m_inlineJobs) { + job.finished = true; + } + for (auto &job : m_workerJobs) { + if (job.context) { + job.context->cancelRequested.store(true); + } + } + cleanupInline(); + m_workerJobs.clear(); + + SchedulerVector(SchedulerAllocator(usePSRAMBuffers_)).swap(m_inlineJobs); + SchedulerVector(SchedulerAllocator(usePSRAMBuffers_)).swap(m_workerJobs); + m_nextId = 1; } bool ESPScheduler::isInitialized() const { - return m_initialized.load(std::memory_order_relaxed); + return m_initialized.load(std::memory_order_relaxed); } void ESPScheduler::ensureInitialized() { - if (!isInitialized()) { - m_initialized.store(true, std::memory_order_relaxed); - } + if (!isInitialized()) { + m_initialized.store(true, std::memory_order_relaxed); + } } void ESPScheduler::setMinValidUnixSeconds(int64_t minEpochSeconds) { - m_minValidEpochSeconds = minEpochSeconds; - if (m_minValidEpochSecondsRef) { - m_minValidEpochSecondsRef->store(minEpochSeconds); - } + m_minValidEpochSeconds = minEpochSeconds; + if (m_minValidEpochSecondsRef) { + m_minValidEpochSecondsRef->store(minEpochSeconds); + } } -void ESPScheduler::setMinValidUtc(const DateTime& minUtc) { - setMinValidUnixSeconds(minUtc.epochSeconds); +void ESPScheduler::setMinValidUtc(const DateTime &minUtc) { + setMinValidUnixSeconds(minUtc.epochSeconds); } int64_t ESPScheduler::minValidUnixSeconds() const { - return m_minValidEpochSeconds; + return m_minValidEpochSeconds; } uint32_t ESPScheduler::nextId() { - if (m_nextId == 0) { - m_nextId = 1; - } - return m_nextId++; + if (m_nextId == 0) { + m_nextId = 1; + } + return m_nextId++; } -bool ESPScheduler::fieldWithinRange(const ScheduleField& field, int min, int max) const { - if (field.isAny()) { - return true; - } - const uint64_t mask = field.rawMask(); - const uint64_t allowed = allowedMask(min, max); - return mask != 0 && (mask & allowed) != 0; +bool ESPScheduler::fieldWithinRange(const ScheduleField &field, int min, int max) const { + if (field.isAny()) { + return true; + } + const uint64_t mask = field.rawMask(); + const uint64_t allowed = allowedMask(min, max); + return mask != 0 && (mask & allowed) != 0; } uint64_t ESPScheduler::allowedMask(int min, int max) const { - if (min < 0) { - min = 0; - } - if (max > 63) { - max = 63; - } - if (max >= 63) { - return ~static_cast(0); - } - const uint64_t upper = (1ULL << (max + 1)) - 1; - const uint64_t lower = (min == 0) ? 0 : ((1ULL << min) - 1); - return upper & ~lower; -} - -bool ESPScheduler::validateSchedule(const Schedule& schedule) const { - const ScheduleKind kind = resolvedScheduleKind(schedule); - switch (kind) { - case ScheduleKind::OneShotUtc: - return true; - case ScheduleKind::Cron: { - const bool minuteOk = fieldWithinRange(schedule.minute, 0, 59); - const bool hourOk = fieldWithinRange(schedule.hour, 0, 23); - const bool domOk = fieldWithinRange(schedule.dayOfMonth, 1, 31); - const bool monthOk = fieldWithinRange(schedule.month, 1, 12); - const bool dowOk = fieldWithinRange(schedule.dayOfWeek, 0, 6); - return minuteOk && hourOk && domOk && monthOk && dowOk; - } - case ScheduleKind::Sunrise: - case ScheduleKind::Sunset: - return schedule.sunOffsetMinutes >= kMinSunOffsetMinutes && schedule.sunOffsetMinutes <= kMaxSunOffsetMinutes; - case ScheduleKind::MoonPhaseAngle: - return schedule.moonPhaseAngleDegrees >= kMinMoonPhaseAngle && - schedule.moonPhaseAngleDegrees <= kMaxMoonPhaseAngle && - schedule.moonPhaseToleranceDegrees >= 0 && - schedule.moonPhaseToleranceDegrees <= kMaxMoonPhaseTolerance; - case ScheduleKind::MoonIlluminationPercent: - return std::isfinite(schedule.moonIlluminationTargetPercent) && - std::isfinite(schedule.moonIlluminationTolerancePercent) && - schedule.moonIlluminationTargetPercent >= kMinIlluminationPercent && - schedule.moonIlluminationTargetPercent <= kMaxIlluminationPercent && - schedule.moonIlluminationTolerancePercent > 0.0 && - schedule.moonIlluminationTolerancePercent <= kMaxIlluminationTolerancePercent; - default: - return false; - } -} - -uint32_t ESPScheduler::addJobOnceUtc(const DateTime& whenUtc, - SchedulerJobMode mode, - SchedulerCallback cb, - void* userData, - const SchedulerTaskConfig* taskCfg) { - return addJobOnceUtc(whenUtc, mode, SchedulerFunction(cb), userData, taskCfg); -} - -uint32_t ESPScheduler::addJobOnceUtc(const DateTime& whenUtc, - SchedulerJobMode mode, - SchedulerFunction cb, - void* userData, - const SchedulerTaskConfig* taskCfg) { - Schedule s = Schedule::onceUtc(whenUtc); - return addJob(s, mode, std::move(cb), userData, taskCfg); -} - -uint32_t ESPScheduler::addJobOnceUtc(const DateTime& whenUtc, - SchedulerJobMode mode, - SchedulerFunctionNoData cb, - const SchedulerTaskConfig* taskCfg) { - if (!cb) { - return 0; - } - SchedulerFunction wrapped = [fn = std::move(cb)](void*) { fn(); }; - return addJobOnceUtc(whenUtc, mode, std::move(wrapped), nullptr, taskCfg); -} - -uint32_t ESPScheduler::addJob(const Schedule& schedule, - SchedulerJobMode mode, - SchedulerCallback cb, - void* userData, - const SchedulerTaskConfig* taskCfg) { - return addJob(schedule, mode, SchedulerFunction(cb), userData, taskCfg); -} - -uint32_t ESPScheduler::addJob(const Schedule& schedule, - SchedulerJobMode mode, - SchedulerFunction cb, - void* userData, - const SchedulerTaskConfig* taskCfg) { - if (!cb) { - return 0; - } - if (!validateSchedule(schedule)) { - return 0; - } - ensureInitialized(); - const uint32_t id = nextId(); - - if (mode == SchedulerJobMode::Inline) { - InlineJob job{}; - job.id = id; - job.schedule = schedule; - job.callback = std::move(cb); - job.userData = userData; - m_inlineJobs.push_back(job); - return id; - } - - auto ctx = std::allocate_shared(SchedulerAllocator(usePSRAMBuffers_)); - ctx->schedule = schedule; - ctx->callback = std::move(cb); - ctx->userData = userData; - ctx->date = &m_date; - ctx->minValidEpochSeconds = m_minValidEpochSecondsRef; - - const SchedulerTaskConfig runtimeCfg = makeTaskConfig(taskCfg); - auto* taskCtx = new (std::nothrow) std::shared_ptr(ctx); - if (!taskCtx) { - return 0; - } - TaskHandle_t taskHandle = nullptr; - const BaseType_t created = xTaskCreatePinnedToCore( - &ESPScheduler::workerTaskEntry, - runtimeCfg.name ? runtimeCfg.name : "sched-job", - runtimeCfg.stackSize, - taskCtx, - runtimeCfg.priority, - &taskHandle, - runtimeCfg.coreId); - if (created != pdPASS || taskHandle == nullptr) { - delete taskCtx; - return 0; - } - - WorkerJob job{}; - job.id = id; - job.context = ctx; - job.task = taskHandle; - m_workerJobs.push_back(job); - return id; -} - -uint32_t ESPScheduler::addJob(const Schedule& schedule, - SchedulerJobMode mode, - SchedulerFunctionNoData cb, - const SchedulerTaskConfig* taskCfg) { - if (!cb) { - return 0; - } - SchedulerFunction wrapped = [fn = std::move(cb)](void*) { fn(); }; - return addJob(schedule, mode, std::move(wrapped), nullptr, taskCfg); + if (min < 0) { + min = 0; + } + if (max > 63) { + max = 63; + } + if (max >= 63) { + return ~static_cast(0); + } + const uint64_t upper = (1ULL << (max + 1)) - 1; + const uint64_t lower = (min == 0) ? 0 : ((1ULL << min) - 1); + return upper & ~lower; +} + +bool ESPScheduler::validateSchedule(const Schedule &schedule) const { + const ScheduleKind kind = resolvedScheduleKind(schedule); + switch (kind) { + case ScheduleKind::OneShotUtc: + return true; + case ScheduleKind::Cron: { + const bool minuteOk = fieldWithinRange(schedule.minute, 0, 59); + const bool hourOk = fieldWithinRange(schedule.hour, 0, 23); + const bool domOk = fieldWithinRange(schedule.dayOfMonth, 1, 31); + const bool monthOk = fieldWithinRange(schedule.month, 1, 12); + const bool dowOk = fieldWithinRange(schedule.dayOfWeek, 0, 6); + return minuteOk && hourOk && domOk && monthOk && dowOk; + } + case ScheduleKind::Sunrise: + case ScheduleKind::Sunset: + return schedule.sunOffsetMinutes >= kMinSunOffsetMinutes && + schedule.sunOffsetMinutes <= kMaxSunOffsetMinutes; + case ScheduleKind::MoonPhaseAngle: + return schedule.moonPhaseAngleDegrees >= kMinMoonPhaseAngle && + schedule.moonPhaseAngleDegrees <= kMaxMoonPhaseAngle && + schedule.moonPhaseToleranceDegrees >= 0 && + schedule.moonPhaseToleranceDegrees <= kMaxMoonPhaseTolerance; + case ScheduleKind::MoonIlluminationPercent: + return std::isfinite(schedule.moonIlluminationTargetPercent) && + std::isfinite(schedule.moonIlluminationTolerancePercent) && + schedule.moonIlluminationTargetPercent >= kMinIlluminationPercent && + schedule.moonIlluminationTargetPercent <= kMaxIlluminationPercent && + schedule.moonIlluminationTolerancePercent > 0.0 && + schedule.moonIlluminationTolerancePercent <= kMaxIlluminationTolerancePercent; + default: + return false; + } +} + +uint32_t ESPScheduler::addJobOnceUtc( + const DateTime &whenUtc, + SchedulerJobMode mode, + SchedulerCallback cb, + void *userData, + const SchedulerTaskConfig *taskCfg +) { + return addJobOnceUtc(whenUtc, mode, SchedulerFunction(cb), userData, taskCfg); +} + +uint32_t ESPScheduler::addJobOnceUtc( + const DateTime &whenUtc, + SchedulerJobMode mode, + SchedulerFunction cb, + void *userData, + const SchedulerTaskConfig *taskCfg +) { + Schedule s = Schedule::onceUtc(whenUtc); + return addJob(s, mode, std::move(cb), userData, taskCfg); +} + +uint32_t ESPScheduler::addJobOnceUtc( + const DateTime &whenUtc, + SchedulerJobMode mode, + SchedulerFunctionNoData cb, + const SchedulerTaskConfig *taskCfg +) { + if (!cb) { + return 0; + } + SchedulerFunction wrapped = [fn = std::move(cb)](void *) { fn(); }; + return addJobOnceUtc(whenUtc, mode, std::move(wrapped), nullptr, taskCfg); +} + +uint32_t ESPScheduler::addJob( + const Schedule &schedule, + SchedulerJobMode mode, + SchedulerCallback cb, + void *userData, + const SchedulerTaskConfig *taskCfg +) { + return addJob(schedule, mode, SchedulerFunction(cb), userData, taskCfg); +} + +uint32_t ESPScheduler::addJob( + const Schedule &schedule, + SchedulerJobMode mode, + SchedulerFunction cb, + void *userData, + const SchedulerTaskConfig *taskCfg +) { + if (!cb) { + return 0; + } + if (!validateSchedule(schedule)) { + return 0; + } + ensureInitialized(); + const uint32_t id = nextId(); + + if (mode == SchedulerJobMode::Inline) { + InlineJob job{}; + job.id = id; + job.schedule = schedule; + job.callback = std::move(cb); + job.userData = userData; + m_inlineJobs.push_back(job); + return id; + } + + auto ctx = std::allocate_shared( + SchedulerAllocator(usePSRAMBuffers_) + ); + ctx->schedule = schedule; + ctx->callback = std::move(cb); + ctx->userData = userData; + ctx->date = &m_date; + ctx->minValidEpochSeconds = m_minValidEpochSecondsRef; + + const SchedulerTaskConfig runtimeCfg = makeTaskConfig(taskCfg); + auto *taskCtx = new (std::nothrow) std::shared_ptr(ctx); + if (!taskCtx) { + return 0; + } + TaskHandle_t taskHandle = nullptr; + const BaseType_t created = xTaskCreatePinnedToCore( + &ESPScheduler::workerTaskEntry, + runtimeCfg.name ? runtimeCfg.name : "sched-job", + runtimeCfg.stackSize, + taskCtx, + runtimeCfg.priority, + &taskHandle, + runtimeCfg.coreId + ); + if (created != pdPASS || taskHandle == nullptr) { + delete taskCtx; + return 0; + } + + WorkerJob job{}; + job.id = id; + job.context = ctx; + job.task = taskHandle; + m_workerJobs.push_back(job); + return id; +} + +uint32_t ESPScheduler::addJob( + const Schedule &schedule, + SchedulerJobMode mode, + SchedulerFunctionNoData cb, + const SchedulerTaskConfig *taskCfg +) { + if (!cb) { + return 0; + } + SchedulerFunction wrapped = [fn = std::move(cb)](void *) { fn(); }; + return addJob(schedule, mode, std::move(wrapped), nullptr, taskCfg); } bool ESPScheduler::cancelJob(uint32_t jobId) { - if (!isInitialized()) { - return false; - } - - bool canceled = false; - for (auto& job : m_inlineJobs) { - if (job.id == jobId && !job.finished) { - job.finished = true; - canceled = true; - } - } - for (auto& job : m_workerJobs) { - if (job.id == jobId && job.context) { - job.context->cancelRequested.store(true); - canceled = true; - } - } - if (canceled) { - cleanupInline(); - cleanupWorkers(); - } - return canceled; + if (!isInitialized()) { + return false; + } + + bool canceled = false; + for (auto &job : m_inlineJobs) { + if (job.id == jobId && !job.finished) { + job.finished = true; + canceled = true; + } + } + for (auto &job : m_workerJobs) { + if (job.id == jobId && job.context) { + job.context->cancelRequested.store(true); + canceled = true; + } + } + if (canceled) { + cleanupInline(); + cleanupWorkers(); + } + return canceled; } bool ESPScheduler::pauseJob(uint32_t jobId) { - if (!isInitialized()) { - return false; - } - - for (auto& job : m_inlineJobs) { - if (job.id == jobId && !job.finished) { - job.paused = true; - return true; - } - } - for (auto& job : m_workerJobs) { - if (job.id == jobId && job.context) { - job.context->paused.store(true); - return true; - } - } - return false; + if (!isInitialized()) { + return false; + } + + for (auto &job : m_inlineJobs) { + if (job.id == jobId && !job.finished) { + job.paused = true; + return true; + } + } + for (auto &job : m_workerJobs) { + if (job.id == jobId && job.context) { + job.context->paused.store(true); + return true; + } + } + return false; } bool ESPScheduler::resumeJob(uint32_t jobId) { - if (!isInitialized()) { - return false; - } - - for (auto& job : m_inlineJobs) { - if (job.id == jobId && !job.finished) { - job.paused = false; - return true; - } - } - for (auto& job : m_workerJobs) { - if (job.id == jobId && job.context) { - job.context->paused.store(false); - return true; - } - } - return false; + if (!isInitialized()) { + return false; + } + + for (auto &job : m_inlineJobs) { + if (job.id == jobId && !job.finished) { + job.paused = false; + return true; + } + } + for (auto &job : m_workerJobs) { + if (job.id == jobId && job.context) { + job.context->paused.store(false); + return true; + } + } + return false; } void ESPScheduler::cancelAll() { - if (!isInitialized()) { - return; - } - - for (auto& job : m_inlineJobs) { - job.finished = true; - } - for (auto& job : m_workerJobs) { - if (job.context) { - job.context->cancelRequested.store(true); - } - } - cleanupInline(); - m_workerJobs.clear(); -} - -void ESPScheduler::tick() { tick(m_date.now()); } - -void ESPScheduler::tick(const DateTime& nowUtc) { - if (!isInitialized()) { - return; - } - - if (!clockValid(nowUtc)) { - return; - } - - for (auto& job : m_inlineJobs) { - if (job.finished || job.paused) { - continue; - } - if (!job.hasNext) { - if (isOneShotSchedule(job.schedule)) { - job.nextRunUtc = job.schedule.onceAtUtc; - job.hasNext = true; - } else { - job.hasNext = computeNextOccurrence(job.schedule, nowUtc, job.nextRunUtc); - if (!job.hasNext) { - job.finished = true; - continue; - } - } - } - - if (m_date.isAfter(job.nextRunUtc, nowUtc)) { - continue; - } - job.callback(job.userData); - if (isOneShotSchedule(job.schedule)) { - job.finished = true; - continue; - } - DateTime from = m_date.addMinutes(job.nextRunUtc, 1); - job.hasNext = computeNextOccurrence(job.schedule, from, job.nextRunUtc); - if (!job.hasNext) { - job.finished = true; - } - } - - cleanupInline(); - cleanupWorkers(); + if (!isInitialized()) { + return; + } + + for (auto &job : m_inlineJobs) { + job.finished = true; + } + for (auto &job : m_workerJobs) { + if (job.context) { + job.context->cancelRequested.store(true); + } + } + cleanupInline(); + m_workerJobs.clear(); +} + +void ESPScheduler::tick() { + tick(m_date.now()); +} + +void ESPScheduler::tick(const DateTime &nowUtc) { + if (!isInitialized()) { + return; + } + + if (!clockValid(nowUtc)) { + return; + } + + for (auto &job : m_inlineJobs) { + if (job.finished || job.paused) { + continue; + } + if (!job.hasNext) { + if (isOneShotSchedule(job.schedule)) { + job.nextRunUtc = job.schedule.onceAtUtc; + job.hasNext = true; + } else { + job.hasNext = computeNextOccurrence(job.schedule, nowUtc, job.nextRunUtc); + if (!job.hasNext) { + job.finished = true; + continue; + } + } + } + + if (m_date.isAfter(job.nextRunUtc, nowUtc)) { + continue; + } + job.callback(job.userData); + if (isOneShotSchedule(job.schedule)) { + job.finished = true; + continue; + } + DateTime from = m_date.addMinutes(job.nextRunUtc, 1); + job.hasNext = computeNextOccurrence(job.schedule, from, job.nextRunUtc); + if (!job.hasNext) { + job.finished = true; + } + } + + cleanupInline(); + cleanupWorkers(); } void ESPScheduler::cleanup() { - if (!isInitialized()) { - return; - } - - cleanupInline(); - cleanupWorkers(); -} - -bool ESPScheduler::getJobInfo(size_t index, JobInfo& out) const { - if (!isInitialized()) { - out = JobInfo{}; - return false; - } - - out = JobInfo{}; - size_t current = 0; - auto fillNext = [this](const Schedule& schedule, bool hasNext, const DateTime& storedNext, DateTime& outNext) { - if (hasNext) { - outNext = storedNext; - return; - } - if (isOneShotSchedule(schedule)) { - outNext = schedule.onceAtUtc; - return; - } - DateTime computed{}; - if (computeNextOccurrence(schedule, m_date.now(), computed)) { - outNext = computed; - } else { - outNext = {}; - } - }; - - for (const auto& job : m_inlineJobs) { - if (job.finished) { - continue; - } - if (current == index) { - out.id = job.id; - out.enabled = !job.paused; - out.mode = SchedulerJobMode::Inline; - out.schedule = job.schedule; - fillNext(job.schedule, job.hasNext, job.nextRunUtc, out.nextRunUtc); - return true; - } - ++current; - } - - for (const auto& job : m_workerJobs) { - if (!job.context) { - continue; - } - if (job.context->cancelRequested.load() || job.context->finished.load()) { - continue; - } - if (current == index) { - out.id = job.id; - out.enabled = !job.context->paused.load(); - out.mode = SchedulerJobMode::WorkerTask; - out.schedule = job.context->schedule; - fillNext(job.context->schedule, job.context->hasNext, job.context->nextRunUtc, out.nextRunUtc); - return true; - } - ++current; - } - - return false; -} - -bool ESPScheduler::computeNextOccurrence(const Schedule& schedule, - const DateTime& fromUtc, - DateTime& outNextUtc) const { - return computeNextOccurrenceForDate(m_date, schedule, fromUtc, outNextUtc); -} - -void ESPScheduler::runWorkerJob(const std::shared_ptr& ctx) { - if (!ctx || !ctx->date) { - return; - } - - ESPDate& date = *ctx->date; - while (!ctx->cancelRequested.load()) { - DateTime now = date.now(); - const int64_t minValidEpochSeconds = - ctx->minValidEpochSeconds ? ctx->minValidEpochSeconds->load() : kDefaultMinValidEpochSeconds; - if (!clockValidForMin(now, minValidEpochSeconds)) { - vTaskDelay(pdMS_TO_TICKS(kWorkerSleepChunkSeconds * 1000)); - continue; - } - if (!ctx->hasNext) { - if (isOneShotSchedule(ctx->schedule)) { - ctx->nextRunUtc = ctx->schedule.onceAtUtc; - ctx->hasNext = true; - } else { - ctx->hasNext = computeNextOccurrenceForDate(date, ctx->schedule, now, ctx->nextRunUtc); - if (!ctx->hasNext) { - break; - } - } - } - - if (ctx->paused.load()) { - vTaskDelay(pdMS_TO_TICKS(kWorkerSleepChunkSeconds * 1000)); - continue; - } - - const int64_t diffSec = date.differenceInSeconds(ctx->nextRunUtc, now); - if (diffSec > 0) { - const int64_t chunk = (diffSec > kWorkerSleepChunkSeconds) ? kWorkerSleepChunkSeconds : diffSec; - vTaskDelay(pdMS_TO_TICKS(static_cast(chunk * 1000))); - continue; - } - - ctx->callback(ctx->userData); - - if (isOneShotSchedule(ctx->schedule)) { - break; - } - DateTime from = date.addMinutes(ctx->nextRunUtc, 1); - ctx->hasNext = computeNextOccurrenceForDate(date, ctx->schedule, from, ctx->nextRunUtc); - if (!ctx->hasNext) { - break; - } - } - ctx->finished.store(true); -} - -bool ESPScheduler::clockValid(const DateTime& nowUtc) const { - return clockValidForMin(nowUtc, m_minValidEpochSeconds); -} - -SchedulerTaskConfig ESPScheduler::makeTaskConfig(const SchedulerTaskConfig* taskCfg) const { - SchedulerTaskConfig cfg{}; - cfg.stackSize = taskCfg ? taskCfg->stackSize : SchedulerTaskConfig{}.stackSize; - cfg.priority = taskCfg ? taskCfg->priority : SchedulerTaskConfig{}.priority; - cfg.coreId = taskCfg ? taskCfg->coreId : SchedulerTaskConfig{}.coreId; - cfg.usePsramStack = taskCfg ? taskCfg->usePsramStack : SchedulerTaskConfig{}.usePsramStack; - cfg.name = taskCfg && taskCfg->name ? taskCfg->name : "sched-job"; - return cfg; -} - -void ESPScheduler::workerTaskEntry(void* arg) { - auto* ctxPtr = static_cast*>(arg); - if (!ctxPtr) { - vTaskDelete(nullptr); - return; - } - std::shared_ptr ctx = *ctxPtr; - delete ctxPtr; - runWorkerJob(ctx); - vTaskDelete(nullptr); + if (!isInitialized()) { + return; + } + + cleanupInline(); + cleanupWorkers(); +} + +bool ESPScheduler::getJobInfo(size_t index, JobInfo &out) const { + if (!isInitialized()) { + out = JobInfo{}; + return false; + } + + out = JobInfo{}; + size_t current = 0; + auto fillNext = [this]( + const Schedule &schedule, + bool hasNext, + const DateTime &storedNext, + DateTime &outNext + ) { + if (hasNext) { + outNext = storedNext; + return; + } + if (isOneShotSchedule(schedule)) { + outNext = schedule.onceAtUtc; + return; + } + DateTime computed{}; + if (computeNextOccurrence(schedule, m_date.now(), computed)) { + outNext = computed; + } else { + outNext = {}; + } + }; + + for (const auto &job : m_inlineJobs) { + if (job.finished) { + continue; + } + if (current == index) { + out.id = job.id; + out.enabled = !job.paused; + out.mode = SchedulerJobMode::Inline; + out.schedule = job.schedule; + fillNext(job.schedule, job.hasNext, job.nextRunUtc, out.nextRunUtc); + return true; + } + ++current; + } + + for (const auto &job : m_workerJobs) { + if (!job.context) { + continue; + } + if (job.context->cancelRequested.load() || job.context->finished.load()) { + continue; + } + if (current == index) { + out.id = job.id; + out.enabled = !job.context->paused.load(); + out.mode = SchedulerJobMode::WorkerTask; + out.schedule = job.context->schedule; + fillNext( + job.context->schedule, + job.context->hasNext, + job.context->nextRunUtc, + out.nextRunUtc + ); + return true; + } + ++current; + } + + return false; +} + +bool ESPScheduler::computeNextOccurrence( + const Schedule &schedule, const DateTime &fromUtc, DateTime &outNextUtc +) const { + return computeNextOccurrenceForDate(m_date, schedule, fromUtc, outNextUtc); +} + +void ESPScheduler::runWorkerJob(const std::shared_ptr &ctx) { + if (!ctx || !ctx->date) { + return; + } + + ESPDate &date = *ctx->date; + while (!ctx->cancelRequested.load()) { + DateTime now = date.now(); + const int64_t minValidEpochSeconds = ctx->minValidEpochSeconds + ? ctx->minValidEpochSeconds->load() + : kDefaultMinValidEpochSeconds; + if (!clockValidForMin(now, minValidEpochSeconds)) { + vTaskDelay(pdMS_TO_TICKS(kWorkerSleepChunkSeconds * 1000)); + continue; + } + if (!ctx->hasNext) { + if (isOneShotSchedule(ctx->schedule)) { + ctx->nextRunUtc = ctx->schedule.onceAtUtc; + ctx->hasNext = true; + } else { + ctx->hasNext = + computeNextOccurrenceForDate(date, ctx->schedule, now, ctx->nextRunUtc); + if (!ctx->hasNext) { + break; + } + } + } + + if (ctx->paused.load()) { + vTaskDelay(pdMS_TO_TICKS(kWorkerSleepChunkSeconds * 1000)); + continue; + } + + const int64_t diffSec = date.differenceInSeconds(ctx->nextRunUtc, now); + if (diffSec > 0) { + const int64_t chunk = + (diffSec > kWorkerSleepChunkSeconds) ? kWorkerSleepChunkSeconds : diffSec; + vTaskDelay(pdMS_TO_TICKS(static_cast(chunk * 1000))); + continue; + } + + ctx->callback(ctx->userData); + + if (isOneShotSchedule(ctx->schedule)) { + break; + } + DateTime from = date.addMinutes(ctx->nextRunUtc, 1); + ctx->hasNext = computeNextOccurrenceForDate(date, ctx->schedule, from, ctx->nextRunUtc); + if (!ctx->hasNext) { + break; + } + } + ctx->finished.store(true); +} + +bool ESPScheduler::clockValid(const DateTime &nowUtc) const { + return clockValidForMin(nowUtc, m_minValidEpochSeconds); +} + +SchedulerTaskConfig ESPScheduler::makeTaskConfig(const SchedulerTaskConfig *taskCfg) const { + SchedulerTaskConfig cfg{}; + cfg.stackSize = taskCfg ? taskCfg->stackSize : SchedulerTaskConfig{}.stackSize; + cfg.priority = taskCfg ? taskCfg->priority : SchedulerTaskConfig{}.priority; + cfg.coreId = taskCfg ? taskCfg->coreId : SchedulerTaskConfig{}.coreId; + cfg.usePsramStack = taskCfg ? taskCfg->usePsramStack : SchedulerTaskConfig{}.usePsramStack; + cfg.name = taskCfg && taskCfg->name ? taskCfg->name : "sched-job"; + return cfg; +} + +void ESPScheduler::workerTaskEntry(void *arg) { + auto *ctxPtr = static_cast *>(arg); + if (!ctxPtr) { + vTaskDelete(nullptr); + return; + } + std::shared_ptr ctx = *ctxPtr; + delete ctxPtr; + runWorkerJob(ctx); + vTaskDelete(nullptr); } void ESPScheduler::cleanupInline() { - m_inlineJobs.erase(std::remove_if(m_inlineJobs.begin(), - m_inlineJobs.end(), - [](const InlineJob& job) { return job.finished; }), - m_inlineJobs.end()); + m_inlineJobs.erase( + std::remove_if( + m_inlineJobs.begin(), + m_inlineJobs.end(), + [](const InlineJob &job) { return job.finished; } + ), + m_inlineJobs.end() + ); } void ESPScheduler::cleanupWorkers() { - m_workerJobs.erase(std::remove_if(m_workerJobs.begin(), - m_workerJobs.end(), - [](const WorkerJob& job) { - return !job.context || job.context->finished.load() || - job.context->cancelRequested.load(); - }), - m_workerJobs.end()); + m_workerJobs.erase( + std::remove_if( + m_workerJobs.begin(), + m_workerJobs.end(), + [](const WorkerJob &job) { + return !job.context || job.context->finished.load() || + job.context->cancelRequested.load(); + } + ), + m_workerJobs.end() + ); } diff --git a/src/esp_scheduler/scheduler.h b/src/esp_scheduler/scheduler.h index a4117a7..b62e658 100644 --- a/src/esp_scheduler/scheduler.h +++ b/src/esp_scheduler/scheduler.h @@ -16,222 +16,239 @@ extern "C" { class ESPWorker; -enum class SchedulerJobMode : uint8_t { - Inline, - WorkerTask -}; +enum class SchedulerJobMode : uint8_t { Inline, WorkerTask }; enum class ScheduleKind : uint8_t { - Cron, - OneShotUtc, - Sunrise, - Sunset, - MoonPhaseAngle, - MoonIlluminationPercent + Cron, + OneShotUtc, + Sunrise, + Sunset, + MoonPhaseAngle, + MoonIlluminationPercent }; enum class MoonPhaseName : uint8_t { - NewMoon, - WaxingCrescent, - FirstQuarter, - WaxingGibbous, - FullMoon, - WaningGibbous, - LastQuarter, - WaningCrescent + NewMoon, + WaxingCrescent, + FirstQuarter, + WaxingGibbous, + FullMoon, + WaningGibbous, + LastQuarter, + WaningCrescent }; struct SchedulerTaskConfig { - const char* name = "sched-job"; - uint32_t stackSize = 4096; // bytes - UBaseType_t priority = 1; - BaseType_t coreId = tskNO_AFFINITY; - bool usePsramStack = false; + const char *name = "sched-job"; + uint32_t stackSize = 4096; // bytes + UBaseType_t priority = 1; + BaseType_t coreId = tskNO_AFFINITY; + bool usePsramStack = false; }; struct ESPSchedulerConfig { - // Prefer PSRAM-backed buffers for scheduler-owned dynamic containers. - // Falls back to default heap automatically when unavailable. - bool usePSRAMBuffers = false; + // Prefer PSRAM-backed buffers for scheduler-owned dynamic containers. + // Falls back to default heap automatically when unavailable. + bool usePSRAMBuffers = false; }; -using SchedulerCallback = void (*)(void* userData); -using SchedulerFunction = std::function; +using SchedulerCallback = void (*)(void *userData); +using SchedulerFunction = std::function; using SchedulerFunctionNoData = std::function; class ScheduleField { -public: - static ScheduleField any(); - static ScheduleField only(int value); - static ScheduleField range(int from, int to); - static ScheduleField every(int step); - static ScheduleField rangeEvery(int from, int to, int step); - // If any value is out of range, the field is cleared and will fail validation. - static ScheduleField list(const int* values, size_t count); - - bool matches(int value) const; - bool isAny() const { return m_isAny; } - bool empty() const { return !m_isAny && m_mask == 0; } - uint64_t rawMask() const { return m_mask; } - -private: - uint64_t m_mask = 0; - bool m_isAny = false; + public: + static ScheduleField any(); + static ScheduleField only(int value); + static ScheduleField range(int from, int to); + static ScheduleField every(int step); + static ScheduleField rangeEvery(int from, int to, int step); + // If any value is out of range, the field is cleared and will fail validation. + static ScheduleField list(const int *values, size_t count); + + bool matches(int value) const; + bool isAny() const { + return m_isAny; + } + bool empty() const { + return !m_isAny && m_mask == 0; + } + uint64_t rawMask() const { + return m_mask; + } + + private: + uint64_t m_mask = 0; + bool m_isAny = false; }; struct Schedule { - ScheduleKind kind = ScheduleKind::Cron; - bool isOneShot = false; - DateTime onceAtUtc{}; - - ScheduleField minute = ScheduleField::any(); - ScheduleField hour = ScheduleField::any(); - ScheduleField dayOfMonth = ScheduleField::any(); - ScheduleField month = ScheduleField::any(); - ScheduleField dayOfWeek = ScheduleField::any(); - - int sunOffsetMinutes = 0; - int moonPhaseAngleDegrees = 0; - int moonPhaseToleranceDegrees = 1; - double moonIlluminationTargetPercent = 0.0; - double moonIlluminationTolerancePercent = 0.5; - - static Schedule onceUtc(const DateTime& whenUtc); - static Schedule dailyAtLocal(int hour, int minute); - // dowMask bits: 0=Sun..6=Sat; empty mask falls back to any day of week. - static Schedule weeklyAtLocal(uint8_t dowMask, int hour, int minute); - static Schedule monthlyOnDayLocal(int dayOfMonth, int hour, int minute); - static Schedule sunrise(int offsetMinutes = 0); - static Schedule sunset(int offsetMinutes = 0); - static Schedule moonPhaseAngle(int angleDegrees, int toleranceDegrees = 1); - static Schedule moonPhase(MoonPhaseName name, int toleranceDegrees = 1); - static Schedule moonIlluminationPercent(double percent, double tolerancePercent = 0.5); - static Schedule custom(const ScheduleField& minute, - const ScheduleField& hour, - const ScheduleField& dom, - const ScheduleField& month, - const ScheduleField& dow); + ScheduleKind kind = ScheduleKind::Cron; + bool isOneShot = false; + DateTime onceAtUtc{}; + + ScheduleField minute = ScheduleField::any(); + ScheduleField hour = ScheduleField::any(); + ScheduleField dayOfMonth = ScheduleField::any(); + ScheduleField month = ScheduleField::any(); + ScheduleField dayOfWeek = ScheduleField::any(); + + int sunOffsetMinutes = 0; + int moonPhaseAngleDegrees = 0; + int moonPhaseToleranceDegrees = 1; + double moonIlluminationTargetPercent = 0.0; + double moonIlluminationTolerancePercent = 0.5; + + static Schedule onceUtc(const DateTime &whenUtc); + static Schedule dailyAtLocal(int hour, int minute); + // dowMask bits: 0=Sun..6=Sat; empty mask falls back to any day of week. + static Schedule weeklyAtLocal(uint8_t dowMask, int hour, int minute); + static Schedule monthlyOnDayLocal(int dayOfMonth, int hour, int minute); + static Schedule sunrise(int offsetMinutes = 0); + static Schedule sunset(int offsetMinutes = 0); + static Schedule moonPhaseAngle(int angleDegrees, int toleranceDegrees = 1); + static Schedule moonPhase(MoonPhaseName name, int toleranceDegrees = 1); + static Schedule moonIlluminationPercent(double percent, double tolerancePercent = 0.5); + static Schedule custom( + const ScheduleField &minute, + const ScheduleField &hour, + const ScheduleField &dom, + const ScheduleField &month, + const ScheduleField &dow + ); }; struct JobInfo { - uint32_t id = 0; - bool enabled = false; - SchedulerJobMode mode = SchedulerJobMode::Inline; - Schedule schedule{}; - DateTime nextRunUtc{}; + uint32_t id = 0; + bool enabled = false; + SchedulerJobMode mode = SchedulerJobMode::Inline; + Schedule schedule{}; + DateTime nextRunUtc{}; }; class ESPScheduler { -public: - // Default guard: block scheduling until at least 2020-01-01T00:00:00Z. - static constexpr int64_t kDefaultMinValidEpochSeconds = 1577836800; - - ESPScheduler(ESPDate& date, ESPWorker* worker = nullptr); - ESPScheduler(ESPDate& date, const ESPSchedulerConfig& config); - ESPScheduler(ESPDate& date, ESPWorker* worker, const ESPSchedulerConfig& config); - ~ESPScheduler(); - void deinit(); - bool isInitialized() const; - - // Configure / inspect the minimum valid wall-clock time; scheduler idles until time >= min. - void setMinValidUnixSeconds(int64_t minEpochSeconds); - void setMinValidUtc(const DateTime& minUtc); - int64_t minValidUnixSeconds() const; - - uint32_t addJobOnceUtc(const DateTime& whenUtc, - SchedulerJobMode mode, - SchedulerCallback cb, - void* userData = nullptr, - const SchedulerTaskConfig* taskCfg = nullptr); - uint32_t addJobOnceUtc(const DateTime& whenUtc, - SchedulerJobMode mode, - SchedulerFunction cb, - void* userData = nullptr, - const SchedulerTaskConfig* taskCfg = nullptr); - uint32_t addJobOnceUtc(const DateTime& whenUtc, - SchedulerJobMode mode, - SchedulerFunctionNoData cb, - const SchedulerTaskConfig* taskCfg = nullptr); - - uint32_t addJob(const Schedule& schedule, - SchedulerJobMode mode, - SchedulerCallback cb, - void* userData = nullptr, - const SchedulerTaskConfig* taskCfg = nullptr); - uint32_t addJob(const Schedule& schedule, - SchedulerJobMode mode, - SchedulerFunction cb, - void* userData = nullptr, - const SchedulerTaskConfig* taskCfg = nullptr); - uint32_t addJob(const Schedule& schedule, - SchedulerJobMode mode, - SchedulerFunctionNoData cb, - const SchedulerTaskConfig* taskCfg = nullptr); - - bool cancelJob(uint32_t jobId); - bool pauseJob(uint32_t jobId); - bool resumeJob(uint32_t jobId); - void cancelAll(); - - void tick(const DateTime& nowUtc); - void tick(); - void cleanup(); - - bool computeNextOccurrence(const Schedule& schedule, - const DateTime& fromUtc, - DateTime& outNextUtc) const; - - bool getJobInfo(size_t index, JobInfo& out) const; - -private: - struct InlineJob { - uint32_t id = 0; - Schedule schedule{}; - SchedulerFunction callback{}; - void* userData = nullptr; - DateTime nextRunUtc{}; - bool hasNext = false; - bool paused = false; - bool finished = false; - }; - - struct WorkerJobContext { - Schedule schedule{}; - SchedulerFunction callback{}; - void* userData = nullptr; - ESPDate* date = nullptr; - std::shared_ptr> minValidEpochSeconds{}; - std::atomic paused{false}; - std::atomic cancelRequested{false}; - std::atomic finished{false}; - DateTime nextRunUtc{}; - bool hasNext = false; - }; - - struct WorkerJob { - uint32_t id = 0; - std::shared_ptr context{}; - TaskHandle_t task = nullptr; - }; - - uint32_t nextId(); - bool validateSchedule(const Schedule& schedule) const; - bool fieldWithinRange(const ScheduleField& field, int min, int max) const; - uint64_t allowedMask(int min, int max) const; - static void runWorkerJob(const std::shared_ptr& ctx); - SchedulerTaskConfig makeTaskConfig(const SchedulerTaskConfig* taskCfg) const; - static void workerTaskEntry(void* arg); - void cleanupInline(); - void cleanupWorkers(); - bool clockValid(const DateTime& nowUtc) const; - void ensureInitialized(); - - ESPDate& m_date; - uint32_t m_nextId = 1; - int64_t m_minValidEpochSeconds = kDefaultMinValidEpochSeconds; - std::shared_ptr> m_minValidEpochSecondsRef; - std::atomic m_initialized{true}; - bool usePSRAMBuffers_ = false; - SchedulerVector m_inlineJobs; - SchedulerVector m_workerJobs; + public: + // Default guard: block scheduling until at least 2020-01-01T00:00:00Z. + static constexpr int64_t kDefaultMinValidEpochSeconds = 1577836800; + + ESPScheduler(ESPDate &date, ESPWorker *worker = nullptr); + ESPScheduler(ESPDate &date, const ESPSchedulerConfig &config); + ESPScheduler(ESPDate &date, ESPWorker *worker, const ESPSchedulerConfig &config); + ~ESPScheduler(); + void deinit(); + bool isInitialized() const; + + // Configure / inspect the minimum valid wall-clock time; scheduler idles until time >= min. + void setMinValidUnixSeconds(int64_t minEpochSeconds); + void setMinValidUtc(const DateTime &minUtc); + int64_t minValidUnixSeconds() const; + + uint32_t addJobOnceUtc( + const DateTime &whenUtc, + SchedulerJobMode mode, + SchedulerCallback cb, + void *userData = nullptr, + const SchedulerTaskConfig *taskCfg = nullptr + ); + uint32_t addJobOnceUtc( + const DateTime &whenUtc, + SchedulerJobMode mode, + SchedulerFunction cb, + void *userData = nullptr, + const SchedulerTaskConfig *taskCfg = nullptr + ); + uint32_t addJobOnceUtc( + const DateTime &whenUtc, + SchedulerJobMode mode, + SchedulerFunctionNoData cb, + const SchedulerTaskConfig *taskCfg = nullptr + ); + + uint32_t addJob( + const Schedule &schedule, + SchedulerJobMode mode, + SchedulerCallback cb, + void *userData = nullptr, + const SchedulerTaskConfig *taskCfg = nullptr + ); + uint32_t addJob( + const Schedule &schedule, + SchedulerJobMode mode, + SchedulerFunction cb, + void *userData = nullptr, + const SchedulerTaskConfig *taskCfg = nullptr + ); + uint32_t addJob( + const Schedule &schedule, + SchedulerJobMode mode, + SchedulerFunctionNoData cb, + const SchedulerTaskConfig *taskCfg = nullptr + ); + + bool cancelJob(uint32_t jobId); + bool pauseJob(uint32_t jobId); + bool resumeJob(uint32_t jobId); + void cancelAll(); + + void tick(const DateTime &nowUtc); + void tick(); + void cleanup(); + + bool computeNextOccurrence( + const Schedule &schedule, const DateTime &fromUtc, DateTime &outNextUtc + ) const; + + bool getJobInfo(size_t index, JobInfo &out) const; + + private: + struct InlineJob { + uint32_t id = 0; + Schedule schedule{}; + SchedulerFunction callback{}; + void *userData = nullptr; + DateTime nextRunUtc{}; + bool hasNext = false; + bool paused = false; + bool finished = false; + }; + + struct WorkerJobContext { + Schedule schedule{}; + SchedulerFunction callback{}; + void *userData = nullptr; + ESPDate *date = nullptr; + std::shared_ptr> minValidEpochSeconds{}; + std::atomic paused{false}; + std::atomic cancelRequested{false}; + std::atomic finished{false}; + DateTime nextRunUtc{}; + bool hasNext = false; + }; + + struct WorkerJob { + uint32_t id = 0; + std::shared_ptr context{}; + TaskHandle_t task = nullptr; + }; + + uint32_t nextId(); + bool validateSchedule(const Schedule &schedule) const; + bool fieldWithinRange(const ScheduleField &field, int min, int max) const; + uint64_t allowedMask(int min, int max) const; + static void runWorkerJob(const std::shared_ptr &ctx); + SchedulerTaskConfig makeTaskConfig(const SchedulerTaskConfig *taskCfg) const; + static void workerTaskEntry(void *arg); + void cleanupInline(); + void cleanupWorkers(); + bool clockValid(const DateTime &nowUtc) const; + void ensureInitialized(); + + ESPDate &m_date; + uint32_t m_nextId = 1; + int64_t m_minValidEpochSeconds = kDefaultMinValidEpochSeconds; + std::shared_ptr> m_minValidEpochSecondsRef; + std::atomic m_initialized{true}; + bool usePSRAMBuffers_ = false; + SchedulerVector m_inlineJobs; + SchedulerVector m_workerJobs; }; diff --git a/src/esp_scheduler/scheduler_allocator.h b/src/esp_scheduler/scheduler_allocator.h index 5435b3c..dffd3ea 100644 --- a/src/esp_scheduler/scheduler_allocator.h +++ b/src/esp_scheduler/scheduler_allocator.h @@ -17,83 +17,80 @@ #include namespace scheduler_allocator_detail { -inline void* allocate(std::size_t bytes, bool usePSRAMBuffers) noexcept { +inline void *allocate(std::size_t bytes, bool usePSRAMBuffers) noexcept { #if ESP_SCHEDULER_HAS_BUFFER_MANAGER - return ESPBufferManager::allocate(bytes, usePSRAMBuffers); + return ESPBufferManager::allocate(bytes, usePSRAMBuffers); #else - (void)usePSRAMBuffers; - return std::malloc(bytes); + (void)usePSRAMBuffers; + return std::malloc(bytes); #endif } -inline void deallocate(void* ptr) noexcept { +inline void deallocate(void *ptr) noexcept { #if ESP_SCHEDULER_HAS_BUFFER_MANAGER - ESPBufferManager::deallocate(ptr); + ESPBufferManager::deallocate(ptr); #else - std::free(ptr); + std::free(ptr); #endif } -} // namespace scheduler_allocator_detail - -template -class SchedulerAllocator { -public: - using value_type = T; - - SchedulerAllocator() noexcept = default; - explicit SchedulerAllocator(bool usePSRAMBuffers) noexcept : usePSRAMBuffers_(usePSRAMBuffers) {} - - template - SchedulerAllocator(const SchedulerAllocator& other) noexcept - : usePSRAMBuffers_(other.usePSRAMBuffers()) {} - - T* allocate(std::size_t n) { - if (n == 0) { - return nullptr; - } - if (n > (std::numeric_limits::max() / sizeof(T))) { +} // namespace scheduler_allocator_detail + +template class SchedulerAllocator { + public: + using value_type = T; + + SchedulerAllocator() noexcept = default; + explicit SchedulerAllocator(bool usePSRAMBuffers) noexcept : usePSRAMBuffers_(usePSRAMBuffers) { + } + + template + SchedulerAllocator(const SchedulerAllocator &other) noexcept + : usePSRAMBuffers_(other.usePSRAMBuffers()) { + } + + T *allocate(std::size_t n) { + if (n == 0) { + return nullptr; + } + if (n > (std::numeric_limits::max() / sizeof(T))) { #if defined(__cpp_exceptions) - throw std::bad_alloc(); + throw std::bad_alloc(); #else - std::abort(); + std::abort(); #endif - } + } - void* memory = scheduler_allocator_detail::allocate(n * sizeof(T), usePSRAMBuffers_); - if (!memory) { + void *memory = scheduler_allocator_detail::allocate(n * sizeof(T), usePSRAMBuffers_); + if (!memory) { #if defined(__cpp_exceptions) - throw std::bad_alloc(); + throw std::bad_alloc(); #else - std::abort(); + std::abort(); #endif - } - return static_cast(memory); - } + } + return static_cast(memory); + } - void deallocate(T* ptr, std::size_t) noexcept { - scheduler_allocator_detail::deallocate(ptr); - } + void deallocate(T *ptr, std::size_t) noexcept { + scheduler_allocator_detail::deallocate(ptr); + } - bool usePSRAMBuffers() const noexcept { - return usePSRAMBuffers_; - } + bool usePSRAMBuffers() const noexcept { + return usePSRAMBuffers_; + } - template - bool operator==(const SchedulerAllocator& other) const noexcept { - return usePSRAMBuffers_ == other.usePSRAMBuffers(); - } + template bool operator==(const SchedulerAllocator &other) const noexcept { + return usePSRAMBuffers_ == other.usePSRAMBuffers(); + } - template - bool operator!=(const SchedulerAllocator& other) const noexcept { - return !(*this == other); - } + template bool operator!=(const SchedulerAllocator &other) const noexcept { + return !(*this == other); + } -private: - template - friend class SchedulerAllocator; + private: + template friend class SchedulerAllocator; - bool usePSRAMBuffers_ = false; + bool usePSRAMBuffers_ = false; }; -template -using SchedulerVector = std::vector>; +template using SchedulerVector = std::vector>; diff --git a/test/test_esp_scheduler/test_esp_scheduler.cpp b/test/test_esp_scheduler/test_esp_scheduler.cpp index e853f99..17761ec 100644 --- a/test/test_esp_scheduler/test_esp_scheduler.cpp +++ b/test/test_esp_scheduler/test_esp_scheduler.cpp @@ -1,311 +1,383 @@ #include #include #include -#include #include +#include ESPDate date; ESPScheduler scheduler(date); static int inlineHits = 0; -static void inlineCallback(void* userData) { - (void)userData; - inlineHits++; +static void inlineCallback(void *userData) { + (void)userData; + inlineHits++; } static double circularDistanceDegrees(double a, double b) { - double delta = std::fmod(std::fabs(a - b), 360.0); - if (delta > 180.0) { - delta = 360.0 - delta; - } - return delta; + double delta = std::fmod(std::fabs(a - b), 360.0); + if (delta > 180.0) { + delta = 360.0 - delta; + } + return delta; } static void test_daily_at_local_next_same_day() { - Schedule s = Schedule::dailyAtLocal(9, 30); - DateTime from = date.fromUtc(2025, 1, 1, 8, 15, 10); - DateTime expected = date.fromUtc(2025, 1, 1, 9, 30, 0); - DateTime next{}; - TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(s, from, next)); - TEST_ASSERT_TRUE(date.isEqual(expected, next)); + Schedule s = Schedule::dailyAtLocal(9, 30); + DateTime from = date.fromUtc(2025, 1, 1, 8, 15, 10); + DateTime expected = date.fromUtc(2025, 1, 1, 9, 30, 0); + DateTime next{}; + TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(s, from, next)); + TEST_ASSERT_TRUE(date.isEqual(expected, next)); } static void test_daily_at_local_rolls_to_next_day() { - Schedule s = Schedule::dailyAtLocal(6, 0); - DateTime from = date.fromUtc(2025, 1, 1, 7, 0, 1); // already past the slot - DateTime next{}; - TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(s, from, next)); - TEST_ASSERT_TRUE(date.isEqual(next, date.fromUtc(2025, 1, 2, 6, 0, 0))); + Schedule s = Schedule::dailyAtLocal(6, 0); + DateTime from = date.fromUtc(2025, 1, 1, 7, 0, 1); // already past the slot + DateTime next{}; + TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(s, from, next)); + TEST_ASSERT_TRUE(date.isEqual(next, date.fromUtc(2025, 1, 2, 6, 0, 0))); } static void test_weekly_mask_advances_to_next_weekday() { - uint8_t weekdaysMask = 0b0111110; // Mon..Fri - Schedule s = Schedule::weeklyAtLocal(weekdaysMask, 18, 30); - DateTime from = date.fromUtc(2025, 3, 4, 19, 0, 0); // Tuesday 19:00 UTC - DateTime next{}; - TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(s, from, next)); - TEST_ASSERT_TRUE(date.isEqual(next, date.fromUtc(2025, 3, 5, 18, 30, 0))); // Wednesday 18:30 + uint8_t weekdaysMask = 0b0111110; // Mon..Fri + Schedule s = Schedule::weeklyAtLocal(weekdaysMask, 18, 30); + DateTime from = date.fromUtc(2025, 3, 4, 19, 0, 0); // Tuesday 19:00 UTC + DateTime next{}; + TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(s, from, next)); + TEST_ASSERT_TRUE(date.isEqual(next, date.fromUtc(2025, 3, 5, 18, 30, 0))); // Wednesday 18:30 } static void test_weekly_zero_mask_defaults_to_any_day() { - Schedule s = Schedule::weeklyAtLocal(0, 10, 45); - DateTime from = date.fromUtc(2025, 3, 1, 10, 0, 0); - DateTime next{}; - TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(s, from, next)); - TEST_ASSERT_TRUE(date.isEqual(next, date.fromUtc(2025, 3, 1, 10, 45, 0))); + Schedule s = Schedule::weeklyAtLocal(0, 10, 45); + DateTime from = date.fromUtc(2025, 3, 1, 10, 0, 0); + DateTime next{}; + TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(s, from, next)); + TEST_ASSERT_TRUE(date.isEqual(next, date.fromUtc(2025, 3, 1, 10, 45, 0))); } static void test_dom_dow_or_logic_matches_either() { - ScheduleField dom = ScheduleField::only(10); - ScheduleField dow = ScheduleField::only(1); // Monday = 1 with ESPDate (0=Sun) - Schedule s = Schedule::custom(ScheduleField::only(0), ScheduleField::only(9), dom, ScheduleField::any(), dow); - DateTime from = date.fromUtc(2024, 7, 1, 8, 0, 0); // Monday, day 1 - DateTime next{}; - TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(s, from, next)); - TEST_ASSERT_TRUE(date.isEqual(next, date.fromUtc(2024, 7, 1, 9, 0, 0))); // passes via DOW even though DOM mismatch + ScheduleField dom = ScheduleField::only(10); + ScheduleField dow = ScheduleField::only(1); // Monday = 1 with ESPDate (0=Sun) + Schedule s = Schedule::custom( + ScheduleField::only(0), + ScheduleField::only(9), + dom, + ScheduleField::any(), + dow + ); + DateTime from = date.fromUtc(2024, 7, 1, 8, 0, 0); // Monday, day 1 + DateTime next{}; + TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(s, from, next)); + TEST_ASSERT_TRUE( + date.isEqual(next, date.fromUtc(2024, 7, 1, 9, 0, 0)) + ); // passes via DOW even though DOM mismatch } static void test_inline_tick_runs_and_reschedules() { - inlineHits = 0; - Schedule s = Schedule::dailyAtLocal(6, 0); - uint32_t id = scheduler.addJob(s, SchedulerJobMode::Inline, &inlineCallback, nullptr); - TEST_ASSERT_NOT_EQUAL(0u, id); - - DateTime first = date.fromUtc(2025, 1, 1, 6, 0, 0); - scheduler.tick(first); - TEST_ASSERT_EQUAL(1, inlineHits); - - // Same day later should not trigger again - scheduler.tick(date.fromUtc(2025, 1, 1, 23, 0, 0)); - TEST_ASSERT_EQUAL(1, inlineHits); - - // Next day at slot should run again - scheduler.tick(date.fromUtc(2025, 1, 2, 6, 0, 0)); - TEST_ASSERT_EQUAL(2, inlineHits); + inlineHits = 0; + Schedule s = Schedule::dailyAtLocal(6, 0); + uint32_t id = scheduler.addJob(s, SchedulerJobMode::Inline, &inlineCallback, nullptr); + TEST_ASSERT_NOT_EQUAL(0u, id); + + DateTime first = date.fromUtc(2025, 1, 1, 6, 0, 0); + scheduler.tick(first); + TEST_ASSERT_EQUAL(1, inlineHits); + + // Same day later should not trigger again + scheduler.tick(date.fromUtc(2025, 1, 1, 23, 0, 0)); + TEST_ASSERT_EQUAL(1, inlineHits); + + // Next day at slot should run again + scheduler.tick(date.fromUtc(2025, 1, 2, 6, 0, 0)); + TEST_ASSERT_EQUAL(2, inlineHits); } static void test_get_job_info_reports_next_run() { - inlineHits = 0; - Schedule s = Schedule::dailyAtLocal(6, 0); - uint32_t id = scheduler.addJob(s, SchedulerJobMode::Inline, &inlineCallback, nullptr); - TEST_ASSERT_NOT_EQUAL(0u, id); - - DateTime now = date.fromUtc(2025, 1, 1, 0, 0, 0); - scheduler.tick(now); // compute next run but do not fire - TEST_ASSERT_EQUAL(0, inlineHits); - - JobInfo info{}; - TEST_ASSERT_TRUE(scheduler.getJobInfo(0, info)); - TEST_ASSERT_EQUAL(id, info.id); - TEST_ASSERT_TRUE(info.enabled); - TEST_ASSERT_EQUAL(static_cast(SchedulerJobMode::Inline), static_cast(info.mode)); - TEST_ASSERT_TRUE(date.isEqual(info.nextRunUtc, date.fromUtc(2025, 1, 1, 6, 0, 0))); + inlineHits = 0; + Schedule s = Schedule::dailyAtLocal(6, 0); + uint32_t id = scheduler.addJob(s, SchedulerJobMode::Inline, &inlineCallback, nullptr); + TEST_ASSERT_NOT_EQUAL(0u, id); + + DateTime now = date.fromUtc(2025, 1, 1, 0, 0, 0); + scheduler.tick(now); // compute next run but do not fire + TEST_ASSERT_EQUAL(0, inlineHits); + + JobInfo info{}; + TEST_ASSERT_TRUE(scheduler.getJobInfo(0, info)); + TEST_ASSERT_EQUAL(id, info.id); + TEST_ASSERT_TRUE(info.enabled); + TEST_ASSERT_EQUAL(static_cast(SchedulerJobMode::Inline), static_cast(info.mode)); + TEST_ASSERT_TRUE(date.isEqual(info.nextRunUtc, date.fromUtc(2025, 1, 1, 6, 0, 0))); } static void test_tick_waits_until_clock_valid() { - inlineHits = 0; - Schedule s = Schedule::dailyAtLocal(6, 0); - uint32_t id = scheduler.addJob(s, SchedulerJobMode::Inline, &inlineCallback, nullptr); - TEST_ASSERT_NOT_EQUAL(0u, id); - - DateTime invalid = date.fromUtc(1970, 1, 1, 0, 0, 0); - scheduler.tick(invalid); - TEST_ASSERT_EQUAL(0, inlineHits); - - DateTime valid = date.fromUtc(2025, 1, 1, 6, 0, 0); - scheduler.tick(valid); - TEST_ASSERT_EQUAL(1, inlineHits); + inlineHits = 0; + Schedule s = Schedule::dailyAtLocal(6, 0); + uint32_t id = scheduler.addJob(s, SchedulerJobMode::Inline, &inlineCallback, nullptr); + TEST_ASSERT_NOT_EQUAL(0u, id); + + DateTime invalid = date.fromUtc(1970, 1, 1, 0, 0, 0); + scheduler.tick(invalid); + TEST_ASSERT_EQUAL(0, inlineHits); + + DateTime valid = date.fromUtc(2025, 1, 1, 6, 0, 0); + scheduler.tick(valid); + TEST_ASSERT_EQUAL(1, inlineHits); } static void test_psram_buffer_config_constructor_adds_inline_job() { - ESPSchedulerConfig cfg{}; - cfg.usePSRAMBuffers = true; - ESPScheduler localScheduler(date, cfg); - - uint32_t id = localScheduler.addJob( - Schedule::dailyAtLocal(12, 0), - SchedulerJobMode::Inline, - &inlineCallback, - nullptr); - TEST_ASSERT_NOT_EQUAL(0u, id); - - localScheduler.cancelAll(); + ESPSchedulerConfig cfg{}; + cfg.usePSRAMBuffers = true; + ESPScheduler localScheduler(date, cfg); + + uint32_t id = localScheduler.addJob( + Schedule::dailyAtLocal(12, 0), + SchedulerJobMode::Inline, + &inlineCallback, + nullptr + ); + TEST_ASSERT_NOT_EQUAL(0u, id); + + localScheduler.cancelAll(); } static void test_deinit_is_idempotent_and_safe_when_uninitialized() { - ESPScheduler localScheduler(date); - TEST_ASSERT_TRUE(localScheduler.isInitialized()); + ESPScheduler localScheduler(date); + TEST_ASSERT_TRUE(localScheduler.isInitialized()); - DateTime when = date.fromUtc(2025, 1, 1, 12, 0, 0); - uint32_t id = localScheduler.addJobOnceUtc(when, SchedulerJobMode::Inline, &inlineCallback, nullptr); - TEST_ASSERT_NOT_EQUAL(0u, id); + DateTime when = date.fromUtc(2025, 1, 1, 12, 0, 0); + uint32_t id = + localScheduler.addJobOnceUtc(when, SchedulerJobMode::Inline, &inlineCallback, nullptr); + TEST_ASSERT_NOT_EQUAL(0u, id); - localScheduler.deinit(); - TEST_ASSERT_FALSE(localScheduler.isInitialized()); + localScheduler.deinit(); + TEST_ASSERT_FALSE(localScheduler.isInitialized()); - JobInfo info{}; - TEST_ASSERT_FALSE(localScheduler.getJobInfo(0, info)); - TEST_ASSERT_FALSE(localScheduler.cancelJob(id)); - TEST_ASSERT_FALSE(localScheduler.pauseJob(id)); - TEST_ASSERT_FALSE(localScheduler.resumeJob(id)); + JobInfo info{}; + TEST_ASSERT_FALSE(localScheduler.getJobInfo(0, info)); + TEST_ASSERT_FALSE(localScheduler.cancelJob(id)); + TEST_ASSERT_FALSE(localScheduler.pauseJob(id)); + TEST_ASSERT_FALSE(localScheduler.resumeJob(id)); - localScheduler.deinit(); - TEST_ASSERT_FALSE(localScheduler.isInitialized()); + localScheduler.deinit(); + TEST_ASSERT_FALSE(localScheduler.isInitialized()); } static void test_scheduler_reinitializes_after_deinit() { - ESPScheduler localScheduler(date); - localScheduler.deinit(); - TEST_ASSERT_FALSE(localScheduler.isInitialized()); - - inlineHits = 0; - DateTime when = date.fromUtc(2025, 1, 1, 12, 0, 0); - uint32_t id = localScheduler.addJobOnceUtc(when, SchedulerJobMode::Inline, &inlineCallback, nullptr); - TEST_ASSERT_NOT_EQUAL(0u, id); - TEST_ASSERT_TRUE(localScheduler.isInitialized()); - - localScheduler.tick(when); - TEST_ASSERT_EQUAL(1, inlineHits); + ESPScheduler localScheduler(date); + localScheduler.deinit(); + TEST_ASSERT_FALSE(localScheduler.isInitialized()); + + inlineHits = 0; + DateTime when = date.fromUtc(2025, 1, 1, 12, 0, 0); + uint32_t id = + localScheduler.addJobOnceUtc(when, SchedulerJobMode::Inline, &inlineCallback, nullptr); + TEST_ASSERT_NOT_EQUAL(0u, id); + TEST_ASSERT_TRUE(localScheduler.isInitialized()); + + localScheduler.tick(when); + TEST_ASSERT_EQUAL(1, inlineHits); } static void test_sunrise_next_occurrence_with_offsets() { - DateTime from = date.fromUtc(2025, 6, 1, 0, 0, 0); - SunCycleResult riseToday = date.sunrise(from); - TEST_ASSERT_TRUE(riseToday.ok); + DateTime from = date.fromUtc(2025, 6, 1, 0, 0, 0); + SunCycleResult riseToday = date.sunrise(from); + TEST_ASSERT_TRUE(riseToday.ok); - DateTime next{}; - TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(Schedule::sunrise(), from, next)); - TEST_ASSERT_TRUE(date.isEqual(next, riseToday.value)); + DateTime next{}; + TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(Schedule::sunrise(), from, next)); + TEST_ASSERT_TRUE(date.isEqual(next, riseToday.value)); - DateTime expectedPlus = date.addMinutes(riseToday.value, 30); - TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(Schedule::sunrise(30), from, next)); - TEST_ASSERT_TRUE(date.isEqual(next, expectedPlus)); + DateTime expectedPlus = date.addMinutes(riseToday.value, 30); + TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(Schedule::sunrise(30), from, next)); + TEST_ASSERT_TRUE(date.isEqual(next, expectedPlus)); - DateTime expectedMinus = date.addMinutes(riseToday.value, -30); - TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(Schedule::sunrise(-30), from, next)); - TEST_ASSERT_TRUE(date.isEqual(next, expectedMinus)); + DateTime expectedMinus = date.addMinutes(riseToday.value, -30); + TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(Schedule::sunrise(-30), from, next)); + TEST_ASSERT_TRUE(date.isEqual(next, expectedMinus)); } static void test_sunset_next_occurrence_with_offsets() { - DateTime from = date.fromUtc(2025, 6, 1, 0, 0, 0); - SunCycleResult setToday = date.sunset(from); - TEST_ASSERT_TRUE(setToday.ok); + DateTime from = date.fromUtc(2025, 6, 1, 0, 0, 0); + SunCycleResult setToday = date.sunset(from); + TEST_ASSERT_TRUE(setToday.ok); - DateTime next{}; - TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(Schedule::sunset(), from, next)); - TEST_ASSERT_TRUE(date.isEqual(next, setToday.value)); + DateTime next{}; + TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(Schedule::sunset(), from, next)); + TEST_ASSERT_TRUE(date.isEqual(next, setToday.value)); - DateTime expectedPlus = date.addMinutes(setToday.value, 20); - TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(Schedule::sunset(20), from, next)); - TEST_ASSERT_TRUE(date.isEqual(next, expectedPlus)); + DateTime expectedPlus = date.addMinutes(setToday.value, 20); + TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(Schedule::sunset(20), from, next)); + TEST_ASSERT_TRUE(date.isEqual(next, expectedPlus)); - DateTime expectedMinus = date.addMinutes(setToday.value, -20); - TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(Schedule::sunset(-20), from, next)); - TEST_ASSERT_TRUE(date.isEqual(next, expectedMinus)); + DateTime expectedMinus = date.addMinutes(setToday.value, -20); + TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(Schedule::sunset(-20), from, next)); + TEST_ASSERT_TRUE(date.isEqual(next, expectedMinus)); } static void test_moon_phase_name_last_quarter_next_occurrence() { - Schedule phaseSchedule = Schedule::moonPhase(MoonPhaseName::LastQuarter, 2); - DateTime from = date.fromUtc(2024, 3, 25, 0, 0, 0); - DateTime next{}; - TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(phaseSchedule, from, next)); - TEST_ASSERT_TRUE(date.differenceInDays(next, from) <= 40); - - MoonPhaseResult phaseAtNext = date.moonPhase(next); - TEST_ASSERT_TRUE(phaseAtNext.ok); - TEST_ASSERT_TRUE(circularDistanceDegrees(static_cast(phaseAtNext.angleDegrees), 270.0) <= 6.0); + Schedule phaseSchedule = Schedule::moonPhase(MoonPhaseName::LastQuarter, 2); + DateTime from = date.fromUtc(2024, 3, 25, 0, 0, 0); + DateTime next{}; + TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(phaseSchedule, from, next)); + TEST_ASSERT_TRUE(date.differenceInDays(next, from) <= 40); + + MoonPhaseResult phaseAtNext = date.moonPhase(next); + TEST_ASSERT_TRUE(phaseAtNext.ok); + TEST_ASSERT_TRUE( + circularDistanceDegrees(static_cast(phaseAtNext.angleDegrees), 270.0) <= 6.0 + ); } static void test_moon_illumination_crossing_and_reschedule() { - Schedule illumSchedule = Schedule::moonIlluminationPercent(75.0, 0.5); - DateTime from = date.fromUtc(2024, 1, 1, 0, 0, 0); - - DateTime first{}; - TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(illumSchedule, from, first)); - MoonPhaseResult firstPhase = date.moonPhase(first); - TEST_ASSERT_TRUE(firstPhase.ok); - TEST_ASSERT_TRUE(std::fabs(firstPhase.illumination * 100.0 - 75.0) <= 2.0); - - DateTime second{}; - TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(illumSchedule, date.addMinutes(first, 1), second)); - TEST_ASSERT_TRUE(date.isAfter(second, first)); - TEST_ASSERT_TRUE(date.differenceInHours(second, first) > 24); + Schedule illumSchedule = Schedule::moonIlluminationPercent(75.0, 0.5); + DateTime from = date.fromUtc(2024, 1, 1, 0, 0, 0); + + DateTime first{}; + TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(illumSchedule, from, first)); + MoonPhaseResult firstPhase = date.moonPhase(first); + TEST_ASSERT_TRUE(firstPhase.ok); + TEST_ASSERT_TRUE(std::fabs(firstPhase.illumination * 100.0 - 75.0) <= 2.0); + + DateTime second{}; + TEST_ASSERT_TRUE( + scheduler.computeNextOccurrence(illumSchedule, date.addMinutes(first, 1), second) + ); + TEST_ASSERT_TRUE(date.isAfter(second, first)); + TEST_ASSERT_TRUE(date.differenceInHours(second, first) > 24); } static void test_invalid_astronomical_schedule_validation() { - TEST_ASSERT_EQUAL(0u, scheduler.addJob(Schedule::sunrise(1500), SchedulerJobMode::Inline, &inlineCallback, nullptr)); - TEST_ASSERT_EQUAL(0u, scheduler.addJob(Schedule::sunset(-1500), SchedulerJobMode::Inline, &inlineCallback, nullptr)); - TEST_ASSERT_EQUAL(0u, scheduler.addJob(Schedule::moonPhaseAngle(-1, 1), SchedulerJobMode::Inline, &inlineCallback, nullptr)); - TEST_ASSERT_EQUAL(0u, scheduler.addJob(Schedule::moonPhaseAngle(360, 1), SchedulerJobMode::Inline, &inlineCallback, nullptr)); - TEST_ASSERT_EQUAL(0u, scheduler.addJob(Schedule::moonPhaseAngle(270, 31), SchedulerJobMode::Inline, &inlineCallback, nullptr)); - TEST_ASSERT_EQUAL( - 0u, scheduler.addJob(Schedule::moonIlluminationPercent(101.0, 0.5), SchedulerJobMode::Inline, &inlineCallback, nullptr)); - TEST_ASSERT_EQUAL( - 0u, scheduler.addJob(Schedule::moonIlluminationPercent(50.0, 0.0), SchedulerJobMode::Inline, &inlineCallback, nullptr)); - TEST_ASSERT_EQUAL( - 0u, scheduler.addJob(Schedule::moonIlluminationPercent(50.0, 51.0), SchedulerJobMode::Inline, &inlineCallback, nullptr)); + TEST_ASSERT_EQUAL( + 0u, + scheduler + .addJob(Schedule::sunrise(1500), SchedulerJobMode::Inline, &inlineCallback, nullptr) + ); + TEST_ASSERT_EQUAL( + 0u, + scheduler + .addJob(Schedule::sunset(-1500), SchedulerJobMode::Inline, &inlineCallback, nullptr) + ); + TEST_ASSERT_EQUAL( + 0u, + scheduler.addJob( + Schedule::moonPhaseAngle(-1, 1), + SchedulerJobMode::Inline, + &inlineCallback, + nullptr + ) + ); + TEST_ASSERT_EQUAL( + 0u, + scheduler.addJob( + Schedule::moonPhaseAngle(360, 1), + SchedulerJobMode::Inline, + &inlineCallback, + nullptr + ) + ); + TEST_ASSERT_EQUAL( + 0u, + scheduler.addJob( + Schedule::moonPhaseAngle(270, 31), + SchedulerJobMode::Inline, + &inlineCallback, + nullptr + ) + ); + TEST_ASSERT_EQUAL( + 0u, + scheduler.addJob( + Schedule::moonIlluminationPercent(101.0, 0.5), + SchedulerJobMode::Inline, + &inlineCallback, + nullptr + ) + ); + TEST_ASSERT_EQUAL( + 0u, + scheduler.addJob( + Schedule::moonIlluminationPercent(50.0, 0.0), + SchedulerJobMode::Inline, + &inlineCallback, + nullptr + ) + ); + TEST_ASSERT_EQUAL( + 0u, + scheduler.addJob( + Schedule::moonIlluminationPercent(50.0, 51.0), + SchedulerJobMode::Inline, + &inlineCallback, + nullptr + ) + ); } static void test_tick_runs_sunrise_and_moon_schedules() { - inlineHits = 0; - DateTime sunriseFrom = date.fromUtc(2025, 6, 1, 0, 0, 0); - DateTime sunriseDue{}; - TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(Schedule::sunrise(), sunriseFrom, sunriseDue)); - - uint32_t sunriseId = scheduler.addJob(Schedule::sunrise(), SchedulerJobMode::Inline, &inlineCallback, nullptr); - TEST_ASSERT_NOT_EQUAL(0u, sunriseId); - scheduler.tick(sunriseDue); - TEST_ASSERT_EQUAL(1, inlineHits); - - scheduler.cancelAll(); - - Schedule moonSchedule = Schedule::moonPhase(MoonPhaseName::LastQuarter, 2); - DateTime moonFrom = date.fromUtc(2024, 3, 25, 0, 0, 0); - DateTime moonDue{}; - TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(moonSchedule, moonFrom, moonDue)); - uint32_t moonId = scheduler.addJob(moonSchedule, SchedulerJobMode::Inline, &inlineCallback, nullptr); - TEST_ASSERT_NOT_EQUAL(0u, moonId); - scheduler.tick(moonDue); - TEST_ASSERT_EQUAL(2, inlineHits); + inlineHits = 0; + DateTime sunriseFrom = date.fromUtc(2025, 6, 1, 0, 0, 0); + DateTime sunriseDue{}; + TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(Schedule::sunrise(), sunriseFrom, sunriseDue)); + + uint32_t sunriseId = + scheduler.addJob(Schedule::sunrise(), SchedulerJobMode::Inline, &inlineCallback, nullptr); + TEST_ASSERT_NOT_EQUAL(0u, sunriseId); + scheduler.tick(sunriseDue); + TEST_ASSERT_EQUAL(1, inlineHits); + + scheduler.cancelAll(); + + Schedule moonSchedule = Schedule::moonPhase(MoonPhaseName::LastQuarter, 2); + DateTime moonFrom = date.fromUtc(2024, 3, 25, 0, 0, 0); + DateTime moonDue{}; + TEST_ASSERT_TRUE(scheduler.computeNextOccurrence(moonSchedule, moonFrom, moonDue)); + uint32_t moonId = + scheduler.addJob(moonSchedule, SchedulerJobMode::Inline, &inlineCallback, nullptr); + TEST_ASSERT_NOT_EQUAL(0u, moonId); + scheduler.tick(moonDue); + TEST_ASSERT_EQUAL(2, inlineHits); } void setUp() { - scheduler.cancelAll(); - scheduler.setMinValidUnixSeconds(ESPScheduler::kDefaultMinValidEpochSeconds); - inlineHits = 0; + scheduler.cancelAll(); + scheduler.setMinValidUnixSeconds(ESPScheduler::kDefaultMinValidEpochSeconds); + inlineHits = 0; } -void tearDown() {} +void tearDown() { +} void setup() { - setenv("TZ", "UTC0", 1); - tzset(); - ESPDateConfig config{}; - config.latitude = 47.4979f; - config.longitude = 19.0402f; - config.timeZone = "UTC0"; - date.init(config); - delay(2000); - UNITY_BEGIN(); - RUN_TEST(test_daily_at_local_next_same_day); - RUN_TEST(test_daily_at_local_rolls_to_next_day); - RUN_TEST(test_weekly_mask_advances_to_next_weekday); - RUN_TEST(test_weekly_zero_mask_defaults_to_any_day); - RUN_TEST(test_dom_dow_or_logic_matches_either); - RUN_TEST(test_inline_tick_runs_and_reschedules); - RUN_TEST(test_get_job_info_reports_next_run); - RUN_TEST(test_tick_waits_until_clock_valid); - RUN_TEST(test_psram_buffer_config_constructor_adds_inline_job); - RUN_TEST(test_deinit_is_idempotent_and_safe_when_uninitialized); - RUN_TEST(test_scheduler_reinitializes_after_deinit); - RUN_TEST(test_sunrise_next_occurrence_with_offsets); - RUN_TEST(test_sunset_next_occurrence_with_offsets); - RUN_TEST(test_moon_phase_name_last_quarter_next_occurrence); - RUN_TEST(test_moon_illumination_crossing_and_reschedule); - RUN_TEST(test_invalid_astronomical_schedule_validation); - RUN_TEST(test_tick_runs_sunrise_and_moon_schedules); - UNITY_END(); + setenv("TZ", "UTC0", 1); + tzset(); + ESPDateConfig config{}; + config.latitude = 47.4979f; + config.longitude = 19.0402f; + config.timeZone = "UTC0"; + date.init(config); + delay(2000); + UNITY_BEGIN(); + RUN_TEST(test_daily_at_local_next_same_day); + RUN_TEST(test_daily_at_local_rolls_to_next_day); + RUN_TEST(test_weekly_mask_advances_to_next_weekday); + RUN_TEST(test_weekly_zero_mask_defaults_to_any_day); + RUN_TEST(test_dom_dow_or_logic_matches_either); + RUN_TEST(test_inline_tick_runs_and_reschedules); + RUN_TEST(test_get_job_info_reports_next_run); + RUN_TEST(test_tick_waits_until_clock_valid); + RUN_TEST(test_psram_buffer_config_constructor_adds_inline_job); + RUN_TEST(test_deinit_is_idempotent_and_safe_when_uninitialized); + RUN_TEST(test_scheduler_reinitializes_after_deinit); + RUN_TEST(test_sunrise_next_occurrence_with_offsets); + RUN_TEST(test_sunset_next_occurrence_with_offsets); + RUN_TEST(test_moon_phase_name_last_quarter_next_occurrence); + RUN_TEST(test_moon_illumination_crossing_and_reschedule); + RUN_TEST(test_invalid_astronomical_schedule_validation); + RUN_TEST(test_tick_runs_sunrise_and_moon_schedules); + UNITY_END(); } -void loop() {} +void loop() { +}