Skip to content

Events and Notifications in Pico-Framework

Pico-Framework provides multiple mechanisms for inter-task communication and asynchronous signaling. These mechanisms are designed to meet the needs of embedded systems developers who require a mix of speed, flexibility, and reliability.

There are three core signaling mechanisms available in the framework:

  • Task Notifications: Fast, lightweight signals delivered directly to tasks using FreeRTOS primitives.
  • EventManager Events: Structured messages that support routing, payloads, and pub-sub communication between components.
  • Direct GPIO Listeners: Low-latency callbacks used for fast response to GPIO changes, especially in interrupt service routines.

Choosing the right mechanism for a given use case depends on performance requirements, data complexity, and system architecture.


Overview and Feature Comparison

Feature Task Notifications EventManager Events Direct GPIO Listeners
Payload support
One-to-one delivery
Broadcast support
ISR-safe ⚠ (with care)
Suitable for ultra-low latency
Strong type safety Moderate High Moderate
Works in task context
Works in ISR context ⚠ (postEventFromISR only)
Best for system-level signals
Best for structured application logic

⚠ Note 1: EventManager::postEventFromISR() is safe to call from ISRs, but care must be taken: the function must not allocate memory or block. Events posted this way are queued and processed later in task context.

⚠ Note 2: While the EventManager supports event injection from ISR, it is not suitable for ultra-low latency or time-critical response paths. Use direct task notifications or GPIO listeners for those cases.


Task Notifications

Task notifications are ideal for simple, low-overhead signaling between tasks. They use FreeRTOS's xTaskNotifyIndexed function and deliver a single integer value to a specific task. Notifications are highly efficient and ISR-safe.

When to Use Task Notifications

  • Notify a specific task of a state change
  • No payload required (or fits in a small integer)
  • Require minimal overhead
  • Need ISR-safe signaling with deterministic latency

Example: Sending a Task Notification

xTaskNotifyIndexed(taskHandle, 0,
                   Notification::NetworkReady.code(),
                   eSetValueWithOverwrite);

Example: Waiting for a Task Notification

void NetworkController::run() {
    waitFor(Notification::NetworkReady);
    printf("Network is ready\n");
}

EventManager Events

The EventManager enables structured communication using Event objects. Events support routing, typed notifications, optional payloads, and source/target fields. They are delivered through task-safe queues.

This is the preferred system for decoupled component communication and for carrying structured data between producers and consumers.

Event Structure

  • Notification field with kind and enum (system or user)
  • Optional source and target pointers
  • Optional payload (pointer + size)

When to Use EventManager

  • You need structured data delivery
  • Multiple subscribers or targeted delivery
  • Clean separation between modules
  • Want to implement pub-sub or FSM-style logic

Example: Posting an Event

Event evt;
evt.source = this;
evt.target = AppContext::get<ConfigController>();
evt.notification = Notification::makeUser(UserNotification::ConfigUpdated);
evt.data = &updatedConfig;
evt.size = sizeof(updatedConfig);

EventManager::postEvent(evt);

Note: Use postEventFromISR() when sending events from interrupt context, and avoid dynamic memory allocations.

Example: Receiving the Event

void ConfigController::onEvent(const Event& event) {
    if (event.notification.kind == NotificationKind::User &&
        event.notification.user == UserNotification::ConfigUpdated) {
        auto* cfg = static_cast<ConfigData*>(event.data);
        applyNewConfig(*cfg);
    }
}

Direct GPIO Listeners

GPIO listeners offer the fastest response to pin changes, using static callbacks that are safe to invoke from ISRs. They are ideal for simple signal processing or state changes that must be detected immediately.

Configuration

In framework_config.h, you can enable GPIO events, notifications, or both:

#define GPIO_NOTIFICATIONS            1
#define GPIO_EVENTS                   2
#define GPIO_EVENTS_AND_NOTIFICATIONS (GPIO_NOTIFICATIONS | GPIO_EVENTS)
#define GPIO_EVENT_HANDLING           GPIO_EVENTS_AND_NOTIFICATIONS

Internal Handler Example

void GpioEventManager::gpio_event_handler(uint gpio, uint32_t events) {
    GpioEvent gpioEvent = { static_cast<uint16_t>(gpio), static_cast<uint16_t>(events) };

#if GPIO_EVENT_HANDLING & GPIO_NOTIFICATIONS
    auto it = listeners.find(gpio);
    if (it != listeners.end()) {
        for (auto& cb : it->second) {
            cb(gpioEvent);
        }
    }
#endif

#if GPIO_EVENT_HANDLING & GPIO_EVENTS
    Event evt(SystemNotification::GpioChange, gpioEvent, sizeof(GpioEvent));
    AppContext::get<EventManager>()->postEvent(evt);
#endif
}

When to Use Direct GPIO Listeners

  • Require sub-millisecond response to GPIO changes
  • Working in an ISR context
  • No routing or payload processing is needed

Example: Adding a Listener

gpioEventManager.addListener(pin, [](const GpioEvent& evt) {
    // React immediately to pin edge
});

Notification Typing and Safety

The framework enforces clear notification domains using NotificationKind. This protects against enum overlap and makes routing logic safe and expressive.

enum class SystemNotification {
    NetworkReady = 1,
    StorageMounted = 2
};

enum class UserNotification {
    ProgramStarted = 1,
    ProgramStopped = 2
};

Use the helper functions to construct correctly typed notifications:

Notification n1 = Notification::makeSystem(SystemNotification::NetworkReady);
Notification n2 = Notification::makeUser(UserNotification::ProgramStarted);

Internally, each notification object includes:

  • NotificationKind (System/User)
  • Enum value (cast to uint8_t)

Summary: Choosing the Right Tool

When to Use Task Notifications

  • Ultra-fast, one-to-one signals
  • ISR or task context
  • No payload required
  • Minimal overhead

When to Use EventManager

  • Structured messages with payloads
  • Targeted or broadcast delivery
  • Clean pub-sub or FSM patterns
  • More complex application logic

When to Use Direct GPIO Listeners

  • Fastest GPIO response (ISR-safe)
  • No routing or structured metadata needed
  • Static callback registration

Each mechanism is designed for a specific category of signaling. You can combine them cleanly within the same app. All systems are integrated with FreeRTOS and respect task-safety constraints.

Pico-Framework encourages flexibility while maintaining discipline — ensuring real-time signaling stays reliable, understandable, and well-type