Skip to content

Implementation scenarios and ideas

Anatoli Arkhipenko edited this page Apr 22, 2020 · 6 revisions

Implementation scenarios and ideas

1. Event driven programming
2. "Native" support for finite state machine
3. Multiple possible callbacks for task
4. Interrupt-driven execution support
5. Using onEnable and onDisable methods
6. Using status request objects
7. Using local task storage pointer
8. Enabling task prioritization


1. Event driven programming

Each of the processes of your application becomes a separate and distinct programming area, which may or may not interact and control each other.

Example:
In a plant watering system you need to measure soil humidity, control pump and display the results Each of the areas becomes a task:

Task tMeasure (TMEASURE_INTERVAL*SECOND, TASK_FOREVER, &measureCallback);
Task tWater   (TWATER_INTERVAL*SECOND, RETRIES, &waterCallback);
Task tDisplay (TDISPLAY_INTERVAL*SECOND, TASK_FOREVER, &displayCallback);

Scheduler taskManager; 

Further, once you turn on the pump, you keep it running for TWATER_INTERVAL interval and then turn it off. Turning off a pump is also a task which only needs to run once for every time the pump is turned on:

Task tWaterOff (WATERTIME*SECOND, TASK_ONCE, &waterOffCallback);

Example of the callback method:

void waterOffCallback()
{
    motorOff();
    tWater.enableDelayed();
}

or

void waterCallback()
{
    if(tWater.getIterations()) {
    // If this is not the last iteration = turn the pump on
    motorOn();
    tWaterOff.set(parameters.watertime * TASK_SECOND, TASK_ONCE, &waterOffCallback);
    tWaterOff.enableDelayed();
    return;
}
// We could not reach target humidity – something is wrong
    motorOff;
    taskManager.disableAll();
    tError.enable();
}

Your sample setup() and loop() (partially) are as follows.
Note: please note that tWater is not activated during setup(). It is activated by tMeasure callback once the watering conditions are met.

void setup()
{
    // ...
    tWater.setIterations(parameters.retries);
    tWaterOff.setInterval(parameters.watertime * SECOND);

    taskManager.init();
    taskManager.addTask(tMeasure);
    taskManager.addTask(tDisplay);
    taskManager.addTask(tWater);
    taskManager.addTask(tWaterOff);

    tMeasure.enable();
    tDisplay.enable();

    currentHumidity = measureHumidity();
}

void loop ()
{
    taskManager.execute();
}

2. "Native" support for finite state machine

Define “states” as callback method or methods. Each callback method executes activities specific to a “state” and then “transitions” to the next state by assigning next callback method to the task.
Transition from one state to the next is achieved by setting next callback method at the end of preceding one.
Note: do not call the next callback method explicitly. Yield to the scheduler, and let the scheduler take care of next iteration during the next pass. (Thus giving other tasks change to run their callback methods).

Example: Blinking LED 2 times a second could be achieved this way

Scheduler ts;
Task tLedBlinker (500, TASK_FOREVER, &ledOnCallback, &ts, true);

void  ledOnCallback()
{
    turnLedOn();
    tLedBlinker.setCallback(&ledOffCallback);
}

void  ledOffCallback()
{ 
    turnLedOff();
    tLedBlinker.setCallback(&ledOnCallback);
}

setup(){}
loop ()
{
    ts.execute();
}

Obviously the example is simple, but gives the idea of how the tasks could be used to go through states.

3. Multiple possible callbacks for task

There may be a need to select an option for callback method based on certain criteria, or randomly. You can achieve that by defining an array of callback method pointers and selecting one based on the criteria you need.

Example: when a robot detects an obstacle, it may go left, right backwards, etc. Each of the “directions” or “behaviors” are represented by a different callback methods.

Another example of using multiple callbacks:
You may need to “initialize” variables for a particular task.
In this case, define a tasks with two callbacks:

Task tWork (T_INTERVAL, TASK_FOREVER, &workCallbackInit);
//
void  workCallbackInit()
{
    // do your initializationstuff here
    // finally assigne the main callback method
    tWork.setCallback(&workCallback);
}
void workCallback()
{
    // main callback method …
}

The task will initialize during first execution pass and switch to “regular” callback execution starting with second pass. There is a delay between first and second passes of the task (scheduling period, if defined). In order to execute the second pass immediately after initialization first pass, change the above code like this:

void  workCallbackInit()
{
    // do your initializationstuff here
    // finally assigne the main callback method
    tWork.setCallback(&workCallback);
    tWork.enable();
}

The task will run initialization first, then immediately second pass, and then switch to processing at regular intervals starting with a third pass.

4. Interrupt-driven execution support

In case of interrupt-driven program flow, tasks could be scheduled to run once to request asynchronous execution (request), and then re-enabled (restarted) again with a different callback method to process the results.

Example: event driven distance calculation for ultrasonic pulses. EchoPin #6 triggers pin change interrupts on rising and falling edges to determine the length of ultrasonic pulse.

#include <DirectIO.h>
#include <TaskScheduler.h>
#include <EnableInterrupt.h>

#define  TRIGGERPIN 5
#define  ECHOPIN    6

Output<TRIGGERPIN>  pTrigger;
Input<ECHOPIN>      pEcho;

Scheduler r;

void measureCallback();
void displayCallback();
void pingCalcCallback();

Task  tMeasure(TASK_SECOND, TASK_FOREVER, &measureCallback, &r, true);
Task  tDisplay(TASK_SECOND, TASK_FOREVER, &displayCallback, &r, true);
Task  tPing(TASK_IMMEDIATE, TASK_ONCE, &pingCalcCallback, &r, false);

volatile bool          pulseBusy    = false;
volatile bool          pulseTimeout = false;
volatile unsigned long pulseStart   = 0; 
volatile unsigned long pulseStop    = 0; 
volatile unsigned long pingDistance = 0;

void pingTrigger(unsigned long aTimeout)
{
    if(pulseBusy) return;  // do not trigger if in the middle of a pulse
    if(pEcho == HIGH) return; // do not trigger if ECHO pin is high    

    pulseBusy = true;
    pulseTimeout = false;

    pTrigger = LOW;
    delayMicroseconds(4);
    pTrigger = HIGH;

    tPing.setInterval (aTimeout);

    delayMicroseconds(10);
    pTrigger = LOW;

    tPing.restartDelayed(); // timeout countdown starts now

    // will start the pulse clock on the rising edge of ECHO pin
    enableInterrupt(ECHOPIN, &pingStartClock, RISING);
}

// Start clock on the rising edge of the ultrasonic pulse
void pingStartClock() 
{
    pulseStart = micros();
    disableInterrupt(ECHOPIN); // not sure this is necessary
    enableInterrupt(ECHOPIN, &pingStopClock, FALLING);
    tPing.restartDelayed();
} 

// Stop clock on the falling edge of the ultrasonic pulse 
void pingStopClock() 
{
    pulseStop = micros();
    disableInterrupt(ECHOPIN);
    pingDistance = pulseStop - pulseStart;
    pulseBusy = false;
    tPing.disable(); // disable timeout 
}

// Stop clock because of the timeout – the wave did not return 
void pingCalcCallback() 
{
    if (pulseBusy)
    {
        pingStopClock();
    }
    pulseTimeout = true;
}

// Initial measure callback sets the trigger 
void measureCallback() 
{
    if (pulseBusy) // already measuring, try again
    {  
        tMeasure.enable();
        return;
    }
    pingTrigger(30); // 30 milliseconds or max range of ~5.1 meters
    tMeasure.setCallback(&measureCallbackWait); 
}

// Wait for the measurement to 
void measureCallbackWait() 
{
    if (pulseBusy) return;
    tMeasure.setCallback(&measureCallback);
}

bool state = true;

void displayCallback() 
{
    char d[256];
    unsigned long cm = pingDistance * 17 / 100; // cm
    snprintf(d, 256, "pulseStart = %8lu\tpulseStop=%8lu\tdistance, cm=%8lu", pulseStart, pulseStop, cm);
    Serial.println(d);
}

void setup()
{
// put your setup code here, to run once:

    Serial.begin(115200);

    pTrigger = LOW;
    pEcho = LOW;  
} 

void loop()
{  
    // put your main code here, to run repeatedly:  
    r.execute(); 
} 

5. Using onEnable and onDisable methods

Consider a task to flash onboard LED for 5 seconds with random frequency. Task should be repeated every 30 seconds indefinitely. Since frequency is random, there are two challenges:

  1. We need to make sure LED is turned OFF at the last iteration
  2. We need to calculate random frequency every time

Below is the implementation using TaskScheduler

#define _TASK_SLEEP_ON_IDLE_RUN
#include <TaskScheduler.h>
#define  LEDPIN    13

Scheduler ts;

void WrapperCallback();
bool BlinkOnEnable();
void BlinkOnDisable();
void LEDOff();

Task tWrapper(30000, TASK_FOREVER, &WrapperCallback, &ts, true);
Task tBlink(5000, TASK_ONCE, NULL, &ts, false, &BlinkOnEnable, &BlinkOnDisable);
Task tLED(TASK_IMMEDIATE, TASK_FOREVER, NULL, &ts, false, NULL, &LEDOff);

void WrapperCallback() 
{
    Serial.println("In WrapperCallback");
    tBlink.restartDelayed();
    // LED blinking  is initiated
    //every 30 seconds for 5 seconds 
}

// Upon being enabled, tBlink will define the parameters
// and enable LED blinking task, which actually controls 
// the hardware (LED in this example)
bool BlinkOnEnable() 
{
    Serial.println("In BlinkOnEnable");
    tLED.setInterval( 500 + random(501));
    tLED.setCallback( &LEDOn);
    tLED.enable();

    return true;  // Task should be enabled 
} 

// tBlink does not really need a callback method 
// since it just waits for 5 seconds for the first 
// and only iteration to occur. Once the iteration 
// takes place, tBlink is disabled by the Scheduler, 
// thus executing its OnDisable method below. 
void BlinkOnDisable() 
{
    Serial.println("In BlinkOnDisable");
    tLED.disable(); 
}

void LEDOn() 
{
    Serial.println("In LEDOn");
    digitalWrite(LEDPIN, HIGH);
    tLED.setCallback( &LEDOff);
}

void LEDOff() 
{
    Serial.println("In LEDOff");
    digitalWrite(LEDPIN, LOW);
    tLED.setCallback( &LEDOn);
} 

// Note that LEDOff method serves as OnDisable method 
// to make sure the LED is turned off when the tBlink 
// task finishes (or disabled ahead of time) 

void setup() 
{
    Serial.begin(115200);
    pinMode(LEDPIN, OUTPUT); 
} 

void loop() 
{
    // put your main code here, to run repeatedly:
    ts.execute(); 
}

6. Using status request objects

This test emulates querying 3 sensors once every 10 seconds, each could respond with a different delay (ultrasonic sensors for instance) and printing a min value of the three when all three have reported their values.
The overall timeout of 1 second is setup as well. An error message needs to be printed if a timeout occurred instead of a value.

#define _TASK_SLEEP_ON_IDLE_RUN 
#define _TASK_STATUS_REQUEST 
#include <TaskScheduler.h> 

StatusRequest measure; 
Scheduler ts; 

Task tCycle(10000, TASK_FOREVER, &CycleCallback, &ts, true); 
Task tMeasure(TASK_SECOND, TASK_ONCE, &MeasureCallback, &ts, false, &MeasureEnable, &MeasureDisable); 
Task tCalculate(&CalcCallback, &ts); 
Task tSensor1(TASK_IMMEDIATE, TASK_ONCE, &S1Callback, &ts, false, &S1Enable); 
Task tSensor2(TASK_IMMEDIATE, TASK_ONCE, &S2Callback, &ts, false, &S2Enable); 
Task tSensor3(TASK_IMMEDIATE, TASK_ONCE, &S3Callback, &ts, false, &S3Enable);

long distance, d1, d2, d3; 

void CycleCallback() 
{
    Serial.println("CycleCallback: Initiating measurement cycle every 10 seconds");
    tMeasure.restartDelayed();
}

bool MeasureEnable()
{
    Serial.println("MeasureEnable: Activating sensors");
    distance = 0;
    measure.setWaiting(3); // Set the StatusRequest to wait for 3 signals.
    tCalculate.waitFor(&measure);

    tSensor1.restart();
    tSensor2.restart();
    tSensor3.restart();
  
    return true;
}

void MeasureCallback()
{
    Serial.println("MeasureCallback: Invoked by calculate task or one second later");

    if(measure.pending()) 
    {
        tCalculate.disable();
        measure.signalComplete(-1);  // signal error
        Serial.println("MeasureCallback: Timeout!");
    }
    else
    {
        Serial.print("MeasureCallback: Min distance=");Serial.println(distance);
    }
} 

void MeasureDisable() 
{
    Serial.println("MeasureDisable: Cleaning up");
    tSensor1.disable();
    tSensor2.disable();
    tSensor3.disable(); 
}

void CalcCallback() 
{
    Serial.println("CalcCallback: calculating");
    distance = -1;
    if(measure.getStatus() >= 0) // only calculate if statusrequest ended successfully
    {
        distance = d1 < d2 ? d1 : d2;
        distance = d3 < distance ? d3 : distance;
        tMeasure.forceNextIteration();
    }
}

/** Simulation code for sensor 1
 *  ---------------------------
 */ 
bool S1Enable() 
{
    Serial.print("S1Enable: Triggering sensor1. Delay=");
    tSensor1.setInterval(random(1200));  // Simulating sensor delay, which could go over 1 second and cause timeout
    d1 = 0;

    Serial.println(tSensor1.getInterval());
    return true; 
} 

void S1Callback() 
{
    Serial.print("S1Callback: Emulating measurement. d1=");
    d1 = random(501); // pick a value from 0 to 500 "centimeters" simulating a measurement   
    measure.signal();
    Serial.println(d1);
}

/** Simulation code for sensor 2
 *  ---------------------------
 */ 
bool S2Enable() 
{
    Serial.print("S2Enable: Triggering sensor2. Delay=");

    tSensor2.setInterval(random(1200));  // Simulating sensor delay, which could go over 1 second and cause timeout 
    d2 = 0;

    Serial.println(tSensor2.getInterval());
    return true; 
} 

void S2Callback() 
{
    Serial.print("S2Callback: Emulating measurement. d2=");
    d2 = random(501); // pick a value from 0 to 500 "centimeters" simulating a measurement  
    measure.signal();

    Serial.println(d2);  
}

/** Simulation code for sensor 3
 *  ---------------------------
 */ 
bool S3Enable() 
{
    Serial.print("S3Enable: Triggering sensor3. Delay=");
    tSensor3.setInterval(random(1200));  // Simulating sensor delay, which could go over 1 second and cause timeout
    d3 = 0;

    Serial.println(tSensor3.getInterval());
    return true; 
} 

void S3Callback() 
{
    Serial.print("S3Callback: Emulating measurement. d3=");    
    d3 = random(501); // pick a value from 0 to 500 "centimeters" simulating a measurement  
    measure.signal();    

    Serial.println(d3); 
}

/** Main Arduino code
 *  Not much is left here - everything is taken care of by the framework
 */

void setup() 
{
    Serial.begin(115200);
    Serial.println("TaskScheduler StatusRequest Sensor Emulation Test. Complex Test.");
    randomSeed(analogRead(A1)+millis()); 
} 

void loop() 
{
    ts.execute(); 
}

7. Using local task storage pointer

Tasks can store a pointer to specific variable, structure or array, which represents variables specific for a particular task. This may be needed if you plan to use same callback method for multiple tasks.
Consider a scenario where you have several sensors of the same type. The actual process of triggering measurement and collecting information is identical. The only difference is the sensor address and a variable for storing the results. In this case each of the tasks, which performs measurement will utilize the same callback methods. The only difference will be the variables (specific for each of the sensor).

Let's define a sensor data structure and declare a couple of variables (for 2 sensors for instance)

typedef struct
{
    unsigned int address;
    unsigned long distance; 
} sensor_data; 
sensor_data s1, s2;

Two separate tasks are running to collect sensor data.
(Note that both tasks refer to the same callback methods)

Scheduler ts;
Task t1(100, TASK_FOREVER, &Measure, &ts, false, &MeasureOn);
Task t2(100, TASK_FOREVER, &Measure, &ts, false, &MeasureOn);

Assign pointers to the respective variables in the setup() method:

void setup() 
{
    …
    t1.setLtsPointer(&s1);
    t2.setLtsPointer(&s2);
    … 
}

Obtain reference to specific sensor_data structure inside the common callback method:

void Measure() 
{
    Task& T = ts.currentTask();
    Sensor_data& V = *((sensor_data*) T.getLtsPointer()); 
    // For t1, V will be pointing at s1
    // For t2, V will be pointing at s2

    // Alternatively use the Scheduler method:
    Sensor_data& V1 = *((sensor_data*) ts.currentLts());
    … 
    V.distance = <calculate your values here>; 
}

8. Enabling task prioritization

In certain cases you want a task to be invoked before others in case of scheduling collision (tasks ready to be invoked at the same time). In a flat execution chain scenario tasks are evaluated for execution in the order they were added to the chain. Therefore a single task has to wait for the rest of the chain to be evaluated to get a chance again.
Consider a scenario where a task taking gyroscope measurements has to be invoked as close to the actual scheduling time as possible. That is when task prioritization comes to help.
Let’s say tasks t4 and t5 are taking measurements from gyroscope and accelerometer, and tasks t1, t2 and t3 are doing something less important.

This is how such setup is coded:

#define _TASK_PRIORITY 
#include <TaskScheduler.h> 

Scheduler r, hpr; 

// Tasks
Task t1(1000, TASK_FOREVER, &tCallback, &r);  //base priority 
Task t2(2000, TASK_FOREVER, &tCallback, &r); 
Task t3(3000, TASK_FOREVER, &tCallback, &r); 

Task t4(10, TASK_FOREVER, &tCallback, &hpr);  // higher priority 
Task t5(100, TASK_FOREVER, &tCallback, &hpr);  //higher priority void setup () 
{
    …
    r.setHighPriorityScheduler(&hpr);
    r.enableAll(true); // this will recursively enable the higher priority tasks as well
}