Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.cerulion.com/llms.txt

Use this file to discover all available pages before exploring further.

A node is a Rust crate with a #[cerulion_node] struct and a tick() method. This guide takes you from cerulion node create to a node that reads inputs, writes outputs, and is ready to stage into a graph. For the full attribute and policy grammar, see the node macro reference and trigger policies reference.
Run these commands from inside a workspace (a directory with a [workspace] Cargo.toml and a graphs/ folder). Create one with cerulion workspace create <name>.

Create the node type

cerulion node create <node_type> scaffolds nodes/<node_type>/ with a Cargo.toml (cdylib) and a src/lib.rs macro template. The type name becomes the folder name and, PascalCased, the struct name. It must be non-empty, alphanumeric or underscore, and must not start with a digit. Add ports while creating the node:
FlagValue namesMeaning
-o / --outputSCHEMA NAMEAdd one output port.
-i / --inputSCHEMA NAMEAdd one regular input port.
-T / --trigger-inputSCHEMA NAMEAdd one trigger input (fires the node on data arrival).
--policySPECSet the trigger policy (see below).
Each of -o, -i, and -T takes two values β€” a schema and a name β€” and you may pass at most one of each per create call. Add more ports later with node modify. Schemas accept both sensor_msgs/Image and sensor_msgs::Image; both canonicalize to the slash form.

Pick a trigger policy

The trigger policy decides when the node fires. Pass it with --policy SPEC:
--policy SPECFires when…
period_ms=NEvery N milliseconds (N > 0).
deadline_ms=NOn data arrival; reports a miss if no data within N ms.
sync_window_ms=NAll trigger inputs have a message within an N-ms window.
externalThe host triggers it explicitly.
data_trigger=NAMEThe named trigger input receives data.
How the policy defaults when you omit --policy depends on the node’s inputs:
  • 0 inputs (no -i, no -T) β€” a source-only node must declare a non-data policy. cerulion node create errors otherwise.
  • 1+ inputs via -i only β€” no node-level policy is written; the runtime fires on any input arrival and emits a graph-build warning.
  • -T set β€” the policy becomes data_trigger for that trigger input.
A source-only node (zero inputs) has nothing to fire it, so you must give it an explicit non-data policy with --policy period_ms=N or --policy external. Combining a non-data --policy with -T is a conflict and errors.

Create a periodic source node

A camera with no inputs needs a period:
cerulion node create camera -o sensor_msgs/Image image --policy period_ms=33
Created node type 'camera'

Create a data-triggered consumer

A detector that fires whenever an image arrives:
cerulion node create detector -T sensor_msgs/Image image -o geometry_msgs/PoseArray detections
Created node type 'detector'

Write the tick()

Open nodes/<node_type>/src/lib.rs. The template pairs two macros:
  • #[cerulion_node(...)] on the struct β€” generates the <Name>Entry wrapper and cdylib FFI.
  • #[cerulion_node_impl] on the adjacent impl block β€” rewrites self.<port> accesses into zero-copy shared-memory reads and writes. It takes no arguments, and the struct must appear before the impl block.
Import everything from the prelude, and import the message types you use:
use cerulion_core::prelude::*;
use native_ros2_messages::geometry_msgs::Vector3;
use native_ros2_messages::sensor_msgs::LaserScan;

#[cerulion_node(period_ms = 10)]      // period-driven; reads the latest scan each tick
#[derive(Default)]
struct SafetyController {
    #[input(lifo, depth = 1)]         // regular input (use `trigger` for data-triggered)
    scan: LaserScan,

    #[output]                         // Vector3 = fixed-only schema (pub x/y/z: f64)
    linear_velocity: Vector3,
}

#[cerulion_node_impl]
impl SafetyController {
    fn tick(&mut self) -> Result<(), NodeError> {
        // Variable field read via typed accessor:
        let obstacle_close = self.scan.ranges().iter().any(|&r| r < 0.5);
        // Fixed field write β€” goes straight to shared memory:
        self.linear_velocity.x = if obstacle_close { 0.0 } else { 0.3 };
        Ok(())
    }
}

Fixed vs variable fields

How you write an output field depends on whether it is fixed-size or variable-length:
  • Fixed primitive fields (for example x, y, z, height, width) are written directly: self.linear_velocity.x = 0.3;. The macro derefs to the schema’s fixed section and writes straight to shared memory.
  • Variable-length fields (string, T[], nested types) must be listed in the #[output(...)] attribute so the macro can route them to the generated setter.
List simple variable fields by name in #[output(...)]:
use cerulion_core::prelude::*;
use native_ros2_messages::sensor_msgs::Image;

#[cerulion_node(period_ms = 33)]
#[derive(Default)]
struct Camera {
    #[output(data, encoding)]   // `data` and `encoding` are variable fields
    image: Image,
}

#[cerulion_node_impl]
impl Camera {
    fn tick(&mut self) -> Result<(), NodeError> {
        self.image.height = 480;          // fixed field, direct write
        self.image.width = 640;           // fixed field, direct write
        self.image.encoding = "rgb8";     // variable field β†’ rewritten to set_encoding(...)?
        Ok(())
    }
}
For nested-typed variable fields, use the complex(...) form, for example #[output(data, complex(header))]. See the node macro reference for the full #[output] grammar.

Build the node

cerulion node build <node_type> compiles the crate into a cdylib. Add --release for an optimized build.
node build shells out to cargo, so cargo must be on your PATH.
cerulion node build camera
cerulion node build detector
Built 'camera'
A successful build prints Built '<node_type>'. On failure the CLI prints Error: with the cargo output, and no cdylib is produced.

Inspect and adjust

Use these commands to review and edit node types after creation.
cerulion node list prints a table of types with input/output counts and a short policy label.
cerulion node list
TYPE       INPUTS  OUTPUTS  POLICY
camera     0       1        period 33ms
detector   1       1        trigger:image
cerulion node info <node_type> prints the type, policy, and each port with its schema. Metadata is parsed from src/lib.rs β€” there is no sidecar file.
cerulion node info detector
cerulion node modify <node_type> mutates src/lib.rs in place, preserving the tick body and comments. It takes the same -i, -T, -o, and --policy flags (at most one of each per call).
cerulion node modify detector -i sensor_msgs/Imu imu
Added input 'imu' to 'detector'
Adding a trigger input with -T also sets the data_trigger policy. To make a node externally triggered, use --policy external (there is no --ext-trigger flag).
cerulion node delete <node_type> removes the node crate and its workspace member entry.
cerulion node delete detector
Deleted node type 'detector'

Next steps

Wire and run a graph

Stage these node types into a graph and run it.

Trigger policies

The full --policy grammar and defaulting matrix.