Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ All notable changes to this project will be documented in this file.
- Added `EventBusConfig::usePSRAMBuffers` and routed subscription/fan-out vectors through `ESPBufferManager` with automatic fallback when PSRAM is unavailable.
- Added a platform-gated static FreeRTOS allocation path (`configSUPPORT_STATIC_ALLOCATION == 1`) so `usePSRAMBuffers` also covers worker queue/mutex storage via `ESPBufferManager`, with fallback to dynamic FreeRTOS allocation when unavailable.
- Switched worker task creation/lifecycle back to native FreeRTOS task handling (`xTaskCreatePinnedToCore`/`vTaskDelete`).
- Added teardown-contract lifecycle coverage (`pre-init deinit`, repeated `deinit()`, and `init -> deinit -> init`) and exposed `isInitialized()` in the public API.

### Fixed
- Disambiguated the README and `examples/basic_usage` subscription callback to avoid overload ambiguity on Arduino.
- Worker task creation now uses native FreeRTOS `xTaskCreatePinnedToCore(...)` and keeps the same non-caps runtime path for broad ESP32 compatibility.
- Hardened subscription-storage teardown/reset so allocator transitions are safe when toggling `usePSRAMBuffers` across lifecycles.

## [1.0.0] - 2025-11-19
### Added
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ void loop() {
eventBus.post(AppEvent::NetworkGotIP, &payload);
delay(5000);
}

void stopEventBusBeforeSleepOrRestart() {
if (eventBus.isInitialized()) {
eventBus.deinit();
}
}
```

Bind private class methods with `std::bind` when needed:
Expand Down Expand Up @@ -97,6 +103,8 @@ Explore the sketches under `examples/`:

## API Reference
- `bool init(const EventBusConfig& cfg = EventBusConfig{})` – creates the subscription mutex, queue, and worker task with native FreeRTOS task creation.
- `void deinit()` – idempotent teardown that stops the worker task, releases queue/mutex resources, and clears subscriptions.
- `bool isInitialized() const` – reports whether runtime resources are currently active.
- `bool post(Id id, void* payload, TickType_t timeout = 0)` / `bool postFromISR(...)` – queue an event from tasks or interrupts.
- `EventBusSub subscribe(Id id, EventCallbackFn cb, void* userArg = nullptr, bool oneshot = false)` – register C-style callbacks; returns `0` on failure.
- `EventBusSub subscribe(Id id, EventCallback cb, void* userArg = nullptr, bool oneshot = false)` – register `std::function` callbacks (bind/captures).
Expand Down Expand Up @@ -138,6 +146,7 @@ Combine `pressureCallback` and `dropCallback` to monitor noisy publishers, and w
- Built and tested on ESP32 (Arduino-ESP32 and ESP-IDF) with FreeRTOS available; other MCUs/frameworks are unsupported.
- Requires C++17 support and a FreeRTOS configuration that enables `xTaskGetCurrentTaskHandle` (Arduino + ESP-IDF do this by default).
- Single ESPEventBus instance manages its own worker task; if you construct multiple buses they each allocate their own queue/task resources.
- Call `deinit()` before deep sleep, component shutdown, or application restart to release worker/task resources explicitly.

## Tests
Unity tests run under PlatformIO: plug in an ESP32 dev board and execute `pio test -e esp32dev` from the repo root. The suite covers overflow policies, subscription caps, payload validation, and graceful shutdown.
Expand Down
8 changes: 8 additions & 0 deletions examples/basic_usage/basic_usage.ino
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,13 @@ void setup() {
}

void loop() {
if (Serial.available() > 0) {
const int ch = Serial.read();
if ((ch == 'x' || ch == 'X') && eventBus.isInitialized()) {
Serial.println("[ESPEventBus] deinitializing by user request");
eventBus.deinit();
}
}

vTaskDelay(pdMS_TO_TICKS(1000));
}
18 changes: 13 additions & 5 deletions src/esp_eventbus/eventbus.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "eventbus.h"

#include <algorithm>
#include <new>

ESPEventBus::ESPEventBus() = default;

Expand All @@ -25,8 +26,7 @@ bool ESPEventBus::init(const EventBusConfig& config) {
config_ = sanitized;
stopEventPending_ = false;
resetKernelStorage();
EventBusVector<Subscription> subStorage{ EventBusAllocator<Subscription>(config_.usePSRAMBuffers) };
subs_.swap(subStorage);
resetSubscriptions(config_.usePSRAMBuffers);

if (!createKernelMutex()) {
return false;
Expand Down Expand Up @@ -68,15 +68,17 @@ void ESPEventBus::deinit() {
}

resetKernelStorage();
subs_.clear();
EventBusVector<Subscription> emptySubs{ EventBusAllocator<Subscription>(false) };
subs_.swap(emptySubs);
resetSubscriptions(false);
nextSubId_ = 0;
stopEventPending_ = false;
config_ = EventBusConfig{};
task_ = nullptr;
}

bool ESPEventBus::isInitialized() const {
return queue_ != nullptr && subMutex_ != nullptr && task_ != nullptr && running_;
}

bool ESPEventBus::post(EventBusId id, void* payload, TickType_t timeout) {
if (!queue_) {
return false;
Expand Down Expand Up @@ -531,6 +533,12 @@ bool ESPEventBus::createWorkerTask(const char* taskName) {
return created == pdPASS && task_ != nullptr;
}

void ESPEventBus::resetSubscriptions(bool usePSRAMBuffers) {
using SubscriptionVector = EventBusVector<Subscription>;
subs_.~SubscriptionVector();
new (&subs_) SubscriptionVector{ EventBusAllocator<Subscription>(usePSRAMBuffers) };
}

void ESPEventBus::resetKernelStorage() {
freeKernelStorage(mutexStorage_);
freeKernelStorage(queueStorage_);
Expand Down
2 changes: 2 additions & 0 deletions src/esp_eventbus/eventbus.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class ESPEventBus {

bool init(const EventBusConfig& config = EventBusConfig{});
void deinit();
bool isInitialized() const;

bool post(EventBusId id, void* payload, TickType_t timeout = 0);

Expand Down Expand Up @@ -140,6 +141,7 @@ class ESPEventBus {
bool createKernelMutex();
bool createKernelQueue();
bool createWorkerTask(const char* taskName);
void resetSubscriptions(bool usePSRAMBuffers);
void resetKernelStorage();
static size_t taskStackWords(uint32_t stackSizeBytes);
static void* allocateKernelStorage(size_t bytes, bool usePSRAMBuffers);
Expand Down
60 changes: 60 additions & 0 deletions test/test_eventbus/test_eventbus.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ struct DropContext {
volatile int dropped = 0;
};

struct CounterContext {
volatile int callbacks = 0;
};

static void slowSubscriber(void*, void* userArg) {
auto* ctx = static_cast<DropContext*>(userArg);
if (ctx) {
Expand All @@ -33,6 +37,13 @@ static void dropCallback(EventBusId, void*, void* userArg) {
}
}

static void counterCallback(void*, void* userArg) {
auto* ctx = static_cast<CounterContext*>(userArg);
if (ctx) {
ctx->callbacks++;
}
}

struct PressureContext {
volatile bool triggered = false;
};
Expand Down Expand Up @@ -224,6 +235,53 @@ void test_deinit_completes_when_queue_is_busy() {
}
}

void test_deinit_is_safe_before_init_and_idempotent() {
ESPEventBus bus;
TestPayload payload{};

TEST_ASSERT_FALSE(bus.isInitialized());

bus.deinit();
TEST_ASSERT_FALSE(bus.isInitialized());
TEST_ASSERT_FALSE(bus.post(TestEvent::FastTick, &payload, 0));

bus.deinit();
TEST_ASSERT_FALSE(bus.isInitialized());
TEST_ASSERT_FALSE(bus.post(TestEvent::FastTick, &payload, 0));
}

void test_reinit_clears_subscriptions_and_restores_bus() {
ESPEventBus bus;
CounterContext ctx{};
TestPayload payload{};

TEST_ASSERT_TRUE(bus.init());
TEST_ASSERT_TRUE(bus.isInitialized());
TEST_ASSERT_NOT_EQUAL(0U, bus.subscribe(TestEvent::FastTick, counterCallback, &ctx));
TEST_ASSERT_TRUE(bus.post(TestEvent::FastTick, &payload, portMAX_DELAY));
vTaskDelay(pdMS_TO_TICKS(30));
TEST_ASSERT_TRUE(ctx.callbacks > 0);

bus.deinit();
TEST_ASSERT_FALSE(bus.isInitialized());

const int callbacksAfterFirstRun = ctx.callbacks;
TEST_ASSERT_TRUE(bus.init());
TEST_ASSERT_TRUE(bus.isInitialized());
TEST_ASSERT_TRUE(bus.post(TestEvent::FastTick, &payload, portMAX_DELAY));
vTaskDelay(pdMS_TO_TICKS(30));
TEST_ASSERT_EQUAL_INT(callbacksAfterFirstRun, ctx.callbacks);

TEST_ASSERT_NOT_EQUAL(0U, bus.subscribe(TestEvent::FastTick, counterCallback, &ctx));
TEST_ASSERT_TRUE(bus.post(TestEvent::FastTick, &payload, portMAX_DELAY));
vTaskDelay(pdMS_TO_TICKS(30));
TEST_ASSERT_TRUE(ctx.callbacks > callbacksAfterFirstRun);

bus.deinit();
bus.deinit();
TEST_ASSERT_FALSE(bus.isInitialized());
}

void setUp() {
}

Expand All @@ -239,6 +297,8 @@ void setup() {
RUN_TEST(test_drop_newest_policy_discards_incoming_event);
RUN_TEST(test_pressure_callback_triggers_on_high_usage);
RUN_TEST(test_deinit_completes_when_queue_is_busy);
RUN_TEST(test_deinit_is_safe_before_init_and_idempotent);
RUN_TEST(test_reinit_clears_subscriptions_and_restores_bus);
UNITY_END();
}

Expand Down