Add freerots-active-object.md
This commit is contained in:
parent
69dd2f0db4
commit
bd1758c416
231
cpp/freertos-active-object.md
Normal file
231
cpp/freertos-active-object.md
Normal file
@ -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<std::function<void()>> mQueue;
|
||||
};
|
||||
```
|
||||
|
||||
## Implementation for FreeRTOS
|
||||
|
||||
Base class implementation:
|
||||
|
||||
```cpp
|
||||
///
|
||||
/// @brief Wrapper around a FreeRTOS queue that provides thread-safe access
|
||||
///
|
||||
///
|
||||
template <typename T>
|
||||
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>, "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<void, Error>
|
||||
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<T>
|
||||
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<void, Error>
|
||||
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 <typename Derived, typename... Actions>
|
||||
class ActiveObjectBase {
|
||||
public:
|
||||
enum class Error { QueueFull, CalledFromISR };
|
||||
|
||||
ActiveObjectBase(std::size_t maxNumActions = 32) : mQueue{maxNumActions} {
|
||||
}
|
||||
|
||||
std::expected<bool, Error> 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<Derived*>(this)->handle_action(arg);
|
||||
},
|
||||
*action);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::expected<void, Error> queue_action(const auto& action) {
|
||||
if (!mQueue.push(action)) return std::unexpected{Error::QueueFull};
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
private:
|
||||
Queue<std::variant<Actions...>> mQueue;
|
||||
};
|
||||
```
|
||||
|
||||
Derived class implementation:
|
||||
|
||||
```cpp
|
||||
|
||||
struct Clear {};
|
||||
struct ShowText { std::array<char, 32> text= {0}; };
|
||||
|
||||
class DisplayController :
|
||||
public ActiveObjectBase<DisplayController, Clear, ShowText> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
```
|
||||
Loading…
Reference in New Issue
Block a user