From 447f766a35dec56b43c55c5f1b6d4b87d68fc900 Mon Sep 17 00:00:00 2001 From: Adrian Del Grosso <10929341+ad3154@users.noreply.github.com> Date: Sat, 27 Jul 2024 13:23:08 -0600 Subject: [PATCH] [TC]: Add distance thresholds and handling of default process data --- .../section_control_implement_sim.cpp | 105 ++++ .../section_control_implement_sim.hpp | 24 +- examples/seeder_example/vt_application.cpp | 3 + .../isobus/can_network_configuration.hpp | 6 +- .../isobus/isobus_task_controller_client.hpp | 142 ++++- .../isobus_task_controller_client_objects.hpp | 2 +- isobus/src/isobus_task_controller_client.cpp | 488 +++++++++++++++--- test/tc_client_tests.cpp | 140 ++++- 8 files changed, 798 insertions(+), 112 deletions(-) diff --git a/examples/seeder_example/section_control_implement_sim.cpp b/examples/seeder_example/section_control_implement_sim.cpp index efd4ed4c..9e07999c 100644 --- a/examples/seeder_example/section_control_implement_sim.cpp +++ b/examples/seeder_example/section_control_implement_sim.cpp @@ -216,6 +216,111 @@ bool SectionControlImplementSimulator::create_ddop(std::shared_ptr(ImplementDDOPElementNumbers::BinElement): + { + switch (DDI) + { + case static_cast(isobus::DataDescriptionIndex::SetpointCountPerAreaApplicationRate): + { + returnedSettings.enableChangeThresholdTrigger = true; + returnedSettings.changeThreshold = 1; + retVal = true; + } + break; + + case static_cast(isobus::DataDescriptionIndex::MaximumCountContent): + case static_cast(isobus::DataDescriptionIndex::ActualCountContent): + case static_cast(isobus::DataDescriptionIndex::ActualCountPerAreaApplicationRate): + { + returnedSettings.enableChangeThresholdTrigger = true; + returnedSettings.enableTimeTrigger = true; + returnedSettings.changeThreshold = 1; + returnedSettings.timeTriggerInterval_ms = 1000; + retVal = true; + } + break; + + case static_cast(isobus::DataDescriptionIndex::PrescriptionControlState): + { + returnedSettings.enableChangeThresholdTrigger = true; + returnedSettings.enableTimeTrigger = true; + returnedSettings.changeThreshold = 1; + returnedSettings.timeTriggerInterval_ms = 5000; + retVal = true; + } + break; + + default: + { + } + break; + } + } + break; + + case static_cast(ImplementDDOPElementNumbers::BoomElement): + { + switch (DDI) + { + case static_cast(isobus::DataDescriptionIndex::ActualWorkingWidth): + case static_cast(isobus::DataDescriptionIndex::SetpointWorkState): + case static_cast(isobus::DataDescriptionIndex::ActualCondensedWorkState1_16): + { + returnedSettings.enableChangeThresholdTrigger = true; + returnedSettings.changeThreshold = 1; + retVal = true; + } + break; + + case static_cast(isobus::DataDescriptionIndex::SectionControlState): + { + returnedSettings.enableChangeThresholdTrigger = true; + returnedSettings.enableTimeTrigger = true; + returnedSettings.changeThreshold = 1; + returnedSettings.timeTriggerInterval_ms = 1000; + retVal = true; + } + break; + + default: + { + } + break; + } + } + break; + + case static_cast(ImplementDDOPElementNumbers::DeviceElement): + { + if (static_cast(isobus::DataDescriptionIndex::ActualWorkState) == DDI) + { + returnedSettings.enableChangeThresholdTrigger = true; + returnedSettings.changeThreshold = 1; + retVal = true; + } + } + break; + + default: + { + } + break; + } + } + return retVal; +} + bool SectionControlImplementSimulator::request_value_command_callback(std::uint16_t, std::uint16_t DDI, std::int32_t &value, diff --git a/examples/seeder_example/section_control_implement_sim.hpp b/examples/seeder_example/section_control_implement_sim.hpp index 53591e2f..c36ea3e2 100644 --- a/examples/seeder_example/section_control_implement_sim.hpp +++ b/examples/seeder_example/section_control_implement_sim.hpp @@ -11,7 +11,7 @@ #include "isobus/isobus/can_NAME.hpp" #include "isobus/isobus/can_message.hpp" -#include "isobus/isobus/isobus_device_descriptor_object_pool.hpp" +#include "isobus/isobus/isobus_task_controller_client.hpp" /// @brief Simulates a planter rate controller with section control /// @note This is just an example. A real rate controller will obviously need to control rate and section @@ -105,6 +105,17 @@ class SectionControlImplementSimulator CountPerAreaPresentation ///< Describes to the TC how to display volume per area units }; + /// @brief Enumerates the elements in the DDOP for easier reference in the application + enum class ImplementDDOPElementNumbers : std::uint16_t + { + DeviceElement = 0, + ConnectorElement = 1, + BoomElement = 2, + BinElement = 3, + Section1Element = 4, + SectionMaxElement = Section1Element + (MAX_NUMBER_SECTIONS_SUPPORTED - 1) + }; + /// @brief Constructor for the simulator /// @param[in] value The number of sections to track for section control explicit SectionControlImplementSimulator(std::uint8_t value); @@ -167,6 +178,17 @@ class SectionControlImplementSimulator /// @returns true if the DDOP was successfully created, otherwise false bool create_ddop(std::shared_ptr poolToPopulate, isobus::NAME clientName) const; + /// @brief Sets up default triggers for various elements in the DDOP when the TC requests it. + /// @param[in] elementNumber The element number to set up triggers for if applicable + /// @param[in] DDI The DDI to set up triggers for if applicable + /// @param[out] returnedSettings The settings to return to the TC + /// @param[in] parentPointer A pointer to the class instance this callback is for + /// @returns true if the triggers were set up successfully, otherwise false if no triggers needed to be configured + static bool default_process_data_request_callback(std::uint16_t elementNumber, + std::uint16_t DDI, + isobus::TaskControllerClient::DefaultProcessDataSettings &returnedSettings, + void *parentPointer); + /// @brief A callback that will be used by the TC client to read values /// @param[in] elementNumber The element number associated to the value being requested /// @param[in] DDI The ddi of the value in the element being requested diff --git a/examples/seeder_example/vt_application.cpp b/examples/seeder_example/vt_application.cpp index 946763fc..a91de5f6 100644 --- a/examples/seeder_example/vt_application.cpp +++ b/examples/seeder_example/vt_application.cpp @@ -106,6 +106,7 @@ bool SeederVtApplication::initialize() TCClientInterface.configure(ddop, 1, 255, 255, true, true, true, false, true); TCClientInterface.add_request_value_callback(SectionControlImplementSimulator::request_value_command_callback, §ionControl); TCClientInterface.add_value_command_callback(SectionControlImplementSimulator::value_command_callback, §ionControl); + TCClientInterface.add_default_process_data_requested_callback(SectionControlImplementSimulator::default_process_data_request_callback, §ionControl); TCClientInterface.initialize(true); } else @@ -184,6 +185,8 @@ void SeederVtApplication::handle_vt_key_events(const isobus::VirtualTerminalClie { update_section_objects(i); } + TCClientInterface.on_value_changed_trigger(static_cast(SectionControlImplementSimulator::ImplementDDOPElementNumbers::BoomElement), + static_cast(isobus::DataDescriptionIndex::RequestDefaultProcessData)); } break; diff --git a/isobus/include/isobus/isobus/can_network_configuration.hpp b/isobus/include/isobus/isobus/can_network_configuration.hpp index 0d137812..3ac49675 100644 --- a/isobus/include/isobus/isobus/can_network_configuration.hpp +++ b/isobus/include/isobus/isobus/can_network_configuration.hpp @@ -40,7 +40,7 @@ namespace isobus /// @brief Sets the minimum time to wait between sending BAM frames /// (default is 50 ms for maximum J1939 compatibility) /// @details The acceptable range as defined by ISO-11783 is 10 to 200 ms. - /// This is a minumum time, so if you set it to some value, like 10 ms, the + /// This is a minimum time, so if you set it to some value, like 10 ms, the /// stack will attempt to transmit it as close to that time as it can, but it is /// not possible to 100% ensure it. /// @param[in] value The minimum time to wait between sending BAM frames @@ -74,13 +74,13 @@ namespace isobus /// @returns The max number of frames to use in transport protocols in each network manager update std::uint8_t get_max_number_of_network_manager_protocol_frames_per_update() const; - /// @brief Set the the number of packets per CTS message for TP sessions. The default + /// @brief Set the number of packets per CTS message for TP sessions. The default /// is 16. Note that the receiving control function may not support this limitation, or choose /// to ignore it and use a different number of packets per CTS packet. /// @param[in] numberPackets The number of packets per CTS packet for TP sessions. void set_number_of_packets_per_cts_message(std::uint8_t numberPackets); - /// @brief Get the the number of packets per CTS packet for TP sessions. + /// @brief Get the number of packets per CTS packet for TP sessions. /// @returns The number of packets per CTS packet for TP sessions. std::uint8_t get_number_of_packets_per_cts_message() const; diff --git a/isobus/include/isobus/isobus/isobus_task_controller_client.hpp b/isobus/include/isobus/isobus/isobus_task_controller_client.hpp index 926cce27..43234556 100644 --- a/isobus/include/isobus/isobus/isobus_task_controller_client.hpp +++ b/isobus/include/isobus/isobus/isobus_task_controller_client.hpp @@ -32,7 +32,7 @@ namespace isobus Disconnected, ///< Not communicating with the TC WaitForStartUpDelay, ///< Client is waiting for the mandatory 6s startup delay WaitForServerStatusMessage, ///< Client is waiting to identify the TC via reception of a valid status message - SendWorkingSetMaster, ///< Client initating communication with TC by sending the working set master message + SendWorkingSetMaster, ///< Client initiating communication with TC by sending the working set master message SendStatusMessage, ///< Enables sending the status message RequestVersion, ///< Requests the TC version and related data from the TC WaitForRequestVersionResponse, ///< Waiting for the TC to respond to a request for its version @@ -64,7 +64,7 @@ namespace isobus { DraftInternationalStandard = 0, ///< The version of the DIS (draft International Standard). FinalDraftInternationalStandardFirstEdition = 1, ///< The version of the FDIS.1 (final draft International Standard, first edition). - FirstPublishedEdition = 2, ///< The version of the FDIS.2 and the first edition published ss an International Standard. + FirstPublishedEdition = 2, ///< The version of the FDIS.2 and the first edition published as an International Standard. SecondEditionDraft = 3, ///< The version of the second edition published as a draft International Standard(E2.DIS). SecondPublishedEdition = 4, ///< The version of the second edition published as the final draft International Standard(E2.FDIS) and as the International Standard(E2.IS) Unknown = 0xFF @@ -83,12 +83,38 @@ namespace isobus ReservedOption3 = 0x80 }; + /// @brief Used to describe the triggers to set up by default when the + /// the TC server requests the default process data from the client. + struct DefaultProcessDataSettings + { + std::int32_t timeTriggerInterval_ms = 0; ///< The time interval for sending the data element specified by the data dictionary identifier. + std::int32_t distanceTriggerInterval_mm = 0; ///< The distance interval for sending the data element specified by the data dictionary identifier. + std::int32_t minimumWithinThreshold = 0; ///< The value of this data element is sent to the TC or DL when the value is higher than the threshold value. + std::int32_t maximumWithinThreshold = 0; ///< The value of this data element is sent to the TC or DL when the value is lower than the threshold value. + std::int32_t changeThreshold = 0; ///< The value of this data element is sent to the TC or DL when the value change is higher than or equal to the change threshold since last transmission. + bool enableTimeTrigger = false; ///< Enable the time trigger + bool enableDistanceTrigger = false; ///< Enable the distance trigger + bool enableMinimumWithinThresholdTrigger = false; ///< Enable the minimum within threshold trigger + bool enableMaximumWithinThresholdTrigger = false; ///< Enable the maximum within threshold trigger + bool enableChangeThresholdTrigger = false; ///< Enable the change threshold trigger + }; + /// @brief A callback for handling a value request command from the TC using RequestValueCommandCallback = bool (*)(std::uint16_t elementNumber, std::uint16_t DDI, std::int32_t &processVariableValue, void *parentPointer); + /// @brief A callback for handling a default process data request from the TC. + /// This callback is used to set up the default process data settings for a process data variable + /// when the TC requests the default process data from the client. When this callback is called, you should + /// edit the content of the `returnedSettings` parameter to set up the triggers you want to use by default for + /// this process data variable. + using DefaultProcessDataRequestedCallback = bool (*)(std::uint16_t elementNumber, + std::uint16_t DDI, + DefaultProcessDataSettings &returnedSettings, + void *parentPointer); + /// @brief A callback for handling a set value command from the TC using ValueCommandCallback = bool (*)(std::uint16_t elementNumber, std::uint16_t DDI, @@ -110,6 +136,16 @@ namespace isobus /// by calling the `update` function. void initialize(bool spawnThread); + /// @brief This function adds a callback that will be called when the TC requests the default process data from the client. + /// When starting a task, the task controller will often send a request for the default process data from the client. + /// When the stack receives those messages, it will call each callback you've added add with this function until one returns true. + /// When a callback returns true, the stack will use the settings provided by that callback to set up the triggers for the process data variable. + /// The stack will then send the process data to the TC, and set up the triggers for the process data variable as requested by the callback. + /// @note The TC may overwrite your desired trigger settings if it wants to. The values you set here are just defaults. + /// @param[in] callback The callback to add + /// @param[in] parentPointer A generic context variable that will be passed into the associated callback when it gets called + void add_default_process_data_requested_callback(DefaultProcessDataRequestedCallback callback, void *parentPointer); + /// @brief This adds a callback that will be called when the TC requests the value of one of your variables. /// @details The task controller will often send a request for the value of a process data variable. /// When the stack receives those messages, it will call this callback to request the value from your @@ -127,6 +163,11 @@ namespace isobus /// @param[in] parentPointer A generic context variable that will be passed into the associated callback when it gets called void add_value_command_callback(ValueCommandCallback callback, void *parentPointer); + /// @brief Removes the specified callback from the list of default process data requested callbacks + /// @param[in] callback The callback to remove + /// @param[in] parentPointer parent pointer associated to the callback being removed + void remove_default_process_data_requested_callback(DefaultProcessDataRequestedCallback callback, void *parentPointer); + /// @brief Removes the specified callback from the list of value request callbacks /// @param[in] callback The callback to remove /// @param[in] parentPointer parent pointer associated to the callback being removed @@ -265,7 +306,7 @@ namespace isobus bool get_is_initialized() const; /// @brief Check whether the client is connected to the TC server - /// @returns true if cconnected, false otherwise + /// @returns true if connected, false otherwise bool get_is_connected() const; /// @brief Returns if a task is active as indicated by the TC @@ -312,39 +353,45 @@ namespace isobus /// @param[in] DDI The DDI of the process data variable that changed void on_value_changed_trigger(std::uint16_t elementNumber, std::uint16_t DDI); - /// @brief Sends a broadcast request to TCs to identify themseleves. + /// @brief Sends a broadcast request to TCs to identify themselves. /// @details Upon receipt of this message, the TC shall display, for a period of 3 s, the TC Number /// @returns `true` if the message was sent, otherwise `false` bool request_task_controller_identification() const; /// @brief If the TC client is connected to a TC, calling this function will - /// cause the TC client interface to delete the currently active DDOP, reupload it, + /// cause the TC client interface to delete the currently active DDOP, re-upload it, /// then reactivate it using the pool passed into the parameter of this function. /// This process is faster than restarting the whole interface, and you have to /// call it if you change certain things in your DDOP at runtime after the DDOP has already been activated. /// @param[in] binaryDDOP The updated device descriptor object pool to upload to the TC - /// @returns true if the interface accepted the command to reupload the pool, or false if the command cannot be handled right now + /// @returns true if the interface accepted the command to re-upload the pool, or false if the command cannot be handled right now bool reupload_device_descriptor_object_pool(std::shared_ptr> binaryDDOP); /// @brief If the TC client is connected to a TC, calling this function will - /// cause the TC client interface to delete the currently active DDOP, reupload it, + /// cause the TC client interface to delete the currently active DDOP, re-upload it, /// then reactivate it using the pool passed into the parameter of this function. /// This process is faster than restarting the whole interface, and you have to /// call it if you change certain things in your DDOP at runtime after the DDOP has already been activated. /// @param[in] binaryDDOP The updated device descriptor object pool to upload to the TC /// @param[in] DDOPSize The number of bytes in the binary DDOP that will be uploaded - /// @returns true if the interface accepted the command to reupload the pool, or false if the command cannot be handled right now + /// @returns true if the interface accepted the command to re-upload the pool, or false if the command cannot be handled right now bool reupload_device_descriptor_object_pool(std::uint8_t const *binaryDDOP, std::uint32_t DDOPSize); /// @brief If the TC client is connected to a TC, calling this function will - /// cause the TC client interface to delete the currently active DDOP, reupload it, + /// cause the TC client interface to delete the currently active DDOP, re-upload it, /// then reactivate it using the pool passed into the parameter of this function. /// This process is faster than restarting the whole interface, and you have to /// call it if you change certain things in your DDOP at runtime after the DDOP has already been activated. /// @param[in] DDOP The updated device descriptor object pool to upload to the TC - /// @returns true if the interface accepted the command to reupload the pool, or false if the command cannot be handled right now + /// @returns true if the interface accepted the command to re-upload the pool, or false if the command cannot be handled right now bool reupload_device_descriptor_object_pool(std::shared_ptr DDOP); + /// @brief If your application has any distance triggers set up in the DDOP, you can call this function + /// to update the distance that the TC client uses to determine if it should send a process data value. + /// This should be the total distance driven by the vehicle since the application started. Not the difference between the last call and this call! + /// @param[in] distance The total, absolute distance in millimeters the vehicle has driven + void set_distance(std::uint32_t distance); + /// @brief The cyclic update function for this interface. /// @note This function may be called by the TC worker thread if you called /// initialize with a parameter of `true`, otherwise you must call it @@ -368,7 +415,7 @@ namespace isobus MeasurementMaximumWithinThreshold = 0x07, ///< The client has to send the value of this data element to the TC or DL when the value is lower than the threshold value. MeasurementChangeThreshold = 0x08, ///< The client has to send the value of this data element to the TC or DL when the value change is higher than or equal to the change threshold since last transmission. PeerControlAssignment = 0x09, ///< This message is used to establish a connection between a setpoint value source and a setpoint value user - SetValueAndAcknowledge = 0x0A, ///< This command is used to set the value of a process data entity and request a reception acknowledgement from the recipient + SetValueAndAcknowledge = 0x0A, ///< This command is used to set the value of a process data entity and request a reception acknowledgment from the recipient Reserved1 = 0x0B, ///< Reserved. Reserved2 = 0x0C, ///< Reserved. ProcessDataAcknowledge = 0x0D, ///< Message is a Process Data Acknowledge (PDACK). @@ -403,9 +450,24 @@ namespace isobus ChangeDesignatorResponse = 0x0D ///< Sent in response to Change Designator message }; + /// @brief Stores data related to requests and commands from the TC + struct ProcessDataCallbackInfo + { + /// @brief Allows easy comparison of callback data + /// @param obj the object to compare against + /// @returns true if the ddi and element numbers of the provided objects match, otherwise false + bool operator==(const ProcessDataCallbackInfo &obj) const; + std::int32_t processDataValue; ///< The value of the value set command + std::int32_t lastValue; ///< Used for measurement commands to store timestamp or previous values + std::uint16_t elementNumber; ///< The element number for the command + std::uint16_t ddi; ///< The DDI for the command + bool ackRequested; ///< Stores if the TC used the mux that also requires a PDACK + bool thresholdPassed; ///< Used when the structure is being used to track measurement command thresholds to know if the threshold has been passed + }; + /// @brief The data callback passed to the network manger's send function for the transport layer messages /// @details We upload the data with callbacks to avoid making yet another complete copy of the pool to - /// accommodate the multiplexor that needs to get passed to the transport layer message's first byte. + /// accommodate the multiplexer that needs to get passed to the transport layer message's first byte. /// @param[in] callbackIndex The number of times the callback has been called /// @param[in] bytesOffset The byte offset at which to get pool data /// @param[in] numberOfBytesNeeded The number of bytes the protocol needs to send another frame (usually 7) @@ -418,6 +480,26 @@ namespace isobus std::uint8_t *chunkBuffer, void *parentPointer); + /// @brief Adds a measurement change threshold to the queue of maintained triggers, checks for duplicates. + /// @param[in] info The information to add to the queue + void add_measurement_change_threshold(ProcessDataCallbackInfo &info); + + /// @brief Adds a measurement distance interval to the queue of maintained triggers, checks for duplicates. + /// @param[in] info The information to add to the queue + void add_measurement_distance_interval(ProcessDataCallbackInfo &info); + + /// @brief Adds a measurement time interval to the queue of maintained triggers, checks for duplicates. + /// @param[in] info The information to add to the queue + void add_measurement_time_interval(ProcessDataCallbackInfo &info); + + /// @brief Adds a measurement max threshold to the queue of maintained triggers, checks for duplicates. + /// @param[in] info The information to add to the queue + void add_measurement_maximum_threshold(ProcessDataCallbackInfo &info); + + /// @brief Adds a measurement minimum threshold to the queue of maintained triggers, checks for duplicates. + /// @param[in] info The information to add to the queue + void add_measurement_minimum_threshold(ProcessDataCallbackInfo &info); + /// @brief Clears all queued TC commands and responses void clear_queues(); @@ -425,6 +507,16 @@ namespace isobus /// @returns true if a DDOP was provided, otherwise false bool get_was_ddop_supplied() const; + /// @brief Sets up triggers for a process data variable based on the default process data settings + /// @param[in] processDataObject The process data variable, used to validate trigger compatibility with the DDOP + /// @param[in] elementNumber The element number of the process data variable + /// @param[in] DDI The DDI of the process data variable + /// @param[in] settings The default process data settings to use for setting up the triggers + void populate_any_triggers_from_settings(std::shared_ptr processDataObject, + std::uint16_t elementNumber, + std::uint16_t DDI, + const DefaultProcessDataSettings &settings); + /// @brief Searches the DDOP for a device object and stores that object's structure and localization labels void process_labels_from_ddop(); @@ -462,9 +554,9 @@ namespace isobus /// @brief Sends a process data message with 1 mux byte and all 0xFFs as payload /// @details This just reduces code duplication by consolidating common message formats - /// @param[in] multiplexor The multiplexor to use for the message + /// @param[in] multiplexer The multiplexer to use for the message /// @returns `true` if the message was sent, otherwise `false` - bool send_generic_process_data(std::uint8_t multiplexor) const; + bool send_generic_process_data(std::uint8_t multiplexer) const; /// @brief Sends the activate object pool message /// @details This message is sent by a client to complete its connection procedure to a TC @@ -568,22 +660,18 @@ namespace isobus static constexpr std::uint16_t TWO_SECOND_TIMEOUT_MS = 2000; ///< Used for sending the status message to the TC private: - /// @brief Stores data related to requests and commands from the TC - struct ProcessDataCallbackInfo + /// @brief Stores a default process data request callback along with its parent pointer + struct DefaultProcessDataRequestCallbackInfo { /// @brief Allows easy comparison of callback data /// @param obj the object to compare against - /// @returns true if the ddi and element numbers of the provided objects match, otherwise false - bool operator==(const ProcessDataCallbackInfo &obj) const; - std::int32_t processDataValue; ///< The value of the value set command - std::int32_t lastValue; ///< Used for measurement commands to store timestamp or previous values - std::uint16_t elementNumber; ///< The element number for the command - std::uint16_t ddi; ///< The DDI for the command - bool ackRequested; ///< Stores if the TC used the mux that also requires a PDACK - bool thresholdPassed; ///< Used when the structure is being used to track measurement command thresholds to know if the threshold has been passed + /// @returns true if the callback and parent pointer match, otherwise false + bool operator==(const DefaultProcessDataRequestCallbackInfo &obj) const; + DefaultProcessDataRequestedCallback callback; ///< The callback itself + void *parent; ///< The parent pointer, generic context value }; - /// @brief Stores a TC value command callback along with its parent pointer + /// @brief Stores a TC request value command callback along with its parent pointer struct RequestValueCommandCallbackInfo { /// @brief Allows easy comparison of callback data @@ -620,10 +708,12 @@ namespace isobus std::uint8_t const *userSuppliedBinaryDDOP = nullptr; ///< Stores a client-provided DDOP if one was provided std::shared_ptr> userSuppliedVectorDDOP; ///< Stores a client-provided DDOP if one was provided std::vector generatedBinaryDDOP; ///< Stores the DDOP in binary form after it has been generated + std::vector defaultProcessDataRequestedCallbacks; ///< A list of callbacks that will be called when the TC requests a process data value std::vector requestValueCallbacks; ///< A list of callbacks that will be called when the TC requests a process data value std::vector valueCommandsCallbacks; ///< A list of callbacks that will be called when the TC sets a process data value std::list queuedValueRequests; ///< A list of queued value requests that will be processed on the next update std::list queuedValueCommands; ///< A list of queued value commands that will be processed on the next update + std::list measurementDistanceIntervalCommands; ///< A list of measurement commands that will be processed on a distance interval std::list measurementTimeIntervalCommands; ///< A list of measurement commands that will be processed on a time interval std::list measurementMinimumThresholdCommands; ///< A list of measurement commands that will be processed when the value drops below a threshold std::list measurementMaximumThresholdCommands; ///< A list of measurement commands that will be processed when the value above a threshold @@ -642,6 +732,7 @@ namespace isobus std::uint32_t serverStatusMessageTimestamp_ms = 0; ///< Timestamp corresponding to the last time we received a status message from the TC std::uint32_t userSuppliedBinaryDDOPSize_bytes = 0; ///< The number of bytes in the user provided binary DDOP (if one was provided) std::uint32_t languageCommandWaitingTimestamp_ms = 0; ///< Timestamp used to determine when to give up on waiting for a language command response + std::uint32_t totalMachineDistance = 0; ///< The total distance the machine has traveled since the application started. Used for distance interval triggers. std::uint8_t numberOfWorkingSetMembers = 1; ///< The number of working set members that will be reported in the working set master message std::uint8_t tcStatusBitfield = 0; ///< The last received TC/DL status from the status message std::uint8_t sourceAddressOfCommandBeingExecuted = 0; ///< Source address of client for which the current command is being executed @@ -665,6 +756,7 @@ namespace isobus bool supportsPeerControlAssignment = false; ///< Determines if the client reports peer control assignment capability to the TC bool supportsImplementSectionControl = false; ///< Determines if the client reports implement section control capability to the TC bool shouldReuploadAfterDDOPDeletion = false; ///< Used to determine how the state machine should progress when updating a DDOP + bool shouldProcessAllDefaultProcessDataRequests = false; ///< Determines if the client should process all default process data requests. Used to offload from the CAN stack's thread if possible. }; } // namespace isobus diff --git a/isobus/include/isobus/isobus/isobus_task_controller_client_objects.hpp b/isobus/include/isobus/isobus/isobus_task_controller_client_objects.hpp index fde28a90..68d9df8d 100644 --- a/isobus/include/isobus/isobus/isobus_task_controller_client_objects.hpp +++ b/isobus/include/isobus/isobus/isobus_task_controller_client_objects.hpp @@ -306,7 +306,7 @@ namespace isobus }; /// @brief Enumerates the trigger methods that can be set in the available trigger bitset of this object - enum class AvailableTriggerMethods + enum class AvailableTriggerMethods : std::uint8_t { TimeInterval = 0x01, ///< The device can provide these device process data based on a time interval DistanceInterval = 0x02, ///< The device can provide these device process data based on a distance interval. diff --git a/isobus/src/isobus_task_controller_client.cpp b/isobus/src/isobus_task_controller_client.cpp index 3fea3f6e..8531f671 100644 --- a/isobus/src/isobus_task_controller_client.cpp +++ b/isobus/src/isobus_task_controller_client.cpp @@ -11,6 +11,7 @@ #include "isobus/isobus/can_general_parameter_group_numbers.hpp" #include "isobus/isobus/can_network_manager.hpp" #include "isobus/isobus/can_stack_logger.hpp" +#include "isobus/isobus/isobus_standard_data_description_indices.hpp" #include "isobus/utility/system_timing.hpp" #include "isobus/utility/to_string.hpp" @@ -70,6 +71,14 @@ namespace isobus } } + void TaskControllerClient::add_default_process_data_requested_callback(DefaultProcessDataRequestedCallback callback, void *parentPointer) + { + LOCK_GUARD(Mutex, clientMutex); + + DefaultProcessDataRequestCallbackInfo callbackData = { callback, parentPointer }; + defaultProcessDataRequestedCallbacks.push_back(callbackData); + } + void TaskControllerClient::add_request_value_callback(RequestValueCommandCallback callback, void *parentPointer) { LOCK_GUARD(Mutex, clientMutex); @@ -86,6 +95,19 @@ namespace isobus valueCommandsCallbacks.push_back(callbackData); } + void TaskControllerClient::remove_default_process_data_requested_callback(DefaultProcessDataRequestedCallback callback, void *parentPointer) + { + LOCK_GUARD(Mutex, clientMutex); + + DefaultProcessDataRequestCallbackInfo callbackData = { callback, parentPointer }; + auto callbackLocation = std::find(defaultProcessDataRequestedCallbacks.begin(), defaultProcessDataRequestedCallbacks.end(), callbackData); + + if (defaultProcessDataRequestedCallbacks.end() != callbackLocation) + { + defaultProcessDataRequestedCallbacks.erase(callbackLocation); + } + } + void TaskControllerClient::remove_request_value_callback(RequestValueCommandCallback callback, void *parentPointer) { LOCK_GUARD(Mutex, clientMutex); @@ -396,8 +418,109 @@ namespace isobus return retVal; } + void TaskControllerClient::set_distance(std::uint32_t distance) + { + if (distance != totalMachineDistance) + { + LOCK_GUARD(Mutex, clientMutex); + + totalMachineDistance = distance; + + // Check all defined distance triggers to see if we need to send a value to the TC + for (auto &distanceTrigger : measurementDistanceIntervalCommands) + { + if (totalMachineDistance >= (distanceTrigger.lastValue + distanceTrigger.processDataValue)) + { + ProcessDataCallbackInfo requestData = { 0, 0, 0, 0, false, false }; + + requestData.elementNumber = distanceTrigger.elementNumber; + requestData.ddi = distanceTrigger.ddi; + queuedValueRequests.push_back(requestData); + distanceTrigger.lastValue = totalMachineDistance; + } + } + } + } + void TaskControllerClient::update() { + DeviceDescriptorObjectPool *pool = nullptr; + + // Check if we need to check all default process data requests before proceeding to queue/state processing. + if (shouldProcessAllDefaultProcessDataRequests) + { + shouldProcessAllDefaultProcessDataRequests = false; + + if (nullptr != clientDDOP) + { + // Already have a DDOP, so we can process the requests. + pool = clientDDOP.get(); + } + else + { + // Need to parse the DDOP to know what the default process data is. This could fail if the DDOP is invalid + // so if it fails, you need to check your DDOP for issues and the log for [DDOP] events! + DeviceDescriptorObjectPool tempPool; + bool poolValid = false; + + if (nullptr != userSuppliedVectorDDOP) + { + poolValid = tempPool.deserialize_binary_object_pool(userSuppliedVectorDDOP->data(), userSuppliedVectorDDOP->size(), myControlFunction->get_NAME()); + } + else if (nullptr != userSuppliedBinaryDDOP) + { + poolValid = tempPool.deserialize_binary_object_pool(userSuppliedBinaryDDOP, userSuppliedBinaryDDOPSize_bytes, myControlFunction->get_NAME()); + } + + if (poolValid) + { + pool = &tempPool; + } + else + { + LOG_ERROR("[TC]: Cannot proceed with default process data request without a valid DDOP. Check log for [DDOP] events and fix any logged issues in your DDOP."); + } + } + + if (nullptr != pool) + { + for (std::size_t i = 0; i < pool->size(); i++) + { + auto object = pool->get_object_by_index(i); + + // Find every element, then find every DPD under it. For every DPD, check if it is a member of the default set + // If it is, then we need to query for the trigger settings and send the initial value to the TC. + + if (task_controller_object::ObjectTypes::DeviceElement == object->get_object_type()) + { + auto deviceElement = std::static_pointer_cast(object); + + for (std::size_t j = 0; j < deviceElement->get_number_child_objects(); j++) + { + std::uint16_t childID = deviceElement->get_child_object_id(j); + auto childObject = pool->get_object_by_id(childID); + + if ((nullptr != childObject) && + (task_controller_object::ObjectTypes::DeviceProcessData == childObject->get_object_type())) + { + auto processData = std::static_pointer_cast(childObject); + DefaultProcessDataSettings settings; + + for (const auto &callback : defaultProcessDataRequestedCallbacks) + { + if (callback.callback(deviceElement->get_element_number(), processData->get_ddi(), settings, callback.parent)) + { + populate_any_triggers_from_settings(processData, deviceElement->get_element_number(), processData->get_ddi(), settings); + break; + } + } + } + } + } + } + } + } + switch (currentState) { case StateMachineState::Disconnected: @@ -871,6 +994,11 @@ namespace isobus } } + bool TaskControllerClient::DefaultProcessDataRequestCallbackInfo::operator==(const DefaultProcessDataRequestCallbackInfo &obj) const + { + return ((obj.callback == this->callback) && (obj.parent == this->parent)); + } + bool TaskControllerClient::ProcessDataCallbackInfo::operator==(const ProcessDataCallbackInfo &obj) const { return ((obj.ddi == this->ddi) && (obj.elementNumber == this->elementNumber)); @@ -886,6 +1014,145 @@ namespace isobus return (obj.callback == this->callback) && (obj.parent == this->parent); } + void TaskControllerClient::add_measurement_change_threshold(ProcessDataCallbackInfo &info) + { + auto previousCommand = std::find(measurementOnChangeThresholdCommands.begin(), measurementOnChangeThresholdCommands.end(), info); + if (measurementOnChangeThresholdCommands.end() == previousCommand) + { + measurementOnChangeThresholdCommands.push_back(info); + LOG_DEBUG("[TC]: New change threshold trigger. Element: " + + isobus::to_string(static_cast(info.elementNumber)) + + " DDI: " + + isobus::to_string(static_cast(info.ddi)) + + " on change by at least: " + + isobus::to_string(static_cast(info.processDataValue))); + } + else + { + // Just update the existing one with the new value + previousCommand->processDataValue = info.processDataValue; + previousCommand->thresholdPassed = false; + LOG_DEBUG("[TC]: Altered change threshold trigger. Element: " + + isobus::to_string(static_cast(info.elementNumber)) + + " DDI: " + + isobus::to_string(static_cast(info.ddi)) + + " new threshold: " + + isobus::to_string(static_cast(info.processDataValue))); + } + } + + void TaskControllerClient::add_measurement_distance_interval(ProcessDataCallbackInfo &info) + { + auto previousCommand = std::find(measurementDistanceIntervalCommands.begin(), measurementDistanceIntervalCommands.end(), info); + if (measurementDistanceIntervalCommands.end() == previousCommand) + { + measurementDistanceIntervalCommands.push_back(info); + LOG_DEBUG("[TC]: New distance interval trigger. Element: " + + isobus::to_string(static_cast(info.elementNumber)) + + " DDI: " + + isobus::to_string(static_cast(info.ddi)) + + " every: " + + isobus::to_string(static_cast(info.processDataValue)) + + " mm."); + } + else + { + // Use the existing one and update the value + previousCommand->processDataValue = info.processDataValue; + LOG_DEBUG("[TC]: Altered distance interval trigger for element: " + + isobus::to_string(static_cast(info.elementNumber)) + + " DDI: " + + isobus::to_string(static_cast(info.ddi)) + + " every: " + + isobus::to_string(static_cast(info.processDataValue)) + + " mm."); + } + } + + void TaskControllerClient::add_measurement_time_interval(ProcessDataCallbackInfo &info) + { + auto previousCommand = std::find(measurementTimeIntervalCommands.begin(), measurementTimeIntervalCommands.end(), info); + if (measurementTimeIntervalCommands.end() == previousCommand) + { + measurementTimeIntervalCommands.push_back(info); + LOG_DEBUG("[TC]: New time interval trigger. Element: " + + isobus::to_string(static_cast(info.elementNumber)) + + " DDI: " + + isobus::to_string(static_cast(info.ddi)) + + " every: " + + isobus::to_string(static_cast(info.processDataValue)) + + " milliseconds."); + } + else + { + // Use the existing one and update the value + previousCommand->processDataValue = info.processDataValue; + LOG_DEBUG("[TC]: Altered time interval trigger for element: " + + isobus::to_string(static_cast(info.elementNumber)) + + " DDI: " + + isobus::to_string(static_cast(info.ddi)) + + " every: " + + isobus::to_string(static_cast(info.processDataValue)) + + " milliseconds."); + } + } + + void TaskControllerClient::add_measurement_maximum_threshold(ProcessDataCallbackInfo &info) + { + auto previousCommand = std::find(measurementMaximumThresholdCommands.begin(), measurementMaximumThresholdCommands.end(), info); + if (measurementMaximumThresholdCommands.end() == previousCommand) + { + measurementMaximumThresholdCommands.push_back(info); + LOG_DEBUG("[TC]: New maximum measurement threshold trigger. Element: " + + isobus::to_string(static_cast(info.elementNumber)) + + " DDI: " + + isobus::to_string(static_cast(info.ddi)) + + " when it is above the raw value: " + + isobus::to_string(static_cast(info.processDataValue))); + } + else + { + // Just update the existing one with the new value + previousCommand->processDataValue = info.processDataValue; + previousCommand->thresholdPassed = false; + + LOG_DEBUG("[TC]: Altered maximum threshold trigger for element: " + + isobus::to_string(static_cast(info.elementNumber)) + + " DDI: " + + isobus::to_string(static_cast(info.ddi)) + + " threshold: " + + isobus::to_string(static_cast(info.processDataValue))); + } + } + + void TaskControllerClient::add_measurement_minimum_threshold(ProcessDataCallbackInfo &info) + { + auto previousCommand = std::find(measurementMinimumThresholdCommands.begin(), measurementMinimumThresholdCommands.end(), info); + if (measurementMinimumThresholdCommands.end() == previousCommand) + { + measurementMinimumThresholdCommands.push_back(info); + LOG_DEBUG("[TC]: New minimum measurement threshold trigger. Element: " + + isobus::to_string(static_cast(info.elementNumber)) + + " DDI: " + + isobus::to_string(static_cast(info.ddi)) + + " when it is below the raw value: " + + isobus::to_string(static_cast(info.processDataValue))); + } + else + { + // Just update the existing one with the new value + previousCommand->processDataValue = info.processDataValue; + previousCommand->thresholdPassed = false; + + LOG_DEBUG("[TC]: Altered minimum threshold trigger for element: " + + isobus::to_string(static_cast(info.elementNumber)) + + " DDI: " + + isobus::to_string(static_cast(info.ddi)) + + " threshold: " + + isobus::to_string(static_cast(info.processDataValue))); + } + } + void TaskControllerClient::clear_queues() { queuedValueRequests.clear(); @@ -894,6 +1161,7 @@ namespace isobus measurementMinimumThresholdCommands.clear(); measurementMaximumThresholdCommands.clear(); measurementOnChangeThresholdCommands.clear(); + measurementDistanceIntervalCommands.clear(); } bool TaskControllerClient::get_was_ddop_supplied() const @@ -928,6 +1196,98 @@ namespace isobus return retVal; } + void TaskControllerClient::populate_any_triggers_from_settings(std::shared_ptr processDataObject, std::uint16_t elementNumber, std::uint16_t DDI, const DefaultProcessDataSettings &settings) + { + if (nullptr != processDataObject) + { + if (settings.enableChangeThresholdTrigger) + { + if (0 != (processDataObject->get_trigger_methods_bitfield() & static_cast(task_controller_object::DeviceProcessDataObject::AvailableTriggerMethods::ThresholdLimits))) + { + ProcessDataCallbackInfo triggerData = { 0, 0, 0, 0, false, false }; + + triggerData.elementNumber = elementNumber; + triggerData.ddi = DDI; + triggerData.processDataValue = settings.changeThreshold; + add_measurement_change_threshold(triggerData); + } + else + { + LOG_ERROR("[TC]: You have enabled a change threshold trigger, but your DDOP does not define it for this object! Element: %d, DDI: %d. You need to edit your DDOP, or remove this trigger.", elementNumber, DDI); + } + } + if (settings.enableDistanceTrigger) + { + if (0 != (processDataObject->get_trigger_methods_bitfield() & static_cast(task_controller_object::DeviceProcessDataObject::AvailableTriggerMethods::DistanceInterval))) + { + ProcessDataCallbackInfo triggerData = { 0, 0, 0, 0, false, false }; + + // Distance triggers are weird because distance needs to be handled by the consuming application, not this interface. + // We track it anyways so that the value can be sent when the trigger is set at least. + triggerData.elementNumber = elementNumber; + triggerData.ddi = DDI; + triggerData.processDataValue = settings.distanceTriggerInterval_mm; + add_measurement_distance_interval(triggerData); + } + else + { + LOG_ERROR("[TC]: You have enabled a distance interval trigger, but your DDOP does not define it for this object! Element: %d, DDI: %d. You need to edit your DDOP, or remove this trigger.", elementNumber, DDI); + } + } + if (settings.enableMaximumWithinThresholdTrigger) + { + if (0 != (processDataObject->get_trigger_methods_bitfield() & static_cast(task_controller_object::DeviceProcessDataObject::AvailableTriggerMethods::ThresholdLimits))) + { + ProcessDataCallbackInfo triggerData = { 0, 0, 0, 0, false, false }; + + triggerData.elementNumber = elementNumber; + triggerData.ddi = DDI; + triggerData.processDataValue = settings.maximumWithinThreshold; + add_measurement_maximum_threshold(triggerData); + } + else + { + LOG_ERROR("[TC]: You have enabled a maximum threshold limit trigger, but your DDOP does not define it for this object! Element: %d, DDI: %d. You need to edit your DDOP, or remove this trigger.", elementNumber, DDI); + } + } + + if (settings.enableMinimumWithinThresholdTrigger) + { + if (0 != (processDataObject->get_trigger_methods_bitfield() & static_cast(task_controller_object::DeviceProcessDataObject::AvailableTriggerMethods::ThresholdLimits))) + { + ProcessDataCallbackInfo triggerData = { 0, 0, 0, 0, false, false }; + + triggerData.elementNumber = elementNumber; + triggerData.ddi = DDI; + triggerData.processDataValue = settings.enableMinimumWithinThresholdTrigger; + add_measurement_minimum_threshold(triggerData); + } + else + { + LOG_ERROR("[TC]: You have enabled a minimum threshold limit trigger, but your DDOP does not define it for this object! Element: %d, DDI: %d. You need to edit your DDOP, or remove this trigger.", elementNumber, DDI); + } + } + + if (settings.enableTimeTrigger) + { + if (0 != (processDataObject->get_trigger_methods_bitfield() & static_cast(task_controller_object::DeviceProcessDataObject::AvailableTriggerMethods::TimeInterval))) + { + ProcessDataCallbackInfo triggerData = { 0, 0, 0, 0, false, false }; + + // We'll automatically send time interval triggers to the TC if we have the timestamp set to zero here, so no need to manually mess with sending the trigger. + triggerData.elementNumber = elementNumber; + triggerData.ddi = DDI; + triggerData.processDataValue = settings.timeTriggerInterval_ms; + add_measurement_time_interval(triggerData); + } + else + { + LOG_ERROR("[TC]: You have enabled a time interval trigger, but your DDOP does not define it for this object! Element: %d, DDI: %d. You need to edit your DDOP, or remove this trigger.", elementNumber, DDI); + } + } + } + } + void TaskControllerClient::process_labels_from_ddop() { std::uint32_t currentByteIndex = 0; @@ -973,7 +1333,17 @@ namespace isobus auto getDDOPByteAt = [this](std::size_t index) { if (ddopUploadMode == DDOPUploadType::UserProvidedBinaryPointer) { - return userSuppliedBinaryDDOP[index]; + assert(nullptr != userSuppliedBinaryDDOP); // Can't be null! + + // just in case assert is disabled + if (nullptr != userSuppliedBinaryDDOP) + { + return userSuppliedBinaryDDOP[index]; + } + else + { + return std::uint8_t(0); + } } else { @@ -1571,6 +1941,21 @@ namespace isobus (static_cast(messageData[6]) << 16) | (static_cast(messageData[7]) << 24)); parentTC->queuedValueRequests.push_back(requestData); + + // Handle requests for default process data specially. Ask the app for all the defaults and send a response. + if (static_cast(DataDescriptionIndex::RequestDefaultProcessData) == requestData.ddi) + { + // The RequestDefaultProcessData DDI may only be requested from device element 0 of a device + if (0 == requestData.elementNumber) + { + LOG_DEBUG("[TC]: Server requested default process data."); + parentTC->shouldProcessAllDefaultProcessDataRequests = true; // Defer to the TC thread or app thread + } + else + { + LOG_WARNING("[TC]: Server requested default process data from an illegal element."); + } + } } break; @@ -1621,31 +2006,7 @@ namespace isobus (static_cast(messageData[6]) << 16) | (static_cast(messageData[7]) << 24)); commandData.lastValue = static_cast(SystemTiming::get_timestamp_ms()); - - auto previousCommand = std::find(parentTC->measurementTimeIntervalCommands.begin(), parentTC->measurementTimeIntervalCommands.end(), commandData); - if (parentTC->measurementTimeIntervalCommands.end() == previousCommand) - { - parentTC->measurementTimeIntervalCommands.push_back(commandData); - LOG_DEBUG("[TC]: TC Requests element: " + - isobus::to_string(static_cast(commandData.elementNumber)) + - " DDI: " + - isobus::to_string(static_cast(commandData.ddi)) + - " every: " + - isobus::to_string(static_cast(commandData.processDataValue)) + - " milliseconds."); - } - else - { - // Use the existing one and update the value - previousCommand->processDataValue = commandData.processDataValue; - LOG_DEBUG("[TC]: TC Altered time interval request for element: " + - isobus::to_string(static_cast(commandData.elementNumber)) + - " DDI: " + - isobus::to_string(static_cast(commandData.ddi)) + - " every: " + - isobus::to_string(static_cast(commandData.processDataValue)) + - " milliseconds."); - } + parentTC->add_measurement_time_interval(commandData); } break; @@ -1661,24 +2022,7 @@ namespace isobus (static_cast(messageData[5]) << 8) | (static_cast(messageData[6]) << 16) | (static_cast(messageData[7]) << 24)); - - auto previousCommand = std::find(parentTC->measurementMaximumThresholdCommands.begin(), parentTC->measurementMaximumThresholdCommands.end(), commandData); - if (parentTC->measurementMaximumThresholdCommands.end() == previousCommand) - { - parentTC->measurementMaximumThresholdCommands.push_back(commandData); - LOG_DEBUG("[TC]: TC Requests element: " + - isobus::to_string(static_cast(commandData.elementNumber)) + - " DDI: " + - isobus::to_string(static_cast(commandData.ddi)) + - " when it is above the raw value: " + - isobus::to_string(static_cast(commandData.processDataValue))); - } - else - { - // Just update the existing one with the new value - previousCommand->processDataValue = commandData.processDataValue; - previousCommand->thresholdPassed = false; - } + parentTC->add_measurement_maximum_threshold(commandData); } break; @@ -1694,24 +2038,7 @@ namespace isobus (static_cast(messageData[5]) << 8) | (static_cast(messageData[6]) << 16) | (static_cast(messageData[7]) << 24)); - - auto previousCommand = std::find(parentTC->measurementMinimumThresholdCommands.begin(), parentTC->measurementMinimumThresholdCommands.end(), commandData); - if (parentTC->measurementMinimumThresholdCommands.end() == previousCommand) - { - parentTC->measurementMinimumThresholdCommands.push_back(commandData); - LOG_DEBUG("[TC]: TC Requests Element " + - isobus::to_string(static_cast(commandData.elementNumber)) + - " DDI: " + - isobus::to_string(static_cast(commandData.ddi)) + - " when it is below the raw value: " + - isobus::to_string(static_cast(commandData.processDataValue))); - } - else - { - // Just update the existing one with the new value - previousCommand->processDataValue = commandData.processDataValue; - previousCommand->thresholdPassed = false; - } + parentTC->add_measurement_minimum_threshold(commandData); } break; @@ -1727,24 +2054,23 @@ namespace isobus (static_cast(messageData[5]) << 8) | (static_cast(messageData[6]) << 16) | (static_cast(messageData[7]) << 24)); + parentTC->add_measurement_change_threshold(commandData); + } + break; - auto previousCommand = std::find(parentTC->measurementOnChangeThresholdCommands.begin(), parentTC->measurementOnChangeThresholdCommands.end(), commandData); - if (parentTC->measurementOnChangeThresholdCommands.end() == previousCommand) - { - parentTC->measurementOnChangeThresholdCommands.push_back(commandData); - LOG_DEBUG("[TC]: TC Requests element " + - isobus::to_string(static_cast(commandData.elementNumber)) + - " DDI: " + - isobus::to_string(static_cast(commandData.ddi)) + - " on change by at least: " + - isobus::to_string(static_cast(commandData.processDataValue))); - } - else - { - // Just update the existing one with the new value - previousCommand->processDataValue = commandData.processDataValue; - previousCommand->thresholdPassed = false; - } + case ProcessDataCommands::MeasurementDistanceInterval: + { + ProcessDataCallbackInfo commandData = { 0, 0, 0, 0, false, false }; + LOCK_GUARD(Mutex, clientMutex); + + commandData.elementNumber = static_cast(static_cast(messageData[0] >> 4) | (static_cast(messageData[1]) << 4)); + commandData.ddi = static_cast(messageData[2]) | + (static_cast(messageData[3]) << 8); + commandData.processDataValue = (static_cast(messageData[4]) | + (static_cast(messageData[5]) << 8) | + (static_cast(messageData[6]) << 16) | + (static_cast(messageData[7]) << 24)); + parentTC->add_measurement_distance_interval(commandData); } break; @@ -1862,9 +2188,9 @@ namespace isobus (static_cast(DeviceDescriptorCommands::ObjectPoolDelete) << 4)); } - bool TaskControllerClient::send_generic_process_data(std::uint8_t multiplexor) const + bool TaskControllerClient::send_generic_process_data(std::uint8_t multiplexer) const { - const std::array buffer = { multiplexor, + const std::array buffer = { multiplexer, 0xFF, 0xFF, 0xFF, diff --git a/test/tc_client_tests.cpp b/test/tc_client_tests.cpp index 3a2c8fb1..64dd60f0 100644 --- a/test/tc_client_tests.cpp +++ b/test/tc_client_tests.cpp @@ -3,6 +3,7 @@ #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_standard_data_description_indices.hpp" #include "isobus/isobus/isobus_task_controller_client.hpp" #include "isobus/isobus/isobus_virtual_terminal_client.hpp" #include "isobus/utility/system_timing.hpp" @@ -1676,7 +1677,50 @@ TEST(TASK_CONTROLLER_CLIENT_TESTS, CallbackTests) requestedDDI = 0; requestedElement = 0; - // Request a value using the public interface + // Test distance thresholds + testFrame.identifier = 0x18CB86F7; + testFrame.data[0] = 0xA5; + testFrame.data[1] = 0x05; + testFrame.data[2] = 0x19; + testFrame.data[3] = 0x3B; + testFrame.data[4] = 0x10; // Distance of 10 + testFrame.data[5] = 0x00; + testFrame.data[6] = 0x00; + testFrame.data[7] = 0x00; + CANNetworkManager::CANNetwork.process_receive_can_message_frame(testFrame); + CANNetworkManager::CANNetwork.update(); + interfaceUnderTest.update(); + + EXPECT_FALSE(valueRequested); + + interfaceUnderTest.set_distance(15); + interfaceUnderTest.update(); + + EXPECT_FALSE(valueRequested); + + interfaceUnderTest.set_distance(16); + interfaceUnderTest.update(); + + EXPECT_TRUE(valueRequested); + EXPECT_EQ(requestedDDI, 0x3B19); + valueRequested = false; + requestedDDI = 0; + requestedElement = 0; + + // Test same value doesn't re-send the value + interfaceUnderTest.set_distance(16); + interfaceUnderTest.update(); + + EXPECT_FALSE(valueRequested); + + // Reset + interfaceUnderTest.test_wrapper_set_state(TaskControllerClient::StateMachineState::Disconnected); // Clear commands + interfaceUnderTest.test_wrapper_set_state(TaskControllerClient::StateMachineState::Connected); // Arbitrary + valueRequested = false; + requestedDDI = 0; + requestedElement = 0; + + // Request a value change using the public interface interfaceUnderTest.on_value_changed_trigger(0x4, 0x3); interfaceUnderTest.update(); @@ -1762,3 +1806,97 @@ TEST(TASK_CONTROLLER_CLIENT_TESTS, LanguageCommandFallback) CANHardwareInterface::stop(); CANNetworkManager::CANNetwork.update(); } + +static bool default_process_data_callback(std::uint16_t elementNumber, + std::uint16_t DDI, + TaskControllerClient::DefaultProcessDataSettings &returnedSettings, + void *) +{ + // Handle two specific default process data variables as an example. + // These are two variables in the bin object, which is element 3 in the object pool. + if (3 == elementNumber) + { + switch (DDI) + { + case static_cast(isobus::DataDescriptionIndex::MaximumVolumeContent): + case static_cast(isobus::DataDescriptionIndex::ActualVolumeContent): + { + returnedSettings.timeTriggerInterval_ms = 1000; + returnedSettings.enableTimeTrigger = true; + return true; + } + break; + + default: + { + } + break; + } + } + return false; +} + +TEST(TASK_CONTROLLER_CLIENT_TESTS, DefaultProcessDataTest) +{ + auto ddop = std::make_shared(); + ddop->set_task_controller_compatibility_level(3); + ASSERT_TRUE(ddop->deserialize_binary_object_pool(DerivedTestTCClient::testBinaryDDOP, sizeof(DerivedTestTCClient::testBinaryDDOP))); + + VirtualCANPlugin serverTC; + serverTC.open(); + + CANHardwareInterface::set_number_of_can_channels(1); + CANHardwareInterface::assign_can_channel_frame_handler(0, std::make_shared()); + CANHardwareInterface::start(); + + auto internalECU = test_helpers::claim_internal_control_function(0x80, 0); + auto TestPartnerTC = test_helpers::force_claim_partnered_control_function(0xDF, 0); + + DerivedTestTCClient interfaceUnderTest(TestPartnerTC, internalECU); + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + interfaceUnderTest.update(); + + CANMessageFrame testFrame = {}; + + ASSERT_TRUE(internalECU->get_address_valid()); + ASSERT_TRUE(TestPartnerTC->get_address_valid()); + interfaceUnderTest.configure(ddop, 1, 32, 32, true, false, true, false, true); + interfaceUnderTest.initialize(false); + interfaceUnderTest.add_default_process_data_requested_callback(default_process_data_callback, &interfaceUnderTest); + interfaceUnderTest.test_wrapper_set_state(TaskControllerClient::StateMachineState::Connected); + + // Force a status message out of the TC + testFrame.identifier = (0x18CBFF00 | static_cast(TestPartnerTC->get_address())); + testFrame.dataLength = CAN_DATA_LENGTH; + testFrame.data[0] = 0xFE; // Status mux + testFrame.data[1] = 0xFF; // Element number, set to not available + testFrame.data[2] = 0xFF; // DDI (N/A) + testFrame.data[3] = 0xFF; // DDI (N/A) + testFrame.data[4] = 0x01; // Status (task active) + testFrame.data[5] = 0x00; // Command address + testFrame.data[6] = 0x00; // Command + testFrame.data[7] = 0xFF; // Reserved + CANNetworkManager::CANNetwork.process_receive_can_message_frame(testFrame); + CANNetworkManager::CANNetwork.update(); + + // Send a request for the default process data DDI + testFrame.identifier = (0x18CB0000 | (static_cast(internalECU->get_address()) << 8) | static_cast(TestPartnerTC->get_address())); + testFrame.data[0] = 0x02; // Mux + Element LSNibble + testFrame.data[1] = 0x00; // Element MSB + testFrame.data[2] = 0xFF; // DDI + testFrame.data[3] = 0xDF; // DDI + testFrame.data[4] = 0x00; + testFrame.data[5] = 0x00; + testFrame.data[6] = 0x00; + testFrame.data[7] = 0x00; + CANNetworkManager::CANNetwork.process_receive_can_message_frame(testFrame); + CANNetworkManager::CANNetwork.update(); + interfaceUnderTest.update(); + + CANNetworkManager::CANNetwork.deactivate_control_function(TestPartnerTC); + CANNetworkManager::CANNetwork.deactivate_control_function(internalECU); + + CANHardwareInterface::stop(); + CANNetworkManager::CANNetwork.update(); +}