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 ExecutorFull,
46}
47
48impl NodeDeclError {
49 pub const fn message(self) -> &'static str {
51 match self {
52 Self::Metadata(NodeMetadataError::Capacity) => "component metadata capacity exceeded",
53 Self::Metadata(NodeMetadataError::NameTooLong) => "component metadata name too long",
54 Self::Metadata(NodeMetadataError::UnknownNode) => {
55 "component entity references an unknown node"
56 }
57 Self::Metadata(NodeMetadataError::UnknownEntity) => {
58 "component callback effect references an unknown entity"
59 }
60 Self::Metadata(NodeMetadataError::DuplicateId) => {
61 "component metadata contains a duplicate stable ID"
62 }
63 Self::MissingExport => MISSING_NODE_EXPORT_ERROR,
64 Self::Runtime => "component runtime rejected declaration",
65 Self::ExecutorFull => {
66 "executor callback table full — raise NROS_EXECUTOR_MAX_CBS \
67 (build-time, default 4)"
68 }
69 }
70 }
71}
72
73impl From<NodeMetadataError> for NodeDeclError {
74 fn from(value: NodeMetadataError) -> Self {
75 Self::Metadata(value)
76 }
77}
78
79pub trait Node {
81 const NAME: &'static str;
83
84 const DISPATCH: crate::DispatchStrategy = crate::DispatchStrategy::Inline;
91
92 fn register(context: &mut NodeContext<'_>) -> NodeResult<()>;
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct NodeOptions<'a> {
99 pub name: &'a str,
101 pub namespace: &'a str,
103 pub domain_id: u32,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
112pub struct Callback<'a> {
113 id: CallbackId<'a>,
114}
115
116impl<'a> Callback<'a> {
117 pub const fn as_str(self) -> &'a str {
119 self.id.as_str()
120 }
121
122 pub fn is_named(self, name: &str) -> bool {
124 self.as_str() == name
125 }
126
127 #[doc(hidden)]
129 pub const fn __from_id(id: CallbackId<'a>) -> Self {
130 Self { id }
131 }
132}
133
134impl<'a> NodeOptions<'a> {
135 pub const fn new(name: &'a str) -> Self {
137 Self {
138 name,
139 namespace: "/",
140 domain_id: 0,
141 }
142 }
143
144 pub const fn namespace(mut self, namespace: &'a str) -> Self {
146 self.namespace = namespace;
147 self
148 }
149
150 pub const fn domain_id(mut self, domain_id: u32) -> Self {
152 self.domain_id = domain_id;
153 self
154 }
155}
156
157pub trait NodeRuntime {
159 fn create_node(&mut self, id: NodeId<'_>, options: NodeOptions<'_>) -> NodeResult<()>;
161
162 fn create_entity(&mut self, metadata: EntityMetadata) -> NodeResult<()>;
164
165 fn record_callback_effect(
167 &mut self,
168 callback_id: CallbackId<'_>,
169 kind: CallbackEffectKind,
170 entity_id: EntityId<'_>,
171 ) -> NodeResult<()>;
172}
173
174impl<const MAX_NODES: usize, const MAX_ENTITIES: usize, const MAX_CALLBACKS: usize> NodeRuntime
175 for MetadataRecorder<MAX_NODES, MAX_ENTITIES, MAX_CALLBACKS>
176{
177 fn create_node(&mut self, id: NodeId<'_>, options: NodeOptions<'_>) -> NodeResult<()> {
178 self.push_node(id, options.name, options.namespace, options.domain_id)?;
179 Ok(())
180 }
181
182 fn create_entity(&mut self, metadata: EntityMetadata) -> NodeResult<()> {
183 self.push_entity(metadata)?;
184 Ok(())
185 }
186
187 fn record_callback_effect(
188 &mut self,
189 callback_id: CallbackId<'_>,
190 kind: CallbackEffectKind,
191 entity_id: EntityId<'_>,
192 ) -> NodeResult<()> {
193 self.push_callback_effect(callback_id, kind, entity_id)?;
194 Ok(())
195 }
196}
197
198pub trait DeclaredNodeRuntime {
205 type NodeHandle: Copy + Eq;
207
208 fn build_component_node(
210 &mut self,
211 id: NodeId<'_>,
212 options: NodeOptions<'_>,
213 ) -> NodeResult<Self::NodeHandle>;
214}
215
216#[derive(Debug, Clone, PartialEq, Eq)]
218pub struct RuntimeNodeRecord<H: Copy + Eq> {
219 slot: NodeSlot,
220 stable_id: MetadataString,
221 source_default_name: MetadataString,
222 handle: H,
223}
224
225impl<H: Copy + Eq> RuntimeNodeRecord<H> {
226 pub const fn slot(&self) -> NodeSlot {
228 self.slot
229 }
230
231 pub fn stable_id(&self) -> &str {
233 &self.stable_id
234 }
235
236 pub fn source_default_name(&self) -> &str {
238 &self.source_default_name
239 }
240
241 pub const fn handle(&self) -> H {
243 self.handle
244 }
245}
246
247pub struct NodeRuntimeAdapter<
249 'a,
250 R: DeclaredNodeRuntime + ?Sized,
251 const MAX_NODES: usize = { crate::node_metadata::DEFAULT_MAX_METADATA_NODES },
252 const MAX_ENTITIES: usize = { crate::node_metadata::DEFAULT_MAX_METADATA_ENTITIES },
253 const MAX_CALLBACKS: usize = { crate::node_metadata::DEFAULT_MAX_METADATA_CALLBACKS },
254> {
255 node_runtime: &'a mut R,
256 nodes: Vec<RuntimeNodeRecord<R::NodeHandle>, MAX_NODES>,
257 entities: Vec<EntityMetadata, MAX_ENTITIES>,
258 callback_effects: Vec<CallbackEffectMetadata, MAX_CALLBACKS>,
259}
260
261impl<
262 'a,
263 R: DeclaredNodeRuntime + ?Sized,
264 const MAX_NODES: usize,
265 const MAX_ENTITIES: usize,
266 const MAX_CALLBACKS: usize,
267> NodeRuntimeAdapter<'a, R, MAX_NODES, MAX_ENTITIES, MAX_CALLBACKS>
268{
269 pub fn new(node_runtime: &'a mut R) -> Self {
271 Self {
272 node_runtime,
273 nodes: Vec::new(),
274 entities: Vec::new(),
275 callback_effects: Vec::new(),
276 }
277 }
278
279 pub fn nodes(&self) -> &[RuntimeNodeRecord<R::NodeHandle>] {
281 &self.nodes
282 }
283
284 pub fn entities(&self) -> &[EntityMetadata] {
286 &self.entities
287 }
288
289 pub fn callback_effects(&self) -> &[CallbackEffectMetadata] {
291 &self.callback_effects
292 }
293
294 pub fn node_handle(&self, stable_id: NodeId<'_>) -> Option<R::NodeHandle> {
296 self.nodes
297 .iter()
298 .find(|node| node.stable_id() == stable_id.as_str())
299 .map(RuntimeNodeRecord::handle)
300 }
301
302 fn contains_node(&self, stable_id: &str) -> bool {
303 self.nodes.iter().any(|node| node.stable_id() == stable_id)
304 }
305
306 fn contains_entity(&self, stable_id: &str) -> bool {
307 self.entities
308 .iter()
309 .any(|entity| entity.id.as_str() == stable_id)
310 }
311
312 fn node_slot_for_id(&self, stable_id: &str) -> Option<NodeSlot> {
313 self.nodes
314 .iter()
315 .find(|node| node.stable_id() == stable_id)
316 .map(RuntimeNodeRecord::slot)
317 }
318
319 fn entity_slot_for_id(&self, stable_id: &str) -> Option<EntitySlot> {
320 self.entities
321 .iter()
322 .find(|entity| entity.id.as_str() == stable_id)
323 .and_then(|entity| entity.slot)
324 }
325
326 fn callback_slot_for_current_entity(
327 &self,
328 id: &str,
329 current_callbacks: &mut Vec<MetadataString, 3>,
330 next_callback_slot: &mut usize,
331 ) -> CallbackSlot {
332 if let Some(slot) = self.callback_slot_for_id(id) {
333 return slot;
334 }
335 if let Some((index, _)) = current_callbacks
336 .iter()
337 .enumerate()
338 .find(|(_, callback_id)| callback_id.as_str() == id)
339 {
340 return CallbackSlot::new(self.callback_slot_count() + index);
341 }
342 let slot = CallbackSlot::new(*next_callback_slot);
343 let _ = current_callbacks
344 .push(copy_str(id).expect("callback ID already fits metadata string capacity"));
345 *next_callback_slot += 1;
346 slot
347 }
348
349 fn callback_slot_for_id(&self, id: &str) -> Option<CallbackSlot> {
350 let mut seen = Vec::<&str, MAX_CALLBACKS>::new();
351 for entity in &self.entities {
352 for callback_id in entity_callback_ids(entity) {
353 let Some(callback_id) = callback_id else {
354 continue;
355 };
356 let callback_id = callback_id.as_str();
357 if seen.contains(&callback_id) {
358 continue;
359 }
360 if callback_id == id {
361 return Some(CallbackSlot::new(seen.len()));
362 }
363 let _ = seen.push(callback_id);
364 }
365 }
366 None
367 }
368
369 fn callback_slot_count(&self) -> usize {
370 let mut seen = Vec::<&str, MAX_CALLBACKS>::new();
371 for entity in &self.entities {
372 for callback_id in entity_callback_ids(entity) {
373 let Some(callback_id) = callback_id else {
374 continue;
375 };
376 let callback_id = callback_id.as_str();
377 if !seen.contains(&callback_id) {
378 let _ = seen.push(callback_id);
379 }
380 }
381 }
382 seen.len()
383 }
384}
385
386impl<
387 R: DeclaredNodeRuntime + ?Sized,
388 const MAX_NODES: usize,
389 const MAX_ENTITIES: usize,
390 const MAX_CALLBACKS: usize,
391> NodeRuntime for NodeRuntimeAdapter<'_, R, MAX_NODES, MAX_ENTITIES, MAX_CALLBACKS>
392{
393 fn create_node(&mut self, id: NodeId<'_>, options: NodeOptions<'_>) -> NodeResult<()> {
394 if self.contains_node(id.as_str()) {
395 return Err(NodeMetadataError::DuplicateId.into());
396 }
397 let handle = self.node_runtime.build_component_node(id, options)?;
398 let slot = NodeSlot::new(self.nodes.len());
399 self.nodes
400 .push(RuntimeNodeRecord {
401 slot,
402 stable_id: copy_str(id.as_str())?,
403 source_default_name: copy_str(options.name)?,
404 handle,
405 })
406 .map_err(|_| NodeDeclError::Metadata(NodeMetadataError::Capacity))?;
407 Ok(())
408 }
409
410 fn create_entity(&mut self, mut metadata: EntityMetadata) -> NodeResult<()> {
411 if !self.contains_node(metadata.node_id.as_str()) {
412 return Err(NodeMetadataError::UnknownNode.into());
413 }
414 if self.contains_entity(metadata.id.as_str()) {
415 return Err(NodeMetadataError::DuplicateId.into());
416 }
417 metadata.slot = Some(EntitySlot::new(self.entities.len()));
418 metadata.node_slot = self.node_slot_for_id(&metadata.node_id);
419 let mut current_callbacks = Vec::<MetadataString, 3>::new();
420 let mut next_callback_slot = self.callback_slot_count();
421 metadata.callback_slot = metadata.callback_id.as_ref().map(|callback_id| {
422 self.callback_slot_for_current_entity(
423 callback_id.as_str(),
424 &mut current_callbacks,
425 &mut next_callback_slot,
426 )
427 });
428 metadata.action_cancel_callback_slot =
429 metadata
430 .action_cancel_callback_id
431 .as_ref()
432 .map(|callback_id| {
433 self.callback_slot_for_current_entity(
434 callback_id.as_str(),
435 &mut current_callbacks,
436 &mut next_callback_slot,
437 )
438 });
439 metadata.action_accepted_callback_slot =
440 metadata
441 .action_accepted_callback_id
442 .as_ref()
443 .map(|callback_id| {
444 self.callback_slot_for_current_entity(
445 callback_id.as_str(),
446 &mut current_callbacks,
447 &mut next_callback_slot,
448 )
449 });
450 self.entities
451 .push(metadata)
452 .map_err(|_| NodeDeclError::Metadata(NodeMetadataError::Capacity))?;
453 Ok(())
454 }
455
456 fn record_callback_effect(
457 &mut self,
458 callback_id: CallbackId<'_>,
459 kind: CallbackEffectKind,
460 entity_id: EntityId<'_>,
461 ) -> NodeResult<()> {
462 if !self.contains_entity(entity_id.as_str()) {
463 return Err(NodeMetadataError::UnknownEntity.into());
464 }
465 self.callback_effects
466 .push(CallbackEffectMetadata {
467 callback_id: copy_str(callback_id.as_str())?,
468 callback_slot: self.callback_slot_for_id(callback_id.as_str()),
469 kind,
470 entity_id: copy_str(entity_id.as_str())?,
471 entity_slot: self.entity_slot_for_id(entity_id.as_str()),
472 })
473 .map_err(|_| NodeDeclError::Metadata(NodeMetadataError::Capacity))?;
474 Ok(())
475 }
476}
477
478#[cfg(feature = "rmw-cffi")]
479impl DeclaredNodeRuntime for crate::Executor<'static> {
480 type NodeHandle = nros_node::executor::NodeId;
481
482 fn build_component_node(
483 &mut self,
484 _id: NodeId<'_>,
485 options: NodeOptions<'_>,
486 ) -> NodeResult<Self::NodeHandle> {
487 self.node_builder(options.name)
488 .namespace(options.namespace)
489 .domain_id(options.domain_id)
490 .build()
491 .map_err(|_| NodeDeclError::Runtime)
492 }
493}
494
495#[cfg(feature = "rmw-cffi")]
497pub type NodeExecutorRuntime<
498 'a,
499 const MAX_NODES: usize = { crate::node_metadata::DEFAULT_MAX_METADATA_NODES },
500 const MAX_ENTITIES: usize = { crate::node_metadata::DEFAULT_MAX_METADATA_ENTITIES },
501 const MAX_CALLBACKS: usize = { crate::node_metadata::DEFAULT_MAX_METADATA_CALLBACKS },
502> = NodeRuntimeAdapter<'a, crate::Executor<'static>, MAX_NODES, MAX_ENTITIES, MAX_CALLBACKS>;
503
504pub struct NodeContext<'a, R: NodeRuntime + ?Sized = dyn NodeRuntime + 'a> {
506 component_name: &'static str,
507 runtime: &'a mut R,
508 params: &'a [(&'a str, &'a str)],
515}
516
517impl<'a, R: NodeRuntime + ?Sized> NodeContext<'a, R> {
518 pub fn new(component_name: &'static str, runtime: &'a mut R) -> Self {
520 Self {
521 component_name,
522 runtime,
523 params: &[],
524 }
525 }
526
527 pub fn set_params(&mut self, params: &'a [(&'a str, &'a str)]) {
530 self.params = params;
531 }
532
533 pub fn param(&self, name: &str) -> Option<&'a str> {
537 self.params
538 .iter()
539 .find(|(k, _)| *k == name)
540 .map(|(_, v)| *v)
541 }
542
543 pub const fn component_name(&self) -> &'static str {
545 self.component_name
546 }
547
548 #[doc(hidden)]
553 pub fn create_node_with_id<'id>(
554 &mut self,
555 id: NodeId<'id>,
556 options: NodeOptions<'_>,
557 ) -> NodeResult<DeclaredNode<'_, 'id, R>> {
558 self.runtime.create_node(id, options)?;
559 Ok(DeclaredNode {
560 runtime: self.runtime,
561 id,
562 current_group: None,
563 })
564 }
565
566 pub fn create_node<'id>(
572 &mut self,
573 options: NodeOptions<'id>,
574 ) -> NodeResult<DeclaredNode<'_, 'id, R>> {
575 self.create_node_with_id(NodeId::new(options.name), options)
576 }
577
578 #[deprecated(note = "use create_node(NodeOptions)")]
580 pub fn create_node_with_options<'id>(
581 &mut self,
582 options: NodeOptions<'id>,
583 ) -> NodeResult<DeclaredNode<'_, 'id, R>> {
584 self.create_node(options)
585 }
586
587 #[doc(hidden)]
589 pub fn callback<'id>(&mut self, id: CallbackId<'id>) -> CallbackEffects<'_, 'id, R> {
590 CallbackEffects {
591 runtime: self.runtime,
592 id,
593 }
594 }
595}
596
597pub struct DeclaredNode<'ctx, 'id, R: NodeRuntime + ?Sized = dyn NodeRuntime + 'ctx> {
599 runtime: &'ctx mut R,
600 id: NodeId<'id>,
601 current_group: Option<MetadataString>,
608}
609
610impl<'ctx, 'id, R: NodeRuntime + ?Sized> DeclaredNode<'ctx, 'id, R> {
611 #[doc(hidden)]
613 pub const fn id(&self) -> NodeId<'id> {
614 self.id
615 }
616
617 #[track_caller]
624 pub fn callback_group(&mut self, group: &str) -> NodeResult<&mut Self> {
625 self.current_group = Some(copy_str(group)?);
626 Ok(self)
627 }
628
629 fn declare_entity(&mut self, mut metadata: EntityMetadata) -> NodeResult<()> {
634 if metadata.callback_group.is_none() {
635 metadata.callback_group = self.current_group.clone();
636 }
637 self.runtime.create_entity(metadata)
638 }
639
640 #[track_caller]
642 #[doc(hidden)]
643 pub fn create_publisher<'entity, M: RosMessage>(
644 &mut self,
645 id: EntityId<'entity>,
646 topic: &str,
647 ) -> NodeResult<NodePublisher<'entity, M>> {
648 self.create_publisher_with_qos::<M>(id, topic, QosSettings::default())
649 }
650
651 #[track_caller]
657 pub fn create_publisher_for_topic<'entity, M: RosMessage>(
658 &mut self,
659 topic: &'entity str,
660 ) -> NodeResult<NodePublisher<'entity, M>> {
661 self.create_publisher_for_topic_with_qos::<M>(topic, QosSettings::default())
662 }
663
664 #[track_caller]
666 pub fn create_publisher_for_topic_with_qos<'entity, M: RosMessage>(
667 &mut self,
668 topic: &'entity str,
669 qos: QosSettings,
670 ) -> NodeResult<NodePublisher<'entity, M>> {
671 self.create_publisher_with_qos::<M>(EntityId::new(topic), topic, qos)
672 }
673
674 #[track_caller]
676 #[doc(hidden)]
677 pub fn create_publisher_with_qos<'entity, M: RosMessage>(
678 &mut self,
679 id: EntityId<'entity>,
680 topic: &str,
681 qos: QosSettings,
682 ) -> NodeResult<NodePublisher<'entity, M>> {
683 let mut metadata = entity_metadata(EntityMetadataSpec {
684 id,
685 node_id: self.id,
686 kind: EntityKind::Publisher,
687 source_name: topic,
688 type_name: M::TYPE_NAME,
689 type_hash: M::TYPE_HASH,
690 qos,
691 })?;
692 metadata.source = SourceLocationMetadata::caller()?;
693 self.declare_entity(metadata)?;
694 Ok(NodePublisher::new(id))
695 }
696
697 #[track_caller]
699 #[doc(hidden)]
700 pub fn create_subscription<'entity, 'callback, M: RosMessage>(
701 &mut self,
702 id: EntityId<'entity>,
703 callback_id: CallbackId<'callback>,
704 topic: &str,
705 ) -> NodeResult<NodeSubscription<'entity, M>> {
706 self.create_subscription_with_qos::<M>(id, callback_id, topic, QosSettings::default())
707 }
708
709 #[track_caller]
714 #[doc(hidden)]
715 pub fn create_subscription_for_callback<'callback, M: RosMessage>(
716 &mut self,
717 callback_id: CallbackId<'callback>,
718 topic: &str,
719 ) -> NodeResult<NodeSubscription<'callback, M>> {
720 self.create_subscription_for_callback_with_qos::<M>(
721 callback_id,
722 topic,
723 QosSettings::default(),
724 )
725 }
726
727 #[track_caller]
730 pub fn create_subscription_for_callback_name<'callback, M: RosMessage>(
731 &mut self,
732 callback_name: &'callback str,
733 topic: &str,
734 ) -> NodeResult<NodeSubscription<'callback, M>> {
735 self.create_subscription_for_callback::<M>(CallbackId::new(callback_name), topic)
736 }
737
738 #[track_caller]
748 pub fn create_subscription_for_callback_name_with_safety<'callback, M: RosMessage>(
749 &mut self,
750 callback_name: &'callback str,
751 topic: &str,
752 ) -> NodeResult<NodeSubscription<'callback, M>> {
753 let callback_id = CallbackId::new(callback_name);
754 let id = EntityId::new(callback_id.as_str());
755 let mut metadata = entity_metadata(EntityMetadataSpec {
756 id,
757 node_id: self.id,
758 kind: EntityKind::Subscription,
759 source_name: topic,
760 type_name: M::TYPE_NAME,
761 type_hash: M::TYPE_HASH,
762 qos: QosSettings::default(),
763 })?;
764 metadata.callback_id = Some(copy_str(callback_id.as_str())?);
765 metadata.callback_source = SourceLocationMetadata::caller()?;
766 metadata.source = metadata.callback_source.clone();
767 metadata.safety = true;
768 self.declare_entity(metadata)?;
769 Ok(NodeSubscription::new(id))
770 }
771
772 #[track_caller]
774 #[doc(hidden)]
775 pub fn create_subscription_for_callback_with_qos<'callback, M: RosMessage>(
776 &mut self,
777 callback_id: CallbackId<'callback>,
778 topic: &str,
779 qos: QosSettings,
780 ) -> NodeResult<NodeSubscription<'callback, M>> {
781 self.create_subscription_with_qos::<M>(
782 EntityId::new(callback_id.as_str()),
783 callback_id,
784 topic,
785 qos,
786 )
787 }
788
789 #[track_caller]
791 pub fn create_subscription_for_topic<'entity, M: RosMessage>(
792 &mut self,
793 topic: &'entity str,
794 ) -> NodeResult<NodeSubscription<'entity, M>> {
795 self.create_subscription_for_topic_with_qos::<M>(topic, QosSettings::default())
796 }
797
798 #[track_caller]
800 pub fn create_subscription_for_topic_with_qos<'entity, M: RosMessage>(
801 &mut self,
802 topic: &'entity str,
803 qos: QosSettings,
804 ) -> NodeResult<NodeSubscription<'entity, M>> {
805 self.create_subscription_with_qos::<M>(
806 EntityId::new(topic),
807 CallbackId::new(topic),
808 topic,
809 qos,
810 )
811 }
812
813 #[track_caller]
815 #[doc(hidden)]
816 pub fn create_subscription_with_qos<'entity, 'callback, M: RosMessage>(
817 &mut self,
818 id: EntityId<'entity>,
819 callback_id: CallbackId<'callback>,
820 topic: &str,
821 qos: QosSettings,
822 ) -> NodeResult<NodeSubscription<'entity, M>> {
823 let mut metadata = entity_metadata(EntityMetadataSpec {
824 id,
825 node_id: self.id,
826 kind: EntityKind::Subscription,
827 source_name: topic,
828 type_name: M::TYPE_NAME,
829 type_hash: M::TYPE_HASH,
830 qos,
831 })?;
832 metadata.callback_id = Some(copy_str(callback_id.as_str())?);
833 metadata.callback_source = SourceLocationMetadata::caller()?;
834 metadata.source = metadata.callback_source.clone();
835 self.declare_entity(metadata)?;
836 Ok(NodeSubscription::new(id))
837 }
838
839 #[track_caller]
851 pub fn create_subscription_static<M: RosMessage>(
852 &mut self,
853 topic: &'static str,
854 ) -> NodeResult<SubscriptionTag> {
855 let id = EntityId::new(topic);
856 let callback_id = CallbackId::new(topic);
857 let mut metadata = entity_metadata(EntityMetadataSpec {
858 id,
859 node_id: self.id,
860 kind: EntityKind::Subscription,
861 source_name: topic,
862 type_name: M::TYPE_NAME,
863 type_hash: M::TYPE_HASH,
864 qos: QosSettings::default(),
865 })?;
866 metadata.callback_id = Some(copy_str(callback_id.as_str())?);
867 metadata.callback_source = SourceLocationMetadata::caller()?;
868 metadata.source = metadata.callback_source.clone();
869 self.declare_entity(metadata)?;
870 Ok(SubscriptionTag::new(topic))
871 }
872
873 #[track_caller]
875 #[doc(hidden)]
876 pub fn create_timer<'entity, 'callback>(
877 &mut self,
878 id: EntityId<'entity>,
879 callback_id: CallbackId<'callback>,
880 period: TimerDuration,
881 ) -> NodeResult<NodeTimer<'entity>> {
882 let mut metadata = entity_metadata(EntityMetadataSpec {
883 id,
884 node_id: self.id,
885 kind: EntityKind::Timer,
886 source_name: "",
887 type_name: "",
888 type_hash: "",
889 qos: QosSettings::default(),
890 })?;
891 metadata.callback_id = Some(copy_str(callback_id.as_str())?);
892 metadata.callback_source = SourceLocationMetadata::caller()?;
893 metadata.source = metadata.callback_source.clone();
894 metadata.period_ms = Some(period.as_millis());
895 self.declare_entity(metadata)?;
896 Ok(NodeTimer::new(id))
897 }
898
899 #[track_caller]
901 #[doc(hidden)]
902 pub fn create_timer_for_callback<'callback>(
903 &mut self,
904 callback_id: CallbackId<'callback>,
905 period: TimerDuration,
906 ) -> NodeResult<NodeTimer<'callback>> {
907 self.create_timer(EntityId::new(callback_id.as_str()), callback_id, period)
908 }
909
910 #[track_caller]
913 pub fn create_timer_for_callback_name<'callback>(
914 &mut self,
915 callback_name: &'callback str,
916 period: TimerDuration,
917 ) -> NodeResult<NodeTimer<'callback>> {
918 self.create_timer_for_callback(CallbackId::new(callback_name), period)
919 }
920
921 #[track_caller]
923 #[doc(hidden)]
924 pub fn create_service_server<'entity, 'callback, S: RosService>(
925 &mut self,
926 id: EntityId<'entity>,
927 callback_id: CallbackId<'callback>,
928 service_name: &str,
929 ) -> NodeResult<NodeServiceServer<'entity, S>> {
930 let mut metadata = entity_metadata(EntityMetadataSpec {
931 id,
932 node_id: self.id,
933 kind: EntityKind::ServiceServer,
934 source_name: service_name,
935 type_name: S::SERVICE_NAME,
936 type_hash: S::SERVICE_HASH,
937 qos: QosSettings::default(),
938 })?;
939 metadata.callback_id = Some(copy_str(callback_id.as_str())?);
940 metadata.callback_source = SourceLocationMetadata::caller()?;
941 metadata.source = metadata.callback_source.clone();
942 self.declare_entity(metadata)?;
943 Ok(NodeServiceServer::new(id))
944 }
945
946 #[track_caller]
949 pub fn create_service_server_for_name<'entity, S: RosService>(
950 &mut self,
951 name: &'entity str,
952 ) -> NodeResult<NodeServiceServer<'entity, S>> {
953 self.create_service_server::<S>(EntityId::new(name), CallbackId::new(name), name)
954 }
955
956 #[track_caller]
959 pub fn create_service_server_for_name_with_callback<'entity, S: RosService>(
960 &mut self,
961 name: &'entity str,
962 callback_name: &str,
963 ) -> NodeResult<NodeServiceServer<'entity, S>> {
964 self.create_service_server::<S>(EntityId::new(name), CallbackId::new(callback_name), name)
965 }
966
967 #[track_caller]
979 pub fn create_service_static<S: RosService>(
980 &mut self,
981 name: &'static str,
982 ) -> NodeResult<ServiceTag> {
983 self.create_service_server_for_name::<S>(name)?;
984 Ok(ServiceTag::new(name))
985 }
986
987 #[track_caller]
989 #[doc(hidden)]
990 pub fn create_service_client<'entity, S: RosService>(
991 &mut self,
992 id: EntityId<'entity>,
993 service_name: &str,
994 ) -> NodeResult<NodeServiceClient<'entity, S>> {
995 let mut metadata = entity_metadata(EntityMetadataSpec {
996 id,
997 node_id: self.id,
998 kind: EntityKind::ServiceClient,
999 source_name: service_name,
1000 type_name: S::SERVICE_NAME,
1001 type_hash: S::SERVICE_HASH,
1002 qos: QosSettings::default(),
1003 })?;
1004 metadata.source = SourceLocationMetadata::caller()?;
1005 self.declare_entity(metadata)?;
1006 Ok(NodeServiceClient::new(id))
1007 }
1008
1009 #[track_caller]
1011 pub fn create_service_client_for_name<'entity, S: RosService>(
1012 &mut self,
1013 name: &'entity str,
1014 ) -> NodeResult<NodeServiceClient<'entity, S>> {
1015 self.create_service_client::<S>(EntityId::new(name), name)
1016 }
1017
1018 #[track_caller]
1020 #[doc(hidden)]
1021 pub fn create_action_server<'entity, 'callback, A: RosAction>(
1022 &mut self,
1023 id: EntityId<'entity>,
1024 callback_id: CallbackId<'callback>,
1025 action_name: &str,
1026 ) -> NodeResult<NodeActionServer<'entity, A>> {
1027 self.create_action_server_with_callbacks::<A>(
1028 id,
1029 callback_id,
1030 callback_id,
1031 callback_id,
1032 action_name,
1033 )
1034 }
1035
1036 #[track_caller]
1038 #[doc(hidden)]
1039 pub fn create_action_server_with_callbacks<'entity, 'goal, 'cancel, 'accepted, A: RosAction>(
1040 &mut self,
1041 id: EntityId<'entity>,
1042 goal_callback_id: CallbackId<'goal>,
1043 cancel_callback_id: CallbackId<'cancel>,
1044 accepted_callback_id: CallbackId<'accepted>,
1045 action_name: &str,
1046 ) -> NodeResult<NodeActionServer<'entity, A>> {
1047 let mut metadata = entity_metadata(EntityMetadataSpec {
1048 id,
1049 node_id: self.id,
1050 kind: EntityKind::ActionServer,
1051 source_name: action_name,
1052 type_name: A::ACTION_NAME,
1053 type_hash: A::ACTION_HASH,
1054 qos: QosSettings::default(),
1055 })?;
1056 metadata.callback_id = Some(copy_str(goal_callback_id.as_str())?);
1057 metadata.callback_source = SourceLocationMetadata::caller()?;
1058 metadata.action_cancel_callback_id = Some(copy_str(cancel_callback_id.as_str())?);
1059 metadata.action_cancel_source = metadata.callback_source.clone();
1060 metadata.action_accepted_callback_id = Some(copy_str(accepted_callback_id.as_str())?);
1061 metadata.action_accepted_source = metadata.callback_source.clone();
1062 metadata.source = metadata.callback_source.clone();
1063 self.declare_entity(metadata)?;
1064 Ok(NodeActionServer::new(id))
1065 }
1066
1067 #[track_caller]
1070 pub fn create_action_server_for_name<'entity, A: RosAction>(
1071 &mut self,
1072 name: &'entity str,
1073 ) -> NodeResult<NodeActionServer<'entity, A>> {
1074 self.create_action_server::<A>(EntityId::new(name), CallbackId::new(name), name)
1075 }
1076
1077 #[track_caller]
1080 pub fn create_action_server_for_name_with_callbacks<'entity, A: RosAction>(
1081 &mut self,
1082 name: &'entity str,
1083 goal_callback_name: &str,
1084 cancel_callback_name: &str,
1085 accepted_callback_name: &str,
1086 ) -> NodeResult<NodeActionServer<'entity, A>> {
1087 self.create_action_server_with_callbacks::<A>(
1088 EntityId::new(name),
1089 CallbackId::new(goal_callback_name),
1090 CallbackId::new(cancel_callback_name),
1091 CallbackId::new(accepted_callback_name),
1092 name,
1093 )
1094 }
1095
1096 #[track_caller]
1112 pub fn create_action_static<A: RosAction>(
1113 &mut self,
1114 name: &'static str,
1115 ) -> NodeResult<ActionTag> {
1116 self.create_action_server_for_name::<A>(name)?;
1117 Ok(ActionTag::new(name))
1118 }
1119
1120 #[track_caller]
1122 #[doc(hidden)]
1123 pub fn create_action_client<'entity, A: RosAction>(
1124 &mut self,
1125 id: EntityId<'entity>,
1126 action_name: &str,
1127 ) -> NodeResult<NodeActionClient<'entity, A>> {
1128 let mut metadata = entity_metadata(EntityMetadataSpec {
1129 id,
1130 node_id: self.id,
1131 kind: EntityKind::ActionClient,
1132 source_name: action_name,
1133 type_name: A::ACTION_NAME,
1134 type_hash: A::ACTION_HASH,
1135 qos: QosSettings::default(),
1136 })?;
1137 metadata.source = SourceLocationMetadata::caller()?;
1138 self.declare_entity(metadata)?;
1139 Ok(NodeActionClient::new(id))
1140 }
1141
1142 #[track_caller]
1144 pub fn create_action_client_for_name<'entity, A: RosAction>(
1145 &mut self,
1146 name: &'entity str,
1147 ) -> NodeResult<NodeActionClient<'entity, A>> {
1148 self.create_action_client::<A>(EntityId::new(name), name)
1149 }
1150
1151 #[track_caller]
1164 pub fn create_action_client_with_callbacks_for_name<'entity, A: RosAction>(
1165 &mut self,
1166 name: &'entity str,
1167 result_callback_name: &str,
1168 feedback_callback_name: &str,
1169 ) -> NodeResult<NodeActionClient<'entity, A>> {
1170 let mut metadata = entity_metadata(EntityMetadataSpec {
1171 id: EntityId::new(name),
1172 node_id: self.id,
1173 kind: EntityKind::ActionClient,
1174 source_name: name,
1175 type_name: A::ACTION_NAME,
1176 type_hash: A::ACTION_HASH,
1177 qos: QosSettings::default(),
1178 })?;
1179 metadata.callback_id = Some(copy_str(result_callback_name)?);
1180 metadata.action_accepted_callback_id = Some(copy_str(feedback_callback_name)?);
1181 metadata.callback_source = SourceLocationMetadata::caller()?;
1182 metadata.source = metadata.callback_source.clone();
1183 self.declare_entity(metadata)?;
1184 Ok(NodeActionClient::new(EntityId::new(name)))
1185 }
1186
1187 #[track_caller]
1189 #[doc(hidden)]
1190 pub fn declare_parameter<'entity>(
1191 &mut self,
1192 id: EntityId<'entity>,
1193 name: &str,
1194 parameter_type: ParameterType,
1195 ) -> NodeResult<NodeParameter<'entity>> {
1196 self.declare_parameter_with_default(id, name, ParameterDefault::for_type(parameter_type)?)
1197 }
1198
1199 #[track_caller]
1201 #[doc(hidden)]
1202 pub fn declare_parameter_with_default<'entity>(
1203 &mut self,
1204 id: EntityId<'entity>,
1205 name: &str,
1206 default: ParameterDefault,
1207 ) -> NodeResult<NodeParameter<'entity>> {
1208 let mut metadata = entity_metadata(EntityMetadataSpec {
1209 id,
1210 node_id: self.id,
1211 kind: EntityKind::Parameter,
1212 source_name: name,
1213 type_name: "",
1214 type_hash: "",
1215 qos: QosSettings::default(),
1216 })?;
1217 metadata.parameter_type = Some(default.parameter_type());
1218 metadata.parameter_default = Some(default);
1219 metadata.source = SourceLocationMetadata::caller()?;
1220 self.declare_entity(metadata)?;
1221 Ok(NodeParameter::new(id))
1222 }
1223
1224 #[track_caller]
1226 pub fn declare_parameter_for_name<'entity>(
1227 &mut self,
1228 name: &'entity str,
1229 parameter_type: ParameterType,
1230 ) -> NodeResult<NodeParameter<'entity>> {
1231 self.declare_parameter(EntityId::new(name), name, parameter_type)
1232 }
1233
1234 #[track_caller]
1237 pub fn declare_parameter_for_name_with_default<'entity>(
1238 &mut self,
1239 name: &'entity str,
1240 default: ParameterDefault,
1241 ) -> NodeResult<NodeParameter<'entity>> {
1242 self.declare_parameter_with_default(EntityId::new(name), name, default)
1243 }
1244
1245 #[doc(hidden)]
1247 pub fn callback<'callback>(
1248 &mut self,
1249 id: CallbackId<'callback>,
1250 ) -> CallbackEffects<'_, 'callback, R> {
1251 CallbackEffects {
1252 runtime: self.runtime,
1253 id,
1254 }
1255 }
1256
1257 pub fn callback_for_name<'callback>(
1260 &mut self,
1261 name: &'callback str,
1262 ) -> CallbackEffects<'_, 'callback, R> {
1263 self.callback(CallbackId::new(name))
1264 }
1265}
1266
1267pub struct CallbackEffects<'ctx, 'id, R: NodeRuntime + ?Sized = dyn NodeRuntime + 'ctx> {
1269 runtime: &'ctx mut R,
1270 id: CallbackId<'id>,
1271}
1272
1273impl<'ctx, 'id, R: NodeRuntime + ?Sized> CallbackEffects<'ctx, 'id, R> {
1274 #[doc(hidden)]
1276 pub fn reads(self, entity_id: EntityId<'_>) -> NodeResult<Self> {
1277 self.runtime
1278 .record_callback_effect(self.id, CallbackEffectKind::Reads, entity_id)?;
1279 Ok(self)
1280 }
1281
1282 pub fn reads_entity(self, entity: &impl DeclaredEntity) -> NodeResult<Self> {
1284 self.reads(entity.entity_id())
1285 }
1286
1287 #[doc(hidden)]
1289 pub fn publishes(self, entity_id: EntityId<'_>) -> NodeResult<Self> {
1290 self.runtime
1291 .record_callback_effect(self.id, CallbackEffectKind::Publishes, entity_id)?;
1292 Ok(self)
1293 }
1294
1295 pub fn publishes_entity(self, entity: &impl DeclaredEntity) -> NodeResult<Self> {
1297 self.publishes(entity.entity_id())
1298 }
1299
1300 #[doc(hidden)]
1302 pub fn writes(self, entity_id: EntityId<'_>) -> NodeResult<Self> {
1303 self.runtime
1304 .record_callback_effect(self.id, CallbackEffectKind::Writes, entity_id)?;
1305 Ok(self)
1306 }
1307
1308 pub fn writes_entity(self, entity: &impl DeclaredEntity) -> NodeResult<Self> {
1310 self.writes(entity.entity_id())
1311 }
1312}
1313
1314#[doc(hidden)]
1316pub trait DeclaredEntity {
1317 fn entity_id(&self) -> EntityId<'_>;
1319}
1320
1321macro_rules! component_handle {
1322 ($name:ident $(, $type_param:ident)?) => {
1323 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1325 pub struct $name<'id $(, $type_param)?> {
1326 id: EntityId<'id>,
1327 _marker: PhantomData<($($type_param,)?)>,
1328 }
1329
1330 impl<'id $(, $type_param)?> $name<'id $(, $type_param)?> {
1331 const fn new(id: EntityId<'id>) -> Self {
1332 Self {
1333 id,
1334 _marker: PhantomData,
1335 }
1336 }
1337
1338 #[doc(hidden)]
1340 pub const fn id(&self) -> EntityId<'id> {
1341 self.id
1342 }
1343 }
1344
1345 impl<'id $(, $type_param)?> DeclaredEntity for $name<'id $(, $type_param)?> {
1346 fn entity_id(&self) -> EntityId<'_> {
1347 self.id
1348 }
1349 }
1350 };
1351}
1352
1353component_handle!(NodePublisher, M);
1354component_handle!(NodeSubscription, M);
1355component_handle!(NodeServiceServer, S);
1356component_handle!(NodeServiceClient, S);
1357component_handle!(NodeActionServer, A);
1358component_handle!(NodeActionClient, A);
1359component_handle!(NodeTimer);
1360component_handle!(NodeParameter);
1361
1362pub trait PublisherResolver {
1383 fn publish_raw(&self, entity_id: &str, data: &[u8]) -> NodeResult<()>;
1387}
1388
1389struct ReplySink<'a> {
1393 buf: &'a mut [u8],
1394 written: &'a mut usize,
1395}
1396
1397enum DecisionSink<'a> {
1403 Goal(&'a mut GoalResponse),
1404 Cancel(&'a mut CancelResponse),
1405}
1406
1407pub struct CallbackCtx<'a> {
1417 payload: &'a [u8],
1418 publishers: &'a dyn PublisherResolver,
1419 reply: Option<ReplySink<'a>>,
1420 decision: Option<DecisionSink<'a>>,
1421 #[cfg(feature = "safety-e2e")]
1427 integrity: Option<&'a crate::IntegrityStatus>,
1428 #[cfg(feature = "param-services")]
1433 params: Option<&'a crate::ParameterServer>,
1434}
1435
1436impl<'a> CallbackCtx<'a> {
1437 pub fn new(payload: &'a [u8], publishers: &'a dyn PublisherResolver) -> Self {
1440 Self {
1441 payload,
1442 publishers,
1443 reply: None,
1444 decision: None,
1445 #[cfg(feature = "safety-e2e")]
1446 integrity: None,
1447 #[cfg(feature = "param-services")]
1448 params: None,
1449 }
1450 }
1451
1452 #[cfg(feature = "safety-e2e")]
1458 pub fn new_with_integrity(
1459 payload: &'a [u8],
1460 publishers: &'a dyn PublisherResolver,
1461 integrity: &'a crate::IntegrityStatus,
1462 ) -> Self {
1463 Self {
1464 payload,
1465 publishers,
1466 reply: None,
1467 decision: None,
1468 integrity: Some(integrity),
1469 #[cfg(feature = "param-services")]
1470 params: None,
1471 }
1472 }
1473
1474 pub fn with_reply(
1478 payload: &'a [u8],
1479 publishers: &'a dyn PublisherResolver,
1480 reply_buf: &'a mut [u8],
1481 reply_written: &'a mut usize,
1482 ) -> Self {
1483 *reply_written = 0;
1484 Self {
1485 payload,
1486 publishers,
1487 reply: Some(ReplySink {
1488 buf: reply_buf,
1489 written: reply_written,
1490 }),
1491 decision: None,
1492 #[cfg(feature = "safety-e2e")]
1493 integrity: None,
1494 #[cfg(feature = "param-services")]
1495 params: None,
1496 }
1497 }
1498
1499 pub fn with_goal_decision(
1503 payload: &'a [u8],
1504 publishers: &'a dyn PublisherResolver,
1505 out: &'a mut GoalResponse,
1506 ) -> Self {
1507 Self {
1508 payload,
1509 publishers,
1510 reply: None,
1511 decision: Some(DecisionSink::Goal(out)),
1512 #[cfg(feature = "safety-e2e")]
1513 integrity: None,
1514 #[cfg(feature = "param-services")]
1515 params: None,
1516 }
1517 }
1518
1519 pub fn with_cancel_decision(
1522 payload: &'a [u8],
1523 publishers: &'a dyn PublisherResolver,
1524 out: &'a mut CancelResponse,
1525 ) -> Self {
1526 Self {
1527 payload,
1528 publishers,
1529 reply: None,
1530 decision: Some(DecisionSink::Cancel(out)),
1531 #[cfg(feature = "safety-e2e")]
1532 integrity: None,
1533 #[cfg(feature = "param-services")]
1534 params: None,
1535 }
1536 }
1537
1538 #[cfg(feature = "param-services")]
1542 pub fn set_param_server(&mut self, params: Option<&'a crate::ParameterServer>) {
1543 self.params = params;
1544 }
1545
1546 #[cfg(feature = "param-services")]
1551 pub fn parameter<T: crate::ParameterVariant>(&self, name: &str) -> Option<T> {
1552 self.params
1553 .and_then(|server| server.get(name))
1554 .and_then(T::from_parameter_value)
1555 }
1556
1557 pub fn set_goal_response(&mut self, response: GoalResponse) -> NodeResult<()> {
1560 match &mut self.decision {
1561 Some(DecisionSink::Goal(slot)) => {
1562 **slot = response;
1563 Ok(())
1564 }
1565 _ => Err(NodeDeclError::Runtime),
1566 }
1567 }
1568
1569 pub fn set_cancel_response(&mut self, response: CancelResponse) -> NodeResult<()> {
1572 match &mut self.decision {
1573 Some(DecisionSink::Cancel(slot)) => {
1574 **slot = response;
1575 Ok(())
1576 }
1577 _ => Err(NodeDeclError::Runtime),
1578 }
1579 }
1580
1581 pub fn reply_raw(&mut self, data: &[u8]) -> NodeResult<()> {
1585 let sink = self.reply.as_mut().ok_or(NodeDeclError::Runtime)?;
1586 if data.len() > sink.buf.len() {
1587 return Err(NodeDeclError::Runtime);
1588 }
1589 sink.buf[..data.len()].copy_from_slice(data);
1590 *sink.written = data.len();
1591 Ok(())
1592 }
1593
1594 pub fn reply<M: RosMessage, const N: usize>(&mut self, msg: &M) -> NodeResult<()> {
1596 let mut buf = [0u8; N];
1597 let mut writer =
1598 crate::CdrWriter::new_with_header(&mut buf).map_err(|_| NodeDeclError::Runtime)?;
1599 msg.serialize(&mut writer)
1600 .map_err(|_| NodeDeclError::Runtime)?;
1601 let len = writer.position();
1602 self.reply_raw(&buf[..len])
1603 }
1604
1605 pub fn payload(&self) -> &[u8] {
1607 self.payload
1608 }
1609
1610 #[cfg(feature = "safety-e2e")]
1616 pub fn integrity(&self) -> Option<&crate::IntegrityStatus> {
1617 self.integrity
1618 }
1619
1620 pub fn message<M: RosMessage>(&self) -> NodeResult<M> {
1623 let mut reader =
1624 crate::CdrReader::new_with_header(self.payload).map_err(|_| NodeDeclError::Runtime)?;
1625 M::deserialize(&mut reader).map_err(|_| NodeDeclError::Runtime)
1626 }
1627
1628 #[doc(hidden)]
1630 pub fn publish_raw(&self, publisher: EntityId<'_>, data: &[u8]) -> NodeResult<()> {
1631 self.publishers.publish_raw(publisher.as_str(), data)
1632 }
1633
1634 #[doc(hidden)]
1638 pub fn publish<M: RosMessage, const N: usize>(
1639 &self,
1640 publisher: EntityId<'_>,
1641 msg: &M,
1642 ) -> NodeResult<()> {
1643 let mut buf = [0u8; N];
1644 let mut writer =
1645 crate::CdrWriter::new_with_header(&mut buf).map_err(|_| NodeDeclError::Runtime)?;
1646 msg.serialize(&mut writer)
1647 .map_err(|_| NodeDeclError::Runtime)?;
1648 let len = writer.position();
1649 self.publish_raw(publisher, &buf[..len])
1650 }
1651
1652 pub fn publish_to_topic<M: RosMessage, const N: usize>(
1659 &self,
1660 topic: &str,
1661 msg: &M,
1662 ) -> NodeResult<()> {
1663 self.publish::<M, N>(EntityId::new(topic), msg)
1664 }
1665}
1666
1667pub trait ActionExecutor {
1684 fn complete_goal_raw(
1686 &mut self,
1687 action_entity: &str,
1688 goal_id: &GoalId,
1689 status: GoalStatus,
1690 result: &[u8],
1691 ) -> NodeResult<()>;
1692
1693 fn publish_feedback_raw(
1695 &mut self,
1696 action_entity: &str,
1697 goal_id: &GoalId,
1698 feedback: &[u8],
1699 ) -> NodeResult<()>;
1700
1701 fn for_each_active_goal(&self, action_entity: &str, visit: &mut dyn FnMut(&GoalId, GoalStatus));
1706}
1707
1708pub trait ClientDispatch {
1723 fn call_raw(
1733 &mut self,
1734 service_entity: &str,
1735 request_cdr: &[u8],
1736 response_buf: &mut [u8],
1737 ) -> NodeResult<usize>;
1738
1739 fn send_goal_raw(&mut self, action_entity: &str, goal_cdr: &[u8]) -> NodeResult<GoalId>;
1744}
1745
1746pub struct TickCtx<'a> {
1753 publishers: &'a dyn PublisherResolver,
1754 actions: &'a mut dyn ActionExecutor,
1755 clients: &'a mut dyn ClientDispatch,
1756 #[cfg(feature = "param-services")]
1760 params: Option<&'a crate::ParameterServer>,
1761}
1762
1763impl<'a> TickCtx<'a> {
1764 pub fn new(
1766 publishers: &'a dyn PublisherResolver,
1767 actions: &'a mut dyn ActionExecutor,
1768 clients: &'a mut dyn ClientDispatch,
1769 ) -> Self {
1770 Self {
1771 publishers,
1772 actions,
1773 clients,
1774 #[cfg(feature = "param-services")]
1775 params: None,
1776 }
1777 }
1778
1779 #[cfg(feature = "param-services")]
1782 pub fn set_param_server(&mut self, params: Option<&'a crate::ParameterServer>) {
1783 self.params = params;
1784 }
1785
1786 #[cfg(feature = "param-services")]
1790 pub fn parameter<T: crate::ParameterVariant>(&self, name: &str) -> Option<T> {
1791 self.params
1792 .and_then(|server| server.get(name))
1793 .and_then(T::from_parameter_value)
1794 }
1795
1796 #[doc(hidden)]
1798 pub fn publish_raw(&self, publisher: EntityId<'_>, data: &[u8]) -> NodeResult<()> {
1799 self.publishers.publish_raw(publisher.as_str(), data)
1800 }
1801
1802 #[doc(hidden)]
1804 pub fn publish<M: RosMessage, const N: usize>(
1805 &self,
1806 publisher: EntityId<'_>,
1807 msg: &M,
1808 ) -> NodeResult<()> {
1809 let mut buf = [0u8; N];
1810 let mut writer =
1811 crate::CdrWriter::new_with_header(&mut buf).map_err(|_| NodeDeclError::Runtime)?;
1812 msg.serialize(&mut writer)
1813 .map_err(|_| NodeDeclError::Runtime)?;
1814 let len = writer.position();
1815 self.publish_raw(publisher, &buf[..len])
1816 }
1817
1818 pub fn publish_to_topic<M: RosMessage, const N: usize>(
1823 &self,
1824 topic: &str,
1825 msg: &M,
1826 ) -> NodeResult<()> {
1827 self.publish::<M, N>(EntityId::new(topic), msg)
1828 }
1829
1830 #[doc(hidden)]
1833 pub fn complete_goal<R: RosMessage, const N: usize>(
1834 &mut self,
1835 action: EntityId<'_>,
1836 goal_id: &GoalId,
1837 status: GoalStatus,
1838 result: &R,
1839 ) -> NodeResult<()> {
1840 let mut buf = [0u8; N];
1848 let mut writer =
1849 crate::CdrWriter::new_with_header(&mut buf).map_err(|_| NodeDeclError::Runtime)?;
1850 result
1851 .serialize(&mut writer)
1852 .map_err(|_| NodeDeclError::Runtime)?;
1853 let len = writer.position();
1854 self.actions
1855 .complete_goal_raw(action.as_str(), goal_id, status, &buf[..len])
1856 }
1857
1858 pub fn complete_goal_for_name<R: RosMessage, const N: usize>(
1864 &mut self,
1865 name: &str,
1866 goal_id: &GoalId,
1867 status: GoalStatus,
1868 result: &R,
1869 ) -> NodeResult<()> {
1870 self.complete_goal::<R, N>(EntityId::new(name), goal_id, status, result)
1871 }
1872
1873 #[doc(hidden)]
1879 pub fn for_each_active_goal(
1880 &self,
1881 action: EntityId<'_>,
1882 visit: &mut dyn FnMut(&GoalId, GoalStatus),
1883 ) {
1884 self.actions.for_each_active_goal(action.as_str(), visit);
1885 }
1886
1887 pub fn for_each_active_goal_for_name(
1889 &self,
1890 name: &str,
1891 visit: &mut dyn FnMut(&GoalId, GoalStatus),
1892 ) {
1893 self.for_each_active_goal(EntityId::new(name), visit);
1894 }
1895
1896 #[doc(hidden)]
1898 pub fn publish_feedback<F: RosMessage, const N: usize>(
1899 &mut self,
1900 action: EntityId<'_>,
1901 goal_id: &GoalId,
1902 feedback: &F,
1903 ) -> NodeResult<()> {
1904 let mut buf = [0u8; N];
1909 let mut writer =
1910 crate::CdrWriter::new_with_header(&mut buf).map_err(|_| NodeDeclError::Runtime)?;
1911 feedback
1912 .serialize(&mut writer)
1913 .map_err(|_| NodeDeclError::Runtime)?;
1914 let len = writer.position();
1915 self.actions
1916 .publish_feedback_raw(action.as_str(), goal_id, &buf[..len])
1917 }
1918
1919 pub fn publish_feedback_for_name<F: RosMessage, const N: usize>(
1921 &mut self,
1922 name: &str,
1923 goal_id: &GoalId,
1924 feedback: &F,
1925 ) -> NodeResult<()> {
1926 self.publish_feedback::<F, N>(EntityId::new(name), goal_id, feedback)
1927 }
1928
1929 #[doc(hidden)]
1933 pub fn call_raw(
1934 &mut self,
1935 service: EntityId<'_>,
1936 request_cdr: &[u8],
1937 response_buf: &mut [u8],
1938 ) -> NodeResult<usize> {
1939 self.clients
1940 .call_raw(service.as_str(), request_cdr, response_buf)
1941 }
1942
1943 pub fn call_raw_for_name(
1946 &mut self,
1947 name: &str,
1948 request_cdr: &[u8],
1949 response_buf: &mut [u8],
1950 ) -> NodeResult<usize> {
1951 self.call_raw(EntityId::new(name), request_cdr, response_buf)
1952 }
1953
1954 #[doc(hidden)]
1959 pub fn call<Req: RosMessage, Resp: RosMessage, const REQ_N: usize, const RESP_N: usize>(
1960 &mut self,
1961 service: EntityId<'_>,
1962 request: &Req,
1963 ) -> NodeResult<Resp> {
1964 let mut req_buf = [0u8; REQ_N];
1965 let mut writer =
1966 crate::CdrWriter::new_with_header(&mut req_buf).map_err(|_| NodeDeclError::Runtime)?;
1967 request
1968 .serialize(&mut writer)
1969 .map_err(|_| NodeDeclError::Runtime)?;
1970 let req_len = writer.position();
1971
1972 let mut resp_buf = [0u8; RESP_N];
1973 let resp_len =
1974 self.clients
1975 .call_raw(service.as_str(), &req_buf[..req_len], &mut resp_buf)?;
1976
1977 let mut reader = crate::CdrReader::new_with_header(&resp_buf[..resp_len])
1978 .map_err(|_| NodeDeclError::Runtime)?;
1979 Resp::deserialize(&mut reader).map_err(|_| NodeDeclError::Runtime)
1980 }
1981
1982 pub fn call_for_name<
1985 Req: RosMessage,
1986 Resp: RosMessage,
1987 const REQ_N: usize,
1988 const RESP_N: usize,
1989 >(
1990 &mut self,
1991 name: &str,
1992 request: &Req,
1993 ) -> NodeResult<Resp> {
1994 self.call::<Req, Resp, REQ_N, RESP_N>(EntityId::new(name), request)
1995 }
1996
1997 #[doc(hidden)]
2001 pub fn send_goal_raw(&mut self, action: EntityId<'_>, goal_cdr: &[u8]) -> NodeResult<GoalId> {
2002 self.clients.send_goal_raw(action.as_str(), goal_cdr)
2003 }
2004
2005 pub fn send_goal_raw_for_name(&mut self, name: &str, goal_cdr: &[u8]) -> NodeResult<GoalId> {
2008 self.send_goal_raw(EntityId::new(name), goal_cdr)
2009 }
2010
2011 #[doc(hidden)]
2015 pub fn send_goal<G: RosMessage, const N: usize>(
2016 &mut self,
2017 action: EntityId<'_>,
2018 goal: &G,
2019 ) -> NodeResult<GoalId> {
2020 let mut buf = [0u8; N];
2021 let mut writer =
2022 crate::CdrWriter::new_with_header(&mut buf).map_err(|_| NodeDeclError::Runtime)?;
2023 goal.serialize(&mut writer)
2024 .map_err(|_| NodeDeclError::Runtime)?;
2025 let len = writer.position();
2026 self.clients.send_goal_raw(action.as_str(), &buf[..len])
2027 }
2028
2029 pub fn send_goal_for_name<G: RosMessage, const N: usize>(
2032 &mut self,
2033 name: &str,
2034 goal: &G,
2035 ) -> NodeResult<GoalId> {
2036 self.send_goal::<G, N>(EntityId::new(name), goal)
2037 }
2038}
2039
2040pub trait ExecutableNode: Node {
2041 type State;
2043
2044 fn init() -> Self::State;
2046
2047 fn on_callback(state: &mut Self::State, callback: Callback<'_>, ctx: &mut CallbackCtx<'_>);
2051
2052 fn tick(_state: &mut Self::State, _ctx: &mut TickCtx<'_>) {}
2057}
2058
2059#[macro_export]
2070macro_rules! declarative_component {
2071 ($ty:ty) => {
2072 impl $crate::ExecutableNode for $ty {
2073 type State = ();
2074 fn init() -> Self::State {}
2075 fn on_callback(
2076 _state: &mut Self::State,
2077 _callback: $crate::Callback<'_>,
2078 _ctx: &mut $crate::CallbackCtx<'_>,
2079 ) {
2080 }
2081 }
2082 };
2083}
2084
2085pub fn register_node<C: Node>(runtime: &mut dyn NodeRuntime) -> NodeResult<()> {
2087 let mut context = NodeContext::new(C::NAME, runtime);
2088 C::register(&mut context)
2089}
2090
2091#[cfg(feature = "alloc")]
2098#[doc(hidden)]
2099pub fn __private_node_state_into_raw<C: ExecutableNode>(state: C::State) -> *mut () {
2100 extern crate alloc;
2101 alloc::boxed::Box::into_raw(alloc::boxed::Box::new(state)) as *mut ()
2102}
2103
2104pub fn record_node_metadata<C: Node>(recorder: &mut dyn NodeRuntime) -> NodeResult<()> {
2106 register_node::<C>(recorder)
2107}
2108
2109#[cfg(test)]
2110mod tests {
2111 use super::*;
2112 use crate::{CdrReader, CdrWriter, DeserError, SerError, SourceNameKind};
2113
2114 #[derive(Default)]
2115 struct FakeNodeRuntime {
2116 next: u8,
2117 created: Vec<MetadataString, 4>,
2118 }
2119
2120 impl DeclaredNodeRuntime for FakeNodeRuntime {
2121 type NodeHandle = u8;
2122
2123 fn build_component_node(
2124 &mut self,
2125 _id: NodeId<'_>,
2126 options: NodeOptions<'_>,
2127 ) -> NodeResult<Self::NodeHandle> {
2128 self.created
2129 .push(copy_str(options.name)?)
2130 .map_err(|_| NodeDeclError::Metadata(NodeMetadataError::Capacity))?;
2131 let handle = self.next;
2132 self.next += 1;
2133 Ok(handle)
2134 }
2135 }
2136
2137 #[derive(Debug, Clone, Copy, Default)]
2138 struct TestMsg;
2139
2140 impl crate::Serialize for TestMsg {
2141 fn serialize(&self, _writer: &mut CdrWriter) -> Result<(), SerError> {
2142 Ok(())
2143 }
2144 }
2145
2146 impl crate::Deserialize for TestMsg {
2147 fn deserialize(_reader: &mut CdrReader) -> Result<Self, DeserError> {
2148 Ok(Self)
2149 }
2150 }
2151
2152 impl RosMessage for TestMsg {
2153 const TYPE_NAME: &'static str = "test_msgs::msg::dds_::Test_";
2154 const TYPE_HASH: &'static str = "test_hash";
2155 }
2156
2157 struct TestService;
2158
2159 impl RosService for TestService {
2160 type Request = TestMsg;
2161 type Reply = TestMsg;
2162
2163 const SERVICE_NAME: &'static str = "test_msgs::srv::dds_::Test_";
2164 const SERVICE_HASH: &'static str = "test_service_hash";
2165 }
2166
2167 struct TestAction;
2168
2169 impl RosAction for TestAction {
2170 type Goal = TestMsg;
2171 type Result = TestMsg;
2172 type Feedback = TestMsg;
2173 type SendGoalRequest = TestMsg;
2174 type SendGoalResponse = TestMsg;
2175 type GetResultRequest = TestMsg;
2176 type GetResultResponse = TestMsg;
2177 type FeedbackMessage = TestMsg;
2178
2179 const ACTION_NAME: &'static str = "test_msgs::action::dds_::Test_";
2180 const ACTION_HASH: &'static str = "test_action_hash";
2181 }
2182
2183 struct TalkerComponent;
2184
2185 impl Node for TalkerComponent {
2186 const NAME: &'static str = "talker_component";
2187
2188 fn register(context: &mut NodeContext<'_>) -> NodeResult<()> {
2189 let mut node =
2190 context.create_node_with_id(NodeId::new("node"), NodeOptions::new("talker"))?;
2191 let _publisher =
2192 node.create_publisher::<TestMsg>(EntityId::new("pub_chatter"), "chatter")?;
2193 let _subscription = node.create_subscription::<TestMsg>(
2194 EntityId::new("sub_cmd"),
2195 CallbackId::new("on_cmd"),
2196 "~/cmd",
2197 )?;
2198 let _timer = node.create_timer(
2199 EntityId::new("timer_tick"),
2200 CallbackId::new("on_tick"),
2201 TimerDuration::from_millis(10),
2202 )?;
2203 let _parameter =
2204 node.declare_parameter(EntityId::new("param_gain"), "gain", ParameterType::Double)?;
2205 node.callback(CallbackId::new("on_tick"))
2206 .publishes(EntityId::new("pub_chatter"))?
2207 .writes(EntityId::new("param_gain"))?;
2208 Ok(())
2209 }
2210 }
2211
2212 #[test]
2213 fn component_records_metadata_without_transport() {
2214 let mut recorder = MetadataRecorder::<2, 8, 4>::new();
2215 record_node_metadata::<TalkerComponent>(&mut recorder).unwrap();
2216
2217 assert_eq!(recorder.nodes().len(), 1);
2218 assert_eq!(recorder.nodes()[0].name.as_str(), "talker");
2219 assert_eq!(recorder.entities().len(), 4);
2220 assert_eq!(recorder.entities()[0].kind, EntityKind::Publisher);
2221 assert_eq!(recorder.entities()[1].source_name.as_str(), "~/cmd");
2222 assert_eq!(
2223 recorder.entities()[1]
2224 .callback_id
2225 .as_ref()
2226 .map(|id| id.as_str()),
2227 Some("on_cmd")
2228 );
2229 assert_eq!(recorder.callback_effects().len(), 2);
2230 }
2231
2232 struct SafetyComponent;
2236 impl Node for SafetyComponent {
2237 const NAME: &'static str = "safety_component";
2238 fn register(context: &mut NodeContext<'_>) -> NodeResult<()> {
2239 let mut node =
2240 context.create_node_with_id(NodeId::new("node"), NodeOptions::new("listener"))?;
2241 let _plain = node.create_subscription_for_callback_name::<TestMsg>("on_plain", "/a")?;
2242 let _safe =
2243 node.create_subscription_for_callback_name_with_safety::<TestMsg>("on_safe", "/b")?;
2244 Ok(())
2245 }
2246 }
2247
2248 #[test]
2249 fn safety_opt_in_records_metadata_flag() {
2250 let mut recorder = MetadataRecorder::<2, 8, 4>::new();
2251 record_node_metadata::<SafetyComponent>(&mut recorder).unwrap();
2252 let ents = recorder.entities();
2253 assert_eq!(ents.len(), 2);
2254 assert_eq!(ents[0].source_name.as_str(), "/a");
2256 assert!(!ents[0].safety, "plain sub must not be flagged");
2257 assert_eq!(ents[1].source_name.as_str(), "/b");
2259 assert!(ents[1].safety, "safety sub must be flagged");
2260 }
2261
2262 struct GroupedComponent;
2263
2264 impl Node for GroupedComponent {
2265 const NAME: &'static str = "grouped_component";
2266
2267 fn register(context: &mut NodeContext<'_>) -> NodeResult<()> {
2268 let mut node =
2269 context.create_node_with_id(NodeId::new("node"), NodeOptions::new("grouped"))?;
2270 let _pub = node.create_publisher::<TestMsg>(EntityId::new("pub_plain"), "plain")?;
2272 node.callback_group("control")?;
2274 let _sub = node.create_subscription::<TestMsg>(
2275 EntityId::new("sub_cmd"),
2276 CallbackId::new("on_cmd"),
2277 "~/cmd",
2278 )?;
2279 let _timer = node.create_timer(
2280 EntityId::new("timer_tick"),
2281 CallbackId::new("on_tick"),
2282 TimerDuration::from_millis(10),
2283 )?;
2284 node.callback_group("telemetry")?;
2286 let _sub2 = node.create_subscription::<TestMsg>(
2287 EntityId::new("sub_diag"),
2288 CallbackId::new("on_diag"),
2289 "~/diag",
2290 )?;
2291 Ok(())
2292 }
2293 }
2294
2295 #[test]
2296 fn sticky_callback_group_stamps_subsequent_entities() {
2297 let mut recorder = MetadataRecorder::<2, 8, 4>::new();
2298 record_node_metadata::<GroupedComponent>(&mut recorder).unwrap();
2299
2300 let group_of = |idx: usize| {
2301 recorder.entities()[idx]
2302 .callback_group
2303 .as_ref()
2304 .map(|g| g.as_str())
2305 };
2306 assert_eq!(group_of(0), None);
2308 assert_eq!(group_of(1), Some("control"));
2310 assert_eq!(group_of(2), Some("control"));
2311 assert_eq!(group_of(3), Some("telemetry"));
2313 }
2314
2315 #[test]
2316 fn runtime_adapter_maps_stable_nodes_to_runtime_handles() {
2317 let mut node_runtime = FakeNodeRuntime::default();
2318 let mut runtime = NodeRuntimeAdapter::<_, 2, 8, 4>::new(&mut node_runtime);
2319
2320 register_node::<TalkerComponent>(&mut runtime).unwrap();
2321
2322 assert_eq!(runtime.nodes().len(), 1);
2323 assert_eq!(runtime.nodes()[0].slot(), NodeSlot::new(0));
2324 assert_eq!(runtime.nodes()[0].stable_id(), "node");
2325 assert_eq!(runtime.nodes()[0].source_default_name(), "talker");
2326 assert_eq!(runtime.node_handle(NodeId::new("node")), Some(0));
2327 assert_eq!(runtime.entities().len(), 4);
2328 assert_eq!(runtime.entities()[0].slot, Some(EntitySlot::new(0)));
2329 assert_eq!(runtime.entities()[0].node_slot, Some(NodeSlot::new(0)));
2330 assert_eq!(
2331 runtime.entities()[1].callback_slot,
2332 Some(CallbackSlot::new(0))
2333 );
2334 assert_eq!(
2335 runtime.entities()[2].callback_slot,
2336 Some(CallbackSlot::new(1))
2337 );
2338 assert_eq!(runtime.callback_effects().len(), 2);
2339 assert_eq!(
2340 runtime.callback_effects()[0].callback_slot,
2341 Some(CallbackSlot::new(1))
2342 );
2343 assert_eq!(
2344 runtime.callback_effects()[0].entity_slot,
2345 Some(EntitySlot::new(0))
2346 );
2347 }
2348
2349 #[test]
2350 fn context_can_synthesize_stable_node_id_from_options_name() {
2351 let mut recorder = MetadataRecorder::<1, 0, 0>::new();
2352 let mut context = NodeContext::new("test", &mut recorder);
2353 let node = context
2354 .create_node(NodeOptions::new("talker").namespace("/demo").domain_id(42))
2355 .unwrap();
2356
2357 assert_eq!(node.id(), NodeId::new("talker"));
2358 let _ = node;
2360 let _ = context;
2361 assert_eq!(recorder.nodes().len(), 1);
2362 assert_eq!(recorder.nodes()[0].id.as_str(), "talker");
2363 assert_eq!(recorder.nodes()[0].name.as_str(), "talker");
2364 assert_eq!(recorder.nodes()[0].namespace.as_str(), "/demo");
2365 assert_eq!(recorder.nodes()[0].domain_id, 42);
2366 }
2367
2368 #[test]
2369 fn synthesized_node_ids_reject_duplicate_names() {
2370 let mut node_runtime = FakeNodeRuntime::default();
2371 let mut runtime = NodeRuntimeAdapter::<_, 2, 0, 0>::new(&mut node_runtime);
2372 {
2373 let mut context = NodeContext::new("test", &mut runtime);
2374 context.create_node(NodeOptions::new("talker")).unwrap();
2375 }
2376 let mut context = NodeContext::new("test", &mut runtime);
2377 let result = context.create_node(NodeOptions::new("talker"));
2378
2379 assert!(matches!(
2380 result,
2381 Err(NodeDeclError::Metadata(NodeMetadataError::DuplicateId))
2382 ));
2383 }
2384
2385 #[test]
2386 fn synthesized_entity_helpers_record_topic_and_callback_ids() {
2387 let mut recorder = MetadataRecorder::<1, 3, 2>::new();
2388 let mut context = NodeContext::new("test", &mut recorder);
2389 let mut node = context.create_node(NodeOptions::new("talker")).unwrap();
2390
2391 let publisher = node
2392 .create_publisher_for_topic::<TestMsg>("/chatter")
2393 .unwrap();
2394 let subscription = node
2395 .create_subscription_for_callback::<TestMsg>(CallbackId::new("on_message"), "/cmd")
2396 .unwrap();
2397 let _timer = node
2398 .create_timer_for_callback(CallbackId::new("on_tick"), TimerDuration::from_millis(10))
2399 .unwrap();
2400
2401 node.callback(CallbackId::new("on_tick"))
2402 .publishes_entity(&publisher)
2403 .unwrap();
2404 node.callback(CallbackId::new("on_message"))
2405 .reads_entity(&subscription)
2406 .unwrap();
2407
2408 assert_eq!(publisher.id(), EntityId::new("/chatter"));
2409 assert_eq!(subscription.id(), EntityId::new("on_message"));
2410 assert_eq!(recorder.entities().len(), 3);
2411
2412 let publisher = &recorder.entities()[0];
2413 assert_eq!(publisher.id.as_str(), "/chatter");
2414 assert_eq!(publisher.kind, EntityKind::Publisher);
2415 assert_eq!(publisher.source_name.as_str(), "/chatter");
2416
2417 let subscription = &recorder.entities()[1];
2418 assert_eq!(subscription.id.as_str(), "on_message");
2419 assert_eq!(subscription.kind, EntityKind::Subscription);
2420 assert_eq!(subscription.source_name.as_str(), "/cmd");
2421 assert_eq!(
2422 subscription.callback_id.as_ref().map(|id| id.as_str()),
2423 Some("on_message")
2424 );
2425
2426 let timer = &recorder.entities()[2];
2427 assert_eq!(timer.id.as_str(), "on_tick");
2428 assert_eq!(timer.kind, EntityKind::Timer);
2429 assert_eq!(
2430 timer.callback_id.as_ref().map(|id| id.as_str()),
2431 Some("on_tick")
2432 );
2433
2434 assert_eq!(recorder.callback_effects().len(), 2);
2435 assert_eq!(
2436 recorder.callback_effects()[0].entity_id.as_str(),
2437 "/chatter"
2438 );
2439 assert_eq!(
2440 recorder.callback_effects()[1].entity_id.as_str(),
2441 "on_message"
2442 );
2443 }
2444
2445 #[test]
2446 fn named_callback_helpers_avoid_manual_callback_ids() {
2447 let mut recorder = MetadataRecorder::<1, 3, 2>::new();
2448 let mut context = NodeContext::new("test", &mut recorder);
2449 let mut node = context.create_node(NodeOptions::new("listener")).unwrap();
2450
2451 let publisher = node
2452 .create_publisher_for_topic::<TestMsg>("/chatter")
2453 .unwrap();
2454 let subscription = node
2455 .create_subscription_for_callback_name::<TestMsg>("on_message", "/chatter")
2456 .unwrap();
2457 let timer = node
2458 .create_timer_for_callback_name("on_tick", TimerDuration::from_millis(10))
2459 .unwrap();
2460
2461 node.callback_for_name("on_message")
2462 .reads_entity(&subscription)
2463 .unwrap();
2464 node.callback_for_name("on_tick")
2465 .publishes_entity(&publisher)
2466 .unwrap();
2467
2468 assert_eq!(subscription.id().as_str(), "on_message");
2469 assert_eq!(timer.id().as_str(), "on_tick");
2470 assert_eq!(
2471 recorder.entities()[1]
2472 .callback_id
2473 .as_ref()
2474 .map(|id| id.as_str()),
2475 Some("on_message")
2476 );
2477 assert_eq!(
2478 recorder.entities()[2]
2479 .callback_id
2480 .as_ref()
2481 .map(|id| id.as_str()),
2482 Some("on_tick")
2483 );
2484 assert_eq!(
2485 recorder.callback_effects()[0].callback_id.as_str(),
2486 "on_message"
2487 );
2488 assert_eq!(
2489 recorder.callback_effects()[0].entity_id.as_str(),
2490 "on_message"
2491 );
2492 assert_eq!(
2493 recorder.callback_effects()[1].callback_id.as_str(),
2494 "on_tick"
2495 );
2496 assert_eq!(
2497 recorder.callback_effects()[1].entity_id.as_str(),
2498 "/chatter"
2499 );
2500 }
2501
2502 #[test]
2503 fn synthesized_entity_ids_reject_collisions() {
2504 let mut recorder = MetadataRecorder::<1, 2, 0>::new();
2505 let mut context = NodeContext::new("test", &mut recorder);
2506 let mut node = context.create_node(NodeOptions::new("talker")).unwrap();
2507
2508 node.create_publisher_for_topic::<TestMsg>("/chatter")
2509 .unwrap();
2510 let result = node.create_publisher_for_topic::<TestMsg>("/chatter");
2511
2512 assert!(matches!(
2513 result,
2514 Err(NodeDeclError::Metadata(NodeMetadataError::DuplicateId))
2515 ));
2516 }
2517
2518 #[test]
2520 fn runtime_adapter_rejects_unknown_entities() {
2521 let mut node_runtime = FakeNodeRuntime::default();
2522 let mut runtime = NodeRuntimeAdapter::<_, 1, 1, 1>::new(&mut node_runtime);
2523 runtime
2524 .create_node(NodeId::new("node"), NodeOptions::new("talker"))
2525 .unwrap();
2526
2527 assert_eq!(
2528 runtime.create_node(NodeId::new("node"), NodeOptions::new("other")),
2529 Err(NodeDeclError::Metadata(NodeMetadataError::DuplicateId))
2530 );
2531 assert_eq!(
2532 runtime.record_callback_effect(
2533 CallbackId::new("cb"),
2534 CallbackEffectKind::Reads,
2535 EntityId::new("missing")
2536 ),
2537 Err(NodeDeclError::Metadata(NodeMetadataError::UnknownEntity))
2538 );
2539 }
2540
2541 #[test]
2542 fn component_rejects_effect_for_unknown_entity() {
2543 let mut recorder = MetadataRecorder::<1, 1, 1>::new();
2544 let mut context = NodeContext::new("test", &mut recorder);
2545 let result = context
2546 .callback(CallbackId::new("cb"))
2547 .reads(EntityId::new("missing"));
2548 assert!(matches!(
2549 result,
2550 Err(NodeDeclError::Metadata(NodeMetadataError::UnknownEntity))
2551 ));
2552 }
2553
2554 #[test]
2555 fn component_missing_export_error_message_is_clear() {
2556 assert_eq!(
2557 NodeDeclError::MissingExport.message(),
2558 MISSING_NODE_EXPORT_ERROR
2559 );
2560 assert_eq!(
2561 NodeDeclError::MissingExport.message(),
2562 "package has no exported nros component"
2563 );
2564 }
2565
2566 struct RobotComponent;
2567
2568 impl Node for RobotComponent {
2569 const NAME: &'static str = "robot_component";
2570
2571 fn register(context: &mut NodeContext<'_>) -> NodeResult<()> {
2572 {
2573 let mut sensors = context.create_node_with_id(
2574 NodeId::new("node_sensors"),
2575 NodeOptions::new("sensors"),
2576 )?;
2577 let _status =
2578 sensors.create_publisher::<TestMsg>(EntityId::new("pub_status"), "~/status")?;
2579 }
2580
2581 let mut control = context
2582 .create_node_with_id(NodeId::new("node_control"), NodeOptions::new("control"))?;
2583 let _cmd = control.create_subscription::<TestMsg>(
2584 EntityId::new("sub_cmd"),
2585 CallbackId::new("cb_cmd"),
2586 "~/cmd",
2587 )?;
2588 let _reset = control.create_service_server::<TestService>(
2589 EntityId::new("srv_reset"),
2590 CallbackId::new("cb_reset"),
2591 "reset",
2592 )?;
2593 let _navigate = control.create_action_server_with_callbacks::<TestAction>(
2594 EntityId::new("act_navigate"),
2595 CallbackId::new("cb_nav_goal"),
2596 CallbackId::new("cb_nav_cancel"),
2597 CallbackId::new("cb_nav_accepted"),
2598 "~/navigate",
2599 )?;
2600 let _gain = control.declare_parameter_with_default(
2601 EntityId::new("param_gain"),
2602 "gain",
2603 ParameterDefault::Double(copy_str("1.5")?),
2604 )?;
2605
2606 control
2607 .callback(CallbackId::new("cb_cmd"))
2608 .publishes(EntityId::new("pub_status"))?
2609 .reads(EntityId::new("param_gain"))?;
2610 control
2611 .callback(CallbackId::new("cb_nav_accepted"))
2612 .writes(EntityId::new("param_gain"))?;
2613
2614 Ok(())
2615 }
2616 }
2617
2618 #[test]
2620 fn component_api_records_multi_node_services() {
2621 let mut recorder = MetadataRecorder::<4, 12, 4>::new();
2622 record_node_metadata::<RobotComponent>(&mut recorder).unwrap();
2623
2624 assert_eq!(recorder.nodes().len(), 2);
2625 assert_eq!(recorder.nodes()[0].id.as_str(), "node_sensors");
2626 assert_eq!(recorder.nodes()[1].id.as_str(), "node_control");
2627
2628 let status = recorder
2629 .entities()
2630 .iter()
2631 .find(|entity| entity.id.as_str() == "pub_status")
2632 .unwrap();
2633 assert_eq!(status.kind, EntityKind::Publisher);
2634 assert_eq!(status.source_name.as_str(), "~/status");
2635 assert_eq!(status.source_name_kind, SourceNameKind::Private);
2636
2637 let reset = recorder
2638 .entities()
2639 .iter()
2640 .find(|entity| entity.id.as_str() == "srv_reset")
2641 .unwrap();
2642 assert_eq!(reset.kind, EntityKind::ServiceServer);
2643 assert_eq!(
2644 reset.callback_id.as_ref().map(|id| id.as_str()),
2645 Some("cb_reset")
2646 );
2647
2648 let navigate = recorder
2649 .entities()
2650 .iter()
2651 .find(|entity| entity.id.as_str() == "act_navigate")
2652 .unwrap();
2653 assert_eq!(navigate.kind, EntityKind::ActionServer);
2654 assert_eq!(
2655 navigate.callback_id.as_ref().map(|id| id.as_str()),
2656 Some("cb_nav_goal")
2657 );
2658 assert_eq!(
2659 navigate
2660 .action_cancel_callback_id
2661 .as_ref()
2662 .map(|id| id.as_str()),
2663 Some("cb_nav_cancel")
2664 );
2665 assert_eq!(
2666 navigate
2667 .action_accepted_callback_id
2668 .as_ref()
2669 .map(|id| id.as_str()),
2670 Some("cb_nav_accepted")
2671 );
2672
2673 let gain = recorder
2674 .entities()
2675 .iter()
2676 .find(|entity| entity.id.as_str() == "param_gain")
2677 .unwrap();
2678 assert_eq!(gain.kind, EntityKind::Parameter);
2679 assert!(matches!(
2680 gain.parameter_default.as_ref(),
2681 Some(ParameterDefault::Double(value)) if value.as_str() == "1.5"
2682 ));
2683
2684 assert_eq!(recorder.callback_effects().len(), 3);
2685 assert!(recorder.callback_effects().iter().any(|effect| {
2686 effect.callback_id.as_str() == "cb_cmd"
2687 && effect.kind == CallbackEffectKind::Publishes
2688 && effect.entity_id.as_str() == "pub_status"
2689 }));
2690 assert!(recorder.callback_effects().iter().any(|effect| {
2691 effect.callback_id.as_str() == "cb_nav_accepted"
2692 && effect.kind == CallbackEffectKind::Writes
2693 && effect.entity_id.as_str() == "param_gain"
2694 }));
2695 }
2696
2697 #[cfg(feature = "std")]
2698 #[test]
2699 fn component_api_json_contains_planner_callback_links() {
2700 let mut recorder = MetadataRecorder::<4, 12, 4>::new();
2701 record_node_metadata::<RobotComponent>(&mut recorder).unwrap();
2702
2703 let json = recorder
2704 .to_source_metadata_json(&crate::SourceMetadataExport::new(
2705 "demo_robot",
2706 RobotComponent::NAME,
2707 ))
2708 .unwrap();
2709
2710 assert!(json.contains("\"callbacks\":["));
2711 assert!(json.contains("\"id\":\"cb_cmd\",\"declaration_slot\":0"));
2712 assert!(json.contains("\"kind\":\"subscription\""));
2713 assert!(json.contains("\"id\":\"cb_reset\",\"declaration_slot\":1"));
2714 assert!(json.contains("\"kind\":\"service\""));
2715 assert!(json.contains("\"id\":\"cb_nav_goal\",\"declaration_slot\":2"));
2716 assert!(json.contains("\"kind\":\"action_goal\""));
2717 assert!(json.contains("\"id\":\"cb_nav_cancel\",\"declaration_slot\":3"));
2718 assert!(json.contains("\"kind\":\"action_cancel\""));
2719 assert!(json.contains("\"id\":\"cb_nav_accepted\",\"declaration_slot\":4"));
2720 assert!(json.contains("\"kind\":\"action_accepted\""));
2721 assert!(json.contains("\"kind\":\"publishes\",\"entity\":\"pub_status\""));
2722 assert!(json.contains("\"kind\":\"reads_parameter\",\"entity\":\"param_gain\""));
2723 assert!(json.contains("\"kind\":\"writes_parameter\",\"entity\":\"param_gain\""));
2724 assert!(json.contains("\"goal_callback\":\"cb_nav_goal\""));
2725 assert!(json.contains("\"cancel_callback\":\"cb_nav_cancel\""));
2726 assert!(json.contains("\"accepted_callback\":\"cb_nav_accepted\""));
2727 }
2728
2729 impl ExecutableNode for TalkerComponent {
2734 type State = u32;
2735
2736 fn init() -> u32 {
2737 0
2738 }
2739
2740 fn on_callback(state: &mut u32, callback: Callback<'_>, ctx: &mut CallbackCtx<'_>) {
2741 if callback.as_str() == "on_tick" {
2742 *state += 1;
2743 let _ = ctx.publish::<TestMsg, 64>(EntityId::new("pub_chatter"), &TestMsg);
2745 }
2746 }
2747 }
2748
2749 #[test]
2750 fn executable_component_callback_publishes_and_mutates_state() {
2751 use core::cell::RefCell;
2752
2753 struct RecordingResolver {
2754 last: RefCell<Option<(MetadataString, usize)>>,
2755 }
2756 impl PublisherResolver for RecordingResolver {
2757 fn publish_raw(&self, entity_id: &str, data: &[u8]) -> NodeResult<()> {
2758 *self.last.borrow_mut() = Some((copy_str(entity_id)?, data.len()));
2759 Ok(())
2760 }
2761 }
2762
2763 let resolver = RecordingResolver {
2764 last: RefCell::new(None),
2765 };
2766 let mut state = TalkerComponent::init();
2767 let mut ctx = CallbackCtx::new(&[], &resolver);
2768
2769 TalkerComponent::on_callback(
2771 &mut state,
2772 Callback::__from_id(CallbackId::new("other")),
2773 &mut ctx,
2774 );
2775 assert_eq!(state, 0);
2776 assert!(resolver.last.borrow().is_none());
2777
2778 TalkerComponent::on_callback(
2780 &mut state,
2781 Callback::__from_id(CallbackId::new("on_tick")),
2782 &mut ctx,
2783 );
2784 assert_eq!(state, 1);
2785 let last = resolver.last.borrow();
2786 let (entity, len) = last.as_ref().expect("a publish was recorded");
2787 assert_eq!(entity.as_str(), "pub_chatter");
2788 assert_eq!(*len, 4);
2790 }
2791
2792 #[test]
2796 fn callback_ctx_reply_sink_roundtrips() {
2797 struct NoopResolver;
2798 impl PublisherResolver for NoopResolver {
2799 fn publish_raw(&self, _entity_id: &str, _data: &[u8]) -> NodeResult<()> {
2800 Ok(())
2801 }
2802 }
2803 let resolver = NoopResolver;
2804 let mut reply_buf = [0u8; 64];
2805 let mut written = 0usize;
2806 {
2807 let mut ctx = CallbackCtx::with_reply(&[], &resolver, &mut reply_buf, &mut written);
2808 ctx.reply::<TestMsg, 64>(&TestMsg).unwrap();
2809 }
2810 assert_eq!(written, 4);
2812
2813 let mut ctx2 = CallbackCtx::new(&[], &resolver);
2815 assert!(ctx2.reply_raw(&[1, 2, 3]).is_err());
2816 }
2817
2818 #[cfg(feature = "param-services")]
2821 #[test]
2822 fn callback_ctx_reads_param() {
2823 struct NoopResolver;
2824 impl PublisherResolver for NoopResolver {
2825 fn publish_raw(&self, _entity_id: &str, _data: &[u8]) -> NodeResult<()> {
2826 Ok(())
2827 }
2828 }
2829 let resolver = NoopResolver;
2830
2831 let ctx_none = CallbackCtx::new(&[], &resolver);
2833 assert_eq!(ctx_none.parameter::<i64>("speed"), None);
2834
2835 let mut server = crate::ParameterServer::new();
2837 assert!(server.declare("speed", crate::ParameterValue::Integer(7)));
2838
2839 let mut ctx = CallbackCtx::new(&[], &resolver);
2840 ctx.set_param_server(Some(&server));
2841 assert_eq!(ctx.parameter::<i64>("speed"), Some(7));
2842 assert_eq!(ctx.parameter::<bool>("speed"), None);
2844 assert_eq!(ctx.parameter::<i64>("missing"), None);
2846 }
2847
2848 #[cfg(feature = "safety-e2e")]
2852 #[test]
2853 fn callback_ctx_integrity_surface() {
2854 struct NoopResolver;
2855 impl PublisherResolver for NoopResolver {
2856 fn publish_raw(&self, _entity_id: &str, _data: &[u8]) -> NodeResult<()> {
2857 Ok(())
2858 }
2859 }
2860 let resolver = NoopResolver;
2861
2862 let ctx = CallbackCtx::new(&[], &resolver);
2864 assert!(ctx.integrity().is_none());
2865
2866 let status = crate::IntegrityStatus {
2868 gap: 2,
2869 duplicate: false,
2870 crc_valid: Some(true),
2871 };
2872 let ctx = CallbackCtx::new_with_integrity(&[], &resolver, &status);
2873 let got = ctx.integrity().expect("safety ctx carries status");
2874 assert_eq!(got.gap, 2);
2875 assert!(!got.duplicate);
2876 assert_eq!(got.crc_valid, Some(true));
2877 }
2878
2879 #[test]
2883 fn callback_ctx_decision_sink() {
2884 struct NoopResolver;
2885 impl PublisherResolver for NoopResolver {
2886 fn publish_raw(&self, _entity_id: &str, _data: &[u8]) -> NodeResult<()> {
2887 Ok(())
2888 }
2889 }
2890 let resolver = NoopResolver;
2891
2892 let mut gr = GoalResponse::Reject;
2893 {
2894 let mut ctx = CallbackCtx::with_goal_decision(&[], &resolver, &mut gr);
2895 ctx.set_goal_response(GoalResponse::AcceptAndExecute)
2896 .unwrap();
2897 assert!(ctx.set_cancel_response(CancelResponse::Ok).is_err());
2899 }
2900 assert!(matches!(gr, GoalResponse::AcceptAndExecute));
2901
2902 let mut cr = CancelResponse::Rejected;
2903 {
2904 let mut ctx = CallbackCtx::with_cancel_decision(&[], &resolver, &mut cr);
2905 ctx.set_cancel_response(CancelResponse::Ok).unwrap();
2906 }
2907 assert!(matches!(cr, CancelResponse::Ok));
2908
2909 let mut ctx3 = CallbackCtx::new(&[], &resolver);
2911 assert!(ctx3.set_goal_response(GoalResponse::Reject).is_err());
2912 assert!(ctx3.set_cancel_response(CancelResponse::Ok).is_err());
2913 }
2914
2915 #[test]
2918 fn tick_ctx_publish_and_action_ops() {
2919 use core::cell::Cell;
2920 struct RecPub {
2921 published: Cell<bool>,
2922 }
2923 impl PublisherResolver for RecPub {
2924 fn publish_raw(&self, _entity_id: &str, _data: &[u8]) -> NodeResult<()> {
2925 self.published.set(true);
2926 Ok(())
2927 }
2928 }
2929 struct RecAct {
2930 completed: bool,
2931 fed: bool,
2932 visited: usize,
2933 }
2934 impl ActionExecutor for RecAct {
2935 fn complete_goal_raw(
2936 &mut self,
2937 _action_entity: &str,
2938 _goal_id: &GoalId,
2939 _status: GoalStatus,
2940 _result: &[u8],
2941 ) -> NodeResult<()> {
2942 self.completed = true;
2943 Ok(())
2944 }
2945 fn publish_feedback_raw(
2946 &mut self,
2947 _action_entity: &str,
2948 _goal_id: &GoalId,
2949 _feedback: &[u8],
2950 ) -> NodeResult<()> {
2951 self.fed = true;
2952 Ok(())
2953 }
2954 fn for_each_active_goal(
2955 &self,
2956 _action_entity: &str,
2957 visit: &mut dyn FnMut(&GoalId, GoalStatus),
2958 ) {
2959 visit(&GoalId::zero(), GoalStatus::Executing);
2961 }
2962 }
2963
2964 struct RecClients;
2965 impl ClientDispatch for RecClients {
2966 fn call_raw(
2967 &mut self,
2968 _service: &str,
2969 _req: &[u8],
2970 _resp: &mut [u8],
2971 ) -> NodeResult<usize> {
2972 Err(NodeDeclError::Runtime)
2973 }
2974 fn send_goal_raw(&mut self, _action: &str, _goal: &[u8]) -> NodeResult<GoalId> {
2975 Err(NodeDeclError::Runtime)
2976 }
2977 }
2978
2979 let pubs = RecPub {
2980 published: Cell::new(false),
2981 };
2982 let mut acts = RecAct {
2983 completed: false,
2984 fed: false,
2985 visited: 0,
2986 };
2987 let mut clients = RecClients;
2988 let goal = GoalId::zero();
2989 let mut seen = 0usize;
2990 {
2991 let mut ctx = TickCtx::new(&pubs, &mut acts, &mut clients);
2992 ctx.publish::<TestMsg, 64>(EntityId::new("pub_x"), &TestMsg)
2993 .unwrap();
2994 ctx.for_each_active_goal(EntityId::new("act"), &mut |_id, _status| seen += 1);
2996 ctx.publish_feedback::<TestMsg, 64>(EntityId::new("act"), &goal, &TestMsg)
2997 .unwrap();
2998 ctx.complete_goal::<TestMsg, 64>(
2999 EntityId::new("act"),
3000 &goal,
3001 GoalStatus::Succeeded,
3002 &TestMsg,
3003 )
3004 .unwrap();
3005 }
3006 acts.visited = seen;
3007 assert!(pubs.published.get());
3008 assert!(acts.completed);
3009 assert!(acts.fed);
3010 assert_eq!(acts.visited, 1);
3011 }
3012
3013 #[test]
3017 fn node_dispatch_default_is_inline() {
3018 struct Dummy;
3019 impl Node for Dummy {
3020 const NAME: &'static str = "dummy";
3021 fn register(_: &mut NodeContext<'_>) -> NodeResult<()> {
3022 Ok(())
3023 }
3024 }
3025 assert_eq!(Dummy::DISPATCH, crate::DispatchStrategy::Inline);
3026 }
3027
3028 #[cfg(all(feature = "alloc", feature = "rmw-cffi"))]
3046 mod dispatch_probe_macro_test {
3047 use super::*;
3051
3052 pub struct DispatchProbe;
3053
3054 impl Node for DispatchProbe {
3055 const NAME: &'static str = "dispatch_probe";
3056 fn register(_: &mut NodeContext<'_>) -> NodeResult<()> {
3058 Ok(())
3059 }
3060 }
3061
3062 impl ExecutableNode for DispatchProbe {
3063 type State = ();
3064 fn init() -> Self::State {}
3065 fn on_callback(
3066 _state: &mut Self::State,
3067 _callback: Callback<'_>,
3068 _ctx: &mut CallbackCtx<'_>,
3069 ) {
3070 }
3071 }
3072
3073 nros_macros::node!(DispatchProbe);
3076 }
3077
3078 #[cfg(all(feature = "alloc", feature = "rmw-cffi"))]
3079 #[test]
3080 fn node_macro_emits_dispatch_strategy_symbol() {
3081 unsafe extern "C" {
3085 fn __nros_node_nros_dispatch_strategy() -> u8;
3086 }
3087 let strategy = unsafe { __nros_node_nros_dispatch_strategy() };
3088 assert_eq!(strategy, crate::DispatchStrategy::Inline as u8);
3092 assert_eq!(strategy, 0);
3093 }
3094
3095 #[cfg(all(feature = "alloc", feature = "rmw-cffi"))]
3112 #[test]
3113 fn node_macro_emits_on_callback_symbol() {
3114 unsafe extern "C" {
3115 fn __nros_node_nros_on_callback(
3116 state: *mut core::ffi::c_void,
3117 cb_id_ptr: *const u8,
3118 cb_id_len: usize,
3119 ctx: *mut core::ffi::c_void,
3120 );
3121 }
3122 let fn_ptr: unsafe extern "C" fn(
3132 *mut core::ffi::c_void,
3133 *const u8,
3134 usize,
3135 *mut core::ffi::c_void,
3136 ) = __nros_node_nros_on_callback;
3137 core::hint::black_box(fn_ptr);
3138 }
3139
3140 #[test]
3141 fn create_subscription_static_returns_tag_matching_topic() {
3142 let mut recorder = MetadataRecorder::<1, 1, 1>::new();
3143 let mut context = NodeContext::new("test", &mut recorder);
3144 let mut node = context.create_node(NodeOptions::new("listener")).unwrap();
3145 let tag = node
3146 .create_subscription_static::<TestMsg>("/chatter")
3147 .unwrap();
3148
3149 assert_eq!(tag.as_str(), "/chatter");
3150 assert!(tag == CallbackId::new("/chatter"));
3151 assert_eq!(recorder.entities().len(), 1);
3152 let entity = &recorder.entities()[0];
3153 assert_eq!(entity.kind, EntityKind::Subscription);
3154 assert_eq!(entity.source_name.as_str(), "/chatter");
3155 assert_eq!(
3156 entity.callback_id.as_ref().map(|id| id.as_str()),
3157 Some("/chatter")
3158 );
3159 }
3160
3161 #[test]
3162 fn create_service_static_returns_tag() {
3163 let mut recorder = MetadataRecorder::<1, 1, 1>::new();
3164 let mut context = NodeContext::new("test", &mut recorder);
3165 let mut node = context.create_node(NodeOptions::new("server")).unwrap();
3166 let tag = node
3167 .create_service_static::<TestService>("/add_two_ints")
3168 .unwrap();
3169
3170 assert_eq!(tag.as_str(), "/add_two_ints");
3171 assert!(tag == CallbackId::new("/add_two_ints"));
3172 assert_eq!(recorder.entities().len(), 1);
3173 let entity = &recorder.entities()[0];
3174 assert_eq!(entity.kind, EntityKind::ServiceServer);
3175 assert_eq!(entity.source_name.as_str(), "/add_two_ints");
3176 assert_eq!(
3177 entity.callback_id.as_ref().map(|id| id.as_str()),
3178 Some("/add_two_ints")
3179 );
3180 }
3181
3182 #[test]
3183 fn create_service_helpers_use_name_as_entity_and_callback_id() {
3184 let mut recorder = MetadataRecorder::<1, 2, 1>::new();
3185 let mut context = NodeContext::new("test", &mut recorder);
3186 let mut node = context.create_node(NodeOptions::new("services")).unwrap();
3187 let server = node
3188 .create_service_server_for_name::<TestService>("/add_two_ints")
3189 .unwrap();
3190 let client = node
3191 .create_service_client_for_name::<TestService>("/reset")
3192 .unwrap();
3193
3194 assert_eq!(server.id(), EntityId::new("/add_two_ints"));
3195 assert_eq!(client.id(), EntityId::new("/reset"));
3196 assert_eq!(recorder.entities().len(), 2);
3197
3198 let server = &recorder.entities()[0];
3199 assert_eq!(server.kind, EntityKind::ServiceServer);
3200 assert_eq!(server.id.as_str(), "/add_two_ints");
3201 assert_eq!(server.source_name.as_str(), "/add_two_ints");
3202 assert_eq!(
3203 server.callback_id.as_ref().map(|id| id.as_str()),
3204 Some("/add_two_ints")
3205 );
3206
3207 let client = &recorder.entities()[1];
3208 assert_eq!(client.kind, EntityKind::ServiceClient);
3209 assert_eq!(client.id.as_str(), "/reset");
3210 assert_eq!(client.source_name.as_str(), "/reset");
3211 assert!(client.callback_id.is_none());
3212 }
3213
3214 #[test]
3215 fn create_action_static_returns_tag() {
3216 let mut recorder = MetadataRecorder::<1, 1, 1>::new();
3217 let mut context = NodeContext::new("test", &mut recorder);
3218 let mut node = context.create_node(NodeOptions::new("server")).unwrap();
3219 let tag = node
3220 .create_action_static::<TestAction>("/fibonacci")
3221 .unwrap();
3222
3223 assert_eq!(tag.as_str(), "/fibonacci");
3224 assert!(tag == CallbackId::new("/fibonacci"));
3225 assert_eq!(recorder.entities().len(), 1);
3226 let entity = &recorder.entities()[0];
3227 assert_eq!(entity.kind, EntityKind::ActionServer);
3228 assert_eq!(entity.source_name.as_str(), "/fibonacci");
3229 assert_eq!(
3230 entity.callback_id.as_ref().map(|id| id.as_str()),
3231 Some("/fibonacci")
3232 );
3233 assert_eq!(
3234 entity
3235 .action_cancel_callback_id
3236 .as_ref()
3237 .map(|id| id.as_str()),
3238 Some("/fibonacci")
3239 );
3240 assert_eq!(
3241 entity
3242 .action_accepted_callback_id
3243 .as_ref()
3244 .map(|id| id.as_str()),
3245 Some("/fibonacci")
3246 );
3247 }
3248
3249 #[test]
3250 fn create_action_helpers_use_name_as_entity_and_default_callback_id() {
3251 let mut recorder = MetadataRecorder::<1, 2, 3>::new();
3252 let mut context = NodeContext::new("test", &mut recorder);
3253 let mut node = context.create_node(NodeOptions::new("actions")).unwrap();
3254 let server = node
3255 .create_action_server_for_name::<TestAction>("/fibonacci")
3256 .unwrap();
3257 let client = node
3258 .create_action_client_for_name::<TestAction>("/navigate")
3259 .unwrap();
3260
3261 assert_eq!(server.id(), EntityId::new("/fibonacci"));
3262 assert_eq!(client.id(), EntityId::new("/navigate"));
3263 assert_eq!(recorder.entities().len(), 2);
3264
3265 let server = &recorder.entities()[0];
3266 assert_eq!(server.kind, EntityKind::ActionServer);
3267 assert_eq!(server.id.as_str(), "/fibonacci");
3268 assert_eq!(server.source_name.as_str(), "/fibonacci");
3269 assert_eq!(
3270 server.callback_id.as_ref().map(|id| id.as_str()),
3271 Some("/fibonacci")
3272 );
3273 assert_eq!(
3274 server
3275 .action_cancel_callback_id
3276 .as_ref()
3277 .map(|id| id.as_str()),
3278 Some("/fibonacci")
3279 );
3280 assert_eq!(
3281 server
3282 .action_accepted_callback_id
3283 .as_ref()
3284 .map(|id| id.as_str()),
3285 Some("/fibonacci")
3286 );
3287
3288 let client = &recorder.entities()[1];
3289 assert_eq!(client.kind, EntityKind::ActionClient);
3290 assert_eq!(client.id.as_str(), "/navigate");
3291 assert_eq!(client.source_name.as_str(), "/navigate");
3292 assert!(client.callback_id.is_none());
3293 }
3294
3295 #[test]
3301 fn node_identity_injected_wins_over_node_options() {
3302 struct CapturingRuntime {
3305 node_identity: Option<(&'static str, &'static str)>,
3306 resolved_name: MetadataString,
3307 resolved_ns: MetadataString,
3308 }
3309 impl NodeRuntime for CapturingRuntime {
3310 fn create_node(&mut self, _id: NodeId<'_>, options: NodeOptions<'_>) -> NodeResult<()> {
3311 let (name, ns) = match self.node_identity {
3313 Some((n, s)) => (n, s),
3314 None => (options.name, options.namespace),
3315 };
3316 self.resolved_name.clear();
3317 let _ = self.resolved_name.push_str(name);
3318 self.resolved_ns.clear();
3319 let _ = self.resolved_ns.push_str(ns);
3320 Ok(())
3321 }
3322 fn create_entity(&mut self, _m: EntityMetadata) -> NodeResult<()> {
3323 Ok(())
3324 }
3325 fn record_callback_effect(
3326 &mut self,
3327 _id: CallbackId<'_>,
3328 _kind: crate::node_metadata::CallbackEffectKind,
3329 _entity: EntityId<'_>,
3330 ) -> NodeResult<()> {
3331 Ok(())
3332 }
3333 }
3334
3335 let mut rt_a = CapturingRuntime {
3337 node_identity: Some(("launched", "/ns")),
3338 resolved_name: MetadataString::new(),
3339 resolved_ns: MetadataString::new(),
3340 };
3341 {
3342 let mut ctx = NodeContext::new("test_node", &mut rt_a);
3343 ctx.create_node(NodeOptions::new("default").namespace("/d"))
3344 .unwrap();
3345 }
3346 assert_eq!(rt_a.resolved_name.as_str(), "launched");
3347 assert_eq!(rt_a.resolved_ns.as_str(), "/ns");
3348
3349 let mut rt_b = CapturingRuntime {
3351 node_identity: None,
3352 resolved_name: MetadataString::new(),
3353 resolved_ns: MetadataString::new(),
3354 };
3355 {
3356 let mut ctx = NodeContext::new("test_node", &mut rt_b);
3357 ctx.create_node(NodeOptions::new("default").namespace("/d"))
3358 .unwrap();
3359 }
3360 assert_eq!(rt_b.resolved_name.as_str(), "default");
3361 assert_eq!(rt_b.resolved_ns.as_str(), "/d");
3362 }
3363}