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}