1use core::marker::PhantomData;
4
5use nros_core::RosAction;
6use nros_rmw::{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_for_each_active_goal, as_publish_feedback,
19 as_raw_active_goal_count, as_raw_complete_goal, as_raw_for_each_active_goal,
20 as_raw_publish_feedback, as_raw_set_goal_status, as_set_goal_status, buffered_region_size,
21 drop_entry, no_pre_sample,
22 },
23 handles::{ActionServer, ActiveGoal},
24 spin::Executor,
25 spsc_ring::SpscRing,
26 triple_buffer::TripleBuffer,
27 types::{
28 HandleId, InvocationMode, NodeError, RawAcceptedCallback, RawCancelCallback,
29 RawFeedbackCallback, RawGoalCallback, RawGoalResponseCallback, RawResultCallback,
30 },
31};
32
33pub const ACTION_SERVER_QUERYABLES: usize = 3;
47
48pub const ACTION_SERVER_PUBLISHERS: usize = 2;
58
59pub const ACTION_CLIENT_SUBSCRIPTIONS: usize = 1;
68
69pub struct RawActionServerSpec<'a> {
79 pub node_id: Option<super::node_record::NodeId>,
84 pub action_name: &'a str,
85 pub type_name: &'a str,
86 pub type_hash: &'a str,
87 pub qos: QoSProfile,
92 pub goal_callback: RawGoalCallback,
93 pub cancel_callback: RawCancelCallback,
94 pub accepted_callback: Option<RawAcceptedCallback>,
95 pub context: *mut core::ffi::c_void,
96}
97
98pub struct RawActionClientSpec<'a> {
104 pub node_id: Option<super::node_record::NodeId>,
109 pub action_name: &'a str,
110 pub type_name: &'a str,
111 pub type_hash: &'a str,
112 pub goal_response_callback: Option<RawGoalResponseCallback>,
113 pub feedback_callback: Option<RawFeedbackCallback>,
114 pub result_callback: Option<RawResultCallback>,
115 pub context: *mut core::ffi::c_void,
116}
117
118impl<'s> Executor<'s> {
123 pub fn register_action_server<A, GoalF, CancelF>(
134 &mut self,
135 action_name: &str,
136 goal_callback: GoalF,
137 cancel_callback: CancelF,
138 ) -> Result<ActionServerHandle<A>, NodeError>
139 where
140 A: RosAction + 'static,
141 A::Goal: Clone + MessageForRmw,
142 A::Result: Clone + Default + MessageForRmw,
143 A::Feedback: MessageForRmw,
144 A::SendGoalRequest: MessageForRmw,
145 A::SendGoalResponse: MessageForRmw,
146 A::GetResultRequest: MessageForRmw,
147 A::GetResultResponse: MessageForRmw,
148 A::FeedbackMessage: MessageForRmw,
149 GoalF: FnMut(&nros_core::GoalId, &A::Goal) -> nros_core::GoalResponse + 'static,
150 CancelF:
151 FnMut(&nros_core::GoalId, nros_core::GoalStatus) -> nros_core::CancelResponse + 'static,
152 {
153 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>(
154 action_name,
155 goal_callback,
156 cancel_callback,
157 )
158 }
159
160 pub fn register_action_server_sized<
162 A,
163 GoalF,
164 CancelF,
165 const GOAL_BUF: usize,
166 const RESULT_BUF: usize,
167 const FEEDBACK_BUF: usize,
168 const MAX_GOALS: usize,
169 >(
170 &mut self,
171 action_name: &str,
172 goal_callback: GoalF,
173 cancel_callback: CancelF,
174 ) -> Result<ActionServerHandle<A>, NodeError>
175 where
176 A: RosAction + 'static,
177 A::Goal: Clone + MessageForRmw,
178 A::Result: Clone + Default + MessageForRmw,
179 A::Feedback: MessageForRmw,
180 A::SendGoalRequest: MessageForRmw,
181 A::SendGoalResponse: MessageForRmw,
182 A::GetResultRequest: MessageForRmw,
183 A::GetResultResponse: MessageForRmw,
184 A::FeedbackMessage: MessageForRmw,
185 GoalF: FnMut(&nros_core::GoalId, &A::Goal) -> nros_core::GoalResponse + 'static,
186 CancelF:
187 FnMut(&nros_core::GoalId, nros_core::GoalStatus) -> nros_core::CancelResponse + 'static,
188 {
189 register_type::<A::Goal>()?;
196 register_type::<A::Result>()?;
197 register_type::<A::Feedback>()?;
198 register_type::<A::SendGoalRequest>()?;
199 register_type::<A::SendGoalResponse>()?;
200 register_type::<A::GetResultRequest>()?;
201 register_type::<A::GetResultResponse>()?;
202 register_type::<A::FeedbackMessage>()?;
203 A::register_protocol_types().map_err(|()| NodeError::ActionCreationFailed)?;
209 type Entry<
210 A,
211 GoalF,
212 CancelF,
213 const GB: usize,
214 const RB: usize,
215 const FB: usize,
216 const MG: usize,
217 > = ActionServerArenaEntry<A, GoalF, CancelF, GB, RB, FB, MG>;
218
219 let slot = self.next_entry_slot()?;
220
221 let action_info = ActionInfo::new(action_name, A::ACTION_NAME, A::ACTION_HASH);
223
224 let send_goal_type = super::action_core::action_service_base_type(
229 <A::SendGoalRequest as nros_core::RosMessage>::TYPE_NAME,
230 A::ACTION_NAME,
231 );
232 let get_result_type = super::action_core::action_service_base_type(
233 <A::GetResultRequest as nros_core::RosMessage>::TYPE_NAME,
234 A::ACTION_NAME,
235 );
236 let feedback_type = <A::FeedbackMessage as nros_core::RosMessage>::TYPE_NAME;
237
238 let send_goal_keyexpr: heapless::String<256> = action_info.send_goal_key();
239 let send_goal_info = ServiceInfo::new(
240 &send_goal_keyexpr,
241 send_goal_type,
242 A::SEND_GOAL_SERVICE_HASH,
243 )
244 .with_domain(self.domain_id);
245 let send_goal_server = self
246 .session
247 .create_service(&send_goal_info, QoSProfile::services_default())
248 .map_err(NodeError::Transport)?;
249
250 let cancel_goal_keyexpr: heapless::String<256> = action_info.cancel_goal_key();
251 let cancel_goal_info = ServiceInfo::new(
252 &cancel_goal_keyexpr,
253 "action_msgs::srv::dds_::CancelGoal_",
254 A::ACTION_HASH,
255 )
256 .with_domain(self.domain_id);
257 let cancel_goal_server = self
258 .session
259 .create_service(&cancel_goal_info, QoSProfile::services_default())
260 .map_err(NodeError::Transport)?;
261
262 let get_result_keyexpr: heapless::String<256> = action_info.get_result_key();
263 let get_result_info = ServiceInfo::new(
264 &get_result_keyexpr,
265 get_result_type,
266 A::GET_RESULT_SERVICE_HASH,
267 )
268 .with_domain(self.domain_id);
269 let get_result_server = self
270 .session
271 .create_service(&get_result_info, QoSProfile::services_default())
272 .map_err(NodeError::Transport)?;
273
274 let feedback_keyexpr: heapless::String<256> = action_info.feedback_key();
275 let feedback_topic = TopicInfo::new(
276 &feedback_keyexpr,
277 feedback_type,
278 <A::FeedbackMessage as nros_core::RosMessage>::TYPE_HASH,
279 )
280 .with_domain(self.domain_id);
281 let feedback_publisher = self
282 .session
283 .create_publisher(&feedback_topic, QoSProfile::QOS_PROFILE_DEFAULT)
284 .map_err(NodeError::Transport)?;
285
286 let status_keyexpr: heapless::String<256> = action_info.status_key();
287 let status_topic = TopicInfo::new(
288 &status_keyexpr,
289 "action_msgs::msg::dds_::GoalStatusArray_",
290 A::ACTION_HASH,
291 )
292 .with_domain(self.domain_id);
293 let status_publisher = self
294 .session
295 .create_publisher(&status_topic, QoSProfile::QOS_PROFILE_ACTION_STATUS_DEFAULT)
296 .map_err(NodeError::Transport)?;
297
298 let server = ActionServer {
299 core: super::action_core::ActionServerCore {
300 send_goal_server,
301 cancel_goal_server,
302 get_result_server,
303 feedback_publisher,
304 status_publisher,
305 active_goals: heapless::Vec::new(),
306 completed_results: heapless::Vec::new(),
307 pending_get_results: heapless::Vec::new(),
308 result_slab: [0u8; RESULT_BUF],
309 result_slab_used: 0,
310 goal_buffer: [0u8; GOAL_BUF],
311 feedback_buffer: [0u8; FEEDBACK_BUF],
312 cancel_buffer: [0u8; 256],
313 },
314 typed_goals: heapless::Vec::new(),
315 completed_goals: heapless::Vec::new(),
316 };
317
318 let offset = self
319 .arena_alloc::<Entry<A, GoalF, CancelF, GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>>(
320 )?;
321
322 unsafe {
323 let arena_ptr = self.arena.as_mut_ptr() as *mut u8;
324 let entry_ptr = arena_ptr.add(offset)
325 as *mut Entry<A, GoalF, CancelF, GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>;
326 core::ptr::write(
327 entry_ptr,
328 Entry {
329 server,
330 goal_callback,
331 cancel_callback,
332 },
333 );
334 }
335
336 let meta = CallbackMeta {
337 offset,
338 kind: EntryKind::ActionServer,
339 has_data: always_ready,
340 pre_sample: no_pre_sample,
341 invocation: InvocationMode::Always,
342 try_process: action_server_try_process::<
343 A,
344 GoalF,
345 CancelF,
346 GOAL_BUF,
347 RESULT_BUF,
348 FEEDBACK_BUF,
349 MAX_GOALS,
350 >,
351 drop_fn: drop_entry::<
352 Entry<A, GoalF, CancelF, GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>,
353 >,
354 };
355 self.emplace_entry(slot, meta, TraceName::Text(action_name));
356
357 Ok(ActionServerHandle {
358 entry_index: slot,
359 publish_feedback_fn: as_publish_feedback::<
360 A,
361 GoalF,
362 CancelF,
363 GOAL_BUF,
364 RESULT_BUF,
365 FEEDBACK_BUF,
366 MAX_GOALS,
367 >,
368 complete_goal_fn: as_complete_goal::<
369 A,
370 GoalF,
371 CancelF,
372 GOAL_BUF,
373 RESULT_BUF,
374 FEEDBACK_BUF,
375 MAX_GOALS,
376 >,
377 set_goal_status_fn: as_set_goal_status::<
378 A,
379 GoalF,
380 CancelF,
381 GOAL_BUF,
382 RESULT_BUF,
383 FEEDBACK_BUF,
384 MAX_GOALS,
385 >,
386 active_goal_count_fn: as_active_goal_count::<
387 A,
388 GoalF,
389 CancelF,
390 GOAL_BUF,
391 RESULT_BUF,
392 FEEDBACK_BUF,
393 MAX_GOALS,
394 >,
395 for_each_active_goal_fn: as_for_each_active_goal::<
396 A,
397 GoalF,
398 CancelF,
399 GOAL_BUF,
400 RESULT_BUF,
401 FEEDBACK_BUF,
402 MAX_GOALS,
403 >,
404 _phantom: PhantomData,
405 })
406 }
407}
408
409#[allow(clippy::type_complexity)]
420pub struct ActionServerHandle<A: RosAction> {
421 pub(crate) entry_index: usize,
422 publish_feedback_fn:
423 unsafe fn(*mut u8, &nros_core::GoalId, &A::Feedback) -> Result<(), NodeError>,
424 complete_goal_fn: unsafe fn(
425 *mut u8,
426 &nros_core::GoalId,
427 nros_core::GoalStatus,
428 A::Result,
429 ) -> Result<(), NodeError>,
430 set_goal_status_fn: unsafe fn(*mut u8, &nros_core::GoalId, nros_core::GoalStatus),
431 active_goal_count_fn: unsafe fn(*const u8) -> usize,
432 for_each_active_goal_fn: unsafe fn(*const u8, &mut dyn FnMut(&ActiveGoal<A>)),
433 _phantom: PhantomData<A>,
434}
435
436impl<A: RosAction> Clone for ActionServerHandle<A> {
437 fn clone(&self) -> Self {
438 *self
439 }
440}
441
442impl<A: RosAction> Copy for ActionServerHandle<A> {}
443
444impl<A: RosAction> ActionServerHandle<A> {
445 pub fn handle_id(&self) -> HandleId {
449 HandleId(self.entry_index)
450 }
451
452 pub fn publish_feedback(
458 &self,
459 executor: &mut Executor,
460 goal_id: &nros_core::GoalId,
461 feedback: &A::Feedback,
462 ) -> Result<(), NodeError> {
463 let meta = executor.entries[self.entry_index]
464 .as_ref()
465 .ok_or(NodeError::BufferTooSmall)?;
466 let arena_ptr = executor.arena.as_mut_ptr() as *mut u8;
467 unsafe {
468 let data_ptr = arena_ptr.add(meta.offset);
469 (self.publish_feedback_fn)(data_ptr, goal_id, feedback)
470 }
471 }
472
473 pub fn succeed(
498 &self,
499 executor: &mut Executor,
500 goal_id: &nros_core::GoalId,
501 result: A::Result,
502 ) -> Result<(), NodeError> {
503 self.complete_goal(executor, goal_id, nros_core::GoalStatus::Succeeded, result)
504 }
505
506 pub fn abort(
508 &self,
509 executor: &mut Executor,
510 goal_id: &nros_core::GoalId,
511 result: A::Result,
512 ) -> Result<(), NodeError> {
513 self.complete_goal(executor, goal_id, nros_core::GoalStatus::Aborted, result)
514 }
515
516 pub fn cancel(
523 &self,
524 executor: &mut Executor,
525 goal_id: &nros_core::GoalId,
526 result: A::Result,
527 ) -> Result<(), NodeError> {
528 self.complete_goal(executor, goal_id, nros_core::GoalStatus::Canceled, result)
529 }
530
531 pub fn complete_goal(
532 &self,
533 executor: &mut Executor,
534 goal_id: &nros_core::GoalId,
535 status: nros_core::GoalStatus,
536 result: A::Result,
537 ) -> Result<(), NodeError> {
538 let meta = executor.entries[self.entry_index]
539 .as_ref()
540 .ok_or(NodeError::BufferTooSmall)?;
541 let arena_ptr = executor.arena.as_mut_ptr() as *mut u8;
542 unsafe {
543 let data_ptr = arena_ptr.add(meta.offset);
544 (self.complete_goal_fn)(data_ptr, goal_id, status, result)
545 }
546 }
547
548 pub fn set_goal_status(
553 &self,
554 executor: &mut Executor,
555 goal_id: &nros_core::GoalId,
556 status: nros_core::GoalStatus,
557 ) {
558 if let Some(meta) = executor.entries[self.entry_index].as_ref() {
559 let arena_ptr = executor.arena.as_mut_ptr() as *mut u8;
560 unsafe {
561 let data_ptr = arena_ptr.add(meta.offset);
562 (self.set_goal_status_fn)(data_ptr, goal_id, status);
563 }
564 }
565 }
566
567 pub fn active_goal_count(&self, executor: &Executor) -> usize {
571 match executor.entries[self.entry_index].as_ref() {
572 Some(meta) => {
573 let arena_ptr = executor.arena.as_ptr() as *const u8;
574 unsafe {
575 let data_ptr = arena_ptr.add(meta.offset);
576 (self.active_goal_count_fn)(data_ptr)
577 }
578 }
579 None => 0,
580 }
581 }
582
583 pub fn for_each_active_goal(&self, executor: &Executor, mut f: impl FnMut(&ActiveGoal<A>)) {
588 if let Some(meta) = executor.entries[self.entry_index].as_ref() {
589 let arena_ptr = executor.arena.as_ptr() as *const u8;
590 unsafe {
591 let data_ptr = arena_ptr.add(meta.offset);
592 (self.for_each_active_goal_fn)(data_ptr, &mut f);
593 }
594 }
595 }
596}
597
598impl<'s> Executor<'s> {
603 #[allow(clippy::too_many_arguments)]
612 pub fn register_action_server_raw(
613 &mut self,
614 spec: RawActionServerSpec<'_>,
615 ) -> Result<ActionServerRawHandle, NodeError> {
616 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>(
617 spec,
618 )
619 }
620
621 pub fn register_action_server_raw_sized<
630 const GOAL_BUF: usize,
631 const RESULT_BUF: usize,
632 const FEEDBACK_BUF: usize,
633 const MAX_GOALS: usize,
634 >(
635 &mut self,
636 spec: RawActionServerSpec<'_>,
637 ) -> Result<ActionServerRawHandle, NodeError> {
638 let RawActionServerSpec {
639 node_id,
640 action_name,
641 type_name,
642 type_hash,
643 qos,
644 goal_callback,
645 cancel_callback,
646 accepted_callback,
647 context,
648 } = spec;
649
650 type Entry<const GB: usize, const RB: usize, const FB: usize, const MG: usize> =
651 ActionServerRawArenaEntry<GB, RB, FB, MG>;
652
653 let slot = self.next_entry_slot()?;
654
655 let action_info = ActionInfo::new(action_name, type_name, type_hash);
656 let domain_id = self.domain_id;
660 let (node_name, ns, session_idx) = match node_id {
661 Some(id) => {
662 let r = self
663 .nodes
664 .get(id.index())
665 .ok_or(NodeError::InvalidSchedContextBinding)?;
666 (r.name.clone(), r.namespace.clone(), r.session_idx)
667 }
668 None => (self.node_name.clone(), self.namespace.clone(), 0u8),
669 };
670
671 let (
680 send_goal_server,
681 cancel_goal_server,
682 get_result_server,
683 feedback_publisher,
684 status_publisher,
685 ) = {
686 let send_goal_type: heapless::String<256> =
692 super::action_core::action_channel_type(type_name, "SendGoal");
693 let get_result_type: heapless::String<256> =
694 super::action_core::action_channel_type(type_name, "GetResult");
695 let feedback_type: heapless::String<256> =
696 super::action_core::action_channel_type(type_name, "FeedbackMessage");
697
698 let send_goal_keyexpr: heapless::String<256> = action_info.send_goal_key();
699 let mut send_goal_info =
700 ServiceInfo::new(&send_goal_keyexpr, &send_goal_type, type_hash)
701 .with_namespace(&ns)
702 .with_domain(domain_id);
703 if !node_name.is_empty() {
704 send_goal_info = send_goal_info.with_node_name(&node_name);
705 }
706
707 let cancel_goal_keyexpr: heapless::String<256> = action_info.cancel_goal_key();
708 let mut cancel_goal_info = ServiceInfo::new(
709 &cancel_goal_keyexpr,
710 "action_msgs::srv::dds_::CancelGoal_",
711 type_hash,
712 )
713 .with_namespace(&ns)
714 .with_domain(domain_id);
715 if !node_name.is_empty() {
716 cancel_goal_info = cancel_goal_info.with_node_name(&node_name);
717 }
718
719 let get_result_keyexpr: heapless::String<256> = action_info.get_result_key();
720 let mut get_result_info =
721 ServiceInfo::new(&get_result_keyexpr, &get_result_type, type_hash)
722 .with_namespace(&ns)
723 .with_domain(domain_id);
724 if !node_name.is_empty() {
725 get_result_info = get_result_info.with_node_name(&node_name);
726 }
727
728 let feedback_keyexpr: heapless::String<256> = action_info.feedback_key();
729 let mut feedback_topic = TopicInfo::new(&feedback_keyexpr, &feedback_type, type_hash)
730 .with_namespace(&ns)
731 .with_domain(domain_id);
732 if !node_name.is_empty() {
733 feedback_topic = feedback_topic.with_node_name(&node_name);
734 }
735
736 let status_keyexpr: heapless::String<256> = action_info.status_key();
737 let mut status_topic = TopicInfo::new(
738 &status_keyexpr,
739 "action_msgs::msg::dds_::GoalStatusArray_",
740 type_hash,
741 )
742 .with_namespace(&ns)
743 .with_domain(domain_id);
744 if !node_name.is_empty() {
745 status_topic = status_topic.with_node_name(&node_name);
746 }
747
748 let session = self
749 .session_at_mut(session_idx)
750 .ok_or(NodeError::BackendMismatch)?;
751 (
752 session
753 .create_service(&send_goal_info, qos)
754 .map_err(NodeError::Transport)?,
755 session
756 .create_service(&cancel_goal_info, qos)
757 .map_err(NodeError::Transport)?,
758 session
759 .create_service(&get_result_info, qos)
760 .map_err(NodeError::Transport)?,
761 session
762 .create_publisher(&feedback_topic, QoSProfile::QOS_PROFILE_DEFAULT)
763 .map_err(NodeError::Transport)?,
764 session
765 .create_publisher(&status_topic, QoSProfile::QOS_PROFILE_ACTION_STATUS_DEFAULT)
766 .map_err(NodeError::Transport)?,
767 )
768 };
769
770 let core = ActionServerCore {
771 send_goal_server,
772 cancel_goal_server,
773 get_result_server,
774 feedback_publisher,
775 status_publisher,
776 active_goals: heapless::Vec::new(),
777 completed_results: heapless::Vec::new(),
778 pending_get_results: heapless::Vec::new(),
779 result_slab: [0u8; RESULT_BUF],
780 result_slab_used: 0,
781 goal_buffer: [0u8; GOAL_BUF],
782 feedback_buffer: [0u8; FEEDBACK_BUF],
783 cancel_buffer: [0u8; 256],
784 };
785
786 let offset = self.arena_alloc::<Entry<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>>()?;
787
788 unsafe {
789 let arena_ptr = self.arena.as_mut_ptr() as *mut u8;
790 let entry_ptr =
791 arena_ptr.add(offset) as *mut Entry<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>;
792 core::ptr::write(
793 entry_ptr,
794 Entry {
795 core,
796 goal_callback,
797 cancel_callback,
798 accepted_callback,
799 context,
800 },
801 );
802 }
803
804 let meta = CallbackMeta {
805 offset,
806 kind: EntryKind::ActionServer,
807 has_data: always_ready,
808 pre_sample: no_pre_sample,
809 invocation: InvocationMode::Always,
810 try_process: action_server_raw_try_process::<
811 GOAL_BUF,
812 RESULT_BUF,
813 FEEDBACK_BUF,
814 MAX_GOALS,
815 >,
816 drop_fn: drop_entry::<Entry<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>>,
817 };
818 self.emplace_entry(slot, meta, TraceName::Text(spec.action_name));
819 self.apply_node_default_sched(slot, node_id, None);
820
821 Ok(ActionServerRawHandle {
822 entry_index: slot,
823 publish_feedback_fn: as_raw_publish_feedback::<
824 GOAL_BUF,
825 RESULT_BUF,
826 FEEDBACK_BUF,
827 MAX_GOALS,
828 >,
829 complete_goal_fn: as_raw_complete_goal::<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS>,
830 set_goal_status_fn: as_raw_set_goal_status::<
831 GOAL_BUF,
832 RESULT_BUF,
833 FEEDBACK_BUF,
834 MAX_GOALS,
835 >,
836 active_goal_count_fn: as_raw_active_goal_count::<
837 GOAL_BUF,
838 RESULT_BUF,
839 FEEDBACK_BUF,
840 MAX_GOALS,
841 >,
842 for_each_active_goal_fn: as_raw_for_each_active_goal::<
843 GOAL_BUF,
844 RESULT_BUF,
845 FEEDBACK_BUF,
846 MAX_GOALS,
847 >,
848 })
849 }
850}
851
852#[repr(C)]
861#[allow(clippy::type_complexity)]
862pub struct ActionServerRawHandle {
863 pub(crate) entry_index: usize,
864 publish_feedback_fn:
865 unsafe fn(*mut u8, &nros_core::GoalId, *const u8, usize) -> Result<(), NodeError>,
866 complete_goal_fn: unsafe fn(
867 *mut u8,
868 &nros_core::GoalId,
869 nros_core::GoalStatus,
870 *const u8,
871 usize,
872 ) -> Result<(), NodeError>,
873 set_goal_status_fn: unsafe fn(*mut u8, &nros_core::GoalId, nros_core::GoalStatus),
874 active_goal_count_fn: unsafe fn(*const u8) -> usize,
875 for_each_active_goal_fn: unsafe fn(*const u8, &mut dyn FnMut(&RawActiveGoal)),
876}
877
878impl Clone for ActionServerRawHandle {
879 fn clone(&self) -> Self {
880 *self
881 }
882}
883
884impl Copy for ActionServerRawHandle {}
885
886pub const INVALID_ENTRY_INDEX: usize = usize::MAX;
894
895impl ActionServerRawHandle {
896 pub const fn invalid() -> Self {
902 unsafe fn unreachable_publish_feedback(
903 _: *mut u8,
904 _: &nros_core::GoalId,
905 _: *const u8,
906 _: usize,
907 ) -> Result<(), NodeError> {
908 unreachable!("ActionServerRawHandle::publish_feedback called on invalid handle")
909 }
910 unsafe fn unreachable_complete_goal(
911 _: *mut u8,
912 _: &nros_core::GoalId,
913 _: nros_core::GoalStatus,
914 _: *const u8,
915 _: usize,
916 ) -> Result<(), NodeError> {
917 unreachable!("ActionServerRawHandle::complete_goal called on invalid handle")
918 }
919 unsafe fn unreachable_set_goal_status(
920 _: *mut u8,
921 _: &nros_core::GoalId,
922 _: nros_core::GoalStatus,
923 ) {
924 unreachable!("ActionServerRawHandle::set_goal_status called on invalid handle")
925 }
926 unsafe fn unreachable_active_goal_count(_: *const u8) -> usize {
927 unreachable!("ActionServerRawHandle::active_goal_count called on invalid handle")
928 }
929 unsafe fn unreachable_for_each_active_goal(
930 _: *const u8,
931 _: &mut dyn FnMut(&RawActiveGoal),
932 ) {
933 unreachable!("ActionServerRawHandle::for_each_active_goal called on invalid handle")
934 }
935 Self {
936 entry_index: INVALID_ENTRY_INDEX,
937 publish_feedback_fn: unreachable_publish_feedback,
938 complete_goal_fn: unreachable_complete_goal,
939 set_goal_status_fn: unreachable_set_goal_status,
940 active_goal_count_fn: unreachable_active_goal_count,
941 for_each_active_goal_fn: unreachable_for_each_active_goal,
942 }
943 }
944
945 pub const fn is_invalid(&self) -> bool {
947 self.entry_index == INVALID_ENTRY_INDEX
948 }
949}
950
951impl Default for ActionServerRawHandle {
952 fn default() -> Self {
953 Self::invalid()
954 }
955}
956
957impl ActionServerRawHandle {
958 pub fn handle_id(&self) -> HandleId {
960 HandleId(self.entry_index)
961 }
962
963 pub fn publish_feedback_raw(
967 &self,
968 executor: &mut Executor,
969 goal_id: &nros_core::GoalId,
970 feedback_data: &[u8],
971 ) -> Result<(), NodeError> {
972 let meta = executor.entries[self.entry_index]
973 .as_ref()
974 .ok_or(NodeError::BufferTooSmall)?;
975 let arena_ptr = executor.arena.as_mut_ptr() as *mut u8;
976 unsafe {
977 let data_ptr = arena_ptr.add(meta.offset);
978 (self.publish_feedback_fn)(
979 data_ptr,
980 goal_id,
981 feedback_data.as_ptr(),
982 feedback_data.len(),
983 )
984 }
985 }
986
987 pub fn complete_goal_raw(
998 &self,
999 executor: &mut Executor,
1000 goal_id: &nros_core::GoalId,
1001 status: nros_core::GoalStatus,
1002 result_data: &[u8],
1003 ) -> Result<(), NodeError> {
1004 let meta = executor.entries[self.entry_index]
1005 .as_ref()
1006 .ok_or(NodeError::BufferTooSmall)?;
1007 let arena_ptr = executor.arena.as_mut_ptr() as *mut u8;
1008 unsafe {
1009 let data_ptr = arena_ptr.add(meta.offset);
1010 (self.complete_goal_fn)(
1011 data_ptr,
1012 goal_id,
1013 status,
1014 result_data.as_ptr(),
1015 result_data.len(),
1016 )
1017 }
1018 }
1019
1020 pub fn set_goal_status(
1025 &self,
1026 executor: &mut Executor,
1027 goal_id: &nros_core::GoalId,
1028 status: nros_core::GoalStatus,
1029 ) {
1030 if let Some(meta) = executor.entries[self.entry_index].as_ref() {
1031 let arena_ptr = executor.arena.as_mut_ptr() as *mut u8;
1032 unsafe {
1033 let data_ptr = arena_ptr.add(meta.offset);
1034 (self.set_goal_status_fn)(data_ptr, goal_id, status);
1035 }
1036 }
1037 }
1038
1039 pub fn active_goal_count(&self, executor: &Executor) -> usize {
1043 match executor.entries[self.entry_index].as_ref() {
1044 Some(meta) => {
1045 let arena_ptr = executor.arena.as_ptr() as *const u8;
1046 unsafe {
1047 let data_ptr = arena_ptr.add(meta.offset);
1048 (self.active_goal_count_fn)(data_ptr)
1049 }
1050 }
1051 None => 0,
1052 }
1053 }
1054
1055 pub fn for_each_active_goal(&self, executor: &Executor, mut f: impl FnMut(&RawActiveGoal)) {
1059 if let Some(meta) = executor.entries[self.entry_index].as_ref() {
1060 let arena_ptr = executor.arena.as_ptr() as *const u8;
1061 unsafe {
1062 let data_ptr = arena_ptr.add(meta.offset);
1063 (self.for_each_active_goal_fn)(data_ptr, &mut f);
1064 }
1065 }
1066 }
1067
1068 pub fn goal_status(
1078 &self,
1079 executor: &Executor,
1080 goal_id: &nros_core::GoalId,
1081 ) -> Option<nros_core::GoalStatus> {
1082 let mut found = None;
1083 self.for_each_active_goal(executor, |g| {
1084 if g.goal_id.uuid == goal_id.uuid && found.is_none() {
1085 found = Some(g.status);
1086 }
1087 });
1088 found
1089 }
1090}
1091
1092impl<'s> Executor<'s> {
1097 #[allow(clippy::too_many_arguments)]
1112 pub fn register_action_client_raw(
1113 &mut self,
1114 spec: RawActionClientSpec<'_>,
1115 ) -> Result<ActionClientRawHandle, NodeError> {
1116 self.register_action_client_raw_sized::<
1117 { crate::config::DEFAULT_RX_BUF_SIZE },
1118 { crate::config::DEFAULT_RX_BUF_SIZE },
1119 { crate::config::DEFAULT_RX_BUF_SIZE },
1120 >(spec)
1121 }
1122
1123 pub fn register_action_client_raw_sized<
1129 const GOAL_BUF: usize,
1130 const RESULT_BUF: usize,
1131 const FEEDBACK_BUF: usize,
1132 >(
1133 &mut self,
1134 spec: RawActionClientSpec<'_>,
1135 ) -> Result<ActionClientRawHandle, NodeError> {
1136 let RawActionClientSpec {
1137 node_id,
1138 action_name,
1139 type_name,
1140 type_hash,
1141 goal_response_callback,
1142 feedback_callback,
1143 result_callback,
1144 context,
1145 } = spec;
1146
1147 type Entry<const GB: usize, const RB: usize, const FB: usize> =
1148 ActionClientRawArenaEntry<GB, RB, FB>;
1149
1150 let slot = self.next_entry_slot()?;
1151
1152 let action_info = ActionInfo::new(action_name, type_name, type_hash);
1153 let domain_id = self.domain_id;
1157 let (node_name, ns, session_idx) = match node_id {
1158 Some(id) => {
1159 let r = self
1160 .nodes
1161 .get(id.index())
1162 .ok_or(NodeError::InvalidSchedContextBinding)?;
1163 (r.name.clone(), r.namespace.clone(), r.session_idx)
1164 }
1165 None => (self.node_name.clone(), self.namespace.clone(), 0u8),
1166 };
1167
1168 let (send_goal_client, cancel_goal_client, get_result_client, feedback_sub) = {
1169 let send_goal_type: heapless::String<256> =
1174 super::action_core::action_channel_type(type_name, "SendGoal");
1175 let get_result_type: heapless::String<256> =
1176 super::action_core::action_channel_type(type_name, "GetResult");
1177 let feedback_type: heapless::String<256> =
1178 super::action_core::action_channel_type(type_name, "FeedbackMessage");
1179
1180 let send_goal_keyexpr: heapless::String<256> = action_info.send_goal_key();
1181 let mut send_goal_info =
1182 ServiceInfo::new(&send_goal_keyexpr, &send_goal_type, type_hash)
1183 .with_namespace(&ns)
1184 .with_domain(domain_id);
1185 if !node_name.is_empty() {
1186 send_goal_info = send_goal_info.with_node_name(&node_name);
1187 }
1188
1189 let cancel_goal_keyexpr: heapless::String<256> = action_info.cancel_goal_key();
1190 let mut cancel_goal_info = ServiceInfo::new(
1191 &cancel_goal_keyexpr,
1192 "action_msgs::srv::dds_::CancelGoal_",
1193 type_hash,
1194 )
1195 .with_namespace(&ns)
1196 .with_domain(domain_id);
1197 if !node_name.is_empty() {
1198 cancel_goal_info = cancel_goal_info.with_node_name(&node_name);
1199 }
1200
1201 let get_result_keyexpr: heapless::String<256> = action_info.get_result_key();
1202 let mut get_result_info =
1203 ServiceInfo::new(&get_result_keyexpr, &get_result_type, type_hash)
1204 .with_namespace(&ns)
1205 .with_domain(domain_id);
1206 if !node_name.is_empty() {
1207 get_result_info = get_result_info.with_node_name(&node_name);
1208 }
1209
1210 let feedback_keyexpr: heapless::String<256> = action_info.feedback_key();
1211 let mut feedback_topic = TopicInfo::new(&feedback_keyexpr, &feedback_type, type_hash)
1212 .with_namespace(&ns)
1213 .with_domain(domain_id);
1214 if !node_name.is_empty() {
1215 feedback_topic = feedback_topic.with_node_name(&node_name);
1216 }
1217
1218 let session = self
1219 .session_at_mut(session_idx)
1220 .ok_or(NodeError::BackendMismatch)?;
1221 (
1231 session
1232 .create_client(&send_goal_info, QoSProfile::services_default())
1233 .map_err(|e| {
1234 nros_log::nros_error!(
1235 nros_log::get_logger("nros_node"),
1236 "action client: send_goal client failed: {:?}",
1237 e
1238 );
1239 NodeError::Transport(e)
1240 })?,
1241 session
1242 .create_client(&cancel_goal_info, QoSProfile::services_default())
1243 .map_err(|e| {
1244 nros_log::nros_error!(
1245 nros_log::get_logger("nros_node"),
1246 "action client: cancel_goal client failed: {:?}",
1247 e
1248 );
1249 NodeError::Transport(e)
1250 })?,
1251 session
1252 .create_client(&get_result_info, QoSProfile::services_default())
1253 .map_err(|e| {
1254 nros_log::nros_error!(
1255 nros_log::get_logger("nros_node"),
1256 "action client: get_result client failed: {:?}",
1257 e
1258 );
1259 NodeError::Transport(e)
1260 })?,
1261 session
1262 .create_subscription(&feedback_topic, QoSProfile::BEST_EFFORT)
1263 .map_err(|e| {
1264 nros_log::nros_error!(
1265 nros_log::get_logger("nros_node"),
1266 "action client: feedback subscription failed: {:?}",
1267 e
1268 );
1269 NodeError::Transport(e)
1270 })?,
1271 )
1272 };
1273
1274 let core = ActionClientCore::new(
1275 send_goal_client,
1276 cancel_goal_client,
1277 get_result_client,
1278 feedback_sub,
1279 );
1280
1281 let offset = self.arena_alloc::<Entry<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>>()?;
1282
1283 unsafe {
1284 let arena_ptr = self.arena.as_mut_ptr() as *mut u8;
1285 let entry_ptr = arena_ptr.add(offset) as *mut Entry<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>;
1286 core::ptr::write(
1287 entry_ptr,
1288 Entry {
1289 core,
1290 goal_response_callback,
1291 feedback_callback,
1292 result_callback,
1293 context,
1294 },
1295 );
1296 }
1297
1298 let meta = CallbackMeta {
1299 offset,
1300 kind: EntryKind::ActionClient,
1301 has_data: always_ready,
1302 pre_sample: no_pre_sample,
1303 invocation: InvocationMode::Always,
1304 try_process: action_client_raw_try_process::<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>,
1305 drop_fn: drop_entry::<Entry<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>>,
1306 };
1307 self.emplace_entry(slot, meta, TraceName::Text(spec.action_name));
1308 self.apply_node_default_sched(slot, node_id, None);
1309
1310 Ok(ActionClientRawHandle { entry_index: slot })
1311 }
1312
1313 #[allow(clippy::too_many_arguments, clippy::type_complexity)]
1320 pub(crate) fn register_action_client_callback<
1321 A,
1322 GRespF,
1323 FbF,
1324 ResF,
1325 const GOAL_BUF: usize,
1326 const RESULT_BUF: usize,
1327 const FEEDBACK_BUF: usize,
1328 >(
1329 &mut self,
1330 node_id: Option<super::node_record::NodeId>,
1331 action_name: &str,
1332 type_name: &str,
1333 type_hash: &str,
1334 feedback_depth: u16,
1335 on_goal_response: GRespF,
1336 on_feedback: FbF,
1337 on_result: ResF,
1338 ) -> Result<
1339 (
1340 HandleId,
1341 *mut ActionClientCore<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>,
1342 ),
1343 NodeError,
1344 >
1345 where
1346 A: nros_core::RosAction + 'static,
1347 GRespF: FnMut(&nros_core::GoalId, bool) + 'static,
1348 FbF: FnMut(&nros_core::GoalId, &A::Feedback) + 'static,
1349 ResF: FnMut(&nros_core::GoalId, nros_core::GoalStatus, &A::Result) + 'static,
1350 {
1351 type Entry<A, G, Fb, R, const GB: usize, const RB: usize, const FB: usize> =
1352 ActionClientCallbackEntry<A, G, Fb, R, GB, RB, FB>;
1353
1354 let slot = self.next_entry_slot()?;
1355 let action_info = ActionInfo::new(action_name, type_name, type_hash);
1356 A::register_protocol_types().map_err(|()| NodeError::ActionCreationFailed)?;
1362 let domain_id = self.domain_id;
1366 let (node_name, ns, session_idx) = match node_id {
1367 Some(id) => {
1368 let r = self
1369 .nodes
1370 .get(id.index())
1371 .ok_or(NodeError::InvalidSchedContextBinding)?;
1372 (r.name.clone(), r.namespace.clone(), r.session_idx)
1373 }
1374 None => (self.node_name.clone(), self.namespace.clone(), 0u8),
1375 };
1376
1377 let (send_goal_client, cancel_goal_client, get_result_client, feedback_sub) = {
1378 let send_goal_type: heapless::String<256> =
1383 super::action_core::action_channel_type(type_name, "SendGoal");
1384 let get_result_type: heapless::String<256> =
1385 super::action_core::action_channel_type(type_name, "GetResult");
1386 let feedback_type: heapless::String<256> =
1387 super::action_core::action_channel_type(type_name, "FeedbackMessage");
1388
1389 let send_goal_keyexpr: heapless::String<256> = action_info.send_goal_key();
1390 let mut send_goal_info =
1391 ServiceInfo::new(&send_goal_keyexpr, &send_goal_type, type_hash)
1392 .with_namespace(&ns)
1393 .with_domain(domain_id);
1394 if !node_name.is_empty() {
1395 send_goal_info = send_goal_info.with_node_name(&node_name);
1396 }
1397 let cancel_goal_keyexpr: heapless::String<256> = action_info.cancel_goal_key();
1398 let mut cancel_goal_info = ServiceInfo::new(
1399 &cancel_goal_keyexpr,
1400 "action_msgs::srv::dds_::CancelGoal_",
1401 type_hash,
1402 )
1403 .with_namespace(&ns)
1404 .with_domain(domain_id);
1405 if !node_name.is_empty() {
1406 cancel_goal_info = cancel_goal_info.with_node_name(&node_name);
1407 }
1408 let get_result_keyexpr: heapless::String<256> = action_info.get_result_key();
1409 let mut get_result_info =
1410 ServiceInfo::new(&get_result_keyexpr, &get_result_type, type_hash)
1411 .with_namespace(&ns)
1412 .with_domain(domain_id);
1413 if !node_name.is_empty() {
1414 get_result_info = get_result_info.with_node_name(&node_name);
1415 }
1416 let feedback_keyexpr: heapless::String<256> = action_info.feedback_key();
1417 let mut feedback_topic = TopicInfo::new(&feedback_keyexpr, &feedback_type, type_hash)
1418 .with_namespace(&ns)
1419 .with_domain(domain_id);
1420 if !node_name.is_empty() {
1421 feedback_topic = feedback_topic.with_node_name(&node_name);
1422 }
1423 let session = self
1424 .session_at_mut(session_idx)
1425 .ok_or(NodeError::BackendMismatch)?;
1426 (
1427 session
1428 .create_client(&send_goal_info, QoSProfile::services_default())
1429 .map_err(NodeError::Transport)?,
1430 session
1431 .create_client(&cancel_goal_info, QoSProfile::services_default())
1432 .map_err(NodeError::Transport)?,
1433 session
1434 .create_client(&get_result_info, QoSProfile::services_default())
1435 .map_err(NodeError::Transport)?,
1436 session
1437 .create_subscription(&feedback_topic, QoSProfile::BEST_EFFORT)
1438 .map_err(NodeError::Transport)?,
1439 )
1440 };
1441
1442 let core = ActionClientCore::new(
1443 send_goal_client,
1444 cancel_goal_client,
1445 get_result_client,
1446 feedback_sub,
1447 );
1448
1449 let (_slot_count, trailing_bytes) =
1453 buffered_region_size(feedback_depth as u32, FEEDBACK_BUF);
1454 let (offset, trailing_offset) = self.arena_alloc_with_trailing::<Entry<
1455 A,
1456 GRespF,
1457 FbF,
1458 ResF,
1459 GOAL_BUF,
1460 RESULT_BUF,
1461 FEEDBACK_BUF,
1462 >>(trailing_bytes)?;
1463 let buf_ptr = unsafe { (self.arena.as_mut_ptr() as *mut u8).add(trailing_offset) };
1464 let feedback_buffer = if feedback_depth <= 1 {
1465 BufferStrategy::Triple(unsafe { TripleBuffer::init(buf_ptr, FEEDBACK_BUF) })
1466 } else {
1467 BufferStrategy::Ring(unsafe {
1468 SpscRing::init(buf_ptr, FEEDBACK_BUF, feedback_depth as usize)
1469 })
1470 };
1471 let core_ptr = unsafe {
1472 let arena_ptr = self.arena.as_mut_ptr() as *mut u8;
1473 let entry_ptr = arena_ptr.add(offset)
1474 as *mut Entry<A, GRespF, FbF, ResF, GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>;
1475 core::ptr::write(
1476 entry_ptr,
1477 ActionClientCallbackEntry {
1478 core,
1479 feedback_buffer,
1480 on_goal_response,
1481 on_feedback,
1482 on_result,
1483 _phantom: core::marker::PhantomData,
1484 },
1485 );
1486 &mut (*entry_ptr).core as *mut ActionClientCore<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>
1487 };
1488
1489 let meta = CallbackMeta {
1490 offset,
1491 kind: EntryKind::ActionClient,
1492 has_data: always_ready,
1493 pre_sample: no_pre_sample,
1494 invocation: InvocationMode::Always,
1495 try_process: action_client_callback_try_process::<
1496 A,
1497 GRespF,
1498 FbF,
1499 ResF,
1500 GOAL_BUF,
1501 RESULT_BUF,
1502 FEEDBACK_BUF,
1503 >,
1504 drop_fn: drop_entry::<Entry<A, GRespF, FbF, ResF, GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>>,
1505 };
1506 self.emplace_entry(slot, meta, TraceName::Text(action_name));
1507 self.apply_node_default_sched(slot, node_id, None);
1508 Ok((HandleId(slot), core_ptr))
1509 }
1510}
1511
1512impl<'s> Executor<'s> {
1513 pub fn register_action_client_core<
1519 const GOAL_BUF: usize,
1520 const RESULT_BUF: usize,
1521 const FEEDBACK_BUF: usize,
1522 >(
1523 &mut self,
1524 core: ActionClientCore<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>,
1525 goal_response_callback: Option<RawGoalResponseCallback>,
1526 feedback_callback: Option<RawFeedbackCallback>,
1527 result_callback: Option<RawResultCallback>,
1528 context: *mut core::ffi::c_void,
1529 ) -> Result<ActionClientRawHandle, NodeError> {
1530 type Entry<const GB: usize, const RB: usize, const FB: usize> =
1531 ActionClientRawArenaEntry<GB, RB, FB>;
1532
1533 let slot = self.next_entry_slot()?;
1534 let offset = self.arena_alloc::<Entry<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>>()?;
1535
1536 unsafe {
1537 let arena_ptr = self.arena.as_mut_ptr() as *mut u8;
1538 let entry_ptr = arena_ptr.add(offset) as *mut Entry<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>;
1539 core::ptr::write(
1540 entry_ptr,
1541 Entry {
1542 core,
1543 goal_response_callback,
1544 feedback_callback,
1545 result_callback,
1546 context,
1547 },
1548 );
1549 }
1550
1551 let meta = CallbackMeta {
1552 offset,
1553 kind: EntryKind::ActionClient,
1554 has_data: always_ready,
1555 pre_sample: no_pre_sample,
1556 invocation: InvocationMode::Always,
1557 try_process: action_client_raw_try_process::<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>,
1558 drop_fn: drop_entry::<Entry<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>>,
1559 };
1560 self.emplace_entry(slot, meta, TraceName::Slot("action_client", slot));
1561
1562 Ok(ActionClientRawHandle { entry_index: slot })
1563 }
1564}
1565
1566pub struct ActionClientRawHandle {
1571 entry_index: usize,
1572}
1573
1574impl ActionClientRawHandle {
1575 pub fn entry_index(&self) -> usize {
1577 self.entry_index
1578 }
1579}