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_blocking(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 (spin_blocking)
55//! - `alloc` - Enable heap allocation (parameter service boxed replies)
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#[cfg(feature = "alloc")]
85extern crate alloc;
86
87/// phase-412 -- the boot self-report, for boards with no reachable log sink.
88pub mod boot_report;
89pub mod c_waker;
90pub mod config;
91/// RFC-0088 / phase-421 W1 — the compile-time message-format check.
92///
93/// Gated exactly like [`session`], whose `IMAGE_SERIALIZATION_FORMAT_ID` it
94/// compares against: with no RMW seam compiled in there is no backend, so
95/// there is no format for a message to disagree with.
96#[cfg(any(has_rmw, test))]
97pub mod format_check;
98/// Phase 212.K.7.6.b — runtime cyclonedds type-descriptor registry hook.
99pub mod rmw_type_registry;
100
101/// Server-discovery probe cadence (issue #224 — one shared constant; was
102/// independently defined at four call sites across nros-node and nros-c).
103/// One probe per second balances "see freshly-declared tokens quickly"
104/// against "burn fewer FFI round-trips on a healthy network"; the outer
105/// wall-clock budget is the caller's.
106pub const SERVER_DISCOVERY_PROBE_TIMEOUT_MS: u32 = 1000;
107pub mod executor;
108pub mod lifecycle;
109pub mod limits;
110pub mod names;
111mod node;
112mod publisher;
113#[cfg(any(has_rmw, test))]
114pub mod session;
115mod subscriber;
116/// phase-425 W3 — the `/clock` time source. Feature-gated: an image that will
117/// never see a simulator should not carry the subscription or the message crate.
118/// Also gated on `has_rmw`, like `session` and the entity API it uses: without a
119/// backend there is no subscription to install, so the module would be a
120/// conversion helper with no caller.
121#[cfg(all(feature = "sim-time", any(has_rmw, test)))]
122pub mod time_source;
123pub mod timer;
124
125// MockSession only matters when neither a real RMW backend feature
126// nor lifecycle-services is enabled — the same gate as
127// `session::ConcreteSession = MockSession` and the executor tests in
128// `executor/mod.rs:42`. Compiling mock.rs unconditionally under
129// `cfg(test)` produced "never constructed / never used" warnings on
130// `cargo build --tests` when feature-unification activated a real
131// RMW backend (e.g. workspace builds with `rmw-uorb` on).
132#[cfg(all(test, not(feature = "rmw-cffi")))]
133pub(crate) mod mock;
134
135// Issue 0092 — the service servers these modules build (`executor::
136// EmbeddedServiceServer`) only exist when an RMW backend is present
137// (`#[cfg(any(has_rmw, test))]` on `executor::handles`). Gate the modules on
138// `has_rmw` too — `--features {lifecycle,param}-services` with no RMW otherwise
139// fails to resolve `EmbeddedServiceServer`. Service servers are meaningless
140// without a backend; every shipping app/entry selects an RMW (→ has_rmw). The
141// `test` arm keeps the modules in test builds.
142#[cfg(all(feature = "param-services", any(has_rmw, test)))]
143pub mod parameter_services;
144
145/// phase-303 W4 (#0267) — construct the outbound CDR writer. Every tx path routes
146/// through here so the wire encoding is chosen in ONE place.
147///
148/// **DEFAULT XCDR1 — corrected 2026-07-26 after live verification.** An earlier
149/// version selected XCDR2 (DELIMITED_CDR2) for iron/jazzy+. That is WRONG: a
150/// default Jazzy peer serializes its types FINAL/XCDR1 on the wire (verified
151/// live + by `nros_serdes::cdr::tests::xcdr1_header_matches_live_jazzy_wire_bytes`),
152/// and an APPENDABLE/XCDR2 writer is DDS-incompatible with its FINAL readers. So
153/// nano-ros emits XCDR1 — byte-identical to a default Jazzy node. The XCDR2 path
154/// (`new_with_header_xcdr2` + the generated DHEADER wrap) stays built for a
155/// future PER-TYPE `@appendable` opt-in, not an edition blanket. See #0267.
156#[inline]
157pub(crate) fn tx_writer(buf: &mut [u8]) -> Result<nros_core::CdrWriter<'_>, nros_core::SerError> {
158    nros_core::CdrWriter::new_with_header(buf)
159}
160
161// Re-export parameter types when param-services is enabled
162#[cfg(feature = "param-services")]
163pub use nros_params::{
164    ParameterDescriptor, ParameterServer, ParameterType, ParameterValue, SetParameterResult,
165};
166
167#[cfg(all(feature = "lifecycle-services", any(has_rmw, test)))]
168pub mod lifecycle_services;
169
170// Export standalone node (without transport)
171pub use node::{Node as StandaloneNode, NodeConfig, NodeError as StandaloneNodeError};
172
173pub use publisher::PublisherHandle;
174pub use subscriber::SubscriptionHandle;
175
176// Re-export transport types for convenience
177pub use nros_rmw::{
178    ActionInfo, QoSDurabilityPolicy, QoSHistoryPolicy, QoSLivelinessPolicy, QoSPolicyMask,
179    QoSProfile, QoSReliabilityPolicy, ServiceInfo, TopicInfo, TransportConfig, TransportError,
180};
181
182// Re-export RMW protocol traits so thin wrappers (nros-c, nros-cpp) can
183// pull them through nros-node instead of going around it. Phase 91.B.
184pub use nros_rmw::{
185    ClientTrait, Publisher, ServiceTrait, Session, Subscription as SubscriptionTrait,
186};
187
188// Re-export action protocol types from nros-core. Same motivation as the
189// RMW trait re-exports above — keeps thin wrappers off the
190// nros-core::* path. Phase 91.B5.
191pub use nros_core::{CancelResponse, CancelReturnCode, GoalId, GoalResponse, GoalStatus};
192
193// Re-export lifecycle protocol types. Phase 91.B2.
194pub use nros_core::lifecycle::{LifecycleState, LifecycleTransition, TransitionResult};
195
196// Re-export CDR ser/de types so the C-side serialization helpers in
197// nros-c/src/cdr.rs don't have to reach past nros-node either. These
198// are themselves re-exports from nros-serdes via nros-core; collecting
199// them here keeps the import boundary uniform. Phase 91.B6.
200pub use nros_core::{
201    CdrReader, CdrWriter, DHeaderMark, DHeaderScope, DeserError, EncodingVersion, SerError,
202};
203
204// Re-export safety types when feature is enabled
205#[cfg(feature = "safety-e2e")]
206pub use nros_rmw::{IntegrityStatus, SafetyValidator};
207
208// Re-export publisher/subscriber options (topic + QoS; backend-agnostic).
209pub use node::{PublisherOptions, SubscriptionOptions};
210
211// Re-export session mode (used by ExecutorConfig)
212pub use nros_rmw::SessionMode;
213
214// Re-export timer types
215pub use timer::{TimerCallbackFn, TimerDuration, TimerHandle, TimerMode, TimerState};
216
217// Re-export lifecycle types
218pub use lifecycle::{LifecycleCallbackFn, LifecycleError, LifecyclePollingNode};
219
220// Re-export types that don't depend on RMW (always available)
221pub use executor::{
222    BOOT_SET_DOMAIN, BOOT_SET_LOCATOR, BOOT_SET_NAMESPACE, BOOT_SET_NODE_NAME, BOOT_SET_RMW,
223    BakedBootConfig, BootConfig, BootConfigError, DOMAIN_ID_EXPLICIT_ZERO_C_ABI, DOMAIN_ID_MAX,
224    EnvRung, ExecutorConfig, ExecutorSemantics, GuardCondition, HandleId, HandleSet,
225    InvocationMode, NROS_BOOT_CONFIG_MAGIC, NROS_BOOT_CONFIG_VERSION, NodeError,
226    RawAcceptedCallback, RawCancelCallback, RawGoalCallback, RawResponseCallback,
227    RawServiceCallback, RawSubscriptionCallback, ReadinessSnapshot, ShutdownCallbackFn,
228    ShutdownCallbackHandle, ShutdownPhase, SpinOnceResult, SpinOptions, SpinPeriodPollingResult,
229    Trigger, baked_domain_from_c_abi,
230};
231
232// Re-export RMW-dependent executor types
233#[cfg(any(has_rmw, test))]
234pub use executor::{
235    ActionClient, ActionClientCore, ActionServer, ActionServerCore, ActionServerHandle,
236    ActionServerRawHandle, ActiveGoal, CallbackGroup, CompletedGoal, EmbeddedPublisher,
237    EmbeddedRawPublisher, EmbeddedServiceClient, EmbeddedServiceServer, Executor, FeedbackStream,
238    GoalFeedbackStream, LoanError, NodeHandle, Promise, PublishLoan, RawActionClientSpec,
239    RawActionServerSpec, RawActiveGoal, RawServiceClient, RawServiceServer, RawSubscription,
240    RecvView, SessionHandle, Subscription, action_channel_type, executor_storage_layout,
241    executor_storage_u64_len,
242};
243#[cfg(any(has_rmw, test))]
244pub use executor::{ExecutorInlineStorage, ExecutorSizing};
245
246// issue 0687 — the selector's CAP (its reader lives at the hosted edge, in
247// `nros::env`) and the hosted wall clock the edge installs on a resolved
248// config. Both are consumed by `nros`, which builds what this crate takes.
249pub use executor::{RMW_SELECTOR_CAP, default_epoch_us_fn};
250
251// Phase 173.5 — bridge multi-session spec (consumed by the generated
252// orchestration package's `Executor::open_multi`). Gated to match
253// `executor::SessionSpec` (needs the cffi vtable surface).
254#[cfg(all(any(has_rmw, test), feature = "rmw-cffi"))]
255pub use executor::SessionSpec;
256
257#[cfg(all(feature = "alloc", any(has_rmw, test)))]
258pub use executor::SpinPeriodResult;
259
260// ---------------------------------------------------------------------------
261// phase-361 W8.e / issue 0594 — capabilities REQUIRE the heap / the standard
262// library, they do not enable it. Turning `alloc` or `std` on for the user
263// silently changes what their firmware image is; naming the feature they must
264// add does not.
265// ---------------------------------------------------------------------------
266#[cfg(all(feature = "param-services", not(feature = "alloc")))]
267compile_error!("`param-services` allocates: add \"alloc\" to this crate's features");
268#[cfg(all(feature = "lifecycle-services", not(feature = "alloc")))]
269compile_error!("`lifecycle-services` allocates: add \"alloc\" to this crate's features");
270// phase-359 W10 — the forwarder's WORKER is a platform task now, not a
271// `std::thread`, and it signals a `NodeWake` rather than a `Condvar`. Two of
272// the three requirements are therefore no longer `std`: it needs the heap for
273// its context (`alloc`) and a linked platform for `nros_platform_task_*` /
274// `nros_platform_wake_*` (`rmw-cffi`, the same proxy `node_wake` uses).
275//
276// `std` REMAINS required, for one reason with a known end: the wake state it
277// forwards into is still `WakeCtx`, the condvar-carrying type gated on `std`.
278// Deleting that type — the campaign's next W10 step, and the one this port
279// unblocked — is what removes this line.
280#[cfg(all(feature = "signal-fd-wake", not(feature = "alloc")))]
281compile_error!("`signal-fd-wake` allocates: add \"alloc\" to this crate's features");
282#[cfg(all(feature = "signal-fd-wake", not(feature = "rmw-cffi")))]
283compile_error!(
284    "`signal-fd-wake` needs the platform task + wake ABI: add \"rmw-cffi\" to this crate's features"
285);
286// phase-359 W10 — this guard said `signal-fd-wake` "still reaches the std-gated
287// `WakeCtx`: add \"std\" (phase-359 W10 removes this)". It did remove it: the
288// wake context is `alloc`-gated, its worker is a platform task, and the only
289// `std::` left in `WakeSignalFd` is a comment about what it stopped returning.
290// What the feature actually needs is the allocator the context is built on.
291#[cfg(all(feature = "signal-fd-wake", not(feature = "alloc")))]
292compile_error!("`signal-fd-wake` builds on the `alloc`-gated wake context: add \"alloc\"");
293// issue 0687 — `env` used to be declared here, and to require `std` for the
294// same reason every capability in this block does. It is gone: reading the
295// process environment moved to the hosted edge (`nros::env`), so the guard has
296// nothing left to guard. What replaced it is a VALUE — `ExecutorConfig::
297// resolve_with` takes an `EnvRung` — which needs no capability at all.