Skip to main content

nros_node/
lib.rs

1//! Node abstraction for nros
2//!
3//! This crate provides the high-level Node API for creating ROS 2 compatible
4//! publishers and subscribers on embedded systems.
5//!
6//! # Executor-Based API
7//!
8//! The executor-based API provides a unified interface that works on both
9//! std (desktop) and no_std (embedded) targets.
10//!
11//! ## Desktop Example
12//!
13//! ```ignore
14//! use nros::prelude::*;
15//! use std_msgs::msg::Int32;
16//!
17//! let config = ExecutorConfig::from_env().node_name("my_node");
18//! let mut executor: Executor = Executor::open(&config)?;
19//!
20//! // Register subscription callback
21//! let node = executor.node_builder("my_node").build()?;
22//! executor.node_mut(node).create_subscription::<Int32, _>("/topic", |msg: &Int32| {
23//!     println!("Received: {}", msg.data);
24//! })?;
25//!
26//! // Spin (processes callbacks)
27//! executor.spin(SpinOptions::default());
28//! ```
29//!
30//! ## Embedded Example
31//!
32//! ```ignore
33//! use nros::prelude::*;
34//! use std_msgs::msg::Int32;
35//!
36//! let config = ExecutorConfig { locator: "tcp/192.168.1.1:7447", ..Default::default() };
37//! let mut executor: Executor = Executor::open(&config)?;
38//!
39//! // Register subscription callback
40//! let node = executor.node_builder("my_node").build()?;
41//! executor.node_mut(node).create_subscription::<Int32, _>("/cmd", |msg: &Int32| {
42//!     // process message...
43//! })?;
44//!
45//! // In your main loop:
46//! loop {
47//!     executor.spin_once(core::time::Duration::from_millis(10));
48//!     // platform delay...
49//! }
50//! ```
51//!
52//! # Features
53//!
54//! - `alloc` - Enable heap allocation (the parameter store's heap fallback, argv parameter overrides)
55
56#![no_std]
57
58// phase-359 W10 — force the POSIX platform port into the UNIT-TEST link.
59//
60// `nros-platform-cffi` (with `posix-c-port`) is a host dev-dependency, but a
61// dev-dep is only LINKED if something references the crate, and nothing in this
62// crate's `src/` does — it reaches the platform through bare `extern "C"`
63// declarations. So the C objects were never pulled in and every
64// `nros_platform_*` symbol was undefined at test-link time, which is why
65// `open_threaded`'s tests could not be run against a platform task until now.
66// Issue 0612 records the same shape for `tests/signal_fd_wake.rs`, which this
67// does NOT fix: that is a separate integration binary with its own link.
68#[cfg(test)]
69extern crate nros_platform_cffi as _;
70
71// Phase 248 (C2) — `nros-rmw-cyclonedds[-sys]` deps removed (issue #60,
72// Tier 1). Per-type descriptor registration is now the generic
73// `nros_rmw::register_type_descriptor` seam (see `rmw_type_registry`);
74// the Cyclone backend installs its registrar from its own crate. The
75// `needs-type-descriptors` capability feature (no dep edge) still emits
76// `cfg(rmw_needs_type_descriptors)` to compile the schema-passing body +
77// `M: Message` bound for builds where a descriptor-needing backend is
78// linked by the umbrella.
79
80// phase-359 W10 — no `std` feature. A test build is a hosted harness, so
81// `std` exists there whatever the features say; nothing else gets it.
82#[cfg(test)]
83extern crate std;
84
85#[cfg(feature = "alloc")]
86extern crate alloc;
87
88/// issue 2014 -- the wire framing of action messages: one spelling of every payload offset.
89pub mod action_frame;
90/// phase-412 -- the boot self-report, for boards with no reachable log sink.
91pub mod boot_report;
92pub mod c_waker;
93pub mod config;
94/// phase-454 W10 — the DECLARED QoS history depth, and the registration check
95/// over it. Rust's half of what C++ does with `NROS_SUBSCRIBE`'s
96/// `static_assert` and C with `NROS_ASSERT_DECLARED_DEPTH`.
97pub mod declared_qos;
98/// RFC-0088 / phase-421 W1 — the compile-time message-format check.
99///
100/// Gated exactly like [`session`], whose `IMAGE_SERIALIZATION_FORMAT_ID` it
101/// compares against: with no RMW seam compiled in there is no backend, so
102/// there is no format for a message to disagree with.
103#[cfg(any(has_rmw, test))]
104pub mod format_check;
105/// Phase 212.K.7.6.b — runtime cyclonedds type-descriptor registry hook.
106pub mod rmw_type_registry;
107
108pub mod executor;
109pub mod lifecycle;
110pub mod limits;
111pub mod names;
112mod node;
113mod node_options;
114mod primitive_options;
115mod publisher;
116pub mod ros_args;
117#[cfg(any(has_rmw, test))]
118pub mod session;
119mod subscriber;
120/// phase-425 W3 — the `/clock` time source. Feature-gated: an image that will
121/// never see a simulator should not carry the subscription or the message crate.
122/// Also gated on `has_rmw`, like `session` and the entity API it uses: without a
123/// backend there is no subscription to install, so the module would be a
124/// conversion helper with no caller.
125#[cfg(all(feature = "sim-time", any(has_rmw, test)))]
126pub mod time_source;
127pub mod timer;
128
129// MockSession only matters when neither a real RMW backend feature
130// nor lifecycle-services is enabled — the same gate as
131// `session::ConcreteSession = MockSession` and the executor tests in
132// `executor/mod.rs:42`. Compiling mock.rs unconditionally under
133// `cfg(test)` produced "never constructed / never used" warnings on
134// `cargo build --tests` when feature-unification activated a real
135// RMW backend (e.g. workspace builds with `rmw-uorb` on).
136#[cfg(all(test, not(feature = "rmw-cffi")))]
137pub(crate) mod mock;
138
139// Issue 0092 — the service servers these modules build (`executor::
140// EmbeddedServiceServer`) only exist when an RMW backend is present
141// (`#[cfg(any(has_rmw, test))]` on `executor::handles`). Gate the modules on
142// `has_rmw` too — `--features {lifecycle,param}-services` with no RMW otherwise
143// fails to resolve `EmbeddedServiceServer`. Service servers are meaningless
144// without a backend; every shipping app/entry selects an RMW (→ has_rmw). The
145// `test` arm keeps the modules in test builds.
146#[cfg(all(feature = "param-services", any(has_rmw, test)))]
147pub mod parameter_services;
148
149// Issue 1897 -- the parameter STORE's executor state, on its own feature.
150// `param-services` implies `param-store`, and the six servers' module above
151// builds on this one; a `param-store` image compiles none of them.
152#[cfg(all(feature = "param-store", any(has_rmw, test)))]
153pub(crate) mod param_store;
154
155// Issue 1468 — the parameter family's message GEOMETRY, which BOTH service
156// families are sized from, so its gate is the DISJUNCTION of theirs.
157//
158// `lifecycle_services` reads three numbers out of here (the inbox slot, the
159// depth, the service-set bound) and phase-461 W2 read them out of
160// `parameter_services` instead, across the gate above. The numbers were right;
161// the module holding them was behind a feature the reader does not select, so
162// `--features lifecycle-services` without `param-services` did not compile.
163//
164// Separating them is the fix and the honest shape: the SERVICES need the
165// `rcl_interfaces` message crate `param-services` brings, the GEOMETRY is
166// arithmetic over `config`'s declared shapes and `nros_params`' resolved
167// capacities and needs nothing. Only the first is a capability a consumer
168// picks. `any(has_rmw, test)` is carried over unchanged from both module
169// gates, so no image compiles this that did not compile it before.
170#[cfg(all(
171    any(feature = "param-store", feature = "lifecycle-services"),
172    any(has_rmw, test)
173))]
174pub mod param_sizing;
175
176/// phase-303 W4 (#0267) — construct the outbound CDR writer. Every tx path routes
177/// through here so the wire encoding is chosen in ONE place.
178///
179/// **DEFAULT XCDR1 — corrected 2026-07-26 after live verification.** An earlier
180/// version selected XCDR2 (DELIMITED_CDR2) for iron/jazzy+. That is WRONG: a
181/// default Jazzy peer serializes its types FINAL/XCDR1 on the wire (verified
182/// live + by `nros_serdes::cdr::tests::xcdr1_header_matches_live_jazzy_wire_bytes`),
183/// and an APPENDABLE/XCDR2 writer is DDS-incompatible with its FINAL readers. So
184/// nano-ros emits XCDR1 — byte-identical to a default Jazzy node. The XCDR2 path
185/// (`new_with_header_xcdr2` + the generated DHEADER wrap) stays built for a
186/// future PER-TYPE `@appendable` opt-in, not an edition blanket. See #0267.
187#[inline]
188pub(crate) fn tx_writer(buf: &mut [u8]) -> Result<nros_core::CdrWriter<'_>, nros_core::SerError> {
189    nros_core::CdrWriter::new_with_header(buf)
190}
191
192// Re-export parameter types when param-services is enabled.
193//
194// phase-417 W4.a — `ParameterRange` and the on-set-hook vocabulary join the
195// list for the reason the list exists: `nros-c` and `nros-cpp` depend on
196// `nros-node` and not on `nros-params`, so a type they cannot name here is a
197// capability their surface cannot expose, whatever the store does.
198/// Deprecated (phase-515): the store answers `Result<(), ParameterError>`.
199#[cfg(feature = "param-store")]
200#[allow(deprecated)]
201pub use nros_params::SetParameterResult;
202#[cfg(feature = "param-store")]
203pub use nros_params::{
204    NodeKey as ParameterNodeKey, OnSetContext, OnSetParameterFn, OnSetParameterHandle,
205    ParameterDescriptor, ParameterError, ParameterRange, ParameterServer, ParameterType,
206    ParameterValue,
207};
208// phase-491 — the bounds a C caller's buffers are sized against (RFC-0105 D10).
209#[cfg(feature = "param-store")]
210pub use nros_params::{
211    MAX_ARRAY_LEN, MAX_BYTE_ARRAY_LEN, MAX_PARAM_NAME_LEN, MAX_STRING_VALUE_LEN,
212};
213
214#[cfg(all(feature = "lifecycle-services", any(has_rmw, test)))]
215pub mod lifecycle_services;
216
217/// phase-467 Q4 / ledger row `c:logging_rosout_enabled` — the `/rosout`
218/// publisher half. `has_rmw` for the same reason `lifecycle_services` has it:
219/// the module names `EmbeddedPublisher`, which does not exist without a
220/// backend.
221#[cfg(all(feature = "rosout", any(has_rmw, test)))]
222pub mod rosout;
223
224// Export standalone node (without transport)
225pub use node::{Node as StandaloneNode, NodeConfig, NodeError as StandaloneNodeError};
226
227pub use publisher::PublisherHandle;
228pub use subscriber::SubscriptionHandle;
229
230// Re-export transport types for convenience
231pub use nros_rmw::{
232    ActionEndpointHashes, ActionInfo, QoSDurabilityPolicy, QoSHistoryPolicy, QoSLivelinessPolicy,
233    QoSPolicyMask, QoSProfile, QoSReliabilityPolicy, ServiceInfo, TopicInfo, TransportConfig,
234    TransportError,
235};
236
237// Re-export RMW protocol traits so thin wrappers (nros-c, nros-cpp) can
238// pull them through nros-node instead of going around it. Phase 91.B.
239pub use nros_rmw::{
240    ClientTrait, Publisher, ServiceTrait, Session, Subscription as SubscriptionTrait,
241};
242
243// Re-export action protocol types from nros-core. Same motivation as the
244// RMW trait re-exports above — keeps thin wrappers off the
245// nros-core::* path. Phase 91.B5.
246pub use nros_core::{CancelResponse, CancelResponseCode, GoalResponse, GoalStatusCode, GoalUuid};
247#[allow(deprecated)]
248pub use nros_core::{CancelReturnCode, GoalId, GoalStatus};
249
250// Re-export lifecycle protocol types. Phase 91.B2.
251pub use nros_core::lifecycle::{LifecycleState, LifecycleTransition, TransitionResult};
252
253// Re-export CDR ser/de types so the C-side serialization helpers in
254// nros-c/src/cdr.rs don't have to reach past nros-node either. These
255// are themselves re-exports from nros-serdes via nros-core; collecting
256// them here keeps the import boundary uniform. Phase 91.B6.
257pub use nros_core::{
258    CdrReader, CdrWriter, DHeaderMark, DHeaderScope, DeserError, EncodingVersion, SerError,
259};
260
261// Re-export safety types when feature is enabled
262#[cfg(feature = "safety-e2e")]
263pub use nros_rmw::{IntegrityStatus, SafetyValidator};
264
265// Re-export publisher/subscriber options (topic + QoS; backend-agnostic).
266pub use node::{PublisherOptions, SubscriptionOptions};
267
268/// phase-515 R2 -- rclrs's node options and `IntoNodeOptions`.
269pub use node_options::{IntoNodeOptions, NodeOptions};
270/// phase-515 R1 -- rclrs's per-entity options and `IntoPrimitiveOptions`.
271pub use primitive_options::{IntoPrimitiveOptions, PrimitiveOptions};
272
273// Re-export session mode (used by ExecutorConfig)
274pub use nros_rmw::SessionMode;
275
276// Re-export timer types
277pub use timer::{
278    TimerCallInfo, TimerCallbackFn, TimerDuration, TimerHandle, TimerMode, TimerState,
279};
280
281// Re-export lifecycle types
282pub use lifecycle::{LifecycleCallbackFn, LifecycleError, LifecyclePollingNode};
283
284// Re-export types that don't depend on RMW (always available)
285pub use executor::{
286    BOOT_SET_DOMAIN, BOOT_SET_LOCATOR, BOOT_SET_NAMESPACE, BOOT_SET_NODE_NAME, BOOT_SET_RMW,
287    BakedBootConfig, BindError, BootConfig, BootConfigError, DOMAIN_ID_EXPLICIT_ZERO_C_ABI,
288    DOMAIN_ID_MAX, DeferNotify, EnvRung, ExecutorConfig, ExecutorSemantics, GuardCondition,
289    HandleId, HandleSet, InvocationMode, NROS_BOOT_CONFIG_MAGIC, NROS_BOOT_CONFIG_VERSION,
290    NodeError, RawAcceptedCallback, RawCancelCallback, RawGoalCallback, RawRequestId,
291    RawResponseCallback, RawResponseRequestIdCallback, RawServiceCallback,
292    RawServiceRequestIdCallback, RawSubscriptionCallback, ReadinessSnapshot, ShutdownCallbackFn,
293    ShutdownCallbackHandle, ShutdownPhase, SpinOnceResult, SpinOptions, SpinPeriodPollingResult,
294    Trigger, baked_domain_from_c_abi,
295};
296// phase-521 W6 — the two entry spin budgets.
297pub use executor::{ENTRY_IDLE_BUDGET, ENTRY_POLL_BUDGET};
298// Issue 1971 -- the launch remap table (RFC-0107 step 1).
299pub use executor::{
300    BadLaunchRemap, BadSessionProperties, LaunchRemap, MAX_SESSION_PROPERTIES, SessionProperty,
301    launch_remaps, lent_session_properties, session_properties, set_launch_remaps,
302    set_session_properties, with_session_properties,
303};
304
305// Re-export RMW-dependent executor types
306#[cfg(any(has_rmw, test))]
307pub use executor::{
308    ActionClient, ActionClientCore, ActionServer, ActionServerCore, ActionServerHandle,
309    ActionServerRawHandle, ActiveGoal, CallbackGroup, CompletedGoal, EmbeddedPublisher,
310    EmbeddedRawPublisher, EmbeddedServiceClient, EmbeddedServiceServer, Executor, FeedbackStream,
311    GoalFeedbackStream, LoanError, Node, NodeHandle, Promise, PublishLoan, RawActionClientSpec,
312    RawActionServerSpec, RawActiveGoal, RawServiceClient, RawServiceServer, RawSubscription,
313    RecvView, RemapRefused, SessionHandle, Subscription, action_channel_type,
314    executor_storage_layout, executor_storage_layout_with, executor_storage_u64_len,
315};
316#[cfg(any(has_rmw, test))]
317pub use executor::{
318    ExecutorInlineStorage, ExecutorSizing, INLINE_BACKING_U64S, INLINE_SIZING, RegionUnit,
319    RegionUnits, levels_backing_bytes, levels_executor_sizing, tier_backing_bytes,
320    tier_executor_sizing, tier_sizing,
321};
322
323// issue 0687 — the selector's CAP (its reader lives at the hosted edge, in
324// `nros::env`) and the hosted wall clock the edge installs on a resolved
325// config. Both are consumed by `nros`, which builds what this crate takes.
326pub use executor::{RMW_SELECTOR_CAP, default_epoch_us_fn};
327
328// Phase 173.5 — bridge multi-session spec (consumed by the generated
329// orchestration package's `Executor::open_multi`). Gated to match
330// `executor::SessionSpec` (needs the cffi vtable surface).
331#[cfg(all(any(has_rmw, test), feature = "rmw-cffi"))]
332pub use executor::SessionSpec;
333
334#[cfg(all(feature = "alloc", any(has_rmw, test)))]
335pub use executor::SpinPeriodResult;
336
337// ---------------------------------------------------------------------------
338// phase-361 W8.e / issue 0594 — capabilities REQUIRE the heap / the standard
339// library, they do not enable it. Turning `alloc` or `std` on for the user
340// silently changes what their firmware image is; naming the feature they must
341// add does not.
342// ---------------------------------------------------------------------------
343// Issue 1812 — the parameter store (`param-store` since issue 1897) no longer
344// requires `alloc`. The store, the
345// service state, the shared buffer pair and the staging slot a remote set
346// decodes into are all CARVED from the executor backing when the build knows
347// the image builds a store (`nros_params::IMPLIED_STORE_SLOTS`). Without a
348// heap there is no fallback for a build that does not know, so that build is
349// refused HERE, naming the knob, rather than at the first declaration.
350// (`lifecycle-services` keeps its own guard below: its processor is still
351// boxed, which is a separate item.)
352#[cfg(all(feature = "param-store", not(feature = "alloc"), any(has_rmw, test)))]
353const _: () = assert!(
354    nros_params::IMPLIED_STORE_SLOTS > 0,
355    "`param-store` without `alloc` needs the parameter store carved from the executor \
356     backing, and this build carves none: set NROS_PARAM_STORE=1 (sized by \
357     NROS_MAX_PARAMETERS), or declare `params:` in the contract / `param_services` in the \
358     bringup, or add \"alloc\" to this crate's features (issue 1812)"
359);
360#[cfg(all(feature = "profile-mode", not(feature = "alloc")))]
361compile_error!("`profile-mode` records into a heap-allocated table: add \"alloc\"");
362#[cfg(all(feature = "lifecycle-services", not(feature = "alloc")))]
363compile_error!("`lifecycle-services` allocates: add \"alloc\" to this crate's features");
364// phase-359 W10 — the forwarder's WORKER is a platform task now, not a
365// `std::thread`, and it signals a `NodeWake` rather than a `Condvar`. Two of
366// the three requirements are therefore no longer `std`: it needs the heap for
367// its context (`alloc`) and a linked platform for `nros_platform_task_*` /
368// `nros_platform_wake_*` (`rmw-cffi`, the same proxy `node_wake` uses).
369//
370// `std` REMAINS required, for one reason with a known end: the wake state it
371// forwards into is still `WakeCtx`, the condvar-carrying type gated on `std`.
372// Deleting that type — the campaign's next W10 step, and the one this port
373// unblocked — is what removes this line.
374#[cfg(all(feature = "signal-fd-wake", not(feature = "alloc")))]
375compile_error!("`signal-fd-wake` allocates: add \"alloc\" to this crate's features");
376#[cfg(all(feature = "signal-fd-wake", not(feature = "rmw-cffi")))]
377compile_error!(
378    "`signal-fd-wake` needs the platform task + wake ABI: add \"rmw-cffi\" to this crate's features"
379);
380// phase-359 W10 — this guard said `signal-fd-wake` "still reaches the std-gated
381// `WakeCtx`: add \"std\" (phase-359 W10 removes this)". It did remove it: the
382// wake context is `alloc`-gated, its worker is a platform task, and the only
383// `std::` left in `WakeSignalFd` is a comment about what it stopped returning.
384// What the feature actually needs is the allocator the context is built on.
385#[cfg(all(feature = "signal-fd-wake", not(feature = "alloc")))]
386compile_error!("`signal-fd-wake` builds on the `alloc`-gated wake context: add \"alloc\"");
387// issue 0687 — `env` used to be declared here, and to require `std` for the
388// same reason every capability in this block does. It is gone: reading the
389// process environment moved to the hosted edge (`nros::env`), so the guard has
390// nothing left to guard. What replaced it is a VALUE — `ExecutorConfig::
391// resolve_with` takes an `EnvRung` — which needs no capability at all.