Skip to content

myles-parfeniuk/data_control

Repository files navigation

image

Table of Contents
  1. About
  2. Getting Started
  3. Usage
  4. Program Flowchart
  5. License
  6. Contact

About

DataControl is a C++ component written for esp-idf v5.0+, intended to simplify the management of data and actions associated with that data.

What does it do?

  • Allows the creation of DataWrapper objects implemented as a template class, used to store data of any type.
  • Call-back functions can be registered to DataWrapper objects.
  • Call-back functions will be executed when the data of DataWrapper object is modified.

Why is it useful?

In embedded applications it's often required to take some sort of action that is associated with a piece of data.
For example, we might want to:

  • Detect the press of a button, then turn on an LED, and power on an external peripheral.
  • Sample a fan's tach signal, then adjust its PWM, and write its speed to an OLED.

Firmware can become complicated due to actions like these, especially in the context of a large system where several actions may need to be associated with a single piece of data. DataControl is intended to remedy this issue.

In the context of the above examples, with DataControl we can:

  • Store the status of the button within a DataWrapper as a member of a"Button" class, register 2 different call-backs to DataWrapper in separate classes (for ex. "LedBackend" and "ExtPeripheralBackend")
  • Store the speed of the fan within a DataWrapper object as a member of a "Fan" class, register 2 different call-backs to DataWrapper in separate classes (for ex. "OledBackend" & "PWMBackend")

When the data is modified due to code within the scope of the Fan or Button class, a task will be created and execute any call-back functions registered in the "LedBackend", "ExtPeripheralBackend", "PWMBackend", and "OledBackend" implementations. Meaning, any "backend" objects will be notified of new data, without being aware of each other's existence.

This leads to well organized firmware that is much easier to work with, especially as a system expands and grows.

It was originally inspired by an in-house component I used at a workplace; however, this component has been re-written without using that component as a reference.
Its functionality shares some similarities, but the implementation is different.

Getting Started

(back to top)

Adding to Project

  1. Create a "components" directory in the root workspace directory of your esp-idf project if it does not exist already.

    In workspace directory:

    mkdir components
  2. Cd into the components directory and clone the DataControl repo.

    cd components
    git clone https://github.com/myles-parfeniuk/data_control.git
  3. Ensure you clean your esp-idf project before rebuilding.
    Within esp-idf enabled terminal:

     idf.py fullclean

(back to top)

Usage

(back to top)

Quick Start

This is intended to be a quick-guide, api documentation generated with doxygen can be found in the documentation directory of the master branch.

(back to top)

Instantiating a DataWrapper Object

To instantiate a DataWrapper, instantiate one of its sub-classes. The differences are as follows:

  • CallAlways will execute follower callback functions always

  • CallDifferent will only execute follower callback functions if the current data is different from the new data set()

  • CallSame will only execute follower callback functions if the current data is the same as the new data set()

    Example syntax:

    //integer data with initial value of 0,
    //callbacks executed every time set() method is called (call always)
    DataControl::CallAlways<int16_t> number(0); 

    //bool data with initial value of false, 
    //and new data is different from current data (call different)
    DataControl::CallDifferent<bool> my_bool(false);

Another example using the second example from the About section:

  /*inside Fan class definition*/
  public:
  DataControl::CallAlways<int16_t> fan_speed(0);

(back to top)

Registering Callback with a DataWrapper Object

To register any desired call back functions, call the follow() method.

Example syntax:

   
   number.follow(
    //lambda call-back function that executes whenever set() is called on number
    [&number](int16_t new_data)
    {
      ESP_LOGI("Current Number: %d, New Number: %d", number.get(), new_data);
    });

Using the same fan example, it makes for good organization to register the call-back functions within the class constructors:

/*in main.cpp*/
Fan *fan = new Fan(/*init vals*/); //instantiate fan
PWMBackend *pwm = PWMBackend(*fan); //instantiate pwm with reference to fan
OledBackend *oled = OledBackend(*fan); //instantiate oled with reference to fan

//PWMBackend constructor in PWMBackend.cpp
PWMBackend:: PWMBackend(Fan &fan):
fan(fan) //initialize fan member as reference to fan
{
  fan->fan_speed.follow(
  [&this, &fan](int16_t new_speed){
    //set pwm according to new speed with PWMBackend method
    this->pwm_control(new_speed); 
  });

}

//OledBackend constructor in Oled.cpp
OledBackend:: OledBackend(Fan &fan):
fan(fan) //initialize fan member as reference to fan
{
  fan->fan_speed.follow(
  [&this, &fan](int16_t new_speed){
    //draw speed to OLED with OledBackend method when new fan speed reading is taken
    this->draw_speed(new_speed); 
  });

}

(back to top)

Modifying Data

Update the data within data wrapper object by calling the set() method, and all call-backs previously registered with the follow() method will be executed automatically. The call-backs will be executed in the order they were registered.

Example syntax:

   number.set(number.get() + 1); //increment number by one

Again, same example:

//inside a task within Fan.cpp
fan_speed.set(0);
while(1)
{
  fan_speed.set(take_tach_reading()); //set fan speed as result of new reading

  /*after set is called, pwm's callback function will execute first (it was registered first),
  * and oled's will execute second (it was registered second) */

  vTaskDelay(200/portTICK_PERIOD_MS); //delay for 200ms 
}

(back to top)

Unfollowing, Pausing, and Unpausing a Callbacks

If it is desired to stop executing a callback, its ID must be saved when it is registered.

Example syntax:

  uint16_t follower_id = 0; //callback function ID

   follower_id = number.follow(  //registering a call-back and saving its ID
    //lambda call-back function that executes whenever set() is called on number
    [&number](int16_t new_data)
    {
      ESP_LOGI("Current Number: %d, New Number: %d", number.get(), new_data);
    });

    //temporarily pauses this callback until un_pause is called, a paused callback never executes
    number.pause(follower_id);
    //un-pauses the callback such that it executes as normal
    number.un_pause(follower_id);
    //un-registers callback permanently  
    number.unfollow(follower_id); 

(back to top)

Quick Example

A quick example that generates some terminal output which can be seen in the gif below.

Code:

/*main.cpp*/

//standard library includes
#include <iostream> 

//in-house component includes
#include "DataControl.hpp"

//esp-idf component includes
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"

extern "C" void app_main()
{
    uint16_t counter_id = 0;

    DataControl::CallAlways<int16_t> counter(0); //initialize data with value of 0
    DataControl::CallDifferent<bool> over_five(false); //initialize data with value of false
    DataControl::CallDifferent<bool> over_ten(false); //initialize data with value of false

    //register callbacks
    counter_id = counter.follow(
    [&counter](int16_t new_data){
        //print current and new counts
        ESP_LOGI("Follower0", "| Current Count: %d | New Count: %d |", counter.get(), new_data); 
    });

    over_five.follow(
    [](bool new_data){
        ESP_LOGE("Follower1", "Count is now over five.");
    });

    over_ten.follow(
    [&counter, counter_id](bool new_data){
        counter.un_follow(counter_id); //un-follow counter when count exceeds ten
        ESP_LOGE("Follower2", "Count is now over ten, counter un-followed.");
    });

    while(1){
        
        counter.set(counter.get() + 1);
        vTaskDelay(500/portTICK_PERIOD_MS); //delay for 0.5 seconds
        if(counter.get() > 5){
           over_five.set(true); //set over_five true after 5 iterations of loop
        }

        if(counter.get() > 10){
            over_ten.set(true); //set over_ten true after 10 iterations of loop
        }
    }
}

Terminal Output:

image

(back to top)

More Examples

Examples are available in the DataControl directory of my esp_idf_cpp_examples repo:

https://github.com/myles-parfeniuk/esp_idf_cpp_examples

My other component ButtonDriver also utilizes DataControl:

https://github.com/myles-parfeniuk/button_driver

(back to top)

Program Flowchart

image

(back to top)

License

Distributed under the MIT License. See LICENSE.md for more information.

(back to top)

Contact

Myles Parfeniuk - myles.parfenyuk@gmail.com

Project Link: https://github.com/myles-parfeniuk/data_control

(back to top)

About

A C++ based data mangment component for esp-idf v5.0+

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages