diff --git a/cpp/freertos-active-object.md b/cpp/freertos-active-object.md new file mode 100644 index 0000000..c10d8ac --- /dev/null +++ b/cpp/freertos-active-object.md @@ -0,0 +1,231 @@ +## Active Object Design Pattern + +"The active object design pattern decouples method execution from method +invocation for objects that each reside in their own thread of control." +([Wikipedia](https://en.wikipedia.org/wiki/Active_object)). With this pattern, +we can make objects thread safe. + +Take for example this class. + +```cpp +class SomeClass { +public: + void increment() { + ++mNum; + } + + void decrement() { + --mNum; + } +private: + int mNum = 0; +}; +``` + +We can make it threadsafe by decoupling the function calls from their execution +by using a (thread safe) queue. + +```cpp +class SomeClass { +public: + SomeClass() { + std::thread thread([&](){ + while (true) { + if (!mQueue.empty()) { + mQueue.front()(); + mQueue.pop(); + } + } + }); + } + + void increment() { + mQueue.push([&](){ ++mNum; }); + } + void decrement() { + mQueue.push([&](){ --mNum; }); + } +private: + int mNum = 0; + thread_safe_queue> mQueue; +}; +``` + +## Implementation for FreeRTOS + +Base class implementation: + +```cpp +/// +/// @brief Wrapper around a FreeRTOS queue that provides thread-safe access +/// +/// +template +class Queue { + constexpr static auto defaultWaitTime = std::chrono::microseconds{0}; + +public: + enum class Error { QueueFull, CalledFromISR }; + + Queue(std::size_t maxNumItems) { + static_assert(std::is_trivially_copyable_v, "T must be trivially " + "copyable"); + + mQueueHandle = xQueueCreate(maxNumItems, sizeof(T)); + if (mQueueHandle == 0) { + LogError("Failed to create queue"); + panic(); // TODO: What should be done here is system dependent + } + } + + /// + /// @brief Push an item to the back of the queue + /// + /// @param waitTime Time to wait for space to become available in the queue + /// + [[nodiscard]] std::expected + push(const T& item, std::chrono::microseconds waitTime = defaultWaitTime) { + if (rtos::this_cpu::is_in_isr()) { + LogError("Cannot call push() from an ISR"); + return std::unexpected{Error::CalledFromISR}; + } + + auto res = + xQueueSendToBack(mQueueHandle, &item, rtos::asTicks(waitTime)); + + if (res != pdPASS) return std::unexpected{Error::QueueFull}; + return {}; + } + + /// + /// @brief Get the front item of the queue without removing it + /// + /// @param waitTime Time to wait for an item to become available in the + /// queue + /// + [[nodiscard]] std::optional + front(std::chrono::microseconds waitTime = defaultWaitTime) const { + if (rtos::this_cpu::is_in_isr()){ + LogError("Cannot call front() from an ISR"); + return std::unexpected{Error::CalledFromISR}; + } + + T item; + + auto res = xQueuePeek(mQueueHandle, &item, rtos::asTicks(waitTime)); + if (res != pdPASS) return std::nullopt; + + return item; + } + + /// + /// @brief Remove the front item of the queue + /// + /// @param waitTime Time to wait for an item to become available in the + /// queue + /// + std::expected + pop(std::chrono::microseconds waitTime = defaultWaitTime) { + if (rtos::this_cpu::is_in_isr()) { + LogError("Cannot call pop() from an ISR"); + return std::unexpected{Error::CalledFromISR}; + } + + T item; + + /// Errors are ignored. pop() should fail quietly if there are no + /// elements in the queue + xQueueReceive(mQueueHandle, &item, rtos::asTicks(waitTime)); + + return {}; + } + + /// + /// @brief Get the number of items in the queue + /// + std::size_t size() const { + return uxQueueMessagesWaiting(mQueueHandle); + } + +private: + QueueHandle_t mQueueHandle; +}; + +/// +/// @brief Active object base class +/// @details Use static polymorphism to avoid virtual function overhead +/// +/// @tparam Derived Derived class (CRTP for static polymorphism) +/// @tparam Actions Parameter pack of all action types +/// +template +class ActiveObjectBase { +public: + enum class Error { QueueFull, CalledFromISR }; + + ActiveObjectBase(std::size_t maxNumActions = 32) : mQueue{maxNumActions} { + } + + std::expected bool process_action() { + if (mQueue.size() == 0) return false; + + auto action = mQueue.front(); + /// This should not happen as we check the size of the queue + /// beforehand, but let's handle all errors nonetheless + if (!action) return false; + + if (!mQueue.pop()) return std::unexpected(Error::CalledFromISR); + + std::visit( + [this](auto&& arg) { + static_cast(this)->handle_action(arg); + }, + *action); + + return true; + } + + std::expected queue_action(const auto& action) { + if (!mQueue.push(action)) return std::unexpected{Error::QueueFull}; + + return {}; + } + +private: + Queue> mQueue; +}; +``` + +Derived class implementation: + +```cpp + +struct Clear {}; +struct ShowText { std::array text= {0}; }; + +class DisplayController : + public ActiveObjectBase { +private: + void handle_action(Clear action) { + // Send appropriate command over SPI + } + + void handle_action(ShowText action) { + // Send appropriate command over SPI + } + +}; + + +int main() { + DisplayController controller; + + // Call these from different threads + controller.queue_action(Clear{}); + controller.queue_action(ShowText{{"asdf"}}); + + while (true) { + controller.process_action(); + } +} +```