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
20 changes: 18 additions & 2 deletions AOGDissector.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ ISOBUS_proto = Proto("ISOBUSVT", "AgISOStack")
local MajorPGNs = {
[0x7F] = "Steer module",
[0xFE] = "From AutoSteer",
[0x70] = "ISOBUS"
[0x70] = "ISOBUS",
[0x80] = "AOG-TaskController"
}
local MinorPGNs = {
[0x00] = "AOG -> ISOBUS",
Expand Down Expand Up @@ -111,7 +112,11 @@ local AOGFields = {
genericSubnet = ProtoField.ipv4("GenericIP.IPSubnet", "Subnet", base.DEC),
genericShortIPRange = ProtoField.string("GenericIP.IPSubnet", "Subnet", base.STRING),

freeFormMessage = ProtoField.string("Freeform.message", "Message", base.STRING)
freeFormMessage = ProtoField.string("Freeform.message", "Message", base.STRING),

hwMsgDuration = ProtoField.uint8("TaskController.HwMsgDuration", "Duration", base.DEC),
hwMsgColor = ProtoField.uint8("TaskController.HwMsgColor", "Background", base.DEC),
hwMsgText = ProtoField.string("TaskController.HwMsgText", "Message", base.STRING)
}

FixQuality = {
Expand Down Expand Up @@ -563,6 +568,17 @@ function AOGProtocol_proto.dissector(buffer, pinfo, tree)
end
end

if MajorPGN == 0x80 then -- 128 AOG-TaskController
if MinorPGN == 0xdd and buffer(4, 1):uint() > 2 then -- 221 hardware message
local textLength = buffer(4, 1):uint() - 2
subtree:add(AOGFields.hwMsgDuration, buffer(5, 1)):append_text(" (x10 render frames)")
subtree:add(AOGFields.hwMsgColor, buffer(6, 1)):append_text(buffer(6, 1):uint() == 0 and
" (Salmon, alert)" or " (Bisque, info)")
subtree:add(AOGFields.hwMsgText, buffer(7, textLength))
pinfo.cols.info = "TC hardware message: " .. buffer(7, textLength):string()
end
end

if MajorPGN == 0x78 then -- From GPS module 120
end

Expand Down
35 changes: 32 additions & 3 deletions docs/PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,37 @@ All PGNs sent **by the TC to AgIO/AgValonia** use source `0x80`.

| PGN | Name | Length | Frequency | Payload |
|---|---|---|---|---|
| `0xDD` (221) | Hardware message | 2 + T | On event | `[duration, colour, text0, text1, ...]` — `T` = UTF-8 text bytes |
| `0xF0` (240) | Section heartbeat / state | 2 + ⌈N/8⌉ | 100 ms | `[mode, num_sections, byte0, byte1, ...]` |

#### `0xDD` — Hardware message

Displays a text banner across the top of the AgOpenGPS map so the operator can see TC state that would otherwise only reach the console log. Payload:

- byte 0: `duration` — see the caveat below
- byte 1: `colour` — `0` renders a Salmon background (alert), any other value renders Bisque (info). The TC sends `1` for info.
- bytes 2..: the message text, UTF-8, **not** NUL-terminated

The frame's `Length` field is therefore `text_bytes + 2`, and AOG reads exactly `Length - 2` bytes starting at wire offset 7. The TC caps text at 60 bytes and truncates on a UTF-8 character boundary — splitting a multi-byte sequence would render as U+FFFD, and the display label clips long text anyway.

**`duration` is frames, not seconds.** AOG stores `duration × 10` into a counter that it decrements once per OpenGL render tick, so the banner lasts `duration × 10` redraws — roughly `duration` seconds at AOG's nominal 10 Hz, but it stretches or shrinks with frame rate. **A condition that persists must be re-sent**; do not send once with a large duration and expect the banner to hold. The operator can also dismiss a banner early by clicking it.

**AgOpenGPS ignores the source byte for this PGN** — it dispatches on the PGN byte alone. The TC still sends `0x80` per §2.2.

**Display is opt-in on the AOG side.** AgOpenGPS drops these frames unless *Config → Data → Hardware Messages* is enabled; the setting defaults to **off**. A correct implementation looks like a no-op until it is switched on, which is the first thing to check when testing. There is no corresponding TC-side setting — the TC always sends.

Messages the TC currently emits:

| Text | Colour | Duration | Trigger |
|---|---|---|---|
| `Implement: <name> (<n> sections, <w> m)` | Info | 5 | An implement with sections registers. The `, <w> m` clause is omitted when the DDOP has no usable geometry. |
| `Implement lost: <name>` | Alert | 10 | A previously seen implement disappears from the client list. |
| `TC address conflict: preferred address in use` | Alert | 20 | Another control function claiming `NAME::Function::TaskController` holds address `0xF7`. Re-sent every 15 s while true. |
| `TC address conflict resolved` | Info | 5 | The above clears. |
| `TECU failed to claim address 240` | Alert | 10 | Once at startup, when the TECU is enabled but could not claim its fixed address. |

The conflict messages surface on the operator's screen what the TC already writes to its console log; the console warning remains and keeps its own 30 s throttle.

#### `0xF0` — Section heartbeat / state

Sent every 100 ms for each connected ISOBUS implement that has sections. The payload is:
Expand Down Expand Up @@ -170,7 +199,7 @@ The TC reads `settings.json` from a per-user config directory:
| Key | Type | Default | Description |
|---|---|---|---|
| `subnet` | `int[3]` | `[192, 168, 5]` | First three octets of the LAN AgIO/AgValonia lives on. Used for NIC selection and broadcast destination. |
| `tecuEnabled` | `bool` | `true` | If `true`, the TC also impersonates a Tractor ECU on the CAN bus (claims address 128, broadcasts Speed Messages/NMEA2000, announces Class 1 BasicTractorECUServer). Set `false` when the tractor already has a TECU. |
| `tecuEnabled` | `bool` | `true` | If `true`, the TC also impersonates a Tractor ECU on the CAN bus (claims address 240, broadcasts Speed Messages/NMEA2000, announces Class 1 BasicTractorECUServer). Set `false` when the tractor already has a TECU. |
| `nmeaSendEnabled` | `bool` | `true` | Enable cyclic NMEA2000 COG/SOG transmission. Requires `tecuEnabled`; the VT disables this control when no TECU interface exists. |
| `aogHeartbeatEnabled` | `bool` | `true` | Send `0xF0` heartbeat to AgIO/AgValonia every 100 ms even with no implement. Disable for AOG < v6.8.2 beta 5. |
| `vtEnabled` | `bool` | `true` | Register the Virtual Terminal client and display the TC UI when a VT is present. |
Expand Down Expand Up @@ -209,8 +238,8 @@ Two control functions, both claiming addresses via standard J1939-81 address cla

| CF | NAME function | Address | Notes |
|---|---|---|---|
| Task Controller | `TaskController` (function code 61) | Preferred `233` (ISO 11783-10 MappingComputer). Walks if claimed. | Always present. |
| Tractor ECU | `TractorECU` (function code 132) | Fixed `128` (non-arbitrary-address-capable per ISO 11783-9). | Only present if `tecuEnabled: true`. |
| Task Controller | `TaskController` (function code 61) | Preferred `247` (ISO 11783-10 MappingComputer). Walks if claimed. | Always present. |
| Tractor ECU | `TractorECU` (function code 132) | Fixed `240` (non-arbitrary-address-capable per ISO 11783-9). | Only present if `tecuEnabled: true`. |

Common NAME fields: Industry Group `2` (Agricultural), Device Class `0`, Manufacturer Code `1407`, Identity `20`. Override these in `app.cpp` if you fork.

Expand Down
24 changes: 24 additions & 0 deletions include/app.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#pragma once

#include <boost/asio.hpp>
#include <cstdint>
#include <map>
#include <memory>
#include <string>
Expand Down Expand Up @@ -36,6 +37,21 @@ class Application
void stop();

private:
struct ImplementDetails
{
std::string displayName = "No implement";
std::uint8_t sections = 0;
std::string widthText;
std::string boomOffsetText;
};

struct ImplementSnapshot
{
std::string displayName;
std::uint8_t sections = 0;
std::string widthText;
};

void send_task_controller_status_message();

bool setup_can_hardware();
Expand All @@ -54,6 +70,11 @@ class Application
void update_vt_status_strings(bool aogConnected);

void send_vt_string_if_changed(std::uint16_t objectID, const std::string &value);
void send_hardware_message(const std::string &text, std::uint8_t duration, std::uint8_t color);
ImplementDetails derive_implement_details(ClientState &state) const;

static constexpr std::uint8_t HW_MSG_ALERT = 0;
static constexpr std::uint8_t HW_MSG_INFO = 1;

std::shared_ptr<Settings> settings = std::make_shared<Settings>();
boost::asio::io_context ioContext = boost::asio::io_context();
Expand Down Expand Up @@ -85,5 +106,8 @@ class Application
std::uint32_t vtDisconnectedSinceMs = 0;
std::uint32_t lastVtStatusUpdateMs = 0;
std::uint32_t lastVtSectionUpdateMs = 0;
bool tcAddressConflictActive = false;
bool tecuAddressClaimFailed = false;
std::map<std::uint64_t, ImplementSnapshot> implementSnapshots;
std::map<std::uint16_t, std::string> lastVtStrings;
};
Loading
Loading