1use core::marker::PhantomData;
4
5use crate::{
6 ActionTag, CallbackId, CancelResponse, EntityId, GoalId, GoalResponse, GoalStatus,
7 ParameterType, QosSettings, RosAction, RosMessage, RosService, ServiceTag, SubscriptionTag,
8 TimerDuration,
9 heapless::Vec,
10 node_metadata::{
11 CallbackEffectKind, CallbackEffectMetadata, CallbackSlot, EntityKind, EntityMetadata,
12 EntityMetadataSpec, EntitySlot, MetadataRecorder, MetadataString, NodeId,
13 NodeMetadataError, NodeSlot, ParameterDefault, SourceLocationMetadata, copy_str,
14 entity_callback_ids, entity_metadata,
15 },
16};
17
18pub const MISSING_NODE_EXPORT_ERROR: &str = "package has no exported nros component";
28
29pub type NodeResult<T = ()> = Result<T, NodeDeclError>;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum NodeDeclError {
35 Metadata(NodeMetadataError),
37 MissingExport,
39 Runtime,
41}
42
43impl NodeDeclError {
44 pub const fn message(self) -> &'static str {
46 match self {
47 Self::Metadata(NodeMetadataError::Capacity) => "component metadata capacity exceeded",
48 Self::Metadata(NodeMetadataError::NameTooLong) => "component metadata name too long",
49 Self::Metadata(NodeMetadataError::UnknownNode) => {
50 "component entity references an unknown node"
51 }
52 Self::Metadata(NodeMetadataError::UnknownEntity) => {
53 "component callback effect references an unknown entity"
54 }
55 Self::Metadata(NodeMetadataError::DuplicateId) => {
56 "component metadata contains a duplicate stable ID"
57 }
58 Self::MissingExport => MISSING_NODE_EXPORT_ERROR,
59 Self::Runtime => "component runtime rejected declaration",
60 }
61 }
62}
63
64impl From<NodeMetadataError> for NodeDeclError {
65 fn from(value: NodeMetadataError) -> Self {
66 Self::Metadata(value)
67 }
68}
69
70pub trait Node {
72 const NAME: &'static str;
74
75 const DISPATCH: crate::DispatchStrategy = crate::DispatchStrategy::Inline;
82
83 fn register(context: &mut NodeContext<'_>) -> NodeResult<()>;
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub struct NodeOptions<'a> {
90 pub name: &'a str,
92 pub namespace: &'a str,
94 pub domain_id: u32,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
103pub struct Callback<'a> {
104 id: CallbackId<'a>,
105}
106
107impl<'a> Callback<'a> {
108 pub const fn as_str(self) -> &'a str {
110 self.id.as_str()
111 }
112
113 pub fn is_named(self, name: &str) -> bool {
115 self.as_str() == name
116 }
117
118 #[doc(hidden)]
120 pub const fn __from_id(id: CallbackId<'a>) -> Self {
121 Self { id }
122 }
123}
124
125impl<'a> NodeOptions<'a> {
126 pub const fn new(name: &'a str) -> Self {
128 Self {
129 name,
130 namespace: "/",
131 domain_id: 0,
132 }
133 }
134
135 pub const fn namespace(mut self, namespace: &'a str) -> Self {
137 self.namespace = namespace;
138 self
139 }
140
141 pub const fn domain_id(mut self, domain_id: u32) -> Self {
143 self.domain_id = domain_id;
144 self
145 }
146}
147
148pub trait NodeRuntime {
150 fn create_node(&mut self, id: NodeId<'_>, options: NodeOptions<'_>) -> NodeResult<()>;
152
153 fn create_entity(&mut self, metadata: EntityMetadata) -> NodeResult<()>;
155
156 fn record_callback_effect(
158 &mut self,
159 callback_id: CallbackId<'_>,
160 kind: CallbackEffectKind,
161 entity_id: EntityId<'_>,
162 ) -> NodeResult<()>;
163}
164
165impl<const MAX_NODES: usize, const MAX_ENTITIES: usize, const MAX_CALLBACKS: usize> NodeRuntime
166 for MetadataRecorder<MAX_NODES, MAX_ENTITIES, MAX_CALLBACKS>
167{
168 fn create_node(&mut self, id: NodeId<'_>, options: NodeOptions<'_>) -> NodeResult<()> {
169 self.push_node(id, options.name, options.namespace, options.domain_id)?;
170 Ok(())
171 }
172
173 fn create_entity(&mut self, metadata: EntityMetadata) -> NodeResult<()> {
174 self.push_entity(metadata)?;
175 Ok(())
176 }
177
178 fn record_callback_effect(
179 &mut self,
180 callback_id: CallbackId<'_>,
181 kind: CallbackEffectKind,
182 entity_id: EntityId<'_>,
183 ) -> NodeResult<()> {
184 self.push_callback_effect(callback_id, kind, entity_id)?;
185 Ok(())
186 }
187}
188
189pub trait DeclaredNodeRuntime {
196 type NodeHandle: Copy + Eq;
198
199 fn build_component_node(
201 &mut self,
202 id: NodeId<'_>,
203 options: NodeOptions<'_>,
204 ) -> NodeResult<Self::NodeHandle>;
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
209pub struct RuntimeNodeRecord<H: Copy + Eq> {
210 slot: NodeSlot,
211 stable_id: MetadataString,
212 source_default_name: MetadataString,
213 handle: H,
214}
215
216impl<H: Copy + Eq> RuntimeNodeRecord<H> {
217 pub const fn slot(&self) -> NodeSlot {
219 self.slot
220 }
221
222 pub fn stable_id(&self) -> &str {
224 &self.stable_id
225 }
226
227 pub fn source_default_name(&self) -> &str {
229 &self.source_default_name
230 }
231
232 pub const fn handle(&self) -> H {
234 self.handle
235 }
236}
237
238pub struct NodeRuntimeAdapter<
240 'a,
241 R: DeclaredNodeRuntime + ?Sized,
242 const MAX_NODES: usize = { crate::node_metadata::DEFAULT_MAX_METADATA_NODES },
243 const MAX_ENTITIES: usize = { crate::node_metadata::DEFAULT_MAX_METADATA_ENTITIES },
244 const MAX_CALLBACKS: usize = { crate::node_metadata::DEFAULT_MAX_METADATA_CALLBACKS },
245> {
246 node_runtime: &'a mut R,
247 nodes: Vec<RuntimeNodeRecord<R::NodeHandle>, MAX_NODES>,
248 entities: Vec<EntityMetadata, MAX_ENTITIES>,
249 callback_effects: Vec<CallbackEffectMetadata, MAX_CALLBACKS>,
250}
251
252impl<
253 'a,
254 R: DeclaredNodeRuntime + ?Sized,
255 const MAX_NODES: usize,
256 const MAX_ENTITIES: usize,
257 const MAX_CALLBACKS: usize,
258> NodeRuntimeAdapter<'a, R, MAX_NODES, MAX_ENTITIES, MAX_CALLBACKS>
259{
260 pub fn new(node_runtime: &'a mut R) -> Self {
262 Self {
263 node_runtime,
264 nodes: Vec::new(),
265 entities: Vec::new(),
266 callback_effects: Vec::new(),
267 }
268 }
269
270 pub fn nodes(&self) -> &[RuntimeNodeRecord<R::NodeHandle>] {
272 &self.nodes
273 }
274
275 pub fn entities(&self) -> &[EntityMetadata] {
277 &self.entities
278 }
279
280 pub fn callback_effects(&self) -> &[CallbackEffectMetadata] {
282 &self.callback_effects
283 }
284
285 pub fn node_handle(&self, stable_id: NodeId<'_>) -> Option<R::NodeHandle> {
287 self.nodes
288 .iter()
289 .find(|node| node.stable_id() == stable_id.as_str())
290 .map(RuntimeNodeRecord::handle)
291 }
292
293 fn contains_node(&self, stable_id: &str) -> bool {
294 self.nodes.iter().any(|node| node.stable_id() == stable_id)
295 }
296
297 fn contains_entity(&self, stable_id: &str) -> bool {
298 self.entities
299 .iter()
300 .any(|entity| entity.id.as_str() == stable_id)
301 }
302
303 fn node_slot_for_id(&self, stable_id: &str) -> Option<NodeSlot> {
304 self.nodes
305 .iter()
306 .find(|node| node.stable_id() == stable_id)
307 .map(RuntimeNodeRecord::slot)
308 }
309
310 fn entity_slot_for_id(&self, stable_id: &str) -> Option<EntitySlot> {
311 self.entities
312 .iter()
313 .find(|entity| entity.id.as_str() == stable_id)
314 .and_then(|entity| entity.slot)
315 }
316
317 fn callback_slot_for_current_entity(
318 &self,
319 id: &str,
320 current_callbacks: &mut Vec<MetadataString, 3>,
321 next_callback_slot: &mut usize,
322 ) -> CallbackSlot {
323 if let Some(slot) = self.callback_slot_for_id(id) {
324 return slot;
325 }
326 if let Some((index, _)) = current_callbacks
327 .iter()
328 .enumerate()
329 .find(|(_, callback_id)| callback_id.as_str() == id)
330 {
331 return CallbackSlot::new(self.callback_slot_count() + index);
332 }
333 let slot = CallbackSlot::new(*next_callback_slot);
334 let _ = current_callbacks
335 .push(copy_str(id).expect("callback ID already fits metadata string capacity"));
336 *next_callback_slot += 1;
337 slot
338 }
339
340 fn callback_slot_for_id(&self, id: &str) -> Option<CallbackSlot> {
341 let mut seen = Vec::<&str, MAX_CALLBACKS>::new();
342 for entity in &self.entities {
343 for callback_id in entity_callback_ids(entity) {
344 let Some(callback_id) = callback_id else {
345 continue;
346 };
347 let callback_id = callback_id.as_str();
348 if seen.contains(&callback_id) {
349 continue;
350 }
351 if callback_id == id {
352 return Some(CallbackSlot::new(seen.len()));
353 }
354 let _ = seen.push(callback_id);
355 }
356 }
357 None
358 }
359
360 fn callback_slot_count(&self) -> usize {
361 let mut seen = Vec::<&str, MAX_CALLBACKS>::new();
362 for entity in &self.entities {
363 for callback_id in entity_callback_ids(entity) {
364 let Some(callback_id) = callback_id else {
365 continue;
366 };
367 let callback_id = callback_id.as_str();
368 if !seen.contains(&callback_id) {
369 let _ = seen.push(callback_id);
370 }
371 }
372 }
373 seen.len()
374 }
375}
376
377impl<
378 R: DeclaredNodeRuntime + ?Sized,
379 const MAX_NODES: usize,
380 const MAX_ENTITIES: usize,
381 const MAX_CALLBACKS: usize,
382> NodeRuntime for NodeRuntimeAdapter<'_, R, MAX_NODES, MAX_ENTITIES, MAX_CALLBACKS>
383{
384 fn create_node(&mut self, id: NodeId<'_>, options: NodeOptions<'_>) -> NodeResult<()> {
385 if self.contains_node(id.as_str()) {
386 return Err(NodeMetadataError::DuplicateId.into());
387 }
388 let handle = self.node_runtime.build_component_node(id, options)?;
389 let slot = NodeSlot::new(self.nodes.len());
390 self.nodes
391 .push(RuntimeNodeRecord {
392 slot,
393 stable_id: copy_str(id.as_str())?,
394 source_default_name: copy_str(options.name)?,
395 handle,
396 })
397 .map_err(|_| NodeDeclError::Metadata(NodeMetadataError::Capacity))?;
398 Ok(())
399 }
400
401 fn create_entity(&mut self, mut metadata: EntityMetadata) -> NodeResult<()> {
402 if !self.contains_node(metadata.node_id.as_str()) {
403 return Err(NodeMetadataError::UnknownNode.into());
404 }
405 if self.contains_entity(metadata.id.as_str()) {
406 return Err(NodeMetadataError::DuplicateId.into());
407 }
408 metadata.slot = Some(EntitySlot::new(self.entities.len()));
409 metadata.node_slot = self.node_slot_for_id(&metadata.node_id);
410 let mut current_callbacks = Vec::<MetadataString, 3>::new();
411 let mut next_callback_slot = self.callback_slot_count();
412 metadata.callback_slot = metadata.callback_id.as_ref().map(|callback_id| {
413 self.callback_slot_for_current_entity(
414 callback_id.as_str(),
415 &mut current_callbacks,
416 &mut next_callback_slot,
417 )
418 });
419 metadata.action_cancel_callback_slot =
420 metadata
421 .action_cancel_callback_id
422 .as_ref()
423 .map(|callback_id| {
424 self.callback_slot_for_current_entity(
425 callback_id.as_str(),
426 &mut current_callbacks,
427 &mut next_callback_slot,
428 )
429 });
430 metadata.action_accepted_callback_slot =
431 metadata
432 .action_accepted_callback_id
433 .as_ref()
434 .map(|callback_id| {
435 self.callback_slot_for_current_entity(
436 callback_id.as_str(),
437 &mut current_callbacks,
438 &mut next_callback_slot,
439 )
440 });
441 self.entities
442 .push(metadata)
443 .map_err(|_| NodeDeclError::Metadata(NodeMetadataError::Capacity))?;
444 Ok(())
445 }
446
447 fn record_callback_effect(
448 &mut self,
449 callback_id: CallbackId<'_>,
450 kind: CallbackEffectKind,
451 entity_id: EntityId<'_>,
452 ) -> NodeResult<()> {
453 if !self.contains_entity(entity_id.as_str()) {
454 return Err(NodeMetadataError::UnknownEntity.into());
455 }
456 self.callback_effects
457 .push(CallbackEffectMetadata {
458 callback_id: copy_str(callback_id.as_str())?,
459 callback_slot: self.callback_slot_for_id(callback_id.as_str()),
460 kind,
461 entity_id: copy_str(entity_id.as_str())?,
462 entity_slot: self.entity_slot_for_id(entity_id.as_str()),
463 })
464 .map_err(|_| NodeDeclError::Metadata(NodeMetadataError::Capacity))?;
465 Ok(())
466 }
467}
468
469#[cfg(feature = "rmw-cffi")]
470impl DeclaredNodeRuntime for crate::Executor {
471 type NodeHandle = nros_node::executor::NodeId;
472
473 fn build_component_node(
474 &mut self,
475 _id: NodeId<'_>,
476 options: NodeOptions<'_>,
477 ) -> NodeResult<Self::NodeHandle> {
478 self.node_builder(options.name)
479 .namespace(options.namespace)
480 .domain_id(options.domain_id)
481 .build()
482 .map_err(|_| NodeDeclError::Runtime)
483 }
484}
485
486#[cfg(feature = "rmw-cffi")]
488pub type NodeExecutorRuntime<
489 'a,
490 const MAX_NODES: usize = { crate::node_metadata::DEFAULT_MAX_METADATA_NODES },
491 const MAX_ENTITIES: usize = { crate::node_metadata::DEFAULT_MAX_METADATA_ENTITIES },
492 const MAX_CALLBACKS: usize = { crate::node_metadata::DEFAULT_MAX_METADATA_CALLBACKS },
493> = NodeRuntimeAdapter<'a, crate::Executor, MAX_NODES, MAX_ENTITIES, MAX_CALLBACKS>;
494
495pub struct NodeContext<'a, R: NodeRuntime + ?Sized = dyn NodeRuntime + 'a> {
497 component_name: &'static str,
498 runtime: &'a mut R,
499}
500
501impl<'a, R: NodeRuntime + ?Sized> NodeContext<'a, R> {
502 pub fn new(component_name: &'static str, runtime: &'a mut R) -> Self {
504 Self {
505 component_name,
506 runtime,
507 }
508 }
509
510 pub const fn component_name(&self) -> &'static str {
512 self.component_name
513 }
514
515 #[doc(hidden)]
520 pub fn create_node_with_id<'id>(
521 &mut self,
522 id: NodeId<'id>,
523 options: NodeOptions<'_>,
524 ) -> NodeResult<DeclaredNode<'_, 'id, R>> {
525 self.runtime.create_node(id, options)?;
526 Ok(DeclaredNode {
527 runtime: self.runtime,
528 id,
529 current_group: None,
530 })
531 }
532
533 pub fn create_node<'id>(
539 &mut self,
540 options: NodeOptions<'id>,
541 ) -> NodeResult<DeclaredNode<'_, 'id, R>> {
542 self.create_node_with_id(NodeId::new(options.name), options)
543 }
544
545 #[deprecated(note = "use create_node(NodeOptions)")]
547 pub fn create_node_with_options<'id>(
548 &mut self,
549 options: NodeOptions<'id>,
550 ) -> NodeResult<DeclaredNode<'_, 'id, R>> {
551 self.create_node(options)
552 }
553
554 #[doc(hidden)]
556 pub fn callback<'id>(&mut self, id: CallbackId<'id>) -> CallbackEffects<'_, 'id, R> {
557 CallbackEffects {
558 runtime: self.runtime,
559 id,
560 }
561 }
562}
563
564pub struct DeclaredNode<'ctx, 'id, R: NodeRuntime + ?Sized = dyn NodeRuntime + 'ctx> {
566 runtime: &'ctx mut R,
567 id: NodeId<'id>,
568 current_group: Option<MetadataString>,
575}
576
577impl<'ctx, 'id, R: NodeRuntime + ?Sized> DeclaredNode<'ctx, 'id, R> {
578 #[doc(hidden)]
580 pub const fn id(&self) -> NodeId<'id> {
581 self.id
582 }
583
584 #[track_caller]
591 pub fn callback_group(&mut self, group: &str) -> NodeResult<&mut Self> {
592 self.current_group = Some(copy_str(group)?);
593 Ok(self)
594 }
595
596 fn declare_entity(&mut self, mut metadata: EntityMetadata) -> NodeResult<()> {
601 if metadata.callback_group.is_none() {
602 metadata.callback_group = self.current_group.clone();
603 }
604 self.runtime.create_entity(metadata)
605 }
606
607 #[track_caller]
609 #[doc(hidden)]
610 pub fn create_publisher<'entity, M: RosMessage>(
611 &mut self,
612 id: EntityId<'entity>,
613 topic: &str,
614 ) -> NodeResult<NodePublisher<'entity, M>> {
615 self.create_publisher_with_qos::<M>(id, topic, QosSettings::default())
616 }
617
618 #[track_caller]
624 pub fn create_publisher_for_topic<'entity, M: RosMessage>(
625 &mut self,
626 topic: &'entity str,
627 ) -> NodeResult<NodePublisher<'entity, M>> {
628 self.create_publisher_for_topic_with_qos::<M>(topic, QosSettings::default())
629 }
630
631 #[track_caller]
633 pub fn create_publisher_for_topic_with_qos<'entity, M: RosMessage>(
634 &mut self,
635 topic: &'entity str,
636 qos: QosSettings,
637 ) -> NodeResult<NodePublisher<'entity, M>> {
638 self.create_publisher_with_qos::<M>(EntityId::new(topic), topic, qos)
639 }
640
641 #[track_caller]
643 #[doc(hidden)]
644 pub fn create_publisher_with_qos<'entity, M: RosMessage>(
645 &mut self,
646 id: EntityId<'entity>,
647 topic: &str,
648 qos: QosSettings,
649 ) -> NodeResult<NodePublisher<'entity, M>> {
650 let mut metadata = entity_metadata(EntityMetadataSpec {
651 id,
652 node_id: self.id,
653 kind: EntityKind::Publisher,
654 source_name: topic,
655 type_name: M::TYPE_NAME,
656 type_hash: M::TYPE_HASH,
657 qos,
658 })?;
659 metadata.source = SourceLocationMetadata::caller()?;
660 self.declare_entity(metadata)?;
661 Ok(NodePublisher::new(id))
662 }
663
664 #[track_caller]
666 #[doc(hidden)]
667 pub fn create_subscription<'entity, 'callback, M: RosMessage>(
668 &mut self,
669 id: EntityId<'entity>,
670 callback_id: CallbackId<'callback>,
671 topic: &str,
672 ) -> NodeResult<NodeSubscription<'entity, M>> {
673 self.create_subscription_with_qos::<M>(id, callback_id, topic, QosSettings::default())
674 }
675
676 #[track_caller]
681 #[doc(hidden)]
682 pub fn create_subscription_for_callback<'callback, M: RosMessage>(
683 &mut self,
684 callback_id: CallbackId<'callback>,
685 topic: &str,
686 ) -> NodeResult<NodeSubscription<'callback, M>> {
687 self.create_subscription_for_callback_with_qos::<M>(
688 callback_id,
689 topic,
690 QosSettings::default(),
691 )
692 }
693
694 #[track_caller]
697 pub fn create_subscription_for_callback_name<'callback, M: RosMessage>(
698 &mut self,
699 callback_name: &'callback str,
700 topic: &str,
701 ) -> NodeResult<NodeSubscription<'callback, M>> {
702 self.create_subscription_for_callback::<M>(CallbackId::new(callback_name), topic)
703 }
704
705 #[track_caller]
715 pub fn create_subscription_for_callback_name_with_safety<'callback, M: RosMessage>(
716 &mut self,
717 callback_name: &'callback str,
718 topic: &str,
719 ) -> NodeResult<NodeSubscription<'callback, M>> {
720 let callback_id = CallbackId::new(callback_name);
721 let id = EntityId::new(callback_id.as_str());
722 let mut metadata = entity_metadata(EntityMetadataSpec {
723 id,
724 node_id: self.id,
725 kind: EntityKind::Subscription,
726 source_name: topic,
727 type_name: M::TYPE_NAME,
728 type_hash: M::TYPE_HASH,
729 qos: QosSettings::default(),
730 })?;
731 metadata.callback_id = Some(copy_str(callback_id.as_str())?);
732 metadata.callback_source = SourceLocationMetadata::caller()?;
733 metadata.source = metadata.callback_source.clone();
734 metadata.safety = true;
735 self.declare_entity(metadata)?;
736 Ok(NodeSubscription::new(id))
737 }
738
739 #[track_caller]
741 #[doc(hidden)]
742 pub fn create_subscription_for_callback_with_qos<'callback, M: RosMessage>(
743 &mut self,
744 callback_id: CallbackId<'callback>,
745 topic: &str,
746 qos: QosSettings,
747 ) -> NodeResult<NodeSubscription<'callback, M>> {
748 self.create_subscription_with_qos::<M>(
749 EntityId::new(callback_id.as_str()),
750 callback_id,
751 topic,
752 qos,
753 )
754 }
755
756 #[track_caller]
758 pub fn create_subscription_for_topic<'entity, M: RosMessage>(
759 &mut self,
760 topic: &'entity str,
761 ) -> NodeResult<NodeSubscription<'entity, M>> {
762 self.create_subscription_for_topic_with_qos::<M>(topic, QosSettings::default())
763 }
764
765 #[track_caller]
767 pub fn create_subscription_for_topic_with_qos<'entity, M: RosMessage>(
768 &mut self,
769 topic: &'entity str,
770 qos: QosSettings,
771 ) -> NodeResult<NodeSubscription<'entity, M>> {
772 self.create_subscription_with_qos::<M>(
773 EntityId::new(topic),
774 CallbackId::new(topic),
775 topic,
776 qos,
777 )
778 }
779
780 #[track_caller]
782 #[doc(hidden)]
783 pub fn create_subscription_with_qos<'entity, 'callback, M: RosMessage>(
784 &mut self,
785 id: EntityId<'entity>,
786 callback_id: CallbackId<'callback>,
787 topic: &str,
788 qos: QosSettings,
789 ) -> NodeResult<NodeSubscription<'entity, M>> {
790 let mut metadata = entity_metadata(EntityMetadataSpec {
791 id,
792 node_id: self.id,
793 kind: EntityKind::Subscription,
794 source_name: topic,
795 type_name: M::TYPE_NAME,
796 type_hash: M::TYPE_HASH,
797 qos,
798 })?;
799 metadata.callback_id = Some(copy_str(callback_id.as_str())?);
800 metadata.callback_source = SourceLocationMetadata::caller()?;
801 metadata.source = metadata.callback_source.clone();
802 self.declare_entity(metadata)?;
803 Ok(NodeSubscription::new(id))
804 }
805
806 #[track_caller]
818 pub fn create_subscription_static<M: RosMessage>(
819 &mut self,
820 topic: &'static str,
821 ) -> NodeResult<SubscriptionTag> {
822 let id = EntityId::new(topic);
823 let callback_id = CallbackId::new(topic);
824 let mut metadata = entity_metadata(EntityMetadataSpec {
825 id,
826 node_id: self.id,
827 kind: EntityKind::Subscription,
828 source_name: topic,
829 type_name: M::TYPE_NAME,
830 type_hash: M::TYPE_HASH,
831 qos: QosSettings::default(),
832 })?;
833 metadata.callback_id = Some(copy_str(callback_id.as_str())?);
834 metadata.callback_source = SourceLocationMetadata::caller()?;
835 metadata.source = metadata.callback_source.clone();
836 self.declare_entity(metadata)?;
837 Ok(SubscriptionTag::new(topic))
838 }
839
840 #[track_caller]
842 #[doc(hidden)]
843 pub fn create_timer<'entity, 'callback>(
844 &mut self,
845 id: EntityId<'entity>,
846 callback_id: CallbackId<'callback>,
847 period: TimerDuration,
848 ) -> NodeResult<NodeTimer<'entity>> {
849 let mut metadata = entity_metadata(EntityMetadataSpec {
850 id,
851 node_id: self.id,
852 kind: EntityKind::Timer,
853 source_name: "",
854 type_name: "",
855 type_hash: "",
856 qos: QosSettings::default(),
857 })?;
858 metadata.callback_id = Some(copy_str(callback_id.as_str())?);
859 metadata.callback_source = SourceLocationMetadata::caller()?;
860 metadata.source = metadata.callback_source.clone();
861 metadata.period_ms = Some(period.as_millis());
862 self.declare_entity(metadata)?;
863 Ok(NodeTimer::new(id))
864 }
865
866 #[track_caller]
868 #[doc(hidden)]
869 pub fn create_timer_for_callback<'callback>(
870 &mut self,
871 callback_id: CallbackId<'callback>,
872 period: TimerDuration,
873 ) -> NodeResult<NodeTimer<'callback>> {
874 self.create_timer(EntityId::new(callback_id.as_str()), callback_id, period)
875 }
876
877 #[track_caller]
880 pub fn create_timer_for_callback_name<'callback>(
881 &mut self,
882 callback_name: &'callback str,
883 period: TimerDuration,
884 ) -> NodeResult<NodeTimer<'callback>> {
885 self.create_timer_for_callback(CallbackId::new(callback_name), period)
886 }
887
888 #[track_caller]
890 #[doc(hidden)]
891 pub fn create_service_server<'entity, 'callback, S: RosService>(
892 &mut self,
893 id: EntityId<'entity>,
894 callback_id: CallbackId<'callback>,
895 service_name: &str,
896 ) -> NodeResult<NodeServiceServer<'entity, S>> {
897 let mut metadata = entity_metadata(EntityMetadataSpec {
898 id,
899 node_id: self.id,
900 kind: EntityKind::ServiceServer,
901 source_name: service_name,
902 type_name: S::SERVICE_NAME,
903 type_hash: S::SERVICE_HASH,
904 qos: QosSettings::default(),
905 })?;
906 metadata.callback_id = Some(copy_str(callback_id.as_str())?);
907 metadata.callback_source = SourceLocationMetadata::caller()?;
908 metadata.source = metadata.callback_source.clone();
909 self.declare_entity(metadata)?;
910 Ok(NodeServiceServer::new(id))
911 }
912
913 #[track_caller]
916 pub fn create_service_server_for_name<'entity, S: RosService>(
917 &mut self,
918 name: &'entity str,
919 ) -> NodeResult<NodeServiceServer<'entity, S>> {
920 self.create_service_server::<S>(EntityId::new(name), CallbackId::new(name), name)
921 }
922
923 #[track_caller]
926 pub fn create_service_server_for_name_with_callback<'entity, S: RosService>(
927 &mut self,
928 name: &'entity str,
929 callback_name: &str,
930 ) -> NodeResult<NodeServiceServer<'entity, S>> {
931 self.create_service_server::<S>(EntityId::new(name), CallbackId::new(callback_name), name)
932 }
933
934 #[track_caller]
946 pub fn create_service_static<S: RosService>(
947 &mut self,
948 name: &'static str,
949 ) -> NodeResult<ServiceTag> {
950 self.create_service_server_for_name::<S>(name)?;
951 Ok(ServiceTag::new(name))
952 }
953
954 #[track_caller]
956 #[doc(hidden)]
957 pub fn create_service_client<'entity, S: RosService>(
958 &mut self,
959 id: EntityId<'entity>,
960 service_name: &str,
961 ) -> NodeResult<NodeServiceClient<'entity, S>> {
962 let mut metadata = entity_metadata(EntityMetadataSpec {
963 id,
964 node_id: self.id,
965 kind: EntityKind::ServiceClient,
966 source_name: service_name,
967 type_name: S::SERVICE_NAME,
968 type_hash: S::SERVICE_HASH,
969 qos: QosSettings::default(),
970 })?;
971 metadata.source = SourceLocationMetadata::caller()?;
972 self.declare_entity(metadata)?;
973 Ok(NodeServiceClient::new(id))
974 }
975
976 #[track_caller]
978 pub fn create_service_client_for_name<'entity, S: RosService>(
979 &mut self,
980 name: &'entity str,
981 ) -> NodeResult<NodeServiceClient<'entity, S>> {
982 self.create_service_client::<S>(EntityId::new(name), name)
983 }
984
985 #[track_caller]
987 #[doc(hidden)]
988 pub fn create_action_server<'entity, 'callback, A: RosAction>(
989 &mut self,
990 id: EntityId<'entity>,
991 callback_id: CallbackId<'callback>,
992 action_name: &str,
993 ) -> NodeResult<NodeActionServer<'entity, A>> {
994 self.create_action_server_with_callbacks::<A>(
995 id,
996 callback_id,
997 callback_id,
998 callback_id,
999 action_name,
1000 )
1001 }
1002
1003 #[track_caller]
1005 #[doc(hidden)]
1006 pub fn create_action_server_with_callbacks<'entity, 'goal, 'cancel, 'accepted, A: RosAction>(
1007 &mut self,
1008 id: EntityId<'entity>,
1009 goal_callback_id: CallbackId<'goal>,
1010 cancel_callback_id: CallbackId<'cancel>,
1011 accepted_callback_id: CallbackId<'accepted>,
1012 action_name: &str,
1013 ) -> NodeResult<NodeActionServer<'entity, A>> {
1014 let mut metadata = entity_metadata(EntityMetadataSpec {
1015 id,
1016 node_id: self.id,
1017 kind: EntityKind::ActionServer,
1018 source_name: action_name,
1019 type_name: A::ACTION_NAME,
1020 type_hash: A::ACTION_HASH,
1021 qos: QosSettings::default(),
1022 })?;
1023 metadata.callback_id = Some(copy_str(goal_callback_id.as_str())?);
1024 metadata.callback_source = SourceLocationMetadata::caller()?;
1025 metadata.action_cancel_callback_id = Some(copy_str(cancel_callback_id.as_str())?);
1026 metadata.action_cancel_source = metadata.callback_source.clone();
1027 metadata.action_accepted_callback_id = Some(copy_str(accepted_callback_id.as_str())?);
1028 metadata.action_accepted_source = metadata.callback_source.clone();
1029 metadata.source = metadata.callback_source.clone();
1030 self.declare_entity(metadata)?;
1031 Ok(NodeActionServer::new(id))
1032 }
1033
1034 #[track_caller]
1037 pub fn create_action_server_for_name<'entity, A: RosAction>(
1038 &mut self,
1039 name: &'entity str,
1040 ) -> NodeResult<NodeActionServer<'entity, A>> {
1041 self.create_action_server::<A>(EntityId::new(name), CallbackId::new(name), name)
1042 }
1043
1044 #[track_caller]
1047 pub fn create_action_server_for_name_with_callbacks<'entity, A: RosAction>(
1048 &mut self,
1049 name: &'entity str,
1050 goal_callback_name: &str,
1051 cancel_callback_name: &str,
1052 accepted_callback_name: &str,
1053 ) -> NodeResult<NodeActionServer<'entity, A>> {
1054 self.create_action_server_with_callbacks::<A>(
1055 EntityId::new(name),
1056 CallbackId::new(goal_callback_name),
1057 CallbackId::new(cancel_callback_name),
1058 CallbackId::new(accepted_callback_name),
1059 name,
1060 )
1061 }
1062
1063 #[track_caller]
1079 pub fn create_action_static<A: RosAction>(
1080 &mut self,
1081 name: &'static str,
1082 ) -> NodeResult<ActionTag> {
1083 self.create_action_server_for_name::<A>(name)?;
1084 Ok(ActionTag::new(name))
1085 }
1086
1087 #[track_caller]
1089 #[doc(hidden)]
1090 pub fn create_action_client<'entity, A: RosAction>(
1091 &mut self,
1092 id: EntityId<'entity>,
1093 action_name: &str,
1094 ) -> NodeResult<NodeActionClient<'entity, A>> {
1095 let mut metadata = entity_metadata(EntityMetadataSpec {
1096 id,
1097 node_id: self.id,
1098 kind: EntityKind::ActionClient,
1099 source_name: action_name,
1100 type_name: A::ACTION_NAME,
1101 type_hash: A::ACTION_HASH,
1102 qos: QosSettings::default(),
1103 })?;
1104 metadata.source = SourceLocationMetadata::caller()?;
1105 self.declare_entity(metadata)?;
1106 Ok(NodeActionClient::new(id))
1107 }
1108
1109 #[track_caller]
1111 pub fn create_action_client_for_name<'entity, A: RosAction>(
1112 &mut self,
1113 name: &'entity str,
1114 ) -> NodeResult<NodeActionClient<'entity, A>> {
1115 self.create_action_client::<A>(EntityId::new(name), name)
1116 }
1117
1118 #[track_caller]
1131 pub fn create_action_client_with_callbacks_for_name<'entity, A: RosAction>(
1132 &mut self,
1133 name: &'entity str,
1134 result_callback_name: &str,
1135 feedback_callback_name: &str,
1136 ) -> NodeResult<NodeActionClient<'entity, A>> {
1137 let mut metadata = entity_metadata(EntityMetadataSpec {
1138 id: EntityId::new(name),
1139 node_id: self.id,
1140 kind: EntityKind::ActionClient,
1141 source_name: name,
1142 type_name: A::ACTION_NAME,
1143 type_hash: A::ACTION_HASH,
1144 qos: QosSettings::default(),
1145 })?;
1146 metadata.callback_id = Some(copy_str(result_callback_name)?);
1147 metadata.action_accepted_callback_id = Some(copy_str(feedback_callback_name)?);
1148 metadata.callback_source = SourceLocationMetadata::caller()?;
1149 metadata.source = metadata.callback_source.clone();
1150 self.declare_entity(metadata)?;
1151 Ok(NodeActionClient::new(EntityId::new(name)))
1152 }
1153
1154 #[track_caller]
1156 #[doc(hidden)]
1157 pub fn declare_parameter<'entity>(
1158 &mut self,
1159 id: EntityId<'entity>,
1160 name: &str,
1161 parameter_type: ParameterType,
1162 ) -> NodeResult<NodeParameter<'entity>> {
1163 self.declare_parameter_with_default(id, name, ParameterDefault::for_type(parameter_type)?)
1164 }
1165
1166 #[track_caller]
1168 #[doc(hidden)]
1169 pub fn declare_parameter_with_default<'entity>(
1170 &mut self,
1171 id: EntityId<'entity>,
1172 name: &str,
1173 default: ParameterDefault,
1174 ) -> NodeResult<NodeParameter<'entity>> {
1175 let mut metadata = entity_metadata(EntityMetadataSpec {
1176 id,
1177 node_id: self.id,
1178 kind: EntityKind::Parameter,
1179 source_name: name,
1180 type_name: "",
1181 type_hash: "",
1182 qos: QosSettings::default(),
1183 })?;
1184 metadata.parameter_type = Some(default.parameter_type());
1185 metadata.parameter_default = Some(default);
1186 metadata.source = SourceLocationMetadata::caller()?;
1187 self.declare_entity(metadata)?;
1188 Ok(NodeParameter::new(id))
1189 }
1190
1191 #[track_caller]
1193 pub fn declare_parameter_for_name<'entity>(
1194 &mut self,
1195 name: &'entity str,
1196 parameter_type: ParameterType,
1197 ) -> NodeResult<NodeParameter<'entity>> {
1198 self.declare_parameter(EntityId::new(name), name, parameter_type)
1199 }
1200
1201 #[track_caller]
1204 pub fn declare_parameter_for_name_with_default<'entity>(
1205 &mut self,
1206 name: &'entity str,
1207 default: ParameterDefault,
1208 ) -> NodeResult<NodeParameter<'entity>> {
1209 self.declare_parameter_with_default(EntityId::new(name), name, default)
1210 }
1211
1212 #[doc(hidden)]
1214 pub fn callback<'callback>(
1215 &mut self,
1216 id: CallbackId<'callback>,
1217 ) -> CallbackEffects<'_, 'callback, R> {
1218 CallbackEffects {
1219 runtime: self.runtime,
1220 id,
1221 }
1222 }
1223
1224 pub fn callback_for_name<'callback>(
1227 &mut self,
1228 name: &'callback str,
1229 ) -> CallbackEffects<'_, 'callback, R> {
1230 self.callback(CallbackId::new(name))
1231 }
1232}
1233
1234pub struct CallbackEffects<'ctx, 'id, R: NodeRuntime + ?Sized = dyn NodeRuntime + 'ctx> {
1236 runtime: &'ctx mut R,
1237 id: CallbackId<'id>,
1238}
1239
1240impl<'ctx, 'id, R: NodeRuntime + ?Sized> CallbackEffects<'ctx, 'id, R> {
1241 #[doc(hidden)]
1243 pub fn reads(self, entity_id: EntityId<'_>) -> NodeResult<Self> {
1244 self.runtime
1245 .record_callback_effect(self.id, CallbackEffectKind::Reads, entity_id)?;
1246 Ok(self)
1247 }
1248
1249 pub fn reads_entity(self, entity: &impl DeclaredEntity) -> NodeResult<Self> {
1251 self.reads(entity.entity_id())
1252 }
1253
1254 #[doc(hidden)]
1256 pub fn publishes(self, entity_id: EntityId<'_>) -> NodeResult<Self> {
1257 self.runtime
1258 .record_callback_effect(self.id, CallbackEffectKind::Publishes, entity_id)?;
1259 Ok(self)
1260 }
1261
1262 pub fn publishes_entity(self, entity: &impl DeclaredEntity) -> NodeResult<Self> {
1264 self.publishes(entity.entity_id())
1265 }
1266
1267 #[doc(hidden)]
1269 pub fn writes(self, entity_id: EntityId<'_>) -> NodeResult<Self> {
1270 self.runtime
1271 .record_callback_effect(self.id, CallbackEffectKind::Writes, entity_id)?;
1272 Ok(self)
1273 }
1274
1275 pub fn writes_entity(self, entity: &impl DeclaredEntity) -> NodeResult<Self> {
1277 self.writes(entity.entity_id())
1278 }
1279}
1280
1281#[doc(hidden)]
1283pub trait DeclaredEntity {
1284 fn entity_id(&self) -> EntityId<'_>;
1286}
1287
1288macro_rules! component_handle {
1289 ($name:ident $(, $type_param:ident)?) => {
1290 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1292 pub struct $name<'id $(, $type_param)?> {
1293 id: EntityId<'id>,
1294 _marker: PhantomData<($($type_param,)?)>,
1295 }
1296
1297 impl<'id $(, $type_param)?> $name<'id $(, $type_param)?> {
1298 const fn new(id: EntityId<'id>) -> Self {
1299 Self {
1300 id,
1301 _marker: PhantomData,
1302 }
1303 }
1304
1305 #[doc(hidden)]
1307 pub const fn id(&self) -> EntityId<'id> {
1308 self.id
1309 }
1310 }
1311
1312 impl<'id $(, $type_param)?> DeclaredEntity for $name<'id $(, $type_param)?> {
1313 fn entity_id(&self) -> EntityId<'_> {
1314 self.id
1315 }
1316 }
1317 };
1318}
1319
1320component_handle!(NodePublisher, M);
1321component_handle!(NodeSubscription, M);
1322component_handle!(NodeServiceServer, S);
1323component_handle!(NodeServiceClient, S);
1324component_handle!(NodeActionServer, A);
1325component_handle!(NodeActionClient, A);
1326component_handle!(NodeTimer);
1327component_handle!(NodeParameter);
1328
1329pub trait PublisherResolver {
1350 fn publish_raw(&self, entity_id: &str, data: &[u8]) -> NodeResult<()>;
1354}
1355
1356struct ReplySink<'a> {
1360 buf: &'a mut [u8],
1361 written: &'a mut usize,
1362}
1363
1364enum DecisionSink<'a> {
1370 Goal(&'a mut GoalResponse),
1371 Cancel(&'a mut CancelResponse),
1372}
1373
1374pub struct CallbackCtx<'a> {
1384 payload: &'a [u8],
1385 publishers: &'a dyn PublisherResolver,
1386 reply: Option<ReplySink<'a>>,
1387 decision: Option<DecisionSink<'a>>,
1388 #[cfg(feature = "safety-e2e")]
1394 integrity: Option<&'a crate::IntegrityStatus>,
1395}
1396
1397impl<'a> CallbackCtx<'a> {
1398 pub fn new(payload: &'a [u8], publishers: &'a dyn PublisherResolver) -> Self {
1401 Self {
1402 payload,
1403 publishers,
1404 reply: None,
1405 decision: None,
1406 #[cfg(feature = "safety-e2e")]
1407 integrity: None,
1408 }
1409 }
1410
1411 #[cfg(feature = "safety-e2e")]
1417 pub fn new_with_integrity(
1418 payload: &'a [u8],
1419 publishers: &'a dyn PublisherResolver,
1420 integrity: &'a crate::IntegrityStatus,
1421 ) -> Self {
1422 Self {
1423 payload,
1424 publishers,
1425 reply: None,
1426 decision: None,
1427 integrity: Some(integrity),
1428 }
1429 }
1430
1431 pub fn with_reply(
1435 payload: &'a [u8],
1436 publishers: &'a dyn PublisherResolver,
1437 reply_buf: &'a mut [u8],
1438 reply_written: &'a mut usize,
1439 ) -> Self {
1440 *reply_written = 0;
1441 Self {
1442 payload,
1443 publishers,
1444 reply: Some(ReplySink {
1445 buf: reply_buf,
1446 written: reply_written,
1447 }),
1448 decision: None,
1449 #[cfg(feature = "safety-e2e")]
1450 integrity: None,
1451 }
1452 }
1453
1454 pub fn with_goal_decision(
1458 payload: &'a [u8],
1459 publishers: &'a dyn PublisherResolver,
1460 out: &'a mut GoalResponse,
1461 ) -> Self {
1462 Self {
1463 payload,
1464 publishers,
1465 reply: None,
1466 decision: Some(DecisionSink::Goal(out)),
1467 #[cfg(feature = "safety-e2e")]
1468 integrity: None,
1469 }
1470 }
1471
1472 pub fn with_cancel_decision(
1475 payload: &'a [u8],
1476 publishers: &'a dyn PublisherResolver,
1477 out: &'a mut CancelResponse,
1478 ) -> Self {
1479 Self {
1480 payload,
1481 publishers,
1482 reply: None,
1483 decision: Some(DecisionSink::Cancel(out)),
1484 #[cfg(feature = "safety-e2e")]
1485 integrity: None,
1486 }
1487 }
1488
1489 pub fn set_goal_response(&mut self, response: GoalResponse) -> NodeResult<()> {
1492 match &mut self.decision {
1493 Some(DecisionSink::Goal(slot)) => {
1494 **slot = response;
1495 Ok(())
1496 }
1497 _ => Err(NodeDeclError::Runtime),
1498 }
1499 }
1500
1501 pub fn set_cancel_response(&mut self, response: CancelResponse) -> NodeResult<()> {
1504 match &mut self.decision {
1505 Some(DecisionSink::Cancel(slot)) => {
1506 **slot = response;
1507 Ok(())
1508 }
1509 _ => Err(NodeDeclError::Runtime),
1510 }
1511 }
1512
1513 pub fn reply_raw(&mut self, data: &[u8]) -> NodeResult<()> {
1517 let sink = self.reply.as_mut().ok_or(NodeDeclError::Runtime)?;
1518 if data.len() > sink.buf.len() {
1519 return Err(NodeDeclError::Runtime);
1520 }
1521 sink.buf[..data.len()].copy_from_slice(data);
1522 *sink.written = data.len();
1523 Ok(())
1524 }
1525
1526 pub fn reply<M: RosMessage, const N: usize>(&mut self, msg: &M) -> NodeResult<()> {
1528 let mut buf = [0u8; N];
1529 let mut writer =
1530 crate::CdrWriter::new_with_header(&mut buf).map_err(|_| NodeDeclError::Runtime)?;
1531 msg.serialize(&mut writer)
1532 .map_err(|_| NodeDeclError::Runtime)?;
1533 let len = writer.position();
1534 self.reply_raw(&buf[..len])
1535 }
1536
1537 pub fn payload(&self) -> &[u8] {
1539 self.payload
1540 }
1541
1542 #[cfg(feature = "safety-e2e")]
1548 pub fn integrity(&self) -> Option<&crate::IntegrityStatus> {
1549 self.integrity
1550 }
1551
1552 pub fn message<M: RosMessage>(&self) -> NodeResult<M> {
1555 let mut reader =
1556 crate::CdrReader::new_with_header(self.payload).map_err(|_| NodeDeclError::Runtime)?;
1557 M::deserialize(&mut reader).map_err(|_| NodeDeclError::Runtime)
1558 }
1559
1560 #[doc(hidden)]
1562 pub fn publish_raw(&self, publisher: EntityId<'_>, data: &[u8]) -> NodeResult<()> {
1563 self.publishers.publish_raw(publisher.as_str(), data)
1564 }
1565
1566 #[doc(hidden)]
1570 pub fn publish<M: RosMessage, const N: usize>(
1571 &self,
1572 publisher: EntityId<'_>,
1573 msg: &M,
1574 ) -> NodeResult<()> {
1575 let mut buf = [0u8; N];
1576 let mut writer =
1577 crate::CdrWriter::new_with_header(&mut buf).map_err(|_| NodeDeclError::Runtime)?;
1578 msg.serialize(&mut writer)
1579 .map_err(|_| NodeDeclError::Runtime)?;
1580 let len = writer.position();
1581 self.publish_raw(publisher, &buf[..len])
1582 }
1583
1584 pub fn publish_to_topic<M: RosMessage, const N: usize>(
1591 &self,
1592 topic: &str,
1593 msg: &M,
1594 ) -> NodeResult<()> {
1595 self.publish::<M, N>(EntityId::new(topic), msg)
1596 }
1597}
1598
1599pub trait ActionExecutor {
1616 fn complete_goal_raw(
1618 &mut self,
1619 action_entity: &str,
1620 goal_id: &GoalId,
1621 status: GoalStatus,
1622 result: &[u8],
1623 ) -> NodeResult<()>;
1624
1625 fn publish_feedback_raw(
1627 &mut self,
1628 action_entity: &str,
1629 goal_id: &GoalId,
1630 feedback: &[u8],
1631 ) -> NodeResult<()>;
1632
1633 fn for_each_active_goal(&self, action_entity: &str, visit: &mut dyn FnMut(&GoalId, GoalStatus));
1638}
1639
1640pub trait ClientDispatch {
1655 fn call_raw(
1665 &mut self,
1666 service_entity: &str,
1667 request_cdr: &[u8],
1668 response_buf: &mut [u8],
1669 ) -> NodeResult<usize>;
1670
1671 fn send_goal_raw(&mut self, action_entity: &str, goal_cdr: &[u8]) -> NodeResult<GoalId>;
1676}
1677
1678pub struct TickCtx<'a> {
1685 publishers: &'a dyn PublisherResolver,
1686 actions: &'a mut dyn ActionExecutor,
1687 clients: &'a mut dyn ClientDispatch,
1688}
1689
1690impl<'a> TickCtx<'a> {
1691 pub fn new(
1693 publishers: &'a dyn PublisherResolver,
1694 actions: &'a mut dyn ActionExecutor,
1695 clients: &'a mut dyn ClientDispatch,
1696 ) -> Self {
1697 Self {
1698 publishers,
1699 actions,
1700 clients,
1701 }
1702 }
1703
1704 #[doc(hidden)]
1706 pub fn publish_raw(&self, publisher: EntityId<'_>, data: &[u8]) -> NodeResult<()> {
1707 self.publishers.publish_raw(publisher.as_str(), data)
1708 }
1709
1710 #[doc(hidden)]
1712 pub fn publish<M: RosMessage, const N: usize>(
1713 &self,
1714 publisher: EntityId<'_>,
1715 msg: &M,
1716 ) -> NodeResult<()> {
1717 let mut buf = [0u8; N];
1718 let mut writer =
1719 crate::CdrWriter::new_with_header(&mut buf).map_err(|_| NodeDeclError::Runtime)?;
1720 msg.serialize(&mut writer)
1721 .map_err(|_| NodeDeclError::Runtime)?;
1722 let len = writer.position();
1723 self.publish_raw(publisher, &buf[..len])
1724 }
1725
1726 pub fn publish_to_topic<M: RosMessage, const N: usize>(
1731 &self,
1732 topic: &str,
1733 msg: &M,
1734 ) -> NodeResult<()> {
1735 self.publish::<M, N>(EntityId::new(topic), msg)
1736 }
1737
1738 #[doc(hidden)]
1741 pub fn complete_goal<R: RosMessage, const N: usize>(
1742 &mut self,
1743 action: EntityId<'_>,
1744 goal_id: &GoalId,
1745 status: GoalStatus,
1746 result: &R,
1747 ) -> NodeResult<()> {
1748 let mut buf = [0u8; N];
1756 let mut writer =
1757 crate::CdrWriter::new_with_header(&mut buf).map_err(|_| NodeDeclError::Runtime)?;
1758 result
1759 .serialize(&mut writer)
1760 .map_err(|_| NodeDeclError::Runtime)?;
1761 let len = writer.position();
1762 self.actions
1763 .complete_goal_raw(action.as_str(), goal_id, status, &buf[..len])
1764 }
1765
1766 pub fn complete_goal_for_name<R: RosMessage, const N: usize>(
1772 &mut self,
1773 name: &str,
1774 goal_id: &GoalId,
1775 status: GoalStatus,
1776 result: &R,
1777 ) -> NodeResult<()> {
1778 self.complete_goal::<R, N>(EntityId::new(name), goal_id, status, result)
1779 }
1780
1781 #[doc(hidden)]
1787 pub fn for_each_active_goal(
1788 &self,
1789 action: EntityId<'_>,
1790 visit: &mut dyn FnMut(&GoalId, GoalStatus),
1791 ) {
1792 self.actions.for_each_active_goal(action.as_str(), visit);
1793 }
1794
1795 pub fn for_each_active_goal_for_name(
1797 &self,
1798 name: &str,
1799 visit: &mut dyn FnMut(&GoalId, GoalStatus),
1800 ) {
1801 self.for_each_active_goal(EntityId::new(name), visit);
1802 }
1803
1804 #[doc(hidden)]
1806 pub fn publish_feedback<F: RosMessage, const N: usize>(
1807 &mut self,
1808 action: EntityId<'_>,
1809 goal_id: &GoalId,
1810 feedback: &F,
1811 ) -> NodeResult<()> {
1812 let mut buf = [0u8; N];
1817 let mut writer =
1818 crate::CdrWriter::new_with_header(&mut buf).map_err(|_| NodeDeclError::Runtime)?;
1819 feedback
1820 .serialize(&mut writer)
1821 .map_err(|_| NodeDeclError::Runtime)?;
1822 let len = writer.position();
1823 self.actions
1824 .publish_feedback_raw(action.as_str(), goal_id, &buf[..len])
1825 }
1826
1827 pub fn publish_feedback_for_name<F: RosMessage, const N: usize>(
1829 &mut self,
1830 name: &str,
1831 goal_id: &GoalId,
1832 feedback: &F,
1833 ) -> NodeResult<()> {
1834 self.publish_feedback::<F, N>(EntityId::new(name), goal_id, feedback)
1835 }
1836
1837 #[doc(hidden)]
1841 pub fn call_raw(
1842 &mut self,
1843 service: EntityId<'_>,
1844 request_cdr: &[u8],
1845 response_buf: &mut [u8],
1846 ) -> NodeResult<usize> {
1847 self.clients
1848 .call_raw(service.as_str(), request_cdr, response_buf)
1849 }
1850
1851 pub fn call_raw_for_name(
1854 &mut self,
1855 name: &str,
1856 request_cdr: &[u8],
1857 response_buf: &mut [u8],
1858 ) -> NodeResult<usize> {
1859 self.call_raw(EntityId::new(name), request_cdr, response_buf)
1860 }
1861
1862 #[doc(hidden)]
1867 pub fn call<Req: RosMessage, Resp: RosMessage, const REQ_N: usize, const RESP_N: usize>(
1868 &mut self,
1869 service: EntityId<'_>,
1870 request: &Req,
1871 ) -> NodeResult<Resp> {
1872 let mut req_buf = [0u8; REQ_N];
1873 let mut writer =
1874 crate::CdrWriter::new_with_header(&mut req_buf).map_err(|_| NodeDeclError::Runtime)?;
1875 request
1876 .serialize(&mut writer)
1877 .map_err(|_| NodeDeclError::Runtime)?;
1878 let req_len = writer.position();
1879
1880 let mut resp_buf = [0u8; RESP_N];
1881 let resp_len =
1882 self.clients
1883 .call_raw(service.as_str(), &req_buf[..req_len], &mut resp_buf)?;
1884
1885 let mut reader = crate::CdrReader::new_with_header(&resp_buf[..resp_len])
1886 .map_err(|_| NodeDeclError::Runtime)?;
1887 Resp::deserialize(&mut reader).map_err(|_| NodeDeclError::Runtime)
1888 }
1889
1890 pub fn call_for_name<
1893 Req: RosMessage,
1894 Resp: RosMessage,
1895 const REQ_N: usize,
1896 const RESP_N: usize,
1897 >(
1898 &mut self,
1899 name: &str,
1900 request: &Req,
1901 ) -> NodeResult<Resp> {
1902 self.call::<Req, Resp, REQ_N, RESP_N>(EntityId::new(name), request)
1903 }
1904
1905 #[doc(hidden)]
1909 pub fn send_goal_raw(&mut self, action: EntityId<'_>, goal_cdr: &[u8]) -> NodeResult<GoalId> {
1910 self.clients.send_goal_raw(action.as_str(), goal_cdr)
1911 }
1912
1913 pub fn send_goal_raw_for_name(&mut self, name: &str, goal_cdr: &[u8]) -> NodeResult<GoalId> {
1916 self.send_goal_raw(EntityId::new(name), goal_cdr)
1917 }
1918
1919 #[doc(hidden)]
1923 pub fn send_goal<G: RosMessage, const N: usize>(
1924 &mut self,
1925 action: EntityId<'_>,
1926 goal: &G,
1927 ) -> NodeResult<GoalId> {
1928 let mut buf = [0u8; N];
1929 let mut writer =
1930 crate::CdrWriter::new_with_header(&mut buf).map_err(|_| NodeDeclError::Runtime)?;
1931 goal.serialize(&mut writer)
1932 .map_err(|_| NodeDeclError::Runtime)?;
1933 let len = writer.position();
1934 self.clients.send_goal_raw(action.as_str(), &buf[..len])
1935 }
1936
1937 pub fn send_goal_for_name<G: RosMessage, const N: usize>(
1940 &mut self,
1941 name: &str,
1942 goal: &G,
1943 ) -> NodeResult<GoalId> {
1944 self.send_goal::<G, N>(EntityId::new(name), goal)
1945 }
1946}
1947
1948pub trait ExecutableNode: Node {
1949 type State;
1951
1952 fn init() -> Self::State;
1954
1955 fn on_callback(state: &mut Self::State, callback: Callback<'_>, ctx: &mut CallbackCtx<'_>);
1959
1960 fn tick(_state: &mut Self::State, _ctx: &mut TickCtx<'_>) {}
1965}
1966
1967#[macro_export]
1978macro_rules! declarative_component {
1979 ($ty:ty) => {
1980 impl $crate::ExecutableNode for $ty {
1981 type State = ();
1982 fn init() -> Self::State {}
1983 fn on_callback(
1984 _state: &mut Self::State,
1985 _callback: $crate::Callback<'_>,
1986 _ctx: &mut $crate::CallbackCtx<'_>,
1987 ) {
1988 }
1989 }
1990 };
1991}
1992
1993pub fn register_node<C: Node>(runtime: &mut dyn NodeRuntime) -> NodeResult<()> {
1995 let mut context = NodeContext::new(C::NAME, runtime);
1996 C::register(&mut context)
1997}
1998
1999#[cfg(feature = "alloc")]
2006#[doc(hidden)]
2007pub fn __private_node_state_into_raw<C: ExecutableNode>(state: C::State) -> *mut () {
2008 extern crate alloc;
2009 alloc::boxed::Box::into_raw(alloc::boxed::Box::new(state)) as *mut ()
2010}
2011
2012pub fn record_node_metadata<C: Node>(recorder: &mut dyn NodeRuntime) -> NodeResult<()> {
2014 register_node::<C>(recorder)
2015}
2016
2017#[cfg(test)]
2018mod tests {
2019 use super::*;
2020 use crate::{CdrReader, CdrWriter, DeserError, SerError, SourceNameKind};
2021
2022 #[derive(Default)]
2023 struct FakeNodeRuntime {
2024 next: u8,
2025 created: Vec<MetadataString, 4>,
2026 }
2027
2028 impl DeclaredNodeRuntime for FakeNodeRuntime {
2029 type NodeHandle = u8;
2030
2031 fn build_component_node(
2032 &mut self,
2033 _id: NodeId<'_>,
2034 options: NodeOptions<'_>,
2035 ) -> NodeResult<Self::NodeHandle> {
2036 self.created
2037 .push(copy_str(options.name)?)
2038 .map_err(|_| NodeDeclError::Metadata(NodeMetadataError::Capacity))?;
2039 let handle = self.next;
2040 self.next += 1;
2041 Ok(handle)
2042 }
2043 }
2044
2045 #[derive(Debug, Clone, Copy, Default)]
2046 struct TestMsg;
2047
2048 impl crate::Serialize for TestMsg {
2049 fn serialize(&self, _writer: &mut CdrWriter) -> Result<(), SerError> {
2050 Ok(())
2051 }
2052 }
2053
2054 impl crate::Deserialize for TestMsg {
2055 fn deserialize(_reader: &mut CdrReader) -> Result<Self, DeserError> {
2056 Ok(Self)
2057 }
2058 }
2059
2060 impl RosMessage for TestMsg {
2061 const TYPE_NAME: &'static str = "test_msgs::msg::dds_::Test_";
2062 const TYPE_HASH: &'static str = "test_hash";
2063 }
2064
2065 struct TestService;
2066
2067 impl RosService for TestService {
2068 type Request = TestMsg;
2069 type Reply = TestMsg;
2070
2071 const SERVICE_NAME: &'static str = "test_msgs::srv::dds_::Test_";
2072 const SERVICE_HASH: &'static str = "test_service_hash";
2073 }
2074
2075 struct TestAction;
2076
2077 impl RosAction for TestAction {
2078 type Goal = TestMsg;
2079 type Result = TestMsg;
2080 type Feedback = TestMsg;
2081 type SendGoalRequest = TestMsg;
2082 type SendGoalResponse = TestMsg;
2083 type GetResultRequest = TestMsg;
2084 type GetResultResponse = TestMsg;
2085 type FeedbackMessage = TestMsg;
2086
2087 const ACTION_NAME: &'static str = "test_msgs::action::dds_::Test_";
2088 const ACTION_HASH: &'static str = "test_action_hash";
2089 }
2090
2091 struct TalkerComponent;
2092
2093 impl Node for TalkerComponent {
2094 const NAME: &'static str = "talker_component";
2095
2096 fn register(context: &mut NodeContext<'_>) -> NodeResult<()> {
2097 let mut node =
2098 context.create_node_with_id(NodeId::new("node"), NodeOptions::new("talker"))?;
2099 let _publisher =
2100 node.create_publisher::<TestMsg>(EntityId::new("pub_chatter"), "chatter")?;
2101 let _subscription = node.create_subscription::<TestMsg>(
2102 EntityId::new("sub_cmd"),
2103 CallbackId::new("on_cmd"),
2104 "~/cmd",
2105 )?;
2106 let _timer = node.create_timer(
2107 EntityId::new("timer_tick"),
2108 CallbackId::new("on_tick"),
2109 TimerDuration::from_millis(10),
2110 )?;
2111 let _parameter =
2112 node.declare_parameter(EntityId::new("param_gain"), "gain", ParameterType::Double)?;
2113 node.callback(CallbackId::new("on_tick"))
2114 .publishes(EntityId::new("pub_chatter"))?
2115 .writes(EntityId::new("param_gain"))?;
2116 Ok(())
2117 }
2118 }
2119
2120 #[test]
2121 fn component_records_metadata_without_transport() {
2122 let mut recorder = MetadataRecorder::<2, 8, 4>::new();
2123 record_node_metadata::<TalkerComponent>(&mut recorder).unwrap();
2124
2125 assert_eq!(recorder.nodes().len(), 1);
2126 assert_eq!(recorder.nodes()[0].name.as_str(), "talker");
2127 assert_eq!(recorder.entities().len(), 4);
2128 assert_eq!(recorder.entities()[0].kind, EntityKind::Publisher);
2129 assert_eq!(recorder.entities()[1].source_name.as_str(), "~/cmd");
2130 assert_eq!(
2131 recorder.entities()[1]
2132 .callback_id
2133 .as_ref()
2134 .map(|id| id.as_str()),
2135 Some("on_cmd")
2136 );
2137 assert_eq!(recorder.callback_effects().len(), 2);
2138 }
2139
2140 struct SafetyComponent;
2144 impl Node for SafetyComponent {
2145 const NAME: &'static str = "safety_component";
2146 fn register(context: &mut NodeContext<'_>) -> NodeResult<()> {
2147 let mut node =
2148 context.create_node_with_id(NodeId::new("node"), NodeOptions::new("listener"))?;
2149 let _plain = node.create_subscription_for_callback_name::<TestMsg>("on_plain", "/a")?;
2150 let _safe =
2151 node.create_subscription_for_callback_name_with_safety::<TestMsg>("on_safe", "/b")?;
2152 Ok(())
2153 }
2154 }
2155
2156 #[test]
2157 fn safety_opt_in_records_metadata_flag() {
2158 let mut recorder = MetadataRecorder::<2, 8, 4>::new();
2159 record_node_metadata::<SafetyComponent>(&mut recorder).unwrap();
2160 let ents = recorder.entities();
2161 assert_eq!(ents.len(), 2);
2162 assert_eq!(ents[0].source_name.as_str(), "/a");
2164 assert!(!ents[0].safety, "plain sub must not be flagged");
2165 assert_eq!(ents[1].source_name.as_str(), "/b");
2167 assert!(ents[1].safety, "safety sub must be flagged");
2168 }
2169
2170 struct GroupedComponent;
2171
2172 impl Node for GroupedComponent {
2173 const NAME: &'static str = "grouped_component";
2174
2175 fn register(context: &mut NodeContext<'_>) -> NodeResult<()> {
2176 let mut node =
2177 context.create_node_with_id(NodeId::new("node"), NodeOptions::new("grouped"))?;
2178 let _pub = node.create_publisher::<TestMsg>(EntityId::new("pub_plain"), "plain")?;
2180 node.callback_group("control")?;
2182 let _sub = node.create_subscription::<TestMsg>(
2183 EntityId::new("sub_cmd"),
2184 CallbackId::new("on_cmd"),
2185 "~/cmd",
2186 )?;
2187 let _timer = node.create_timer(
2188 EntityId::new("timer_tick"),
2189 CallbackId::new("on_tick"),
2190 TimerDuration::from_millis(10),
2191 )?;
2192 node.callback_group("telemetry")?;
2194 let _sub2 = node.create_subscription::<TestMsg>(
2195 EntityId::new("sub_diag"),
2196 CallbackId::new("on_diag"),
2197 "~/diag",
2198 )?;
2199 Ok(())
2200 }
2201 }
2202
2203 #[test]
2204 fn sticky_callback_group_stamps_subsequent_entities() {
2205 let mut recorder = MetadataRecorder::<2, 8, 4>::new();
2206 record_node_metadata::<GroupedComponent>(&mut recorder).unwrap();
2207
2208 let group_of = |idx: usize| {
2209 recorder.entities()[idx]
2210 .callback_group
2211 .as_ref()
2212 .map(|g| g.as_str())
2213 };
2214 assert_eq!(group_of(0), None);
2216 assert_eq!(group_of(1), Some("control"));
2218 assert_eq!(group_of(2), Some("control"));
2219 assert_eq!(group_of(3), Some("telemetry"));
2221 }
2222
2223 #[test]
2224 fn runtime_adapter_maps_stable_nodes_to_runtime_handles() {
2225 let mut node_runtime = FakeNodeRuntime::default();
2226 let mut runtime = NodeRuntimeAdapter::<_, 2, 8, 4>::new(&mut node_runtime);
2227
2228 register_node::<TalkerComponent>(&mut runtime).unwrap();
2229
2230 assert_eq!(runtime.nodes().len(), 1);
2231 assert_eq!(runtime.nodes()[0].slot(), NodeSlot::new(0));
2232 assert_eq!(runtime.nodes()[0].stable_id(), "node");
2233 assert_eq!(runtime.nodes()[0].source_default_name(), "talker");
2234 assert_eq!(runtime.node_handle(NodeId::new("node")), Some(0));
2235 assert_eq!(runtime.entities().len(), 4);
2236 assert_eq!(runtime.entities()[0].slot, Some(EntitySlot::new(0)));
2237 assert_eq!(runtime.entities()[0].node_slot, Some(NodeSlot::new(0)));
2238 assert_eq!(
2239 runtime.entities()[1].callback_slot,
2240 Some(CallbackSlot::new(0))
2241 );
2242 assert_eq!(
2243 runtime.entities()[2].callback_slot,
2244 Some(CallbackSlot::new(1))
2245 );
2246 assert_eq!(runtime.callback_effects().len(), 2);
2247 assert_eq!(
2248 runtime.callback_effects()[0].callback_slot,
2249 Some(CallbackSlot::new(1))
2250 );
2251 assert_eq!(
2252 runtime.callback_effects()[0].entity_slot,
2253 Some(EntitySlot::new(0))
2254 );
2255 }
2256
2257 #[test]
2258 fn context_can_synthesize_stable_node_id_from_options_name() {
2259 let mut recorder = MetadataRecorder::<1, 0, 0>::new();
2260 let mut context = NodeContext::new("test", &mut recorder);
2261 let node = context
2262 .create_node(NodeOptions::new("talker").namespace("/demo").domain_id(42))
2263 .unwrap();
2264
2265 assert_eq!(node.id(), NodeId::new("talker"));
2266 let _ = node;
2268 let _ = context;
2269 assert_eq!(recorder.nodes().len(), 1);
2270 assert_eq!(recorder.nodes()[0].id.as_str(), "talker");
2271 assert_eq!(recorder.nodes()[0].name.as_str(), "talker");
2272 assert_eq!(recorder.nodes()[0].namespace.as_str(), "/demo");
2273 assert_eq!(recorder.nodes()[0].domain_id, 42);
2274 }
2275
2276 #[test]
2277 fn synthesized_node_ids_reject_duplicate_names() {
2278 let mut node_runtime = FakeNodeRuntime::default();
2279 let mut runtime = NodeRuntimeAdapter::<_, 2, 0, 0>::new(&mut node_runtime);
2280 {
2281 let mut context = NodeContext::new("test", &mut runtime);
2282 context.create_node(NodeOptions::new("talker")).unwrap();
2283 }
2284 let mut context = NodeContext::new("test", &mut runtime);
2285 let result = context.create_node(NodeOptions::new("talker"));
2286
2287 assert!(matches!(
2288 result,
2289 Err(NodeDeclError::Metadata(NodeMetadataError::DuplicateId))
2290 ));
2291 }
2292
2293 #[test]
2294 fn synthesized_entity_helpers_record_topic_and_callback_ids() {
2295 let mut recorder = MetadataRecorder::<1, 3, 2>::new();
2296 let mut context = NodeContext::new("test", &mut recorder);
2297 let mut node = context.create_node(NodeOptions::new("talker")).unwrap();
2298
2299 let publisher = node
2300 .create_publisher_for_topic::<TestMsg>("/chatter")
2301 .unwrap();
2302 let subscription = node
2303 .create_subscription_for_callback::<TestMsg>(CallbackId::new("on_message"), "/cmd")
2304 .unwrap();
2305 let _timer = node
2306 .create_timer_for_callback(CallbackId::new("on_tick"), TimerDuration::from_millis(10))
2307 .unwrap();
2308
2309 node.callback(CallbackId::new("on_tick"))
2310 .publishes_entity(&publisher)
2311 .unwrap();
2312 node.callback(CallbackId::new("on_message"))
2313 .reads_entity(&subscription)
2314 .unwrap();
2315
2316 assert_eq!(publisher.id(), EntityId::new("/chatter"));
2317 assert_eq!(subscription.id(), EntityId::new("on_message"));
2318 assert_eq!(recorder.entities().len(), 3);
2319
2320 let publisher = &recorder.entities()[0];
2321 assert_eq!(publisher.id.as_str(), "/chatter");
2322 assert_eq!(publisher.kind, EntityKind::Publisher);
2323 assert_eq!(publisher.source_name.as_str(), "/chatter");
2324
2325 let subscription = &recorder.entities()[1];
2326 assert_eq!(subscription.id.as_str(), "on_message");
2327 assert_eq!(subscription.kind, EntityKind::Subscription);
2328 assert_eq!(subscription.source_name.as_str(), "/cmd");
2329 assert_eq!(
2330 subscription.callback_id.as_ref().map(|id| id.as_str()),
2331 Some("on_message")
2332 );
2333
2334 let timer = &recorder.entities()[2];
2335 assert_eq!(timer.id.as_str(), "on_tick");
2336 assert_eq!(timer.kind, EntityKind::Timer);
2337 assert_eq!(
2338 timer.callback_id.as_ref().map(|id| id.as_str()),
2339 Some("on_tick")
2340 );
2341
2342 assert_eq!(recorder.callback_effects().len(), 2);
2343 assert_eq!(
2344 recorder.callback_effects()[0].entity_id.as_str(),
2345 "/chatter"
2346 );
2347 assert_eq!(
2348 recorder.callback_effects()[1].entity_id.as_str(),
2349 "on_message"
2350 );
2351 }
2352
2353 #[test]
2354 fn named_callback_helpers_avoid_manual_callback_ids() {
2355 let mut recorder = MetadataRecorder::<1, 3, 2>::new();
2356 let mut context = NodeContext::new("test", &mut recorder);
2357 let mut node = context.create_node(NodeOptions::new("listener")).unwrap();
2358
2359 let publisher = node
2360 .create_publisher_for_topic::<TestMsg>("/chatter")
2361 .unwrap();
2362 let subscription = node
2363 .create_subscription_for_callback_name::<TestMsg>("on_message", "/chatter")
2364 .unwrap();
2365 let timer = node
2366 .create_timer_for_callback_name("on_tick", TimerDuration::from_millis(10))
2367 .unwrap();
2368
2369 node.callback_for_name("on_message")
2370 .reads_entity(&subscription)
2371 .unwrap();
2372 node.callback_for_name("on_tick")
2373 .publishes_entity(&publisher)
2374 .unwrap();
2375
2376 assert_eq!(subscription.id().as_str(), "on_message");
2377 assert_eq!(timer.id().as_str(), "on_tick");
2378 assert_eq!(
2379 recorder.entities()[1]
2380 .callback_id
2381 .as_ref()
2382 .map(|id| id.as_str()),
2383 Some("on_message")
2384 );
2385 assert_eq!(
2386 recorder.entities()[2]
2387 .callback_id
2388 .as_ref()
2389 .map(|id| id.as_str()),
2390 Some("on_tick")
2391 );
2392 assert_eq!(
2393 recorder.callback_effects()[0].callback_id.as_str(),
2394 "on_message"
2395 );
2396 assert_eq!(
2397 recorder.callback_effects()[0].entity_id.as_str(),
2398 "on_message"
2399 );
2400 assert_eq!(
2401 recorder.callback_effects()[1].callback_id.as_str(),
2402 "on_tick"
2403 );
2404 assert_eq!(
2405 recorder.callback_effects()[1].entity_id.as_str(),
2406 "/chatter"
2407 );
2408 }
2409
2410 #[test]
2411 fn synthesized_entity_ids_reject_collisions() {
2412 let mut recorder = MetadataRecorder::<1, 2, 0>::new();
2413 let mut context = NodeContext::new("test", &mut recorder);
2414 let mut node = context.create_node(NodeOptions::new("talker")).unwrap();
2415
2416 node.create_publisher_for_topic::<TestMsg>("/chatter")
2417 .unwrap();
2418 let result = node.create_publisher_for_topic::<TestMsg>("/chatter");
2419
2420 assert!(matches!(
2421 result,
2422 Err(NodeDeclError::Metadata(NodeMetadataError::DuplicateId))
2423 ));
2424 }
2425
2426 #[test]
2428 fn runtime_adapter_rejects_unknown_entities() {
2429 let mut node_runtime = FakeNodeRuntime::default();
2430 let mut runtime = NodeRuntimeAdapter::<_, 1, 1, 1>::new(&mut node_runtime);
2431 runtime
2432 .create_node(NodeId::new("node"), NodeOptions::new("talker"))
2433 .unwrap();
2434
2435 assert_eq!(
2436 runtime.create_node(NodeId::new("node"), NodeOptions::new("other")),
2437 Err(NodeDeclError::Metadata(NodeMetadataError::DuplicateId))
2438 );
2439 assert_eq!(
2440 runtime.record_callback_effect(
2441 CallbackId::new("cb"),
2442 CallbackEffectKind::Reads,
2443 EntityId::new("missing")
2444 ),
2445 Err(NodeDeclError::Metadata(NodeMetadataError::UnknownEntity))
2446 );
2447 }
2448
2449 #[test]
2450 fn component_rejects_effect_for_unknown_entity() {
2451 let mut recorder = MetadataRecorder::<1, 1, 1>::new();
2452 let mut context = NodeContext::new("test", &mut recorder);
2453 let result = context
2454 .callback(CallbackId::new("cb"))
2455 .reads(EntityId::new("missing"));
2456 assert!(matches!(
2457 result,
2458 Err(NodeDeclError::Metadata(NodeMetadataError::UnknownEntity))
2459 ));
2460 }
2461
2462 #[test]
2463 fn component_missing_export_error_message_is_clear() {
2464 assert_eq!(
2465 NodeDeclError::MissingExport.message(),
2466 MISSING_NODE_EXPORT_ERROR
2467 );
2468 assert_eq!(
2469 NodeDeclError::MissingExport.message(),
2470 "package has no exported nros component"
2471 );
2472 }
2473
2474 struct RobotComponent;
2475
2476 impl Node for RobotComponent {
2477 const NAME: &'static str = "robot_component";
2478
2479 fn register(context: &mut NodeContext<'_>) -> NodeResult<()> {
2480 {
2481 let mut sensors = context.create_node_with_id(
2482 NodeId::new("node_sensors"),
2483 NodeOptions::new("sensors"),
2484 )?;
2485 let _status =
2486 sensors.create_publisher::<TestMsg>(EntityId::new("pub_status"), "~/status")?;
2487 }
2488
2489 let mut control = context
2490 .create_node_with_id(NodeId::new("node_control"), NodeOptions::new("control"))?;
2491 let _cmd = control.create_subscription::<TestMsg>(
2492 EntityId::new("sub_cmd"),
2493 CallbackId::new("cb_cmd"),
2494 "~/cmd",
2495 )?;
2496 let _reset = control.create_service_server::<TestService>(
2497 EntityId::new("srv_reset"),
2498 CallbackId::new("cb_reset"),
2499 "reset",
2500 )?;
2501 let _navigate = control.create_action_server_with_callbacks::<TestAction>(
2502 EntityId::new("act_navigate"),
2503 CallbackId::new("cb_nav_goal"),
2504 CallbackId::new("cb_nav_cancel"),
2505 CallbackId::new("cb_nav_accepted"),
2506 "~/navigate",
2507 )?;
2508 let _gain = control.declare_parameter_with_default(
2509 EntityId::new("param_gain"),
2510 "gain",
2511 ParameterDefault::Double(copy_str("1.5")?),
2512 )?;
2513
2514 control
2515 .callback(CallbackId::new("cb_cmd"))
2516 .publishes(EntityId::new("pub_status"))?
2517 .reads(EntityId::new("param_gain"))?;
2518 control
2519 .callback(CallbackId::new("cb_nav_accepted"))
2520 .writes(EntityId::new("param_gain"))?;
2521
2522 Ok(())
2523 }
2524 }
2525
2526 #[test]
2528 fn component_api_records_multi_node_services() {
2529 let mut recorder = MetadataRecorder::<4, 12, 4>::new();
2530 record_node_metadata::<RobotComponent>(&mut recorder).unwrap();
2531
2532 assert_eq!(recorder.nodes().len(), 2);
2533 assert_eq!(recorder.nodes()[0].id.as_str(), "node_sensors");
2534 assert_eq!(recorder.nodes()[1].id.as_str(), "node_control");
2535
2536 let status = recorder
2537 .entities()
2538 .iter()
2539 .find(|entity| entity.id.as_str() == "pub_status")
2540 .unwrap();
2541 assert_eq!(status.kind, EntityKind::Publisher);
2542 assert_eq!(status.source_name.as_str(), "~/status");
2543 assert_eq!(status.source_name_kind, SourceNameKind::Private);
2544
2545 let reset = recorder
2546 .entities()
2547 .iter()
2548 .find(|entity| entity.id.as_str() == "srv_reset")
2549 .unwrap();
2550 assert_eq!(reset.kind, EntityKind::ServiceServer);
2551 assert_eq!(
2552 reset.callback_id.as_ref().map(|id| id.as_str()),
2553 Some("cb_reset")
2554 );
2555
2556 let navigate = recorder
2557 .entities()
2558 .iter()
2559 .find(|entity| entity.id.as_str() == "act_navigate")
2560 .unwrap();
2561 assert_eq!(navigate.kind, EntityKind::ActionServer);
2562 assert_eq!(
2563 navigate.callback_id.as_ref().map(|id| id.as_str()),
2564 Some("cb_nav_goal")
2565 );
2566 assert_eq!(
2567 navigate
2568 .action_cancel_callback_id
2569 .as_ref()
2570 .map(|id| id.as_str()),
2571 Some("cb_nav_cancel")
2572 );
2573 assert_eq!(
2574 navigate
2575 .action_accepted_callback_id
2576 .as_ref()
2577 .map(|id| id.as_str()),
2578 Some("cb_nav_accepted")
2579 );
2580
2581 let gain = recorder
2582 .entities()
2583 .iter()
2584 .find(|entity| entity.id.as_str() == "param_gain")
2585 .unwrap();
2586 assert_eq!(gain.kind, EntityKind::Parameter);
2587 assert!(matches!(
2588 gain.parameter_default.as_ref(),
2589 Some(ParameterDefault::Double(value)) if value.as_str() == "1.5"
2590 ));
2591
2592 assert_eq!(recorder.callback_effects().len(), 3);
2593 assert!(recorder.callback_effects().iter().any(|effect| {
2594 effect.callback_id.as_str() == "cb_cmd"
2595 && effect.kind == CallbackEffectKind::Publishes
2596 && effect.entity_id.as_str() == "pub_status"
2597 }));
2598 assert!(recorder.callback_effects().iter().any(|effect| {
2599 effect.callback_id.as_str() == "cb_nav_accepted"
2600 && effect.kind == CallbackEffectKind::Writes
2601 && effect.entity_id.as_str() == "param_gain"
2602 }));
2603 }
2604
2605 #[cfg(feature = "std")]
2606 #[test]
2607 fn component_api_json_contains_planner_callback_links() {
2608 let mut recorder = MetadataRecorder::<4, 12, 4>::new();
2609 record_node_metadata::<RobotComponent>(&mut recorder).unwrap();
2610
2611 let json = recorder
2612 .to_source_metadata_json(&crate::SourceMetadataExport::new(
2613 "demo_robot",
2614 RobotComponent::NAME,
2615 ))
2616 .unwrap();
2617
2618 assert!(json.contains("\"callbacks\":["));
2619 assert!(json.contains("\"id\":\"cb_cmd\",\"declaration_slot\":0"));
2620 assert!(json.contains("\"kind\":\"subscription\""));
2621 assert!(json.contains("\"id\":\"cb_reset\",\"declaration_slot\":1"));
2622 assert!(json.contains("\"kind\":\"service\""));
2623 assert!(json.contains("\"id\":\"cb_nav_goal\",\"declaration_slot\":2"));
2624 assert!(json.contains("\"kind\":\"action_goal\""));
2625 assert!(json.contains("\"id\":\"cb_nav_cancel\",\"declaration_slot\":3"));
2626 assert!(json.contains("\"kind\":\"action_cancel\""));
2627 assert!(json.contains("\"id\":\"cb_nav_accepted\",\"declaration_slot\":4"));
2628 assert!(json.contains("\"kind\":\"action_accepted\""));
2629 assert!(json.contains("\"kind\":\"publishes\",\"entity\":\"pub_status\""));
2630 assert!(json.contains("\"kind\":\"reads_parameter\",\"entity\":\"param_gain\""));
2631 assert!(json.contains("\"kind\":\"writes_parameter\",\"entity\":\"param_gain\""));
2632 assert!(json.contains("\"goal_callback\":\"cb_nav_goal\""));
2633 assert!(json.contains("\"cancel_callback\":\"cb_nav_cancel\""));
2634 assert!(json.contains("\"accepted_callback\":\"cb_nav_accepted\""));
2635 }
2636
2637 impl ExecutableNode for TalkerComponent {
2642 type State = u32;
2643
2644 fn init() -> u32 {
2645 0
2646 }
2647
2648 fn on_callback(state: &mut u32, callback: Callback<'_>, ctx: &mut CallbackCtx<'_>) {
2649 if callback.as_str() == "on_tick" {
2650 *state += 1;
2651 let _ = ctx.publish::<TestMsg, 64>(EntityId::new("pub_chatter"), &TestMsg);
2653 }
2654 }
2655 }
2656
2657 #[test]
2658 fn executable_component_callback_publishes_and_mutates_state() {
2659 use core::cell::RefCell;
2660
2661 struct RecordingResolver {
2662 last: RefCell<Option<(MetadataString, usize)>>,
2663 }
2664 impl PublisherResolver for RecordingResolver {
2665 fn publish_raw(&self, entity_id: &str, data: &[u8]) -> NodeResult<()> {
2666 *self.last.borrow_mut() = Some((copy_str(entity_id)?, data.len()));
2667 Ok(())
2668 }
2669 }
2670
2671 let resolver = RecordingResolver {
2672 last: RefCell::new(None),
2673 };
2674 let mut state = TalkerComponent::init();
2675 let mut ctx = CallbackCtx::new(&[], &resolver);
2676
2677 TalkerComponent::on_callback(
2679 &mut state,
2680 Callback::__from_id(CallbackId::new("other")),
2681 &mut ctx,
2682 );
2683 assert_eq!(state, 0);
2684 assert!(resolver.last.borrow().is_none());
2685
2686 TalkerComponent::on_callback(
2688 &mut state,
2689 Callback::__from_id(CallbackId::new("on_tick")),
2690 &mut ctx,
2691 );
2692 assert_eq!(state, 1);
2693 let last = resolver.last.borrow();
2694 let (entity, len) = last.as_ref().expect("a publish was recorded");
2695 assert_eq!(entity.as_str(), "pub_chatter");
2696 assert_eq!(*len, 4);
2698 }
2699
2700 #[test]
2704 fn callback_ctx_reply_sink_roundtrips() {
2705 struct NoopResolver;
2706 impl PublisherResolver for NoopResolver {
2707 fn publish_raw(&self, _entity_id: &str, _data: &[u8]) -> NodeResult<()> {
2708 Ok(())
2709 }
2710 }
2711 let resolver = NoopResolver;
2712 let mut reply_buf = [0u8; 64];
2713 let mut written = 0usize;
2714 {
2715 let mut ctx = CallbackCtx::with_reply(&[], &resolver, &mut reply_buf, &mut written);
2716 ctx.reply::<TestMsg, 64>(&TestMsg).unwrap();
2717 }
2718 assert_eq!(written, 4);
2720
2721 let mut ctx2 = CallbackCtx::new(&[], &resolver);
2723 assert!(ctx2.reply_raw(&[1, 2, 3]).is_err());
2724 }
2725
2726 #[cfg(feature = "safety-e2e")]
2730 #[test]
2731 fn callback_ctx_integrity_surface() {
2732 struct NoopResolver;
2733 impl PublisherResolver for NoopResolver {
2734 fn publish_raw(&self, _entity_id: &str, _data: &[u8]) -> NodeResult<()> {
2735 Ok(())
2736 }
2737 }
2738 let resolver = NoopResolver;
2739
2740 let ctx = CallbackCtx::new(&[], &resolver);
2742 assert!(ctx.integrity().is_none());
2743
2744 let status = crate::IntegrityStatus {
2746 gap: 2,
2747 duplicate: false,
2748 crc_valid: Some(true),
2749 };
2750 let ctx = CallbackCtx::new_with_integrity(&[], &resolver, &status);
2751 let got = ctx.integrity().expect("safety ctx carries status");
2752 assert_eq!(got.gap, 2);
2753 assert!(!got.duplicate);
2754 assert_eq!(got.crc_valid, Some(true));
2755 }
2756
2757 #[test]
2761 fn callback_ctx_decision_sink() {
2762 struct NoopResolver;
2763 impl PublisherResolver for NoopResolver {
2764 fn publish_raw(&self, _entity_id: &str, _data: &[u8]) -> NodeResult<()> {
2765 Ok(())
2766 }
2767 }
2768 let resolver = NoopResolver;
2769
2770 let mut gr = GoalResponse::Reject;
2771 {
2772 let mut ctx = CallbackCtx::with_goal_decision(&[], &resolver, &mut gr);
2773 ctx.set_goal_response(GoalResponse::AcceptAndExecute)
2774 .unwrap();
2775 assert!(ctx.set_cancel_response(CancelResponse::Ok).is_err());
2777 }
2778 assert!(matches!(gr, GoalResponse::AcceptAndExecute));
2779
2780 let mut cr = CancelResponse::Rejected;
2781 {
2782 let mut ctx = CallbackCtx::with_cancel_decision(&[], &resolver, &mut cr);
2783 ctx.set_cancel_response(CancelResponse::Ok).unwrap();
2784 }
2785 assert!(matches!(cr, CancelResponse::Ok));
2786
2787 let mut ctx3 = CallbackCtx::new(&[], &resolver);
2789 assert!(ctx3.set_goal_response(GoalResponse::Reject).is_err());
2790 assert!(ctx3.set_cancel_response(CancelResponse::Ok).is_err());
2791 }
2792
2793 #[test]
2796 fn tick_ctx_publish_and_action_ops() {
2797 use core::cell::Cell;
2798 struct RecPub {
2799 published: Cell<bool>,
2800 }
2801 impl PublisherResolver for RecPub {
2802 fn publish_raw(&self, _entity_id: &str, _data: &[u8]) -> NodeResult<()> {
2803 self.published.set(true);
2804 Ok(())
2805 }
2806 }
2807 struct RecAct {
2808 completed: bool,
2809 fed: bool,
2810 visited: usize,
2811 }
2812 impl ActionExecutor for RecAct {
2813 fn complete_goal_raw(
2814 &mut self,
2815 _action_entity: &str,
2816 _goal_id: &GoalId,
2817 _status: GoalStatus,
2818 _result: &[u8],
2819 ) -> NodeResult<()> {
2820 self.completed = true;
2821 Ok(())
2822 }
2823 fn publish_feedback_raw(
2824 &mut self,
2825 _action_entity: &str,
2826 _goal_id: &GoalId,
2827 _feedback: &[u8],
2828 ) -> NodeResult<()> {
2829 self.fed = true;
2830 Ok(())
2831 }
2832 fn for_each_active_goal(
2833 &self,
2834 _action_entity: &str,
2835 visit: &mut dyn FnMut(&GoalId, GoalStatus),
2836 ) {
2837 visit(&GoalId::zero(), GoalStatus::Executing);
2839 }
2840 }
2841
2842 struct RecClients;
2843 impl ClientDispatch for RecClients {
2844 fn call_raw(
2845 &mut self,
2846 _service: &str,
2847 _req: &[u8],
2848 _resp: &mut [u8],
2849 ) -> NodeResult<usize> {
2850 Err(NodeDeclError::Runtime)
2851 }
2852 fn send_goal_raw(&mut self, _action: &str, _goal: &[u8]) -> NodeResult<GoalId> {
2853 Err(NodeDeclError::Runtime)
2854 }
2855 }
2856
2857 let pubs = RecPub {
2858 published: Cell::new(false),
2859 };
2860 let mut acts = RecAct {
2861 completed: false,
2862 fed: false,
2863 visited: 0,
2864 };
2865 let mut clients = RecClients;
2866 let goal = GoalId::zero();
2867 let mut seen = 0usize;
2868 {
2869 let mut ctx = TickCtx::new(&pubs, &mut acts, &mut clients);
2870 ctx.publish::<TestMsg, 64>(EntityId::new("pub_x"), &TestMsg)
2871 .unwrap();
2872 ctx.for_each_active_goal(EntityId::new("act"), &mut |_id, _status| seen += 1);
2874 ctx.publish_feedback::<TestMsg, 64>(EntityId::new("act"), &goal, &TestMsg)
2875 .unwrap();
2876 ctx.complete_goal::<TestMsg, 64>(
2877 EntityId::new("act"),
2878 &goal,
2879 GoalStatus::Succeeded,
2880 &TestMsg,
2881 )
2882 .unwrap();
2883 }
2884 acts.visited = seen;
2885 assert!(pubs.published.get());
2886 assert!(acts.completed);
2887 assert!(acts.fed);
2888 assert_eq!(acts.visited, 1);
2889 }
2890
2891 #[test]
2895 fn node_dispatch_default_is_inline() {
2896 struct Dummy;
2897 impl Node for Dummy {
2898 const NAME: &'static str = "dummy";
2899 fn register(_: &mut NodeContext<'_>) -> NodeResult<()> {
2900 Ok(())
2901 }
2902 }
2903 assert_eq!(Dummy::DISPATCH, crate::DispatchStrategy::Inline);
2904 }
2905
2906 #[cfg(all(feature = "alloc", feature = "rmw-cffi"))]
2924 mod dispatch_probe_macro_test {
2925 use super::*;
2929
2930 pub struct DispatchProbe;
2931
2932 impl Node for DispatchProbe {
2933 const NAME: &'static str = "dispatch_probe";
2934 fn register(_: &mut NodeContext<'_>) -> NodeResult<()> {
2936 Ok(())
2937 }
2938 }
2939
2940 impl ExecutableNode for DispatchProbe {
2941 type State = ();
2942 fn init() -> Self::State {}
2943 fn on_callback(
2944 _state: &mut Self::State,
2945 _callback: Callback<'_>,
2946 _ctx: &mut CallbackCtx<'_>,
2947 ) {
2948 }
2949 }
2950
2951 nros_macros::node!(DispatchProbe);
2954 }
2955
2956 #[cfg(all(feature = "alloc", feature = "rmw-cffi"))]
2957 #[test]
2958 fn node_macro_emits_dispatch_strategy_symbol() {
2959 unsafe extern "C" {
2963 fn __nros_node_nros_dispatch_strategy() -> u8;
2964 }
2965 let strategy = unsafe { __nros_node_nros_dispatch_strategy() };
2966 assert_eq!(strategy, crate::DispatchStrategy::Inline as u8);
2970 assert_eq!(strategy, 0);
2971 }
2972
2973 #[cfg(all(feature = "alloc", feature = "rmw-cffi"))]
2990 #[test]
2991 fn node_macro_emits_on_callback_symbol() {
2992 unsafe extern "C" {
2993 fn __nros_node_nros_on_callback(
2994 state: *mut core::ffi::c_void,
2995 cb_id_ptr: *const u8,
2996 cb_id_len: usize,
2997 ctx: *mut core::ffi::c_void,
2998 );
2999 }
3000 let fn_ptr: unsafe extern "C" fn(
3010 *mut core::ffi::c_void,
3011 *const u8,
3012 usize,
3013 *mut core::ffi::c_void,
3014 ) = __nros_node_nros_on_callback;
3015 core::hint::black_box(fn_ptr);
3016 }
3017
3018 #[test]
3019 fn create_subscription_static_returns_tag_matching_topic() {
3020 let mut recorder = MetadataRecorder::<1, 1, 1>::new();
3021 let mut context = NodeContext::new("test", &mut recorder);
3022 let mut node = context.create_node(NodeOptions::new("listener")).unwrap();
3023 let tag = node
3024 .create_subscription_static::<TestMsg>("/chatter")
3025 .unwrap();
3026
3027 assert_eq!(tag.as_str(), "/chatter");
3028 assert!(tag == CallbackId::new("/chatter"));
3029 assert_eq!(recorder.entities().len(), 1);
3030 let entity = &recorder.entities()[0];
3031 assert_eq!(entity.kind, EntityKind::Subscription);
3032 assert_eq!(entity.source_name.as_str(), "/chatter");
3033 assert_eq!(
3034 entity.callback_id.as_ref().map(|id| id.as_str()),
3035 Some("/chatter")
3036 );
3037 }
3038
3039 #[test]
3040 fn create_service_static_returns_tag() {
3041 let mut recorder = MetadataRecorder::<1, 1, 1>::new();
3042 let mut context = NodeContext::new("test", &mut recorder);
3043 let mut node = context.create_node(NodeOptions::new("server")).unwrap();
3044 let tag = node
3045 .create_service_static::<TestService>("/add_two_ints")
3046 .unwrap();
3047
3048 assert_eq!(tag.as_str(), "/add_two_ints");
3049 assert!(tag == CallbackId::new("/add_two_ints"));
3050 assert_eq!(recorder.entities().len(), 1);
3051 let entity = &recorder.entities()[0];
3052 assert_eq!(entity.kind, EntityKind::ServiceServer);
3053 assert_eq!(entity.source_name.as_str(), "/add_two_ints");
3054 assert_eq!(
3055 entity.callback_id.as_ref().map(|id| id.as_str()),
3056 Some("/add_two_ints")
3057 );
3058 }
3059
3060 #[test]
3061 fn create_service_helpers_use_name_as_entity_and_callback_id() {
3062 let mut recorder = MetadataRecorder::<1, 2, 1>::new();
3063 let mut context = NodeContext::new("test", &mut recorder);
3064 let mut node = context.create_node(NodeOptions::new("services")).unwrap();
3065 let server = node
3066 .create_service_server_for_name::<TestService>("/add_two_ints")
3067 .unwrap();
3068 let client = node
3069 .create_service_client_for_name::<TestService>("/reset")
3070 .unwrap();
3071
3072 assert_eq!(server.id(), EntityId::new("/add_two_ints"));
3073 assert_eq!(client.id(), EntityId::new("/reset"));
3074 assert_eq!(recorder.entities().len(), 2);
3075
3076 let server = &recorder.entities()[0];
3077 assert_eq!(server.kind, EntityKind::ServiceServer);
3078 assert_eq!(server.id.as_str(), "/add_two_ints");
3079 assert_eq!(server.source_name.as_str(), "/add_two_ints");
3080 assert_eq!(
3081 server.callback_id.as_ref().map(|id| id.as_str()),
3082 Some("/add_two_ints")
3083 );
3084
3085 let client = &recorder.entities()[1];
3086 assert_eq!(client.kind, EntityKind::ServiceClient);
3087 assert_eq!(client.id.as_str(), "/reset");
3088 assert_eq!(client.source_name.as_str(), "/reset");
3089 assert!(client.callback_id.is_none());
3090 }
3091
3092 #[test]
3093 fn create_action_static_returns_tag() {
3094 let mut recorder = MetadataRecorder::<1, 1, 1>::new();
3095 let mut context = NodeContext::new("test", &mut recorder);
3096 let mut node = context.create_node(NodeOptions::new("server")).unwrap();
3097 let tag = node
3098 .create_action_static::<TestAction>("/fibonacci")
3099 .unwrap();
3100
3101 assert_eq!(tag.as_str(), "/fibonacci");
3102 assert!(tag == CallbackId::new("/fibonacci"));
3103 assert_eq!(recorder.entities().len(), 1);
3104 let entity = &recorder.entities()[0];
3105 assert_eq!(entity.kind, EntityKind::ActionServer);
3106 assert_eq!(entity.source_name.as_str(), "/fibonacci");
3107 assert_eq!(
3108 entity.callback_id.as_ref().map(|id| id.as_str()),
3109 Some("/fibonacci")
3110 );
3111 assert_eq!(
3112 entity
3113 .action_cancel_callback_id
3114 .as_ref()
3115 .map(|id| id.as_str()),
3116 Some("/fibonacci")
3117 );
3118 assert_eq!(
3119 entity
3120 .action_accepted_callback_id
3121 .as_ref()
3122 .map(|id| id.as_str()),
3123 Some("/fibonacci")
3124 );
3125 }
3126
3127 #[test]
3128 fn create_action_helpers_use_name_as_entity_and_default_callback_id() {
3129 let mut recorder = MetadataRecorder::<1, 2, 3>::new();
3130 let mut context = NodeContext::new("test", &mut recorder);
3131 let mut node = context.create_node(NodeOptions::new("actions")).unwrap();
3132 let server = node
3133 .create_action_server_for_name::<TestAction>("/fibonacci")
3134 .unwrap();
3135 let client = node
3136 .create_action_client_for_name::<TestAction>("/navigate")
3137 .unwrap();
3138
3139 assert_eq!(server.id(), EntityId::new("/fibonacci"));
3140 assert_eq!(client.id(), EntityId::new("/navigate"));
3141 assert_eq!(recorder.entities().len(), 2);
3142
3143 let server = &recorder.entities()[0];
3144 assert_eq!(server.kind, EntityKind::ActionServer);
3145 assert_eq!(server.id.as_str(), "/fibonacci");
3146 assert_eq!(server.source_name.as_str(), "/fibonacci");
3147 assert_eq!(
3148 server.callback_id.as_ref().map(|id| id.as_str()),
3149 Some("/fibonacci")
3150 );
3151 assert_eq!(
3152 server
3153 .action_cancel_callback_id
3154 .as_ref()
3155 .map(|id| id.as_str()),
3156 Some("/fibonacci")
3157 );
3158 assert_eq!(
3159 server
3160 .action_accepted_callback_id
3161 .as_ref()
3162 .map(|id| id.as_str()),
3163 Some("/fibonacci")
3164 );
3165
3166 let client = &recorder.entities()[1];
3167 assert_eq!(client.kind, EntityKind::ActionClient);
3168 assert_eq!(client.id.as_str(), "/navigate");
3169 assert_eq!(client.source_name.as_str(), "/navigate");
3170 assert!(client.callback_id.is_none());
3171 }
3172}