From 55f8549b4f11c88cffea2be85b162825b9c4226b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunics=20Bal=C3=A1zs?= Date: Wed, 11 Nov 2026 13:24:42 +0100 Subject: [PATCH 1/2] Add allSectionsFollowFirst: drive every implement section from AOG section 1 For implements that declare several sections that really are one, such as an RDS Artemis with three dispensers on one working width. With the setting on, every implement section follows AOG's section 1, and the heartbeat reports a single section to AOG (on while any implement section is on). Default off. Co-Authored-By: Claude Opus 5.5 --- include/settings.hpp | 10 ++++++++++ readme.md | 1 + src/app.cpp | 19 +++++++++++++++++++ src/settings.cpp | 13 +++++++++++++ 4 files changed, 43 insertions(+) diff --git a/include/settings.hpp b/include/settings.hpp index 06f0278..04d052a 100644 --- a/include/settings.hpp +++ b/include/settings.hpp @@ -98,6 +98,14 @@ class Settings */ bool set_vt_enabled(bool enabled, bool save = true); + /// @brief Check whether every implement section follows AOG's section 1. + /// @details For implements that declare several sections (or booms) that really are one, such as + /// several dispensers on one working width. AOG then sees the implement as a single section. + bool is_all_sections_follow_first() const; + + /// @brief Set whether every implement section follows AOG's section 1. + bool set_all_sections_follow_first(bool enabled, bool save = true); + /** * @brief Get the configured TC version * @return The configured version (0-4, default 3) @@ -161,6 +169,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_ALL_SECTIONS_FOLLOW_FIRST = false; 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 +178,7 @@ class Settings bool nmeaSendEnabled = DEFAULT_NMEA_SEND_ENABLED; bool aogHeartbeatEnabled = DEFAULT_AOG_HEARTBEAT_ENABLED; bool vtEnabled = DEFAULT_VT_ENABLED; + bool allSectionsFollowFirst = DEFAULT_ALL_SECTIONS_FOLLOW_FIRST; 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 8c67159..1ab0222 100644 --- a/readme.md +++ b/readme.md @@ -64,6 +64,7 @@ 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. | +| `allSectionsFollowFirst` | `bool` | `false` | Every implement section follows AOG's section 1, and AOG sees the implement as one section (on while any section is on). For implements that declare several sections or booms that really are one, such as several dispensers on one working width. Set AOG to 1 section. | | `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 a697e8b..20cb58d 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -507,6 +507,11 @@ void Application::setup_udp_connections() sectionDemand = sectionStates; sectionDemandChangesSinceLog++; } + if (settings->is_all_sections_follow_first()) + { + // The implement's sections are really one: all follow AOG's section 1 (sectionDemand above stays as AOG sent it) + std::fill(sectionStates.begin(), sectionStates.end(), sectionStates.front()); + } tcServer->update_section_states(sectionStates); } else if (pgn == 0xF1 && !data.empty()) // 241 - Section Control @@ -739,6 +744,20 @@ bool Application::update() } anyClientWithSections = true; + + if (settings->is_all_sections_follow_first()) + { + // AOG is set up with one section: report it on while any implement section is on + bool anyOn = false; + for (std::uint8_t i = 0; i < state.get_number_of_sections(); i++) + { + anyOn = anyOn || (state.get_section_actual_state(i) == SectionState::ON); + } + std::vector oneSection = { state.is_section_control_enabled(), 1, static_cast(anyOn ? 1 : 0) }; + udpConnections->send(0x80, 0xF0, oneSection); + continue; + } + std::vector data = { state.is_section_control_enabled(), state.get_number_of_sections() }; std::uint8_t sectionIndex = 0; diff --git a/src/settings.cpp b/src/settings.cpp index 960927a..cdc73d8 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); + allSectionsFollowFirst = loadBoolean("allSectionsFollowFirst", DEFAULT_ALL_SECTIONS_FOLLOW_FIRST); if (data.contains("tcVersion")) { @@ -143,6 +144,7 @@ bool Settings::save() const data["nmeaSendEnabled"] = nmeaSendEnabled; data["aogHeartbeatEnabled"] = aogHeartbeatEnabled; data["vtEnabled"] = vtEnabled; + data["allSectionsFollowFirst"] = allSectionsFollowFirst; 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_all_sections_follow_first() const +{ + std::scoped_lock lock(settingsMutex); + return allSectionsFollowFirst; +} + +bool Settings::set_all_sections_follow_first(bool enabled, bool save) +{ + return set_boolean(allSectionsFollowFirst, enabled, save); +} + bool Settings::is_aog_heartbeat_enabled() const { std::scoped_lock lock(settingsMutex); From 46e3b32f57a91b3de1354ec56c77257884ae588c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunics=20Bal=C3=A1zs?= Date: Wed, 11 Nov 2026 19:28:06 +0100 Subject: [PATCH 2/2] allSectionsFollowFirst: cover sections past 64, one combined heartbeat, demand from bit 1 only PGN 229 has 64 bits, so sections 65-255 kept their old setpoint; the follow-first vector now covers every section a DDOP can declare. With several section-controlled implements the heartbeat sent one one-section report per implement. It now sends a single report: auto while any implement is in auto, on while any implement section is on. check_section_demand looked at AOG's bit for each implement section; in follow-first mode only AOG's section 1 is demand, so a stray bit can no longer hide the working-without-demand warning. Addresses Sourcery's review of #130. Co-Authored-By: Claude Opus 5.5 --- readme.md | 2 +- src/app.cpp | 29 ++++++++++++++++++++--------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/readme.md b/readme.md index 1ab0222..e769bf8 100644 --- a/readme.md +++ b/readme.md @@ -64,7 +64,7 @@ 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. | -| `allSectionsFollowFirst` | `bool` | `false` | Every implement section follows AOG's section 1, and AOG sees the implement as one section (on while any section is on). For implements that declare several sections or booms that really are one, such as several dispensers on one working width. Set AOG to 1 section. | +| `allSectionsFollowFirst` | `bool` | `false` | Every implement section follows AOG's section 1, and AOG sees the implement as one section (on while any section is on). For implements that declare several sections or booms that really are one, such as several dispensers on one working width. Set AOG to 1 section. Applies to every connected implement: with two section-controlled implements, both follow section 1 and AOG gets one combined report. | | `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 20cb58d..82d6ba4 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -509,8 +509,9 @@ void Application::setup_udp_connections() } if (settings->is_all_sections_follow_first()) { - // The implement's sections are really one: all follow AOG's section 1 (sectionDemand above stays as AOG sent it) - std::fill(sectionStates.begin(), sectionStates.end(), sectionStates.front()); + // The implement's sections are really one: all follow AOG's section 1 (sectionDemand above stays as AOG sent it). + // PGN 229 carries 64 bits; cover every section a DDOP can declare (up to 255), not just the first 64. + sectionStates.assign(256, sectionStates.front()); } tcServer->update_section_states(sectionStates); } @@ -733,6 +734,9 @@ bool Application::update() if (isobus::SystemTiming::time_expired_ms(lastHeartbeatTransmit, 100)) { bool anyClientWithSections = false; + const bool followFirst = settings->is_all_sections_follow_first(); + bool followFirstEnabled = false; + bool followFirstOn = false; for (auto &client : tcServer->get_clients()) { auto &state = client.second; @@ -745,16 +749,15 @@ bool Application::update() anyClientWithSections = true; - if (settings->is_all_sections_follow_first()) + if (followFirst) { - // AOG is set up with one section: report it on while any implement section is on - bool anyOn = false; + // AOG is set up with one section, and section 1 drives every connected implement: + // collect them all into one report, on while any implement section is on + followFirstEnabled = followFirstEnabled || state.is_section_control_enabled(); for (std::uint8_t i = 0; i < state.get_number_of_sections(); i++) { - anyOn = anyOn || (state.get_section_actual_state(i) == SectionState::ON); + followFirstOn = followFirstOn || (state.get_section_actual_state(i) == SectionState::ON); } - std::vector oneSection = { state.is_section_control_enabled(), 1, static_cast(anyOn ? 1 : 0) }; - udpConnections->send(0x80, 0xF0, oneSection); continue; } @@ -777,6 +780,12 @@ bool Application::update() udpConnections->send(0x80, 0xF0, data); } + if (followFirst && anyClientWithSections) + { + std::vector oneSection = { followFirstEnabled, 1, static_cast(followFirstOn ? 1 : 0) }; + udpConnections->send(0x80, 0xF0, oneSection); + } + // If no clients with sections, send heartbeat with 0 sections if (settings->is_aog_heartbeat_enabled() && !anyClientWithSections) { @@ -1048,7 +1057,9 @@ void Application::check_section_demand() for (std::uint8_t i = 0; i < state.get_number_of_sections(); i++) { actual.push_back(SectionState::ON == state.get_section_actual_state(i)); - demanded = demanded || (arriving && (i < sectionDemand.size()) && sectionDemand[i]); + // With allSectionsFollowFirst, AOG has one section: only its bit is demand, any others are padding + const std::uint8_t demandIndex = settings->is_all_sections_follow_first() ? 0 : i; + demanded = demanded || (arriving && (demandIndex < sectionDemand.size()) && sectionDemand[demandIndex]); } const bool working = (actual.end() != std::find(actual.begin(), actual.end(), true));