Event

Larry now has his distance sensor sorted out, but he’s still clumsy. We’ll add two more sensors:

  • A battery sensor that warns the system health monitor when juice is running low.

  • A bump sensor that tattles when Larry walks into a wall.

This is where the event messaging pattern comes in. Instead of streaming values, a participant can send a one-off notification through a notifier port to another participant that is sleeping on a listener port. The listener wakes up only when something worth caring about happens.

In our case: low battery or wall collision. The health monitor reacts by lighting up the battery LED or going into a parking position.

Before diving into code, let’s clear up two terms:

  • State: the current facts in memory (e.g. the battery is low or normal).

  • Event: the notification that state changed.

If we hammer the listener with “battery low” notifications, it will still only see one event until it wakes up and resets the flag. This prevents events from being lost, but also means they don’t queue up like publish-subscribe messages. Order is undefined; you only know that at least once someone signaled a state change.

Notifier

As usual, everything begins with a node and a service, but this time we create an event service:

use iceoryx2::prelude::*;

let node = NodeBuilder::new().create::<ipc::Service>()?;

let event_service = node
    .service_builder(&"system_health_events".try_into()?)
    .event()
    .open_or_create()?;
import iceoryx2 as iox2

iox2.set_log_level_from_env_or(iox2.LogLevel.Info)
node = iox2.NodeBuilder.new().create(iox2.ServiceType.Ipc)

event = (
    node.service_builder(iox2.ServiceName.new("system_health_events"))
    .event()
    .open_or_create()
)
auto node = NodeBuilder().create<ServiceType::Ipc>().value();

auto service =
    node.service_builder(ServiceName::create("system_health_events").value()).event().open_or_create().value();
iox2_node_builder_h node_builder_handle = iox2_node_builder_new(NULL);
iox2_node_h node = NULL;
if (iox2_node_builder_create(node_builder_handle, NULL, iox2_service_type_e_IPC, &node) != IOX2_OK) {
    printf("Could not create node!\n");
    exit(-1);
}

const char* service_name_value = "system_health_events";
iox2_service_name_h service_name = NULL;
if (iox2_service_name_new(NULL, service_name_value, strlen(service_name_value), &service_name) != IOX2_OK) {
    printf("Unable to create service name!\n");
    exit(-1);
}

iox2_service_name_ptr service_name_ptr = iox2_cast_service_name_ptr(service_name);
iox2_service_builder_h service_builder = iox2_node_service_builder(&node, NULL, service_name_ptr);
iox2_service_builder_event_h service_builder_event = iox2_service_builder_event(service_builder);
iox2_port_factory_event_h service = NULL;
if (iox2_service_builder_event_open_or_create(service_builder_event, NULL, &service) != IOX2_OK) {
    printf("Unable to create service!\n");
    exit(-1);
}

Now we create the notifier port, which sends notifications:

let notifier = event_service.notifier_builder().create()?;
notifier = event.notifier_builder().create()
auto notifier = service.notifier_builder().create().value();
iox2_port_factory_notifier_builder_h notifier_builder = iox2_port_factory_event_notifier_builder(&service, NULL);
iox2_notifier_h notifier = NULL;
if (iox2_port_factory_notifier_builder_create(notifier_builder, NULL, &notifier) != IOX2_OK) {
    printf("Unable to create notifier!\n");
    exit(-1);
}

We want to notify about two distinct things: wall collisions and low battery. So we define two event IDs:

let wall_was_hit = EventId::new(0);
let battery_is_low = EventId::new(1);
wall_was_hit = iox2.EventId.new(0)
battery_is_low = iox2.EventId.new(1)
const auto wall_was_hit = EventId(0);
const auto battery_is_low = EventId(1);
iox2_event_id_t wall_was_hit = { .value = 0 };
iox2_event_id_t battery_is_low = { .value = 1 };

Checking the sensors every second is good enough for health monitoring, so we send notifications only when something changes:

while node.wait(Duration::from_secs(1)).is_ok() {
    if bump_sensor_was_activated() {
        notifier.notify_with_custom_event_id(wall_was_hit)?;
    }

    if battery_state() < battery_threshold {
        notifier.notify_with_custom_event_id(battery_is_low)?;
    }
}
try:
    while True:
        node.wait(iox2.Duration.from_secs(1))
        if bump_sensor_was_activated():
            notifier.notify_with_custom_event_id(wall_was_hit)

        if battery_state() < battery_threshold:
            notifier.notify_with_custom_event_id(battery_is_low)

except iox2.NodeWaitFailure:
    print("exit")
while (node.wait(iox2::bb::Duration::from_secs(1)).has_value()) {
    if (bump_sensor_was_activated()) {
        notifier.notify_with_custom_event_id(wall_was_hit).value();
    }

    if (battery_state() < battery_threshold) {
        notifier.notify_with_custom_event_id(battery_is_low).value();
    }
}
while (iox2_node_wait(&node, 1, 0) == IOX2_OK) {
    if (bump_sensor_was_activated()) {
        if (iox2_notifier_notify_with_custom_event_id(&notifier, &wall_was_hit, NULL) != IOX2_OK) {
            printf("Failed to notify listener!\n");
            exit(-1);
        }
    }

    if (battery_state() < BATTERY_THRESHOLD) {
        if (iox2_notifier_notify_with_custom_event_id(&notifier, &battery_is_low, NULL) != IOX2_OK) {
            printf("Failed to notify listener!\n");
            exit(-1);
        }
    }
}

// release every handle once the loop exits
iox2_notifier_drop(notifier);
iox2_port_factory_event_drop(service);
iox2_service_name_drop(service_name);
iox2_node_drop(node);

Listener

On the other side, the listener is the participant waiting for the health events. First, create the node and open the event service:

use iceoryx2::prelude::*;

let node = NodeBuilder::new().create::<ipc::Service>()?;

let event_service = node
    .service_builder(&"system_health_events".try_into()?)
    .event()
    .open_or_create()?;
import iceoryx2 as iox2

node = iox2.NodeBuilder.new().create(iox2.ServiceType.Ipc)

event = (
    node.service_builder(iox2.ServiceName.new("system_health_events"))
    .event()
    .open_or_create()
)
auto node = NodeBuilder().create<ServiceType::Ipc>().value();

auto service =
    node.service_builder(ServiceName::create("system_health_events").value()).event().open_or_create().value();
iox2_node_builder_h node_builder_handle = iox2_node_builder_new(NULL);
iox2_node_h node = NULL;
if (iox2_node_builder_create(node_builder_handle, NULL, iox2_service_type_e_IPC, &node) != IOX2_OK) {
    printf("Could not create node!\n");
    exit(-1);
}

const char* service_name_value = "system_health_events";
iox2_service_name_h service_name = NULL;
if (iox2_service_name_new(NULL, service_name_value, strlen(service_name_value), &service_name) != IOX2_OK) {
    printf("Unable to create service name!\n");
    exit(-1);
}

iox2_service_name_ptr service_name_ptr = iox2_cast_service_name_ptr(service_name);
iox2_service_builder_h service_builder = iox2_node_service_builder(&node, NULL, service_name_ptr);
iox2_service_builder_event_h service_builder_event = iox2_service_builder_event(service_builder);
iox2_port_factory_event_h service = NULL;
if (iox2_service_builder_event_open_or_create(service_builder_event, NULL, &service) != IOX2_OK) {
    printf("Unable to create service!\n");
    exit(-1);
}

Now we create the listener port:

let listener = event_service.listener_builder().create()?;
listener = event.listener_builder().create()
auto listener = service.listener_builder().create().value();
iox2_port_factory_listener_builder_h listener_builder = iox2_port_factory_event_listener_builder(&service, NULL);
iox2_listener_h listener = NULL;
if (iox2_port_factory_listener_builder_create(listener_builder, NULL, &listener) != IOX2_OK) {
    printf("Unable to create listener!\n");
    exit(-1);
}

Each event maps to an action. We wrap that in a small handler:

let react_to_event = |event: EventActivation| {
    if event.id == battery_is_low {
        activate_battery_warning_light();
    }
    if event.id == wall_was_hit {
        go_into_parking_position();
    }
};
def react_to_event(event) -> None:
    if event.id == battery_is_low:
        activate_battery_warning_light()
    if event.id == wall_was_hit:
        go_into_parking_position()
auto react_to_event = [&](auto event) {
    if (event.id() == battery_is_low) {
        activate_battery_warning_light();
    }
    if (event.id() == wall_was_hit) {
        go_into_parking_position();
    }
};
static void react_to_event(const iox2_event_id_t* event_id, uint64_t count, void* context) {
    (void) count;
    (void) context;
    if (event_id->value == battery_is_low.value) {
        activate_battery_warning_light();
    }
    if (event_id->value == wall_was_hit.value) {
        go_into_parking_position();
    }
}

This participant doesn’t need to poll. It just blocks until an event arrives, then runs the handler for each pending notification:

while listener.blocking_wait(react_to_event).is_ok() {}
try:
    while True:
        for event in listener.blocking_wait():
            react_to_event(event)
except iox2.ListenerWaitError:
    print("exit")
while (listener.blocking_wait(react_to_event).has_value()) {}
uint64_t number_of_notifications = 0;
while (iox2_node_wait(&node, 0, 0) == IOX2_OK) {
    if (iox2_listener_blocking_wait(&listener, &number_of_notifications, react_to_event, NULL) != IOX2_OK) {
        printf("Unable to wait for notification!\n");
        exit(-1);
    }
}

// release every handle once the loop exits
iox2_listener_drop(listener);
iox2_port_factory_event_drop(service);
iox2_service_name_drop(service_name);
iox2_node_drop(node);