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 06f0278..d66fe4c 100644 --- a/include/settings.hpp +++ b/include/settings.hpp @@ -98,6 +98,18 @@ 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 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) @@ -161,6 +173,8 @@ 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 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; @@ -169,6 +183,8 @@ 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; + 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/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/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 9ac8802..2600a9b 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( @@ -446,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; @@ -469,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 960927a..7925979 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -70,6 +70,8 @@ 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); + sectionControlOnFieldOpen = loadBoolean("sectionControlOnFieldOpen", DEFAULT_SECTION_CONTROL_ON_FIELD_OPEN); if (data.contains("tcVersion")) { @@ -143,6 +145,8 @@ bool Settings::save() const data["nmeaSendEnabled"] = nmeaSendEnabled; data["aogHeartbeatEnabled"] = aogHeartbeatEnabled; data["vtEnabled"] = vtEnabled; + data["sectionControlOnConnect"] = sectionControlOnConnect; + data["sectionControlOnFieldOpen"] = sectionControlOnFieldOpen; data["tcVersion"] = tcVersion; data["languageCode"] = languageCode; data["countryCode"] = countryCode; @@ -261,6 +265,28 @@ 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_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); 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); } }