ekxide's iceoryx2 Deep Dive - Service Attributes

Christian Eltzschig - 15/06/2024

iceoryx2 rust

With this article, we are starting a new series where we dive deep into the current development progress of iceoryx2. We'll explain the newest features, the problems they solve, and the cool new things you can build with iceoryx2. This series will let you see what our open-source company, ekxide IO GmbH, is currently working on. It also allows us to collect feedback from the community, plan and refine new features, and discover interesting projects using iceoryx2.

For those who are not familiar with iceoryx2: it is an open-source library that handles reliable and incredibly fast inter-process communication, suitable for applications ranging from desktops to mission-critical systems like cars or medical devices. So, if you need to send data or signal events from process A to process B, iceoryx2 is your go-to library.

https://github.com/eclipse-iceoryx/iceoryx2

The Problem

What problem does iceoryx2 solve? It is a service-oriented inter-process middleware where you can create services with a name and send data or signals to other processes.

Assume you are building a robot with multiple camera sensors. You may have several services producing video streams, publishing them on services like "camera:front," "camera:back," "camera:left," and so on. In iceoryx2, you could implement it like this:

let service = zero_copy::Service::new(ServiceName::new("camera:front")?)
    .publish_subscribe::<CameraImage>()
    .create()?;

let publisher = service.publisher().create()?;

loop {
    let sample = publisher.loan_uninit()?;
    sample.write_payload(get_camera_image()).send()?;
}

If you now write a process that requires a video stream to detect obstacles and perform an emergency brake if necessary, you could easily subscribe to such a service like this:

let service = zero_copy::Service::new(ServiceName::new("camera:front")?)
    .publish_subscribe::<CameraImage>()
    .open()?;

let subscriber = service.subscriber().create()?;

loop {
    if let Some(image) = subscriber.receive()? {
        perform_some_processing(*image);
    }
}

But what if you have another service that wants to create high-quality snapshots of the scenes the robot captures? It would be advantageous if the service used a 4k camera. Or, if the robot is moving at high speed, it would be preferable to use only services where the camera produces images at a rate of 60 frames per second.

Where could we store this information for the consumers of the data? We could add this in the header of the message, but for efficiency, this is not the best place to write this information repeatedly, especially when it never changes.

The solution is service attributes.

Service Attributes

Service attributes are key-value pairs that remain constant during the service's lifetime. They can be set when the service is created and can be read by any participant and during service discovery.

let service = zero_copy::Service::new(ServiceName::new("camera:front")?)
    .publish_subscribe::<CameraImage>()
    .create_with_attributes(
        &AttributeSpecifier::new()
            .define("camera-resolution", "1920x1080")
            .define("frames-per-second", "60"),
    )?;

When you perform a service discovery, you immediately see what attributes are set and can select the right service that satisfies all your requirements. It also allows you to acquire additional information about the counterpart.

let service = zero_copy::Service::new(ServiceName::new("camera:front")?)
    .publish_subscribe::<CameraImage>()
    .open()?;

for attribute in service.attributes().iter() {
    println!("{} = {}", attribute.key(), attribute.value());
}

Another option is to define the service attributes as requirements. For instance, it could be important that a specific key is defined without considering the value, or that a specific key-value pair is defined. Let's go back to our example and assume that we do not care about the camera resolution as long as it is defined as an attribute, but we need 60 frames per second to perform an emergency brake.

let service = zero_copy::Service::new(ServiceName::new("camera:front")?)
    .publish_subscribe::<CameraImage>()
    .open_with_attributes(
        &AttributeVerifier::new()
            .require_key("camera-resolution")
            .require("frames-per-second", "60"),
    )?;

One of our internal iceoryx2 use cases for service attributes is gateways. When you want to forward a message from iceoryx2 via the MQTT protocol, you may want to use a different service name. Sometimes it is even mandatory since the protocol does not support the iceoryx2 naming scheme, like Some/IP. With service attributes, we can now define the translation for the gateway directly in the service and specify that the "camera:front" service should map to the MQTT service "camera/front."

let service = zero_copy::Service::new(ServiceName::new("camera:front")?)
    .publish_subscribe::<CameraImage>()
    .create_with_attributes(
        &AttributeSpecifier::new()
            .define("mqtt-service-name", "camera/front"),
    )?;

What's Next?

One of the things that are missing is a mechanism to make the attributes more scalable. We need to come up with a configuration file or another innovative solution that allows us to define attributes, such as the iceoryx2 service to MQTT service mapping, in a more centralized manner rather than hardcoding them in the code. Let's see what we can come up with — we'll keep you posted.

Happy Hacking