nros C++ API
Lightweight ROS 2 client for embedded real-time systems (C++ headers)
Loading...
Searching...
No Matches
client.hpp
Go to the documentation of this file.
1// nros-cpp: Action client class
2// Freestanding C++ — no exceptions, no STL required
3
9
10#ifndef NROS_RCLCPP_ACTION_CLIENT_HPP
11#define NROS_RCLCPP_ACTION_CLIENT_HPP
12
13#include <cstdint>
14#include <cstddef>
15#include <string.h>
16
18#include "rclcpp/detail/entity_name.hpp" // phase-444 — the one entity-name copy
19#include "rclcpp/logging.hpp" // phase-417 stage 3 — NROS_RCLCPP_REFUSE_* + rclcpp::detail::refuse
20#include "rclcpp/owned.hpp" // issue 2015 — `Client<A>::SharedPtr`
21#include "rclcpp/result.hpp"
22#include "rclcpp/size_bound.hpp" // rclcpp::rx_buffer_capacity<M> — the receive-buffer size
23#include "rclcpp/future.hpp"
24#include "rclcpp/stream.hpp"
25#include "rclcpp/time.hpp" // issue 2027 — `async_cancel_goals_before(const rclcpp::Time&)`
26// Issue 0796 — `CancelReturnCode` (the `action_msgs/srv/CancelGoal` RPC status
27// `cancel_goal` resolves to) is declared beside its per-goal sibling
28// `CancelResponse` in action_server.hpp, which is also where
29// polling_action_server.hpp reaches for them.
31#include "rclcpp_action/detail/upstream.hpp" // issue 2015 — hosted goal-handle plumbing
32
33// Phase 118.D: FFI declarations sourced from cbindgen-generated
34// `nros_cpp_ffi.h`. cbindgen renders Rust `*const [u8; 16]` /
35// `*mut [u8; 16]` parameters as C++ pointer-to-array
36// (`const uint8_t (*goal_id)[16]`); member-function bodies below
37// take `goal_id` as a 16-byte array (decays to `uint8_t*`) and
38// `reinterpret_cast` at the call site to bridge the two ABI
39// equivalent shapes.
40#include "rclcpp/detail/nros_cpp_ffi.h"
41
42extern "C" {
44 const uint8_t goal_id[16],
45 void* ctx);
46typedef void (*nros_cpp_action_client_feedback_callback_t)(const uint8_t goal_id[16],
47 const uint8_t* data, size_t len,
48 void* ctx);
49typedef void (*nros_cpp_action_client_result_callback_t)(const uint8_t goal_id[16], int32_t status,
50 const uint8_t* data, size_t len,
51 void* ctx);
52
56 nros_cpp_action_client_result_callback_t result, void* context);
57
60typedef void (*nros_cpp_action_client_cancel_reply_callback_t)(const uint8_t goal_id[16],
61 int64_t seq, const uint8_t* reply,
62 size_t reply_len, void* ctx);
63
67} // extern "C"
68
76namespace rclcpp {
77class Node;
78}
79
80// ============================================================================
81// `rclcpp_action::Client<A>` -- DEFINED here (RFC-0089: rclcpp_action:: is the home)
82// ============================================================================
83//
84// phase-428: the definition moved from `nros::ActionClient<A>` to
85// `rclcpp_action::Client<A>` (the `nros::` alias went in phase-483 W1). A RENAME
86// as well as a move -- upstream's namespace already says "action", so the type
87// is `Client`, not `ActionClient`. Note the two `Client`s do not collide:
88// `rclcpp::Client<S>` is the SERVICE client, in a different namespace.
89namespace rclcpp_action {
90
96enum class ResultCode : int8_t {
101};
102
107static const size_t MAX_CANCEL_INFOS = 16;
108
114static const size_t CANCEL_STASH_CAP = 4;
115
116template <typename A> class Client;
117
129template <typename A> class ClientGoalHandle {
130 public:
131 using Feedback = typename A::Feedback;
132 using Result = typename A::Result;
133#ifdef NROS_CPP_HAS_SHARED_PTR // hosted-family: action-interop
134 using SharedPtr = ::std::shared_ptr<ClientGoalHandle>;
135 using ConstSharedPtr = ::std::shared_ptr<const ClientGoalHandle>;
137 struct WrappedResult {
138 GoalUUID goal_id;
139 ResultCode code;
140 ::std::shared_ptr<Result> result;
141 };
142 using GoalResponseCallback = ::std::function<void(SharedPtr)>;
143 using FeedbackCallback =
144 ::std::function<void(SharedPtr, const ::std::shared_ptr<const Feedback>)>;
145 using ResultCallback = ::std::function<void(const WrappedResult&)>;
146#else
147 using SharedPtr = detail::Refused<ClientGoalHandle>;
148 using ConstSharedPtr = detail::Refused<ClientGoalHandle>;
149 using WrappedResult = detail::Refused<ClientGoalHandle>;
150#endif
151
152 explicit ClientGoalHandle(const GoalUUID& id)
153 : goal_id_(id), status_(static_cast<int8_t>(GoalStatus::Accepted)), feedback_aware_(false),
154 result_aware_(false) {}
157
159 const GoalUUID& get_goal_id() const { return goal_id_; }
161 int8_t get_status() const { return status_; }
163 bool is_feedback_aware() const { return feedback_aware_; }
165 bool is_result_aware() const { return result_aware_; }
166
167 private:
168 friend class Client<A>;
169
170#ifdef NROS_CPP_HAS_STD_PROMISE // hosted-family: action-interop
174 struct ResultState {
175 ::std::promise<WrappedResult> promise;
176 ::std::shared_future<WrappedResult> future;
177 bool done = false;
178 };
179 ResultState* result_state() {
180 ResultState* st = result_.template as<ResultState>();
181 if (st == nullptr) {
182 st = new ResultState();
183 st->future = st->promise.get_future().share();
184 result_.reset(st, [](void* p) { delete static_cast<ResultState*>(p); });
185 }
186 return st;
187 }
189 void set_result(const WrappedResult& wrapped) {
190 ResultState* st = result_.template as<ResultState>();
191 if (st != nullptr && !st->done) {
192 st->done = true;
193 st->promise.set_value(wrapped);
194 }
195 }
196 bool result_done() const {
197 ResultState* st = result_.template as<ResultState>();
198 return st != nullptr && st->done;
199 }
200#endif
201
202 GoalUUID goal_id_;
203 int8_t status_;
204 bool feedback_aware_;
205 bool result_aware_;
208 detail::HostedBox result_;
209};
210
211#if defined(NROS_CPP_HAS_SHARED_PTR) // hosted-family: action-interop
212namespace detail {
215struct CancelGoalInfo {
216 struct {
217 uint8_t uuid[16];
218 } goal_id;
219 struct {
220 int32_t sec;
221 uint32_t nanosec;
222 } stamp;
223};
227struct CancelResponse {
228 static constexpr int8_t ERROR_NONE = 0;
229 static constexpr int8_t ERROR_REJECTED = 1;
230 static constexpr int8_t ERROR_UNKNOWN_GOAL_ID = 2;
231 static constexpr int8_t ERROR_GOAL_TERMINATED = 3;
232 using SharedPtr = ::std::shared_ptr<CancelResponse>;
233 int8_t return_code = ERROR_NONE;
234 ::std::vector<CancelGoalInfo> goals_canceling;
235};
236} // namespace detail
237#endif
238
256template <typename A> class Client {
257 public:
258 using GoalType = typename A::Goal;
259 using ResultType = typename A::Result;
260 using FeedbackType = typename A::Feedback;
261
265 using SharedPtr = ::rclcpp::Owned<Client>;
270#if defined(NROS_CPP_HAS_SHARED_PTR) // hosted-family: action-interop
273 using CancelResponse = detail::CancelResponse;
274 using CancelCallback = ::std::function<void(typename CancelResponse::SharedPtr)>;
275 using ResultCallback = typename GoalHandle::ResultCallback;
276#else
277 using CancelResponse = detail::Refused<Client>;
278#endif
279
280 private:
281#ifdef NROS_CPP_HAS_SHARED_PTR // hosted-family: action-interop
282 using FeedbackPtr = ::std::shared_ptr<const FeedbackType>;
283#else
284 using FeedbackPtr = detail::Refused<FeedbackType>;
285#endif
286
287 public:
292 struct GoalAccept {
296 uint8_t goal_id[16];
298
299 // Receive-buffer size for `Future<GoalAccept>`; the payload is 17 bytes
300 // (16-byte goal_id + 1 accepted byte), rounded up.
301 static const size_t SERIALIZED_SIZE_MAX = 32;
302
303 // Issue 0329 — the 17-byte wire layout is owned Rust-side
304 // (`nros_cpp_action_goal_accept_decode`, beside its producer); the header
305 // no longer hand-decodes it, so the two cannot drift.
306 static int ffi_deserialize(const uint8_t* data, size_t len, GoalAccept* out) {
307 if (!out) return -1;
308 return nros_cpp_action_goal_accept_decode(data, len, &out->goal_id, &out->accepted) ==
309 NROS_CPP_RET_OK
310 ? 0
311 : -1;
312 }
313 };
314
338 if (!initialized_) return ::rclcpp::Result(::rclcpp::ErrorCode::NotInitialized);
339 return ::rclcpp::Result(
340 nros_cpp_action_client_wait_for_action_server(storage_, timeout_ms));
341 }
342
344 template <typename T = void>::rclcpp::Result wait_for_action_server() {
345 static_assert(::rclcpp::detail::refuse<T>::value, NROS_RCLCPP_REFUSE_UNBOUNDED_WAIT);
346 return ::rclcpp::Result(::rclcpp::ErrorCode::Unsupported);
347 }
348
349#ifdef NROS_CPP_HAS_STD_CHRONO // hosted-family: chrono-interop
355 template <typename Rep, typename Period>
356 ::rclcpp::Result wait_for_action_server(::std::chrono::duration<Rep, Period> timeout) {
357 const int64_t ms = ::rclcpp::detail::chrono_wait_budget_ms(timeout);
358 if (ms < 0) {
359 NROS_RCLCPP_SAY_REFUSED(NROS_RCLCPP_REFUSE_UNBOUNDED_WAIT_RUNTIME);
360 return ::rclcpp::Result(::rclcpp::ErrorCode::Unsupported);
361 }
362 return wait_for_action_server(static_cast<uint32_t>(ms));
363 }
364#endif // NROS_CPP_HAS_STD_CHRONO
365
380 if (!initialized_) return false;
381 return nros_cpp_action_client_action_server_is_ready(
382 const_cast<void*>(static_cast<const void*>(storage_)));
383 }
384
391 const char* get_action_name() const { return initialized_ ? action_name_ : ""; }
392
406 ::rclcpp::Result send_goal(const GoalType& goal, uint8_t goal_id[16]) {
407 if (!initialized_) return ::rclcpp::Result(::rclcpp::ErrorCode::NotInitialized);
408
409 uint8_t buf[::rclcpp::detail::buffer_bounds<GoalType>::tx];
410 size_t len = 0;
411 if (GoalType::ffi_serialize(&goal, buf, sizeof(buf), &len) != 0) {
412 return ::rclcpp::Result(::rclcpp::ErrorCode::Error);
413 }
414 return ::rclcpp::Result(nros_cpp_action_client_send_goal(
415 storage_, buf, len, reinterpret_cast<uint8_t(*)[16]>(goal_id)));
416 }
417
421 return send_goal(goal, goal_id.data());
422 }
423
432 ::rclcpp::Result get_result(const uint8_t goal_id[16], ResultType& result) {
434 }
435
437 ::rclcpp::Result get_result(const ::rclcpp_action::GoalUUID& goal_id, ResultType& result) {
439 result);
440 }
441
444 template <size_t Cap>
445 ::rclcpp::Result get_result_sized(const uint8_t goal_id[16], ResultType& result) {
446 if (!initialized_) return ::rclcpp::Result(::rclcpp::ErrorCode::NotInitialized);
447
448 uint8_t buf[Cap];
449 size_t len = 0;
450 nros_cpp_ret_t ret = nros_cpp_action_client_get_result(
451 storage_, executor_, reinterpret_cast<const uint8_t(*)[16]>(goal_id), buf, sizeof(buf),
452 &len);
453 if (ret != 0) return ::rclcpp::Result(ret);
454
455 if (ResultType::ffi_deserialize(buf, len, &result) != 0) {
456 return ::rclcpp::Result(::rclcpp::ErrorCode::Error);
457 }
458 return ::rclcpp::Result::success();
459 }
460
461 // =================================================================
462 // Future-based API — non-blocking, polled via Future<T>
463 // =================================================================
464
476
491 ::rclcpp::Future<GoalAccept> send_goal_future(const GoalType& goal) {
492 if (!initialized_) return ::rclcpp::Future<GoalAccept>();
493
494 uint8_t buf[::rclcpp::detail::buffer_bounds<GoalType>::tx];
495 size_t len = 0;
496 if (GoalType::ffi_serialize(&goal, buf, sizeof(buf), &len) != 0) {
497 return ::rclcpp::Future<GoalAccept>();
498 }
500 uint8_t goal_id[16];
501 nros_cpp_ret_t ret = nros_cpp_action_client_send_goal_async(
502 storage_, buf, len, reinterpret_cast<uint8_t(*)[16]>(goal_id));
503 if (ret != 0) return ::rclcpp::Future<GoalAccept>();
504
505 // Issue 2011 -- keyed by THIS goal on this client's acceptance list:
506 // the acceptance reaches it whether the executor dispatched it during
507 // `spin_once` (through `on_goal_response`) or the future's own poll
508 // took it (`nros_cpp_action_client_poll`, same callbacks).
509 return ::rclcpp::Future<GoalAccept>(storage_, &nros_cpp_action_client_poll, goal_id,
510 &accepts_,
511 executor_ // driven by `Future::wait_for` (issue 1996)
512 );
513 }
528 /// Keyed by `goal_id` (issue 2011): the future receives that goal's result
529 /// and no other, whichever client or future polls first. A result for a
530 /// goal no future waits on -- including one whose future was cancelled or
531 /// destroyed first -- is dropped (the client's result callback still sees
532 /// it). Several futures for one goal each receive its result, one reply
533 /// apiece.
534 ///
535 /// @param goal_id 16-byte goal UUID from send_goal() or GoalAccept.
536 /// @return Future that resolves to ResultType. Returns a consumed
537 /// (empty) future on send failure, including a full table (at most
538 /// `MAX_CONCURRENT_GOALS` get_result requests pending per client).
542
544 ::rclcpp::Future<ResultType> get_result_future(const ::rclcpp_action::GoalUUID& goal_id) {
546 goal_id.data());
547 }
548
554 template <size_t Cap>
557 if (!initialized_) return Fut();
558
559 nros_cpp_ret_t ret = nros_cpp_action_client_get_result_async(
560 storage_, reinterpret_cast<const uint8_t(*)[16]>(goal_id));
561 if (ret != 0) return Fut();
562
563 // Issue 2011 -- keyed by THIS goal on this client's result list; see
564 // `send_goal_future`.
565 return Fut(storage_, &nros_cpp_action_client_poll, goal_id, &results_,
566 executor_ // driven by `Future::wait_for` (issue 1996)
567 );
568 }
569
570
581
584 template <size_t Cap>::rclcpp::Result try_recv_feedback_sized(FeedbackType& feedback) {
585 if (!initialized_) return ::rclcpp::Result(::rclcpp::ErrorCode::NotInitialized);
586
587 uint8_t buf[Cap];
588 size_t len = 0;
589 nros_cpp_ret_t ret = take_feedback(this, buf, sizeof(buf), &len);
590 if (ret != 0) return ::rclcpp::Result(ret);
591 if (len == 0) return ::rclcpp::Result(::rclcpp::ErrorCode::TryAgain);
592 if (FeedbackType::ffi_deserialize(buf, len, &feedback) != 0) {
593 return ::rclcpp::Result(::rclcpp::ErrorCode::Error);
594 }
595 return ::rclcpp::Result::success();
596 }
597
601 /// goals for this client — feedback is not goal-scoped at this layer. It
602 /// holds the LATEST feedback per client (issue 2011: it used to be one slot
603 /// for every action client in the process); an unread one is overwritten.
604 /// Callers that need per-goal separation should use the callback API
605 /// (`set_callbacks(SendGoalOptions{ .feedback = … })`), which delivers
606 /// `(goal_id, bytes, len, ctx)` via an executor-driven trampoline.
607 ///
608 /// Usage (blocking):
609 /// ```cpp
610 /// FeedbackType fb;
611 /// RCLCPP_TRY(client.feedback_stream().wait_next(executor.handle(), 500, fb));
612 /// ```
613 ///
614 /// Usage (non-blocking):
615 /// ```cpp
616 /// FeedbackType fb;
617 /// Result r = client.feedback_stream().try_next(fb);
618 /// if (r.ok()) { ... }
619 /// ```
621 if (initialized_ && !feedback_stream_.is_valid()) {
622 feedback_stream_.bind(this, &Client::take_feedback);
623 }
624 return feedback_stream_;
625 }
626
627 const ::rclcpp::Stream<FeedbackType>& feedback_stream() const { return feedback_stream_; }
629 // =================================================================
630 // Async (non-blocking) API — callbacks invoked during spin_once()
631 // =================================================================
632
639
642 /// `result_callback`, taking upstream's shapes (`GoalHandle::SharedPtr`,
643 /// `std::shared_ptr<const Feedback>`, `const WrappedResult&`). Those are
644 /// read by `async_send_goal`, per goal, as upstream does; the raw pointers
645 /// are read by `set_callbacks`, per client. A freestanding build refuses an
646 /// assignment to the upstream fields.
649 void (*goal_response)(bool accepted, const uint8_t goal_id[16], void* ctx);
651 void (*feedback)(const uint8_t goal_id[16], const uint8_t* data, size_t len, void* ctx);
653 void (*result)(const uint8_t goal_id[16], int32_t status, const uint8_t* data, size_t len,
654 void* ctx);
656 void* context;
657
659 detail::UpstreamCallback<void(typename GoalHandle::SharedPtr)> goal_response_callback;
661 detail::UpstreamCallback<void(typename GoalHandle::SharedPtr, FeedbackPtr)>
665 detail::UpstreamCallback<void(const WrappedResult&)> result_callback;
666
668 };
669
679 ::rclcpp::Result send_goal_async(const GoalType& goal, uint8_t goal_id[16]) {
680 if (!initialized_) return ::rclcpp::Result(::rclcpp::ErrorCode::NotInitialized);
681
682 uint8_t buf[::rclcpp::detail::buffer_bounds<GoalType>::tx];
683 size_t len = 0;
684 if (GoalType::ffi_serialize(&goal, buf, sizeof(buf), &len) != 0) {
685 return ::rclcpp::Result(::rclcpp::ErrorCode::Error);
686 }
687 return ::rclcpp::Result(nros_cpp_action_client_send_goal_async(
688 storage_, buf, len, reinterpret_cast<uint8_t(*)[16]>(goal_id)));
689 }
690
694 return send_goal_async(goal, goal_id.data());
695 }
696
697#ifdef NROS_CPP_HAS_SHARED_PTR // hosted-family: action-interop
715 ::rclcpp::Future<GoalAccept>
716 async_send_goal(const GoalType& goal, const SendGoalOptions& options = SendGoalOptions()) {
717 uint8_t goal_id[16];
718 if (!send_goal_async(goal, goal_id).ok()) return ::rclcpp::Future<GoalAccept>();
719 UpstreamState* st = upstream_state();
720 prune_upstream(st);
721 UpstreamGoal g;
722 g.id = GoalUUID(goal_id);
723 g.options = options;
724 st->goals.push_back(g);
725 return ::rclcpp::Future<GoalAccept>(storage_, &nros_cpp_action_client_poll, goal_id,
726 &accepts_, executor_);
727 }
728#else
730 template <typename T = void>
732 const SendGoalOptions& = SendGoalOptions()) {
733 static_assert(::rclcpp::detail::refuse<T>::value, NROS_RCLCPP_REFUSE_ACTION_GOAL_HANDLE);
734 return ::rclcpp::Future<GoalAccept>();
735 }
736#endif
737
738#ifdef NROS_CPP_HAS_STD_PROMISE // hosted-family: action-interop
760 detail::DrivenSharedFuture<WrappedResult>
761 async_get_result(typename GoalHandle::SharedPtr goal_handle,
762 ResultCallback result_callback = nullptr) {
763 if (!initialized_ || !goal_handle) return detail::DrivenSharedFuture<WrappedResult>();
764 typename GoalHandle::ResultState* rs = goal_handle->result_state();
765 detail::DrivenSharedFuture<WrappedResult> fut(rs->future, pump_cell());
766 if (rs->done) return fut;
767 UpstreamState* st = upstream_state();
768 size_t at = 0;
769 bool created = false;
770 if (upstream_goal(goal_handle->get_goal_id().data(), at) == nullptr) {
771 UpstreamGoal g;
772 g.id = goal_handle->get_goal_id();
773 g.accepted = true;
774 st->goals.push_back(g);
775 at = st->goals.size() - 1;
776 created = true;
777 }
778 if (!goal_handle->result_aware_) {
779 if (!get_result_async(goal_handle->get_goal_id()).ok()) {
780 if (created) st->goals.erase(st->goals.begin() + static_cast<long>(at));
781 return detail::DrivenSharedFuture<WrappedResult>();
782 }
783 goal_handle->result_aware_ = true;
784 }
785 UpstreamGoal& g = st->goals[at];
786 g.accepted = true;
787 g.handle = goal_handle;
788 g.weak.reset();
789 if (result_callback) g.options.result_callback = result_callback;
790 return fut;
791 }
792
809 detail::DrivenSharedFuture<typename CancelResponse::SharedPtr>
810 async_cancel_goal(typename GoalHandle::SharedPtr goal_handle,
811 CancelCallback cancel_callback = nullptr) {
812 if (!goal_handle) return detail::DrivenSharedFuture<typename CancelResponse::SharedPtr>();
813 return send_cancel(goal_handle->get_goal_id(), 0, 0, cancel_callback);
814 }
815
819 detail::DrivenSharedFuture<typename CancelResponse::SharedPtr>
820 async_cancel_all_goals(CancelCallback cancel_callback = nullptr) {
821 return send_cancel(GoalUUID(), 0, 0, cancel_callback);
822 }
823
828 detail::DrivenSharedFuture<typename CancelResponse::SharedPtr>
829 async_cancel_goals_before(const ::rclcpp::Time& stamp,
830 CancelCallback cancel_callback = nullptr) {
831 const int64_t ns = stamp.nanoseconds();
832 const int64_t sec = ns >= 0 ? ns / 1000000000LL : -((-ns + 999999999LL) / 1000000000LL);
833 const int64_t nsec = ns - sec * 1000000000LL;
834 return send_cancel(GoalUUID(), static_cast<int32_t>(sec), static_cast<uint32_t>(nsec),
835 cancel_callback);
836 }
837#endif
838#ifndef NROS_CPP_HAS_STD_PROMISE // hosted-family: action-interop
843 template <typename T = void, typename... Args> void async_get_result(Args&&...) {
844 static_assert(::rclcpp::detail::refuse<T>::value, NROS_RCLCPP_REFUSE_ACTION_GOAL_HANDLE);
845 }
846
847 template <typename T = void, typename... Args> void async_cancel_goal(Args&&...) {
848 static_assert(::rclcpp::detail::refuse<T>::value, NROS_RCLCPP_REFUSE_ACTION_GOAL_HANDLE);
849 }
850
851 template <typename T = void, typename... Args> void async_cancel_all_goals(Args&&...) {
852 static_assert(::rclcpp::detail::refuse<T>::value, NROS_RCLCPP_REFUSE_ACTION_GOAL_HANDLE);
853 }
854
855 template <typename T = void, typename... Args> void async_cancel_goals_before(Args&&...) {
856 static_assert(::rclcpp::detail::refuse<T>::value, NROS_RCLCPP_REFUSE_ACTION_GOAL_HANDLE);
857 }
858#endif
859
874 ::rclcpp::Result cancel_goal(const uint8_t goal_id[16]) {
875 if (!initialized_) return ::rclcpp::Result(::rclcpp::ErrorCode::NotInitialized);
876 return ::rclcpp::Result(nros_cpp_action_client_cancel_goal(
877 storage_, reinterpret_cast<const uint8_t(*)[16]>(goal_id)));
878 }
879
881 ::rclcpp::Result cancel_goal(const ::rclcpp_action::GoalUUID& goal_id) {
882 return cancel_goal(goal_id.data());
883 }
884
897 if (!initialized_) return ::rclcpp::Result(::rclcpp::ErrorCode::NotInitialized);
898 int8_t code = 0;
899 // Issue 2034 — a reply the spin took off the wire for a request no
900 // future or callback waits on is kept here, not dropped.
901 if (take_stray_cancel(code)) {
902 out = static_cast<::rclcpp_action::CancelReturnCode>(code);
903 return ::rclcpp::Result::success();
904 }
905 nros_cpp_ret_t ret = nros_cpp_action_client_try_recv_cancel_response(storage_, &code);
906 if (ret != 0) return ::rclcpp::Result(ret);
907 out = static_cast<::rclcpp_action::CancelReturnCode>(code);
908 return ::rclcpp::Result::success();
909 }
910
918 ::rclcpp::Result get_result_async(const uint8_t goal_id[16]) {
919 if (!initialized_) return ::rclcpp::Result(::rclcpp::ErrorCode::NotInitialized);
920 return ::rclcpp::Result(nros_cpp_action_client_get_result_async(
921 storage_, reinterpret_cast<const uint8_t(*)[16]>(goal_id)));
922 }
923
925 ::rclcpp::Result get_result_async(const ::rclcpp_action::GoalUUID& goal_id) {
926 return get_result_async(goal_id.data());
927 }
928
942 if (!initialized_) return ::rclcpp::Result(::rclcpp::ErrorCode::NotInitialized);
943 // Issue 2011 -- the client keeps its OWN callbacks installed (they feed
944 // the goal-keyed futures and the feedback slot) and forwards to these.
945 user_ = options;
946 return install_dispatch();
947 }
948
954 void poll() {
955 if (!initialized_) return;
956 // Issue 2034 — cancel replies included: they reach `on_cancel_reply`.
957 nros_cpp_action_client_poll(storage_);
958 }
959
961 bool is_valid() const { return initialized_; }
962
977 abandon_upstream(); // issue 2027 — waiting futures settle; no future names this client
978 accepts_.orphan_all();
979 results_.orphan_all();
980 if (initialized_) {
981 nros_cpp_action_client_destroy(storage_);
982 initialized_ = false;
983 }
984 feedback_stream_ = ::rclcpp::Stream<FeedbackType>();
985 }
986
987 // Move semantics (non-copyable). Relocation goes through the
988 // `nros_cpp_action_client_relocate` runtime call (Phase 84.C1), which also
989 // RE-POINTS the arena entry's callback `context` at the new `storage_`
990 // (issue 1496 — before that, a moved-from temporary left the entry naming
991 // dead bytes). The feedback stream is rebound to the new storage afterwards.
992 //
993 // Issue 2011 -- the pending futures and the client's own callbacks follow
994 // it too: both lists move (each future's storage pointer re-targeted) and
995 // the callbacks are re-installed with the new address as their context.
996 Client(Client&& other)
997 : executor_(other.executor_), initialized_(other.initialized_), action_name_{} {
998 ::memcpy(action_name_, other.action_name_, sizeof(action_name_));
999 adopt(other);
1000 }
1001
1003 if (this != &other) {
1004 abandon_upstream();
1005 accepts_.orphan_all();
1006 results_.orphan_all();
1007 if (initialized_) {
1008 nros_cpp_action_client_destroy(storage_);
1009 feedback_stream_ = ::rclcpp::Stream<FeedbackType>();
1010 }
1011 executor_ = other.executor_;
1012 initialized_ = other.initialized_;
1013 ::memcpy(action_name_, other.action_name_, sizeof(action_name_));
1014 adopt(other);
1015 }
1016 return *this;
1017 }
1018
1021 Client() : executor_(nullptr), initialized_(false), action_name_{} {}
1022
1023 private:
1024 Client(const Client&) = delete;
1025 Client& operator=(const Client&) = delete;
1026
1027 friend class ::rclcpp::Node;
1028
1030 ::rclcpp::Result install_dispatch() {
1031 // Issue 2027 — an abandoned future tells THIS client, which frees the
1032 // request's slot unless something else still waits on its reply.
1033 accepts_.forget = &Client::forget_accept;
1034 accepts_.forget_ctx = this;
1035 results_.forget = &Client::forget_result;
1036 results_.forget_ctx = this;
1038 storage_, &Client::on_goal_response, &Client::on_feedback, &Client::on_result, this);
1039 if (ret != 0) return ::rclcpp::Result(ret);
1040 // Issue 2034 — the executor's spin takes cancel replies and hands them
1041 // here (same `this` context).
1042 return ::rclcpp::Result(
1043 nros_cpp_action_client_set_cancel_reply_callback(storage_, &Client::on_cancel_reply));
1044 }
1045
1046 // Issue 2027 — the goal-slot leak. A request in the Rust core's bounded
1047 // per-sub-client table (`MAX_CONCURRENT_GOALS`) used to come back only
1048 // with its reply, so a future dropped first — with a reply that never
1049 // came — held its slot forever, and the fifth such goal refused every
1050 // later send. The future is the request's owner; when it is abandoned the
1051 // request is forgotten, UNLESS another owner still waits on that reply:
1052 // a client-wide callback (`set_callbacks`) or an upstream goal
1053 // (`async_send_goal` / `async_get_result`). A late reply for a forgotten
1054 // request is dropped by the correlator.
1055 static void forget_accept(void* ctx, const uint8_t goal[16]) {
1056 static_cast<Client*>(ctx)->forget_request(NROS_CPP_ACTION_CHANNEL_SEND_GOAL, goal);
1057 }
1058 static void forget_result(void* ctx, const uint8_t goal[16]) {
1059 static_cast<Client*>(ctx)->forget_request(NROS_CPP_ACTION_CHANNEL_GET_RESULT, goal);
1060 }
1061 void forget_request(int32_t channel, const uint8_t goal[16]) {
1062 if (!initialized_) return;
1063 const bool accept = channel == NROS_CPP_ACTION_CHANNEL_SEND_GOAL;
1064 if (accept ? user_.goal_response != nullptr : user_.result != nullptr) return;
1065 if (upstream_wants(accept, goal)) return;
1066 (void)nros_cpp_action_client_forget(storage_, channel,
1067 reinterpret_cast<const uint8_t(*)[16]>(goal));
1068 }
1069
1071 void adopt(Client& other) {
1072 user_ = other.user_;
1073 upstream_ = static_cast<detail::HostedBox&&>(other.upstream_);
1074 repoint_pump(); // issue 2027 — the futures this client handed out follow it
1075 feedback_len_ = other.feedback_len_;
1076 feedback_ready_ = other.feedback_ready_;
1077 for (size_t i = 0; i < CANCEL_STASH_CAP; ++i)
1078 stash_codes_[i] = other.stash_codes_[i];
1079 stash_head_ = other.stash_head_;
1080 stash_len_ = other.stash_len_;
1081 other.stash_len_ = 0;
1082 feedback_truncated_ = other.feedback_truncated_;
1083 for (size_t i = 0; i < feedback_len_; ++i)
1084 feedback_buf_[i] = other.feedback_buf_[i];
1085 other.feedback_ready_ = false;
1086 if (other.initialized_) {
1087 nros_cpp_action_client_relocate(other.storage_, storage_);
1088 other.accepts_.move_to(accepts_, storage_);
1089 other.results_.move_to(results_, storage_);
1090 other.initialized_ = false;
1091 (void)install_dispatch();
1092 }
1093 other.feedback_stream_ = ::rclcpp::Stream<FeedbackType>();
1094 }
1095
1096 // Issue 2011 -- the three callbacks every `Client<A>` keeps installed. The
1097 // executor's dispatch (and `nros_cpp_action_client_poll`) hands each reply
1098 // here, attributed to the goal its request named (issue 2009); it goes to
1099 // the future waiting on THAT goal on THIS client, if any -- dropped
1100 // otherwise, never handed to another goal -- and then to the user's
1101 // callback (`set_callbacks`).
1102 static void on_goal_response(bool accepted, const uint8_t goal_id[16], void* ctx) {
1103 Client* self = static_cast<Client*>(ctx);
1104 ::rclcpp::detail::PendingLink* owner = self->accepts_.resolve_goal(goal_id);
1105 if (owner != nullptr) {
1106 // The 17-byte layout `GoalAccept::ffi_deserialize` decodes
1107 // (`nros_cpp_action_goal_accept_decode`).
1108 uint8_t wire[17];
1109 for (int i = 0; i < 16; ++i)
1110 wire[i] = goal_id[i];
1111 wire[16] = accepted ? 1 : 0;
1112 self->accepts_.deliver(owner, wire, sizeof(wire));
1113 }
1114 self->upstream_goal_response(accepted, goal_id);
1115 if (self->user_.goal_response != nullptr)
1116 self->user_.goal_response(accepted, goal_id, self->user_.context);
1117 }
1118
1119 static void on_feedback(const uint8_t goal_id[16], const uint8_t* data, size_t len, void* ctx) {
1120 Client* self = static_cast<Client*>(ctx);
1121 const size_t n = len < sizeof(self->feedback_buf_) ? len : sizeof(self->feedback_buf_);
1122 for (size_t i = 0; i < n; ++i)
1123 self->feedback_buf_[i] = data[i];
1124 self->feedback_len_ = n;
1125 self->feedback_truncated_ = n < len;
1126 self->feedback_ready_ = true;
1127 self->upstream_feedback(goal_id, data, len);
1128 if (self->user_.feedback != nullptr)
1129 self->user_.feedback(goal_id, data, len, self->user_.context);
1130 }
1131
1132 static void on_result(const uint8_t goal_id[16], int32_t status, const uint8_t* data,
1133 size_t len, void* ctx) {
1134 Client* self = static_cast<Client*>(ctx);
1135 ::rclcpp::detail::PendingLink* owner = self->results_.resolve_goal(goal_id);
1136 if (owner != nullptr) self->results_.deliver(owner, data, len);
1137 self->upstream_result(goal_id, status, data, len);
1138 if (self->user_.result != nullptr)
1139 self->user_.result(goal_id, status, data, len, self->user_.context);
1140 }
1141
1142 // Issue 2015 — upstream's per-goal callbacks (`async_send_goal`). Hosted
1143 // only: `upstream_` is empty on a freestanding build, so these do nothing.
1144#ifdef NROS_CPP_HAS_SHARED_PTR // hosted-family: action-interop
1150 struct UpstreamGoal {
1151 GoalUUID id;
1152 SendGoalOptions options;
1154 ::std::weak_ptr<GoalHandle> weak;
1155 bool accepted = false;
1156 };
1157#ifdef NROS_CPP_HAS_STD_PROMISE // hosted-family: action-interop
1160 struct CancelWaiter {
1161 GoalUUID key;
1164 int64_t seq;
1165 ::std::promise<typename CancelResponse::SharedPtr> promise;
1166 CancelCallback callback;
1167 };
1168#endif
1169 struct UpstreamState {
1170 ::std::vector<UpstreamGoal> goals;
1171#ifdef NROS_CPP_HAS_STD_PROMISE // hosted-family: action-interop
1172 ::std::vector<CancelWaiter> cancels;
1173#endif
1174 ::std::shared_ptr<detail::PumpCell> pump;
1175 };
1176
1177 UpstreamState* upstream_state() {
1178 UpstreamState* st = upstream_.template as<UpstreamState>();
1179 if (st == nullptr) {
1180 st = new UpstreamState();
1181 upstream_.reset(st, [](void* p) { delete static_cast<UpstreamState*>(p); });
1182 }
1183 return st;
1184 }
1185
1186 UpstreamState* upstream_goal(const uint8_t goal_id[16], size_t& at) const {
1187 UpstreamState* st = upstream_.template as<UpstreamState>();
1188 if (st == nullptr) return nullptr;
1189 const GoalUUID id(goal_id);
1190 for (at = 0; at < st->goals.size(); ++at)
1191 if (st->goals[at].id == id) return st;
1192 return nullptr;
1193 }
1194
1196 static void prune_upstream(UpstreamState* st) {
1197 for (size_t i = 0; i < st->goals.size();) {
1198 const UpstreamGoal& g = st->goals[i];
1199 if (g.accepted && !g.handle && g.weak.expired())
1200 st->goals.erase(st->goals.begin() + static_cast<long>(i));
1201 else
1202 ++i;
1203 }
1204 }
1205
1207 ::std::shared_ptr<detail::PumpCell> pump_cell() {
1208 UpstreamState* st = upstream_state();
1209 if (!st->pump) {
1210 st->pump = ::std::make_shared<detail::PumpCell>();
1211 st->pump->fn = &Client::pump_entry;
1212 }
1213 st->pump->ctx = this;
1214 st->pump->executor = executor_;
1215 return st->pump;
1216 }
1217
1218 static void pump_entry(void* ctx) {
1219 Client* self = static_cast<Client*>(ctx);
1220 if (!self->initialized_) return;
1221 (void)nros_cpp_action_client_poll(self->storage_); // cancels too (issue 2034)
1222 }
1223#endif
1224
1230 bool upstream_wants(bool accept, const uint8_t goal_id[16]) {
1231#ifdef NROS_CPP_HAS_SHARED_PTR // hosted-family: action-interop
1232 size_t at = 0;
1233 UpstreamState* st = upstream_goal(goal_id, at);
1234 if (st == nullptr) return false;
1235 UpstreamGoal& g = st->goals[at];
1236 if (accept) {
1237 if (g.accepted) return false;
1238 const bool wanted = static_cast<bool>(g.options.goal_response_callback) ||
1239 static_cast<bool>(g.options.feedback_callback) ||
1240 static_cast<bool>(g.options.result_callback);
1241 if (!wanted) st->goals.erase(st->goals.begin() + static_cast<long>(at));
1242 return wanted;
1243 }
1244 return g.handle && g.handle->result_aware_;
1245#else
1246 (void)accept;
1247 (void)goal_id;
1248 return false;
1249#endif
1250 }
1251
1253 void repoint_pump() {
1254#ifdef NROS_CPP_HAS_SHARED_PTR // hosted-family: action-interop
1255 UpstreamState* st = upstream_.template as<UpstreamState>();
1256 if (st != nullptr && st->pump) {
1257 st->pump->ctx = this;
1258 st->pump->executor = executor_;
1259 }
1260#endif
1261 }
1262
1267 void abandon_upstream() {
1268#ifdef NROS_CPP_HAS_SHARED_PTR // hosted-family: action-interop
1269 UpstreamState* st = upstream_.template as<UpstreamState>();
1270 if (st == nullptr) return;
1271 if (st->pump) st->pump->ctx = nullptr;
1272#ifdef NROS_CPP_HAS_STD_PROMISE // hosted-family: action-interop
1273 for (CancelWaiter& w : st->cancels)
1274 w.promise.set_value(typename CancelResponse::SharedPtr());
1275 st->cancels.clear();
1276 for (UpstreamGoal& g : st->goals) {
1277 if (g.handle && g.handle->result_aware_) {
1278 WrappedResult wrapped;
1279 wrapped.goal_id = g.id;
1280 wrapped.code = ResultCode::UNKNOWN;
1281 g.handle->set_result(wrapped);
1282 }
1283 }
1284#endif
1285 upstream_.reset();
1286#endif
1287 }
1288
1289#ifdef NROS_CPP_HAS_STD_PROMISE // hosted-family: action-interop
1290 detail::DrivenSharedFuture<typename CancelResponse::SharedPtr>
1291 send_cancel(const GoalUUID& key, int32_t sec, uint32_t nanosec, CancelCallback& cb) {
1292 using Fut = detail::DrivenSharedFuture<typename CancelResponse::SharedPtr>;
1293 if (!initialized_) return Fut();
1295 if (nros_cpp_action_client_cancel_goals(storage_,
1296 reinterpret_cast<const uint8_t(*)[16]>(key.data()),
1297 sec, nanosec, &seq) != 0)
1298 return Fut();
1299 UpstreamState* st = upstream_state();
1300 CancelWaiter w;
1301 w.key = key;
1302 w.seq = seq;
1303 w.callback = cb;
1304 ::std::shared_future<typename CancelResponse::SharedPtr> f = w.promise.get_future().share();
1305 st->cancels.push_back(static_cast<CancelWaiter&&>(w));
1306 return Fut(f, pump_cell());
1307 }
1308#endif
1309
1319 static void on_cancel_reply(const uint8_t goal[16], int64_t seq, const uint8_t* reply,
1320 size_t len, void* ctx) {
1321 Client* self = static_cast<Client*>(ctx);
1322 int8_t code = 0;
1323 size_t count = 0;
1324 nros_cpp_action_goal_info_t infos[::rclcpp_action::MAX_CANCEL_INFOS];
1325 const bool taken =
1326 nros_cpp_action_cancel_reply_decode(reply, len, &code, infos,
1328 if (self->upstream_cancel_reply(goal, seq, taken, code, infos, count)) return;
1329 if (taken) self->stash_cancel(code);
1330 }
1331
1333 void stash_cancel(int8_t code) {
1334 if (stash_len_ == CANCEL_STASH_CAP) {
1335 stash_head_ = static_cast<uint8_t>((stash_head_ + 1) % CANCEL_STASH_CAP);
1336 --stash_len_;
1337 }
1338 stash_codes_[(stash_head_ + stash_len_) % CANCEL_STASH_CAP] = code;
1339 ++stash_len_;
1340 }
1341
1342 bool take_stray_cancel(int8_t& code) {
1343 if (stash_len_ == 0) return false;
1344 code = stash_codes_[stash_head_];
1345 stash_head_ = static_cast<uint8_t>((stash_head_ + 1) % CANCEL_STASH_CAP);
1346 --stash_len_;
1347 return true;
1348 }
1349
1352 bool upstream_cancel_reply(const uint8_t goal[16], int64_t seq, bool taken, int8_t code,
1353 const nros_cpp_action_goal_info_t* infos, size_t count) {
1354#ifdef NROS_CPP_HAS_STD_PROMISE // hosted-family: action-interop
1355 UpstreamState* st = upstream_.template as<UpstreamState>();
1356 if (st == nullptr) return false;
1357 const GoalUUID key(goal);
1358 size_t at = 0;
1359 while (at < st->cancels.size() &&
1360 !(seq != ::rclcpp::detail::SEQUENCE_ID_UNKNOWN ? st->cancels[at].seq == seq
1361 : st->cancels[at].key == key))
1362 ++at;
1363 if (at == st->cancels.size()) return false;
1364 CancelWaiter w = static_cast<CancelWaiter&&>(st->cancels[at]);
1365 st->cancels.erase(st->cancels.begin() + static_cast<long>(at));
1366 if (!taken) { // malformed: the request is over, with no response
1367 w.promise.set_value(typename CancelResponse::SharedPtr());
1368 if (w.callback) w.callback(typename CancelResponse::SharedPtr());
1369 return true;
1370 }
1371 typename CancelResponse::SharedPtr resp = ::std::make_shared<CancelResponse>();
1372 resp->return_code = code;
1373 const size_t n =
1375 for (size_t k = 0; k < n; ++k) {
1376 detail::CancelGoalInfo info;
1377 for (int b = 0; b < 16; ++b)
1378 info.goal_id.uuid[b] = infos[k].uuid[b];
1379 info.stamp.sec = infos[k].stamp_sec;
1380 info.stamp.nanosec = infos[k].stamp_nanosec;
1381 resp->goals_canceling.push_back(info);
1382 }
1383 w.promise.set_value(resp);
1384 if (w.callback) w.callback(resp);
1385 return true;
1386#else
1387 (void)goal;
1388 (void)seq;
1389 (void)taken;
1390 (void)code;
1391 (void)infos;
1392 (void)count;
1393 return false;
1394#endif
1395 }
1396
1397 void upstream_goal_response(bool accepted, const uint8_t goal_id[16]) {
1398#ifdef NROS_CPP_HAS_SHARED_PTR // hosted-family: action-interop
1399 size_t at = 0;
1400 UpstreamState* st = upstream_goal(goal_id, at);
1401 if (st == nullptr) return;
1402 UpstreamGoal& g = st->goals[at];
1403 if (g.accepted) return; // a duplicate acceptance
1404 auto cb = g.options.goal_response_callback;
1405 if (!accepted) {
1406 st->goals.erase(st->goals.begin() + static_cast<long>(at));
1407 if (cb) cb(nullptr);
1408 return;
1409 }
1410 typename GoalHandle::SharedPtr handle = ::std::make_shared<GoalHandle>(g.id);
1411 handle->feedback_aware_ = static_cast<bool>(g.options.feedback_callback);
1412 handle->result_aware_ = static_cast<bool>(g.options.result_callback);
1413 g.accepted = true;
1414 if (handle->result_aware_)
1415 g.handle = handle; // the pending result request keeps it, as upstream's does
1416 else
1417 g.weak = handle; // feedback-only: the caller's copy keeps it
1418 if (!handle->feedback_aware_ && !handle->result_aware_)
1419 st->goals.erase(st->goals.begin() + static_cast<long>(at));
1420 // `g` may be gone from here on: the callback can send another goal.
1421 if (cb) cb(handle);
1422 if (handle->result_aware_ && !get_result_async(goal_id).ok()) {
1423 // Nobody could ever answer it: unbind, as upstream would have thrown.
1424 handle->result_aware_ = false;
1425 if (upstream_goal(goal_id, at) != nullptr) {
1426 st->goals[at].handle.reset();
1427 st->goals[at].weak = handle;
1428 }
1429 }
1430#else
1431 (void)accepted;
1432 (void)goal_id;
1433#endif
1434 }
1435
1436 void upstream_feedback(const uint8_t goal_id[16], const uint8_t* data, size_t len) {
1437#ifdef NROS_CPP_HAS_SHARED_PTR // hosted-family: action-interop
1438 size_t at = 0;
1439 UpstreamState* st = upstream_goal(goal_id, at);
1440 if (st == nullptr) return;
1441 UpstreamGoal& g = st->goals[at];
1442 typename GoalHandle::SharedPtr handle = g.handle ? g.handle : g.weak.lock();
1443 if (!handle) {
1444 // A feedback-only goal whose handle the caller dropped (issue 2027).
1445 if (g.accepted) st->goals.erase(st->goals.begin() + static_cast<long>(at));
1446 return;
1447 }
1448 auto cb = g.options.feedback_callback;
1449 if (!cb) return;
1450 ::std::shared_ptr<FeedbackType> fb = ::std::make_shared<FeedbackType>();
1451 if (FeedbackType::ffi_deserialize(data, len, fb.get()) != 0) return;
1452 cb(handle, fb);
1453#else
1454 (void)goal_id;
1455 (void)data;
1456 (void)len;
1457#endif
1458 }
1459
1460 void upstream_result(const uint8_t goal_id[16], int32_t status, const uint8_t* data,
1461 size_t len) {
1462#ifdef NROS_CPP_HAS_SHARED_PTR // hosted-family: action-interop
1463 size_t at = 0;
1464 UpstreamState* st = upstream_goal(goal_id, at);
1465 if (st == nullptr) return;
1466 UpstreamGoal g = st->goals[at];
1467 st->goals.erase(st->goals.begin() + static_cast<long>(at));
1468 typename GoalHandle::SharedPtr handle = g.handle ? g.handle : g.weak.lock();
1469 if (handle) handle->status_ = static_cast<int8_t>(status);
1470 WrappedResult wrapped;
1471 wrapped.goal_id = g.id;
1472 wrapped.code = (status == 4 || status == 5 || status == 6) ? static_cast<ResultCode>(status)
1473 : ResultCode::UNKNOWN;
1474 wrapped.result = ::std::make_shared<ResultType>();
1475 if (ResultType::ffi_deserialize(data, len, wrapped.result.get()) != 0) {
1476 wrapped.result.reset();
1477 }
1478#ifdef NROS_CPP_HAS_STD_PROMISE // hosted-family: action-interop
1479 if (handle) handle->set_result(wrapped);
1480#endif
1481 if (g.options.result_callback) g.options.result_callback(wrapped);
1482#else
1483 (void)goal_id;
1484 (void)status;
1485 (void)data;
1486 (void)len;
1487#endif
1488 }
1489
1493 static nros_cpp_ret_t take_feedback(void* ctx, uint8_t* buf, size_t cap, size_t* out_len) {
1494 Client* self = static_cast<Client*>(ctx);
1495 *out_len = 0;
1496 if (!self->initialized_)
1498 if (!self->feedback_ready_) (void)nros_cpp_action_client_poll(self->storage_);
1499 if (!self->feedback_ready_) return 0;
1500 self->feedback_ready_ = false;
1501 if (self->feedback_truncated_ || self->feedback_len_ > cap) {
1502 *out_len = self->feedback_len_;
1503 return static_cast<nros_cpp_ret_t>(::rclcpp::ErrorCode::Error);
1504 }
1505 for (size_t i = 0; i < self->feedback_len_; ++i)
1506 buf[i] = self->feedback_buf_[i];
1507 *out_len = self->feedback_len_;
1508 return 0;
1509 }
1510
1511 alignas(8) uint8_t storage_[NROS_CPP_ACTION_CLIENT_STORAGE_SIZE];
1512 void* executor_; // Stashed executor handle (Phase 82) for blocking helpers
1513 bool initialized_;
1514 ::rclcpp::Stream<FeedbackType> feedback_stream_;
1515 // Phase 87.6 put a `char action_name_[256]` here for an accessor that was
1516 // never written, and an earlier phase-417 W4.b wave deleted it as dead
1517 // state. W4.b's second half writes `get_action_name()`, so it is back and
1518 // it is read — see the note on `Server<A>::get_action_name`.
1519 char action_name_[::rclcpp_action::ACTION_NAME_MAX];
1528 SendGoalOptions user_;
1530 uint8_t feedback_buf_[::rclcpp::rx_buffer_capacity<FeedbackType>::value];
1531 size_t feedback_len_ = 0;
1532 bool feedback_ready_ = false;
1533 bool feedback_truncated_ = false;
1536 int8_t stash_codes_[CANCEL_STASH_CAP] = {};
1537 uint8_t stash_head_ = 0;
1538 uint8_t stash_len_ = 0;
1542 detail::HostedBox upstream_;
1543};
1544
1545} // namespace rclcpp_action
1546
1547// Phase 84.G8: out-of-line definition of Node::create_action_client<A>().
1548#include "rclcpp/node.hpp"
1549
1550namespace rclcpp {} // namespace rclcpp
1551
1552namespace rclcpp {
1553template <typename A>
1555 const ::rclcpp::QoS& qos) {
1556 if (!initialized_) return Result(::rclcpp::ErrorCode::NotInitialized);
1558 const nros_cpp_action_hashes_t hashes = ::rclcpp::detail::action_endpoint_hashes<A>();
1559 nros_cpp_ret_t ret = nros_cpp_action_client_create(
1560 &handle_, action_name, A::TYPE_NAME, A::Goal::TYPE_HASH, &hashes, ffi_qos, out.storage_);
1561 if (ret == 0) {
1562 out.executor_ = executor_handle_;
1563 // Issue 2011 -- the client's own callbacks, from the start: they feed
1564 // its goal-keyed futures and its feedback slot.
1565 out.initialized_ = true;
1566 (void)out.install_dispatch();
1567 // phase-417 W4.b — remember the name for `get_action_name()`, truncating
1568 // at `rclcpp_action::ACTION_NAME_MAX` exactly as the server and the polling tiers do.
1569 ::rclcpp::detail::assign_entity_name(out.action_name_, action_name);
1570 out.initialized_ = true;
1571 }
1572 return Result(ret);
1573}
1574} // namespace rclcpp
1575
1576namespace rclcpp {} // namespace rclcpp
1577
1578#endif // NROS_RCLCPP_ACTION_CLIENT_HPP
Definition client.hpp:130
Definition future.hpp:337
Definition node.hpp:845
Result create_action_client(::rclcpp_action::Client< A > &out, const char *action_name, const ::rclcpp::QoS &qos=::rclcpp::QoS::services())
Definition client.hpp:1554
bool ok() const
Returns true if the operation succeeded.
Definition result.hpp:227
Definition stream.hpp:62
Definition client.hpp:129
typename A::Result Result
Definition client.hpp:132
detail::Refused< ClientGoalHandle > SharedPtr
Definition client.hpp:147
bool is_feedback_aware() const
A feedback callback was given for this goal.
Definition client.hpp:163
const GoalUUID & get_goal_id() const
The goal's id.
Definition client.hpp:159
int8_t get_status() const
The last status this client saw for the goal (action_msgs numbers).
Definition client.hpp:161
ClientGoalHandle(const ClientGoalHandle &)=delete
ClientGoalHandle & operator=(const ClientGoalHandle &)=delete
detail::Refused< ClientGoalHandle > ConstSharedPtr
Definition client.hpp:148
detail::Refused< ClientGoalHandle > WrappedResult
Definition client.hpp:149
typename A::Feedback Feedback
Definition client.hpp:131
bool is_result_aware() const
A result callback was given for this goal (its result was requested).
Definition client.hpp:165
ClientGoalHandle(const GoalUUID &id)
Definition client.hpp:152
Definition client.hpp:256
::rclcpp::Result send_goal(const GoalType &goal, uint8_t goal_id[16])
Definition client.hpp:406
::rclcpp_action::ClientGoalHandle< A > GoalHandle
Upstream's goal handle for this action (issue 2015).
Definition client.hpp:267
::rclcpp::Result wait_for_action_server(uint32_t timeout_ms)
Definition client.hpp:337
typename A::Result ResultType
Definition client.hpp:259
::rclcpp::Result send_goal(const GoalType &goal, ::rclcpp_action::GoalUUID &goal_id)
Definition client.hpp:420
typename A::Feedback FeedbackType
Definition client.hpp:260
bool action_server_is_ready() const
Definition client.hpp:379
::rclcpp::Result get_result(const uint8_t goal_id[16], ResultType &result)
Definition client.hpp:432
::rclcpp::Result wait_for_action_server()
REFUSED — wait_for_action_server() with no budget.
Definition client.hpp:344
::rclcpp::Result get_result(const ::rclcpp_action::GoalUUID &goal_id, ResultType &result)
get_result taking a GoalUUID value (phase-417 W4.b).
Definition client.hpp:437
detail::Refused< Client > CancelResponse
Definition client.hpp:277
::rclcpp::Owned< Client > SharedPtr
Definition client.hpp:265
const char * get_action_name() const
Definition client.hpp:391
typename GoalHandle::WrappedResult WrappedResult
Upstream's result wrapper (hosted; refused on a freestanding build).
Definition client.hpp:269
::rclcpp::Result get_result_sized(const uint8_t goal_id[16], ResultType &result)
Definition client.hpp:445
typename A::Goal GoalType
Definition client.hpp:258
Inline storage-size macros for opaque entity buffers.
rclcpp::Future<T> — single-shot deferred result.
int nros_cpp_ret_t
Definition future.hpp:25
constexpr nros_cpp_qos_t qos_to_ffi(const QoS &qos)
Definition qos.hpp:719
nros_cpp_action_hashes_t action_endpoint_hashes()
Definition server.hpp:89
constexpr int64_t SEQUENCE_ID_UNKNOWN
Definition future.hpp:59
Definition node.hpp:203
::rclcpp::Result send_goal_async(const GoalType &goal, uint8_t goal_id[16])
Definition client.hpp:679
::rclcpp::Result try_recv_feedback(FeedbackType &feedback)
Definition client.hpp:578
::rclcpp::Future< GoalAccept > async_send_goal(const GoalType &, const SendGoalOptions &=SendGoalOptions())
REFUSED on a freestanding build — issue 2015.
Definition client.hpp:731
::rclcpp::Stream< FeedbackType > & feedback_stream()
Definition client.hpp:620
GoalStatus
Definition server.hpp:264
@ Accepted
Definition server.hpp:266
::rclcpp::Result cancel_goal(const uint8_t goal_id[16])
Definition client.hpp:874
static const size_t ACTION_NAME_MAX
Definition server.hpp:191
::rclcpp::Result try_recv_cancel_response(::rclcpp_action::CancelReturnCode &out)
Definition client.hpp:896
void poll()
Definition client.hpp:954
void async_cancel_goals_before(Args &&...)
REFUSED without std::promise — issue 2027.
Definition client.hpp:855
::rclcpp::Result try_recv_feedback_sized(FeedbackType &feedback)
Definition client.hpp:584
static const size_t CANCEL_STASH_CAP
Definition client.hpp:114
::rclcpp::Future< ResultType, Cap > get_result_future_sized(const uint8_t goal_id[16])
Definition client.hpp:555
::rclcpp::Result get_result_async(const uint8_t goal_id[16])
Definition client.hpp:918
Client & operator=(Client &&other)
Definition client.hpp:1002
static const size_t MAX_CANCEL_INFOS
Definition client.hpp:107
void async_cancel_all_goals(Args &&...)
REFUSED without std::promise — issue 2027.
Definition client.hpp:851
CancelReturnCode
Definition server.hpp:255
::rclcpp::Result set_callbacks(const SendGoalOptions &options)
Definition client.hpp:941
bool is_valid() const
Check if the action client is initialized and valid.
Definition client.hpp:961
ResultCode
Definition client.hpp:96
@ ABORTED
Definition client.hpp:100
@ UNKNOWN
Definition client.hpp:97
@ CANCELED
Definition client.hpp:99
@ SUCCEEDED
Definition client.hpp:98
::rclcpp::Future< ResultType > get_result_future(const uint8_t goal_id[16])
Definition client.hpp:539
void async_cancel_goal(Args &&...)
REFUSED without std::promise — issue 2027.
Definition client.hpp:847
Definition rclcpp.hpp:75
bool ok()
Definition node.hpp:3564
@ Error
Generic failure not covered by a more specific code.
Definition result.hpp:118
@ NotInitialized
Definition result.hpp:131
@ Unsupported
Operation not implemented by the active backend.
Definition result.hpp:152
@ TryAgain
Transient — no data ready yet (non-blocking take). Retry later.
Definition result.hpp:148
ResultOf< void > Result
Definition result.hpp:247
rclcpp::Node and global session helpers.
**auto handle
Definition client.hpp:127
nros_cpp_ret_t nros_cpp_action_client_set_callbacks(void *handle, nros_cpp_action_client_goal_response_callback_t goal_response, nros_cpp_action_client_feedback_callback_t feedback, nros_cpp_action_client_result_callback_t result, void *context)
nros_cpp_ret_t nros_cpp_action_client_set_cancel_reply_callback(void *handle, nros_cpp_action_client_cancel_reply_callback_t cb)
typename GoalHandle::WrappedResult WrappedResult
Upstream's result wrapper (hosted; refused on a freestanding build).
Definition client.hpp:491
typename A::Goal GoalType
Definition client.hpp:480
typename A::Feedback FeedbackType
Definition client.hpp:482
void(* nros_cpp_action_client_goal_response_callback_t)(bool accepted, const uint8_t goal_id[16], void *ctx)
Definition client.hpp:43
::rclcpp::Result send_goal(const GoalType &goal, uint8_t goal_id[16])
Definition client.hpp:628
::rclcpp::Result wait_for_action_server()
REFUSED — wait_for_action_server() with no budget.
Definition client.hpp:566
void(* nros_cpp_action_client_feedback_callback_t)(const uint8_t goal_id[16], const uint8_t *data, size_t len, void *ctx)
Definition client.hpp:46
void(* nros_cpp_action_client_cancel_reply_callback_t)(const uint8_t goal_id[16], int64_t seq, const uint8_t *reply, size_t reply_len, void *ctx)
Definition client.hpp:60
::rclcpp::Owned< Client > SharedPtr
Definition client.hpp:487
void(* nros_cpp_action_client_result_callback_t)(const uint8_t goal_id[16], int32_t status, const uint8_t *data, size_t len, void *ctx)
Definition client.hpp:49
detail::Refused< Client > CancelResponse
Definition client.hpp:499
rclcpp::Result, rclcpp::ErrorCode, and the RCLCPP_TRY macro.
rclcpp::Stream<T> — multi-shot message receiver.
Definition qos.hpp:84
Definition future.hpp:126
void * forget_ctx
Definition future.hpp:136
void(* forget)(void *ctx, const uint8_t goal[16])
Definition future.hpp:135
Definition client.hpp:292
static int ffi_deserialize(const uint8_t *data, size_t len, GoalAccept *out)
Definition client.hpp:306
static const size_t SERIALIZED_SIZE_MAX
Definition client.hpp:301
uint8_t goal_id[16]
Definition client.hpp:296
bool accepted
Definition client.hpp:297
Definition server.hpp:126
uint8_t * data()
Pointer to the 16 bytes — what the raw-array overloads and the FFI take.
Definition server.hpp:144
Definition client.hpp:647
SendGoalOptions()
Definition client.hpp:667
detail::UpstreamCallback< void(typename GoalHandle::SharedPtr, FeedbackPtr)> feedback_callback
Upstream's: feedback for the goal.
Definition client.hpp:662
void(* feedback)(const uint8_t goal_id[16], const uint8_t *data, size_t len, void *ctx)
Called when feedback is received for the goal.
Definition client.hpp:651
detail::UpstreamCallback< void(const WrappedResult &)> result_callback
Definition client.hpp:665
void(* goal_response)(bool accepted, const uint8_t goal_id[16], void *ctx)
Called when the server accepts or rejects the goal.
Definition client.hpp:649
void(* result)(const uint8_t goal_id[16], int32_t status, const uint8_t *data, size_t len, void *ctx)
Called when the result is received.
Definition client.hpp:653
detail::UpstreamCallback< void(typename GoalHandle::SharedPtr)> goal_response_callback
Upstream's: the goal was accepted (a handle) or rejected (null).
Definition client.hpp:659
void * context
User context pointer passed to all callbacks.
Definition client.hpp:656