#pragma once #include #include #include #include #include #include /// Cyphal OSI layer 6 facade, aka Presentation layer. namespace cyphal_l6 { enum class Priority : std::uint8_t { Exceptional [[maybe_unused]] = 0, ///< Highest Immediate [[maybe_unused]] = 1, Fast [[maybe_unused]] = 2, High [[maybe_unused]] = 3, Nominal [[maybe_unused]] = 4, Low [[maybe_unused]] = 5, Slow [[maybe_unused]] = 6, Optional [[maybe_unused]] = 7, ///< Lowest }; [[nodiscard]] inline Priority makePriority(const auto numeric) noexcept { return static_cast(std::min(static_cast>(numeric), static_cast>(Priority::Optional))); } [[nodiscard]] inline Priority reducePriority(const Priority base) noexcept { return makePriority(static_cast>(base) + 1); } enum class PortKind : std::uint8_t { Publisher, Subscriber, Client, Server, }; using PortID = std::uint16_t; using NodeID = std::uint16_t; static constexpr PortID MaxSubjectID = 8191; static constexpr PortID MaxServiceID = 511; static constexpr std::uint8_t MaxTransportRedundancyFactor = 3; static constexpr std::chrono::microseconds DefaultTransferIDTimeout{2'000'000}; /// A separate publisher instance is created per subject (topic). template class IPublisher { public: using Message = M; /// True if enqueued successfully, false if this message cannot be serialized or sent at this time. virtual bool publish(const M& msg) = 0; /// This is like above but accepts an already serialized DSDL object. virtual bool publish(const std::size_t size, const std::uint8_t* const payload) = 0; IPublisher() = default; virtual ~IPublisher() = default; IPublisher(IPublisher&) = delete; IPublisher(IPublisher&&) = delete; IPublisher& operator=(IPublisher&) = delete; IPublisher& operator=(IPublisher&&) = delete; }; /// Subscription is terminated when this instance is destroyed (RAII). /// The handler is of type: (Metadata, Message) -> void template class ISubscriber { public: using Message = M; struct Metadata { /// When the first frame of the transfer was picked up from the network, as provided by the driver. std::chrono::microseconds timestamp{}; /// Empty option if this is an anonymous transfer. std::optional source; }; ISubscriber() = default; virtual ~ISubscriber() = default; ISubscriber(ISubscriber&) = delete; ISubscriber(ISubscriber&&) = delete; ISubscriber& operator=(ISubscriber&) = delete; ISubscriber& operator=(ISubscriber&&) = delete; }; /// Serving is ceased when this instance is destroyed (RAII). /// The handler is of type: (Context, Request) -> void template class IServer { public: using Request = Q; using Response = A; /// The context is passed around (by value) until the corresponding request has been responded to. /// This approach allows the application to respond asynchronously at any moment as long as the context is known. /// Context shall not outlive its server. struct Context { IServer* server{}; std::chrono::microseconds timestamp{}; ///< Request transfer timestamp, from the media driver. Priority prio{}; NodeID client_node_id{}; std::uint64_t transfer_id{}; }; /// The application sends the service response after the corresponding request is processed. /// It shall be invoked EXACTLY ONCE per service request, otherwise it's an UB. The latency is not limited. virtual bool respond(const Context context, const Response& response) = 0; /// This is like above but accepts an already serialized DSDL object. virtual bool respond(const Context context, const std::size_t size, const std::uint8_t* const payload) = 0; IServer() = default; virtual ~IServer() = default; IServer(IServer&) = delete; IServer(IServer&&) = delete; IServer& operator=(IServer&) = delete; IServer& operator=(IServer&&) = delete; }; /// Memory manager interface used by node implementations to manage dynamic storage. /// All operations shall be O(1). class IHeap { protected: /// A helper that calls the destructor before deallocating the memory. /// It is implicitly constructible from a pointer to IHeap to simplify integration with std::unique_ptr<>. class Destroyer final { public: explicit Destroyer(IHeap* const heap) : heap_(heap) {} template void operator()(T* const obj) const { // NOLINTNEXTLINE(bugprone-sizeof-expression) static_assert((sizeof(T) > 0) && (!std::is_void_v), "incomplete type"); if (obj != nullptr) { obj->~T(); assert(heap_ != nullptr); heap_->deallocate(obj); } } private: IHeap* heap_; }; public: template class Ptr final : public std::unique_ptr { public: using typename std::unique_ptr::pointer; Ptr() noexcept : std::unique_ptr(nullptr, Destroyer(nullptr)) {} Ptr(T* const p, IHeap& heap) noexcept : std::unique_ptr(p, Destroyer(&heap)) {} template ::pointer, pointer> && !std::is_array_v>> // NOLINTNEXTLINE(google-explicit-constructor,hicpp-explicit-conversions) Ptr(Ptr&& other) noexcept : std::unique_ptr(std::move(other)) { } Ptr(Ptr&&) noexcept = default; Ptr& operator=(Ptr&&) noexcept = default; ~Ptr() noexcept = default; Ptr(const Ptr&) = delete; Ptr& operator=(const Ptr&) = delete; }; [[nodiscard]] virtual void* allocate(const std::size_t amount) noexcept = 0; virtual void deallocate(void* const ptr) noexcept = 0; /// Simple object construction helper that allocates memory and calls placement new on it with the specified args. /// Returns unique pointer which is nullptr if OOM. template [[nodiscard]] Ptr construct(CtorArgs&&... args) { if (void* const mem = this->allocate(sizeof(T))) { return Ptr(new (mem) T(std::forward(args)...), *this); } return {nullptr, *this}; } IHeap() = default; virtual ~IHeap() = default; IHeap(const IHeap&) = delete; IHeap(IHeap&&) = delete; IHeap& operator=(const IHeap&) = delete; IHeap& operator=(IHeap&&) = delete; }; /// Convenience aliases. template using Ptr = IHeap::Ptr; template using PublisherPtr = Ptr>; template using SubscriberPtr = Ptr>; template using ServerPtr = Ptr>; /// This template shall be instantiable for each data type used by the application. template struct DSDL final { using Parent = void; // Defined only for service request/response types. static constexpr const char* getFullNameAndVersion() noexcept; static constexpr std::size_t getExtent() noexcept; struct Serializer { /// Returns pointer to the internal buffer where serialized data is stored. [[nodiscard]] const std::uint8_t* getBuffer() const noexcept; /// Returns the number of bytes stored in the internal buffer; empty option if the object is invalid. [[nodiscard]] std::optional serialize(const D& obj); }; /// Returns the deserialized object; empty option if the serialized representation is invalid. [[nodiscard]] static std::optional deserialize(const std::size_t size, const std::uint8_t* const buffer); /// This type is not instantiable. DSDL() = delete; }; } // namespace cyphal_l6 /// This is used as a stop-gap solution until C++ code generation is implemented. /// Use it like this with Nunavut-generated C code, once for every message type used in the application: /// CYPHAL_L6_NUNAVUT_C_MESSAGE(cyphal_node_Heartbeat, 1, 0); // NOLINTNEXTLINE(cppcoreguidelines-macro-usage) #define CYPHAL_L6_NUNAVUT_C_MESSAGE(basename, major, minor) CYPHAL_L6_NUNAVUT_C(basename##_##major##_##minor, void) /// Likewise, but for service types: /// CYPHAL_L6_NUNAVUT_C_SERVICE(cyphal_node_GetInfo, 1, 0); // NOLINTNEXTLINE(cppcoreguidelines-macro-usage) #define CYPHAL_L6_NUNAVUT_C_SERVICE(basename, major, minor) \ struct basename##_##major##_##minor \ { \ }; \ template <> \ struct cyphal_l6::DSDL final \ { \ using Parent = void; \ static constexpr const char* getFullNameAndVersion() noexcept \ { \ return basename##_##major##_##minor##_FULL_NAME_AND_VERSION_; \ } \ DSDL() = delete; \ }; \ CYPHAL_L6_NUNAVUT_C(basename##_Request##_##major##_##minor, basename##_##major##_##minor); \ CYPHAL_L6_NUNAVUT_C(basename##_Response##_##major##_##minor, basename##_##major##_##minor) /// Implementation detail. Do not use this directly. // NOLINTNEXTLINE(cppcoreguidelines-macro-usage) #define CYPHAL_L6_NUNAVUT_C(nunavut_type, parent_type) \ template <> \ struct cyphal_l6::DSDL final \ { \ using Parent = parent_type; \ static constexpr const char* getFullNameAndVersion() noexcept \ { \ return nunavut_type##_FULL_NAME_AND_VERSION_; \ } \ static constexpr std::size_t getExtent() \ { \ return nunavut_type##_EXTENT_BYTES_; \ } \ struct Serializer final \ { \ /* NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init,hicpp-member-init) */ \ Serializer() \ { \ if constexpr (nunavut_type##_SERIALIZATION_BUFFER_SIZE_BYTES_ > 0) \ { \ buffer_.front() = 0; \ } \ } \ [[nodiscard]] const std::uint8_t* getBuffer() const \ { \ return buffer_.data(); \ } \ [[nodiscard]] std::optional serialize(const nunavut_type& obj) \ { \ std::size_t sz = buffer_.size(); \ const bool ok = nunavut_type##_serialize_(&obj, buffer_.data(), &sz) >= 0; \ assert(sz <= buffer_.size()); \ return ok ? sz : std::optional{}; \ } \ \ private: \ std::array buffer_; \ }; \ [[nodiscard]] static std::optional deserialize(const std::size_t size, \ const std::uint8_t* const buffer) \ { \ nunavut_type obj{}; \ std::size_t sz = size; \ const bool ok = nunavut_type##_deserialize_(&obj, buffer, &sz) >= 0; \ return ok ? obj : std::optional{}; \ } \ DSDL() = delete; \ }