Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions include/app.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions include/settings.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions include/task_controller.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <cstdint>
#include <map>
#include <mutex>
#include <optional>
#include <queue>

constexpr std::uint8_t NUMBER_SECTIONS_PER_CONDENSED_MESSAGE = 16;
Expand Down Expand Up @@ -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;
Expand All @@ -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<std::uint32_t> initialSectionControlDueMs; ///< When to apply the TC's section control state after connect
std::map<isobus::DataDescriptionIndex, std::uint16_t> ddiToElementNumber; ///< Mapping of DDI to element number // TODO: better way to do this?

std::uint8_t numberOfSections;
Expand Down Expand Up @@ -116,13 +122,19 @@ class MyTCServer : public isobus::TaskControllerServer
void update_section_states(std::vector<bool> &sectionStates);
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<isobus::ControlFunction> client, std::uint8_t ddiOffset);
void send_section_control_state(std::shared_ptr<isobus::ControlFunction> 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<isobus::ControlFunction> client, ClientState &state, bool enabled);
bool is_ddi_settable(std::shared_ptr<isobus::ControlFunction> client, std::uint16_t ddi);

std::map<std::shared_ptr<isobus::ControlFunction>, ClientState> clients;
std::map<std::shared_ptr<isobus::ControlFunction>, std::queue<std::vector<std::uint8_t>>> uploadedPools;
bool desiredSectionControlEnabled = false; ///< Applied to implements when they connect; follows AOG's ISOBUS SC button. Guarded by clientsMutex

/// @brief Guards clients and uploadedPools.
///
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
44 changes: 44 additions & 0 deletions src/app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<isobus::ControlFunctionFunctionalities>(tcCF);
tcFunctionalities->set_functionality_is_supported(
Expand Down Expand Up @@ -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;
Expand All @@ -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
{
Expand Down
26 changes: 26 additions & 0 deletions src/settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
{
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
61 changes: 50 additions & 11 deletions src/task_controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <bitset>
#include <filesystem>
Expand All @@ -21,6 +22,10 @@
#include <set>
#include <sstream>

/// 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)
{
Expand Down Expand Up @@ -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<std::int32_t>(nowMs - *initialSectionControlDueMs) < 0)
{
return false;
}
initialSectionControlDueMs.reset();
return true;
}

bool ClientState::uses_per_element_control() const
{
return usesPerElementControl;
Expand Down Expand Up @@ -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);
}
}
}
Expand Down Expand Up @@ -932,20 +962,29 @@ void MyTCServer::update_section_states(std::vector<bool> &sectionStates)
void MyTCServer::update_section_control_enabled(bool enabled)
{
std::lock_guard<std::recursive_mutex> 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<std::recursive_mutex> lock(clientsMutex);
desiredSectionControlEnabled = enabled;
}

void MyTCServer::apply_section_control_state(std::shared_ptr<isobus::ControlFunction> 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);
}
}

Expand Down