From 572b78abe1e8b3739030756752f9d38852fa98f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Gunics?= Date: Wed, 24 Dec 2025 11:02:52 +0100 Subject: [PATCH 01/11] Tested with 16 sections, and added the check for the master switch. When the activity is suspended (e.g. AUX-N master switch) the painting is also suspended in AgOpenGPS. --- include/task_controller.hpp | 5 ++++ src/task_controller.cpp | 52 ++++++++++++++++++++++++++++++++++--- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/include/task_controller.hpp b/include/task_controller.hpp index 4e7685b..e300d9f 100644 --- a/include/task_controller.hpp +++ b/include/task_controller.hpp @@ -9,6 +9,7 @@ #pragma once +#include "isobus/isobus/isobus_data_dictionary.hpp" #include "isobus/isobus/isobus_device_descriptor_object_pool.hpp" #include "isobus/isobus/isobus_standard_data_description_indices.hpp" #include "isobus/isobus/isobus_task_controller_server.hpp" @@ -48,6 +49,9 @@ class ClientState void mark_measurement_commands_sent(); std::uint16_t get_element_number_for_ddi(isobus::DataDescriptionIndex ddi) const; void set_element_number_for_ddi(isobus::DataDescriptionIndex ddi, std::uint16_t elementNumber); + // Element work state management these act like master / override for actual sections + void set_element_work_state(std::uint16_t elementNumber, bool isWorking); + bool get_element_work_state(std::uint16_t elementNumber, bool &isWorking) const; private: isobus::DeviceDescriptorObjectPool pool; ///< The device descriptor object pool (DDOP) for the TC @@ -59,6 +63,7 @@ class ClientState std::vector sectionActualStates; // 2 bits per section (0 = off, 1 = on, 2 = error, 3 = not installed) bool setpointWorkState = false; ///< The overall work state desired bool actualWorkState = false; ///< The overall work state actual + std::map elementWorkStates; ///< Work state per element (element number -> is working) bool isSectionControlEnabled = false; ///< Stores auto vs manual mode setting }; diff --git a/src/task_controller.cpp b/src/task_controller.cpp index d776345..ce930c1 100644 --- a/src/task_controller.cpp +++ b/src/task_controller.cpp @@ -135,6 +135,22 @@ void ClientState::set_element_number_for_ddi(isobus::DataDescriptionIndex ddi, s ddiToElementNumber[ddi] = elementNumber; } +void ClientState::set_element_work_state(std::uint16_t elementNumber, bool isWorking) +{ + elementWorkStates[elementNumber] = isWorking; +} + +bool ClientState::get_element_work_state(std::uint16_t elementNumber, bool &isWorking) const +{ + auto it = elementWorkStates.find(elementNumber); + if (it != elementWorkStates.end()) + { + isWorking = it->second; + return true; + } + return false; +} + MyTCServer::MyTCServer(std::shared_ptr internalControlFunction) : TaskControllerServer(internalControlFunction, 1, // AOG limits to 1 boom @@ -327,9 +343,33 @@ bool MyTCServer::on_value_command(std::shared_ptr partn { std::uint8_t sectionIndexOffset = NUMBER_SECTIONS_PER_CONDENSED_MESSAGE * static_cast(dataDescriptionIndex - static_cast(isobus::DataDescriptionIndex::ActualCondensedWorkState1_16)); + // Check if ActualWorkState is off (0) for either the current element or element 0 (main implement) + // If either is off, all sections should be treated as off + bool workStateOff = false; + auto &clientState = clients[partner]; + // Check if the current element's work state is off + bool currentElementWorkState; + if (clientState.get_element_work_state(elementNumber, currentElementWorkState) && !currentElementWorkState) + { + workStateOff = true; + //std::cout << "Element " << elementNumber << " work state is OFF, forcing sections to OFF" << std::endl; + } + // Check if element 0's work state is off (main implement) + bool mainElementWorkState; + if (clientState.get_element_work_state(0, mainElementWorkState)) + { + if (!mainElementWorkState) + { + workStateOff = true; + //std::cout << "Element 0 work state is OFF, forcing sections to OFF" << std::endl; + } + } for (std::uint_fast8_t i = 0; i < NUMBER_SECTIONS_PER_CONDENSED_MESSAGE; i++) { - clients[partner].set_section_actual_state(i + sectionIndexOffset, (processDataValue >> (2 * i)) & 0x03); + // When work state is off, force all sections to off state + // Otherwise, use the actual values from the implement + std::uint8_t sectionState = workStateOff ? SectionState::OFF : ((processDataValue >> (2 * i)) & 0x03); + clients[partner].set_section_actual_state(i + sectionIndexOffset, sectionState); } } break; @@ -342,7 +382,8 @@ bool MyTCServer::on_value_command(std::shared_ptr partn case static_cast(isobus::DataDescriptionIndex::ActualWorkState): { - clients[partner].set_setpoint_work_state(processDataValue == 1); + // Store the work state per element rather than globally + clients[partner].set_element_work_state(elementNumber, processDataValue == 1); } } @@ -394,10 +435,15 @@ void MyTCServer::request_measurement_commands() { // TODO: This is a bit of a hack, but it works for now client.second.set_element_number_for_ddi(static_cast(processDataObject->get_ddi()), elementObject->get_element_number()); + const auto &entryB = isobus::DataDictionary::get_entry(processDataObject->get_ddi()); + std::cout << "Mapped DDI " << processDataObject->get_ddi() << " (" << entryB.to_string() << ") to element " + << elementObject->get_element_number() << std::endl; if (processDataObject->has_trigger_method(isobus::task_controller_object::DeviceProcessDataObject::AvailableTriggerMethods::OnChange)) { send_change_threshold_measurement_command(client.first, processDataObject->get_ddi(), elementObject->get_element_number(), 1); + std::cout << "Subscribed (OnChange) to DDI " << processDataObject->get_ddi() << " (" << entryB.to_string() << ") for element " + << elementObject->get_element_number() << std::endl; } if (processDataObject->has_trigger_method(isobus::task_controller_object::DeviceProcessDataObject::AvailableTriggerMethods::TimeInterval)) { @@ -488,7 +534,7 @@ void MyTCServer::update_section_states(std::vector §ionStates) } if (requiresUpdate) { - std::uint8_t ddiOffset = state.get_number_of_sections() / NUMBER_SECTIONS_PER_CONDENSED_MESSAGE; + std::uint8_t ddiOffset = (state.get_number_of_sections() -1) / NUMBER_SECTIONS_PER_CONDENSED_MESSAGE; send_section_setpoint_states(client.first, ddiOffset); } } From b7e62737c70712d8235154c83bc3af5ed4de3790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Gunics?= Date: Wed, 24 Dec 2025 13:53:53 +0100 Subject: [PATCH 02/11] =?UTF-8?q?Added=20support=20for=20legacy=20section?= =?UTF-8?q?=20control=20DDI-s=20required=20by=20Damman=20/=20M=C3=BCller?= =?UTF-8?q?=20Jobrechner=20ECU.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/task_controller.cpp | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/task_controller.cpp b/src/task_controller.cpp index ce930c1..63aac5b 100644 --- a/src/task_controller.cpp +++ b/src/task_controller.cpp @@ -561,16 +561,29 @@ void MyTCServer::send_section_setpoint_states(std::shared_ptr(isobus::DataDescriptionIndex::SetpointCondensedWorkState1_16) + ddiOffset; - std::uint16_t elementNumber = clients[client].get_element_number_for_ddi(static_cast(ddiTarget)); - send_set_value(client, ddiTarget, elementNumber, value); + if (clients[client].get_element_number_for_ddi(static_cast(ddiTarget)) != 0) + { + std::uint16_t elementNumber = clients[client].get_element_number_for_ddi(static_cast(ddiTarget)); + send_set_value(client, ddiTarget, elementNumber, value); - bool setpointWorkState = clients[client].is_any_section_setpoint_on(); - if ((clients[client].get_setpoint_work_state() != setpointWorkState)) + bool setpointWorkState = clients[client].is_any_section_setpoint_on(); + if ((clients[client].get_setpoint_work_state() != setpointWorkState)) + { + send_set_value(client, static_cast(isobus::DataDescriptionIndex::SetpointWorkState), clients[client].get_element_number_for_ddi(isobus::DataDescriptionIndex::SetpointWorkState), setpointWorkState ? 1 : 0); + clients[client].set_setpoint_work_state(setpointWorkState); + } + return; + } + ddiTarget = static_cast(isobus::DataDescriptionIndex::ActualCondensedWorkState1_16) + ddiOffset; + if (clients[client].get_element_number_for_ddi(static_cast(ddiTarget)) != 0) { - send_set_value(client, static_cast(isobus::DataDescriptionIndex::SetpointWorkState), clients[client].get_element_number_for_ddi(isobus::DataDescriptionIndex::SetpointWorkState), setpointWorkState ? 1 : 0); - clients[client].set_setpoint_work_state(setpointWorkState); + send_set_value(client, ddiTarget, clients[client].get_element_number_for_ddi(static_cast(ddiTarget)), value); + return; } + + std::cout << "[TC Server] Neither condensed nor controllable-actual work state supported Missing DDI 290 and 141!" << std::endl; } void MyTCServer::send_section_control_state(std::shared_ptr client, bool enabled) From 983c3fbc5d910f2080fcece1275218b36148c6cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Gunics?= Date: Wed, 24 Dec 2025 14:09:05 +0100 Subject: [PATCH 03/11] Increased CTS packet count to get rid of message flood from some implement. Fixed the parameter names in the help function of the console. --- src/app.cpp | 2 ++ src/main.cpp | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index ef3b463..06755ce 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -41,6 +41,8 @@ bool Application::initialize() return false; } + isobus::CANNetworkManager::CANNetwork.get_configuration().set_number_of_packets_per_cts_message(255); + isobus::NAME ourNAME(0); //! Make sure you change these for your device!!!! diff --git a/src/main.cpp b/src/main.cpp index 12274a7..6ec6b6c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -116,8 +116,8 @@ class ArgumentProcessor std::cout << "Options:\n"; std::cout << " --help\t\tShow this help message\n"; std::cout << " --version\t\tShow the version of the application\n"; - std::cout << " --adapter=\tSelect the CAN driver\n"; - std::cout << " --channel=\tSelect the CAN channel\n"; + std::cout << " --can_adapter=\tSelect the CAN driver\n"; + std::cout << " --can_channel=\tSelect the CAN channel\n"; std::cout << " --log_level=\tSet the log level (debug, info, warning, error, critical)\n"; std::cout << " --log2file\t\tLog to file\n"; exit(0); From c677e8c87f367583035cbf3af418af495b4a36a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Gunics?= Date: Wed, 24 Dec 2025 14:21:15 +0100 Subject: [PATCH 04/11] Renamed the get_element to try_get_element_work_state as I found it confusing that it works with 2 boolean values at the same time. --- include/task_controller.hpp | 2 +- src/task_controller.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/task_controller.hpp b/include/task_controller.hpp index e300d9f..07e2312 100644 --- a/include/task_controller.hpp +++ b/include/task_controller.hpp @@ -51,7 +51,7 @@ class ClientState void set_element_number_for_ddi(isobus::DataDescriptionIndex ddi, std::uint16_t elementNumber); // Element work state management these act like master / override for actual sections void set_element_work_state(std::uint16_t elementNumber, bool isWorking); - bool get_element_work_state(std::uint16_t elementNumber, bool &isWorking) const; + bool try_get_element_work_state(std::uint16_t elementNumber, bool &isWorking) const; private: isobus::DeviceDescriptorObjectPool pool; ///< The device descriptor object pool (DDOP) for the TC diff --git a/src/task_controller.cpp b/src/task_controller.cpp index 63aac5b..223c88d 100644 --- a/src/task_controller.cpp +++ b/src/task_controller.cpp @@ -140,7 +140,7 @@ void ClientState::set_element_work_state(std::uint16_t elementNumber, bool isWor elementWorkStates[elementNumber] = isWorking; } -bool ClientState::get_element_work_state(std::uint16_t elementNumber, bool &isWorking) const +bool ClientState::try_get_element_work_state(std::uint16_t elementNumber, bool &isWorking) const { auto it = elementWorkStates.find(elementNumber); if (it != elementWorkStates.end()) @@ -349,14 +349,14 @@ bool MyTCServer::on_value_command(std::shared_ptr partn auto &clientState = clients[partner]; // Check if the current element's work state is off bool currentElementWorkState; - if (clientState.get_element_work_state(elementNumber, currentElementWorkState) && !currentElementWorkState) + if (clientState.try_get_element_work_state(elementNumber, currentElementWorkState) && !currentElementWorkState) { workStateOff = true; //std::cout << "Element " << elementNumber << " work state is OFF, forcing sections to OFF" << std::endl; } // Check if element 0's work state is off (main implement) bool mainElementWorkState; - if (clientState.get_element_work_state(0, mainElementWorkState)) + if (clientState.try_get_element_work_state(0, mainElementWorkState)) { if (!mainElementWorkState) { From fccd331245bab88793d75758a17613dbd5c09b5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Gunics?= Date: Wed, 24 Dec 2025 14:21:15 +0100 Subject: [PATCH 05/11] Renamed the get_element to try_get_element_work_state as I found it confusing that it works with 2 boolean values at the same time. + Fix formatting --- include/task_controller.hpp | 2 +- src/task_controller.cpp | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/include/task_controller.hpp b/include/task_controller.hpp index e300d9f..07e2312 100644 --- a/include/task_controller.hpp +++ b/include/task_controller.hpp @@ -51,7 +51,7 @@ class ClientState void set_element_number_for_ddi(isobus::DataDescriptionIndex ddi, std::uint16_t elementNumber); // Element work state management these act like master / override for actual sections void set_element_work_state(std::uint16_t elementNumber, bool isWorking); - bool get_element_work_state(std::uint16_t elementNumber, bool &isWorking) const; + bool try_get_element_work_state(std::uint16_t elementNumber, bool &isWorking) const; private: isobus::DeviceDescriptorObjectPool pool; ///< The device descriptor object pool (DDOP) for the TC diff --git a/src/task_controller.cpp b/src/task_controller.cpp index 63aac5b..49bcaca 100644 --- a/src/task_controller.cpp +++ b/src/task_controller.cpp @@ -140,7 +140,7 @@ void ClientState::set_element_work_state(std::uint16_t elementNumber, bool isWor elementWorkStates[elementNumber] = isWorking; } -bool ClientState::get_element_work_state(std::uint16_t elementNumber, bool &isWorking) const +bool ClientState::try_get_element_work_state(std::uint16_t elementNumber, bool &isWorking) const { auto it = elementWorkStates.find(elementNumber); if (it != elementWorkStates.end()) @@ -349,14 +349,14 @@ bool MyTCServer::on_value_command(std::shared_ptr partn auto &clientState = clients[partner]; // Check if the current element's work state is off bool currentElementWorkState; - if (clientState.get_element_work_state(elementNumber, currentElementWorkState) && !currentElementWorkState) + if (clientState.try_get_element_work_state(elementNumber, currentElementWorkState) && !currentElementWorkState) { workStateOff = true; //std::cout << "Element " << elementNumber << " work state is OFF, forcing sections to OFF" << std::endl; } // Check if element 0's work state is off (main implement) bool mainElementWorkState; - if (clientState.get_element_work_state(0, mainElementWorkState)) + if (clientState.try_get_element_work_state(0, mainElementWorkState)) { if (!mainElementWorkState) { @@ -437,13 +437,13 @@ void MyTCServer::request_measurement_commands() client.second.set_element_number_for_ddi(static_cast(processDataObject->get_ddi()), elementObject->get_element_number()); const auto &entryB = isobus::DataDictionary::get_entry(processDataObject->get_ddi()); std::cout << "Mapped DDI " << processDataObject->get_ddi() << " (" << entryB.to_string() << ") to element " - << elementObject->get_element_number() << std::endl; + << elementObject->get_element_number() << std::endl; if (processDataObject->has_trigger_method(isobus::task_controller_object::DeviceProcessDataObject::AvailableTriggerMethods::OnChange)) { send_change_threshold_measurement_command(client.first, processDataObject->get_ddi(), elementObject->get_element_number(), 1); std::cout << "Subscribed (OnChange) to DDI " << processDataObject->get_ddi() << " (" << entryB.to_string() << ") for element " - << elementObject->get_element_number() << std::endl; + << elementObject->get_element_number() << std::endl; } if (processDataObject->has_trigger_method(isobus::task_controller_object::DeviceProcessDataObject::AvailableTriggerMethods::TimeInterval)) { @@ -534,7 +534,7 @@ void MyTCServer::update_section_states(std::vector §ionStates) } if (requiresUpdate) { - std::uint8_t ddiOffset = (state.get_number_of_sections() -1) / NUMBER_SECTIONS_PER_CONDENSED_MESSAGE; + std::uint8_t ddiOffset = (state.get_number_of_sections() - 1) / NUMBER_SECTIONS_PER_CONDENSED_MESSAGE; send_section_setpoint_states(client.first, ddiOffset); } } @@ -575,7 +575,7 @@ void MyTCServer::send_section_setpoint_states(std::shared_ptr(isobus::DataDescriptionIndex::ActualCondensedWorkState1_16) + ddiOffset; if (clients[client].get_element_number_for_ddi(static_cast(ddiTarget)) != 0) { From 94369f92e7a5e21aa02b1dbcbf4b46a19285dac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Gunics?= Date: Wed, 24 Dec 2025 14:21:15 +0100 Subject: [PATCH 06/11] Renamed the get_element to try_get_element_work_state as I found it confusing that it works with 2 boolean values at the same time. From 6e1108bfea230dfc096a3ac7da37db969fbde033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Gunics?= Date: Sun, 28 Dec 2025 22:53:02 +0100 Subject: [PATCH 07/11] Fix Kverneland planter section control where DDI 290 is part of element 0 (cherry picked from commit d50617579a79db37a8e6aa74f8e26a6919c317d2) --- include/task_controller.hpp | 1 + src/task_controller.cpp | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/include/task_controller.hpp b/include/task_controller.hpp index 07e2312..709d248 100644 --- a/include/task_controller.hpp +++ b/include/task_controller.hpp @@ -49,6 +49,7 @@ class ClientState void mark_measurement_commands_sent(); std::uint16_t get_element_number_for_ddi(isobus::DataDescriptionIndex ddi) const; void set_element_number_for_ddi(isobus::DataDescriptionIndex ddi, std::uint16_t elementNumber); + bool has_element_number_for_ddi(isobus::DataDescriptionIndex ddi) const; // Element work state management these act like master / override for actual sections void set_element_work_state(std::uint16_t elementNumber, bool isWorking); bool try_get_element_work_state(std::uint16_t elementNumber, bool &isWorking) const; diff --git a/src/task_controller.cpp b/src/task_controller.cpp index 49bcaca..7bef8de 100644 --- a/src/task_controller.cpp +++ b/src/task_controller.cpp @@ -135,6 +135,11 @@ void ClientState::set_element_number_for_ddi(isobus::DataDescriptionIndex ddi, s ddiToElementNumber[ddi] = elementNumber; } +bool ClientState::has_element_number_for_ddi(isobus::DataDescriptionIndex ddi) const +{ + return ddiToElementNumber.find(ddi) != ddiToElementNumber.end(); +} + void ClientState::set_element_work_state(std::uint16_t elementNumber, bool isWorking) { elementWorkStates[elementNumber] = isWorking; @@ -482,10 +487,18 @@ void MyTCServer::request_measurement_commands() { // TODO: This is a bit of a hack, but it works for now client.second.set_element_number_for_ddi(static_cast(processDataObject->get_ddi()), elementObject->get_element_number()); + const auto &entryB = isobus::DataDictionary::get_entry(processDataObject->get_ddi()); if (processDataObject->has_trigger_method(isobus::task_controller_object::DeviceProcessDataObject::AvailableTriggerMethods::OnChange)) { send_change_threshold_measurement_command(client.first, processDataObject->get_ddi(), elementObject->get_element_number(), 1); + std::cout << "Subscribed (OnChange) to DDI " << processDataObject->get_ddi() << " (" << entryB.to_string() << ") for element " + << elementObject->get_element_number() << std::endl; + } + else + { + std::cout << "Mapped (no OnChange) DDI " << processDataObject->get_ddi() << " (" << entryB.to_string() << ") to element " + << elementObject->get_element_number() << std::endl; } } } @@ -563,7 +576,7 @@ void MyTCServer::send_section_setpoint_states(std::shared_ptr(isobus::DataDescriptionIndex::SetpointCondensedWorkState1_16) + ddiOffset; - if (clients[client].get_element_number_for_ddi(static_cast(ddiTarget)) != 0) + if (clients[client].has_element_number_for_ddi(static_cast(ddiTarget))) { std::uint16_t elementNumber = clients[client].get_element_number_for_ddi(static_cast(ddiTarget)); send_set_value(client, ddiTarget, elementNumber, value); @@ -577,7 +590,7 @@ void MyTCServer::send_section_setpoint_states(std::shared_ptr(isobus::DataDescriptionIndex::ActualCondensedWorkState1_16) + ddiOffset; - if (clients[client].get_element_number_for_ddi(static_cast(ddiTarget)) != 0) + if (clients[client].has_element_number_for_ddi(static_cast(ddiTarget))) { send_set_value(client, ddiTarget, clients[client].get_element_number_for_ddi(static_cast(ddiTarget)), value); return; From af0cff0f56fcaf6ad447a1bc8932cf385e7a3f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Gunics?= Date: Sun, 28 Dec 2025 22:58:55 +0100 Subject: [PATCH 08/11] clang-format --- src/task_controller.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/task_controller.cpp b/src/task_controller.cpp index 7bef8de..7beb760 100644 --- a/src/task_controller.cpp +++ b/src/task_controller.cpp @@ -493,12 +493,12 @@ void MyTCServer::request_measurement_commands() { send_change_threshold_measurement_command(client.first, processDataObject->get_ddi(), elementObject->get_element_number(), 1); std::cout << "Subscribed (OnChange) to DDI " << processDataObject->get_ddi() << " (" << entryB.to_string() << ") for element " - << elementObject->get_element_number() << std::endl; + << elementObject->get_element_number() << std::endl; } else { std::cout << "Mapped (no OnChange) DDI " << processDataObject->get_ddi() << " (" << entryB.to_string() << ") to element " - << elementObject->get_element_number() << std::endl; + << elementObject->get_element_number() << std::endl; } } } From 78da411de3c9a2937407c981dc7e33aafb169603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Gunics?= Date: Tue, 30 Dec 2025 14:30:05 +0100 Subject: [PATCH 09/11] Check if Setpoint Work State exists. The planter seems to recognize it and every time we tried to set the value for it all of its sections turned on. --- src/task_controller.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/task_controller.cpp b/src/task_controller.cpp index 7beb760..10615c8 100644 --- a/src/task_controller.cpp +++ b/src/task_controller.cpp @@ -582,7 +582,7 @@ void MyTCServer::send_section_setpoint_states(std::shared_ptr(isobus::DataDescriptionIndex::SetpointWorkState), clients[client].get_element_number_for_ddi(isobus::DataDescriptionIndex::SetpointWorkState), setpointWorkState ? 1 : 0); clients[client].set_setpoint_work_state(setpointWorkState); From 22dbfe8725c46caf06c7000e0c72f13f5c19fb42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Gunics?= Date: Tue, 30 Dec 2025 19:57:21 +0100 Subject: [PATCH 10/11] Extract is_ddi_settable() function - searches the DDOP for a given DDI and returns whether it has the Settable property. Added CondensedSectionOverrideState (DDI 291-296) as it was useful for debugging Simplify SetpointWorkState error handling - Consolidated error logging to only report when DDI 289 is not available, as there are some implement that do not use it. --- include/task_controller.hpp | 7 +- src/task_controller.cpp | 145 ++++++++++++++++++++++++++++-------- 2 files changed, 118 insertions(+), 34 deletions(-) diff --git a/include/task_controller.hpp b/include/task_controller.hpp index 709d248..9546457 100644 --- a/include/task_controller.hpp +++ b/include/task_controller.hpp @@ -37,6 +37,8 @@ class ClientState std::uint8_t get_number_of_sections() const; std::uint8_t get_section_setpoint_state(std::uint8_t section) const; std::uint8_t get_section_actual_state(std::uint8_t section) const; + std::uint16_t get_element_number_for_section(std::uint8_t section) const; + void set_element_number_for_section(std::uint8_t section, std::uint16_t elementNumber); bool is_any_section_setpoint_on() const; bool get_setpoint_work_state() const; void set_setpoint_work_state(bool state); @@ -50,6 +52,7 @@ class ClientState std::uint16_t get_element_number_for_ddi(isobus::DataDescriptionIndex ddi) const; void set_element_number_for_ddi(isobus::DataDescriptionIndex ddi, std::uint16_t elementNumber); bool has_element_number_for_ddi(isobus::DataDescriptionIndex ddi) const; + bool is_element_or_parent_off(std::uint16_t elementNumber) const; ///< Recursively checks if element or any parent is off // Element work state management these act like master / override for actual sections void set_element_work_state(std::uint16_t elementNumber, bool isWorking); bool try_get_element_work_state(std::uint16_t elementNumber, bool &isWorking) const; @@ -62,7 +65,8 @@ class ClientState std::uint8_t numberOfSections; std::vector sectionSetpointStates; // 2 bits per section (0 = off, 1 = on, 2 = error, 3 = not installed) std::vector sectionActualStates; // 2 bits per section (0 = off, 1 = on, 2 = error, 3 = not installed) - bool setpointWorkState = false; ///< The overall work state desired + std::vector sectionToElementNumber; // Maps section index to element number for hierarchy checking + bool setpointWorkState = false; ///< The overall work state desired (DDI 289) bool actualWorkState = false; ///< The overall work state actual std::map elementWorkStates; ///< Work state per element (element number -> is working) bool isSectionControlEnabled = false; ///< Stores auto vs manual mode setting @@ -97,6 +101,7 @@ class MyTCServer : public isobus::TaskControllerServer private: void send_section_setpoint_states(std::shared_ptr client, std::uint8_t ddiOffset); void send_section_control_state(std::shared_ptr client, bool enabled); + bool is_ddi_settable(std::shared_ptr client, std::uint16_t ddi); std::map, ClientState> clients; std::map, std::queue>> uploadedPools; diff --git a/src/task_controller.cpp b/src/task_controller.cpp index 10615c8..271a0f2 100644 --- a/src/task_controller.cpp +++ b/src/task_controller.cpp @@ -21,6 +21,7 @@ void ClientState::set_number_of_sections(std::uint8_t number) numberOfSections = number; sectionSetpointStates.resize(number); sectionActualStates.resize(number); + sectionToElementNumber.resize(number, 0); // Initialize all sections mapped to element 0 by default } void ClientState::set_section_setpoint_state(std::uint8_t section, std::uint8_t state) @@ -39,6 +40,23 @@ void ClientState::set_section_actual_state(std::uint8_t section, std::uint8_t st } } +std::uint16_t ClientState::get_element_number_for_section(std::uint8_t section) const +{ + if (section < numberOfSections && section < sectionToElementNumber.size()) + { + return sectionToElementNumber[section]; + } + return 0; +} + +void ClientState::set_element_number_for_section(std::uint8_t section, std::uint16_t elementNumber) +{ + if (section < numberOfSections && section < sectionToElementNumber.size()) + { + sectionToElementNumber[section] = elementNumber; + } +} + std::uint8_t ClientState::get_number_of_sections() const { return numberOfSections; @@ -57,6 +75,12 @@ std::uint8_t ClientState::get_section_actual_state(std::uint8_t section) const { if (section < numberOfSections) { + // Check if the element or any parent is off + std::uint16_t elementNumber = get_element_number_for_section(section); + if (is_element_or_parent_off(elementNumber)) + { + return SectionState::OFF; + } return sectionActualStates[section]; } return SectionState::NOT_INSTALLED; @@ -140,6 +164,51 @@ bool ClientState::has_element_number_for_ddi(isobus::DataDescriptionIndex ddi) c return ddiToElementNumber.find(ddi) != ddiToElementNumber.end(); } +bool ClientState::is_element_or_parent_off(std::uint16_t elementNumber) const +{ + // Check if this element is off + bool elementWorkState; + if (try_get_element_work_state(elementNumber, elementWorkState) && !elementWorkState) + { + return true; // Element is off + } + + // Find the parent element(s) by searching through the pool + // Use const_cast to access non-const methods on the pool from a const context + auto &nonConstPool = const_cast(pool); + for (std::uint32_t i = 0; i < nonConstPool.size(); i++) + { + auto object = nonConstPool.get_object_by_index(i); + if (object->get_object_type() == isobus::task_controller_object::ObjectTypes::DeviceElement) + { + auto elementObject = std::dynamic_pointer_cast(object); + // Check if this element has elementNumber as a child + for (std::uint16_t childId : elementObject->get_child_object_ids()) + { + // Find the child object + for (std::uint32_t j = 0; j < nonConstPool.size(); j++) + { + auto childObject = nonConstPool.get_object_by_index(j); + if (childObject && childObject->get_object_id() == childId) + { + if (childObject->get_object_type() == isobus::task_controller_object::ObjectTypes::DeviceElement) + { + auto childElementObject = std::dynamic_pointer_cast(childObject); + if (childElementObject && childElementObject->get_element_number() == elementNumber) + { + // Found the parent, recursively check if parent or its parents are off + return is_element_or_parent_off(elementObject->get_element_number()); + } + } + } + } + } + } + } + + return false; // No parent found or no parents are off +} + void ClientState::set_element_work_state(std::uint16_t elementNumber, bool isWorking) { elementWorkStates[elementNumber] = isWorking; @@ -347,34 +416,12 @@ bool MyTCServer::on_value_command(std::shared_ptr partn case static_cast(isobus::DataDescriptionIndex::ActualCondensedWorkState241_256): { std::uint8_t sectionIndexOffset = NUMBER_SECTIONS_PER_CONDENSED_MESSAGE * static_cast(dataDescriptionIndex - static_cast(isobus::DataDescriptionIndex::ActualCondensedWorkState1_16)); - - // Check if ActualWorkState is off (0) for either the current element or element 0 (main implement) - // If either is off, all sections should be treated as off - bool workStateOff = false; - auto &clientState = clients[partner]; - // Check if the current element's work state is off - bool currentElementWorkState; - if (clientState.try_get_element_work_state(elementNumber, currentElementWorkState) && !currentElementWorkState) - { - workStateOff = true; - //std::cout << "Element " << elementNumber << " work state is OFF, forcing sections to OFF" << std::endl; - } - // Check if element 0's work state is off (main implement) - bool mainElementWorkState; - if (clientState.try_get_element_work_state(0, mainElementWorkState)) - { - if (!mainElementWorkState) - { - workStateOff = true; - //std::cout << "Element 0 work state is OFF, forcing sections to OFF" << std::endl; - } - } + for (std::uint_fast8_t i = 0; i < NUMBER_SECTIONS_PER_CONDENSED_MESSAGE; i++) { - // When work state is off, force all sections to off state - // Otherwise, use the actual values from the implement - std::uint8_t sectionState = workStateOff ? SectionState::OFF : ((processDataValue >> (2 * i)) & 0x03); + std::uint8_t sectionState = ((processDataValue >> (2 * i)) & 0x03); clients[partner].set_section_actual_state(i + sectionIndexOffset, sectionState); + clients[partner].set_element_number_for_section(i + sectionIndexOffset, elementNumber); } } break; @@ -425,7 +472,9 @@ void MyTCServer::request_measurement_commands() auto processDataObject = std::dynamic_pointer_cast(object); if (processDataObject->get_ddi() == static_cast(isobus::DataDescriptionIndex::ActualWorkState) || (processDataObject->get_ddi() >= static_cast(isobus::DataDescriptionIndex::ActualCondensedWorkState1_16) && - processDataObject->get_ddi() <= static_cast(isobus::DataDescriptionIndex::ActualCondensedWorkState241_256))) + processDataObject->get_ddi() <= static_cast(isobus::DataDescriptionIndex::ActualCondensedWorkState241_256)) || + (processDataObject->get_ddi() >= static_cast(isobus::DataDescriptionIndex::CondensedSectionOverrideState1_16) && + processDataObject->get_ddi() <= static_cast(isobus::DataDescriptionIndex::CondensedSectionOverrideState241_256))) { // Loop over all objects to find the elements that are the parents of the actual condensed work state objects for (std::uint32_t j = 0; j < client.second.get_pool().size(); j++) @@ -576,6 +625,8 @@ void MyTCServer::send_section_setpoint_states(std::shared_ptr(isobus::DataDescriptionIndex::SetpointCondensedWorkState1_16) + ddiOffset; + // Legacy ECU? (DDI 161 ActualCondensedWorkState1_16 exists and Settable) + std::uint16_t ddiTargetLegacy = static_cast(isobus::DataDescriptionIndex::ActualCondensedWorkState1_16) + ddiOffset; if (clients[client].has_element_number_for_ddi(static_cast(ddiTarget))) { std::uint16_t elementNumber = clients[client].get_element_number_for_ddi(static_cast(ddiTarget)); @@ -587,19 +638,47 @@ void MyTCServer::send_section_setpoint_states(std::shared_ptr(isobus::DataDescriptionIndex::SetpointWorkState), clients[client].get_element_number_for_ddi(isobus::DataDescriptionIndex::SetpointWorkState), setpointWorkState ? 1 : 0); clients[client].set_setpoint_work_state(setpointWorkState); } - return; - } - ddiTarget = static_cast(isobus::DataDescriptionIndex::ActualCondensedWorkState1_16) + ddiOffset; - if (clients[client].has_element_number_for_ddi(static_cast(ddiTarget))) + else if (!clients[client].has_element_number_for_ddi(isobus::DataDescriptionIndex::SetpointWorkState)) + { + std::cout << "[TC Server] DDI 289 (SetpointWorkState) not available!" << std::endl; + } + } + else if (clients[client].has_element_number_for_ddi(static_cast(ddiTargetLegacy))) { - send_set_value(client, ddiTarget, clients[client].get_element_number_for_ddi(static_cast(ddiTarget)), value); + if (is_ddi_settable(client, ddiTargetLegacy)) + { + send_set_value(client, ddiTargetLegacy, clients[client].get_element_number_for_ddi(static_cast(ddiTargetLegacy)), value); + } + else + { + std::cout << "[TC Server] Legacy DDI " << ddiTargetLegacy << " (ActualCondensedWorkState) is not settable!" << std::endl; + } return; + } + else + { + std::cout << "[TC Server] Neither condensed nor controllable-actual work state supported Missing DDI 290 and 141!" << std::endl; } - - std::cout << "[TC Server] Neither condensed nor controllable-actual work state supported Missing DDI 290 and 141!" << std::endl; } void MyTCServer::send_section_control_state(std::shared_ptr client, bool enabled) { send_set_value(client, static_cast(isobus::DataDescriptionIndex::SectionControlState), clients[client].get_element_number_for_ddi(isobus::DataDescriptionIndex::SectionControlState), enabled ? 1 : 0); } + +bool MyTCServer::is_ddi_settable(std::shared_ptr client, std::uint16_t ddi) +{ + for (std::uint32_t i = 0; i < clients[client].get_pool().size(); i++) + { + auto object = clients[client].get_pool().get_object_by_index(i); + if (object->get_object_type() == isobus::task_controller_object::ObjectTypes::DeviceProcessData) + { + auto processDataObject = std::dynamic_pointer_cast(object); + if (processDataObject->get_ddi() == ddi) + { + return processDataObject->has_property(isobus::task_controller_object::DeviceProcessDataObject::PropertiesBit::Settable); + } + } + } + return false; +} From 34fa41c4710eec8ee529f658e6a509ec709a9cba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Gunics?= Date: Tue, 30 Dec 2025 20:00:31 +0100 Subject: [PATCH 11/11] clang-format --- src/task_controller.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/task_controller.cpp b/src/task_controller.cpp index 271a0f2..4ee92bb 100644 --- a/src/task_controller.cpp +++ b/src/task_controller.cpp @@ -416,7 +416,7 @@ bool MyTCServer::on_value_command(std::shared_ptr partn case static_cast(isobus::DataDescriptionIndex::ActualCondensedWorkState241_256): { std::uint8_t sectionIndexOffset = NUMBER_SECTIONS_PER_CONDENSED_MESSAGE * static_cast(dataDescriptionIndex - static_cast(isobus::DataDescriptionIndex::ActualCondensedWorkState1_16)); - + for (std::uint_fast8_t i = 0; i < NUMBER_SECTIONS_PER_CONDENSED_MESSAGE; i++) { std::uint8_t sectionState = ((processDataValue >> (2 * i)) & 0x03); @@ -642,7 +642,7 @@ void MyTCServer::send_section_setpoint_states(std::shared_ptr(ddiTargetLegacy))) { if (is_ddi_settable(client, ddiTargetLegacy)) @@ -654,7 +654,7 @@ void MyTCServer::send_section_setpoint_states(std::shared_ptr