diff --git a/include/libcyphal/config.hpp b/include/libcyphal/config.hpp index 635d8c4e7..acfc53e28 100644 --- a/include/libcyphal/config.hpp +++ b/include/libcyphal/config.hpp @@ -6,7 +6,9 @@ #ifndef LIBCYPHAL_CONFIG_HPP_INCLUDED #define LIBCYPHAL_CONFIG_HPP_INCLUDED +#include #include +#include namespace libcyphal { @@ -24,6 +26,21 @@ namespace libcyphal /// struct Config { + /// Defines time representation as 64-bit microseconds. + /// + /// This is in line with the lizards that use `uint64_t`-typed microsecond counters throughout. + /// + struct MonotonicClock final + { + using rep = std::int64_t; + using period = std::micro; + using duration = std::chrono::duration; + using time_point = std::chrono::time_point; + + static constexpr bool is_steady = true; + + }; // MonotonicClock + /// Defines max footprint of a callback function in use by the executor. /// static constexpr std::size_t IExecutor_Callback_FunctionMaxSize() // NOSONAR cpp:S799 diff --git a/include/libcyphal/transport/can/can_transport_impl.hpp b/include/libcyphal/transport/can/can_transport_impl.hpp index 82a0f70cc..9a6e2c30b 100644 --- a/include/libcyphal/transport/can/can_transport_impl.hpp +++ b/include/libcyphal/transport/can/can_transport_impl.hpp @@ -578,8 +578,8 @@ class TransportImpl final : private TransportDelegate, public ICanTransport } const IMedia::PopResult::Metadata& pop_meta = *pop_success; - const auto timestamp_us = - std::chrono::duration_cast(pop_meta.timestamp.time_since_epoch()); + const auto timestamp_us = std::chrono::duration_cast( // + pop_meta.timestamp.time_since_epoch()); const CanardFrame canard_frame{pop_meta.can_id, {pop_meta.payload_size, payload.data()}}; CanardRxTransfer out_transfer{}; @@ -605,7 +605,8 @@ class TransportImpl final : private TransportDelegate, public ICanTransport 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}}; + const auto timestamp = TimePoint{std::chrono::duration_cast( // + std::chrono::microseconds{out_transfer.timestamp_usec})}; session_delegate->acceptRxTransfer(CanardMemory{memory(), out_transfer.payload}, TransferRxMetadata{{transfer_id, priority}, timestamp}, @@ -625,9 +626,10 @@ class TransportImpl final : private TransportDelegate, public ICanTransport &media.interface().getTxMemoryResource()}; frame.payload = {0, nullptr, 0}; - auto push_result = media.interface().push(TimePoint{std::chrono::microseconds{deadline}}, // - frame.extended_can_id, - payload); + auto push_result = media.interface().push( // + TimePoint{std::chrono::duration_cast(std::chrono::microseconds{deadline})}, + frame.extended_can_id, + payload); if (const auto* const push = cetl::get_if(&push_result)) { @@ -716,7 +718,8 @@ class TransportImpl final : private TransportDelegate, public ICanTransport // Otherwise, we would push it to the media interface. // We use strictly `<` (instead of `<=`) to give this frame a chance (one extra 1us) at the media. // - const auto deadline = TimePoint{std::chrono::microseconds{tx_item->tx_deadline_usec}}; + const auto deadline = TimePoint{std::chrono::duration_cast( // + std::chrono::microseconds{tx_item->tx_deadline_usec})}; if (now < deadline) { out_deadline = deadline; diff --git a/include/libcyphal/transport/udp/msg_tx_session.hpp b/include/libcyphal/transport/udp/msg_tx_session.hpp index 3ecd8253c..470ee363e 100644 --- a/include/libcyphal/transport/udp/msg_tx_session.hpp +++ b/include/libcyphal/transport/udp/msg_tx_session.hpp @@ -80,8 +80,8 @@ class MessageTxSession final : public IMessageTxSession CETL_NODISCARD cetl::optional send(const TransferTxMetadata& metadata, const PayloadFragments payload_fragments) override { - const auto deadline_us = - std::chrono::duration_cast(metadata.deadline.time_since_epoch()); + const auto deadline_us = std::chrono::duration_cast( // + metadata.deadline.time_since_epoch()); const auto tx_metadata = AnyUdpardTxMetadata::Publish{static_cast(deadline_us.count()), static_cast(metadata.base.priority), diff --git a/include/libcyphal/transport/udp/svc_tx_sessions.hpp b/include/libcyphal/transport/udp/svc_tx_sessions.hpp index 24091ccd8..882f311a2 100644 --- a/include/libcyphal/transport/udp/svc_tx_sessions.hpp +++ b/include/libcyphal/transport/udp/svc_tx_sessions.hpp @@ -91,8 +91,8 @@ class SvcRequestTxSession final : public IRequestTxSession return ArgumentError{}; } - const auto deadline_us = - std::chrono::duration_cast(metadata.deadline.time_since_epoch()); + const auto deadline_us = std::chrono::duration_cast( // + metadata.deadline.time_since_epoch()); const auto tx_metadata = AnyUdpardTxMetadata::Request{static_cast(deadline_us.count()), static_cast(metadata.base.priority), @@ -171,8 +171,8 @@ class SvcResponseTxSession final : public IResponseTxSession return ArgumentError{}; } - const auto deadline_us = - std::chrono::duration_cast(metadata.tx_meta.deadline.time_since_epoch()); + const auto deadline_us = std::chrono::duration_cast( // + metadata.tx_meta.deadline.time_since_epoch()); const auto tx_metadata = AnyUdpardTxMetadata::Respond{static_cast(deadline_us.count()), diff --git a/include/libcyphal/transport/udp/udp_transport_impl.hpp b/include/libcyphal/transport/udp/udp_transport_impl.hpp index 15a1b9124..b9d147178 100644 --- a/include/libcyphal/transport/udp/udp_transport_impl.hpp +++ b/include/libcyphal/transport/udp/udp_transport_impl.hpp @@ -788,7 +788,8 @@ class TransportImpl final : private TransportDelegate, public IUdpTransport // Otherwise, we would send it to the media TX socket interface. // We use strictly `<` (instead of `<=`) to give this frame a chance (one extra 1us) at the socket. // - const auto deadline = TimePoint{std::chrono::microseconds{tx_item->deadline_usec}}; + const auto deadline = TimePoint{std::chrono::duration_cast( // + std::chrono::microseconds{tx_item->deadline_usec})}; if (now < deadline) { out_deadline = deadline; @@ -919,8 +920,8 @@ class TransportImpl final : private TransportDelegate, public IUdpTransport // 2. We've got a new frame from the media RX socket, so let's try to pass it into libudpard RPC dispatcher. - const auto timestamp_us = - std::chrono::duration_cast(rx_meta.timestamp.time_since_epoch()); + const auto timestamp_us = std::chrono::duration_cast( // + rx_meta.timestamp.time_since_epoch()); const auto payload_deleter = rx_meta.payload_ptr.get_deleter(); @@ -961,7 +962,8 @@ class TransportImpl final : private TransportDelegate, public IUdpTransport 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}}; + const auto timestamp = TimePoint{std::chrono::duration_cast( // + std::chrono::microseconds{out_transfer.base.timestamp_usec})}; session_delegate->acceptRxTransfer(UdpardMemory{memoryResources(), out_transfer.base}, TransferRxMetadata{{transfer_id, priority}, timestamp}, @@ -985,8 +987,8 @@ class TransportImpl final : private TransportDelegate, public IUdpTransport // 2. We've got a new frame from the media RX socket, so let's try to pass it into libudpard subscription. - const auto timestamp_us = - std::chrono::duration_cast(rx_meta.timestamp.time_since_epoch()); + const auto timestamp_us = std::chrono::duration_cast( // + rx_meta.timestamp.time_since_epoch()); const auto payload_deleter = rx_meta.payload_ptr.get_deleter(); @@ -1017,7 +1019,8 @@ class TransportImpl final : private TransportDelegate, public IUdpTransport { 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}}; + const auto timestamp = TimePoint{std::chrono::duration_cast( // + std::chrono::microseconds{out_transfer.timestamp_usec})}; session_delegate.acceptRxTransfer(UdpardMemory{memoryResources(), out_transfer}, TransferRxMetadata{{transfer_id, priority}, timestamp}, diff --git a/include/libcyphal/types.hpp b/include/libcyphal/types.hpp index 23bb632c0..eb9beefed 100644 --- a/include/libcyphal/types.hpp +++ b/include/libcyphal/types.hpp @@ -6,6 +6,8 @@ #ifndef LIBCYPHAL_TYPES_HPP_INCLUDED #define LIBCYPHAL_TYPES_HPP_INCLUDED +#include "libcyphal/config.hpp" + #include #include #include @@ -23,23 +25,8 @@ namespace libcyphal { -/// @brief The internal time representation is in microseconds. -/// -/// This is in line with the lizards that use `uint64_t`-typed microsecond counters throughout. -/// -struct MonotonicClock final -{ - using rep = std::int64_t; - using period = std::micro; - using duration = std::chrono::duration; - using time_point = std::chrono::time_point; - - static constexpr bool is_steady = true; - -}; // MonotonicClock - -using TimePoint = MonotonicClock::time_point; -using Duration = MonotonicClock::duration; +using TimePoint = config::MonotonicClock::time_point; +using Duration = config::MonotonicClock::duration; template using UniquePtr = cetl::pmr::InterfacePtr; diff --git a/test/unittest/custom_libcyphal_config.hpp b/test/unittest/custom_libcyphal_config.hpp index 9db0856e7..d0e77a170 100644 --- a/test/unittest/custom_libcyphal_config.hpp +++ b/test/unittest/custom_libcyphal_config.hpp @@ -26,6 +26,23 @@ namespace custom struct MyConfig : libcyphal::Config { + /// Redefines time representation as 32-bit milliseconds. + /// + /// Milliseconds are chosen b/c there is no implicit conversion from native lizard's microseconds + /// to lower precision units like milliseconds, so proper explicit `std::chrono::duration_cast` is needed. + /// For details also see https://github.com/OpenCyphal-Garage/libcyphal/issues/431. + /// + struct MonotonicClock final + { + using rep = std::int32_t; + using period = std::milli; + using duration = std::chrono::duration; + using time_point = std::chrono::time_point; + + static constexpr bool is_steady = true; + + }; // MonotonicClock + struct Presentation : Config::Presentation { static constexpr std::size_t SmallPayloadSize() diff --git a/test/unittest/transport/can/test_can_transport_custom_config_millisecs.cpp b/test/unittest/transport/can/test_can_transport_custom_config_millisecs.cpp new file mode 100644 index 000000000..690716b9f --- /dev/null +++ b/test/unittest/transport/can/test_can_transport_custom_config_millisecs.cpp @@ -0,0 +1,82 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +// The main purpose of this test file is make sure that CAN transport +// could be compiled with custom time representation (32-bit milliseconds) instead of the default one. +// Milliseconds are chosen b/c there is no implicit conversion from native lizard's microseconds +// to lower precision units like milliseconds, so proper explicit `std::chrono::duration_cast` is needed. +// For details also see https://github.com/OpenCyphal-Garage/libcyphal/issues/431. +#include "custom_libcyphal_config.hpp" // NOLINT(misc-include-cleaner) + +#include "media_mock.hpp" +#include "tracking_memory_resource.hpp" +#include "virtual_time_scheduler.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace +{ + +using libcyphal::UniquePtr; +using namespace libcyphal::transport::can; // NOLINT This our main concern here in the unit tests. + +using testing::Eq; +using testing::Return; +using testing::IsEmpty; +using testing::NotNull; +using testing::ReturnRef; +using testing::StrictMock; +using testing::VariantWith; + +class TestCanTransportCustomConfigMilliseconds : public testing::Test +{ +protected: + void SetUp() override + { + cetl::pmr::set_default_resource(&mr_); + + EXPECT_CALL(media_mock_, getMtu()).WillRepeatedly(Return(CANARD_MTU_CAN_CLASSIC)); + EXPECT_CALL(media_mock_, getTxMemoryResource()).WillRepeatedly(ReturnRef(mr_)); + } + + void TearDown() override + { + EXPECT_THAT(mr_.allocations, IsEmpty()); + EXPECT_THAT(mr_.total_allocated_bytes, mr_.total_deallocated_bytes); + } + + // MARK: Data members: + + // NOLINTBEGIN + libcyphal::VirtualTimeScheduler scheduler_{}; + TrackingMemoryResource mr_; + StrictMock media_mock_{}; + // NOLINTEND +}; + +// MARK: - Tests: + +TEST_F(TestCanTransportCustomConfigMilliseconds, makeTransport_getLocalNodeId) +{ + std::array media_array{&media_mock_}; + auto maybe_transport = makeTransport(mr_, scheduler_, media_array, 0); + ASSERT_THAT(maybe_transport, VariantWith>(NotNull())); + + const auto transport = cetl::get>(std::move(maybe_transport)); + EXPECT_THAT(transport->getLocalNodeId(), Eq(cetl::nullopt)); +} + +} // namespace diff --git a/test/unittest/transport/udp/test_udp_transport_custom_config_millisecs.cpp b/test/unittest/transport/udp/test_udp_transport_custom_config_millisecs.cpp new file mode 100644 index 000000000..b1514e67b --- /dev/null +++ b/test/unittest/transport/udp/test_udp_transport_custom_config_millisecs.cpp @@ -0,0 +1,79 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +// The main purpose of this test file is make sure that UDP transport +// could be compiled with custom time representation (32-bit milliseconds) instead of the default one. +// Milliseconds are chosen b/c there is no implicit conversion from native lizard's microseconds +// to lower precision units like milliseconds, so proper explicit `std::chrono::duration_cast` is needed. +// For details also see https://github.com/OpenCyphal-Garage/libcyphal/issues/431. +#include "custom_libcyphal_config.hpp" // NOLINT(misc-include-cleaner) + +#include "media_mock.hpp" +#include "tracking_memory_resource.hpp" +#include "virtual_time_scheduler.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace +{ + +using libcyphal::UniquePtr; +using namespace libcyphal::transport::udp; // NOLINT This our main concern here in the unit tests. + +using testing::Eq; +using testing::IsEmpty; +using testing::NotNull; +using testing::ReturnRef; +using testing::StrictMock; +using testing::VariantWith; + +class TestUpdTransportCustomConfigMilliseconds : public testing::Test +{ +protected: + void SetUp() override + { + cetl::pmr::set_default_resource(&mr_); + + EXPECT_CALL(media_mock_, getTxMemoryResource()).WillRepeatedly(ReturnRef(mr_)); + } + + void TearDown() override + { + EXPECT_THAT(mr_.allocations, IsEmpty()); + EXPECT_THAT(mr_.total_allocated_bytes, mr_.total_deallocated_bytes); + } + + // MARK: Data members: + + // NOLINTBEGIN + libcyphal::VirtualTimeScheduler scheduler_{}; + TrackingMemoryResource mr_; + StrictMock media_mock_{}; + // NOLINTEND +}; + +// MARK: - Tests: + +TEST_F(TestUpdTransportCustomConfigMilliseconds, makeTransport_getLocalNodeId) +{ + std::array media_array{&media_mock_}; + auto maybe_transport = makeTransport({mr_}, scheduler_, media_array, 0); + ASSERT_THAT(maybe_transport, VariantWith>(NotNull())); + + const auto transport = cetl::get>(std::move(maybe_transport)); + EXPECT_THAT(transport->getLocalNodeId(), Eq(cetl::nullopt)); +} + +} // namespace