From 741a07af6d18871899cb6a70e2fb24fabc7bc7e8 Mon Sep 17 00:00:00 2001 From: Adrian Del Grosso <10929341+ad3154@users.noreply.github.com> Date: Sat, 1 Jun 2024 16:32:02 -0600 Subject: [PATCH] [FS]: Add ISOBUS/J1939 Time/Date interface Added an interface for interacting with the time and date PGN. This is related to the ISOUS file server client, where time and date may need to be known for some operations. Also, updated a few more github action versions. --- .github/workflows/pio.yml | 4 +- isobus/CMakeLists.txt | 2 + .../can_general_parameter_group_numbers.hpp | 1 + .../isobus/isobus_time_date_interface.hpp | 135 +++++++++ isobus/src/isobus_time_date_interface.cpp | 217 +++++++++++++++ test/CMakeLists.txt | 1 + test/time_date_tests.cpp | 260 ++++++++++++++++++ 7 files changed, 618 insertions(+), 2 deletions(-) create mode 100644 isobus/include/isobus/isobus/isobus_time_date_interface.hpp create mode 100644 isobus/src/isobus_time_date_interface.cpp create mode 100644 test/time_date_tests.cpp diff --git a/.github/workflows/pio.yml b/.github/workflows/pio.yml index c18a3a20..56de0dce 100644 --- a/.github/workflows/pio.yml +++ b/.github/workflows/pio.yml @@ -11,14 +11,14 @@ jobs: example: [examples/virtual_terminal/esp32_platformio_object_pool] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/cache@v4 with: path: | ~/.cache/pip ~/.platformio/.cache key: ${{ runner.os }}-pio - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.9" cache: "pip" diff --git a/isobus/CMakeLists.txt b/isobus/CMakeLists.txt index 1ca000f3..a0cc6ae2 100644 --- a/isobus/CMakeLists.txt +++ b/isobus/CMakeLists.txt @@ -30,6 +30,7 @@ set(ISOBUS_SRC "nmea2000_fast_packet_protocol.cpp" "isobus_data_dictionary.cpp" "isobus_language_command_interface.cpp" + "isobus_time_date_interface.cpp" "isobus_task_controller_client_objects.cpp" "isobus_task_controller_client.cpp" "isobus_device_descriptor_object_pool.cpp" @@ -79,6 +80,7 @@ set(ISOBUS_INCLUDE "isobus_data_dictionary.hpp" "isobus_virtual_terminal_objects.hpp" "isobus_language_command_interface.hpp" + "isobus_time_date_interface.hpp" "isobus_standard_data_description_indices.hpp" "isobus_task_controller_client_objects.hpp" "isobus_task_controller_client.hpp" diff --git a/isobus/include/isobus/isobus/can_general_parameter_group_numbers.hpp b/isobus/include/isobus/isobus/can_general_parameter_group_numbers.hpp index 38afae65..2f238693 100644 --- a/isobus/include/isobus/isobus/can_general_parameter_group_numbers.hpp +++ b/isobus/include/isobus/isobus/can_general_parameter_group_numbers.hpp @@ -51,6 +51,7 @@ namespace isobus CommandedAddress = 0xFED8, SoftwareIdentification = 0xFEDA, AllImplementsStopOperationsSwitchState = 0xFD02, + TimeDate = 0xFEE6, VesselHeading = 0x1F112, RateOfTurn = 0x1F113, PositionRapidUpdate = 0x1F801, diff --git a/isobus/include/isobus/isobus/isobus_time_date_interface.hpp b/isobus/include/isobus/isobus/isobus_time_date_interface.hpp new file mode 100644 index 00000000..fefc1439 --- /dev/null +++ b/isobus/include/isobus/isobus/isobus_time_date_interface.hpp @@ -0,0 +1,135 @@ +//================================================================================================ +/// @file isobus_time_date_interface.hpp +/// +/// @brief Defines an interface for accessing or sending time and date information using +/// the Time/Date (TD) PGN. Can be useful for interacting with an ISOBUS file server, +/// or just for keeping track of time and date information as provided by some authoritative +/// control function on the bus. Control functions which provide the message this interface +/// manages are expected to have a real-time clock (RTC) or GPS time source. +/// @author Adrian Del Grosso +/// +/// @copyright 2024 The Open-Agriculture Developers +//================================================================================================ +#ifndef ISOBUS_TIME_DATE_INTERFACE_HPP +#define ISOBUS_TIME_DATE_INTERFACE_HPP + +#include "isobus/isobus/can_callbacks.hpp" +#include "isobus/isobus/can_internal_control_function.hpp" +#include "isobus/isobus/can_message.hpp" +#include "isobus/utility/event_dispatcher.hpp" + +namespace isobus +{ + /// @brief An interface for sending and receiving time and date information using the Time/Date (TD) PGN, 0xFEE6. + /// You may hear this time referred to as "ISOBUS Time" in some cases. It is normally provided by control functions with a + /// real-time clock (RTC) or GPS source. This is not the same thing as the NMEA2000 time and date, which is PGN 129033 (0x1F809), and is + /// backwards compatible with J1939 which uses this same PGN and message structure. + class TimeDateInterface + { + public: + /// @brief A struct to hold time and date information. + /// This will generally be a UTC time and date, unless the local hour offset is 0, + /// in which case it will be a local time and date. + /// We store it slightly differently than the PGN to make it easier to work with. + struct TimeAndDate + { + std::uint8_t milliseconds = 0; ///< Number of milliseconds. This has resolution of 0.25s, so it will be either 0, 250, 500, or 750 + std::uint8_t seconds = 0; ///< Number of seconds, range: 0 to 59s + std::uint8_t minutes = 0; ///< Number of minutes, range: 0 to 59m + std::uint8_t hours = 0; ///< Number of hours, range: 0 to 23h + std::uint8_t quarterDays = 0; ///< Number of quarter days. This is a less precise version of "hours" that is used in some cases. Range: 0 to 3. 0 is midnight, 1 is 6am, 2 is noon, 3 is 6pm + std::uint8_t day = 0; ///< Number of days, range 0 to 31 + std::uint8_t month = 0; ///< Number of months, range 1 to 12 + std::uint16_t year = 1985; ///< The year. Range: 1985 to 2235 + std::int8_t localMinuteOffset = 0; ///< Local minute offset is the number of minutes between the UTC time and date and a local time and date. This value is added to UTC time and date to determine the local time and date. The local offset is a positive value for times east of the Prime Meridian to the International Date Line. + std::int8_t localHourOffset = 0; ///< Local hour offset is the number of hours between the UTC time and date and a local time and date. This value is added to UTC time and date to determine the local time and date. The local offset is a positive value for times east of the Prime Meridian to the International Date Line. + }; + + /// @brief A struct to hold time and date information and the control function that sent it. + /// Used by the event dispatcher to provide event driven access to time and date information. + struct TimeAndDateInformation + { + TimeAndDate timeAndDate; ///< The time and date information + std::shared_ptr controlFunction; ///< The control function that sent the time and date information + }; + + /// @brief Constructor for the TimeDateInterface class, with no source control function. + /// Receives time and date information from the bus, and does not transmit. + /// This is generally the normal use case for this class. + TimeDateInterface() = default; + + /// @brief Constructor for the TimeDateInterface class, used for when you want to also transmit the time/date. + /// @param sourceControlFunction If you want to transmit the time and date information, you + /// can pass a control function in this parameter to be used as the source of the information. + /// @param timeAndDateCallback A callback that will be called when the interface needs you to tell it the current time and date. + /// This is used to populate the time and date information that will be sent out on the bus. The function you use for this callback + /// should be relatively quick as it will be called from the CAN stack's thread, and you don't want to delay the stack's update thread. + /// The function should return "true" if the time and date information was successfully populated, and "false" if it was not. + /// Note that if it returns false, the request will probably be NACKed, which is not ideal. + TimeDateInterface(std::shared_ptr sourceControlFunction, std::function timeAndDateCallback); + + /// @brief Destructor for the TimeDateInterface class. + ~TimeDateInterface(); + + /// @brief Deleted copy constructor for TimeDateInterface + TimeDateInterface(TimeDateInterface &) = delete; + + /// @brief Initializes the interface. + /// @details This needs to be called before the interface is usable. + /// It registers its PGN callback and sets up the PGN request interface + /// if needed. + void initialize(); + + /// @brief Returns if initialize has been called yet + /// @return `true` if initialize has been called, otherwise false + bool is_initialized() const; + + /// @brief Returns the event dispatcher for time and date information. + /// Use this to subscribe to event-driven time and date information events. + /// @return The event dispatcher for time and date information + EventDispatcher &get_event_dispatcher(); + + /// @brief Sends a time and date message (a broadcast message) as long as the interface + /// has been initialized and a control function has been set. + /// @param timeAndDateToSend The time and date information to send + /// @return `true` if the message was sent, otherwise `false` + bool send_time_and_date(const TimeAndDate &timeAndDateToSend) const; + + /// @brief Requests time and date information from a specific control function, or from all control functions to see if any respond. + /// Responses can be monitored by using the event dispatcher. See get_event_dispatcher. + /// This is really just a very thin wrapper around the PGN request interface for convenience. + /// @param requestingControlFunction This control function will be used to send the request. + /// @param optionalDestination If you want to request time and date information from a specific control function, you can pass it here, otherwise pass an empty pointer. + /// @return `true` if the request was sent, otherwise `false` + bool request_time_and_date(std::shared_ptr requestingControlFunction, std::shared_ptr optionalDestination = nullptr) const; + + /// @brief Returns the control function that is being used as the source of the time and date information if one was set. + /// @return The control function that is being used as the source of the time and date information, or an empty pointer if one was not set. + std::shared_ptr get_control_function() const; + + private: + /// @brief Parses incoming CAN messages into usable unit and language settings + /// @param message The CAN message to parse + /// @param parentPointer A generic context variable, usually the `this` pointer for this interface instance + static void process_rx_message(const CANMessage &message, void *parentPointer); + + /// @brief Processes a PGN request + /// @param[in] parameterGroupNumber The PGN being requested + /// @param[in] requestingControlFunction The control function that is requesting the PGN + /// @param[in] acknowledge If the request should be acknowledged (will always be false for this interface) + /// @param[in] acknowledgeType How to acknowledge the request (will always be NACK for this interface) + /// @param[in] parentPointer A context variable to find the relevant instance of this class + /// @returns True if the request was serviced, otherwise false. + static bool process_request_for_time_date(std::uint32_t parameterGroupNumber, + std::shared_ptr requestingControlFunction, + bool &acknowledge, + AcknowledgementType &acknowledgeType, + void *parentPointer); + + std::shared_ptr myControlFunction; ///< The control function to send messages as, or an empty pointer if not sending + std::function userTimeDateCallback; ///< The callback the user provided to get the time and date information at runtime to be transmitted + EventDispatcher timeAndDateEventDispatcher; ///< The event dispatcher for time and date information + bool initialized = false; ///< If the interface has been initialized yet + }; +} // namespace isobus +#endif // ISOBUS_TIME_DATE_INTERFACE_HPP diff --git a/isobus/src/isobus_time_date_interface.cpp b/isobus/src/isobus_time_date_interface.cpp new file mode 100644 index 00000000..299d6ad8 --- /dev/null +++ b/isobus/src/isobus_time_date_interface.cpp @@ -0,0 +1,217 @@ +//================================================================================================ +/// @file isobus_time_date_interface.cpp +/// +/// @brief Implements an interface to handle to transmit the time and date information using the +/// Time/Date (TD) PGN. +/// +/// @author Adrian Del Grosso +/// +/// @copyright 2024 The Open-Agriculture Developers +//================================================================================================ +#include "isobus/isobus/isobus_time_date_interface.hpp" + +#include "isobus/isobus/can_general_parameter_group_numbers.hpp" +#include "isobus/isobus/can_parameter_group_number_request_protocol.hpp" +#include "isobus/isobus/can_stack_logger.hpp" +#include "isobus/utility/to_string.hpp" + +#include + +#ifndef DISABLE_CAN_STACK_LOGGER +#include +#include +#endif + +namespace isobus +{ + TimeDateInterface::TimeDateInterface(std::shared_ptr sourceControlFunction, std::function timeAndDateCallback) : + myControlFunction(sourceControlFunction), + userTimeDateCallback(timeAndDateCallback) + { + if (nullptr != sourceControlFunction) + { + assert(nullptr != timeAndDateCallback); // You need a callback to populate the time and date information... the interface needs to know the time and date to send it out on the bus! + } + } + + TimeDateInterface::~TimeDateInterface() + { + if (initialized && (nullptr != myControlFunction)) + { + auto pgnRequestProtocol = myControlFunction->get_pgn_request_protocol().lock(); + + if (nullptr != pgnRequestProtocol) + { + pgnRequestProtocol->remove_pgn_request_callback(static_cast(CANLibParameterGroupNumber::TimeDate), process_request_for_time_date, this); + } + } + } + + void TimeDateInterface::initialize() + { + if (!initialized) + { + CANNetworkManager::CANNetwork.add_global_parameter_group_number_callback(static_cast(CANLibParameterGroupNumber::TimeDate), + process_rx_message, + this); + + if (nullptr != myControlFunction) + { + auto pgnRequestProtocol = myControlFunction->get_pgn_request_protocol().lock(); + + if (nullptr != pgnRequestProtocol) + { + pgnRequestProtocol->register_pgn_request_callback(static_cast(CANLibParameterGroupNumber::TimeDate), process_request_for_time_date, this); + } + } + initialized = true; + } + } + + bool TimeDateInterface::is_initialized() const + { + return initialized; + } + + EventDispatcher &TimeDateInterface::get_event_dispatcher() + { + return timeAndDateEventDispatcher; + } + + bool TimeDateInterface::send_time_and_date(const TimeAndDate &timeAndDateToSend) const + { + // If you hit any of these assertions, it's because you are trying to send an invalid time and date. + // Sending invalid values on the network is bad. + // Please check the values you are trying to send and make sure they are within the valid ranges noted below. + // These values can also be found in the ISO11783-7 standard (on isobus.net), or in the J1939 standard. + // Also, please only send the time and date if you have a good RTC or GPS source. Sending bad time and date information can cause issues for other devices on the network. + assert(timeAndDateToSend.year >= 1985 && timeAndDateToSend.year <= 2235); // The year must be between 1985 and 2235 + assert(timeAndDateToSend.month >= 1 && timeAndDateToSend.month <= 12); // The month must be between 1 and 12 + assert(timeAndDateToSend.day <= 31); // The day must be between 0 and 31 + assert(timeAndDateToSend.hours <= 23); // The hours must be between 0 and 23 + assert(timeAndDateToSend.minutes <= 59); // The minutes must be between 0 and 59 + assert(timeAndDateToSend.seconds <= 59); // The seconds must be between 0 and 59 + assert(timeAndDateToSend.quarterDays <= 3); // The quarter days must be between 0 and 3 + assert(timeAndDateToSend.milliseconds == 0 || timeAndDateToSend.milliseconds == 250 || timeAndDateToSend.milliseconds == 500 || timeAndDateToSend.milliseconds == 750); // The milliseconds must be 0, 250, 500, or 750 + assert(timeAndDateToSend.localHourOffset >= -23 && timeAndDateToSend.localHourOffset <= 23); // The local hour offset must be between -23 and 23 + assert(timeAndDateToSend.localMinuteOffset >= -59 && timeAndDateToSend.localMinuteOffset <= 59); // The local minute offset must be between -59 and 59 + + const std::array buffer = { + static_cast(timeAndDateToSend.seconds * 4 + (timeAndDateToSend.milliseconds / 250)), + timeAndDateToSend.minutes, + timeAndDateToSend.hours, + timeAndDateToSend.month, + static_cast(timeAndDateToSend.day * 4 + timeAndDateToSend.quarterDays), + static_cast(timeAndDateToSend.year - 1985), + static_cast(timeAndDateToSend.localMinuteOffset + 125), + static_cast(timeAndDateToSend.localHourOffset + 125) + }; + return CANNetworkManager::CANNetwork.send_can_message(static_cast(CANLibParameterGroupNumber::TimeDate), + buffer.data(), + buffer.size(), + myControlFunction, + nullptr, + CANIdentifier::CANPriority::PriorityDefault6); + } + + bool TimeDateInterface::request_time_and_date(std::shared_ptr requestingControlFunction, std::shared_ptr optionalDestination) const + { + bool retVal = false; + + if (nullptr != requestingControlFunction) + { + retVal = ParameterGroupNumberRequestProtocol::request_parameter_group_number(static_cast(CANLibParameterGroupNumber::TimeDate), + requestingControlFunction, + optionalDestination); + } + return retVal; + } + + std::shared_ptr TimeDateInterface::get_control_function() const + { + return myControlFunction; + } + + void TimeDateInterface::process_rx_message(const CANMessage &message, void *parentPointer) + { + auto timeDateInterface = static_cast(parentPointer); + + if ((nullptr != timeDateInterface) && + (static_cast(CANLibParameterGroupNumber::TimeDate) == message.get_identifier().get_parameter_group_number()) && + (nullptr != message.get_source_control_function())) + { + if (CAN_DATA_LENGTH == message.get_data_length()) + { + TimeAndDateInformation timeAndDateInformation; + + timeAndDateInformation.controlFunction = message.get_source_control_function(); + timeAndDateInformation.timeAndDate.seconds = message.get_uint8_at(0) / 4; // This is SPN 959 + timeAndDateInformation.timeAndDate.milliseconds = (message.get_uint8_at(0) % 4) * 250; // This is also part of SPN 959 + timeAndDateInformation.timeAndDate.minutes = message.get_uint8_at(1); // This is SPN 960 + timeAndDateInformation.timeAndDate.hours = message.get_uint8_at(2); // This is SPN 961 + timeAndDateInformation.timeAndDate.month = message.get_uint8_at(3); // This is SPN 963 + timeAndDateInformation.timeAndDate.day = message.get_uint8_at(4) / 4; // This is SPN 962 + timeAndDateInformation.timeAndDate.quarterDays = message.get_uint8_at(4) % 4; // This is also part of SPN 962 + timeAndDateInformation.timeAndDate.year = static_cast(message.get_uint8_at(5) + 1985); // This is SPN 964 + timeAndDateInformation.timeAndDate.localMinuteOffset = static_cast(message.get_uint8_at(6) - 125); // This is SPN 1601 + timeAndDateInformation.timeAndDate.localHourOffset = static_cast(message.get_int8_at(7) - 125); // This is SPN 1602 + +#ifndef DISABLE_CAN_STACK_LOGGER + if (CANStackLogger::get_log_level() == CANStackLogger::LoggingLevel::Debug) // This is a very heavy log statement, so only do it if we are logging at debug level + { + std::ostringstream oss; + oss << "[Time/Date]: Control Function 0x"; + oss << std::setfill('0') << std::setw(16) << std::hex << message.get_source_control_function()->get_NAME().get_full_name(); + oss << " at address " << static_cast(message.get_source_control_function()->get_address()); + oss << " reports it is: " << isobus::to_string(static_cast(timeAndDateInformation.timeAndDate.hours)) << ":"; + oss << isobus::to_string(static_cast(timeAndDateInformation.timeAndDate.minutes)) << ":"; + oss << isobus::to_string(static_cast(timeAndDateInformation.timeAndDate.seconds)); + oss << " on day " << isobus::to_string(static_cast(timeAndDateInformation.timeAndDate.day)); + oss << " of month " << isobus::to_string(static_cast(timeAndDateInformation.timeAndDate.month)); + oss << " in the year " << isobus::to_string(static_cast(timeAndDateInformation.timeAndDate.year)); + oss << " with a local offset of " << isobus::to_string(static_cast(timeAndDateInformation.timeAndDate.localHourOffset)); + oss << " hours and " << isobus::to_string(static_cast(timeAndDateInformation.timeAndDate.localMinuteOffset)) << " minutes."; + CANStackLogger::debug(oss.str()); + } +#endif + timeDateInterface->timeAndDateEventDispatcher.invoke(std::move(timeAndDateInformation)); + } + else + { + CANStackLogger::warn("[Time/Date]: Received a Time/Date message with an invalid data length. DLC must be 8."); + } + } + } + + bool TimeDateInterface::process_request_for_time_date(std::uint32_t parameterGroupNumber, + std::shared_ptr, + bool &acknowledge, + AcknowledgementType &acknowledgeType, + void *parentPointer) + { + bool retVal = false; + + if ((nullptr != parentPointer) && + (static_cast(CANLibParameterGroupNumber::TimeDate) == parameterGroupNumber)) + { + auto interface = static_cast(parentPointer); + + if ((nullptr != interface->myControlFunction) && + (nullptr != interface->userTimeDateCallback)) + { + TimeAndDate timeAndDateInformation; + if (interface->userTimeDateCallback(timeAndDateInformation)) // Getting the time and date information from the user callback + { + CANStackLogger::debug("[Time/Date]: Received a request for Time/Date information and interface is configured to reply. Sending Time/Date."); + retVal = interface->send_time_and_date(timeAndDateInformation); + acknowledge = false; + } + else + { + CANStackLogger::error("[Time/Date]: Your application failed to provide Time/Date information when requested! You are probably doing something wrong. The request may be NACKed as a result."); + } + } + } + return retVal; + } +} // namespace isobus diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 4c501955..f3b54792 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -51,6 +51,7 @@ set(TEST_SRC event_dispatcher_tests.cpp isb_tests.cpp cf_functionalities_tests.cpp + time_date_tests.cpp guidance_tests.cpp speed_distance_message_tests.cpp maintain_power_tests.cpp diff --git a/test/time_date_tests.cpp b/test/time_date_tests.cpp new file mode 100644 index 00000000..b96f1ae7 --- /dev/null +++ b/test/time_date_tests.cpp @@ -0,0 +1,260 @@ +//================================================================================================ +/// @file time_date_tests.cpp +/// +/// @brief Unit tests for the TimeDateInterface class. +/// @author Adrian Del Grosso +/// +/// @copyright 2024 The Open-Agriculture Developers +//================================================================================================ +#include + +#include "helpers/control_function_helpers.hpp" +#include "helpers/messaging_helpers.hpp" +#include "isobus/hardware_integration/can_hardware_interface.hpp" +#include "isobus/hardware_integration/virtual_can_plugin.hpp" +#include "isobus/isobus/can_network_manager.hpp" +#include "isobus/isobus/isobus_time_date_interface.hpp" + +using namespace isobus; + +static TimeDateInterface::TimeAndDateInformation testTimeDateInformation; +static bool isRxCallbackCalled = false; +void test_time_date_rx_callback(TimeDateInterface::TimeAndDateInformation timeDate) +{ + testTimeDateInformation = timeDate; + isRxCallbackCalled = true; +} + +TEST(TIME_DATE_TESTS, ReceivingMessages) +{ + VirtualCANPlugin testPlugin; + testPlugin.open(); + + CANHardwareInterface::set_number_of_can_channels(1); + CANHardwareInterface::assign_can_channel_frame_handler(0, std::make_shared()); + CANHardwareInterface::start(); + + TimeDateInterface timeDateInterfaceUnderTest; + + EXPECT_FALSE(timeDateInterfaceUnderTest.is_initialized()); + timeDateInterfaceUnderTest.initialize(); + EXPECT_TRUE(timeDateInterfaceUnderTest.is_initialized()); + + // Test receiving a time and date message + auto partner = test_helpers::force_claim_partnered_control_function(0x47, 0); + + CANMessageFrame testFrame; + memset(&testFrame, 0, sizeof(testFrame)); + testFrame.isExtendedFrame = true; + + // Register with the event dispatcher + timeDateInterfaceUnderTest.get_event_dispatcher().add_listener(test_time_date_rx_callback); + CANNetworkManager::CANNetwork.update(); // Extra update to help when running under ctest + + // Construct a message that says the following: + // 1. The year is 2023 + // 2. The month is August + // 3. 7 Days into the month + // 4. 22 hours into the day + // 5. 49 minutes into the hour + // 6. 41.000 seconds into the minute + // 7. Local hour offset is -5 (Eastern Standard Time, which implies the stuff above is UTC time/date) + // 8. Local minute offset is 0 + testFrame.identifier = 0x18FEE647; + testFrame.dataLength = 8; + testFrame.data[0] = 0xA4; + testFrame.data[1] = 0x31; + testFrame.data[2] = 0x16; + testFrame.data[3] = 0x08; + testFrame.data[4] = 0x1C; + testFrame.data[5] = 0x26; + testFrame.data[6] = 0x7D; + testFrame.data[7] = 0x78; + CANNetworkManager::CANNetwork.process_receive_can_message_frame(testFrame); + CANNetworkManager::CANNetwork.update(); + + EXPECT_TRUE(isRxCallbackCalled); + EXPECT_EQ(testTimeDateInformation.timeAndDate.year, 2023); + EXPECT_EQ(testTimeDateInformation.timeAndDate.month, 8); + EXPECT_EQ(testTimeDateInformation.timeAndDate.day, 7); + EXPECT_EQ(testTimeDateInformation.timeAndDate.quarterDays, 0); + EXPECT_EQ(testTimeDateInformation.timeAndDate.hours, 22); + EXPECT_EQ(testTimeDateInformation.timeAndDate.minutes, 49); + EXPECT_EQ(testTimeDateInformation.timeAndDate.seconds, 41); + EXPECT_EQ(testTimeDateInformation.timeAndDate.milliseconds, 0); + EXPECT_EQ(testTimeDateInformation.timeAndDate.localHourOffset, -5); + EXPECT_EQ(testTimeDateInformation.timeAndDate.localMinuteOffset, 0); + + isRxCallbackCalled = false; + // Send a message with wrong length, make sure it's rejected + testFrame.dataLength = 7; + CANNetworkManager::CANNetwork.process_receive_can_message_frame(testFrame); + CANNetworkManager::CANNetwork.update(); + EXPECT_FALSE(isRxCallbackCalled); + + CANHardwareInterface::stop(); +} + +TEST(TIME_DATE_TESTS, TransmitMessages) +{ + VirtualCANPlugin testPlugin; + testPlugin.open(); + + CANHardwareInterface::set_number_of_can_channels(1); + CANHardwareInterface::assign_can_channel_frame_handler(0, std::make_shared()); + CANHardwareInterface::start(); + + auto testInternalControlFunction = test_helpers::claim_internal_control_function(0x44, 0); + test_helpers::force_claim_partnered_control_function(0x25, 0); + + // To test transmitting, we need to provide a callback that will populate the time and date information + // that will be sent out on the bus. + // This is so that the PGN request protocol can ask for the time and date information at any time. + TimeDateInterface timeDateInterfaceUnderTest(testInternalControlFunction, [](TimeDateInterface::TimeAndDate &timeAndDateToPopulate) -> bool { + timeAndDateToPopulate.year = 2023; + timeAndDateToPopulate.month = 8; + timeAndDateToPopulate.day = 7; + timeAndDateToPopulate.quarterDays = 0; + timeAndDateToPopulate.hours = 22; + timeAndDateToPopulate.minutes = 49; + timeAndDateToPopulate.seconds = 41; + timeAndDateToPopulate.milliseconds = 0; + timeAndDateToPopulate.localHourOffset = -5; + timeAndDateToPopulate.localMinuteOffset = 0; + return true; + }); + + EXPECT_FALSE(timeDateInterfaceUnderTest.is_initialized()); + timeDateInterfaceUnderTest.initialize(); + EXPECT_TRUE(timeDateInterfaceUnderTest.is_initialized()); + + EXPECT_EQ(timeDateInterfaceUnderTest.get_control_function(), testInternalControlFunction); + + // Get the virtual CAN plugin back to a known state + CANMessageFrame testFrame; + while (!testPlugin.get_queue_empty()) + { + testPlugin.read_frame(testFrame); + } + ASSERT_TRUE(testPlugin.get_queue_empty()); + + // Now, we can see if it works by receiving a PGN request for the time and date information PGN + testFrame.isExtendedFrame = true; + testFrame.channel = 0; + testFrame.dataLength = 3; + testFrame.identifier = 0x18EAFF25; + testFrame.data[0] = 0xE6; + testFrame.data[1] = 0xFE; + testFrame.data[2] = 0x00; + CANNetworkManager::CANNetwork.process_receive_can_message_frame(testFrame); + CANNetworkManager::CANNetwork.update(); + + // This data should match the data we provided in the callback, and the one we processed in the other unit test + EXPECT_TRUE(testPlugin.read_frame(testFrame)); + EXPECT_EQ(0x18FEE644, testFrame.identifier); + EXPECT_EQ(0x08, testFrame.dataLength); + EXPECT_EQ(0xA4, testFrame.data[0]); + EXPECT_EQ(0x31, testFrame.data[1]); + EXPECT_EQ(0x16, testFrame.data[2]); + EXPECT_EQ(0x08, testFrame.data[3]); + EXPECT_EQ(0x1C, testFrame.data[4]); + EXPECT_EQ(0x26, testFrame.data[5]); + EXPECT_EQ(0x7D, testFrame.data[6]); + EXPECT_EQ(0x78, testFrame.data[7]); + + // Test emitting a request for the time and date information + timeDateInterfaceUnderTest.request_time_and_date(testInternalControlFunction, nullptr); + CANNetworkManager::CANNetwork.update(); + EXPECT_TRUE(testPlugin.read_frame(testFrame)); + EXPECT_EQ(0x18EAFF44, testFrame.identifier); + EXPECT_EQ(0x03, testFrame.dataLength); + EXPECT_EQ(0xE6, testFrame.data[0]); + EXPECT_EQ(0xFE, testFrame.data[1]); + EXPECT_EQ(0x00, testFrame.data[2]); + + CANNetworkManager::CANNetwork.deactivate_control_function(testInternalControlFunction); + CANHardwareInterface::stop(); +} + +TEST(TIME_DATE_TESTS, MiscTests) +{ + // Test rejection of invalid parameters + TimeDateInterface timeDateInterfaceUnderTest; + + TimeDateInterface::TimeAndDate dataToSend; + + // Valid state + dataToSend.year = 2023; + dataToSend.month = 8; + dataToSend.day = 7; + dataToSend.quarterDays = 0; + dataToSend.hours = 22; + dataToSend.minutes = 49; + dataToSend.seconds = 41; + dataToSend.milliseconds = 0; + dataToSend.localHourOffset = -5; + dataToSend.localMinuteOffset = 0; + + // Test invalid year + dataToSend.year = 1984; + EXPECT_DEATH(timeDateInterfaceUnderTest.send_time_and_date(dataToSend), ""); + + // Test year high limit + dataToSend.year = 2236; + EXPECT_DEATH(timeDateInterfaceUnderTest.send_time_and_date(dataToSend), ""); + + // Test invalid month + dataToSend.year = 2023; + dataToSend.month = 0; + EXPECT_DEATH(timeDateInterfaceUnderTest.send_time_and_date(dataToSend), ""); + + // Test month high limit + dataToSend.month = 13; + EXPECT_DEATH(timeDateInterfaceUnderTest.send_time_and_date(dataToSend), ""); + + // Test invalid day + dataToSend.month = 8; + dataToSend.day = 90; + EXPECT_DEATH(timeDateInterfaceUnderTest.send_time_and_date(dataToSend), ""); + + // Test invalid quarter days + dataToSend.day = 7; + dataToSend.quarterDays = 4; + EXPECT_DEATH(timeDateInterfaceUnderTest.send_time_and_date(dataToSend), ""); + + // Test invalid hours + dataToSend.quarterDays = 0; + dataToSend.hours = 24; + EXPECT_DEATH(timeDateInterfaceUnderTest.send_time_and_date(dataToSend), ""); + + // Test invalid minutes + dataToSend.hours = 22; + dataToSend.minutes = 60; + EXPECT_DEATH(timeDateInterfaceUnderTest.send_time_and_date(dataToSend), ""); + + // Test invalid seconds + dataToSend.minutes = 49; + dataToSend.seconds = 60; + EXPECT_DEATH(timeDateInterfaceUnderTest.send_time_and_date(dataToSend), ""); + + // Test invalid milliseconds + dataToSend.seconds = 41; + dataToSend.milliseconds = 134; + EXPECT_DEATH(timeDateInterfaceUnderTest.send_time_and_date(dataToSend), ""); + + // Test invalid local hour offset + dataToSend.milliseconds = 0; + dataToSend.localHourOffset = -24; + EXPECT_DEATH(timeDateInterfaceUnderTest.send_time_and_date(dataToSend), ""); + + dataToSend.localHourOffset = 24; + EXPECT_DEATH(timeDateInterfaceUnderTest.send_time_and_date(dataToSend), ""); + + // Test invalid local minute offset + dataToSend.localHourOffset = -5; + dataToSend.localMinuteOffset = 60; + EXPECT_DEATH(timeDateInterfaceUnderTest.send_time_and_date(dataToSend), ""); + + dataToSend.localMinuteOffset = -60; + EXPECT_DEATH(timeDateInterfaceUnderTest.send_time_and_date(dataToSend), ""); +}