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/10] 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/10] =?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/10] 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/10] 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/10] 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/10] 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 ec80df3cd26af44addd84f5c64aadb3b6524ae50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Gunics?= Date: Sat, 27 Dec 2025 17:08:04 +0100 Subject: [PATCH 07/10] Separate the Tractor ECU function from Task Controller. The ISO Wheel and Radar speed is coming from the Tractor BUS and is provided by the Tractor ECU. The NMEA2000 speed was x10 --- include/app.hpp | 1 + src/app.cpp | 122 ++++++++++++++++++++++++++++++++---------------- 2 files changed, 83 insertions(+), 40 deletions(-) diff --git a/include/app.hpp b/include/app.hpp index f1b782a..784984e 100644 --- a/include/app.hpp +++ b/include/app.hpp @@ -35,6 +35,7 @@ class Application std::shared_ptr canDriver; std::shared_ptr tcServer; + std::shared_ptr tecuCF = nullptr; std::unique_ptr speedMessagesInterface; std::unique_ptr nmea2000MessageInterface; std::uint8_t nmea2000SequenceIdentifier = 0; diff --git a/src/app.cpp b/src/app.cpp index ef3b463..aa56814 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -47,27 +47,54 @@ bool Application::initialize() ourNAME.set_arbitrary_address_capable(true); ourNAME.set_industry_group(2); ourNAME.set_device_class(0); - ourNAME.set_function_code(static_cast(isobus::NAME::Function::TaskController)); ourNAME.set_identity_number(20); ourNAME.set_ecu_instance(0); ourNAME.set_function_instance(0); // TC #1. If you want to change the TC number, change this. ourNAME.set_device_class_instance(0); ourNAME.set_manufacturer_code(1407); - auto serverCF = isobus::CANNetworkManager::CANNetwork.create_internal_control_function(ourNAME, 0, isobus::preferred_addresses::IndustryGroup2::TaskController_MappingComputer); // The preferred address for a TC is defined in ISO 11783 - auto addressClaimedFuture = std::async(std::launch::async, [&serverCF]() { - while (!serverCF->get_address_valid()) - std::this_thread::sleep_for(std::chrono::milliseconds(100)); }); + isobus::NAME tcNAME = ourNAME; + tcNAME.set_function_code(static_cast(isobus::NAME::Function::TaskController)); + + isobus::NAME tecuNAME = ourNAME; + tecuNAME.set_function_code(static_cast(isobus::NAME::Function::TractorECU)); + tecuNAME.set_arbitrary_address_capable(false); // TECU address is fixed + tecuNAME.set_ecu_instance(0); + + std::cout << "[Init] Creating Task Controller control function..." << std::endl; + auto tcCF = isobus::CANNetworkManager::CANNetwork.create_internal_control_function(tcNAME, 0, isobus::preferred_addresses::IndustryGroup2::TaskController_MappingComputer); // The preferred address for a TC is defined in ISO 11783 + auto tcAddressClaimedFuture = std::async(std::launch::async, [&tcCF]() { + while (!tcCF->get_address_valid()) + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + isobus::CANNetworkManager::CANNetwork.update(); + } + }); // If this fails, probably the update thread is not started - addressClaimedFuture.wait_for(std::chrono::seconds(5)); - if (!serverCF->get_address_valid()) + tcAddressClaimedFuture.wait_for(std::chrono::seconds(5)); + if (!tcCF->get_address_valid()) { std::cout << "Failed to claim address for TC server. The control function might be invalid." << std::endl; return false; } - tcServer = std::make_shared(serverCF); + // Create TECU control function + // TODO: Should we wait between this and TC? + // TODO: If there's already a TECU on the bus we should not create ours + if (tcCF) { // Only create TECU if TC was created + std::cout << "[Init] Creating Tractor ECU control function..." << std::endl; + tecuCF = isobus::CANNetworkManager::CANNetwork.create_internal_control_function(tecuNAME, 0, isobus::preferred_addresses::IndustryGroup2::TractorECU); + std::cout << "[Init] Tractor ECU control function created, waiting 1.5 seconds..." << std::endl; + + // Update the network manager to process TECU CF claiming + for (int i = 0; i < 15; i++) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + isobus::CANNetworkManager::CANNetwork.update(); + } + } + + tcServer = std::make_shared(tcCF); auto &languageInterface = tcServer->get_language_command_interface(); languageInterface.set_language_code("en"); // This is the default, but you can change it if you want languageInterface.set_country_code("US"); // This is the default, but you can change it if you want @@ -75,23 +102,34 @@ bool Application::initialize() tcServer->set_task_totals_active(true); // TODO: make this dynamic based on status in AOG // Initialize speed and distance messages - speedMessagesInterface = std::make_unique(serverCF, true, true, true, false); //TODO: make configurable whether to send these messages - speedMessagesInterface->initialize(); - nmea2000MessageInterface = std::make_unique(serverCF, false, false, false, false, false, false, false); - nmea2000MessageInterface->initialize(); - nmea2000MessageInterface->set_enable_sending_cog_sog_cyclically(true); // TODO: make configurable whether to send these messages - - speedMessagesInterface->wheelBasedSpeedTransmitData.set_implement_start_stop_operations_state(isobus::SpeedMessagesInterface::WheelBasedMachineSpeedData::ImplementStartStopOperations::NotAvailable); - speedMessagesInterface->wheelBasedSpeedTransmitData.set_key_switch_state(isobus::SpeedMessagesInterface::WheelBasedMachineSpeedData::KeySwitchState::NotAvailable); - speedMessagesInterface->wheelBasedSpeedTransmitData.set_operator_direction_reversed_state(isobus::SpeedMessagesInterface::WheelBasedMachineSpeedData::OperatorDirectionReversed::NotAvailable); - speedMessagesInterface->machineSelectedSpeedTransmitData.set_speed_source(isobus::SpeedMessagesInterface::MachineSelectedSpeedData::SpeedSource::NavigationBasedSpeed); + if(tecuCF) + { + std::cout << "[Init] Creating Speed Messages Interface on TECU..." << std::endl; + speedMessagesInterface = std::make_unique(tecuCF, true, true, true, false); //TODO: make configurable whether to send these messages + speedMessagesInterface->initialize(); + speedMessagesInterface->wheelBasedSpeedTransmitData.set_implement_start_stop_operations_state(isobus::SpeedMessagesInterface::WheelBasedMachineSpeedData::ImplementStartStopOperations::NotAvailable); + speedMessagesInterface->wheelBasedSpeedTransmitData.set_key_switch_state(isobus::SpeedMessagesInterface::WheelBasedMachineSpeedData::KeySwitchState::NotAvailable); + speedMessagesInterface->wheelBasedSpeedTransmitData.set_operator_direction_reversed_state(isobus::SpeedMessagesInterface::WheelBasedMachineSpeedData::OperatorDirectionReversed::NotAvailable); + speedMessagesInterface->machineSelectedSpeedTransmitData.set_speed_source(isobus::SpeedMessagesInterface::MachineSelectedSpeedData::SpeedSource::NavigationBasedSpeed); + std::cout << "[Init] Speed Messages Interface created and initialized." << std::endl; + + std::cout << "[Init] Creating NMEA2000 Message Interface on TECU..." << std::endl; + nmea2000MessageInterface = std::make_unique(tecuCF, false, false, false, false, false, false, false); + nmea2000MessageInterface->initialize(); + nmea2000MessageInterface->set_enable_sending_cog_sog_cyclically(true); // TODO: make configurable whether to send these messages + std::cout << "[Init] NMEA2000 Message Interface created and initialized." << std::endl; + } + else + { + std::cout << "[Warning] TECU Control Function not available, Speed/NMEA interfaces not created" << std::endl; + } std::cout << "Task controller server started." << std::endl; static std::uint8_t xteSid = 0; static std::uint32_t lastXteTransmit = 0; - auto packetHandler = [this, serverCF](std::uint8_t src, std::uint8_t pgn, std::span data) { + auto packetHandler = [this, tcCF](std::uint8_t src, std::uint8_t pgn, std::span data) { if (src == 0x7F && pgn == 0xFE) // 254 - Steer Data { // TODO: hack to get desired section states. probably want to make a new pgn later when we need more than 16 sections @@ -122,23 +160,26 @@ bool Application::initialize() { std::uint16_t speed = std::abs(value); auto direction = value < 0 ? isobus::SpeedMessagesInterface::MachineDirection::Reverse : isobus::SpeedMessagesInterface::MachineDirection::Forward; - speedMessagesInterface->groundBasedSpeedTransmitData.set_machine_direction_of_travel(direction); - speedMessagesInterface->wheelBasedSpeedTransmitData.set_machine_direction_of_travel(direction); - speedMessagesInterface->machineSelectedSpeedTransmitData.set_machine_direction_of_travel(direction); - - speedMessagesInterface->groundBasedSpeedTransmitData.set_machine_speed(speed); - speedMessagesInterface->wheelBasedSpeedTransmitData.set_machine_speed(speed); - speedMessagesInterface->machineSelectedSpeedTransmitData.set_machine_speed(speed); - - speedMessagesInterface->groundBasedSpeedTransmitData.set_machine_distance(0); // TODO: Implement distance - speedMessagesInterface->wheelBasedSpeedTransmitData.set_machine_distance(0); // TODO: Implement distance - speedMessagesInterface->machineSelectedSpeedTransmitData.set_machine_distance(0); // TODO: Implement distance - - auto &cog_sog_message = nmea2000MessageInterface->get_cog_sog_transmit_message(); - cog_sog_message.set_sequence_id(nmea2000SequenceIdentifier++); - cog_sog_message.set_speed_over_ground(speed); - cog_sog_message.set_course_over_ground(0); // TODO: Implement course - cog_sog_message.set_course_over_ground_reference(isobus::NMEA2000Messages::CourseOverGroundSpeedOverGroundRapidUpdate::CourseOverGroundReference::NotApplicableOrNull); + if(speedMessagesInterface) { + speedMessagesInterface->groundBasedSpeedTransmitData.set_machine_direction_of_travel(direction); + speedMessagesInterface->wheelBasedSpeedTransmitData.set_machine_direction_of_travel(direction); + speedMessagesInterface->machineSelectedSpeedTransmitData.set_machine_direction_of_travel(direction); + + speedMessagesInterface->groundBasedSpeedTransmitData.set_machine_speed(speed); + speedMessagesInterface->wheelBasedSpeedTransmitData.set_machine_speed(speed); + speedMessagesInterface->machineSelectedSpeedTransmitData.set_machine_speed(speed); + + speedMessagesInterface->groundBasedSpeedTransmitData.set_machine_distance(0); // TODO: Implement distance + speedMessagesInterface->wheelBasedSpeedTransmitData.set_machine_distance(0); // TODO: Implement distance + speedMessagesInterface->machineSelectedSpeedTransmitData.set_machine_distance(0); // TODO: Implement distance + } + if(nmea2000MessageInterface){ + auto &cog_sog_message = nmea2000MessageInterface->get_cog_sog_transmit_message(); + cog_sog_message.set_sequence_id(nmea2000SequenceIdentifier++); + cog_sog_message.set_speed_over_ground(speed/10); + cog_sog_message.set_course_over_ground(0); // TODO: Implement course + cog_sog_message.set_course_over_ground_reference(isobus::NMEA2000Messages::CourseOverGroundSpeedOverGroundRapidUpdate::CourseOverGroundReference::NotApplicableOrNull); + } } else if (identifier == isobus::DataDescriptionIndex::GuidanceLineDeviation) { @@ -160,13 +201,13 @@ bool Application::initialize() }; if (isobus::SystemTiming::time_expired_ms(lastXteTransmit, 1000)) // Transmit every second { - if (isobus::CANNetworkManager::CANNetwork.send_can_message(0x1F903, xteData.data(), xteData.size(), serverCF)) + if (isobus::CANNetworkManager::CANNetwork.send_can_message(0x1F903, xteData.data(), xteData.size(), tcCF)) { lastXteTransmit = isobus::SystemTiming::get_timestamp_ms(); } } } - else if (static_cast(identifier) == 597 /*isobus::DataDescriptionIndex::TotalDistance*/) + else if (static_cast(identifier) == 597 /*isobus::DataDescriptionIndex::TotalDistance*/ && speedMessagesInterface) { auto distance = static_cast(value); speedMessagesInterface->groundBasedSpeedTransmitData.set_machine_distance(distance); @@ -186,14 +227,15 @@ bool Application::initialize() bool Application::update() { static std::uint32_t lastHeartbeatTransmit = 0; + static std::uint32_t lastTECUStatusTransmit = 0; udpConnections->handle_address_detection(); udpConnections->handle_incoming_packets(); tcServer->request_measurement_commands(); tcServer->update(); - speedMessagesInterface->update(); - nmea2000MessageInterface->update(); + if(speedMessagesInterface) speedMessagesInterface->update(); + if(nmea2000MessageInterface) nmea2000MessageInterface->update(); if (isobus::SystemTiming::time_expired_ms(lastHeartbeatTransmit, 100)) { From 69f9bf46987612294038f2697ca0b0a5f76aaada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Gunics?= Date: Sat, 27 Dec 2025 17:14:36 +0100 Subject: [PATCH 08/10] Execute clang-format --- src/app.cpp | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index aa56814..ce5fb83 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -82,13 +82,15 @@ bool Application::initialize() // Create TECU control function // TODO: Should we wait between this and TC? // TODO: If there's already a TECU on the bus we should not create ours - if (tcCF) { // Only create TECU if TC was created + if (tcCF) + { // Only create TECU if TC was created std::cout << "[Init] Creating Tractor ECU control function..." << std::endl; tecuCF = isobus::CANNetworkManager::CANNetwork.create_internal_control_function(tecuNAME, 0, isobus::preferred_addresses::IndustryGroup2::TractorECU); std::cout << "[Init] Tractor ECU control function created, waiting 1.5 seconds..." << std::endl; // Update the network manager to process TECU CF claiming - for (int i = 0; i < 15; i++) { + for (int i = 0; i < 15; i++) + { std::this_thread::sleep_for(std::chrono::milliseconds(100)); isobus::CANNetworkManager::CANNetwork.update(); } @@ -102,7 +104,7 @@ bool Application::initialize() tcServer->set_task_totals_active(true); // TODO: make this dynamic based on status in AOG // Initialize speed and distance messages - if(tecuCF) + if (tecuCF) { std::cout << "[Init] Creating Speed Messages Interface on TECU..." << std::endl; speedMessagesInterface = std::make_unique(tecuCF, true, true, true, false); //TODO: make configurable whether to send these messages @@ -119,7 +121,7 @@ bool Application::initialize() nmea2000MessageInterface->set_enable_sending_cog_sog_cyclically(true); // TODO: make configurable whether to send these messages std::cout << "[Init] NMEA2000 Message Interface created and initialized." << std::endl; } - else + else { std::cout << "[Warning] TECU Control Function not available, Speed/NMEA interfaces not created" << std::endl; } @@ -160,7 +162,8 @@ bool Application::initialize() { std::uint16_t speed = std::abs(value); auto direction = value < 0 ? isobus::SpeedMessagesInterface::MachineDirection::Reverse : isobus::SpeedMessagesInterface::MachineDirection::Forward; - if(speedMessagesInterface) { + if (speedMessagesInterface) + { speedMessagesInterface->groundBasedSpeedTransmitData.set_machine_direction_of_travel(direction); speedMessagesInterface->wheelBasedSpeedTransmitData.set_machine_direction_of_travel(direction); speedMessagesInterface->machineSelectedSpeedTransmitData.set_machine_direction_of_travel(direction); @@ -173,10 +176,11 @@ bool Application::initialize() speedMessagesInterface->wheelBasedSpeedTransmitData.set_machine_distance(0); // TODO: Implement distance speedMessagesInterface->machineSelectedSpeedTransmitData.set_machine_distance(0); // TODO: Implement distance } - if(nmea2000MessageInterface){ + if (nmea2000MessageInterface) + { auto &cog_sog_message = nmea2000MessageInterface->get_cog_sog_transmit_message(); cog_sog_message.set_sequence_id(nmea2000SequenceIdentifier++); - cog_sog_message.set_speed_over_ground(speed/10); + cog_sog_message.set_speed_over_ground(speed / 10); cog_sog_message.set_course_over_ground(0); // TODO: Implement course cog_sog_message.set_course_over_ground_reference(isobus::NMEA2000Messages::CourseOverGroundSpeedOverGroundRapidUpdate::CourseOverGroundReference::NotApplicableOrNull); } @@ -234,8 +238,10 @@ bool Application::update() tcServer->request_measurement_commands(); tcServer->update(); - if(speedMessagesInterface) speedMessagesInterface->update(); - if(nmea2000MessageInterface) nmea2000MessageInterface->update(); + if (speedMessagesInterface) + speedMessagesInterface->update(); + if (nmea2000MessageInterface) + nmea2000MessageInterface->update(); if (isobus::SystemTiming::time_expired_ms(lastHeartbeatTransmit, 100)) { From 4287e03dafcbeb2a26ff162eab8241071534ae5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Gunics?= Date: Sat, 27 Dec 2025 19:05:57 +0100 Subject: [PATCH 09/10] 64 sections are now supported by TC (no changes are necessary in AgOpenGPS other than enabling Zones) --- src/app.cpp | 15 ++++++--------- src/task_controller.cpp | 4 ++-- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index 06755ce..ffdf8f5 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -94,19 +94,16 @@ bool Application::initialize() static std::uint32_t lastXteTransmit = 0; auto packetHandler = [this, serverCF](std::uint8_t src, std::uint8_t pgn, std::span data) { - if (src == 0x7F && pgn == 0xFE) // 254 - Steer Data + if (src == 0x7F && pgn == 0xE5) // 229 - Section Data 1 to 64 { - // TODO: hack to get desired section states. probably want to make a new pgn later when we need more than 16 sections std::vector sectionStates; - for (std::uint8_t i = 0; i < 8; i++) + for (std::uint8_t scb = 0; scb < 8; scb++) { - sectionStates.push_back(data[6] & (1 << i)); - } - for (std::uint8_t i = 0; i < 8; i++) - { - sectionStates.push_back(data[7] & (1 << i)); + for (std::uint8_t i = 0; i < 8; i++) + { + sectionStates.push_back(data[scb] & (1 << i)); + } } - tcServer->update_section_states(sectionStates); } else if (src == 0x7F && pgn == 0xF1) // 241 - Section Control diff --git a/src/task_controller.cpp b/src/task_controller.cpp index 49bcaca..760a38f 100644 --- a/src/task_controller.cpp +++ b/src/task_controller.cpp @@ -154,8 +154,8 @@ bool ClientState::try_get_element_work_state(std::uint16_t elementNumber, bool & MyTCServer::MyTCServer(std::shared_ptr internalControlFunction) : TaskControllerServer(internalControlFunction, 1, // AOG limits to 1 boom - 16, // AOG limits to 16 sections of unique width - 16, // 16 channels for position based control + 64, // AOG limits to 16 sections of unique width but with zones it can be 64 + 64, // 64 channels for position based control isobus::TaskControllerOptions() .with_implement_section_control(), // We support section control TaskControllerVersion::SecondEditionDraft) From 01d2bc0807b99aaefb7ea59ba0a39a6694e7039a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Gunics?= Date: Tue, 30 Dec 2025 12:20:46 +0100 Subject: [PATCH 10/10] Fix the DDOP filename generation. Some implements have the "no more text" ASCII character in their name. Also the extension should be .ddop not .iop --- src/task_controller.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/task_controller.cpp b/src/task_controller.cpp index 760a38f..5c4a856 100644 --- a/src/task_controller.cpp +++ b/src/task_controller.cpp @@ -197,7 +197,14 @@ bool MyTCServer::activate_object_pool(std::shared_ptr p break; } } - auto fileName = std::to_string(partnerCF->get_NAME().get_full_name()) + "\\" + std::string(deviceObject->get_localization_label().begin(), deviceObject->get_localization_label().end()) + ".iop"; + + auto labelBytes = deviceObject->get_localization_label(); + std::string label(reinterpret_cast(labelBytes.data()), labelBytes.size()); + // trim at first occurrence of null or ETX (0x03) + auto it = std::find_if(label.begin(), label.end(), [](char c) { return c == '\0' || static_cast(c) == 0x03; }); + label.erase(it, label.end()); + + auto fileName = std::to_string(partnerCF->get_NAME().get_full_name()) + "\\" + label + ".ddop"; std::vector binaryPool; if (state.get_pool().generate_binary_object_pool(binaryPool)) { @@ -206,10 +213,11 @@ bool MyTCServer::activate_object_pool(std::shared_ptr p { outFile.write(reinterpret_cast(binaryPool.data()), binaryPool.size()); outFile.close(); + std::cout << "Saved DDOP to file: " << fileName << std::endl; } else { - std::cout << "Unable to save DDOP to NVM. (Failed to open file)" << std::endl; + std::cout << "Unable to save DDOP to NVM. (Failed to open file) file: " << fileName << std::endl; } } else