Task APIs

Task

The Task component provides a cross-platform API around std::thread with some additional configuration of stack size, priority, and core affinity for FreeRTOS / ESP, as well as providing a callback API which enables interruptible sleeps and termination of the task.

Code examples for the task API are provided in the task example folder.

API Reference

Header File

Classes

class Task : public espp::BaseComponent

Task provides an abstraction over std::thread which optionally includes memory / priority configuration on ESP systems. It allows users to easily stop the task, and will automatically stop itself if destroyed.

There is also a utility function which can be used to get the info for the task of the current context, or for a provided Task object.

There is also a helper function to run a lambda on a specific core, which can be used to run a specific function on a specific core, as you might want to do when registering an interrupt driver on a specific core.

Basic Task Example

    espp::Task::configure_task_watchdog(1000ms);
    auto task_fn = [](std::mutex &m, std::condition_variable &cv) {
      static size_t task_iterations{0};
      fmt::print("Task: #iterations = {}\n", task_iterations);
      task_iterations++;
      // NOTE: sleeping in this way allows the sleep to exit early when the
      // task is being stopped / destroyed
      {
        std::unique_lock<std::mutex> lk(m);
        cv.wait_for(lk, 500ms);
      }
      // we don't want to stop, so return false
      return false;
    };
    auto task = espp::Task({.callback = task_fn,
                            .task_config = {.name = "Task 1"},
                            .log_level = espp::Logger::Verbosity::DEBUG});
    task.start();
    task.start_watchdog(); // start the watchdog timer for this task
    std::this_thread::sleep_for(num_seconds_to_run * 1s);
    task.stop_watchdog(); // stop the watchdog timer for this task
    // show explicitly stopping the task (though the destructor called at the
    // end of this scope would do it for us)
    task.stop();
    std::error_code ec;
    std::string watchdog_info = espp::Task::get_watchdog_info(ec);
    if (ec) {
      fmt::print("Error getting watchdog info: {}\n", ec.message());
    } else if (!watchdog_info.empty()) {
      fmt::print("Watchdog info: {}\n", watchdog_info);
    } else {
      fmt::print("No watchdog info available\n");
    }

Task Watchdog Example

    static constexpr bool panic_on_watchdog_timeout = false;
    espp::Task::configure_task_watchdog(300ms, panic_on_watchdog_timeout);
    auto task_fn = [](std::mutex &m, std::condition_variable &cv) {
      static size_t task_iterations{0};
      fmt::print("Task: #iterations = {}\n", task_iterations);
      task_iterations++;
      std::unique_lock<std::mutex> lk(m);
      // note our sleep here is longer than the watchdog timeout, so we should
      // trigger the watchdog timeout
      cv.wait_for(lk, 500ms);
      // we don't want to stop, so return false
      return false;
    };
    auto task = espp::Task({.callback = task_fn,
                            .task_config = {.name = "Task 1"},
                            .log_level = espp::Logger::Verbosity::DEBUG});
    task.start();
    task.start_watchdog(); // start the watchdog timer for this task
    std::this_thread::sleep_for(500ms);
    std::error_code ec;
    std::string watchdog_info = espp::Task::get_watchdog_info(ec);
    if (ec) {
      fmt::print("Error getting watchdog info: {}\n", ec.message());
    } else if (!watchdog_info.empty()) {
      fmt::print("Watchdog info: {}\n", watchdog_info);
    } else {
      fmt::print("No watchdog info available\n");
    }
    // NOTE: the task and the watchdog will both automatically get stopped when
    // the task goes out of scope and is destroyed.

Many Task Example

    std::vector<std::unique_ptr<espp::Task>> tasks;
    size_t num_tasks = 10;
    logger.info("Many task example: spawning {} tasks!", num_tasks);
    tasks.resize(num_tasks);
    auto start = std::chrono::high_resolution_clock::now();
    for (size_t i = 0; i < num_tasks; i++) {
      size_t iterations{0};
      // copy the loop variables and indicate that we intend to mutate them!
      auto task_fn = [i, iterations](std::mutex &m, std::condition_variable &cv) mutable {
        fmt::print("Task {}: #iterations = {}\n", i, iterations);
        iterations++;
        // NOTE: sleeping in this way allows the sleep to exit early when the
        // task is being stopped / destroyed
        {
          std::unique_lock<std::mutex> lk(m);
          cv.wait_for(lk, 1s);
        }
        // we don't want to stop, so return false
        return false;
      };
      std::string task_name = fmt::format("Task {}", i);
      auto task =
          espp::Task::make_unique({.callback = task_fn, .task_config = {.name = task_name}});
      tasks[i] = std::move(task);
      tasks[i]->start();
    }
    fmt::print("Tasks spawned, waiting for {} seconds!\n", num_seconds_to_run);
    std::this_thread::sleep_until(start + num_seconds_to_run * 1s);

Long Running Task Example

    auto task_fn = [](std::mutex &m, std::condition_variable &cv) {
      static size_t task_iterations{0};
      const size_t num_steps_per_iteration = 10;
      fmt::print("Task processing iteration {}...\n", task_iterations);
      for (size_t i = 0; i < num_steps_per_iteration; i++) {
        // NOTE: sleeping in this way allows the sleep to exit early when the
        // task is being stopped / destroyed
        {
          std::unique_lock<std::mutex> lk(m);
          // NOTE: using the return value from the cv.wait_for() allows us to
          // know if the task was asked to stop, for which we can handle and
          // return early.
          auto cv_retval = cv.wait_for(lk, std::chrono::milliseconds(100));
          if (cv_retval == std::cv_status::no_timeout) {
            // if there was no timeout, then we were notified, therefore we need
            // to shut down.
            fmt::print("Task stopping early (step {}/{}) on iteration {}\n", i,
                       num_steps_per_iteration, task_iterations);
            // NOTE: use this_thread::sleep_for() to fake cleaning up work that
            // we would do
            std::this_thread::sleep_for(10ms);
            // now that we've (fake) cleaned-up our work, return from the task
            // function so the task can fully destruct.
            return false;
          }
        }
      }
      fmt::print("Task processing iteration {} complete\n", task_iterations);
      task_iterations++;
      // we don't want to stop, so return false
      return false;
    };
    auto task = espp::Task({.callback = task_fn,
                            .task_config = {.name = "Complex Task"},
                            .log_level = espp::Logger::Verbosity::DEBUG});
    task.start();

Long Running Task Notified Example (Recommended)

    auto task_fn = [](std::mutex &m, std::condition_variable &cv, bool &task_notified) {
      static size_t task_iterations{0};
      const size_t num_steps_per_iteration = 10;
      fmt::print("Task processing iteration {}...\n", task_iterations);
      for (size_t i = 0; i < num_steps_per_iteration; i++) {
        // NOTE: sleeping in this way allows the sleep to exit early when the
        // task is being stopped / destroyed
        {
          std::unique_lock<std::mutex> lk(m);
          // NOTE: using the return value from the cv.wait_for() allows us to
          // know if the task was asked to stop, for which we can handle and
          // return early.
          auto stop_requested = cv.wait_for(lk, std::chrono::milliseconds(100),
                                            [&task_notified] { return task_notified; });
          if (stop_requested) {
            fmt::print("Task was notified, stopping early (step {}/{}) on iteration {}\n", i,
                       num_steps_per_iteration, task_iterations);
            // NOTE: use this_thread::sleep_for() to fake cleaning up work that
            // we would do
            std::this_thread::sleep_for(10ms);
            // now that we've (fake) cleaned-up our work, return from the task
            // function so the task can fully destruct.
            return false;
          }
        }
      }
      fmt::print("Task processing iteration {} complete\n", task_iterations);
      task_iterations++;
      // we don't want to stop, so return false
      return false;
    };
    auto task = espp::Task({.callback = task_fn,
                            .task_config = {.name = "Notified Complex Task"},
                            .log_level = espp::Logger::Verbosity::DEBUG});
    task.start();

Task Info Example

    auto task_fn = [](std::mutex &m, std::condition_variable &cv) {
      static size_t task_iterations{0};
      task_iterations++;
      // allocate stack
      size_t num_bytes = 256 * task_iterations;
      char buffer[num_bytes];
      // do something with the bufer (which also uses stack)
      snprintf(buffer, num_bytes, "%.06f\n", (float)task_iterations);
      fmt::print("{}\n", espp::Task::get_info());
      // NOTE: sleeping in this way allows the sleep to exit early when the
      // task is being stopped / destroyed
      {
        std::unique_lock<std::mutex> lk(m);
        cv.wait_for(lk, 500ms);
      }
      // we don't want to stop, so return false
      return false;
    };
    auto task = espp::Task({.callback = task_fn,
                            .task_config = {.name = "DynamicTask"},
                            .log_level = espp::Logger::Verbosity::DEBUG});
    task.start();
    auto now = std::chrono::high_resolution_clock::now();
    auto elapsed = std::chrono::duration<float>(now - test_start).count();
    while (elapsed < num_seconds_to_run) {
      std::this_thread::sleep_for(200ms);
      now = std::chrono::high_resolution_clock::now();
      elapsed = std::chrono::duration<float>(now - test_start).count();
    }
    fmt::print("Final: {}\n", espp::Task::get_info(task));

Task Request Stop Example

    auto task_fn = [&num_seconds_to_run](std::mutex &m, std::condition_variable &cv) {
      static auto begin = std::chrono::high_resolution_clock::now();
      auto now = std::chrono::high_resolution_clock::now();
      auto elapsed = std::chrono::duration<float>(now - begin).count();
      if (elapsed > num_seconds_to_run) {
        // we've gone long enough, time to stop our task!
        return true;
      }
      // NOTE: sleeping in this way allows the sleep to exit early when the
      // task is being stopped / destroyed
      {
        std::unique_lock<std::mutex> lk(m);
        cv.wait_for(lk, 10ms);
      }
      // we don't want to stop yet, so return false
      return false;
    };
    auto task = espp::Task({.callback = task_fn,
                            .task_config = {.name = "AutoStop Task"},
                            .log_level = espp::Logger::Verbosity::DEBUG});
    task.start();
    while (task.is_started()) {
      std::this_thread::sleep_for(10ms);
    }

Run on Core Example

    logger.info("espp::task::run_on_core example: main thread core ID: {}", xPortGetCoreID());
    // NOTE: in these examples, because we're logging with libfmt in the
    // function to be run, we need a little more than the default 2k stack size,
    // so we're using 3k.

    // test running a function that returns void on a specific core
    auto task_fn = []() -> void { fmt::print("Void Task running on core {}\n", xPortGetCoreID()); };
    espp::task::run_on_core(task_fn, 0, 3 * 1024);
    fmt::print("Void Function returned\n");
    espp::task::run_on_core(task_fn, 1, 3 * 1024);
    fmt::print("Void Function returned\n");

    // test running a function that returns bool on a specific core
    auto task_fn2 = []() -> bool {
      auto core_id = xPortGetCoreID();
      fmt::print("Bool Task running on core {}\n", core_id);
      return core_id == 1;
    };
    auto result0 = espp::task::run_on_core(task_fn2, 0, 3 * 1024);
    fmt::print("Bool Function returned {}\n", result0);
    auto result1 = espp::task::run_on_core(
        task_fn2, {.name = "test", .stack_size_bytes = 3 * 1024, .core_id = 1});
    fmt::print("Bool Function returned {}\n", result1);

    // test running a function that returns esp_err_t on a specific core
    auto task_fn3 = []() -> esp_err_t {
      auto core_id = xPortGetCoreID();
      fmt::print("esp_err_t Task running on core {}\n", core_id);
      return core_id == 1 ? ESP_OK : ESP_FAIL;
    };
    auto err0 = espp::task::run_on_core(task_fn3, 0, 3 * 1024);
    fmt::print("esp_err_t Function returned {}\n", esp_err_to_name(err0));
    auto err1 = espp::task::run_on_core(task_fn3, 1, 3 * 1024);
    fmt::print("esp_err_t Function returned {}\n", esp_err_to_name(err1));

Run on Core (Non-Blocking) Example

    logger.info("espp::task::run_on_core non-blocking example: main thread core ID: {}",
                xPortGetCoreID());
    // NOTE: in these examples, because we're logging with libfmt in the
    // function to be run, we need a little more than the default 2k stack size,
    // so we're using 3k.

    // test running a function which takes a while to complete
    auto task_fn = []() -> void {
      fmt::print("[{0}] Task running on core {0}\n", xPortGetCoreID());
      std::this_thread::sleep_for(1s);
      fmt::print("[{0}] Task done!\n", xPortGetCoreID());
    };
    espp::task::run_on_core_non_blocking(task_fn, 0, 3 * 1024);
    espp::task::run_on_core_non_blocking(task_fn, {.name = "test", .core_id = 1});
    fmt::print("Started tasks on cores 0 and 1\n");

    // sleep for a bit to let the tasks run
    std::this_thread::sleep_for(2s);

Public Types

typedef std::function<bool(std::mutex &m, std::condition_variable &cv, bool &notified)> callback_m_cv_notified_fn

Task callback function signature.

Note

The callback is run repeatedly within the Task, therefore it MUST return, and also SHOULD have a sleep to give the processor over to other tasks. For this reason, the callback is provided a std::condition_variable (and associated mutex) which the callback can use when they need to wait. If the cv.wait_for / cv.wait_until return false, no action is necessary (as the timeout properly occurred / the task was not notified). If they return true, then the callback function should return immediately since the task is being stopped (optionally performing any task-specific tear-down).

Param m

mutex associated with the condition variable (should be locked before calling cv.wait_for() or cv.wait_until())

Param cv

condition variable the callback can use to perform an interruptible wait. Use the notified parameter as the predicate for the wait to ensure spurious wake-ups are properly ignored.

Param notified

Whether or not the task has been notified to stop. This is a reference to the task’s notified member, and is intended to be used with the condition variable parameter cv when waiting to avoid spurious wakeups. The task function is responsible for clearing this flag after each wait. Do not release the lock on the mutex m until the notified flag has been checked and cleared.

Return

Whether or not the callback’s thread / task should stop - True to stop, false to continue running.

typedef std::function<bool(std::mutex &m, std::condition_variable &cv)> callback_m_cv_fn

Task callback function signature.

Note

The callback is run repeatedly within the Task, therefore it MUST return, and also SHOULD have a sleep to give the processor over to other tasks. For this reason, the callback is provided a std::condition_variable (and associated mutex) which the callback can use when they need to wait. If the cv.wait_for / cv.wait_until return std::cv_status::timeout, no action is necessary, but if they return std::cv_status::no_timeout, then the function should return immediately since the task is being stopped (optionally performing any task-specific tear-down).

Warning

This is an older callback function signature, and is kept for backwards compatibility. It is recommended to use the newer callback signature (callback_m_cv_notified_fn) which includes the notified parameter, enabling the task callback function to wait on the condition variable and ignore spurious wakeups.

Param m

mutex associated with the condition variable (should be locked before calling cv.wait_for() or cv.wait_until())

Param cv

condition variable the callback can use to perform an interruptible wait.

Return

Whether or not the callback’s thread / task should stop - True to stop, false to continue running.

typedef std::function<bool()> callback_no_params_fn

Simple callback function signature.

Note

The callback is run repeatedly within the Task, therefore it MUST return, and also SHOULD have a sleep to give the processor over to other tasks.

Return

True to stop the task, false to continue running.

typedef std::variant<callback_m_cv_notified_fn, callback_m_cv_fn, callback_no_params_fn> callback_variant

Variant of the callback function for the task.

This is primarily used to enable simpler API upgrades in the future and maximize backwards compatibility.

Note

This is a std::variant of the different callback function signatures that can be used with the Task. This allows the Task to be configured with a callback function that takes no parameters, a callback function that takes a mutex and condition variable, or a callback function that takes a mutex, condition variable, and a bool reference to the notified flag.

Public Functions

explicit Task(const espp::Task::Config &config)

Construct a new Task object using the Config struct.

Parameters

configConfig struct to initialize the Task with.

~Task()

Destroy the task, stopping it if it was started.

bool start()

Start executing the task.

Returns

true if the task started, false if it was already started.

bool stop()

Stop the task execution.

This will request the task to stop, notify the condition variable, and (if this calling context is not the task context) join the thread.

Returns

true if the task stopped, false if it was not started / already stopped.

bool is_started() const

Has the task been started or not?

Returns

true if the task is started / running, false otherwise.

bool is_running() const

Is the task running?

Returns

true if the task is running, false otherwise.

bool start_watchdog()

Start the task watchdog for this task.

Note

This function is only available on ESP

Note

This function will do nothing unless CONFIG_ESP_TASK_WDT_EN is enabled in the menuconfig. Default is y (enabled).

Returns

true if the watchdog was started, false otherwise.

bool stop_watchdog()

Stop the task watchdog for this task.

Note

This function is only available on ESP

Note

This function will do nothing unless CONFIG_ESP_TASK_WDT_EN is enabled in the menuconfig. Default is y (enabled).

Returns

true if the watchdog was stopped, false otherwise.

inline task_id_t get_id() const

Get the ID for this Task’s thread / task context.

Returns

ID for this Task’s thread / task context.

inline const std::string &get_name() const

Get the name of the component

Note

This is the tag of the logger

Returns

A const reference to the name of the component

inline void set_log_tag(const std::string_view &tag)

Set the tag for the logger

Parameters

tag – The tag to use for the logger

inline espp::Logger::Verbosity get_log_level() const

Get the log level for the logger

Returns

The verbosity level of the logger

inline void set_log_level(espp::Logger::Verbosity level)

Set the log level for the logger

Parameters

level – The verbosity level to use for the logger

inline void set_log_verbosity(espp::Logger::Verbosity level)

Set the log verbosity for the logger

See also

set_log_level

Note

This is a convenience method that calls set_log_level

Parameters

level – The verbosity level to use for the logger

inline espp::Logger::Verbosity get_log_verbosity() const

Get the log verbosity for the logger

See also

get_log_level

Note

This is a convenience method that calls get_log_level

Returns

The verbosity level of the logger

inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)

Set the rate limit for the logger

Note

Only calls to the logger that have _rate_limit suffix will be rate limited

Parameters

rate_limit – The rate limit to use for the logger

Public Static Functions

static std::unique_ptr<Task> make_unique(const espp::Task::Config &config)

Get a unique pointer to a new task created with config. Useful to not have to use templated std::make_unique (less typing).

Parameters

configConfig struct to initialize the Task with.

Returns

std::unique_ptr<Task> pointer to the newly created task.

static bool configure_task_watchdog(uint32_t timeout_ms, bool panic_on_timeout = true)

Initialize/Configure the task watchdog for the current task.

Note

This function is only available on ESP

Note

This function will do nothing unless CONFIG_ESP_TASK_WDT_EN is enabled in the menuconfig. Default is y (enabled).

Note

This function will not monitor the idle tasks.

Note

If the watchdog has not been configured, then this function will call esp_task_wdt_init, otherwise it will then call esp_task_wdt_reconfigure.

Parameters
  • timeout_ms – Timeout in milliseconds for the watchdog.

  • panic_on_timeout – Whether or not to panic on timeout.

Returns

true if the watchdog was initialized, false otherwise.

static bool configure_task_watchdog(const std::chrono::milliseconds &timeout, bool panic_on_timeout = true)

Initialize/Configure the task watchdog for the current task.

Note

This function is only available on ESP

Note

This function will do nothing unless CONFIG_ESP_TASK_WDT_EN is enabled in the menuconfig. Default is y (enabled).

Note

This function will not monitor the idle tasks.

Note

If the watchdog has not been configured, then this function will call esp_task_wdt_init, otherwise it will then call esp_task_wdt_reconfigure.

Parameters
  • timeout – The timeout for the watchdog.

  • panic_on_timeout – Whether or not to panic on timeout.

Returns

true if the watchdog was initialized, false otherwise.

static std::string get_watchdog_info(std::error_code &ec)

Retrieve the info about tasks / users which triggered the task watchdog timeout.

Note

This function is only available on ESP

Note

This function will do nothing unless CONFIG_ESP_TASK_WDT_EN is enabled in the menuconfig. Default is y (enabled).

Note

This function will only return info for tasks which are still registered with the watchdog. If you call this after you have called stop_watchdog() for a task, then even if the task triggered the watchdog timeout you will not get that information.

Parameters

ec – Error code to set if there was an error retrieving the info.

Returns

std::string containing the task watchdog info, or an empty string if there was no timeout or there was an error retrieving the info.

static std::string get_info()

Get the info (as a string) for the task of the current context.

Note

This function is only available on ESP

Returns

std::string containing name, core ID, priority, and stack high water mark (B)

static std::string get_info(const Task &task)

Get the info (as a string) for the provided task.

Note

This function is only available on ESP

Parameters

task – Reference to the task for which you want the information.

Returns

std::string containing name, core ID, priority, and stack high water mark (B)

static inline task_id_t get_current_id()

Get the ID for the current thread / task context.

Returns

ID for the current thread / task context.

struct BaseConfig

Base configuration struct for the Task.

Note

This is designed to be used as a configuration struct in other classes that may have a Task as a member.

Public Members

std::string name

Name of the task

size_t stack_size_bytes = {4096}

Stack Size (B) allocated to the task.

size_t priority = {0}

Priority of the task, 0 is lowest priority on ESP / FreeRTOS.

int core_id = {-1}

Core ID of the task, -1 means it is not pinned to any core.

struct Config

Configuration struct for the Task. Can be initialized with any of the supported callback function signatures.

Public Members

espp::Task::callback_variant callback

Callback function

espp::Task::BaseConfig task_config

Base configuration for the task.

espp::Logger::Verbosity log_level = {espp::Logger::Verbosity::WARN}

Log verbosity for the task.

Header File