Bläddra i källkod

KBF-31 rework task handling

Bence Balint 3 år sedan
förälder
incheckning
b32b00bb79
8 ändrade filer med 384 tillägg och 3 borttagningar
  1. 1 0
      CMakeLists.txt
  2. 30 0
      include/kbf/exception.h
  3. 1 0
      include/kbf/rtos.h
  4. 139 0
      include/kbf/task.h
  5. 62 0
      src/task.cpp
  6. 1 1
      test/rtos/test_task.cpp
  7. 146 0
      test/test_task.cpp
  8. 4 2
      test_app/sdkconfig

+ 1 - 0
CMakeLists.txt

@@ -16,6 +16,7 @@ idf_component_register(
         "src/spiffs.cpp"
         "src/rtos/event_group.cpp"
         "src/rtos/task.cpp"
+        "src/task.cpp"
         "src/uart.cpp"
         "src/web_service.cpp"
         "src/wifi/ap.cpp"

+ 30 - 0
include/kbf/exception.h

@@ -0,0 +1,30 @@
+#ifndef KBF_EXCEPTION_H
+#define KBF_EXCEPTION_H
+
+#include <exception>
+#include <string>
+
+#include <esp_err.h>
+
+class ESPError : public std::exception {
+    const esp_err_t err;
+public:
+    explicit ESPError(const esp_err_t err) : err(err) {};
+
+    [[nodiscard]] const char *
+    what() const _GLIBCXX_TXN_SAFE_DYN _GLIBCXX_USE_NOEXCEPT override { return esp_err_to_name(err); }
+};
+
+class RTOSError : public std::exception {
+    const std::string message;
+public:
+    explicit RTOSError(const uint32_t err) : message("RTOS error: " + std::to_string(err)) {};
+
+    [[nodiscard]] const char *
+    what() const _GLIBCXX_TXN_SAFE_DYN _GLIBCXX_USE_NOEXCEPT override { return message.c_str(); }
+};
+
+class OutOfMemoryError : public std::exception {
+};
+
+#endif //KBF_EXCEPTION_H

+ 1 - 0
include/kbf/rtos.h

@@ -11,6 +11,7 @@
 namespace kbf::rtos {
     /**
      * @brief RTOS task handling functions.
+     * @deprecated use kbf::task instead
      */
     class Task {
     public:

+ 139 - 0
include/kbf/task.h

@@ -0,0 +1,139 @@
+#ifndef KBF_TASK_H
+#define KBF_TASK_H
+
+#include <string>
+#include <vector>
+#include <memory>
+#include <utility>
+
+#include <freertos/FreeRTOS.h>
+#include <freertos/task.h>
+
+#include "kbf/exception.h"
+
+/**
+ * @brief Task handling functions.
+ */
+namespace kbf::task {
+    /** @brief Tag used for logging. */
+    static constexpr const char *const TAG = "kbf::task";
+
+    class Task;
+
+    template<class T>
+    std::shared_ptr<Task>
+    start(const std::string &name, void *arg, uint32_t stackSize = 2048, uint32_t priority = 5);
+
+    /**
+     * @brief Base class for tasks.
+     * To start a task, call kbf::task::start<MyTask>() where MyTask is a subclass of Task.
+     */
+    class Task {
+        template<class T>
+        friend std::shared_ptr<Task>
+        start(const std::string &name, void *arg,  // NOLINT(readability-redundant-declaration
+              uint32_t stackSize, uint32_t priority);
+
+    public:
+        /** @brief Name of the RTOS task. */
+        const std::string name;
+
+        /** @brief Task stack size. Default is 2048. */
+        [[maybe_unused]] const uint32_t stackSize;
+
+        /** @brief Task priority. Default is 5. */
+        [[maybe_unused]] const uint32_t priority;
+
+        /** @brief Tag used for logging. */
+        static constexpr const char *const TAG = "kbf::task::Task";
+
+        Task() = delete;
+
+        /**
+         * @brief Returns whether the task is running or not.
+         * @return true if running, false otherwise
+         */
+        [[nodiscard]] bool running() const { return _running; }
+
+        /**
+         * @brief Stops the task.
+         */
+        void stop();
+
+    protected:
+        /**
+         * @brief Constructor.
+         * @param name name of the RTOS task
+         * @param arg arguments to be passed to the task
+         * @param stackSize stack size
+         * @param priority priority
+         */
+        explicit Task(std::string name, void *arg, uint32_t stackSize, uint32_t priority);
+
+        /**
+         * @brief Function to be called when the task is started.
+         * @param arg arguments
+         */
+        virtual void run(void *arg) = 0;
+
+        /**
+         * @brief Destructor.
+         */
+        ~Task();
+
+    private:
+        const uint32_t id;
+
+        [[maybe_unused]] static void runInternal(void *arg);
+
+        void *arg;
+
+        TaskHandle_t handle{};
+
+        bool _running = false;
+
+        class TaskList {
+            static constexpr const char *const TAG = "kbf::task::TaskList";
+            template<class T>
+            friend std::shared_ptr<Task>
+            start(const std::string &name, void *arg,  // NOLINT(readability-redundant-declaration
+                  uint32_t stackSize, uint32_t priority);
+
+            friend class Task;
+
+            static std::vector<std::shared_ptr<Task>> tasks;
+
+            static void add(const std::shared_ptr<Task> &task);
+
+            static bool remove(uint32_t id);
+        };
+    };
+
+    /**
+     * @brief Starts a task.
+     * @tparam T must be subclass of kbf::task::Task
+     * @param name RTOS task name
+     * @param arg arguments passed to run()
+     * @param stackSize stack size
+     * @param priority priority
+     * @return shared_ptr to the newly created task
+     */
+    template<class T>
+    std::shared_ptr<Task>
+    start(const std::string &name, void *arg, const uint32_t stackSize, const uint32_t priority) {
+        static_assert(std::is_base_of<Task, T>::value, "kbf::task::run(): type must be a kbf::task::Task");
+        auto task = std::make_shared<T>(name, arg, stackSize, priority);
+
+        esp_err_t err = xTaskCreate(Task::runInternal, name.c_str(), stackSize, task.get(), priority, &task->handle);
+        if (err == errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY) {
+            throw OutOfMemoryError();
+        } else if (err != pdPASS) {
+            throw RTOSError(err);
+        }
+
+        Task::TaskList::add(task);
+        return task;
+    }
+}
+
+#endif //KBF_TASK_H

+ 62 - 0
src/task.cpp

@@ -0,0 +1,62 @@
+#include "kbf/task.h"
+
+#include <esp_log.h>
+
+using std::string;
+using std::vector;
+using std::shared_ptr;
+using std::make_shared;
+using namespace kbf::task;
+
+static uint32_t taskId = 0;
+
+std::vector<std::shared_ptr<Task>> Task::TaskList::tasks;
+
+void Task::TaskList::add(const shared_ptr<Task>& task) {
+    ESP_LOGD(TAG, "%s(%d), refcount=%ld", __func__, task->id, task.use_count());
+    tasks.push_back(task);
+};
+
+bool Task::TaskList::remove(uint32_t id) {
+    ESP_LOGD(TAG, "%s(%d)", __func__, id);
+    for (auto it = tasks.begin(); it != tasks.end(); it++) {
+        if ((*it)->id == id) {
+            tasks.erase(it);
+            return true;
+        }
+    }
+    ESP_LOGE(TAG, "%s(%d): not found", __func__, id);
+    return false;
+}
+
+Task::Task(string name, void *arg, const uint32_t stackSize, const uint32_t priority) :
+        name(std::move(name)),
+        stackSize(stackSize),
+        priority(priority),
+        id(taskId++),
+        arg(arg) {
+    ESP_LOGI(TAG, "%s(): id=%d, name=\"%s\"", __func__, id, this->name.c_str());
+}
+
+Task::~Task() {
+    ESP_LOGI(TAG, "%s(%d)", __func__, id);
+}
+
+[[maybe_unused]] void Task::runInternal(void *arg) {
+    ESP_LOGD(TAG, "%s()", __func__);
+
+    auto task = static_cast<Task *>(arg);
+    task->_running = true;
+    task->run(task->arg);
+
+    // TODO call onFinish
+    task->_running = false;
+    TaskList::remove(task->id);
+    vTaskDelete(nullptr);
+}
+
+void Task::stop() {
+    ESP_LOGI(TAG, "%s(%d)", __func__, id);
+    TaskList::remove(id);
+    vTaskDelete(handle);
+}

+ 1 - 1
test/rtos/test_task.cpp

@@ -19,7 +19,7 @@
 
 using namespace std;
 
-atomic<int> counter{0};
+static atomic<int> counter{0};
 
 /** function that runs until stopped */
 [[noreturn]] void runner(void *arg);

+ 146 - 0
test/test_task.cpp

@@ -0,0 +1,146 @@
+#include <atomic>
+
+#include <esp_log.h>
+
+#include "kbf.h"
+#include "kbf/task.h"
+
+#include <unity.h>
+
+/** delay between cycles */
+#define KBF_TASK_TEST_DELAY 200
+
+/** counter increase factor for the "runner" task */
+#define KBF_TASK_TEST_RUNNER_INC 10
+
+/** counter increase factor for the "quitter" task */
+#define KBF_TASK_TEST_QUITTER_INC 1
+
+/** number of cycles to run for each testing stage */
+#define KBF_TASK_TEST_CYCLES 5
+
+using namespace std;
+using namespace kbf;
+
+static atomic<int> counter{0};
+
+/** runs until stopped */
+class Runner : public task::Task {
+public:
+    Runner(const string &name, void *arg, const uint32_t stackSize, const uint32_t priority) :
+            Task(name, arg, stackSize, priority) {}
+
+    static constexpr const char *const TAG = "Runner";
+
+protected:
+    [[noreturn]] void run(void *arg) override {
+        auto delay = static_cast<int *>(arg);
+        TEST_ASSERT_EQUAL(KBF_TASK_TEST_DELAY, *delay);
+
+        int i = 0;
+        while (true) {
+            ESP_LOGI(TAG, "cycle %d", i++);
+            counter += KBF_TASK_TEST_RUNNER_INC;
+            kbf::sleep(*delay);
+        }
+    }
+};
+
+struct QuitterArg {
+    int delay;
+    int cycles;
+};
+
+/** stops after KBF_TASK_TEST_CYCLES cycles */
+class Quitter : public task::Task {
+public:
+    Quitter(const string &name, void *arg, const uint32_t stackSize, const uint32_t priority) :
+            Task(name, arg, stackSize, priority) {}
+
+    static constexpr const char *const TAG = "Quitter";
+
+protected:
+    void run(void *arg) override {
+        auto argData = static_cast<QuitterArg *>(arg);
+
+        TEST_ASSERT_EQUAL(KBF_TASK_TEST_DELAY, argData->delay);
+        TEST_ASSERT_EQUAL(KBF_TASK_TEST_CYCLES, argData->cycles);
+
+        int i;
+        for (i = 0; i < argData->cycles; i++) {
+            ESP_LOGI(TAG, "cycle %d/%d", i, argData->cycles);
+            counter += KBF_TASK_TEST_QUITTER_INC;
+            kbf::sleep(argData->delay);
+        }
+
+        ESP_LOGI(TAG, "done: %d/%d", i, argData->cycles);
+    }
+};
+
+TEST_CASE("Task", "[kbf_task]") {
+    counter = 0;
+
+    uint32_t   delay      = KBF_TASK_TEST_DELAY;
+    QuitterArg quitterArg = {KBF_TASK_TEST_DELAY, KBF_TASK_TEST_CYCLES};
+
+    auto runnerTask  = task::start<Runner>("test_runner", &delay);
+    auto quitterTask = task::start<Quitter>("test_quitter", &quitterArg);
+
+    kbf::sleep(40); // make sure the tasks have started
+
+    int expected = 0;
+
+    // both tasks are running
+    for (int i = 0; i < KBF_TASK_TEST_CYCLES; i++) {
+        expected += KBF_TASK_TEST_RUNNER_INC + KBF_TASK_TEST_QUITTER_INC;
+        TEST_ASSERT_EQUAL(expected, counter);
+        TEST_ASSERT_TRUE(runnerTask->running())
+        TEST_ASSERT_TRUE(quitterTask->running())
+        kbf::sleep(KBF_TASK_TEST_DELAY);
+    }
+
+    // quitter has quit, only runner is left
+    for (int i = 0; i < KBF_TASK_TEST_CYCLES; i++) {
+        expected += KBF_TASK_TEST_RUNNER_INC;
+        TEST_ASSERT_EQUAL(expected, counter);
+        TEST_ASSERT_TRUE(runnerTask->running())
+        TEST_ASSERT_FALSE(quitterTask->running())
+        kbf::sleep(KBF_TASK_TEST_DELAY);
+    }
+
+    // stop runner; no more updates
+    runnerTask->stop();
+    expected += KBF_TASK_TEST_RUNNER_INC; // runner went another cycle while we were sleeping
+    for (int i = 0; i < KBF_TASK_TEST_CYCLES; i++) {
+        TEST_ASSERT_EQUAL(expected, counter);
+        kbf::sleep(KBF_TASK_TEST_DELAY);
+    }
+}
+
+static QuitterArg scopeTestArg = {KBF_TASK_TEST_DELAY, KBF_TASK_TEST_CYCLES};
+
+void createTask() {
+    task::start<Quitter>("test_task_scope", &scopeTestArg);
+}
+
+TEST_CASE("Task out of scope", "[kbf_task]") {
+    counter = 0;
+    createTask();
+    kbf::sleep(KBF_TASK_TEST_DELAY * KBF_TASK_TEST_CYCLES);
+
+    TEST_ASSERT_EQUAL(KBF_TASK_TEST_CYCLES * KBF_TASK_TEST_QUITTER_INC, counter);
+}
+
+TEST_CASE("Task out of memory", "[kbf_task]") {
+    QuitterArg quitterArg = {KBF_TASK_TEST_DELAY, KBF_TASK_TEST_CYCLES};
+    counter = 0;
+
+    bool caught = false;
+    try {
+        task::start<Quitter>("test_task_oom", &quitterArg, 2 << 24);
+    } catch (OutOfMemoryError &e) {
+        caught = true;
+    }
+
+    TEST_ASSERT_TRUE(caught)
+}

+ 4 - 2
test_app/sdkconfig

@@ -135,7 +135,8 @@ CONFIG_COMPILER_OPTIMIZATION_DEFAULT=y
 CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE=y
 # CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT is not set
 # CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE is not set
-# CONFIG_COMPILER_CXX_EXCEPTIONS is not set
+CONFIG_COMPILER_CXX_EXCEPTIONS=y
+CONFIG_COMPILER_CXX_EXCEPTIONS_EMG_POOL_SIZE=0
 # CONFIG_COMPILER_CXX_RTTI is not set
 CONFIG_COMPILER_STACK_CHECK_MODE_NONE=y
 # CONFIG_COMPILER_STACK_CHECK_MODE_NORM is not set
@@ -1200,7 +1201,8 @@ CONFIG_COMPILER_OPTIMIZATION_LEVEL_DEBUG=y
 CONFIG_OPTIMIZATION_ASSERTIONS_ENABLED=y
 # CONFIG_OPTIMIZATION_ASSERTIONS_SILENT is not set
 # CONFIG_OPTIMIZATION_ASSERTIONS_DISABLED is not set
-# CONFIG_CXX_EXCEPTIONS is not set
+CONFIG_CXX_EXCEPTIONS=y
+CONFIG_CXX_EXCEPTIONS_EMG_POOL_SIZE=0
 CONFIG_STACK_CHECK_NONE=y
 # CONFIG_STACK_CHECK_NORM is not set
 # CONFIG_STACK_CHECK_STRONG is not set