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:
| Item | Description |
|---|
SafetyControllerEntry | Wrapper containing inner and context. |
::new() | Constructor (requires Default). |
::with_state(inner) | Constructor from an existing state value. |
impl NodeEntry | Provides info, init, tick, shutdown. |
| cdylib FFI symbols | Exported 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.
| Attribute | Semantics |
|---|
period_ms = N | Fire every N ms. |
deadline_ms = N | Fire 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 = N | Bounded sync: fire when all #[input(trigger)] ports have an unconsumed message that arrived within an N-ms window. Requires β₯2 trigger inputs. |
unbounded_sync | Unbounded sync: fire when every #[input(trigger)] port has an unconsumed message, with no time bound. Requires β₯2 trigger inputs. Not recommended for control loops. |
external | Externally triggered; the host calls Scheduler::trigger_external(node_id). |
tick_deadline_ms = N | QoS, 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:
| Attribute | Reason |
|---|
type_name | Removed. The node type is the folder name. |
inputs(...) | Removed. Ports are declared as field attributes. |
outputs(...) | Removed. Ports are declared as field attributes. |
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)].
| Key | Meaning |
|---|
trigger | This input fires the node (data trigger / sync member). |
fifo / lifo | Queue discipline (mutually exclusive; duplicate β error). |
depth = N | In-flight receive queue depth. |
backpressure = drop_oldest | drop_newest | block | throttle(ms) | Overflow policy. |
max_age_ms = N | Drop messages older than N ms. |
filter = "fn" | Named filter function. |
deadline_ms = N | QoS (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)].
| Key | Meaning |
|---|
| 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 = N | QoS (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:
| Variant | Source attribute |
|---|
Period { period_ms } | period_ms = N |
Deadline { deadline_ms } | deadline_ms = N |
Sync { window_ms } | sync_window_ms = N |
UnboundedSync | unbounded_sync |
External | external |
DataTrigger { input_name } | inferred from #[input(trigger)] |
PolicyJson is the wire-shape mirror used in the cdylib cerulion_node_info() JSON.