An asynchronous, FreeRTOS-native event bus for ESP32 projects. Producers post payloads and continue running while the bus fan-outs the work on its own task. Consumers subscribe with callbacks or synchronously block for the next matching event with waitFor.
- Tiny API, single header include (
ESPEventBus.h). - Dedicated FreeRTOS worker task with configurable queue depth, stack size, priority, and core affinity.
- Thread-safe and ISR-safe posting (ISRs can queue events without waking the worker unless needed).
- Unlimited subscriptions per event with optional user data and one-shot semantics.
std::functioncallback support so you can bind private member methods or use capturing lambdas.- Per-task
waitForhelper implemented with short-lived queues so any task can await the next payload. - Queue overflow policies, pressure callbacks, and payload validation hooks for defensive firmware.
Define strongly typed event IDs (the bus only needs integral IDs internally):
// app_events.h
#pragma once
#include <cstdint>
enum class AppEvent : uint16_t {
NetworkGotIP,
NetworkLostIP,
LoggerFlush,
};Wire up subscriptions and post from any task:
#include <ESPEventBus.h>
#include "app_events.h"
struct NetworkGotIpPayload {
String ip;
};
ESPEventBus eventBus;
void onNetworkGotIp(void* payload, void*) {
auto* info = static_cast<NetworkGotIpPayload*>(payload);
Serial.printf("[ESPEventBus] Network IP %s\n", info->ip.c_str());
}
void setup() {
eventBus.init();
eventBus.subscribe(AppEvent::NetworkGotIP, onNetworkGotIp);
}
void loop() {
static NetworkGotIpPayload payload{};
payload.ip = WiFi.localIP().toString();
eventBus.post(AppEvent::NetworkGotIP, &payload);
delay(5000);
}Bind private class methods with std::bind when needed:
eventBus.subscribe(AppEvent::NetworkGotIP,
std::bind(&HostedFirmwareUpdater::onNetEvent,
this,
std::placeholders::_1,
std::placeholders::_2));Need to suspend the caller until a payload arrives? waitFor creates a one-shot subscription and blocks on a temporary queue:
auto* payload = static_cast<NetworkGotIpPayload*>(
eventBus.waitFor(AppEvent::NetworkGotIP, pdMS_TO_TICKS(1000))
);
if (payload) {
Serial.printf("Received IP %s\n", payload->ip.c_str());
}Explore the sketches under examples/:
examples/basic_usage– minimal two-task sketch with posting andwaitFor.examples/request_response– request/response pattern between tasks.examples/overflow_monitor– queue pressure instrumentation and overflow policies.
- The bus only stores the pointer you supply; keep payloads alive for as long as subscribers need them or use pools/ref-counted buffers.
waitForrefuses to run on the ESPEventBus worker task. EnableINCLUDE_xTaskGetCurrentTaskHandle=1(set by default on ESP-IDF/Arduino) so the guard can detect misuse.postFromISRbehaves likepostbut still runs callbacks on the worker. Long callbacks block the worker and delay other subscribers.- Overflow policies that drop events fire user callbacks in the posting context—keep those callbacks short and ISR-safe where applicable.
bool init(const EventBusConfig& cfg = EventBusConfig{})– creates the subscription mutex, queue, and worker task.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; returns0on failure.EventBusSub subscribe(Id id, EventCallback cb, void* userArg = nullptr, bool oneshot = false)– registerstd::functioncallbacks (bind/captures).void unsubscribe(EventBusSub subId)– deactivate one subscription.void* waitFor(Id id, TickType_t timeout = portMAX_DELAY)– block the caller on a temporary queue and return the payload pointer ornullptron timeout.
EventBusConfig exposes guardrails so multiple components can safely share one bus:
enum class EventBusOverflowPolicy : uint8_t {
Block,
DropNewest,
DropOldest,
};
struct EventBusConfig {
uint16_t queueLength = 16;
UBaseType_t priority = 5;
uint32_t stackSize = 4096 * sizeof(StackType_t);
BaseType_t coreId = tskNO_AFFINITY;
const char* taskName = "ESPEventBus";
uint16_t maxSubscriptions = 0;
EventBusOverflowPolicy overflowPolicy = EventBusOverflowPolicy::Block;
uint8_t pressureThresholdPercent = 90;
EventBusQueuePressureFn pressureCallback = nullptr;
EventBusDropFn dropCallback = nullptr;
EventBusPayloadValidatorFn payloadValidator = nullptr;
};
Stack sizes are expressed in bytes.Combine pressureCallback and dropCallback to monitor noisy publishers, and wire payloadValidator to enforce shared ownership rules before any payload reaches the queue.
- 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.
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.
MIT — see LICENSE.md.
- Check out other libraries: https://github.com/orgs/ESPToolKit/repositories
- Hang out on Discord: https://discord.gg/WG8sSqAy
- Support the project: https://ko-fi.com/esptoolkit
- Visit the website: https://www.esptoolkit.hu/