Arduino – Scheduler and Pub/Sub Libraries

Arduino setup() and loop() functions make writing simple programs easy. However, for complicated programs the code can become hard to organize and understand. The Scheduler and Publisher/Subscriber libraries improve code readability, maintainability, extensibility and performance.

Issue

As my projects grew in complexity, they became difficult to read, maintain and extend. I researched the schedulers available for Arduino and concluded that I needed something simple to avoid the use of delay(), minimize memory usage and provided better code organization. Therefore, I wrote my own task scheduler and publisher/subscriber libraries which you can download using the link in the top right corner.

Solution – Scheduler

The scheduler maintains a linked list of tasks. Each task has a function, a period between calls and its next run time. Each time the scheduler’s run() function is called, it loops thru the linked list and calls the function of each task who’s nextTime has past.

The scheduler design:

Solution – Publisher/Subscriber

The pub/sub design pattern allows functions to be decoupled. Sensors are an excellent usage of this pattern. For instance you have a function that periodically reads data from a GPS, performs some calculations then posts a notifications to all subscribers that are interested in using the GPS data. Using the sub/pub pattern the GPS function has no relationship or knowledge of the functions that are consuming the data it is providing. Without the pub/sub pattern, the GPS function would need to personally call all the consumer functions leading to ugly code or the consumer function would need to poll for GPS data updates resulting in wasted CPU cycles.

The full benefit of the pub/sub design pattern is realized in operating systems that have real threads since the subscriber can run within a separate thread not blocking the publisher. For Arduino, the benefits are mostly improved readability since the relationships can be specified in the setup() function.

The publisher/subscriber design:

Solution – Example

The following example demonstrates how simple it is to use these libraries and the improvement in readability. Without the scheduler, the interrupt handler function could not be as short as it is.


#include <SPI.h>
#include <Wire.h>

#include <JB_Publisher_Subscriber.h>
#include <JB_TaskScheduler.h>

using namespace JB;

/* ****************************************************************************************************
    Global Variables
**************************************************************************************************** */
Scheduler* scheduler = new Scheduler();
Publisher* publisherLedOne;
Publisher* publisherLedTwo;

#define LED_1 0
#define LED_2 1
#define LED_3 2

#define BUTTON_1 3


/* ****************************************************************************************************
    Setup
**************************************************************************************************** */
void setup()
{
    /* -------------------------------------------------------------------------------
        Initialize IO.
    ------------------------------------------------------------------------------- */
    pinMode(LED_1, OUTPUT);
    pinMode(LED_2, OUTPUT);
    pinMode(LED_3, OUTPUT);

    digitalWrite(LED_1, LOW);
    digitalWrite(LED_2, LOW);
    digitalWrite(LED_3, LOW);

    pinMode(BUTTON_1, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(BUTTON_1), ButtonOnePressed, FALLING);

    /* -------------------------------------------------------------------------------
        Add tasks to the scheduler.
    ------------------------------------------------------------------------------- */
    scheduler->Add(new Task(ToggleLedOne, 500));

    /* -------------------------------------------------------------------------------
        Specify the relations between publishers and subscribers.
    ------------------------------------------------------------------------------- */
    publisherLedOne = new Publisher();
    publisherLedOne->Add(new Subscriber(ToggleLedThree));
    publisherLedTwo = new Publisher();
    publisherLedTwo->Add(new Subscriber(ToggleLedThree));
}


/* ****************************************************************************************************
    The loop() function only needs to call the Scheduler's run() function which then calls only 
    those tasks whose next run time has past.
**************************************************************************************************** */
void loop()
{
    scheduler->Run();
}


/* ****************************************************************************************************
    Toggle LED 1 is a scheduled task configured in setup().
**************************************************************************************************** */
bool isLedOneOn = false;
void ToggleLedOne()
{
    isLedOneOn = !isLedOneOn;

    if (isLedOneOn)
        digitalWrite(LED_1, HIGH);
    else
        digitalWrite(LED_1, LOW);

    publisherLedOne->Notify();
}


/* ****************************************************************************************************
    Toggle LED 2 is a scheduled task configured in the button interrupt function ButtonOnePressed().
**************************************************************************************************** */
bool isLedTwoOn = false;
void ToggleLedTwo()
{
    isLedTwoOn = !isLedTwoOn;

    if (isLedTwoOn)
        digitalWrite(LED_2, HIGH);
    else
        digitalWrite(LED_2, LOW);

    publisherLedTwo->Notify();
}


/* ****************************************************************************************************
    Toggle LED 3 is a subscriber to publisherLedOne and publisherLedTwo.
**************************************************************************************************** */
void ToggleLedThree()
{
    if (isLedOneOn && isLedTwoOn)
        digitalWrite(LED_3, HIGH);
    else
        digitalWrite(LED_3, LOW);
}


/* ****************************************************************************************************
    Button 1 Interrupt Handler toggles adding/removing ToggleLedTwo() to/from the scheduler.
**************************************************************************************************** */
/*  ---------------------------------------------------------------------------------------------------
    We want to create one instance of the Task, toggleLedTwoTask, to avoid a memory leak.  
    Since it is created outside the scope of the interrupt handler function, we need to use the volatile
    keyword to tell the compiler to not optimize out this instance (See: 
    https://www.keil.com/support/man/docs/armclang_intro/armclang_intro_chr1385110934192.htm).
    Since the task parameters are defined once, we need to use a Volatile pointer to non-volatile data 
    (See: https://barrgroup.com/embedded-systems/how-to/c-volatile-keyword).
---------------------------------------------------------------------------------------------------- */
Task* volatile toggleLedTwoTask = new Task(ToggleLedTwo, 250);

bool isLedTwoScheduled = false;
unsigned long buttonOneDebounceTime = millis();
void ButtonOnePressed()
{
    if (millis() > buttonOneDebounceTime)
    {
        isLedTwoScheduled = !isLedTwoScheduled;
        if (isLedTwoScheduled)
        {
            scheduler->Add(toggleLedTwoTask);
        }
        else
        {
            scheduler->Remove(toggleLedTwoTask);
        }
    }

    // Add time to wait before handing next call.
    buttonOneDebounceTime = millis() + 5;
}

Conclusion

The Task Scheduler and Publisher/Subscriber libraries make code more readable, maintainable, extensible and performant.

Published: 2021/02/01
Revised: 2023/01/18