Skip to content

Creating TaskScheduler friendly libraries

stevstrong edited this page Nov 1, 2018 · 17 revisions

Types of 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.

Typical issues

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.

How to re-write a library in a TaskScheduler-fiendly way

Single operations oriented libraries

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 before read method was called
  • check that enough time have passed between request and read methods (by using millis() method for instance) and return some error value otherwise.

Repetitive process oriented libraries

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.

Another example of existing library refactoring

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.

Cooperative Multitasking

Generally, all principles described here apply to libraries as much as they apply to sketches.