-
Notifications
You must be signed in to change notification settings - Fork 224
Creating TaskScheduler friendly libraries
From the task scheduling perspective all libraries fall into two major categories:
- Single-operation oriented
- Repetitive process oriented
Single operations oriented libraries are those that do something (or multiple things) once, and then exit (return a value, an array, etc.). There is no internal scheduling required, although some pre-defined sequence of actions is needed, and there could be certain delays necessary due to hardware timing, etc.
Example: libraries to read sensor values.
Repetitive process oriented libraries do something many times on behalf of the developer in a transparent manner. Those need internal scheduling to perform actions periodically or continuously in the background.
Example: mesh network libraries.
Single operations oriented libraries deal with I/O (hardware, telecommunications, measurements) very frequently require delays to "wait" for events, data, hardware to become ready, etc. Those libraries are often implemented in a synchronous way, i.e., the whole process stops until library finishes processing, while in many cases, library is just waiting for something to happen. Since most micro-controllers are single-processor/single thread devices, everything else stops as well.
Repetitive process oriented libraries often implement their own scheduling mechanism which is embedded and hidden from the developers, and does not play weell with external schedulers, like TaskScheduler.
The solution is quite simple - re-write the library in an asynchronous way. That means avoiding blocking operations (like delay()
or pulseIn()
), and splitting methods which require waiting into request/response pairs.
Let's consider an example:
Library: SparkFun Si7021 Temperature and Humidity Breakout
Located: here
Problem is with the uint16_t Weather::makeMeasurment(uint8_t command)
method:
it has a 100ms delay here
uint16_t Weather::makeMeasurment(uint8_t command)
{
// Take one ADDRESS measurement given by command.
// It can be either temperature or relative humidity
// TODO: implement checksum checking
uint16_t nBytes = 3;
// if we are only reading old temperature, read olny msb and lsb
if (command == 0xE0) nBytes = 2;
Wire.beginTransmission(ADDRESS);
Wire.write(command);
Wire.endTransmission();
// When not using clock stretching (*_NOHOLD commands) delay here
// is needed to wait for the measurement.
// According to datasheet the max. conversion time is ~22ms
delay(100);
Wire.requestFrom(ADDRESS,nBytes);
if(Wire.available() != nBytes)
return 100;
unsigned int msb = Wire.read();
unsigned int lsb = Wire.read();
// Clear the last to bits of LSB to 00.
// According to datasheet LSB of RH is always xxxxxx10
lsb &= 0xFC;
unsigned int mesurment = msb << 8 | lsb;
return mesurment;
}
Let see what we can do to make this library TaskScheduler friendly.
I would split the method into two separate ones: request measurement, and respond with a value.
void Weather::requestMeasurment(uint8_t command)
{
// Take one ADDRESS measurement given by command.
// It can be either temperature or relative humidity
// TODO: implement checksum checking
Wire.beginTransmission(ADDRESS);
Wire.write(command);
Wire.endTransmission();
// When not using clock stretching (*_NOHOLD commands) delay here
// is needed to wait for the measurement.
// According to datasheet the max. conversion time is ~22ms
}
uint16_t Weather::readMeasurment(uint8_t command)
{
uint8_t nBytes = 3;
// if we are only reading old temperature, read olny msb and lsb
if (command == 0xE0) nBytes = 2;
Wire.requestFrom(ADDRESS,nBytes);
if(Wire.available() != nBytes)
return 100;
unsigned int msb = Wire.read();
unsigned int lsb = Wire.read();
// Clear the last to bits of LSB to 00.
// According to datasheet LSB of RH is always xxxxxx10
lsb &= 0xFC;
unsigned int mesurment = msb << 8 | lsb;
return mesurment;
}
So we got rid of delay, and freed up 100 ms of processor time for other tasks to run.
The library could be made backwards compatible with this method:
uint16_t Weather::makeMeasurment(uint8_t command) {
requestMeasurment(command);
delay(100);
return readMeasurment(command);
}
In the task scheduling environment the scheduling could be done in the following manner:
// According to datasheet the max. conversion time is ~22ms
#define MEASUREMENT_DELAY 23
Scheduler ts;
Weather w;
Task tMeasure(MEASUREMENT_DELAY, TASK_ONCE, &mCallback, &ts, false, &mOnEnable);
bool mOnEnable() {
w.requestMeasurment(ADDRESS);
return true;
}
void mCallback() {
GlobalValue = w.readMeasurment();
}
Now to initiate measurement you need to do this:
tMeasure.restartDelayed();
And when tMeasure task completes, which you can detect by either using tMeasure's StatusRequest object, or by periodically checking tMeasure.isEnabled()
method, you have your value.
Your other tasks will have a chance to run while the humidity sensor is doing it's hardware things, without waiting.
NOTE: To be completely fail-safe, a well-written request/read methods pair should include some kind of workflow enforcement:
- check that
request
method was executed beforeread
method was called - check that enough time have passed between
request
andread
methods (by usingmillis()
method for instance) and return some error value otherwise.
The solution depends on how internal scheduling is implemented.
It could be completely encapsulated into the library hiding even the loop()
method.
Some libraries use a copy of TaskScheduler in a rebranded, but not refactored manner, making it impossible to utilize later versions of TaskScheduler concurrently.
Example: BlackEdder / painlessMesh - because painlessScheduler
has not been refactored, using TaskScheduler
library in parallel leads to multiple definition errors at compile time.
My recommendation are:
- Create a library using TaskScheduler, not embedding it
- Share the scheduler
- Document and explain exactly what Tasks and compilation directives are required by the library, so developer does not interfere with the flow logic of your library
- If your library is time-critical, create your own scheduler, and set it as a higher priority scheduler to the user's scheduler. This way your tasks are going to be given priority execution
Good sharing practices:
Pass scheduler to via constructor:
Scheduler ts;
MySuperLibrary lib(&ts);
...
Make your library scheduler a higher priority scheduler:
// Constructor
MySuperLibrary::MySuperLibrary(Scheduler *ts) {
if (ts != NULL) {
ts->setHighPriorityScheduler(this->scheduler);
}
}
This is better than running one chain after the other:
(*(this->scheduler)).execute();
(*(this->externalScheduler)).execute();
In the prioritized scenario, your library tasks are interwoven in the chain of user tasks, preserving your sequence.
DFRobotDFPlayerMini library by DFRobot here is another example of a library peppered with delay
statements to wait for completion of serial communication.
I modified the library to work asynchronously and support cooperative multitasking. For details, please refer to Readme file here.
Generally, all principles described here apply to libraries as much as they apply to sketches.