1use core::marker::PhantomData;
4
5use nros_core::RosAction;
6use nros_rmw::{ActionEndpointHashes, ActionInfo, QoSProfile, ServiceInfo, Session, TopicInfo};
7
8#[allow(unused_imports)]
9use crate::rmw_type_registry::{MessageForRmw, register_type};
10
11use super::{
12 action_core::{ActionClientCore, ActionServerCore, RawActiveGoal},
13 arena::{
14 ActionClientCallbackEntry, ActionClientRawArenaEntry, ActionServerArenaEntry,
15 ActionServerRawArenaEntry, BufferStrategy, CallbackMeta, EntryKind, TraceName,
16 action_client_callback_try_process, action_client_raw_try_process,
17 action_server_raw_try_process, action_server_try_process, always_ready,
18 as_active_goal_count, as_complete_goal, as_expire_goals, as_for_each_active_goal,
19 as_goal_exists, as_publish_feedback, as_raw_active_goal_count, as_raw_complete_goal,
20 as_raw_expire_goals, as_raw_for_each_active_goal, as_raw_goal_exists,
21 as_raw_publish_feedback, as_raw_set_goal_status, as_set_goal_status, buffered_region_size,
22 drop_entry, no_pre_sample,
23 },
24 handles::{ActionServer, ActiveGoal},
25 spin::Executor,
26 spsc_ring::SpscRing,
27 triple_buffer::TripleBuffer,
28 types::{
29 HandleId, InvocationMode, NodeError, RawAcceptedCallback, RawCancelCallback,
30 RawFeedbackCallback, RawGoalCallback, RawGoalResponseCallback, RawResultCallback,
31 },
32};
33
34pub const ACTION_SERVER_QUERYABLES: usize = 3;
48
49pub const ACTION_SERVER_PUBLISHERS: usize = 2;
59
60pub const ACTION_CLIENT_SUBSCRIPTIONS: usize = 1;
69
70pub const ACTION_CLIENT_SERVICE_CLIENTS: usize = 3;
80
81pub struct RawActionServerSpec<'a> {
91 pub node_id: Option<super::node_record::NodeId>,
96 pub action_name: &'a str,
97 pub type_name: &'a str,
98 pub type_hash: &'a str,
101 pub hashes: ActionEndpointHashes<'a>,
103 pub qos: QoSProfile,
108 pub goal_callback: RawGoalCallback,
109 pub cancel_callback: RawCancelCallback,
110 pub accepted_callback: Option<RawAcceptedCallback>,
111 pub context: *mut core::ffi::c_void,
112}
113
114pub struct RawActionClientSpec<'a> {
120 pub node_id: Option<super::node_record::NodeId>,
125 pub action_name: &'a str,
126 pub type_name: &'a str,
127 pub type_hash: &'a str,
130 pub hashes: ActionEndpointHashes<'a>,
132 pub goal_response_callback: Option<RawGoalResponseCallback>,
133 pub feedback_callback: Option<RawFeedbackCallback>,
134 pub result_callback: Option<RawResultCallback>,
135 pub context: *mut core::ffi::c_void,
136}
137
138impl<'s> Executor<'s> {
143 pub fn register_action_server<A, GoalF, CancelF>(
154 &mut self,
155 action_name: &str,
156 goal_callback: GoalF,
157 cancel_callback: CancelF,
158 ) -> Result<ActionServerHandle<A>, NodeError>
159 where
160 A: RosAction + 'static,
161 A::Goal: Clone + MessageForRmw,
162 A::Result: Clone + Default + MessageForRmw,
163 A::Feedback: MessageForRmw,
164 A::SendGoalRequest: MessageForRmw,
165 A::SendGoalResponse: MessageForRmw,
166 A::GetResultRequest: MessageForRmw,
167 A::GetResultResponse: MessageForRmw,
168 A::FeedbackMessage: MessageForRmw,
169 GoalF: FnMut(&nros_core::GoalId, &A::Goal) -> nros_core::GoalResponse + 'static,
170 CancelF:
171 FnMut(&nros_core::GoalId, nros_core::GoalStatus) -> nros_core::CancelResponse + 'static,
172 {
173 self.register_action_server_sized::<A, GoalF, CancelF, { crate::config::DEFAULT_RX_BUF_SIZE }, { crate::config::DEFAULT_RX_BUF_SIZE }, { crate::config::DEFAULT_RX_BUF_SIZE }, 4>(
174 action_name,
175 goal_callback,
176 cancel_callback,
177 )
178 }
179
180 pub fn register_action_server_sized<
182 A,
183 GoalF,
184 CancelF,
185 const GOAL_BUF: usize,
186 const RESULT_BUF: usize,
187 const FEEDBACK_BUF: usize,
188 const MAX_GOALS: usize,
189 >(
190 &mut self,
191 action_name: &str,
192 goal_callback: GoalF,
193 cancel_callback: CancelF,
194 ) -> Result<ActionServerHandle<A>, NodeError>
195 where
196 A: RosAction + 'static,
197 A::Goal: Clone + MessageForRmw,
198 A::Result: Clone + Default + MessageForRmw,
199 A::Feedback: MessageForRmw,
200 A::SendGoalRequest: MessageForRmw,
201 A::SendGoalResponse: MessageForRmw,
202 A::GetResultRequest: MessageForRmw,
203 A::GetResultResponse: MessageForRmw,
204 A::FeedbackMessage: MessageForRmw,
205 GoalF: FnMut(&nros_core::GoalId, &A::Goal) -> nros_core::GoalResponse + 'static,
206 CancelF:
207 FnMut(&nros_core::GoalId, nros_core::GoalStatus) -> nros_core::CancelResponse + 'static,
208 {
209 register_type::<A::Goal>()?;
216 register_type::<A::Result>()?;
217 register_type::<A::Feedback>()?;
218 register_type::<A::SendGoalRequest>()?;
219 register_type::<A::SendGoalResponse>()?;
220 register_type::<A::GetResultRequest>()?;
221 register_type::<A::GetResultResponse>()?;
222 register_type::<A::FeedbackMessage>()?;
223 A::register_protocol_types().map_err(|()| NodeError::ActionCreationFailed)?;
229 type Entry<
230 A,
231 GoalF,
232 CancelF,
233 const GB: usize,
234 const RB: usize,
235 const FB: usize,
236 const MG: usize,
237 > = ActionServerArenaEntry<A, GoalF, CancelF, GB, RB, FB, MG>;
238
239 let slot = self.next_entry_slot()?;
240
241 let action_info = ActionInfo::new(action_name, A::ACTION_NAME, A::ACTION_HASH);
243
244 let send_goal_type = super::action_core::action_service_base_type(
249 <A::SendGoalRequest as nros_core::RosMessage>::TYPE_NAME,
250 A::ACTION_NAME,
251 );
252 let get_result_type = super::action_core::action_service_base_type(
253 <A::GetResultRequest as nros_core::RosMessage>::TYPE_NAME,
254 A::ACTION_NAME,
255 );
256 let feedback_type = <A::FeedbackMessage as nros_core::RosMessage>::TYPE_NAME;
257
258 let send_goal_keyexpr: heapless::String<256> = action_info.send_goal_key();
259 let send_goal_info = ServiceInfo::new(
260 &send_goal_keyexpr,
261 send_goal_type,
262 A::SEND_GOAL_SERVICE_HASH,
263 )
264 .with_domain(self.domain_id);
265 let send_goal_server = self
266 .session
267 .create_service(&send_goal_info, QoSProfile::services_default())
268 .map_err(NodeError::Transport)?;
269
270 let cancel_goal_keyexpr: heapless::String<256> = action_info.cancel_goal_key();
271 let cancel_goal_info = ServiceInfo::new(
272 &cancel_goal_keyexpr,
273 "action_msgs::srv::dds_::CancelGoal_",
274 A::CANCEL_GOAL_SERVICE_HASH,
275 )
276 .with_domain(self.domain_id);
277 let cancel_goal_server = self
278 .session
279 .create_service(&cancel_goal_info, QoSProfile::services_default())
280 .map_err(NodeError::Transport)?;
281
282 let get_result_keyexpr: heapless::String<256> = action_info.get_result_key();
283 let get_result_info = ServiceInfo::new(
284 &get_result_keyexpr,
285 get_result_type,
286 A::GET_RESULT_SERVICE_HASH,
287 )
288 .with_domain(self.domain_id);
289 let get_result_server = self
290 .session
291 .create_service(&get_result_info, QoSProfile::services_default())
292 .map_err(NodeError::Transport)?;
293
294 let feedback_keyexpr: heapless::String<256> = action_info.feedback_key();
295 let feedback_topic = TopicInfo::new(
296 &feedback_keyexpr,
297 feedback_type,
298 <A::FeedbackMessage as nros_core::RosMessage>::TYPE_HASH,
299 )
300 .with_domain(self.domain_id);
301 let feedback_publisher = self
302 .session
303 .create_publisher(&feedback_topic, QoSProfile::QOS_PROFILE_DEFAULT)
304 .map_err(NodeError::Transport)?;
305
306 let status_keyexpr: heapless::String<256> = action_info.status_key();
307 let status_topic = TopicInfo::new(
308 &status_keyexpr,
309 "action_msgs::msg::dds_::GoalStatusArray_",
310 A::STATUS_MESSAGE_HASH,
311 )
312 .with_domain(self.domain_id);
313 let status_publisher = self
314 .session
315 .create_publisher(&status_topic, QoSProfile::QOS_PROFILE_ACTION_STATUS_DEFAULT)
316 .map_err(NodeError::Transport)?;
317
318 let server = ActionServer {
319 core: super::action_core::ActionServerCore {
320 send_goal_server,
321 cancel_goal_server,
322 get_result_server,
323 feedback_publisher,
324 status_publisher,
325 active_goals: heapless::Vec::new(),
326 completed_results: heapless::Vec::new(),
327 pending_get_results: heapless::Vec::new(),
328 result_slab: [0u8; RESULT_BUF],
329 result_slab_used: 0,
330 goal_buffer: [0u8; GOAL_BUF],
331 feedback_buffer: [0u8; FEEDBACK_BUF],
332 cancel_buffer: [0u8; 256],
333 },
334 typed_goals: heapless::Vec::new(),
335 completed_goals: heapless::Vec::new(),
336 };
337
338 let offset = self
339 .arena_alloc::<Entry<A, GoalF, CancelF, GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>>(
340 )?;
341
342 unsafe {
343 let arena_ptr = self.arena.as_mut_ptr() as *mut u8;
344 let entry_ptr = arena_ptr.add(offset)
345 as *mut Entry<A, GoalF, CancelF, GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>;
346 core::ptr::write(
347 entry_ptr,
348 Entry {
349 server,
350 goal_callback,
351 cancel_callback,
352 },
353 );
354 }
355
356 let meta = CallbackMeta {
357 offset,
358 kind: EntryKind::ActionServer,
359 arena_len: super::arena::ArenaLen::UNRECORDED,
360 has_data: always_ready,
361 pre_sample: no_pre_sample,
362 invocation: InvocationMode::Always,
363 try_process: action_server_try_process::<
364 A,
365 GoalF,
366 CancelF,
367 GOAL_BUF,
368 RESULT_BUF,
369 FEEDBACK_BUF,
370 MAX_GOALS,
371 >,
372 drop_fn: drop_entry::<
373 Entry<A, GoalF, CancelF, GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>,
374 >,
375 };
376 self.emplace_entry(slot, meta, TraceName::Text(action_name));
377
378 Ok(ActionServerHandle {
379 entry_index: slot,
380 publish_feedback_fn: as_publish_feedback::<
381 A,
382 GoalF,
383 CancelF,
384 GOAL_BUF,
385 RESULT_BUF,
386 FEEDBACK_BUF,
387 MAX_GOALS,
388 >,
389 complete_goal_fn: as_complete_goal::<
390 A,
391 GoalF,
392 CancelF,
393 GOAL_BUF,
394 RESULT_BUF,
395 FEEDBACK_BUF,
396 MAX_GOALS,
397 >,
398 set_goal_status_fn: as_set_goal_status::<
399 A,
400 GoalF,
401 CancelF,
402 GOAL_BUF,
403 RESULT_BUF,
404 FEEDBACK_BUF,
405 MAX_GOALS,
406 >,
407 active_goal_count_fn: as_active_goal_count::<
408 A,
409 GoalF,
410 CancelF,
411 GOAL_BUF,
412 RESULT_BUF,
413 FEEDBACK_BUF,
414 MAX_GOALS,
415 >,
416 goal_exists_fn: as_goal_exists::<
417 A,
418 GoalF,
419 CancelF,
420 GOAL_BUF,
421 RESULT_BUF,
422 FEEDBACK_BUF,
423 MAX_GOALS,
424 >,
425 expire_goals_fn: as_expire_goals::<
426 A,
427 GoalF,
428 CancelF,
429 GOAL_BUF,
430 RESULT_BUF,
431 FEEDBACK_BUF,
432 MAX_GOALS,
433 >,
434 for_each_active_goal_fn: as_for_each_active_goal::<
435 A,
436 GoalF,
437 CancelF,
438 GOAL_BUF,
439 RESULT_BUF,
440 FEEDBACK_BUF,
441 MAX_GOALS,
442 >,
443 _phantom: PhantomData,
444 })
445 }
446}
447
448#[allow(clippy::type_complexity)]
459pub struct ActionServerHandle<A: RosAction> {
460 pub(crate) entry_index: usize,
461 publish_feedback_fn:
462 unsafe fn(*mut u8, &nros_core::GoalId, &A::Feedback) -> Result<(), NodeError>,
463 complete_goal_fn: unsafe fn(
464 *mut u8,
465 &nros_core::GoalId,
466 nros_core::GoalStatus,
467 A::Result,
468 ) -> Result<(), NodeError>,
469 set_goal_status_fn: unsafe fn(*mut u8, &nros_core::GoalId, nros_core::GoalStatus),
470 active_goal_count_fn: unsafe fn(*const u8) -> usize,
471 goal_exists_fn: unsafe fn(*const u8, &nros_core::GoalId) -> bool,
472 expire_goals_fn: unsafe fn(*mut u8) -> usize,
473 for_each_active_goal_fn: unsafe fn(*const u8, &mut dyn FnMut(&ActiveGoal<A>)),
474 _phantom: PhantomData<A>,
475}
476
477impl<A: RosAction> Clone for ActionServerHandle<A> {
478 fn clone(&self) -> Self {
479 *self
480 }
481}
482
483impl<A: RosAction> Copy for ActionServerHandle<A> {}
484
485impl<A: RosAction> ActionServerHandle<A> {
486 pub fn handle_id(&self) -> HandleId {
490 HandleId::for_owned_slot(self.entry_index)
491 }
492
493 pub fn publish_feedback(
499 &self,
500 executor: &mut Executor,
501 goal_id: &nros_core::GoalId,
502 feedback: &A::Feedback,
503 ) -> Result<(), NodeError> {
504 let meta = executor.entries[self.entry_index]
505 .as_ref()
506 .ok_or(NodeError::BufferTooSmall)?;
507 let arena_ptr = executor.arena.as_mut_ptr() as *mut u8;
508 unsafe {
509 let data_ptr = arena_ptr.add(meta.offset);
510 (self.publish_feedback_fn)(data_ptr, goal_id, feedback)
511 }
512 }
513
514 pub fn succeed(
541 &self,
542 executor: &mut Executor,
543 goal_id: &nros_core::GoalId,
544 result: A::Result,
545 ) -> Result<(), NodeError> {
546 self.complete_goal(executor, goal_id, nros_core::GoalStatus::Succeeded, result)
547 }
548
549 pub fn abort(
551 &self,
552 executor: &mut Executor,
553 goal_id: &nros_core::GoalId,
554 result: A::Result,
555 ) -> Result<(), NodeError> {
556 self.complete_goal(executor, goal_id, nros_core::GoalStatus::Aborted, result)
557 }
558
559 pub fn canceled(
570 &self,
571 executor: &mut Executor,
572 goal_id: &nros_core::GoalId,
573 result: A::Result,
574 ) -> Result<(), NodeError> {
575 self.complete_goal(executor, goal_id, nros_core::GoalStatus::Canceled, result)
576 }
577
578 pub fn complete_goal(
579 &self,
580 executor: &mut Executor,
581 goal_id: &nros_core::GoalId,
582 status: nros_core::GoalStatus,
583 result: A::Result,
584 ) -> Result<(), NodeError> {
585 let meta = executor.entries[self.entry_index]
586 .as_ref()
587 .ok_or(NodeError::BufferTooSmall)?;
588 let arena_ptr = executor.arena.as_mut_ptr() as *mut u8;
589 unsafe {
590 let data_ptr = arena_ptr.add(meta.offset);
591 (self.complete_goal_fn)(data_ptr, goal_id, status, result)
592 }
593 }
594
595 pub fn set_goal_status(
600 &self,
601 executor: &mut Executor,
602 goal_id: &nros_core::GoalId,
603 status: nros_core::GoalStatus,
604 ) {
605 if let Some(meta) = executor.entries[self.entry_index].as_ref() {
606 let arena_ptr = executor.arena.as_mut_ptr() as *mut u8;
607 unsafe {
608 let data_ptr = arena_ptr.add(meta.offset);
609 (self.set_goal_status_fn)(data_ptr, goal_id, status);
610 }
611 }
612 }
613
614 pub fn active_goal_count(&self, executor: &Executor) -> usize {
618 match executor.entries[self.entry_index].as_ref() {
619 Some(meta) => {
620 let arena_ptr = executor.arena.as_ptr() as *const u8;
621 unsafe {
622 let data_ptr = arena_ptr.add(meta.offset);
623 (self.active_goal_count_fn)(data_ptr)
624 }
625 }
626 None => 0,
627 }
628 }
629
630 pub fn goal_exists(&self, executor: &Executor, goal_id: &nros_core::GoalId) -> bool {
642 match executor.entries[self.entry_index].as_ref() {
643 Some(meta) => {
644 let arena_ptr = executor.arena.as_ptr() as *const u8;
645 unsafe {
646 let data_ptr = arena_ptr.add(meta.offset);
647 (self.goal_exists_fn)(data_ptr, goal_id)
648 }
649 }
650 None => false,
651 }
652 }
653
654 pub fn expire_goals(&self, executor: &mut Executor) -> usize {
664 match executor.entries[self.entry_index].as_ref() {
665 Some(meta) => {
666 let arena_ptr = executor.arena.as_mut_ptr() as *mut u8;
667 unsafe {
668 let data_ptr = arena_ptr.add(meta.offset);
669 (self.expire_goals_fn)(data_ptr)
670 }
671 }
672 None => 0,
673 }
674 }
675
676 pub fn for_each_active_goal(&self, executor: &Executor, mut f: impl FnMut(&ActiveGoal<A>)) {
681 if let Some(meta) = executor.entries[self.entry_index].as_ref() {
682 let arena_ptr = executor.arena.as_ptr() as *const u8;
683 unsafe {
684 let data_ptr = arena_ptr.add(meta.offset);
685 (self.for_each_active_goal_fn)(data_ptr, &mut f);
686 }
687 }
688 }
689}
690
691impl<'s> Executor<'s> {
696 #[allow(clippy::too_many_arguments)]
705 pub fn register_action_server_raw(
706 &mut self,
707 spec: RawActionServerSpec<'_>,
708 ) -> Result<ActionServerRawHandle, NodeError> {
709 self.register_action_server_raw_sized::<{ crate::config::DEFAULT_RX_BUF_SIZE }, { crate::config::DEFAULT_RX_BUF_SIZE }, { crate::config::DEFAULT_RX_BUF_SIZE }, 4>(
710 spec,
711 )
712 }
713
714 pub fn register_action_server_raw_sized<
723 const GOAL_BUF: usize,
724 const RESULT_BUF: usize,
725 const FEEDBACK_BUF: usize,
726 const MAX_GOALS: usize,
727 >(
728 &mut self,
729 spec: RawActionServerSpec<'_>,
730 ) -> Result<ActionServerRawHandle, NodeError> {
731 let RawActionServerSpec {
732 node_id,
733 action_name,
734 type_name,
735 type_hash,
736 hashes,
737 qos,
738 goal_callback,
739 cancel_callback,
740 accepted_callback,
741 context,
742 } = spec;
743
744 type Entry<const GB: usize, const RB: usize, const FB: usize, const MG: usize> =
745 ActionServerRawArenaEntry<GB, RB, FB, MG>;
746
747 let slot = self.next_entry_slot()?;
748
749 let action_info = ActionInfo::new(action_name, type_name, type_hash);
750 let domain_id = self.domain_id;
754 let (node_name, ns, session_idx) = match node_id {
755 Some(id) => {
756 let r = self
757 .nodes
758 .get(id.index())
759 .ok_or(NodeError::InvalidSchedContextBinding)?;
760 (r.name.clone(), r.namespace.clone(), r.session_idx)
761 }
762 None => (self.node_name.clone(), self.namespace.clone(), 0u8),
763 };
764
765 let (
774 send_goal_server,
775 cancel_goal_server,
776 get_result_server,
777 feedback_publisher,
778 status_publisher,
779 ) = {
780 let send_goal_type: heapless::String<256> =
786 super::action_core::action_channel_type(type_name, "SendGoal");
787 let get_result_type: heapless::String<256> =
788 super::action_core::action_channel_type(type_name, "GetResult");
789 let feedback_type: heapless::String<256> =
790 super::action_core::action_channel_type(type_name, "FeedbackMessage");
791
792 let send_goal_keyexpr: heapless::String<256> = action_info.send_goal_key();
793 let mut send_goal_info =
794 ServiceInfo::new(&send_goal_keyexpr, &send_goal_type, hashes.send_goal)
795 .with_namespace(&ns)
796 .with_domain(domain_id);
797 if !node_name.is_empty() {
798 send_goal_info = send_goal_info.with_node_name(&node_name);
799 }
800
801 let cancel_goal_keyexpr: heapless::String<256> = action_info.cancel_goal_key();
802 let mut cancel_goal_info = ServiceInfo::new(
803 &cancel_goal_keyexpr,
804 "action_msgs::srv::dds_::CancelGoal_",
805 hashes.cancel_goal,
806 )
807 .with_namespace(&ns)
808 .with_domain(domain_id);
809 if !node_name.is_empty() {
810 cancel_goal_info = cancel_goal_info.with_node_name(&node_name);
811 }
812
813 let get_result_keyexpr: heapless::String<256> = action_info.get_result_key();
814 let mut get_result_info =
815 ServiceInfo::new(&get_result_keyexpr, &get_result_type, hashes.get_result)
816 .with_namespace(&ns)
817 .with_domain(domain_id);
818 if !node_name.is_empty() {
819 get_result_info = get_result_info.with_node_name(&node_name);
820 }
821
822 let feedback_keyexpr: heapless::String<256> = action_info.feedback_key();
823 let mut feedback_topic =
824 TopicInfo::new(&feedback_keyexpr, &feedback_type, hashes.feedback)
825 .with_namespace(&ns)
826 .with_domain(domain_id);
827 if !node_name.is_empty() {
828 feedback_topic = feedback_topic.with_node_name(&node_name);
829 }
830
831 let status_keyexpr: heapless::String<256> = action_info.status_key();
832 let mut status_topic = TopicInfo::new(
833 &status_keyexpr,
834 "action_msgs::msg::dds_::GoalStatusArray_",
835 hashes.status,
836 )
837 .with_namespace(&ns)
838 .with_domain(domain_id);
839 if !node_name.is_empty() {
840 status_topic = status_topic.with_node_name(&node_name);
841 }
842
843 let session = self
844 .session_at_mut(session_idx)
845 .ok_or(NodeError::BackendMismatch)?;
846 (
847 session
848 .create_service(&send_goal_info, qos)
849 .map_err(NodeError::Transport)?,
850 session
851 .create_service(&cancel_goal_info, qos)
852 .map_err(NodeError::Transport)?,
853 session
854 .create_service(&get_result_info, qos)
855 .map_err(NodeError::Transport)?,
856 session
857 .create_publisher(&feedback_topic, QoSProfile::QOS_PROFILE_DEFAULT)
858 .map_err(NodeError::Transport)?,
859 session
860 .create_publisher(&status_topic, QoSProfile::QOS_PROFILE_ACTION_STATUS_DEFAULT)
861 .map_err(NodeError::Transport)?,
862 )
863 };
864
865 let core = ActionServerCore {
866 send_goal_server,
867 cancel_goal_server,
868 get_result_server,
869 feedback_publisher,
870 status_publisher,
871 active_goals: heapless::Vec::new(),
872 completed_results: heapless::Vec::new(),
873 pending_get_results: heapless::Vec::new(),
874 result_slab: [0u8; RESULT_BUF],
875 result_slab_used: 0,
876 goal_buffer: [0u8; GOAL_BUF],
877 feedback_buffer: [0u8; FEEDBACK_BUF],
878 cancel_buffer: [0u8; 256],
879 };
880
881 let offset = self.arena_alloc::<Entry<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>>()?;
882
883 unsafe {
884 let arena_ptr = self.arena.as_mut_ptr() as *mut u8;
885 let entry_ptr =
886 arena_ptr.add(offset) as *mut Entry<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>;
887 core::ptr::write(
888 entry_ptr,
889 Entry {
890 core,
891 goal_callback,
892 cancel_callback,
893 accepted_callback,
894 context,
895 },
896 );
897 }
898
899 let meta = CallbackMeta {
900 offset,
901 kind: EntryKind::ActionServer,
902 arena_len: super::arena::ArenaLen::UNRECORDED,
903 has_data: always_ready,
904 pre_sample: no_pre_sample,
905 invocation: InvocationMode::Always,
906 try_process: action_server_raw_try_process::<
907 GOAL_BUF,
908 RESULT_BUF,
909 FEEDBACK_BUF,
910 MAX_GOALS,
911 >,
912 drop_fn: drop_entry::<Entry<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>>,
913 };
914 self.emplace_entry(slot, meta, TraceName::Text(spec.action_name));
915 self.apply_node_default_sched(slot, node_id, None);
916
917 Ok(ActionServerRawHandle {
918 entry_index: slot,
919 publish_feedback_fn: as_raw_publish_feedback::<
920 GOAL_BUF,
921 RESULT_BUF,
922 FEEDBACK_BUF,
923 MAX_GOALS,
924 >,
925 complete_goal_fn: as_raw_complete_goal::<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>,
926 set_goal_status_fn: as_raw_set_goal_status::<
927 GOAL_BUF,
928 RESULT_BUF,
929 FEEDBACK_BUF,
930 MAX_GOALS,
931 >,
932 active_goal_count_fn: as_raw_active_goal_count::<
933 GOAL_BUF,
934 RESULT_BUF,
935 FEEDBACK_BUF,
936 MAX_GOALS,
937 >,
938 goal_exists_fn: as_raw_goal_exists::<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>,
939 expire_goals_fn: as_raw_expire_goals::<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>,
940 for_each_active_goal_fn: as_raw_for_each_active_goal::<
941 GOAL_BUF,
942 RESULT_BUF,
943 FEEDBACK_BUF,
944 MAX_GOALS,
945 >,
946 })
947 }
948}
949
950unsafe extern "C" fn detached_goal_callback(
969 _goal_id: *const nros_core::GoalId,
970 _goal_data: *const u8,
971 _goal_len: usize,
972 _context: *mut core::ffi::c_void,
973) -> nros_core::GoalResponse {
974 nros_core::GoalResponse::Reject
975}
976
977unsafe extern "C" fn detached_cancel_callback(
988 _goal_id: *const nros_core::GoalId,
989 _status: nros_core::GoalStatus,
990 _context: *mut core::ffi::c_void,
991) -> nros_core::CancelResponse {
992 nros_core::CancelResponse::Accept
993}
994
995impl<'s> Executor<'s> {
996 pub unsafe fn detach_action_server_raw_sized<
1028 const GOAL_BUF: usize,
1029 const RESULT_BUF: usize,
1030 const FEEDBACK_BUF: usize,
1031 const MAX_GOALS: usize,
1032 >(
1033 &mut self,
1034 entry_index: usize,
1035 ) -> bool {
1036 let Some(meta) = self.entries.get(entry_index).and_then(|e| e.as_ref()) else {
1037 return false;
1038 };
1039 if !matches!(meta.kind, EntryKind::ActionServer) {
1040 return false;
1041 }
1042 let offset = meta.offset;
1043 let arena_ptr = self.arena.as_mut_ptr() as *mut u8;
1044 unsafe {
1045 let entry = &mut *(arena_ptr.add(offset)
1046 as *mut ActionServerRawArenaEntry<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>);
1047 entry.goal_callback = detached_goal_callback;
1048 entry.cancel_callback = detached_cancel_callback;
1049 entry.accepted_callback = None;
1050 entry.context = core::ptr::null_mut();
1051 }
1052 true
1053 }
1054
1055 pub unsafe fn detach_action_server_raw(&mut self, handle: &ActionServerRawHandle) -> bool {
1069 unsafe {
1070 self.detach_action_server_raw_sized::<
1071 { crate::config::DEFAULT_RX_BUF_SIZE },
1072 { crate::config::DEFAULT_RX_BUF_SIZE },
1073 { crate::config::DEFAULT_RX_BUF_SIZE },
1074 4,
1075 >(handle.entry_index)
1076 }
1077 }
1078
1079 pub unsafe fn detach_action_client_raw(&mut self, entry_index: usize) -> bool {
1098 let Some(meta) = self.entries.get(entry_index).and_then(|e| e.as_ref()) else {
1099 return false;
1100 };
1101 if !matches!(meta.kind, EntryKind::ActionClient) {
1102 return false;
1103 }
1104 let offset = meta.offset;
1105 let arena_ptr = self.arena.as_mut_ptr() as *mut u8;
1106 unsafe {
1107 let entry = &mut *(arena_ptr.add(offset)
1108 as *mut ActionClientRawArenaEntry<
1109 { crate::config::DEFAULT_RX_BUF_SIZE },
1110 { crate::config::DEFAULT_RX_BUF_SIZE },
1111 { crate::config::DEFAULT_RX_BUF_SIZE },
1112 >);
1113 entry.goal_response_callback = None;
1114 entry.feedback_callback = None;
1115 entry.result_callback = None;
1116 entry.context = core::ptr::null_mut();
1117 }
1118 true
1119 }
1120
1121 pub unsafe fn retarget_action_client_raw(
1133 &mut self,
1134 entry_index: usize,
1135 context: *mut core::ffi::c_void,
1136 ) -> bool {
1137 let Some(meta) = self.entries.get(entry_index).and_then(|e| e.as_ref()) else {
1138 return false;
1139 };
1140 if !matches!(meta.kind, EntryKind::ActionClient) {
1141 return false;
1142 }
1143 let offset = meta.offset;
1144 unsafe {
1145 let entry = &mut *((self.arena.as_mut_ptr() as *mut u8).add(offset)
1146 as *mut ActionClientRawArenaEntry<
1147 { crate::config::DEFAULT_RX_BUF_SIZE },
1148 { crate::config::DEFAULT_RX_BUF_SIZE },
1149 { crate::config::DEFAULT_RX_BUF_SIZE },
1150 >);
1151 entry.context = context;
1152 }
1153 true
1154 }
1155
1156 pub unsafe fn release_action_server_raw_sized<
1179 const GOAL_BUF: usize,
1180 const RESULT_BUF: usize,
1181 const FEEDBACK_BUF: usize,
1182 const MAX_GOALS: usize,
1183 >(
1184 &mut self,
1185 entry_index: usize,
1186 ) -> bool {
1187 unsafe { self.release_entry(entry_index, EntryKind::ActionServer) }
1188 }
1189
1190 pub unsafe fn release_action_server_raw(&mut self, handle: &ActionServerRawHandle) -> bool {
1197 unsafe {
1198 self.release_action_server_raw_sized::<
1199 { crate::config::DEFAULT_RX_BUF_SIZE },
1200 { crate::config::DEFAULT_RX_BUF_SIZE },
1201 { crate::config::DEFAULT_RX_BUF_SIZE },
1202 4,
1203 >(handle.entry_index)
1204 }
1205 }
1206
1207 pub unsafe fn release_action_client_raw(&mut self, entry_index: usize) -> bool {
1217 unsafe {
1218 self.release_action_client_raw_sized::<
1219 { crate::config::DEFAULT_RX_BUF_SIZE },
1220 { crate::config::DEFAULT_RX_BUF_SIZE },
1221 { crate::config::DEFAULT_RX_BUF_SIZE },
1222 >(entry_index)
1223 }
1224 }
1225
1226 pub unsafe fn release_action_client_raw_sized<
1234 const GOAL_BUF: usize,
1235 const RESULT_BUF: usize,
1236 const FEEDBACK_BUF: usize,
1237 >(
1238 &mut self,
1239 entry_index: usize,
1240 ) -> bool {
1241 unsafe { self.release_entry(entry_index, EntryKind::ActionClient) }
1242 }
1243}
1244
1245#[repr(C)]
1254#[allow(clippy::type_complexity)]
1255pub struct ActionServerRawHandle {
1256 pub(crate) entry_index: usize,
1257 publish_feedback_fn:
1258 unsafe fn(*mut u8, &nros_core::GoalId, *const u8, usize) -> Result<(), NodeError>,
1259 complete_goal_fn: unsafe fn(
1260 *mut u8,
1261 &nros_core::GoalId,
1262 nros_core::GoalStatus,
1263 *const u8,
1264 usize,
1265 ) -> Result<(), NodeError>,
1266 set_goal_status_fn: unsafe fn(*mut u8, &nros_core::GoalId, nros_core::GoalStatus),
1267 active_goal_count_fn: unsafe fn(*const u8) -> usize,
1268 goal_exists_fn: unsafe fn(*const u8, &nros_core::GoalId) -> bool,
1269 expire_goals_fn: unsafe fn(*mut u8) -> usize,
1270 for_each_active_goal_fn: unsafe fn(*const u8, &mut dyn FnMut(&RawActiveGoal)),
1271}
1272
1273impl Clone for ActionServerRawHandle {
1274 fn clone(&self) -> Self {
1275 *self
1276 }
1277}
1278
1279impl Copy for ActionServerRawHandle {}
1280
1281pub const INVALID_ENTRY_INDEX: usize = usize::MAX;
1289
1290impl ActionServerRawHandle {
1291 pub const fn invalid() -> Self {
1297 unsafe fn unreachable_publish_feedback(
1298 _: *mut u8,
1299 _: &nros_core::GoalId,
1300 _: *const u8,
1301 _: usize,
1302 ) -> Result<(), NodeError> {
1303 unreachable!("ActionServerRawHandle::publish_feedback called on invalid handle")
1304 }
1305 unsafe fn unreachable_complete_goal(
1306 _: *mut u8,
1307 _: &nros_core::GoalId,
1308 _: nros_core::GoalStatus,
1309 _: *const u8,
1310 _: usize,
1311 ) -> Result<(), NodeError> {
1312 unreachable!("ActionServerRawHandle::complete_goal called on invalid handle")
1313 }
1314 unsafe fn unreachable_set_goal_status(
1315 _: *mut u8,
1316 _: &nros_core::GoalId,
1317 _: nros_core::GoalStatus,
1318 ) {
1319 unreachable!("ActionServerRawHandle::set_goal_status called on invalid handle")
1320 }
1321 unsafe fn unreachable_active_goal_count(_: *const u8) -> usize {
1322 unreachable!("ActionServerRawHandle::active_goal_count called on invalid handle")
1323 }
1324 unsafe fn unreachable_goal_exists(_: *const u8, _: &nros_core::GoalId) -> bool {
1325 unreachable!("ActionServerRawHandle::goal_exists called on invalid handle")
1326 }
1327 unsafe fn unreachable_expire_goals(_: *mut u8) -> usize {
1328 unreachable!("ActionServerRawHandle::expire_goals called on invalid handle")
1329 }
1330 unsafe fn unreachable_for_each_active_goal(
1331 _: *const u8,
1332 _: &mut dyn FnMut(&RawActiveGoal),
1333 ) {
1334 unreachable!("ActionServerRawHandle::for_each_active_goal called on invalid handle")
1335 }
1336 Self {
1337 entry_index: INVALID_ENTRY_INDEX,
1338 publish_feedback_fn: unreachable_publish_feedback,
1339 complete_goal_fn: unreachable_complete_goal,
1340 set_goal_status_fn: unreachable_set_goal_status,
1341 active_goal_count_fn: unreachable_active_goal_count,
1342 goal_exists_fn: unreachable_goal_exists,
1343 expire_goals_fn: unreachable_expire_goals,
1344 for_each_active_goal_fn: unreachable_for_each_active_goal,
1345 }
1346 }
1347
1348 pub const fn is_invalid(&self) -> bool {
1350 self.entry_index == INVALID_ENTRY_INDEX
1351 }
1352}
1353
1354impl Default for ActionServerRawHandle {
1355 fn default() -> Self {
1356 Self::invalid()
1357 }
1358}
1359
1360impl ActionServerRawHandle {
1361 pub fn handle_id(&self) -> HandleId {
1363 HandleId::for_owned_slot(self.entry_index)
1364 }
1365
1366 pub fn publish_feedback_raw(
1370 &self,
1371 executor: &mut Executor,
1372 goal_id: &nros_core::GoalId,
1373 feedback_data: &[u8],
1374 ) -> Result<(), NodeError> {
1375 let meta = executor.entries[self.entry_index]
1376 .as_ref()
1377 .ok_or(NodeError::BufferTooSmall)?;
1378 let arena_ptr = executor.arena.as_mut_ptr() as *mut u8;
1379 unsafe {
1380 let data_ptr = arena_ptr.add(meta.offset);
1381 (self.publish_feedback_fn)(
1382 data_ptr,
1383 goal_id,
1384 feedback_data.as_ptr(),
1385 feedback_data.len(),
1386 )
1387 }
1388 }
1389
1390 pub fn complete_goal_raw(
1401 &self,
1402 executor: &mut Executor,
1403 goal_id: &nros_core::GoalId,
1404 status: nros_core::GoalStatus,
1405 result_data: &[u8],
1406 ) -> Result<(), NodeError> {
1407 let meta = executor.entries[self.entry_index]
1408 .as_ref()
1409 .ok_or(NodeError::BufferTooSmall)?;
1410 let arena_ptr = executor.arena.as_mut_ptr() as *mut u8;
1411 unsafe {
1412 let data_ptr = arena_ptr.add(meta.offset);
1413 (self.complete_goal_fn)(
1414 data_ptr,
1415 goal_id,
1416 status,
1417 result_data.as_ptr(),
1418 result_data.len(),
1419 )
1420 }
1421 }
1422
1423 pub fn set_goal_status(
1428 &self,
1429 executor: &mut Executor,
1430 goal_id: &nros_core::GoalId,
1431 status: nros_core::GoalStatus,
1432 ) {
1433 if let Some(meta) = executor.entries[self.entry_index].as_ref() {
1434 let arena_ptr = executor.arena.as_mut_ptr() as *mut u8;
1435 unsafe {
1436 let data_ptr = arena_ptr.add(meta.offset);
1437 (self.set_goal_status_fn)(data_ptr, goal_id, status);
1438 }
1439 }
1440 }
1441
1442 pub fn active_goal_count(&self, executor: &Executor) -> usize {
1446 match executor.entries[self.entry_index].as_ref() {
1447 Some(meta) => {
1448 let arena_ptr = executor.arena.as_ptr() as *const u8;
1449 unsafe {
1450 let data_ptr = arena_ptr.add(meta.offset);
1451 (self.active_goal_count_fn)(data_ptr)
1452 }
1453 }
1454 None => 0,
1455 }
1456 }
1457
1458 pub fn goal_exists(&self, executor: &Executor, goal_id: &nros_core::GoalId) -> bool {
1465 match executor.entries[self.entry_index].as_ref() {
1466 Some(meta) => {
1467 let arena_ptr = executor.arena.as_ptr() as *const u8;
1468 unsafe {
1469 let data_ptr = arena_ptr.add(meta.offset);
1470 (self.goal_exists_fn)(data_ptr, goal_id)
1471 }
1472 }
1473 None => false,
1474 }
1475 }
1476
1477 pub fn expire_goals(&self, executor: &mut Executor) -> usize {
1481 match executor.entries[self.entry_index].as_ref() {
1482 Some(meta) => {
1483 let arena_ptr = executor.arena.as_mut_ptr() as *mut u8;
1484 unsafe {
1485 let data_ptr = arena_ptr.add(meta.offset);
1486 (self.expire_goals_fn)(data_ptr)
1487 }
1488 }
1489 None => 0,
1490 }
1491 }
1492
1493 pub fn for_each_active_goal(&self, executor: &Executor, mut f: impl FnMut(&RawActiveGoal)) {
1497 if let Some(meta) = executor.entries[self.entry_index].as_ref() {
1498 let arena_ptr = executor.arena.as_ptr() as *const u8;
1499 unsafe {
1500 let data_ptr = arena_ptr.add(meta.offset);
1501 (self.for_each_active_goal_fn)(data_ptr, &mut f);
1502 }
1503 }
1504 }
1505
1506 pub fn goal_status(
1516 &self,
1517 executor: &Executor,
1518 goal_id: &nros_core::GoalId,
1519 ) -> Option<nros_core::GoalStatus> {
1520 let mut found = None;
1521 self.for_each_active_goal(executor, |g| {
1522 if g.goal_id.uuid == goal_id.uuid && found.is_none() {
1523 found = Some(g.status);
1524 }
1525 });
1526 found
1527 }
1528}
1529
1530impl<'s> Executor<'s> {
1535 #[allow(clippy::too_many_arguments)]
1550 pub fn register_action_client_raw(
1551 &mut self,
1552 spec: RawActionClientSpec<'_>,
1553 ) -> Result<ActionClientRawHandle, NodeError> {
1554 self.register_action_client_raw_sized::<
1555 { crate::config::DEFAULT_RX_BUF_SIZE },
1556 { crate::config::DEFAULT_RX_BUF_SIZE },
1557 { crate::config::DEFAULT_RX_BUF_SIZE },
1558 >(spec)
1559 }
1560
1561 pub fn register_action_client_raw_sized<
1567 const GOAL_BUF: usize,
1568 const RESULT_BUF: usize,
1569 const FEEDBACK_BUF: usize,
1570 >(
1571 &mut self,
1572 spec: RawActionClientSpec<'_>,
1573 ) -> Result<ActionClientRawHandle, NodeError> {
1574 let RawActionClientSpec {
1575 node_id,
1576 action_name,
1577 type_name,
1578 type_hash,
1579 hashes,
1580 goal_response_callback,
1581 feedback_callback,
1582 result_callback,
1583 context,
1584 } = spec;
1585
1586 type Entry<const GB: usize, const RB: usize, const FB: usize> =
1587 ActionClientRawArenaEntry<GB, RB, FB>;
1588
1589 let slot = self.next_entry_slot()?;
1590
1591 let action_info = ActionInfo::new(action_name, type_name, type_hash);
1592 let domain_id = self.domain_id;
1596 let (node_name, ns, session_idx) = match node_id {
1597 Some(id) => {
1598 let r = self
1599 .nodes
1600 .get(id.index())
1601 .ok_or(NodeError::InvalidSchedContextBinding)?;
1602 (r.name.clone(), r.namespace.clone(), r.session_idx)
1603 }
1604 None => (self.node_name.clone(), self.namespace.clone(), 0u8),
1605 };
1606
1607 let (send_goal_client, cancel_goal_client, get_result_client, feedback_sub) = {
1608 let send_goal_type: heapless::String<256> =
1613 super::action_core::action_channel_type(type_name, "SendGoal");
1614 let get_result_type: heapless::String<256> =
1615 super::action_core::action_channel_type(type_name, "GetResult");
1616 let feedback_type: heapless::String<256> =
1617 super::action_core::action_channel_type(type_name, "FeedbackMessage");
1618
1619 let send_goal_keyexpr: heapless::String<256> = action_info.send_goal_key();
1620 let mut send_goal_info =
1621 ServiceInfo::new(&send_goal_keyexpr, &send_goal_type, hashes.send_goal)
1622 .with_namespace(&ns)
1623 .with_domain(domain_id);
1624 if !node_name.is_empty() {
1625 send_goal_info = send_goal_info.with_node_name(&node_name);
1626 }
1627
1628 let cancel_goal_keyexpr: heapless::String<256> = action_info.cancel_goal_key();
1629 let mut cancel_goal_info = ServiceInfo::new(
1630 &cancel_goal_keyexpr,
1631 "action_msgs::srv::dds_::CancelGoal_",
1632 hashes.cancel_goal,
1633 )
1634 .with_namespace(&ns)
1635 .with_domain(domain_id);
1636 if !node_name.is_empty() {
1637 cancel_goal_info = cancel_goal_info.with_node_name(&node_name);
1638 }
1639
1640 let get_result_keyexpr: heapless::String<256> = action_info.get_result_key();
1641 let mut get_result_info =
1642 ServiceInfo::new(&get_result_keyexpr, &get_result_type, hashes.get_result)
1643 .with_namespace(&ns)
1644 .with_domain(domain_id);
1645 if !node_name.is_empty() {
1646 get_result_info = get_result_info.with_node_name(&node_name);
1647 }
1648
1649 let feedback_keyexpr: heapless::String<256> = action_info.feedback_key();
1650 let mut feedback_topic =
1651 TopicInfo::new(&feedback_keyexpr, &feedback_type, hashes.feedback)
1652 .with_namespace(&ns)
1653 .with_domain(domain_id);
1654 if !node_name.is_empty() {
1655 feedback_topic = feedback_topic.with_node_name(&node_name);
1656 }
1657
1658 let session = self
1659 .session_at_mut(session_idx)
1660 .ok_or(NodeError::BackendMismatch)?;
1661 (
1671 session
1672 .create_client(&send_goal_info, QoSProfile::services_default())
1673 .map_err(|e| {
1674 nros_log::log_error!(
1675 nros_log::get_logger("nros_node"),
1676 "action client: send_goal client failed: {:?}",
1677 e
1678 );
1679 NodeError::Transport(e)
1680 })?,
1681 session
1682 .create_client(&cancel_goal_info, QoSProfile::services_default())
1683 .map_err(|e| {
1684 nros_log::log_error!(
1685 nros_log::get_logger("nros_node"),
1686 "action client: cancel_goal client failed: {:?}",
1687 e
1688 );
1689 NodeError::Transport(e)
1690 })?,
1691 session
1692 .create_client(&get_result_info, QoSProfile::services_default())
1693 .map_err(|e| {
1694 nros_log::log_error!(
1695 nros_log::get_logger("nros_node"),
1696 "action client: get_result client failed: {:?}",
1697 e
1698 );
1699 NodeError::Transport(e)
1700 })?,
1701 session
1702 .create_subscription(&feedback_topic, QoSProfile::BEST_EFFORT)
1703 .map_err(|e| {
1704 nros_log::log_error!(
1705 nros_log::get_logger("nros_node"),
1706 "action client: feedback subscription failed: {:?}",
1707 e
1708 );
1709 NodeError::Transport(e)
1710 })?,
1711 )
1712 };
1713
1714 let core = ActionClientCore::new(
1715 send_goal_client,
1716 cancel_goal_client,
1717 get_result_client,
1718 feedback_sub,
1719 );
1720
1721 let offset = self.arena_alloc::<Entry<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>>()?;
1722
1723 unsafe {
1724 let arena_ptr = self.arena.as_mut_ptr() as *mut u8;
1725 let entry_ptr = arena_ptr.add(offset) as *mut Entry<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>;
1726 core::ptr::write(
1727 entry_ptr,
1728 Entry {
1729 core,
1730 goal_response_callback,
1731 feedback_callback,
1732 result_callback,
1733 context,
1734 },
1735 );
1736 }
1737
1738 let meta = CallbackMeta {
1739 offset,
1740 kind: EntryKind::ActionClient,
1741 arena_len: super::arena::ArenaLen::UNRECORDED,
1742 has_data: always_ready,
1743 pre_sample: no_pre_sample,
1744 invocation: InvocationMode::Always,
1745 try_process: action_client_raw_try_process::<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>,
1746 drop_fn: drop_entry::<Entry<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>>,
1747 };
1748 self.emplace_entry(slot, meta, TraceName::Text(spec.action_name));
1749 self.apply_node_default_sched(slot, node_id, None);
1750
1751 Ok(ActionClientRawHandle { entry_index: slot })
1752 }
1753
1754 #[allow(clippy::too_many_arguments, clippy::type_complexity)]
1761 pub(crate) fn register_action_client_callback<
1762 A,
1763 GRespF,
1764 FbF,
1765 ResF,
1766 const GOAL_BUF: usize,
1767 const RESULT_BUF: usize,
1768 const FEEDBACK_BUF: usize,
1769 >(
1770 &mut self,
1771 node_id: Option<super::node_record::NodeId>,
1772 action_name: &str,
1773 type_name: &str,
1774 type_hash: &str,
1775 feedback_depth: u16,
1776 on_goal_response: GRespF,
1777 on_feedback: FbF,
1778 on_result: ResF,
1779 ) -> Result<
1780 (
1781 HandleId,
1782 *mut ActionClientCore<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>,
1783 ),
1784 NodeError,
1785 >
1786 where
1787 A: nros_core::RosAction + 'static,
1788 GRespF: FnMut(&nros_core::GoalId, bool) + 'static,
1789 FbF: FnMut(&nros_core::GoalId, &A::Feedback) + 'static,
1790 ResF: FnMut(&nros_core::GoalId, nros_core::GoalStatus, &A::Result) + 'static,
1791 {
1792 type Entry<A, G, Fb, R, const GB: usize, const RB: usize, const FB: usize> =
1793 ActionClientCallbackEntry<A, G, Fb, R, GB, RB, FB>;
1794
1795 let slot = self.next_entry_slot()?;
1796 let action_info = ActionInfo::new(action_name, type_name, type_hash);
1797 let hashes = ActionEndpointHashes::of::<A>();
1799 A::register_protocol_types().map_err(|()| NodeError::ActionCreationFailed)?;
1805 let domain_id = self.domain_id;
1809 let (node_name, ns, session_idx) = match node_id {
1810 Some(id) => {
1811 let r = self
1812 .nodes
1813 .get(id.index())
1814 .ok_or(NodeError::InvalidSchedContextBinding)?;
1815 (r.name.clone(), r.namespace.clone(), r.session_idx)
1816 }
1817 None => (self.node_name.clone(), self.namespace.clone(), 0u8),
1818 };
1819
1820 let (send_goal_client, cancel_goal_client, get_result_client, feedback_sub) = {
1821 let send_goal_type: heapless::String<256> =
1826 super::action_core::action_channel_type(type_name, "SendGoal");
1827 let get_result_type: heapless::String<256> =
1828 super::action_core::action_channel_type(type_name, "GetResult");
1829 let feedback_type: heapless::String<256> =
1830 super::action_core::action_channel_type(type_name, "FeedbackMessage");
1831
1832 let send_goal_keyexpr: heapless::String<256> = action_info.send_goal_key();
1833 let mut send_goal_info =
1834 ServiceInfo::new(&send_goal_keyexpr, &send_goal_type, hashes.send_goal)
1835 .with_namespace(&ns)
1836 .with_domain(domain_id);
1837 if !node_name.is_empty() {
1838 send_goal_info = send_goal_info.with_node_name(&node_name);
1839 }
1840 let cancel_goal_keyexpr: heapless::String<256> = action_info.cancel_goal_key();
1841 let mut cancel_goal_info = ServiceInfo::new(
1842 &cancel_goal_keyexpr,
1843 "action_msgs::srv::dds_::CancelGoal_",
1844 hashes.cancel_goal,
1845 )
1846 .with_namespace(&ns)
1847 .with_domain(domain_id);
1848 if !node_name.is_empty() {
1849 cancel_goal_info = cancel_goal_info.with_node_name(&node_name);
1850 }
1851 let get_result_keyexpr: heapless::String<256> = action_info.get_result_key();
1852 let mut get_result_info =
1853 ServiceInfo::new(&get_result_keyexpr, &get_result_type, hashes.get_result)
1854 .with_namespace(&ns)
1855 .with_domain(domain_id);
1856 if !node_name.is_empty() {
1857 get_result_info = get_result_info.with_node_name(&node_name);
1858 }
1859 let feedback_keyexpr: heapless::String<256> = action_info.feedback_key();
1860 let mut feedback_topic =
1861 TopicInfo::new(&feedback_keyexpr, &feedback_type, hashes.feedback)
1862 .with_namespace(&ns)
1863 .with_domain(domain_id);
1864 if !node_name.is_empty() {
1865 feedback_topic = feedback_topic.with_node_name(&node_name);
1866 }
1867 let session = self
1868 .session_at_mut(session_idx)
1869 .ok_or(NodeError::BackendMismatch)?;
1870 (
1871 session
1872 .create_client(&send_goal_info, QoSProfile::services_default())
1873 .map_err(NodeError::Transport)?,
1874 session
1875 .create_client(&cancel_goal_info, QoSProfile::services_default())
1876 .map_err(NodeError::Transport)?,
1877 session
1878 .create_client(&get_result_info, QoSProfile::services_default())
1879 .map_err(NodeError::Transport)?,
1880 session
1881 .create_subscription(&feedback_topic, QoSProfile::BEST_EFFORT)
1882 .map_err(NodeError::Transport)?,
1883 )
1884 };
1885
1886 let core = ActionClientCore::new(
1887 send_goal_client,
1888 cancel_goal_client,
1889 get_result_client,
1890 feedback_sub,
1891 );
1892
1893 let (_slot_count, trailing_bytes) =
1897 buffered_region_size(feedback_depth as u32, FEEDBACK_BUF);
1898 let (offset, trailing_offset) = self.arena_alloc_with_trailing::<Entry<
1899 A,
1900 GRespF,
1901 FbF,
1902 ResF,
1903 GOAL_BUF,
1904 RESULT_BUF,
1905 FEEDBACK_BUF,
1906 >>(trailing_bytes)?;
1907 let buf_ptr = unsafe { (self.arena.as_mut_ptr() as *mut u8).add(trailing_offset) };
1908 let feedback_buffer = if feedback_depth <= 1 {
1909 BufferStrategy::Triple(unsafe { TripleBuffer::init(buf_ptr, FEEDBACK_BUF) })
1910 } else {
1911 BufferStrategy::Ring(unsafe {
1912 SpscRing::init(buf_ptr, FEEDBACK_BUF, feedback_depth as usize)
1913 })
1914 };
1915 let core_ptr = unsafe {
1916 let arena_ptr = self.arena.as_mut_ptr() as *mut u8;
1917 let entry_ptr = arena_ptr.add(offset)
1918 as *mut Entry<A, GRespF, FbF, ResF, GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>;
1919 core::ptr::write(
1920 entry_ptr,
1921 ActionClientCallbackEntry {
1922 core,
1923 feedback_buffer,
1924 on_goal_response,
1925 on_feedback,
1926 on_result,
1927 _phantom: core::marker::PhantomData,
1928 },
1929 );
1930 &mut (*entry_ptr).core as *mut ActionClientCore<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>
1931 };
1932
1933 let meta = CallbackMeta {
1934 offset,
1935 kind: EntryKind::ActionClient,
1936 arena_len: super::arena::ArenaLen::UNRECORDED,
1937 has_data: always_ready,
1938 pre_sample: no_pre_sample,
1939 invocation: InvocationMode::Always,
1940 try_process: action_client_callback_try_process::<
1941 A,
1942 GRespF,
1943 FbF,
1944 ResF,
1945 GOAL_BUF,
1946 RESULT_BUF,
1947 FEEDBACK_BUF,
1948 >,
1949 drop_fn: drop_entry::<Entry<A, GRespF, FbF, ResF, GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>>,
1950 };
1951 self.emplace_entry(slot, meta, TraceName::Text(action_name));
1952 self.apply_node_default_sched(slot, node_id, None);
1953 Ok((self.handle_for(slot), core_ptr))
1954 }
1955}
1956
1957impl<'s> Executor<'s> {
1958 pub fn register_action_client_core<
1964 const GOAL_BUF: usize,
1965 const RESULT_BUF: usize,
1966 const FEEDBACK_BUF: usize,
1967 >(
1968 &mut self,
1969 core: ActionClientCore<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>,
1970 goal_response_callback: Option<RawGoalResponseCallback>,
1971 feedback_callback: Option<RawFeedbackCallback>,
1972 result_callback: Option<RawResultCallback>,
1973 context: *mut core::ffi::c_void,
1974 ) -> Result<ActionClientRawHandle, NodeError> {
1975 type Entry<const GB: usize, const RB: usize, const FB: usize> =
1976 ActionClientRawArenaEntry<GB, RB, FB>;
1977
1978 let slot = self.next_entry_slot()?;
1979 let offset = self.arena_alloc::<Entry<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>>()?;
1980
1981 unsafe {
1982 let arena_ptr = self.arena.as_mut_ptr() as *mut u8;
1983 let entry_ptr = arena_ptr.add(offset) as *mut Entry<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>;
1984 core::ptr::write(
1985 entry_ptr,
1986 Entry {
1987 core,
1988 goal_response_callback,
1989 feedback_callback,
1990 result_callback,
1991 context,
1992 },
1993 );
1994 }
1995
1996 let meta = CallbackMeta {
1997 offset,
1998 kind: EntryKind::ActionClient,
1999 arena_len: super::arena::ArenaLen::UNRECORDED,
2000 has_data: always_ready,
2001 pre_sample: no_pre_sample,
2002 invocation: InvocationMode::Always,
2003 try_process: action_client_raw_try_process::<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>,
2004 drop_fn: drop_entry::<Entry<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>>,
2005 };
2006 self.emplace_entry(slot, meta, TraceName::Slot("action_client", slot));
2007
2008 Ok(ActionClientRawHandle { entry_index: slot })
2009 }
2010}
2011
2012pub struct ActionClientRawHandle {
2017 entry_index: usize,
2018}
2019
2020impl ActionClientRawHandle {
2021 pub fn entry_index(&self) -> usize {
2023 self.entry_index
2024 }
2025}
2026
2027#[cfg(all(test, feature = "alloc", not(feature = "rmw-cffi")))]
2034mod terminal_verb_tests {
2035 use super::*;
2036 use core::sync::atomic::{AtomicI8, Ordering};
2037 use nros_core::{CdrReader, CdrWriter, DeserError, Deserialize, SerError, Serialize};
2038 use nros_rmw::TransportError;
2039
2040 use crate::mock::MockSession;
2041
2042 static LAST_STATUS: AtomicI8 = AtomicI8::new(-1);
2046
2047 #[derive(Default)]
2048 struct Unit;
2049
2050 impl Serialize for Unit {
2051 fn serialize(&self, _w: &mut CdrWriter) -> Result<(), SerError> {
2052 Ok(())
2053 }
2054 }
2055 impl Deserialize for Unit {
2056 fn deserialize(_r: &mut CdrReader) -> Result<Self, DeserError> {
2057 Ok(Self)
2058 }
2059 }
2060 impl nros_core::RosMessage for Unit {
2061 const TYPE_NAME: &'static str = "test/action/Verbs_Unit";
2062 const TYPE_HASH: &'static str = "test_hash";
2063 }
2064
2065 struct VerbAction;
2066
2067 impl RosAction for VerbAction {
2068 type Goal = Unit;
2069 type Result = Unit;
2070 type Feedback = Unit;
2071 type SendGoalRequest = Unit;
2072 type SendGoalResponse = Unit;
2073 type GetResultRequest = Unit;
2074 type GetResultResponse = Unit;
2075 type FeedbackMessage = Unit;
2076 const ACTION_NAME: &'static str = "test/action/dds_/Verbs_";
2077 const ACTION_HASH: &'static str = "test_hash";
2078 }
2079
2080 unsafe fn record_status(
2081 _data: *mut u8,
2082 _goal_id: &nros_core::GoalId,
2083 status: nros_core::GoalStatus,
2084 _result: Unit,
2085 ) -> Result<(), NodeError> {
2086 LAST_STATUS.store(status as i8, Ordering::SeqCst);
2087 Ok(())
2088 }
2089
2090 unsafe fn unused_feedback(
2091 _data: *mut u8,
2092 _goal_id: &nros_core::GoalId,
2093 _fb: &Unit,
2094 ) -> Result<(), NodeError> {
2095 unreachable!("the terminal-verb test never publishes feedback")
2096 }
2097
2098 unsafe fn unused_set_status(
2099 _data: *mut u8,
2100 _goal_id: &nros_core::GoalId,
2101 _status: nros_core::GoalStatus,
2102 ) {
2103 unreachable!("the terminal-verb test never sets a non-terminal status")
2104 }
2105
2106 unsafe fn unused_count(_data: *const u8) -> usize {
2107 unreachable!("the terminal-verb test never counts active goals")
2108 }
2109
2110 unsafe fn unused_for_each(_data: *const u8, _f: &mut dyn FnMut(&ActiveGoal<VerbAction>)) {
2111 unreachable!("the terminal-verb test never iterates active goals")
2112 }
2113
2114 unsafe fn unused_goal_exists(_data: *const u8, _goal_id: &nros_core::GoalId) -> bool {
2115 unreachable!("the terminal-verb test never asks whether a goal exists")
2116 }
2117
2118 unsafe fn unused_expire_goals(_data: *mut u8) -> usize {
2119 unreachable!("the terminal-verb test never expires goals")
2120 }
2121
2122 unsafe fn never_processes(
2123 _data: *mut u8,
2124 _delta_us: u64,
2125 _slot: u8,
2126 ) -> Result<bool, TransportError> {
2127 Ok(false)
2128 }
2129
2130 unsafe fn drops_nothing(_data: *mut u8) {}
2131
2132 fn handle() -> ActionServerHandle<VerbAction> {
2133 ActionServerHandle {
2134 entry_index: 0,
2135 publish_feedback_fn: unused_feedback,
2136 complete_goal_fn: record_status,
2137 set_goal_status_fn: unused_set_status,
2138 active_goal_count_fn: unused_count,
2139 goal_exists_fn: unused_goal_exists,
2140 expire_goals_fn: unused_expire_goals,
2141 for_each_active_goal_fn: unused_for_each,
2142 _phantom: PhantomData,
2143 }
2144 }
2145
2146 fn meta() -> CallbackMeta {
2147 CallbackMeta {
2148 offset: 0,
2149 kind: EntryKind::ActionServer,
2150 arena_len: crate::executor::arena::ArenaLen::UNRECORDED,
2151 try_process: never_processes,
2152 has_data: always_ready,
2153 pre_sample: no_pre_sample,
2154 invocation: InvocationMode::OnNewData,
2155 drop_fn: drops_nothing,
2156 }
2157 }
2158
2159 #[test]
2168 fn terminal_verbs_carry_their_spec_state_and_cancel_forwards_to_canceled() {
2169 let mut executor = Executor::from_session(MockSession::new());
2170 executor.entries[0] = Some(meta());
2171
2172 let h = handle();
2173 let goal = nros_core::GoalId::zero();
2174
2175 LAST_STATUS.store(-1, Ordering::SeqCst);
2176 h.succeed(&mut executor, &goal, Unit).unwrap();
2177 assert_eq!(
2178 LAST_STATUS.load(Ordering::SeqCst),
2179 nros_core::GoalStatus::Succeeded as i8,
2180 "succeed() must terminate the goal as SUCCEEDED"
2181 );
2182
2183 LAST_STATUS.store(-1, Ordering::SeqCst);
2184 h.abort(&mut executor, &goal, Unit).unwrap();
2185 assert_eq!(
2186 LAST_STATUS.load(Ordering::SeqCst),
2187 nros_core::GoalStatus::Aborted as i8,
2188 "abort() must terminate the goal as ABORTED"
2189 );
2190
2191 LAST_STATUS.store(-1, Ordering::SeqCst);
2195 h.canceled(&mut executor, &goal, Unit).unwrap();
2196 assert_eq!(
2197 LAST_STATUS.load(Ordering::SeqCst),
2198 nros_core::GoalStatus::Canceled as i8,
2199 "canceled() must terminate the goal as CANCELED"
2200 );
2201
2202 executor.entries[0] = None;
2205 }
2206}