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/2] 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/2] 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);