diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b067a4..93665f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 949c0f7..536b5b6 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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). @@ -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. diff --git a/examples/basic_usage/basic_usage.ino b/examples/basic_usage/basic_usage.ino index 8dd7e64..125d9bb 100644 --- a/examples/basic_usage/basic_usage.ino +++ b/examples/basic_usage/basic_usage.ino @@ -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)); } diff --git a/src/esp_eventbus/eventbus.cpp b/src/esp_eventbus/eventbus.cpp index 554ddae..6f2c8f3 100644 --- a/src/esp_eventbus/eventbus.cpp +++ b/src/esp_eventbus/eventbus.cpp @@ -1,6 +1,7 @@ #include "eventbus.h" #include +#include ESPEventBus::ESPEventBus() = default; @@ -25,8 +26,7 @@ bool ESPEventBus::init(const EventBusConfig& config) { config_ = sanitized; stopEventPending_ = false; resetKernelStorage(); - EventBusVector subStorage{ EventBusAllocator(config_.usePSRAMBuffers) }; - subs_.swap(subStorage); + resetSubscriptions(config_.usePSRAMBuffers); if (!createKernelMutex()) { return false; @@ -68,15 +68,17 @@ void ESPEventBus::deinit() { } resetKernelStorage(); - subs_.clear(); - EventBusVector emptySubs{ EventBusAllocator(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; @@ -531,6 +533,12 @@ bool ESPEventBus::createWorkerTask(const char* taskName) { return created == pdPASS && task_ != nullptr; } +void ESPEventBus::resetSubscriptions(bool usePSRAMBuffers) { + using SubscriptionVector = EventBusVector; + subs_.~SubscriptionVector(); + new (&subs_) SubscriptionVector{ EventBusAllocator(usePSRAMBuffers) }; +} + void ESPEventBus::resetKernelStorage() { freeKernelStorage(mutexStorage_); freeKernelStorage(queueStorage_); diff --git a/src/esp_eventbus/eventbus.h b/src/esp_eventbus/eventbus.h index ba2b38f..04687e8 100644 --- a/src/esp_eventbus/eventbus.h +++ b/src/esp_eventbus/eventbus.h @@ -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); @@ -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); diff --git a/test/test_eventbus/test_eventbus.cpp b/test/test_eventbus/test_eventbus.cpp index f322322..5ca7b11 100644 --- a/test/test_eventbus/test_eventbus.cpp +++ b/test/test_eventbus/test_eventbus.cpp @@ -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(userArg); if (ctx) { @@ -33,6 +37,13 @@ static void dropCallback(EventBusId, void*, void* userArg) { } } +static void counterCallback(void*, void* userArg) { + auto* ctx = static_cast(userArg); + if (ctx) { + ctx->callbacks++; + } +} + struct PressureContext { volatile bool triggered = false; }; @@ -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() { } @@ -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(); }