nros/lib.rs
1//! # nros
2//!
3//! A lightweight ROS 2 client library for embedded systems.
4//!
5//! This crate provides a unified API for building ROS 2 nodes in Rust,
6//! with support for `no_std` environments and embedded targets.
7//!
8//! ## Features
9//!
10//! - **no_std compatible**: Works on bare-metal and RTOS targets
11//! - **Zero-copy where possible**: Minimizes memory allocations
12//! - **Type-safe**: Compile-time verification of message types
13//! - **ROS 2 compatible**: Interoperates with standard ROS 2 nodes via rmw_zenoh
14//!
15//! ## Quick Start
16//!
17//! ```ignore
18//! use nros::prelude::*;
19//! use std_msgs::msg::Int32;
20//!
21//! let config = ExecutorConfig::from_env().node_name("my_node");
22//! let mut executor = Executor::open(&config)?;
23//!
24//! let node = executor.node_builder("my_node").build()?;
25//! let publisher = executor.node_mut(node).create_publisher::<Int32>("/my_topic")?;
26//! publisher.publish(&Int32 { data: 42 })?;
27//!
28//! executor.node_mut(node).create_subscription::<Int32, _>("/topic", |msg: &Int32| {
29//! println!("Received: {}", msg.data);
30//! })?;
31//!
32//! executor.spin(SpinOptions::default());
33//! ```
34//!
35//! ## Executor Sizing
36//!
37//! The executor's static memory layout is controlled via environment variables
38//! at build time:
39//!
40//! - **`NROS_EXECUTOR_MAX_CBS`** (default 4) — maximum number of registered
41//! callbacks (subscriptions + timers + services + guard conditions).
42//! - **`NROS_EXECUTOR_ARENA_SIZE`** (default 4096) — byte budget for storing
43//! callback closures inline.
44//!
45//! A subscription's arena receive buffer is sized from the MESSAGE TYPE's own
46//! serialized bound (phase-392 W3c), not from that knob, wherever the bound is
47//! reachable. `.rx_buffer::<N>()` is the opt-out: it says "give me exactly `N`
48//! bytes per slot" and skips the derivation, which is what a peer that pads or
49//! a `.msg` bound looser than the traffic needs (e.g.
50//! `node_mut(id).subscription(t).typed::<M>().rx_buffer::<4096>().build(cb)`).
51//!
52//! ## Transport Backends
53//!
54//! Phase 248 C5c — `nros` is RMW- and platform-AGNOSTIC. It carries only the
55//! `rmw-cffi` vtable; the concrete backend (zenoh / xrce / cyclonedds) enters
56//! the link graph via the board crate (embedded), the board-less app's own
57//! `nros-rmw-*` dep (native), or the `nros-c`/`nros-cpp` staticlib root (D3),
58//! and self-registers through the `RMW_INIT_ENTRIES` walker at `Executor::open`.
59//! The concrete session type is resolved automatically; advanced users can
60//! access it via `nros::internals::RmwSession`.
61//!
62//! ## Crate Features
63//!
64//! `nros` exposes only FUNCTIONAL features — `std`/`alloc`, the `rmw-cffi`
65//! vtable, `lending`, `bridge`/`config`, `param-services`,
66//! `lifecycle-services`, `safety-e2e`, `stream`, `ffi-sync`, and the ROS
67//! edition (`ros-humble`/`ros-iron`). There are NO `platform-*` or concrete
68//! `rmw-*` selector features (Phase 248 C7). Platform + RMW are selected by the
69//! board / staticlib root via dependencies, not `nros` features. The
70//! `zephyr_component_main!` entry macro is gated only on `rmw-cffi` (it's
71//! framework entry codegen, like `nros::main!`), not a platform feature.
72//!
73//! **ROS edition** (select one; RFC-0056 — the per-distro interop profile):
74//! - `ros-humble` - ROS 2 Humble (default; `TypeHashNotSupported`, XCDR1)
75//! - `ros-iron` - ROS 2 Iron (RIHS01 type hash)
76//! - `ros-jazzy` - ROS 2 Jazzy (RIHS01; XCDR2/appendable is phase-303)
77//!
78//! **Other**:
79//! - `std` (default) - Enable standard library support
80//! - `alloc` - Enable heap allocation without full std
81//!
82//! ## Further Reading
83//!
84//! - [`guide`] — tutorials: getting started, services, configuration,
85//! ROS 2 interop, and troubleshooting
86//! - [Message Generation](https://github.com/jerry73204/nano-ros/blob/main/docs/guides/message-generation.md)
87//! — codegen reference (all options, output structure, bundled interfaces)
88//! - [Environment Variables](https://github.com/jerry73204/nano-ros/blob/main/docs/reference/environment-variables.md)
89//! — complete buffer tuning reference
90//! - [ROS 2 Interop](https://github.com/jerry73204/nano-ros/blob/main/docs/reference/rmw_zenoh_interop.md)
91//! — protocol details (key expressions, liveliness, attachments)
92//! - [Examples](https://github.com/jerry73204/nano-ros/tree/main/examples)
93//! — working examples by platform (native, QEMU, ESP32, Zephyr)
94
95#![no_std]
96
97// ── Feature validation (mutual exclusivity) ─────────────────────────────
98// Phase 248 C5c/C7 — `nros` carries NO `platform-*` selector features, so the
99// platform mutual-exclusion `compile_error!` is gone. The platform is selected
100// by the board / staticlib root via an `nros-platform` dep, and nros-node picks
101// the kernel primitive at runtime (C2 wake-probe).
102// Only `rmw-cffi` is exposed at this layer; the cffi shim selects the
103// concrete backend at the C ABI level via the `RMW_INIT_ENTRIES` walker.
104
105// At most one ROS edition (RFC-0056 — the axis is compile-time exclusive).
106#[cfg(any(
107 all(
108 feature = "ros-humble",
109 any(feature = "ros-iron", feature = "ros-jazzy")
110 ),
111 all(feature = "ros-iron", feature = "ros-jazzy"),
112))]
113compile_error!("`ros-{humble,iron,jazzy}` are mutually exclusive — select one ROS edition.");
114
115#[cfg(feature = "std")]
116extern crate std;
117
118#[cfg(feature = "alloc")]
119extern crate alloc;
120
121// Phase 216.A.5 — the `nros::node!()` proc-macro emits absolute paths
122// under `::nros::*` (so downstream Node pkgs only need a single `nros`
123// dep). For the in-crate macro-expansion test in `node.rs`, alias the
124// `nros` crate name to itself so those absolute paths resolve. Gated on
125// `cfg(test)` to keep the alias out of normal builds.
126#[cfg(test)]
127extern crate self as nros;
128
129// Phase 248 C5c — the umbrella's force-link statics
130// (`__FORCE_LINK_{PLATFORM_CFFI,ZENOH,XRCE,CYCLONEDDS_SYS}`) are REMOVED along
131// with `nros`'s concrete-backend deps. `nros` no longer references any concrete
132// RMW or platform crate, so it has nothing to force-link. Registration + the
133// `nros_platform_*` link anchor now live with whoever owns the concrete crate:
134// * embedded — the BOARD crate force-links its backend + calls
135// `<backend>::register()` in its boot path (C5a);
136// * board-less native — the APP owns `nros-rmw-*` + a `#[used]` force-link in
137// its `main.rs`, and `nros-platform-cffi[posix-c-port]` anchors the C symbols;
138// * C/C++ staticlib — `nros-c`/`nros-cpp` bundle one backend (D3) and anchor
139// `nros-platform` themselves.
140
141// Phase 249 P1 — `__register_linked_rmw()` (a Phase 248 C5c no-op kept only so the
142// `nros::main!` framework's call sites compiled) is REMOVED along with those call
143// sites. Backend registration never routed through the backend-agnostic `nros` crate:
144// hosted auto-registers via the `RMW_INIT_ENTRIES` walk at `Executor::open`; embedded
145// boards perform the explicit `<backend>::register()` in their boot path (C5a). One
146// Rust trigger = the board/app explicit register (phase-249).
147
148// phase-391 W5 — build-time knobs (`MAX_COMPONENTS`, `COMPONENT_SLOT_BYTES`).
149// Ungated: they are plain consts, useful to size caller-supplied storage
150// whether or not the runtime module that consumes them is compiled in.
151pub mod config;
152
153// phase-391 W5 — caller-supplied component-pool storage sizing. Ungated for the
154// same reason `config` is: plain arithmetic, useful to size a `static` whether
155// or not `node_runtime` is compiled in.
156// Issue 1419 / issue 1556 item 1 — the census hooks every language's node API
157// calls (unconditionally; the bodies exist only under `metadata-mode`).
158#[doc(hidden)]
159pub mod census_hooks;
160pub mod dispatch_tag;
161pub mod guide;
162#[cfg(feature = "metadata-mode")]
163pub mod metadata_mode;
164/// phase-463 W7 — the host profile (`nros_node::executor::profile`), for the
165/// hosted edges that read `$NROS_PROFILE_OUT` (`nros-cpp`, `nros-board-linux`).
166#[cfg(all(feature = "profile-mode", feature = "rmw-cffi"))]
167pub use nros_node::executor::profile;
168pub mod node;
169pub mod node_metadata;
170/// Phase 212.M.5.a.2 — executor-backed component runtime.
171///
172/// Binds [`Node`] / [`ExecutableNode`] to a live
173/// [`Executor`] so a Node pkg can actually run (versus
174/// [`MetadataRecorder`] which
175/// is the planner-side metadata sink).
176///
177/// Gated on `rmw-cffi`; the underlying [`Executor`] is only present
178/// when an RMW backend is linked. W5-endgame (issue 0843): the MACRO install
179/// path (per-class static storage, placed cells, slabbed ctxs) is alloc-free,
180/// so the module no longer demands `alloc` — only the dynamic
181/// `ExecutorNodeRuntime` half does, item-gated inside.
182#[cfg(feature = "rmw-cffi")]
183pub mod node_runtime;
184pub mod runtime_storage;
185
186/// Issue 1535 — what a `void*` executor handle crossing the language seam is,
187/// and the one check every seam in this crate makes before treating it as an
188/// `Executor` (a C/C++ entry's handle is an nros-cpp context, not one).
189pub mod executor_handle;
190
191/// Phase 212.L.5 — top-level init API; phase-427 W9 — on every target.
192///
193/// Re-exported flat at the crate root: `nros::Context` (`alloc`),
194/// `nros::ContextSource`, `nros::InitError`, `nros::InitOptions` (every
195/// target); `nros::init()`, `nros::init_with_args(args)` (`env`).
196/// The `env` feature gates only the process-environment reader and the
197/// argv entry; freestanding, `Context::default_from_env()` is the
198/// bake (`Context::baked()`).
199pub mod init;
200
201/// issue 0687 — the hosted edge of configuration: every env var nano-ros
202/// honours is read here, and the core takes values.
203#[cfg(feature = "env")]
204pub mod env;
205
206/// Issue 1763 — the same env rung for a `#![no_std]` image inside a host
207/// process (threadx-linux, freertos-posix), read with the C library's `getenv`.
208/// `target_os = "linux"` is "a C library is linked" on this tree. Compiled on
209/// every Linux build, not only `not(feature = "env")` ones: feature
210/// unification can turn `nros/env` on under a caller whose own `env` is off,
211/// and the caller must still find this. Where a caller has `env`, it uses the
212/// `std::env` reader instead (`nros-c`/`nros-cpp` `resolve_boot`).
213#[cfg(target_os = "linux")]
214pub mod host_env;
215
216/// phase-484 W8c — the retired env aliases both hosted readers refuse.
217pub mod retired_env;
218pub use retired_env::RETIRED_ENV_VARS;
219
220#[cfg(feature = "env")]
221pub use env::{ExecutorConfigEnvExt, rmw_selector};
222
223pub use init::{ContextSource, InitError, InitOptions};
224
225#[cfg(feature = "alloc")]
226pub use init::Context;
227
228#[cfg(feature = "env")]
229pub use init::{init, init_with_args};
230
231/// Compile-time opaque storage sizes for FFI consumers.
232///
233/// See [`sizes`] for the `export_size!` pattern used to expose these values
234/// to `nros-c` / `nros-cpp` at build time.
235pub mod sizes;
236
237/// Monotonic time for portable node code (issue #504).
238///
239/// phase-359 W10 — `rmw-cffi`, not `any(std, rmw-cffi)`: the clock is the
240/// platform port's, and a build with no port has none to offer.
241#[cfg(feature = "rmw-cffi")]
242pub mod time;
243
244/// CDR encapsulation constants and helpers for FFI layers that handle raw
245/// CDR bytes (e.g. nros-c, nros-cpp action and service paths).
246pub mod cdr {
247 pub use nros_serdes::{
248 CDR_BE_HEADER, CDR_HEADER_LEN, CDR_LE_HEADER, strip_cdr_header, write_cdr_le_header,
249 };
250}
251
252// Re-export core types
253pub use nros_core::{
254 CdrReader, CdrWriter, Clock, ClockType, DeserError, Deserialize, Duration, MessageInfo,
255 PUBLISHER_GID_SIZE, RawMessageInfo, RosMessage, RosService, SecNanosecMsg, SerError, Serialize,
256 Time,
257};
258
259// -----------------------------------------------------------------------------
260// Logging (phase-417 W4.d, issue 0589).
261//
262// `nros_log` was in this crate's dependency graph and reachable from NONE of
263// its surface: no `pub use`, nothing in the prelude. So from the façade the
264// shortest path to "print something" was `std::println!` — which is FATAL on
265// Zephyr `native_sim`: `zvfs_write(1, …)` dispatches to
266// `stdinout_write_vmeth`, which calls `zvfs_write(1, …)` again, and because
267// `k_mutex` is recursive it exhausts the stack instead of deadlocking. The
268// image dies with no message. A logging facade nobody can reach from the
269// umbrella is not a facade.
270//
271// `nros::Logger` IS RE-BOUND, and that is the point rather than a side effect.
272// It used to name `nros_core::logger::Logger`, which formats through the `log`
273// crate: on a target with no `log` backend installed — every embedded one —
274// the record is constructed and dropped, and it NEVER reaches
275// `nros_platform_log_write`. The type the C and C++ FFI hands out, the type
276// the `nros_*!` macros dispatch through, and the type the platform sink
277// receives is `nros_log::Logger`. One name, one meaning.
278//
279// Migration: the two types share `new` and `name` and NOTHING else — the old
280// one has inherent `.info(&str)` / `.warn(&str)` / `*_once` / `*_throttle`
281// methods, the new one is driven by the `nros_*!` macros. So every call site
282// that used the old surface FAILS TO COMPILE against the new binding rather
283// than silently changing where its records go. That is the RFC-0089 rule
284// applied to our own rename, and it is why this is a re-binding with a named
285// escape hatch instead of a quiet swap. The escape hatch was `LogCrateLogger`,
286// deprecated on arrival and deleted by phase-482 W6.
287pub use nros_log::{
288 DEFAULT_LOGGER, LogSink, Logger, MAX_LOGGERS, Record as LogRecord, Severity, ThrottleState,
289 get_logger, get_or_create_logger, register_logger,
290};
291// The emission macros, at the crate root, because a user who has
292// `use nros::prelude::*` should not have to learn a second crate name to log a
293// line. `#[macro_export]` puts them at `nros_log`'s root; this puts them at
294// ours.
295//
296// NAMES (phase-417, RFC-0089 "each language follows ITS OWN upstream",
297// settled 2026-09-04). The three ROS 2 client libraries disagree about how to
298// spell a log line — `rclrs::log_info!`, `RCLCPP_INFO`,
299// `RCUTILS_LOG_INFO_NAMED` — so there is no one "ROS 2 spelling" to match.
300// Rust follows rclrs, because a node ported from rclrs is read beside rclrs.
301// Hence the five severity macros below carry rclrs's names.
302//
303// Two families keep OUR prefix, and that is a decision rather than an
304// oversight: a name takes rclrs's spelling exactly when rclrs HAS that name.
305//
306// * `nros_trace!` — rclrs stops at `debug`. TRACE has no upstream twin, so
307// giving it one would claim a correspondence that does not exist.
308// * `nros_*_throttle!` / `nros_*_throttle_at!` — rclrs throttles with a
309// MODIFIER on one macro (`log_info!(logger.throttle(d), "…")` via
310// `LogParams` / `ToLogParams`), not a macro per severity. Adopting that is
311// a shape change, not a rename; see `nros-log/src/macros.rs`'s throttle
312// note for the three ledger rows it would have to reverse.
313pub use nros_log::{
314 log_debug, log_error, log_fatal, log_info, log_warn, nros_debug_throttle,
315 nros_debug_throttle_at, nros_error_throttle, nros_error_throttle_at, nros_fatal_throttle,
316 nros_fatal_throttle_at, nros_info_throttle, nros_info_throttle_at, nros_trace,
317 nros_trace_throttle, nros_trace_throttle_at, nros_warn_throttle, nros_warn_throttle_at,
318};
319/// The whole logging facade — sinks, the early-record ring, the throttle
320/// primitives, `init`/`flush`/`add_sink`.
321///
322/// The root re-exports above are the surface a node author needs; this is
323/// everything, for a board or a bridge composing its own delivery.
324pub mod logging {
325 pub use nros_log::*;
326}
327
328// Re-export heapless for generated message types and examples
329pub use nros_core::heapless;
330
331// Phase 212.M.5.a.2 — executor-backed runtime entry points.
332// (`component_register_symbol` retired in the Phase 212.N.7 closing
333// sweep — the helper had no live callers after the BSP baker + macro
334// extern emit were deleted.)
335pub use node::{
336 ActionExecutor, Callback, CallbackCtx, ClientDispatch, EntityBounds, NodeActionClient,
337 NodeActionServer, NodeDeclError, NodeOptions, NodeParameter, NodePublisher, NodeResult,
338 NodeRuntime, NodeServiceClient, NodeServiceServer, NodeSubscription, NodeTimer,
339 PublisherResolver, TickCtx,
340};
341// phase-483 W3 — the component API registers into a live executor, so it
342// exists where the executor does.
343#[cfg(feature = "rmw-cffi")]
344pub use node::{CallbackEffects, Component, DeclarativeNode, ExecutableNode, NodeContext};
345// Issue 0784 — the macro and codegen PLUMBING, out of the documented surface.
346// `nros::` publishes three audiences under one namespace; these six are for the
347// `nros::node!` expansion and the generated metadata probe
348// (`nros-cli-core/src/orchestration/metadata_build.rs`), never for a node
349// author, and none is named by any hand-written file in `examples/` or
350// `packages/`. Hidden, not deleted: the generated code reaches them by path, the
351// same treatment as `__private_node_state_into_raw` below.
352#[cfg(feature = "rmw-cffi")]
353#[doc(hidden)]
354pub use node::{__record_with_context, record_node_metadata, register_node};
355// Phase 212.M.5.a.4 — internal helper consumed by `nros::node!()`
356// for the BSP dispatch path. Public-but-doc-hidden so the macro expand
357// resolves it as `::nros::__private_node_state_into_raw`.
358#[cfg(all(feature = "alloc", feature = "rmw-cffi"))]
359#[doc(hidden)]
360pub use node::__private_node_state_into_raw;
361// phase-359 W8 — follows `node_metadata`'s re-gate: the type needs `alloc`,
362// not `std`.
363#[cfg(feature = "alloc")]
364pub use node_metadata::SourceMetadataExport;
365/// phase-381 follow-up — the node-FQN join, at the crate root because that is
366/// where a user looks after `Executor::get_node_names` hands them the two
367/// halves. `nros_node::names` also has `expand_name` / `resolve_name`, which
368/// stay behind `node_metadata` — they are the entity-name seam, not this.
369pub use node_metadata::fully_qualified_name;
370pub use node_metadata::{
371 CallbackEffectKind, CallbackEffectMetadata, EntityKind, EntityMetadata, MetadataRecorder,
372 MetadataString, NodeMetadata, NodeMetadataError, ParameterDefault, SourceLocationMetadata,
373 SourceNameKind,
374};
375#[doc(hidden)]
376pub use node_metadata::{CallbackId, EntityId, NodeId};
377// Phase 216.A.4 — opaque tag types Node authors hold on `Self::State`
378// and match against the `Callback<'_>` delivered to
379// `ExecutableNode::on_callback`.
380pub use dispatch_tag::{ActionTag, ServiceTag, SubscriptionTag};
381// W5-endgame (issue 0843) — the alloc-free half of the seam: per-class static
382// storage + the `_in` installs the macro emits. Available on `rmw-cffi` alone.
383#[cfg(feature = "rmw-cffi")]
384pub use node_runtime::{
385 ComponentSlotStorage,
386 // Phase 257 (W0-B) — the uniform cross-language component-install seam backing
387 // `__nros_component_<pkg>_install` (nros::node!): register an ExecutableNode on the
388 // shared executor a foreign typed entry hands in. (`register_node_borrowed` stays
389 // crate-internal — it returns the private `ComponentCell`.)
390 install_node_typed_in,
391 // Phase 305 W3 (issue 0255) — same seam plus launch `<remap>` rules; the variant
392 // `nros::node!()` emits.
393 install_node_typed_with_launch_in,
394};
395// The dynamic runtime + the leak-per-call conveniences still need `alloc`.
396#[cfg(all(feature = "rmw-cffi", feature = "alloc"))]
397pub use node_runtime::{
398 ExecutorError,
399 ExecutorNodeRuntime,
400 RegisteredNode,
401 install_node_typed,
402 install_node_typed_with_launch,
403 // Phase 268 W1 — same seam with both `<param>` initials AND `<node name= namespace=>`
404 // identity injection (RFC-0046).
405 install_node_typed_with_node_identity,
406 // W4a — same seam, seeding the node's NodeContext with launch-baked `<param>` initials.
407 install_node_typed_with_params,
408};
409
410// phase-483 W3 — the `install_node_typed*` STUBS for builds without
411// `rmw-cffi` are gone. They existed so a `nros::node!()` package compiled
412// without the executor stayed linkable; a component now registers into a live
413// executor (`NodeContext::create_node` returns a `Node`), so a component
414// package without `rmw-cffi` has nothing to register into and does not build.
415// Phase 212.N.12 — canonical `nros::node!()` macro. Replaces the legacy
416// `nros::node!()` macro (retired in the N.12 hard rename — both the
417// proc-macro forwarder and the Cargo metadata key are gone).
418#[cfg(feature = "macros")]
419pub use nros_macros::node;
420// Phase 212.N.9 — `nros::main!()` proc-macro family. One-line Entry-pkg
421// `main.rs` (replaces the legacy `build.rs + include!()` shape). See
422// `docs/design/0024-multi-node-workspace-layout.md` §11.6.
423#[cfg(feature = "macros")]
424pub use nros_macros::main;
425
426/// Route this image's panics to its platform — phase-366 W5.c / RFC-0077.
427///
428/// Emits the `#[panic_handler]` for an embedded image, forwarding the message
429/// to `nros_platform_panic` so a Rust panic ends the same way a C precondition
430/// failure or a C++ terminate does. What that ending IS belongs to the port:
431/// `k_panic()` on Zephyr, `esp_system_abort()` on ESP-IDF, UART-then-exit-QEMU
432/// on the ThreadX RV64 board.
433///
434/// # Why this is a macro you INVOKE, not something `nros::main!` emits
435///
436/// `#[panic_handler]` is a singleton of the final artifact, and the image owns
437/// it. Emitting one silently from `nros::main!()` would collide with every image
438/// that already declares its own — `examples/esp32-c3-baremetal` writes
439/// `use esp_backtrace as _;`, and `logging-smoke-freertos-mps2` uses
440/// `panic-semihosting` with `features = ["exit"]` so a panic exits QEMU instead
441/// of hanging the test harness. Those images are RIGHT, and an invisible default
442/// would fight them.
443///
444/// So the line is written in the entry, where it can be read, swapped for
445/// `use panic_halt as _;`, or replaced by a hand-written handler that logs to
446/// NVM and reboots. A default you cannot see is a constraint, not a default.
447///
448/// # Use — `main!(panic = …)` is the normal way; this is the escape hatch
449///
450/// phase-366 R3. An entry that goes through `nros::main!()` should say
451/// `panic = "platform"` (or nothing — that is the default since M5) and let the
452/// macro emit this body. One line, and the build can check it.
453///
454/// ```ignore
455/// #![no_std]
456/// nros::main!(); // ends through `nros_platform_panic`
457/// ```
458///
459/// Invoke this macro directly only where no `main!()` expansion can carry the
460/// item — a hand-rolled `no_std` binary, or the lib side of a crate whose
461/// `crate-type` includes `staticlib` and whose entry macro is
462/// `zephyr_component_main!` / a board `app_main!`. It is kept for exactly those:
463/// deleting it would strand the images that cannot use the macro replacing it.
464///
465/// ```ignore
466/// // src/app_main.rs — the .a is a final artifact to rustc, and `main!()` in
467/// // the bin target never reaches it.
468/// nros::panic_to_platform!();
469/// ```
470///
471/// Do NOT invoke it in a `std` image: libstd supplies the lang item there and a
472/// second one does not compile. Do not invoke it alongside `use panic_halt as _`
473/// or any other provider, for the same reason — that is the duplicate
474/// `check-archive-lang-items` exists to catch.
475/// Park the core on panic — the `halt` value of `nros::main!(panic = …)`.
476///
477/// For an image that must not print: no formatting, no allocation, no call out
478/// to the platform. Interrupts are masked first so the parked core cannot be
479/// woken back into a half-dead system by a timer or a driver ISR still armed
480/// from before the panic.
481///
482/// This is a body rather than a re-export of the `panic-halt` crate so that
483/// choosing it costs the entry no new dependency — `main!(panic = "halt")` is a
484/// word in a macro the crate already calls, which is the point of the surface.
485/// The behaviour is the same: mask, then spin forever.
486///
487/// Prefer `panic = "platform"`. Halting discards the diagnosis, and every port
488/// implements `nros_platform_panic` precisely so a dying image can say why.
489/// Phase 392 W3b — the receive-buffer size for a message type, as a constant
490/// that cannot drift from the type.
491///
492/// ```ignore
493/// node.subscription::<PointCloud2>("points")
494/// .rx_buffer::<{ nros::rx_buffer_for!(PointCloud2) }>()
495/// .build(on_cloud)?;
496/// ```
497///
498/// `.rx_buffer::<N>()` has always accepted a number. The problem is where the
499/// number comes from: a literal is correct until someone appends a field to the
500/// message, and then it is silently too small — the sample is received, ACKed
501/// and dropped at the transport, which needs a packet capture to attribute
502/// (`report_dropped_take`, and the 13.4 KiB Autoware trajectory case). This
503/// expands to the type's own bound, computed from its schema by phase 380, so
504/// appending a field moves the buffer with it.
505///
506/// **phase-392 W3c narrowed what this is FOR.** A plain
507/// `.typed::<M>().build()` now derives its arena receive buffer from `M` on its
508/// own, and `.rx_buffer::<N>()` is the opt-out, so the macro is no longer how a
509/// buffered subscription gets a type-sized buffer. What still needs it is the
510/// entry shapes whose buffer is a real inline `[u8; RX]` and therefore a
511/// const-generic argument: `.message_info()` and `.safety()`. Those are the
512/// call sites this expands for.
513///
514/// **Why a macro and not a method.** The builder cannot do this for you:
515/// inside `impl<M> TypedSubscriptionBuilder<M>` the type is a generic
516/// parameter, and on stable Rust a generic parameter may not appear in a const
517/// operation — `error: generic parameters may not be used in const operations`.
518/// At a call site the type is CONCRETE, which is legal, so the size has to be
519/// named where the type is. That constraint is also why phase-392's size
520/// classes were "decoupled from codegen" in the first place.
521///
522/// **Unbounded types are a BUILD ERROR** (phase-403 W0). A type with an
523/// unbounded `string`/`wstring`/`sequence` has no bound, and this refuses to
524/// invent one. It used to expand to `DEFAULT_RX_BUF_SIZE`; it now fails to
525/// compile, naming both remedies.
526///
527/// Every message type is REQUIRED to carry a derived upper bound, stated in the
528/// `.msg` (`string<=64`) or capped in `nros-codegen.toml`. Phase 380 is explicit
529/// that `None` means "no bound EXISTS", never "unknown", and that a buffer must
530/// not be sized from a fallback. Substituting the configured default was the
531/// violation of that rule; refusing is what it licenses. `report_dropped_take`
532/// is a backstop for a buffer that is too small, not a licence to pick one.
533///
534/// ```compile_fail
535/// use nros_serdes::schema::{Field, FieldType, Message};
536/// struct Unbounded;
537/// impl Message for Unbounded {
538/// const TYPE_NAME: &'static str = "test/msg/Unbounded";
539/// const FIELDS: &'static [Field] = &[Field {
540/// name: "s",
541/// ty: FieldType::String,
542/// offset: 0,
543/// }];
544/// }
545/// let _n: usize = nros::rx_buffer_for!(Unbounded);
546/// ```
547///
548/// The positive control for that `compile_fail`, so it cannot pass because the
549/// fixture stopped compiling for an unrelated reason:
550///
551/// ```
552/// use nros_serdes::schema::{Field, FieldType, Message};
553/// struct Bounded;
554/// impl Message for Bounded {
555/// const TYPE_NAME: &'static str = "test/msg/Bounded";
556/// const FIELDS: &'static [Field] = &[Field {
557/// name: "a",
558/// ty: FieldType::Uint64,
559/// offset: 0,
560/// }];
561/// }
562/// let n: usize = nros::rx_buffer_for!(Bounded);
563/// assert!(n > 0);
564/// ```
565///
566/// **Why the `const` block.** The macro is used in two positions: as a
567/// const-generic argument (`.rx_buffer::<{ rx_buffer_for!(M) }>()`) and as a
568/// plain expression (`let n = rx_buffer_for!(M);`). A bare `panic!` is a build
569/// error in the first and a RUNTIME panic in the second, which would make the
570/// rule depend on where the macro appears. Wrapping the whole match in an inline
571/// `const` block forces compile-time evaluation in BOTH, so an unbounded type
572/// can never reach a running image.
573///
574/// **What the error names.** rustc points at this macro invocation, where the
575/// type is written literally, so the TYPE is named. The MEMBER that costs the
576/// bound cannot be in the message: const evaluation does not format, so a
577/// `panic!` there takes a literal only. The member is named by the codegen
578/// diagnostic for the same type -- `unbounded_reason` in the generated C header
579/// (`packs/c/message.h.jinja`), which names EVERY member that costs the bound in
580/// one build (phase-403 W0), or `nros_serdes::size::visit_unbounded` over its
581/// `FIELDS`.
582#[macro_export]
583macro_rules! rx_buffer_for {
584 ($msg:ty) => {
585 // The `const` block is load-bearing; see "Why the `const` block" above.
586 const {
587 match $crate::__rx_bound::<$msg>() {
588 ::core::option::Option::Some(n) => n,
589 ::core::option::Option::None => ::core::panic!(
590 "nros: this message type has NO serialized-size bound, so no \
591 receive buffer can be sized from it.\n\
592 Every message type must carry a derived upper bound. Bound the \
593 member that costs it, either:\n\
594 \x20 - in the `.msg`: `string<=64`, `wstring<=64`, \
595 `sequence<T, N>`, `T[<=N]`; or\n\
596 \x20 - as an INLINE `cap` in `nros-codegen.toml`: under \
597 `[fields]`, `\"pkg/Msg.field\" = 64`. A `heap` or `view` cap \
598 is a sizing hint that nothing enforces (RFC-0033), so it \
599 deliberately does NOT bound.\n\
600 WHICH members cost the bound -- all of them, not just the \
601 first -- are named by the codegen diagnostic for this same \
602 type: `unbounded_reason` in the generated C header, or \
603 `nros_serdes::size::visit_unbounded` over its `FIELDS`. The type itself is named by the \
604 `rx_buffer_for!` invocation rustc points at.\n\
605 Phase 380: `None` means no bound EXISTS, never \"unknown\", and \
606 a buffer sized from a fallback is the failure that rule was \
607 written to prevent. Erroring honours it; defaulting to \
608 `DEFAULT_RX_BUF_SIZE` did not."
609 ),
610 }
611 }
612 };
613}
614
615#[macro_export]
616macro_rules! panic_halt {
617 () => {
618 #[panic_handler]
619 fn __nros_panic_halt(_info: &::core::panic::PanicInfo) -> ! {
620 // Mask interrupts through the platform's critical section, which is
621 // the one IRQ primitive that is portable across the ports (the
622 // `cortex_m`/`riscv` intrinsics are not). Entering and never
623 // leaving is deliberate.
624 unsafe extern "C" {
625 fn nros_platform_critical_section_acquire() -> u32;
626 }
627 // SAFETY: the ABI's acquire takes no argument and returns a restore
628 // token we deliberately drop — nothing after this point runs.
629 unsafe {
630 let _ = nros_platform_critical_section_acquire();
631 }
632 loop {
633 ::core::hint::spin_loop();
634 }
635 }
636 };
637}
638
639#[macro_export]
640macro_rules! panic_to_platform {
641 () => {
642 #[panic_handler]
643 fn __nros_panic(info: &::core::panic::PanicInfo) -> ! {
644 use ::core::fmt::Write as _;
645
646 // Fixed buffer, never the heap: this runs when the allocator may be
647 // exactly what failed. Truncation is deliberate — a short panic line
648 // still diagnoses; a missing one does not.
649 struct Buf {
650 bytes: [u8; 192],
651 used: usize,
652 }
653 impl ::core::fmt::Write for Buf {
654 fn write_str(&mut self, s: &str) -> ::core::fmt::Result {
655 let room = self.bytes.len() - self.used;
656 let n = s.len().min(room);
657 self.bytes[self.used..self.used + n].copy_from_slice(&s.as_bytes()[..n]);
658 self.used += n;
659 Ok(())
660 }
661 }
662
663 let mut buf = Buf {
664 bytes: [0u8; 192],
665 used: 0,
666 };
667 let _ = write!(buf, "{info}");
668
669 unsafe extern "C" {
670 fn nros_platform_panic(msg: *const u8, len: usize) -> !;
671 }
672 // SAFETY: `buf.bytes[..used]` is initialised and outlives the
673 // diverging call; the ABI takes a length-delimited diagnostic, not
674 // a C string.
675 unsafe { nros_platform_panic(buf.bytes.as_ptr(), buf.used) }
676 }
677 };
678}
679
680/// Define Zephyr's `rust_main` for a self-bringup Rust component package.
681///
682/// The macro is intended for `rust_cargo_application()` apps whose crate
683/// already invokes `nros::node!()`. It opens a Zephyr executor, registers
684/// the supplied component through [`ExecutorNodeRuntime`], and spins forever.
685/// Issue 0330 — force-link an RMW backend crate into a pure-Rust staticlib.
686///
687/// On a Rust-only image (Zephyr and friends) the Zephyr module emits a weak
688/// `nros_rmw_<name>_register` and calls it only if it resolves. The strong
689/// definition is the backend crate's `#[no_mangle]` export — and rustc's
690/// staticlib DCE drops it unless something in the crate being compiled into the
691/// staticlib references that crate. The symbol is then present in the rlib and
692/// absent from the `.a`, the weak call sees NULL, and the image comes up with no
693/// backend registered (issues 0155 / 0163).
694///
695/// This emits the reference, without naming any backend in nano-ros' own
696/// RMW-agnostic layers — the app crate names it, because the app crate is what
697/// selects an RMW:
698///
699/// ```ignore
700/// #[cfg(feature = "rmw-zenoh")]
701/// nros::force_link_backend!(nros_rmw_zenoh);
702/// #[cfg(feature = "rmw-xrce")]
703/// nros::force_link_backend!(nros_rmw_xrce_cffi);
704/// ```
705///
706/// It is an ANCHOR, not a registration call — the static is never executed
707/// (same class as `nros-c`'s `FORCE_LINK` and `nros-rmw-cffi`'s section anchor).
708/// Registration happens through `nros_app_register_backends`. Backends whose
709/// register entry lives in a C/C++ library the image already links (cyclonedds
710/// on Zephyr) need no anchor at all.
711///
712/// Invoke at module scope. Multiple invocations in one crate are fine — each
713/// expands inside its own anonymous const, so the static names cannot collide.
714#[macro_export]
715macro_rules! force_link_backend {
716 // `ident`, not `path`: a `path` fragment may not be followed by `::`, so
717 // `$backend::register()` fails to parse at the CALL site with a misleading
718 // "expected an operator". Backend crate names are single idents anyway.
719 ($backend:ident) => {
720 const _: () = {
721 #[used]
722 static __NROS_FORCE_LINK_BACKEND: fn() = || {
723 let _ = $backend::register();
724 };
725 };
726 };
727}
728
729// Phase 248 C7 (Method A) — gated on `rmw-cffi` only (needs `Executor`), NOT a
730// `platform-*` feature. This is a framework ENTRY macro (same category as
731// `nros::main!`'s zephyr `rust_main` codegen) — `#[macro_export]` so it emits
732// nothing unless a Zephyr example invokes it; the body's `::zephyr::*` /
733// `::nros_platform::zephyr::wait_network` resolve only in that zephyr-build
734// context (the example deps the `zephyr` crate + `nros-platform[platform-zephyr]`).
735#[cfg(feature = "rmw-cffi")]
736#[macro_export]
737macro_rules! zephyr_component_main {
738 ($node:ty) => {
739 #[unsafe(no_mangle)]
740 pub extern "C" fn rust_main() {
741 // issue 1123 — publish an `nros_log` sink list. The `log` FACADE
742 // (what an example body writes) is bridged into it just below
743 // (issue 1324 retired `zephyr::set_logger`); the sink list is what
744 // `nros_log` needs, which is what the
745 // FRAMEWORK writes (nros-node's executor, nros-rmw-zenoh's session
746 // and pool diagnostics, nros-rmw-cffi). With no sink list
747 // `nros_log::dispatch_to_sinks` holds each record in the bounded
748 // `nros_log::early` ring — issue 0710 deliberately removed the
749 // implicit platform fallback — so on a pure-Rust Zephyr image every
750 // framework record was constructed, dispatched and never seen.
751 //
752 // This is the funnel: a pure-Rust Zephyr image has no
753 // `libnros_c.a` (issue 0163), so `rclc_log_init` / `rclc_log_emit`'s
754 // lazy `ensure_default_sinks()` never run, and `nros-board-zephyr`
755 // covers only `run_tiers`. Reached through `nros_platform` because
756 // the example leaves dep that and not `nros-platform-cffi`.
757 //
758 // FIRST, before `wait_network`: `init` DRAINS the early ring, and
759 // anything raised during bringup is exactly what the ring exists to
760 // serve (the esp32 half of this class, issue 1048, got this order
761 // backwards and drained into a writer that was still `None`). The
762 // Zephyr writer is `nros_platform_log_write`, defined
763 // unconditionally by the C port, so there is no such window here.
764 ::nros_platform::log::init_default();
765 // issue 1324 — the `log` facade into the same sinks. This was
766 // `zephyr::set_logger()`, whose full-`CONFIG_LOG` arm exists only
767 // under `CONFIG_RUST_ALLOC`, the allocator this image no longer links.
768 ::nros_platform::log::install_log_crate_bridge();
769 // Phase 248 C7 step 1 — relocated helper (was `$crate::platform::zephyr`).
770 let _ = ::nros_platform::zephyr::wait_network(2000);
771 // Phase 249 P1 — RMW register is board/platform-owned (Phase 248 C5a);
772 // the backend-agnostic `nros` crate cannot register (no backend dep).
773 // Issue 0155 — the "board/platform boot path" that was supposed to
774 // register never fired for pure-Rust Zephyr images: the zephyr
775 // module emits a STRONG `nros_app_register_backends` stub for the
776 // Kconfig-selected RMW (zephyr/CMakeLists.txt Phase 160.A), but
777 // only the C/C++ `nros_cpp_init` path ever CALLED it — a Rust-only
778 // image reached `Executor::open` with no backend registered and
779 // died with Transport(ConnectionFailed) (silently, pre-0155).
780 // Call the hook explicitly, exactly like the C++ init path.
781 unsafe extern "C" {
782 fn nros_app_register_backends();
783 }
784 unsafe { nros_app_register_backends() };
785 // Issue 0163 — a pure-Rust image has no `libnros_c.a`, so the
786 // backend must ride in THIS staticlib and be referenced from the
787 // app crate, or rustc's staticlib DCE drops the whole backend
788 // closure (the `#[no_mangle]` C export included), leaving the
789 // module's weak `nros_rmw_<x>_register` resolving to NULL and the
790 // hook above registering nothing.
791 //
792 // Issue 0330 — that reference used to be a pair of hardcoded
793 // `::nros_rmw_zenoh::register()` / `::nros_rmw_xrce_cffi::register()`
794 // calls emitted RIGHT HERE, which named two concrete backends in
795 // the RMW-agnostic facade (and left cyclonedds handled asymmetrically
796 // through the C hook). It also forced every consumer to carry
797 // `rmw-zenoh` / `rmw-xrce` feature rows purely so these `cfg`s would
798 // resolve — the cyclonedds-only example carried both as inert
799 // placeholders. The anchor now lives in the app crate, which is the
800 // layer that legitimately selects an RMW: see
801 // [`nros::force_link_backend!`]. Registration itself is unchanged —
802 // the `nros_app_register_backends` hook above does it.
803 // phase-427 W9 — the baked identity has ONE source. This block
804 // used to read `option_env!("NROS_LOCATOR")` / `NROS_DOMAIN_ID`
805 // itself and assemble an `ExecutorConfig` by hand (a copy
806 // `nros::main!`'s Zephyr arm carried too, minus the domain — issue
807 // 0161's class); `Context::baked()` is where that lives now, and
808 // `default_from_env()` resolves to it on a freestanding build.
809 // `nros`'s own `build.rs` re-exports the Kconfig values into that
810 // crate's build (`nros_zephyr_build::bake_nros_config`), the same
811 // channel the leaf's `build.rs` uses for itself.
812 //
813 // #166 / phase-286 W1 — native_sim test parallelism. The test
814 // harness launches the image with `-testargs --nros-locator=<loc>`
815 // and starts a per-test zenohd on that (ephemeral) port; preferring
816 // it over the build-time bake lets every test dial a DISTINCT router,
817 // retiring the shared-baked-port serialization of the zenoh e2e
818 // lanes. Provided by `nros-platform-zephyr` (argv-backed, process
819 // lifetime); returns NULL on real embedded → the bake stands. The
820 // hook is an `extern "C"` only a Zephyr image defines, so it is
821 // resolved HERE, in the entry, and handed to the context.
822 unsafe extern "C" {
823 fn nros_runtime_locator_override() -> *const ::core::ffi::c_char;
824 }
825 let runtime_locator: ::core::option::Option<&str> = {
826 let p = unsafe { nros_runtime_locator_override() };
827 if p.is_null() {
828 ::core::option::Option::None
829 } else {
830 unsafe { ::core::ffi::CStr::from_ptr(p) }.to_str().ok()
831 }
832 };
833 let ctx = match $crate::Context::default_from_env() {
834 ::core::result::Result::Ok(ctx) => ctx.with_locator_override(runtime_locator),
835 ::core::result::Result::Err(e) => {
836 panic!("nros zephyr entry: baked Context is invalid: {e:?}")
837 }
838 };
839 let config = ctx.config(<$node as $crate::Component>::NAME);
840 // Issue 0155 — fail LOUD (repo rule: panic, not silent
841 // early-return). A silent `return` here idles the image with zero
842 // output; the zephyr-cyclonedds rust lane was undiagnosable until
843 // this printed the real error.
844 let executor = match $crate::Executor::open(&config) {
845 Ok(executor) => executor,
846 Err(e) => {
847 panic!("nros zephyr entry: Executor::open failed: {e:?}");
848 }
849 };
850 let mut runtime = $crate::ExecutorNodeRuntime::from_executor(executor);
851 // phase-436 — Zephyr's microsecond park, which the tier runners
852 // install on every tier and this single-executor entry did not.
853 // Zephyr's `platform.c` always defines both symbols, and this macro
854 // only expands in a Zephyr image, so they are plain references.
855 {
856 unsafe extern "C" {
857 fn nros_platform_wake_park_until_us(
858 w: *mut ::core::ffi::c_void,
859 deadline_us: u64,
860 ) -> i8;
861 fn nros_platform_wake_park_granularity_us() -> u64;
862 }
863 // SAFETY: a pure query of the linked port, no arguments.
864 let granularity = unsafe { nros_platform_wake_park_granularity_us() };
865 let _ = $crate::port_park::install_port_park(
866 runtime.executor_mut(),
867 nros_platform_wake_park_until_us,
868 granularity,
869 );
870 }
871 if let Err(e) = runtime.register_node::<$node>() {
872 panic!("nros zephyr entry: register_node failed: {e:?}");
873 }
874 // Readiness marker. The C/C++ Zephyr listeners print
875 // "Waiting for messages..." from their `main()` before the spin
876 // loop; the e2e harness polls for that substring to know the
877 // subscriber has declared before starting the talker (Phase 89.12).
878 // The Rust path's spin loop lives in this macro (the node only owns
879 // callbacks), so emit the same canonical marker here — without it a
880 // fully-working Rust listener never signals readiness and the e2e
881 // times out at 30 s (issue #35: the zenoh native_sim rust pubsub /
882 // service / action failures were this missing marker, not a
883 // transport fault — `Executor::open` + `register_node` had already
884 // succeeded).
885 ::log::info!("Waiting for messages");
886 loop {
887 let _ = runtime.spin_once(::core::time::Duration::from_millis(10));
888 }
889 }
890 };
891}
892
893// phase-483 W4 (issue 0784) — `StandaloneNode` (the transport-less
894// pre-component node) and its `NodeConfig` / `PublisherHandle` /
895// `SubscriptionHandle` are no longer re-exported. `nros::Node` is THE node,
896// and these have no rclrs counterpart; the two bench programs that use them
897// import `nros_node` directly.
898
899// Re-export publisher/subscriber options (topic + QoS; always available).
900pub use nros_node::{PublisherOptions, SubscriptionOptions};
901
902// Re-export timer types
903pub use nros_node::{TimerCallbackFn, TimerDuration, TimerHandle, TimerMode, TimerState};
904// phase-425 W4/W5 — which clock advances a timer. Re-exported here for the
905// reason `sim-time` is a feature here: an application deps `nros`, not
906// `nros-node`, so a capability that stops at the core crate is one no
907// application can name. W4 added the type and the registrar and stopped at the
908// core, which the `/clock` fixture found the moment it tried to use them.
909// UNGATED since phase-430 W4, and the enum moved to `nros_node::timer` to make
910// that possible. The `rmw-cffi` gate was right while the only way to name a
911// clock was `Executor::register_timer_on_clock`; W4 put the axis on the
912// DECLARATIVE surface too (`DeclaredNode::create_timer_on_clock`), which a
913// metadata-mode build compiles with no transport linked. A type the metadata
914// layer cannot name is a type the metadata layer re-invents.
915pub use nros_node::timer::TimerClockSource;
916
917/// phase-467 Q4 / ledger row `c:logging_rosout_enabled` — the `/rosout` log
918/// bridge. A module re-export rather than a flat one: the names a caller
919/// needs (`enable`, `pump`, `qos`, `qos_bounded`, `TOPIC`) are generic words
920/// that only read correctly under the `rosout::` prefix.
921/// Gated like the module it comes from: `nros_node::rosout` is
922/// `#[cfg(all(feature = "rosout", any(has_rmw, test)))]` there, and `has_rmw`
923/// is `nros-node`'s own build-script cfg — this crate spells the same
924/// condition as `feature = "rmw-cffi"`, exactly as the `parameter_services`
925/// re-export below does.
926#[cfg(all(feature = "rosout", feature = "rmw-cffi"))]
927pub use nros_node::rosout;
928
929// Re-export transport types (middleware-agnostic)
930pub use nros_rmw::{
931 ClientTrait, Publisher, QoSDurabilityPolicy, QoSHistoryPolicy, QoSLivelinessPolicy,
932 QoSOverride, QoSOverrideRole, QoSOverrideValue, QoSPolicyMask, QoSProfile,
933 QoSReliabilityPolicy, Rmw, RmwConfig, ServiceInfo, ServiceRequest, ServiceTrait, Session,
934 SessionMode, Subscription as SubscriptionTrait, TopicInfo, Transport, TransportConfig,
935};
936
937/// Phase 108.B — standard ROS-2-equivalent QoS profiles. Match
938/// upstream `rmw_qos_profile_default` etc. field-by-field. Backends
939/// validate against these synchronously at create time; no silent
940/// downgrade.
941// phase-379 W5 — rclrs exports the eight QoS presets as CRATE-LEVEL consts
942// (`rclrs::QOS_PROFILE_DEFAULT`); ours were associated consts on `QoSProfile`
943// and nothing re-exported them, so `use rclrs::QOS_PROFILE_DEFAULT` had no
944// counterpart to port to. The NAMES already matched exactly — only the path
945// did not, which is why the ledger filed these as a re-export and not a
946// rename.
947//
948// These ALIAS the associated consts rather than restating them. `nros::qos`
949// below is a second, hand-written copy of the same presets that predates them;
950// `qos_presets_agree` in the test module asserts the two never drift, which is
951// the hand-mirror class (issues 0088/0160/0245) one layer up.
952/// `rmw_qos_profile_default` — reliable, volatile, keep-last(10).
953pub const QOS_PROFILE_DEFAULT: QoSProfile = QoSProfile::QOS_PROFILE_DEFAULT;
954/// `rmw_qos_profile_system_default` — the RMW implementation's own defaults.
955pub const QOS_PROFILE_SYSTEM_DEFAULT: QoSProfile = QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT;
956/// `rmw_qos_profile_sensor_data` — best-effort, keep-last(5).
957pub const QOS_PROFILE_SENSOR_DATA: QoSProfile = QoSProfile::QOS_PROFILE_SENSOR_DATA;
958/// `rmw_qos_profile_services_default`.
959pub const QOS_PROFILE_SERVICES_DEFAULT: QoSProfile = QoSProfile::QOS_PROFILE_SERVICES_DEFAULT;
960/// `rmw_qos_profile_parameters` — reliable, depth 1000.
961pub const QOS_PROFILE_PARAMETERS: QoSProfile = QoSProfile::QOS_PROFILE_PARAMETERS;
962/// `rmw_qos_profile_parameter_events`.
963pub const QOS_PROFILE_PARAMETER_EVENTS: QoSProfile = QoSProfile::QOS_PROFILE_PARAMETER_EVENTS;
964/// The clock preset — sensor-data shaped with depth 1.
965pub const QOS_PROFILE_CLOCK: QoSProfile = QoSProfile::QOS_PROFILE_CLOCK;
966/// The action-status preset — reliable + transient-local, depth 1.
967pub const QOS_PROFILE_ACTION_STATUS_DEFAULT: QoSProfile =
968 QoSProfile::QOS_PROFILE_ACTION_STATUS_DEFAULT;
969
970/// The five presets under their older, shorter names.
971///
972/// **Every one of these is an ALIAS.** phase-428 W10 — this module used to
973/// RESTATE four of the five as struct literals: `DEFAULT` spelled all ten fields
974/// out, `SENSOR_DATA` and `PARAMETERS` were `..DEFAULT` updates of it, and
975/// `SERVICES_DEFAULT` was `= DEFAULT` — a second, independent transcription of
976/// upstream's table living 40 lines below the first. Only `SYSTEM_DEFAULT` was a
977/// real alias, and the comment that made it one claimed it was joining "the
978/// other four", which was not true of any of them.
979///
980/// That is the mirror this work item removes: upstream's 7 profiles were
981/// hand-written in four places across the repo — here, the crate-level
982/// `QOS_PROFILE_*` consts above, `nros-rmw/src/traits.rs`, and
983/// `nros-rmw-abi/include/nros/rmw_entity.h` — about 26 constants for a 7-row
984/// table, with no gate binding them. `QOS_PROFILE_PARAMETER_EVENTS` is the proof
985/// that the arrangement does not hold: it is `KeepAll, 0` in `traits.rs` under a
986/// comment claiming it matched `rmw_qos_profile_parameter_events`, which is
987/// `KEEP_LAST, 1000`.
988///
989/// So this module now declares no values at all. The SSoT is
990/// [`QoSProfile`]'s associated consts, and a correction there — including the
991/// `PARAMETER_EVENTS` one — reaches these names without anyone remembering to
992/// come here. Note that the alias set is a SUBSET: `CLOCK`,
993/// `PARAMETER_EVENTS`, `ACTION_STATUS_DEFAULT` and `PX4` have no short spelling
994/// and are reached as `nros::QOS_PROFILE_*` or `QoSProfile::QOS_PROFILE_*`.
995pub mod qos {
996 use crate::QoSProfile;
997
998 /// `rmw_qos_profile_default` — reliable + volatile + keep-last(10),
999 /// automatic liveliness, no deadline / lifespan.
1000 pub const DEFAULT: QoSProfile = QoSProfile::QOS_PROFILE_DEFAULT;
1001
1002 /// `rmw_qos_profile_sensor_data` — best-effort + volatile + keep-last(5).
1003 pub const SENSOR_DATA: QoSProfile = QoSProfile::QOS_PROFILE_SENSOR_DATA;
1004
1005 /// `rmw_qos_profile_services_default` — reliable + volatile +
1006 /// keep-last(10).
1007 ///
1008 /// It aliases its OWN associated const, not `DEFAULT`. The two profiles
1009 /// hold the same values upstream and are two upstream constants; writing
1010 /// this one as `= DEFAULT`, which is what it said before W10, would make a
1011 /// future divergence between them invisible here.
1012 pub const SERVICES_DEFAULT: QoSProfile = QoSProfile::QOS_PROFILE_SERVICES_DEFAULT;
1013
1014 /// `rmw_qos_profile_parameters` — reliable + volatile + keep-last(**1000**).
1015 pub const PARAMETERS: QoSProfile = QoSProfile::QOS_PROFILE_PARAMETERS;
1016
1017 /// `rmw_qos_profile_system_default` — **an absence, not a profile**: every
1018 /// policy is the SYSTEM_DEFAULT sentinel and the depth is the depth
1019 /// sentinel, resolved by whichever backend is linked.
1020 ///
1021 /// issue 0829 — this said `= DEFAULT` (depth 10) while
1022 /// `QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT` said depth 1, so one name
1023 /// shipped two queue depths depending on which spelling a caller reached.
1024 /// It was the first of these to become an alias, and since W10 it is no
1025 /// longer the only one: see `QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT`.
1026 pub const SYSTEM_DEFAULT: QoSProfile = QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT;
1027}
1028
1029// Re-export safety types when feature is enabled
1030#[cfg(feature = "safety-e2e")]
1031pub use nros_rmw::{IntegrityStatus, SafetyValidator, crc32};
1032
1033// Phase 248 C7 step 1 — the `nros::platform::zephyr` module (the
1034// `wait_for_network` FFI wrapper) RELOCATED to `nros-platform`
1035// (`nros_platform::zephyr::wait_network`); callers reference it via
1036// `::nros_platform::zephyr::wait_network`. nros no longer hosts a platform
1037// helper module. (The `zephyr_component_main!` macro relocation is C7 step 2.)
1038//
1039/// Backend-specific internal types.
1040///
1041/// These types are implementation details of the transport backends.
1042/// Most users should use the high-level APIs (`Executor`, etc.)
1043/// instead of these types directly.
1044///
1045/// The `Rmw*` type aliases resolve to whichever backend is active at compile time,
1046/// providing a backend-agnostic way to reference concrete transport types.
1047pub mod internals {
1048 // ── Backend-agnostic type aliases ────────────────────────────────────
1049 // These resolve to the concrete types of the active RMW backend.
1050 // Today the only exposed backend at this layer is the cffi shim.
1051
1052 #[cfg(feature = "rmw-cffi")]
1053 pub type RmwSession = nros_rmw_cffi::CffiSession;
1054 #[cfg(feature = "rmw-cffi")]
1055 pub type RmwPublisher = nros_rmw_cffi::CffiPublisher;
1056 #[cfg(feature = "rmw-cffi")]
1057 pub type RmwSubscriber = nros_rmw_cffi::CffiSubscription;
1058 #[cfg(feature = "rmw-cffi")]
1059 pub type RmwServiceServer = nros_rmw_cffi::CffiService;
1060 #[cfg(feature = "rmw-cffi")]
1061 pub type RmwServiceClient = nros_rmw_cffi::CffiClient;
1062
1063 /// Phase 124.A — zero-copy publisher slot type. Lives in the
1064 /// `internals` module so `nros-c` can construct + transmute the
1065 /// lifetime when boxing the slot for the C-side `_loan` /
1066 /// `_commit` / `_discard` token plumbing.
1067 #[cfg(all(feature = "rmw-cffi", feature = "lending"))]
1068 pub type RmwSlot<'a> = nros_rmw_cffi::CffiSlot<'a>;
1069
1070 /// Phase 124.A — zero-copy subscriber view type.
1071 #[cfg(all(feature = "rmw-cffi", feature = "lending"))]
1072 pub type RmwView<'a> = nros_rmw_cffi::CffiView<'a>;
1073
1074 /// Open a new middleware session.
1075 ///
1076 /// Wraps the backend-specific session constructor behind a common signature.
1077 /// Used by the C API (`nros-c`); Rust users should prefer `Executor::open()`.
1078 ///
1079 /// Phase 156 — takes an explicit primary backend by name, mirroring what
1080 /// `Executor::open` does for Rust callers. Without it, C bridges built with
1081 /// two linked backends (e.g. xrce + dds) get whichever ctor fires first —
1082 /// non-deterministic across link orderings, and often the wrong backend for
1083 /// the bridge's intended primary.
1084 ///
1085 /// Issue 1050 defect (3) — `rmw` is the RESOLVED selector, not a second
1086 /// reading of the environment. This function used to consult
1087 /// `rmw_selector()` itself, which made it the tree's second answer to "which
1088 /// backend?" and blind to every rung but the environment: a C image with a
1089 /// BAKED selector had it discarded here. The caller resolves the whole
1090 /// ladder (`env > baked > none`) through `ExecutorConfig` and passes the
1091 /// result; `None` means the registry must hold exactly one backend, which
1092 /// `nros_rmw_cffi::get_vtable` now enforces rather than assumes.
1093 #[cfg(feature = "rmw-cffi")]
1094 pub fn open_session(
1095 locator: &str,
1096 mode: nros_rmw::SessionMode,
1097 domain_id: u32,
1098 node_name: &str,
1099 rmw: Option<&str>,
1100 ) -> Result<RmwSession, nros_rmw::TransportError> {
1101 use nros_rmw::Rmw;
1102
1103 // Phase 249 P4b.1 — every linked backend self-registered via
1104 // its `.init_array` ctor before `main` (RFC-0042 §D3.3); no
1105 // runtime section walk.
1106
1107 let config = nros_rmw::RmwConfig {
1108 locator,
1109 mode,
1110 domain_id,
1111 node_name,
1112 namespace: "",
1113 properties: &[],
1114 };
1115 // Phase 155.B — propagate the real `TransportError` instead of
1116 // collapsing every backend failure to `ConnectionFailed`. The
1117 // C-side `rclc_support_init` decodes the variant into a
1118 // specific `NROS_RET_*` code so "init -> -X" tells the user
1119 // which precondition the backend rejected.
1120 // issue 1050 defect (3) — the selector ARRIVES here now. It used to be
1121 // read from `$NROS_RMW` on the spot, which is why a baked one could not
1122 // reach this path: the reader knew about one rung and the resolver knew
1123 // about two. The environment still wins; it wins in `ExecutorConfig`,
1124 // where every other field's precedence is decided, instead of here.
1125 if let Some(name) = rmw {
1126 return nros_rmw_cffi::CffiRmw::open_with_rmw(name, &config);
1127 }
1128 nros_rmw_cffi::CffiRmw.open(&config)
1129 }
1130
1131 /// Drive middleware I/O for pull-based backends.
1132 ///
1133 /// Delegates to [`Session::drive_io()`](nros_rmw::Session::drive_io),
1134 /// which each backend implements appropriately (no-op for push-based,
1135 /// poll for pull-based).
1136 ///
1137 /// Used by the C API executor before polling handles.
1138 #[cfg(feature = "rmw-cffi")]
1139 pub fn drive_session_io(session: &mut RmwSession, timeout_ms: i32) {
1140 use nros_rmw::Session;
1141 let _ = session.drive_io(timeout_ms);
1142 }
1143}
1144
1145// Re-export types that don't depend on RMW (always available)
1146pub use nros_node::{
1147 BOOT_SET_DOMAIN, BOOT_SET_LOCATOR, BOOT_SET_NAMESPACE, BOOT_SET_NODE_NAME, BakedBootConfig,
1148 BootConfig, BootConfigError, DOMAIN_ID_EXPLICIT_ZERO_C_ABI, DOMAIN_ID_MAX, ExecutorConfig,
1149 ExecutorSemantics, GuardCondition, HandleId, HandleSet, InvocationMode, NROS_BOOT_CONFIG_MAGIC,
1150 NROS_BOOT_CONFIG_VERSION, RawCancelCallback, RawGoalCallback, RawServiceCallback,
1151 RawSubscriptionCallback, ReadinessSnapshot, ShutdownCallbackFn, ShutdownCallbackHandle,
1152 ShutdownPhase, SpinOnceResult, SpinOptions, SpinPeriodPollingResult, Trigger,
1153 baked_domain_from_c_abi,
1154};
1155
1156// ---------------------------------------------------------------------------
1157// The error vocabulary (issue 0783). Both re-exports are unconditional.
1158//
1159// Every fallible call in the Rust user API returns `NodeError`, and
1160// `NodeError::Transport(TransportError)` is its most common variant — so
1161// handling one means naming both. They used to be exported from two unrelated
1162// blocks (`NodeError` with the boot/executor types, `TransportError` with the
1163// transport traits), which is a discoverability nit rather than a capability
1164// gap: both were always reachable, and `prelude` already listed them together.
1165// Nothing here changes what `nros::` exports.
1166//
1167// There is deliberately NO numeric code type beside them. A Rust caller matches
1168// a flat enum; the numeric vocabulary is the C ABI's `rcl_ret_t` /
1169// `NROS_RET_*` (0, -1..-16), which is its own space and not `rcl_ret_t`'s.
1170// nros-core carried an `rcl_ret_t` mirror (`RclReturnCode`) and an error that
1171// wrapped it (`NanoRosError`); neither was ever reachable from this facade and
1172// neither had a producer, so issue 0783 deleted them rather than exporting a
1173// type a user could name and never receive. RFC-0036's Errors row now describes
1174// what these two are.
1175// ---------------------------------------------------------------------------
1176/// phase-436 W7 — installing a port's park primitive, with the wake-object
1177/// constraint stated once. Board entries reach it through this facade because
1178/// that is the dependency they already carry.
1179///
1180/// Gated like every other `executor::` re-export here, and it has to be: the
1181/// module itself is `#[cfg(any(has_rmw, test))]`, and `has_rmw` is emitted by
1182/// `nros-node`'s build script exactly when `CARGO_FEATURE_RMW_CFFI` is set. An
1183/// ungated `pub use` therefore names a module that does not exist wherever no
1184/// RMW seam is compiled in — which is not a hypothetical configuration but the
1185/// one `nros build --metadata-mode` uses, where it is `error[E0432]:
1186/// unresolved import` before the harness reaches a single component.
1187#[cfg(feature = "rmw-cffi")]
1188pub use nros_node::executor::port_park;
1189
1190/// phase-436 B2 — the other half of the seam, for the boards that now use it.
1191///
1192/// `port_park` above is what a port hands the executor to WAIT with;
1193/// these are what it hands the executor to be told WHEN. A board registering
1194/// a deadline source receives a [`WakeSourceId`] and reads it back out of
1195/// `Executor::last_park()`, so without this re-export it could match on
1196/// neither.
1197#[cfg(feature = "rmw-cffi")]
1198pub use nros_node::executor::{MAX_WAKE_SOURCES, NextDeadlineFn, WakeSourceId};
1199
1200/// issue 1571 — the spawned tiers' executor backing. `nros::main!` emits one
1201/// `TierExecutorBacking` static per tiered entry and hands its slots to the
1202/// board's `run_tiers`, which refuses a short block through
1203/// `check_tier_executor_backing` and opens each spawned tier with
1204/// `Executor::open_with_session_slot`. Gated like the module it names
1205/// (`has_rmw` + `alloc` in nros-node).
1206#[cfg(all(feature = "rmw-cffi", feature = "alloc"))]
1207pub use nros_node::executor::{
1208 EXECUTOR_BACKING_DEFAULT_U64S, TierBackingShort, TierExecutorBacking, TierExecutorBackingSlot,
1209 TierSetupGate, TierSetupGuard, check_tier_executor_backing,
1210};
1211/// issue 1598 — each spawned tier's TASK memory (stack + control block):
1212/// `nros::main!` emits one `TierTaskMemory` static per spawned tier, sized from
1213/// its `stack_bytes`, and hands the `TierTaskMemorySet` rows to a board whose
1214/// tier runner creates the task over them (FreeRTOS `xTaskCreateStatic`).
1215#[cfg(all(feature = "rmw-cffi", feature = "alloc"))]
1216pub use nros_node::executor::{
1217 TIER_TASK_TCB_U64S, TierTaskMemory, TierTaskMemoryRaw, TierTaskMemorySet,
1218};
1219
1220pub use nros_node::NodeError;
1221pub use nros_rmw::TransportError;
1222
1223// RFC-0052 / phase-296 W3b — on-target contract-monitor types. Baked
1224// `system_monitors.rs` uses the fully-qualified `::nros_node::executor::
1225// monitor::*` path; this re-export lets hand-written entries and fixtures
1226// reach the same types through the `nros` umbrella (they install the
1227// tables via `Executor::set_monitor_table` / `set_age_table` and drain
1228// with `drain_violations`). The monitor module is `has_rmw`-gated in
1229// nros-node (it names entity types), so mirror that with `rmw-cffi`.
1230#[cfg(feature = "rmw-cffi")]
1231pub mod monitor {
1232 pub use nros_node::executor::monitor::{
1233 AgeMonitorSpec, GuardRef, GuardSpec, HazardCell, HazardSpec, MAX_AGE_MONITORS,
1234 MAX_MONITORS, MAX_VIOLATIONS, MECHANISM_EXECUTOR, MECHANISM_TIMER, MonitorArming,
1235 MonitorSpec, MonitorTableFull, NO_SINK, PubMonitorCell, REACTION_NONE, RULE_IDS,
1236 ReactionEvent, ReactionHookFn, SubMonitorCell, Violation, check_table_capacity, fqn_hash,
1237 request_monitor_arming, route_next_due_us, rule_code, rule_name,
1238 };
1239}
1240
1241// Issues 1635 / 1676 — the contract-monitor install and the `/diagnostics`
1242// reporter, ONE implementation: `nros::main!` calls `install_contract_monitors`
1243// on every executor it builds, and `nros-cpp`'s `nros_cpp_install_monitors`
1244// arms the same `DiagSink`.
1245#[cfg(feature = "rmw-cffi")]
1246pub mod contract;
1247
1248// Re-export RMW-dependent types (require an active transport backend)
1249#[cfg(feature = "rmw-cffi")]
1250pub use nros_node::{
1251 ActionClient, ActionClientCore, ActionServer, ActionServerCore, ActionServerHandle,
1252 ActionServerRawHandle, ActiveGoal, CompletedGoal, EmbeddedPublisher, EmbeddedRawPublisher,
1253 EmbeddedServiceClient, EmbeddedServiceServer, Executor, ExecutorSizing, FeedbackStream,
1254 GoalFeedbackStream, LoanError, Node, Promise, PublishLoan, RawActionClientSpec,
1255 RawActionServerSpec, RawActiveGoal, RawSubscription, RecvView, SessionHandle, SessionSpec,
1256 Subscription, action_channel_type,
1257};
1258
1259// phase-271 (issue #110) — per-entry executor sizing helper: the orchestration
1260// codegen's `build_executor` sizes its backing to the system's callback count
1261// via `nros::arena_size_for(CALLBACK_COUNT)` + `ExecutorSizing`, replacing the
1262// workspace-global `NROS_EXECUTOR_MAX_CBS`.
1263#[cfg(feature = "rmw-cffi")]
1264pub use nros_node::config::arena_size_for;
1265
1266/// The configured default receive-buffer size (`NROS_SUBSCRIPTION_BUFFER_SIZE`).
1267///
1268/// NOT a fallback for a missing bound any more (phase-403 W0): [`rx_buffer_for!`]
1269/// used to expand to this for an unbounded type and now refuses to compile
1270/// instead. It stays public, and stays the default, for the paths that have no
1271/// `M` to ask rather than a bound they declined to use -- `create_subscription_raw`
1272/// and the RFC-0043 type-name-string subscriptions, the service / action / TX
1273/// buffer defaults, the `pubsub_entry` term of the arena derivation in
1274/// `nros-node/build.rs`, and its weld to the C API's `MESSAGE_BUFFER_SIZE`.
1275/// Whether the arena derivation should stop leaning on it is a phase-403 W5
1276/// question.
1277///
1278/// Re-exported here (rather than reached through `nros_node`) because a macro
1279/// expands at the CALLER, where `nros_node` may not be a dependency at all.
1280pub use nros_node::config::DEFAULT_RX_BUF_SIZE;
1281
1282/// Phase 392 W3b — the bound behind [`rx_buffer_for!`]. Not part of the stable
1283/// surface; call the macro.
1284///
1285/// Public only because a `macro_rules!` body is expanded in the caller's crate
1286/// and can reach nothing private. `#[doc(hidden)]` and `__`-prefixed for the
1287/// same reason every other macro-support item in this crate is.
1288#[doc(hidden)]
1289pub const fn __rx_bound<M: nros_serdes::schema::Message>() -> Option<usize> {
1290 nros_serdes::size::max_serialized_bound::<M>()
1291}
1292
1293// Phase 173.5 — board config traits. `BoardConfig` (read locator /
1294// domain). `BoardTransportConfig` was removed with its dead
1295// setters (issue 1064); the live path is the deploy overlay.
1296pub use nros_platform::BoardConfig;
1297
1298// Phase 216.A.1 — `DispatchStrategy` enum. User-visible at
1299// `nros::DispatchStrategy`; the canonical home is `nros_platform::
1300// board::dispatch` so the C ABI symbol the `nros::node!()` macro emits
1301// (`__nros_node_<pkg>_dispatch_strategy() -> u8`) lives next to the
1302// other board-side trampolines.
1303pub use nros_platform::DispatchStrategy;
1304
1305/// Implementation detail — used by `nros::node!()` macro expansion.
1306///
1307/// Re-exports `nros_platform` so the macro's emitted trampoline can
1308/// reference `RuntimeCtx` / `RuntimeError` / the `Node*Fn`
1309/// fn-pointer aliases without forcing every consumer Node pkg's
1310/// `Cargo.toml` to carry an explicit `nros-platform` dep on top of
1311/// `nros`. Phase 212.M-F.13 path (b).
1312///
1313/// Not part of the public API — paths under this module may change at
1314/// any time. End users should depend on `nros` alone and invoke
1315/// `nros::node!()`; the macro routes through here automatically.
1316#[doc(hidden)]
1317pub mod __macro_support {
1318 pub use ::nros_platform;
1319
1320 /// phase-314 — whether THIS `nros` build carries the parameter services.
1321 ///
1322 /// The `nros::main!` expansion const-asserts it when the system declares
1323 /// `[param_services]`. A cfg in the entry crate cannot see this: the
1324 /// feature belongs to `nros`, and the entry enables it through its
1325 /// dependency, so only `nros` itself can report the answer.
1326 ///
1327 /// Without the assert the mismatch is SILENT — `apply_param_services` is a
1328 /// no-op, the build succeeds, the image boots, and `ros2 param list`
1329 /// returns nothing.
1330 pub const PARAM_SERVICES_ENABLED: bool = cfg!(feature = "param-services");
1331
1332 /// phase-461 W6 — whether THIS `nros` build carries the parameter STORE
1333 /// (`param-store`, which `param-services` implies). A system declaring the
1334 /// `params` axis const-asserts it, for the reason above: without the
1335 /// feature `apply_param_store` is a no-op and every `declare_parameter`
1336 /// is refused at boot instead of at compile time.
1337 pub const PARAM_STORE_ENABLED: bool = cfg!(feature = "param-store");
1338
1339 /// Issue 1766 — the same answer for the other capability axes a leaf
1340 /// can declare in `[system] features`. A Form-1 `nros::main!()`
1341 /// const-asserts the flag of every declared axis
1342 /// (`nros_orchestration_ir::leaf_capabilities::LeafAxis::compiled_flag`),
1343 /// because without the feature the axis is compiled out and the
1344 /// declaration would be dropped in silence.
1345 pub const LIFECYCLE_SERVICES_ENABLED: bool = cfg!(feature = "lifecycle-services");
1346 /// See [`LIFECYCLE_SERVICES_ENABLED`].
1347 pub const SAFETY_E2E_ENABLED: bool = cfg!(feature = "safety-e2e");
1348 /// See [`LIFECYCLE_SERVICES_ENABLED`].
1349 pub const ROSOUT_ENABLED: bool = cfg!(feature = "rosout");
1350
1351 /// Issue 0257 — the build-time executor callback-table size
1352 /// (`NROS_EXECUTOR_MAX_CBS`, default 4). Re-exported so the `nros::main!`
1353 /// expansion can `const`-assert the model's entity count against the
1354 /// capacity that ACTUALLY compiles in, instead of letting the image boot
1355 /// and die on `create_timer (code=-6 Full)`.
1356 pub use ::nros_node::config::MAX_CBS as EXECUTOR_MAX_CBS;
1357}
1358
1359// Phase 110.B / 110.G — scheduling-context API surface. Consumers
1360// of the Phase 110 cyclic / TT scheduler need these types to
1361// describe schedules and bind handles; re-exporting them here
1362// keeps user code free of `nros_node::executor::sched_context`
1363// path noise. Gated on `rmw-cffi`: the source module is
1364// `#[cfg(any(has_rmw, test))]` in nros-node, so it only exists once
1365// an RMW backend is linked (matches the re-export block above).
1366#[cfg(feature = "rmw-cffi")]
1367pub use nros_node::executor::sched_context::{
1368 DeadlineAction, DeadlinePolicy, OptUs, Priority, SchedClass, SchedContext, SchedContextId,
1369 TimeTriggeredSchedule, TimeTriggeredScheduleError, TimeTriggeredWindow,
1370};
1371
1372#[cfg(all(feature = "std", feature = "rmw-cffi"))]
1373pub use nros_node::SpinPeriodResult;
1374
1375// Re-export service types
1376pub use nros_core::{ServiceClient, ServiceServer};
1377
1378// Re-export action types.
1379//
1380// issue 0796 — `CancelResponse` (the per-goal Reject/Accept decision) and
1381// `CancelReturnCode` (the `action_msgs/srv/CancelGoal` RPC status) are two
1382// concepts that shared one name until the split. Both are exported: without
1383// `CancelReturnCode` here, `ActionClient::cancel_goal`'s `Promise<CancelReturnCode>`
1384// could not be NAMED from `nros::` alone.
1385pub use nros_core::{
1386 CancelResponse, CancelReturnCode, GoalId, GoalInfo, GoalResponse, GoalStatus,
1387 GoalStatusStamped, RosAction,
1388};
1389
1390// Re-export lifecycle types (always available, no_std compatible)
1391pub use nros_core::{LifecycleState, LifecycleTransition, TransitionResult};
1392pub use nros_node::{LifecycleCallbackFn, LifecycleError, LifecyclePollingNode};
1393
1394/// Re-export of the full lifecycle module so examples can reach
1395/// `LifecycleCallbackSlot`, `LifecyclePollingNodeCtx`, etc.
1396pub mod lifecycle {
1397 pub use nros_core::lifecycle::{LifecycleState, LifecycleTransition, TransitionResult};
1398 pub use nros_node::lifecycle::*;
1399}
1400
1401// Phase 128.G — bridge surface re-exports. Gated behind the
1402// `bridge` / `config` umbrella features so single-backend builds
1403// don't pull in `nros-bridge` (or, for `config`, the TOML stack).
1404#[cfg(feature = "bridge")]
1405pub use nros_bridge as bridge;
1406
1407#[cfg(feature = "config")]
1408pub use nros_bridge::run_from_config;
1409
1410// Re-export parameter types.
1411//
1412// phase-382 W2' — `ParameterStorage` / `ParameterTable` are here because the
1413// store's slots are CALLER-OWNED: anyone constructing a `ParameterServer`
1414// outside an executor has to place the storage and lend it.
1415// phase-426 W1 — `NodeKey` is here because every per-parameter call names the
1416// node it is about; a consumer holding a `ParameterServer` needs it to say so.
1417// phase-417 W4.a — the rest of the parameter vocabulary. Five ledger rows
1418// (`rust:ParameterRange`, `rust:ParameterRanges`, `rust:Parameters`,
1419// `rust:Node::use_undeclared_parameters`, `rust:RmwParameterConversionError`)
1420// were not missing capabilities: `nros_params` had every one of them and this
1421// list stopped at ten names, so a user of the facade could build a range
1422// through the builder's scalar arguments and never NAME the type it produced.
1423// An unexported type is a capability a user cannot write down.
1424pub use nros_params::{
1425 DeclarationError, FloatingPointRange, IntegerRange, MAX_ON_SET_CALLBACKS, MandatoryParameter,
1426 NodeKey, OnSetContext, OnSetParameterFn, OnSetParameterHandle, OptionalParameter, Parameter,
1427 ParameterBuilder, ParameterDescriptor, ParameterError, ParameterRange, ParameterServer,
1428 ParameterStorage, ParameterTable, ParameterType, ParameterValue, ParameterVariant,
1429 ReadOnlyParameter, SetParameterResult, UndeclaredParameters,
1430};
1431// The wire-side half: why an `rcl_interfaces` parameter value could not be
1432// turned into a native one. The `~/set_parameters` handler already puts
1433// `reason()` into the `SetParametersResult`; without the type a user could read
1434// the sentence and not match on the cause.
1435// Gated like the module it comes from: `parameter_services` is the six
1436// `rcl_interfaces/srv/*` servers, and an image without them has no wire side
1437// for this error to describe.
1438// Gated like the module it comes from: `parameter_services` is
1439// `#[cfg(all(feature = "param-services", any(has_rmw, test)))]` in nros-node,
1440// so it exists only once an RMW backend is linked — the same pairing the
1441// `sched_context` re-export above uses.
1442#[cfg(all(feature = "param-services", feature = "rmw-cffi"))]
1443pub use nros_node::parameter_services::ValueConversionError;
1444/// Prelude module for convenient imports
1445///
1446/// Import everything you need with a single statement:
1447/// ```
1448/// use nros::prelude::*;
1449/// ```
1450/// phase-379 W5 — the RTOS machinery, named explicitly.
1451///
1452/// The second tier of the two-tier surface. `nros::prelude` is the API a ported
1453/// ROS 2 node uses; everything here exists because the target is an RTOS, has no
1454/// correspondent in rclrs/rclcpp/rclc, and a ROS 2 developer reading a node
1455/// should not have to step over it.
1456///
1457/// Nothing moved out of `nros::` — these are re-exports, and every name is still
1458/// reachable at its old path. What changed is that they are no longer dragged in
1459/// by `use nros::prelude::*`.
1460///
1461/// The membership rule is mechanical rather than taste: a name belongs in the
1462/// PRELUDE iff the parity ledger gives it a non-`extension` verdict — i.e. it
1463/// corresponds to something in rclrs, rclcpp or rclc. Names with no
1464/// correspondent belong here, unless they are load-bearing for startup
1465/// (`ExecutorConfig`, `SpinOptions`), which is an argued allow-list rather than
1466/// an exception anyone may grow.
1467pub mod embedded {
1468 // Wire encoding. A node publishes typed messages; these are for code that
1469 // handles bytes, which upstream hides entirely.
1470 pub use crate::{CdrReader, CdrWriter};
1471
1472 // Handle bookkeeping — the static tables that replace an allocator.
1473 #[cfg(feature = "rmw-cffi")]
1474 pub use crate::{HandleId, HandleSet, InvocationMode};
1475
1476 // Component/runtime plumbing, and the source-metadata capture the
1477 // orchestration layer records. None of it appears in a ported node.
1478 pub use crate::MetadataRecorder;
1479
1480 // Entity REGISTRATION vocabulary. A ported node writes
1481 // `create_publisher(...)`; these are what the declaration macros and the
1482 // orchestration layer use to describe what was created.
1483 pub use crate::{
1484 ActionTag, Callback, CallbackEffectKind, EntityKind, NodeRuntime, ServiceTag,
1485 SourceLocationMetadata, SourceNameKind, SubscriptionTag,
1486 };
1487 #[cfg(feature = "rmw-cffi")]
1488 pub use crate::{CallbackEffects, record_node_metadata, register_node};
1489 // Gated where the root gates it — the export follows the capability, not
1490 // the tier.
1491 #[cfg(feature = "alloc")]
1492 pub use crate::SourceMetadataExport;
1493}
1494
1495pub mod prelude {
1496 // phase-379 W5 — the RTOS machinery moved to `nros::embedded`. Removed
1497 // here rather than re-exported from both: a two-tier surface that still
1498 // drags tier two in through the glob is one tier with extra words.
1499
1500 pub use crate::{
1501 Deserialize, Logger, MessageInfo, QoSDurabilityPolicy, QoSHistoryPolicy, QoSProfile,
1502 QoSReliabilityPolicy, RosMessage, RosService, Serialize, TopicInfo,
1503 };
1504
1505 // phase-417 / issue 0589 — logging, in the glob a node author already
1506 // writes. `Logger` above now means `nros_log::Logger`; these are what
1507 // drives it. Without them the shortest path from `use nros::prelude::*` to
1508 // a printed line was `std::println!`, which kills a Zephyr `native_sim`
1509 // image outright.
1510 //
1511 // The five severity macros now carry rclrs's OWN names (RFC-0089, settled
1512 // 2026-09-04), so a ROS 2 developer meeting one through the glob meets the
1513 // exact spelling their client library has, not a near-miss of it. That is
1514 // the property `check-prelude-tiers` enforces mechanically: a ledger
1515 // `extension` may not sit in the glob.
1516 //
1517 // `Severity`, `get_logger`, `get_or_create_logger`, `register_logger` and
1518 // `nros_trace` are ledger `extension`s -- rclrs has no correspondent for a
1519 // free logger lookup, a registry, or a TRACE severity (it stops at debug).
1520 // They stay behind `nros::logging::` / the crate root, where reaching for
1521 // one is a decision. phase-379 W5's rule, applied to this phase's own work.
1522 //
1523 // The throttle macros ARE here: they are ledger `divergence`s, not
1524 // extensions -- rclrs has the capability under a different decomposition
1525 // (`log_info!(logger.throttle(d), "…")`), so the glob is not introducing a
1526 // name with no upstream counterpart.
1527 #[cfg(all(feature = "alloc", feature = "rmw-cffi"))]
1528 pub use crate::{Component, DeclarativeNode, NodeContext};
1529 #[cfg(feature = "alloc")]
1530 pub use crate::{
1531 NodeActionClient, NodeActionServer, NodeDeclError, NodeOptions, NodeParameter,
1532 NodePublisher, NodeResult, NodeServiceClient, NodeServiceServer, NodeSubscription,
1533 NodeTimer, ParameterDefault, node,
1534 };
1535 pub use crate::{
1536 log_debug, log_error, log_fatal, log_info, log_warn, nros_debug_throttle,
1537 nros_debug_throttle_at, nros_error_throttle, nros_error_throttle_at, nros_fatal_throttle,
1538 nros_fatal_throttle_at, nros_info_throttle, nros_info_throttle_at, nros_warn_throttle,
1539 nros_warn_throttle_at,
1540 };
1541
1542 // Re-export lifecycle types
1543 pub use crate::{
1544 LifecycleCallbackFn, LifecycleError, LifecyclePollingNode, LifecycleState,
1545 LifecycleTransition, TransitionResult,
1546 };
1547
1548 // Re-export executor config + handle types (always available)
1549 pub use crate::{
1550 ExecutorConfig, GuardCondition, NodeError, SessionMode, SpinOnceResult, SpinOptions,
1551 SpinPeriodPollingResult, TransportError, Trigger,
1552 };
1553
1554 // issue 0687 — `ExecutorConfig::from_env()` is an extension trait now (the
1555 // environment is read at this crate's edge, not in the core), so the
1556 // spelling only works where the trait is in scope. It is in the prelude
1557 // precisely so that the consumers written against the inherent method —
1558 // the native examples, the benches — keep compiling unchanged.
1559 #[cfg(feature = "env")]
1560 pub use crate::ExecutorConfigEnvExt;
1561
1562 // Re-export RMW-dependent executor + handle types
1563 #[cfg(feature = "rmw-cffi")]
1564 pub use crate::{
1565 EmbeddedPublisher, EmbeddedServiceClient, Executor, FeedbackStream, Node, Promise,
1566 Subscription,
1567 };
1568
1569 // Publisher/Subscriber options (topic + QoS).
1570 pub use crate::{PublisherOptions, SubscriptionOptions};
1571
1572 #[cfg(all(feature = "std", feature = "rmw-cffi"))]
1573 pub use crate::SpinPeriodResult;
1574
1575 // Re-export parameter types
1576 pub use crate::{ParameterServer, ParameterStorage, ParameterType, ParameterValue};
1577
1578 // Re-export typed parameter API (rclrs-compatible builder pattern)
1579 pub use crate::{
1580 MandatoryParameter, OptionalParameter, ParameterBuilder, ParameterError, ParameterVariant,
1581 ReadOnlyParameter,
1582 };
1583
1584 // Re-export action types
1585 pub use crate::{GoalId, GoalInfo, GoalResponse, GoalStatus, GoalStatusStamped, RosAction};
1586
1587 // Re-export Time, Duration, Clock from core
1588 pub use nros_core::{Clock, ClockType, Duration, Time};
1589
1590 // Re-export timer types
1591 pub use crate::{TimerCallbackFn, TimerDuration, TimerHandle, TimerMode};
1592}
1593
1594/// Derive macros for message types
1595///
1596/// Use these macros to generate message serialization code.
1597/// These macros help you create custom message types that are compatible
1598/// with ROS 2's CDR serialization format.
1599pub mod derive {
1600 #[cfg(feature = "macros")]
1601 pub use nros_macros::RosMessage;
1602}
1603
1604#[cfg(test)]
1605mod tests {
1606 #[test]
1607 fn test_prelude_imports() {
1608 // This test just verifies that the prelude compiles
1609 use crate::prelude::*;
1610
1611 let _ = QoSProfile::BEST_EFFORT;
1612 }
1613
1614 /// The component trait, its registration context and the result type
1615 /// resolve under the names phase-483 W2/W3 gave them: `Component` (the
1616 /// trait, which phase-212 had named `Node`) and a `NodeContext` with no
1617 /// runtime parameter.
1618 #[cfg(feature = "rmw-cffi")]
1619 #[test]
1620 fn node_context_types_resolve() {
1621 fn _take_node_ctx<N: crate::Component>(_: &mut crate::NodeContext<'_>) {}
1622 let _: crate::NodeResult<()> = Ok(());
1623 }
1624}
1625
1626// ---------------------------------------------------------------------------
1627// phase-361 W8.e / issue 0594 — capabilities REQUIRE the heap / the standard
1628// library, they do not enable it. Turning `alloc` or `std` on for the user
1629// silently changes what their firmware image is; naming the feature they must
1630// add does not.
1631// ---------------------------------------------------------------------------
1632// issue 0687 follow-up — `alloc`, not `std`. The reason recorded here ("writes
1633// a file and exits") was never this crate's: the file write is `nros-cpp`'s
1634// `metadata_hooks`. What `metadata_mode.rs` actually needed was a `Sync`
1635// global, and it named `std::sync::Mutex` for it; on the portable mutex the
1636// capability is `String` + `format!` + a lock, i.e. the heap and nothing more.
1637#[cfg(all(feature = "metadata-mode", not(feature = "alloc")))]
1638compile_error!(
1639 "`metadata-mode` records into a heap-allocated global: add \"alloc\" to this crate's features"
1640);
1641// Emitted here as well as in `nros-node`, and NOT for the reason it first
1642// looks like. `nros = { features = ["env"] }` alone does not reach this line:
1643// `env` forwards to `nros-node/env`, nros-node is compiled first, and its own
1644// guard aborts the build there — measured, not assumed. What this covers is
1645// the case feature unification creates, where some other crate in the graph
1646// turns `nros-node/std` on so that guard stays quiet while THIS crate's `std`
1647// is still off. Rare, and exactly the shape that would otherwise compile a
1648// hosted capability into a build that never named the standard library.
1649#[cfg(all(feature = "env", not(feature = "std")))]
1650compile_error!(
1651 "`env` reads the process environment, which needs the standard library: add \"std\" to this crate's features"
1652);
1653
1654// phase-379 W5, revised by phase-428 W10 — the crate-level `QOS_PROFILE_*`
1655// presets (rclrs parity) and the older `nros::qos::*` module are two spellings
1656// of the same profiles. BOTH now alias `QoSProfile`'s associated consts; until
1657// W10 the second restated them as struct literals, and these tests were the only
1658// thing holding the two copies together.
1659//
1660// They are kept, and what they assert has changed meaning. They no longer catch
1661// a hand-mirror drifting (there is no longer a hand mirror to drift — issues
1662// 0088 / 0160 / 0245). They catch a REINTRODUCTION: the moment someone writes a
1663// literal into either surface instead of aliasing, a value can differ and one of
1664// these fails. That is cheap enough to keep and is the only mechanism standing
1665// between this crate and the arrangement W10 removed.
1666//
1667// What they do NOT check, deliberately, is whether the SSoT matches UPSTREAM.
1668// Two aliases of a wrong constant agree perfectly — which is exactly the state
1669// `QOS_PROFILE_PARAMETER_EVENTS` was in (`KeepAll, 0` against upstream's
1670// `KEEP_LAST, 1000`). That assertion belongs beside the associated consts in
1671// `nros-rmw/src/traits.rs`, not here.
1672#[cfg(test)]
1673mod qos_preset_parity {
1674 use super::*;
1675
1676 #[test]
1677 fn crate_level_presets_alias_the_associated_consts() {
1678 assert_eq!(QOS_PROFILE_DEFAULT, QoSProfile::QOS_PROFILE_DEFAULT);
1679 assert_eq!(QOS_PROFILE_SENSOR_DATA, QoSProfile::QOS_PROFILE_SENSOR_DATA);
1680 assert_eq!(QOS_PROFILE_PARAMETERS, QoSProfile::QOS_PROFILE_PARAMETERS);
1681 assert_eq!(
1682 QOS_PROFILE_ACTION_STATUS_DEFAULT,
1683 QoSProfile::QOS_PROFILE_ACTION_STATUS_DEFAULT
1684 );
1685 }
1686
1687 /// `qos::*` was a SEPARATE hand-written copy until phase-428 W10; all five
1688 /// names alias the associated consts now, so this passes by construction.
1689 /// It stays as the tripwire for re-introducing a literal — see the module
1690 /// comment above.
1691 #[test]
1692 fn qos_module_agrees_with_the_presets() {
1693 // issue 0829 FIXED — SYSTEM_DEFAULT was the first to become an alias.
1694 // It is here rather than in its own pinned test because the two copies
1695 // no longer say anything a copy could get wrong: both alias the one
1696 // associated const, which is all sentinel.
1697 assert_eq!(
1698 qos::SYSTEM_DEFAULT,
1699 QOS_PROFILE_SYSTEM_DEFAULT,
1700 "qos::SYSTEM_DEFAULT drifted"
1701 );
1702 assert_eq!(qos::DEFAULT, QOS_PROFILE_DEFAULT, "qos::DEFAULT drifted");
1703 assert_eq!(
1704 qos::SENSOR_DATA,
1705 QOS_PROFILE_SENSOR_DATA,
1706 "qos::SENSOR_DATA drifted"
1707 );
1708 assert_eq!(
1709 qos::SERVICES_DEFAULT,
1710 QOS_PROFILE_SERVICES_DEFAULT,
1711 "qos::SERVICES_DEFAULT drifted"
1712 );
1713 assert_eq!(
1714 qos::PARAMETERS,
1715 QOS_PROFILE_PARAMETERS,
1716 "qos::PARAMETERS drifted"
1717 );
1718 }
1719
1720 /// issue 0829, RESOLVED — this test used to PIN the divergence, asserting
1721 /// depth 10 on the façade side and depth 1 on the `nros-rmw` side because
1722 /// neither was obviously the live one. Both numbers are gone, and the
1723 /// replacement is not "we picked one": no concrete depth can be right,
1724 /// because the two reference RMWs resolve the same sentinel differently
1725 /// (`rmw_cyclonedds_cpp` → `KEEP_LAST, 1`; `rmw_zenoh_cpp` →
1726 /// `RMW_ZENOH_DEFAULT_HISTORY_DEPTH`, 42).
1727 ///
1728 /// So what is asserted now is the SHAPE: `SYSTEM_DEFAULT` states nothing,
1729 /// on every field. That is what makes it different from `DEFAULT` — the
1730 /// two being byte-identical was the older defect, and asserting they
1731 /// DIFFER is what keeps anyone from quietly aliasing them again.
1732 #[test]
1733 fn system_default_states_nothing_on_every_field() {
1734 use nros_rmw::{
1735 DEPTH_SYSTEM_DEFAULT, QoSDurabilityPolicy, QoSHistoryPolicy, QoSLivelinessPolicy,
1736 QoSReliabilityPolicy,
1737 };
1738 let sd = QOS_PROFILE_SYSTEM_DEFAULT;
1739 assert_eq!(sd.reliability, QoSReliabilityPolicy::SystemDefault);
1740 assert_eq!(sd.durability, QoSDurabilityPolicy::SystemDefault);
1741 assert_eq!(sd.history, QoSHistoryPolicy::SystemDefault);
1742 assert_eq!(sd.depth, DEPTH_SYSTEM_DEFAULT);
1743 // `None` IS the liveliness sentinel — it lowers to
1744 // `NROS_RMW_LIVELINESS_SYSTEM_DEFAULT` (0), the two having collapsed
1745 // onto one value in phase-376 W5/B2.
1746 assert_eq!(sd.liveliness_kind, QoSLivelinessPolicy::None);
1747 assert!(sd.has_unresolved_system_default());
1748
1749 // The whole point of the name: it is NOT a synonym for DEFAULT.
1750 assert_ne!(
1751 sd, QOS_PROFILE_DEFAULT,
1752 "SYSTEM_DEFAULT aliased DEFAULT again — see issue 0829"
1753 );
1754 assert!(!QOS_PROFILE_DEFAULT.has_unresolved_system_default());
1755 }
1756}