From 2fe551f9cab899abe0a0d8c191a56b66d8fdbdd4 Mon Sep 17 00:00:00 2001 From: Khalil Estell Date: Fri, 3 Jan 2025 15:13:35 -0800 Subject: [PATCH] :sparkles: Add can_message_finder (#52) --- conanfile.py | 2 +- include/libhal-util/can.hpp | 102 ++++++++++++++++++ tests/can.test.cpp | 202 ++++++++++++++++++++++++++++++++++++ 3 files changed, 305 insertions(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index a351dea..c629d7e 100644 --- a/conanfile.py +++ b/conanfile.py @@ -62,7 +62,7 @@ def build_requirements(self): self.test_requires("boost-ext-ut/2.1.0") def requirements(self): - self.requires("libhal/[^4.7.0]", transitive_headers=True) + self.requires("libhal/[^4.9.0]", transitive_headers=True) def layout(self): cmake_layout(self) diff --git a/include/libhal-util/can.hpp b/include/libhal-util/can.hpp index cf6c2c7..325f51e 100644 --- a/include/libhal-util/can.hpp +++ b/include/libhal-util/can.hpp @@ -190,4 +190,106 @@ struct can_bus_divider_t return timing; } + +/** + * @brief A hal::can_transceiver wrapper class that makes finding message via ID + * easier with a find API + * + * If your driver plans to use this wrapper, rather than storing both the + * pointer to the hal::can_transceiver and this object, simply construct this + * object with the hal::can_transceiver object's address and use this object's + * transceiver() API to get access to the underlying transceiver implementation. + * This will ensure that the driver doesn't store the implementation's address + * twice. + * + */ +class can_message_finder +{ +public: + /** + * @brief Construct a new can reader object + * + * @param p_transceiver - the transceiver to read messages from + * @param p_id - the message ID to search for + */ + can_message_finder(hal::can_transceiver& p_transceiver, hal::u32 p_id) + : m_transceiver(&p_transceiver) + , m_id(p_id) + { + } + + /** + * @brief Find a message within the receive buffer matching the input message + * ID. + * + * This API performs a double copy of the can message in order to confirm that + * the message did not get modified by the driver during the copy. + * + * @return std::optional - a copy of the can message within + * the buffer. The return is set to std::nullopt if the message could not be + * found OR was modified during the copy. + */ + [[nodiscard]] std::optional find() + { + auto const buffer = m_transceiver->receive_buffer(); + auto cursor = m_transceiver->receive_cursor(); + hal::can_message message = {}; + + // Run through the circular buffer until the cursor is reached or until an + // ID match is found. + while (m_receive_cursor != cursor) { + if (m_id == buffer[m_receive_cursor].id) { + message = buffer[m_receive_cursor]; + // Can message buffers are typically updated via interrupt which can + // happen at any time. If the cursor has moved past the received message + // and changed its bits mid way To ensure that an interrupt has not + // occurred and modified the bits of the buffer message, a second copy + // is created in order to perform a stability check. + hal::can_message const copy = buffer[m_receive_cursor]; + bool const message_is_stable = copy == message; + bool const copy_id_matches = copy.id == m_id; + if (message_is_stable and copy_id_matches) { + // Perform circular increment of the m_receive_cursor + m_receive_cursor = (m_receive_cursor + 1) % buffer.size(); + return message; + } + } + // Acquire latest cursor + cursor = m_transceiver->receive_cursor(); + // Perform circular increment of the m_receive_cursor + m_receive_cursor = (m_receive_cursor + 1) % buffer.size(); + } + + return std::nullopt; + } + + /** + * @brief Returns the underlying can hal::can_transceiver allowing + * + * This API makes it possible for drivers and applications using this wrapper + * to fully access the hal::can_transceiver without needing to store the + * pointer twice. + * + * @return hal::can_transceiver& - the underlying hal::can_transceiver + */ + [[nodiscard]] inline hal::can_transceiver& transceiver() const + { + return *m_transceiver; + } + + /** + * @brief Returns the ID to being searched for + * + * @return auto - the ID being searched for + */ + [[nodiscard]] inline auto id() const + { + return m_id; + } + +private: + hal::can_transceiver* m_transceiver; + hal::u32 m_id; + std::size_t m_receive_cursor = 0; +}; } // namespace hal diff --git a/tests/can.test.cpp b/tests/can.test.cpp index f2f790b..cff5d61 100644 --- a/tests/can.test.cpp +++ b/tests/can.test.cpp @@ -3,6 +3,8 @@ #include #include +#include +#include namespace hal { @@ -71,6 +73,40 @@ void check_validity(hal::hertz p_operating_frequency, static_cast(p_target_baud_rate)) << "Failure to get the expected baud rate with " << test_subject; } + +struct test_can_transceiver : hal::can_transceiver +{ + test_can_transceiver() = default; + hal::u32 driver_baud_rate() override + { + using namespace hal::literals; + return 100_kHz; + } + + void driver_send(hal::can_message const& p_message) override + { + last_sent_message = p_message; + } + + std::span driver_receive_buffer() override + { + return m_buffer; + } + + std::size_t driver_receive_cursor() override + { + return m_cursor % m_buffer.size(); + }; + + void add_to_received_messages(hal::can_message const& p_message) + { + m_buffer[m_cursor++ % m_buffer.size()] = p_message; + } + + hal::can_message last_sent_message{}; + std::array m_buffer{}; + std::size_t m_cursor = 0; +}; } // namespace namespace hal { @@ -159,5 +195,171 @@ boost::ut::suite<"can_test"> can_test = [] { expect(that % not fail2.has_value()); expect(that % not fail4.has_value()); }; + + "hal::can_message_finder"_test = []() { + "hal::can_message_finder::find() two messages"_test = []() { + // Setup + constexpr hal::u32 search_id = 0x115; + test_can_transceiver test_transceiver; + hal::can_message_finder reader(test_transceiver, search_id); + hal::can_message findable_message0{ + .id = search_id, + .extended = false, + .remote_request = false, + .length = 3, + .payload = { 0xAB, 0xCD, 0xEF }, + }; + + hal::can_message findable_message1 = { + .id = search_id, + .extended = false, + .remote_request = false, + .length = 2, + .payload = { 0xDE, 0xAD }, + }; + + hal::can_message skipped_message = { + .id = search_id + 5, + .extended = false, + .remote_request = false, + .length = 1, + .payload = { 0xCC }, + }; + + // Ensure + auto const ensure_message = reader.find(); + expect(not ensure_message.has_value()); + + // Exercise + test_transceiver.add_to_received_messages(skipped_message); + test_transceiver.add_to_received_messages(findable_message0); + test_transceiver.add_to_received_messages(skipped_message); + test_transceiver.add_to_received_messages(skipped_message); + test_transceiver.add_to_received_messages(findable_message1); + test_transceiver.add_to_received_messages(findable_message1); + test_transceiver.add_to_received_messages(skipped_message); + test_transceiver.add_to_received_messages(skipped_message); + test_transceiver.add_to_received_messages(skipped_message); + test_transceiver.add_to_received_messages(findable_message0); + test_transceiver.add_to_received_messages(skipped_message); + auto const found_message0 = reader.find(); + auto const found_message1 = reader.find(); + auto const found_message2 = reader.find(); + auto const found_message3 = reader.find(); + auto const no_message_found0 = reader.find(); + auto const no_message_found1 = reader.find(); + + test_transceiver.add_to_received_messages(skipped_message); + test_transceiver.add_to_received_messages(skipped_message); + test_transceiver.add_to_received_messages(skipped_message); + test_transceiver.add_to_received_messages(findable_message1); + test_transceiver.add_to_received_messages(skipped_message); + + auto const found_message4 = reader.find(); + + test_transceiver.add_to_received_messages(skipped_message); + test_transceiver.add_to_received_messages(skipped_message); + test_transceiver.add_to_received_messages(skipped_message); + test_transceiver.add_to_received_messages(skipped_message); + test_transceiver.add_to_received_messages(skipped_message); + test_transceiver.add_to_received_messages(skipped_message); + test_transceiver.add_to_received_messages(skipped_message); + + auto const no_message_found2 = reader.find(); + + // Verify + expect(found_message0.has_value()); + expect(found_message0.value() == findable_message0); + expect(found_message1.has_value()); + expect(found_message1.value() == findable_message1); + expect(found_message2.has_value()); + expect(found_message2.value() == findable_message1); + expect(found_message3.has_value()); + expect(found_message3.value() == findable_message0); + expect(found_message4.has_value()); + expect(found_message4.value() == findable_message1); + expect(not no_message_found0.has_value()); + expect(not no_message_found1.has_value()); + expect(not no_message_found2.has_value()); + }; + + "hal::can_message_finder::find() overflow cursor"_test = []() { + // Setup + constexpr hal::u32 expected_id = 0x115; + test_can_transceiver test_transceiver; + hal::can_message_finder reader(test_transceiver, expected_id); + hal::can_message desired_message{ + .id = expected_id, + .extended = false, + .remote_request = false, + .length = 3, + .payload = { 0xAB, 0xCD, 0xEF }, + }; + + hal::can_message undesired_message = desired_message; + undesired_message.id = expected_id + 1; + // Setup: With 8 messages to fill the buffer, the cursor will land back at + // 0. Meaning that a find will come up with nothing. This is to be + // expected with a circular buffer if the receive cursor gets lapped. + + // Ensure + auto const ensure_message0 = reader.find(); + expect(not ensure_message0.has_value()); + auto const ensure_message1 = reader.find(); + expect(not ensure_message1.has_value()); + + // Exercise + test_transceiver.add_to_received_messages(desired_message); + for (std::size_t i = 0; i < test_transceiver.m_buffer.size(); i++) { + test_transceiver.add_to_received_messages(undesired_message); + } + + auto const found_message0 = reader.find(); + + // Verify + expect(not found_message0.has_value()); + }; + + "hal::can_message_finder::find() std::nullopt from stability failure"_test = + []() { + // This is empty because we haven't come up with a practical cross + // platform way to verify this check. + }; + + "hal::can_message_finder::transceiver() should work"_test = []() { + // Setup + test_can_transceiver test_transceiver; + hal::can_message_finder reader(test_transceiver, 0x111); + constexpr hal::can_message expected_message{ + .id = 0x111, + .extended = false, + .remote_request = false, + .length = 3, + .payload = { 0xAB, 0xCD, 0xEF }, + }; + // Setup: With 8 messages to fill the buffer, the cursor will land back at + // 0. Meaning that a find will come up with nothing. This is to be + // expected with a circular buffer if the receive cursor gets lapped. + + // Exercise + reader.transceiver().send(expected_message); + + // Verify + expect(expected_message == test_transceiver.last_sent_message); + }; + + "hal::can_message_finder::id()"_test = []() { + // Setup + constexpr auto desired_id = 0x087; + test_can_transceiver test_transceiver; + hal::can_message_finder reader(test_transceiver, desired_id); + + // Exercise + auto const actual_id = reader.id(); + + // Verify + expect(desired_id == actual_id); + }; + }; }; } // namespace hal