TaskManager is a very simple co-operative coroutines / executor framework that allows work to be scheduled in an internal queue. Instead of writing code using delays, one simply adds jobs to be done at some point in the future. In addition to this, interrupt handling is also supported, such that the interrupt is “marshalled” and handled like a very high priority task to be processed immediately.
When using this library you should never use any method that will delay for more than a few microseconds, failing to follow that guideline will cause problems with interrupt handling and timing.
Firstly include it
#include <TaskManagerIO.h>
There is no need to create a task manager, as one is created for you at the global scope called taskManager
.
For every scheduled task that we create, we provide a call back function that will be called at the specified time. These are declared as below. You can also use C11’s shortened lambda syntax if you wish, see the examples in the taskManagement.ino sketch.
void onTimer() {
// do something here
}
To schedule onTimer to be called once in the future - very similar to setTimeout
in javascript:
uint8_t taskId = taskManager.scheduleOnce(millisFromNow, onTimer);
Or optionally provide a unit of time as well:
// timerUnit is one of enum TimerUnit TIME_MICROS, TIME_SECONDS, TIME_MILLIS
uint8_t taskId = taskManager.scheduleOnce(millisFromNow, onTimer, timerUnit);
To schedule onTimer to be called over and over at a scheduled interval, again the time unit is optional:
uint8_t taskId = taskManager.scheduleFixedRate(millisInterval, onTimer);
uint8_t taskId = taskManager.scheduleFixedRate(millisInterval, onTimer, timerUnit);
To cancel a Task just pass in the taskId (return value) from a schedule call.
taskManager.cancelTask(uint8_t taskId);
Should you wish to provide an instance of a class for scheduling then you extend from Executable
and implement the exec()
method, every time the task runs, the exec
method will be called. The optional parameter deleteWhenDone can be set to true to indicate the object was allocated using new
and should be deleted when the task completes (or is cancelled).
class MyScheduledClass : public Executable {
public void exec() override {
// your task code here
}
};
MyScheduledClass schedInstance;
taskManager.scheduleOnce(when, &schedInstance);
taskManager.scheduleOnce(when, &schedInstance, timerUnit, [deleteWhenDone=false]);
taskManager.scheduleFixedRate(howOften, &schedInstance);
taskManager.scheduleFixedRate(howOften, &schedInstance, timerUnit, [deleteWhenDone=false]);
taskManager.execute(function, [deleteWhenDone=false]);
taskManager.execute(executable, [deleteWhenDone=false]);
Should you wish to schedule a function that takes parameters, you can use the ExecWithParameter helper class as follows. Let’s say we wanted to pass the Serial
object to the task callback, then we would do as follows:
Firstly, include the required extra header file
#include <ExecWithParameter.h>
Next, create a function that takes the Serial
parameter
void myTaskCallback(HardwareSerial *serial) {
// do something with Serial
}
Lastly, we create the task
// note that the deleteWhenDone parameter is set to true
// if you allocate using new like this, you must set that parameter.
auto task = new ExecWithParameter<HardwareSerial*>(myTaskCallback, &Serial);
taskManager.scheduleOnce(when, task, TIME_MILLIS, true);
There’s also ExecWith2Parameters
that allows for two parameters instead of one.
A more complete example that can be copied into an Arduino IDE, that stores integers in a class that extends from Executable
:
#include <TaskManagerIO.h>
// create a class that extends executable and stores an integer.
class IntegerExec : public Executable {
private:
int intValue;
public:
IntegerExec(int val) {
intValue = val;
}
// its exec is called at the schedule interval
void exec() override {
Serial.print("Int value is ");
Serial.println(intValue);
}
void increment() {
intValue++;
}
};
// we now create a couple of globals to be called back.
IntegerExec firstExec(42);
IntegerExec anotherExec(1001);
IntegerExec yetAnotherExec(10000);
void setup() {
// start up the serial port.
Serial.begin(115200);
// now register three executable tasks.
taskManager.scheduleOnce(1000, &firstExec);
taskManager.scheduleOnce(2000, &anotherExec);
taskManager.scheduleFixedRate(500, &yetAnotherExec);
// and one regular function based task.
taskManager.scheduleFixedRate(250, [] {
yetAnotherExec.increment();
});
}
void loop() {
// we must always call runLoop on taskManager within loop
taskManager.runLoop();
}
Example output snippet in the Serial Monitor from the above sketch:
Int value is 10001
Int value is 42
Int value is 10003
Int value is 10005
Int value is 1001
Int value is 10007
Int value is 10009
If in your own code you need to wait for a few microseconds for hardware states to settle, you can use this method which actually lets other tasks continue until the number of microseconds has passed:
void yieldForMicros(micros);
In the Arduino loop method, just put one call to the task manager. You must not do anything here that sleeps or goes into long loops, otherwise the taskManager won’t schedule things on time.
void loop() {
taskManager.runLoop();
}
Debugging what slots are used and free is possible as below, where I dump all the tasks to the serial port, the output is the state of each task in turn.
State | Meaning |
---|---|
R | Repeating task |
U | In use |
F | Free |
r | Repeated running |
u | In use running |
char debugData[10]; // this must be at least as big as the number of slots
Serial.println(checkAvailableSlots(debugData));
There’s rarely a need to change the number of tasks, task manager grows the number of tasks automatically in a way that will suit 99% of cases. However, in extreme cases, you can define DEFAULT_TASK_SIZE
that sets the initial number of task slots, and DEFAULT_TASK_BLOCKS
that sets the number of times it will create an additional block of the same size.