Skip to content
Merged
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
11 changes: 11 additions & 0 deletions include/app.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,17 @@ class Application
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);
/// 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
static constexpr std::uint32_t WORKING_WITHOUT_DEMAND_WARNING_MS = 10000;
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
23 changes: 22 additions & 1 deletion include/task_controller.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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 @@ -56,6 +57,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 @@ -69,6 +74,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 @@ -120,17 +126,32 @@ class MyTCServer : public isobus::TaskControllerServer
void update_section_states(std::vector<bool> &sectionStates);
void update_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<isobus::ControlFunction> 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<isobus::ControlFunction> client);
void send_section_setpoint_states(std::shared_ptr<isobus::ControlFunction> client, std::uint8_t ddiOffset, bool forceWorkState = false);
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
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<bool> 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.
///
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,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 (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. |
Expand Down
48 changes: 48 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_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<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))
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Resolved
{
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 Down Expand Up @@ -501,13 +523,19 @@ void Application::setup_udp_connections()
sectionStates.push_back(data[j] & (1 << i));
}
}
// Older AOG versions do not send the field name, but PGN 229 only flows while a field is open
const bool resumed = (0 == lastSectionDemandMs) || isobus::SystemTiming::time_expired_ms(lastSectionDemandMs, SECTION_DEMAND_TIMEOUT_MS);
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Resolved
lastSectionDemandMs = lastAogPacketMs;
if (sectionStates != sectionDemand)
{
sectionDemand = sectionStates;
sectionDemandChangesSinceLog++;
}
tcServer->update_section_states(sectionStates);
if (resumed && !aogSendsFieldName)
{
on_aog_field_opened("started sending section states (PGN 229)");
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Resolved
}
}
else if (pgn == 0xF1 && !data.empty()) // 241 - Section Control
{
Expand All @@ -516,6 +544,26 @@ void Application::setup_udp_connections()
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();
const std::string fieldName(data.begin(), data.end());
aogSendsFieldName = true;
if (fieldName.empty())
{
if (!openFieldName.empty())
{
log("Field") << "AOG closed field '" << openFieldName << "'" << std::endl;
openFieldName.clear();
}
}
else if (fieldName != openFieldName)
{
openFieldName = fieldName;
log("Field") << "AOG opened field '" << fieldName << "'" << std::endl;
on_aog_field_opened("opened field '" + fieldName + "'");
}
}
else if (pgn == 0xF2 && data.size() >= 6) // Process Data
{
lastAogPacketMs = isobus::SystemTiming::get_timestamp_ms();
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
Loading
Loading