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