Skip to main content
The Cerulion lion wiring a camera input to a monitor output, building a two-node pipeline

What you’ll build

In this tutorial you build a complete two-node graph:
  • A periodic publisher (sensor) that emits a sensor_msgs/Image on a fixed interval.
  • A data-triggered consumer (detector) that fires each time a new image arrives and publishes a geometry_msgs/PoseArray of detections.
You will wire them into a graph, run it, and watch the messages flow with cerulion topic echo in a second terminal. Figure: The two-node graph you build. The periodic sensor node publishes a sensor_msgs/Image; the data-triggered detector fires on each image and publishes a geometry_msgs/PoseArray.
This guide assumes the cerulion CLI is installed and cargo is on your PATH. If not, follow Installation first.

Build it step by step

Create and enter a workspace

A workspace is the project container that holds your nodes, graphs, and schemas.
cerulion workspace create my_robot && cd my_robot
Expected output:
Created workspace at my_robot

Create the publisher node

Create a sensor node with one output port named image carrying a sensor_msgs/Image. Because it has no inputs, a source-only node must declare a non-data trigger policy; here, a 33 ms period.
cerulion node create sensor -o sensor_msgs/Image image --policy period_ms=33
Expected output:
Created node type 'sensor'
The period is set with --policy period_ms=33. Note the underscore and the =.

Create the consumer node

Create a detector node whose image input is a trigger (-T), so the node fires whenever an image arrives. Give it an output named detections carrying a geometry_msgs/PoseArray.
cerulion node create detector -T sensor_msgs/Image image -o geometry_msgs/PoseArray detections
Expected output:
Created node type 'detector'
-T and -o each take two values in the order SCHEMA NAME. The trigger input also sets the node’s policy to fire on that input’s data.

Fill in the publisher's tick

Open nodes/sensor/src/lib.rs and replace its contents with the node below. Each tick stamps the image dimensions and bumps a frame counter.
use cerulion_core::prelude::*;
use native_ros2_messages::sensor_msgs::Image;

#[cerulion_node(period_ms = 33)]
#[derive(Default)]
struct SensorNode {
  #[output(data, encoding)]
  image: Image,

  frame_count: u32,
}

#[cerulion_node_impl]
impl SensorNode {
  fn tick(&mut self) -> Result<(), NodeError> {
    self.frame_count += 1;
    self.image.height = 480;
    self.image.width = 640;
    self.image.encoding = "rgb8";
    Ok(())
  }
}
height and width are fixed fields, written straight to shared memory. encoding is a variable-length field, so it is listed in #[output(data, encoding)] and the macro rewrites the assignment into a zero-copy set_encoding(...) call for you.

Fill in the consumer's tick

Open nodes/detector/src/lib.rs and replace its contents with the node below. Each tick reads the latest image dimensions and counts a detection.
use cerulion_core::prelude::*;
use native_ros2_messages::sensor_msgs::Image;
use native_ros2_messages::geometry_msgs::PoseArray;

#[cerulion_node]
#[derive(Default)]
struct DetectorNode {
  #[input(trigger)]
  image: Image,

  #[output]
  detections: PoseArray,

  detections_seen: u32,
}

#[cerulion_node_impl]
impl DetectorNode {
  fn tick(&mut self) -> Result<(), NodeError> {
    let pixels = self.image.height * self.image.width;
    if pixels > 0 {
      self.detections_seen += 1;
    }
    Ok(())
  }
}
The #[input(trigger)] attribute on image is what makes detector fire on each incoming image. The node-level macro has no policy attribute because the trigger comes from the field.

Build both node crates

Compile each node into a loadable library. These commands shell out to cargo.
cerulion node build sensor
cerulion node build detector
Expected output:
Built 'sensor'
Built 'detector'

Create a graph

Create an empty graph named perception with an explicit topic prefix of perception. The -n/--prefix flag fixes the prefix so the topic names are predictable; without it, the prefix would default to your machine’s hostname.
cerulion graph create perception -n perception
Expected output:
Created graph 'perception'
Cerulion composes each topic name as {prefix}/{node_id}/{output_name}. With the prefix perception, the sensor instance’s image output publishes to perception/sensor/image.

Stage the nodes and wire them

Add an instance of each node to the graph. For detector, wire its image input to the sensor instance’s image output with -I.
cerulion node stage sensor -g perception
cerulion node stage detector -g perception -I image [sensor,image]
Expected output:
Staged 'sensor' into graph 'perception'
Staged 'detector' into graph 'perception'
-I takes the input port name followed by its source. The [sensor,image] form means “the image output of the node instance sensor.”

Run the graph

Run the graph. Real-time pacing is on by default, so the simulated clock tracks wall time. The run continues until you stop it with Ctrl+C.
cerulion graph run perception
Leave this terminal running.

Watch the topics in a second terminal

Open a new terminal. Topic discovery needs no workspace. List the active topics, then echo the one carrying images:
cerulion topic list
cerulion topic echo perception/sensor/image
topic echo pretty-prints sensor_msgs/Image, so you will see the image dimensions and encoding update as frames arrive. Stop echoing with Ctrl+C.
Because you set the prefix to perception, the topic is perception/sensor/image. Always run cerulion topic list first to confirm the exact names before echoing.
You should see image messages streaming in the echo terminal while the graph runs. Your two-node graph is live: sensor publishes images on a 33 ms period and detector fires on each one. Press Ctrl+C in the run terminal to stop the graph.
The Cerulion lion on a podium with a gold medal celebrating a first successful graph run

Next steps

Concepts

The mental model behind workspaces, nodes, graphs, topics, and schemas.

Guides

Task-focused walkthroughs for defining nodes and wiring graphs.

CLI reference

Every command and flag, with synopses and examples.