From 99abf3d7a25d34af1cd443541f046f795ba541da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunics=20Bal=C3=A1zs?= Date: Mon, 12 Oct 2026 23:48:22 +0200 Subject: [PATCH 1/5] Switch section control on automatically when an implement connects Until now an implement stayed in manual mode after connecting until the operator pressed AOG's ISOBUS SC button, which sends PGN 241. The TC now remembers the section control state it wants and applies it to each client 2 s after its pool is activated: it sets the local auto flag and writes DDI 160 when the implement declares it. The initial state comes from the new settings.json key "sectionControlOnConnect" (default true). Pressing AOG's button still switches all clients and also becomes the state for implements that connect later. The implement can still decline or go back to manual; its DDI 160 report is followed as before. Co-Authored-By: Claude Opus 5.5 --- include/settings.hpp | 8 +++++ include/task_controller.hpp | 12 ++++++++ src/app.cpp | 1 + src/settings.cpp | 13 ++++++++ src/task_controller.cpp | 61 ++++++++++++++++++++++++++++++------- 5 files changed, 84 insertions(+), 11 deletions(-) diff --git a/include/settings.hpp b/include/settings.hpp index 06f0278..0373b55 100644 --- a/include/settings.hpp +++ b/include/settings.hpp @@ -98,6 +98,12 @@ class Settings */ bool set_vt_enabled(bool enabled, bool save = true); + /// @brief Check whether section control is switched on automatically when an implement connects. + bool is_section_control_on_connect() const; + + /// @brief Set whether section control is switched on automatically when an implement connects. + bool set_section_control_on_connect(bool enabled, bool save = true); + /** * @brief Get the configured TC version * @return The configured version (0-4, default 3) @@ -161,6 +167,7 @@ class Settings constexpr static bool DEFAULT_NMEA_SEND_ENABLED = true; constexpr static bool DEFAULT_AOG_HEARTBEAT_ENABLED = true; constexpr static bool DEFAULT_VT_ENABLED = true; + constexpr static bool DEFAULT_SECTION_CONTROL_ON_CONNECT = true; constexpr static std::uint8_t DEFAULT_TC_VERSION = 3; ///< SecondEditionDraft (V3 default for maximum implement compatibility) static const std::string DEFAULT_LANGUAGE_CODE; static const std::string DEFAULT_COUNTRY_CODE; @@ -169,6 +176,7 @@ class Settings bool nmeaSendEnabled = DEFAULT_NMEA_SEND_ENABLED; bool aogHeartbeatEnabled = DEFAULT_AOG_HEARTBEAT_ENABLED; bool vtEnabled = DEFAULT_VT_ENABLED; + bool sectionControlOnConnect = DEFAULT_SECTION_CONTROL_ON_CONNECT; std::uint8_t tcVersion = DEFAULT_TC_VERSION; std::string languageCode = DEFAULT_LANGUAGE_CODE; std::string countryCode = DEFAULT_COUNTRY_CODE; diff --git a/include/task_controller.hpp b/include/task_controller.hpp index 705605e..c590659 100644 --- a/include/task_controller.hpp +++ b/include/task_controller.hpp @@ -17,6 +17,7 @@ #include #include #include +#include #include constexpr std::uint8_t NUMBER_SECTIONS_PER_CONDENSED_MESSAGE = 16; @@ -55,6 +56,10 @@ class ClientState isobus::DeviceDescriptorObjectPool &get_pool(); bool are_measurement_commands_sent() const; void mark_measurement_commands_sent(); + /// @brief Schedules the TC's section control state to be applied to this client at the given time. + void schedule_initial_section_control(std::uint32_t dueMs); + /// @brief Returns true (once) when the scheduled initial section control state is due. + bool take_due_initial_section_control(std::uint32_t nowMs); 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; @@ -66,6 +71,7 @@ class ClientState private: isobus::DeviceDescriptorObjectPool pool; ///< The device descriptor object pool (DDOP) for the TC bool areMeasurementCommandsSent = false; ///< Whether or not the measurement commands have been sent + std::optional initialSectionControlDueMs; ///< When to apply the TC's section control state after connect std::map ddiToElementNumber; ///< Mapping of DDI to element number // TODO: better way to do this? std::uint8_t numberOfSections; @@ -116,13 +122,19 @@ class MyTCServer : public isobus::TaskControllerServer void update_section_states(std::vector §ionStates); void update_section_control_enabled(bool enabled); + /// @brief Sets the section control state applied to implements as they connect, before AOG asks for one. + void set_initial_section_control_enabled(bool enabled); + 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); + /// @brief Sets the local auto/manual flag and commands DDI 160 if the client supports it. Caller must hold clientsMutex. + void apply_section_control_state(std::shared_ptr client, ClientState &state, bool enabled); bool is_ddi_settable(std::shared_ptr client, std::uint16_t ddi); std::map, ClientState> clients; std::map, std::queue>> uploadedPools; + bool desiredSectionControlEnabled = false; ///< Applied to implements when they connect; follows AOG's ISOBUS SC button. Guarded by clientsMutex /// @brief Guards clients and uploadedPools. /// diff --git a/src/app.cpp b/src/app.cpp index 9ac8802..fcff617 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -381,6 +381,7 @@ void Application::setup_task_controller_server() languageInterface.set_language_code(settings->get_language_code()); languageInterface.set_country_code(settings->get_country_code()); tcServer->initialize(); + tcServer->set_initial_section_control_enabled(settings->is_section_control_on_connect()); tcServer->set_task_totals_active(true); // TODO: make this dynamic based on status in AOG tcFunctionalities = std::make_unique(tcCF); tcFunctionalities->set_functionality_is_supported( diff --git a/src/settings.cpp b/src/settings.cpp index 960927a..8b6c53f 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -70,6 +70,7 @@ bool Settings::load() nmeaSendEnabled = loadBoolean("nmeaSendEnabled", DEFAULT_NMEA_SEND_ENABLED); aogHeartbeatEnabled = loadBoolean("aogHeartbeatEnabled", DEFAULT_AOG_HEARTBEAT_ENABLED); vtEnabled = loadBoolean("vtEnabled", DEFAULT_VT_ENABLED); + sectionControlOnConnect = loadBoolean("sectionControlOnConnect", DEFAULT_SECTION_CONTROL_ON_CONNECT); if (data.contains("tcVersion")) { @@ -143,6 +144,7 @@ bool Settings::save() const data["nmeaSendEnabled"] = nmeaSendEnabled; data["aogHeartbeatEnabled"] = aogHeartbeatEnabled; data["vtEnabled"] = vtEnabled; + data["sectionControlOnConnect"] = sectionControlOnConnect; data["tcVersion"] = tcVersion; data["languageCode"] = languageCode; data["countryCode"] = countryCode; @@ -261,6 +263,17 @@ bool Settings::set_vt_enabled(bool enabled, bool save) return set_boolean(vtEnabled, enabled, save); } +bool Settings::is_section_control_on_connect() const +{ + std::scoped_lock lock(settingsMutex); + return sectionControlOnConnect; +} + +bool Settings::set_section_control_on_connect(bool enabled, bool save) +{ + return set_boolean(sectionControlOnConnect, enabled, save); +} + bool Settings::is_aog_heartbeat_enabled() const { std::scoped_lock lock(settingsMutex); diff --git a/src/task_controller.cpp b/src/task_controller.cpp index f60afef..61f7e6a 100644 --- a/src/task_controller.cpp +++ b/src/task_controller.cpp @@ -12,6 +12,7 @@ #include "isobus/isobus/isobus_device_descriptor_object_pool_helpers.hpp" #include "isobus/isobus/isobus_task_controller_server.hpp" +#include "isobus/utility/system_timing.hpp" #include #include @@ -21,6 +22,10 @@ #include #include +/// Delay between pool activation and applying the initial section control state, so the implement +/// has settled and answered the measurement commands before it is switched to automatic. +static constexpr std::uint32_t INITIAL_SECTION_CONTROL_DELAY_MS = 2000; + // Sanitize a string for use as a filename by replacing invalid characters with underscores static std::string sanitize_filename(const std::string &input) { @@ -210,6 +215,22 @@ void ClientState::set_section_control_enabled(bool state) isSectionControlEnabled = state; } +void ClientState::schedule_initial_section_control(std::uint32_t dueMs) +{ + initialSectionControlDueMs = dueMs; +} + +bool ClientState::take_due_initial_section_control(std::uint32_t nowMs) +{ + // Signed difference so the comparison survives timestamp wrap-around + if (!initialSectionControlDueMs || static_cast(nowMs - *initialSectionControlDueMs) < 0) + { + return false; + } + initialSectionControlDueMs.reset(); + return true; +} + bool ClientState::uses_per_element_control() const { return usesPerElementControl; @@ -879,6 +900,15 @@ void MyTCServer::request_measurement_commands() log() << "Measurement commands sent." << std::endl; client.second.mark_measurement_commands_sent(); + + // Switch the implement to automatic shortly after it connects, so AOG's SC button + // does not need pressing on every connect. + client.second.schedule_initial_section_control(isobus::SystemTiming::get_timestamp_ms() + INITIAL_SECTION_CONTROL_DELAY_MS); + } + if (client.second.take_due_initial_section_control(isobus::SystemTiming::get_timestamp_ms())) + { + log("TC") << "Applying initial section control state: " << (desiredSectionControlEnabled ? "enabled" : "disabled") << std::endl; + apply_section_control_state(client.first, client.second, desiredSectionControlEnabled); } } } @@ -932,20 +962,29 @@ void MyTCServer::update_section_states(std::vector §ionStates) void MyTCServer::update_section_control_enabled(bool enabled) { std::lock_guard lock(clientsMutex); + desiredSectionControlEnabled = enabled; for (auto &client : clients) { - // Always update the local flag - if (client.second.is_section_control_enabled() != enabled) - { - client.second.set_section_control_enabled(enabled); - } + apply_section_control_state(client.first, client.second, enabled); + } +} - // Only send ISOBUS command to clients that support SectionControlState DDI and have sections - if (client.second.has_element_number_for_ddi(isobus::DataDescriptionIndex::SectionControlState) && - client.second.get_number_of_sections() > 0) - { - send_section_control_state(client.first, enabled); - } +void MyTCServer::set_initial_section_control_enabled(bool enabled) +{ + std::lock_guard lock(clientsMutex); + desiredSectionControlEnabled = enabled; +} + +void MyTCServer::apply_section_control_state(std::shared_ptr client, ClientState &state, bool enabled) +{ + // Always update the local flag + state.set_section_control_enabled(enabled); + + // Only send ISOBUS command to clients that support SectionControlState DDI and have sections + if (state.has_element_number_for_ddi(isobus::DataDescriptionIndex::SectionControlState) && + state.get_number_of_sections() > 0) + { + send_section_control_state(client, enabled); } } From a0463b6f8969076673dbdf631eb4f8c48849807d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunics=20Bal=C3=A1zs?= Date: Wed, 14 Oct 2026 09:09:40 +0200 Subject: [PATCH 2/5] Switch section control on when AOG opens a field New sectionControlOnFieldOpen setting (default on): when AOG opens a field, the TC switches section control on as if AOG's SC button was pressed (the same update_section_control_enabled(true) that PGN 241 "enabled" triggers). AOG sends the field name (PGN 0xF3) on every field open and close, and again when the TC reconnects. Older AOG versions do not, so PGN 229 starting after a 2 s gap (AOG sends it only while a field is open) counts as a field opening too. Both report the same opening, so it is acted on at most once per 5 s. Field open and close are logged. Co-Authored-By: Claude Opus 5.5 --- include/app.hpp | 10 ++++++++++ include/settings.hpp | 8 ++++++++ readme.md | 2 ++ src/app.cpp | 43 +++++++++++++++++++++++++++++++++++++++++++ src/settings.cpp | 13 +++++++++++++ 5 files changed, 76 insertions(+) diff --git a/include/app.hpp b/include/app.hpp index be6ae94..f7bed73 100644 --- a/include/app.hpp +++ b/include/app.hpp @@ -104,6 +104,16 @@ class Application std::int32_t lastXteValue = 0; std::uint32_t lastDistanceMm = 0; std::uint32_t lastAogPacketMs = 0; + + /// @brief AOG opened a field: switch section control on, as if AOG's SC button was pressed (if enabled in settings) + /// @param how What showed the field opening, for the log + void on_aog_field_opened(const std::string &how); + /// AOG sends PGN 229 every frame while a field is open; a gap this long means no field was open + static constexpr std::uint32_t FIELD_CLOSED_GAP_MS = 2000; + /// The field name (PGN 0xF3) and PGN 229 resuming both report one field opening; act on it once + static constexpr std::uint32_t FIELD_OPEN_DEDUPLICATE_MS = 5000; + std::uint32_t lastSectionStatesMs = 0; ///< When PGN 229 was last received, 0 = never + std::uint32_t lastFieldOpenAppliedMs = 0; ///< When on_aog_field_opened() last switched section control on, 0 = never std::uint32_t vtDisconnectedSinceMs = 0; std::uint32_t lastVtStatusUpdateMs = 0; std::uint32_t lastVtSectionUpdateMs = 0; diff --git a/include/settings.hpp b/include/settings.hpp index 0373b55..d66fe4c 100644 --- a/include/settings.hpp +++ b/include/settings.hpp @@ -104,6 +104,12 @@ class Settings /// @brief Set whether section control is switched on automatically when an implement connects. bool set_section_control_on_connect(bool enabled, bool save = true); + /// @brief Check whether section control is switched on when AOG opens a field, as if AOG's SC button was pressed. + bool is_section_control_on_field_open() const; + + /// @brief Set whether section control is switched on when AOG opens a field. + bool set_section_control_on_field_open(bool enabled, bool save = true); + /** * @brief Get the configured TC version * @return The configured version (0-4, default 3) @@ -168,6 +174,7 @@ class Settings constexpr static bool DEFAULT_AOG_HEARTBEAT_ENABLED = true; constexpr static bool DEFAULT_VT_ENABLED = true; constexpr static bool DEFAULT_SECTION_CONTROL_ON_CONNECT = true; + constexpr static bool DEFAULT_SECTION_CONTROL_ON_FIELD_OPEN = true; constexpr static std::uint8_t DEFAULT_TC_VERSION = 3; ///< SecondEditionDraft (V3 default for maximum implement compatibility) static const std::string DEFAULT_LANGUAGE_CODE; static const std::string DEFAULT_COUNTRY_CODE; @@ -177,6 +184,7 @@ class Settings bool aogHeartbeatEnabled = DEFAULT_AOG_HEARTBEAT_ENABLED; bool vtEnabled = DEFAULT_VT_ENABLED; bool sectionControlOnConnect = DEFAULT_SECTION_CONTROL_ON_CONNECT; + bool sectionControlOnFieldOpen = DEFAULT_SECTION_CONTROL_ON_FIELD_OPEN; std::uint8_t tcVersion = DEFAULT_TC_VERSION; std::string languageCode = DEFAULT_LANGUAGE_CODE; std::string countryCode = DEFAULT_COUNTRY_CODE; diff --git a/readme.md b/readme.md index 997f0a3..63fed97 100644 --- a/readme.md +++ b/readme.md @@ -59,6 +59,8 @@ AOG-TaskController reads its configuration from a `settings.json` file located i | `nmeaSendEnabled` | `bool` | `true` | Enables cyclic NMEA2000 COG/SOG transmission. Requires `tecuEnabled`. | | `aogHeartbeatEnabled` | `bool` | `true` | Sends the 100 ms zero-section heartbeat when no implement is connected. Disable for AgOpenGPS versions before v6.8.2 beta 5. | | `vtEnabled` | `bool` | `true` | Enables the ISOBUS Virtual Terminal UI. | +| `sectionControlOnConnect` | `bool` | `true` | Switches section control on 2 s after an implement connects, as if AOG's SC button was pressed. | +| `sectionControlOnFieldOpen` | `bool` | `true` | Switches section control on when AOG opens a field (field name, PGN 0xF3, or section states, PGN 229, starting after a gap), as if AOG's SC button was pressed. | | `tcVersion` | `integer` | `3` | Task Controller version code, from `0` through `4`; `3` is Second Edition Draft. | | `languageCode` | `string` | `"en"` | Two-character language code advertised on ISOBUS. | | `countryCode` | `string` | `"US"` | Two-character country code advertised on ISOBUS. | diff --git a/src/app.cpp b/src/app.cpp index fcff617..2600a9b 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -447,6 +447,27 @@ void Application::setup_tecu_interfaces() } } +void Application::on_aog_field_opened(const std::string &how) +{ + if (!settings->is_section_control_on_field_open()) + { + return; + } + if ((0 != lastFieldOpenAppliedMs) && !isobus::SystemTiming::time_expired_ms(lastFieldOpenAppliedMs, FIELD_OPEN_DEDUPLICATE_MS)) + { + return; + } + lastFieldOpenAppliedMs = isobus::SystemTiming::get_timestamp_ms(); + if (0 == lastFieldOpenAppliedMs) + { + lastFieldOpenAppliedMs = 1; // 0 means "never" + } + + // Saves pressing AOG's SC button after every field change; same effect as PGN 241 "enabled" + log("TC") << "AOG " << how << ": switching section control on, as if AOG's SC button was pressed" << std::endl; + tcServer->update_section_control_enabled(true); +} + void Application::setup_udp_connections() { static std::uint8_t xteSid = 0; @@ -470,6 +491,28 @@ void Application::setup_udp_connections() } } tcServer->update_section_states(sectionStates); + + // Older AOG versions do not send the field name, but PGN 229 only flows while a field is open + const bool resumed = (0 == lastSectionStatesMs) || isobus::SystemTiming::time_expired_ms(lastSectionStatesMs, FIELD_CLOSED_GAP_MS); + lastSectionStatesMs = (0 == lastAogPacketMs) ? 1 : lastAogPacketMs; + if (resumed) + { + on_aog_field_opened("started sending section states (PGN 229)"); + } + } + else if (pgn == 0xF3) // 243 - Field name, empty when the field is closed + { + lastAogPacketMs = isobus::SystemTiming::get_timestamp_ms(); + const std::string fieldName(data.begin(), data.end()); + if (fieldName.empty()) + { + log("Field") << "AOG closed the field" << std::endl; + } + else + { + log("Field") << "AOG opened field '" << fieldName << "'" << std::endl; + on_aog_field_opened("opened field '" + fieldName + "'"); + } } else if (pgn == 0xF1 && !data.empty()) // 241 - Section Control { diff --git a/src/settings.cpp b/src/settings.cpp index 8b6c53f..7925979 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -71,6 +71,7 @@ bool Settings::load() aogHeartbeatEnabled = loadBoolean("aogHeartbeatEnabled", DEFAULT_AOG_HEARTBEAT_ENABLED); vtEnabled = loadBoolean("vtEnabled", DEFAULT_VT_ENABLED); sectionControlOnConnect = loadBoolean("sectionControlOnConnect", DEFAULT_SECTION_CONTROL_ON_CONNECT); + sectionControlOnFieldOpen = loadBoolean("sectionControlOnFieldOpen", DEFAULT_SECTION_CONTROL_ON_FIELD_OPEN); if (data.contains("tcVersion")) { @@ -145,6 +146,7 @@ bool Settings::save() const data["aogHeartbeatEnabled"] = aogHeartbeatEnabled; data["vtEnabled"] = vtEnabled; data["sectionControlOnConnect"] = sectionControlOnConnect; + data["sectionControlOnFieldOpen"] = sectionControlOnFieldOpen; data["tcVersion"] = tcVersion; data["languageCode"] = languageCode; data["countryCode"] = countryCode; @@ -274,6 +276,17 @@ bool Settings::set_section_control_on_connect(bool enabled, bool save) return set_boolean(sectionControlOnConnect, enabled, save); } +bool Settings::is_section_control_on_field_open() const +{ + std::scoped_lock lock(settingsMutex); + return sectionControlOnFieldOpen; +} + +bool Settings::set_section_control_on_field_open(bool enabled, bool save) +{ + return set_boolean(sectionControlOnFieldOpen, enabled, save); +} + bool Settings::is_aog_heartbeat_enabled() const { std::scoped_lock lock(settingsMutex); From 1e31bde9425dcc8ea78c23edb315edb1400fbd7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunics=20Bal=C3=A1zs?= Date: Sun, 8 Nov 2026 20:14:55 +0100 Subject: [PATCH 3/5] Keep the field name branch where the guidance track code adds its own PR #84 also adds a PGN 0xF3 branch to this if/else chain, between 0xF1 and 0xF2. In different places, the two merge without a conflict and the second never runs. In the same place, Git reports the conflict. No behaviour change. Co-Authored-By: Claude Opus 5.5 --- src/app.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index a264eab..4ce0397 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -537,6 +537,13 @@ void Application::setup_udp_connections() on_aog_field_opened("started sending section states (PGN 229)"); } } + else if (pgn == 0xF1 && !data.empty()) // 241 - Section Control + { + lastAogPacketMs = isobus::SystemTiming::get_timestamp_ms(); + std::uint8_t sectionControlState = data[0]; + log() << "Received request from AOG to change section control state to " << (sectionControlState == 1 ? "enabled" : "disabled") << std::endl; + tcServer->update_section_control_enabled(sectionControlState == 1); + } else if (pgn == 0xF3) // 243 - Field name, empty when the field is closed { lastAogPacketMs = isobus::SystemTiming::get_timestamp_ms(); @@ -551,13 +558,6 @@ void Application::setup_udp_connections() on_aog_field_opened("opened field '" + fieldName + "'"); } } - else if (pgn == 0xF1 && !data.empty()) // 241 - Section Control - { - lastAogPacketMs = isobus::SystemTiming::get_timestamp_ms(); - std::uint8_t sectionControlState = data[0]; - log() << "Received request from AOG to change section control state to " << (sectionControlState == 1 ? "enabled" : "disabled") << std::endl; - tcServer->update_section_control_enabled(sectionControlState == 1); - } else if (pgn == 0xF2 && data.size() >= 6) // Process Data { lastAogPacketMs = isobus::SystemTiming::get_timestamp_ms(); From 2f7b70ba7ac432b2c4d55ccdc26d745f84d8ac7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunics=20Bal=C3=A1zs?= Date: Tue, 10 Nov 2026 22:39:56 +0100 Subject: [PATCH 4/5] Only a new field name counts as a field opening once AOG sends one A gap of more than 2 s in PGN 229 (AOG stalling, lost UDP packets) was treated as a field opening and switched section control back on, even when the operator had switched it off and the same field stayed open. Once AOG has sent a field name (PGN 0xF3), only a change of field name switches section control on; the PGN 229 fallback stays for AOG versions that do not send the field name. Co-Authored-By: Claude Opus 5.5 --- include/app.hpp | 4 ++++ readme.md | 2 +- src/app.cpp | 12 +++++++++--- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/include/app.hpp b/include/app.hpp index 8c0f09f..4b90b0f 100644 --- a/include/app.hpp +++ b/include/app.hpp @@ -126,6 +126,10 @@ class Application /// The field name (PGN 0xF3) and PGN 229 resuming both report one field opening; act on it once static constexpr std::uint32_t FIELD_OPEN_DEDUPLICATE_MS = 5000; std::uint32_t lastFieldOpenAppliedMs = 0; ///< When on_aog_field_opened() last switched section control on, 0 = never + /// Set once AOG sent a field name (PGN 0xF3). From then on only a new field name counts as a field opening: + /// a gap in PGN 229 (AOG stalling, lost UDP packets) must not override the operator switching section control off. + bool aogSendsFieldName = false; + std::string openFieldName; ///< Last field name AOG reported, empty while no field is open static constexpr std::uint32_t SECTION_DEMAND_LOG_INTERVAL_MS = 200; static constexpr std::uint32_t SECTION_DEMAND_TIMEOUT_MS = 2000; ///< AOG sends PGN 229 every frame while a field is open diff --git a/readme.md b/readme.md index adf8f16..570f26f 100644 --- a/readme.md +++ b/readme.md @@ -65,7 +65,7 @@ AOG-TaskController reads its configuration from a `settings.json` file located i | `aogHeartbeatEnabled` | `bool` | `true` | Sends the 100 ms zero-section heartbeat when no implement is connected. Disable for AgOpenGPS versions before v6.8.2 beta 5. | | `vtEnabled` | `bool` | `true` | Enables the ISOBUS Virtual Terminal UI. | | `sectionControlOnConnect` | `bool` | `true` | Switches section control on 2 s after an implement connects, as if AOG's SC button was pressed. | -| `sectionControlOnFieldOpen` | `bool` | `true` | Switches section control on when AOG opens a field (field name, PGN 0xF3, or section states, PGN 229, starting after a gap), as if AOG's SC button was pressed. | +| `sectionControlOnFieldOpen` | `bool` | `true` | Switches section control on when AOG opens a field (a new field name, PGN 0xF3; with AOG versions that do not send the field name, section states, PGN 229, starting after a gap of more than 2 s), as if AOG's SC button was pressed. | | `tcVersion` | `integer` | `3` | Task Controller version code, from `0` through `4`; `3` is Second Edition Draft. | | `languageCode` | `string` | `"en"` | Two-character language code advertised on ISOBUS. | | `countryCode` | `string` | `"US"` | Two-character country code advertised on ISOBUS. | diff --git a/src/app.cpp b/src/app.cpp index 4ce0397..e8bb490 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -532,7 +532,7 @@ void Application::setup_udp_connections() sectionDemandChangesSinceLog++; } tcServer->update_section_states(sectionStates); - if (resumed) + if (resumed && !aogSendsFieldName) { on_aog_field_opened("started sending section states (PGN 229)"); } @@ -548,12 +548,18 @@ void Application::setup_udp_connections() { lastAogPacketMs = isobus::SystemTiming::get_timestamp_ms(); const std::string fieldName(data.begin(), data.end()); + aogSendsFieldName = true; if (fieldName.empty()) { - log("Field") << "AOG closed the field" << std::endl; + if (!openFieldName.empty()) + { + log("Field") << "AOG closed field '" << openFieldName << "'" << std::endl; + openFieldName.clear(); + } } - else + else if (fieldName != openFieldName) { + openFieldName = fieldName; log("Field") << "AOG opened field '" << fieldName << "'" << std::endl; on_aog_field_opened("opened field '" + fieldName + "'"); } From 3c90c2068206fc203c89154bccf35998bb496a6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunics=20Bal=C3=A1zs?= Date: Wed, 11 Nov 2026 08:26:49 +0100 Subject: [PATCH 5/5] Send the full setpoint on enable; send nothing on connect when the setting is off - After section control is switched on (connect, field open, PGN 241), send every condensed setpoint block and DDI 289, forced, from AOG's current demand (all off when none arrived in the last 2 s). Change-only sends never told the implement "all off" when AOG's Auto was off, so it kept running on its own state (ISO 11783-10 F.3.5.3 b). - With sectionControlOnConnect off, schedule nothing on connect. Before, DDI 160 = 0 went out on every connect, and after one SC press or field open every later connect switched on. Helpers match #106 (send_all_section_setpoint_states, forceWorkState) so the two merge cleanly. Co-Authored-By: Claude Opus 5.5 --- include/task_controller.hpp | 15 ++++++++-- src/app.cpp | 2 +- src/task_controller.cpp | 60 ++++++++++++++++++++++++++++++------- 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/include/task_controller.hpp b/include/task_controller.hpp index 6606829..01b1c61 100644 --- a/include/task_controller.hpp +++ b/include/task_controller.hpp @@ -126,15 +126,19 @@ class MyTCServer : public isobus::TaskControllerServer void update_section_states(std::vector §ionStates); void update_section_control_enabled(bool enabled); - /// @brief Sets the section control state applied to implements as they connect, before AOG asks for one. - void set_initial_section_control_enabled(bool enabled); + /// @brief Sets whether implements are switched to section control when they connect (settings: sectionControlOnConnect). + /// @details When on, section control starts enabled for implements that connect before AOG asks for a state. + /// When off, nothing is sent on connect, as before; AOG's SC button still switches every connected client. + void set_section_control_on_connect(bool enabled); /// @brief Sends GNSS quality (DDI 514) to every client that declares that DDI. /// @param quality NMEA 2000 GNSS Method: 0=No GNSS, 1=GNSS, 2=DGNSS, 3=Precise, 4=RTK Fixed, 5=RTK Float, 6=Estimated, 7=Manual, 8=Simulated void send_gnss_quality(std::uint8_t quality); private: - void send_section_setpoint_states(std::shared_ptr client, std::uint8_t ddiOffset); + /// @brief Sends every condensed setpoint block and DDI 289, even when unchanged (ISO 11783-10 F.3.5.3 b) + void send_all_section_setpoint_states(std::shared_ptr client); + void send_section_setpoint_states(std::shared_ptr client, std::uint8_t ddiOffset, bool forceWorkState = false); void send_section_control_state(std::shared_ptr client, bool enabled); /// @brief Sets the local auto/manual flag and commands DDI 160 if the client supports it. Caller must hold clientsMutex. void apply_section_control_state(std::shared_ptr client, ClientState &state, bool enabled); @@ -143,6 +147,11 @@ class MyTCServer : public isobus::TaskControllerServer std::map, ClientState> clients; std::map, std::queue>> uploadedPools; bool desiredSectionControlEnabled = false; ///< Applied to implements when they connect; follows AOG's ISOBUS SC button. Guarded by clientsMutex + bool sectionControlOnConnect = false; ///< Whether desiredSectionControlEnabled is applied on connect at all. Guarded by clientsMutex + + static constexpr std::uint32_t SECTION_DEMAND_TIMEOUT_MS = 2000; ///< AOG sends PGN 229 every frame while a field is open + std::vector aogSectionDemand; ///< Last section states from AOG (PGN 229). Guarded by clientsMutex + std::uint32_t lastSectionDemandMs = 0; ///< When AOG's section demand was last received, 0 = never. Guarded by clientsMutex /// @brief Guards clients and uploadedPools. /// diff --git a/src/app.cpp b/src/app.cpp index e8bb490..4c5ac0f 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -381,7 +381,7 @@ void Application::setup_task_controller_server() languageInterface.set_language_code(settings->get_language_code()); languageInterface.set_country_code(settings->get_country_code()); tcServer->initialize(); - tcServer->set_initial_section_control_enabled(settings->is_section_control_on_connect()); + tcServer->set_section_control_on_connect(settings->is_section_control_on_connect()); tcServer->set_task_totals_active(true); // TODO: make this dynamic based on status in AOG tcFunctionalities = std::make_unique(tcCF); tcFunctionalities->set_functionality_is_supported( diff --git a/src/task_controller.cpp b/src/task_controller.cpp index d17667c..48b7caf 100644 --- a/src/task_controller.cpp +++ b/src/task_controller.cpp @@ -895,8 +895,11 @@ void MyTCServer::request_measurement_commands() client.second.mark_measurement_commands_sent(); // Switch the implement to automatic shortly after it connects, so AOG's SC button - // does not need pressing on every connect. - client.second.schedule_initial_section_control(isobus::SystemTiming::get_timestamp_ms() + INITIAL_SECTION_CONTROL_DELAY_MS); + // does not need pressing on every connect. With the setting off, send nothing, as before. + if (sectionControlOnConnect) + { + client.second.schedule_initial_section_control(isobus::SystemTiming::get_timestamp_ms() + INITIAL_SECTION_CONTROL_DELAY_MS); + } } if (client.second.take_due_initial_section_control(isobus::SystemTiming::get_timestamp_ms())) { @@ -909,6 +912,12 @@ void MyTCServer::request_measurement_commands() void MyTCServer::update_section_states(std::vector §ionStates) { std::lock_guard lock(clientsMutex); + aogSectionDemand = sectionStates; + lastSectionDemandMs = isobus::SystemTiming::get_timestamp_ms(); + if (0 == lastSectionDemandMs) + { + lastSectionDemandMs = 1; // 0 means "never" + } for (auto &client : clients) { auto &state = client.second; @@ -962,9 +971,10 @@ void MyTCServer::update_section_control_enabled(bool enabled) } } -void MyTCServer::set_initial_section_control_enabled(bool enabled) +void MyTCServer::set_section_control_on_connect(bool enabled) { std::lock_guard lock(clientsMutex); + sectionControlOnConnect = enabled; desiredSectionControlEnabled = enabled; } @@ -973,12 +983,42 @@ void MyTCServer::apply_section_control_state(std::shared_ptr 0) + // Skip clients that don't have any sections configured (e.g., tractors or other non-implement devices) + if (state.get_number_of_sections() == 0) + { + return; + } + + // Only send ISOBUS command to clients that support SectionControlState DDI + if (state.has_element_number_for_ddi(isobus::DataDescriptionIndex::SectionControlState)) { send_section_control_state(client, enabled); } + + if (enabled) + { + // ISO 11783-10 F.3.5.3 b): after switching to automatic, send the setpoint. update_section_states() + // only sends changes, so an all-off demand (AOG's Auto off after a field change) would never reach + // the implement and it would keep running on its own state. A demand older than + // SECTION_DEMAND_TIMEOUT_MS (no field open) counts as all off. + const bool demandCurrent = (0 != lastSectionDemandMs) && !isobus::SystemTiming::time_expired_ms(lastSectionDemandMs, SECTION_DEMAND_TIMEOUT_MS); + for (std::uint8_t i = 0; i < state.get_number_of_sections(); i++) + { + const bool on = demandCurrent && (i < aogSectionDemand.size()) && aogSectionDemand[i]; + state.set_section_setpoint_state(i, on ? SectionState::ON : SectionState::OFF); + } + send_all_section_setpoint_states(client); + } +} + +void MyTCServer::send_all_section_setpoint_states(std::shared_ptr client) +{ + std::lock_guard lock(clientsMutex); + const std::uint8_t numberOfSections = clients[client].get_number_of_sections(); + for (std::uint8_t block = 0; block * NUMBER_SECTIONS_PER_CONDENSED_MESSAGE < numberOfSections; block++) + { + send_section_setpoint_states(client, block, true); + } } void MyTCServer::send_gnss_quality(std::uint8_t quality) @@ -994,7 +1034,7 @@ void MyTCServer::send_gnss_quality(std::uint8_t quality) } } -void MyTCServer::send_section_setpoint_states(std::shared_ptr client, std::uint8_t ddiOffset) +void MyTCServer::send_section_setpoint_states(std::shared_ptr client, std::uint8_t ddiOffset, bool forceWorkState) { std::lock_guard lock(clientsMutex); std::uint8_t sectionOffset = ddiOffset * NUMBER_SECTIONS_PER_CONDENSED_MESSAGE; @@ -1014,12 +1054,12 @@ 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); } - else if (!clients[client].has_element_number_for_ddi(isobus::DataDescriptionIndex::SetpointWorkState)) + else if (!forceWorkState && !clients[client].has_element_number_for_ddi(isobus::DataDescriptionIndex::SetpointWorkState)) { log("TC Server") << "DDI 289 (SetpointWorkState) not available!" << std::endl; } @@ -1058,7 +1098,7 @@ void MyTCServer::send_section_setpoint_states(std::shared_ptr