diff --git a/include/libcyphal/transport/can/can_transport_impl.hpp b/include/libcyphal/transport/can/can_transport_impl.hpp index c61bf9016..d4be277bd 100644 --- a/include/libcyphal/transport/can/can_transport_impl.hpp +++ b/include/libcyphal/transport/can/can_transport_impl.hpp @@ -11,6 +11,7 @@ #include "media.hpp" #include "msg_rx_session.hpp" #include "msg_tx_session.hpp" +#include "rx_session_tree_node.hpp" #include "svc_rx_sessions.hpp" #include "svc_tx_sessions.hpp" @@ -19,6 +20,7 @@ #include "libcyphal/transport/errors.hpp" #include "libcyphal/transport/lizard_helpers.hpp" #include "libcyphal/transport/msg_sessions.hpp" +#include "libcyphal/transport/session_tree.hpp" #include "libcyphal/transport/svc_sessions.hpp" #include "libcyphal/transport/types.hpp" #include "libcyphal/types.hpp" @@ -167,8 +169,7 @@ class TransportImpl final : private TransportDelegate, public ICanTransport : TransportDelegate{memory} , executor_{executor} , media_array_{std::move(media_array)} - , total_msg_rx_ports_{0} - , total_svc_rx_ports_{0} + , svc_response_rx_session_nodes_{memory} { scheduleConfigOfFilters(); } @@ -187,9 +188,7 @@ class TransportImpl final : private TransportDelegate, public ICanTransport flushCanardTxQueue(media.canard_tx_queue(), canardInstance()); } - CETL_DEBUG_ASSERT(total_msg_rx_ports_ == 0, // - "Message sessions must be destroyed before transport."); - CETL_DEBUG_ASSERT(total_svc_rx_ports_ == 0, // + CETL_DEBUG_ASSERT(svc_response_rx_session_nodes_.isEmpty(), "Service sessions must be destroyed before transport."); } @@ -245,7 +244,8 @@ class TransportImpl final : private TransportDelegate, public ICanTransport // // @see scheduleConfigOfFilters // - if (total_svc_rx_ports_ > 0) + const auto& subs_stats = getSubscriptionStats(); + if (subs_stats.total_svc_rx_ports > 0) { const bool result = configure_filters_callback_.schedule(Callback::Schedule::Once{executor_.now()}); (void) result; @@ -271,41 +271,43 @@ class TransportImpl final : private TransportDelegate, public ICanTransport CETL_NODISCARD Expected, AnyFailure> makeMessageRxSession( const MessageRxParams& params) override { - return makeRxSession(CanardTransferKindMessage, params.subject_id, params); + return makeRxSessionImpl( // + CanardTransferKindMessage, + params.subject_id, + params); } CETL_NODISCARD Expected, AnyFailure> makeMessageTxSession( const MessageTxParams& params) override { - return MessageTxSession::make(asDelegate(), params); + return MessageTxSession::make(memory(), asDelegate(), params); } CETL_NODISCARD Expected, AnyFailure> makeRequestRxSession( const RequestRxParams& params) override { - return makeRxSession(CanardTransferKindRequest, - params.service_id, - params); + return makeRxSessionImpl( // + CanardTransferKindRequest, + params.service_id, + params); } CETL_NODISCARD Expected, AnyFailure> makeRequestTxSession( const RequestTxParams& params) override { - return SvcRequestTxSession::make(asDelegate(), params); + return SvcRequestTxSession::make(memory(), asDelegate(), params); } CETL_NODISCARD Expected, AnyFailure> makeResponseRxSession( const ResponseRxParams& params) override { - return makeRxSession(CanardTransferKindResponse, - params.service_id, - params); + return makeResponseRxSessionImpl(params); } CETL_NODISCARD Expected, AnyFailure> makeResponseTxSession( const ResponseTxParams& params) override { - return SvcResponseTxSession::make(asDelegate(), params); + return SvcResponseTxSession::make(memory(), asDelegate(), params); } // MARK: TransportDelegate @@ -358,16 +360,39 @@ class TransportImpl final : private TransportDelegate, public ICanTransport return cetl::nullopt; } - void onSessionEvent(const SessionEvent::Variant& event_var) override + void onSessionEvent(const SessionEvent::Variant& event_var) noexcept override { - SessionEventHandler handler_with{*this}; - cetl::visit(handler_with, event_var); + // `visit` might hypothetically throw, so we need to catch it. + const auto result = libcyphal::detail::performWithoutThrowing([this, &event_var] { + // + cetl::visit(cetl::make_overloaded( // + [this](const SessionEvent::SvcResponseDestroyed& event) noexcept { + // + svc_response_rx_session_nodes_.removeNodeFor(event.params); + }, + [](const auto&) noexcept { + // No specific action needed for other events. + // But we still might need to reconfigure filters (see below after `visit`). + }), + event_var); + }); + (void) result; + CETL_DEBUG_ASSERT(result, ""); cancelRxCallbacksIfNoPortsLeft(); scheduleConfigOfFilters(); } - void scheduleConfigOfFilters() + IRxSessionDelegate* tryFindRxSessionDelegateFor(const ResponseRxParams& params) override + { + if (auto* const node = svc_response_rx_session_nodes_.tryFindNodeFor(params)) + { + return node->delegate(); + } + return nullptr; + } + + void scheduleConfigOfFilters() noexcept { if (!configure_filters_callback_) { @@ -386,61 +411,61 @@ class TransportImpl final : private TransportDelegate, public ICanTransport using Self = TransportImpl; - struct SessionEventHandler + template + using SessionTree = transport::detail::SessionTree; + + template + CETL_NODISCARD auto makeRxSessionImpl( // + const CanardTransferKind transfer_kind, + const PortId port_id, + const Params& params) -> Expected, AnyFailure> { - explicit SessionEventHandler(Self& self) - : self_{self} + const std::int8_t has_port = ::canardRxGetSubscription(&canardInstance(), transfer_kind, port_id, nullptr); + CETL_DEBUG_ASSERT(has_port >= 0, "There is no way currently to get an error here."); + if (has_port > 0) { + return AlreadyExistsError{}; } - void operator()(const SessionEvent::MsgRxLifetime& lifetime) const + auto session_result = Factory::make(memory(), asDelegate(), params); + if (auto* const make_failure = cetl::get_if(&session_result)) { - if (lifetime.is_added) - { - ++self_.total_msg_rx_ports_; - } - else - { - // We are not going to allow negative number of ports. - CETL_DEBUG_ASSERT(self_.total_msg_rx_ports_ > 0, ""); - self_.total_msg_rx_ports_ -= std::min(static_cast(1), self_.total_msg_rx_ports_); - } + return std::move(*make_failure); } - void operator()(const SessionEvent::SvcRxLifetime& lifetime) const + for (Media& media : media_array_) { - if (lifetime.is_added) - { - ++self_.total_svc_rx_ports_; - } - else + if (!media.rx_callback()) { - // We are not going to allow negative number of ports. - CETL_DEBUG_ASSERT(self_.total_svc_rx_ports_ > 0, ""); - self_.total_svc_rx_ports_ -= std::min(static_cast(1), self_.total_svc_rx_ports_); + media.rx_callback() = media.interface().registerPopCallback([this, &media](const auto&) { // + // + receiveNextFrame(media); + }); } } - private: - Self& self_; - - }; // SessionEventHandler + return session_result; + } - template - CETL_NODISCARD auto makeRxSession(const CanardTransferKind transfer_kind, - const PortId port_id, - const RxParams& rx_params) -> Expected, AnyFailure> + CETL_NODISCARD auto makeResponseRxSessionImpl( // + const ResponseRxParams& params) -> Expected, AnyFailure> { - const std::int8_t has_port = ::canardRxGetSubscription(&canardInstance(), transfer_kind, port_id, nullptr); - CETL_DEBUG_ASSERT(has_port >= 0, "There is no way currently to get an error here."); - if (has_port > 0) + // Make sure that session is unique per given parameters. + // For response sessions, the uniqueness is based on the service ID and the server node ID. + // + auto node_result = svc_response_rx_session_nodes_.ensureNodeFor(params); // should be new + if (auto* const failure = cetl::get_if(&node_result)) { - return AlreadyExistsError{}; + return std::move(*failure); } + auto& new_svc_node = cetl::get(node_result).get(); - auto session_result = Factory::make(asDelegate(), rx_params); + auto session_result = SvcResponseRxSession::make(memory(), asDelegate(), params, new_svc_node); if (auto* const make_failure = cetl::get_if(&session_result)) { + // We failed to create the session, so we need to release the unique node. + // The sockets we made earlier will be released in the destructor of whole transport. + svc_response_rx_session_nodes_.removeNodeFor(params); return std::move(*make_failure); } @@ -568,9 +593,16 @@ class TransportImpl final : private TransportDelegate, public ICanTransport // No Sonar `cpp:S5357` b/c the raw `user_reference` is part of libcanard api, // and it was set by us at a RX session constructor (see f.e. `MessageRxSession` ctor). - auto* const delegate = + auto* const session_delegate = static_cast(out_subscription->user_reference); // NOSONAR cpp:S5357 - delegate->acceptRxTransfer(out_transfer); + + const auto transfer_id = static_cast(out_transfer.metadata.transfer_id); + const auto priority = static_cast(out_transfer.metadata.priority); + const auto timestamp = TimePoint{std::chrono::microseconds{out_transfer.timestamp_usec}}; + + session_delegate->acceptRxTransfer(CanardMemory{memory(), out_transfer.payload}, + TransferRxMetadata{{transfer_id, priority}, timestamp}, + out_transfer.metadata.remote_node_id); } } @@ -712,71 +744,10 @@ class TransportImpl final : private TransportDelegate, public ICanTransport } } - /// @brief Fills an array with filters for each active RX port. - /// - CETL_NODISCARD bool fillMediaFiltersArray(libcyphal::detail::VarArray& filters) - { - using RxSubscription = const CanardRxSubscription; - using RxSubscriptionTree = CanardConcreteTree; - - // Total "active" RX ports depends on the local node ID. For anonymous nodes, - // we don't account for service ports (b/c they don't work while being anonymous). - // - const auto local_node_id = static_cast(getNodeId()); - const auto is_anonymous = local_node_id > CANARD_NODE_ID_MAX; - const std::size_t total_active_ports = total_msg_rx_ports_ + (is_anonymous ? 0 : total_svc_rx_ports_); - if (total_active_ports == 0) - { - // No need to allocate memory for zero filters. - return true; - } - - // Now we know that we have at least one active port, - // so we need preallocate temp memory for total number of active ports. - // - filters.reserve(total_active_ports); - if (filters.capacity() < total_active_ports) - { - // This is out of memory situation. - return false; - } - - // `ports_count` counting is just for the sake of debug verification. - std::size_t ports_count = 0; - - const auto& subs_trees = canardInstance().rx_subscriptions; - - if (total_msg_rx_ports_ > 0) - { - const auto msg_visitor = [&filters](RxSubscription& rx_subscription) { - // Make and store a single message filter. - const auto flt = ::canardMakeFilterForSubject(rx_subscription.port_id); - filters.emplace_back(Filter{flt.extended_can_id, flt.extended_mask}); - }; - ports_count += RxSubscriptionTree::visitCounting(subs_trees[CanardTransferKindMessage], msg_visitor); - } - - // No need to make service filters if we don't have a local node ID. - // - if ((total_svc_rx_ports_ > 0) && (!is_anonymous)) - { - const auto svc_visitor = [&filters, local_node_id](RxSubscription& rx_subscription) { - // Make and store a single service filter. - const auto flt = ::canardMakeFilterForService(rx_subscription.port_id, local_node_id); - filters.emplace_back(Filter{flt.extended_can_id, flt.extended_mask}); - }; - ports_count += RxSubscriptionTree::visitCounting(subs_trees[CanardTransferKindRequest], svc_visitor); - ports_count += RxSubscriptionTree::visitCounting(subs_trees[CanardTransferKindResponse], svc_visitor); - } - - (void) ports_count; - CETL_DEBUG_ASSERT(ports_count == total_active_ports, ""); - return true; - } - - void cancelRxCallbacksIfNoPortsLeft() + void cancelRxCallbacksIfNoPortsLeft() noexcept { - if (0 == (total_msg_rx_ports_ + total_svc_rx_ports_)) + const auto& subs_stats = getSubscriptionStats(); + if (0 == (subs_stats.total_msg_rx_ports + subs_stats.total_svc_rx_ports)) { for (Media& media : media_array_) { @@ -787,12 +758,11 @@ class TransportImpl final : private TransportDelegate, public ICanTransport // MARK: Data members: - IExecutor& executor_; - MediaArray media_array_; - std::size_t total_msg_rx_ports_; - std::size_t total_svc_rx_ports_; - TransientErrorHandler transient_error_handler_; - Callback::Any configure_filters_callback_; + IExecutor& executor_; + MediaArray media_array_; + TransientErrorHandler transient_error_handler_; + Callback::Any configure_filters_callback_; + SessionTree svc_response_rx_session_nodes_; }; // TransportImpl diff --git a/include/libcyphal/transport/can/delegate.hpp b/include/libcyphal/transport/can/delegate.hpp index c8bb9d077..635a8eedf 100644 --- a/include/libcyphal/transport/can/delegate.hpp +++ b/include/libcyphal/transport/can/delegate.hpp @@ -6,8 +6,14 @@ #ifndef LIBCYPHAL_TRANSPORT_CAN_DELEGATE_HPP_INCLUDED #define LIBCYPHAL_TRANSPORT_CAN_DELEGATE_HPP_INCLUDED +#include "media.hpp" +#include "rx_session_tree_node.hpp" + #include "libcyphal/transport/errors.hpp" +#include "libcyphal/transport/msg_sessions.hpp" #include "libcyphal/transport/scattered_buffer.hpp" +#include "libcyphal/transport/session_tree.hpp" +#include "libcyphal/transport/svc_sessions.hpp" #include "libcyphal/transport/types.hpp" #include "libcyphal/types.hpp" @@ -34,95 +40,120 @@ namespace can namespace detail { -/// This internal transport delegate class serves the following purposes: -/// 1. It provides memory management functions for the Canard library. -/// 2. It provides a way to convert Canard error codes to `AnyFailure` type. -/// 3. It provides an interface to access the transport from various session classes. +/// @brief RAII class to manage memory allocated by Canard library. /// -class TransportDelegate +class CanardMemory final : public ScatteredBuffer::IStorage { public: - /// @brief RAII class to manage memory allocated by Canard library. - /// - class CanardMemory final : public ScatteredBuffer::IStorage + // No Sonar `cpp:S5356` and `cpp:S5357` b/c we need to pass raw data from C libcanard api. + CanardMemory(cetl::pmr::memory_resource& memory, CanardMutablePayload& payload) + : memory_{memory} + , allocated_size_{std::exchange(payload.allocated_size, 0)} + , buffer_{static_cast(std::exchange(payload.data, nullptr))} // NOSONAR cpp:S5356 cpp:S5357 + , payload_size_{std::exchange(payload.size, 0)} { - public: - CanardMemory(TransportDelegate& delegate, - const std::size_t allocated_size, - cetl::byte* const buffer, - const std::size_t payload_size) - : delegate_{delegate} - , allocated_size_{allocated_size} - , buffer_{buffer} - , payload_size_{payload_size} - { - } - CanardMemory(CanardMemory&& other) noexcept - : delegate_{other.delegate_} - , allocated_size_{std::exchange(other.allocated_size_, 0)} - , buffer_{std::exchange(other.buffer_, nullptr)} - , payload_size_{std::exchange(other.payload_size_, 0)} - { - } - CanardMemory(const CanardMemory&) = delete; + } + CanardMemory(CanardMemory&& other) noexcept + : memory_{other.memory_} + , allocated_size_{std::exchange(other.allocated_size_, 0)} + , buffer_{std::exchange(other.buffer_, nullptr)} + , payload_size_{std::exchange(other.payload_size_, 0)} + { + } + CanardMemory(const CanardMemory&) = delete; - ~CanardMemory() + ~CanardMemory() + { + if (buffer_ != nullptr) { - if (buffer_ != nullptr) - { - // No Sonar `cpp:S5356` b/c we integrate here with C libcanard memory management. - delegate_.freeCanardMemory(buffer_, allocated_size_); // NOSONAR cpp:S5356 - } + // No Sonar `cpp:S5356` b/c we integrate here with C libcanard memory management. + memory_.deallocate(buffer_, allocated_size_); // NOSONAR cpp:S5356 } + } + + CanardMemory& operator=(const CanardMemory&) = delete; + CanardMemory& operator=(CanardMemory&&) noexcept = delete; - CanardMemory& operator=(const CanardMemory&) = delete; - CanardMemory& operator=(CanardMemory&&) noexcept = delete; + // MARK: ScatteredBuffer::IStorage + + CETL_NODISCARD std::size_t size() const noexcept override + { + return payload_size_; + } - // MARK: ScatteredBuffer::IStorage + CETL_NODISCARD std::size_t copy(const std::size_t offset_bytes, + cetl::byte* const destination, + const std::size_t length_bytes) const override + { + CETL_DEBUG_ASSERT((destination != nullptr) || (length_bytes == 0), + "Destination could be null only with zero bytes ask."); - CETL_NODISCARD std::size_t size() const noexcept override + if ((destination == nullptr) || (buffer_ == nullptr) || (payload_size_ <= offset_bytes)) { - return payload_size_; + return 0; } - CETL_NODISCARD std::size_t copy(const std::size_t offset_bytes, - cetl::byte* const destination, - const std::size_t length_bytes) const override - { - CETL_DEBUG_ASSERT((destination != nullptr) || (length_bytes == 0), - "Destination could be null only with zero bytes ask."); + const std::size_t bytes_to_copy = std::min(length_bytes, payload_size_ - offset_bytes); + // Next nolint is unavoidable: we need to offset from the beginning of the buffer. + // No Sonar `cpp:S5356` b/c we integrate here with libcanard raw C buffers. + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) + (void) std::memmove(destination, buffer_ + offset_bytes, bytes_to_copy); // NOSONAR cpp:S5356 + return bytes_to_copy; + } - if ((destination == nullptr) || (buffer_ == nullptr) || (payload_size_ <= offset_bytes)) - { - return 0; - } +private: + // MARK: Data members: - const std::size_t bytes_to_copy = std::min(length_bytes, payload_size_ - offset_bytes); - // Next nolint is unavoidable: we need offset from the beginning of the buffer. - // No Sonar `cpp:S5356` b/c we integrate here with libcanard raw C buffers. - // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) - (void) std::memmove(destination, buffer_ + offset_bytes, bytes_to_copy); // NOSONAR cpp:S5356 - return bytes_to_copy; - } + cetl::pmr::memory_resource& memory_; + std::size_t allocated_size_; + cetl::byte* buffer_; + std::size_t payload_size_; - private: - // MARK: Data members: +}; // CanardMemory - TransportDelegate& delegate_; - std::size_t allocated_size_; - cetl::byte* buffer_; - std::size_t payload_size_; +// MARK: - - }; // CanardMemory +/// This internal session delegate class serves the following purpose: it provides an interface (aka gateway) +/// to access RX session from transport (by casting canard's `user_reference` member to this class). +/// +class IRxSessionDelegate +{ +public: + IRxSessionDelegate(const IRxSessionDelegate&) = delete; + IRxSessionDelegate(IRxSessionDelegate&&) noexcept = delete; + IRxSessionDelegate& operator=(const IRxSessionDelegate&) = delete; + IRxSessionDelegate& operator=(IRxSessionDelegate&&) noexcept = delete; + /// @brief Accepts a received transfer from the transport dedicated to this RX session. + /// + virtual void acceptRxTransfer(CanardMemory&& lizard_memory, + const TransferRxMetadata& rx_metadata, + const NodeId source_node_id) = 0; + +protected: + IRxSessionDelegate() = default; + ~IRxSessionDelegate() = default; + +}; // IRxSessionDelegate + +// MARK: - + +/// This internal transport delegate class serves the following purposes: +/// 1. It provides memory management functions for the Canard library. +/// 2. It provides a way to convert Canard error codes to `AnyFailure` type. +/// 3. It provides an interface to access the transport from various session classes. +/// +class TransportDelegate +{ +public: /// @brief Utility type with various helpers related to Canard AVL trees. /// - /// @tparam Node Type of a concrete AVL tree node. + /// @tparam Node Type of concrete AVL tree node. /// template struct CanardConcreteTree { - /// @brief Visits in-order each node of the AVL tree, counting total number of nodes visited. + /// @brief Visits in-order each node of the AVL tree, counting the total number of nodes visited. /// /// @tparam Visitor Type of the visitor callable. /// @param root The root node of the AVL tree. Could be `nullptr`. @@ -187,25 +218,41 @@ class TransportDelegate private: static Node& down(CanardTreeNode& node) { - // Next nolint & NOSONAR are unavoidable: this is integration with Canard C AVL trees. + // Following nolint & NOSONAR are unavoidable: this is integration with Canard C AVL trees. // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) return reinterpret_cast(node); // NOSONAR cpp:S3630 } }; // CanardConcreteTree + /// Umbrella type for all session-related events. + /// + /// These are passed to the `onSessionEvent` method of the transport implementation. + /// struct SessionEvent { - struct MsgRxLifetime - { - bool is_added; - }; - struct SvcRxLifetime + struct MsgCreated + {}; + struct MsgDestroyed + {}; + struct SvcRequestCreated + {}; + struct SvcRequestDestroyed + {}; + struct SvcResponseCreated + {}; + struct SvcResponseDestroyed { - bool is_added; + ResponseRxParams params; }; - using Variant = cetl::variant; + using Variant = cetl::variant< // + MsgCreated, + MsgDestroyed, + SvcRequestCreated, + SvcRequestDestroyed, + SvcResponseCreated, + SvcResponseDestroyed>; }; // SessionEvent @@ -239,6 +286,62 @@ class TransportDelegate return memory_; } + void listenForRxSubscription(CanardRxSubscription& subscription, const MessageRxParams& params) + { + listenForRxSubscriptionImpl(subscription, CanardTransferKindMessage, params.subject_id, params.extent_bytes); + } + + void listenForRxSubscription(CanardRxSubscription& subscription, const RequestRxParams& params) + { + listenForRxSubscriptionImpl(subscription, CanardTransferKindRequest, params.service_id, params.extent_bytes); + } + + void listenForRxSubscription(CanardRxSubscription& subscription, const ResponseRxParams& params) + { + listenForRxSubscriptionImpl(subscription, CanardTransferKindResponse, params.service_id, params.extent_bytes); + } + + void retainRxSubscriptionFor(const ResponseRxParams& params) + { + const auto maybe_node = rx_subs_demux_nodes_.ensureNodeFor(params, std::ref(*this)); + if (const auto* const node = cetl::get_if(&maybe_node)) + { + node->get().retain(); + } + } + + CETL_NODISCARD CanardRxSubscription* findRxSubscriptionFor(const ResponseRxParams& params) + { + if (auto* const node = rx_subs_demux_nodes_.tryFindNodeFor(params)) + { + return &node->subscription(); + } + return nullptr; + } + + void releaseRxSubscriptionFor(const ResponseRxParams& params) + { + if (auto* const node = rx_subs_demux_nodes_.tryFindNodeFor(params)) + { + if (node->release()) + { + rx_subs_demux_nodes_.removeNodeFor(params); + } + } + } + + void cancelRxSubscriptionFor(const CanardRxSubscription& subscription, + const CanardTransferKind transfer_kind) noexcept + { + const std::int8_t result = ::canardRxUnsubscribe(&canard_instance_, transfer_kind, subscription.port_id); + (void) result; + CETL_DEBUG_ASSERT(result >= 0, "There is no way currently to get an error here."); + CETL_DEBUG_ASSERT(result > 0, "Subscription supposed to be made at node constructor."); + + subs_stats_.total_msg_rx_ports -= (transfer_kind == CanardTransferKindMessage) ? 1 : 0; + subs_stats_.total_svc_rx_ports -= (transfer_kind != CanardTransferKindMessage) ? 1 : 0; + } + CETL_NODISCARD static cetl::optional optAnyFailureFromCanard(const std::int32_t result) { // Canard error results are negative, so we need to negate them to get the error code. @@ -276,7 +379,7 @@ class TransportDelegate /// @param tx_queue The TX queue from which the item should be popped. /// @param canard_instance The Canard instance to be used for the item deallocation. /// @param tx_item The TX queue item to be popped and freed. - /// @param whole_transfer If `true` then whole transfer should be released from the queue. + /// @param whole_transfer If `true` then the whole transfer should be released from the queue. /// static void popAndFreeCanardTxQueueItem(CanardTxQueue& tx_queue, const CanardInstance& canard_instance, @@ -307,20 +410,187 @@ class TransportDelegate /// /// @param event_var Describes variant of the session even has happened. /// - virtual void onSessionEvent(const SessionEvent::Variant& event_var) = 0; + virtual void onSessionEvent(const SessionEvent::Variant& event_var) noexcept = 0; + + /// @brief Tries to find a response RX session delegate for the given parameters. + /// + /// @return `nullptr` if no session delegate found for the given parameters. + /// + virtual IRxSessionDelegate* tryFindRxSessionDelegateFor(const ResponseRxParams& params) = 0; protected: + struct SubscriptionStats + { + std::size_t total_msg_rx_ports; + std::size_t total_svc_rx_ports; + }; + explicit TransportDelegate(cetl::pmr::memory_resource& memory) : memory_{memory} , canard_instance_{::canardInit(makeCanardMemoryResource())} + , rx_subs_demux_nodes_{memory} + , subs_stats_{} { // No Sonar `cpp:S5356` b/c we integrate here with C libcanard API. canardInstance().user_reference = this; // NOSONAR cpp:S5356 } - ~TransportDelegate() = default; + ~TransportDelegate() + { + CETL_DEBUG_ASSERT(subs_stats_.total_msg_rx_ports == 0, // + "Message subscriptions must be destroyed before transport."); + CETL_DEBUG_ASSERT(subs_stats_.total_svc_rx_ports == 0, // + "Service subscriptions must be destroyed before transport."); + } + + const SubscriptionStats& getSubscriptionStats() const + { + return subs_stats_; + } + + /// @brief Fills an array with filters for each active RX port. + /// + CETL_NODISCARD bool fillMediaFiltersArray(libcyphal::detail::VarArray& filters) + { + using RxSubscription = const CanardRxSubscription; + using RxSubscriptionTree = CanardConcreteTree; + + // Total "active" RX ports depends on the local node ID. For anonymous nodes, + // we don't account for service ports (b/c they don't work while being anonymous). + // + const auto local_node_id = canard_instance_.node_id; + const auto is_anonymous = local_node_id > CANARD_NODE_ID_MAX; + const std::size_t total_active_ports = subs_stats_.total_msg_rx_ports // + + (is_anonymous ? 0 : subs_stats_.total_svc_rx_ports); + if (total_active_ports == 0) + { + // No need to allocate memory for zero filters. + return true; + } + + // Now we know that we have at least one active port, + // so we need preallocate temp memory for the total number of active ports. + // + filters.reserve(total_active_ports); + if (filters.capacity() < total_active_ports) + { + // This is out of memory situation. + return false; + } + + // `ports_count` counting is just for the sake of debug verification. + std::size_t ports_count = 0; + + const auto& subs_trees = canardInstance().rx_subscriptions; + + if (subs_stats_.total_msg_rx_ports > 0) + { + const auto msg_visitor = [&filters](RxSubscription& rx_subscription) { + // + // Make and store a single message filter. + const auto flt = ::canardMakeFilterForSubject(rx_subscription.port_id); + filters.emplace_back(Filter{flt.extended_can_id, flt.extended_mask}); + }; + ports_count += RxSubscriptionTree::visitCounting(subs_trees[CanardTransferKindMessage], msg_visitor); + } + + // No need to make service filters if we don't have a local node ID. + // + if ((subs_stats_.total_svc_rx_ports > 0) && (!is_anonymous)) + { + const auto svc_visitor = [&filters, local_node_id](RxSubscription& rx_subscription) { + // + // Make and store a single service filter. + const auto flt = ::canardMakeFilterForService(rx_subscription.port_id, local_node_id); + filters.emplace_back(Filter{flt.extended_can_id, flt.extended_mask}); + }; + ports_count += RxSubscriptionTree::visitCounting(subs_trees[CanardTransferKindRequest], svc_visitor); + ports_count += RxSubscriptionTree::visitCounting(subs_trees[CanardTransferKindResponse], svc_visitor); + } + + (void) ports_count; + CETL_DEBUG_ASSERT(ports_count == total_active_ports, ""); + return true; + } private: + template + using SessionTree = transport::detail::SessionTree; + + /// Accepts transfers from RX subscription and forwards them to the appropriate session (according to source node + /// id). Has reference counting so that it will be destroyed when no longer referenced by any RX session. + /// + class RxSubsDemuxNode final : public SessionTree::NodeBase, public IRxSessionDelegate + { + public: + RxSubsDemuxNode(const ResponseRxParams& params, std::tuple args_tuple) + : transport_delegate_{std::get<0>(args_tuple)} + , ref_count_{0} + , subscription_{} + { + transport_delegate_.listenForRxSubscription(subscription_, params); + + // No Sonar `cpp:S5356` b/c we integrate here with C libudpard API. + subscription_.user_reference = static_cast(this); // NOSONAR cpp:S5356 + } + + RxSubsDemuxNode(const RxSubsDemuxNode&) = delete; + RxSubsDemuxNode(RxSubsDemuxNode&&) noexcept = delete; + RxSubsDemuxNode& operator=(const RxSubsDemuxNode&) = delete; + RxSubsDemuxNode& operator=(RxSubsDemuxNode&&) noexcept = delete; + + ~RxSubsDemuxNode() + { + transport_delegate_.cancelRxSubscriptionFor(subscription_, CanardTransferKindResponse); + } + + CETL_NODISCARD std::int32_t compareByParams(const ResponseRxParams& params) const + { + return static_cast(subscription_.port_id) - static_cast(params.service_id); + } + + CETL_NODISCARD CanardRxSubscription& subscription() noexcept + { + return subscription_; + } + + void retain() noexcept + { + ++ref_count_; + } + + bool release() noexcept + { + CETL_DEBUG_ASSERT(ref_count_ > 0, ""); + --ref_count_; + return ref_count_ == 0; + } + + private: + // IRxSessionDelegate + + void acceptRxTransfer(CanardMemory&& lizard_memory, + const TransferRxMetadata& rx_metadata, + const NodeId source_node_id) override + { + // This is where de-multiplexing happens: the transfer is forwarded to the appropriate session. + // It's ok not to find the session delegate here - we drop unsolicited transfers. + // + const ResponseRxParams params{0, subscription_.port_id, source_node_id}; + if (auto* const session_delegate = transport_delegate_.tryFindRxSessionDelegateFor(params)) + { + session_delegate->acceptRxTransfer(std::move(lizard_memory), rx_metadata, source_node_id); + } + } + + // MARK: Data members: + + TransportDelegate& transport_delegate_; + std::size_t ref_count_; + CanardRxSubscription subscription_; + + }; // RxSubsDemuxNode + /// @brief Converts Canard instance to the transport delegate. /// /// In use to bridge two worlds: canard library and transport entities. @@ -332,6 +602,7 @@ class TransportDelegate // No Sonar `cpp:S5357` b/c the raw `user_reference` is part of libcanard api, // and it was set by us at this delegate constructor (see `TransportDelegate` ctor). + // NOLINTNEXTLINE return *static_cast(user_reference); // NOSONAR cpp:S5357 } @@ -364,36 +635,34 @@ class TransportDelegate return {this, freeCanardMemory, allocateMemoryForCanard}; // NOSONAR cpp:S5356 } + void listenForRxSubscriptionImpl(CanardRxSubscription& subscription, + const CanardTransferKind transfer_kind, + const CanardPortID port_id, + const size_t extent_bytes) + { + const std::int8_t result = ::canardRxSubscribe(&canard_instance_, + transfer_kind, + port_id, + extent_bytes, + CANARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &subscription); + (void) result; + CETL_DEBUG_ASSERT(result >= 0, "There is no way currently to get an error here."); + CETL_DEBUG_ASSERT(result > 0, "New subscription supposed to be made."); + + subs_stats_.total_msg_rx_ports += (transfer_kind == CanardTransferKindMessage) ? 1 : 0; + subs_stats_.total_svc_rx_ports += (transfer_kind != CanardTransferKindMessage) ? 1 : 0; + } + // MARK: Data members: - cetl::pmr::memory_resource& memory_; - CanardInstance canard_instance_; + cetl::pmr::memory_resource& memory_; + CanardInstance canard_instance_; + SessionTree rx_subs_demux_nodes_; + SubscriptionStats subs_stats_; }; // TransportDelegate -// MARK: - - -/// This internal session delegate class serves the following purpose: it provides an interface (aka gateway) -/// to access RX session from transport (by casting canard's `user_reference` member to this class). -/// -class IRxSessionDelegate -{ -public: - IRxSessionDelegate(const IRxSessionDelegate&) = delete; - IRxSessionDelegate(IRxSessionDelegate&&) noexcept = delete; - IRxSessionDelegate& operator=(const IRxSessionDelegate&) = delete; - IRxSessionDelegate& operator=(IRxSessionDelegate&&) noexcept = delete; - - /// @brief Accepts a received transfer from the transport dedicated to this RX session. - /// - virtual void acceptRxTransfer(const CanardRxTransfer& transfer) = 0; - -protected: - IRxSessionDelegate() = default; - ~IRxSessionDelegate() = default; - -}; // IRxSessionDelegate - } // namespace detail } // namespace can } // namespace transport diff --git a/include/libcyphal/transport/can/msg_rx_session.hpp b/include/libcyphal/transport/can/msg_rx_session.hpp index 689954ac4..c049d5524 100644 --- a/include/libcyphal/transport/can/msg_rx_session.hpp +++ b/include/libcyphal/transport/can/msg_rx_session.hpp @@ -50,15 +50,17 @@ class MessageRxSession final : private IRxSessionDelegate, public IMessageRxSess }; public: - CETL_NODISCARD static Expected, AnyFailure> make(TransportDelegate& delegate, - const MessageRxParams& params) + CETL_NODISCARD static Expected, AnyFailure> make( // + cetl::pmr::memory_resource& memory, + TransportDelegate& delegate, + const MessageRxParams& params) { if (params.subject_id > CANARD_SUBJECT_ID_MAX) { return ArgumentError{}; } - auto session = libcyphal::detail::makeUniquePtr(delegate.memory(), Spec{}, delegate, params); + auto session = libcyphal::detail::makeUniquePtr(memory, Spec{}, delegate, params); if (session == nullptr) { return MemoryError{}; @@ -72,20 +74,12 @@ class MessageRxSession final : private IRxSessionDelegate, public IMessageRxSess , params_{params} , subscription_{} { - const std::int8_t result = ::canardRxSubscribe(&delegate.canardInstance(), - CanardTransferKindMessage, - params_.subject_id, - params_.extent_bytes, - CANARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &subscription_); - (void) result; - CETL_DEBUG_ASSERT(result >= 0, "There is no way currently to get an error here."); - CETL_DEBUG_ASSERT(result > 0, "New subscription supposed to be made."); + delegate.listenForRxSubscription(subscription_, params); // No Sonar `cpp:S5356` b/c we integrate here with C libcanard API. subscription_.user_reference = static_cast(this); // NOSONAR cpp:S5356 - delegate_.onSessionEvent(TransportDelegate::SessionEvent::MsgRxLifetime{true /* is_added */}); + delegate_.onSessionEvent(TransportDelegate::SessionEvent::MsgCreated{}); } MessageRxSession(const MessageRxSession&) = delete; @@ -95,13 +89,8 @@ class MessageRxSession final : private IRxSessionDelegate, public IMessageRxSess ~MessageRxSession() { - const std::int8_t result = - ::canardRxUnsubscribe(&delegate_.canardInstance(), CanardTransferKindMessage, params_.subject_id); - (void) result; - CETL_DEBUG_ASSERT(result >= 0, "There is no way currently to get an error here."); - CETL_DEBUG_ASSERT(result > 0, "Subscription supposed to be made at constructor."); - - delegate_.onSessionEvent(TransportDelegate::SessionEvent::MsgRxLifetime{false /* is_added */}); + delegate_.cancelRxSubscriptionFor(subscription_, CanardTransferKindMessage); + delegate_.onSessionEvent(TransportDelegate::SessionEvent::MsgDestroyed{}); } private: @@ -141,26 +130,15 @@ class MessageRxSession final : private IRxSessionDelegate, public IMessageRxSess // MARK: IRxSessionDelegate - void acceptRxTransfer(const CanardRxTransfer& transfer) override + void acceptRxTransfer(CanardMemory&& lizard_memory, + const TransferRxMetadata& rx_metadata, + const NodeId source_node_id) override { - const auto priority = static_cast(transfer.metadata.priority); - const auto transfer_id = static_cast(transfer.metadata.transfer_id); - const auto timestamp = TimePoint{std::chrono::microseconds{transfer.timestamp_usec}}; - const cetl::optional publisher_node_id = - transfer.metadata.remote_node_id > CANARD_NODE_ID_MAX - ? cetl::nullopt - : cetl::make_optional(transfer.metadata.remote_node_id); - - // No Sonar `cpp:S5356` and `cpp:S5357` b/c we need to pass raw data from C libcanard api. - auto* const buffer = static_cast(transfer.payload.data); // NOSONAR cpp:S5356 cpp:S5357 - TransportDelegate::CanardMemory canard_memory{delegate_, - transfer.payload.allocated_size, - buffer, - transfer.payload.size}; - - const MessageRxMetadata meta{{{transfer_id, priority}, timestamp}, publisher_node_id}; - MessageRxTransfer msg_rx_transfer{meta, ScatteredBuffer{std::move(canard_memory)}}; + source_node_id > CANARD_NODE_ID_MAX ? cetl::nullopt : cetl::make_optional(source_node_id); + + const MessageRxMetadata meta{rx_metadata, publisher_node_id}; + MessageRxTransfer msg_rx_transfer{meta, ScatteredBuffer{std::move(lizard_memory)}}; if (on_receive_cb_fn_) { on_receive_cb_fn_(OnReceiveCallback::Arg{msg_rx_transfer}); diff --git a/include/libcyphal/transport/can/msg_tx_session.hpp b/include/libcyphal/transport/can/msg_tx_session.hpp index 788d12372..15c1eb414 100644 --- a/include/libcyphal/transport/can/msg_tx_session.hpp +++ b/include/libcyphal/transport/can/msg_tx_session.hpp @@ -42,15 +42,17 @@ class MessageTxSession final : public IMessageTxSession }; public: - CETL_NODISCARD static Expected, AnyFailure> make(TransportDelegate& delegate, - const MessageTxParams& params) + CETL_NODISCARD static Expected, AnyFailure> make( // + cetl::pmr::memory_resource& memory, + TransportDelegate& delegate, + const MessageTxParams& params) { if (params.subject_id > CANARD_SUBJECT_ID_MAX) { return ArgumentError{}; } - auto session = libcyphal::detail::makeUniquePtr(delegate.memory(), Spec{}, delegate, params); + auto session = libcyphal::detail::makeUniquePtr(memory, Spec{}, delegate, params); if (session == nullptr) { return MemoryError{}; diff --git a/include/libcyphal/transport/can/rx_session_tree_node.hpp b/include/libcyphal/transport/can/rx_session_tree_node.hpp new file mode 100644 index 000000000..d3ec8abc7 --- /dev/null +++ b/include/libcyphal/transport/can/rx_session_tree_node.hpp @@ -0,0 +1,44 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_CAN_RX_SESSION_TREE_NODE_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_CAN_RX_SESSION_TREE_NODE_HPP_INCLUDED + +#include "libcyphal/transport/session_tree.hpp" + +namespace libcyphal +{ +namespace transport +{ +namespace can +{ + +/// Internal implementation details of the UDP transport. +/// Not supposed to be used directly by the users of the library. +/// +namespace detail +{ + +class IRxSessionDelegate; + +/// Umbrella type for various RX session tree nodes in use at the CAN transport. +/// +/// Currently, it contains only one `Response` subtype, +/// but still kept nested to match the UDP transport approach (where there are several subtypes). +/// +struct RxSessionTreeNode +{ + /// @brief Represents a service response RX session node. + /// + using Response = transport::detail::ResponseRxSessionNode; + +}; // RxSessionTreeNode + +} // namespace detail +} // namespace can +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_CAN_RX_SESSION_TREE_NODE_HPP_INCLUDED diff --git a/include/libcyphal/transport/can/svc_rx_sessions.hpp b/include/libcyphal/transport/can/svc_rx_sessions.hpp index 818023561..9ebcc38a6 100644 --- a/include/libcyphal/transport/can/svc_rx_sessions.hpp +++ b/include/libcyphal/transport/can/svc_rx_sessions.hpp @@ -10,8 +10,8 @@ #include "libcyphal/errors.hpp" #include "libcyphal/transport/errors.hpp" +#include "libcyphal/transport/svc_rx_session_base.hpp" #include "libcyphal/transport/svc_sessions.hpp" -#include "libcyphal/transport/types.hpp" #include "libcyphal/types.hpp" #include @@ -19,8 +19,6 @@ #include #include -#include -#include namespace libcyphal { @@ -35,21 +33,15 @@ namespace can namespace detail { -/// @brief A template class to represent a service request/response RX session (both for server and client sides). -/// -/// @tparam Interface_ Type of the session interface. -/// Could be either `IRequestRxSession` or `IResponseRxSession`. -/// @tparam Params Type of the session parameters. -/// Could be either `RequestRxParams` or `ResponseRxParams`. -/// @tparam TransferKind Kind of the service transfer. -/// Could be either `CanardTransferKindRequest` or `CanardTransferKindResponse`. +/// @brief A concrete class to represent a service request RX session (aka server side). /// -template -class SvcRxSession final : private IRxSessionDelegate, public Interface_ +class SvcRequestRxSession final + : public transport::detail:: // + SvcRxSessionBase { /// @brief Defines private specification for making interface unique ptr. /// - struct Spec : libcyphal::detail::UniquePtrSpec + struct Spec : libcyphal::detail::UniquePtrSpec { // `explicit` here is in use to disable public construction of derived private `Spec` structs. // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ @@ -57,15 +49,17 @@ class SvcRxSession final : private IRxSessionDelegate, public Interface_ }; public: - CETL_NODISCARD static Expected, AnyFailure> make(TransportDelegate& delegate, - const Params& params) + CETL_NODISCARD static Expected, AnyFailure> make( // + cetl::pmr::memory_resource& memory, + TransportDelegate& delegate, + const RequestRxParams& params) { if (params.service_id > CANARD_SERVICE_ID_MAX) { return ArgumentError{}; } - auto session = libcyphal::detail::makeUniquePtr(delegate.memory(), Spec{}, delegate, params); + auto session = libcyphal::detail::makeUniquePtr(memory, Spec{}, delegate, params); if (session == nullptr) { return MemoryError{}; @@ -74,65 +68,31 @@ class SvcRxSession final : private IRxSessionDelegate, public Interface_ return session; } - SvcRxSession(const Spec, TransportDelegate& delegate, const Params& params) - : delegate_{delegate} - , params_{params} + SvcRequestRxSession(const Spec, TransportDelegate& delegate, const RequestRxParams& params) + : Base{delegate, params} , subscription_{} { - const std::int8_t result = ::canardRxSubscribe(&delegate.canardInstance(), - TransferKind, - params_.service_id, - params_.extent_bytes, - CANARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &subscription_); - (void) result; - CETL_DEBUG_ASSERT(result >= 0, "There is no way currently to get an error here."); - CETL_DEBUG_ASSERT(result > 0, "New subscription supposed to be made."); + delegate.listenForRxSubscription(subscription_, params); // No Sonar `cpp:S5356` b/c we integrate here with C libcanard API. subscription_.user_reference = static_cast(this); // NOSONAR cpp:S5356 - delegate_.onSessionEvent(TransportDelegate::SessionEvent::SvcRxLifetime{true /* is_added */}); + delegate.onSessionEvent(TransportDelegate::SessionEvent::SvcRequestCreated{}); } - SvcRxSession(const SvcRxSession&) = delete; - SvcRxSession(SvcRxSession&&) noexcept = delete; - SvcRxSession& operator=(const SvcRxSession&) = delete; - SvcRxSession& operator=(SvcRxSession&&) noexcept = delete; + SvcRequestRxSession(const SvcRequestRxSession&) = delete; + SvcRequestRxSession(SvcRequestRxSession&&) noexcept = delete; + SvcRequestRxSession& operator=(const SvcRequestRxSession&) = delete; + SvcRequestRxSession& operator=(SvcRequestRxSession&&) noexcept = delete; - ~SvcRxSession() + ~SvcRequestRxSession() { - const std::int8_t result = ::canardRxUnsubscribe(&delegate_.canardInstance(), TransferKind, params_.service_id); - (void) result; - CETL_DEBUG_ASSERT(result >= 0, "There is no way currently to get an error here."); - CETL_DEBUG_ASSERT(result > 0, "Subscription supposed to be made at constructor."); - - delegate_.onSessionEvent(TransportDelegate::SessionEvent::SvcRxLifetime{false /* is_added */}); + delegate().cancelRxSubscriptionFor(subscription_, CanardTransferKindRequest); + delegate().onSessionEvent(TransportDelegate::SessionEvent::SvcRequestDestroyed{}); } private: - // MARK: Interface - - CETL_NODISCARD Params getParams() const noexcept override - { - return params_; - } - - CETL_NODISCARD cetl::optional receive() override - { - if (last_rx_transfer_) - { - auto transfer = std::move(*last_rx_transfer_); - last_rx_transfer_.reset(); - return transfer; - } - return cetl::nullopt; - } - - void setOnReceiveCallback(ISvcRxSession::OnReceiveCallback::Function&& function) override - { - on_receive_cb_fn_ = std::move(function); - } + using Base = SvcRxSessionBase; // MARK: IRxSession @@ -145,51 +105,92 @@ class SvcRxSession final : private IRxSessionDelegate, public Interface_ } } - // MARK: IRxSessionDelegate + // MARK: Data members: + + CanardRxSubscription subscription_; - void acceptRxTransfer(const CanardRxTransfer& transfer) override +}; // SvcRequestRxSession + +// MARK: - + +/// @brief A concrete class to represent a service response RX session (aka client side). +/// +class SvcResponseRxSession final + : public transport::detail:: // + SvcRxSessionBase +{ + /// @brief Defines private specification for making interface unique ptr. + /// + struct Spec : libcyphal::detail::UniquePtrSpec + { + // `explicit` here is in use to disable public construction of derived private `Spec` structs. + // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ + explicit Spec() = default; + }; + +public: + CETL_NODISCARD static Expected, AnyFailure> make( // + cetl::pmr::memory_resource& memory, + TransportDelegate& delegate, + const ResponseRxParams& params, + RxSessionTreeNode::Response& rx_session_node) { - const auto priority = static_cast(transfer.metadata.priority); - const auto remote_node_id = static_cast(transfer.metadata.remote_node_id); - const auto transfer_id = static_cast(transfer.metadata.transfer_id); - const auto timestamp = TimePoint{std::chrono::microseconds{transfer.timestamp_usec}}; - - // No Sonar `cpp:S5356` and `cpp:S5357` b/c we need to pass raw data from C libcanard api. - auto* const buffer = static_cast(transfer.payload.data); // NOSONAR cpp:S5356 cpp:S5357 - TransportDelegate::CanardMemory canard_memory{delegate_, - transfer.payload.allocated_size, - buffer, - transfer.payload.size}; - - const ServiceRxMetadata meta{{{transfer_id, priority}, timestamp}, remote_node_id}; - ServiceRxTransfer svc_rx_transfer{meta, ScatteredBuffer{std::move(canard_memory)}}; - if (on_receive_cb_fn_) + if (params.service_id > CANARD_SERVICE_ID_MAX) + { + return ArgumentError{}; + } + + auto session = libcyphal::detail::makeUniquePtr(memory, Spec{}, delegate, params, rx_session_node); + if (session == nullptr) { - on_receive_cb_fn_(ISvcRxSession::OnReceiveCallback::Arg{svc_rx_transfer}); - return; + return MemoryError{}; } - (void) last_rx_transfer_.emplace(std::move(svc_rx_transfer)); + + return session; } - // MARK: Data members: + SvcResponseRxSession(const Spec, + TransportDelegate& delegate, + const ResponseRxParams& params, + RxSessionTreeNode::Response& rx_session_node) + : Base{delegate, params} + { + delegate.retainRxSubscriptionFor(params); - TransportDelegate& delegate_; - const Params params_; - CanardRxSubscription subscription_; - cetl::optional last_rx_transfer_; - ISvcRxSession::OnReceiveCallback::Function on_receive_cb_fn_; + rx_session_node.delegate() = this; -}; // SvcRxSession + delegate.onSessionEvent(TransportDelegate::SessionEvent::SvcResponseCreated{}); + } -// MARK: - + SvcResponseRxSession(const SvcResponseRxSession&) = delete; + SvcResponseRxSession(SvcResponseRxSession&&) noexcept = delete; + SvcResponseRxSession& operator=(const SvcResponseRxSession&) = delete; + SvcResponseRxSession& operator=(SvcResponseRxSession&&) noexcept = delete; -/// @brief A concrete class to represent a service request RX session (aka server side). -/// -using SvcRequestRxSession = SvcRxSession; + ~SvcResponseRxSession() + { + delegate().releaseRxSubscriptionFor(getParams()); + delegate().onSessionEvent(TransportDelegate::SessionEvent::SvcResponseDestroyed{getParams()}); + } -/// @brief A concrete class to represent a service response RX session (aka client side). -/// -using SvcResponseRxSession = SvcRxSession; +private: + using Base = SvcRxSessionBase; + + // MARK: IRxSession + + void setTransferIdTimeout(const Duration timeout) override + { + const auto timeout_us = std::chrono::duration_cast(timeout); + if (timeout_us >= Duration::zero()) + { + if (auto* const subscription = delegate().findRxSubscriptionFor(getParams())) + { + subscription->transfer_id_timeout_usec = static_cast(timeout_us.count()); + } + } + } + +}; // SvcResponseRxSession } // namespace detail } // namespace can diff --git a/include/libcyphal/transport/can/svc_tx_sessions.hpp b/include/libcyphal/transport/can/svc_tx_sessions.hpp index 4db80b66c..d1160fbd3 100644 --- a/include/libcyphal/transport/can/svc_tx_sessions.hpp +++ b/include/libcyphal/transport/can/svc_tx_sessions.hpp @@ -44,15 +44,17 @@ class SvcRequestTxSession final : public IRequestTxSession }; public: - CETL_NODISCARD static Expected, AnyFailure> make(TransportDelegate& delegate, - const RequestTxParams& params) + CETL_NODISCARD static Expected, AnyFailure> make( // + cetl::pmr::memory_resource& memory, + TransportDelegate& delegate, + const RequestTxParams& params) { if ((params.service_id > CANARD_SERVICE_ID_MAX) || (params.server_node_id > CANARD_NODE_ID_MAX)) { return ArgumentError{}; } - auto session = libcyphal::detail::makeUniquePtr(delegate.memory(), Spec{}, delegate, params); + auto session = libcyphal::detail::makeUniquePtr(memory, Spec{}, delegate, params); if (session == nullptr) { return MemoryError{}; @@ -119,15 +121,17 @@ class SvcResponseTxSession final : public IResponseTxSession }; public: - CETL_NODISCARD static Expected, AnyFailure> make(TransportDelegate& delegate, - const ResponseTxParams& params) + CETL_NODISCARD static Expected, AnyFailure> make( // + cetl::pmr::memory_resource& memory, + TransportDelegate& delegate, + const ResponseTxParams& params) { if (params.service_id > CANARD_SERVICE_ID_MAX) { return ArgumentError{}; } - auto session = libcyphal::detail::makeUniquePtr(delegate.memory(), Spec{}, delegate, params); + auto session = libcyphal::detail::makeUniquePtr(memory, Spec{}, delegate, params); if (session == nullptr) { return MemoryError{}; diff --git a/include/libcyphal/transport/session.hpp b/include/libcyphal/transport/session.hpp index 5c60e51f1..d476e3d3c 100644 --- a/include/libcyphal/transport/session.hpp +++ b/include/libcyphal/transport/session.hpp @@ -48,9 +48,14 @@ class IRxSession : public ISession /// @brief Sets the timeout for a transmission. /// + /// Note that b/c of the nature of the Lizard libraries, the timeout is shared + /// between different sessions of the same kind and port id. This means that + /// the timeout set for one session will affect all other sessions of the same kind and port id. + /// For example, two RPC clients (on the same service id) of two different RPC server nodes + /// will share the same timeout for transfer ids. /// See Cyphal specification about transfer-ID timeouts. /// - /// @param timeout - Positive duration for the timeout. Default value is 2 second. + /// @param timeout - Positive duration for the timeout. The default value is 2 seconds. /// Zero or negative values are ignored. /// virtual void setTransferIdTimeout(const Duration timeout) = 0; diff --git a/include/libcyphal/transport/session_tree.hpp b/include/libcyphal/transport/session_tree.hpp new file mode 100644 index 000000000..a1ee1df0f --- /dev/null +++ b/include/libcyphal/transport/session_tree.hpp @@ -0,0 +1,221 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_SESSION_TREE_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_SESSION_TREE_HPP_INCLUDED + +#include "errors.hpp" +#include "libcyphal/common/cavl/cavl.hpp" +#include "libcyphal/types.hpp" +#include "svc_sessions.hpp" + +#include +#include + +#include +#include +#include + +namespace libcyphal +{ +namespace transport +{ + +/// Internal implementation details of a transport. +/// Not supposed to be used directly by the users of the library. +/// +namespace detail +{ + +/// @brief Defines a tree of sessions for a transport. +/// +/// @tparam Node The type of the session node. Expected to be a subclass of `SessionTree::NodeBase`, +/// and to have a method `compareByParams` that compares nodes by its parameters. +/// +template +class SessionTree final +{ +public: + /// Base class for the session tree node. + /// + class NodeBase : public common::cavl::Node + { + public: + using common::cavl::Node::getChildNode; + using RefWrapper = std::reference_wrapper; + + NodeBase(const NodeBase&) = delete; + NodeBase(NodeBase&&) noexcept = delete; + NodeBase& operator=(const NodeBase&) = delete; + NodeBase& operator=(NodeBase&&) noexcept = delete; + + protected: + NodeBase() = default; + ~NodeBase() = default; + + }; // NodeBase + + explicit SessionTree(cetl::pmr::memory_resource& mr) + : allocator_{&mr} + { + } + + SessionTree(const SessionTree&) = delete; + SessionTree(SessionTree&&) noexcept = delete; + SessionTree& operator=(const SessionTree&) = delete; + SessionTree& operator=(SessionTree&&) noexcept = delete; + + ~SessionTree() + { + nodes_.traversePostOrder([this](auto& node) { destroyNode(node); }); + } + + CETL_NODISCARD bool isEmpty() const noexcept + { + return nodes_.empty(); + } + + /// @brief Ensures that a node for the given parameters exists in the tree. + /// + /// @tparam ShouldBeNew If `true`, the function will return an error if node with given + // parametes already exists (see also `Node::compareByParams` method). + /// @tparam Params The type of the parameters to be used to find or create the node. + /// @tparam Args The types of the arguments to be passed to the constructor of the node. + /// @param params The parameters to be used to find or create the node. + /// @param args The extra arguments to be forwarded to the constructor of the node (as a tuple). + /// @return The reference to the node, or an error if the node could not be created. + /// + template + CETL_NODISCARD auto ensureNodeFor(const Params& params, + Args&&... args) -> Expected + { + // In c++14 we can't capture `args` with forwarding, so we pack them into a tuple. + auto args_tuple = std::make_tuple(std::forward(args)...); + + const auto node_existing = nodes_.search( + [¶ms](const Node& node) { // predicate + // + return node.compareByParams(params); + }, + [this, ¶ms, args_tuple_ = std::move(args_tuple)] { + // + return constructNewNode(params, std::move(args_tuple_)); + }); + + auto* const node = std::get<0>(node_existing); + if (nullptr == node) + { + return MemoryError{}; + } + if (ShouldBeNew && std::get<1>(node_existing)) + { + return AlreadyExistsError{}; + } + + return *node; + } + + template + CETL_NODISCARD Node* tryFindNodeFor(const Params& params) + { + return nodes_.search([¶ms](const Node& node) { // predicate + // + return node.compareByParams(params); + }); + } + + template + void removeNodeFor(const Params& params) noexcept + { + removeAndDestroyNode(tryFindNodeFor(params)); + } + + template + CETL_NODISCARD cetl::optional forEachNode(Action&& action) + { + return nodes_.traverse(std::forward(action)); + } + +private: + template + CETL_NODISCARD Node* constructNewNode(const Params& params, ArgsTuple&& args_tuple) + { + Node* const node = allocator_.allocate(1); + if (nullptr != node) + { + allocator_.construct(node, params, std::forward(args_tuple)); + } + return node; + } + + void removeAndDestroyNode(Node* node) + { + if (nullptr != node) + { + nodes_.remove(node); + destroyNode(*node); + } + } + + void destroyNode(Node& node) + { + // No Sonar cpp:M23_329 b/c we do our own low-level PMR management here. + node.~Node(); // NOSONAR cpp:M23_329 + allocator_.deallocate(&node, 1); + } + + // MARK: Data members: + + common::cavl::Tree nodes_; + libcyphal::detail::PmrAllocator allocator_; + +}; // SessionTree + +// MARK: - + +/// @brief Represents a service response RX session node. +/// +template +class ResponseRxSessionNode final : public SessionTree>::NodeBase +{ +public: + // Empty tuple parameter is used to allow for the same constructor signature + // as for other session nodes (see also `SessionTree::ensureNodeFor` method). + // + explicit ResponseRxSessionNode(const ResponseRxParams& params, const std::tuple<>&) + : service_id_{params.service_id} + , server_node_id{params.server_node_id} + , delegate_{nullptr} + { + } + + CETL_NODISCARD std::int32_t compareByParams(const ResponseRxParams& params) const + { + if (service_id_ != params.service_id) + { + return static_cast(service_id_) - static_cast(params.service_id); + } + return static_cast(server_node_id) - static_cast(params.server_node_id); + } + + CETL_NODISCARD RxSessionDelegate*& delegate() noexcept + { + return delegate_; + } + +private: + // MARK: Data members: + + const PortId service_id_; + const NodeId server_node_id; + RxSessionDelegate* delegate_; + +}; // ResponseRxSessionNode + +} // namespace detail +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_SESSION_TREE_HPP_INCLUDED diff --git a/include/libcyphal/transport/svc_rx_session_base.hpp b/include/libcyphal/transport/svc_rx_session_base.hpp new file mode 100644 index 000000000..8c59e8125 --- /dev/null +++ b/include/libcyphal/transport/svc_rx_session_base.hpp @@ -0,0 +1,111 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_SVC_RX_SESSION_BASE_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_SVC_RX_SESSION_BASE_HPP_INCLUDED + +#include "scattered_buffer.hpp" + +#include +#include + +#include + +namespace libcyphal +{ +namespace transport +{ + +/// Internal implementation details of a transport. +/// Not supposed to be used directly by the users of the library. +/// +namespace detail +{ + +/// @brief A base template class to represent a service RX session. +/// +/// Should be suitable for any transport. +/// +template +class SvcRxSessionBase : public IRxSessionDelegate, public Interface +{ +public: + SvcRxSessionBase(TransportDelegate& delegate, const Params& params) + : delegate_{delegate} + , params_{params} + { + } + + SvcRxSessionBase(const SvcRxSessionBase&) = delete; + SvcRxSessionBase(SvcRxSessionBase&&) noexcept = delete; + SvcRxSessionBase& operator=(const SvcRxSessionBase&) = delete; + SvcRxSessionBase& operator=(SvcRxSessionBase&&) noexcept = delete; + +protected: + ~SvcRxSessionBase() = default; + + TransportDelegate& delegate() + { + return delegate_; + } + + // MARK: Interface + + CETL_NODISCARD Params getParams() const noexcept final + { + return params_; + } + + CETL_NODISCARD cetl::optional receive() final + { + if (last_rx_transfer_) + { + auto transfer = std::move(*last_rx_transfer_); + last_rx_transfer_.reset(); + return transfer; + } + return cetl::nullopt; + } + + void setOnReceiveCallback(ISvcRxSession::OnReceiveCallback::Function&& function) final + { + on_receive_cb_fn_ = std::move(function); + } + + // MARK: IRxSessionDelegate + + void acceptRxTransfer(LizardMemory&& lizard_memory, + const TransferRxMetadata& rx_metadata, + const NodeId source_node_id) final + { + const ServiceRxMetadata meta{rx_metadata, source_node_id}; + ServiceRxTransfer svc_rx_transfer{meta, ScatteredBuffer{std::move(lizard_memory)}}; + if (on_receive_cb_fn_) + { + on_receive_cb_fn_(ISvcRxSession::OnReceiveCallback::Arg{svc_rx_transfer}); + return; + } + (void) last_rx_transfer_.emplace(std::move(svc_rx_transfer)); + } + +private: + // MARK: Data members: + + TransportDelegate& delegate_; + const Params params_; + cetl::optional last_rx_transfer_; + ISvcRxSession::OnReceiveCallback::Function on_receive_cb_fn_; + +}; // SvcRxSessionBase + +} // namespace detail +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_SVC_RX_SESSION_BASE_HPP_INCLUDED diff --git a/include/libcyphal/transport/udp/delegate.hpp b/include/libcyphal/transport/udp/delegate.hpp index b420964aa..3a7149706 100644 --- a/include/libcyphal/transport/udp/delegate.hpp +++ b/include/libcyphal/transport/udp/delegate.hpp @@ -6,11 +6,15 @@ #ifndef LIBCYPHAL_TRANSPORT_UDP_DELEGATE_HPP_INCLUDED #define LIBCYPHAL_TRANSPORT_UDP_DELEGATE_HPP_INCLUDED +#include "rx_session_tree_node.hpp" + #include "libcyphal/transport/errors.hpp" +#include "libcyphal/transport/msg_sessions.hpp" #include "libcyphal/transport/scattered_buffer.hpp" +#include "libcyphal/transport/session_tree.hpp" +#include "libcyphal/transport/svc_sessions.hpp" #include "libcyphal/transport/types.hpp" #include "libcyphal/transport/udp/tx_rx_sockets.hpp" -#include "libcyphal/types.hpp" #include #include @@ -21,6 +25,8 @@ #include #include #include +#include +#include #include namespace libcyphal @@ -68,128 +74,204 @@ struct AnyUdpardTxMetadata }; // AnyUdpardTxMetadata -/// This internal transport delegate class serves the following purposes: -/// 1. It provides memory management functions for the Udpard library. -/// 2. It provides a way to convert Udpard error codes to `AnyFailure` type. -/// 3. It provides an interface to access the transport from various session classes. +// MARK: - + +/// @brief Defines an internal set of memory resources used by the UDP transport. /// -class TransportDelegate +struct MemoryResources +{ + /// The general purpose memory resource is used to provide memory for the libcyphal library. + /// It is NOT used for any Udpard TX or RX transfers, payload (de)fragmentation or transient handles, + /// but only for the libcyphal internal needs (like `make*[Rx|Tx]Session` factory calls). + cetl::pmr::memory_resource& general; + + /// The session memory resource is used to provide memory for the Udpard session instances. + /// Each instance is fixed-size, so a trivial zero-fragmentation block allocator is enough. + UdpardMemoryResource session; + + /// The fragment handles are allocated per payload fragment; each handle contains a pointer to its fragment. + /// Each instance is of a very small fixed size, so a trivial zero-fragmentation block allocator is enough. + UdpardMemoryResource fragment; + + /// The library never allocates payload buffers itself, as they are handed over by the application via + /// reception calls. Once a buffer is handed over, the library may choose to keep it if it is deemed to be + /// necessary to complete a transfer reassembly, or to discard it if it is deemed to be unnecessary. + /// Discarded payload buffers are freed using this memory resource. + UdpardMemoryDeleter payload; +}; + +/// @brief RAII class to manage memory allocated by Udpard library. +/// +class UdpardMemory final : public ScatteredBuffer::IStorage { public: - /// @brief RAII class to manage memory allocated by Udpard library. - /// - class UdpardMemory final : public ScatteredBuffer::IStorage + UdpardMemory(const MemoryResources& memory_resources, UdpardRxTransfer& transfer) + : memory_resources_{memory_resources} + , payload_size_{std::exchange(transfer.payload_size, 0)} + , payload_{std::exchange(transfer.payload, {})} { - public: - UdpardMemory(TransportDelegate& delegate, UdpardRxTransfer& transfer) - : delegate_{delegate} - , payload_size_{std::exchange(transfer.payload_size, 0)} - , payload_{std::exchange(transfer.payload, {})} - { - } - UdpardMemory(UdpardMemory&& other) noexcept - : delegate_{other.delegate_} - , payload_size_{std::exchange(other.payload_size_, 0)} - , payload_{std::exchange(other.payload_, {})} + } + UdpardMemory(UdpardMemory&& other) noexcept + : memory_resources_{other.memory_resources_} + , payload_size_{std::exchange(other.payload_size_, 0)} + , payload_{std::exchange(other.payload_, {})} + { + } + + ~UdpardMemory() + { + ::udpardRxFragmentFree(payload_, memory_resources_.fragment, memory_resources_.payload); + } + + UdpardMemory(const UdpardMemory&) = delete; + UdpardMemory& operator=(const UdpardMemory&) = delete; + UdpardMemory& operator=(UdpardMemory&&) noexcept = delete; + + // MARK: ScatteredBuffer::IStorage + + CETL_NODISCARD std::size_t size() const noexcept override + { + return payload_size_; + } + + CETL_NODISCARD std::size_t copy(const std::size_t offset_bytes, + cetl::byte* const destination, + const std::size_t length_bytes) const override + { + using FragSpan = const cetl::span; + + // TODO: Use `udpardGather` function when it will be available with offset support. + + CETL_DEBUG_ASSERT((destination != nullptr) || (length_bytes == 0), + "Destination could be null only with zero bytes ask."); + + if ((destination == nullptr) || (payload_.view.data == nullptr) || (payload_size_ <= offset_bytes)) { + return 0; } - ~UdpardMemory() + // Find first fragment to start from (according to source `offset_bytes`). + // + std::size_t src_offset = 0; + const struct UdpardFragment* frag = &payload_; + while ((nullptr != frag) && (offset_bytes >= (src_offset + frag->view.size))) { - ::udpardRxFragmentFree(payload_, delegate_.memoryResources().fragment, delegate_.memoryResources().payload); + src_offset += frag->view.size; + frag = frag->next; } - UdpardMemory(const UdpardMemory&) = delete; - UdpardMemory& operator=(const UdpardMemory&) = delete; - UdpardMemory& operator=(UdpardMemory&&) noexcept = delete; + std::size_t dst_offset = 0; + std::size_t total_bytes_copied = 0; - // MARK: ScatteredBuffer::IStorage + CETL_DEBUG_ASSERT(offset_bytes >= src_offset, ""); + std::size_t view_offset = offset_bytes - src_offset; - CETL_NODISCARD std::size_t size() const noexcept override + while ((nullptr != frag) && (dst_offset < length_bytes)) { - return payload_size_; + CETL_DEBUG_ASSERT(nullptr != frag->view.data, ""); + // Next nolint-s are unavoidable: we need offset from the beginning of the buffer. + // No Sonar `cpp:S5356` b/c we integrate here with libcanard raw C buffers. + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) + FragSpan frag_span{static_cast(frag->view.data) + view_offset, // NOSONAR cpp:S5356 + std::min(frag->view.size - view_offset, length_bytes - dst_offset)}; + CETL_DEBUG_ASSERT(frag_span.size() <= (frag->view.size - view_offset), ""); + + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) + (void) std::memmove(destination + dst_offset, frag_span.data(), frag_span.size()); // NOSONAR cpp:S5356 + + src_offset += frag_span.size(); + dst_offset += frag_span.size(); + total_bytes_copied += frag_span.size(); + CETL_DEBUG_ASSERT(dst_offset <= length_bytes, ""); + frag = frag->next; + view_offset = 0; } - CETL_NODISCARD std::size_t copy(const std::size_t offset_bytes, - cetl::byte* const destination, - const std::size_t length_bytes) const override - { - using FragSpan = const cetl::span; + return total_bytes_copied; + } - // TODO: Use `udpardGather` function when it will be available with offset support. +private: + // MARK: Data members: - CETL_DEBUG_ASSERT((destination != nullptr) || (length_bytes == 0), - "Destination could be null only with zero bytes ask."); + const MemoryResources& memory_resources_; + std::size_t payload_size_; + UdpardFragment payload_; - if ((destination == nullptr) || (payload_.view.data == nullptr) || (payload_size_ <= offset_bytes)) - { - return 0; - } +}; // UdpardMemory - // Find first fragment to start from (according to source `offset_bytes`). - // - std::size_t src_offset = 0; - const struct UdpardFragment* frag = &payload_; - while ((nullptr != frag) && (offset_bytes >= (src_offset + frag->view.size))) - { - src_offset += frag->view.size; - frag = frag->next; - } +// MARK: - - std::size_t dst_offset = 0; - std::size_t total_bytes_copied = 0; +/// This internal session delegate class serves the following purpose: it provides an interface (aka gateway) +/// to access RX session from transport (by casting udpard `user_reference` member to this class). +/// +class IRxSessionDelegate +{ +public: + IRxSessionDelegate(const IRxSessionDelegate&) = delete; + IRxSessionDelegate(IRxSessionDelegate&&) noexcept = delete; + IRxSessionDelegate& operator=(const IRxSessionDelegate&) = delete; + IRxSessionDelegate& operator=(IRxSessionDelegate&&) noexcept = delete; - CETL_DEBUG_ASSERT(offset_bytes >= src_offset, ""); - std::size_t view_offset = offset_bytes - src_offset; + /// @brief Accepts a received transfer from the transport dedicated to this RX session. + /// + virtual void acceptRxTransfer(UdpardMemory&& lizard_memory, + const TransferRxMetadata& rx_metadata, + const NodeId source_node_id) = 0; - while ((nullptr != frag) && (dst_offset < length_bytes)) - { - CETL_DEBUG_ASSERT(nullptr != frag->view.data, ""); - // Next nolint-s are unavoidable: we need offset from the beginning of the buffer. - // No Sonar `cpp:S5356` b/c we integrate here with libcanard raw C buffers. - // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) - FragSpan frag_span{static_cast(frag->view.data) + view_offset, // NOSONAR cpp:S5356 - std::min(frag->view.size - view_offset, length_bytes - dst_offset)}; - CETL_DEBUG_ASSERT(frag_span.size() <= (frag->view.size - view_offset), ""); - - // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) - (void) std::memmove(destination + dst_offset, frag_span.data(), frag_span.size()); // NOSONAR cpp:S5356 - - src_offset += frag_span.size(); - dst_offset += frag_span.size(); - total_bytes_copied += frag_span.size(); - CETL_DEBUG_ASSERT(dst_offset <= length_bytes, ""); - frag = frag->next; - view_offset = 0; - } +protected: + IRxSessionDelegate() = default; + ~IRxSessionDelegate() = default; - return total_bytes_copied; - } +}; // IRxSessionDelegate - private: - // MARK: Data members: +/// This internal session delegate class serves the following purpose: +/// it provides an interface (aka gateway) to access Message RX session from transport. +/// +class IMsgRxSessionDelegate : public IRxSessionDelegate +{ +public: + IMsgRxSessionDelegate(const IMsgRxSessionDelegate&) = delete; + IMsgRxSessionDelegate(IMsgRxSessionDelegate&&) noexcept = delete; + IMsgRxSessionDelegate& operator=(const IMsgRxSessionDelegate&) = delete; + IMsgRxSessionDelegate& operator=(IMsgRxSessionDelegate&&) noexcept = delete; - TransportDelegate& delegate_; - std::size_t payload_size_; - UdpardFragment payload_; + CETL_NODISCARD virtual UdpardRxSubscription& getSubscription() = 0; - }; // UdpardMemory +protected: + IMsgRxSessionDelegate() = default; + ~IMsgRxSessionDelegate() = default; +}; // IMsgRxSessionDelegate + +// MARK: - + +/// This internal transport delegate class serves the following purposes: +/// 1. It provides memory management functions for the Udpard library. +/// 2. It provides a way to convert Udpard error codes to `AnyFailure` type. +/// 3. It provides an interface to access the transport from various session classes. +/// +class TransportDelegate +{ +public: + /// Umbrella type for all session-related events. + /// + /// These are passed to the `onSessionEvent` method of the transport implementation. + /// struct SessionEvent { struct MsgDestroyed { - PortId subject_id; + MessageRxParams params; }; struct SvcRequestDestroyed { - PortId service_id; + RequestRxParams params; }; struct SvcResponseDestroyed { - PortId service_id; + ResponseRxParams params; }; using Variant = cetl::variant; @@ -211,7 +293,7 @@ class TransportDelegate udpard_node_id_ = node_id; UdpardUDPIPEndpoint endpoint{}; - const std::int8_t result = ::udpardRxRPCDispatcherStart(&rpc_dispatcher_, node_id, &endpoint); + const std::int8_t result = ::udpardRxRPCDispatcherStart(&rx_rpc_dispatcher_, node_id, &endpoint); (void) result; CETL_DEBUG_ASSERT(result == 0, "There is no way currently to get an error here."); @@ -220,7 +302,59 @@ class TransportDelegate CETL_NODISCARD UdpardRxRPCDispatcher& getUdpardRpcDispatcher() noexcept { - return rpc_dispatcher_; + return rx_rpc_dispatcher_; + } + + template + void listenForRxRpcPort(UdpardRxRPCPort& rpc_port, const Params& params) + { + const std::int8_t result = ::udpardRxRPCDispatcherListen(&rx_rpc_dispatcher_, + &rpc_port, + params.service_id, + IsRequest, + params.extent_bytes); + (void) result; + CETL_DEBUG_ASSERT(result >= 0, "There is no way currently to get an error here."); + CETL_DEBUG_ASSERT(result == 1, "A new registration has been expected to be created."); + } + + void retainRxRpcPortFor(const ResponseRxParams& params) + { + const auto maybe_node = rx_rpc_port_demux_nodes_.ensureNodeFor(params, std::ref(*this)); + if (const auto* const node = cetl::get_if(&maybe_node)) + { + node->get().retain(); + } + } + + CETL_NODISCARD UdpardRxRPCPort* findRxRpcPortFor(const ResponseRxParams& params) + { + if (auto* const node = rx_rpc_port_demux_nodes_.tryFindNodeFor(params)) + { + return &node->port(); + } + return nullptr; + } + + void releaseRxRpcPortFor(const ResponseRxParams& params) noexcept + { + if (auto* const node = rx_rpc_port_demux_nodes_.tryFindNodeFor(params)) + { + if (node->release()) + { + rx_rpc_port_demux_nodes_.removeNodeFor(params); + } + } + } + + void cancelRxRpcPortFor(const UdpardRxRPCPort& rpc_port_, const bool is_request) noexcept + { + const std::int8_t result = ::udpardRxRPCDispatcherCancel(&rx_rpc_dispatcher_, + rpc_port_.service_id, + is_request); // request + (void) result; + CETL_DEBUG_ASSERT(result >= 0, "There is no way currently to get an error here."); + CETL_DEBUG_ASSERT(result == 1, "Existing registration has been expected to be cancelled."); } CETL_NODISCARD static cetl::optional optAnyFailureFromUdpard(const std::int32_t result) @@ -286,39 +420,22 @@ class TransportDelegate /// /// @param event_var Describes variant of the session even has happened. /// - virtual void onSessionEvent(const SessionEvent::Variant& event_var) = 0; + virtual void onSessionEvent(const SessionEvent::Variant& event_var) noexcept = 0; -protected: - /// @brief Defines internal set of memory resources used by the UDP transport. + /// @brief Tries to find a response RX session delegate for the given parameters. /// - struct MemoryResources - { - /// The general purpose memory resource is used to provide memory for the libcyphal library. - /// It is NOT used for any Udpard TX or RX transfers, payload (de)fragmentation or transient handles, - /// but only for the libcyphal internal needs (like `make*[Rx|Tx]Session` factory calls). - cetl::pmr::memory_resource& general; - - /// The session memory resource is used to provide memory for the Udpard session instances. - /// Each instance is fixed-size, so a trivial zero-fragmentation block allocator is enough. - UdpardMemoryResource session; - - /// The fragment handles are allocated per payload fragment; each handle contains a pointer to its fragment. - /// Each instance is of a very small fixed size, so a trivial zero-fragmentation block allocator is enough. - UdpardMemoryResource fragment; - - /// The library never allocates payload buffers itself, as they are handed over by the application via - /// reception calls. Once a buffer is handed over, the library may choose to keep it if it is deemed to be - /// necessary to complete a transfer reassembly, or to discard it if it is deemed to be unnecessary. - /// Discarded payload buffers are freed using this memory resource. - UdpardMemoryDeleter payload; - }; + /// @return `nullptr` if no session delegate found for the given parameters. + /// + virtual IRxSessionDelegate* tryFindRxSessionDelegateFor(const ResponseRxParams& params) = 0; +protected: explicit TransportDelegate(const MemoryResources& memory_resources) : udpard_node_id_{UDPARD_NODE_ID_UNSET} , memory_resources_{memory_resources} - , rpc_dispatcher_{} + , rx_rpc_dispatcher_{} + , rx_rpc_port_demux_nodes_{memory_resources.general} { - const std::int8_t result = ::udpardRxRPCDispatcherInit(&rpc_dispatcher_, makeUdpardRxMemoryResources()); + const std::int8_t result = ::udpardRxRPCDispatcherInit(&rx_rpc_dispatcher_, makeUdpardRxMemoryResources()); (void) result; CETL_DEBUG_ASSERT(result == 0, "There is no way currently to get an error here."); } @@ -347,6 +464,83 @@ class TransportDelegate } private: + template + using SessionTree = transport::detail::SessionTree; + + /// Accepts transfers from RX RPC port and forwards them to the appropriate session (according to source node id). + /// Has reference counting so that it will be destroyed when no longer referenced by any RX session. + /// + class RxRpcPortDemuxNode final : public SessionTree::NodeBase, public IRxSessionDelegate + { + public: + RxRpcPortDemuxNode(const ResponseRxParams& params, std::tuple args_tuple) + : transport_delegate_{std::get<0>(args_tuple)} + , ref_count_{0} + , port_{} + { + transport_delegate_.listenForRxRpcPort(port_, params); + + // No Sonar `cpp:S5356` b/c we integrate here with C libudpard API. + port_.user_reference = static_cast(this); // NOSONAR cpp:S5356 + } + + RxRpcPortDemuxNode(const RxRpcPortDemuxNode&) = delete; + RxRpcPortDemuxNode(RxRpcPortDemuxNode&&) noexcept = delete; + RxRpcPortDemuxNode& operator=(const RxRpcPortDemuxNode&) = delete; + RxRpcPortDemuxNode& operator=(RxRpcPortDemuxNode&&) noexcept = delete; + + ~RxRpcPortDemuxNode() + { + transport_delegate_.cancelRxRpcPortFor(port_, false); // response + } + + CETL_NODISCARD std::int32_t compareByParams(const ResponseRxParams& params) const + { + return static_cast(port_.service_id) - static_cast(params.service_id); + } + + CETL_NODISCARD UdpardRxRPCPort& port() noexcept + { + return port_; + } + + void retain() noexcept + { + ++ref_count_; + } + + bool release() noexcept + { + CETL_DEBUG_ASSERT(ref_count_ > 0, ""); + --ref_count_; + return ref_count_ == 0; + } + + private: + // IRxSessionDelegate + + void acceptRxTransfer(UdpardMemory&& lizard_memory, + const TransferRxMetadata& rx_metadata, + const NodeId source_node_id) override + { + // This is where de-multiplexing happens: the transfer is forwarded to the appropriate session. + // It's ok not to find the session delegate here - we drop unsolicited transfers. + // + const ResponseRxParams params{0, port_.service_id, source_node_id}; + if (auto* const session_delegate = transport_delegate_.tryFindRxSessionDelegateFor(params)) + { + session_delegate->acceptRxTransfer(std::move(lizard_memory), rx_metadata, source_node_id); + } + } + + // MARK: Data members: + + TransportDelegate& transport_delegate_; + std::size_t ref_count_; + UdpardRxRPCPort port_; + + }; // RxRpcPortDemuxNode + /// @brief Allocates memory for udpard. /// /// NOSONAR cpp:S5008 is unavoidable: this is integration with Udpard C memory management. @@ -383,58 +577,13 @@ class TransportDelegate // MARK: Data members: - UdpardNodeID udpard_node_id_; - const MemoryResources memory_resources_; - UdpardRxRPCDispatcher rpc_dispatcher_; + UdpardNodeID udpard_node_id_; + const MemoryResources memory_resources_; + UdpardRxRPCDispatcher rx_rpc_dispatcher_; + SessionTree rx_rpc_port_demux_nodes_; }; // TransportDelegate -// MARK: - - -/// This internal session delegate class serves the following purpose: it provides an interface (aka gateway) -/// to access RX session from transport (by casting udpard `user_reference` member to this class). -/// -class IRxSessionDelegate -{ -public: - IRxSessionDelegate(const IRxSessionDelegate&) = delete; - IRxSessionDelegate(IRxSessionDelegate&&) noexcept = delete; - IRxSessionDelegate& operator=(const IRxSessionDelegate&) = delete; - IRxSessionDelegate& operator=(IRxSessionDelegate&&) noexcept = delete; - - /// @brief Accepts a received transfer from the transport dedicated to this RX session. - /// - /// @param inout_transfer The received transfer to be accepted. An implementation is expected to take ownership - /// of the transfer payload, and to release it when it is no longer needed. - /// On exit the original transfer's `payload_size` and `payload` fields are set to zero. - /// - virtual void acceptRxTransfer(UdpardRxTransfer& inout_transfer) = 0; - -protected: - IRxSessionDelegate() = default; - ~IRxSessionDelegate() = default; - -}; // IRxSessionDelegate - -/// This internal session delegate class serves the following purpose: -/// it provides an interface (aka gateway) to access Message RX session from transport. -/// -class IMsgRxSessionDelegate : public IRxSessionDelegate -{ -public: - IMsgRxSessionDelegate(const IMsgRxSessionDelegate&) = delete; - IMsgRxSessionDelegate(IMsgRxSessionDelegate&&) noexcept = delete; - IMsgRxSessionDelegate& operator=(const IMsgRxSessionDelegate&) = delete; - IMsgRxSessionDelegate& operator=(IMsgRxSessionDelegate&&) noexcept = delete; - - CETL_NODISCARD virtual UdpardRxSubscription& getSubscription() = 0; - -protected: - IMsgRxSessionDelegate() = default; - ~IMsgRxSessionDelegate() = default; - -}; // IMsgRxSessionDelegate - } // namespace detail } // namespace udp } // namespace transport diff --git a/include/libcyphal/transport/udp/msg_rx_session.hpp b/include/libcyphal/transport/udp/msg_rx_session.hpp index 6b5311eee..07f2c7801 100644 --- a/include/libcyphal/transport/udp/msg_rx_session.hpp +++ b/include/libcyphal/transport/udp/msg_rx_session.hpp @@ -7,7 +7,6 @@ #define LIBCYPHAL_TRANSPORT_UDP_MSG_RX_SESSION_HPP_INCLUDED #include "delegate.hpp" -#include "session_tree.hpp" #include "libcyphal/errors.hpp" #include "libcyphal/transport/errors.hpp" @@ -50,7 +49,7 @@ class MessageRxSession final : private IMsgRxSessionDelegate, public IMessageRxS }; public: - CETL_NODISCARD static Expected, AnyFailure> make( + CETL_NODISCARD static Expected, AnyFailure> make( // cetl::pmr::memory_resource& memory, TransportDelegate& delegate, const MessageRxParams& params, @@ -97,7 +96,7 @@ class MessageRxSession final : private IMsgRxSessionDelegate, public IMessageRxS { ::udpardRxSubscriptionFree(&subscription_); - delegate_.onSessionEvent(TransportDelegate::SessionEvent::MsgDestroyed{params_.subject_id}); + delegate_.onSessionEvent(TransportDelegate::SessionEvent::MsgDestroyed{params_}); } // In use (public) for unit tests only. @@ -143,21 +142,15 @@ class MessageRxSession final : private IMsgRxSessionDelegate, public IMessageRxS // MARK: IRxSessionDelegate - void acceptRxTransfer(UdpardRxTransfer& inout_transfer) override + void acceptRxTransfer(UdpardMemory&& lizard_memory, + const TransferRxMetadata& rx_metadata, + const NodeId source_node_id) override { - const auto transfer_id = inout_transfer.transfer_id; - const auto priority = static_cast(inout_transfer.priority); - const auto timestamp = TimePoint{std::chrono::microseconds{inout_transfer.timestamp_usec}}; - const cetl::optional publisher_node_id = - inout_transfer.source_node_id > UDPARD_NODE_ID_MAX - ? cetl::nullopt - : cetl::make_optional(inout_transfer.source_node_id); - - TransportDelegate::UdpardMemory udpard_memory{delegate_, inout_transfer}; + source_node_id > UDPARD_NODE_ID_MAX ? cetl::nullopt : cetl::make_optional(source_node_id); - const MessageRxMetadata meta{{{transfer_id, priority}, timestamp}, publisher_node_id}; - MessageRxTransfer msg_rx_transfer{meta, ScatteredBuffer{std::move(udpard_memory)}}; + const MessageRxMetadata meta{rx_metadata, publisher_node_id}; + MessageRxTransfer msg_rx_transfer{meta, ScatteredBuffer{std::move(lizard_memory)}}; if (on_receive_cb_fn_) { on_receive_cb_fn_(OnReceiveCallback::Arg{msg_rx_transfer}); diff --git a/include/libcyphal/transport/udp/msg_tx_session.hpp b/include/libcyphal/transport/udp/msg_tx_session.hpp index 6a6cbc5d7..3ecd8253c 100644 --- a/include/libcyphal/transport/udp/msg_tx_session.hpp +++ b/include/libcyphal/transport/udp/msg_tx_session.hpp @@ -44,9 +44,10 @@ class MessageTxSession final : public IMessageTxSession }; public: - CETL_NODISCARD static Expected, AnyFailure> make(cetl::pmr::memory_resource& memory, - TransportDelegate& delegate, - const MessageTxParams& params) + CETL_NODISCARD static Expected, AnyFailure> make( // + cetl::pmr::memory_resource& memory, + TransportDelegate& delegate, + const MessageTxParams& params) { if (params.subject_id > UDPARD_SUBJECT_ID_MAX) { diff --git a/include/libcyphal/transport/udp/rx_session_tree_node.hpp b/include/libcyphal/transport/udp/rx_session_tree_node.hpp new file mode 100644 index 000000000..072034286 --- /dev/null +++ b/include/libcyphal/transport/udp/rx_session_tree_node.hpp @@ -0,0 +1,129 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_UDP_RX_SESSION_TREE_NODE_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_UDP_RX_SESSION_TREE_NODE_HPP_INCLUDED + +#include "tx_rx_sockets.hpp" + +#include "libcyphal/executor.hpp" +#include "libcyphal/transport/msg_sessions.hpp" +#include "libcyphal/transport/session_tree.hpp" +#include "libcyphal/transport/svc_sessions.hpp" +#include "libcyphal/transport/types.hpp" +#include "libcyphal/types.hpp" + +#include +#include + +#include +#include +#include + +namespace libcyphal +{ +namespace transport +{ +namespace udp +{ + +/// Internal implementation details of the UDP transport. +/// Not supposed to be used directly by the users of the library. +/// +namespace detail +{ + +template +struct SocketState +{ + UniquePtr interface; + IExecutor::Callback::Any callback; + +}; // SocketState + +class IRxSessionDelegate; +class IMsgRxSessionDelegate; + +/// Umbrella type for various RX session tree nodes in use at the UDP transport. +/// +struct RxSessionTreeNode +{ + /// @brief Represents a message RX session node. + /// + class Message final : public transport::detail::SessionTree::NodeBase + { + public: + // Empty tuple parameter is used to allow for the same constructor signature + // as for other session nodes (see also `SessionTree::ensureNodeFor` method). + // + explicit Message(const MessageRxParams& params, const std::tuple<>&) + : subject_id_{params.subject_id} + { + } + + CETL_NODISCARD std::int32_t compareByParams(const MessageRxParams& params) const + { + return static_cast(subject_id_) - static_cast(params.subject_id); + } + + CETL_NODISCARD IMsgRxSessionDelegate*& delegate() noexcept + { + return delegate_; + } + + CETL_NODISCARD SocketState& socketState(const std::uint8_t media_index) noexcept + { + CETL_DEBUG_ASSERT(media_index < socket_states_.size(), ""); + + // No lint b/c at transport constructor we made sure that the number of media interfaces is bound. + return socket_states_[media_index]; // NOLINT(cppcoreguidelines-pro-bounds-constant-array-index) + } + + private: + // MARK: Data members: + + const PortId subject_id_; + IMsgRxSessionDelegate* delegate_{nullptr}; + std::array, UDPARD_NETWORK_INTERFACE_COUNT_MAX> socket_states_; + + }; // Message + + /// @brief Represents a service request RX session node. + /// + class Request final : public transport::detail::SessionTree::NodeBase + { + public: + // Empty tuple parameter is used to allow for the same constructor signature + // as for other session nodes (see also `SessionTree::ensureNodeFor` method). + // + explicit Request(const RequestRxParams& params, const std::tuple<>&) + : service_id_{params.service_id} + { + } + + CETL_NODISCARD std::int32_t compareByParams(const RequestRxParams& params) const + { + return static_cast(service_id_) - static_cast(params.service_id); + } + + private: + // MARK: Data members: + + const PortId service_id_; + + }; // Request + + /// @brief Represents a service response RX session node. + /// + using Response = transport::detail::ResponseRxSessionNode; + +}; // RxSessionTreeNode + +} // namespace detail +} // namespace udp +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_UDP_RX_SESSION_TREE_NODE_HPP_INCLUDED diff --git a/include/libcyphal/transport/udp/session_tree.hpp b/include/libcyphal/transport/udp/session_tree.hpp deleted file mode 100644 index c2fd77870..000000000 --- a/include/libcyphal/transport/udp/session_tree.hpp +++ /dev/null @@ -1,227 +0,0 @@ -/// @copyright -/// Copyright (C) OpenCyphal Development Team -/// Copyright Amazon.com Inc. or its affiliates. -/// SPDX-License-Identifier: MIT - -#ifndef LIBCYPHAL_TRANSPORT_UDP_SESSION_TREE_HPP_INCLUDED -#define LIBCYPHAL_TRANSPORT_UDP_SESSION_TREE_HPP_INCLUDED - -#include "delegate.hpp" -#include "tx_rx_sockets.hpp" - -#include "libcyphal/common/cavl/cavl.hpp" -#include "libcyphal/executor.hpp" -#include "libcyphal/transport/errors.hpp" -#include "libcyphal/transport/types.hpp" -#include "libcyphal/types.hpp" - -#include -#include -#include - -#include -#include -#include - -namespace libcyphal -{ -namespace transport -{ -namespace udp -{ - -/// Internal implementation details of the UDP transport. -/// Not supposed to be used directly by the users of the library. -/// -namespace detail -{ - -template -struct SocketState -{ - UniquePtr interface; - IExecutor::Callback::Any callback; - -}; // SocketState - -/// @brief Defines a tree of sessions for the UDP transport. -/// -template -class SessionTree final -{ -public: - using NodeRef = typename Node::ReferenceWrapper; - - explicit SessionTree(cetl::pmr::memory_resource& mr) - : allocator_{&mr} - { - } - - SessionTree(const SessionTree&) = delete; - SessionTree(SessionTree&&) noexcept = delete; - SessionTree& operator=(const SessionTree&) = delete; - SessionTree& operator=(SessionTree&&) noexcept = delete; - - ~SessionTree() - { - nodes_.traversePostOrder([this](auto& node) { destroyNode(node); }); - } - - CETL_NODISCARD bool isEmpty() const noexcept - { - return nodes_.empty(); - } - - CETL_NODISCARD Expected ensureNewNodeFor(const PortId port_id) - { - const auto node_existing = nodes_.search( - [port_id](const Node& node) { // predicate - // - return node.compareByPortId(port_id); - }, - [this, port_id]() { // factory - // - return constructNewNode(port_id); - }); - - auto* const node = std::get<0>(node_existing); - if (nullptr == node) - { - return MemoryError{}; - } - if (std::get<1>(node_existing)) - { - return AlreadyExistsError{}; - } - - return *node; - } - - void removeNodeFor(const PortId port_id) - { - removeAndDestroyNode(nodes_.search([port_id](const Node& node) { // predicate - // - return node.compareByPortId(port_id); - })); - } - - template - CETL_NODISCARD cetl::optional forEachNode(Action&& action) - { - return nodes_.traverse(std::forward(action)); - } - -private: - CETL_NODISCARD Node* constructNewNode(const PortId port_id) - { - Node* const node = allocator_.allocate(1); - if (nullptr != node) - { - allocator_.construct(node, port_id); - } - return node; - } - - void removeAndDestroyNode(Node* node) - { - if (nullptr != node) - { - nodes_.remove(node); - destroyNode(*node); - } - } - - void destroyNode(Node& node) - { - // No Sonar cpp:M23_329 b/c we do our own low-level PMR management here. - node.~Node(); // NOSONAR cpp:M23_329 - allocator_.deallocate(&node, 1); - } - - // MARK: Data members: - - common::cavl::Tree nodes_; - libcyphal::detail::PmrAllocator allocator_; - -}; // SessionTree - -// MARK: - - -struct RxSessionTreeNode -{ - template - class Base : public common::cavl::Node - { - public: - using common::cavl::Node::getChildNode; - using ReferenceWrapper = std::reference_wrapper; - - explicit Base(const PortId port_id) - : port_id_{port_id} - { - } - - CETL_NODISCARD std::int32_t compareByPortId(const PortId port_id) const - { - return static_cast(port_id_) - static_cast(port_id); - } - - private: - // MARK: Data members: - - PortId port_id_; - - }; // Base - - /// @brief Represents a message RX session node. - /// - class Message final : public Base - { - public: - using Base::Base; - - CETL_NODISCARD IMsgRxSessionDelegate*& delegate() noexcept - { - return delegate_; - } - - CETL_NODISCARD SocketState& socketState(const std::uint8_t media_index) noexcept - { - CETL_DEBUG_ASSERT(media_index < socket_states_.size(), ""); - - // No lint b/c at transport constructor we made sure that number of media interfaces is bound. - return socket_states_[media_index]; // NOLINT(cppcoreguidelines-pro-bounds-constant-array-index) - } - - private: - // MARK: Data members: - - IMsgRxSessionDelegate* delegate_{nullptr}; - std::array, UDPARD_NETWORK_INTERFACE_COUNT_MAX> socket_states_; - - }; // Message - - /// @brief Represents a service request RX session node. - /// - class Request final : public Base - { - public: - using Base::Base; - }; - - /// @brief Represents a service response RX session node. - /// - class Response final : public Base - { - public: - using Base::Base; - }; - -}; // RxSessionTreeNode - -} // namespace detail -} // namespace udp -} // namespace transport -} // namespace libcyphal - -#endif // LIBCYPHAL_TRANSPORT_UDP_SESSION_TREE_HPP_INCLUDED diff --git a/include/libcyphal/transport/udp/svc_rx_sessions.hpp b/include/libcyphal/transport/udp/svc_rx_sessions.hpp index d2777327c..5e4516e52 100644 --- a/include/libcyphal/transport/udp/svc_rx_sessions.hpp +++ b/include/libcyphal/transport/udp/svc_rx_sessions.hpp @@ -10,8 +10,8 @@ #include "libcyphal/errors.hpp" #include "libcyphal/transport/errors.hpp" +#include "libcyphal/transport/svc_rx_session_base.hpp" #include "libcyphal/transport/svc_sessions.hpp" -#include "libcyphal/transport/types.hpp" #include "libcyphal/types.hpp" #include @@ -19,8 +19,6 @@ #include #include -#include -#include namespace libcyphal { @@ -35,19 +33,15 @@ namespace udp namespace detail { -/// @brief A template class to represent a service request/response RX session (both for server and client sides). -/// -/// @tparam Interface_ Type of the session interface. -/// Could be either `IRequestRxSession` or `IResponseRxSession`. -/// @tparam Params Type of the session parameters. -/// Could be either `RequestRxParams` or `ResponseRxParams`. +/// @brief A concrete class to represent a service request RX session (aka server side). /// -template -class SvcRxSession final : private IRxSessionDelegate, public Interface_ +class SvcRequestRxSession final + : public transport::detail:: // + SvcRxSessionBase { /// @brief Defines private specification for making interface unique ptr. /// - struct Spec : libcyphal::detail::UniquePtrSpec + struct Spec : libcyphal::detail::UniquePtrSpec { // `explicit` here is in use to disable public construction of derived private `Spec` structs. // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ @@ -55,9 +49,11 @@ class SvcRxSession final : private IRxSessionDelegate, public Interface_ }; public: - CETL_NODISCARD static Expected, AnyFailure> make(cetl::pmr::memory_resource& memory, - TransportDelegate& delegate, - const Params& params) + CETL_NODISCARD static Expected, AnyFailure> make( // + cetl::pmr::memory_resource& memory, + TransportDelegate& delegate, + const RequestRxParams& params, + const RxSessionTreeNode::Request&) { if (params.service_id > UDPARD_SERVICE_ID_MAX) { @@ -73,38 +69,25 @@ class SvcRxSession final : private IRxSessionDelegate, public Interface_ return session; } - SvcRxSession(const Spec, TransportDelegate& delegate, const Params& params) - : delegate_{delegate} - , params_{params} + SvcRequestRxSession(const Spec, TransportDelegate& delegate, const RequestRxParams& params) + : Base{delegate, params} , rpc_port_{} { - const std::int8_t result = ::udpardRxRPCDispatcherListen(&delegate.getUdpardRpcDispatcher(), - &rpc_port_, - params.service_id, - IsRequest, - params.extent_bytes); - (void) result; - CETL_DEBUG_ASSERT(result >= 0, "There is no way currently to get an error here."); - CETL_DEBUG_ASSERT(result == 1, "A new registration has been expected to be created."); + delegate.listenForRxRpcPort(rpc_port_, params); // No Sonar `cpp:S5356` b/c we integrate here with C libudpard API. rpc_port_.user_reference = static_cast(this); // NOSONAR cpp:S5356 } - SvcRxSession(const SvcRxSession&) = delete; - SvcRxSession(SvcRxSession&&) noexcept = delete; - SvcRxSession& operator=(const SvcRxSession&) = delete; - SvcRxSession& operator=(SvcRxSession&&) noexcept = delete; + SvcRequestRxSession(const SvcRequestRxSession&) = delete; + SvcRequestRxSession(SvcRequestRxSession&&) noexcept = delete; + SvcRequestRxSession& operator=(const SvcRequestRxSession&) = delete; + SvcRequestRxSession& operator=(SvcRequestRxSession&&) noexcept = delete; - ~SvcRxSession() + ~SvcRequestRxSession() { - const std::int8_t result = - ::udpardRxRPCDispatcherCancel(&delegate_.getUdpardRpcDispatcher(), params_.service_id, IsRequest); - (void) result; - CETL_DEBUG_ASSERT(result >= 0, "There is no way currently to get an error here."); - CETL_DEBUG_ASSERT(result == 1, "Existing registration has been expected to be cancelled."); - - delegate_.onSessionEvent(SessionEvent{params_.service_id}); + delegate().cancelRxRpcPortFor(rpc_port_, true); // request + delegate().onSessionEvent(TransportDelegate::SessionEvent::SvcRequestDestroyed{getParams()}); } // In use (public) for unit tests only. @@ -114,28 +97,7 @@ class SvcRxSession final : private IRxSessionDelegate, public Interface_ } private: - // MARK: Interface - - CETL_NODISCARD Params getParams() const noexcept override - { - return params_; - } - - CETL_NODISCARD cetl::optional receive() override - { - if (last_rx_transfer_) - { - auto transfer = std::move(*last_rx_transfer_); - last_rx_transfer_.reset(); - return transfer; - } - return cetl::nullopt; - } - - void setOnReceiveCallback(ISvcRxSession::OnReceiveCallback::Function&& function) override - { - on_receive_cb_fn_ = std::move(function); - } + using Base = SvcRxSessionBase; // MARK: IRxSession @@ -148,52 +110,90 @@ class SvcRxSession final : private IRxSessionDelegate, public Interface_ } } - // MARK: IRxSessionDelegate + // MARK: Data members: + + UdpardRxRPCPort rpc_port_; + +}; // SvcRequestRxSession + +// MARK: - - void acceptRxTransfer(UdpardRxTransfer& inout_transfer) override +/// @brief A concrete class to represent a service response RX session (aka client side). +/// +class SvcResponseRxSession final + : public transport::detail:: // + SvcRxSessionBase +{ + /// @brief Defines private specification for making interface unique ptr. + /// + struct Spec : libcyphal::detail::UniquePtrSpec { - const auto transfer_id = inout_transfer.transfer_id; - const auto remote_node_id = inout_transfer.source_node_id; - const auto priority = static_cast(inout_transfer.priority); - const auto timestamp = TimePoint{std::chrono::microseconds{inout_transfer.timestamp_usec}}; + // `explicit` here is in use to disable public construction of derived private `Spec` structs. + // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ + explicit Spec() = default; + }; - TransportDelegate::UdpardMemory udpard_memory{delegate_, inout_transfer}; +public: + CETL_NODISCARD static Expected, AnyFailure> make( // + cetl::pmr::memory_resource& memory, + TransportDelegate& delegate, + const ResponseRxParams& params, + RxSessionTreeNode::Response& rx_session_node) + { + if (params.service_id > UDPARD_SERVICE_ID_MAX) + { + return ArgumentError{}; + } - const ServiceRxMetadata meta{{{transfer_id, priority}, timestamp}, remote_node_id}; - ServiceRxTransfer svc_rx_transfer{meta, ScatteredBuffer{std::move(udpard_memory)}}; - if (on_receive_cb_fn_) + auto session = libcyphal::detail::makeUniquePtr(memory, Spec{}, delegate, params, rx_session_node); + if (session == nullptr) { - on_receive_cb_fn_(ISvcRxSession::OnReceiveCallback::Arg{svc_rx_transfer}); - return; + return MemoryError{}; } - (void) last_rx_transfer_.emplace(std::move(svc_rx_transfer)); + + return session; } - // MARK: Data members: + SvcResponseRxSession(const Spec, + TransportDelegate& delegate, + const ResponseRxParams& params, + RxSessionTreeNode::Response& rx_session_node) + : Base{delegate, params} + { + delegate.retainRxRpcPortFor(params); - TransportDelegate& delegate_; - const Params params_; - UdpardRxRPCPort rpc_port_; - cetl::optional last_rx_transfer_; - ISvcRxSession::OnReceiveCallback::Function on_receive_cb_fn_; + rx_session_node.delegate() = this; + } -}; // SvcRxSession + SvcResponseRxSession(const SvcResponseRxSession&) = delete; + SvcResponseRxSession(SvcResponseRxSession&&) noexcept = delete; + SvcResponseRxSession& operator=(const SvcResponseRxSession&) = delete; + SvcResponseRxSession& operator=(SvcResponseRxSession&&) noexcept = delete; -// MARK: - + ~SvcResponseRxSession() + { + delegate().releaseRxRpcPortFor(getParams()); + delegate().onSessionEvent(TransportDelegate::SessionEvent::SvcResponseDestroyed{getParams()}); + } -/// @brief A concrete class to represent a service request RX session (aka server side). -/// -using SvcRequestRxSession = SvcRxSession; +private: + using Base = SvcRxSessionBase; -/// @brief A concrete class to represent a service response RX session (aka client side). -/// -using SvcResponseRxSession = SvcRxSession; + // MARK: IRxSession + + void setTransferIdTimeout(const Duration timeout) override + { + const auto timeout_us = std::chrono::duration_cast(timeout); + if (timeout_us >= Duration::zero()) + { + if (auto* const rpc_port = delegate().findRxRpcPortFor(getParams())) + { + rpc_port->port.transfer_id_timeout_usec = static_cast(timeout_us.count()); + } + } + } + +}; // SvcResponseRxSession } // namespace detail } // namespace udp diff --git a/include/libcyphal/transport/udp/svc_tx_sessions.hpp b/include/libcyphal/transport/udp/svc_tx_sessions.hpp index 5a6653751..24091ccd8 100644 --- a/include/libcyphal/transport/udp/svc_tx_sessions.hpp +++ b/include/libcyphal/transport/udp/svc_tx_sessions.hpp @@ -46,9 +46,10 @@ class SvcRequestTxSession final : public IRequestTxSession }; public: - CETL_NODISCARD static Expected, AnyFailure> make(cetl::pmr::memory_resource& memory, - TransportDelegate& delegate, - const RequestTxParams& params) + CETL_NODISCARD static Expected, AnyFailure> make( // + cetl::pmr::memory_resource& memory, + TransportDelegate& delegate, + const RequestTxParams& params) { if ((params.service_id > UDPARD_SERVICE_ID_MAX) || (params.server_node_id > UDPARD_NODE_ID_MAX)) { @@ -125,9 +126,10 @@ class SvcResponseTxSession final : public IResponseTxSession }; public: - CETL_NODISCARD static Expected, AnyFailure> make(cetl::pmr::memory_resource& memory, - TransportDelegate& delegate, - const ResponseTxParams& params) + CETL_NODISCARD static Expected, AnyFailure> make( // + cetl::pmr::memory_resource& memory, + TransportDelegate& delegate, + const ResponseTxParams& params) { if (params.service_id > UDPARD_SERVICE_ID_MAX) { diff --git a/include/libcyphal/transport/udp/udp_transport_impl.hpp b/include/libcyphal/transport/udp/udp_transport_impl.hpp index a2eb5b84c..89862b678 100644 --- a/include/libcyphal/transport/udp/udp_transport_impl.hpp +++ b/include/libcyphal/transport/udp/udp_transport_impl.hpp @@ -10,7 +10,7 @@ #include "media.hpp" #include "msg_rx_session.hpp" #include "msg_tx_session.hpp" -#include "session_tree.hpp" +#include "rx_session_tree_node.hpp" #include "svc_rx_sessions.hpp" #include "svc_tx_sessions.hpp" #include "tx_rx_sockets.hpp" @@ -21,6 +21,7 @@ #include "libcyphal/transport/errors.hpp" #include "libcyphal/transport/lizard_helpers.hpp" #include "libcyphal/transport/msg_sessions.hpp" +#include "libcyphal/transport/session_tree.hpp" #include "libcyphal/transport/svc_sessions.hpp" #include "libcyphal/transport/types.hpp" #include "libcyphal/types.hpp" @@ -275,7 +276,7 @@ class TransportImpl final : private TransportDelegate, public IUdpTransport CETL_NODISCARD Expected, AnyFailure> makeMessageRxSession( const MessageRxParams& params) override { - return makeMsgRxSession(params, msg_rx_session_nodes_); + return makeMsgRxSessionImpl(params, msg_rx_session_nodes_); } CETL_NODISCARD Expected, AnyFailure> makeMessageTxSession( @@ -293,9 +294,9 @@ class TransportImpl final : private TransportDelegate, public IUdpTransport CETL_NODISCARD Expected, AnyFailure> makeRequestRxSession( const RequestRxParams& params) override { - return makeSvcRxSession(params.service_id, - params, - svc_request_rx_session_nodes_); + return makeSvcRxSessionImpl( // + params, + svc_request_rx_session_nodes_); } CETL_NODISCARD Expected, AnyFailure> makeRequestTxSession( @@ -313,9 +314,9 @@ class TransportImpl final : private TransportDelegate, public IUdpTransport CETL_NODISCARD Expected, AnyFailure> makeResponseRxSession( const ResponseRxParams& params) override { - return makeSvcRxSession(params.service_id, - params, - svc_response_rx_session_nodes_); + return makeSvcRxSessionImpl( // + params, + svc_response_rx_session_nodes_); } CETL_NODISCARD Expected, AnyFailure> makeResponseTxSession( @@ -384,24 +385,39 @@ class TransportImpl final : private TransportDelegate, public IUdpTransport return cetl::nullopt; } - void onSessionEvent(const SessionEvent::Variant& event_var) override + void onSessionEvent(const SessionEvent::Variant& event_var) noexcept override { - cetl::visit(cetl::make_overloaded( - [this](const SessionEvent::MsgDestroyed& msg_session_destroyed) { - // - msg_rx_session_nodes_.removeNodeFor(msg_session_destroyed.subject_id); - }, - [this](const SessionEvent::SvcRequestDestroyed& req_session_destroyed) { - // - svc_request_rx_session_nodes_.removeNodeFor(req_session_destroyed.service_id); - cancelRxCallbacksIfNoSvcLeft(); - }, - [this](const SessionEvent::SvcResponseDestroyed& res_session_destroyed) { - // - svc_response_rx_session_nodes_.removeNodeFor(res_session_destroyed.service_id); - cancelRxCallbacksIfNoSvcLeft(); - }), - event_var); + // `visit` might hypothetically throw, so we need to catch it. + const auto result = libcyphal::detail::performWithoutThrowing([this, &event_var] { + // + cetl::visit(cetl::make_overloaded( // + [this](const SessionEvent::MsgDestroyed& msg_session_destroyed) noexcept { + // + msg_rx_session_nodes_.removeNodeFor(msg_session_destroyed.params); + }, + [this](const SessionEvent::SvcRequestDestroyed& req_session_destroyed) noexcept { + // + svc_request_rx_session_nodes_.removeNodeFor(req_session_destroyed.params); + cancelRxCallbacksIfNoSvcLeft(); + }, + [this](const SessionEvent::SvcResponseDestroyed& res_session_destroyed) noexcept { + // + svc_response_rx_session_nodes_.removeNodeFor(res_session_destroyed.params); + cancelRxCallbacksIfNoSvcLeft(); + }), + event_var); + }); + (void) result; + CETL_DEBUG_ASSERT(result, ""); + } + + IRxSessionDelegate* tryFindRxSessionDelegateFor(const ResponseRxParams& params) override + { + if (auto* const node = svc_response_rx_session_nodes_.tryFindNodeFor(params)) + { + return node->delegate(); + } + return nullptr; } // MARK: Privates: @@ -409,6 +425,9 @@ class TransportImpl final : private TransportDelegate, public IUdpTransport using Self = TransportImpl; using ContiguousPayload = transport::detail::ContiguousPayload; + template + using SessionTree = transport::detail::SessionTree; + struct TxTransferHandler { // No Sonar `cpp:S5356` b/c we integrate here with libudpard raw C buffers. @@ -473,21 +492,24 @@ class TransportImpl final : private TransportDelegate, public IUdpTransport }; // TxTransferHandler - CETL_NODISCARD auto makeMsgRxSession(const MessageRxParams& rx_params, - SessionTree& tree_nodes) - -> Expected, AnyFailure> + CETL_NODISCARD auto makeMsgRxSessionImpl( // + const MessageRxParams& params, + SessionTree& tree_nodes) -> Expected, AnyFailure> { - auto node_result = tree_nodes.ensureNewNodeFor(rx_params.subject_id); + // Make sure that session is unique per given parameters. + // For message sessions, the uniqueness is based on the subject ID. + // + auto node_result = tree_nodes.ensureNodeFor(params); // should be new if (auto* const failure = cetl::get_if(&node_result)) { return std::move(*failure); } - auto& new_msg_node = cetl::get(node_result).get(); + auto& new_msg_node = cetl::get(node_result).get(); - auto session_result = MessageRxSession::make(memoryResources().general, asDelegate(), rx_params, new_msg_node); + auto session_result = MessageRxSession::make(memoryResources().general, asDelegate(), params, new_msg_node); if (auto* const failure = cetl::get_if(&session_result)) { - tree_nodes.removeNodeFor(rx_params.subject_id); + tree_nodes.removeNodeFor(params); return std::move(*failure); } @@ -517,10 +539,10 @@ class TransportImpl final : private TransportDelegate, public IUdpTransport return session_result; } - template - CETL_NODISCARD auto makeSvcRxSession(const PortId port_id, - const RxParams& rx_params, - Tree& tree_nodes) -> Expected, AnyFailure> + template + CETL_NODISCARD auto makeSvcRxSessionImpl( // + const Params& params, + Tree& tree_nodes) -> Expected, AnyFailure> { // Try to create all (per each media) shared RX sockets for services. // For now, we're just creating them, without any attempt to use them yet - hence the "do nothing" action. @@ -541,20 +563,23 @@ class TransportImpl final : private TransportDelegate, public IUdpTransport return std::move(media_failure.value()); } - // Make sure that session is unique per port. + // Make sure that session is unique per given parameters. + // For request sessions, the uniqueness is based on the service ID. + // For response sessions, the uniqueness is based on the service ID and the server node ID. // - auto node_result = tree_nodes.ensureNewNodeFor(port_id); + auto node_result = tree_nodes.template ensureNodeFor(params); // should be new if (auto* const failure = cetl::get_if(&node_result)) { return std::move(*failure); } + auto& new_svc_node = cetl::get(node_result).get(); - auto session_result = Concrete::make(memoryResources().general, asDelegate(), rx_params); + auto session_result = Concrete::make(memoryResources().general, asDelegate(), params, new_svc_node); if (nullptr != cetl::get_if(&session_result)) { - // We failed to create the session, so we need to release the unique port id node. + // We failed to create the session, so we need to release the unique node. // The sockets we made earlier will be released in the destructor of whole transport. - tree_nodes.removeNodeFor(port_id); + tree_nodes.removeNodeFor(params); } return session_result; @@ -922,8 +947,16 @@ class TransportImpl final : private TransportDelegate, public IUdpTransport // No Sonar `cpp:S5357` b/c the raw `user_reference` is part of libudpard api, // and it was set by us at a RX session constructor (see f.e. `MessageRxSession` ctor). - auto* const delegate = static_cast(out_port->user_reference); // NOSONAR cpp:S5357 - delegate->acceptRxTransfer(out_transfer.base); + auto* const session_delegate = + static_cast(out_port->user_reference); // NOSONAR cpp:S5357 + + const auto transfer_id = out_transfer.base.transfer_id; + const auto priority = static_cast(out_transfer.base.priority); + const auto timestamp = TimePoint{std::chrono::microseconds{out_transfer.base.timestamp_usec}}; + + session_delegate->acceptRxTransfer(UdpardMemory{memoryResources(), out_transfer.base}, + TransferRxMetadata{{transfer_id, priority}, timestamp}, + out_transfer.base.source_node_id); } } @@ -973,11 +1006,17 @@ class TransportImpl final : private TransportDelegate, public IUdpTransport const auto failure = tryHandleTransientUdpardResult(media, result, subscription); if ((!failure.has_value()) && (result > 0)) { - session_delegate.acceptRxTransfer(out_transfer); + const auto transfer_id = out_transfer.transfer_id; + const auto priority = static_cast(out_transfer.priority); + const auto timestamp = TimePoint{std::chrono::microseconds{out_transfer.timestamp_usec}}; + + session_delegate.acceptRxTransfer(UdpardMemory{memoryResources(), out_transfer}, + TransferRxMetadata{{transfer_id, priority}, timestamp}, + out_transfer.source_node_id); } } - void cancelRxCallbacksIfNoSvcLeft() + void cancelRxCallbacksIfNoSvcLeft() noexcept { if (svc_request_rx_session_nodes_.isEmpty() && svc_response_rx_session_nodes_.isEmpty()) { diff --git a/include/libcyphal/types.hpp b/include/libcyphal/types.hpp index 18b795db3..23bb632c0 100644 --- a/include/libcyphal/types.hpp +++ b/include/libcyphal/types.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -149,6 +150,31 @@ CETL_NODISCARD UpVariant upcastVariant(Variant&& variant) std::forward(variant)); } +/// @brief Wraps the given action into a try/catch block, and performs it without throwing the given exception type. +/// +/// In use f.e. for `cetl::visit` which might hypothetically throw an exception. +/// +/// @return `true` if the action was performed successfully, `false` if an exception was thrown. +/// Always `true` if exceptions are disabled. +/// +template +CETL_NODISCARD bool performWithoutThrowing(Action&& action) noexcept +{ +#if defined(__cpp_exceptions) + try + { +#endif + std::forward(action)(); + return true; + +#if defined(__cpp_exceptions) + } catch (const Exception& ex) + { + return false; + } +#endif +} + } // namespace detail /// @brief A deleter which uses Polymorphic Memory Resource (PMR) for de-allocation of raw bytes memory buffers. diff --git a/test/unittest/transport/can/test_can_delegate.cpp b/test/unittest/transport/can/test_can_delegate.cpp index 9a5a32929..37fda2ea3 100644 --- a/test/unittest/transport/can/test_can_delegate.cpp +++ b/test/unittest/transport/can/test_can_delegate.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -52,7 +53,10 @@ using testing::VariantWith; class TestCanDelegate : public testing::Test { protected: - class TransportDelegateImpl final : public detail::TransportDelegate + using CanardMemory = can::detail::CanardMemory; + using TransportDelegate = can::detail::TransportDelegate; + + class TransportDelegateImpl final : public TransportDelegate { public: explicit TransportDelegateImpl(cetl::pmr::memory_resource& memory) @@ -68,7 +72,13 @@ class TestCanDelegate : public testing::Test const CanardTransferMetadata& metadata, const PayloadFragments payload_fragments), (override)); - MOCK_METHOD(void, onSessionEvent, (const SessionEvent::Variant& event_var), (override)); + + MOCK_METHOD(void, onSessionEvent, (const SessionEvent::Variant& event_var), (noexcept, override)); // NOLINT + + MOCK_METHOD(can::detail::IRxSessionDelegate*, + tryFindRxSessionDelegateFor, + (const ResponseRxParams& params), + (override)); }; void SetUp() override @@ -93,23 +103,25 @@ class TestCanDelegate : public testing::Test TEST_F(TestCanDelegate, CanardMemory_copy) { - using CanardMemory = detail::TransportDelegate::CanardMemory; - TransportDelegateImpl delegate{mr_}; auto& canard_instance = delegate.canardInstance(); - const std::size_t payload_size = 4; - const std::size_t allocated_size = payload_size + 1; - auto* const payload = static_cast( - canard_instance.memory.allocate(static_cast(&delegate), allocated_size)); + constexpr std::size_t payload_size = 4; + constexpr std::size_t allocated_size = payload_size + 1; + auto* const payload = + static_cast(canard_instance.memory.allocate(static_cast(&delegate), allocated_size)); fillIotaBytes({payload, allocated_size}, b('0')); - const CanardMemory canard_memory{delegate, allocated_size, payload, payload_size}; + CanardMutablePayload canard_payload{payload_size, payload, allocated_size}; + const CanardMemory canard_memory{mr_, canard_payload}; EXPECT_THAT(canard_memory.size(), payload_size); + EXPECT_THAT(canard_payload.size, 0); + EXPECT_THAT(canard_payload.data, nullptr); + EXPECT_THAT(canard_payload.allocated_size, 0); // Ask exactly as payload { - const std::size_t ask_size = payload_size; + constexpr std::size_t ask_size = payload_size; std::array buffer{}; EXPECT_THAT(canard_memory.copy(0, buffer.data(), ask_size), ask_size); @@ -118,7 +130,7 @@ TEST_F(TestCanDelegate, CanardMemory_copy) // Ask more than payload { - const std::size_t ask_size = payload_size + 2; + constexpr std::size_t ask_size = payload_size + 2; std::array buffer{}; EXPECT_THAT(canard_memory.copy(0, buffer.data(), ask_size), payload_size); @@ -127,7 +139,7 @@ TEST_F(TestCanDelegate, CanardMemory_copy) // Ask less than payload (with different offsets) { - const std::size_t ask_size = payload_size - 2; + constexpr std::size_t ask_size = payload_size - 2; std::array buffer{}; EXPECT_THAT(canard_memory.copy(0, buffer.data(), ask_size), ask_size); @@ -153,18 +165,20 @@ TEST_F(TestCanDelegate, CanardMemory_copy) TEST_F(TestCanDelegate, CanardMemory_copy_on_moved) { - using CanardMemory = can::detail::TransportDelegate::CanardMemory; - TransportDelegateImpl delegate{mr_}; auto& canard_instance = delegate.canardInstance(); constexpr std::size_t payload_size = 4; - auto* const payload = static_cast( - canard_instance.memory.allocate(static_cast(&delegate), payload_size)); + auto* const payload = + static_cast(canard_instance.memory.allocate(static_cast(&delegate), payload_size)); fillIotaBytes({payload, payload_size}, b('0')); - CanardMemory old_canard_memory{delegate, payload_size, payload, payload_size}; + CanardMutablePayload canard_payload{payload_size, payload, payload_size}; + CanardMemory old_canard_memory{mr_, canard_payload}; EXPECT_THAT(old_canard_memory.size(), payload_size); + EXPECT_THAT(canard_payload.size, 0); + EXPECT_THAT(canard_payload.data, nullptr); + EXPECT_THAT(canard_payload.allocated_size, 0); const CanardMemory new_canard_memory{std::move(old_canard_memory)}; // NOLINTNEXTLINE(clang-analyzer-cplusplus.Move,bugprone-use-after-move,hicpp-invalid-access-moved) @@ -179,7 +193,7 @@ TEST_F(TestCanDelegate, CanardMemory_copy_on_moved) EXPECT_THAT(buffer, Each(b('\0'))); } - // Try new one + // Try a new one { std::array buffer{}; EXPECT_THAT(new_canard_memory.copy(0, buffer.data(), buffer.size()), payload_size); @@ -189,15 +203,15 @@ TEST_F(TestCanDelegate, CanardMemory_copy_on_moved) TEST_F(TestCanDelegate, optAnyFailureFromCanard) { - EXPECT_THAT(can::detail::TransportDelegate::optAnyFailureFromCanard(-CANARD_ERROR_OUT_OF_MEMORY), + EXPECT_THAT(TransportDelegate::optAnyFailureFromCanard(-CANARD_ERROR_OUT_OF_MEMORY), Optional(VariantWith(_))); - EXPECT_THAT(can::detail::TransportDelegate::optAnyFailureFromCanard(-CANARD_ERROR_INVALID_ARGUMENT), + EXPECT_THAT(TransportDelegate::optAnyFailureFromCanard(-CANARD_ERROR_INVALID_ARGUMENT), Optional(VariantWith(_))); - EXPECT_THAT(can::detail::TransportDelegate::optAnyFailureFromCanard(0), Eq(cetl::nullopt)); - EXPECT_THAT(can::detail::TransportDelegate::optAnyFailureFromCanard(1), Eq(cetl::nullopt)); - EXPECT_THAT(can::detail::TransportDelegate::optAnyFailureFromCanard(-1), Eq(cetl::nullopt)); + EXPECT_THAT(TransportDelegate::optAnyFailureFromCanard(0), Eq(cetl::nullopt)); + EXPECT_THAT(TransportDelegate::optAnyFailureFromCanard(1), Eq(cetl::nullopt)); + EXPECT_THAT(TransportDelegate::optAnyFailureFromCanard(-1), Eq(cetl::nullopt)); } TEST_F(TestCanDelegate, canardMemoryAllocate_no_memory) @@ -205,13 +219,13 @@ TEST_F(TestCanDelegate, canardMemoryAllocate_no_memory) StrictMock mr_mock; TransportDelegateImpl delegate{mr_mock}; - auto& canard_instance = delegate.canardInstance(); + const auto& canard_instance = delegate.canardInstance(); // Emulate that there is no memory at all. EXPECT_CALL(mr_mock, do_allocate(_, _)) // .WillOnce(Return(nullptr)); - EXPECT_THAT(canard_instance.memory.allocate(static_cast(&delegate), 1), IsNull()); + EXPECT_THAT(canard_instance.memory.allocate(static_cast(&delegate), 1), IsNull()); } TEST_F(TestCanDelegate, CanardConcreteTree_visitCounting) @@ -248,7 +262,7 @@ TEST_F(TestCanDelegate, CanardConcreteTree_visitCounting) right_rl.up = &right_r; right_r.lr[0] = &right_rl; - using MyTree = can::detail::TransportDelegate::CanardConcreteTree; + using MyTree = TransportDelegate::CanardConcreteTree; { std::vector names; auto count = MyTree::visitCounting(&root, [&names](const MyNode& node) { names.push_back(node.name); }); diff --git a/test/unittest/transport/can/test_can_svc_rx_sessions.cpp b/test/unittest/transport/can/test_can_svc_rx_sessions.cpp index 8e2db6fc4..511e36cf5 100644 --- a/test/unittest/transport/can/test_can_svc_rx_sessions.cpp +++ b/test/unittest/transport/can/test_can_svc_rx_sessions.cpp @@ -115,8 +115,8 @@ class TestCanSvcRxSessions : public testing::Test return transport; } - IMedia::PopResult::Metadata makeFragmentFromCanDumpLine(const std::string& can_dump_line, - cetl::span payload) + IMedia::PopResult::Metadata makeFragmentFromCanDumpLine(const std::string& can_dump_line, + const cetl::span payload) const { const auto pound = can_dump_line.find('#'); @@ -197,18 +197,33 @@ TEST_F(TestCanSvcRxSessions, make_response_no_memory) EXPECT_CALL(mr_mock, do_allocate(sizeof(can::detail::SvcResponseRxSession), _)) // .WillOnce(Return(nullptr)); - auto transport = makeTransport(mr_mock, 0x13); + const auto transport = makeTransport(mr_mock, 0x13); - auto maybe_session = transport->makeResponseRxSession({64, 0x23, 0x45}); + const auto maybe_session = transport->makeResponseRxSession({64, 0x23, 0x45}); + EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); +} + +TEST_F(TestCanSvcRxSessions, make_request_no_memory) +{ + StrictMock mr_mock; + mr_mock.redirectExpectedCallsTo(mr_); + + // Emulate that there is no memory available for the message session. + EXPECT_CALL(mr_mock, do_allocate(sizeof(can::detail::SvcRequestRxSession), _)) // + .WillOnce(Return(nullptr)); + + const auto transport = makeTransport(mr_mock, 0x31); + + const auto maybe_session = transport->makeRequestRxSession({64, 123}); EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); } TEST_F(TestCanSvcRxSessions, make_request_fails_due_to_argument_error) { - auto transport = makeTransport(mr_, 0x31); + const auto transport = makeTransport(mr_, 0x31); // Try invalid subject id - auto maybe_session = transport->makeRequestRxSession({64, CANARD_SERVICE_ID_MAX + 1}); + const auto maybe_session = transport->makeRequestRxSession({64, CANARD_SERVICE_ID_MAX + 1}); EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); } @@ -228,7 +243,7 @@ TEST_F(TestCanSvcRxSessions, receive_request) auto session = cetl::get>(std::move(maybe_session)); EXPECT_CALL(media_mock_, setFilters(SizeIs(1))) // - .WillOnce([&](Filters filters) { + .WillOnce([&](const Filters filters) { EXPECT_THAT(filters, Contains(FilterEq({0b1'0'0'101111011'0110001'0000000, 0b1'0'1'111111111'1111111'0000000}))); return cetl::nullopt; @@ -301,7 +316,7 @@ TEST_F(TestCanSvcRxSessions, receive_request) TEST_F(TestCanSvcRxSessions, receive_request_via_callback) { - auto transport = makeTransport(mr_, 0x31); + const auto transport = makeTransport(mr_, 0x31); EXPECT_CALL(media_mock_, registerPopCallback(_)) // .WillOnce(Invoke([&](auto function) { // @@ -313,7 +328,7 @@ TEST_F(TestCanSvcRxSessions, receive_request_via_callback) auto session = cetl::get>(std::move(maybe_session)); EXPECT_CALL(media_mock_, setFilters(SizeIs(1))) // - .WillOnce([&](Filters filters) { + .WillOnce([&](const Filters filters) { EXPECT_THAT(filters, Contains(FilterEq({0b1'0'0'101111011'0110001'0000000, 0b1'0'1'111111111'1111111'0000000}))); return cetl::nullopt; @@ -367,25 +382,31 @@ TEST_F(TestCanSvcRxSessions, receive_response) return scheduler_.registerNamedCallback("rx", std::move(function)); })); - constexpr std::size_t extent_bytes = 8; - auto maybe_session = transport->makeResponseRxSession({extent_bytes, 0x17B, 0x31}); - ASSERT_THAT(maybe_session, VariantWith>(NotNull())); - auto session = cetl::get>(std::move(maybe_session)); + constexpr std::size_t extent_bytes = 8; + auto maybe_session_n31 = transport->makeResponseRxSession({extent_bytes, 0x17B, 0x31}); + ASSERT_THAT(maybe_session_n31, VariantWith>(NotNull())); + auto session_n31 = cetl::get>(std::move(maybe_session_n31)); EXPECT_CALL(media_mock_, setFilters(SizeIs(1))) // - .WillOnce([&](Filters filters) { + .WillOnce([&](const Filters filters) { EXPECT_THAT(filters, Contains(FilterEq({0b1'0'0'101111011'0010011'0000000, 0b1'0'1'111111111'1111111'0000000}))); return cetl::nullopt; }); - const auto params = session->getParams(); + const auto params = session_n31->getParams(); EXPECT_THAT(params.extent_bytes, extent_bytes); EXPECT_THAT(params.service_id, 0x17B); EXPECT_THAT(params.server_node_id, 0x31); constexpr auto timeout = 200ms; - session->setTransferIdTimeout(timeout); + session_n31->setTransferIdTimeout(timeout); + + // Create another session with the same port ID but different server node ID (0x32). + // + auto maybe_session_n32 = transport->makeResponseRxSession({extent_bytes, 0x17B, 0x32}); + ASSERT_THAT(maybe_session_n32, VariantWith>(NotNull())); + auto session_n32 = cetl::get>(std::move(maybe_session_n32)); TimePoint rx_timestamp; @@ -407,8 +428,9 @@ TEST_F(TestCanSvcRxSessions, receive_response) scheduler_.scheduleAt(rx_timestamp + 1ms, [&](const auto&) { // - const auto maybe_rx_transfer = session->receive(); + const auto maybe_rx_transfer = session_n31->receive(); ASSERT_THAT(maybe_rx_transfer, Optional(_)); + EXPECT_THAT(session_n32->receive(), Eq(cetl::nullopt)); // Different server node ID. // NOLINTNEXTLINE(bugprone-unchecked-optional-access) const auto& rx_transfer = maybe_rx_transfer.value(); @@ -438,17 +460,43 @@ TEST_F(TestCanSvcRxSessions, receive_response) scheduler_.scheduleAt(rx_timestamp + 1ms, [&](const auto&) { // - const auto maybe_rx_transfer = session->receive(); + const auto maybe_rx_transfer = session_n31->receive(); EXPECT_THAT(maybe_rx_transfer, Eq(cetl::nullopt)); }); }); + scheduler_.scheduleAt(3s, [&](const auto&) { + // + SCOPED_TRACE("3-rd iteration: unsolicited (node 0x33) response @ 3s"); + + rx_timestamp = now() + 10ms; + EXPECT_CALL(media_mock_, pop(_)) // + .WillOnce([&](auto p) { + EXPECT_THAT(now(), rx_timestamp); + EXPECT_THAT(p.size(), CANARD_MTU_MAX); + p[0] = b(0b111'11101); + return IMedia::PopResult::Metadata{rx_timestamp, 0b011'1'0'0'101111011'0010011'0110011, 1}; + }); + scheduler_.scheduleNamedCallback("rx", rx_timestamp); + + scheduler_.scheduleAt(rx_timestamp + 1ms, [&](const auto&) { + // + EXPECT_THAT(session_n31->receive(), Eq(cetl::nullopt)); + EXPECT_THAT(session_n32->receive(), Eq(cetl::nullopt)); + }); + }); + scheduler_.scheduleAt(9s, [&](const auto&) { + // + session_n31.reset(); + session_n32.reset(); + transport.reset(); + }); scheduler_.spinFor(10s); } // NOLINTNEXTLINE(readability-function-cognitive-complexity) TEST_F(TestCanSvcRxSessions, receive_two_frames) { - auto transport = makeTransport(mr_, 0x31); + const auto transport = makeTransport(mr_, 0x31); EXPECT_CALL(media_mock_, registerPopCallback(_)) // .WillOnce(Invoke([&](auto function) { // @@ -458,16 +506,16 @@ TEST_F(TestCanSvcRxSessions, receive_two_frames) constexpr std::size_t extent_bytes = 8; auto maybe_session = transport->makeRequestRxSession({extent_bytes, 0x17B}); ASSERT_THAT(maybe_session, VariantWith>(NotNull())); - auto session = cetl::get>(std::move(maybe_session)); + const auto session = cetl::get>(std::move(maybe_session)); EXPECT_CALL(media_mock_, setFilters(SizeIs(1))) // - .WillOnce([&](Filters filters) { + .WillOnce([&](const Filters filters) { EXPECT_THAT(filters, Contains(FilterEq({0b1'0'0'101111011'0110001'0000000, 0b1'0'1'111111111'1111111'0000000}))); return cetl::nullopt; }); - auto first_rx_timestamp = TimePoint{1s + 10ms}; + constexpr auto first_rx_timestamp = TimePoint{1s + 10ms}; scheduler_.scheduleAt(1s, [&](const auto&) { // @@ -532,7 +580,7 @@ TEST_F(TestCanSvcRxSessions, receive_two_frames) TEST_F(TestCanSvcRxSessions, unsubscribe) { - auto transport = makeTransport(mr_, 0x31); + const auto transport = makeTransport(mr_, 0x31); EXPECT_CALL(media_mock_, registerPopCallback(_)) // .WillOnce(Invoke([&](auto function) { // @@ -545,7 +593,7 @@ TEST_F(TestCanSvcRxSessions, unsubscribe) auto session = cetl::get>(std::move(maybe_session)); EXPECT_CALL(media_mock_, setFilters(SizeIs(1))) // - .WillOnce([&](Filters filters) { + .WillOnce([&](const Filters filters) { EXPECT_THAT(filters, Contains(FilterEq({0x025ED880, 0x02FFFF80}))); return cetl::nullopt; }); @@ -553,7 +601,7 @@ TEST_F(TestCanSvcRxSessions, unsubscribe) scheduler_.scheduleAt(1s, [&](const auto&) { // EXPECT_CALL(media_mock_, setFilters(IsEmpty())) // - .WillOnce([&](Filters) { // + .WillOnce([&](const Filters) { // return cetl::nullopt; }); session.reset(); @@ -596,7 +644,7 @@ TEST_F(TestCanSvcRxSessions, receive_multiple_tids_frames) // // response 1001, tid=0, accepted std::make_tuple(std::ref(media_mock_), 350755us, "slcan0", "1224D52F#E9030000000000A0"), // ☑️create!,iface←0 - std::make_tuple(std::ref(media_mock_), 350764us, "slcan0", "1224D52F#00C08C40"), // ⚡️0️⃣tid←1 + std::make_tuple(std::ref(media_mock_), 350764us, "slcan0", "1224D52F#00C08C40"), // ⚡️0️⃣tid←1 // // CAN2 response 1001, tid=0, dropped as duplicate std::make_tuple(std::ref(media_mock2), 350783us, "slcan2", "1224D52F#E9030000000000A0"), // ❌tid≠1 @@ -604,7 +652,7 @@ TEST_F(TestCanSvcRxSessions, receive_multiple_tids_frames) // // CAN2 response 2001, tid=1, accepted by resync to interface #2 std::make_tuple(std::ref(media_mock2), 351336us, "slcan2", "1224D52F#D1070000000000A1"), // ☑️tid=1,iface←2 - std::make_tuple(std::ref(media_mock2), 351338us, "slcan2", "1224D52F#00594C41"), // ⚡️1️⃣tid←2 + std::make_tuple(std::ref(media_mock2), 351338us, "slcan2", "1224D52F#00594C41"), // ⚡️1️⃣tid←2 // // CAN2 partial response 3001, accepted std::make_tuple(std::ref(media_mock2), 351340us, "slcan2", "1224D52F#B90B0000000000A2"), // ☑️ @@ -615,14 +663,14 @@ TEST_F(TestCanSvcRxSessions, receive_multiple_tids_frames) // // CAN0 response 3001, tid=2, dropped as wrong interface (expected #2) std::make_tuple(std::ref(media_mock_), 351478us, "slcan0", "1224D52F#B90B0000000000A2"), // ❌iface≠2 & !idle - std::make_tuple(std::ref(media_mock_), 351479us, "slcan0", "1224D52F#00984542"), // ❌iface≠2 & !idle + std::make_tuple(std::ref(media_mock_), 351479us, "slcan0", "1224D52F#00984542"), // ❌iface≠2 & !idle // // CAN2 final fragment response 3001, tid=2, accepted std::make_tuple(std::ref(media_mock2), 351697us, "slcan2", "1224D52F#00984542"), // // ⚡️2️⃣tid←3 // // CAN2 response 4001, tid=3, accepted std::make_tuple(std::ref(media_mock2), 351700us, "slcan2", "1224D52F#A10F0000000000A3"), // ☑️ - std::make_tuple(std::ref(media_mock2), 351702us, "slcan2", "1224D52F#007AED43"), // ⚡️3️⃣tid←4 + std::make_tuple(std::ref(media_mock2), 351702us, "slcan2", "1224D52F#007AED43"), // ⚡️3️⃣tid←4 // // CAN0 response 4001, tid=3, dropped as duplicate std::make_tuple(std::ref(media_mock_), 351730us, "slcan0", "1224D52F#A10F0000000000A3"), // ❌tid≠4 @@ -630,7 +678,7 @@ TEST_F(TestCanSvcRxSessions, receive_multiple_tids_frames) // // CAN2 response 5001, tid=4, accepted std::make_tuple(std::ref(media_mock2), 352747us, "slcan2", "1224D52F#89130000000000A4"), // ☑️ - std::make_tuple(std::ref(media_mock2), 352777us, "slcan2", "1224D52F#007A4F44"), // ⚡️4️⃣tid←5 + std::make_tuple(std::ref(media_mock2), 352777us, "slcan2", "1224D52F#007A4F44"), // ⚡️4️⃣tid←5 // // CAN0 response 5001, tid=4, dropped as duplicate std::make_tuple(std::ref(media_mock_), 352800us, "slcan0", "1224D52F#89130000000000A4"), // ❌tid≠5 diff --git a/test/unittest/transport/can/test_can_transport.cpp b/test/unittest/transport/can/test_can_transport.cpp index 6e3e04224..407f14e30 100644 --- a/test/unittest/transport/can/test_can_transport.cpp +++ b/test/unittest/transport/can/test_can_transport.cpp @@ -468,6 +468,10 @@ TEST_F(TestCanTransport, makeResponseRxSession_invalid_resubscription) EXPECT_CALL(media_mock_, setFilters(IsEmpty())) // .WillOnce([&](Filters) { return cetl::nullopt; }); + + // Different remote node id 0x32! + auto maybe_rx_session3 = transport->makeResponseRxSession({0, test_subject_id, 0x32}); + ASSERT_THAT(maybe_rx_session3, VariantWith>(NotNull())); }); scheduler_.spinFor(10s); } diff --git a/test/unittest/transport/test_session_tree.cpp b/test/unittest/transport/test_session_tree.cpp new file mode 100644 index 000000000..b284dee09 --- /dev/null +++ b/test/unittest/transport/test_session_tree.cpp @@ -0,0 +1,206 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include "memory_resource_mock.hpp" +#include "tracking_memory_resource.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace +{ + +using libcyphal::MemoryError; +using namespace libcyphal::transport; // NOLINT This our main concern here in the unit tests. + +using testing::_; +using testing::StrEq; +using testing::IsNull; +using testing::Return; +using testing::IsEmpty; +using testing::NotNull; +using testing::Pointer; +using testing::StrictMock; +using testing::VariantWith; + +// NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers) + +class TestSessionTree : public testing::Test +{ +protected: + class MyNode final : public libcyphal::common::cavl::Node + { + public: + using Params = std::int32_t; + using ReferenceWrapper = std::reference_wrapper; + + explicit MyNode(const Params& params, const std::tuple& args_tuple) + : params_{params} + , extra_arg_{std::get<0>(args_tuple)} + { + } + + MyNode(const MyNode&) = delete; + MyNode(MyNode&&) noexcept = delete; + MyNode& operator=(const MyNode&) = delete; + MyNode& operator=(MyNode&&) noexcept = delete; + + ~MyNode() + { + if (notifier_) + { + notifier_("~"); + } + } + + CETL_NODISCARD std::int32_t compareByParams(const Params& params) const + { + return params_ - params; + } + + const char* getExtraArg() const noexcept + { + return extra_arg_; + } + + void setNotifier(std::function notifier) + { + notifier_ = std::move(notifier); + } + + private: + const Params params_; + const char* const extra_arg_; + std::function notifier_; + + }; // MyNode + + void SetUp() override + { + cetl::pmr::set_default_resource(&mr_); + } + + void TearDown() override + { + EXPECT_THAT(mr_.allocations, IsEmpty()); + EXPECT_THAT(mr_.total_allocated_bytes, mr_.total_deallocated_bytes); + } + + // MARK: Data members: + + // NOLINTBEGIN + TrackingMemoryResource mr_; + // NOLINTEND + +}; // TestSessionTree + +// MARK: - Tests: + +TEST_F(TestSessionTree, constructor_destructor_empty_tree) +{ + const detail::SessionTree tree{mr_}; + EXPECT_THAT(tree.isEmpty(), true); +} + +TEST_F(TestSessionTree, ensureNodeFor_should_be_new) +{ + detail::SessionTree tree{mr_}; + + EXPECT_THAT(tree.ensureNodeFor(0, "0a"), VariantWith(_)); + EXPECT_THAT(tree.isEmpty(), false); + + EXPECT_THAT(tree.ensureNodeFor(1, "1a"), VariantWith(_)); + EXPECT_THAT(tree.ensureNodeFor(2, "2a"), VariantWith(_)); + EXPECT_THAT(tree.ensureNodeFor(0, "0b"), VariantWith(VariantWith(_))); + EXPECT_THAT(tree.ensureNodeFor(1, "1b"), VariantWith(VariantWith(_))); + EXPECT_THAT(tree.ensureNodeFor(2, "2b"), VariantWith(VariantWith(_))); + + EXPECT_THAT(tree.tryFindNodeFor(0)->getExtraArg(), StrEq("0a")); + EXPECT_THAT(tree.tryFindNodeFor(1)->getExtraArg(), StrEq("1a")); + EXPECT_THAT(tree.tryFindNodeFor(2)->getExtraArg(), StrEq("2a")); + EXPECT_THAT(tree.tryFindNodeFor(3), IsNull()); +} + +TEST_F(TestSessionTree, ensureNodeFor_existing_is_fine) +{ + detail::SessionTree tree{mr_}; + + auto maybe_node_0a = tree.ensureNodeFor(0, "0a"); + ASSERT_THAT(maybe_node_0a, VariantWith(_)); + const auto node_0a = cetl::get(maybe_node_0a); + + EXPECT_THAT(tree.isEmpty(), false); + + auto maybe_node_1a = tree.ensureNodeFor(1, "1a"); + EXPECT_THAT(maybe_node_1a, VariantWith(_)); + const auto node_1a = cetl::get(maybe_node_1a); + + EXPECT_THAT(tree.ensureNodeFor(2, "2a"), VariantWith(_)); + + auto maybe_node_0b = tree.ensureNodeFor(0, "0b"); + EXPECT_THAT(maybe_node_0b, VariantWith(Pointer(&node_0a.get()))); + EXPECT_THAT(tree.tryFindNodeFor(0)->getExtraArg(), StrEq("0a")); + + auto maybe_node_1b = tree.ensureNodeFor(1, "1b"); + EXPECT_THAT(maybe_node_1b, VariantWith(Pointer(&node_1a.get()))); + EXPECT_THAT(tree.tryFindNodeFor(1)->getExtraArg(), StrEq("1a")); + + EXPECT_THAT(tree.ensureNodeFor(2, "2b"), VariantWith(_)); + EXPECT_THAT(tree.tryFindNodeFor(2)->getExtraArg(), StrEq("2a")); +} + +TEST_F(TestSessionTree, ensureNodeFor_no_memory) +{ + StrictMock mr_mock; + mr_mock.redirectExpectedCallsTo(mr_); + + detail::SessionTree tree{mr_mock}; + + // Emulate that there is no memory available for the message session. + EXPECT_CALL(mr_mock, do_allocate(sizeof(MyNode), _)) // + .WillOnce(Return(nullptr)); + + EXPECT_THAT(tree.ensureNodeFor(0, "0a"), VariantWith(VariantWith(_))); + EXPECT_THAT(tree.tryFindNodeFor(0), IsNull()); +} + +TEST_F(TestSessionTree, removeNodeFor) +{ + detail::SessionTree tree{mr_}; + + tree.removeNodeFor(13); + + auto maybe_node = tree.ensureNodeFor(42, "42a"); + ASSERT_THAT(maybe_node, VariantWith(_)); + EXPECT_THAT(tree.tryFindNodeFor(42), NotNull()); + EXPECT_THAT(tree.isEmpty(), false); + + const auto node_ref = cetl::get(maybe_node); + + std::string side_effects; + node_ref.get().setNotifier([&](const std::string& msg) { side_effects += msg; }); + + tree.removeNodeFor(42); + EXPECT_THAT(side_effects, "~"); + + EXPECT_THAT(tree.isEmpty(), true); + EXPECT_THAT(tree.tryFindNodeFor(42), IsNull()); +} + +// NOLINTEND(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers) + +} // namespace diff --git a/test/unittest/transport/udp/test_session_tree.cpp b/test/unittest/transport/udp/test_session_tree.cpp deleted file mode 100644 index 43d1ef68d..000000000 --- a/test/unittest/transport/udp/test_session_tree.cpp +++ /dev/null @@ -1,145 +0,0 @@ -/// @copyright -/// Copyright (C) OpenCyphal Development Team -/// Copyright Amazon.com Inc. or its affiliates. -/// SPDX-License-Identifier: MIT - -#include "memory_resource_mock.hpp" -#include "tracking_memory_resource.hpp" - -#include -#include -#include -#include - -#include -#include - -#include -#include -#include - -namespace -{ - -using libcyphal::MemoryError; -using namespace libcyphal::transport; // NOLINT This our main concern here in the unit tests. -using namespace libcyphal::transport::udp; // NOLINT This our main concern here in the unit tests. - -using testing::_; -using testing::Return; -using testing::IsEmpty; -using testing::StrictMock; -using testing::VariantWith; - -// NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers) - -class TestSessionTree : public testing::Test -{ -protected: - class MyNode final : public detail::RxSessionTreeNode::Base - { - public: - using Base::Base; - - MyNode(const MyNode&) = delete; - MyNode(MyNode&&) noexcept = delete; - MyNode& operator=(const MyNode&) = delete; - MyNode& operator=(MyNode&&) noexcept = delete; - - ~MyNode() - { - if (notifier_) - { - notifier_("~"); - } - } - - void setNotifier(std::function notifier) - { - notifier_ = std::move(notifier); - } - - private: - std::function notifier_; - }; - - void SetUp() override - { - cetl::pmr::set_default_resource(&mr_); - } - - void TearDown() override - { - EXPECT_THAT(mr_.allocations, IsEmpty()); - EXPECT_THAT(mr_.total_allocated_bytes, mr_.total_deallocated_bytes); - } - - // MARK: Data members: - - // NOLINTBEGIN - TrackingMemoryResource mr_; - // NOLINTEND - -}; // TestSessionTree - -// MARK: - Tests: - -TEST_F(TestSessionTree, constructor_destructor_empty_tree) -{ - const detail::SessionTree tree{mr_}; - EXPECT_THAT(tree.isEmpty(), true); -} - -TEST_F(TestSessionTree, ensureNewNodeFor) -{ - detail::SessionTree tree{mr_}; - - EXPECT_THAT(tree.ensureNewNodeFor(0), VariantWith(_)); - EXPECT_THAT(tree.isEmpty(), false); - - EXPECT_THAT(tree.ensureNewNodeFor(1), VariantWith(_)); - EXPECT_THAT(tree.ensureNewNodeFor(2), VariantWith(_)); - - EXPECT_THAT(tree.ensureNewNodeFor(0), VariantWith(VariantWith(_))); - EXPECT_THAT(tree.ensureNewNodeFor(1), VariantWith(VariantWith(_))); - EXPECT_THAT(tree.ensureNewNodeFor(2), VariantWith(VariantWith(_))); -} - -TEST_F(TestSessionTree, ensureNewNodeFor_no_memory) -{ - StrictMock mr_mock; - mr_mock.redirectExpectedCallsTo(mr_); - - detail::SessionTree tree{mr_mock}; - - // Emulate that there is no memory available for the message session. - EXPECT_CALL(mr_mock, do_allocate(sizeof(MyNode), _)) // - .WillOnce(Return(nullptr)); - - EXPECT_THAT(tree.ensureNewNodeFor(0), VariantWith(VariantWith(_))); -} - -TEST_F(TestSessionTree, removeNodeFor) -{ - detail::SessionTree tree{mr_}; - - tree.removeNodeFor(13); - - auto maybe_node = tree.ensureNewNodeFor(42); - ASSERT_THAT(maybe_node, VariantWith(_)); - EXPECT_THAT(tree.isEmpty(), false); - - auto node_ref = cetl::get(maybe_node); - - std::string side_effects; - node_ref.get().setNotifier([&](const std::string& msg) { side_effects += msg; }); - - tree.removeNodeFor(42); - EXPECT_THAT(side_effects, "~"); - - EXPECT_THAT(tree.isEmpty(), true); -} - -// NOLINTEND(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers) - -} // namespace diff --git a/test/unittest/transport/udp/test_udp_delegate.cpp b/test/unittest/transport/udp/test_udp_delegate.cpp index 41fe99c0d..989440d16 100644 --- a/test/unittest/transport/udp/test_udp_delegate.cpp +++ b/test/unittest/transport/udp/test_udp_delegate.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -53,10 +54,13 @@ using testing::VariantWith; class TestUdpDelegate : public testing::Test { protected: - class TransportDelegateImpl final : public detail::TransportDelegate + using UdpardMemory = udp::detail::UdpardMemory; + using MemoryResources = udp::detail::MemoryResources; + using TransportDelegate = udp::detail::TransportDelegate; + + class TransportDelegateImpl final : public TransportDelegate { public: - using TransportDelegate::MemoryResources; using TransportDelegate::memoryResources; using TransportDelegate::makeUdpardMemoryDeleter; using TransportDelegate::makeUdpardMemoryResource; @@ -79,7 +83,12 @@ class TestUdpDelegate : public testing::Test const PayloadFragments payload_fragments), (override)); - MOCK_METHOD(void, onSessionEvent, (const SessionEvent::Variant& event_var), (override)); + MOCK_METHOD(void, onSessionEvent, (const SessionEvent::Variant& event_var), (noexcept, override)); // NOLINT + + MOCK_METHOD(udp::detail::IRxSessionDelegate*, + tryFindRxSessionDelegateFor, + (const ResponseRxParams& params), + (override)); }; // TransportDelegateImpl @@ -108,7 +117,7 @@ class TestUdpDelegate : public testing::Test UdpardFragment* allocateNewUdpardFragment(const std::size_t size) { // This structure mimics internal Udpard `RxFragment` layout. - // We need this to know its size, so that test tear down can check if all memory was deallocated. + // We need this to know its size, so that test teardown can check if all memory was deallocated. // @see `EXPECT_THAT(fragment_mr_.total_allocated_bytes, fragment_mr_.total_deallocated_bytes);` // struct RxFragment @@ -141,9 +150,7 @@ class TestUdpDelegate : public testing::Test TEST_F(TestUdpDelegate, UdpardMemory_copy) { - using UdpardMemory = udp::detail::TransportDelegate::UdpardMemory; - - TransportDelegateImpl delegate{general_mr_, &fragment_mr_, &payload_mr_}; + const TransportDelegateImpl delegate{general_mr_, &fragment_mr_, &payload_mr_}; auto* const payload = allocateNewUdpardPayload(4); fillIotaBytes({payload, 4}, b('0')); @@ -153,12 +160,18 @@ TEST_F(TestUdpDelegate, UdpardMemory_copy) rx_transfer.payload_size = payload_size; rx_transfer.payload = UdpardFragment{nullptr, {payload_size, payload}, {payload_size, payload}}; - const UdpardMemory udpard_memory{delegate, rx_transfer}; + const UdpardMemory udpard_memory{delegate.memoryResources(), rx_transfer}; EXPECT_THAT(udpard_memory.size(), payload_size); + EXPECT_THAT(rx_transfer.payload_size, 0); + EXPECT_THAT(rx_transfer.payload.next, nullptr); + EXPECT_THAT(rx_transfer.payload.view.size, 0); + EXPECT_THAT(rx_transfer.payload.view.data, nullptr); + EXPECT_THAT(rx_transfer.payload.origin.size, 0); + EXPECT_THAT(rx_transfer.payload.origin.data, nullptr); // Ask exactly as payload { - const std::size_t ask_size = payload_size; + constexpr std::size_t ask_size = payload_size; std::array buffer{}; EXPECT_THAT(udpard_memory.copy(0, buffer.data(), ask_size), ask_size); @@ -167,7 +180,7 @@ TEST_F(TestUdpDelegate, UdpardMemory_copy) // Ask more than payload { - const std::size_t ask_size = payload_size + 2; + constexpr std::size_t ask_size = payload_size + 2; std::array buffer{}; EXPECT_THAT(udpard_memory.copy(0, buffer.data(), ask_size), payload_size); @@ -176,7 +189,7 @@ TEST_F(TestUdpDelegate, UdpardMemory_copy) // Ask less than payload (with different offsets) { - const std::size_t ask_size = payload_size - 2; + constexpr std::size_t ask_size = payload_size - 2; std::array buffer{}; EXPECT_THAT(udpard_memory.copy(0, buffer.data(), ask_size), ask_size); @@ -202,9 +215,7 @@ TEST_F(TestUdpDelegate, UdpardMemory_copy) TEST_F(TestUdpDelegate, UdpardMemory_copy_on_moved) { - using UdpardMemory = udp::detail::TransportDelegate::UdpardMemory; - - TransportDelegateImpl delegate{general_mr_, &fragment_mr_, &payload_mr_}; + const TransportDelegateImpl delegate{general_mr_, &fragment_mr_, &payload_mr_}; constexpr std::size_t payload_size = 4; auto* const payload = allocateNewUdpardPayload(payload_size); @@ -214,7 +225,7 @@ TEST_F(TestUdpDelegate, UdpardMemory_copy_on_moved) rx_transfer.payload_size = payload_size; rx_transfer.payload = UdpardFragment{nullptr, {payload_size, payload}, {payload_size, payload}}; - UdpardMemory old_udpard_memory{delegate, rx_transfer}; + UdpardMemory old_udpard_memory{delegate.memoryResources(), rx_transfer}; EXPECT_THAT(old_udpard_memory.size(), payload_size); const UdpardMemory new_udpard_memory{std::move(old_udpard_memory)}; @@ -230,7 +241,7 @@ TEST_F(TestUdpDelegate, UdpardMemory_copy_on_moved) EXPECT_THAT(buffer, Each(b('\0'))); } - // Try new one + // Try a new one { std::array buffer{}; EXPECT_THAT(new_udpard_memory.copy(0, buffer.data(), buffer.size()), payload_size); @@ -240,9 +251,7 @@ TEST_F(TestUdpDelegate, UdpardMemory_copy_on_moved) TEST_F(TestUdpDelegate, UdpardMemory_copy_multi_fragmented) { - using UdpardMemory = udp::detail::TransportDelegate::UdpardMemory; - - TransportDelegateImpl delegate{general_mr_, &fragment_mr_, &payload_mr_}; + const TransportDelegateImpl delegate{general_mr_, &fragment_mr_, &payload_mr_}; auto* const payload0 = allocateNewUdpardPayload(7); @@ -257,18 +266,18 @@ TEST_F(TestUdpDelegate, UdpardMemory_copy_multi_fragmented) fillIotaBytes({payload1, 8}, b('A')); fillIotaBytes({payload2, 9}, b('a')); - const std::size_t payload_size = 3 + 4 + 2; + constexpr std::size_t payload_size = 3 + 4 + 2; rx_transfer.payload_size = payload_size; rx_transfer.payload.view = {3, payload0 + 2}; rx_transfer.payload.next->view = {4, payload1 + 1}; rx_transfer.payload.next->next->view = {2, payload2 + 3}; - const UdpardMemory udpard_memory{delegate, rx_transfer}; + const UdpardMemory udpard_memory{delegate.memoryResources(), rx_transfer}; EXPECT_THAT(udpard_memory.size(), payload_size); // Ask exactly as payload { - const std::size_t ask_size = payload_size; + constexpr std::size_t ask_size = payload_size; std::array buffer{}; EXPECT_THAT(udpard_memory.copy(0, buffer.data(), ask_size), ask_size); @@ -277,7 +286,7 @@ TEST_F(TestUdpDelegate, UdpardMemory_copy_multi_fragmented) // Ask more than payload { - const std::size_t ask_size = payload_size + 2; + constexpr std::size_t ask_size = payload_size + 2; std::array buffer{}; EXPECT_THAT(udpard_memory.copy(0, buffer.data(), ask_size), payload_size); @@ -287,7 +296,7 @@ TEST_F(TestUdpDelegate, UdpardMemory_copy_multi_fragmented) // Ask less than payload (with different offsets) { - const std::size_t ask_size = payload_size - 2; + constexpr std::size_t ask_size = payload_size - 2; std::array buffer{}; EXPECT_THAT(udpard_memory.copy(0, buffer.data(), ask_size), ask_size); @@ -316,15 +325,13 @@ TEST_F(TestUdpDelegate, UdpardMemory_copy_multi_fragmented) TEST_F(TestUdpDelegate, UdpardMemory_copy_empty) { - using UdpardMemory = udp::detail::TransportDelegate::UdpardMemory; - - TransportDelegateImpl delegate{general_mr_, &fragment_mr_, &payload_mr_}; + const TransportDelegateImpl delegate{general_mr_, &fragment_mr_, &payload_mr_}; UdpardRxTransfer rx_transfer{}; rx_transfer.payload_size = 0; rx_transfer.payload = UdpardFragment{nullptr, {0, nullptr}, {0, nullptr}}; - const UdpardMemory udpard_memory{delegate, rx_transfer}; + const UdpardMemory udpard_memory{delegate.memoryResources(), rx_transfer}; EXPECT_THAT(udpard_memory.size(), 0); std::array buffer{}; @@ -335,21 +342,21 @@ TEST_F(TestUdpDelegate, UdpardMemory_copy_empty) TEST_F(TestUdpDelegate, optAnyFailureFromUdpard) { - EXPECT_THAT(udp::detail::TransportDelegate::optAnyFailureFromUdpard(-UDPARD_ERROR_MEMORY), + EXPECT_THAT(TransportDelegate::optAnyFailureFromUdpard(-UDPARD_ERROR_MEMORY), Optional(VariantWith(_))); - EXPECT_THAT(udp::detail::TransportDelegate::optAnyFailureFromUdpard(-UDPARD_ERROR_ARGUMENT), + EXPECT_THAT(TransportDelegate::optAnyFailureFromUdpard(-UDPARD_ERROR_ARGUMENT), Optional(VariantWith(_))); - EXPECT_THAT(udp::detail::TransportDelegate::optAnyFailureFromUdpard(-UDPARD_ERROR_CAPACITY), + EXPECT_THAT(TransportDelegate::optAnyFailureFromUdpard(-UDPARD_ERROR_CAPACITY), Optional(VariantWith(_))); - EXPECT_THAT(udp::detail::TransportDelegate::optAnyFailureFromUdpard(-UDPARD_ERROR_ANONYMOUS), + EXPECT_THAT(TransportDelegate::optAnyFailureFromUdpard(-UDPARD_ERROR_ANONYMOUS), Optional(VariantWith(_))); - EXPECT_THAT(udp::detail::TransportDelegate::optAnyFailureFromUdpard(0), Eq(cetl::nullopt)); - EXPECT_THAT(udp::detail::TransportDelegate::optAnyFailureFromUdpard(1), Eq(cetl::nullopt)); - EXPECT_THAT(udp::detail::TransportDelegate::optAnyFailureFromUdpard(-1), Eq(cetl::nullopt)); + EXPECT_THAT(TransportDelegate::optAnyFailureFromUdpard(0), Eq(cetl::nullopt)); + EXPECT_THAT(TransportDelegate::optAnyFailureFromUdpard(1), Eq(cetl::nullopt)); + EXPECT_THAT(TransportDelegate::optAnyFailureFromUdpard(-1), Eq(cetl::nullopt)); } TEST_F(TestUdpDelegate, makeUdpardMemoryResource) diff --git a/test/unittest/transport/udp/test_udp_svc_rx_sessions.cpp b/test/unittest/transport/udp/test_udp_svc_rx_sessions.cpp index 3df5c06a5..d526528cd 100644 --- a/test/unittest/transport/udp/test_udp_svc_rx_sessions.cpp +++ b/test/unittest/transport/udp/test_udp_svc_rx_sessions.cpp @@ -42,7 +42,6 @@ namespace using libcyphal::TimePoint; using libcyphal::UniquePtr; using libcyphal::MemoryError; -using Callback = libcyphal::IExecutor::Callback; using namespace libcyphal::transport; // NOLINT This our main concern here in the unit tests. using namespace libcyphal::transport::udp; // NOLINT This our main concern here in the unit tests. @@ -174,9 +173,9 @@ TEST_F(TestUdpSvcRxSessions, make_response_no_memory) EXPECT_CALL(mr_mock, do_allocate(sizeof(udp::detail::SvcResponseRxSession), _)) // .WillOnce(Return(nullptr)); - auto transport = makeTransport({mr_mock}); + const auto transport = makeTransport({mr_mock}); - auto maybe_session = transport->makeResponseRxSession({64, 0x23, 0x45}); + const auto maybe_session = transport->makeResponseRxSession({64, 0x23, 0x45}); EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); } @@ -215,12 +214,27 @@ TEST_F(TestUdpSvcRxSessions, make_response_fails_due_to_rx_socket_error) } } +TEST_F(TestUdpSvcRxSessions, make_request_no_memory) +{ + StrictMock mr_mock; + mr_mock.redirectExpectedCallsTo(mr_); + + // Emulate that there is no memory available for the message session. + EXPECT_CALL(mr_mock, do_allocate(sizeof(udp::detail::SvcRequestRxSession), _)) // + .WillOnce(Return(nullptr)); + + const auto transport = makeTransport({mr_mock}); + + const auto maybe_session = transport->makeRequestRxSession({64, 0x7B}); + EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); +} + TEST_F(TestUdpSvcRxSessions, make_request_fails_due_to_argument_error) { - auto transport = makeTransport({mr_}); + const auto transport = makeTransport({mr_}); // Try invalid subject id - auto maybe_session = transport->makeRequestRxSession({64, UDPARD_SERVICE_ID_MAX + 1}); + const auto maybe_session = transport->makeRequestRxSession({64, UDPARD_SERVICE_ID_MAX + 1}); EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); } @@ -494,20 +508,26 @@ TEST_F(TestUdpSvcRxSessions, receive_response) })); constexpr std::size_t extent_bytes = 8; - auto maybe_session = transport->makeResponseRxSession({extent_bytes, 0x17B, 0x31}); - ASSERT_THAT(maybe_session, VariantWith>(NotNull())); - auto session = cetl::get>(std::move(maybe_session)); + auto maybe_session_n31 = transport->makeResponseRxSession({extent_bytes, 0x17B, 0x31}); + ASSERT_THAT(maybe_session_n31, VariantWith>(NotNull())); + auto session_n31 = cetl::get>(std::move(maybe_session_n31)); EXPECT_THAT(rx_socket_mock_.getEndpoint().ip_address, 0xEF010013); EXPECT_THAT(rx_socket_mock_.getEndpoint().udp_port, 9382); - const auto params = session->getParams(); + const auto params = session_n31->getParams(); EXPECT_THAT(params.extent_bytes, extent_bytes); EXPECT_THAT(params.service_id, 0x17B); EXPECT_THAT(params.server_node_id, 0x31); constexpr auto timeout = 200ms; - session->setTransferIdTimeout(timeout); + session_n31->setTransferIdTimeout(timeout); + + // Create another session with the same port ID but different server node ID (0x32). + // + auto maybe_session_n32 = transport->makeResponseRxSession({extent_bytes, 0x17B, 0x32}); + ASSERT_THAT(maybe_session_n32, VariantWith>(NotNull())); + auto session_n32 = cetl::get>(std::move(maybe_session_n32)); TimePoint rx_timestamp; @@ -537,8 +557,9 @@ TEST_F(TestUdpSvcRxSessions, receive_response) scheduler_.scheduleAt(rx_timestamp + 1ms, [&](const auto&) { // - const auto maybe_rx_transfer = session->receive(); + const auto maybe_rx_transfer = session_n31->receive(); ASSERT_THAT(maybe_rx_transfer, Optional(_)); + EXPECT_THAT(session_n32->receive(), Eq(cetl::nullopt)); // Different server node ID. // NOLINTNEXTLINE(bugprone-unchecked-optional-access) const auto& rx_transfer = maybe_rx_transfer.value(); @@ -572,13 +593,48 @@ TEST_F(TestUdpSvcRxSessions, receive_response) scheduler_.scheduleAt(rx_timestamp + 1ms, [&](const auto&) { // - const auto maybe_rx_transfer = session->receive(); + const auto maybe_rx_transfer = session_n31->receive(); EXPECT_THAT(maybe_rx_transfer, Eq(cetl::nullopt)); }); }); + scheduler_.scheduleAt(3s, [&](const auto&) { + // + SCOPED_TRACE("3-rd iteration: unsolicited (node 0x33) response @ 3s"); + + constexpr std::size_t payload_size = 2; + constexpr std::size_t frame_size = UdpardFrame::SizeOfHeaderAndTxCrc + payload_size; + + rx_timestamp = now() + 10ms; + EXPECT_CALL(rx_socket_mock_, receive()) // + .WillOnce([&]() -> IRxSocket::ReceiveResult::Metadata { + EXPECT_THAT(now(), rx_timestamp); + auto frame = UdpardFrame(0x33, 0x13, 0x1D, payload_size, &payload_mr_mock, Priority::High); + frame.payload()[0] = b(42); + frame.payload()[1] = b(147); + frame.setPortId(0x17B, true /*is_service*/, false /*is_request*/); + std::uint32_t tx_crc = UdpardFrame::InitialTxCrc; + return {rx_timestamp, std::move(frame).release(tx_crc)}; + }); + EXPECT_CALL(payload_mr_mock, do_allocate(frame_size, alignof(std::max_align_t))) + .WillOnce([this](const std::size_t size_bytes, const std::size_t alignment) -> void* { + return payload_mr_.allocate(size_bytes, alignment); + }); + EXPECT_CALL(payload_mr_mock, do_deallocate(_, frame_size, alignof(std::max_align_t))) + .WillOnce([this](void* const p, const std::size_t size_bytes, const std::size_t alignment) { + payload_mr_.deallocate(p, size_bytes, alignment); + }); + scheduler_.scheduleNamedCallback("rx_socket", rx_timestamp); + + scheduler_.scheduleAt(rx_timestamp + 1ms, [&](const auto&) { + // + EXPECT_THAT(session_n31->receive(), Eq(cetl::nullopt)); + EXPECT_THAT(session_n32->receive(), Eq(cetl::nullopt)); + }); + }); scheduler_.scheduleAt(9s, [&](const auto&) { // - session.reset(); + session_n31.reset(); + session_n32.reset(); EXPECT_CALL(rx_socket_mock_, deinit()); transport.reset(); testing::Mock::VerifyAndClearExpectations(&rx_socket_mock_); diff --git a/test/unittest/transport/udp/test_udp_transport.cpp b/test/unittest/transport/udp/test_udp_transport.cpp index 26b36866f..25ef2d527 100644 --- a/test/unittest/transport/udp/test_udp_transport.cpp +++ b/test/unittest/transport/udp/test_udp_transport.cpp @@ -458,6 +458,10 @@ TEST_F(TestUpdTransport, makeResponseRxSession_invalid_resubscription) // auto maybe_rx_session2 = transport->makeResponseRxSession({0, test_subject_id, 0x31}); ASSERT_THAT(maybe_rx_session2, VariantWith>(NotNull())); + + // Different remote node id 0x32! + auto maybe_rx_session3 = transport->makeResponseRxSession({0, test_subject_id, 0x32}); + ASSERT_THAT(maybe_rx_session3, VariantWith>(NotNull())); }); scheduler_.scheduleAt(9s, [&](const auto&) { //