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