Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d66555e42f | ||
|
|
9f52d8a3b1 | ||
|
|
757ea91932 | ||
|
|
02a804f1c5 | ||
|
|
9b500df0d9 | ||
|
|
5a89575b3a | ||
|
|
b8c4d7527b | ||
|
|
ac5d46cfa7 | ||
|
|
bc9e96e86f | ||
|
|
f88c435dd0 | ||
|
|
e4b91005cf | ||
|
|
a260bfd83b | ||
|
|
18e262a100 | ||
|
|
4bd1f526ab | ||
|
|
27847d7eb2 | ||
|
|
b2a8d3f0f3 | ||
|
|
49adac9564 | ||
|
|
a19d1133b1 | ||
|
|
dde91717e4 | ||
|
|
e9050afd67 | ||
|
|
165d2837cf | ||
|
|
ac7549edca | ||
|
|
4d96bc72e0 | ||
|
|
3411eee20f | ||
|
|
74de97eeca | ||
|
|
4e3cc25012 | ||
|
|
90c1db393e | ||
|
|
2f43274aa4 | ||
|
|
aeca09db09 | ||
|
|
c107f22c67 | ||
|
|
68fe51e8da | ||
|
|
8d08d67cf1 | ||
|
|
4f1782f66e | ||
|
|
3f77725cd3 | ||
|
|
5635f33a32 | ||
|
|
bca4b7111b | ||
|
|
6a6366b0d6 | ||
|
|
16bf2c70c5 | ||
|
|
4b3edb742c |
@@ -262,6 +262,7 @@ if (WITH_GUI)
|
||||
add_subdirectory(outlineviewer)
|
||||
if (LIBSSH_FOUND)
|
||||
add_subdirectory(roborioteamnumbersetter)
|
||||
add_subdirectory(datalogtool)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
macro(wpilib_target_warnings target)
|
||||
if(NOT MSVC)
|
||||
target_compile_options(${target} PRIVATE -Wall -pedantic -Wextra -Werror -Wno-unused-parameter -Wno-error=deprecated-declarations)
|
||||
target_compile_options(${target} PRIVATE -Wall -pedantic -Wextra -Werror -Wno-unused-parameter -Wno-error=deprecated-declarations ${WPILIB_TARGET_WARNINGS})
|
||||
else()
|
||||
target_compile_options(${target} PRIVATE /wd4146 /wd4244 /wd4251 /wd4267 /wd4996 /WX)
|
||||
target_compile_options(${target} PRIVATE /wd4146 /wd4244 /wd4251 /wd4267 /wd4996 /WX ${WPILIB_TARGET_WARNINGS})
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
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"
|
||||
@@ -47,6 +47,8 @@ static std::unique_ptr<glass::Window> gNetworkTablesLogWindow;
|
||||
|
||||
static glass::MainMenuBar gMainMenu;
|
||||
static bool gAbout = false;
|
||||
static bool gSetEnterKey = false;
|
||||
static bool gKeyEdit = false;
|
||||
|
||||
static void NtInitialize() {
|
||||
// update window title when connection status changes
|
||||
@@ -178,6 +180,9 @@ int main(int argc, char** argv) {
|
||||
|
||||
gMainMenu.AddMainMenu([] {
|
||||
if (ImGui::BeginMenu("View")) {
|
||||
if (ImGui::MenuItem("Set Enter Key")) {
|
||||
gSetEnterKey = true;
|
||||
}
|
||||
if (ImGui::MenuItem("Reset Time")) {
|
||||
glass::ResetTime();
|
||||
}
|
||||
@@ -231,6 +236,57 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
int& enterKey = glass::GetStorageRoot().GetInt("enterKey", GLFW_KEY_ENTER);
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.KeyMap[ImGuiKey_Enter] = enterKey;
|
||||
|
||||
if (gSetEnterKey) {
|
||||
ImGui::OpenPopup("Set Enter Key");
|
||||
gSetEnterKey = false;
|
||||
}
|
||||
if (ImGui::BeginPopupModal("Set Enter Key")) {
|
||||
ImGui::Text("Set the key to use to mean 'Enter'");
|
||||
ImGui::Text("This is useful to edit values without the DS disabling");
|
||||
ImGui::Separator();
|
||||
|
||||
if (gKeyEdit) {
|
||||
for (int i = 0; i < IM_ARRAYSIZE(io.KeysDown); ++i) {
|
||||
if (io.KeysDown[i]) {
|
||||
// remove all other uses
|
||||
enterKey = i;
|
||||
gKeyEdit = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Text("Key:");
|
||||
ImGui::SameLine();
|
||||
char editLabel[40];
|
||||
char nameBuf[32];
|
||||
const char* name = glfwGetKeyName(enterKey, 0);
|
||||
if (!name) {
|
||||
std::snprintf(nameBuf, sizeof(nameBuf), "%d", enterKey);
|
||||
name = nameBuf;
|
||||
}
|
||||
std::snprintf(editLabel, sizeof(editLabel), "%s###edit",
|
||||
gKeyEdit ? "(press key)" : name);
|
||||
if (ImGui::SmallButton(editLabel)) {
|
||||
gKeyEdit = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Reset")) {
|
||||
enterKey = GLFW_KEY_ENTER;
|
||||
}
|
||||
|
||||
if (ImGui::Button("Close")) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
gKeyEdit = false;
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
});
|
||||
|
||||
gui::Initialize("Glass - DISCONNECTED", 1024, 768);
|
||||
|
||||
@@ -14,7 +14,7 @@ using namespace glass;
|
||||
static const char* stations[] = {"Red 1", "Red 2", "Red 3",
|
||||
"Blue 1", "Blue 2", "Blue 3"};
|
||||
|
||||
void glass::DisplayFMS(FMSModel* model, bool* matchTimeEnabled) {
|
||||
void glass::DisplayFMS(FMSModel* model) {
|
||||
if (!model->Exists() || model->IsReadOnly()) {
|
||||
return DisplayFMSReadOnly(model);
|
||||
}
|
||||
@@ -49,10 +49,6 @@ void glass::DisplayFMS(FMSModel* model, bool* matchTimeEnabled) {
|
||||
|
||||
// Match Time
|
||||
if (auto data = model->GetMatchTimeData()) {
|
||||
if (matchTimeEnabled) {
|
||||
ImGui::Checkbox("Match Time Enabled", matchTimeEnabled);
|
||||
}
|
||||
|
||||
double val = data->GetValue();
|
||||
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
|
||||
if (ImGui::InputDouble("Match Time", &val, 0, 0, "%.1f",
|
||||
@@ -60,9 +56,17 @@ void glass::DisplayFMS(FMSModel* model, bool* matchTimeEnabled) {
|
||||
model->SetMatchTime(val);
|
||||
}
|
||||
data->EmitDrag();
|
||||
bool enabled = false;
|
||||
if (auto enabledData = model->GetEnabledData()) {
|
||||
enabled = enabledData->GetValue();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Reset")) {
|
||||
model->SetMatchTime(0.0);
|
||||
if (ImGui::Button("Auto") && !enabled) {
|
||||
model->SetMatchTime(15.0);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Teleop") && !enabled) {
|
||||
model->SetMatchTime(135.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ class PlotSeries {
|
||||
int& m_digitalBitGap;
|
||||
|
||||
// value storage
|
||||
static constexpr int kMaxSize = 2000;
|
||||
static constexpr int kMaxSize = 20000;
|
||||
static constexpr double kTimeGap = 0.05;
|
||||
std::atomic<int> m_size = 0;
|
||||
std::atomic<int> m_offset = 0;
|
||||
@@ -246,7 +246,7 @@ void PlotSeries::SetSource(DataSource* source) {
|
||||
m_source = source;
|
||||
|
||||
// add initial value
|
||||
m_data[m_size++] = ImPlotPoint{wpi::Now() * 1.0e-6, source->GetValue()};
|
||||
AppendValue(source->GetValue(), 0);
|
||||
|
||||
m_newValueConn = source->valueChanged.connect_connection(
|
||||
[this](double value, uint64_t time) { AppendValue(value, time); });
|
||||
|
||||
@@ -47,7 +47,7 @@ class FMSModel : public Model {
|
||||
* @param matchTimeEnabled If not null, a checkbox is displayed for
|
||||
* "enable match time" linked to this value
|
||||
*/
|
||||
void DisplayFMS(FMSModel* model, bool* matchTimeEnabled = nullptr);
|
||||
void DisplayFMS(FMSModel* model);
|
||||
void DisplayFMSReadOnly(FMSModel* model);
|
||||
|
||||
} // namespace glass
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
@@ -48,4 +48,6 @@ int32_t HALSIM_RegisterSimPeriodicAfterCallback(
|
||||
|
||||
void HALSIM_CancelSimPeriodicAfterCallback(int32_t uid) {}
|
||||
|
||||
void HALSIM_CancelAllSimPeriodicCallbacks(void) {}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -36,4 +36,6 @@ int32_t HALSIM_RegisterSimPeriodicAfterCallback(
|
||||
HALSIM_SimPeriodicCallback callback, void* param);
|
||||
void HALSIM_CancelSimPeriodicAfterCallback(int32_t uid);
|
||||
|
||||
void HALSIM_CancelAllSimPeriodicCallbacks(void);
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -409,6 +409,11 @@ void HALSIM_CancelSimPeriodicAfterCallback(int32_t uid) {
|
||||
gSimPeriodicAfter.Cancel(uid);
|
||||
}
|
||||
|
||||
void HALSIM_CancelAllSimPeriodicCallbacks(void) {
|
||||
gSimPeriodicBefore.Reset();
|
||||
gSimPeriodicAfter.Reset();
|
||||
}
|
||||
|
||||
int64_t HAL_Report(int32_t resource, int32_t instanceNumber, int32_t context,
|
||||
const char* feature) {
|
||||
return 0; // Do nothing for now
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <stdint.h>
|
||||
|
||||
namespace hal {
|
||||
constexpr int32_t kAccelerometers = 1;
|
||||
constexpr int32_t kNumAccumulators = 2;
|
||||
constexpr int32_t kNumAnalogTriggers = 8;
|
||||
constexpr int32_t kNumAnalogInputs = 8;
|
||||
@@ -18,6 +19,7 @@ constexpr int32_t kNumDigitalChannels = 31;
|
||||
constexpr int32_t kNumPWMChannels = 20;
|
||||
constexpr int32_t kNumDigitalPWMOutputs = 6;
|
||||
constexpr int32_t kNumEncoders = 8;
|
||||
constexpr int32_t kI2CPorts = 2;
|
||||
constexpr int32_t kNumInterrupts = 8;
|
||||
constexpr int32_t kNumRelayChannels = 8;
|
||||
constexpr int32_t kNumRelayHeaders = kNumRelayChannels / 2;
|
||||
@@ -27,8 +29,13 @@ constexpr int32_t kNumCTREPDPModules = 63;
|
||||
constexpr int32_t kNumCTREPDPChannels = 16;
|
||||
constexpr int32_t kNumREVPDHModules = 63;
|
||||
constexpr int32_t kNumREVPDHChannels = 24;
|
||||
constexpr int32_t kNumPDSimModules = kNumREVPDHModules;
|
||||
constexpr int32_t kNumPDSimChannels = kNumREVPDHChannels;
|
||||
constexpr int32_t kNumDutyCycles = 8;
|
||||
constexpr int32_t kNumAddressableLEDs = 1;
|
||||
constexpr int32_t kNumREVPHModules = 63;
|
||||
constexpr int32_t kNumREVPHChannels = 16;
|
||||
constexpr int32_t kSPIAccelerometers = 5;
|
||||
constexpr int32_t kSPIPorts = 5;
|
||||
|
||||
} // namespace hal
|
||||
|
||||
@@ -9,7 +9,7 @@ using namespace hal;
|
||||
|
||||
namespace hal::init {
|
||||
void InitializeAccelerometerData() {
|
||||
static AccelerometerData sad[1];
|
||||
static AccelerometerData sad[kAccelerometers];
|
||||
::hal::SimAccelerometerData = sad;
|
||||
}
|
||||
} // namespace hal::init
|
||||
|
||||
@@ -30,7 +30,7 @@ void DriverStationData::ResetData() {
|
||||
fmsAttached.Reset(false);
|
||||
dsAttached.Reset(true);
|
||||
allianceStationId.Reset(static_cast<HAL_AllianceStationID>(0));
|
||||
matchTime.Reset(0.0);
|
||||
matchTime.Reset(-1.0);
|
||||
|
||||
{
|
||||
std::scoped_lock lock(m_joystickDataMutex);
|
||||
|
||||
@@ -126,7 +126,7 @@ class DriverStationData {
|
||||
SimDataValue<HAL_AllianceStationID, MakeAllianceStationIdValue,
|
||||
GetAllianceStationIdName>
|
||||
allianceStationId{static_cast<HAL_AllianceStationID>(0)};
|
||||
SimDataValue<double, HAL_MakeDouble, GetMatchTimeName> matchTime{0.0};
|
||||
SimDataValue<double, HAL_MakeDouble, GetMatchTimeName> matchTime{-1.0};
|
||||
|
||||
private:
|
||||
SimCallbackRegistry<HAL_JoystickAxesCallback, GetJoystickAxesName>
|
||||
|
||||
@@ -9,7 +9,7 @@ using namespace hal;
|
||||
|
||||
namespace hal::init {
|
||||
void InitializeI2CData() {
|
||||
static I2CData sid[2];
|
||||
static I2CData sid[kI2CPorts];
|
||||
::hal::SimI2CData = sid;
|
||||
}
|
||||
} // namespace hal::init
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
#include "hal/simulation/SimDataValue.h"
|
||||
|
||||
namespace hal {
|
||||
constexpr int32_t kNumPDSimModules = hal::kNumREVPDHModules;
|
||||
constexpr int32_t kNumPDSimChannels = hal::kNumREVPDHChannels;
|
||||
|
||||
class PowerDistributionData {
|
||||
HAL_SIMDATAVALUE_DEFINE_NAME(Initialized)
|
||||
|
||||
@@ -9,7 +9,7 @@ using namespace hal;
|
||||
|
||||
namespace hal::init {
|
||||
void InitializeSPIAccelerometerData() {
|
||||
static SPIAccelerometerData ssad[5];
|
||||
static SPIAccelerometerData ssad[kSPIAccelerometers];
|
||||
::hal::SimSPIAccelerometerData = ssad;
|
||||
}
|
||||
} // namespace hal::init
|
||||
|
||||
@@ -9,7 +9,7 @@ using namespace hal;
|
||||
|
||||
namespace hal::init {
|
||||
void InitializeSPIData() {
|
||||
static SPIData ssd[5];
|
||||
static SPIData ssd[kSPIPorts];
|
||||
::hal::SimSPIData = ssd;
|
||||
}
|
||||
} // namespace hal::init
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
package edu.wpi.first.networktables;
|
||||
|
||||
import edu.wpi.first.util.datalog.DataLog;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -997,6 +998,50 @@ public final class NetworkTableInstance implements AutoCloseable {
|
||||
return NetworkTablesJNI.loadEntries(m_handle, filename, prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts logging entry changes to a DataLog.
|
||||
*
|
||||
* @param log data log object; lifetime must extend until StopEntryDataLog is called or the
|
||||
* instance is destroyed
|
||||
* @param prefix only store entries with names that start with this prefix; the prefix is not
|
||||
* included in the data log entry name
|
||||
* @param logPrefix prefix to add to data log entry names
|
||||
* @return Data logger handle
|
||||
*/
|
||||
public int startEntryDataLog(DataLog log, String prefix, String logPrefix) {
|
||||
return NetworkTablesJNI.startEntryDataLog(m_handle, log, prefix, logPrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops logging entry changes to a DataLog.
|
||||
*
|
||||
* @param logger data logger handle
|
||||
*/
|
||||
public static void stopEntryDataLog(int logger) {
|
||||
NetworkTablesJNI.stopEntryDataLog(logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts logging connection changes to a DataLog.
|
||||
*
|
||||
* @param log data log object; lifetime must extend until StopConnectionDataLog is called or the
|
||||
* instance is destroyed
|
||||
* @param name data log entry name
|
||||
* @return Data logger handle
|
||||
*/
|
||||
public int startConnectionDataLog(DataLog log, String name) {
|
||||
return NetworkTablesJNI.startConnectionDataLog(m_handle, log, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops logging connection changes to a DataLog.
|
||||
*
|
||||
* @param logger data logger handle
|
||||
*/
|
||||
public static void stopConnectionDataLog(int logger) {
|
||||
NetworkTablesJNI.stopConnectionDataLog(logger);
|
||||
}
|
||||
|
||||
private final ReentrantLock m_loggerLock = new ReentrantLock();
|
||||
private final Map<Integer, Consumer<LogMessage>> m_loggers = new HashMap<>();
|
||||
private int m_loggerPoller;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package edu.wpi.first.networktables;
|
||||
|
||||
import edu.wpi.first.util.RuntimeLoader;
|
||||
import edu.wpi.first.util.datalog.DataLog;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -255,6 +256,22 @@ public final class NetworkTablesJNI {
|
||||
|
||||
public static native long now();
|
||||
|
||||
private static native int startEntryDataLog(int inst, long log, String prefix, String logPrefix);
|
||||
|
||||
public static int startEntryDataLog(int inst, DataLog log, String prefix, String logPrefix) {
|
||||
return startEntryDataLog(inst, log.getImpl(), prefix, logPrefix);
|
||||
}
|
||||
|
||||
public static native void stopEntryDataLog(int logger);
|
||||
|
||||
private static native int startConnectionDataLog(int inst, long log, String name);
|
||||
|
||||
public static int startConnectionDataLog(int inst, DataLog log, String name) {
|
||||
return startConnectionDataLog(inst, log.getImpl(), name);
|
||||
}
|
||||
|
||||
public static native void stopConnectionDataLog(int logger);
|
||||
|
||||
public static native int createLoggerPoller(int inst);
|
||||
|
||||
public static native void destroyLoggerPoller(int poller);
|
||||
|
||||
@@ -21,6 +21,10 @@ unsigned int ConnectionNotifier::AddPolled(unsigned int poller_uid) {
|
||||
return DoAdd(poller_uid);
|
||||
}
|
||||
|
||||
void ConnectionNotifier::Remove(unsigned int uid) {
|
||||
CallbackManager::Remove(uid);
|
||||
}
|
||||
|
||||
void ConnectionNotifier::NotifyConnection(bool connected,
|
||||
const ConnectionInfo& conn_info,
|
||||
unsigned int only_listener) {
|
||||
|
||||
@@ -63,6 +63,8 @@ class ConnectionNotifier
|
||||
callback) override;
|
||||
unsigned int AddPolled(unsigned int poller_uid) override;
|
||||
|
||||
void Remove(unsigned int uid) override;
|
||||
|
||||
void NotifyConnection(bool connected, const ConnectionInfo& conn_info,
|
||||
unsigned int only_listener = UINT_MAX) override;
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include <wpi/StringExtras.h>
|
||||
#include <wpi/TCPAcceptor.h>
|
||||
#include <wpi/TCPConnector.h>
|
||||
#include <wpi/json_serializer.h>
|
||||
#include <wpi/raw_ostream.h>
|
||||
#include <wpi/timestamp.h>
|
||||
|
||||
#include "IConnectionNotifier.h"
|
||||
@@ -20,6 +22,24 @@
|
||||
|
||||
using namespace nt;
|
||||
|
||||
static std::string ConnInfoToJson(bool connected, const ConnectionInfo& info) {
|
||||
std::string str;
|
||||
wpi::raw_string_ostream os{str};
|
||||
wpi::json::serializer s{os, ' ', 0};
|
||||
os << "{\"connected\":" << (connected ? "true" : "false");
|
||||
os << ",\"remote_id\":\"";
|
||||
s.dump_escaped(info.remote_id, false);
|
||||
os << "\",\"remote_ip\":\"";
|
||||
s.dump_escaped(info.remote_ip, false);
|
||||
os << "\",\"remote_port\":";
|
||||
s.dump_integer(static_cast<uint64_t>(info.remote_port));
|
||||
os << ",\"protocol_version\":";
|
||||
s.dump_integer(static_cast<uint64_t>(info.protocol_version));
|
||||
os << "}";
|
||||
os.flush();
|
||||
return str;
|
||||
}
|
||||
|
||||
void Dispatcher::StartServer(std::string_view persist_filename,
|
||||
const char* listen_address, unsigned int port) {
|
||||
std::string listen_address_copy(wpi::trim(listen_address));
|
||||
@@ -101,6 +121,13 @@ DispatcherBase::DispatcherBase(IStorage& storage, IConnectionNotifier& notifier,
|
||||
|
||||
DispatcherBase::~DispatcherBase() {
|
||||
Stop();
|
||||
|
||||
{
|
||||
std::scoped_lock lock(m_user_mutex);
|
||||
for (auto&& datalog : m_dataloggers) {
|
||||
m_notifier.Remove(datalog.notifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int DispatcherBase::GetNetworkMode() const {
|
||||
@@ -302,6 +329,33 @@ unsigned int DispatcherBase::AddPolledListener(unsigned int poller_uid,
|
||||
return uid;
|
||||
}
|
||||
|
||||
unsigned int DispatcherBase::StartDataLog(wpi::log::DataLog& log,
|
||||
std::string_view name) {
|
||||
std::scoped_lock lock(m_user_mutex);
|
||||
auto now = nt::Now();
|
||||
unsigned int uid = m_dataloggers.emplace_back(log, name, now);
|
||||
m_dataloggers[uid].notifier =
|
||||
m_notifier.Add([this, uid](const ConnectionNotification& n) {
|
||||
std::scoped_lock lock(m_user_mutex);
|
||||
if (uid < m_dataloggers.size() && m_dataloggers[uid].entry) {
|
||||
m_dataloggers[uid].entry.Append(ConnInfoToJson(n.connected, n.conn),
|
||||
nt::Now());
|
||||
}
|
||||
});
|
||||
for (auto& conn : m_connections) {
|
||||
if (conn->state() != NetworkConnection::kActive) {
|
||||
continue;
|
||||
}
|
||||
m_dataloggers[uid].entry.Append(ConnInfoToJson(true, conn->info()), now);
|
||||
}
|
||||
return uid;
|
||||
}
|
||||
|
||||
void DispatcherBase::StopDataLog(unsigned int logger) {
|
||||
std::scoped_lock lock(m_user_mutex);
|
||||
m_notifier.Remove(m_dataloggers.erase(logger).notifier);
|
||||
}
|
||||
|
||||
void DispatcherBase::SetConnector(Connector connector) {
|
||||
std::scoped_lock lock(m_user_mutex);
|
||||
m_client_connector = std::move(connector);
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <wpi/DataLog.h>
|
||||
#include <wpi/UidVector.h>
|
||||
#include <wpi/condition_variable.h>
|
||||
#include <wpi/mutex.h>
|
||||
#include <wpi/span.h>
|
||||
@@ -61,6 +63,9 @@ class DispatcherBase : public IDispatcher {
|
||||
unsigned int AddPolledListener(unsigned int poller_uid,
|
||||
bool immediate_notify) const;
|
||||
|
||||
unsigned int StartDataLog(wpi::log::DataLog& log, std::string_view name);
|
||||
void StopDataLog(unsigned int logger);
|
||||
|
||||
void SetConnector(Connector connector);
|
||||
void SetConnectorOverride(Connector connector);
|
||||
void ClearConnectorOverride();
|
||||
@@ -120,6 +125,20 @@ class DispatcherBase : public IDispatcher {
|
||||
unsigned int m_reconnect_proto_rev = 0x0300;
|
||||
bool m_do_reconnect = true;
|
||||
|
||||
struct DataLogger {
|
||||
DataLogger() = default;
|
||||
DataLogger(wpi::log::DataLog& log, std::string_view name, int64_t time)
|
||||
: entry{log, name,
|
||||
"{\"schema\":\"NTConnectionInfo\",\"source\":\"NT\"}", "json",
|
||||
time} {}
|
||||
|
||||
explicit operator bool() const { return static_cast<bool>(entry); }
|
||||
|
||||
wpi::log::StringLogEntry entry;
|
||||
unsigned int notifier = 0;
|
||||
};
|
||||
wpi::UidVector<DataLogger, 4> m_dataloggers;
|
||||
|
||||
protected:
|
||||
wpi::Logger& m_logger;
|
||||
};
|
||||
|
||||
@@ -28,7 +28,9 @@ class Handle {
|
||||
kLogger,
|
||||
kLoggerPoller,
|
||||
kRpcCall,
|
||||
kRpcCallPoller
|
||||
kRpcCallPoller,
|
||||
kDataLogger,
|
||||
kConnectionDataLogger
|
||||
};
|
||||
enum { kIndexMax = 0xfffff };
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ class IConnectionNotifier {
|
||||
virtual unsigned int Add(
|
||||
std::function<void(const ConnectionNotification& event)> callback) = 0;
|
||||
virtual unsigned int AddPolled(unsigned int poller_uid) = 0;
|
||||
virtual void Remove(unsigned int uid) = 0;
|
||||
virtual void NotifyConnection(bool connected, const ConnectionInfo& conn_info,
|
||||
unsigned int only_listener = UINT_MAX) = 0;
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "Storage.h"
|
||||
|
||||
#include <wpi/DataLog.h>
|
||||
#include <wpi/StringExtras.h>
|
||||
#include <wpi/timestamp.h>
|
||||
|
||||
@@ -13,6 +14,7 @@
|
||||
#include "INetworkConnection.h"
|
||||
#include "IRpcServer.h"
|
||||
#include "Log.h"
|
||||
#include "ntcore_c.h"
|
||||
|
||||
using namespace nt;
|
||||
|
||||
@@ -144,8 +146,7 @@ void Storage::ProcessIncomingEntryAssign(std::shared_ptr<Message> msg,
|
||||
entry->seq_num = seq_num;
|
||||
|
||||
// notify
|
||||
m_notifier.NotifyEntry(entry->local_id, name, entry->value,
|
||||
NT_NOTIFY_NEW);
|
||||
Notify(entry, NT_NOTIFY_NEW, false);
|
||||
return;
|
||||
}
|
||||
may_need_update = true; // we may need to send an update message
|
||||
@@ -208,7 +209,7 @@ void Storage::ProcessIncomingEntryAssign(std::shared_ptr<Message> msg,
|
||||
entry->seq_num = seq_num;
|
||||
|
||||
// notify
|
||||
m_notifier.NotifyEntry(entry->local_id, name, entry->value, notify_flags);
|
||||
Notify(entry, notify_flags, false);
|
||||
|
||||
// broadcast to all other connections (note for client there won't
|
||||
// be any other connections, so don't bother)
|
||||
@@ -250,8 +251,7 @@ void Storage::ProcessIncomingEntryUpdate(std::shared_ptr<Message> msg,
|
||||
}
|
||||
|
||||
// notify
|
||||
m_notifier.NotifyEntry(entry->local_id, entry->name, entry->value,
|
||||
NT_NOTIFY_UPDATE);
|
||||
Notify(entry, NT_NOTIFY_UPDATE, false);
|
||||
|
||||
// broadcast to all other connections (note for client there won't
|
||||
// be any other connections, so don't bother)
|
||||
@@ -453,8 +453,7 @@ void Storage::ApplyInitialAssignments(
|
||||
entry->value = msg->value();
|
||||
entry->flags = msg->flags();
|
||||
// notify
|
||||
m_notifier.NotifyEntry(entry->local_id, name, entry->value,
|
||||
NT_NOTIFY_NEW);
|
||||
Notify(entry, NT_NOTIFY_NEW, false);
|
||||
} else {
|
||||
// if we have written the value locally and the value is not persistent,
|
||||
// then we don't update the local value and instead send it back to the
|
||||
@@ -474,8 +473,7 @@ void Storage::ApplyInitialAssignments(
|
||||
entry->flags = msg->flags();
|
||||
}
|
||||
// notify
|
||||
m_notifier.NotifyEntry(entry->local_id, name, entry->value,
|
||||
notify_flags);
|
||||
Notify(entry, notify_flags, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,11 +626,9 @@ void Storage::SetEntryValueImpl(Entry* entry, std::shared_ptr<Value> value,
|
||||
|
||||
// notify
|
||||
if (!old_value) {
|
||||
m_notifier.NotifyEntry(entry->local_id, entry->name, value,
|
||||
NT_NOTIFY_NEW | (local ? NT_NOTIFY_LOCAL : 0));
|
||||
Notify(entry, NT_NOTIFY_NEW, local);
|
||||
} else if (*old_value != *value) {
|
||||
m_notifier.NotifyEntry(entry->local_id, entry->name, value,
|
||||
NT_NOTIFY_UPDATE | (local ? NT_NOTIFY_LOCAL : 0));
|
||||
Notify(entry, NT_NOTIFY_UPDATE, local);
|
||||
}
|
||||
|
||||
// remember local changes
|
||||
@@ -732,8 +728,7 @@ void Storage::SetEntryFlagsImpl(Entry* entry, unsigned int flags,
|
||||
entry->flags = flags;
|
||||
|
||||
// notify
|
||||
m_notifier.NotifyEntry(entry->local_id, entry->name, entry->value,
|
||||
NT_NOTIFY_FLAGS | (local ? NT_NOTIFY_LOCAL : 0));
|
||||
Notify(entry, NT_NOTIFY_FLAGS, local);
|
||||
|
||||
// generate message
|
||||
if (!local || !m_dispatcher) {
|
||||
@@ -817,8 +812,7 @@ void Storage::DeleteEntryImpl(Entry* entry, std::unique_lock<wpi::mutex>& lock,
|
||||
}
|
||||
|
||||
// notify
|
||||
m_notifier.NotifyEntry(entry->local_id, entry->name, old_value,
|
||||
NT_NOTIFY_DELETE | (local ? NT_NOTIFY_LOCAL : 0));
|
||||
Notify(entry, NT_NOTIFY_DELETE, local, old_value);
|
||||
|
||||
// if it had a value, generate message
|
||||
// don't send an update if we don't have an assigned id yet
|
||||
@@ -832,14 +826,160 @@ void Storage::DeleteEntryImpl(Entry* entry, std::unique_lock<wpi::mutex>& lock,
|
||||
}
|
||||
}
|
||||
|
||||
static std::string_view GetStorageTypeStr(NT_Type type) {
|
||||
switch (type) {
|
||||
case NT_BOOLEAN:
|
||||
return wpi::log::BooleanLogEntry::kDataType;
|
||||
case NT_DOUBLE:
|
||||
return wpi::log::DoubleLogEntry::kDataType;
|
||||
case NT_STRING:
|
||||
return wpi::log::StringLogEntry::kDataType;
|
||||
case NT_RAW:
|
||||
return wpi::log::RawLogEntry::kDataType;
|
||||
case NT_BOOLEAN_ARRAY:
|
||||
return wpi::log::BooleanArrayLogEntry::kDataType;
|
||||
case NT_DOUBLE_ARRAY:
|
||||
return wpi::log::DoubleArrayLogEntry::kDataType;
|
||||
case NT_STRING_ARRAY:
|
||||
return wpi::log::StringArrayLogEntry::kDataType;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
void Storage::Notify(Entry* entry, unsigned int flags, bool local,
|
||||
std::shared_ptr<Value> value) {
|
||||
auto& v = value ? value : entry->value;
|
||||
|
||||
// notifications
|
||||
m_notifier.NotifyEntry(entry->local_id, entry->name, v,
|
||||
flags | (local ? NT_NOTIFY_LOCAL : 0));
|
||||
|
||||
if (m_dataloggers.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// data logging
|
||||
// fast path the common case
|
||||
if (entry->datalogs.empty() && (flags & NT_NOTIFY_NEW) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (flags & NT_NOTIFY_DELETE) {
|
||||
// remove all of the datalog entries
|
||||
auto now = nt::Now();
|
||||
for (auto&& datalog : entry->datalogs) {
|
||||
datalog.log->Finish(datalog.entry, now);
|
||||
}
|
||||
entry->datalogs.clear();
|
||||
entry->datalog_type = NT_UNASSIGNED;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (v->type() != entry->datalog_type) {
|
||||
if (!entry->datalogs.empty()) {
|
||||
// data type changed; need to finish any current logs
|
||||
for (auto&& datalog : entry->datalogs) {
|
||||
datalog.log->Finish(datalog.entry, v->time());
|
||||
}
|
||||
entry->datalogs.clear();
|
||||
}
|
||||
|
||||
// create matching loggers
|
||||
auto type = GetStorageTypeStr(v->type());
|
||||
if (type.empty()) {
|
||||
return; // not a type we're going to log
|
||||
}
|
||||
for (auto&& logger : m_dataloggers) {
|
||||
if (wpi::starts_with(entry->name, logger.prefix)) {
|
||||
entry->datalogs.emplace_back(
|
||||
logger.log,
|
||||
logger.log->Start(
|
||||
fmt::format("{}{}", logger.log_prefix,
|
||||
wpi::drop_front(entry->name, logger.prefix.size())),
|
||||
type, "{\"source\":\"NT\"}", v->time()),
|
||||
logger.uid);
|
||||
}
|
||||
}
|
||||
|
||||
if (entry->datalogs.empty()) {
|
||||
return; // we're done, nothing to log
|
||||
}
|
||||
|
||||
// set datalog_type
|
||||
entry->datalog_type = v->type();
|
||||
}
|
||||
|
||||
auto time = v->time();
|
||||
switch (v->type()) {
|
||||
case NT_BOOLEAN: {
|
||||
auto val = v->GetBoolean();
|
||||
for (auto&& datalog : entry->datalogs) {
|
||||
datalog.log->AppendBoolean(datalog.entry, val, time);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NT_DOUBLE: {
|
||||
auto val = v->GetDouble();
|
||||
for (auto&& datalog : entry->datalogs) {
|
||||
datalog.log->AppendDouble(datalog.entry, val, time);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NT_STRING: {
|
||||
auto val = v->GetString();
|
||||
for (auto&& datalog : entry->datalogs) {
|
||||
datalog.log->AppendString(datalog.entry, val, time);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NT_RAW: {
|
||||
auto val = v->GetRaw();
|
||||
for (auto&& datalog : entry->datalogs) {
|
||||
datalog.log->AppendRaw(
|
||||
datalog.entry,
|
||||
{reinterpret_cast<const uint8_t*>(val.data()), val.size()}, time);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NT_BOOLEAN_ARRAY: {
|
||||
auto val = v->GetBooleanArray();
|
||||
for (auto&& datalog : entry->datalogs) {
|
||||
datalog.log->AppendBooleanArray(datalog.entry, val, time);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NT_DOUBLE_ARRAY: {
|
||||
auto val = v->GetDoubleArray();
|
||||
for (auto&& datalog : entry->datalogs) {
|
||||
datalog.log->AppendDoubleArray(datalog.entry, val, time);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NT_STRING_ARRAY: {
|
||||
auto val = v->GetStringArray();
|
||||
for (auto&& datalog : entry->datalogs) {
|
||||
datalog.log->AppendStringArray(datalog.entry, val, time);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NT_UNASSIGNED:
|
||||
case NT_RPC:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
void Storage::DeleteAllEntriesImpl(bool local, F should_delete) {
|
||||
for (auto& i : m_entries) {
|
||||
Entry* entry = i.getValue();
|
||||
if (entry->value && should_delete(entry)) {
|
||||
// notify it's being deleted
|
||||
m_notifier.NotifyEntry(entry->local_id, i.getKey(), entry->value,
|
||||
NT_NOTIFY_DELETE | (local ? NT_NOTIFY_LOCAL : 0));
|
||||
Notify(entry, NT_NOTIFY_DELETE, local);
|
||||
// remove it from idmap
|
||||
if (entry->id < m_idmap.size()) {
|
||||
m_idmap[entry->id] = nullptr;
|
||||
@@ -1069,6 +1209,94 @@ unsigned int Storage::AddPolledListener(unsigned int poller,
|
||||
return uid;
|
||||
}
|
||||
|
||||
unsigned int Storage::StartDataLog(wpi::log::DataLog& log,
|
||||
std::string_view prefix,
|
||||
std::string_view log_prefix) {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
|
||||
// create
|
||||
unsigned int uid = m_dataloggers.emplace_back(log, prefix, log_prefix);
|
||||
m_dataloggers[uid].uid = uid;
|
||||
|
||||
// start logging any matching entries
|
||||
auto now = nt::Now();
|
||||
for (auto&& entry : m_entries) {
|
||||
if (!entry.second || !wpi::starts_with(entry.second->name, prefix) ||
|
||||
!entry.second->value) {
|
||||
continue;
|
||||
}
|
||||
auto type = GetStorageTypeStr(entry.second->value->type());
|
||||
if (type.empty()) {
|
||||
continue; // not a type we're going to log
|
||||
}
|
||||
int logentry = log.Start(
|
||||
fmt::format("{}{}", log_prefix,
|
||||
wpi::drop_front(entry.second->name, prefix.size())),
|
||||
type, "{\"source\":\"NT\"}", now);
|
||||
entry.second->datalogs.emplace_back(&log, logentry, uid);
|
||||
// log current value
|
||||
auto& v = *entry.second->value;
|
||||
entry.second->datalog_type = v.type();
|
||||
auto time = v.time();
|
||||
switch (v.type()) {
|
||||
case NT_BOOLEAN:
|
||||
log.AppendBoolean(logentry, v.GetBoolean(), time);
|
||||
break;
|
||||
case NT_DOUBLE:
|
||||
log.AppendDouble(logentry, v.GetDouble(), time);
|
||||
break;
|
||||
case NT_STRING:
|
||||
log.AppendString(logentry, v.GetString(), time);
|
||||
break;
|
||||
case NT_RAW: {
|
||||
auto val = v.GetRaw();
|
||||
log.AppendRaw(
|
||||
logentry,
|
||||
{reinterpret_cast<const uint8_t*>(val.data()), val.size()}, time);
|
||||
break;
|
||||
}
|
||||
case NT_BOOLEAN_ARRAY:
|
||||
log.AppendBooleanArray(logentry, v.GetBooleanArray(), time);
|
||||
break;
|
||||
case NT_DOUBLE_ARRAY:
|
||||
log.AppendDoubleArray(logentry, v.GetDoubleArray(), time);
|
||||
break;
|
||||
case NT_STRING_ARRAY:
|
||||
log.AppendStringArray(logentry, v.GetStringArray(), time);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return uid;
|
||||
}
|
||||
|
||||
void Storage::StopDataLog(unsigned int uid) {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
|
||||
// erase the datalogger
|
||||
auto datalogger = m_dataloggers.erase(uid);
|
||||
if (!datalogger) {
|
||||
return;
|
||||
}
|
||||
|
||||
// finish any active entries
|
||||
auto now = nt::Now();
|
||||
for (auto&& entry : m_entries) {
|
||||
if (!entry.second || entry.second->datalogs.empty()) {
|
||||
continue;
|
||||
}
|
||||
auto it = std::find_if(
|
||||
entry.second->datalogs.begin(), entry.second->datalogs.end(),
|
||||
[&](const auto& elem) { return elem.logger_uid == uid; });
|
||||
if (it != entry.second->datalogs.end()) {
|
||||
it->log->Finish(it->entry, now);
|
||||
entry.second->datalogs.erase(it);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Storage::GetPersistentEntries(
|
||||
bool periodic,
|
||||
std::vector<std::pair<std::string, std::shared_ptr<Value>>>* entries)
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
|
||||
#include <wpi/DenseMap.h>
|
||||
#include <wpi/SmallSet.h>
|
||||
#include <wpi/SmallVector.h>
|
||||
#include <wpi/StringMap.h>
|
||||
#include <wpi/UidVector.h>
|
||||
#include <wpi/condition_variable.h>
|
||||
#include <wpi/mutex.h>
|
||||
#include <wpi/span.h>
|
||||
@@ -117,6 +119,10 @@ class Storage : public IStorage {
|
||||
unsigned int AddPolledListener(unsigned int poller_uid, unsigned int local_id,
|
||||
unsigned int flags) const;
|
||||
|
||||
unsigned int StartDataLog(wpi::log::DataLog& log, std::string_view prefix,
|
||||
std::string_view log_prefix);
|
||||
void StopDataLog(unsigned int uid);
|
||||
|
||||
// Index-only
|
||||
unsigned int GetEntry(std::string_view name);
|
||||
std::vector<unsigned int> GetEntries(std::string_view prefix,
|
||||
@@ -161,6 +167,29 @@ class Storage : public IStorage {
|
||||
void CancelRpcResult(unsigned int local_id, unsigned int call_uid);
|
||||
|
||||
private:
|
||||
struct DataLoggerEntry {
|
||||
DataLoggerEntry(wpi::log::DataLog* log, int entry, unsigned int logger_uid)
|
||||
: log{log}, entry{entry}, logger_uid{logger_uid} {}
|
||||
|
||||
wpi::log::DataLog* log;
|
||||
int entry;
|
||||
unsigned int logger_uid;
|
||||
};
|
||||
|
||||
struct DataLogger {
|
||||
DataLogger() = default;
|
||||
DataLogger(wpi::log::DataLog& log, std::string_view prefix,
|
||||
std::string_view log_prefix)
|
||||
: log{&log}, prefix{prefix}, log_prefix{log_prefix} {}
|
||||
|
||||
explicit operator bool() const { return log != nullptr; }
|
||||
|
||||
wpi::log::DataLog* log = nullptr;
|
||||
std::string prefix;
|
||||
std::string log_prefix;
|
||||
unsigned int uid;
|
||||
};
|
||||
|
||||
// Data for each table entry.
|
||||
struct Entry {
|
||||
explicit Entry(std::string_view name_) : name(name_) {}
|
||||
@@ -195,6 +224,10 @@ class Storage : public IStorage {
|
||||
// Last UID used when calling this RPC (primarily for client use). This
|
||||
// is incremented for each call.
|
||||
unsigned int rpc_call_uid{0};
|
||||
|
||||
// log entries
|
||||
wpi::SmallVector<DataLoggerEntry, 0> datalogs;
|
||||
NT_Type datalog_type{NT_UNASSIGNED};
|
||||
};
|
||||
|
||||
using EntriesMap = wpi::StringMap<Entry*>;
|
||||
@@ -210,6 +243,7 @@ class Storage : public IStorage {
|
||||
LocalMap m_localmap;
|
||||
RpcResultMap m_rpc_results;
|
||||
RpcBlockingCallSet m_rpc_blocking_calls;
|
||||
wpi::UidVector<DataLogger, 4> m_dataloggers;
|
||||
// If any persistent values have changed
|
||||
mutable bool m_persistent_dirty = false;
|
||||
|
||||
@@ -255,6 +289,9 @@ class Storage : public IStorage {
|
||||
void DeleteEntryImpl(Entry* entry, std::unique_lock<wpi::mutex>& lock,
|
||||
bool local);
|
||||
|
||||
void Notify(Entry* entry, unsigned int flags, bool local,
|
||||
std::shared_ptr<Value> value = {});
|
||||
|
||||
// Must be called with m_mutex held
|
||||
template <typename F>
|
||||
void DeleteAllEntriesImpl(bool local, F should_delete);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
#include "edu_wpi_first_networktables_NetworkTablesJNI.h"
|
||||
#include "ntcore.h"
|
||||
#include "ntcore_cpp.h"
|
||||
|
||||
using namespace wpi::java;
|
||||
|
||||
@@ -1858,6 +1859,57 @@ Java_edu_wpi_first_networktables_NetworkTablesJNI_now
|
||||
return nt::Now();
|
||||
}
|
||||
|
||||
/*
|
||||
* Class: edu_wpi_first_networktables_NetworkTablesJNI
|
||||
* Method: startEntryDataLog
|
||||
* Signature: (IJLjava/lang/String;Ljava/lang/String;)I
|
||||
*/
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_edu_wpi_first_networktables_NetworkTablesJNI_startEntryDataLog
|
||||
(JNIEnv* env, jclass, jint inst, jlong log, jstring prefix, jstring logPrefix)
|
||||
{
|
||||
return nt::StartEntryDataLog(inst, *reinterpret_cast<wpi::log::DataLog*>(log),
|
||||
JStringRef{env, prefix},
|
||||
JStringRef{env, logPrefix});
|
||||
}
|
||||
|
||||
/*
|
||||
* Class: edu_wpi_first_networktables_NetworkTablesJNI
|
||||
* Method: stopEntryDataLog
|
||||
* Signature: (I)V
|
||||
*/
|
||||
JNIEXPORT void JNICALL
|
||||
Java_edu_wpi_first_networktables_NetworkTablesJNI_stopEntryDataLog
|
||||
(JNIEnv*, jclass, jint logger)
|
||||
{
|
||||
nt::StopEntryDataLog(logger);
|
||||
}
|
||||
|
||||
/*
|
||||
* Class: edu_wpi_first_networktables_NetworkTablesJNI
|
||||
* Method: startConnectionDataLog
|
||||
* Signature: (IJLjava/lang/String;)I
|
||||
*/
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_edu_wpi_first_networktables_NetworkTablesJNI_startConnectionDataLog
|
||||
(JNIEnv* env, jclass, jint inst, jlong log, jstring name)
|
||||
{
|
||||
return nt::StartConnectionDataLog(
|
||||
inst, *reinterpret_cast<wpi::log::DataLog*>(log), JStringRef{env, name});
|
||||
}
|
||||
|
||||
/*
|
||||
* Class: edu_wpi_first_networktables_NetworkTablesJNI
|
||||
* Method: stopConnectionDataLog
|
||||
* Signature: (I)V
|
||||
*/
|
||||
JNIEXPORT void JNICALL
|
||||
Java_edu_wpi_first_networktables_NetworkTablesJNI_stopConnectionDataLog
|
||||
(JNIEnv*, jclass, jint logger)
|
||||
{
|
||||
nt::StopConnectionDataLog(logger);
|
||||
}
|
||||
|
||||
/*
|
||||
* Class: edu_wpi_first_networktables_NetworkTablesJNI
|
||||
* Method: createLoggerPoller
|
||||
|
||||
@@ -820,6 +820,57 @@ uint64_t Now() {
|
||||
return wpi::Now();
|
||||
}
|
||||
|
||||
/*
|
||||
* Data Logger Functions
|
||||
*/
|
||||
NT_DataLogger StartEntryDataLog(NT_Inst inst, wpi::log::DataLog& log,
|
||||
std::string_view prefix,
|
||||
std::string_view logPrefix) {
|
||||
int i = Handle{inst}.GetTypedInst(Handle::kInstance);
|
||||
auto ii = InstanceImpl::Get(i);
|
||||
if (!ii) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Handle(i, ii->storage.StartDataLog(log, prefix, logPrefix),
|
||||
Handle::kDataLogger);
|
||||
}
|
||||
|
||||
void StopEntryDataLog(NT_DataLogger logger) {
|
||||
Handle handle{logger};
|
||||
int id = handle.GetTypedIndex(Handle::kDataLogger);
|
||||
auto ii = InstanceImpl::Get(handle.GetInst());
|
||||
if (id < 0 || !ii) {
|
||||
return;
|
||||
}
|
||||
|
||||
ii->storage.StopDataLog(id);
|
||||
}
|
||||
|
||||
NT_ConnectionDataLogger StartConnectionDataLog(NT_Inst inst,
|
||||
wpi::log::DataLog& log,
|
||||
std::string_view name) {
|
||||
int i = Handle{inst}.GetTypedInst(Handle::kInstance);
|
||||
auto ii = InstanceImpl::Get(i);
|
||||
if (!ii) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Handle(i, ii->dispatcher.StartDataLog(log, name),
|
||||
Handle::kConnectionDataLogger);
|
||||
}
|
||||
|
||||
void StopConnectionDataLog(NT_ConnectionDataLogger logger) {
|
||||
Handle handle{logger};
|
||||
int id = handle.GetTypedIndex(Handle::kConnectionDataLogger);
|
||||
auto ii = InstanceImpl::Get(handle.GetInst());
|
||||
if (id < 0 || !ii) {
|
||||
return;
|
||||
}
|
||||
|
||||
ii->dispatcher.StopDataLog(id);
|
||||
}
|
||||
|
||||
/*
|
||||
* Client/Server Functions
|
||||
*/
|
||||
|
||||
@@ -505,6 +505,52 @@ class NetworkTableInstance final {
|
||||
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* @{
|
||||
* @name Data Logger Functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Starts logging entry changes to a DataLog.
|
||||
*
|
||||
* @param log data log object; lifetime must extend until StopEntryDataLog is
|
||||
* called or the instance is destroyed
|
||||
* @param prefix only store entries with names that start with this prefix;
|
||||
* the prefix is not included in the data log entry name
|
||||
* @param logPrefix prefix to add to data log entry names
|
||||
* @return Data logger handle
|
||||
*/
|
||||
NT_DataLogger StartEntryDataLog(wpi::log::DataLog& log,
|
||||
std::string_view prefix,
|
||||
std::string_view logPrefix);
|
||||
|
||||
/**
|
||||
* Stops logging entry changes to a DataLog.
|
||||
*
|
||||
* @param logger data logger handle
|
||||
*/
|
||||
static void StopEntryDataLog(NT_DataLogger logger);
|
||||
|
||||
/**
|
||||
* Starts logging connection changes to a DataLog.
|
||||
*
|
||||
* @param log data log object; lifetime must extend until
|
||||
* StopConnectionDataLog is called or the instance is destroyed
|
||||
* @param name data log entry name
|
||||
* @return Data logger handle
|
||||
*/
|
||||
NT_ConnectionDataLogger StartConnectionDataLog(wpi::log::DataLog& log,
|
||||
std::string_view name);
|
||||
|
||||
/**
|
||||
* Stops logging connection changes to a DataLog.
|
||||
*
|
||||
* @param logger data logger handle
|
||||
*/
|
||||
static void StopConnectionDataLog(NT_ConnectionDataLogger logger);
|
||||
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* @{
|
||||
* @name Logger Functions
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "networktables/NetworkTableInstance.h"
|
||||
#include "ntcore_cpp.h"
|
||||
|
||||
namespace nt {
|
||||
|
||||
@@ -192,6 +193,26 @@ inline const char* NetworkTableInstance::LoadEntries(
|
||||
return ::nt::LoadEntries(m_handle, filename, prefix, warn);
|
||||
}
|
||||
|
||||
inline NT_DataLogger NetworkTableInstance::StartEntryDataLog(
|
||||
wpi::log::DataLog& log, std::string_view prefix,
|
||||
std::string_view logPrefix) {
|
||||
return ::nt::StartEntryDataLog(m_handle, log, prefix, logPrefix);
|
||||
}
|
||||
|
||||
inline void NetworkTableInstance::StopEntryDataLog(NT_DataLogger logger) {
|
||||
::nt::StopEntryDataLog(logger);
|
||||
}
|
||||
|
||||
inline NT_ConnectionDataLogger NetworkTableInstance::StartConnectionDataLog(
|
||||
wpi::log::DataLog& log, std::string_view name) {
|
||||
return ::nt::StartConnectionDataLog(m_handle, log, name);
|
||||
}
|
||||
|
||||
inline void NetworkTableInstance::StopConnectionDataLog(
|
||||
NT_ConnectionDataLogger logger) {
|
||||
::nt::StopConnectionDataLog(logger);
|
||||
}
|
||||
|
||||
inline NT_Logger NetworkTableInstance::AddLogger(
|
||||
std::function<void(const LogMessage& msg)> func, unsigned int min_level,
|
||||
unsigned int max_level) {
|
||||
|
||||
@@ -29,8 +29,10 @@ extern "C" {
|
||||
typedef int NT_Bool;
|
||||
|
||||
typedef unsigned int NT_Handle;
|
||||
typedef NT_Handle NT_ConnectionDataLogger;
|
||||
typedef NT_Handle NT_ConnectionListener;
|
||||
typedef NT_Handle NT_ConnectionListenerPoller;
|
||||
typedef NT_Handle NT_DataLogger;
|
||||
typedef NT_Handle NT_Entry;
|
||||
typedef NT_Handle NT_EntryListener;
|
||||
typedef NT_Handle NT_EntryListenerPoller;
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
|
||||
#include "networktables/NetworkTableValue.h"
|
||||
|
||||
namespace wpi::log {
|
||||
class DataLog;
|
||||
} // namespace wpi::log
|
||||
|
||||
/** NetworkTables (ntcore) namespace */
|
||||
namespace nt {
|
||||
|
||||
@@ -1233,6 +1237,55 @@ uint64_t Now();
|
||||
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* @defgroup ntcore_data_logger_func Data Logger Functions
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Starts logging entry changes to a DataLog.
|
||||
*
|
||||
* @param inst instance handle
|
||||
* @param log data log object; lifetime must extend until StopEntryDataLog is
|
||||
* called or the instance is destroyed
|
||||
* @param prefix only store entries with names that start with this prefix;
|
||||
* the prefix is not included in the data log entry name
|
||||
* @param logPrefix prefix to add to data log entry names
|
||||
* @return Data logger handle
|
||||
*/
|
||||
NT_DataLogger StartEntryDataLog(NT_Inst inst, wpi::log::DataLog& log,
|
||||
std::string_view prefix,
|
||||
std::string_view logPrefix);
|
||||
|
||||
/**
|
||||
* Stops logging entry changes to a DataLog.
|
||||
*
|
||||
* @param logger data logger handle
|
||||
*/
|
||||
void StopEntryDataLog(NT_DataLogger logger);
|
||||
|
||||
/**
|
||||
* Starts logging connection changes to a DataLog.
|
||||
*
|
||||
* @param inst instance handle
|
||||
* @param log data log object; lifetime must extend until StopConnectionDataLog
|
||||
* is called or the instance is destroyed
|
||||
* @param name data log entry name
|
||||
* @return Data logger handle
|
||||
*/
|
||||
NT_ConnectionDataLogger StartConnectionDataLog(NT_Inst inst,
|
||||
wpi::log::DataLog& log,
|
||||
std::string_view name);
|
||||
|
||||
/**
|
||||
* Stops logging connection changes to a DataLog.
|
||||
*
|
||||
* @param logger data logger handle
|
||||
*/
|
||||
void StopConnectionDataLog(NT_ConnectionDataLogger logger);
|
||||
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* @defgroup ntcore_logger_func Logger Functions
|
||||
* @{
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
@@ -33,6 +33,7 @@ include 'fieldImages'
|
||||
include 'glass'
|
||||
include 'outlineviewer'
|
||||
include 'roborioteamnumbersetter'
|
||||
include 'datalogtool'
|
||||
include 'simulation:gz_msgs'
|
||||
include 'simulation:frc_gazebo_plugins'
|
||||
include 'simulation:halsim_gazebo'
|
||||
|
||||
@@ -310,6 +310,10 @@ void DSCommPacket::SetupJoystickTag(wpi::raw_uv_ostream& buf) {
|
||||
void DSCommPacket::SendUDPToHALSim(void) {
|
||||
SendJoysticks();
|
||||
|
||||
if (!m_control_word.enabled) {
|
||||
m_match_time = -1;
|
||||
}
|
||||
|
||||
HALSIM_SetDriverStationMatchTime(m_match_time);
|
||||
HALSIM_SetDriverStationEnabled(m_control_word.enabled);
|
||||
HALSIM_SetDriverStationAutonomous(m_control_word.autonomous);
|
||||
|
||||
@@ -66,7 +66,7 @@ class DSCommPacket {
|
||||
HAL_AllianceStationID m_alliance_station;
|
||||
HAL_MatchInfo matchInfo;
|
||||
std::array<DSCommJoystickPacket, HAL_kMaxJoysticks> m_joystick_packets;
|
||||
double m_match_time;
|
||||
double m_match_time = -1;
|
||||
};
|
||||
|
||||
} // namespace halsim
|
||||
|
||||
@@ -247,8 +247,6 @@ class FMSSimModel : public glass::FMSModel {
|
||||
}
|
||||
void SetMatchTime(double val) override {
|
||||
HALSIM_SetDriverStationMatchTime(val);
|
||||
int32_t status = 0;
|
||||
m_startMatchTime = HAL_GetFPGATime(&status) * 1.0e-6 - val;
|
||||
}
|
||||
void SetEStop(bool val) override { HALSIM_SetDriverStationEStop(val); }
|
||||
void SetEnabled(bool val) override { HALSIM_SetDriverStationEnabled(val); }
|
||||
@@ -266,8 +264,6 @@ class FMSSimModel : public glass::FMSModel {
|
||||
|
||||
bool IsReadOnly() override;
|
||||
|
||||
bool m_matchTimeEnabled = true;
|
||||
|
||||
private:
|
||||
glass::DataSource m_fmsAttached{"FMS:FMSAttached"};
|
||||
glass::DataSource m_dsAttached{"FMS:DSAttached"};
|
||||
@@ -277,8 +273,7 @@ class FMSSimModel : public glass::FMSModel {
|
||||
glass::DataSource m_enabled{"FMS:RobotEnabled"};
|
||||
glass::DataSource m_test{"FMS:TestMode"};
|
||||
glass::DataSource m_autonomous{"FMS:AutonomousMode"};
|
||||
double m_startMatchTime = 0.0;
|
||||
double m_prevTime = 0.0;
|
||||
double m_startMatchTime = -1.0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -1138,6 +1133,7 @@ FMSSimModel::FMSSimModel() {
|
||||
m_enabled.SetDigital(true);
|
||||
m_test.SetDigital(true);
|
||||
m_autonomous.SetDigital(true);
|
||||
m_matchTime.SetValue(-1.0);
|
||||
}
|
||||
|
||||
void FMSSimModel::Update() {
|
||||
@@ -1151,25 +1147,23 @@ void FMSSimModel::Update() {
|
||||
m_autonomous.SetValue(HALSIM_GetDriverStationAutonomous());
|
||||
|
||||
double matchTime = HALSIM_GetDriverStationMatchTime();
|
||||
if (m_matchTimeEnabled && !IsDSDisabled()) {
|
||||
if (!IsDSDisabled() && enabled) {
|
||||
int32_t status = 0;
|
||||
double curTime = HAL_GetFPGATime(&status) * 1.0e-6;
|
||||
if (m_startMatchTime == 0.0) {
|
||||
m_startMatchTime = curTime;
|
||||
if (m_startMatchTime == -1.0) {
|
||||
m_startMatchTime = matchTime + curTime;
|
||||
}
|
||||
if (enabled) {
|
||||
matchTime = curTime - m_startMatchTime;
|
||||
HALSIM_SetDriverStationMatchTime(matchTime);
|
||||
} else {
|
||||
if (m_prevTime == 0.0) {
|
||||
m_prevTime = curTime;
|
||||
}
|
||||
m_startMatchTime += (curTime - m_prevTime);
|
||||
matchTime = m_startMatchTime - curTime;
|
||||
if (matchTime < 0) {
|
||||
matchTime = -1.0;
|
||||
}
|
||||
m_prevTime = curTime;
|
||||
HALSIM_SetDriverStationMatchTime(matchTime);
|
||||
} else {
|
||||
m_startMatchTime = 0.0;
|
||||
m_prevTime = 0.0;
|
||||
if (m_startMatchTime != -1.0) {
|
||||
matchTime = -1.0;
|
||||
HALSIM_SetDriverStationMatchTime(matchTime);
|
||||
}
|
||||
m_startMatchTime = -1.0;
|
||||
}
|
||||
m_matchTime.SetValue(matchTime);
|
||||
}
|
||||
@@ -1424,9 +1418,8 @@ void DriverStationGui::GlobalInit() {
|
||||
win->SetDefaultSize(300, 560);
|
||||
}
|
||||
}
|
||||
if (auto win = dsManager->AddWindow("FMS", [] {
|
||||
DisplayFMS(gFMSModel.get(), &gFMSModel->m_matchTimeEnabled);
|
||||
})) {
|
||||
if (auto win =
|
||||
dsManager->AddWindow("FMS", [] { DisplayFMS(gFMSModel.get()); })) {
|
||||
win->DisableRenamePopup();
|
||||
win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize);
|
||||
win->SetDefaultPos(5, 540);
|
||||
|
||||
@@ -27,9 +27,6 @@ HALProvider::HALProvider(glass::Storage& storage) : Provider{storage} {
|
||||
for (auto&& entry : m_viewEntries) {
|
||||
if (entry->showDefault) {
|
||||
Show(entry.get(), entry->window);
|
||||
if (entry->window) {
|
||||
entry->window->SetDefaultVisibility(glass::Window::kShow);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -59,6 +56,9 @@ void HALProvider::DisplayMenu() {
|
||||
visible || exists)) {
|
||||
if (!wasVisible && visible) {
|
||||
Show(viewEntry.get(), viewEntry->window);
|
||||
if (viewEntry->window) {
|
||||
viewEntry->window->SetVisible(true);
|
||||
}
|
||||
} else if (wasVisible && !visible && viewEntry->window) {
|
||||
viewEntry->window->SetVisible(false);
|
||||
}
|
||||
@@ -81,9 +81,8 @@ glass::Model* HALProvider::GetModel(std::string_view name) {
|
||||
}
|
||||
|
||||
void HALProvider::Show(ViewEntry* entry, glass::Window* window) {
|
||||
// if there's already a window, just show it
|
||||
// if there's already a window, we're done
|
||||
if (entry->window) {
|
||||
entry->window->SetVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -97,7 +96,9 @@ void HALProvider::Show(ViewEntry* entry, glass::Window* window) {
|
||||
|
||||
// the window might exist and we're just not associated to it yet
|
||||
if (!window) {
|
||||
window = GetOrAddWindow(entry->name, true, glass::Window::kHide);
|
||||
window = GetOrAddWindow(
|
||||
entry->name, true,
|
||||
entry->showDefault ? glass::Window::kShow : glass::Window::kHide);
|
||||
}
|
||||
if (!window) {
|
||||
return;
|
||||
@@ -110,6 +111,4 @@ void HALProvider::Show(ViewEntry* entry, glass::Window* window) {
|
||||
return;
|
||||
}
|
||||
window->SetView(std::move(view));
|
||||
|
||||
entry->window->SetVisible(true);
|
||||
}
|
||||
|
||||
@@ -32,3 +32,23 @@ void HALSimGui::GlobalInit() {
|
||||
|
||||
glass::AddStandardNetworkTablesViews(*ntProvider);
|
||||
}
|
||||
|
||||
namespace halsimgui {
|
||||
|
||||
void AddGuiInit(std::function<void()> initialize) {
|
||||
wpi::gui::AddInit(std::move(initialize));
|
||||
}
|
||||
|
||||
void AddGuiEarlyExecute(std::function<void()> execute) {
|
||||
wpi::gui::AddEarlyExecute(std::move(execute));
|
||||
}
|
||||
|
||||
void AddGuiLateExecute(std::function<void()> execute) {
|
||||
wpi::gui::AddLateExecute(std::move(execute));
|
||||
}
|
||||
|
||||
void GuiExit() {
|
||||
wpi::gui::Exit();
|
||||
}
|
||||
|
||||
} // namespace halsimgui
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "DriverStationGui.h"
|
||||
#include "EncoderSimGui.h"
|
||||
#include "HALSimGui.h"
|
||||
#include "HALSimGuiExt.h"
|
||||
#include "NetworkTablesSimGui.h"
|
||||
#include "PCMSimGui.h"
|
||||
#include "PWMSimGui.h"
|
||||
@@ -50,6 +51,17 @@ __declspec(dllexport)
|
||||
|
||||
glass::SetStorageName("simgui");
|
||||
|
||||
HAL_RegisterExtension(HALSIMGUI_EXT_ADDGUIINIT,
|
||||
reinterpret_cast<void*>((AddGuiInitFn)&AddGuiInit));
|
||||
HAL_RegisterExtension(
|
||||
HALSIMGUI_EXT_ADDGUILATEEXECUTE,
|
||||
reinterpret_cast<void*>((AddGuiLateExecuteFn)&AddGuiLateExecute));
|
||||
HAL_RegisterExtension(
|
||||
HALSIMGUI_EXT_ADDGUIEARLYEXECUTE,
|
||||
reinterpret_cast<void*>((AddGuiEarlyExecuteFn)&AddGuiEarlyExecute));
|
||||
HAL_RegisterExtension(HALSIMGUI_EXT_GUIEXIT,
|
||||
reinterpret_cast<void*>((GuiExitFn)&GuiExit));
|
||||
|
||||
HALSimGui::GlobalInit();
|
||||
DriverStationGui::GlobalInit();
|
||||
gPlotProvider = std::make_unique<glass::PlotProvider>(
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <glass/WindowManager.h>
|
||||
#include <glass/networktables/NetworkTablesProvider.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include "HALProvider.h"
|
||||
@@ -25,4 +26,9 @@ class HALSimGui {
|
||||
static std::unique_ptr<glass::NetworkTablesProvider> ntProvider;
|
||||
};
|
||||
|
||||
void AddGuiInit(std::function<void()> initialize);
|
||||
void AddGuiLateExecute(std::function<void()> execute);
|
||||
void AddGuiEarlyExecute(std::function<void()> execute);
|
||||
void GuiExit();
|
||||
|
||||
} // namespace halsimgui
|
||||
|
||||
26
simulation/halsim_gui/src/main/native/include/HALSimGuiExt.h
Normal file
@@ -0,0 +1,26 @@
|
||||
// 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 <functional>
|
||||
|
||||
namespace halsimgui {
|
||||
|
||||
// These functions can be used to hook into the GUI, and can be accessed
|
||||
// via HAL_RegisterExtensionListener
|
||||
|
||||
#define HALSIMGUI_EXT_ADDGUIINIT "halsimgui::AddGuiInit"
|
||||
using AddGuiInitFn = void (*)(std::function<void()> initialize);
|
||||
|
||||
#define HALSIMGUI_EXT_ADDGUILATEEXECUTE "halsimgui::AddGuiLateExecute"
|
||||
using AddGuiLateExecuteFn = void (*)(std::function<void()> execute);
|
||||
|
||||
#define HALSIMGUI_EXT_ADDGUIEARLYEXECUTE "halsimgui::AddGuiEarlyExecute"
|
||||
using AddGuiEarlyExecuteFn = void (*)(std::function<void()> execute);
|
||||
|
||||
#define HALSIMGUI_EXT_GUIEXIT "halsimgui::GuiExit"
|
||||
using GuiExitFn = void (*)();
|
||||
|
||||
} // namespace halsimgui
|
||||
@@ -3,7 +3,6 @@
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
@@ -35,6 +35,11 @@
|
||||
#ifndef _POSIX_C_SOURCE
|
||||
# define _POSIX_C_SOURCE 2 // for popen()
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
# ifndef _DARWIN_C_SOURCE
|
||||
# define _DARWIN_C_SOURCE
|
||||
# endif
|
||||
#endif
|
||||
#include <cstdio> // popen()
|
||||
#include <cstdlib> // std::getenv()
|
||||
#include <fcntl.h> // fcntl()
|
||||
@@ -46,11 +51,21 @@
|
||||
#ifdef _WIN32
|
||||
#include <set>
|
||||
#endif
|
||||
#include <iostream>
|
||||
#include <regex>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
// Versions of mingw64 g++ up to 9.3.0 do not have a complete IFileDialog
|
||||
#ifndef PFD_HAS_IFILEDIALOG
|
||||
# define PFD_HAS_IFILEDIALOG 1
|
||||
# if (defined __MINGW64__ || defined __MINGW32__) && defined __GXX_ABI_VERSION
|
||||
# if __GXX_ABI_VERSION <= 1013
|
||||
# undef PFD_HAS_IFILEDIALOG
|
||||
# define PFD_HAS_IFILEDIALOG 0
|
||||
# endif
|
||||
# endif
|
||||
#endif
|
||||
|
||||
//
|
||||
// Below this are all the method implementations.
|
||||
//
|
||||
@@ -90,6 +105,18 @@ public:
|
||||
HMODULE handle;
|
||||
};
|
||||
|
||||
// Helper class around CoInitialize() and CoUnInitialize()
|
||||
class ole32_dll : public dll
|
||||
{
|
||||
public:
|
||||
ole32_dll();
|
||||
~ole32_dll();
|
||||
bool is_initialized();
|
||||
|
||||
private:
|
||||
HRESULT m_state;
|
||||
};
|
||||
|
||||
// Helper class around CreateActCtx() and ActivateActCtx()
|
||||
class new_style_context
|
||||
{
|
||||
@@ -489,6 +516,30 @@ internal::platform::dll::~dll()
|
||||
}
|
||||
#endif // _WIN32
|
||||
|
||||
// ole32_dll implementation
|
||||
|
||||
#if _WIN32
|
||||
internal::platform::ole32_dll::ole32_dll()
|
||||
: dll("ole32.dll")
|
||||
{
|
||||
// Use COINIT_MULTITHREADED because COINIT_APARTMENTTHREADED causes crashes.
|
||||
// See https://github.com/samhocevar/portable-file-dialogs/issues/51
|
||||
auto coinit = proc<HRESULT WINAPI (LPVOID, DWORD)>(*this, "CoInitializeEx");
|
||||
m_state = coinit(nullptr, COINIT_MULTITHREADED);
|
||||
}
|
||||
|
||||
internal::platform::ole32_dll::~ole32_dll()
|
||||
{
|
||||
if (is_initialized())
|
||||
proc<void WINAPI ()>(*this, "CoUninitialize")();
|
||||
}
|
||||
|
||||
bool internal::platform::ole32_dll::is_initialized()
|
||||
{
|
||||
return m_state == S_OK || m_state == S_FALSE;
|
||||
}
|
||||
#endif
|
||||
|
||||
// new_style_context implementation
|
||||
|
||||
#if _WIN32
|
||||
@@ -595,13 +646,17 @@ std::string internal::dialog::get_icon_name(icon _icon)
|
||||
}
|
||||
}
|
||||
|
||||
// THis is only used for debugging purposes
|
||||
std::ostream& operator <<(std::ostream &s, std::vector<std::string> const &v)
|
||||
// This is only used for debugging purposes
|
||||
void print_command(std::vector<std::string> const &v)
|
||||
{
|
||||
int not_first = 0;
|
||||
for (auto &e : v)
|
||||
s << (not_first++ ? " " : "") << e;
|
||||
return s;
|
||||
fputs("pfd: ", stderr);
|
||||
for (size_t i = 0; i < v.size(); ++i) {
|
||||
if (i > 0) {
|
||||
fputc(' ', stderr);
|
||||
}
|
||||
fputs(v[i].c_str(), stderr);
|
||||
}
|
||||
fputc('\n', stderr);
|
||||
}
|
||||
|
||||
// Properly quote a string for Powershell: replace ' or " with '' or ""
|
||||
@@ -634,7 +689,9 @@ class internal::file_dialog::Impl {
|
||||
public:
|
||||
#if _WIN32
|
||||
static int CALLBACK bffcallback(HWND hwnd, UINT uMsg, LPARAM, LPARAM pData);
|
||||
#if PFD_HAS_IFILEDIALOG
|
||||
std::string select_folder_vista(IFileDialog *ifd, bool force_path);
|
||||
#endif
|
||||
|
||||
std::wstring m_wtitle;
|
||||
std::wstring m_wdefault_path;
|
||||
@@ -668,13 +725,16 @@ internal::file_dialog::file_dialog(type in_type,
|
||||
m_impl->m_wdefault_path = internal::str2wstr(default_path);
|
||||
auto wfilter_list = internal::str2wstr(filter_list);
|
||||
|
||||
// Initialise COM. This is required for the new folder selection window,
|
||||
// (see https://github.com/samhocevar/portable-file-dialogs/pull/21)
|
||||
// and to avoid random crashes with GetOpenFileNameW() (see
|
||||
// https://github.com/samhocevar/portable-file-dialogs/issues/51)
|
||||
internal::platform::ole32_dll ole32;
|
||||
|
||||
// Folder selection uses a different method
|
||||
if (in_type == type::folder)
|
||||
{
|
||||
internal::platform::dll ole32("ole32.dll");
|
||||
|
||||
auto status = internal::platform::dll::proc<HRESULT WINAPI (LPVOID, DWORD)>(ole32, "CoInitializeEx")
|
||||
(nullptr, COINIT_APARTMENTTHREADED);
|
||||
#if PFD_HAS_IFILEDIALOG
|
||||
if (flags(flag::is_vista))
|
||||
{
|
||||
// On Vista and higher we should be able to use IFileDialog for folder selection
|
||||
@@ -686,6 +746,7 @@ internal::file_dialog::file_dialog(type in_type,
|
||||
if (SUCCEEDED(hr))
|
||||
return m_impl->select_folder_vista(ifd, options & opt::force_path);
|
||||
}
|
||||
#endif
|
||||
|
||||
BROWSEINFOW bi;
|
||||
memset(&bi, 0, sizeof(bi));
|
||||
@@ -695,9 +756,7 @@ internal::file_dialog::file_dialog(type in_type,
|
||||
|
||||
if (flags(flag::is_vista))
|
||||
{
|
||||
// This hangs on Windows XP, as reported here:
|
||||
// https://github.com/samhocevar/portable-file-dialogs/pull/21
|
||||
if (status == S_OK)
|
||||
if (ole32.is_initialized())
|
||||
bi.ulFlags |= BIF_NEWDIALOGSTYLE;
|
||||
bi.ulFlags |= BIF_EDITBOX;
|
||||
bi.ulFlags |= BIF_STATUSTEXT;
|
||||
@@ -713,8 +772,6 @@ internal::file_dialog::file_dialog(type in_type,
|
||||
ret = internal::wstr2str(buffer);
|
||||
delete[] buffer;
|
||||
}
|
||||
if (status == S_OK)
|
||||
internal::platform::dll::proc<void WINAPI ()>(ole32, "CoUninitialize")();
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -750,37 +807,36 @@ internal::file_dialog::file_dialog(type in_type,
|
||||
|
||||
internal::platform::dll comdlg32("comdlg32.dll");
|
||||
|
||||
// Apply new visual style (required for windows XP)
|
||||
internal::platform::new_style_context ctx;
|
||||
|
||||
if (in_type == type::save)
|
||||
{
|
||||
if (!(options & opt::force_overwrite))
|
||||
ofn.Flags |= OFN_OVERWRITEPROMPT;
|
||||
|
||||
// using set context to apply new visual style (required for windows XP)
|
||||
internal::platform::new_style_context ctx;
|
||||
|
||||
internal::platform::dll::proc<BOOL WINAPI (LPOPENFILENAMEW)> get_save_file_name(comdlg32, "GetSaveFileNameW");
|
||||
if (get_save_file_name(&ofn) == 0)
|
||||
return "";
|
||||
return internal::wstr2str(woutput.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (options & opt::multiselect)
|
||||
ofn.Flags |= OFN_ALLOWMULTISELECT;
|
||||
ofn.Flags |= OFN_PATHMUSTEXIST;
|
||||
|
||||
if (options & opt::multiselect)
|
||||
ofn.Flags |= OFN_ALLOWMULTISELECT;
|
||||
ofn.Flags |= OFN_PATHMUSTEXIST;
|
||||
|
||||
// using set context to apply new visual style (required for windows XP)
|
||||
internal::platform::new_style_context ctx;
|
||||
|
||||
internal::platform::dll::proc<BOOL WINAPI (LPOPENFILENAMEW)> get_open_file_name(comdlg32, "GetOpenFileNameW");
|
||||
if (get_open_file_name(&ofn) == 0)
|
||||
return "";
|
||||
internal::platform::dll::proc<BOOL WINAPI (LPOPENFILENAMEW)> get_open_file_name(comdlg32, "GetOpenFileNameW");
|
||||
if (get_open_file_name(&ofn) == 0)
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string prefix;
|
||||
for (wchar_t const *p = woutput.c_str(); *p; )
|
||||
{
|
||||
auto filename = internal::wstr2str(p);
|
||||
p += filename.size();
|
||||
// In multiselect mode, we advance p one step more and
|
||||
p += wcslen(p);
|
||||
// In multiselect mode, we advance p one wchar further and
|
||||
// check for another filename. If there is one and the
|
||||
// prefix is empty, it means we just read the prefix.
|
||||
if ((options & opt::multiselect) && *++p && prefix.empty())
|
||||
@@ -895,7 +951,10 @@ internal::file_dialog::file_dialog(type in_type,
|
||||
case type::folder: command.push_back("--getexistingdirectory"); break;
|
||||
}
|
||||
if (options & opt::multiselect)
|
||||
command.push_back(" --multiple");
|
||||
{
|
||||
command.push_back("--multiple");
|
||||
command.push_back("--separate-output");
|
||||
}
|
||||
|
||||
command.push_back(default_path);
|
||||
|
||||
@@ -909,7 +968,10 @@ internal::file_dialog::file_dialog(type in_type,
|
||||
}
|
||||
|
||||
if (flags(flag::is_verbose))
|
||||
std::cerr << "pfd: " << command << std::endl;
|
||||
print_command(command);
|
||||
|
||||
if (!available())
|
||||
fputs("pfd: Unable to find zenity/matedialog/qarma/kdialog to open file chooser\n", stderr);
|
||||
|
||||
m_async->start_process(command);
|
||||
#endif
|
||||
@@ -923,8 +985,8 @@ std::string internal::file_dialog::string_result()
|
||||
auto ret = m_async->result();
|
||||
// Strip potential trailing newline (zenity). Also strip trailing slash
|
||||
// added by osascript for consistency with other backends.
|
||||
while (ret.back() == '\n' || ret.back() == '/')
|
||||
ret = ret.substr(0, ret.size() - 1);
|
||||
while (!ret.empty() && (ret.back() == '\n' || ret.back() == '/'))
|
||||
ret.pop_back();
|
||||
return ret;
|
||||
#endif
|
||||
}
|
||||
@@ -965,6 +1027,7 @@ int CALLBACK internal::file_dialog::Impl::bffcallback(HWND hwnd, UINT uMsg,
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if PFD_HAS_IFILEDIALOG
|
||||
std::string internal::file_dialog::Impl::select_folder_vista(IFileDialog *ifd, bool force_path)
|
||||
{
|
||||
std::string result;
|
||||
@@ -1016,8 +1079,7 @@ std::string internal::file_dialog::Impl::select_folder_vista(IFileDialog *ifd, b
|
||||
if (wselected)
|
||||
{
|
||||
result = internal::wstr2str(std::wstring(wselected));
|
||||
internal::platform::dll ole32("ole32.dll");
|
||||
internal::platform::dll::proc<void WINAPI (LPVOID)>(ole32, "CoTaskMemFree")(wselected);
|
||||
internal::platform::dll::proc<void WINAPI (LPVOID)>(internal::platform::ole32_dll(), "CoTaskMemFree")(wselected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1027,6 +1089,7 @@ std::string internal::file_dialog::Impl::select_folder_vista(IFileDialog *ifd, b
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// notify implementation
|
||||
|
||||
@@ -1127,7 +1190,10 @@ notify::notify(std::string const &title,
|
||||
}
|
||||
|
||||
if (flags(flag::is_verbose))
|
||||
std::cerr << "pfd: " << command << std::endl;
|
||||
print_command(command);
|
||||
|
||||
if (!available())
|
||||
fputs("pfd: Unable to find zenity/matedialog/qarma/kdialog to open file chooser\n", stderr);
|
||||
|
||||
m_async->start_process(command);
|
||||
#endif
|
||||
@@ -1141,7 +1207,9 @@ message::message(std::string const &title,
|
||||
icon _icon /* = icon::info */)
|
||||
{
|
||||
#if _WIN32
|
||||
UINT style = MB_TOPMOST;
|
||||
// Use MB_SYSTEMMODAL rather than MB_TOPMOST to ensure the message window is brought
|
||||
// to front. See https://github.com/samhocevar/portable-file-dialogs/issues/52
|
||||
UINT style = MB_SYSTEMMODAL;
|
||||
switch (_icon)
|
||||
{
|
||||
case icon::warning: style |= MB_ICONWARNING; break;
|
||||
@@ -1168,11 +1236,11 @@ message::message(std::string const &title,
|
||||
m_mappings[IDRETRY] = button::retry;
|
||||
m_mappings[IDIGNORE] = button::ignore;
|
||||
|
||||
m_async->start_func([this, text, title, style](int* exit_code) -> std::string
|
||||
m_async->start_func([text, title, style](int* exit_code) -> std::string
|
||||
{
|
||||
auto wtext = internal::str2wstr(text);
|
||||
auto wtitle = internal::str2wstr(title);
|
||||
// using set context to apply new visual style (required for all windows versions)
|
||||
// Apply new visual style (required for all Windows versions)
|
||||
internal::platform::new_style_context ctx;
|
||||
*exit_code = MessageBoxW(GetActiveWindow(), wtext.c_str(), wtitle.c_str(), style);
|
||||
return "";
|
||||
@@ -1328,7 +1396,10 @@ message::message(std::string const &title,
|
||||
}
|
||||
|
||||
if (flags(flag::is_verbose))
|
||||
std::cerr << "pfd: " << command << std::endl;
|
||||
print_command(command);
|
||||
|
||||
if (!available())
|
||||
fputs("pfd: Unable to find zenity/matedialog/qarma/kdialog to open file chooser\n", stderr);
|
||||
|
||||
m_async->start_process(command);
|
||||
#endif
|
||||
|
||||
@@ -91,10 +91,29 @@ public interface Command {
|
||||
* @param condition the interrupt condition
|
||||
* @return the command with the interrupt condition added
|
||||
*/
|
||||
default ParallelRaceGroup withInterrupt(BooleanSupplier condition) {
|
||||
default ParallelRaceGroup until(BooleanSupplier condition) {
|
||||
return raceWith(new WaitUntilCommand(condition));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorates this command with an interrupt condition. If the specified condition becomes true
|
||||
* before the command finishes normally, the command will be interrupted and un-scheduled. Note
|
||||
* that this only applies to the command returned by this method; the calling command is not
|
||||
* itself changed.
|
||||
*
|
||||
* <p>Note: This decorator works by composing this command within a CommandGroup. The command
|
||||
* cannot be used independently after being decorated, or be re-decorated with a different
|
||||
* decorator, unless it is manually cleared from the list of grouped commands with {@link
|
||||
* CommandGroupBase#clearGroupedCommand(Command)}. The decorated command can, however, be further
|
||||
* decorated without issue.
|
||||
*
|
||||
* @param condition the interrupt condition
|
||||
* @return the command with the interrupt condition added
|
||||
*/
|
||||
default ParallelRaceGroup withInterrupt(BooleanSupplier condition) {
|
||||
return until(condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorates this command with a runnable to run before this command starts.
|
||||
*
|
||||
|
||||
@@ -112,9 +112,9 @@ public class MecanumControllerCommand extends CommandBase {
|
||||
|
||||
m_controller =
|
||||
new HolonomicDriveController(
|
||||
requireNonNullParam(xController, "xController", "SwerveControllerCommand"),
|
||||
requireNonNullParam(yController, "xController", "SwerveControllerCommand"),
|
||||
requireNonNullParam(thetaController, "thetaController", "SwerveControllerCommand"));
|
||||
requireNonNullParam(xController, "xController", "MecanumControllerCommand"),
|
||||
requireNonNullParam(yController, "yController", "MecanumControllerCommand"),
|
||||
requireNonNullParam(thetaController, "thetaController", "MecanumControllerCommand"));
|
||||
|
||||
m_desiredRotation =
|
||||
requireNonNullParam(desiredRotation, "desiredRotation", "MecanumControllerCommand");
|
||||
@@ -255,9 +255,9 @@ public class MecanumControllerCommand extends CommandBase {
|
||||
|
||||
m_controller =
|
||||
new HolonomicDriveController(
|
||||
requireNonNullParam(xController, "xController", "SwerveControllerCommand"),
|
||||
requireNonNullParam(yController, "xController", "SwerveControllerCommand"),
|
||||
requireNonNullParam(thetaController, "thetaController", "SwerveControllerCommand"));
|
||||
requireNonNullParam(xController, "xController", "MecanumControllerCommand"),
|
||||
requireNonNullParam(yController, "yController", "MecanumControllerCommand"),
|
||||
requireNonNullParam(thetaController, "thetaController", "MecanumControllerCommand"));
|
||||
|
||||
m_desiredRotation =
|
||||
requireNonNullParam(desiredRotation, "desiredRotation", "MecanumControllerCommand");
|
||||
|
||||
@@ -9,7 +9,7 @@ import edu.wpi.first.wpilibj.Notifier;
|
||||
/**
|
||||
* A command that starts a notifier to run the given runnable periodically in a separate thread. Has
|
||||
* no end condition as-is; either subclass it or use {@link Command#withTimeout(double)} or {@link
|
||||
* Command#withInterrupt(java.util.function.BooleanSupplier)} to give it one.
|
||||
* Command#until(java.util.function.BooleanSupplier)} to give it one.
|
||||
*
|
||||
* <p>WARNING: Do not use this class unless you are confident in your ability to make the executed
|
||||
* code thread-safe. If you do not know what "thread-safe" means, that is a good sign that you
|
||||
|
||||
@@ -39,10 +39,10 @@ public class PIDCommand extends CommandBase {
|
||||
DoubleSupplier setpointSource,
|
||||
DoubleConsumer useOutput,
|
||||
Subsystem... requirements) {
|
||||
requireNonNullParam(controller, "controller", "SynchronousPIDCommand");
|
||||
requireNonNullParam(measurementSource, "measurementSource", "SynchronousPIDCommand");
|
||||
requireNonNullParam(setpointSource, "setpointSource", "SynchronousPIDCommand");
|
||||
requireNonNullParam(useOutput, "useOutput", "SynchronousPIDCommand");
|
||||
requireNonNullParam(controller, "controller", "PIDCommand");
|
||||
requireNonNullParam(measurementSource, "measurementSource", "PIDCommand");
|
||||
requireNonNullParam(setpointSource, "setpointSource", "PIDCommand");
|
||||
requireNonNullParam(useOutput, "useOutput", "PIDCommand");
|
||||
|
||||
m_controller = controller;
|
||||
m_useOutput = useOutput;
|
||||
|
||||
@@ -42,10 +42,10 @@ public class ProfiledPIDCommand extends CommandBase {
|
||||
Supplier<State> goalSource,
|
||||
BiConsumer<Double, State> useOutput,
|
||||
Subsystem... requirements) {
|
||||
requireNonNullParam(controller, "controller", "SynchronousPIDCommand");
|
||||
requireNonNullParam(measurementSource, "measurementSource", "SynchronousPIDCommand");
|
||||
requireNonNullParam(goalSource, "goalSource", "SynchronousPIDCommand");
|
||||
requireNonNullParam(useOutput, "useOutput", "SynchronousPIDCommand");
|
||||
requireNonNullParam(controller, "controller", "ProfiledPIDCommand");
|
||||
requireNonNullParam(measurementSource, "measurementSource", "ProfiledPIDCommand");
|
||||
requireNonNullParam(goalSource, "goalSource", "ProfiledPIDCommand");
|
||||
requireNonNullParam(useOutput, "useOutput", "ProfiledPIDCommand");
|
||||
|
||||
m_controller = controller;
|
||||
m_useOutput = useOutput;
|
||||
|
||||
@@ -119,7 +119,8 @@ public class RamseteCommand extends CommandBase {
|
||||
m_pose = requireNonNullParam(pose, "pose", "RamseteCommand");
|
||||
m_follower = requireNonNullParam(follower, "follower", "RamseteCommand");
|
||||
m_kinematics = requireNonNullParam(kinematics, "kinematics", "RamseteCommand");
|
||||
m_output = requireNonNullParam(outputMetersPerSecond, "output", "RamseteCommand");
|
||||
m_output =
|
||||
requireNonNullParam(outputMetersPerSecond, "outputMetersPerSecond", "RamseteCommand");
|
||||
|
||||
m_feedforward = null;
|
||||
m_speeds = null;
|
||||
|
||||
@@ -10,8 +10,8 @@ import java.util.function.BooleanSupplier;
|
||||
|
||||
/**
|
||||
* A command that runs a Runnable continuously. Has no end condition as-is; either subclass it or
|
||||
* use {@link Command#withTimeout(double)} or {@link Command#withInterrupt(BooleanSupplier)} to give
|
||||
* it one. If you only wish to execute a Runnable once, use {@link InstantCommand}.
|
||||
* use {@link Command#withTimeout(double)} or {@link Command#until(BooleanSupplier)} to give it one.
|
||||
* If you only wish to execute a Runnable once, use {@link InstantCommand}.
|
||||
*
|
||||
* <p>This class is provided by the NewCommands VendorDep
|
||||
*/
|
||||
|
||||
@@ -10,7 +10,7 @@ import static edu.wpi.first.wpilibj.util.ErrorMessages.requireNonNullParam;
|
||||
* A command that runs a given runnable when it is initialized, and another runnable when it ends.
|
||||
* Useful for running and then stopping a motor, or extending and then retracting a solenoid. Has no
|
||||
* end condition as-is; either subclass it or use {@link Command#withTimeout(double)} or {@link
|
||||
* Command#withInterrupt(java.util.function.BooleanSupplier)} to give it one.
|
||||
* Command#until(java.util.function.BooleanSupplier)} to give it one.
|
||||
*
|
||||
* <p>This class is provided by the NewCommands VendorDep
|
||||
*/
|
||||
|
||||
@@ -79,11 +79,11 @@ public class SwerveControllerCommand extends CommandBase {
|
||||
m_controller =
|
||||
new HolonomicDriveController(
|
||||
requireNonNullParam(xController, "xController", "SwerveControllerCommand"),
|
||||
requireNonNullParam(yController, "xController", "SwerveControllerCommand"),
|
||||
requireNonNullParam(yController, "yController", "SwerveControllerCommand"),
|
||||
requireNonNullParam(thetaController, "thetaController", "SwerveControllerCommand"));
|
||||
|
||||
m_outputModuleStates =
|
||||
requireNonNullParam(outputModuleStates, "frontLeftOutput", "SwerveControllerCommand");
|
||||
requireNonNullParam(outputModuleStates, "outputModuleStates", "SwerveControllerCommand");
|
||||
|
||||
m_desiredRotation =
|
||||
requireNonNullParam(desiredRotation, "desiredRotation", "SwerveControllerCommand");
|
||||
|
||||
@@ -37,6 +37,13 @@ ParallelRaceGroup Command::WithTimeout(units::second_t duration) && {
|
||||
return ParallelRaceGroup(std::move(temp));
|
||||
}
|
||||
|
||||
ParallelRaceGroup Command::Until(std::function<bool()> condition) && {
|
||||
std::vector<std::unique_ptr<Command>> temp;
|
||||
temp.emplace_back(std::make_unique<WaitUntilCommand>(std::move(condition)));
|
||||
temp.emplace_back(std::move(*this).TransferOwnership());
|
||||
return ParallelRaceGroup(std::move(temp));
|
||||
}
|
||||
|
||||
ParallelRaceGroup Command::WithInterrupt(std::function<bool()> condition) && {
|
||||
std::vector<std::unique_ptr<Command>> temp;
|
||||
temp.emplace_back(std::make_unique<WaitUntilCommand>(std::move(condition)));
|
||||
|
||||
@@ -111,6 +111,17 @@ class Command {
|
||||
*/
|
||||
virtual ParallelRaceGroup WithTimeout(units::second_t duration) &&;
|
||||
|
||||
/**
|
||||
* Decorates this command with an interrupt condition. If the specified
|
||||
* condition becomes true before the command finishes normally, the command
|
||||
* will be interrupted and un-scheduled. Note that this only applies to the
|
||||
* command returned by this method; the calling command is not itself changed.
|
||||
*
|
||||
* @param condition the interrupt condition
|
||||
* @return the command with the interrupt condition added
|
||||
*/
|
||||
virtual ParallelRaceGroup Until(std::function<bool()> condition) &&;
|
||||
|
||||
/**
|
||||
* Decorates this command with an interrupt condition. If the specified
|
||||
* condition becomes true before the command finishes normally, the command
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace frc2 {
|
||||
/**
|
||||
* A command that starts a notifier to run the given runnable periodically in a
|
||||
* separate thread. Has no end condition as-is; either subclass it or use
|
||||
* Command::WithTimeout(double) or Command::WithInterrupt(BooleanSupplier) to
|
||||
* Command::WithTimeout(double) or Command::Until(BooleanSupplier) to
|
||||
* give it one.
|
||||
*
|
||||
* <p>WARNING: Do not use this class unless you are confident in your ability to
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace frc2 {
|
||||
/**
|
||||
* A command that runs a Runnable continuously. Has no end condition as-is;
|
||||
* either subclass it or use Command.WithTimeout() or
|
||||
* Command.WithInterrupt() to give it one. If you only wish
|
||||
* Command.Until() to give it one. If you only wish
|
||||
* to execute a Runnable once, use InstantCommand.
|
||||
*
|
||||
* This class is provided by the NewCommands VendorDep
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace frc2 {
|
||||
* A command that runs a given runnable when it is initialized, and another
|
||||
* runnable when it ends. Useful for running and then stopping a motor, or
|
||||
* extending and then retracting a solenoid. Has no end condition as-is; either
|
||||
* subclass it or use Command.WithTimeout() or Command.WithInterrupt() to give
|
||||
* subclass it or use Command.WithTimeout() or Command.Until() to give
|
||||
* it one.
|
||||
*
|
||||
* This class is provided by the NewCommands VendorDep
|
||||
|
||||
@@ -39,11 +39,11 @@ class CommandDecoratorTest extends CommandTestBase {
|
||||
}
|
||||
|
||||
@Test
|
||||
void withInterruptTest() {
|
||||
void untilTest() {
|
||||
try (CommandScheduler scheduler = new CommandScheduler()) {
|
||||
ConditionHolder condition = new ConditionHolder();
|
||||
|
||||
Command command = new WaitCommand(10).withInterrupt(condition::getCondition);
|
||||
Command command = new WaitCommand(10).until(condition::getCondition);
|
||||
|
||||
scheduler.schedule(command);
|
||||
scheduler.run();
|
||||
|
||||
@@ -40,7 +40,7 @@ class CommandGroupErrorTest extends CommandTestBase {
|
||||
void redecoratedCommandErrorTest() {
|
||||
Command command = new InstantCommand();
|
||||
|
||||
assertDoesNotThrow(() -> command.withTimeout(10).withInterrupt(() -> false));
|
||||
assertDoesNotThrow(() -> command.withTimeout(10).until(() -> false));
|
||||
assertThrows(IllegalArgumentException.class, () -> command.withTimeout(10));
|
||||
CommandGroupBase.clearGroupedCommand(command);
|
||||
assertDoesNotThrow(() -> command.withTimeout(10));
|
||||
|
||||
@@ -34,13 +34,12 @@ TEST_F(CommandDecoratorTest, WithTimeout) {
|
||||
frc::sim::ResumeTiming();
|
||||
}
|
||||
|
||||
TEST_F(CommandDecoratorTest, WithInterrupt) {
|
||||
TEST_F(CommandDecoratorTest, Until) {
|
||||
CommandScheduler scheduler = GetScheduler();
|
||||
|
||||
bool finished = false;
|
||||
|
||||
auto command =
|
||||
RunCommand([] {}, {}).WithInterrupt([&finished] { return finished; });
|
||||
auto command = RunCommand([] {}, {}).Until([&finished] { return finished; });
|
||||
|
||||
scheduler.Schedule(&command);
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ public class PIDBase implements PIDInterface, PIDOutput, Sendable, AutoCloseable
|
||||
*/
|
||||
@SuppressWarnings("ParameterName")
|
||||
public PIDBase(double Kp, double Ki, double Kd, double Kf, PIDSource source, PIDOutput output) {
|
||||
requireNonNullParam(source, "PIDSource", "PIDBase");
|
||||
requireNonNullParam(source, "source", "PIDBase");
|
||||
requireNonNullParam(output, "output", "PIDBase");
|
||||
|
||||
m_setpointTimer = new Timer();
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include <hal/HAL.h>
|
||||
@@ -174,7 +173,6 @@ bool ADIS16448_IMU::SwitchToStandardSPI() {
|
||||
while (!m_thread_idle) {
|
||||
Wait(10_ms);
|
||||
}
|
||||
std::cout << "Paused the IMU processing thread successfully!" << std::endl;
|
||||
// Maybe we're in auto SPI mode? If so, kill auto SPI, and then SPI.
|
||||
if (m_spi != nullptr && m_auto_configured) {
|
||||
m_spi->StopAuto();
|
||||
@@ -193,12 +191,10 @@ bool ADIS16448_IMU::SwitchToStandardSPI() {
|
||||
/* Update remaining buffer count */
|
||||
data_count = m_spi->ReadAutoReceivedData(trashBuffer, 0, 0_s);
|
||||
}
|
||||
std::cout << "Paused the auto SPI successfully!" << std::endl;
|
||||
}
|
||||
}
|
||||
// There doesn't seem to be a SPI port active. Let's try to set one up
|
||||
if (m_spi == nullptr) {
|
||||
std::cout << "Setting up a new SPI port." << std::endl;
|
||||
m_spi = new SPI(m_spi_port);
|
||||
m_spi->SetClockRate(1000000);
|
||||
m_spi->SetMSBFirst();
|
||||
@@ -292,11 +288,9 @@ bool ADIS16448_IMU::SwitchToAutoSPI() {
|
||||
InitOffsetBuffer(m_avg_size);
|
||||
// Kick off acquire thread
|
||||
m_acquire_task = std::thread(&ADIS16448_IMU::Acquire, this);
|
||||
std::cout << "New IMU Processing thread activated!" << std::endl;
|
||||
} else {
|
||||
m_first_run = true;
|
||||
m_thread_active = true;
|
||||
std::cout << "Old IMU Processing thread re-activated!" << std::endl;
|
||||
}
|
||||
// Looks like the thread didn't start for some reason. Abort.
|
||||
/*
|
||||
@@ -344,9 +338,6 @@ void ADIS16448_IMU::Calibrate() {
|
||||
m_integ_gyro_angle_x = 0.0;
|
||||
m_integ_gyro_angle_y = 0.0;
|
||||
m_integ_gyro_angle_z = 0.0;
|
||||
// std::cout << "Avg Size: " << gyroAverageSize << " X off: " <<
|
||||
// m_gyro_rate_offset_x << " Y off: " << m_gyro_rate_offset_y << " Z off: " <<
|
||||
// m_gyro_rate_offset_z << std::endl;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -415,7 +406,6 @@ void ADIS16448_IMU::Close() {
|
||||
m_spi = nullptr;
|
||||
}
|
||||
delete[] m_offset_buffer;
|
||||
std::cout << "Finished cleaning up after the IMU driver." << std::endl;
|
||||
}
|
||||
|
||||
ADIS16448_IMU::~ADIS16448_IMU() {
|
||||
@@ -517,16 +507,6 @@ void ADIS16448_IMU::Acquire() {
|
||||
baro = BuffToShort(&buffer[i + 23]) * 0.02;
|
||||
temp = BuffToShort(&buffer[i + 25]) * 0.07386 + 31.0;
|
||||
|
||||
/*std::cout << BuffToShort(&buffer[i + 3]) << "," <<
|
||||
BuffToShort(&buffer[i + 5]) << "," << BuffToShort(&buffer[i + 7]) <<
|
||||
"," << BuffToShort(&buffer[i + 9]) << "," << BuffToShort(&buffer[i +
|
||||
11]) << "," << BuffToShort(&buffer[i + 13]) << "," <<
|
||||
BuffToShort(&buffer[i + 15]) << "," << BuffToShort(&buffer[i + 17]) <<
|
||||
"," << BuffToShort(&buffer[i + 19]) << "," << BuffToShort(&buffer[i +
|
||||
21]) << "," << BuffToShort(&buffer[i + 23]) << "," <<
|
||||
BuffToShort(&buffer[i + 25]) << "," <<
|
||||
BuffToShort(&buffer[i + 27]) << std::endl; */
|
||||
|
||||
// Convert scaled sensor data to SI units
|
||||
gyro_rate_x_si = gyro_rate_x * deg_to_rad;
|
||||
gyro_rate_y_si = gyro_rate_y * deg_to_rad;
|
||||
@@ -609,22 +589,6 @@ void ADIS16448_IMU::Acquire() {
|
||||
}
|
||||
}
|
||||
m_first_run = false;
|
||||
} else {
|
||||
/*
|
||||
// Print notification when crc fails and bad data is rejected
|
||||
std::cout << "IMU Data CRC Mismatch Detected." << std::endl;
|
||||
std::cout << "Calculated CRC: " << calc_crc << std::endl;
|
||||
std::cout << "Read CRC: " << imu_crc << std::endl;
|
||||
// DEBUG: Plot sub-array data in terminal
|
||||
std::cout << BuffToUShort(&buffer[i + 3]) << "," <<
|
||||
BuffToUShort(&buffer[i + 5]) << "," << BuffToUShort(&buffer[i + 7]) <<
|
||||
"," << BuffToUShort(&buffer[i + 9]) << "," << BuffToUShort(&buffer[i +
|
||||
11]) << "," << BuffToUShort(&buffer[i + 13]) << "," <<
|
||||
BuffToUShort(&buffer[i + 15]) << "," << BuffToUShort(&buffer[i + 17])
|
||||
<< "," << BuffToUShort(&buffer[i + 19]) << "," <<
|
||||
BuffToUShort(&buffer[i + 21]) << "," << BuffToUShort(&buffer[i + 23])
|
||||
<< "," << BuffToUShort(&buffer[i + 25]) << "," <<
|
||||
BuffToUShort(&buffer[i + 27]) << std::endl; */
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
#include <frc/Timer.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include <hal/HAL.h>
|
||||
@@ -171,7 +170,6 @@ bool ADIS16470_IMU::SwitchToStandardSPI() {
|
||||
while (!m_thread_idle) {
|
||||
Wait(10_ms);
|
||||
}
|
||||
std::cout << "Paused the IMU processing thread successfully!" << std::endl;
|
||||
// Maybe we're in auto SPI mode? If so, kill auto SPI, and then SPI.
|
||||
if (m_spi != nullptr && m_auto_configured) {
|
||||
m_spi->StopAuto();
|
||||
@@ -190,12 +188,10 @@ bool ADIS16470_IMU::SwitchToStandardSPI() {
|
||||
/*Get the reamining data count */
|
||||
data_count = m_spi->ReadAutoReceivedData(trashBuffer, 0, 0_s);
|
||||
}
|
||||
std::cout << "Paused the auto SPI successfully!" << std::endl;
|
||||
}
|
||||
}
|
||||
// There doesn't seem to be a SPI port active. Let's try to set one up
|
||||
if (m_spi == nullptr) {
|
||||
std::cout << "Setting up a new SPI port." << std::endl;
|
||||
m_spi = new SPI(m_spi_port);
|
||||
m_spi->SetClockRate(2000000);
|
||||
m_spi->SetMSBFirst();
|
||||
@@ -284,11 +280,9 @@ bool ADIS16470_IMU::SwitchToAutoSPI() {
|
||||
m_first_run = true;
|
||||
m_thread_active = true;
|
||||
m_acquire_task = std::thread(&ADIS16470_IMU::Acquire, this);
|
||||
std::cout << "New IMU Processing thread activated!" << std::endl;
|
||||
} else {
|
||||
m_first_run = true;
|
||||
m_thread_active = true;
|
||||
std::cout << "Old IMU Processing thread re-activated!" << std::endl;
|
||||
}
|
||||
// Looks like the thread didn't start for some reason. Abort.
|
||||
/*
|
||||
@@ -465,7 +459,6 @@ void ADIS16470_IMU::Close() {
|
||||
}
|
||||
m_spi = nullptr;
|
||||
}
|
||||
std::cout << "Finished cleaning up after the IMU driver." << std::endl;
|
||||
}
|
||||
|
||||
ADIS16470_IMU::~ADIS16470_IMU() {
|
||||
@@ -547,18 +540,6 @@ void ADIS16470_IMU::Acquire() {
|
||||
buffer, data_to_read,
|
||||
0_s); // Read data from DMA buffer (only complete sets)
|
||||
|
||||
/*
|
||||
// DEBUG: Print buffer size and contents to terminal
|
||||
std::cout << "Start - " << data_count << "," << data_remainder << "," <<
|
||||
data_to_read << std::endl; for (int m = 0; m < data_to_read - 1; m++ )
|
||||
{
|
||||
std::cout << buffer[m] << ",";
|
||||
}
|
||||
std::cout << " " << std::endl;
|
||||
std::cout << "End" << std::endl;
|
||||
std::cout << "Reading " << data_count << " bytes." << std::endl;
|
||||
*/
|
||||
|
||||
// Could be multiple data sets in the buffer. Handle each one.
|
||||
for (int i = 0; i < data_to_read; i += dataset_len) {
|
||||
// Timestamp is at buffer[i]
|
||||
@@ -585,12 +566,6 @@ void ADIS16470_IMU::Acquire() {
|
||||
// Store timestamp for next iteration
|
||||
previous_timestamp = buffer[i];
|
||||
|
||||
/*
|
||||
// DEBUG: Print timestamp and delta values
|
||||
std::cout << previous_timestamp << "," << delta_x << "," << delta_y <<
|
||||
"," << delta_z << std::endl;
|
||||
*/
|
||||
|
||||
m_alpha = m_tau / (m_tau + m_dt);
|
||||
|
||||
if (m_first_run) {
|
||||
@@ -614,9 +589,6 @@ void ADIS16470_IMU::Acquire() {
|
||||
CompFilterProcess(compAngleY, accelAngleY, gyro_rate_x_si);
|
||||
}
|
||||
|
||||
// DEBUG: Print accumulated values
|
||||
// std::cout << m_compAngleX << "," << m_compAngleY << std::endl;
|
||||
|
||||
{
|
||||
std::scoped_lock sync(m_mutex);
|
||||
/* Push data to global variables */
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "frc/AnalogInput.h"
|
||||
#include "frc/Counter.h"
|
||||
#include "frc/Errors.h"
|
||||
#include "frc/RobotController.h"
|
||||
|
||||
using namespace frc;
|
||||
|
||||
@@ -42,6 +43,8 @@ void AnalogEncoder::Init() {
|
||||
|
||||
if (m_simDevice) {
|
||||
m_simPosition = m_simDevice.CreateDouble("Position", false, 0.0);
|
||||
m_simAbsolutePosition =
|
||||
m_simDevice.CreateDouble("absPosition", hal::SimDevice::kInput, 0.0);
|
||||
}
|
||||
|
||||
m_analogTrigger.SetLimitsVoltage(1.25, 3.75);
|
||||
@@ -54,6 +57,11 @@ void AnalogEncoder::Init() {
|
||||
m_analogInput->GetChannel());
|
||||
}
|
||||
|
||||
static bool DoubleEquals(double a, double b) {
|
||||
constexpr double epsilon = 0.00001;
|
||||
return std::abs(a - b) < epsilon;
|
||||
}
|
||||
|
||||
units::turn_t AnalogEncoder::Get() const {
|
||||
if (m_simPosition) {
|
||||
return units::turn_t{m_simPosition.Get()};
|
||||
@@ -66,7 +74,8 @@ units::turn_t AnalogEncoder::Get() const {
|
||||
auto pos = m_analogInput->GetVoltage();
|
||||
auto counter2 = m_counter.Get();
|
||||
auto pos2 = m_analogInput->GetVoltage();
|
||||
if (counter == counter2 && pos == pos2) {
|
||||
if (counter == counter2 && DoubleEquals(pos, pos2)) {
|
||||
pos = pos / frc::RobotController::GetVoltage5V();
|
||||
units::turn_t turns{counter + pos - m_positionOffset};
|
||||
m_lastPosition = turns;
|
||||
return turns;
|
||||
@@ -80,10 +89,22 @@ units::turn_t AnalogEncoder::Get() const {
|
||||
return m_lastPosition;
|
||||
}
|
||||
|
||||
double AnalogEncoder::GetAbsolutePosition() const {
|
||||
if (m_simAbsolutePosition) {
|
||||
return m_simAbsolutePosition.Get();
|
||||
}
|
||||
|
||||
return m_analogInput->GetVoltage() / frc::RobotController::GetVoltage5V();
|
||||
}
|
||||
|
||||
double AnalogEncoder::GetPositionOffset() const {
|
||||
return m_positionOffset;
|
||||
}
|
||||
|
||||
void AnalogEncoder::SetPositionOffset(double offset) {
|
||||
m_positionOffset = std::clamp(offset, 0.0, 1.0);
|
||||
}
|
||||
|
||||
void AnalogEncoder::SetDistancePerRotation(double distancePerRotation) {
|
||||
m_distancePerRotation = distancePerRotation;
|
||||
}
|
||||
@@ -98,7 +119,8 @@ double AnalogEncoder::GetDistance() const {
|
||||
|
||||
void AnalogEncoder::Reset() {
|
||||
m_counter.Reset();
|
||||
m_positionOffset = m_analogInput->GetVoltage();
|
||||
m_positionOffset =
|
||||
m_analogInput->GetVoltage() / frc::RobotController::GetVoltage5V();
|
||||
}
|
||||
|
||||
int AnalogEncoder::GetChannel() const {
|
||||
|
||||
311
wpilibc/src/main/native/cpp/DataLogManager.cpp
Normal file
@@ -0,0 +1,311 @@
|
||||
// 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 "frc/DataLogManager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <ctime>
|
||||
#include <random>
|
||||
|
||||
#include <fmt/chrono.h>
|
||||
#include <fmt/format.h>
|
||||
#include <networktables/NetworkTableInstance.h>
|
||||
#include <wpi/DataLog.h>
|
||||
#include <wpi/SafeThread.h>
|
||||
#include <wpi/StringExtras.h>
|
||||
#include <wpi/fs.h>
|
||||
#include <wpi/timestamp.h>
|
||||
|
||||
#include "frc/DriverStation.h"
|
||||
#include "frc/Filesystem.h"
|
||||
|
||||
using namespace frc;
|
||||
|
||||
namespace {
|
||||
|
||||
struct Thread final : public wpi::SafeThread {
|
||||
Thread(std::string_view dir, std::string_view filename, double period);
|
||||
|
||||
void Main() final;
|
||||
|
||||
void StartNTLog();
|
||||
void StopNTLog();
|
||||
|
||||
std::string m_logDir;
|
||||
bool m_filenameOverride;
|
||||
wpi::log::DataLog m_log;
|
||||
bool m_ntLoggerEnabled = false;
|
||||
NT_DataLogger m_ntEntryLogger = 0;
|
||||
NT_ConnectionDataLogger m_ntConnLogger = 0;
|
||||
wpi::log::StringLogEntry m_messageLog;
|
||||
};
|
||||
|
||||
struct Instance {
|
||||
Instance(std::string_view dir, std::string_view filename, double period);
|
||||
wpi::SafeThreadOwner<Thread> owner;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// if less than this much free space, delete log files until there is this much
|
||||
// free space OR there are this many files remaining.
|
||||
static constexpr uintmax_t kFreeSpaceThreshold = 50000000;
|
||||
static constexpr int kFileCountThreshold = 10;
|
||||
|
||||
static std::string MakeLogDir(std::string_view dir) {
|
||||
if (!dir.empty()) {
|
||||
return std::string{dir};
|
||||
}
|
||||
#ifdef __FRC_ROBORIO__
|
||||
// prefer a mounted USB drive if one is accessible
|
||||
constexpr std::string_view usbDir{"/u"};
|
||||
std::error_code ec;
|
||||
auto s = fs::status(usbDir, ec);
|
||||
if (!ec && fs::is_directory(s) &&
|
||||
(s.permissions() & fs::perms::others_write) != fs::perms::none) {
|
||||
return std::string{usbDir};
|
||||
}
|
||||
#endif
|
||||
return frc::filesystem::GetOperatingDirectory();
|
||||
}
|
||||
|
||||
static std::string MakeLogFilename(std::string_view filenameOverride) {
|
||||
if (!filenameOverride.empty()) {
|
||||
return std::string{filenameOverride};
|
||||
}
|
||||
static std::random_device dev;
|
||||
static std::mt19937 rng(dev());
|
||||
std::uniform_int_distribution<int> dist(0, 15);
|
||||
const char* v = "0123456789abcdef";
|
||||
std::string filename = "FRC_TBD_";
|
||||
for (int i = 0; i < 16; i++) {
|
||||
filename += v[dist(rng)];
|
||||
}
|
||||
filename += ".wpilog";
|
||||
return filename;
|
||||
}
|
||||
|
||||
Thread::Thread(std::string_view dir, std::string_view filename, double period)
|
||||
: m_logDir{dir},
|
||||
m_filenameOverride{!filename.empty()},
|
||||
m_log{dir, MakeLogFilename(filename), period},
|
||||
m_messageLog{m_log, "messages"} {
|
||||
StartNTLog();
|
||||
}
|
||||
|
||||
void Thread::Main() {
|
||||
// based on free disk space, scan for "old" FRC_*.wpilog files and remove
|
||||
{
|
||||
uintmax_t freeSpace = fs::space(m_logDir).free;
|
||||
if (freeSpace < kFreeSpaceThreshold) {
|
||||
// Delete oldest FRC_*.wpilog files (ignore FRC_TBD_*.wpilog as we just
|
||||
// created one)
|
||||
std::vector<fs::directory_entry> entries;
|
||||
std::error_code ec;
|
||||
for (auto&& entry : fs::directory_iterator{m_logDir, ec}) {
|
||||
auto stem = entry.path().stem().string();
|
||||
if (wpi::starts_with(stem, "FRC_") &&
|
||||
entry.path().extension() == ".wpilog" &&
|
||||
!wpi::starts_with(stem, "FRC_TBD_")) {
|
||||
entries.emplace_back(entry);
|
||||
}
|
||||
}
|
||||
std::sort(entries.begin(), entries.end(),
|
||||
[](const auto& a, const auto& b) {
|
||||
return a.last_write_time() < b.last_write_time();
|
||||
});
|
||||
|
||||
int count = entries.size();
|
||||
for (auto&& entry : entries) {
|
||||
--count;
|
||||
if (count < kFileCountThreshold) {
|
||||
break;
|
||||
}
|
||||
auto size = entry.file_size();
|
||||
if (fs::remove(entry.path(), ec)) {
|
||||
freeSpace += size;
|
||||
if (freeSpace >= kFreeSpaceThreshold) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
fmt::print(stderr, "DataLogManager: could not delete {}\n",
|
||||
entry.path().string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int timeoutCount = 0;
|
||||
bool paused = false;
|
||||
int dsAttachCount = 0;
|
||||
int fmsAttachCount = 0;
|
||||
bool dsRenamed = m_filenameOverride;
|
||||
bool fmsRenamed = m_filenameOverride;
|
||||
int sysTimeCount = 0;
|
||||
wpi::log::IntegerLogEntry sysTimeEntry{
|
||||
m_log, "systemTime",
|
||||
"{\"source\":\"DataLogManager\",\"format\":\"time_t_us\"}"};
|
||||
|
||||
for (;;) {
|
||||
bool newData = DriverStation::WaitForData(0.25_s);
|
||||
if (!m_active) {
|
||||
break;
|
||||
}
|
||||
if (!newData) {
|
||||
++timeoutCount;
|
||||
// pause logging after being disconnected for 10 seconds
|
||||
if (timeoutCount > 40 && !paused) {
|
||||
timeoutCount = 0;
|
||||
paused = true;
|
||||
m_log.Pause();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// when we connect to the DS, resume logging
|
||||
timeoutCount = 0;
|
||||
if (paused) {
|
||||
paused = false;
|
||||
m_log.Resume();
|
||||
}
|
||||
|
||||
if (!dsRenamed) {
|
||||
// track DS attach
|
||||
if (DriverStation::IsDSAttached()) {
|
||||
++dsAttachCount;
|
||||
} else {
|
||||
dsAttachCount = 0;
|
||||
}
|
||||
if (dsAttachCount > 50) { // 1 second
|
||||
std::time_t now = std::time(nullptr);
|
||||
auto tm = std::gmtime(&now);
|
||||
if (tm->tm_year > 100) {
|
||||
// assume local clock is now synchronized to DS, so rename based on
|
||||
// local time
|
||||
m_log.SetFilename(fmt::format("FRC_{:%Y%m%d_%H%M%S}.wpilog", *tm));
|
||||
dsRenamed = true;
|
||||
} else {
|
||||
dsAttachCount = 0; // wait a bit and try again
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!fmsRenamed) {
|
||||
// track FMS attach
|
||||
if (DriverStation::IsFMSAttached()) {
|
||||
++fmsAttachCount;
|
||||
} else {
|
||||
fmsAttachCount = 0;
|
||||
}
|
||||
if (fmsAttachCount > 100) { // 2 seconds
|
||||
// match info comes through TCP, so we need to double-check we've
|
||||
// actually received it
|
||||
auto matchType = DriverStation::GetMatchType();
|
||||
if (matchType != DriverStation::kNone) {
|
||||
// rename per match info
|
||||
char matchTypeChar;
|
||||
switch (matchType) {
|
||||
case DriverStation::kPractice:
|
||||
matchTypeChar = 'P';
|
||||
break;
|
||||
case DriverStation::kQualification:
|
||||
matchTypeChar = 'Q';
|
||||
break;
|
||||
case DriverStation::kElimination:
|
||||
matchTypeChar = 'E';
|
||||
break;
|
||||
default:
|
||||
matchTypeChar = '_';
|
||||
break;
|
||||
}
|
||||
std::time_t now = std::time(nullptr);
|
||||
m_log.SetFilename(
|
||||
fmt::format("FRC_{:%Y%m%d_%H%M%S}_{}_{}{}.wpilog",
|
||||
*std::gmtime(&now), DriverStation::GetEventName(),
|
||||
matchTypeChar, DriverStation::GetMatchNumber()));
|
||||
fmsRenamed = true;
|
||||
dsRenamed = true; // don't override FMS rename
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write system time every ~5 seconds
|
||||
++sysTimeCount;
|
||||
if (sysTimeCount >= 250) {
|
||||
sysTimeCount = 0;
|
||||
sysTimeEntry.Append(wpi::GetSystemTime(), wpi::Now());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Thread::StartNTLog() {
|
||||
if (!m_ntLoggerEnabled) {
|
||||
m_ntLoggerEnabled = true;
|
||||
auto inst = nt::NetworkTableInstance::GetDefault();
|
||||
m_ntEntryLogger = inst.StartEntryDataLog(m_log, "", "NT:");
|
||||
m_ntConnLogger = inst.StartConnectionDataLog(m_log, "NTConnection");
|
||||
}
|
||||
}
|
||||
|
||||
void Thread::StopNTLog() {
|
||||
if (m_ntLoggerEnabled) {
|
||||
m_ntLoggerEnabled = false;
|
||||
nt::NetworkTableInstance::StopEntryDataLog(m_ntEntryLogger);
|
||||
nt::NetworkTableInstance::StopConnectionDataLog(m_ntConnLogger);
|
||||
}
|
||||
}
|
||||
|
||||
Instance::Instance(std::string_view dir, std::string_view filename,
|
||||
double period) {
|
||||
// Delete all previously existing FRC_TBD_*.wpilog files. These only exist
|
||||
// when the robot never connects to the DS, so they are very unlikely to
|
||||
// have useful data and just clutter the filesystem.
|
||||
auto logDir = MakeLogDir(dir);
|
||||
std::error_code ec;
|
||||
for (auto&& entry : fs::directory_iterator{logDir, ec}) {
|
||||
if (wpi::starts_with(entry.path().stem().string(), "FRC_TBD_") &&
|
||||
entry.path().extension() == ".wpilog") {
|
||||
if (!fs::remove(entry, ec)) {
|
||||
fmt::print(stderr, "DataLogManager: could not delete {}\n",
|
||||
entry.path().string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
owner.Start(logDir, filename, period);
|
||||
}
|
||||
|
||||
static Instance& GetInstance(std::string_view dir = "",
|
||||
std::string_view filename = "",
|
||||
double period = 0.25) {
|
||||
static Instance instance(dir, filename, period);
|
||||
return instance;
|
||||
}
|
||||
|
||||
void DataLogManager::Start(std::string_view dir, std::string_view filename,
|
||||
double period) {
|
||||
GetInstance(dir, filename, period);
|
||||
}
|
||||
|
||||
void DataLogManager::Log(std::string_view message) {
|
||||
GetInstance().owner.GetThread()->m_messageLog.Append(message);
|
||||
fmt::print("{}\n", message);
|
||||
}
|
||||
|
||||
wpi::log::DataLog& DataLogManager::GetLog() {
|
||||
return GetInstance().owner.GetThread()->m_log;
|
||||
}
|
||||
|
||||
std::string DataLogManager::GetLogDir() {
|
||||
return GetInstance().owner.GetThread()->m_logDir;
|
||||
}
|
||||
|
||||
void DataLogManager::LogNetworkTables(bool enabled) {
|
||||
if (auto thr = GetInstance().owner.GetThread()) {
|
||||
if (enabled) {
|
||||
thr->StartNTLog();
|
||||
} else if (!enabled) {
|
||||
thr->StopNTLog();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
#include "frc/DigitalInput.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
|
||||
#include <hal/DIO.h>
|
||||
|
||||
@@ -22,8 +22,10 @@
|
||||
#include <networktables/NetworkTable.h>
|
||||
#include <networktables/NetworkTableEntry.h>
|
||||
#include <networktables/NetworkTableInstance.h>
|
||||
#include <wpi/DataLog.h>
|
||||
#include <wpi/condition_variable.h>
|
||||
#include <wpi/mutex.h>
|
||||
#include <wpi/timestamp.h>
|
||||
|
||||
#include "frc/Errors.h"
|
||||
#include "frc/MotorSafety.h"
|
||||
@@ -86,11 +88,48 @@ struct MatchDataSender {
|
||||
MatchDataSenderEntry<double> controlWord{table, "FMSControlData", 0.0};
|
||||
};
|
||||
|
||||
class JoystickLogSender {
|
||||
public:
|
||||
void Init(wpi::log::DataLog& log, unsigned int stick, int64_t timestamp);
|
||||
void Send(uint64_t timestamp);
|
||||
|
||||
private:
|
||||
void AppendButtons(HAL_JoystickButtons buttons, uint64_t timestamp);
|
||||
void AppendPOVs(const HAL_JoystickPOVs& povs, uint64_t timestamp);
|
||||
|
||||
unsigned int m_stick;
|
||||
HAL_JoystickButtons m_prevButtons;
|
||||
HAL_JoystickAxes m_prevAxes;
|
||||
HAL_JoystickPOVs m_prevPOVs;
|
||||
wpi::log::BooleanArrayLogEntry m_logButtons;
|
||||
wpi::log::FloatArrayLogEntry m_logAxes;
|
||||
wpi::log::IntegerArrayLogEntry m_logPOVs;
|
||||
};
|
||||
|
||||
class DataLogSender {
|
||||
public:
|
||||
void Init(wpi::log::DataLog& log, bool logJoysticks, int64_t timestamp);
|
||||
void Send(uint64_t timestamp);
|
||||
|
||||
private:
|
||||
std::atomic_bool m_initialized{false};
|
||||
|
||||
HAL_ControlWord m_prevControlWord;
|
||||
wpi::log::BooleanLogEntry m_logEnabled;
|
||||
wpi::log::BooleanLogEntry m_logAutonomous;
|
||||
wpi::log::BooleanLogEntry m_logTest;
|
||||
wpi::log::BooleanLogEntry m_logEstop;
|
||||
|
||||
bool m_logJoysticks;
|
||||
std::array<JoystickLogSender, DriverStation::kJoystickPorts> m_joysticks;
|
||||
};
|
||||
|
||||
struct Instance {
|
||||
Instance();
|
||||
~Instance();
|
||||
|
||||
MatchDataSender matchDataSender;
|
||||
std::atomic<DataLogSender*> dataLogSender{nullptr};
|
||||
|
||||
// Joystick button rising/falling edge flags
|
||||
wpi::mutex buttonEdgeMutex;
|
||||
@@ -188,6 +227,10 @@ Instance::~Instance() {
|
||||
// Trigger a DS mutex release in case there is no driver station running.
|
||||
HAL_ReleaseDSMutex();
|
||||
dsThread.join();
|
||||
|
||||
if (dataLogSender) {
|
||||
delete dataLogSender.load();
|
||||
}
|
||||
}
|
||||
|
||||
DriverStation& DriverStation::GetInstance() {
|
||||
@@ -678,6 +721,9 @@ void GetData() {
|
||||
|
||||
DriverStation::WakeupWaitForData();
|
||||
SendMatchData();
|
||||
if (auto sender = inst.dataLogSender.load()) {
|
||||
sender->Send(wpi::Now());
|
||||
}
|
||||
}
|
||||
|
||||
void DriverStation::SilenceJoystickConnectionWarning(bool silence) {
|
||||
@@ -688,6 +734,24 @@ bool DriverStation::IsJoystickConnectionWarningSilenced() {
|
||||
return !IsFMSAttached() && ::GetInstance().silenceJoystickWarning;
|
||||
}
|
||||
|
||||
void DriverStation::StartDataLog(wpi::log::DataLog& log, bool logJoysticks) {
|
||||
auto& inst = ::GetInstance();
|
||||
// Note: cannot safely replace, because we wouldn't know when to delete the
|
||||
// "old" one. Instead do a compare and exchange with nullptr. We check first
|
||||
// with a simple load to avoid the new in the common case.
|
||||
if (inst.dataLogSender.load()) {
|
||||
return;
|
||||
}
|
||||
DataLogSender* oldSender = nullptr;
|
||||
DataLogSender* newSender = new DataLogSender;
|
||||
inst.dataLogSender.compare_exchange_strong(oldSender, newSender);
|
||||
if (oldSender) {
|
||||
delete newSender; // already had a sender
|
||||
} else {
|
||||
newSender->Init(log, logJoysticks, wpi::Now());
|
||||
}
|
||||
}
|
||||
|
||||
void ReportJoystickUnpluggedErrorV(fmt::string_view format,
|
||||
fmt::format_args args) {
|
||||
auto& inst = GetInstance();
|
||||
@@ -793,3 +857,131 @@ void SendMatchData() {
|
||||
std::memcpy(&wordInt, &ctlWord, sizeof(wordInt));
|
||||
inst.matchDataSender.controlWord.Set(wordInt);
|
||||
}
|
||||
|
||||
void JoystickLogSender::Init(wpi::log::DataLog& log, unsigned int stick,
|
||||
int64_t timestamp) {
|
||||
m_stick = stick;
|
||||
|
||||
m_logButtons = wpi::log::BooleanArrayLogEntry{
|
||||
log, fmt::format("DS:joystick{}/buttons", stick), timestamp};
|
||||
m_logAxes = wpi::log::FloatArrayLogEntry{
|
||||
log, fmt::format("DS:joystick{}/axes", stick), timestamp};
|
||||
m_logPOVs = wpi::log::IntegerArrayLogEntry{
|
||||
log, fmt::format("DS:joystick{}/povs", stick), timestamp};
|
||||
|
||||
HAL_GetJoystickButtons(m_stick, &m_prevButtons);
|
||||
HAL_GetJoystickAxes(m_stick, &m_prevAxes);
|
||||
HAL_GetJoystickPOVs(m_stick, &m_prevPOVs);
|
||||
AppendButtons(m_prevButtons, timestamp);
|
||||
m_logAxes.Append(
|
||||
wpi::span<const float>{m_prevAxes.axes,
|
||||
static_cast<size_t>(m_prevAxes.count)},
|
||||
timestamp);
|
||||
AppendPOVs(m_prevPOVs, timestamp);
|
||||
}
|
||||
|
||||
void JoystickLogSender::Send(uint64_t timestamp) {
|
||||
HAL_JoystickButtons buttons;
|
||||
HAL_GetJoystickButtons(m_stick, &buttons);
|
||||
if (buttons.count != m_prevButtons.count ||
|
||||
buttons.buttons != m_prevButtons.buttons) {
|
||||
AppendButtons(buttons, timestamp);
|
||||
}
|
||||
m_prevButtons = buttons;
|
||||
|
||||
HAL_JoystickAxes axes;
|
||||
HAL_GetJoystickAxes(m_stick, &axes);
|
||||
if (axes.count != m_prevAxes.count ||
|
||||
std::memcmp(axes.axes, m_prevAxes.axes,
|
||||
sizeof(axes.axes[0]) * axes.count) != 0) {
|
||||
m_logAxes.Append(
|
||||
wpi::span<const float>{axes.axes, static_cast<size_t>(axes.count)},
|
||||
timestamp);
|
||||
}
|
||||
m_prevAxes = axes;
|
||||
|
||||
HAL_JoystickPOVs povs;
|
||||
HAL_GetJoystickPOVs(m_stick, &povs);
|
||||
if (povs.count != m_prevPOVs.count ||
|
||||
std::memcmp(povs.povs, m_prevPOVs.povs,
|
||||
sizeof(povs.povs[0]) * povs.count) != 0) {
|
||||
AppendPOVs(povs, timestamp);
|
||||
}
|
||||
m_prevPOVs = povs;
|
||||
}
|
||||
|
||||
void JoystickLogSender::AppendButtons(HAL_JoystickButtons buttons,
|
||||
uint64_t timestamp) {
|
||||
uint8_t buttonsArr[32];
|
||||
for (unsigned int i = 0; i < buttons.count; ++i) {
|
||||
buttonsArr[i] = (buttons.buttons & (1u << i)) != 0;
|
||||
}
|
||||
m_logButtons.Append(wpi::span<const uint8_t>{buttonsArr, buttons.count},
|
||||
timestamp);
|
||||
}
|
||||
|
||||
void JoystickLogSender::AppendPOVs(const HAL_JoystickPOVs& povs,
|
||||
uint64_t timestamp) {
|
||||
int64_t povsArr[HAL_kMaxJoystickPOVs];
|
||||
for (int i = 0; i < povs.count; ++i) {
|
||||
povsArr[i] = povs.povs[i];
|
||||
}
|
||||
m_logPOVs.Append(
|
||||
wpi::span<const int64_t>{povsArr, static_cast<size_t>(povs.count)},
|
||||
timestamp);
|
||||
}
|
||||
|
||||
void DataLogSender::Init(wpi::log::DataLog& log, bool logJoysticks,
|
||||
int64_t timestamp) {
|
||||
m_logEnabled = wpi::log::BooleanLogEntry{log, "DS:enabled", timestamp};
|
||||
m_logAutonomous = wpi::log::BooleanLogEntry{log, "DS:autonomous", timestamp};
|
||||
m_logTest = wpi::log::BooleanLogEntry{log, "DS:test", timestamp};
|
||||
m_logEstop = wpi::log::BooleanLogEntry{log, "DS:estop", timestamp};
|
||||
|
||||
// append initial control word values
|
||||
HAL_GetControlWord(&m_prevControlWord);
|
||||
m_logEnabled.Append(m_prevControlWord.enabled, timestamp);
|
||||
m_logAutonomous.Append(m_prevControlWord.autonomous, timestamp);
|
||||
m_logTest.Append(m_prevControlWord.test, timestamp);
|
||||
m_logEstop.Append(m_prevControlWord.eStop, timestamp);
|
||||
|
||||
m_logJoysticks = logJoysticks;
|
||||
if (logJoysticks) {
|
||||
unsigned int i = 0;
|
||||
for (auto&& joystick : m_joysticks) {
|
||||
joystick.Init(log, i++, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
m_initialized = true;
|
||||
}
|
||||
|
||||
void DataLogSender::Send(uint64_t timestamp) {
|
||||
if (!m_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// append control word value changes
|
||||
HAL_ControlWord ctlWord;
|
||||
HAL_GetControlWord(&ctlWord);
|
||||
if (ctlWord.enabled != m_prevControlWord.enabled) {
|
||||
m_logEnabled.Append(ctlWord.enabled, timestamp);
|
||||
}
|
||||
if (ctlWord.autonomous != m_prevControlWord.autonomous) {
|
||||
m_logAutonomous.Append(ctlWord.autonomous, timestamp);
|
||||
}
|
||||
if (ctlWord.test != m_prevControlWord.test) {
|
||||
m_logTest.Append(ctlWord.test, timestamp);
|
||||
}
|
||||
if (ctlWord.eStop != m_prevControlWord.eStop) {
|
||||
m_logEstop.Append(ctlWord.eStop, timestamp);
|
||||
}
|
||||
m_prevControlWord = ctlWord;
|
||||
|
||||
if (m_logJoysticks) {
|
||||
// append joystick value changes
|
||||
for (auto&& joystick : m_joysticks) {
|
||||
joystick.Send(timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,8 @@ void DutyCycleEncoder::Init() {
|
||||
m_simDevice.CreateDouble("position", hal::SimDevice::kInput, 0.0);
|
||||
m_simDistancePerRotation = m_simDevice.CreateDouble(
|
||||
"distance_per_rot", hal::SimDevice::kOutput, 1.0);
|
||||
m_simAbsolutePosition =
|
||||
m_simDevice.CreateDouble("absPosition", hal::SimDevice::kInput, 0.0);
|
||||
m_simIsConnected =
|
||||
m_simDevice.CreateBoolean("connected", hal::SimDevice::kInput, true);
|
||||
} else {
|
||||
@@ -76,6 +78,11 @@ void DutyCycleEncoder::Init() {
|
||||
m_dutyCycle->GetSourceChannel());
|
||||
}
|
||||
|
||||
static bool DoubleEquals(double a, double b) {
|
||||
constexpr double epsilon = 0.00001;
|
||||
return std::abs(a - b) < epsilon;
|
||||
}
|
||||
|
||||
units::turn_t DutyCycleEncoder::Get() const {
|
||||
if (m_simPosition) {
|
||||
return units::turn_t{m_simPosition.Get()};
|
||||
@@ -88,15 +95,9 @@ units::turn_t DutyCycleEncoder::Get() const {
|
||||
auto pos = m_dutyCycle->GetOutput();
|
||||
auto counter2 = m_counter->Get();
|
||||
auto pos2 = m_dutyCycle->GetOutput();
|
||||
if (counter == counter2 && pos == pos2) {
|
||||
if (counter == counter2 && DoubleEquals(pos, pos2)) {
|
||||
// map sensor range
|
||||
if (pos < m_sensorMin) {
|
||||
pos = m_sensorMin;
|
||||
}
|
||||
if (pos > m_sensorMax) {
|
||||
pos = m_sensorMax;
|
||||
}
|
||||
pos = (pos - m_sensorMin) / (m_sensorMax - m_sensorMin);
|
||||
pos = MapSensorRange(pos);
|
||||
units::turn_t turns{counter + pos - m_positionOffset};
|
||||
m_lastPosition = turns;
|
||||
return turns;
|
||||
@@ -110,6 +111,33 @@ units::turn_t DutyCycleEncoder::Get() const {
|
||||
return m_lastPosition;
|
||||
}
|
||||
|
||||
double DutyCycleEncoder::MapSensorRange(double pos) const {
|
||||
if (pos < m_sensorMin) {
|
||||
pos = m_sensorMin;
|
||||
}
|
||||
if (pos > m_sensorMax) {
|
||||
pos = m_sensorMax;
|
||||
}
|
||||
pos = (pos - m_sensorMin) / (m_sensorMax - m_sensorMin);
|
||||
return pos;
|
||||
}
|
||||
|
||||
double DutyCycleEncoder::GetAbsolutePosition() const {
|
||||
if (m_simAbsolutePosition) {
|
||||
return m_simAbsolutePosition.Get();
|
||||
}
|
||||
|
||||
return MapSensorRange(m_dutyCycle->GetOutput());
|
||||
}
|
||||
|
||||
double DutyCycleEncoder::GetPositionOffset() const {
|
||||
return m_positionOffset;
|
||||
}
|
||||
|
||||
void DutyCycleEncoder::SetPositionOffset(double offset) {
|
||||
m_positionOffset = std::clamp(offset, 0.0, 1.0);
|
||||
}
|
||||
|
||||
void DutyCycleEncoder::SetDutyCycleRange(double min, double max) {
|
||||
m_sensorMin = std::clamp(min, 0.0, 1.0);
|
||||
m_sensorMax = std::clamp(max, 0.0, 1.0);
|
||||
|
||||
@@ -17,6 +17,15 @@ using namespace frc;
|
||||
static wpi::SmallPtrSet<MotorSafety*, 32> instanceList;
|
||||
static wpi::mutex listMutex;
|
||||
|
||||
#ifndef __FRC_ROBORIO__
|
||||
namespace frc::impl {
|
||||
void ResetMotorSafety() {
|
||||
std::scoped_lock lock(listMutex);
|
||||
instanceList.clear();
|
||||
}
|
||||
} // namespace frc::impl
|
||||
#endif
|
||||
|
||||
MotorSafety::MotorSafety() {
|
||||
std::scoped_lock lock(listMutex);
|
||||
instanceList.insert(this);
|
||||
@@ -89,7 +98,15 @@ void MotorSafety::Check() {
|
||||
if (stopTime < Timer::GetFPGATimestamp()) {
|
||||
FRC_ReportError(err::Timeout, "{}... Output not updated often enough",
|
||||
GetDescription());
|
||||
StopMotor();
|
||||
|
||||
try {
|
||||
StopMotor();
|
||||
} catch (frc::RuntimeError& e) {
|
||||
e.Report();
|
||||
} catch (std::exception& e) {
|
||||
FRC_ReportError(err::Error, "{} StopMotor threw unexpected exception: {}",
|
||||
GetDescription(), e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,14 @@ static Instance& GetInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
#ifndef __FRC_ROBORIO__
|
||||
namespace frc::impl {
|
||||
void ResetPreferencesInstance() {
|
||||
GetInstance() = Instance();
|
||||
}
|
||||
} // namespace frc::impl
|
||||
#endif
|
||||
|
||||
Preferences* Preferences::GetInstance() {
|
||||
::GetInstance();
|
||||
static Preferences instance;
|
||||
@@ -79,6 +87,7 @@ void Preferences::PutString(std::string_view key, std::string_view value) {
|
||||
void Preferences::InitString(std::string_view key, std::string_view value) {
|
||||
auto entry = ::GetInstance().table->GetEntry(key);
|
||||
entry.SetDefaultString(value);
|
||||
entry.SetPersistent();
|
||||
}
|
||||
|
||||
void Preferences::SetInt(std::string_view key, int value) {
|
||||
@@ -94,6 +103,7 @@ void Preferences::PutInt(std::string_view key, int value) {
|
||||
void Preferences::InitInt(std::string_view key, int value) {
|
||||
auto entry = ::GetInstance().table->GetEntry(key);
|
||||
entry.SetDefaultDouble(value);
|
||||
entry.SetPersistent();
|
||||
}
|
||||
|
||||
void Preferences::SetDouble(std::string_view key, double value) {
|
||||
@@ -109,6 +119,7 @@ void Preferences::PutDouble(std::string_view key, double value) {
|
||||
void Preferences::InitDouble(std::string_view key, double value) {
|
||||
auto entry = ::GetInstance().table->GetEntry(key);
|
||||
entry.SetDefaultDouble(value);
|
||||
entry.SetPersistent();
|
||||
}
|
||||
|
||||
void Preferences::SetFloat(std::string_view key, float value) {
|
||||
@@ -124,6 +135,7 @@ void Preferences::PutFloat(std::string_view key, float value) {
|
||||
void Preferences::InitFloat(std::string_view key, float value) {
|
||||
auto entry = ::GetInstance().table->GetEntry(key);
|
||||
entry.SetDefaultDouble(value);
|
||||
entry.SetPersistent();
|
||||
}
|
||||
|
||||
void Preferences::SetBoolean(std::string_view key, bool value) {
|
||||
@@ -139,6 +151,7 @@ void Preferences::PutBoolean(std::string_view key, bool value) {
|
||||
void Preferences::InitBoolean(std::string_view key, bool value) {
|
||||
auto entry = ::GetInstance().table->GetEntry(key);
|
||||
entry.SetDefaultBoolean(value);
|
||||
entry.SetPersistent();
|
||||
}
|
||||
|
||||
void Preferences::SetLong(std::string_view key, int64_t value) {
|
||||
@@ -154,6 +167,7 @@ void Preferences::PutLong(std::string_view key, int64_t value) {
|
||||
void Preferences::InitLong(std::string_view key, int64_t value) {
|
||||
auto entry = ::GetInstance().table->GetEntry(key);
|
||||
entry.SetDefaultDouble(value);
|
||||
entry.SetPersistent();
|
||||
}
|
||||
|
||||
bool Preferences::ContainsKey(std::string_view key) {
|
||||
|
||||