ESPScheduler v2 is a C++17 scheduler for ESP32 firmware that keeps the cron-style DSL from v1, but replaces the old task-per-job async model with one central scheduler core, one optional background service task, and pluggable executors.
- One
SchedulerCoreowns job state, next-run computation, overlap handling, and rescheduling. - Background mode uses one scheduler task and queue-based control-plane serialization. No mandatory
tick()in background mode. - Async execution goes through executors. The default path is a fixed worker pool instead of one FreeRTOS task per job.
- Manual mode still exists for tight firmware loops and uses the same core as background mode.
- The public API now uses
SchedulerResult<T>for mutating/query operations. - A thin
ESPSchedulerV1Compatwrapper is shipped for migration.
- Cron-style schedule DSL with
ScheduleField,Schedule, one-shot UTC helpers, sunrise/sunset helpers, and moon helpers. - Manual mode and background mode under one API.
DispatchPolicy::InlineorDispatchPolicy::Async.OverlapPolicy::SkipIfRunning,QueueOne, andAllowParallel.- Built-in worker-pool executor, dedicated-task executor, and
ESPWorkerExecutorAdapter. - Optional built-in
ESPWorkerasync backend via config when you want scheduler-managed integration without manual executor registration. - Deterministic lifecycle with
begin()/end(). - Clock validity guard via
setMinValidUnixSeconds()/setMinValidUtc(). refreshAllSchedules()to recompute recurring jobs after wall-clock changes.usePSRAMMetadataroutes scheduler-owned job metadata, slot storage, and due-heap buffers through the scheduler allocator.usePsramStackis honored for the scheduler service task, worker-pool workers, and dedicated-task jobs when the target supports external task stacks.- Arduino / ESP-IDF friendly metadata handling, branch-push CI builds, and device tests.
- PlatformIO:
lib_deps = https://github.com/ESPToolKit/esp-date.git https://github.com/ESPToolKit/esp-worker.git https://github.com/ESPToolKit/esp-scheduler.git - Arduino CLI:
arduino-cli lib install "ArduinoJson" arduino-cli lib install --git-url https://github.com/ESPToolKit/esp-date.git arduino-cli lib install --git-url https://github.com/ESPToolKit/esp-worker.git arduino-cli lib install --git-url https://github.com/ESPToolKit/esp-scheduler.git
#include <Arduino.h>
#include <ESPDate.h>
#include <ESPScheduler.h>
ESPDate date;
SchedulerConfig schedulerConfig() {
SchedulerConfig config{};
config.mode = SchedulerMode::Manual;
return config;
}
ESPScheduler scheduler(date, schedulerConfig());
static void pulse(void* /*userData*/) {
Serial.println("manual inline pulse");
}
void setup() {
Serial.begin(115200);
scheduler.begin();
JobOptions options{};
scheduler.addJob(Schedule::dailyAtLocal(8, 15), options, &pulse, nullptr);
}
void loop() {
scheduler.tick();
delay(1000);
}#include <Arduino.h>
#include <ESPDate.h>
#include <ESPScheduler.h>
ESPDate date;
ESPScheduler scheduler(date); // background mode by default
static void syncJob(void* /*userData*/) {
Serial.println("background worker-pool job");
}
void setup() {
Serial.begin(115200);
scheduler.begin();
JobOptions options{};
options.dispatch = DispatchPolicy::Async;
scheduler.addJob(
Schedule::weeklyAtLocal(0b0111110, 18, 30),
options,
&syncJob,
nullptr
);
}
void loop() {
delay(2000);
}bool begin()/void end(bool waitForRunningJobs = true, uint32_t timeoutMs = 5000)SchedulerResult<uint32_t> addJob(...)SchedulerResult<uint32_t> addJobOnceUtc(...)SchedulerResult<void> cancelJob(...),pauseJob(...),resumeJob(...),cancelAll(),refreshAllSchedules()void tick()/tick(nowUtc)for manual modeSchedulerResult<size_t> jobCount() constSchedulerResult<void> getJobInfo(jobId, out) constSchedulerResult<uint8_t> registerExecutor(ISchedulerExecutor*)beforebegin()uint8_t defaultWorkerExecutor() constuint8_t defaultESPWorkerExecutor() constuint8_t defaultDedicatedExecutor() const
ScheduleField:any(),only(),range(),every(),rangeEvery(),list()Schedule:onceUtc,dailyAtLocal,weeklyAtLocal,monthlyOnDayLocal,sunrise,sunset,moonPhase,moonPhaseAngle,moonIlluminationPercent,custom
JobOptions options{};
options.dispatch = DispatchPolicy::Async;
options.overlap = OverlapPolicy::SkipIfRunning;
options.executorId = scheduler.defaultWorkerExecutor();
options.name = "db-sync";ESPWorker worker;
SchedulerConfig config{};
config.defaultAsyncBackend = AsyncExecutorBackend::ESPWorker;
config.espWorker = &worker;
ESPScheduler scheduler(date, config);
JobOptions options{};
options.dispatch = DispatchPolicy::Async;
options.executorId = scheduler.defaultESPWorkerExecutor();DedicatedTaskOptions task{};
task.name = "isolated-job";
task.stackSize = 8192;
task.priority = 2;
JobOptions options{};
options.dispatch = DispatchPolicy::Async;
options.executorId = scheduler.defaultDedicatedExecutor();
options.dedicatedTask = &task;InlineExecutor: runs in scheduler context.WorkerPoolExecutor: default async executor for ESP32.AsyncExecutorBackend::ESPWorker: optional built-in async backend when configured withSchedulerConfig::espWorker.ESPWorkerExecutorAdapter: bridges to an existingESPWorker.DedicatedTaskExecutor: advanced opt-in path, also used by the v1 compatibility wrapper for per-job task config.
- Scheduler-owned runtime metadata now uses explicit non-throwing allocation paths and reports
SchedulerError::NoMemoryon API paths that can fail cleanly. - Job lookup and completion bookkeeping are direct-indexed internally, and jobs waiting for schedule recomputation are tracked explicitly instead of being recovered by scanning the whole job table each wake cycle.
- Queue submission in background mode returns
QueueFullif the command queue has no space;Timeoutis reserved for commands that were accepted but not acknowledged within the control timeout. - Background control commands use a strict ownership handoff: the caller keeps ownership until
send()succeeds and may free only afterwait()returns, while the service frees only abandoned commands chosen before signaling completion. end(false)stops intake, detaches completion routing, and tears down scheduler-owned resources without waiting for async callbacks to finish posting back into the core.end(true)cancels pending work, drains active async completions, then stops executors and the background service.- Worker-pool and service shutdown still use force-delete after timeout as a deliberate best-effort fallback for stuck tasks; that path is documented behavior, not graceful draining.
- Recurring schedules are evaluated in local time.
- One-shot UTC schedules stay exact.
refreshAllSchedules()only affects recurring jobs; one-shot UTC jobs keep their original timestamp.- Recurring jobs auto-refresh on NTP sync and after local midnight. In manual mode that refresh happens on the next
tick(). In background mode the scheduler wakes immediately on sync and also caps sleep to the next local midnight. dayOfMonthanddayOfWeekfollow classic cron OR semantics.- Moon helpers trigger on crossings with tolerance windows.
- The scheduler idles until
now >= minValidUnixSeconds.
examples/v2_manual_inlineexamples/v2_background_worker_poolexamples/v2_espworker_adapterexamples/v2_shutdownexamples/v1_compat_wrapperexamples/v2_api_compilefor CI coverage of the native v2 API surface
The older v1-style sketches are still present and routed through ESPSchedulerV1Compat. That wrapper is migration support, not the primary API.
ESPSchedulerV1Compat preserves the old shape:
SchedulerJobMode::Inlinemaps to v2 inline dispatch.SchedulerJobMode::WorkerTaskmaps to async dispatch.- Per-job
SchedulerTaskConfigis routed through the dedicated-task executor. deinit(),cleanup(), and index-basedgetJobInfo()remain available on the compatibility wrapper.
- Device Unity tests live under
test/test_esp_scheduler. - CI runs on pushes, pull requests, and workflow dispatch.
- PlatformIO CI is split into v2 API compile coverage, example builds, and device test sketch builds.
- Arduino CLI CI separately compiles the v2 API sketch and the example set.
ESPScheduler is released under the MIT License.
- Website: https://www.esptoolkit.hu/
- GitHub: https://github.com/ESPToolKit
- Support: https://ko-fi.com/esptoolkit