diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5296f0209c5..54f08c1f08d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -262,6 +262,7 @@ if (WITH_GUI)
add_subdirectory(outlineviewer)
if (LIBSSH_FOUND)
add_subdirectory(roborioteamnumbersetter)
+ add_subdirectory(datalogtool)
endif()
endif()
diff --git a/datalogtool/.styleguide b/datalogtool/.styleguide
new file mode 100644
index 00000000000..87142fbc90a
--- /dev/null
+++ b/datalogtool/.styleguide
@@ -0,0 +1,29 @@
+cppHeaderFileInclude {
+ \.h$
+ \.inc$
+ \.inl$
+}
+
+cppSrcFileInclude {
+ \.cpp$
+}
+
+generatedFileExclude {
+ src/main/native/resources/
+ src/main/native/win/datalogtool.ico
+ src/main/native/mac/datalogtool.icns
+}
+
+repoRootNameOverride {
+ datalogtool
+}
+
+includeOtherLibs {
+ ^GLFW
+ ^fmt/
+ ^glass/
+ ^imgui
+ ^portable-file-dialog
+ ^wpi/
+ ^wpigui
+}
diff --git a/datalogtool/CMakeLists.txt b/datalogtool/CMakeLists.txt
new file mode 100644
index 00000000000..a5e23581e45
--- /dev/null
+++ b/datalogtool/CMakeLists.txt
@@ -0,0 +1,29 @@
+project(datalogtool)
+
+include(CompileWarnings)
+include(GenResources)
+include(LinkMacOSGUI)
+
+configure_file(src/main/generate/WPILibVersion.cpp.in WPILibVersion.cpp)
+GENERATE_RESOURCES(src/main/native/resources generated/main/cpp DLT dlt datalogtool_resources_src)
+
+file(GLOB datalogtool_src src/main/native/cpp/*.cpp ${CMAKE_CURRENT_BINARY_DIR}/WPILibVersion.cpp)
+
+if (WIN32)
+ set(datalogtool_rc src/main/native/win/datalogtool.rc)
+elseif(APPLE)
+ set(MACOSX_BUNDLE_ICON_FILE datalogtool.icns)
+ set(APP_ICON_MACOSX src/main/native/mac/datalogtool.icns)
+ set_source_files_properties(${APP_ICON_MACOSX} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
+endif()
+
+add_executable(datalogtool ${datalogtool_src} ${datalogtool_resources_src} ${datalogtool_rc} ${APP_ICON_MACOSX})
+wpilib_link_macos_gui(datalogtool)
+target_link_libraries(datalogtool libglass ${LIBSSH_LIBRARIES})
+target_include_directories(datalogtool PRIVATE ${LIBSSH_INCLUDE_DIRS})
+
+if (WIN32)
+ set_target_properties(datalogtool PROPERTIES WIN32_EXECUTABLE YES)
+elseif(APPLE)
+ set_target_properties(datalogtool PROPERTIES MACOSX_BUNDLE YES OUTPUT_NAME "datalogTool")
+endif()
diff --git a/datalogtool/Info.plist b/datalogtool/Info.plist
new file mode 100644
index 00000000000..db23e093738
--- /dev/null
+++ b/datalogtool/Info.plist
@@ -0,0 +1,32 @@
+
+
+
+
+ CFBundleName
+ datalogTool
+ CFBundleExecutable
+ datalogtool
+ CFBundleDisplayName
+ datalogTool
+ CFBundleIdentifier
+ edu.wpi.first.tools.datalogTool
+ CFBundleIconFile
+ datalogtool.icns
+ CFBundlePackageType
+ APPL
+ CFBundleSupportedPlatforms
+
+ MacOSX
+
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleShortVersionString
+ 2021
+ CFBundleVersion
+ 2021
+ LSMinimumSystemVersion
+ 10.11
+ NSHighResolutionCapable
+
+
+
diff --git a/datalogtool/build.gradle b/datalogtool/build.gradle
new file mode 100644
index 00000000000..8c8fc6c7d46
--- /dev/null
+++ b/datalogtool/build.gradle
@@ -0,0 +1,134 @@
+import org.gradle.internal.os.OperatingSystem
+
+if (!project.hasProperty('onlylinuxathena') && !project.hasProperty('onlylinuxraspbian') && !project.hasProperty('onlylinuxaarch64bionic')) {
+
+ description = "roboRIO Team Number Setter"
+
+ apply plugin: 'cpp'
+ apply plugin: 'c'
+ apply plugin: 'google-test-test-suite'
+ apply plugin: 'visual-studio'
+ apply plugin: 'edu.wpi.first.NativeUtils'
+
+ if (OperatingSystem.current().isWindows()) {
+ apply plugin: 'windows-resources'
+ }
+
+ ext {
+ nativeName = 'datalogtool'
+ }
+
+ apply from: "${rootDir}/shared/resources.gradle"
+ apply from: "${rootDir}/shared/config.gradle"
+
+ def wpilibVersionFileInput = file("src/main/generate/WPILibVersion.cpp.in")
+ def wpilibVersionFileOutput = file("$buildDir/generated/main/cpp/WPILibVersion.cpp")
+
+ nativeUtils {
+ nativeDependencyContainer {
+ libssh(getNativeDependencyTypeClass('WPIStaticMavenDependency')) {
+ groupId = "edu.wpi.first.thirdparty.frc2022"
+ artifactId = "libssh"
+ headerClassifier = "headers"
+ sourceClassifier = "sources"
+ ext = "zip"
+ version = '0.95-1'
+ targetPlatforms.addAll(nativeUtils.wpi.platforms.desktopPlatforms)
+ }
+ }
+ }
+
+ task generateCppVersion() {
+ description = 'Generates the wpilib version class'
+ group = 'WPILib'
+
+ outputs.file wpilibVersionFileOutput
+ inputs.file wpilibVersionFileInput
+
+ if (wpilibVersioning.releaseMode) {
+ outputs.upToDateWhen { false }
+ }
+
+ // We follow a simple set of checks to determine whether we should generate a new version file:
+ // 1. If the release type is not development, we generate a new version file
+ // 2. If there is no generated version number, we generate a new version file
+ // 3. If there is a generated build number, and the release type is development, then we will
+ // only generate if the publish task is run.
+ doLast {
+ def version = wpilibVersioning.version.get()
+ println "Writing version ${version} to $wpilibVersionFileOutput"
+
+ if (wpilibVersionFileOutput.exists()) {
+ wpilibVersionFileOutput.delete()
+ }
+ def read = wpilibVersionFileInput.text.replace('${wpilib_version}', version)
+ wpilibVersionFileOutput.write(read)
+ }
+ }
+
+ gradle.taskGraph.addTaskExecutionGraphListener { graph ->
+ def willPublish = graph.hasTask(publish)
+ if (willPublish) {
+ generateCppVersion.outputs.upToDateWhen { false }
+ }
+ }
+
+ def generateTask = createGenerateResourcesTask('main', 'DLT', 'dlt', project)
+
+ project(':').libraryBuild.dependsOn build
+ tasks.withType(CppCompile) {
+ dependsOn generateTask
+ dependsOn generateCppVersion
+ }
+
+ model {
+ components {
+ // By default, a development executable will be generated. This is to help the case of
+ // testing specific functionality of the library.
+ "${nativeName}"(NativeExecutableSpec) {
+ baseName = 'datalogtool'
+ sources {
+ cpp {
+ source {
+ srcDirs 'src/main/native/cpp', "$buildDir/generated/main/cpp"
+ include '**/*.cpp'
+ }
+ exportedHeaders {
+ srcDirs 'src/main/native/include'
+ }
+ }
+ if (OperatingSystem.current().isWindows()) {
+ rc {
+ source {
+ srcDirs 'src/main/native/win'
+ include '*.rc'
+ }
+ }
+ }
+ }
+ binaries.all {
+ if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio || it.targetPlatform.name == nativeUtils.wpi.platforms.raspbian || it.targetPlatform.name == nativeUtils.wpi.platforms.aarch64bionic) {
+ it.buildable = false
+ return
+ }
+ it.cppCompiler.define("LIBSSH_STATIC")
+ lib project: ':glass', library: 'glass', linkage: 'static'
+ lib project: ':wpiutil', library: 'wpiutil', linkage: 'static'
+ lib project: ':wpigui', library: 'wpigui', linkage: 'static'
+ nativeUtils.useRequiredLibrary(it, 'imgui_static', 'libssh')
+ if (it.targetPlatform.operatingSystem.isWindows()) {
+ it.linker.args << 'Gdi32.lib' << 'Shell32.lib' << 'd3d11.lib' << 'd3dcompiler.lib'
+ it.linker.args << 'ws2_32.lib' << 'advapi32.lib' << 'crypt32.lib' << 'user32.lib'
+ } else if (it.targetPlatform.operatingSystem.isMacOsX()) {
+ it.linker.args << '-framework' << 'Metal' << '-framework' << 'MetalKit' << '-framework' << 'Cocoa' << '-framework' << 'IOKit' << '-framework' << 'CoreFoundation' << '-framework' << 'CoreVideo' << '-framework' << 'QuartzCore'
+ it.linker.args << '-framework' << 'Kerberos'
+ } else {
+ it.linker.args << '-lX11'
+ }
+ }
+ }
+ }
+ }
+
+ apply from: 'publish.gradle'
+}
diff --git a/datalogtool/publish.gradle b/datalogtool/publish.gradle
new file mode 100644
index 00000000000..131009cfc67
--- /dev/null
+++ b/datalogtool/publish.gradle
@@ -0,0 +1,107 @@
+apply plugin: 'maven-publish'
+
+def baseArtifactId = 'DataLogTool'
+def artifactGroupId = 'edu.wpi.first.tools'
+def zipBaseName = '_GROUP_edu_wpi_first_tools_ID_DataLogTool_CLS'
+
+def outputsFolder = file("$project.buildDir/outputs")
+
+model {
+ tasks {
+ // Create the run task.
+ $.components.datalogtool.binaries.each { bin ->
+ if (bin.buildable && bin.name.toLowerCase().contains("debug")) {
+ Task run = project.tasks.create("run", Exec) {
+ commandLine bin.tasks.install.runScriptFile.get().asFile.toString()
+ }
+ run.dependsOn bin.tasks.install
+ }
+ }
+ }
+ publishing {
+ def dataLogToolTaskList = []
+ $.components.each { component ->
+ component.binaries.each { binary ->
+ if (binary in NativeExecutableBinarySpec && binary.component.name.contains("datalogtool")) {
+ if (binary.buildable && binary.name.contains("Release")) {
+ // We are now in the binary that we want.
+ // This is the default application path for the ZIP task.
+ def applicationPath = binary.executable.file
+ def icon = file("$project.projectDir/src/main/native/mac/datalogtool.icns")
+
+ // Create the macOS bundle.
+ def bundleTask = project.tasks.create("bundleDataLogToolOsxApp", Copy) {
+ description("Creates a macOS application bundle for DataLogTool")
+ from(file("$project.projectDir/Info.plist"))
+ into(file("$project.buildDir/outputs/bundles/DataLogTool.app/Contents"))
+ into("MacOS") { with copySpec { from binary.executable.file } }
+ into("Resources") { with copySpec { from icon } }
+
+ doLast {
+ if (project.hasProperty("developerID")) {
+ // Get path to binary.
+ exec {
+ workingDir rootDir
+ def args = [
+ "sh",
+ "-c",
+ "codesign --force --strict --deep " +
+ "--timestamp --options=runtime " +
+ "--verbose -s ${project.findProperty("developerID")} " +
+ "$project.buildDir/outputs/bundles/DataLogTool.app/"
+ ]
+ commandLine args
+ }
+ }
+ }
+ }
+
+ // Reset the application path if we are creating a bundle.
+ if (binary.targetPlatform.operatingSystem.isMacOsX()) {
+ applicationPath = file("$project.buildDir/outputs/bundles")
+ project.build.dependsOn bundleTask
+ }
+
+ // Create the ZIP.
+ def task = project.tasks.create("copyDataLogToolExecutable", Zip) {
+ description("Copies the DataLogTool executable to the outputs directory.")
+ destinationDirectory = outputsFolder
+
+ archiveBaseName = '_M_' + zipBaseName
+ duplicatesStrategy = 'exclude'
+ classifier = nativeUtils.getPublishClassifier(binary)
+
+ from(licenseFile) {
+ into '/'
+ }
+
+ from(applicationPath)
+ into(nativeUtils.getPlatformPath(binary))
+ }
+
+ if (binary.targetPlatform.operatingSystem.isMacOsX()) {
+ bundleTask.dependsOn binary.tasks.link
+ task.dependsOn(bundleTask)
+ }
+
+ task.dependsOn binary.tasks.link
+ dataLogToolTaskList.add(task)
+ project.build.dependsOn task
+ project.artifacts { task }
+ addTaskToCopyAllOutputs(task)
+ }
+ }
+ }
+ }
+
+ publications {
+ datalogtool(MavenPublication) {
+ dataLogToolTaskList.each { artifact it }
+
+ artifactId = baseArtifactId
+ groupId = artifactGroupId
+ version wpilibVersioning.version.get()
+ }
+ }
+ }
+}
diff --git a/datalogtool/src/main/generate/WPILibVersion.cpp.in b/datalogtool/src/main/generate/WPILibVersion.cpp.in
new file mode 100644
index 00000000000..b0a44905207
--- /dev/null
+++ b/datalogtool/src/main/generate/WPILibVersion.cpp.in
@@ -0,0 +1,7 @@
+/*
+ * Autogenerated file! Do not manually edit this file. This version is regenerated
+ * any time the publish task is run, or when this file is deleted.
+ */
+const char* GetWPILibVersion() {
+ return "${wpilib_version}";
+}
diff --git a/datalogtool/src/main/native/cpp/App.cpp b/datalogtool/src/main/native/cpp/App.cpp
new file mode 100644
index 00000000000..a4b466b57c3
--- /dev/null
+++ b/datalogtool/src/main/native/cpp/App.cpp
@@ -0,0 +1,156 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "App.h"
+
+#include
+
+#include
+#include
+
+#define IMGUI_DEFINE_MATH_OPERATORS
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "Downloader.h"
+#include "Exporter.h"
+
+namespace gui = wpi::gui;
+
+const char* GetWPILibVersion();
+
+namespace dlt {
+std::string_view GetResource_dlt_16_png();
+std::string_view GetResource_dlt_32_png();
+std::string_view GetResource_dlt_48_png();
+std::string_view GetResource_dlt_64_png();
+std::string_view GetResource_dlt_128_png();
+std::string_view GetResource_dlt_256_png();
+std::string_view GetResource_dlt_512_png();
+} // namespace dlt
+
+bool gShutdown = false;
+
+static std::unique_ptr gDownloader;
+static bool* gDownloadVisible;
+static float gDefaultScale = 1.0;
+
+void SetNextWindowPos(const ImVec2& pos, ImGuiCond cond, const ImVec2& pivot) {
+ if ((cond & ImGuiCond_FirstUseEver) != 0) {
+ ImGui::SetNextWindowPos(pos * gDefaultScale, cond, pivot);
+ } else {
+ ImGui::SetNextWindowPos(pos, cond, pivot);
+ }
+}
+
+void SetNextWindowSize(const ImVec2& size, ImGuiCond cond) {
+ if ((cond & ImGuiCond_FirstUseEver) != 0) {
+ ImGui::SetNextWindowSize(size * gDefaultScale, cond);
+ } else {
+ ImGui::SetNextWindowPos(size, cond);
+ }
+}
+
+static void DisplayDownload() {
+ if (!*gDownloadVisible) {
+ return;
+ }
+ SetNextWindowPos(ImVec2{0, 250}, ImGuiCond_FirstUseEver);
+ SetNextWindowSize(ImVec2{375, 260}, ImGuiCond_FirstUseEver);
+ if (ImGui::Begin("Download", gDownloadVisible)) {
+ if (!gDownloader) {
+ gDownloader = std::make_unique(
+ glass::GetStorageRoot().GetChild("download"));
+ }
+ gDownloader->Display();
+ }
+ ImGui::End();
+}
+
+static void DisplayMainMenu() {
+ ImGui::BeginMainMenuBar();
+
+ static glass::MainMenuBar mainMenu;
+ mainMenu.WorkspaceMenu();
+ gui::EmitViewMenu();
+
+ if (ImGui::BeginMenu("Window")) {
+ ImGui::MenuItem("Download", nullptr, gDownloadVisible);
+ ImGui::EndMenu();
+ }
+
+ bool about = false;
+ if (ImGui::BeginMenu("Info")) {
+ if (ImGui::MenuItem("About")) {
+ about = true;
+ }
+ ImGui::EndMenu();
+ }
+
+ ImGui::EndMainMenuBar();
+
+ if (about) {
+ ImGui::OpenPopup("About");
+ }
+ if (ImGui::BeginPopupModal("About")) {
+ ImGui::Text("Datalog Tool");
+ ImGui::Separator();
+ ImGui::Text("v%s", GetWPILibVersion());
+ ImGui::Separator();
+ ImGui::Text("Save location: %s", glass::GetStorageDir().c_str());
+ if (ImGui::Button("Close")) {
+ ImGui::CloseCurrentPopup();
+ }
+ ImGui::EndPopup();
+ }
+}
+
+static void DisplayGui() {
+ DisplayMainMenu();
+ DisplayInputFiles();
+ DisplayEntries();
+ DisplayOutput(glass::GetStorageRoot().GetChild("output"));
+ DisplayDownload();
+}
+
+void Application(std::string_view saveDir) {
+ ssh_init();
+
+ gui::CreateContext();
+ glass::CreateContext();
+
+ // Add icons
+ gui::AddIcon(dlt::GetResource_dlt_16_png());
+ gui::AddIcon(dlt::GetResource_dlt_32_png());
+ gui::AddIcon(dlt::GetResource_dlt_48_png());
+ gui::AddIcon(dlt::GetResource_dlt_64_png());
+ gui::AddIcon(dlt::GetResource_dlt_128_png());
+ gui::AddIcon(dlt::GetResource_dlt_256_png());
+ gui::AddIcon(dlt::GetResource_dlt_512_png());
+
+ glass::SetStorageName("datalogtool");
+ glass::SetStorageDir(saveDir.empty() ? gui::GetPlatformSaveFileDir()
+ : saveDir);
+
+ gui::AddWindowScaler([](float scale) { gDefaultScale = scale; });
+ gui::AddLateExecute(DisplayGui);
+ gui::Initialize("Datalog Tool", 925, 510);
+
+ gDownloadVisible =
+ &glass::GetStorageRoot().GetChild("download").GetBool("visible", true);
+
+ gui::Main();
+
+ gShutdown = true;
+ glass::DestroyContext();
+ gui::DestroyContext();
+
+ gDownloader.reset();
+ ssh_finalize();
+}
diff --git a/datalogtool/src/main/native/cpp/App.h b/datalogtool/src/main/native/cpp/App.h
new file mode 100644
index 00000000000..9d1520c8a0a
--- /dev/null
+++ b/datalogtool/src/main/native/cpp/App.h
@@ -0,0 +1,11 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include
+
+void SetNextWindowPos(const ImVec2& pos, ImGuiCond cond = 0,
+ const ImVec2& pivot = ImVec2(0, 0));
+void SetNextWindowSize(const ImVec2& size, ImGuiCond cond = 0);
diff --git a/datalogtool/src/main/native/cpp/DataLogThread.cpp b/datalogtool/src/main/native/cpp/DataLogThread.cpp
new file mode 100644
index 00000000000..90c8b196c62
--- /dev/null
+++ b/datalogtool/src/main/native/cpp/DataLogThread.cpp
@@ -0,0 +1,72 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "DataLogThread.h"
+
+#include
+
+DataLogThread::~DataLogThread() {
+ if (m_thread.joinable()) {
+ m_active = false;
+ m_thread.join();
+ }
+}
+
+void DataLogThread::ReadMain() {
+ for (auto record : m_reader) {
+ if (!m_active) {
+ break;
+ }
+ ++m_numRecords;
+ if (record.IsStart()) {
+ wpi::log::StartRecordData data;
+ if (record.GetStartData(&data)) {
+ std::scoped_lock lock{m_mutex};
+ if (m_entries.find(data.entry) != m_entries.end()) {
+ fmt::print("...DUPLICATE entry ID, overriding\n");
+ }
+ m_entries[data.entry] = data;
+ m_entryNames.emplace(data.name, data);
+ sigEntryAdded(data);
+ } else {
+ fmt::print("Start(INVALID)\n");
+ }
+ } else if (record.IsFinish()) {
+ int entry;
+ if (record.GetFinishEntry(&entry)) {
+ std::scoped_lock lock{m_mutex};
+ auto it = m_entries.find(entry);
+ if (it == m_entries.end()) {
+ fmt::print("...ID not found\n");
+ } else {
+ m_entries.erase(it);
+ }
+ } else {
+ fmt::print("Finish(INVALID)\n");
+ }
+ } else if (record.IsSetMetadata()) {
+ wpi::log::MetadataRecordData data;
+ if (record.GetSetMetadataData(&data)) {
+ std::scoped_lock lock{m_mutex};
+ auto it = m_entries.find(data.entry);
+ if (it == m_entries.end()) {
+ fmt::print("...ID not found\n");
+ } else {
+ it->second.metadata = data.metadata;
+ auto nameIt = m_entryNames.find(it->second.name);
+ if (nameIt != m_entryNames.end()) {
+ nameIt->second.metadata = data.metadata;
+ }
+ }
+ } else {
+ fmt::print("SetMetadata(INVALID)\n");
+ }
+ } else if (record.IsControl()) {
+ fmt::print("Unrecognized control record\n");
+ }
+ }
+
+ sigDone();
+ m_done = true;
+}
diff --git a/datalogtool/src/main/native/cpp/DataLogThread.h b/datalogtool/src/main/native/cpp/DataLogThread.h
new file mode 100644
index 00000000000..0a273369ca2
--- /dev/null
+++ b/datalogtool/src/main/native/cpp/DataLogThread.h
@@ -0,0 +1,71 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include
+#include
+#include