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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ jobs:
- name: Configure
run: cmake -S . -B build -DBUILD_EXAMPLES=OFF -DBUILD_TESTING=OFF -DCAN_DRIVER=None -DCMAKE_BUILD_TYPE=Release
- name: Compile tests
run: cmake --build build --target example_pool_test
run: cmake --build build --target example_pool_test task_data_import_test
- name: Run tests
run: ctest --test-dir build --output-on-failure

3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
[submodule "submodules/agisostack"]
path = submodules/agisostack
url = https://github.com/Open-Agriculture/AgIsoStack-plus-plus.git
[submodule "submodules/tinyxml2"]
path = submodules/tinyxml2
url = https://github.com/leethomason/tinyxml2
12 changes: 10 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ find_package(OpenGL REQUIRED)
set(BUILD_TESTING OFF)
add_subdirectory(submodules/agisostack)
add_subdirectory(submodules/sdl)
add_subdirectory(submodules/tinyxml2 EXCLUDE_FROM_ALL)

target_sources(
AgIsoDDOPGenerator
PRIVATE src/main.cpp
src/gui.cpp
src/task_data_import.cpp
submodules/imgui/imgui.cpp
submodules/imgui/imgui_demo.cpp
submodules/imgui/imgui_draw.cpp
Expand All @@ -51,8 +53,14 @@ target_include_directories(
submodules/imgui/backends submodules/sdl/include)

target_link_libraries(
AgIsoDDOPGenerator PRIVATE isobus::Isobus isobus::Utility OpenGL::GL SDL2
SDL2main ${CMAKE_DL_LIBS})
AgIsoDDOPGenerator
PRIVATE isobus::Isobus
isobus::Utility
OpenGL::GL
SDL2
SDL2main
tinyxml2::tinyxml2
${CMAKE_DL_LIBS})

enable_testing()
add_subdirectory(test)
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ When used in combination with AgIsoStack (or any other TC client), it provides a

### Using it

**File → New** starts an empty DDOP and asks for your device information; **File → Open** loads an existing one. `EXAMPLE.ddop` and `EXAMPLE_V4.ddop` in this repository are there to open and poke at.
**File → New** starts an empty DDOP and asks for your device information; **File → Open** loads an existing one. `EXAMPLE.ddop`, `EXAMPLE_V4.ddop` and `TASKDATA.XML` in [examples/](examples/) are there to open and poke at; the XML goes in through **File → Import ISOXML**.

**File → Import ISOXML** reads the device description in an ISOXML TASKDATA.XML, and asks you to pick a device if the file holds several. **File → Save** then asks for a new file name, as it writes a binary DDOP.

The left pane is the object tree, and whatever you select in it is edited in the right pane. **Create Object** adds a device element, process data, property or value presentation, and a device element gains children by referencing objects that already exist.

Expand Down
File renamed without changes.
File renamed without changes.
89 changes: 89 additions & 0 deletions examples/TASKDATA.XML
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- The UTF-8 byte order mark, CRLF line endings and non-DVC elements between devices are deliberate. -->
<ISO11783_TaskData VersionMajor="4" VersionMinor="2" DataTransferOrigin="2">
<DVC A="DVC-1" B="Tractor ECU" C="1.0" D="A00084000C200002" E="1234" F="00000000000001" G="FF000000006564"/>
<CTR A="CTR-1" B="Test Customer"/>
<TSK A="TSK-1" G="1"/>
<DVC A="DVC-2" B="Sprayer &amp; Boom" C="2.3.4" D="A00086000CE01234" F="31323334353637" G="FF000000006E65">
<DET A="DET-1" B="5001" C="1" D="Sprayer" E="0" F="0">
<DOR A="5002"/>
<DOR A="5004"/>
</DET>
<DET A="DET-2" B="5003" C="4" E="1" F="5001">
<DOR A="5005"/>
<DOR A="5006"/>
</DET>
<DPD A="5002" B="0074" C="1" D="8" E="Total Area" F="6002"/>
<DPD A="5004" B="0077" C="1" D="8" E="Total Time" F="6001"/>
<DPD A="5006" B="008D" C="1" D="31" E="Work State"/>
<DPT A="5005" B="0086" C="-1200" D="Offset X"/>
<DVP A="6001" B="0" C="0.0000012345" D="1" E="h"/>
<DVP A="6002" B="0" C="0.000099999997474" D="2" E="ha "/>
</DVC>
<PFD A="PFD-1" C="Test Field" D="1000"/>
<!-- DVC-3: objects, DORs and attributes deliberately out of order; DPD 7101 is shared by two DETs -->
<DVC
G="FF000000007372"
F="41424344454647"
E="SN-0003"
D="A00086000CE05678"
C="3.1"
B="Sprayer Plus"
A="DVC-3">
<DPD E="Proprietary Rate" D="31" C="5" B="8001" A="7104"/>
<DVP E="" D="7" C="0.0000001" B="2000000000" A="7203"/>
<DET F="7004" E="4" D="Unit &#x48;&#105;" C="5" B="7005" A="DET-7">
<DOR A="7103"/>
<DOR A="7105"/>
</DET>
<DET A="DET-3" B="7001" C="1" D="Sprayer Device" E="0" F="0">
<DOR A="7104"/>
<DOR A="7101"/>
<DOR A="7114"/>
<DOR A="7102"/>
</DET>
<DPT D="Int32 Min" C="-2147483648" B="0086" A="7114"/>
<DET A="DET-5" B="7003" C="3" D="Tank ä" E="2" F="7002">
<DOR A="7113"/>
<DOR A="7111"/>
</DET>
<DET A="DET-4" B="7002" C="2" D="Function" E="1" F="7001"/>
<DET A="DET-6" B="7004" C="4" D="Sec&#228;tion" E="3" F="7002">
<DOR A="7101"/>
<DOR A="7106"/>
</DET>
<!-- exactly 32 bytes -->
<DET A="DET-8" B="7006" C="6" D="ääääääääääääääää" E="5" F="7001">
<DOR A="7112"/>
</DET>
<DET A="DET-9" B="7007" C="7" D="Nav" E="6" F="7001">
<DOR A="7115"/>
</DET>
<DPD A="7101" B="0074" C="1" D="8" E="Total Area" F="7201"/>
<DPD A="7102" B="0077" C="3" D="9" E="Total Time" F="7202"/>
<DPD D="0" C="0" B="008D" A="7103"/>
<!-- lowercase on purpose -->
<DPD A="7105" B="00af" C="1" D="2" E="Distance Driven" F="7203"/>
<!-- exactly 32 bytes -->
<DPD A="7106" B="9ABC" C="2" D="4" E="ABCDEFGHIJKLMNOPQRSTUVWXYZ012345"/>
<DPT A="7111" B="0086" C="0" D="Zero Offset"/>
<DPT A="7112" B="0087" C="-1200" D="Negative Offset" E="7202"/>
<DPT A="7113" B="0088" C="2147483647" D="Int32 Max"/>
<DPT A="7115" B="0089" C="65536" D="Large With DVP" E="7204"/>
<DVP A="7201" B="0" C="1" D="0" E="m"/>
<DVP A="7202" B="-100" C="0.001" D="3" E="mm"/>
<DVP A="7204" B="-2147483648" C="100" D="0" E="%"/>
</DVC>
<VPN A="VPN-1" B="0" C="0" D="1" E="0"/>
<!-- DVC-4: entities and character references the exporter must escape to round trip -->
<DVC A="DVC-4" B="Quote &quot;Q&quot; &lt;tag&gt; &amp; &apos;a&apos;" C="&lt;4&gt;" D="A00086000CE09ABC" E="&amp;" F="41424344454647" G="FF000000007372">
<DET A="DET-10" B="8001" C="1" D="Root &lt;1&gt;" E="0" F="0"/>
<DET A="DET-11" B="8002" C="4" D="a &amp; b" E="1" F="8001">
<DOR A="8101"/>
<DOR A="8102"/>
</DET>
<DPD A="8101" B="0074" C="1" D="8" E="Rate &quot;x&quot;" F="8201"/>
<DPT A="8102" B="0086" C="7" D="It&apos;s &#60;NCR&#62; ä" E="8201"/>
<DVP A="8201" B="0" C="1" D="0" E="&lt;&amp;&gt;"/>
</DVC>
</ISO11783_TaskData>
4 changes: 4 additions & 0 deletions include/gui.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ class DDOPGeneratorGUI
char hexIsoNameBuffer[17] = { 0 };
char languageCodeBuffer[3] = { 0 };
std::string lastFileName;
std::string pendingTaskData;
std::vector<std::string> taskDataDevices;
int selectedTaskDataDevice = 0;
int elementNumberBuffer = 0;
int parentObjectBuffer = 0;
int ddiBuffer = 0;
Expand All @@ -94,6 +97,7 @@ class DDOPGeneratorGUI
std::array<bool, 8> propertiesBitfieldBuffer = { false };
std::array<bool, 8> triggerBitfieldBuffer = { false };
bool openFileDialogue = false;
bool importTaskDataDialogue = false;
bool saveModal = false;
bool saveAsModal = false;
bool exportModal = false;
Expand Down
24 changes: 24 additions & 0 deletions include/task_data_import.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//================================================================================================
/// @file task_data_import.hpp
///
/// @brief Builds a DDOP from a device description in an ISOXML TASKDATA.XML
/// @author Sujan Dumaru
///
/// @copyright 2026 The Open-Agriculture developers
//================================================================================================
#ifndef TASK_DATA_IMPORT_HPP
#define TASK_DATA_IMPORT_HPP

#include "isobus/isobus/isobus_device_descriptor_object_pool.hpp"

#include <cstddef>
#include <string>
#include <vector>

/// The DVC elements of a TASKDATA.XML as "A B" labels, in file order. Empty if it does not parse or has no inline DVC.
std::vector<std::string> list_task_data_devices(const std::string &taskData);

/// Adds the objects of the deviceIndex-th DVC to an empty pool whose TC level is already set.
bool import_task_data_device(const std::string &taskData, std::size_t deviceIndex, isobus::DeviceDescriptorObjectPool &pool);

#endif // TASK_DATA_IMPORT_HPP
126 changes: 105 additions & 21 deletions src/gui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
#include "isobus/isobus/isobus_data_dictionary.hpp"
#include "isobus/utility/iop_file_interface.hpp"
#include "logsink.hpp"
#include "task_data_import.hpp"

#include <algorithm>
#include <cerrno>
#include <cstdio>
#include <cstring>
Expand Down Expand Up @@ -328,6 +330,13 @@ bool DDOPGeneratorGUI::render_menu_bar()
{
FileDialog::file_dialog_open = true;
openFileDialogue = true;
importTaskDataDialogue = false;
}
if (ImGui::MenuItem("Import ISOXML", "Load a device from an ISOXML TASKDATA.XML file"))
{
FileDialog::file_dialog_open = true;
importTaskDataDialogue = true;
openFileDialogue = false;
}

if (!currentPoolValid)
Expand Down Expand Up @@ -549,49 +558,96 @@ bool DDOPGeneratorGUI::render_menu_bar()
return retVal;
}

/// @brief Renders a modal listing the logged errors of the last load attempt
static void render_log_error_popup(const char *title, const char *heading)
{
if (ImGui::BeginPopupModal(title, NULL, ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::Text("%s", heading);
ImGui::Separator();

for (auto &logString : logger.logHistory)
{
ImGui::Text("%s", logString.logText.c_str());
}

ImGui::SetItemDefaultFocus();
if (ImGui::Button("OK", ImVec2(120, 0)))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}

static std::unique_ptr<isobus::DeviceDescriptorObjectPool> make_pool()
{
auto pool = std::make_unique<isobus::DeviceDescriptorObjectPool>();
pool->set_task_controller_compatibility_level((0 == FileDialog::versions_current_idx) ? 3 : 4);
return pool;
}

void DDOPGeneratorGUI::render_open_file_menu()
{
bool importSelectedDevice = false;

if (FileDialog::file_dialog_open)
{
FileDialog::ShowFileDialog(&FileDialog::file_dialog_open, filePathBuffer, FILE_PATH_BUFFER_MAX_LENGTH, FileDialog::FileDialogType::OpenFile);
}
else if (openFileDialogue)
else if (openFileDialogue || importTaskDataDialogue)
{
std::string selectedFileToRead(filePathBuffer);
memset(filePathBuffer, 0, FILE_PATH_BUFFER_MAX_LENGTH);
const bool importingTaskData = importTaskDataDialogue;
openFileDialogue = false;
importTaskDataDialogue = false;

if (!selectedFileToRead.empty())
{
logger.logHistory.clear();
loadedIopData = isobus::IOPFileInterface::read_iop_file(selectedFileToRead);

if (!loadedIopData.empty())
if (loadedIopData.empty())
{
selectedObjectID = 0xFFFF;
logger.logHistory.clear();
currentObjectPool.reset();
currentObjectPool = std::make_unique<isobus::DeviceDescriptorObjectPool>();
if (importingTaskData)
{
LOG_ERROR("[DDOP]: Could not read \"%s\", or it is empty.", selectedFileToRead.c_str());
ImGui::OpenPopup("Error Loading XML File");
}
}
else if (importingTaskData)
{
pendingTaskData.assign(loadedIopData.begin(), loadedIopData.end());
taskDataDevices = list_task_data_devices(pendingTaskData);
selectedTaskDataDevice = 0;

if (0 == FileDialog::versions_current_idx)
if (taskDataDevices.empty())
{
currentObjectPool->set_task_controller_compatibility_level(3);
ImGui::OpenPopup("Error Loading XML File");
}
else if (1 == taskDataDevices.size())
{
importSelectedDevice = true;
}
else
{
currentObjectPool->set_task_controller_compatibility_level(4);
ImGui::OpenPopup("Select Device");
}
}
else
{
auto openedPool = make_pool();

if (true == currentObjectPool->deserialize_binary_object_pool(loadedIopData, isobus::NAME(0)))
if (true == openedPool->deserialize_binary_object_pool(loadedIopData, isobus::NAME(0)))
{
// Valid pool?
currentObjectPool = std::move(openedPool);
selectedObjectID = 0xFFFF;
currentPoolValid = true;
lastFileName = selectedFileToRead;
}
else
{
currentObjectPool.reset();
currentPoolValid = false;

ImGui::OpenPopup("Error Loading DDOP");
}
}
Expand All @@ -602,23 +658,51 @@ void DDOPGeneratorGUI::render_open_file_menu()
}
}

if (ImGui::BeginPopupModal("Error Loading DDOP", NULL, ImGuiWindowFlags_AlwaysAutoResize))
if (ImGui::BeginPopupModal("Select Device", NULL, ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::Text("There were errors loading the DDOP. Make sure you selected the correct TC version.");
ImGui::Separator();

for (auto &logString : logger.logHistory)
std::vector<const char *> deviceLabels;
float listWidth = 0.0f;
for (const auto &label : taskDataDevices)
{
ImGui::Text("%s", logString.logText.c_str());
deviceLabels.push_back(label.c_str());
listWidth = std::max(listWidth, ImGui::CalcTextSize(label.c_str()).x);
}
ImGui::SetNextItemWidth(listWidth + ImGui::GetStyle().ScrollbarSize + 2.0f * ImGui::GetStyle().WindowPadding.x);
ImGui::ListBox("##Devices", &selectedTaskDataDevice, deviceLabels.data(), static_cast<int>(deviceLabels.size()));

ImGui::SetItemDefaultFocus();
if (ImGui::Button("OK", ImVec2(120, 0)))
{
importSelectedDevice = true;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(120, 0)))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}

// After EndPopup, because a popup opened inside another modal never shows.
if (importSelectedDevice)
{
auto importedPool = make_pool();

if (import_task_data_device(pendingTaskData, static_cast<std::size_t>(selectedTaskDataDevice), *importedPool))
{
currentObjectPool = std::move(importedPool);
selectedObjectID = 0xFFFF;
currentPoolValid = true;
lastFileName.clear();
}
else
{
ImGui::OpenPopup("Error Loading XML File");
}
}

render_log_error_popup("Error Loading DDOP", "There were errors loading the DDOP. Make sure you selected the correct TC version.");
render_log_error_popup("Error Loading XML File", "There were errors reading the TASKDATA.XML:");
}

void DDOPGeneratorGUI::parseElementChildrenOfElement(std::uint16_t aObjectID)
Expand Down
Loading
Loading