Skip to main content

nros/
node.rs

1//! Rust component API shared by metadata discovery and generated runtimes.
2
3// phase-483 W3 — the component API (`Component`, `NodeContext`,
4// `DeclarativeNode`) registers into a live executor and exists only with
5// `rmw-cffi`. Without it this module still carries the metadata vocabulary the
6// C/C++ adapters use, and the declaration helpers the component API calls go
7// unused.
8#![cfg_attr(not(feature = "rmw-cffi"), allow(unused_imports, dead_code))]
9
10use core::marker::PhantomData;
11
12#[cfg(feature = "param-store")]
13use nros_params::ParameterServerOps as _; // phase-515 R10 stage 2
14
15// issue 0413 — the descriptor-registration bound. `MessageForRmw` is
16// `RosMessage` alone unless a descriptor-needing backend is linked, in which
17// case it also requires `nros_serdes::schema::Message` (the schema the Cyclone
18// descriptor builder walks). Generated message crates implement both.
19use nros_node::rmw_type_registry::MessageForRmw;
20
21use crate::{
22    ActionTag, CallbackId, CancelResponse, EntityId, GoalResponse, GoalStatusCode, GoalUuid,
23    ParameterType, QoSProfile, RosAction, RosMessage, RosService, ServiceTag, SubscriptionTag,
24    TimerClockSource, TimerDuration,
25    heapless::Vec,
26    node_metadata::{
27        CallbackEffectKind, EntityKind, EntityMetadata, EntityMetadataSpec, MetadataRecorder,
28        MetadataString, NodeId, NodeMetadataError, ParameterDefault, SourceLocationMetadata,
29        copy_str, entity_metadata,
30    },
31};
32
33// Phase 212.N.7 step-6 closing sweep — ~~`component_register_symbol`~~
34// removed. It built the legacy `__nros_component_<pkg>_register`
35// symbol name for the M.5.a BSP baker to look up by literal. step-6
36// retired the macro emit + step-4 deleted the FreeRTOS BSP baker
37// crate that was the sole live consumer. The Phase 212.N Entry pkg
38// path calls `<pkg>::register(runtime)` through the path API, so this
39// helper has no live callers.
40
41/// Result type for component declarations.
42pub type NodeResult<T = ()> = Result<T, NodeDeclError>;
43
44/// Register `M`'s runtime type descriptor with a descriptor-needing backend
45/// (Cyclone DDS), from the DECLARATIVE path — issue 0413.
46///
47/// The imperative API's typed creators (`Node::create_publisher_with_qos::<M>`
48/// in `nros-node`) already call `register_type::<M>()` before asking the cffi
49/// vtable for the entity, because Cyclone resolves topic types through a
50/// RUNTIME registry and `dds_create_topic` fails without it.
51///
52/// The declarative Node API does not reach those creators. `NodeContext`
53/// records `EntityMetadata` and the sink calls the type-ERASED
54/// `create_generic_publisher_with_qos(topic, type_name, type_hash, qos)` —
55/// which has a type NAME and no `M`, so it cannot register anything. The
56/// descriptor was therefore never built, `find_descriptor` returned null in
57/// `publisher.cpp`, and the entity failed with `NROS_RMW_RET_UNSUPPORTED` ->
58/// `TransportError::PublisherCreationFailed` -> `NodeDeclError::Runtime` ->
59/// `RuntimeError::NodeRegister("<pkg>")`, four collapses away from the cause.
60///
61/// So it is registered HERE, at the last point that still knows `M`. A no-op
62/// unless a descriptor-needing backend installed a registrar
63/// (`nros_rmw::register_type_descriptor` returns `Ok` when the slot is empty),
64/// so zenoh / XRCE builds are unaffected.
65///
66/// Why this only surfaced now: every native Rust example was
67/// `[package.metadata.nros.application]` (imperative, typed creators) until
68/// phase-338 W3 made them Node-class. C and C++ were never affected — they use
69/// the static `descriptors.cpp` table, which is why `c/talker` published
70/// normally against the same backend while `rust/talker` could not.
71#[inline]
72fn register_declared_type<M: nros_node::rmw_type_registry::MessageForRmw>() -> NodeResult<()> {
73    nros_node::rmw_type_registry::register_type::<M>().map_err(|_| NodeDeclError::Runtime)
74}
75
76/// issue 0413, service half — register both payload types of `S`.
77///
78/// Services reach the same type-erased sink path as publishers, so the
79/// descriptor has to be built here too. The bound is a WHERE-CLAUSE on the
80/// declarative methods rather than on `RosService` itself: `RosService` lives in
81/// `nros-core`, which cannot depend on `nros-node` where `MessageForRmw` is
82/// defined. Generated service crates satisfy it (their payloads implement
83/// `schema::Message`); a hand-rolled service used only with zenoh/XRCE is
84/// unaffected, because `MessageForRmw` collapses to `RosMessage` when no
85/// descriptor-needing backend is linked.
86#[inline]
87fn register_declared_service<S: RosService>() -> NodeResult<()>
88where
89    S::Request: nros_node::rmw_type_registry::MessageForRmw,
90    S::Reply: nros_node::rmw_type_registry::MessageForRmw,
91{
92    register_declared_type::<S::Request>()?;
93    register_declared_type::<S::Reply>()
94}
95
96/// Node declaration error.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum NodeDeclError {
99    /// Metadata recorder rejected the declaration.
100    Metadata(NodeMetadataError),
101    /// Host/runtime discovery could not find `nros::node!` export.
102    MissingExport,
103    /// Generated runtime rejected the declaration.
104    Runtime,
105    /// The executor's fixed callback-entry table is full — a timer /
106    /// subscription / service / action could not claim a slot (issue 0095).
107    /// Carries the capacity cause through the `NodeError → NodeDeclError`
108    /// collapse so the register seam can name `NROS_EXECUTOR_MAX_CBS`.
109    ExecutorFull,
110    /// A publish named an entity this component has no publisher for — the
111    /// LOOKUP failed, nothing was ever handed to the transport.
112    ///
113    /// issue 0736 — split out of [`Self::Runtime`], which the publish path
114    /// returned for BOTH "the transport rejected the sample" and "there is no
115    /// such publisher": ~~`lookup_publisher(...).unwrap_or(Err(Runtime))`~~. Those
116    /// are different bugs in different layers with different fixes, and from a
117    /// serial console they were the same line. Exactly the conflation #572
118    /// removed one level out, where discarding the result made "the timer never
119    /// fired" and "every publish failed" the same observation.
120    UnknownPublisher,
121    /// The parameter store refused a declaration and the name is still absent:
122    /// the store is full, or it rejected the value. A name already declared is
123    /// NOT this — the store answers `false` for that too, but the parameter is
124    /// there.
125    ///
126    /// Split out of [`Self::Runtime`] for the reason [`Self::UnknownPublisher`]
127    /// was (issue 0736): a refused parameter and a rejected transport handle are
128    /// different faults with different fixes, and one opaque variant made them
129    /// the same line on a serial console.
130    ParameterRejected,
131    /// A component CELL registry is full -- the class creates more entities of
132    /// one kind (publishers, service servers/clients, action servers/clients)
133    /// than its registry holds.
134    ///
135    /// issue 1130 -- split out of [`Self::Runtime`] BEFORE that capacity became
136    /// derivable. phase-412's rule: a derived count is safe only where
137    /// exhaustion NAMES the knob, and a bare "component runtime rejected
138    /// declaration" named nothing. The capacity is the class's `ENTITY_BOUNDS`
139    /// when it states one, else `NROS_RUNTIME_MAX_CELL_ENTITIES`.
140    CellRegistryFull,
141    /// Issue 2075 — a `reset_timer` / `cancel_timer` named an entity this
142    /// component has no CONTROLLABLE timer for: it declared no such timer,
143    /// or its class's [`EntityBounds::timers`] holds fewer timers than it
144    /// declares (the timers past the bound still run; they just cannot be
145    /// reset or cancelled by id).
146    UnknownTimer,
147}
148
149impl NodeDeclError {
150    /// Human-readable static message for diagnostics that cross FFI/plugin boundaries.
151    pub const fn message(self) -> &'static str {
152        match self {
153            Self::Metadata(NodeMetadataError::Capacity) => "component metadata capacity exceeded",
154            Self::Metadata(NodeMetadataError::NameTooLong) => "component metadata name too long",
155            Self::Metadata(NodeMetadataError::UnknownNode) => {
156                "component entity references an unknown node"
157            }
158            Self::Metadata(NodeMetadataError::UnknownEntity) => {
159                "component callback effect references an unknown entity"
160            }
161            Self::Metadata(NodeMetadataError::DuplicateId) => {
162                "component metadata contains a duplicate stable ID"
163            }
164            Self::MissingExport => "package has no exported nros component",
165            Self::Runtime => "component runtime rejected declaration",
166            Self::UnknownPublisher => "no publisher declared for that entity",
167            Self::UnknownTimer => {
168                "no controllable timer for that entity — it was not declared, or \
169                 the class's ENTITY_BOUNDS.timers (EntityBounds::with_timers) holds \
170                 fewer timers than it declares"
171            }
172            Self::ParameterRejected => {
173                "the parameter store refused the declaration (the store is full, or it rejected the value)"
174            }
175            Self::ExecutorFull => {
176                "executor callback table full — raise NROS_EXECUTOR_MAX_CBS \
177                 (build-time, default 4)"
178            }
179            Self::CellRegistryFull => {
180                "component cell registry full — raise the class's ENTITY_BOUNDS, \
181                 or NROS_RUNTIME_MAX_CELL_ENTITIES (build-time, default 8) for a \
182                 class that states none"
183            }
184        }
185    }
186}
187
188impl From<NodeMetadataError> for NodeDeclError {
189    fn from(value: NodeMetadataError) -> Self {
190        Self::Metadata(value)
191    }
192}
193
194#[cfg(test)]
195mod cell_registry_full_tests {
196    use super::NodeDeclError;
197
198    /// Issue 1130 -- the precondition for deriving the cell capacity: running
199    /// out NAMES both remedies, where it used to read "component runtime
200    /// rejected declaration".
201    #[test]
202    fn a_full_cell_registry_names_the_knob_and_the_class_bound() {
203        let m = NodeDeclError::CellRegistryFull.message();
204        assert!(m.contains("NROS_RUNTIME_MAX_CELL_ENTITIES"), "{m}");
205        assert!(m.contains("ENTITY_BOUNDS"), "{m}");
206        assert_ne!(m, NodeDeclError::Runtime.message());
207    }
208
209    /// Issue 2075 -- `exact` asks for no timer table (a class that states its
210    /// bounds pays for none), `with_timers` adds one, the knob default has
211    /// one, and running out names the bound.
212    #[test]
213    fn timer_bounds_default_to_none_under_exact_and_name_the_bound() {
214        use super::EntityBounds;
215        let exact = EntityBounds::exact(1, 0, 0, 0, 0);
216        assert_eq!(exact.timers, 0);
217        assert_eq!(exact.with_timers(2).timers, 2);
218        assert_eq!(exact.with_timers(2).publishers, 1);
219        assert_eq!(
220            EntityBounds::knob_caps().timers,
221            crate::config::MAX_CELL_ENTITIES
222        );
223        let m = NodeDeclError::UnknownTimer.message();
224        assert!(m.contains("ENTITY_BOUNDS.timers"), "{m}");
225    }
226}
227
228/// issue 0413, action half — register every wire payload type of `A`.
229///
230/// Mirrors the eight `register_type::<A::…>()` calls the IMPERATIVE action
231/// creator makes (`nros-node/src/executor/action.rs`); the declarative path
232/// reaches the same type-erased sink and would otherwise register none of them.
233/// `A::register_protocol_types()` covers the fixed `action_msgs` types the
234/// cancel/status plumbing serializes, exactly as the imperative path does.
235#[inline]
236fn register_declared_action<A: RosAction>() -> NodeResult<()>
237where
238    A::Goal: nros_node::rmw_type_registry::MessageForRmw,
239    A::Result: nros_node::rmw_type_registry::MessageForRmw,
240    A::Feedback: nros_node::rmw_type_registry::MessageForRmw,
241    A::SendGoalRequest: nros_node::rmw_type_registry::MessageForRmw,
242    A::SendGoalResponse: nros_node::rmw_type_registry::MessageForRmw,
243    A::GetResultRequest: nros_node::rmw_type_registry::MessageForRmw,
244    A::GetResultResponse: nros_node::rmw_type_registry::MessageForRmw,
245    A::FeedbackMessage: nros_node::rmw_type_registry::MessageForRmw,
246{
247    register_declared_type::<A::Goal>()?;
248    register_declared_type::<A::Result>()?;
249    register_declared_type::<A::Feedback>()?;
250    register_declared_type::<A::SendGoalRequest>()?;
251    register_declared_type::<A::SendGoalResponse>()?;
252    register_declared_type::<A::GetResultRequest>()?;
253    register_declared_type::<A::GetResultResponse>()?;
254    register_declared_type::<A::FeedbackMessage>()?;
255    A::register_protocol_types().map_err(|()| NodeDeclError::Runtime)
256}
257
258/// phase-391 W5-endgame step 2c (issue 0857) — a component class's declared
259/// upper bounds, PER ENTITY KIND, for sizing its cell registries at compile
260/// time.
261///
262/// The runtime's per-class cell storage is static, so its registries pay
263/// their CAPACITY whether or not entities fill it — and one publisher slot
264/// costs ~1.35 KiB (the loan arena rides inside). The default is the
265/// `NROS_RUNTIME_MAX_CELL_ENTITIES` knob per kind, which always works;
266/// a class that declares its real bounds pays exactly what it uses.
267/// Declaring FEWER than `register()` creates is a loud registration error
268/// (registry full), never a silent drop.
269///
270/// Public and non-generic on purpose (the `ExecutorSizing` rule): the const
271/// generics this feeds stay behind the `nros::node!` macro emission, so no
272/// other language ever sees them.
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub struct EntityBounds {
275    /// Publishers this class creates (each slot ~1.35 KiB — the big one).
276    pub publishers: usize,
277    /// Service servers (sizes the trampoline-context slab, not a registry).
278    pub service_servers: usize,
279    /// Service clients.
280    pub service_clients: usize,
281    /// Action clients.
282    pub action_clients: usize,
283    /// Action servers.
284    pub action_servers: usize,
285    /// Issue 2075 — timers the component can START, RESET or CANCEL by id
286    /// ([`CallbackCtx::reset_timer`] / [`TickCtx::cancel_timer`]). Sizes a
287    /// small id-to-handle table (16 B a row), not the timers themselves: a
288    /// timer past this bound still registers and fires, it just cannot be
289    /// controlled, and naming it answers [`NodeDeclError::UnknownTimer`].
290    pub timers: usize,
291}
292
293impl EntityBounds {
294    /// The knob-capped default (`NROS_RUNTIME_MAX_CELL_ENTITIES` per kind).
295    pub const fn knob_caps() -> Self {
296        Self {
297            publishers: crate::config::MAX_CELL_ENTITIES,
298            service_servers: crate::config::MAX_CELL_ENTITIES,
299            service_clients: crate::config::MAX_CELL_ENTITIES,
300            action_clients: crate::config::MAX_CELL_ENTITIES,
301            action_servers: crate::config::MAX_CELL_ENTITIES,
302            timers: crate::config::MAX_CELL_ENTITIES,
303        }
304    }
305
306    /// Exact bounds, spelled positionally:
307    /// `(publishers, service_servers, service_clients, action_clients, action_servers)`.
308    ///
309    /// Controllable timers default to 0 here (issue 2075), so a class that
310    /// states exact bounds pays nothing for the timer table it did not ask
311    /// for. Its timers still run; add [`with_timers`](Self::with_timers) to
312    /// reset or cancel them by id.
313    pub const fn exact(
314        publishers: usize,
315        service_servers: usize,
316        service_clients: usize,
317        action_clients: usize,
318        action_servers: usize,
319    ) -> Self {
320        Self {
321            publishers,
322            service_servers,
323            service_clients,
324            action_clients,
325            action_servers,
326            timers: 0,
327        }
328    }
329
330    /// Issue 2075 — these bounds with `timers` controllable timers: how many
331    /// declared timers the component can start, reset or cancel by id.
332    ///
333    /// ```ignore
334    /// const ENTITY_BOUNDS: nros::EntityBounds =
335    ///     nros::EntityBounds::exact(1, 0, 0, 0, 0).with_timers(2);
336    /// ```
337    pub const fn with_timers(self, timers: usize) -> Self {
338        Self { timers, ..self }
339    }
340}
341
342/// Rust component entry point.
343#[cfg(feature = "rmw-cffi")]
344pub trait Component {
345    /// Source component name used in metadata and diagnostics.
346    const NAME: &'static str;
347
348    /// Phase 216.A.3 — declares which dispatch strategy this Node
349    /// requires from the runtime. Defaults to
350    /// [`crate::DispatchStrategy::Inline`] so every existing component
351    /// keeps compiling without source change; the substrate (Phase
352    /// 216.A.2) and `nros check` (Phase 216.D.1) consume it to
353    /// pick / validate the board-side dispatch path.
354    const DISPATCH: crate::DispatchStrategy = crate::DispatchStrategy::Inline;
355
356    /// phase-391 W5-endgame step 2c — this class's per-kind entity bounds,
357    /// sizing its static cell registries. Defaults to the knob caps so every
358    /// existing component keeps compiling; declare [`EntityBounds::exact`]
359    /// to stop paying for capacity `register()` never fills.
360    const ENTITY_BOUNDS: EntityBounds = EntityBounds::knob_caps();
361
362    /// Declare nodes, entities, callbacks, params, and optional effects.
363    fn register(context: &mut NodeContext<'_>) -> NodeResult<()>;
364}
365
366pub use nros_node::{IntoNodeOptions, NodeOptions};
367
368/// Runtime callback event delivered to an executable Node.
369///
370/// The value carries the source callback name declared by the component, but
371/// does not expose the generated/internal callback ID type to product code.
372#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
373pub struct Callback<'a> {
374    id: CallbackId<'a>,
375}
376
377impl<'a> Callback<'a> {
378    /// Borrow the source callback name.
379    pub const fn as_str(self) -> &'a str {
380        self.id.as_str()
381    }
382
383    /// Return true when this callback matches `name`.
384    pub fn is_named(self, name: &str) -> bool {
385        self.as_str() == name
386    }
387
388    /// Build a callback event from the internal/generated callback ID.
389    #[doc(hidden)]
390    pub const fn __from_id(id: CallbackId<'a>) -> Self {
391        Self { id }
392    }
393}
394
395/// Declaration sink implemented by metadata recorders and generated runtimes.
396pub trait NodeRuntime {
397    /// Declare a component node.
398    fn create_node(&mut self, id: NodeId<'_>, options: NodeOptions<'_>) -> NodeResult<()>;
399
400    /// Declare a publisher, subscription, timer, service, action, or parameter.
401    fn create_entity(&mut self, metadata: EntityMetadata) -> NodeResult<()>;
402
403    /// Add optional callback effect metadata.
404    fn record_callback_effect(
405        &mut self,
406        callback_id: CallbackId<'_>,
407        kind: CallbackEffectKind,
408        entity_id: EntityId<'_>,
409    ) -> NodeResult<()>;
410}
411
412impl<const MAX_NODES: usize, const MAX_ENTITIES: usize, const MAX_CALLBACKS: usize> NodeRuntime
413    for MetadataRecorder<MAX_NODES, MAX_ENTITIES, MAX_CALLBACKS>
414{
415    fn create_node(&mut self, id: NodeId<'_>, options: NodeOptions<'_>) -> NodeResult<()> {
416        self.push_node(id, options.name, options.namespace, options.domain_id)?;
417        Ok(())
418    }
419
420    fn create_entity(&mut self, mut metadata: EntityMetadata) -> NodeResult<()> {
421        // phase-457 W5 (issue 1522) — state this subscription's registration
422        // path, because on THIS road nothing will observe it.
423        //
424        // `Executor::open_subscription` reports `in_place_capable` to
425        // `registration_observer`, which is how the C/C++ probes get theirs.
426        // The Rust probe (`record_node_metadata::<C>`) runs `register()`
427        // against this recorder and opens no executor, so that seam never
428        // fires and every Rust endpoint's row refused — leaving every consumer
429        // on the receive-region budget for a fact the declaration determines.
430        //
431        // This is the ONE `NodeRuntime` seam a Rust declaration crosses; the
432        // C/C++ adapters reach the recorder through `push_entity` and are
433        // untouched, so their observed fact still wins its own way.
434        //
435        // Not a second opinion: `declared_subscription_shape` is the same call
436        // the declarative registrar branches on, and its
437        // `in_place_capable()` is the same expression the entry point writes
438        // into its `SubscriptionRequest`. Gated by
439        // `check-declared-subscription-shape`.
440        if metadata.kind == EntityKind::Subscription && metadata.in_place_capable.is_none() {
441            metadata.in_place_capable =
442                Some(metadata.declared_subscription_shape().in_place_capable());
443        }
444        self.push_entity(metadata)?;
445        Ok(())
446    }
447
448    fn record_callback_effect(
449        &mut self,
450        callback_id: CallbackId<'_>,
451        kind: CallbackEffectKind,
452        entity_id: EntityId<'_>,
453    ) -> NodeResult<()> {
454        self.push_callback_effect(callback_id, kind, entity_id)?;
455        Ok(())
456    }
457}
458
459/// phase-483 W3 — where a component's declarations go while its `register`
460/// runs: the live component runtime (`ExecutorSink`, which owns the
461/// component's cell) or the metadata recorder (the host probe). Both receive
462/// the executor explicitly, because the [`crate::Node`] a component holds
463/// borrows that same executor; a sink that held it too would alias it.
464#[cfg(feature = "rmw-cffi")]
465pub(crate) trait FrameSink {
466    /// Create the executor node a component asked for and return its id.
467    fn create_node(
468        &mut self,
469        executor: &mut crate::Executor<'static>,
470        id: NodeId<'_>,
471        options: NodeOptions<'_>,
472    ) -> NodeResult<nros_node::executor::NodeId>;
473
474    /// Declare one entity.
475    fn create_entity(
476        &mut self,
477        executor: &mut crate::Executor<'static>,
478        metadata: EntityMetadata,
479    ) -> NodeResult<()>;
480
481    /// Record one callback effect.
482    fn record_callback_effect(
483        &mut self,
484        executor: &mut crate::Executor<'static>,
485        callback_id: CallbackId<'_>,
486        kind: CallbackEffectKind,
487        entity_id: EntityId<'_>,
488    ) -> NodeResult<()>;
489}
490
491/// The most nodes one component registration may create. A component
492/// declares one in every shipped shape.
493#[cfg(feature = "rmw-cffi")]
494const MAX_FRAME_NODES: usize = 4;
495
496#[cfg(feature = "rmw-cffi")]
497struct FrameNode {
498    exec_id: nros_node::executor::NodeId,
499    stable: MetadataString,
500    group: Option<MetadataString>,
501}
502
503/// phase-483 W3 — one component registration in progress. Installed on the
504/// executor (`Executor::__set_component_frame`) for exactly the lifetime of
505/// the [`NodeContext`] that owns it, which is how a [`DeclarativeNode`]
506/// method on a plain [`crate::Node`] finds the sink.
507#[cfg(feature = "rmw-cffi")]
508pub(crate) struct ComponentFrame<'a> {
509    pub(crate) sink: &'a mut dyn FrameSink,
510    nodes: Vec<FrameNode, MAX_FRAME_NODES>,
511}
512
513#[cfg(feature = "rmw-cffi")]
514impl<'a> ComponentFrame<'a> {
515    pub(crate) fn new(sink: &'a mut dyn FrameSink) -> Self {
516        Self {
517            sink,
518            nodes: Vec::new(),
519        }
520    }
521
522    fn push(&mut self, exec_id: nros_node::executor::NodeId, stable: &str) -> NodeResult<()> {
523        self.nodes
524            .push(FrameNode {
525                exec_id,
526                stable: copy_str(stable)?,
527                group: None,
528            })
529            .map_err(|_| NodeDeclError::Metadata(NodeMetadataError::Capacity))
530    }
531
532    fn node(&self, exec_id: nros_node::executor::NodeId) -> NodeResult<&FrameNode> {
533        self.nodes
534            .iter()
535            .find(|n| n.exec_id == exec_id)
536            .ok_or(NodeDeclError::Metadata(NodeMetadataError::UnknownNode))
537    }
538
539    fn stable_id_of(&self, exec_id: nros_node::executor::NodeId) -> NodeResult<NodeId<'_>> {
540        Ok(NodeId::new(self.node(exec_id)?.stable.as_str()))
541    }
542
543    fn group_of(&self, exec_id: nros_node::executor::NodeId) -> Option<MetadataString> {
544        self.node(exec_id).ok().and_then(|n| n.group.clone())
545    }
546
547    fn set_group(&mut self, stable: &str, group: Option<MetadataString>) -> NodeResult<()> {
548        let node = self
549            .nodes
550            .iter_mut()
551            .find(|n| n.stable.as_str() == stable)
552            .ok_or(NodeDeclError::Metadata(NodeMetadataError::UnknownNode))?;
553        node.group = group;
554        Ok(())
555    }
556}
557
558/// The registration frame installed on `executor`, or `Runtime` when no
559/// component registration is in progress.
560///
561/// The `'static` is a lie told to the borrow checker and kept honest by
562/// [`NodeContext`]: the frame is installed when the context is built and
563/// cleared when it drops, and every caller here runs inside that window and
564/// drops the reference before returning.
565#[cfg(feature = "rmw-cffi")]
566fn frame_mut(
567    executor: &crate::Executor<'static>,
568) -> NodeResult<&'static mut ComponentFrame<'static>> {
569    let ptr = executor.__component_frame().ok_or(NodeDeclError::Runtime)?;
570    // SAFETY: installed by `NodeContext::new` from a `&mut ComponentFrame`
571    // that outlives the context, and cleared by its `Drop`; the frame is
572    // separate memory from the executor, so this does not alias the
573    // executor borrow the caller also holds.
574    Ok(unsafe { &mut *ptr.cast::<ComponentFrame<'static>>().as_ptr() })
575}
576
577/// The metadata recorder as a registration sink — the host probe's road.
578///
579/// It records every declaration and ALSO creates the node on the executor
580/// the probe opened on the `metadata` backend, so the component's `register`
581/// gets a real [`crate::Node`] exactly as it does on a board.
582#[cfg(feature = "rmw-cffi")]
583pub(crate) struct RecordSink<'r> {
584    pub(crate) recorder: &'r mut dyn NodeRuntime,
585}
586
587#[cfg(feature = "rmw-cffi")]
588impl FrameSink for RecordSink<'_> {
589    fn create_node(
590        &mut self,
591        executor: &mut crate::Executor<'static>,
592        id: NodeId<'_>,
593        options: NodeOptions<'_>,
594    ) -> NodeResult<nros_node::executor::NodeId> {
595        self.recorder.create_node(id, options)?;
596        executor
597            .node_builder(options.name)
598            .namespace(options.namespace)
599            .domain_id(options.domain_id)
600            .build()
601            .map_err(|_| NodeDeclError::Runtime)
602    }
603
604    fn create_entity(
605        &mut self,
606        _executor: &mut crate::Executor<'static>,
607        metadata: EntityMetadata,
608    ) -> NodeResult<()> {
609        self.recorder.create_entity(metadata)
610    }
611
612    fn record_callback_effect(
613        &mut self,
614        _executor: &mut crate::Executor<'static>,
615        callback_id: CallbackId<'_>,
616        kind: CallbackEffectKind,
617        entity_id: EntityId<'_>,
618    ) -> NodeResult<()> {
619        self.recorder
620            .record_callback_effect(callback_id, kind, entity_id)
621    }
622}
623
624/// Node declaration context — what a component's
625/// [`register`](Component::register) receives.
626///
627/// phase-483 W3: it creates REAL nodes. [`create_node`](Self::create_node)
628/// returns a [`crate::Node`], the same type `Executor::create_node` gives a
629/// standalone program, so a component's node code and a program's node code
630/// are one code. The component-only declarations are the
631/// [`DeclarativeNode`] methods on that node.
632#[cfg(feature = "rmw-cffi")]
633pub struct NodeContext<'a> {
634    component_name: &'static str,
635    executor: &'a mut crate::Executor<'static>,
636    /// Phase 264 W4a — this node instance's parameters, the COMPILE-BAKED initial
637    /// values from the launch `<param name=… value=…/>` entries (`nros::main!`
638    /// bakes them + threads them through `install_node_typed_with_params`). Empty
639    /// for the metadata-recorder / no-launch paths. A node reads them in
640    /// `register()` via [`param`](Self::param) and stashes the typed value on its
641    /// `State` (RFC-0004 §10 — baked initials; runtime reconfig is W4b).
642    params: &'a [(&'a str, &'a str)],
643}
644
645#[cfg(feature = "rmw-cffi")]
646impl<'a> NodeContext<'a> {
647    /// Build a context over `executor`, installing `frame` on it until this
648    /// context drops.
649    pub(crate) fn new(
650        component_name: &'static str,
651        executor: &'a mut crate::Executor<'static>,
652        frame: &'a mut ComponentFrame<'_>,
653    ) -> Self {
654        executor.__set_component_frame(Some(core::ptr::NonNull::from(frame).cast()));
655        Self {
656            component_name,
657            executor,
658            params: &[],
659        }
660    }
661
662    /// Phase 264 W4a — seed this node instance's baked launch parameters (called by
663    /// `install_node_typed_with_params` before `Component::register`).
664    pub fn set_params(&mut self, params: &'a [(&'a str, &'a str)]) {
665        self.params = params;
666    }
667
668    /// Phase 264 W4a — the baked initial value of launch parameter `name`, or
669    /// `None` if the launch declared no `<param name="…"/>` for this node
670    /// instance. Read in `register()` and parse/stash on `State` (RFC-0004 §10).
671    pub fn param(&self, name: &str) -> Option<&'a str> {
672        self.params
673            .iter()
674            .find(|(k, _)| *k == name)
675            .map(|(_, v)| *v)
676    }
677
678    /// Source component name.
679    pub const fn component_name(&self) -> &'static str {
680        self.component_name
681    }
682
683    /// Create a node with an explicit stable node ID.
684    ///
685    /// Generated/internal form; product code should use
686    /// [`create_node`](Self::create_node).
687    #[doc(hidden)]
688    pub fn create_node_with_id(
689        &mut self,
690        id: NodeId<'_>,
691        options: NodeOptions<'_>,
692    ) -> NodeResult<crate::Node<'_, 'static>> {
693        let frame = frame_mut(self.executor)?;
694        let exec_id = frame.sink.create_node(self.executor, id, options)?;
695        frame.push(exec_id, id.as_str())?;
696        Ok(self.executor.node_mut(exec_id))
697    }
698
699    /// Create this component's node — rclrs's `executor.create_node(options)`.
700    ///
701    /// The node's name is the launch file's when it names one, otherwise
702    /// `options.name`, which is also the component-stable node id the
703    /// declarations are keyed on.
704    ///
705    /// phase-515 R2 -- takes rclrs's `impl IntoNodeOptions`, so
706    /// `context.create_node("talker")` reads as rclrs does. Unlike the
707    /// executor's, a component's options are applied as written.
708    pub fn create_node<'o>(
709        &mut self,
710        options: impl IntoNodeOptions<'o>,
711    ) -> NodeResult<crate::Node<'_, 'static>> {
712        let options = options.into_node_options();
713        self.create_node_with_id(NodeId::new(options.name), options)
714    }
715
716    /// Record optional effects for a callback not tied to a node.
717    #[doc(hidden)]
718    pub fn callback<'id>(&mut self, id: CallbackId<'id>) -> CallbackEffects<'_, 'id> {
719        CallbackEffects {
720            executor: self.executor,
721            id,
722        }
723    }
724}
725
726#[cfg(feature = "rmw-cffi")]
727impl Drop for NodeContext<'_> {
728    fn drop(&mut self) {
729        self.executor.__set_component_frame(None);
730    }
731}
732
733/// phase-483 W3 — the DECLARATIVE entity constructors, on the one node type.
734///
735/// A component's `register` creates its node with
736/// [`NodeContext::create_node`] and gets a [`crate::Node`], the same type a
737/// standalone program gets from `Executor::create_node`. Everything rclrs's
738/// node has is an inherent method there. The constructors here are the RTOS
739/// extension a component adds on top: each one DECLARES an entity — a stable
740/// id, a callback NAME, effects — to the component runtime, which owns the
741/// entity in the component's static cell and dispatches its callback by name
742/// to [`ExecutableNode::on_callback`]. That is what framework dispatch
743/// (RTIC/Embassy) and the metadata probe need, and what a closure cannot give
744/// them.
745///
746/// Only callable inside a component's `register`: outside one there is no
747/// component to declare into, and every method answers
748/// [`NodeDeclError::Runtime`].
749///
750/// The explicit-id forms that would collide with an rclrs-named inherent
751/// method on `Node` are spelled `declare_*` (`declare_publisher`,
752/// `declare_subscription`, `declare_timer`, …).
753#[cfg(feature = "rmw-cffi")]
754pub trait DeclarativeNode {
755    /// The component-stable id of this node. Not a user API.
756    #[doc(hidden)]
757    fn __node_id(&mut self) -> NodeResult<NodeId<'static>>;
758
759    /// Hand one entity declaration to the component runtime. Not a user API.
760    #[doc(hidden)]
761    fn __declare_entity(&mut self, metadata: EntityMetadata) -> NodeResult<()>;
762
763    /// The executor the node borrows. Not a user API.
764    #[doc(hidden)]
765    fn __executor_mut(&mut self) -> &mut crate::Executor<'static>;
766
767    /// Set the sticky callback-group label applied to every entity
768    /// declared after this call (until changed again). The group is the
769    /// symbolic name the node author exposes; `system.toml` maps it to a
770    /// scheduling tier (RFC-0015). Entities declared while no group is set
771    /// remain unlabeled (wildcard-eligible).
772    #[track_caller]
773    fn callback_group(&mut self, group: &str) -> NodeResult<&mut Self>
774    where
775        Self: Sized,
776    {
777        let group = copy_str(group)?;
778        let executor = self.__executor_mut() as *mut crate::Executor<'static>;
779        // SAFETY: `executor` is the node's own borrow, live for this call.
780        let frame = frame_mut(unsafe { &*executor })?;
781        let node = self.__node_id()?;
782        frame.set_group(node.as_str(), Some(group))?;
783        Ok(self)
784    }
785
786    /// Declare a publisher with default QoS. Stable publisher ID is required.
787    #[track_caller]
788    #[doc(hidden)]
789    fn declare_publisher<'entity, M: MessageForRmw>(
790        &mut self,
791        id: EntityId<'entity>,
792        topic: &str,
793    ) -> NodeResult<NodePublisher<'entity, M>> {
794        self.declare_publisher_with_qos::<M>(id, topic, QoSProfile::default())
795    }
796
797    /// Declare a publisher using `topic` as the stable entity ID.
798    ///
799    /// Use the explicit [`declare_publisher`](Self::declare_publisher) form when
800    /// a node declares more than one publisher on the same topic or needs a
801    /// stable metadata ID that differs from the ROS topic name.
802    #[track_caller]
803    fn create_publisher_for_topic<'entity, M: MessageForRmw>(
804        &mut self,
805        topic: &'entity str,
806    ) -> NodeResult<NodePublisher<'entity, M>> {
807        self.create_publisher_for_topic_with_qos::<M>(topic, QoSProfile::default())
808    }
809
810    /// Declare a publisher with explicit QoS, using `topic` as the stable entity ID.
811    #[track_caller]
812    fn create_publisher_for_topic_with_qos<'entity, M: MessageForRmw>(
813        &mut self,
814        topic: &'entity str,
815        qos: QoSProfile,
816    ) -> NodeResult<NodePublisher<'entity, M>> {
817        self.declare_publisher_with_qos::<M>(EntityId::new(topic), topic, qos)
818    }
819
820    /// Declare a publisher with explicit QoS.
821    #[track_caller]
822    #[doc(hidden)]
823    fn declare_publisher_with_qos<'entity, M: MessageForRmw>(
824        &mut self,
825        id: EntityId<'entity>,
826        topic: &str,
827        qos: QoSProfile,
828    ) -> NodeResult<NodePublisher<'entity, M>> {
829        register_declared_type::<M>()?;
830        let mut metadata = entity_metadata(EntityMetadataSpec {
831            id,
832            node_id: self.__node_id()?,
833            kind: EntityKind::Publisher,
834            source_name: topic,
835            // issue 0413 — `MessageForRmw` can pull `schema::Message` into scope,
836            // which also has a `TYPE_NAME`; disambiguate to the ROS-facing one.
837            type_name: <M as RosMessage>::TYPE_NAME,
838            type_hash: <M as RosMessage>::TYPE_HASH,
839            qos,
840        })?;
841        metadata.source = SourceLocationMetadata::caller()?;
842        self.__declare_entity(metadata)?;
843        Ok(NodePublisher::new(id))
844    }
845
846    /// Declare a subscription. Stable subscription and callback IDs are required.
847    #[track_caller]
848    #[doc(hidden)]
849    fn declare_subscription<'entity, 'callback, M: MessageForRmw>(
850        &mut self,
851        id: EntityId<'entity>,
852        callback_id: CallbackId<'callback>,
853        topic: &str,
854    ) -> NodeResult<NodeSubscription<'entity, M>> {
855        self.declare_subscription_with_qos::<M>(id, callback_id, topic, QoSProfile::default())
856    }
857
858    /// Declare a subscription using `callback_id` as the stable entity ID.
859    ///
860    /// Generated/internal form; product code should use
861    /// [`create_subscription_for_callback_name`](Self::create_subscription_for_callback_name).
862    #[track_caller]
863    #[doc(hidden)]
864    fn create_subscription_for_callback<'callback, M: MessageForRmw>(
865        &mut self,
866        callback_id: CallbackId<'callback>,
867        topic: &str,
868    ) -> NodeResult<NodeSubscription<'callback, M>> {
869        self.create_subscription_for_callback_with_qos::<M>(
870            callback_id,
871            topic,
872            QoSProfile::default(),
873        )
874    }
875
876    /// Declare a subscription using `callback_name` as the source callback
877    /// name and synthesized entity ID.
878    #[track_caller]
879    fn create_subscription_for_callback_name<'callback, M: MessageForRmw>(
880        &mut self,
881        callback_name: &'callback str,
882        topic: &str,
883    ) -> NodeResult<NodeSubscription<'callback, M>> {
884        self.create_subscription_for_callback::<M>(CallbackId::new(callback_name), topic)
885    }
886
887    /// Phase 250 (Wave 2b) — declare a subscription with E2E message-integrity
888    /// validation enabled (the declarative `.safety()` opt-in). Identical to
889    /// [`create_subscription_for_callback_name`](Self::create_subscription_for_callback_name)
890    /// but flags the entity so the runtime registers it via
891    /// `create_generic_subscription_with_integrity`; the callback then reads
892    /// [`CallbackCtx::integrity`](CallbackCtx::integrity) alongside the message.
893    /// The config-driven `[safety]` axis (Wave 4 codegen) emits this call; it is
894    /// also usable by hand. Ungated — when `safety-e2e` is off the flag is simply
895    /// ignored and the subscription registers as a basic one.
896    #[track_caller]
897    fn create_subscription_for_callback_name_with_safety<'callback, M: MessageForRmw>(
898        &mut self,
899        callback_name: &'callback str,
900        topic: &str,
901    ) -> NodeResult<NodeSubscription<'callback, M>> {
902        let callback_id = CallbackId::new(callback_name);
903        let id = EntityId::new(callback_id.as_str());
904        register_declared_type::<M>()?;
905        let mut metadata = entity_metadata(EntityMetadataSpec {
906            id,
907            node_id: self.__node_id()?,
908            kind: EntityKind::Subscription,
909            source_name: topic,
910            // issue 0413 — `MessageForRmw` can pull `schema::Message` into scope,
911            // which also has a `TYPE_NAME`; disambiguate to the ROS-facing one.
912            type_name: <M as RosMessage>::TYPE_NAME,
913            type_hash: <M as RosMessage>::TYPE_HASH,
914            qos: QoSProfile::default(),
915        })?;
916        metadata.callback_id = Some(copy_str(callback_id.as_str())?);
917        metadata.callback_source = SourceLocationMetadata::caller()?;
918        metadata.source = metadata.callback_source.clone();
919        metadata.safety = true;
920        // Issue 2208 -- the backend's `rx_buffer_hint`, from the type.
921        metadata.rx_bound = <M as RosMessage>::MAX_SERIALIZED_BOUND;
922        self.__declare_entity(metadata)?;
923        Ok(NodeSubscription::new(id))
924    }
925
926    /// Declare a subscription with explicit QoS, using `callback_id` as the stable entity ID.
927    #[track_caller]
928    #[doc(hidden)]
929    fn create_subscription_for_callback_with_qos<'callback, M: MessageForRmw>(
930        &mut self,
931        callback_id: CallbackId<'callback>,
932        topic: &str,
933        qos: QoSProfile,
934    ) -> NodeResult<NodeSubscription<'callback, M>> {
935        self.declare_subscription_with_qos::<M>(
936            EntityId::new(callback_id.as_str()),
937            callback_id,
938            topic,
939            qos,
940        )
941    }
942
943    /// Declare a subscription using `topic` as both the stable entity ID and callback ID.
944    #[track_caller]
945    fn create_subscription_for_topic<'entity, M: MessageForRmw>(
946        &mut self,
947        topic: &'entity str,
948    ) -> NodeResult<NodeSubscription<'entity, M>> {
949        self.create_subscription_for_topic_with_qos::<M>(topic, QoSProfile::default())
950    }
951
952    /// Declare a subscription with explicit QoS, using `topic` as both IDs.
953    #[track_caller]
954    fn create_subscription_for_topic_with_qos<'entity, M: MessageForRmw>(
955        &mut self,
956        topic: &'entity str,
957        qos: QoSProfile,
958    ) -> NodeResult<NodeSubscription<'entity, M>> {
959        self.declare_subscription_with_qos::<M>(
960            EntityId::new(topic),
961            CallbackId::new(topic),
962            topic,
963            qos,
964        )
965    }
966
967    /// Declare a subscription with explicit QoS.
968    #[track_caller]
969    #[doc(hidden)]
970    fn declare_subscription_with_qos<'entity, 'callback, M: MessageForRmw>(
971        &mut self,
972        id: EntityId<'entity>,
973        callback_id: CallbackId<'callback>,
974        topic: &str,
975        qos: QoSProfile,
976    ) -> NodeResult<NodeSubscription<'entity, M>> {
977        // Phase 380 W4 — a subscription whose receive buffer provably cannot
978        // hold its own message type fails the BUILD, not the field.
979        //
980        // Without this the sample is received, ACKed, and then dropped, and
981        // `report_dropped_take` can only say "raise the knob" because nothing
982        // knows what value would have worked (issues 0757, 0776). The number is
983        // known at compile time for any BOUNDED type, so the check costs
984        // nothing at runtime and cannot be forgotten at a call site.
985        //
986        // `bound_fits`, not `buffer_fits`: an UNBOUNDED type passes, because
987        // there is nothing to prove and no finite buffer fits a `String` — the
988        // other predicate would refuse the most common message in ROS. Both
989        // encodings are checked and the larger taken; the peer chooses the
990        // encoding at runtime, so sizing from XCDR1 alone is a trap.
991        // Only where `MessageForRmw` guarantees a schema. The other arm accepts a
992        // hand-written `RosMessage` with none (see `rmw_type_registry`), and
993        // requiring one there would make codegen mandatory for a user's own
994        // message type — too large a price for a build assertion. Backends that
995        // register type descriptors already demand the schema, so this is free
996        // there and absent elsewhere; `size::bound_fits` stays public so a
997        // caller can assert it explicitly.
998        const {
999            assert!(
1000                nros_node::rmw_type_registry::subscription_buffer_ok::<M>(),
1001                "this message type's maximum serialized size exceeds \
1002                 NROS_SUBSCRIPTION_BUFFER_SIZE — every sample would be received, \
1003                 ACKed and then DROPPED. Raise the knob to at least the type's \
1004                 bound (`<M as nros_serdes::schema::Message>::MAX_SERIALIZED_SIZE_XCDR2`)."
1005            )
1006        }
1007        register_declared_type::<M>()?;
1008        let mut metadata = entity_metadata(EntityMetadataSpec {
1009            id,
1010            node_id: self.__node_id()?,
1011            kind: EntityKind::Subscription,
1012            source_name: topic,
1013            // issue 0413 — `MessageForRmw` can pull `schema::Message` into scope,
1014            // which also has a `TYPE_NAME`; disambiguate to the ROS-facing one.
1015            type_name: <M as RosMessage>::TYPE_NAME,
1016            type_hash: <M as RosMessage>::TYPE_HASH,
1017            qos,
1018        })?;
1019        metadata.callback_id = Some(copy_str(callback_id.as_str())?);
1020        metadata.callback_source = SourceLocationMetadata::caller()?;
1021        metadata.source = metadata.callback_source.clone();
1022        // Issue 2208 -- the backend's `rx_buffer_hint`, from the type. The
1023        // runtime registers this subscription type-erased, so this is the
1024        // last place `M` is known.
1025        metadata.rx_bound = <M as RosMessage>::MAX_SERIALIZED_BOUND;
1026        self.__declare_entity(metadata)?;
1027        Ok(NodeSubscription::new(id))
1028    }
1029
1030    /// Declare a subscription whose stable entity and callback IDs are
1031    /// both synthesized from the topic literal, returning a
1032    /// [`SubscriptionTag`] the Node author stores on `Self::State` and
1033    /// matches against the `Callback<'_>` delivered to
1034    /// [`ExecutableNode::on_callback`].
1035    ///
1036    /// Use this on the Phase 216.A Deferred Node path where the Node
1037    /// author does not need to invent a separate stable entity ID — the
1038    /// topic literal becomes both the entity ID and the callback ID,
1039    /// and the returned tag preserves that identifier for compile-time
1040    /// `state.sub_chatter == cb` matches in `on_callback`.
1041    #[track_caller]
1042    fn create_subscription_static<M: MessageForRmw>(
1043        &mut self,
1044        topic: &'static str,
1045    ) -> NodeResult<SubscriptionTag> {
1046        let id = EntityId::new(topic);
1047        let callback_id = CallbackId::new(topic);
1048        register_declared_type::<M>()?;
1049        let mut metadata = entity_metadata(EntityMetadataSpec {
1050            id,
1051            node_id: self.__node_id()?,
1052            kind: EntityKind::Subscription,
1053            source_name: topic,
1054            // issue 0413 — `MessageForRmw` can pull `schema::Message` into scope,
1055            // which also has a `TYPE_NAME`; disambiguate to the ROS-facing one.
1056            type_name: <M as RosMessage>::TYPE_NAME,
1057            type_hash: <M as RosMessage>::TYPE_HASH,
1058            qos: QoSProfile::default(),
1059        })?;
1060        metadata.callback_id = Some(copy_str(callback_id.as_str())?);
1061        metadata.callback_source = SourceLocationMetadata::caller()?;
1062        metadata.source = metadata.callback_source.clone();
1063        // Issue 2208 -- the backend's `rx_buffer_hint`, from the type.
1064        metadata.rx_bound = <M as RosMessage>::MAX_SERIALIZED_BOUND;
1065        self.__declare_entity(metadata)?;
1066        Ok(SubscriptionTag::new(topic))
1067    }
1068
1069    /// Declare a timer. Stable timer and callback IDs are required.
1070    #[track_caller]
1071    #[doc(hidden)]
1072    fn declare_timer<'entity, 'callback>(
1073        &mut self,
1074        id: EntityId<'entity>,
1075        callback_id: CallbackId<'callback>,
1076        period: TimerDuration,
1077    ) -> NodeResult<NodeTimer<'entity>> {
1078        // phase-430 W4 — the wall spelling IS the `Steady` clock, so it is the
1079        // same declaration with the axis defaulted rather than a second body.
1080        // `#[track_caller]` on both hops keeps `SourceLocationMetadata::caller`
1081        // pointing at the component, not at this line.
1082        self.declare_timer_on_clock(id, callback_id, period, TimerClockSource::Steady)
1083    }
1084
1085    /// Declare a timer using `callback_id` as the stable timer entity ID.
1086    #[track_caller]
1087    #[doc(hidden)]
1088    fn create_timer_for_callback<'callback>(
1089        &mut self,
1090        callback_id: CallbackId<'callback>,
1091        period: TimerDuration,
1092    ) -> NodeResult<NodeTimer<'callback>> {
1093        self.declare_timer(EntityId::new(callback_id.as_str()), callback_id, period)
1094    }
1095
1096    /// Declare a timer using `callback_name` as the source callback name and
1097    /// synthesized entity ID.
1098    #[track_caller]
1099    fn create_timer_for_callback_name<'callback>(
1100        &mut self,
1101        callback_name: &'callback str,
1102        period: TimerDuration,
1103    ) -> NodeResult<NodeTimer<'callback>> {
1104        self.create_timer_for_callback(CallbackId::new(callback_name), period)
1105    }
1106
1107    /// Declare a timer on a chosen CLOCK — phase-430 W4, the declarative
1108    /// spelling of [`Executor::register_timer_on_clock`](crate::Executor::register_timer_on_clock)
1109    /// and the counterpart of C++'s `create_timer(clock, period, cb)`.
1110    ///
1111    /// [`create_timer`](Self::create_timer) is this with
1112    /// [`TimerClockSource::Steady`], which is the WALL case and is what every
1113    /// timer declared before W4 gets: the runtime registers it exactly as it
1114    /// always did. A [`TimerClockSource::Ros`] timer instead follows `/clock` —
1115    /// it stops while the simulator is paused and tracks a bag's replay rate —
1116    /// and with no `/clock` source installed it reads system time, the same
1117    /// fallback `rclcpp::Clock` has.
1118    #[track_caller]
1119    #[doc(hidden)]
1120    fn declare_timer_on_clock<'entity, 'callback>(
1121        &mut self,
1122        id: EntityId<'entity>,
1123        callback_id: CallbackId<'callback>,
1124        period: TimerDuration,
1125        clock: TimerClockSource,
1126    ) -> NodeResult<NodeTimer<'entity>> {
1127        self.declare_timer_with_autostart(id, callback_id, period, clock, true)
1128    }
1129
1130    /// The ONE timer declaration — issue 2038. Every other timer verb on this
1131    /// trait is this with `autostart = true` (and, for the wall spellings,
1132    /// [`TimerClockSource::Steady`]).
1133    ///
1134    /// `autostart == false` declares the timer STOPPED: the runtime registers
1135    /// it cancelled (rcl's `rcl_timer_init2(..., autostart = false)`, the same
1136    /// executor call `Node::create_timer_with_info` makes), so it does not fire
1137    /// until it is reset.
1138    #[track_caller]
1139    #[doc(hidden)]
1140    fn declare_timer_with_autostart<'entity, 'callback>(
1141        &mut self,
1142        id: EntityId<'entity>,
1143        callback_id: CallbackId<'callback>,
1144        period: TimerDuration,
1145        clock: TimerClockSource,
1146        autostart: bool,
1147    ) -> NodeResult<NodeTimer<'entity>> {
1148        let mut metadata = entity_metadata(EntityMetadataSpec {
1149            id,
1150            node_id: self.__node_id()?,
1151            kind: EntityKind::Timer,
1152            source_name: "",
1153            type_name: "",
1154            type_hash: "",
1155            qos: QoSProfile::default(),
1156        })?;
1157        metadata.callback_id = Some(copy_str(callback_id.as_str())?);
1158        metadata.callback_source = SourceLocationMetadata::caller()?;
1159        metadata.source = metadata.callback_source.clone();
1160        metadata.period_ms = Some(period.as_millis());
1161        metadata.period_us = Some(period.as_micros());
1162        metadata.timer_clock = clock;
1163        metadata.timer_autostart = autostart;
1164        self.__declare_entity(metadata)?;
1165        Ok(NodeTimer::new(id))
1166    }
1167
1168    /// [`declare_timer_on_clock`](Self::declare_timer_on_clock) using
1169    /// `callback_id` as the stable timer entity ID.
1170    #[track_caller]
1171    #[doc(hidden)]
1172    fn create_timer_for_callback_on_clock<'callback>(
1173        &mut self,
1174        callback_id: CallbackId<'callback>,
1175        period: TimerDuration,
1176        clock: TimerClockSource,
1177    ) -> NodeResult<NodeTimer<'callback>> {
1178        self.declare_timer_on_clock(
1179            EntityId::new(callback_id.as_str()),
1180            callback_id,
1181            period,
1182            clock,
1183        )
1184    }
1185
1186    /// [`declare_timer_on_clock`](Self::declare_timer_on_clock) using
1187    /// `callback_name` as the source callback name and synthesized entity ID —
1188    /// the spelling a `nros::main!` component writes:
1189    ///
1190    /// ```ignore
1191    /// node.create_timer_for_callback_name_on_clock(
1192    ///     "on_tick",
1193    ///     TimerDuration::from_millis(100),
1194    ///     TimerClockSource::Ros,
1195    /// )?;
1196    /// ```
1197    #[track_caller]
1198    fn create_timer_for_callback_name_on_clock<'callback>(
1199        &mut self,
1200        callback_name: &'callback str,
1201        period: TimerDuration,
1202        clock: TimerClockSource,
1203    ) -> NodeResult<NodeTimer<'callback>> {
1204        self.create_timer_for_callback_on_clock(CallbackId::new(callback_name), period, clock)
1205    }
1206
1207    /// [`create_timer_for_callback_name_on_clock`](Self::create_timer_for_callback_name_on_clock)
1208    /// with rcl's `autostart` flag — issue 2038, the declarative spelling of
1209    /// [`Node::create_timer_with_info`](crate::Node::create_timer_with_info)'s
1210    /// flag and of C++'s `create_wall_timer(period, cb, group, autostart)`.
1211    ///
1212    /// `autostart == false` declares the timer STOPPED: it is registered
1213    /// cancelled and does not fire until it is reset. Like every declared timer,
1214    /// its callback can read the activation's call info with
1215    /// [`CallbackCtx::timer_info`].
1216    ///
1217    /// ```ignore
1218    /// node.create_timer_for_callback_name_with_autostart(
1219    ///     "on_tick",
1220    ///     TimerDuration::from_millis(100),
1221    ///     TimerClockSource::Steady,
1222    ///     false,
1223    /// )?;
1224    /// ```
1225    #[track_caller]
1226    fn create_timer_for_callback_name_with_autostart<'callback>(
1227        &mut self,
1228        callback_name: &'callback str,
1229        period: TimerDuration,
1230        clock: TimerClockSource,
1231        autostart: bool,
1232    ) -> NodeResult<NodeTimer<'callback>> {
1233        self.declare_timer_with_autostart(
1234            EntityId::new(callback_name),
1235            CallbackId::new(callback_name),
1236            period,
1237            clock,
1238            autostart,
1239        )
1240    }
1241
1242    /// Declare a service server. Stable service and callback IDs are required.
1243    #[track_caller]
1244    #[doc(hidden)]
1245    fn create_service_server<
1246        'entity,
1247        'callback,
1248        S: RosService<
1249                Request: nros_node::rmw_type_registry::MessageForRmw,
1250                Reply: nros_node::rmw_type_registry::MessageForRmw,
1251            >,
1252    >(
1253        &mut self,
1254        id: EntityId<'entity>,
1255        callback_id: CallbackId<'callback>,
1256        service_name: &str,
1257    ) -> NodeResult<NodeServiceServer<'entity, S>> {
1258        register_declared_service::<S>()?;
1259        let mut metadata = entity_metadata(EntityMetadataSpec {
1260            id,
1261            node_id: self.__node_id()?,
1262            kind: EntityKind::ServiceServer,
1263            source_name: service_name,
1264            type_name: S::SERVICE_NAME,
1265            type_hash: S::SERVICE_HASH,
1266            qos: QoSProfile::default(),
1267        })?;
1268        metadata.callback_id = Some(copy_str(callback_id.as_str())?);
1269        metadata.callback_source = SourceLocationMetadata::caller()?;
1270        metadata.source = metadata.callback_source.clone();
1271        self.__declare_entity(metadata)?;
1272        Ok(NodeServiceServer::new(id))
1273    }
1274
1275    /// Declare a service server using `name` as both the stable entity ID
1276    /// and callback ID.
1277    #[track_caller]
1278    fn create_service_server_for_name<
1279        'entity,
1280        S: RosService<
1281                Request: nros_node::rmw_type_registry::MessageForRmw,
1282                Reply: nros_node::rmw_type_registry::MessageForRmw,
1283            >,
1284    >(
1285        &mut self,
1286        name: &'entity str,
1287    ) -> NodeResult<NodeServiceServer<'entity, S>> {
1288        self.create_service_server::<S>(EntityId::new(name), CallbackId::new(name), name)
1289    }
1290
1291    /// Declare a service server using `name` as the stable entity ID and
1292    /// `callback_name` as the source callback name.
1293    #[track_caller]
1294    fn create_service_server_for_name_with_callback<
1295        'entity,
1296        S: RosService<
1297                Request: nros_node::rmw_type_registry::MessageForRmw,
1298                Reply: nros_node::rmw_type_registry::MessageForRmw,
1299            >,
1300    >(
1301        &mut self,
1302        name: &'entity str,
1303        callback_name: &str,
1304    ) -> NodeResult<NodeServiceServer<'entity, S>> {
1305        self.create_service_server::<S>(EntityId::new(name), CallbackId::new(callback_name), name)
1306    }
1307
1308    /// Declare a service server whose stable entity and callback IDs are
1309    /// both synthesized from the service-name literal, returning a
1310    /// [`ServiceTag`] the Node author stores on `Self::State` and matches
1311    /// against the `Callback<'_>` delivered to
1312    /// [`ExecutableNode::on_callback`].
1313    ///
1314    /// Tag-only registration is restricted to the SERVER side: clients
1315    /// need a USABLE handle (`NodeServiceClient`) to issue requests, so
1316    /// use the existing
1317    /// [`create_service_client_for_name`](Self::create_service_client_for_name) builder
1318    /// for the client side.
1319    #[track_caller]
1320    fn create_service_static<
1321        S: RosService<
1322                Request: nros_node::rmw_type_registry::MessageForRmw,
1323                Reply: nros_node::rmw_type_registry::MessageForRmw,
1324            >,
1325    >(
1326        &mut self,
1327        name: &'static str,
1328    ) -> NodeResult<ServiceTag> {
1329        self.create_service_server_for_name::<S>(name)?;
1330        Ok(ServiceTag::new(name))
1331    }
1332
1333    /// Declare a service client. Stable service client ID is required.
1334    #[track_caller]
1335    #[doc(hidden)]
1336    fn create_service_client<
1337        'entity,
1338        S: RosService<
1339                Request: nros_node::rmw_type_registry::MessageForRmw,
1340                Reply: nros_node::rmw_type_registry::MessageForRmw,
1341            >,
1342    >(
1343        &mut self,
1344        id: EntityId<'entity>,
1345        service_name: &str,
1346    ) -> NodeResult<NodeServiceClient<'entity, S>> {
1347        register_declared_service::<S>()?;
1348        let mut metadata = entity_metadata(EntityMetadataSpec {
1349            id,
1350            node_id: self.__node_id()?,
1351            kind: EntityKind::ServiceClient,
1352            source_name: service_name,
1353            type_name: S::SERVICE_NAME,
1354            type_hash: S::SERVICE_HASH,
1355            qos: QoSProfile::default(),
1356        })?;
1357        metadata.source = SourceLocationMetadata::caller()?;
1358        self.__declare_entity(metadata)?;
1359        Ok(NodeServiceClient::new(id))
1360    }
1361
1362    /// Declare a service client using `name` as the stable entity ID.
1363    #[track_caller]
1364    fn create_service_client_for_name<
1365        'entity,
1366        S: RosService<
1367                Request: nros_node::rmw_type_registry::MessageForRmw,
1368                Reply: nros_node::rmw_type_registry::MessageForRmw,
1369            >,
1370    >(
1371        &mut self,
1372        name: &'entity str,
1373    ) -> NodeResult<NodeServiceClient<'entity, S>> {
1374        self.create_service_client::<S>(EntityId::new(name), name)
1375    }
1376
1377    /// Declare an action server. Stable action and callback IDs are required.
1378    #[track_caller]
1379    #[doc(hidden)]
1380    fn declare_action_server<
1381        'entity,
1382        'callback,
1383        A: RosAction<
1384                Goal: nros_node::rmw_type_registry::MessageForRmw,
1385                Result: nros_node::rmw_type_registry::MessageForRmw,
1386                Feedback: nros_node::rmw_type_registry::MessageForRmw,
1387                SendGoalRequest: nros_node::rmw_type_registry::MessageForRmw,
1388                SendGoalResponse: nros_node::rmw_type_registry::MessageForRmw,
1389                GetResultRequest: nros_node::rmw_type_registry::MessageForRmw,
1390                GetResultResponse: nros_node::rmw_type_registry::MessageForRmw,
1391                FeedbackMessage: nros_node::rmw_type_registry::MessageForRmw,
1392            >,
1393    >(
1394        &mut self,
1395        id: EntityId<'entity>,
1396        callback_id: CallbackId<'callback>,
1397        action_name: &str,
1398    ) -> NodeResult<NodeActionServer<'entity, A>> {
1399        self.create_action_server_with_callbacks::<A>(
1400            id,
1401            callback_id,
1402            callback_id,
1403            callback_id,
1404            action_name,
1405        )
1406    }
1407
1408    /// Declare an action server with distinct goal/cancel/accepted callbacks.
1409    #[track_caller]
1410    #[doc(hidden)]
1411    fn create_action_server_with_callbacks<
1412        'entity,
1413        'goal,
1414        'cancel,
1415        'accepted,
1416        A: RosAction<
1417                Goal: nros_node::rmw_type_registry::MessageForRmw,
1418                Result: nros_node::rmw_type_registry::MessageForRmw,
1419                Feedback: nros_node::rmw_type_registry::MessageForRmw,
1420                SendGoalRequest: nros_node::rmw_type_registry::MessageForRmw,
1421                SendGoalResponse: nros_node::rmw_type_registry::MessageForRmw,
1422                GetResultRequest: nros_node::rmw_type_registry::MessageForRmw,
1423                GetResultResponse: nros_node::rmw_type_registry::MessageForRmw,
1424                FeedbackMessage: nros_node::rmw_type_registry::MessageForRmw,
1425            >,
1426    >(
1427        &mut self,
1428        id: EntityId<'entity>,
1429        goal_callback_id: CallbackId<'goal>,
1430        cancel_callback_id: CallbackId<'cancel>,
1431        accepted_callback_id: CallbackId<'accepted>,
1432        action_name: &str,
1433    ) -> NodeResult<NodeActionServer<'entity, A>> {
1434        register_declared_action::<A>()?;
1435        let mut metadata = entity_metadata(EntityMetadataSpec {
1436            id,
1437            node_id: self.__node_id()?,
1438            kind: EntityKind::ActionServer,
1439            source_name: action_name,
1440            type_name: A::ACTION_NAME,
1441            type_hash: A::ACTION_HASH,
1442            qos: QoSProfile::default(),
1443        })?;
1444        metadata.action_hashes = Some(nros_node::ActionEndpointHashes::of::<A>());
1445        metadata.callback_id = Some(copy_str(goal_callback_id.as_str())?);
1446        metadata.callback_source = SourceLocationMetadata::caller()?;
1447        metadata.action_cancel_callback_id = Some(copy_str(cancel_callback_id.as_str())?);
1448        metadata.action_cancel_source = metadata.callback_source.clone();
1449        metadata.action_accepted_callback_id = Some(copy_str(accepted_callback_id.as_str())?);
1450        metadata.action_accepted_source = metadata.callback_source.clone();
1451        metadata.source = metadata.callback_source.clone();
1452        self.__declare_entity(metadata)?;
1453        Ok(NodeActionServer::new(id))
1454    }
1455
1456    /// Declare an action server using `name` as the stable entity ID and
1457    /// default goal/cancel/accepted callback ID.
1458    #[track_caller]
1459    fn create_action_server_for_name<
1460        'entity,
1461        A: RosAction<
1462                Goal: nros_node::rmw_type_registry::MessageForRmw,
1463                Result: nros_node::rmw_type_registry::MessageForRmw,
1464                Feedback: nros_node::rmw_type_registry::MessageForRmw,
1465                SendGoalRequest: nros_node::rmw_type_registry::MessageForRmw,
1466                SendGoalResponse: nros_node::rmw_type_registry::MessageForRmw,
1467                GetResultRequest: nros_node::rmw_type_registry::MessageForRmw,
1468                GetResultResponse: nros_node::rmw_type_registry::MessageForRmw,
1469                FeedbackMessage: nros_node::rmw_type_registry::MessageForRmw,
1470            >,
1471    >(
1472        &mut self,
1473        name: &'entity str,
1474    ) -> NodeResult<NodeActionServer<'entity, A>> {
1475        self.declare_action_server::<A>(EntityId::new(name), CallbackId::new(name), name)
1476    }
1477
1478    /// Declare an action server using `name` as the stable entity ID and
1479    /// explicit source callback names for goal, cancel, and accepted events.
1480    #[track_caller]
1481    fn create_action_server_for_name_with_callbacks<
1482        'entity,
1483        A: RosAction<
1484                Goal: nros_node::rmw_type_registry::MessageForRmw,
1485                Result: nros_node::rmw_type_registry::MessageForRmw,
1486                Feedback: nros_node::rmw_type_registry::MessageForRmw,
1487                SendGoalRequest: nros_node::rmw_type_registry::MessageForRmw,
1488                SendGoalResponse: nros_node::rmw_type_registry::MessageForRmw,
1489                GetResultRequest: nros_node::rmw_type_registry::MessageForRmw,
1490                GetResultResponse: nros_node::rmw_type_registry::MessageForRmw,
1491                FeedbackMessage: nros_node::rmw_type_registry::MessageForRmw,
1492            >,
1493    >(
1494        &mut self,
1495        name: &'entity str,
1496        goal_callback_name: &str,
1497        cancel_callback_name: &str,
1498        accepted_callback_name: &str,
1499    ) -> NodeResult<NodeActionServer<'entity, A>> {
1500        self.create_action_server_with_callbacks::<A>(
1501            EntityId::new(name),
1502            CallbackId::new(goal_callback_name),
1503            CallbackId::new(cancel_callback_name),
1504            CallbackId::new(accepted_callback_name),
1505            name,
1506        )
1507    }
1508
1509    /// Declare an action server whose stable entity and callback IDs are
1510    /// both synthesized from the action-name literal, returning an
1511    /// [`ActionTag`] the Node author stores on `Self::State` and matches
1512    /// against the `Callback<'_>` delivered to
1513    /// [`ExecutableNode::on_callback`].
1514    ///
1515    /// The synthesized callback ID is shared by the goal / cancel /
1516    /// accepted callbacks (matching the default behavior of
1517    /// [`declare_action_server`](Self::declare_action_server)).
1518    ///
1519    /// Tag-only registration is restricted to the SERVER side: clients
1520    /// need a USABLE handle (`NodeActionClient`) to dispatch goals, so
1521    /// use the existing
1522    /// [`create_action_client_for_name`](Self::create_action_client_for_name) builder
1523    /// for the client side.
1524    #[track_caller]
1525    fn create_action_static<
1526        A: RosAction<
1527                Goal: nros_node::rmw_type_registry::MessageForRmw,
1528                Result: nros_node::rmw_type_registry::MessageForRmw,
1529                Feedback: nros_node::rmw_type_registry::MessageForRmw,
1530                SendGoalRequest: nros_node::rmw_type_registry::MessageForRmw,
1531                SendGoalResponse: nros_node::rmw_type_registry::MessageForRmw,
1532                GetResultRequest: nros_node::rmw_type_registry::MessageForRmw,
1533                GetResultResponse: nros_node::rmw_type_registry::MessageForRmw,
1534                FeedbackMessage: nros_node::rmw_type_registry::MessageForRmw,
1535            >,
1536    >(
1537        &mut self,
1538        name: &'static str,
1539    ) -> NodeResult<ActionTag> {
1540        self.create_action_server_for_name::<A>(name)?;
1541        Ok(ActionTag::new(name))
1542    }
1543
1544    /// Declare an action client. Stable action client ID is required.
1545    #[track_caller]
1546    #[doc(hidden)]
1547    fn declare_action_client<
1548        'entity,
1549        A: RosAction<
1550                Goal: nros_node::rmw_type_registry::MessageForRmw,
1551                Result: nros_node::rmw_type_registry::MessageForRmw,
1552                Feedback: nros_node::rmw_type_registry::MessageForRmw,
1553                SendGoalRequest: nros_node::rmw_type_registry::MessageForRmw,
1554                SendGoalResponse: nros_node::rmw_type_registry::MessageForRmw,
1555                GetResultRequest: nros_node::rmw_type_registry::MessageForRmw,
1556                GetResultResponse: nros_node::rmw_type_registry::MessageForRmw,
1557                FeedbackMessage: nros_node::rmw_type_registry::MessageForRmw,
1558            >,
1559    >(
1560        &mut self,
1561        id: EntityId<'entity>,
1562        action_name: &str,
1563    ) -> NodeResult<NodeActionClient<'entity, A>> {
1564        register_declared_action::<A>()?;
1565        let mut metadata = entity_metadata(EntityMetadataSpec {
1566            id,
1567            node_id: self.__node_id()?,
1568            kind: EntityKind::ActionClient,
1569            source_name: action_name,
1570            type_name: A::ACTION_NAME,
1571            type_hash: A::ACTION_HASH,
1572            qos: QoSProfile::default(),
1573        })?;
1574        metadata.action_hashes = Some(nros_node::ActionEndpointHashes::of::<A>());
1575        metadata.source = SourceLocationMetadata::caller()?;
1576        self.__declare_entity(metadata)?;
1577        Ok(NodeActionClient::new(id))
1578    }
1579
1580    /// Declare an action client using `name` as the stable entity ID.
1581    #[track_caller]
1582    fn create_action_client_for_name<
1583        'entity,
1584        A: RosAction<
1585                Goal: nros_node::rmw_type_registry::MessageForRmw,
1586                Result: nros_node::rmw_type_registry::MessageForRmw,
1587                Feedback: nros_node::rmw_type_registry::MessageForRmw,
1588                SendGoalRequest: nros_node::rmw_type_registry::MessageForRmw,
1589                SendGoalResponse: nros_node::rmw_type_registry::MessageForRmw,
1590                GetResultRequest: nros_node::rmw_type_registry::MessageForRmw,
1591                GetResultResponse: nros_node::rmw_type_registry::MessageForRmw,
1592                FeedbackMessage: nros_node::rmw_type_registry::MessageForRmw,
1593            >,
1594    >(
1595        &mut self,
1596        name: &'entity str,
1597    ) -> NodeResult<NodeActionClient<'entity, A>> {
1598        self.declare_action_client::<A>(EntityId::new(name), name)
1599    }
1600
1601    /// Declare an action client that delivers the goal RESULT + FEEDBACK to
1602    /// named callbacks (Phase 212.M-F.23). `name` is the stable entity ID. The
1603    /// executor auto-drives accept → feedback stream → result during spin and
1604    /// dispatches `ExecutableNode::on_callback` with `result_callback_name`
1605    /// (payload = result CDR) on completion, and with `feedback_callback_name`
1606    /// (payload = feedback CDR) per feedback message. Read either with
1607    /// `CallbackCtx::message::<A::Result>()` / `::<A::Feedback>()`. Without
1608    /// these the client can only `send_goal`; result + feedback are dropped.
1609    ///
1610    /// (Layout note: the action-client variant reuses the server-side
1611    /// `action_accepted_callback_id` metadata slot for the feedback callback —
1612    /// that field is unused on a client, so no new schema field is needed.)
1613    #[track_caller]
1614    fn create_action_client_with_callbacks_for_name<
1615        'entity,
1616        A: RosAction<
1617                Goal: nros_node::rmw_type_registry::MessageForRmw,
1618                Result: nros_node::rmw_type_registry::MessageForRmw,
1619                Feedback: nros_node::rmw_type_registry::MessageForRmw,
1620                SendGoalRequest: nros_node::rmw_type_registry::MessageForRmw,
1621                SendGoalResponse: nros_node::rmw_type_registry::MessageForRmw,
1622                GetResultRequest: nros_node::rmw_type_registry::MessageForRmw,
1623                GetResultResponse: nros_node::rmw_type_registry::MessageForRmw,
1624                FeedbackMessage: nros_node::rmw_type_registry::MessageForRmw,
1625            >,
1626    >(
1627        &mut self,
1628        name: &'entity str,
1629        result_callback_name: &str,
1630        feedback_callback_name: &str,
1631    ) -> NodeResult<NodeActionClient<'entity, A>> {
1632        register_declared_action::<A>()?;
1633        let mut metadata = entity_metadata(EntityMetadataSpec {
1634            id: EntityId::new(name),
1635            node_id: self.__node_id()?,
1636            kind: EntityKind::ActionClient,
1637            source_name: name,
1638            type_name: A::ACTION_NAME,
1639            type_hash: A::ACTION_HASH,
1640            qos: QoSProfile::default(),
1641        })?;
1642        metadata.action_hashes = Some(nros_node::ActionEndpointHashes::of::<A>());
1643        metadata.callback_id = Some(copy_str(result_callback_name)?);
1644        metadata.action_accepted_callback_id = Some(copy_str(feedback_callback_name)?);
1645        metadata.callback_source = SourceLocationMetadata::caller()?;
1646        metadata.source = metadata.callback_source.clone();
1647        self.__declare_entity(metadata)?;
1648        Ok(NodeActionClient::new(EntityId::new(name)))
1649    }
1650
1651    /// Declare a parameter. Stable parameter ID is required.
1652    ///
1653    /// phase-515 R10: spelled `declare_parameter` until `Node` gained rclrs's
1654    /// inherent `declare_parameter::<T>(name)`, which an inherent method wins
1655    /// over a trait one with.
1656    #[track_caller]
1657    #[doc(hidden)]
1658    fn declare_parameter_with_id<'entity>(
1659        &mut self,
1660        id: EntityId<'entity>,
1661        name: &str,
1662        parameter_type: ParameterType,
1663    ) -> NodeResult<NodeParameter<'entity>> {
1664        self.declare_parameter_with_default(id, name, ParameterDefault::for_type(parameter_type)?)
1665    }
1666
1667    /// Declare a parameter with a concrete source default.
1668    #[track_caller]
1669    #[doc(hidden)]
1670    fn declare_parameter_with_default<'entity>(
1671        &mut self,
1672        id: EntityId<'entity>,
1673        name: &str,
1674        default: ParameterDefault,
1675    ) -> NodeResult<NodeParameter<'entity>> {
1676        let mut metadata = entity_metadata(EntityMetadataSpec {
1677            id,
1678            node_id: self.__node_id()?,
1679            kind: EntityKind::Parameter,
1680            source_name: name,
1681            type_name: "",
1682            type_hash: "",
1683            qos: QoSProfile::default(),
1684        })?;
1685        metadata.parameter_type = Some(default.parameter_type());
1686        metadata.parameter_default = Some(default);
1687        metadata.source = SourceLocationMetadata::caller()?;
1688        self.__declare_entity(metadata)?;
1689        Ok(NodeParameter::new(id))
1690    }
1691
1692    /// Declare a parameter using `name` as the generated stable entity ID.
1693    #[track_caller]
1694    fn declare_parameter_for_name<'entity>(
1695        &mut self,
1696        name: &'entity str,
1697        parameter_type: ParameterType,
1698    ) -> NodeResult<NodeParameter<'entity>> {
1699        self.declare_parameter_with_id(EntityId::new(name), name, parameter_type)
1700    }
1701
1702    /// Declare a parameter with a concrete source default, using `name` as
1703    /// the generated stable entity ID.
1704    #[track_caller]
1705    fn declare_parameter_for_name_with_default<'entity>(
1706        &mut self,
1707        name: &'entity str,
1708        default: ParameterDefault,
1709    ) -> NodeResult<NodeParameter<'entity>> {
1710        self.declare_parameter_with_default(EntityId::new(name), name, default)
1711    }
1712
1713    /// Record optional effects for a callback.
1714    #[doc(hidden)]
1715    fn callback<'callback>(&mut self, id: CallbackId<'callback>) -> CallbackEffects<'_, 'callback> {
1716        CallbackEffects {
1717            executor: self.__executor_mut(),
1718            id,
1719        }
1720    }
1721
1722    /// Record optional effects for a named callback without exposing
1723    /// `CallbackId` at the declaration site.
1724    fn callback_for_name<'callback>(
1725        &mut self,
1726        name: &'callback str,
1727    ) -> CallbackEffects<'_, 'callback> {
1728        self.callback(CallbackId::new(name))
1729    }
1730}
1731
1732#[cfg(feature = "rmw-cffi")]
1733impl DeclarativeNode for crate::Node<'_, 'static> {
1734    fn __node_id(&mut self) -> NodeResult<NodeId<'static>> {
1735        let exec_id = self.id();
1736        frame_mut(self.__executor())?.stable_id_of(exec_id)
1737    }
1738
1739    fn __declare_entity(&mut self, mut metadata: EntityMetadata) -> NodeResult<()> {
1740        let exec_id = self.id();
1741        let executor = self.__executor();
1742        let frame = frame_mut(executor)?;
1743        if metadata.callback_group.is_none() {
1744            metadata.callback_group = frame.group_of(exec_id);
1745        }
1746        frame.sink.create_entity(executor, metadata)
1747    }
1748
1749    fn __executor_mut(&mut self) -> &mut crate::Executor<'static> {
1750        self.__executor()
1751    }
1752}
1753
1754/// Builder for optional callback effect metadata.
1755#[cfg(feature = "rmw-cffi")]
1756pub struct CallbackEffects<'ctx, 'id> {
1757    executor: &'ctx mut crate::Executor<'static>,
1758    id: CallbackId<'id>,
1759}
1760
1761#[cfg(feature = "rmw-cffi")]
1762impl<'ctx, 'id> CallbackEffects<'ctx, 'id> {
1763    /// Record that callback reads from an entity.
1764    #[doc(hidden)]
1765    pub fn reads(self, entity_id: EntityId<'_>) -> NodeResult<Self> {
1766        let frame = frame_mut(self.executor)?;
1767        frame.sink.record_callback_effect(
1768            self.executor,
1769            self.id,
1770            CallbackEffectKind::Reads,
1771            entity_id,
1772        )?;
1773        Ok(self)
1774    }
1775
1776    /// Record that callback reads from a declared entity handle.
1777    pub fn reads_entity(self, entity: &impl DeclaredEntity) -> NodeResult<Self> {
1778        self.reads(entity.entity_id())
1779    }
1780
1781    /// Record that callback publishes via an entity.
1782    #[doc(hidden)]
1783    pub fn publishes(self, entity_id: EntityId<'_>) -> NodeResult<Self> {
1784        let frame = frame_mut(self.executor)?;
1785        frame.sink.record_callback_effect(
1786            self.executor,
1787            self.id,
1788            CallbackEffectKind::Publishes,
1789            entity_id,
1790        )?;
1791        Ok(self)
1792    }
1793
1794    /// Record that callback publishes via a declared entity handle.
1795    pub fn publishes_entity(self, entity: &impl DeclaredEntity) -> NodeResult<Self> {
1796        self.publishes(entity.entity_id())
1797    }
1798
1799    /// Record that callback writes to an entity or parameter.
1800    #[doc(hidden)]
1801    pub fn writes(self, entity_id: EntityId<'_>) -> NodeResult<Self> {
1802        let frame = frame_mut(self.executor)?;
1803        frame.sink.record_callback_effect(
1804            self.executor,
1805            self.id,
1806            CallbackEffectKind::Writes,
1807            entity_id,
1808        )?;
1809        Ok(self)
1810    }
1811
1812    /// Record that callback writes to a declared entity handle.
1813    pub fn writes_entity(self, entity: &impl DeclaredEntity) -> NodeResult<Self> {
1814        self.writes(entity.entity_id())
1815    }
1816}
1817
1818/// A declared source-level entity handle that can be referenced by callback effects.
1819#[doc(hidden)]
1820pub trait DeclaredEntity {
1821    /// Stable entity ID for metadata and generated runtime lookup.
1822    fn entity_id(&self) -> EntityId<'_>;
1823}
1824
1825macro_rules! component_handle {
1826    ($name:ident $(, $type_param:ident)?) => {
1827        /// Source-level component entity handle.
1828        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1829        pub struct $name<'id $(, $type_param)?> {
1830            id: EntityId<'id>,
1831            _marker: PhantomData<($($type_param,)?)>,
1832        }
1833
1834        impl<'id $(, $type_param)?> $name<'id $(, $type_param)?> {
1835            const fn new(id: EntityId<'id>) -> Self {
1836                Self {
1837                    id,
1838                    _marker: PhantomData,
1839                }
1840            }
1841
1842            /// Stable entity ID.
1843            #[doc(hidden)]
1844            pub const fn id(&self) -> EntityId<'id> {
1845                self.id
1846            }
1847        }
1848
1849        impl<'id $(, $type_param)?> DeclaredEntity for $name<'id $(, $type_param)?> {
1850            fn entity_id(&self) -> EntityId<'_> {
1851                self.id
1852            }
1853        }
1854    };
1855}
1856
1857component_handle!(NodePublisher, M);
1858component_handle!(NodeSubscription, M);
1859component_handle!(NodeServiceServer, S);
1860component_handle!(NodeServiceClient, S);
1861component_handle!(NodeActionServer, A);
1862component_handle!(NodeActionClient, A);
1863component_handle!(NodeTimer);
1864component_handle!(NodeParameter);
1865
1866// ============================================================================
1867// Phase 172 W.5.1 — executable component layer (callback bodies)
1868// ============================================================================
1869//
1870// The declarative `Node::register` above stays the planning/metadata SSOT.
1871// This layer binds *runnable* bodies: the generated runtime builds the
1872// component `State` once, then routes each fired callback to `on_callback` with
1873// a `CallbackCtx` that exposes the triggering payload + an immediate publish
1874// path. Publishers are self-contained transport handles
1875// (`EmbeddedRawPublisher::publish_raw(&self)`), so a body publishes immediately
1876// mid-spin with no executor re-entrancy and no deferred queue (causality
1877// preserved). Shared state across a component's callbacks is `&mut State`
1878// behind the generated runtime's `'static` storage — `no_std`, no `alloc`.
1879
1880/// Resolves a component publisher by its stable [`EntityId`] for the
1881/// callback-body publish path (W.5.1).
1882///
1883/// The generated runtime implements this over its owned `'static` publishers;
1884/// metadata/discovery mode never constructs a [`CallbackCtx`], so it need not
1885/// implement this.
1886pub trait PublisherResolver {
1887    /// Publish raw CDR bytes through the publisher with this stable entity id.
1888    /// `Err(NodeDeclError::Runtime)` if no such publisher is registered or the
1889    /// transport rejects the write.
1890    fn publish_raw(&self, entity_id: &str, data: &[u8]) -> NodeResult<()>;
1891
1892    /// Issue 2075 — request that the declared timer `entity_id` be reset
1893    /// (started, or restarted on a fresh period) or cancelled. The component
1894    /// runtime records it and applies it at the component's next tick, where
1895    /// the executor is free. The default answers
1896    /// [`NodeDeclError::UnknownTimer`]: a resolver with no timer table controls
1897    /// nothing.
1898    #[doc(hidden)]
1899    fn request_timer(&self, entity_id: &str, op: TimerOp) -> NodeResult<()> {
1900        let _ = (entity_id, op);
1901        Err(NodeDeclError::UnknownTimer)
1902    }
1903}
1904
1905/// Issue 2075 — what [`CallbackCtx::reset_timer`] / [`CallbackCtx::cancel_timer`]
1906/// (and their [`TickCtx`] twins) ask of a declared timer.
1907#[doc(hidden)]
1908#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1909pub enum TimerOp {
1910    /// rcl's `rcl_timer_reset`: clear the cancelled state and start a fresh
1911    /// period. Starts a timer declared with `autostart = false`.
1912    Reset,
1913    /// rcl's `rcl_timer_cancel`: the timer stops firing until reset.
1914    Cancel,
1915}
1916
1917/// Where a service / action-result callback body writes its reply (W.5.3): the
1918/// generated trampoline lends a `buf`; the body fills it via
1919/// [`CallbackCtx::reply`] and the trampoline reads `*written` back out.
1920struct ReplySink<'a> {
1921    buf: &'a mut [u8],
1922    written: &'a mut usize,
1923}
1924
1925/// Where an action goal / cancel-decision callback writes its accept/reject
1926/// (W.5.3): the generated trampoline lends the out-slot, the body fills it via
1927/// [`CallbackCtx::set_goal_response`] / [`set_cancel_response`](CallbackCtx::set_cancel_response),
1928/// and the trampoline returns it. Decisions need no executor — unlike feedback /
1929/// result, which do (see the action-execution note in Phase 172 W.5.3).
1930enum DecisionSink<'a> {
1931    Goal(&'a mut GoalResponse),
1932    Cancel(&'a mut CancelResponse),
1933}
1934
1935/// Context handed to an executable component callback body (W.5.1).
1936///
1937/// Carries the triggering payload (raw CDR — empty for timers) plus the
1938/// publisher resolver, so a body can read its message and publish immediately.
1939/// Service / action-result callbacks additionally carry a `ReplySink` the body
1940/// fills via [`reply`](Self::reply); action goal / cancel callbacks carry a
1941/// `DecisionSink` the body fills via
1942/// [`set_goal_response`](Self::set_goal_response) /
1943/// [`set_cancel_response`](Self::set_cancel_response) (W.5.3).
1944/// issue 0461 — bytes of `unique_identifier_msgs/UUID` that precede the goal
1945/// fields in a SendGoal request. A fixed `uint8[16]` array: no length prefix.
1946const GOAL_UUID_LEN: usize = 16;
1947
1948pub struct CallbackCtx<'a> {
1949    payload: &'a [u8],
1950    publishers: &'a dyn PublisherResolver,
1951    reply: Option<ReplySink<'a>>,
1952    decision: Option<DecisionSink<'a>>,
1953    /// Phase 250 (Wave 2) — E2E message-integrity status for a subscription that
1954    /// opted in via `.safety()`; `None` for every other callback (timers,
1955    /// services, non-safety subscriptions). Read with
1956    /// [`integrity`](Self::integrity). Gated with the capability so it is
1957    /// zero-cost when `safety-e2e` is off.
1958    #[cfg(feature = "safety-e2e")]
1959    integrity: Option<&'a crate::IntegrityStatus>,
1960    /// Phase 264 W4c — the executor's volatile parameter store, threaded by the dispatch
1961    /// site from the component cell (`None` until `[param_services]` registers the store).
1962    /// Read with [`parameter`](Self::parameter). Gated so it is zero-cost when
1963    /// `param-services` is off.
1964    #[cfg(feature = "param-store")]
1965    params: Option<&'a crate::ParameterServer<'a>>,
1966    /// phase-426 W3 — WHICH node's parameters `parameter()` reads.
1967    ///
1968    /// One set of six parameter services is registered per node and the store
1969    /// is keyed the same way, so a context that carries the store without the
1970    /// key can only ever answer for one node — which is how every component on
1971    /// a multi-node executor came to read the primary's values.
1972    #[cfg(feature = "param-store")]
1973    param_node: nros_params::NodeKey,
1974    /// Issue 2038 — the activation's call info, for a TIMER callback the
1975    /// runtime dispatched (`None` for every other callback). Read with
1976    /// [`timer_info`](Self::timer_info).
1977    timer_info: Option<crate::TimerCallInfo>,
1978}
1979
1980impl<'a> CallbackCtx<'a> {
1981    /// Build a callback context with no reply sink (timer / subscription).
1982    /// `payload` is the entity's raw CDR (empty slice for timers).
1983    pub fn new(payload: &'a [u8], publishers: &'a dyn PublisherResolver) -> Self {
1984        Self {
1985            payload,
1986            publishers,
1987            reply: None,
1988            decision: None,
1989            #[cfg(feature = "safety-e2e")]
1990            integrity: None,
1991            #[cfg(feature = "param-store")]
1992            params: None,
1993            // phase-426 W3 — the store is keyed by node and this context does
1994            // not know which one until the dispatch site says so
1995            // (`set_param_server`). PRIMARY until then, which is the executor's
1996            // first node and what a single-node image means.
1997            #[cfg(feature = "param-store")]
1998            param_node: nros_params::NodeKey::PRIMARY,
1999            timer_info: None,
2000        }
2001    }
2002
2003    /// Phase 250 (Wave 2) — build a subscription context carrying E2E
2004    /// [`IntegrityStatus`](crate::IntegrityStatus) (the declarative `.safety()`
2005    /// path). The body reads both the message ([`message`](Self::message)) and
2006    /// the status ([`integrity`](Self::integrity)) in one callback, mirroring the
2007    /// imperative `FnMut(&M, &IntegrityStatus)` shape.
2008    #[cfg(feature = "safety-e2e")]
2009    pub fn new_with_integrity(
2010        payload: &'a [u8],
2011        publishers: &'a dyn PublisherResolver,
2012        integrity: &'a crate::IntegrityStatus,
2013    ) -> Self {
2014        Self {
2015            payload,
2016            publishers,
2017            reply: None,
2018            decision: None,
2019            integrity: Some(integrity),
2020            #[cfg(feature = "param-store")]
2021            params: None,
2022            // phase-426 W3 — the store is keyed by node and this context does
2023            // not know which one until the dispatch site says so
2024            // (`set_param_server`). PRIMARY until then, which is the executor's
2025            // first node and what a single-node image means.
2026            #[cfg(feature = "param-store")]
2027            param_node: nros_params::NodeKey::PRIMARY,
2028            timer_info: None,
2029        }
2030    }
2031
2032    /// Build a callback context with a reply sink (service / action-result;
2033    /// W.5.3). The body fills `reply_buf` via [`reply`](Self::reply); the
2034    /// generated trampoline reads `*reply_written` back as the response length.
2035    pub fn with_reply(
2036        payload: &'a [u8],
2037        publishers: &'a dyn PublisherResolver,
2038        reply_buf: &'a mut [u8],
2039        reply_written: &'a mut usize,
2040    ) -> Self {
2041        *reply_written = 0;
2042        Self {
2043            payload,
2044            publishers,
2045            reply: Some(ReplySink {
2046                buf: reply_buf,
2047                written: reply_written,
2048            }),
2049            decision: None,
2050            #[cfg(feature = "safety-e2e")]
2051            integrity: None,
2052            #[cfg(feature = "param-store")]
2053            params: None,
2054            // phase-426 W3 — the store is keyed by node and this context does
2055            // not know which one until the dispatch site says so
2056            // (`set_param_server`). PRIMARY until then, which is the executor's
2057            // first node and what a single-node image means.
2058            #[cfg(feature = "param-store")]
2059            param_node: nros_params::NodeKey::PRIMARY,
2060            timer_info: None,
2061        }
2062    }
2063
2064    /// Build a context for an action **goal** callback (W.5.3): the body decides
2065    /// accept/reject via [`set_goal_response`](Self::set_goal_response); the
2066    /// generated trampoline returns `*out`. `payload` is the goal CDR.
2067    pub fn with_goal_decision(
2068        payload: &'a [u8],
2069        publishers: &'a dyn PublisherResolver,
2070        out: &'a mut GoalResponse,
2071    ) -> Self {
2072        Self {
2073            payload,
2074            publishers,
2075            reply: None,
2076            decision: Some(DecisionSink::Goal(out)),
2077            #[cfg(feature = "safety-e2e")]
2078            integrity: None,
2079            #[cfg(feature = "param-store")]
2080            params: None,
2081            // phase-426 W3 — the store is keyed by node and this context does
2082            // not know which one until the dispatch site says so
2083            // (`set_param_server`). PRIMARY until then, which is the executor's
2084            // first node and what a single-node image means.
2085            #[cfg(feature = "param-store")]
2086            param_node: nros_params::NodeKey::PRIMARY,
2087            timer_info: None,
2088        }
2089    }
2090
2091    /// Build a context for an action **cancel** callback (W.5.3): the body decides
2092    /// accept/reject via [`set_cancel_response`](Self::set_cancel_response).
2093    pub fn with_cancel_decision(
2094        payload: &'a [u8],
2095        publishers: &'a dyn PublisherResolver,
2096        out: &'a mut CancelResponse,
2097    ) -> Self {
2098        Self {
2099            payload,
2100            publishers,
2101            reply: None,
2102            decision: Some(DecisionSink::Cancel(out)),
2103            #[cfg(feature = "safety-e2e")]
2104            integrity: None,
2105            #[cfg(feature = "param-store")]
2106            params: None,
2107            // phase-426 W3 — the store is keyed by node and this context does
2108            // not know which one until the dispatch site says so
2109            // (`set_param_server`). PRIMARY until then, which is the executor's
2110            // first node and what a single-node image means.
2111            #[cfg(feature = "param-store")]
2112            param_node: nros_params::NodeKey::PRIMARY,
2113            timer_info: None,
2114        }
2115    }
2116
2117    /// Phase 264 W4c — thread the executor's volatile parameter store into this context.
2118    /// The dispatch site calls this after construction (the store reaches the callback via
2119    /// the component cell, not the constructor args). No-op-equivalent when `None`.
2120    #[cfg(feature = "param-store")]
2121    pub fn set_param_server(
2122        &mut self,
2123        params: Option<&'a crate::ParameterServer<'a>>,
2124        node: nros_params::NodeKey,
2125    ) {
2126        self.params = params;
2127        self.param_node = node;
2128    }
2129
2130    /// Phase 264 W4c — read this node's parameter `name` as `T`, or `None` if the
2131    /// parameter is undeclared, the wrong type, or `[param_services]` is not enabled.
2132    /// Returns the **live** value — the launch-baked initial, or whatever a `ros2 param
2133    /// set` last wrote (RFC-0004 §10; values are volatile, lost at the next boot).
2134    #[cfg(feature = "param-store")]
2135    pub fn parameter<T: crate::ParameterVariant>(&self, name: &str) -> Option<T> {
2136        self.params
2137            // phase-426 W3 — the store is keyed by node, and the key is the
2138            // component's OWN node, threaded in beside the store by the
2139            // dispatch site. Reading PRIMARY here (which is what this did
2140            // until W3) makes the second node on an executor read its
2141            // sibling's values.
2142            .and_then(|server| server.get(self.param_node, name))
2143            .and_then(T::from_parameter_value)
2144    }
2145
2146    /// Set the action goal-callback's accept/reject decision (W.5.3). `Err` when
2147    /// the callback is not a goal decision.
2148    pub fn set_goal_response(&mut self, response: GoalResponse) -> NodeResult<()> {
2149        match &mut self.decision {
2150            Some(DecisionSink::Goal(slot)) => {
2151                **slot = response;
2152                Ok(())
2153            }
2154            _ => Err(NodeDeclError::Runtime),
2155        }
2156    }
2157
2158    /// Set the action cancel-callback's accept/reject decision (W.5.3). `Err` when
2159    /// the callback is not a cancel decision.
2160    ///
2161    /// Issue 0796 — `CancelResponse` here is the PER-GOAL decision
2162    /// (`Reject` / `Accept`), the twin of [`GoalResponse`] and the same two
2163    /// values C's `rclc_cancel_response_t` and C++'s `nros::CancelResponse`
2164    /// carry. It used to be the `action_msgs/srv/CancelGoal` RPC return code
2165    /// (now `nros_core::CancelResponseCode`), so answering "cancel this goal"
2166    /// was spelled `CancelResponse::Ok` — a whole-request status code used to
2167    /// decide one goal.
2168    pub fn set_cancel_response(&mut self, response: CancelResponse) -> NodeResult<()> {
2169        match &mut self.decision {
2170            Some(DecisionSink::Cancel(slot)) => {
2171                **slot = response;
2172                Ok(())
2173            }
2174            _ => Err(NodeDeclError::Runtime),
2175        }
2176    }
2177
2178    /// Write the service / action reply as raw CDR bytes (W.5.3). `Err` when the
2179    /// callback has no reply sink (timer / subscription) or the reply exceeds the
2180    /// lent buffer.
2181    pub fn reply_raw(&mut self, data: &[u8]) -> NodeResult<()> {
2182        let sink = self.reply.as_mut().ok_or(NodeDeclError::Runtime)?;
2183        if data.len() > sink.buf.len() {
2184            return Err(NodeDeclError::Runtime);
2185        }
2186        sink.buf[..data.len()].copy_from_slice(data);
2187        *sink.written = data.len();
2188        Ok(())
2189    }
2190
2191    /// Serialize `msg` and write it as the service / action reply (W.5.3).
2192    pub fn reply<M: RosMessage, const N: usize>(&mut self, msg: &M) -> NodeResult<()> {
2193        let mut buf = [0u8; N];
2194        let mut writer =
2195            crate::CdrWriter::new_with_header(&mut buf).map_err(|_| NodeDeclError::Runtime)?;
2196        msg.serialize(&mut writer)
2197            .map_err(|_| NodeDeclError::Runtime)?;
2198        let len = writer.position();
2199        self.reply_raw(&buf[..len])
2200    }
2201
2202    /// Raw CDR payload of the triggering message / request. Empty for timers.
2203    pub fn payload(&self) -> &[u8] {
2204        self.payload
2205    }
2206
2207    /// Issue 2038 — the call info of the timer activation being dispatched:
2208    /// when it was due (`expected_call_time`) and when the executor ran it
2209    /// (`actual_call_time`), in nanoseconds on the timer's clock. `Some` only
2210    /// in a declared TIMER's callback; `None` for subscriptions, services and
2211    /// actions. The same two numbers `Node::create_timer_with_info` and C++'s
2212    /// `rclcpp::TimerInfo` receive — one producer, the executor's dispatcher.
2213    pub fn timer_info(&self) -> Option<&crate::TimerCallInfo> {
2214        self.timer_info.as_ref()
2215    }
2216
2217    /// Issue 2038 — attach a timer activation's call info. The runtime's
2218    /// timer dispatch calls this after construction, as it does
2219    /// `set_param_server`.
2220    #[doc(hidden)]
2221    pub fn set_timer_info(&mut self, info: crate::TimerCallInfo) {
2222        self.timer_info = Some(info);
2223    }
2224
2225    /// Issue 2075 — reset the declared timer `timer`: rclcpp's
2226    /// `TimerBase::reset()`. Starts a timer declared with `autostart = false`
2227    /// ([`DeclarativeNode::create_timer_for_callback_name_with_autostart`]),
2228    /// and restarts a running one on a fresh period.
2229    ///
2230    /// The request is applied at this component's next tick, in the same
2231    /// spin, once the callbacks of the pass have run: a callback does not hold
2232    /// the executor. `timer` is the timer's entity id, which for the
2233    /// `..._for_callback_name` verbs is the callback name. Answers
2234    /// [`NodeDeclError::UnknownTimer`] when this component has no controllable
2235    /// timer by that id (see [`EntityBounds::timers`]).
2236    ///
2237    /// ```ignore
2238    /// ctx.reset_timer(nros::EntityId::new("on_tick"))?;
2239    /// ```
2240    pub fn reset_timer(&self, timer: EntityId<'_>) -> NodeResult<()> {
2241        self.publishers
2242            .request_timer(timer.as_str(), TimerOp::Reset)
2243    }
2244
2245    /// Issue 2075 — cancel the declared timer `timer`: rclcpp's
2246    /// `TimerBase::cancel()`. It stops firing until [`reset_timer`](Self::reset_timer).
2247    /// Applied at the next tick, like `reset_timer`.
2248    pub fn cancel_timer(&self, timer: EntityId<'_>) -> NodeResult<()> {
2249        self.publishers
2250            .request_timer(timer.as_str(), TimerOp::Cancel)
2251    }
2252
2253    /// Phase 250 (Wave 2) — E2E message-integrity status (CRC + sequence gap/dup)
2254    /// for this dispatch. `Some` only when the firing subscription opted in via
2255    /// `.safety()`; `None` for timers, services, and non-safety subscriptions.
2256    /// Read it alongside [`message`](Self::message) — the status describes the
2257    /// message you just received.
2258    #[cfg(feature = "safety-e2e")]
2259    pub fn integrity(&self) -> Option<&crate::IntegrityStatus> {
2260        self.integrity
2261    }
2262
2263    /// Deserialize the triggering payload as `M` (subscription / service-request
2264    /// bodies). `Err` if the payload is malformed for `M`.
2265    pub fn message<M: RosMessage>(&self) -> NodeResult<M> {
2266        let mut reader =
2267            crate::CdrReader::new_with_header(self.payload).map_err(|_| NodeDeclError::Runtime)?;
2268        // issue 0461 — an action GOAL callback's payload is the whole SendGoal
2269        // request, `[CDR header][goal_id uuid][goal fields]`. Without this skip
2270        // the reader is sitting on the uuid and a goal type decodes its first
2271        // four bytes — the goal counter, so every goal looked like `order = 1`.
2272        //
2273        // The goal_id reaches the callback by other means (the server's
2274        // `for_each_active_goal_for_name`), so it is framing here, not data.
2275        // Same shape as the typed `try_accept_goal` path, which has always
2276        // skipped it and has always decoded correctly.
2277        if matches!(self.decision, Some(DecisionSink::Goal(_))) {
2278            for _ in 0..GOAL_UUID_LEN {
2279                let _ = reader.read_u8();
2280            }
2281        }
2282        M::deserialize(&mut reader).map_err(|_| NodeDeclError::Runtime)
2283    }
2284
2285    /// Publish raw CDR bytes through the named publisher entity (immediate).
2286    #[doc(hidden)]
2287    pub fn publish_raw(&self, publisher: EntityId<'_>, data: &[u8]) -> NodeResult<()> {
2288        self.publishers.publish_raw(publisher.as_str(), data)
2289    }
2290
2291    /// Serialize `msg` into an `N`-byte stack buffer and publish it (immediate).
2292    /// `N` must be ≥ the CDR-encoded size of `msg`; the generated runtime picks
2293    /// it from the message type.
2294    #[doc(hidden)]
2295    pub fn publish<M: RosMessage, const N: usize>(
2296        &self,
2297        publisher: EntityId<'_>,
2298        msg: &M,
2299    ) -> NodeResult<()> {
2300        let mut buf = [0u8; N];
2301        let mut writer =
2302            crate::CdrWriter::new_with_header(&mut buf).map_err(|_| NodeDeclError::Runtime)?;
2303        msg.serialize(&mut writer)
2304            .map_err(|_| NodeDeclError::Runtime)?;
2305        let len = writer.position();
2306        self.publish_raw(publisher, &buf[..len])
2307    }
2308
2309    /// Serialize `msg` and publish through the entity synthesized from `topic`.
2310    ///
2311    /// This pairs with
2312    /// [`DeclarativeNode::create_publisher_for_topic`], allowing simple callback
2313    /// bodies to use the ROS topic literal instead of restating an unrelated
2314    /// stable entity ID.
2315    pub fn publish_to_topic<M: RosMessage, const N: usize>(
2316        &self,
2317        topic: &str,
2318        msg: &M,
2319    ) -> NodeResult<()> {
2320        self.publish::<M, N>(EntityId::new(topic), msg)
2321    }
2322}
2323
2324/// The executable counterpart of [`Component`] (W.5.1).
2325///
2326/// `register` (declarative) stays the planning SSOT; this binds runnable
2327/// bodies. The generated runtime builds [`State`](ExecutableNode::State) once via
2328/// [`init`](ExecutableNode::init), then routes every fired callback to
2329/// [`on_callback`](ExecutableNode::on_callback). Trait-dispatch (no boxed `dyn`, no
2330/// `alloc`) keeps it `no_std`.
2331/// Executor-backed action operations a [`TickCtx`] drives (W.5.6).
2332///
2333/// Action result/feedback need `&mut Executor` (`complete_goal_raw` /
2334/// `publish_feedback_raw`), which a mid-spin *callback* can't hold (the executor
2335/// is borrowed) — so they run from [`ExecutableNode::tick`], between spins.
2336/// The generated runtime implements this over the real executor + the action
2337/// servers' handles (resolved by stable action entity id); the component never
2338/// sees the executor directly. Kept as a trait so [`TickCtx`] stays `no_std` +
2339/// free of the `rmw-cffi`-gated `Executor` type.
2340pub trait ActionExecutor {
2341    /// Complete the goal `goal_id` on action `action_entity` with raw CDR result.
2342    fn complete_goal_raw(
2343        &mut self,
2344        action_entity: &str,
2345        goal_id: &GoalUuid,
2346        status: GoalStatusCode,
2347        result: &[u8],
2348    ) -> NodeResult<()>;
2349
2350    /// Publish raw CDR feedback for `goal_id` on action `action_entity`.
2351    fn publish_feedback_raw(
2352        &mut self,
2353        action_entity: &str,
2354        goal_id: &GoalUuid,
2355        feedback: &[u8],
2356    ) -> NodeResult<()>;
2357
2358    /// Visit every goal on `action_entity` that has been accepted but not yet
2359    /// completed, with its id + current status. The execution seam: a `tick` body
2360    /// has no other way to learn an accepted goal's id (the goal-decision callback
2361    /// doesn't surface it), so it iterates here to drive feedback / completion.
2362    fn for_each_active_goal(
2363        &self,
2364        action_entity: &str,
2365        visit: &mut dyn FnMut(&GoalUuid, GoalStatusCode),
2366    );
2367}
2368
2369/// Executor-backed CLIENT operations a [`TickCtx`] drives (Phase 212.M-F.4).
2370///
2371/// Service-client `call` + action-client `send_goal` need `&mut Executor`
2372/// (the W.5.6 client handles live on the executor), which a mid-spin
2373/// callback can't hold. They run from [`ExecutableNode::tick`], between
2374/// spins. The generated runtime impls this over the real executor + the
2375/// service/action client handles (resolved by stable client entity id); the
2376/// component never sees the executor directly. Kept as a trait so [`TickCtx`]
2377/// stays `no_std` + free of the `rmw-cffi`-gated `Executor` type.
2378///
2379/// Mirrors the sibling [`ActionExecutor`] (server-side ops). Splitting
2380/// client vs server keeps each trait small + lets an implementor (the
2381/// single-node runtime's tick-side client dispatch; once the codegen-side
2382/// ~~`GenClientDispatch`~~) resolve client handles independently from
2383/// server handles.
2384pub trait ClientDispatch {
2385    /// Issue a service-client request on `service_entity` carrying CDR
2386    /// `request_cdr`; block on the reply, write the response CDR into
2387    /// `response_buf`, return the response length in bytes.
2388    ///
2389    /// The synchronous block is built on the executor-driven
2390    /// `send_request_raw` + `take_response_raw` pair (phase-301: the
2391    /// RMW layer has no blocking call) — the tick hook drives the
2392    /// executor between callback dispatch, so a blocked `call_raw`
2393    /// does not starve other callbacks (each tick yields back to the
2394    /// runtime after returning).
2395    fn call_raw(
2396        &mut self,
2397        service_entity: &str,
2398        request_cdr: &[u8],
2399        response_buf: &mut [u8],
2400    ) -> NodeResult<usize>;
2401
2402    /// Send an action-client goal request on `action_entity` carrying
2403    /// CDR `goal_cdr`; return the assigned [`GoalUuid`] (server-stamped on
2404    /// the goal-accept response). Result + feedback streams arrive via
2405    /// callback dispatch — not this method.
2406    fn send_goal_raw(&mut self, action_entity: &str, goal_cdr: &[u8]) -> NodeResult<GoalUuid>;
2407
2408    /// Whether a server for the service client `service_entity` is CURRENTLY
2409    /// discoverable — `rclcpp::ClientBase::service_is_ready`, for a tick.
2410    ///
2411    /// phase-428 W13. Three answers: `Ok(true)`, `Ok(false)`, or `Err` when
2412    /// the backend cannot say (no discovery channel, or the entity is
2413    /// unknown). A tick that gates its first `call_raw` on `Ok(true)` and
2414    /// treats `Err` as "call anyway" keeps the request-is-the-probe behaviour
2415    /// on backends without discovery and stops sending into the void on the
2416    /// ones with it.
2417    ///
2418    /// Default `Err(NodeDeclError::Runtime)` — "cannot say" — so an
2419    /// implementor that predates this method (the orchestration codegen's
2420    /// dispatch) keeps its shape.
2421    fn service_is_ready(&self, service_entity: &str) -> NodeResult<bool> {
2422        let _ = service_entity;
2423        Err(NodeDeclError::Runtime)
2424    }
2425
2426    /// Whether a server for the action client `action_entity` is CURRENTLY
2427    /// discoverable — `rclcpp_action::Client::action_server_is_ready`, for a
2428    /// tick. Same three answers as [`Self::service_is_ready`], read off the
2429    /// action's `send_goal` service (issue 1918): a goal sent before the server
2430    /// has declared it is answered by nobody, and the declarative client sends
2431    /// ONCE.
2432    fn action_server_is_ready(&self, action_entity: &str) -> NodeResult<bool> {
2433        let _ = action_entity;
2434        Err(NodeDeclError::Runtime)
2435    }
2436}
2437
2438/// Context handed to [`ExecutableNode::tick`] (W.5.6 + M-F.4): the per-spin
2439/// hook that runs *between* callback dispatch, where the executor is free.
2440/// Exposes the immediate publish path (like `CallbackCtx`) plus executor-backed
2441/// action-server ops (complete goal / publish feedback) AND executor-backed
2442/// client-side ops (service `call` / action-client `send_goal`). Callbacks
2443/// can't perform any of these since they don't hold the executor.
2444pub struct TickCtx<'a> {
2445    publishers: &'a dyn PublisherResolver,
2446    actions: &'a mut dyn ActionExecutor,
2447    clients: &'a mut dyn ClientDispatch,
2448    /// Phase 264 W4c — the executor's volatile parameter store, threaded by the tick
2449    /// driver (`tick_one_cell` already holds the executor). `None` until
2450    /// `[param_services]` registers the store. Read with [`parameter`](Self::parameter).
2451    #[cfg(feature = "param-store")]
2452    params: Option<&'a crate::ParameterServer<'a>>,
2453    /// phase-426 W3 — WHICH node's parameters `parameter()` reads.
2454    ///
2455    /// One set of six parameter services is registered per node and the store
2456    /// is keyed the same way, so a context that carries the store without the
2457    /// key can only ever answer for one node — which is how every component on
2458    /// a multi-node executor came to read the primary's values.
2459    #[cfg(feature = "param-store")]
2460    param_node: nros_params::NodeKey,
2461}
2462
2463impl<'a> TickCtx<'a> {
2464    /// Build a tick context (called by the generated runtime each spin).
2465    pub fn new(
2466        publishers: &'a dyn PublisherResolver,
2467        actions: &'a mut dyn ActionExecutor,
2468        clients: &'a mut dyn ClientDispatch,
2469    ) -> Self {
2470        Self {
2471            publishers,
2472            actions,
2473            clients,
2474            #[cfg(feature = "param-store")]
2475            params: None,
2476            // phase-426 W3 — the store is keyed by node and this context does
2477            // not know which one until the dispatch site says so
2478            // (`set_param_server`). PRIMARY until then, which is the executor's
2479            // first node and what a single-node image means.
2480            #[cfg(feature = "param-store")]
2481            param_node: nros_params::NodeKey::PRIMARY,
2482        }
2483    }
2484
2485    /// Phase 264 W4c — thread the executor's volatile parameter store in (the tick
2486    /// driver holds the executor directly). No-op-equivalent when `None`.
2487    #[cfg(feature = "param-store")]
2488    pub fn set_param_server(
2489        &mut self,
2490        params: Option<&'a crate::ParameterServer<'a>>,
2491        node: nros_params::NodeKey,
2492    ) {
2493        self.params = params;
2494        self.param_node = node;
2495    }
2496
2497    /// Phase 264 W4c — read this node's parameter `name` as `T` during `tick`, or `None`
2498    /// if undeclared, the wrong type, or `[param_services]` is off. Returns the live
2499    /// value (baked initial or last `ros2 param set`; volatile — RFC-0004 §10).
2500    #[cfg(feature = "param-store")]
2501    pub fn parameter<T: crate::ParameterVariant>(&self, name: &str) -> Option<T> {
2502        self.params
2503            // phase-426 W3 — the store is keyed by node, and the key is the
2504            // component's OWN node, threaded in beside the store by the
2505            // dispatch site. Reading PRIMARY here (which is what this did
2506            // until W3) makes the second node on an executor read its
2507            // sibling's values.
2508            .and_then(|server| server.get(self.param_node, name))
2509            .and_then(T::from_parameter_value)
2510    }
2511
2512    /// Issue 2075 — reset the declared timer `timer` from `tick`: rclcpp's
2513    /// `TimerBase::reset()`. See [`CallbackCtx::reset_timer`]; from a tick the
2514    /// request is applied as soon as `tick` returns.
2515    pub fn reset_timer(&self, timer: EntityId<'_>) -> NodeResult<()> {
2516        self.publishers
2517            .request_timer(timer.as_str(), TimerOp::Reset)
2518    }
2519
2520    /// Issue 2075 — cancel the declared timer `timer` from `tick`: rclcpp's
2521    /// `TimerBase::cancel()`. See [`CallbackCtx::cancel_timer`].
2522    pub fn cancel_timer(&self, timer: EntityId<'_>) -> NodeResult<()> {
2523        self.publishers
2524            .request_timer(timer.as_str(), TimerOp::Cancel)
2525    }
2526
2527    /// Publish raw CDR bytes through the named publisher entity (immediate).
2528    #[doc(hidden)]
2529    pub fn publish_raw(&self, publisher: EntityId<'_>, data: &[u8]) -> NodeResult<()> {
2530        self.publishers.publish_raw(publisher.as_str(), data)
2531    }
2532
2533    /// Serialize `msg` into an `N`-byte stack buffer and publish it (immediate).
2534    #[doc(hidden)]
2535    pub fn publish<M: RosMessage, const N: usize>(
2536        &self,
2537        publisher: EntityId<'_>,
2538        msg: &M,
2539    ) -> NodeResult<()> {
2540        let mut buf = [0u8; N];
2541        let mut writer =
2542            crate::CdrWriter::new_with_header(&mut buf).map_err(|_| NodeDeclError::Runtime)?;
2543        msg.serialize(&mut writer)
2544            .map_err(|_| NodeDeclError::Runtime)?;
2545        let len = writer.position();
2546        self.publish_raw(publisher, &buf[..len])
2547    }
2548
2549    /// Serialize `msg` and publish through the entity synthesized from `topic`.
2550    ///
2551    /// This pairs with [`DeclarativeNode::create_publisher_for_topic`] for
2552    /// executable tick hooks.
2553    pub fn publish_to_topic<M: RosMessage, const N: usize>(
2554        &self,
2555        topic: &str,
2556        msg: &M,
2557    ) -> NodeResult<()> {
2558        self.publish::<M, N>(EntityId::new(topic), msg)
2559    }
2560
2561    /// Complete an action goal with a typed result (W.5.6 — needs the executor,
2562    /// hence tick-only).
2563    #[doc(hidden)]
2564    pub fn complete_goal<R: RosMessage, const N: usize>(
2565        &mut self,
2566        action: EntityId<'_>,
2567        goal_id: &GoalUuid,
2568        status: GoalStatusCode,
2569        result: &R,
2570    ) -> NodeResult<()> {
2571        // RFC-0069 / issue 0418 — NO inner encapsulation header. ROS 2's
2572        // `<Action>_GetResult_Response` is ONE CDR message: `[header][status][result
2573        // fields]`. This used to write a second header inside the envelope, which
2574        // made the payload `[outer][status][pad][INNER][fields]` — self-consistent
2575        // with nano-ros's own raw consumer and undecodable by any `rcl_action` peer
2576        // or by nano-ros's TYPED path.
2577        //
2578        // The issue-#35 corruption that motivated the old header ("the reader eats
2579        // the first data word … `sequence` deserialized to len 0") is now prevented
2580        // on the READ side instead: the executor splices the envelope's encap onto
2581        // the body before the callback sees it, so `CallbackCtx::message` still
2582        // receives a well-formed CDR message. Producer and consumer changed
2583        // together — either alone reproduces #35.
2584        let mut buf = [0u8; N];
2585        let mut writer = crate::CdrWriter::new(&mut buf);
2586        result
2587            .serialize(&mut writer)
2588            .map_err(|_| NodeDeclError::Runtime)?;
2589        let len = writer.position();
2590        self.actions
2591            .complete_goal_raw(action.as_str(), goal_id, status, &buf[..len])
2592    }
2593
2594    /// Complete an action goal on the action entity synthesized from `name`.
2595    ///
2596    /// This pairs with
2597    /// [`DeclarativeNode::create_action_server_for_name`] and
2598    /// [`DeclarativeNode::create_action_server_for_name_with_callbacks`].
2599    pub fn complete_goal_for_name<R: RosMessage, const N: usize>(
2600        &mut self,
2601        name: &str,
2602        goal_id: &GoalUuid,
2603        status: GoalStatusCode,
2604        result: &R,
2605    ) -> NodeResult<()> {
2606        self.complete_goal::<R, N>(EntityId::new(name), goal_id, status, result)
2607    }
2608
2609    /// Visit each active (accepted, not yet completed) goal on `action` with its
2610    /// id + status — how a `tick` body discovers goals to feed / complete. Collect
2611    /// the ids you want to act on, then call [`Self::publish_feedback`] /
2612    /// [`Self::complete_goal`] after the visit returns (those borrow `self`
2613    /// mutably, so they can't run inside `visit`).
2614    #[doc(hidden)]
2615    pub fn for_each_active_goal(
2616        &self,
2617        action: EntityId<'_>,
2618        visit: &mut dyn FnMut(&GoalUuid, GoalStatusCode),
2619    ) {
2620        self.actions.for_each_active_goal(action.as_str(), visit);
2621    }
2622
2623    /// Visit active goals on the action entity synthesized from `name`.
2624    pub fn for_each_active_goal_for_name(
2625        &self,
2626        name: &str,
2627        visit: &mut dyn FnMut(&GoalUuid, GoalStatusCode),
2628    ) {
2629        self.for_each_active_goal(EntityId::new(name), visit);
2630    }
2631
2632    /// Publish typed feedback for an active action goal (W.5.6 — tick-only).
2633    #[doc(hidden)]
2634    pub fn publish_feedback<F: RosMessage, const N: usize>(
2635        &mut self,
2636        action: EntityId<'_>,
2637        goal_id: &GoalUuid,
2638        feedback: &F,
2639    ) -> NodeResult<()> {
2640        // RFC-0069 / issue 0418 — NO inner encapsulation header; see
2641        // `complete_goal` above. ROS 2's `<Action>_FeedbackMessage` is ONE CDR
2642        // message: `[header][goal_id][feedback fields]`. The executor frames
2643        // `[header][goal_id]` and this payload is the fields alone.
2644        let mut buf = [0u8; N];
2645        let mut writer = crate::CdrWriter::new(&mut buf);
2646        feedback
2647            .serialize(&mut writer)
2648            .map_err(|_| NodeDeclError::Runtime)?;
2649        let len = writer.position();
2650        self.actions
2651            .publish_feedback_raw(action.as_str(), goal_id, &buf[..len])
2652    }
2653
2654    /// Publish feedback on the action entity synthesized from `name`.
2655    pub fn publish_feedback_for_name<F: RosMessage, const N: usize>(
2656        &mut self,
2657        name: &str,
2658        goal_id: &GoalUuid,
2659        feedback: &F,
2660    ) -> NodeResult<()> {
2661        self.publish_feedback::<F, N>(EntityId::new(name), goal_id, feedback)
2662    }
2663
2664    /// Issue a service-client raw-CDR request and block on the reply
2665    /// (M-F.4 — tick-only). Writes the response CDR into `response_buf`
2666    /// and returns the response length in bytes.
2667    #[doc(hidden)]
2668    pub fn call_raw(
2669        &mut self,
2670        service: EntityId<'_>,
2671        request_cdr: &[u8],
2672        response_buf: &mut [u8],
2673    ) -> NodeResult<usize> {
2674        self.clients
2675            .call_raw(service.as_str(), request_cdr, response_buf)
2676    }
2677
2678    /// Whether a server for the service client `service` is CURRENTLY
2679    /// discoverable (M-F.4 — tick-only). Mirrors
2680    /// `rclcpp::ClientBase::service_is_ready`; see
2681    /// [`ClientDispatch::service_is_ready`] for the three answers. There is no
2682    /// blocking `wait_for_service` here because a tick must not block the
2683    /// executor that drives it — check once per tick and return, which is the
2684    /// rclcpp `while (!wait_for_service(1s)) { "waiting again..." }` idiom
2685    /// with the timer as the cadence (phase-428 W13).
2686    #[doc(hidden)]
2687    pub fn service_is_ready(&self, service: EntityId<'_>) -> NodeResult<bool> {
2688        self.clients.service_is_ready(service.as_str())
2689    }
2690
2691    /// [`Self::service_is_ready`] through the entity synthesized from `name`.
2692    pub fn service_is_ready_for_name(&self, name: &str) -> NodeResult<bool> {
2693        self.service_is_ready(EntityId::new(name))
2694    }
2695
2696    /// Whether a server for the action client `action` is CURRENTLY
2697    /// discoverable (tick-only) — `rclcpp_action::Client::action_server_is_ready`.
2698    /// Gate a goal on `Ok(true)` and treat `Err` ("the backend cannot say") as
2699    /// "send anyway": a goal sent to a server nobody has declared yet is lost,
2700    /// because the send is not retried (issue 1918). Check once per tick, the
2701    /// way [`Self::service_is_ready`] documents.
2702    #[doc(hidden)]
2703    pub fn action_server_is_ready(&self, action: EntityId<'_>) -> NodeResult<bool> {
2704        self.clients.action_server_is_ready(action.as_str())
2705    }
2706
2707    /// [`Self::action_server_is_ready`] through the entity synthesized from `name`.
2708    pub fn action_server_is_ready_for_name(&self, name: &str) -> NodeResult<bool> {
2709        self.action_server_is_ready(EntityId::new(name))
2710    }
2711
2712    /// Issue a raw service-client request through the entity synthesized
2713    /// from `name`.
2714    pub fn call_raw_for_name(
2715        &mut self,
2716        name: &str,
2717        request_cdr: &[u8],
2718        response_buf: &mut [u8],
2719    ) -> NodeResult<usize> {
2720        self.call_raw(EntityId::new(name), request_cdr, response_buf)
2721    }
2722
2723    /// Issue a typed service-client request and decode the reply
2724    /// (M-F.4 — tick-only). `REQ_N` / `RESP_N` stack-size the request /
2725    /// response CDR buffers; size them via
2726    /// `<<Req as RosMessage>::SerializedSize as nros::SerializedSize>::SIZE`.
2727    #[doc(hidden)]
2728    pub fn call<Req: RosMessage, Resp: RosMessage, const REQ_N: usize, const RESP_N: usize>(
2729        &mut self,
2730        service: EntityId<'_>,
2731        request: &Req,
2732    ) -> NodeResult<Resp> {
2733        let mut req_buf = [0u8; REQ_N];
2734        let mut writer =
2735            crate::CdrWriter::new_with_header(&mut req_buf).map_err(|_| NodeDeclError::Runtime)?;
2736        request
2737            .serialize(&mut writer)
2738            .map_err(|_| NodeDeclError::Runtime)?;
2739        let req_len = writer.position();
2740
2741        let mut resp_buf = [0u8; RESP_N];
2742        let resp_len =
2743            self.clients
2744                .call_raw(service.as_str(), &req_buf[..req_len], &mut resp_buf)?;
2745
2746        let mut reader = crate::CdrReader::new_with_header(&resp_buf[..resp_len])
2747            .map_err(|_| NodeDeclError::Runtime)?;
2748        Resp::deserialize(&mut reader).map_err(|_| NodeDeclError::Runtime)
2749    }
2750
2751    /// Issue a typed service-client request through the entity synthesized
2752    /// from `name`.
2753    pub fn call_for_name<
2754        Req: RosMessage,
2755        Resp: RosMessage,
2756        const REQ_N: usize,
2757        const RESP_N: usize,
2758    >(
2759        &mut self,
2760        name: &str,
2761        request: &Req,
2762    ) -> NodeResult<Resp> {
2763        self.call::<Req, Resp, REQ_N, RESP_N>(EntityId::new(name), request)
2764    }
2765
2766    /// Send a raw-CDR action-client goal and return the assigned
2767    /// [`GoalUuid`] (M-F.4 — tick-only). Result + feedback streams arrive
2768    /// via callback dispatch; this method only kicks off the request.
2769    #[doc(hidden)]
2770    pub fn send_goal_raw(&mut self, action: EntityId<'_>, goal_cdr: &[u8]) -> NodeResult<GoalUuid> {
2771        self.clients.send_goal_raw(action.as_str(), goal_cdr)
2772    }
2773
2774    /// Send a raw-CDR action-client goal through the entity synthesized
2775    /// from `name`.
2776    pub fn send_goal_raw_for_name(&mut self, name: &str, goal_cdr: &[u8]) -> NodeResult<GoalUuid> {
2777        self.send_goal_raw(EntityId::new(name), goal_cdr)
2778    }
2779
2780    /// Send a typed action-client goal and return the assigned
2781    /// [`GoalUuid`] (M-F.4 — tick-only). `N` stack-sizes the goal CDR
2782    /// buffer.
2783    #[doc(hidden)]
2784    pub fn send_goal<G: RosMessage, const N: usize>(
2785        &mut self,
2786        action: EntityId<'_>,
2787        goal: &G,
2788    ) -> NodeResult<GoalUuid> {
2789        // RFC-0069 / issue 0418 — NO inner encapsulation header, same rule as
2790        // `publish_feedback` / `complete_goal`. ROS 2's `<Action>_SendGoal_Request`
2791        // is ONE CDR message: `[header][goal_id][goal fields]`. `send_goal_raw`
2792        // frames `[header][goal_id]`, so this payload is the fields alone.
2793        //
2794        // Issue 0448: this path used `new_with_header` and shipped a SECOND
2795        // encapsulation (`header|uuid|header|fields`) — 4 bytes over the ROS 2
2796        // layout. Fast-DDS sizes its reader history from the type and drops the
2797        // sample outright ("Change payload size of '28' bytes is larger than the
2798        // history payload size of '27'"), so the goal never reached the server
2799        // and the client saw a zeroed default result. nros-c / nros-cpp already
2800        // stripped the header; only this Rust path was missed.
2801        //
2802        // Confirmed independently while root-causing issue 0461: the extra
2803        // header also made every SERVER decode the wrong offset over zenoh
2804        // (C/C++ read the inner header as the first field, so an order of 10
2805        // arrived as 256). Same defect, a second symptom.
2806        let mut buf = [0u8; N];
2807        let mut writer = crate::CdrWriter::new(&mut buf);
2808        goal.serialize(&mut writer)
2809            .map_err(|_| NodeDeclError::Runtime)?;
2810        let len = writer.position();
2811        self.clients.send_goal_raw(action.as_str(), &buf[..len])
2812    }
2813
2814    /// Send a typed action-client goal through the entity synthesized
2815    /// from `name`.
2816    pub fn send_goal_for_name<G: RosMessage, const N: usize>(
2817        &mut self,
2818        name: &str,
2819        goal: &G,
2820    ) -> NodeResult<GoalUuid> {
2821        self.send_goal::<G, N>(EntityId::new(name), goal)
2822    }
2823}
2824
2825#[cfg(feature = "rmw-cffi")]
2826pub trait ExecutableNode: Component {
2827    /// Per-instance mutable state shared across the component's callbacks.
2828    type State;
2829
2830    /// Build the initial state (called once by the generated runtime).
2831    fn init() -> Self::State;
2832
2833    /// Run the body for `callback`. `ctx` exposes the triggering payload + the
2834    /// immediate publish path. Bodies match on the source callback name declared
2835    /// by `create_*_for_callback_name` and related helpers.
2836    fn on_callback(state: &mut Self::State, callback: Callback<'_>, ctx: &mut CallbackCtx<'_>);
2837
2838    /// Per-spin execution hook (W.5.6), run *between* callback dispatch by the
2839    /// generated runtime — where the executor is free, so this is the only place
2840    /// a component can complete action goals / publish feedback (via `ctx`) or do
2841    /// periodic work. Default: no-op (timer/sub/service-only components).
2842    fn tick(_state: &mut Self::State, _ctx: &mut TickCtx<'_>) {}
2843
2844    /// phase-521 W6 (RFC-0113 D2) — whether [`tick`](Self::tick) does any
2845    /// work. A tick runs once per spin, so while any registered component
2846    /// ticks the entry keeps a 10 ms spin cadence; once none does, the entry
2847    /// parks until its next deadline or arrival. Defaults to `true`, the safe
2848    /// reading for a component that overrides `tick`. A component with no
2849    /// tick says `false`.
2850    const HAS_TICK: bool = true;
2851}
2852
2853/// Emit a no-op [`ExecutableNode`] impl for a declarative-only component
2854/// (W.5.1). The generated runtime calls `on_callback` unconditionally, so a
2855/// component instantiated into a generated binary must impl `ExecutableNode`;
2856/// components without callback bodies use this to satisfy that contract:
2857///
2858/// ```ignore
2859/// pub struct Node;
2860/// impl nros::Component for Node { /* register(...) */ }
2861/// nros::declarative_component!(Node);
2862/// ```
2863#[macro_export]
2864macro_rules! declarative_component {
2865    ($ty:ty) => {
2866        impl $crate::ExecutableNode for $ty {
2867            type State = ();
2868            const HAS_TICK: bool = false;
2869            fn init() -> Self::State {}
2870            fn on_callback(
2871                _state: &mut Self::State,
2872                _callback: $crate::Callback<'_>,
2873                _ctx: &mut $crate::CallbackCtx<'_>,
2874            ) {
2875            }
2876        }
2877    };
2878}
2879
2880/// Run component registration against a metadata recorder, on `executor`.
2881///
2882/// phase-483 W3: registration creates real nodes, so it needs an executor even
2883/// on the probe's road — open one on the `metadata` backend
2884/// (`ExecutorConfig::new("").rmw("metadata")`). The recorder receives every
2885/// declaration exactly as it did when it was the whole runtime.
2886#[cfg(feature = "rmw-cffi")]
2887pub fn register_node<C: Component>(
2888    recorder: &mut dyn NodeRuntime,
2889    executor: &mut crate::Executor<'static>,
2890) -> NodeResult<()> {
2891    let mut sink = RecordSink { recorder };
2892    let mut frame = ComponentFrame::new(&mut sink);
2893    let mut context = NodeContext::new(C::NAME, executor, &mut frame);
2894    C::register(&mut context)
2895}
2896
2897/// Run `f` against a [`NodeContext`] whose declarations go to `recorder`,
2898/// on `executor` (opened on the `metadata` backend). The tests of the
2899/// declarative surface use it; the probe uses [`record_node_metadata`].
2900#[cfg(feature = "rmw-cffi")]
2901#[doc(hidden)]
2902pub fn __record_with_context<T>(
2903    component_name: &'static str,
2904    recorder: &mut dyn NodeRuntime,
2905    executor: &mut crate::Executor<'static>,
2906    f: impl FnOnce(&mut NodeContext<'_>) -> T,
2907) -> T {
2908    let mut sink = RecordSink { recorder };
2909    let mut frame = ComponentFrame::new(&mut sink);
2910    let mut context = NodeContext::new(component_name, executor, &mut frame);
2911    f(&mut context)
2912}
2913
2914/// Run component registration against an in-memory metadata recorder, on an
2915/// executor the caller opened on the `metadata` backend.
2916#[cfg(feature = "rmw-cffi")]
2917pub fn record_node_metadata<C: Component>(
2918    recorder: &mut dyn NodeRuntime,
2919    executor: &mut crate::Executor<'static>,
2920) -> NodeResult<()> {
2921    register_node::<C>(recorder, executor)
2922}
2923
2924#[cfg(test)]
2925mod tests {
2926    use super::*;
2927    use crate::{CdrReader, CdrWriter, DeserError, SerError};
2928
2929    #[derive(Debug, Clone, Copy, Default)]
2930    struct TestMsg;
2931
2932    impl crate::Serialize for TestMsg {
2933        fn serialize(&self, _writer: &mut CdrWriter) -> Result<(), SerError> {
2934            Ok(())
2935        }
2936    }
2937
2938    impl crate::Deserialize for TestMsg {
2939        fn deserialize(_reader: &mut CdrReader) -> Result<Self, DeserError> {
2940            Ok(Self)
2941        }
2942    }
2943
2944    impl RosMessage for TestMsg {
2945        const TYPE_NAME: &'static str = "test_msgs::msg::dds_::Test_";
2946        const TYPE_HASH: &'static str = "test_hash";
2947    }
2948
2949    // Phase 380 W4 — `MessageForRmw` now also requires a field schema, because
2950    // that is where a subscription's size bound comes from. `TestMsg` is an
2951    // empty struct, so its schema is the empty slice and its bound is just the
2952    // encapsulation header — which is exactly what the build assertion should
2953    // see for it.
2954    impl nros_serdes::schema::Message for TestMsg {
2955        const TYPE_NAME: &'static str = "test_msgs/msg/Test";
2956        const FIELDS: &'static [nros_serdes::schema::Field] = &[];
2957    }
2958
2959    #[cfg(feature = "rmw-cffi")]
2960    struct TalkerComponent;
2961
2962    #[cfg(feature = "rmw-cffi")]
2963    impl Component for TalkerComponent {
2964        const NAME: &'static str = "talker_component";
2965
2966        fn register(context: &mut NodeContext<'_>) -> NodeResult<()> {
2967            let mut node =
2968                context.create_node_with_id(NodeId::new("node"), NodeOptions::new("talker"))?;
2969            let _publisher =
2970                node.declare_publisher::<TestMsg>(EntityId::new("pub_chatter"), "chatter")?;
2971            let _subscription = node.declare_subscription::<TestMsg>(
2972                EntityId::new("sub_cmd"),
2973                CallbackId::new("on_cmd"),
2974                "~/cmd",
2975            )?;
2976            let _timer = node.declare_timer(
2977                EntityId::new("timer_tick"),
2978                CallbackId::new("on_tick"),
2979                TimerDuration::from_millis(10),
2980            )?;
2981            let _parameter = node.declare_parameter_with_id(
2982                EntityId::new("param_gain"),
2983                "gain",
2984                ParameterType::Double,
2985            )?;
2986            node.callback(CallbackId::new("on_tick"))
2987                .publishes(EntityId::new("pub_chatter"))?
2988                .writes(EntityId::new("param_gain"))?;
2989            Ok(())
2990        }
2991    }
2992
2993    #[test]
2994    fn component_missing_export_error_message_is_clear() {
2995        assert_eq!(
2996            NodeDeclError::MissingExport.message(),
2997            "package has no exported nros component"
2998        );
2999    }
3000
3001    // W.5.1 — an executable component callback runs its body: mutates state +
3002    // publishes immediately through the resolver (the substrate the generator
3003    // will wire). `TalkerComponent` already impls `Node` (declarative);
3004    // here it also impls `ExecutableNode`.
3005    #[cfg(feature = "rmw-cffi")]
3006    impl ExecutableNode for TalkerComponent {
3007        type State = u32;
3008
3009        fn init() -> u32 {
3010            0
3011        }
3012
3013        fn on_callback(state: &mut u32, callback: Callback<'_>, ctx: &mut CallbackCtx<'_>) {
3014            if callback.as_str() == "on_tick" {
3015                *state += 1;
3016                // Publish through the declared publisher entity.
3017                let _ = ctx.publish::<TestMsg, 64>(EntityId::new("pub_chatter"), &TestMsg);
3018            }
3019        }
3020    }
3021
3022    #[cfg(feature = "rmw-cffi")]
3023    #[test]
3024    fn executable_component_callback_publishes_and_mutates_state() {
3025        use core::cell::RefCell;
3026
3027        struct RecordingResolver {
3028            last: RefCell<Option<(MetadataString, usize)>>,
3029        }
3030        impl PublisherResolver for RecordingResolver {
3031            fn publish_raw(&self, entity_id: &str, data: &[u8]) -> NodeResult<()> {
3032                *self.last.borrow_mut() = Some((copy_str(entity_id)?, data.len()));
3033                Ok(())
3034            }
3035        }
3036
3037        let resolver = RecordingResolver {
3038            last: RefCell::new(None),
3039        };
3040        let mut state = TalkerComponent::init();
3041        let mut ctx = CallbackCtx::new(&[], &resolver);
3042
3043        // An unrelated callback id does nothing.
3044        TalkerComponent::on_callback(
3045            &mut state,
3046            Callback::__from_id(CallbackId::new("other")),
3047            &mut ctx,
3048        );
3049        assert_eq!(state, 0);
3050        assert!(resolver.last.borrow().is_none());
3051
3052        // The bound callback bumps state + publishes through "pub_chatter".
3053        TalkerComponent::on_callback(
3054            &mut state,
3055            Callback::__from_id(CallbackId::new("on_tick")),
3056            &mut ctx,
3057        );
3058        assert_eq!(state, 1);
3059        let last = resolver.last.borrow();
3060        let (entity, len) = last.as_ref().expect("a publish was recorded");
3061        assert_eq!(entity.as_str(), "pub_chatter");
3062        // Empty TestMsg ⇒ just the 4-byte CDR header.
3063        assert_eq!(*len, 4);
3064    }
3065
3066    // W.5.3 — a service-style body writes its reply through the CallbackCtx
3067    // reply sink; the trampoline reads `*written` back. A timer/sub ctx (no
3068    // sink) rejects a reply.
3069    #[test]
3070    fn callback_ctx_reply_sink_roundtrips() {
3071        struct NoopResolver;
3072        impl PublisherResolver for NoopResolver {
3073            fn publish_raw(&self, _entity_id: &str, _data: &[u8]) -> NodeResult<()> {
3074                Ok(())
3075            }
3076        }
3077        let resolver = NoopResolver;
3078        let mut reply_buf = [0u8; 64];
3079        let mut written = 0usize;
3080        {
3081            let mut ctx = CallbackCtx::with_reply(&[], &resolver, &mut reply_buf, &mut written);
3082            ctx.reply::<TestMsg, 64>(&TestMsg).unwrap();
3083        }
3084        // Empty TestMsg ⇒ just the 4-byte CDR header.
3085        assert_eq!(written, 4);
3086
3087        // A reply-less ctx (timer / subscription) rejects a reply.
3088        let mut ctx2 = CallbackCtx::new(&[], &resolver);
3089        assert!(ctx2.reply_raw(&[1, 2, 3]).is_err());
3090    }
3091
3092    // Issue 2038 — a context carries a timer's call info only when the
3093    // dispatch site attached one; every constructor starts without.
3094    #[test]
3095    fn callback_ctx_timer_info_is_attached_by_the_dispatch_site() {
3096        struct NoopResolver;
3097        impl PublisherResolver for NoopResolver {
3098            fn publish_raw(&self, _entity_id: &str, _data: &[u8]) -> NodeResult<()> {
3099                Ok(())
3100            }
3101        }
3102        let resolver = NoopResolver;
3103        let mut ctx = CallbackCtx::new(&[], &resolver);
3104        assert!(ctx.timer_info().is_none());
3105        let info = crate::TimerCallInfo {
3106            expected_call_time: 1_000,
3107            actual_call_time: 1_250,
3108            clock: crate::TimerClockSource::Ros,
3109            ..Default::default()
3110        };
3111        ctx.set_timer_info(info);
3112        assert_eq!(ctx.timer_info(), Some(&info));
3113        assert_eq!(ctx.timer_info().map(|i| i.lateness_ns()), Some(250));
3114    }
3115
3116    // Phase 264 W4c — a callback reads the live parameter value through
3117    // `CallbackCtx::parameter::<T>` once the dispatch site threads the store in.
3118    #[cfg(feature = "param-store")]
3119    #[test]
3120    fn callback_ctx_reads_param() {
3121        struct NoopResolver;
3122        impl PublisherResolver for NoopResolver {
3123            fn publish_raw(&self, _entity_id: &str, _data: &[u8]) -> NodeResult<()> {
3124                Ok(())
3125            }
3126        }
3127        let resolver = NoopResolver;
3128
3129        // No store threaded ⇒ every read is None (param-services off / not registered).
3130        let ctx_none = CallbackCtx::new(&[], &resolver);
3131        assert_eq!(ctx_none.parameter::<i64>("speed"), None);
3132
3133        // Seed a store with the typed value a `ros2 param set speed 7` would land on.
3134        // phase-382 W2' — the slots are the caller's; a local `ParameterStorage`
3135        // is the shape a test wants, sized for what it actually declares rather
3136        // than the build-time `MAX_PARAMETERS` default.
3137        // phase-426 W1/W3 — every store call names the node it is about.
3138        // `talker` is the executor's first node, `listener` its second.
3139        let talker = nros_params::NodeKey::PRIMARY;
3140        let listener = nros_params::NodeKey::new(1);
3141        let mut storage = crate::ParameterStorage::<4>::new();
3142        let mut server = crate::ParameterServer::new_in(storage.as_table());
3143        assert!(server.declare(talker, "speed", crate::ParameterValue::Integer(7)));
3144        assert!(server.declare(listener, "speed", crate::ParameterValue::Integer(99)));
3145
3146        let mut ctx = CallbackCtx::new(&[], &resolver);
3147        ctx.set_param_server(Some(&server), talker);
3148        assert_eq!(ctx.parameter::<i64>("speed"), Some(7));
3149        // Wrong type ⇒ None, not a panic.
3150        assert_eq!(ctx.parameter::<bool>("speed"), None);
3151        // Undeclared ⇒ None.
3152        assert_eq!(ctx.parameter::<i64>("missing"), None);
3153
3154        // phase-426 W3 — the SIBLING's identically-named parameter is a
3155        // different parameter, and the context reads the node it was given.
3156        // Before W3 this ctx was pinned to PRIMARY, so a listener component
3157        // read the talker's 7.
3158        let mut sibling = CallbackCtx::new(&[], &resolver);
3159        sibling.set_param_server(Some(&server), listener);
3160        assert_eq!(sibling.parameter::<i64>("speed"), Some(99));
3161    }
3162
3163    // Phase 250 Wave 2 — the declarative `.safety()` surface: a normal ctx has
3164    // no integrity status; one built with `new_with_integrity` exposes it, read
3165    // alongside the message in the same callback (Shape A).
3166    #[cfg(feature = "safety-e2e")]
3167    #[test]
3168    fn callback_ctx_integrity_surface() {
3169        struct NoopResolver;
3170        impl PublisherResolver for NoopResolver {
3171            fn publish_raw(&self, _entity_id: &str, _data: &[u8]) -> NodeResult<()> {
3172                Ok(())
3173            }
3174        }
3175        let resolver = NoopResolver;
3176
3177        // Non-safety dispatch (timer / plain sub) → None.
3178        let ctx = CallbackCtx::new(&[], &resolver);
3179        assert!(ctx.integrity().is_none());
3180
3181        // Safety dispatch → the status rides alongside the payload.
3182        let status = crate::IntegrityStatus {
3183            gap: 2,
3184            duplicate: false,
3185            crc_valid: Some(true),
3186        };
3187        let ctx = CallbackCtx::new_with_integrity(&[], &resolver, &status);
3188        let got = ctx.integrity().expect("safety ctx carries status");
3189        assert_eq!(got.gap, 2);
3190        assert!(!got.duplicate);
3191        assert_eq!(got.crc_valid, Some(true));
3192    }
3193
3194    // W.5.3 — an action goal / cancel body sets its accept/reject decision
3195    // through the CallbackCtx decision sink; the trampoline returns `*out`. A
3196    // wrong-kind setter (or a sink-less ctx) errors.
3197    #[test]
3198    fn callback_ctx_decision_sink() {
3199        struct NoopResolver;
3200        impl PublisherResolver for NoopResolver {
3201            fn publish_raw(&self, _entity_id: &str, _data: &[u8]) -> NodeResult<()> {
3202                Ok(())
3203            }
3204        }
3205        let resolver = NoopResolver;
3206
3207        let mut gr = GoalResponse::Reject;
3208        {
3209            let mut ctx = CallbackCtx::with_goal_decision(&[], &resolver, &mut gr);
3210            ctx.set_goal_response(GoalResponse::AcceptAndExecute)
3211                .unwrap();
3212            // Wrong-kind setter on a goal ctx errors.
3213            assert!(ctx.set_cancel_response(CancelResponse::Accept).is_err());
3214        }
3215        assert!(matches!(gr, GoalResponse::AcceptAndExecute));
3216
3217        let mut cr = CancelResponse::Reject;
3218        {
3219            let mut ctx = CallbackCtx::with_cancel_decision(&[], &resolver, &mut cr);
3220            ctx.set_cancel_response(CancelResponse::Accept).unwrap();
3221        }
3222        assert!(matches!(cr, CancelResponse::Accept));
3223
3224        // A timer/sub ctx (no decision sink) rejects both.
3225        let mut ctx3 = CallbackCtx::new(&[], &resolver);
3226        assert!(ctx3.set_goal_response(GoalResponse::Reject).is_err());
3227        assert!(ctx3.set_cancel_response(CancelResponse::Accept).is_err());
3228    }
3229
3230    // W.5.6 — the tick hook publishes (immediate) + drives executor-backed action
3231    // ops (complete goal / publish feedback) through the ActionExecutor seam.
3232    #[test]
3233    fn tick_ctx_publish_and_action_ops() {
3234        use core::cell::Cell;
3235        struct RecPub {
3236            published: Cell<bool>,
3237        }
3238        impl PublisherResolver for RecPub {
3239            fn publish_raw(&self, _entity_id: &str, _data: &[u8]) -> NodeResult<()> {
3240                self.published.set(true);
3241                Ok(())
3242            }
3243        }
3244        struct RecAct {
3245            completed: bool,
3246            fed: bool,
3247            visited: usize,
3248        }
3249        impl ActionExecutor for RecAct {
3250            fn complete_goal_raw(
3251                &mut self,
3252                _action_entity: &str,
3253                _goal_id: &GoalUuid,
3254                _status: GoalStatusCode,
3255                _result: &[u8],
3256            ) -> NodeResult<()> {
3257                self.completed = true;
3258                Ok(())
3259            }
3260            fn publish_feedback_raw(
3261                &mut self,
3262                _action_entity: &str,
3263                _goal_id: &GoalUuid,
3264                _feedback: &[u8],
3265            ) -> NodeResult<()> {
3266                self.fed = true;
3267                Ok(())
3268            }
3269            fn for_each_active_goal(
3270                &self,
3271                _action_entity: &str,
3272                visit: &mut dyn FnMut(&GoalUuid, GoalStatusCode),
3273            ) {
3274                // One pretend-active goal, so the tick body has something to drive.
3275                visit(&GoalUuid::zero(), GoalStatusCode::Executing);
3276            }
3277        }
3278
3279        struct RecClients;
3280        impl ClientDispatch for RecClients {
3281            fn call_raw(
3282                &mut self,
3283                _service: &str,
3284                _req: &[u8],
3285                _resp: &mut [u8],
3286            ) -> NodeResult<usize> {
3287                Err(NodeDeclError::Runtime)
3288            }
3289            fn send_goal_raw(&mut self, _action: &str, _goal: &[u8]) -> NodeResult<GoalUuid> {
3290                Err(NodeDeclError::Runtime)
3291            }
3292        }
3293
3294        let pubs = RecPub {
3295            published: Cell::new(false),
3296        };
3297        let mut acts = RecAct {
3298            completed: false,
3299            fed: false,
3300            visited: 0,
3301        };
3302        let mut clients = RecClients;
3303        let goal = GoalUuid::zero();
3304        let mut seen = 0usize;
3305        {
3306            let mut ctx = TickCtx::new(&pubs, &mut acts, &mut clients);
3307            ctx.publish::<TestMsg, 64>(EntityId::new("pub_x"), &TestMsg)
3308                .unwrap();
3309            // Discover the active goal the way a real tick body does, then act on it.
3310            ctx.for_each_active_goal(EntityId::new("act"), &mut |_id, _status| seen += 1);
3311            ctx.publish_feedback::<TestMsg, 64>(EntityId::new("act"), &goal, &TestMsg)
3312                .unwrap();
3313            ctx.complete_goal::<TestMsg, 64>(
3314                EntityId::new("act"),
3315                &goal,
3316                GoalStatusCode::Succeeded,
3317                &TestMsg,
3318            )
3319            .unwrap();
3320        }
3321        acts.visited = seen;
3322        assert!(pubs.published.get());
3323        assert!(acts.completed);
3324        assert!(acts.fed);
3325        assert_eq!(acts.visited, 1);
3326    }
3327
3328    /// Phase 216.A.3 — `Node::DISPATCH` defaults to
3329    /// `DispatchStrategy::Inline` so every pre-216 `impl Node`
3330    /// keeps compiling unchanged.
3331    #[cfg(feature = "rmw-cffi")]
3332    #[test]
3333    fn node_dispatch_default_is_inline() {
3334        struct Dummy;
3335        impl Component for Dummy {
3336            const NAME: &'static str = "dummy";
3337            fn register(_: &mut NodeContext<'_>) -> NodeResult<()> {
3338                Ok(())
3339            }
3340        }
3341        assert_eq!(Dummy::DISPATCH, crate::DispatchStrategy::Inline);
3342    }
3343
3344    // Phase 216.A.5 — `nros::node!()` emits the
3345    // `__nros_node_<pkg>_dispatch_strategy()` ABI export. We invoke the
3346    // macro on a dummy Node + ExecutableNode pair in a private sub-module
3347    // here so the macro expansion lives inside the `nros` crate itself;
3348    // the emitted `#[unsafe(no_mangle)] extern "C"` symbol is global, so
3349    // the test below re-declares + calls it. If the macro stopped
3350    // emitting the symbol (or renamed it) this would link-fail.
3351    //
3352    // `<pkg>` resolves to `CARGO_PKG_NAME` after
3353    // `sanitize_pkg_name_for_symbol`. The `nros` crate's pkg name is
3354    // literal `nros`, so the expected symbol is
3355    // `__nros_node_nros_dispatch_strategy`.
3356    // Phase 216 final wave — the macro emit now references
3357    // `::nros::Executor` (rmw-cffi-gated) in addition to the existing
3358    // alloc-gated `__private_node_state_into_raw`. Gate the test on
3359    // both features so the macro invocation only attempts to expand
3360    // when every referenced symbol is present.
3361    #[cfg(all(feature = "alloc", feature = "rmw-cffi", feature = "macros"))]
3362    mod dispatch_probe_macro_test {
3363        // `extern crate self as nros;` at the crate root (in `lib.rs`,
3364        // `cfg(test)`-gated) lets the `::nros::*` paths the macro emits
3365        // resolve in-crate.
3366        use super::*;
3367
3368        pub struct DispatchProbe;
3369
3370        impl Component for DispatchProbe {
3371            const NAME: &'static str = "dispatch_probe";
3372            // Default `DISPATCH = Inline` ⇒ discriminant 0.
3373            fn register(_: &mut NodeContext<'_>) -> NodeResult<()> {
3374                Ok(())
3375            }
3376        }
3377
3378        impl ExecutableNode for DispatchProbe {
3379            type State = ();
3380            fn init() -> Self::State {}
3381            fn on_callback(
3382                _state: &mut Self::State,
3383                _callback: Callback<'_>,
3384                _ctx: &mut CallbackCtx<'_>,
3385            ) {
3386            }
3387        }
3388
3389        // Emits both the per-pkg `register` wrapper AND the new
3390        // `__nros_node_nros_dispatch_strategy` ABI symbol.
3391        nros_macros::node!(DispatchProbe);
3392    }
3393
3394    // Also `macros`: this asserts the ABI symbol the `node!` invocation above
3395    // emits, so without the macro there is nothing to assert and the extern
3396    // would not resolve.
3397    #[cfg(all(feature = "alloc", feature = "rmw-cffi", feature = "macros"))]
3398    #[test]
3399    fn node_macro_emits_dispatch_strategy_symbol() {
3400        // Re-declare the ABI export the macro just emitted. If the macro
3401        // elides the symbol (or renames it) this fails to link — exactly
3402        // the regression the test is meant to catch.
3403        unsafe extern "C" {
3404            fn __nros_node_nros_dispatch_strategy() -> u8;
3405        }
3406        let strategy = unsafe { __nros_node_nros_dispatch_strategy() };
3407        // The probe Node uses the default `DISPATCH = Inline`
3408        // (discriminant 0) — confirms the macro is splicing
3409        // `<Type as Component>::DISPATCH as u8`, not a hard-coded zero.
3410        assert_eq!(strategy, crate::DispatchStrategy::Inline as u8);
3411        assert_eq!(strategy, 0);
3412    }
3413}