[datalogtool] Add datalogtool
This is a support tool for datalog file conversion (and eventually download/remote datalog file management).
@@ -262,6 +262,7 @@ if (WITH_GUI)
|
|||||||
add_subdirectory(outlineviewer)
|
add_subdirectory(outlineviewer)
|
||||||
if (LIBSSH_FOUND)
|
if (LIBSSH_FOUND)
|
||||||
add_subdirectory(roborioteamnumbersetter)
|
add_subdirectory(roborioteamnumbersetter)
|
||||||
|
add_subdirectory(datalogtool)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|||||||
29
datalogtool/.styleguide
Normal file
@@ -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
|
||||||
|
}
|
||||||
29
datalogtool/CMakeLists.txt
Normal file
@@ -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()
|
||||||
32
datalogtool/Info.plist
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>datalogTool</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>datalogtool</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>datalogTool</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>edu.wpi.first.tools.datalogTool</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>datalogtool.icns</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleSupportedPlatforms</key>
|
||||||
|
<array>
|
||||||
|
<string>MacOSX</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>2021</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>2021</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.11</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
134
datalogtool/build.gradle
Normal file
@@ -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'
|
||||||
|
}
|
||||||
107
datalogtool/publish.gradle
Normal file
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
datalogtool/src/main/generate/WPILibVersion.cpp.in
Normal file
@@ -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}";
|
||||||
|
}
|
||||||
156
datalogtool/src/main/native/cpp/App.cpp
Normal file
@@ -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 <libssh/libssh.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||||
|
|
||||||
|
#include <glass/Context.h>
|
||||||
|
#include <glass/MainMenuBar.h>
|
||||||
|
#include <glass/Storage.h>
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <imgui_internal.h>
|
||||||
|
#include <wpigui.h>
|
||||||
|
|
||||||
|
#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<Downloader> 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<Downloader>(
|
||||||
|
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();
|
||||||
|
}
|
||||||
11
datalogtool/src/main/native/cpp/App.h
Normal file
@@ -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 <imgui.h>
|
||||||
|
|
||||||
|
void SetNextWindowPos(const ImVec2& pos, ImGuiCond cond = 0,
|
||||||
|
const ImVec2& pivot = ImVec2(0, 0));
|
||||||
|
void SetNextWindowSize(const ImVec2& size, ImGuiCond cond = 0);
|
||||||
72
datalogtool/src/main/native/cpp/DataLogThread.cpp
Normal file
@@ -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 <fmt/format.h>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
71
datalogtool/src/main/native/cpp/DataLogThread.h
Normal file
@@ -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 <atomic>
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <thread>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <wpi/DataLogReader.h>
|
||||||
|
#include <wpi/DenseMap.h>
|
||||||
|
#include <wpi/Signal.h>
|
||||||
|
#include <wpi/mutex.h>
|
||||||
|
|
||||||
|
class DataLogThread {
|
||||||
|
public:
|
||||||
|
explicit DataLogThread(wpi::log::DataLogReader reader)
|
||||||
|
: m_reader{std::move(reader)}, m_thread{[=] { ReadMain(); }} {}
|
||||||
|
~DataLogThread();
|
||||||
|
|
||||||
|
bool IsDone() const { return m_done; }
|
||||||
|
std::string_view GetBufferIdentifier() const {
|
||||||
|
return m_reader.GetBufferIdentifier();
|
||||||
|
}
|
||||||
|
unsigned int GetNumRecords() const { return m_numRecords; }
|
||||||
|
unsigned int GetNumEntries() const {
|
||||||
|
std::scoped_lock lock{m_mutex};
|
||||||
|
return m_entryNames.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passes wpi::log::StartRecordData to func
|
||||||
|
template <typename T>
|
||||||
|
void ForEachEntryName(T&& func) {
|
||||||
|
std::scoped_lock lock{m_mutex};
|
||||||
|
for (auto&& kv : m_entryNames) {
|
||||||
|
func(kv.second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wpi::log::StartRecordData GetEntry(std::string_view name) const {
|
||||||
|
std::scoped_lock lock{m_mutex};
|
||||||
|
auto it = m_entryNames.find(name);
|
||||||
|
if (it == m_entryNames.end()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wpi::log::DataLogReader& GetReader() const { return m_reader; }
|
||||||
|
|
||||||
|
// note: these are called on separate thread
|
||||||
|
wpi::sig::Signal_mt<const wpi::log::StartRecordData&> sigEntryAdded;
|
||||||
|
wpi::sig::Signal_mt<> sigDone;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ReadMain();
|
||||||
|
|
||||||
|
wpi::log::DataLogReader m_reader;
|
||||||
|
mutable wpi::mutex m_mutex;
|
||||||
|
std::atomic_bool m_active{true};
|
||||||
|
std::atomic_bool m_done{false};
|
||||||
|
std::atomic<unsigned int> m_numRecords{0};
|
||||||
|
std::map<std::string, wpi::log::StartRecordData, std::less<>> m_entryNames;
|
||||||
|
wpi::DenseMap<int, wpi::log::StartRecordData> m_entries;
|
||||||
|
std::thread m_thread;
|
||||||
|
};
|
||||||
393
datalogtool/src/main/native/cpp/Downloader.cpp
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
// 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 "Downloader.h"
|
||||||
|
|
||||||
|
#include <libssh/sftp.h>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <io.h>
|
||||||
|
#else
|
||||||
|
#include <sys/fcntl.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
#include <fmt/format.h>
|
||||||
|
#include <glass/Storage.h>
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <imgui_stdlib.h>
|
||||||
|
#include <portable-file-dialogs.h>
|
||||||
|
#include <wpi/StringExtras.h>
|
||||||
|
#include <wpi/fs.h>
|
||||||
|
|
||||||
|
#include "Sftp.h"
|
||||||
|
|
||||||
|
Downloader::Downloader(glass::Storage& storage)
|
||||||
|
: m_serverTeam{storage.GetString("serverTeam")},
|
||||||
|
m_remoteDir{storage.GetString("remoteDir", "/home/lvuser")},
|
||||||
|
m_username{storage.GetString("username", "lvuser")},
|
||||||
|
m_localDir{storage.GetString("localDir")},
|
||||||
|
m_deleteAfter{storage.GetBool("deleteAfter", true)},
|
||||||
|
m_thread{[this] { ThreadMain(); }} {}
|
||||||
|
|
||||||
|
Downloader::~Downloader() {
|
||||||
|
{
|
||||||
|
std::scoped_lock lock{m_mutex};
|
||||||
|
m_state = kExit;
|
||||||
|
}
|
||||||
|
m_cv.notify_all();
|
||||||
|
m_thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Downloader::DisplayConnect() {
|
||||||
|
// IP or Team Number text box
|
||||||
|
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 12);
|
||||||
|
ImGui::InputText("Team Number / Address", &m_serverTeam);
|
||||||
|
|
||||||
|
// Username/password
|
||||||
|
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 12);
|
||||||
|
ImGui::InputText("Username", &m_username);
|
||||||
|
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 12);
|
||||||
|
ImGui::InputText("Password", &m_password, ImGuiInputTextFlags_Password);
|
||||||
|
|
||||||
|
// Connect button
|
||||||
|
if (ImGui::Button("Connect")) {
|
||||||
|
m_state = kConnecting;
|
||||||
|
m_cv.notify_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Downloader::DisplayDisconnectButton() {
|
||||||
|
if (ImGui::Button("Disconnect")) {
|
||||||
|
m_state = kDisconnecting;
|
||||||
|
m_cv.notify_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Downloader::DisplayRemoteDirSelector() {
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Refresh")) {
|
||||||
|
m_state = kGetFiles;
|
||||||
|
m_cv.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remote directory text box
|
||||||
|
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 20);
|
||||||
|
if (ImGui::InputText("Remote Dir", &m_remoteDir,
|
||||||
|
ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||||
|
m_state = kGetFiles;
|
||||||
|
m_cv.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
// List directories
|
||||||
|
for (auto&& dir : m_dirList) {
|
||||||
|
if (ImGui::Selectable(dir.c_str())) {
|
||||||
|
if (dir == "..") {
|
||||||
|
if (wpi::ends_with(m_remoteDir, '/')) {
|
||||||
|
m_remoteDir.resize(m_remoteDir.size() - 1);
|
||||||
|
}
|
||||||
|
m_remoteDir = wpi::rsplit(m_remoteDir, '/').first;
|
||||||
|
if (m_remoteDir.empty()) {
|
||||||
|
m_remoteDir = "/";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!wpi::ends_with(m_remoteDir, '/')) {
|
||||||
|
m_remoteDir += '/';
|
||||||
|
}
|
||||||
|
m_remoteDir += dir;
|
||||||
|
}
|
||||||
|
m_state = kGetFiles;
|
||||||
|
m_cv.notify_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Downloader::DisplayLocalDirSelector() {
|
||||||
|
// Local directory text / select button
|
||||||
|
if (ImGui::Button("Select Download Folder...")) {
|
||||||
|
m_localDirSelector =
|
||||||
|
std::make_unique<pfd::select_folder>("Select Download Folder");
|
||||||
|
}
|
||||||
|
ImGui::TextUnformatted(m_localDir.c_str());
|
||||||
|
|
||||||
|
// Delete after download (checkbox)
|
||||||
|
ImGui::Checkbox("Delete after download", &m_deleteAfter);
|
||||||
|
|
||||||
|
// Download button
|
||||||
|
if (!m_localDir.empty()) {
|
||||||
|
if (ImGui::Button("Download")) {
|
||||||
|
m_state = kDownload;
|
||||||
|
m_cv.notify_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t Downloader::DisplayFiles() {
|
||||||
|
// List of files (multi-select) (changes to progress bar for downloading)
|
||||||
|
size_t fileCount = 0;
|
||||||
|
if (ImGui::BeginTable(
|
||||||
|
"files", 3,
|
||||||
|
ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp)) {
|
||||||
|
ImGui::TableSetupColumn("File");
|
||||||
|
ImGui::TableSetupColumn("Size");
|
||||||
|
ImGui::TableSetupColumn("Download");
|
||||||
|
ImGui::TableHeadersRow();
|
||||||
|
for (auto&& download : m_downloadList) {
|
||||||
|
if ((m_state == kDownload || m_state == kDownloadDone) &&
|
||||||
|
!download.enabled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
++fileCount;
|
||||||
|
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::TextUnformatted(download.name.c_str());
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
auto sizeText = fmt::format("{}", download.size);
|
||||||
|
ImGui::TextUnformatted(sizeText.c_str());
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
if (m_state == kDownload || m_state == kDownloadDone) {
|
||||||
|
if (!download.status.empty()) {
|
||||||
|
ImGui::TextUnformatted(download.status.c_str());
|
||||||
|
} else {
|
||||||
|
ImGui::ProgressBar(download.complete);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
auto checkboxLabel = fmt::format("##{}", download.name);
|
||||||
|
ImGui::Checkbox(checkboxLabel.c_str(), &download.enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Downloader::Display() {
|
||||||
|
if (m_localDirSelector && m_localDirSelector->ready(0)) {
|
||||||
|
m_localDir = m_localDirSelector->result();
|
||||||
|
m_localDirSelector.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::scoped_lock lock{m_mutex};
|
||||||
|
|
||||||
|
if (!m_error.empty()) {
|
||||||
|
ImGui::TextUnformatted(m_error.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (m_state) {
|
||||||
|
case kDisconnected:
|
||||||
|
DisplayConnect();
|
||||||
|
break;
|
||||||
|
case kConnecting:
|
||||||
|
DisplayDisconnectButton();
|
||||||
|
ImGui::Text("Connecting to %s...", m_serverTeam.c_str());
|
||||||
|
break;
|
||||||
|
case kDisconnecting:
|
||||||
|
ImGui::TextUnformatted("Disconnecting...");
|
||||||
|
break;
|
||||||
|
case kConnected:
|
||||||
|
case kGetFiles:
|
||||||
|
DisplayDisconnectButton();
|
||||||
|
DisplayRemoteDirSelector();
|
||||||
|
if (DisplayFiles() > 0) {
|
||||||
|
DisplayLocalDirSelector();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case kDownload:
|
||||||
|
case kDownloadDone:
|
||||||
|
DisplayDisconnectButton();
|
||||||
|
DisplayFiles();
|
||||||
|
if (m_state == kDownloadDone) {
|
||||||
|
if (ImGui::Button("Download complete!")) {
|
||||||
|
m_state = kGetFiles;
|
||||||
|
m_cv.notify_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Downloader::ThreadMain() {
|
||||||
|
std::unique_ptr<sftp::Session> session;
|
||||||
|
|
||||||
|
static constexpr size_t kBufSize = 32 * 1024;
|
||||||
|
std::unique_ptr<uint8_t[]> copyBuf = std::make_unique<uint8_t[]>(kBufSize);
|
||||||
|
|
||||||
|
std::unique_lock lock{m_mutex};
|
||||||
|
while (m_state != kExit) {
|
||||||
|
State prev = m_state;
|
||||||
|
m_cv.wait(lock, [&] { return m_state != prev; });
|
||||||
|
m_error.clear();
|
||||||
|
try {
|
||||||
|
switch (m_state) {
|
||||||
|
case kConnecting:
|
||||||
|
if (auto team = wpi::parse_integer<unsigned int>(m_serverTeam, 10)) {
|
||||||
|
// team number
|
||||||
|
session = std::make_unique<sftp::Session>(
|
||||||
|
fmt::format("roborio-{}-frc.local", team.value()), 22,
|
||||||
|
m_username, m_password);
|
||||||
|
} else {
|
||||||
|
session = std::make_unique<sftp::Session>(m_serverTeam, 22,
|
||||||
|
m_username, m_password);
|
||||||
|
}
|
||||||
|
lock.unlock();
|
||||||
|
try {
|
||||||
|
session->Connect();
|
||||||
|
} catch (...) {
|
||||||
|
lock.lock();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
lock.lock();
|
||||||
|
// FALLTHROUGH
|
||||||
|
case kGetFiles: {
|
||||||
|
std::string dir = m_remoteDir;
|
||||||
|
std::vector<sftp::Attributes> fileList;
|
||||||
|
lock.unlock();
|
||||||
|
try {
|
||||||
|
fileList = session->ReadDir(dir);
|
||||||
|
} catch (sftp::Exception& ex) {
|
||||||
|
lock.lock();
|
||||||
|
if (ex.err == SSH_FX_OK || ex.err == SSH_FX_CONNECTION_LOST) {
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
m_error = ex.what();
|
||||||
|
m_dirList.clear();
|
||||||
|
m_downloadList.clear();
|
||||||
|
m_state = kConnected;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::sort(
|
||||||
|
fileList.begin(), fileList.end(),
|
||||||
|
[](const auto& l, const auto& r) { return l.name < r.name; });
|
||||||
|
lock.lock();
|
||||||
|
|
||||||
|
m_dirList.clear();
|
||||||
|
m_downloadList.clear();
|
||||||
|
for (auto&& attr : fileList) {
|
||||||
|
if (attr.type == SSH_FILEXFER_TYPE_DIRECTORY) {
|
||||||
|
if (attr.name != ".") {
|
||||||
|
m_dirList.emplace_back(attr.name);
|
||||||
|
}
|
||||||
|
} else if (attr.type == SSH_FILEXFER_TYPE_REGULAR &&
|
||||||
|
(attr.flags & SSH_FILEXFER_ATTR_SIZE) != 0 &&
|
||||||
|
wpi::ends_with(attr.name, ".wpilog")) {
|
||||||
|
m_downloadList.emplace_back(attr.name, attr.size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_state = kConnected;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case kDisconnecting:
|
||||||
|
session.reset();
|
||||||
|
m_state = kDisconnected;
|
||||||
|
break;
|
||||||
|
case kDownload: {
|
||||||
|
for (auto&& download : m_downloadList) {
|
||||||
|
if (m_state != kDownload) {
|
||||||
|
// user aborted
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!download.enabled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto remoteFilename = fmt::format(
|
||||||
|
"{}{}{}", m_remoteDir,
|
||||||
|
wpi::ends_with(m_remoteDir, '/') ? "" : "/", download.name);
|
||||||
|
auto localFilename = fs::path{m_localDir} / download.name;
|
||||||
|
uint64_t fileSize = download.size;
|
||||||
|
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
// open local file
|
||||||
|
std::error_code ec;
|
||||||
|
fs::file_t of = fs::OpenFileForWrite(localFilename, ec,
|
||||||
|
fs::CD_CreateNew, fs::OF_None);
|
||||||
|
if (ec) {
|
||||||
|
// failed to open
|
||||||
|
lock.lock();
|
||||||
|
download.status = ec.message();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int ofd = fs::FileToFd(of, ec, fs::OF_None);
|
||||||
|
if (ofd == -1 || ec) {
|
||||||
|
// failed to convert to fd
|
||||||
|
lock.lock();
|
||||||
|
download.status = ec.message();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// open remote file
|
||||||
|
sftp::File f = session->Open(remoteFilename, O_RDONLY, 0);
|
||||||
|
|
||||||
|
// copy in chunks
|
||||||
|
uint64_t total = 0;
|
||||||
|
while (total < fileSize) {
|
||||||
|
uint64_t toCopy = (std::min)(fileSize - total,
|
||||||
|
static_cast<uint64_t>(kBufSize));
|
||||||
|
auto copied = f.Read(copyBuf.get(), toCopy);
|
||||||
|
if (write(ofd, copyBuf.get(), copied) !=
|
||||||
|
static_cast<int64_t>(copied)) {
|
||||||
|
// error writing
|
||||||
|
close(ofd);
|
||||||
|
fs::remove(localFilename, ec);
|
||||||
|
lock.lock();
|
||||||
|
download.status = "error writing local file";
|
||||||
|
goto err;
|
||||||
|
}
|
||||||
|
total += copied;
|
||||||
|
lock.lock();
|
||||||
|
download.complete = static_cast<float>(total) / fileSize;
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// close local file
|
||||||
|
close(ofd);
|
||||||
|
ofd = -1;
|
||||||
|
|
||||||
|
// delete remote file (if enabled)
|
||||||
|
if (m_deleteAfter) {
|
||||||
|
f = sftp::File{};
|
||||||
|
session->Unlink(remoteFilename);
|
||||||
|
}
|
||||||
|
} catch (sftp::Exception& ex) {
|
||||||
|
if (ofd != -1) {
|
||||||
|
// close local file and delete it (due to failure)
|
||||||
|
close(ofd);
|
||||||
|
fs::remove(localFilename, ec);
|
||||||
|
}
|
||||||
|
lock.lock();
|
||||||
|
download.status = ex.what();
|
||||||
|
if (ex.err == SSH_FX_OK || ex.err == SSH_FX_CONNECTION_LOST) {
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
lock.lock();
|
||||||
|
err : {}
|
||||||
|
}
|
||||||
|
if (m_state == kDownload) {
|
||||||
|
m_state = kDownloadDone;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (sftp::Exception& ex) {
|
||||||
|
m_error = ex.what();
|
||||||
|
session.reset();
|
||||||
|
m_state = kDisconnected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
datalogtool/src/main/native/cpp/Downloader.h
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// 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 <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <wpi/condition_variable.h>
|
||||||
|
#include <wpi/mutex.h>
|
||||||
|
|
||||||
|
namespace glass {
|
||||||
|
class Storage;
|
||||||
|
} // namespace glass
|
||||||
|
|
||||||
|
namespace pfd {
|
||||||
|
class select_folder;
|
||||||
|
} // namespace pfd
|
||||||
|
|
||||||
|
class Downloader {
|
||||||
|
public:
|
||||||
|
explicit Downloader(glass::Storage& storage);
|
||||||
|
~Downloader();
|
||||||
|
|
||||||
|
void Display();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void DisplayConnect();
|
||||||
|
void DisplayDisconnectButton();
|
||||||
|
void DisplayRemoteDirSelector();
|
||||||
|
void DisplayLocalDirSelector();
|
||||||
|
size_t DisplayFiles();
|
||||||
|
|
||||||
|
void ThreadMain();
|
||||||
|
|
||||||
|
wpi::mutex m_mutex;
|
||||||
|
enum State {
|
||||||
|
kDisconnected,
|
||||||
|
kConnecting,
|
||||||
|
kConnected,
|
||||||
|
kDisconnecting,
|
||||||
|
kGetFiles,
|
||||||
|
kDownload,
|
||||||
|
kDownloadDone,
|
||||||
|
kExit
|
||||||
|
} m_state = kDisconnected;
|
||||||
|
std::condition_variable m_cv;
|
||||||
|
|
||||||
|
std::string& m_serverTeam;
|
||||||
|
std::string& m_remoteDir;
|
||||||
|
std::string& m_username;
|
||||||
|
std::string m_password;
|
||||||
|
|
||||||
|
std::string& m_localDir;
|
||||||
|
std::unique_ptr<pfd::select_folder> m_localDirSelector;
|
||||||
|
|
||||||
|
bool& m_deleteAfter;
|
||||||
|
|
||||||
|
std::vector<std::string> m_dirList;
|
||||||
|
struct DownloadState {
|
||||||
|
DownloadState(std::string_view name, uint64_t size)
|
||||||
|
: name{name}, size{size} {}
|
||||||
|
|
||||||
|
std::string name;
|
||||||
|
uint64_t size;
|
||||||
|
bool enabled = true;
|
||||||
|
float complete = 0.0;
|
||||||
|
std::string status;
|
||||||
|
};
|
||||||
|
std::vector<DownloadState> m_downloadList;
|
||||||
|
|
||||||
|
std::string m_error;
|
||||||
|
|
||||||
|
std::thread m_thread;
|
||||||
|
};
|
||||||
655
datalogtool/src/main/native/cpp/Exporter.cpp
Normal file
@@ -0,0 +1,655 @@
|
|||||||
|
// 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 "Exporter.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <ctime>
|
||||||
|
#include <future>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fmt/chrono.h>
|
||||||
|
#include <fmt/format.h>
|
||||||
|
#include <glass/Storage.h>
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <imgui_internal.h>
|
||||||
|
#include <imgui_stdlib.h>
|
||||||
|
#include <portable-file-dialogs.h>
|
||||||
|
#include <wpi/DenseMap.h>
|
||||||
|
#include <wpi/MemoryBuffer.h>
|
||||||
|
#include <wpi/SmallVector.h>
|
||||||
|
#include <wpi/SpanExtras.h>
|
||||||
|
#include <wpi/StringExtras.h>
|
||||||
|
#include <wpi/fmt/raw_ostream.h>
|
||||||
|
#include <wpi/fs.h>
|
||||||
|
#include <wpi/mutex.h>
|
||||||
|
#include <wpi/raw_ostream.h>
|
||||||
|
|
||||||
|
#include "App.h"
|
||||||
|
#include "DataLogThread.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
struct InputFile {
|
||||||
|
explicit InputFile(std::unique_ptr<DataLogThread> datalog);
|
||||||
|
|
||||||
|
InputFile(std::string_view filename, std::string_view status)
|
||||||
|
: filename{filename},
|
||||||
|
stem{fs::path{filename}.stem().string()},
|
||||||
|
status{status} {}
|
||||||
|
|
||||||
|
~InputFile();
|
||||||
|
|
||||||
|
std::string filename;
|
||||||
|
std::string stem;
|
||||||
|
std::unique_ptr<DataLogThread> datalog;
|
||||||
|
std::string status;
|
||||||
|
bool highlight = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
explicit Entry(const wpi::log::StartRecordData& srd)
|
||||||
|
: name{srd.name}, type{srd.type}, metadata{srd.metadata} {}
|
||||||
|
|
||||||
|
std::string name;
|
||||||
|
std::string type;
|
||||||
|
std::string metadata;
|
||||||
|
std::set<InputFile*> inputFiles;
|
||||||
|
bool typeConflict = false;
|
||||||
|
bool metadataConflict = false;
|
||||||
|
bool selected = true;
|
||||||
|
|
||||||
|
// used only during export
|
||||||
|
int column = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EntryTreeNode {
|
||||||
|
explicit EntryTreeNode(std::string_view name) : name{name} {}
|
||||||
|
std::string name; // name of just this node
|
||||||
|
std::string path; // full path if entry is nullptr
|
||||||
|
Entry* entry = nullptr;
|
||||||
|
std::vector<EntryTreeNode> children; // children, sorted by name
|
||||||
|
int selected = 1;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
static std::map<std::string, std::unique_ptr<InputFile>, std::less<>>
|
||||||
|
gInputFiles;
|
||||||
|
static wpi::mutex gEntriesMutex;
|
||||||
|
static std::map<std::string, std::unique_ptr<Entry>, std::less<>> gEntries;
|
||||||
|
static std::vector<EntryTreeNode> gEntryTree;
|
||||||
|
std::atomic_int gExportCount{0};
|
||||||
|
|
||||||
|
// must be called with gEntriesMutex held
|
||||||
|
static void RebuildEntryTree() {
|
||||||
|
gEntryTree.clear();
|
||||||
|
wpi::SmallVector<std::string_view, 16> parts;
|
||||||
|
for (auto& kv : gEntries) {
|
||||||
|
parts.clear();
|
||||||
|
// split on first : if one is present
|
||||||
|
auto [prefix, mainpart] = wpi::split(kv.first, ':');
|
||||||
|
if (mainpart.empty() || wpi::contains(prefix, '/')) {
|
||||||
|
mainpart = kv.first;
|
||||||
|
} else {
|
||||||
|
parts.emplace_back(prefix);
|
||||||
|
}
|
||||||
|
wpi::split(mainpart, parts, '/', -1, false);
|
||||||
|
|
||||||
|
// ignore a raw "/" key
|
||||||
|
if (parts.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get to leaf
|
||||||
|
auto nodes = &gEntryTree;
|
||||||
|
for (auto part : wpi::drop_back(wpi::span{parts.begin(), parts.end()})) {
|
||||||
|
auto it =
|
||||||
|
std::find_if(nodes->begin(), nodes->end(),
|
||||||
|
[&](const auto& node) { return node.name == part; });
|
||||||
|
if (it == nodes->end()) {
|
||||||
|
nodes->emplace_back(part);
|
||||||
|
// path is from the beginning of the string to the end of the current
|
||||||
|
// part; this works because part is a reference to the internals of
|
||||||
|
// kv.first
|
||||||
|
nodes->back().path.assign(kv.first.data(),
|
||||||
|
part.data() + part.size() - kv.first.data());
|
||||||
|
it = nodes->end() - 1;
|
||||||
|
}
|
||||||
|
nodes = &it->children;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto it = std::find_if(nodes->begin(), nodes->end(), [&](const auto& node) {
|
||||||
|
return node.name == parts.back();
|
||||||
|
});
|
||||||
|
if (it == nodes->end()) {
|
||||||
|
nodes->emplace_back(parts.back());
|
||||||
|
// no need to set path, as it's identical to kv.first
|
||||||
|
it = nodes->end() - 1;
|
||||||
|
}
|
||||||
|
it->entry = kv.second.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InputFile::InputFile(std::unique_ptr<DataLogThread> datalog_)
|
||||||
|
: filename{datalog_->GetBufferIdentifier()},
|
||||||
|
stem{fs::path{filename}.stem().string()},
|
||||||
|
datalog{std::move(datalog_)} {
|
||||||
|
datalog->sigEntryAdded.connect([this](const wpi::log::StartRecordData& srd) {
|
||||||
|
std::scoped_lock lock{gEntriesMutex};
|
||||||
|
auto it = gEntries.find(srd.name);
|
||||||
|
if (it == gEntries.end()) {
|
||||||
|
it = gEntries.emplace(srd.name, std::make_unique<Entry>(srd)).first;
|
||||||
|
RebuildEntryTree();
|
||||||
|
} else {
|
||||||
|
if (it->second->type != srd.type) {
|
||||||
|
it->second->typeConflict = true;
|
||||||
|
}
|
||||||
|
if (it->second->metadata != srd.metadata) {
|
||||||
|
it->second->metadataConflict = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it->second->inputFiles.emplace(this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
InputFile::~InputFile() {
|
||||||
|
if (gShutdown || !datalog) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::scoped_lock lock{gEntriesMutex};
|
||||||
|
bool changed = false;
|
||||||
|
for (auto it = gEntries.begin(); it != gEntries.end();) {
|
||||||
|
it->second->inputFiles.erase(this);
|
||||||
|
if (it->second->inputFiles.empty()) {
|
||||||
|
it = gEntries.erase(it);
|
||||||
|
changed = true;
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
RebuildEntryTree();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::unique_ptr<InputFile> LoadDataLog(std::string_view filename) {
|
||||||
|
std::error_code ec;
|
||||||
|
auto buf = wpi::MemoryBuffer::GetFile(filename, ec);
|
||||||
|
std::string fn{filename};
|
||||||
|
if (ec) {
|
||||||
|
return std::make_unique<InputFile>(
|
||||||
|
fn, fmt::format("Could not open file: {}", ec.message()));
|
||||||
|
}
|
||||||
|
|
||||||
|
wpi::log::DataLogReader reader{std::move(buf)};
|
||||||
|
if (!reader.IsValid()) {
|
||||||
|
return std::make_unique<InputFile>(fn, "Not a valid datalog file");
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_unique<InputFile>(
|
||||||
|
std::make_unique<DataLogThread>(std::move(reader)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayInputFiles() {
|
||||||
|
static std::unique_ptr<pfd::open_file> dataFileSelector;
|
||||||
|
|
||||||
|
SetNextWindowPos(ImVec2{0, 20}, ImGuiCond_FirstUseEver);
|
||||||
|
SetNextWindowSize(ImVec2{375, 230}, ImGuiCond_FirstUseEver);
|
||||||
|
if (ImGui::Begin("Input Files")) {
|
||||||
|
if (ImGui::Button("Open File(s)...")) {
|
||||||
|
dataFileSelector = std::make_unique<pfd::open_file>(
|
||||||
|
"Select Data Log", "",
|
||||||
|
std::vector<std::string>{"DataLog Files", "*.wpilog"},
|
||||||
|
pfd::opt::multiselect);
|
||||||
|
}
|
||||||
|
ImGui::BeginTable(
|
||||||
|
"Input Files", 3,
|
||||||
|
ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp);
|
||||||
|
ImGui::TableSetupColumn("File");
|
||||||
|
ImGui::TableSetupColumn("Status");
|
||||||
|
ImGui::TableSetupColumn("X", ImGuiTableColumnFlags_WidthFixed |
|
||||||
|
ImGuiTableColumnFlags_NoHeaderLabel |
|
||||||
|
ImGuiTableColumnFlags_NoHeaderWidth);
|
||||||
|
ImGui::TableHeadersRow();
|
||||||
|
for (auto it = gInputFiles.begin(); it != gInputFiles.end();) {
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
if (it->second->highlight) {
|
||||||
|
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,
|
||||||
|
IM_COL32(0, 64, 0, 255));
|
||||||
|
it->second->highlight = false;
|
||||||
|
}
|
||||||
|
ImGui::TextUnformatted(it->first.c_str());
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::SetTooltip("%s", it->second->filename.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
if (it->second->datalog) {
|
||||||
|
ImGui::Text("%u records, %u entries%s",
|
||||||
|
it->second->datalog->GetNumRecords(),
|
||||||
|
it->second->datalog->GetNumEntries(),
|
||||||
|
it->second->datalog->IsDone() ? "" : " (working)");
|
||||||
|
} else {
|
||||||
|
ImGui::TextUnformatted(it->second->status.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::PushID(it->first.c_str());
|
||||||
|
if (ImGui::SmallButton("X")) {
|
||||||
|
it = gInputFiles.erase(it);
|
||||||
|
gExportCount = 0;
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
ImGui::EndTable();
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
|
||||||
|
// Load data file(s)
|
||||||
|
if (dataFileSelector && dataFileSelector->ready(0)) {
|
||||||
|
auto result = dataFileSelector->result();
|
||||||
|
for (auto&& filename : result) {
|
||||||
|
// don't allow duplicates
|
||||||
|
std::string stem = fs::path{filename}.stem().string();
|
||||||
|
auto it = gInputFiles.find(stem);
|
||||||
|
if (it == gInputFiles.end()) {
|
||||||
|
gInputFiles.emplace(std::move(stem), LoadDataLog(filename));
|
||||||
|
gExportCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataFileSelector.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool EmitEntry(const std::string& name, Entry& entry) {
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
bool rv = ImGui::Checkbox(name.c_str(), &entry.selected);
|
||||||
|
if (ImGui::IsItemHovered() && gInputFiles.size() > 1) {
|
||||||
|
for (auto inputFile : entry.inputFiles) {
|
||||||
|
inputFile->highlight = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
if (entry.typeConflict) {
|
||||||
|
ImGui::TextUnformatted("(Inconsistent)");
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::BeginTooltip();
|
||||||
|
for (auto inputFile : entry.inputFiles) {
|
||||||
|
ImGui::Text(
|
||||||
|
"%s: %s", inputFile->stem.c_str(),
|
||||||
|
std::string{inputFile->datalog->GetEntry(entry.name).type}.c_str());
|
||||||
|
}
|
||||||
|
ImGui::EndTooltip();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ImGui::TextUnformatted(entry.type.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
if (entry.metadataConflict) {
|
||||||
|
ImGui::TextUnformatted("(Inconsistent)");
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::BeginTooltip();
|
||||||
|
for (auto inputFile : entry.inputFiles) {
|
||||||
|
ImGui::Text(
|
||||||
|
"%s: %s", inputFile->stem.c_str(),
|
||||||
|
std::string{inputFile->datalog->GetEntry(entry.name).metadata}
|
||||||
|
.c_str());
|
||||||
|
}
|
||||||
|
ImGui::EndTooltip();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ImGui::TextUnformatted(entry.metadata.c_str());
|
||||||
|
}
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool EmitEntryTree(std::vector<EntryTreeNode>& tree) {
|
||||||
|
bool rv = false;
|
||||||
|
for (auto&& node : tree) {
|
||||||
|
if (node.entry) {
|
||||||
|
if (EmitEntry(node.name, *node.entry)) {
|
||||||
|
rv = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!node.children.empty()) {
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
auto label = fmt::format("##check_{}", node.name);
|
||||||
|
if (node.selected == -1) {
|
||||||
|
ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, true);
|
||||||
|
bool b = false;
|
||||||
|
if (ImGui::Checkbox(label.c_str(), &b)) {
|
||||||
|
node.selected = 3; // 3 = enable group
|
||||||
|
rv = true;
|
||||||
|
}
|
||||||
|
ImGui::PopItemFlag();
|
||||||
|
} else {
|
||||||
|
bool b = node.selected == 1 || node.selected == 3;
|
||||||
|
if (ImGui::Checkbox(label.c_str(), &b)) {
|
||||||
|
node.selected = b ? 3 : 2; // 2 = disable group
|
||||||
|
rv = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
bool open = ImGui::TreeNodeEx(node.name.c_str(),
|
||||||
|
ImGuiTreeNodeFlags_SpanFullWidth);
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
if (open) {
|
||||||
|
if (EmitEntryTree(node.children)) {
|
||||||
|
rv = true;
|
||||||
|
}
|
||||||
|
ImGui::TreePop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void RefreshTreeCheckboxes(std::vector<EntryTreeNode>& tree,
|
||||||
|
int* selected) {
|
||||||
|
bool first = true;
|
||||||
|
for (auto&& node : tree) {
|
||||||
|
if (node.entry) {
|
||||||
|
if (first && *selected == -1) {
|
||||||
|
*selected = node.entry->selected ? 1 : 0;
|
||||||
|
}
|
||||||
|
if ((*selected == 0 && node.entry->selected) ||
|
||||||
|
(*selected == 1 && !node.entry->selected)) {
|
||||||
|
*selected = -1; // inconsistent
|
||||||
|
} else if (*selected == 2) { // disable group
|
||||||
|
node.entry->selected = false;
|
||||||
|
} else if (*selected == 3) { // enable group
|
||||||
|
node.entry->selected = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!node.children.empty()) {
|
||||||
|
if (*selected == 2) { // disable group
|
||||||
|
node.selected = 2;
|
||||||
|
} else if (*selected == 3) { // enable group
|
||||||
|
node.selected = 3;
|
||||||
|
}
|
||||||
|
RefreshTreeCheckboxes(node.children, &node.selected);
|
||||||
|
if (node.selected == 2) {
|
||||||
|
node.selected = 0;
|
||||||
|
} else if (node.selected == 3) {
|
||||||
|
node.selected = 1;
|
||||||
|
}
|
||||||
|
if (first && *selected == -1) {
|
||||||
|
*selected = node.selected;
|
||||||
|
} else if (node.selected == -1 ||
|
||||||
|
(*selected == 0 && node.selected == 1) ||
|
||||||
|
(*selected == 1 && node.selected == 0)) {
|
||||||
|
*selected = -1; // inconsistent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayEntries() {
|
||||||
|
SetNextWindowPos(ImVec2{380, 20}, ImGuiCond_FirstUseEver);
|
||||||
|
SetNextWindowSize(ImVec2{540, 365}, ImGuiCond_FirstUseEver);
|
||||||
|
if (ImGui::Begin("Entries")) {
|
||||||
|
static bool treeView = true;
|
||||||
|
if (ImGui::BeginPopupContextItem()) {
|
||||||
|
ImGui::MenuItem("Tree View", "", &treeView);
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
std::scoped_lock lock{gEntriesMutex};
|
||||||
|
ImGui::BeginTable(
|
||||||
|
"Entries", 3,
|
||||||
|
ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp);
|
||||||
|
ImGui::TableSetupColumn("Name");
|
||||||
|
ImGui::TableSetupColumn("Type");
|
||||||
|
ImGui::TableSetupColumn("Metadata");
|
||||||
|
ImGui::TableHeadersRow();
|
||||||
|
if (treeView) {
|
||||||
|
if (EmitEntryTree(gEntryTree)) {
|
||||||
|
int selected = -1;
|
||||||
|
RefreshTreeCheckboxes(gEntryTree, &selected);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (auto&& kv : gEntries) {
|
||||||
|
EmitEntry(kv.first, *kv.second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndTable();
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
|
static wpi::mutex gExportMutex;
|
||||||
|
static std::vector<std::string> gExportErrors;
|
||||||
|
|
||||||
|
static void PrintEscapedCsvString(wpi::raw_ostream& os, std::string_view str) {
|
||||||
|
auto s = str;
|
||||||
|
while (!s.empty()) {
|
||||||
|
std::string_view fragment;
|
||||||
|
std::tie(fragment, s) = wpi::split(s, '"');
|
||||||
|
os << fragment;
|
||||||
|
if (!s.empty()) {
|
||||||
|
os << '"' << '"';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (wpi::ends_with(str, '"')) {
|
||||||
|
os << '"' << '"';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ValueToCsv(wpi::raw_ostream& os, const Entry& entry,
|
||||||
|
const wpi::log::DataLogRecord& record) {
|
||||||
|
// handle systemTime specially
|
||||||
|
if (entry.name == "systemTime" && entry.type == "int64") {
|
||||||
|
int64_t val;
|
||||||
|
if (record.GetInteger(&val)) {
|
||||||
|
std::time_t timeval = val / 1000000;
|
||||||
|
fmt::print(os, "{:%Y-%m-%d %H:%M:%S}.{:06}", *std::localtime(&timeval),
|
||||||
|
val % 1000000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (entry.type == "double") {
|
||||||
|
double val;
|
||||||
|
if (record.GetDouble(&val)) {
|
||||||
|
fmt::print(os, "{}", val);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (entry.type == "int64") {
|
||||||
|
int64_t val;
|
||||||
|
if (record.GetInteger(&val)) {
|
||||||
|
fmt::print(os, "{}", val);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (entry.type == "string" || entry.type == "json") {
|
||||||
|
std::string_view val;
|
||||||
|
record.GetString(&val);
|
||||||
|
os << '"';
|
||||||
|
PrintEscapedCsvString(os, val);
|
||||||
|
os << '"';
|
||||||
|
return;
|
||||||
|
} else if (entry.type == "boolean") {
|
||||||
|
bool val;
|
||||||
|
if (record.GetBoolean(&val)) {
|
||||||
|
fmt::print(os, "{}", val);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (entry.type == "double[]") {
|
||||||
|
std::vector<double> val;
|
||||||
|
if (record.GetDoubleArray(&val)) {
|
||||||
|
fmt::print(os, "{}", fmt::join(val, ";"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (entry.type == "float[]") {
|
||||||
|
std::vector<float> val;
|
||||||
|
if (record.GetFloatArray(&val)) {
|
||||||
|
fmt::print(os, "{}", fmt::join(val, ";"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (entry.type == "int64[]") {
|
||||||
|
std::vector<int64_t> val;
|
||||||
|
if (record.GetIntegerArray(&val)) {
|
||||||
|
fmt::print(os, "{}", fmt::join(val, ";"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (entry.type == "string[]") {
|
||||||
|
std::vector<std::string_view> val;
|
||||||
|
if (record.GetStringArray(&val)) {
|
||||||
|
os << '"';
|
||||||
|
bool first = true;
|
||||||
|
for (auto&& v : val) {
|
||||||
|
if (!first) {
|
||||||
|
os << ';';
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
PrintEscapedCsvString(os, v);
|
||||||
|
}
|
||||||
|
os << '"';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt::print(os, "<invalid>");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ExportCsvFile(InputFile& f, wpi::raw_ostream& os, int style) {
|
||||||
|
// header
|
||||||
|
if (style == 0) {
|
||||||
|
os << "Timestamp,Name,Value\n";
|
||||||
|
} else if (style == 1) {
|
||||||
|
// scan for exported fields for this file to print header and assign columns
|
||||||
|
os << "Timestamp";
|
||||||
|
int columnNum = 0;
|
||||||
|
for (auto&& entry : gEntries) {
|
||||||
|
if (entry.second->selected &&
|
||||||
|
entry.second->inputFiles.find(&f) != entry.second->inputFiles.end()) {
|
||||||
|
os << ',' << '"';
|
||||||
|
PrintEscapedCsvString(os, entry.first);
|
||||||
|
os << '"';
|
||||||
|
entry.second->column = columnNum++;
|
||||||
|
} else {
|
||||||
|
entry.second->column = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
wpi::DenseMap<int, Entry*> nameMap;
|
||||||
|
for (auto&& record : f.datalog->GetReader()) {
|
||||||
|
if (record.IsStart()) {
|
||||||
|
wpi::log::StartRecordData data;
|
||||||
|
if (record.GetStartData(&data)) {
|
||||||
|
auto it = gEntries.find(data.name);
|
||||||
|
if (it != gEntries.end() && it->second->selected) {
|
||||||
|
nameMap[data.entry] = it->second.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (record.IsFinish()) {
|
||||||
|
int entry;
|
||||||
|
if (record.GetFinishEntry(&entry)) {
|
||||||
|
nameMap.erase(entry);
|
||||||
|
}
|
||||||
|
} else if (!record.IsControl()) {
|
||||||
|
auto entryIt = nameMap.find(record.GetEntry());
|
||||||
|
if (entryIt == nameMap.end()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Entry* entry = entryIt->second;
|
||||||
|
|
||||||
|
if (style == 0) {
|
||||||
|
fmt::print(os, "{},\"", record.GetTimestamp() / 1000000.0);
|
||||||
|
PrintEscapedCsvString(os, entry->name);
|
||||||
|
os << '"' << ',';
|
||||||
|
ValueToCsv(os, *entry, record);
|
||||||
|
os << '\n';
|
||||||
|
} else if (style == 1 && entry->column != -1) {
|
||||||
|
fmt::print(os, "{},", record.GetTimestamp() / 1000000.0);
|
||||||
|
for (int i = 0; i < entry->column; ++i) {
|
||||||
|
os << ',';
|
||||||
|
}
|
||||||
|
ValueToCsv(os, *entry, record);
|
||||||
|
os << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ExportCsv(std::string_view outputFolder, int style) {
|
||||||
|
fs::path outPath{outputFolder};
|
||||||
|
for (auto&& f : gInputFiles) {
|
||||||
|
if (f.second->datalog) {
|
||||||
|
std::error_code ec;
|
||||||
|
auto of = fs::OpenFileForWrite(
|
||||||
|
outPath / fs::path{f.first}.replace_extension("csv"), ec,
|
||||||
|
fs::CD_CreateNew, fs::OF_Text);
|
||||||
|
if (ec) {
|
||||||
|
std::scoped_lock lock{gExportMutex};
|
||||||
|
gExportErrors.emplace_back(
|
||||||
|
fmt::format("{}: {}", f.first, ec.message()));
|
||||||
|
++gExportCount;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
wpi::raw_fd_ostream os{fs::FileToFd(of, ec, fs::OF_Text), true};
|
||||||
|
ExportCsvFile(*f.second, os, style);
|
||||||
|
}
|
||||||
|
++gExportCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayOutput(glass::Storage& storage) {
|
||||||
|
static std::string& outputFolder = storage.GetString("outputFolder");
|
||||||
|
static std::unique_ptr<pfd::select_folder> outputFolderSelector;
|
||||||
|
|
||||||
|
SetNextWindowPos(ImVec2{380, 390}, ImGuiCond_FirstUseEver);
|
||||||
|
SetNextWindowSize(ImVec2{540, 120}, ImGuiCond_FirstUseEver);
|
||||||
|
if (ImGui::Begin("Output")) {
|
||||||
|
if (ImGui::Button("Select Output Folder...")) {
|
||||||
|
outputFolderSelector =
|
||||||
|
std::make_unique<pfd::select_folder>("Select Output Folder");
|
||||||
|
}
|
||||||
|
ImGui::TextUnformatted(outputFolder.c_str());
|
||||||
|
|
||||||
|
static const char* const options[] = {"List", "Table"};
|
||||||
|
static int style = 0;
|
||||||
|
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
|
||||||
|
ImGui::Combo("Style", &style, options,
|
||||||
|
sizeof(options) / sizeof(const char*));
|
||||||
|
|
||||||
|
static std::future<void> exporter;
|
||||||
|
if (!gInputFiles.empty() && !outputFolder.empty() &&
|
||||||
|
ImGui::Button("Export CSV") &&
|
||||||
|
(gExportCount == 0 ||
|
||||||
|
gExportCount == static_cast<int>(gInputFiles.size()))) {
|
||||||
|
gExportCount = 0;
|
||||||
|
gExportErrors.clear();
|
||||||
|
exporter = std::async(std::launch::async, ExportCsv, outputFolder, style);
|
||||||
|
}
|
||||||
|
if (exporter.valid()) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Text("Exported %d/%d", gExportCount.load(),
|
||||||
|
static_cast<int>(gInputFiles.size()));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::scoped_lock lock{gExportMutex};
|
||||||
|
for (auto&& err : gExportErrors) {
|
||||||
|
ImGui::TextUnformatted(err.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
|
||||||
|
if (outputFolderSelector && outputFolderSelector->ready(0)) {
|
||||||
|
outputFolder = outputFolderSelector->result();
|
||||||
|
outputFolderSelector.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
15
datalogtool/src/main/native/cpp/Exporter.h
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
namespace glass {
|
||||||
|
class Storage;
|
||||||
|
} // namespace glass
|
||||||
|
|
||||||
|
void DisplayInputFiles();
|
||||||
|
void DisplayEntries();
|
||||||
|
void DisplayOutput(glass::Storage& storage);
|
||||||
|
|
||||||
|
extern bool gShutdown;
|
||||||
215
datalogtool/src/main/native/cpp/Sftp.cpp
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
// 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 "Sftp.h"
|
||||||
|
|
||||||
|
#include <fmt/format.h>
|
||||||
|
|
||||||
|
using namespace sftp;
|
||||||
|
|
||||||
|
Attributes::Attributes(sftp_attributes&& attr)
|
||||||
|
: name{attr->name}, flags{attr->flags}, type{attr->type}, size{attr->size} {
|
||||||
|
sftp_attributes_free(attr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string GetError(sftp_session sftp) {
|
||||||
|
switch (sftp_get_error(sftp)) {
|
||||||
|
case SSH_FX_EOF:
|
||||||
|
return "end of file";
|
||||||
|
case SSH_FX_NO_SUCH_FILE:
|
||||||
|
return "no such file";
|
||||||
|
case SSH_FX_PERMISSION_DENIED:
|
||||||
|
return "permission denied";
|
||||||
|
case SSH_FX_FAILURE:
|
||||||
|
return "SFTP failure";
|
||||||
|
case SSH_FX_BAD_MESSAGE:
|
||||||
|
return "SFTP bad message";
|
||||||
|
case SSH_FX_NO_CONNECTION:
|
||||||
|
return "SFTP no connection";
|
||||||
|
case SSH_FX_CONNECTION_LOST:
|
||||||
|
return "SFTP connection lost";
|
||||||
|
case SSH_FX_OP_UNSUPPORTED:
|
||||||
|
return "SFTP operation unsupported";
|
||||||
|
case SSH_FX_INVALID_HANDLE:
|
||||||
|
return "SFTP invalid handle";
|
||||||
|
case SSH_FX_NO_SUCH_PATH:
|
||||||
|
return "no such path";
|
||||||
|
case SSH_FX_FILE_ALREADY_EXISTS:
|
||||||
|
return "file already exists";
|
||||||
|
case SSH_FX_WRITE_PROTECT:
|
||||||
|
return "write protected filesystem";
|
||||||
|
case SSH_FX_NO_MEDIA:
|
||||||
|
return "no media inserted";
|
||||||
|
default:
|
||||||
|
return ssh_get_error(sftp->session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Exception::Exception(sftp_session sftp)
|
||||||
|
: runtime_error{GetError(sftp)}, err{sftp_get_error(sftp)} {}
|
||||||
|
|
||||||
|
File::~File() {
|
||||||
|
if (m_handle) {
|
||||||
|
sftp_close(m_handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Attributes File::Stat() const {
|
||||||
|
sftp_attributes attr = sftp_fstat(m_handle);
|
||||||
|
if (!attr) {
|
||||||
|
throw Exception{m_handle->sftp};
|
||||||
|
}
|
||||||
|
return Attributes{std::move(attr)};
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t File::Read(void* buf, uint32_t count) {
|
||||||
|
auto rv = sftp_read(m_handle, buf, count);
|
||||||
|
if (rv < 0) {
|
||||||
|
throw Exception{m_handle->sftp};
|
||||||
|
}
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
File::AsyncId File::AsyncReadBegin(uint32_t len) const {
|
||||||
|
int rv = sftp_async_read_begin(m_handle, len);
|
||||||
|
if (rv < 0) {
|
||||||
|
throw Exception{m_handle->sftp};
|
||||||
|
}
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t File::AsyncRead(void* data, uint32_t len, AsyncId id) {
|
||||||
|
auto rv = sftp_async_read(m_handle, data, len, id);
|
||||||
|
if (rv == SSH_ERROR) {
|
||||||
|
throw Exception{ssh_get_error(m_handle->sftp->session)};
|
||||||
|
}
|
||||||
|
if (rv == SSH_AGAIN) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t File::Write(wpi::span<const uint8_t> data) {
|
||||||
|
auto rv = sftp_write(m_handle, data.data(), data.size());
|
||||||
|
if (rv < 0) {
|
||||||
|
throw Exception{m_handle->sftp};
|
||||||
|
}
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
void File::Seek(uint64_t offset) {
|
||||||
|
if (sftp_seek64(m_handle, offset) < 0) {
|
||||||
|
throw Exception{m_handle->sftp};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t File::Tell() const {
|
||||||
|
return sftp_tell64(m_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void File::Rewind() {
|
||||||
|
sftp_rewind(m_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void File::Sync() {
|
||||||
|
if (sftp_fsync(m_handle) < 0) {
|
||||||
|
throw Exception{m_handle->sftp};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Session::Session(std::string_view host, int port, std::string_view user,
|
||||||
|
std::string_view pass)
|
||||||
|
: m_host{host}, m_port{port}, m_username{user}, m_password{pass} {
|
||||||
|
// Create a new SSH session.
|
||||||
|
m_session = ssh_new();
|
||||||
|
if (!m_session) {
|
||||||
|
throw Exception{"The SSH session could not be allocated."};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the host, user, and port.
|
||||||
|
ssh_options_set(m_session, SSH_OPTIONS_HOST, m_host.c_str());
|
||||||
|
ssh_options_set(m_session, SSH_OPTIONS_USER, m_username.c_str());
|
||||||
|
ssh_options_set(m_session, SSH_OPTIONS_PORT, &m_port);
|
||||||
|
|
||||||
|
// Set timeout to 3 seconds.
|
||||||
|
int64_t timeout = 3L;
|
||||||
|
ssh_options_set(m_session, SSH_OPTIONS_TIMEOUT, &timeout);
|
||||||
|
|
||||||
|
// Set other miscellaneous options.
|
||||||
|
ssh_options_set(m_session, SSH_OPTIONS_STRICTHOSTKEYCHECK, "no");
|
||||||
|
}
|
||||||
|
|
||||||
|
Session::~Session() {
|
||||||
|
if (m_sftp) {
|
||||||
|
sftp_free(m_sftp);
|
||||||
|
}
|
||||||
|
if (m_session) {
|
||||||
|
ssh_free(m_session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::Connect() {
|
||||||
|
// Connect to the server.
|
||||||
|
int rc = ssh_connect(m_session);
|
||||||
|
if (rc != SSH_OK) {
|
||||||
|
throw Exception{ssh_get_error(m_session)};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate with password.
|
||||||
|
rc = ssh_userauth_password(m_session, nullptr, m_password.c_str());
|
||||||
|
if (rc != SSH_AUTH_SUCCESS) {
|
||||||
|
throw Exception{ssh_get_error(m_session)};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate the SFTP session.
|
||||||
|
m_sftp = sftp_new(m_session);
|
||||||
|
if (!m_sftp) {
|
||||||
|
throw Exception{ssh_get_error(m_session)};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize.
|
||||||
|
rc = sftp_init(m_sftp);
|
||||||
|
if (rc != SSH_OK) {
|
||||||
|
sftp_free(m_sftp);
|
||||||
|
m_sftp = nullptr;
|
||||||
|
throw Exception{ssh_get_error(m_session)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::Disconnect() {
|
||||||
|
if (m_sftp) {
|
||||||
|
sftp_free(m_sftp);
|
||||||
|
m_sftp = nullptr;
|
||||||
|
}
|
||||||
|
ssh_disconnect(m_session);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Attributes> Session::ReadDir(const std::string& path) {
|
||||||
|
sftp_dir dir = sftp_opendir(m_sftp, path.c_str());
|
||||||
|
if (!dir) {
|
||||||
|
throw Exception{m_sftp};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Attributes> rv;
|
||||||
|
while (sftp_attributes attr = sftp_readdir(m_sftp, dir)) {
|
||||||
|
rv.emplace_back(std::move(attr));
|
||||||
|
}
|
||||||
|
|
||||||
|
sftp_closedir(dir);
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::Unlink(const std::string& filename) {
|
||||||
|
if (sftp_unlink(m_sftp, filename.c_str()) < 0) {
|
||||||
|
throw Exception{m_sftp};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
File Session::Open(const std::string& filename, int accesstype, mode_t mode) {
|
||||||
|
sftp_file f = sftp_open(m_sftp, filename.c_str(), accesstype, mode);
|
||||||
|
if (!f) {
|
||||||
|
throw Exception{m_sftp};
|
||||||
|
}
|
||||||
|
return File{std::move(f)};
|
||||||
|
}
|
||||||
144
datalogtool/src/main/native/cpp/Sftp.h
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
// 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 <libssh/libssh.h>
|
||||||
|
#include <libssh/sftp.h>
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <wpi/span.h>
|
||||||
|
|
||||||
|
namespace sftp {
|
||||||
|
|
||||||
|
struct Attributes {
|
||||||
|
Attributes() = default;
|
||||||
|
explicit Attributes(sftp_attributes&& attr);
|
||||||
|
|
||||||
|
std::string name;
|
||||||
|
uint32_t flags = 0;
|
||||||
|
uint8_t type = 0;
|
||||||
|
uint64_t size = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the exception that will be thrown if something goes wrong.
|
||||||
|
*/
|
||||||
|
class Exception : public std::runtime_error {
|
||||||
|
public:
|
||||||
|
explicit Exception(const std::string& msg) : std::runtime_error{msg} {}
|
||||||
|
explicit Exception(sftp_session sftp);
|
||||||
|
|
||||||
|
int err = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class File {
|
||||||
|
public:
|
||||||
|
File() = default;
|
||||||
|
explicit File(sftp_file&& handle) : m_handle{handle} {}
|
||||||
|
~File();
|
||||||
|
|
||||||
|
Attributes Stat() const;
|
||||||
|
|
||||||
|
void SetNonblocking() { sftp_file_set_nonblocking(m_handle); }
|
||||||
|
void SetBlocking() { sftp_file_set_blocking(m_handle); }
|
||||||
|
|
||||||
|
using AsyncId = uint32_t;
|
||||||
|
|
||||||
|
size_t Read(void* buf, uint32_t count);
|
||||||
|
AsyncId AsyncReadBegin(uint32_t len) const;
|
||||||
|
size_t AsyncRead(void* data, uint32_t len, AsyncId id);
|
||||||
|
size_t Write(wpi::span<const uint8_t> data);
|
||||||
|
|
||||||
|
void Seek(uint64_t offset);
|
||||||
|
uint64_t Tell() const;
|
||||||
|
void Rewind();
|
||||||
|
|
||||||
|
void Sync();
|
||||||
|
|
||||||
|
std::string_view GetName() const { return m_handle->name; }
|
||||||
|
uint64_t GetOffset() const { return m_handle->offset; }
|
||||||
|
bool IsEof() const { return m_handle->eof; }
|
||||||
|
bool IsNonblocking() const { return m_handle->nonblocking; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
sftp_file m_handle{nullptr};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is a C++ implementation of the SshSessionController in
|
||||||
|
* wpilibsuite/deploy-utils. It handles connecting to an SSH server, running
|
||||||
|
* commands, and transferring files.
|
||||||
|
*/
|
||||||
|
class Session {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Constructs a new session controller.
|
||||||
|
*
|
||||||
|
* @param host The hostname of the server to connect to.
|
||||||
|
* @param port The port that the sshd server is operating on.
|
||||||
|
* @param user The username to login as.
|
||||||
|
* @param pass The password for the given username.
|
||||||
|
*/
|
||||||
|
Session(std::string_view host, int port, std::string_view user,
|
||||||
|
std::string_view pass);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroys the controller object. This also disconnects the session from the
|
||||||
|
* server.
|
||||||
|
*/
|
||||||
|
~Session();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the SSH connection to the given host.
|
||||||
|
*/
|
||||||
|
void Connect();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnects the SSH connection.
|
||||||
|
*/
|
||||||
|
void Disconnect();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads directory entries
|
||||||
|
*
|
||||||
|
* @param path remote path
|
||||||
|
* @return vector of file attributes
|
||||||
|
*/
|
||||||
|
std::vector<Attributes> ReadDir(const std::string& path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlinks (deletes) a file.
|
||||||
|
*
|
||||||
|
* @param filename filename
|
||||||
|
*/
|
||||||
|
void Unlink(const std::string& filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a file.
|
||||||
|
*
|
||||||
|
* @param filename filename
|
||||||
|
* @param accesstype O_RDONLY, O_WRONLY, or O_RDWR, combined with O_CREAT,
|
||||||
|
* O_EXCL, or O_TRUNC
|
||||||
|
* @param mode permissions to use if a new file is created
|
||||||
|
* @return File
|
||||||
|
*/
|
||||||
|
File Open(const std::string& filename, int accesstype, mode_t mode);
|
||||||
|
|
||||||
|
private:
|
||||||
|
ssh_session m_session{nullptr};
|
||||||
|
sftp_session m_sftp{nullptr};
|
||||||
|
std::string m_host;
|
||||||
|
|
||||||
|
int m_port;
|
||||||
|
|
||||||
|
std::string m_username;
|
||||||
|
std::string m_password;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sftp
|
||||||
25
datalogtool/src/main/native/cpp/main.cpp
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// 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 <string_view>
|
||||||
|
|
||||||
|
void Application(std::string_view saveDir);
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
int __stdcall WinMain(void* hInstance, void* hPrevInstance, char* pCmdLine,
|
||||||
|
int nCmdShow) {
|
||||||
|
int argc = __argc;
|
||||||
|
char** argv = __argv;
|
||||||
|
#else
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
#endif
|
||||||
|
std::string_view saveDir;
|
||||||
|
if (argc == 2) {
|
||||||
|
saveDir = argv[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
Application(saveDir);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
BIN
datalogtool/src/main/native/mac/datalogtool.icns
Normal file
BIN
datalogtool/src/main/native/resources/dlt-128.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
datalogtool/src/main/native/resources/dlt-16.png
Normal file
|
After Width: | Height: | Size: 609 B |
BIN
datalogtool/src/main/native/resources/dlt-256.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
datalogtool/src/main/native/resources/dlt-32.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
datalogtool/src/main/native/resources/dlt-48.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
datalogtool/src/main/native/resources/dlt-512.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
datalogtool/src/main/native/resources/dlt-64.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
datalogtool/src/main/native/win/datalogtool.ico
Normal file
|
After Width: | Height: | Size: 361 KiB |
1
datalogtool/src/main/native/win/datalogtool.rc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
IDI_ICON1 ICON "datalogtool.ico"
|
||||||
@@ -33,6 +33,7 @@ include 'fieldImages'
|
|||||||
include 'glass'
|
include 'glass'
|
||||||
include 'outlineviewer'
|
include 'outlineviewer'
|
||||||
include 'roborioteamnumbersetter'
|
include 'roborioteamnumbersetter'
|
||||||
|
include 'datalogtool'
|
||||||
include 'simulation:gz_msgs'
|
include 'simulation:gz_msgs'
|
||||||
include 'simulation:frc_gazebo_plugins'
|
include 'simulation:frc_gazebo_plugins'
|
||||||
include 'simulation:halsim_gazebo'
|
include 'simulation:halsim_gazebo'
|
||||||
|
|||||||