Tasks¶
A Mycila::Task is the unit of work. It holds your function, its scheduling state (type, interval, pause/enable), an optional data payload and an optional completion callback.
Creating a Task¶
#include <MycilaTaskManager.h>
// Default type is FOREVER
Mycila::Task sayHello("sayHello", [](void* params) {
Serial.println("Hello");
});
// Explicit type
Mycila::Task boot("boot", Mycila::Task::Type::ONCE, [](void* params) {
Serial.println("Booting");
});
The constructor signature is Task(const char* name, Function fn) or Task(const char* name, Type type, Function fn). The name must be a stable string literal or a static buffer: the task keeps the raw pointer.
Tasks are cooperative
A task must be short and non-blocking. The scheduler never preempts a running task: while your function runs, no other task of the same manager is executed. Long delay() calls starve the other tasks (see the Watchdog example which demonstrates exactly this failure).
Task Types¶
FOREVER¶
The task runs every time its interval elapses, for as long as it is enabled and not paused. This is the default.
Mycila::Task poll("poll", [](void* params) {
// read a sensor
});
poll.setType(Mycila::Task::Type::FOREVER);
poll.setInterval(1000); // every second
ONCE¶
The task runs at most one time, then pauses itself. A ONCE task is created paused, so it will not run until you call resume().
Mycila::Task boot("boot", Mycila::Task::Type::ONCE, [](void* params) {
Serial.println("Boot sequence done");
});
boot.setEnabled(true);
boot.resume(); // run on the next loop()
// or: boot.resume(5000); // run 5 seconds from now
This makes ONCE tasks a convenient deferred-call primitive: re-arm one at any time with resume(). This also holds while the task is executing: the task pauses itself when its run starts, not when it ends, so a resume() from another FreeRTOS task (or from the function body itself) during the run is preserved and the task executes once more afterwards.
setType() only pauses on an actual type change
setType() returns early when the type is unchanged, so it will not re-pause a task:
boot.resume(); // un-paused, run once
boot.setType(Mycila::Task::Type::ONCE); // no-op: already ONCE, stays un-paused
To re-arm, call resume() again. Switching a finished ONCE task to FOREVER un-pauses it immediately, since the type actually changed.
lastEndTime() == 0 means “due now”
A task with no interval, or one that has never run, has lastEndTime() == 0 and is considered due immediately. This is also why requestEarlyRun() works by resetting that value.
Scheduling¶
Interval¶
setInterval(uint32_t intervalMillis) defines the minimum time between the end of one run and the beginning of the next.
An interval of 0 means the task runs on every loop() pass.
Should it run now?¶
bool scheduled = task.scheduled(); // enabled && !paused
bool due = task.shouldRun(); // scheduled && interval elapsed
uint32_t left = task.remainingTime(); // ms until the next run (0 if due or interval 0)
remainingTime() returns 0 when no interval is configured, and also once the task is due or overdue.
remainingTime() and shouldRun() disagree for a never-run task
A task that has never run has lastEndTime() == 0, which shouldRun() treats as due now. remainingTime() does not apply that rule — it subtracts millis() - 0, i.e. the whole uptime — so it can report a non-zero value while the task is actually due:
// task never ran, interval = 1000, board has been up for 500 ms
task.shouldRun(); // true -> runs on the next loop()
task.remainingTime(); // 500 -> misleading, the task is not waiting
Do not use remainingTime() to decide whether a task will run; use shouldRun() (or scheduled() plus your own accounting).
Immediate run¶
forceRun() bypasses all guards — including the enabled predicate — but still records statistics and fires onDone().
Early run¶
Unlike forceRun(), an early run still honours paused() and enabled(). It is useful when an event downstream of the task means the next scheduled pass is now useless.
Delayed resume¶
resume(delayMillis) both un-pauses the task and re-bases its interval clock, which is the idiomatic way to schedule a "run in N ms" task:
resume(delay) permanently overwrites the interval
resume(delayMillis) calls setInterval(delayMillis), so the task's configured period is replaced. A later resume() with no argument does not restore it:
task.setInterval(1000);
task.resume(5000); // interval is now 5000, not 1000
task.resume(); // interval is still 5000
If you only want a one-off deferred run and need to keep the period, save it first and restore it in the completion callback, or use a dedicated ONCE task.
Passing Data¶
When MYCILA_TASK_MANAGER_NO_PARAMS is not defined, the task function receives a void*:
const char* message = "Pong";
Mycila::Task ping("ping", [](void* params) {
Serial.println((const char*)params);
});
ping.setData((void*)message);
ping.setType(Mycila::Task::Type::ONCE);
ping.resume();
setData() stores the pointer; the library never owns or frees it. Keep the pointee alive as long as the task may run — a string literal, a global, or a static buffer. Never pass the address of a local variable.
Conditional Execution¶
Instead of toggling a task manually, attach a predicate:
bool systemReady = false;
Mycila::Task announce("announce", [](void* params) {
Serial.println("System is ready");
});
announce.setEnabledWhen([]() { return systemReady; });
announce.setInterval(1000);
The predicate is evaluated on every scheduling decision. It must therefore be fast and side-effect free.
Predicate vs. pause
setEnabled(false) installs an internal always-false predicate; pause() is a separate flag. Use the predicate for external conditions and pause() for internal control flow (e.g. ONCE completion).
Completion Callbacks¶
sayHello.onDone([](const Mycila::Task& me, uint32_t elapsed) {
ESP_LOGD("app", "Task '%s' executed in %" PRIu32 " ms", me.name(), elapsed);
ping.resume(); // chain another task
});
elapsed is the raw execution time in milliseconds (millis() difference), independent of any profiling configuration. The callback runs synchronously at the end of the task, still inside _run(), and can safely change the state of other tasks (but not of the task currently running). While it runs, the task reports Task::State::FINALIZING, so active() stays true until the callback returns.
Disable callbacks entirely with -D MYCILA_TASK_MANAGER_NO_CALLBACKS.
Pause, Resume and Enable¶
task.pause(); // skip runs until resumed
task.resume(); // resume immediately
task.resume(5000); // resume, first run in 5 s
task.setEnabled(false); // equivalent to an always-false predicate
task.setEnabled(true); // re-enable
| Query | Returns true when |
|---|---|
enabled() |
the enabled predicate is absent or returns true |
paused() |
the task is paused (including a finished ONCE, or an executing one) |
scheduled() |
enabled() && !paused() |
shouldRun() |
scheduled() and the interval has elapsed |
running() |
the task function is currently executing |
active() |
the task function or its onDone is executing |
idle() |
the task is executing nothing at all |
earlyRunRequested() |
lastEndTime() == 0 |
Execution State¶
An execution is not finished when the task function returns: the onDone callback still runs, and it is allowed to touch shared resources (that is exactly how a task chains another one). Use state() to see where the task is:
enum class State : uint8_t {
IDLE, // nothing executing
RUNNING, // the task function is executing
FINALIZING // the function returned, the onDone callback (and statistics) are running
};
switch (task.state()) {
case Mycila::Task::State::IDLE: break; // safe: no callback of this task can be active
case Mycila::Task::State::RUNNING: break; // task code only
case Mycila::Task::State::FINALIZING: break; // task code returned, onDone is still running
}
running() is strictly the RUNNING state; active() is shorthand for state() != State::IDLE. When you must be sure a task will not touch a shared resource anymore, wait for active() to become false — waiting for running() is not enough, because onDone can still resume and start another task. TaskManager::waitForAllTasksToComplete() uses active() for that reason, and returns immediately when called from the manager's own worker or from a callback, since the calling task would be active forever.
v5: running() no longer covers onDone
In v4, running() became false before the onDone callback ran. Code polling running() to know when a task was completely done would exit too early. Use active() instead — see Migration.
Running a Task¶
A task can be driven on its own, without a manager:
Mycila::Task task("task", [](void* params) {
// ...
});
task.setInterval(100);
void loop() {
task.tryRun(); // returns true if the task was executed
}
tryRun() is also what TaskManager::loop() calls for you — see Task Managers.