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
2 changes: 2 additions & 0 deletions docs/CONCURRENCY.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ registration point itself defers to an `update()` call, or invokes the listener/
object. `TractorFacilities`' PGN 65032 callback may still run once after removal, which is harmless: it only
logs and never touches the object. `TractorFacilities::providedFacilities` is under its own mutex
(set on the main thread, read when answering a PGN 65033 request).
- **`TractorFacilities::timeDateActive`** — atomic: set on the main thread from the TECU plan, read on the
background thread when answering a request for PGN 65033 or PGN 65254.

## What's *not* protected yet — known gap

Expand Down
7 changes: 4 additions & 3 deletions docs/PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ Common NAME fields: Industry Group `2` (Agricultural), Device Class `0`, Manufac
| `0xFC8E` (Control Function Functionalities) | At claim + periodic | TECU | Announces Class 1 BasicTractorECUServer (no options). |
| `0xFE48`, `0xFE49`, `0xF022` (PGN 65096, 65097, 61474) | 100 ms | TECU | Wheel-based speed, ground-based speed, machine selected speed (with distance and direction). As a secondary, only those no other Tractor ECU provides (§5.7). |
| `0xFE09` (PGN 65033 Tractor Facilities) | Power-up + on request, and when it changes | TECU | 8-byte facility bitmask advertising which PGNs the TECU actually broadcasts. See §5.6. |
| `0xFEE6` (PGN 65254 Time/Date) | On request only (ISO 11783-7 B.1) | TECU | UTC from the PC clock plus the local offset, including daylight saving time. A secondary TECU leaves requests to a Tractor ECU that provides it. |
| NMEA2000 COG/SOG | Periodic | TECU | Optional course/speed over ground. Not while another Tractor ECU sends it (§5.7). |
| GNSS Quality (DDI 514, via `0xCB00` Process Data) | 250 ms | TC | GPS fix quality from AgIO (PGN `0xD6`, see §2.5), sent to each client whose DDOP declares DDI 514 as settable. Falls back to `1` if `0xD6` has never arrived, and to `0` once it stops arriving (see §2.5). |

Expand Down Expand Up @@ -313,14 +314,14 @@ When the TECU is enabled, the TC responds to PGN 65033 requests (ISO 11783-7 B.2
| 1 | 8,7 | TECU class | Always `00` (Class 1). |
| 1 | 2 | Ground-based speed (PGN 65097) | The TECU sends it: always as the primary; as a secondary only if no other Tractor ECU provides it (§5.7). |
| 1 | 3 | Wheel-based speed (PGN 65096) | Same rule. |
| 3 | 8 | Time/date (PGN 65254) | Our TECU answers requests for it: as the primary always, as a secondary only when no lower Tractor ECU advertises or sends it. |
| 3 | 7,6 | Ground-based distance + direction | Same as ground-based speed. |
| 3 | 5,4 | Wheel-based distance + direction | Same as wheel-based speed. |
| 5 | 3 | Selected speed (machine selected speed, PGN 61474) | Same rule. |

**Facilities NOT advertised (bits always 0):**

- Engine speed — no engine CAN access.
- Time/date (PGN 65254) — the TC does not broadcast it.
- Power management — no key switch or power timer signals.
- Hitch position / in-work / draft — the hydraulic lift output is a command we issue, not measured feedback; implements would trust it for work-state logic.
- PTO shaft speed / engagement — no PTO sensor.
Expand All @@ -331,7 +332,7 @@ When the TECU is enabled, the TC responds to PGN 65033 requests (ISO 11783-7 B.2
- Front hitch / PTO — no front hitch or PTO sensors.
- All reserved bits (byte 2 bits 2–1, byte 4 bits 3–1, byte 5 bit 5, byte 7, byte 8 including the reserved-bit indicator at bit 1).

**Default payload** (TECU enabled as the primary): `[0x06, 0x00, 0x78, 0x00, 0x04, 0x00, 0x00, 0x00]`.
**Default payload** (TECU enabled as the primary): `[0x06, 0x00, 0xF8, 0x00, 0x04, 0x00, 0x00, 0x00]`.

**PGN 65032 (Required Tractor Facilities):** When an implement broadcasts what it needs, the TC logs the request at debug level. The response is not modified based on the implement's requirements — a facility bit is set to 1 only when backed by a live broadcast.

Expand All @@ -342,7 +343,7 @@ ISO 11783-9:2012 §4.4.2.1 allows several Tractor ECUs on the implement bus. The
- **`tecuEnabled` picks the role.** `true`: AOG is the tractor's TECU. At startup the TC's own address claim makes every control function on the bus announce itself; if no other Tractor ECU (function code 134) is there, our TECU claims `240` as the primary (function instance 0) and sends everything in §5.2, otherwise it starts as a secondary. `false` ("the tractor has its own TECU"): always a secondary at an arbitrary address, with a function instance above every Tractor ECU on the bus. With no other Tractor ECU there, every message is a gap and it sends them all; once the tractor's appears, it cuts back to what that one lacks.
- **Never taking `240` from a tractor:** our TECU NAME is arbitrary-address-capable in both roles. That is the most significant NAME bit, so in an address claim contest for `240` our primary loses to a fixed-address Tractor ECU and moves elsewhere, instead of knocking the tractor's ECU off the bus.
- **Stepping down at runtime** (`tecuEnabled: true`): once a second the TC checks for other Tractor ECUs. If another one with function instance 0 appears while ours is the primary, or ours lost `240`, our TECU is rebuilt as a secondary above every Tractor ECU's function instance. Another secondary appearing is no reason to step down. The operator sees a hardware message ("Tractor ECU found: AOG adds only missing data").
- **What a secondary sends:** nothing until the other Tractor ECUs have answered PGN 65033 (and at least 1 s has passed) or 2.5 s have passed. Then each of ground-based speed, wheel-based speed, machine selected speed, J1939 vehicle direction/speed and NMEA 2000 COG/SOG is sent only if no Tractor ECU with a lower function instance advertises it in PGN 65033 *and* none has sent it in the last 3 s. Only lower instances count, so two secondaries never wait on each other. Its PGN 65033 advertises exactly what it sends, and is re-sent when that changes.
- **What a secondary sends:** nothing until the other Tractor ECUs have answered PGN 65033 (and at least 1 s has passed) or 2.5 s have passed. Then each of ground-based speed, wheel-based speed, machine selected speed, J1939 vehicle direction/speed and NMEA 2000 COG/SOG is sent only if no Tractor ECU with a lower function instance advertises it in PGN 65033 *and* none has sent it in the last 3 s. Answering requests for time/date follows the same rule, except that a lower Tractor ECU that has sent time/date counts as providing it until it leaves the bus: it may only send it when asked, so no time window would do. Time/date is advertised only while it can be answered (request handler registered, PC clock between 1985 and 2235). Only lower instances count, so two secondaries never wait on each other. Its PGN 65033 advertises exactly what it sends, and is re-sent when that changes.
- A secondary does not step back up to primary if the tractor's Tractor ECU leaves; it then sends everything, still as a secondary.

---
Expand Down
38 changes: 36 additions & 2 deletions include/tecu_coexistence.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,39 @@ struct OtherTecu
std::uint16_t manufacturerCode = 0;
};

/// @brief The other Tractor ECUs that have sent Time/Date (PGN 65254) since they appeared on the bus.
///
/// Time/Date is only sent on request (ISO 11783-7 B.1), so a time window cannot tell whether a lower Tractor
/// ECU provides it: with requests further apart than the window, both TECUs would answer every one. Instead a
/// TECU counts as providing Time/Date from the first time it sends it until it leaves the bus (forget()).
/// mark_sent() runs on the CAN stack's background thread, forget() and any() on the main thread.
class TimeDateProviders
{
public:
void mark_sent(std::uint8_t address)
{
sent[address] = true;
}
void forget(std::uint8_t address)
{
sent[address] = false;
}
bool any() const
{
for (const auto &address : sent)
{
if (address)
{
return true;
}
}
return false;
}

private:
std::array<std::atomic<bool>, 256> sent{};
};

/// @brief Tracks the other Tractor ECUs: who they are, the facilities they advertise and what they send.
///
/// refresh() and the queries run on the main thread. on_frame() and the PGN 65033 callback run on the
Expand All @@ -108,7 +141,7 @@ struct OtherTecu
class OtherTecuMonitor
{
public:
/// How long a message seen from another TECU counts as "sent by it"
/// How long a message seen from another TECU counts as "sent by it" (not time/date, see TimeDateProviders)
static constexpr std::uint32_t SENT_BY_OTHERS_WINDOW_MS = 3000;

/// @brief Register the PGN 65033 listener. Call once.
Expand All @@ -130,7 +163,7 @@ class OtherTecuMonitor
/// @brief Whether every other TECU has answered with PGN 65033
bool all_facilities_received() const;

/// @brief Messages seen from another TECU within SENT_BY_OTHERS_WINDOW_MS
/// @brief Messages seen from another TECU within SENT_BY_OTHERS_WINDOW_MS. Time/date: sent by a TECU still on the bus
std::array<bool, TECU_MESSAGE_COUNT> sent_by_others() const;

/// @brief Whether we already asked this address for PGN 65033 since it appeared
Expand All @@ -142,6 +175,7 @@ class OtherTecuMonitor

std::array<std::atomic<bool>, 256> isOtherTecuAddress{};
std::array<std::atomic<std::uint32_t>, TECU_MESSAGE_COUNT> lastSentByOthersMs{}; ///< 0 = never
TimeDateProviders timeDateProviders;

mutable std::mutex mutex; ///< Guards the members below
std::map<std::uint8_t, Facilities> facilitiesByAddress;
Expand Down
31 changes: 25 additions & 6 deletions include/tractor_facilities.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include <array>
#include <atomic>
#include <cstdint>
#include <ctime>
#include <memory>
#include <mutex>
#include <set>
Expand Down Expand Up @@ -94,13 +95,22 @@ std::array<std::uint8_t, 8> encode_facilities(const Facilities &f);
/// @brief Decode an 8-byte PGN 65033 payload into a Facilities struct.
Facilities decode_facilities(const std::array<std::uint8_t, 8> &payload);

/// @brief The minutes to add to UTC to get local time, positive east of Greenwich.
/// Compares the two broken-down clocks, so it includes daylight saving time and needs no time zone lookup.
int utc_offset_minutes(const std::tm &utc, const std::tm &local);

/// @brief Encode the Time/Date payload (PGN 65254, ISO 11783-7 B.1) from the same instant as UTC and local time.
/// @param milliseconds Milliseconds into the current second; the message has a resolution of 250 ms.
std::array<std::uint8_t, 8> encode_time_date(const std::tm &utc, const std::tm &local, int milliseconds);

/// @brief Implements ISO 11783-7 Tractor Facilities response (PGN 65033)
/// and Required Tractor Facilities diagnostic logging (PGN 65032).
class TractorFacilities
{
public:
static constexpr std::uint32_t PGN_TRACTOR_FACILITIES = 65033; ///< 0xFE09
static constexpr std::uint32_t PGN_REQUIRED_TRACTOR_FACILITIES = 65032; ///< 0xFE08
static constexpr std::uint32_t PGN_TIME_DATE = 65254; ///< 0xFEE6

TractorFacilities(std::shared_ptr<isobus::InternalControlFunction> tecuCF,
std::shared_ptr<Settings> settings);
Expand Down Expand Up @@ -129,8 +139,9 @@ class TractorFacilities
/// @brief Provide a pointer to the NMEA 2000 message interface.
void set_nmea2000_message_interface(isobus::NMEA2000MessageInterface *iface);

/// @brief Set whether time/date (PGN 65254 / FEE6) is being broadcast.
/// When true, the time/date facility bit is advertised in PGN 65033.
/// @brief Set whether our TECU provides time/date (PGN 65254 / FEE6).
/// When true, it answers requests for that PGN and advertises the time/date facility bit in PGN 65033.
/// When false, it leaves requests to the other Tractor ECU. It is never sent unrequested (ISO 11783-7 B.1).
void set_time_date_active(bool active);

/// @brief Advertise exactly these facilities (the time/date bit still follows set_time_date_active()).
Expand All @@ -149,6 +160,13 @@ class TractorFacilities
isobus::AcknowledgementType &acknowledgeType,
void *parentPointer);

/// @brief Static PGN-request callback registered for PGN 65254 (Time/Date).
static bool on_time_date_request(std::uint32_t parameterGroupNumber,
std::shared_ptr<isobus::ControlFunction> requestingControlFunction,
bool &acknowledge,
isobus::AcknowledgementType &acknowledgeType,
void *parentPointer);

/// @brief Static global-PGN callback for PGN 65032 (Required Tractor
/// Facilities). Diagnostic only — logs the request unconditionally
/// (not gated by log level) and never changes our response.
Expand All @@ -158,11 +176,12 @@ class TractorFacilities
std::shared_ptr<Settings> settings;
isobus::SpeedMessagesInterface *speedMessagesInterface = nullptr;
isobus::NMEA2000MessageInterface *nmea2000MessageInterface = nullptr;
/// Whether we are broadcasting PGN 65254 (FEE6). Atomic because
/// set_time_date_active() and build_payload() are called from different
/// isobus callback contexts (see docs/CONCURRENCY.md) that can run
/// concurrently on different threads.
/// Whether we provide PGN 65254 (FEE6). Atomic because set_time_date_active()
/// runs on the main thread, while the PGN request callbacks that read it run
/// on the CAN stack's thread (see docs/CONCURRENCY.md).
std::atomic<bool> timeDateActive{ false };
std::atomic<bool> timeDateRequestLogged{ false }; ///< To log only the first request answered
std::atomic<bool> timeDateRequestHandlerRegistered{ false }; ///< Without it, requests for PGN 65254 go unanswered

/// Set by set_provided_facilities() on the main thread, read by build_payload() from PGN request callbacks
mutable std::mutex providedFacilitiesMutex;
Expand Down
4 changes: 3 additions & 1 deletion src/app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ void Application::setup_tecu_interfaces()

// The primary sends everything. A secondary sends nothing until it knows what the other
// Tractor ECUs provide (update_tecu()), so it never doubles one of their messages.
const TecuMessagePlan plan = plan_tecu_messages(TecuRole::Primary == tecuRole, Facilities{}, {});
const TecuMessagePlan plan = (TecuRole::Primary == tecuRole) ? plan_tecu_messages(true, Facilities{}, {}) : TecuMessagePlan{};

log("Init") << "Creating Speed Messages Interface on TECU..." << std::endl;
create_speed_messages_interface(plan);
Expand All @@ -502,6 +502,7 @@ void Application::setup_tecu_interfaces()
tractorFacilities->set_speed_messages_interface(speedMessagesInterface.get());
tractorFacilities->set_nmea2000_message_interface(nmea2000MessageInterface.get());
tractorFacilities->set_provided_facilities(facilities_for_plan(plan));
tractorFacilities->set_time_date_active(plan.sends(TecuMessage::TimeDate));
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Resolved
tractorFacilities->initialize();
tecuPlan = plan;
tecuInterfacesReady = true;
Expand Down Expand Up @@ -636,6 +637,7 @@ void Application::apply_tecu_plan(const TecuMessagePlan &plan)
if (tractorFacilities)
{
tractorFacilities->set_provided_facilities(facilities_for_plan(plan));
tractorFacilities->set_time_date_active(plan.sends(TecuMessage::TimeDate));
if (tractorFacilitiesSentOnPowerUp)
{
// Implements that cached our earlier response learn what changed
Expand Down
24 changes: 23 additions & 1 deletion src/tecu_coexistence.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,17 @@ std::vector<OtherTecu> OtherTecuMonitor::refresh(std::uint8_t maxFunctionInstanc

std::lock_guard<std::mutex> lock(mutex);
for (std::size_t address = 0; address < isOtherTecuAddress.size(); address++)
{
// A TECU that left, or a different one now at that address, no longer provides time/date. Done before
// isOtherTecuAddress is set, so a TECU that just appeared cannot have its first time/date cleared.
const auto name = names.find(static_cast<std::uint8_t>(address));
const auto previous = nameByAddress.find(static_cast<std::uint8_t>(address));
if ((names.end() == name) || (nameByAddress.end() == previous) || (name->second != previous->second))
{
timeDateProviders.forget(static_cast<std::uint8_t>(address));
}
}
for (std::size_t address = 0; address < isOtherTecuAddress.size(); address++)
{
isOtherTecuAddress[address] = (addresses.count(static_cast<std::uint8_t>(address)) > 0);
}
Expand Down Expand Up @@ -213,6 +224,10 @@ void OtherTecuMonitor::on_frame(const isobus::CANMessageFrame &frame)
{
const std::uint32_t now = isobus::SystemTiming::get_timestamp_ms();
lastSentByOthersMs[i] = (0 == now) ? 1 : now; // 0 means "never"
if (static_cast<std::size_t>(TecuMessage::TimeDate) == i)
{
timeDateProviders.mark_sent(static_cast<std::uint8_t>(frame.identifier & 0xFF));
}
}
}
}
Expand Down Expand Up @@ -274,7 +289,14 @@ std::array<bool, TECU_MESSAGE_COUNT> OtherTecuMonitor::sent_by_others() const
for (std::size_t i = 0; i < TECU_MESSAGE_COUNT; i++)
{
const std::uint32_t last = lastSentByOthersMs[i];
sent[i] = (0 != last) && !isobus::SystemTiming::time_expired_ms(last, SENT_BY_OTHERS_WINDOW_MS);
if (static_cast<std::size_t>(TecuMessage::TimeDate) == i)
{
sent[i] = timeDateProviders.any();
}
else
{
sent[i] = (0 != last) && !isobus::SystemTiming::time_expired_ms(last, SENT_BY_OTHERS_WINDOW_MS);
}
}
return sent;
}
Expand Down
Loading
Loading