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 struct annotated with #[cerulion_node(...)] paired with an adjacent impl block annotated with #[cerulion_node_impl]. The macros generate a <Name>Entry wrapper, a NodeEntry implementation, and cdylib FFI symbols. Import everything from the prelude:
use cerulion_core::prelude::*;
The prelude re-exports cerulion_node, cerulion_node_impl, NodeError, NodeResult, and the message/transport types.

The two macros

The macros are used as a pair:
  • #[cerulion_node(...)] on the struct generates the <Name>Entry wrapper, the NodeEntry impl, and the cdylib FFI (behind #[cfg(feature = "cdylib")]).
  • #[cerulion_node_impl] on an adjacent impl <Name> { fn tick(&mut self) -> Result<(), NodeError> { ... } } AST-rewrites self.<port> accesses into zero-copy shared-memory reads and writes.
#[cerulion_node_impl] takes no arguments. The struct must appear before the impl block.

Complete example

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 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 via Deref<Target = Vector3Shm> β€” writes straight to SHM:
        self.linear_velocity.x = if obstacle_close { 0.0 } else { 0.3 };
        Ok(())
    }
}
// Use in a graph: SafetyControllerEntry::new() implements NodeEntry.

Generated code

For a struct named SafetyController, the macros generate:
ItemDescription
SafetyControllerEntryWrapper containing inner and context.
::new()Constructor (requires Default).
::with_state(inner)Constructor from an existing state value.
impl NodeEntryProvides info, init, tick, shutdown.
cdylib FFI symbolsExported for dynamic loading.
For a struct <Name>, the macro generates <Name>Entry. For example, CameraNode generates CameraNodeEntry.

Node-level attributes

#[cerulion_node(...)] attributes are all optional. Exactly one trigger-policy hint applies (or the trigger is inferred from a field marked #[input(trigger)]). tick_deadline_ms is orthogonal and stacks with any trigger policy.
AttributeSemantics
period_ms = NFire every N ms.
deadline_ms = NFire on data arrival (like a data trigger); if no data arrives within N ms, report a miss in the trace. Does not fire on timeout.
sync_window_ms = NBounded sync: fire when all #[input(trigger)] ports have an unconsumed message that arrived within an N-ms window. Requires β‰₯2 trigger inputs.
unbounded_syncUnbounded sync: fire when every #[input(trigger)] port has an unconsumed message, with no time bound. Requires β‰₯2 trigger inputs. Not recommended for control loops.
externalExternally triggered; the host calls Scheduler::trigger_external(node_id).
tick_deadline_ms = NQoS, not a trigger. Per-node tick-execution deadline; tick callbacks taking longer than N ms increment execution_deadline_missed_count and emit tracing::warn!. Stacks with any trigger policy.

Mutual exclusion

unbounded_sync is mutually exclusive with sync_window_ms, period_ms, deadline_ms, and external. unbounded_sync and sync_window_ms each require β‰₯2 trigger inputs. tick_deadline_ms is orthogonal and stacks with any trigger policy.

Rejected attributes

These attributes are rejected at parse time:
AttributeReason
type_nameRemoved. The node type is the folder name.
inputs(...)Removed. Ports are declared as field attributes.
outputs(...)Removed. Ports are declared as field attributes.

#[input(...)] attribute

Forms: #[input], #[input(trigger)], #[input(trigger, lifo, depth = 1)], #[input(fifo, depth = 100, backpressure = drop_oldest, max_age_ms = 200)], #[input(filter = "fn_name")], #[input(deadline_ms = N)].
KeyMeaning
triggerThis input fires the node (data trigger / sync member).
fifo / lifoQueue discipline (mutually exclusive; duplicate β†’ error).
depth = NIn-flight receive queue depth.
backpressure = drop_oldest | drop_newest | block | throttle(ms)Overflow policy.
max_age_ms = NDrop messages older than N ms.
filter = "fn"Named filter function.
deadline_ms = NQoS (subscriber-side): if N ms elapse with no new message, increment requested_deadline_missed_count and warn. Not a trigger.

#[output(...)] attribute

Forms: #[output] (fixed-only schema), #[output(data, encoding)] (lists simple variable fields β†’ rewriter targets set_<f>(...)?), #[output(data, complex(header))] (complex(...) lists nested-typed variable fields β†’ set_<f>_bytes(...)?), #[output(deadline_ms = N)].
KeyMeaning
bare ident(s)Simple variable-length fields (string, T[]). Rewriter: self.<port>.<f> = expr β†’ set_<f>(&expr[..])?.
complex(name, ...)Nested-type variable fields. Rewriter targets set_<name>_bytes.
deadline_ms = NQoS (publisher-side): if N ms elapse with no publish, increment offered_deadline_missed_count and warn. N must be > 0.
Fixed primitive fields are written directly (for example self.image.height = ...) via Deref to the schema’s …FixedSection/…Shm.

MacroPolicy enum

The macro attributes map to the MacroPolicy enum in cerulion_core::graph::node:
VariantSource attribute
Period { period_ms }period_ms = N
Deadline { deadline_ms }deadline_ms = N
Sync { window_ms }sync_window_ms = N
UnboundedSyncunbounded_sync
Externalexternal
DataTrigger { input_name }inferred from #[input(trigger)]
PolicyJson is the wire-shape mirror used in the cdylib cerulion_node_info() JSON.