Compare commits

...

39 Commits

Author SHA1 Message Date
Peter Johnson
d66555e42f [datalogtool] Add datalogtool
This is a support tool for datalog file conversion (and eventually
download/remote datalog file management).
2022-02-26 09:49:34 -08:00
Peter Johnson
9f52d8a3b1 [wpilib] DriverStation: Add DataLog support for modes and joystick data 2022-02-26 09:49:34 -08:00
Peter Johnson
757ea91932 [wpilib] Add DataLogManager
This creates a default log file that captures NT changes and
automatically renames the log file based on time and match info.

DriverStation joystick logging will be implemented by the DriverStation
class instead.
2022-02-26 09:49:34 -08:00
Peter Johnson
02a804f1c5 [ntcore] Add DataLog support 2022-02-26 09:49:34 -08:00
Peter Johnson
9b500df0d9 [wpiutil] Add high speed data logging 2022-02-26 09:49:34 -08:00
Peter Johnson
5a89575b3a [wpiutil] Import customized LLVM MemoryBuffer 2022-02-26 09:49:34 -08:00
Peter Johnson
b8c4d7527b [wpiutil] Add MappedFileRegion 2022-02-26 09:49:34 -08:00
Alberto Jahuey Moncada
ac5d46cfa7 [wpilibc] Fix ProfiledPID SetTolerance default velocity value (#4054)
When trying to set the tolerance of a ProfiledPID, it fails if you don't give it a velocity value. It was missing a conversion from double to the appropiate unit.
2022-02-25 20:27:56 -08:00
Thad House
bc9e96e86f [wpilib] Absolute Encoder API and behavior fixes (#4052)
SetPositionOffset was added. Been requested multiple times, and easy to implement.

The javadocs mentioned GetPositionInRotation. It has tripped up many people how to get the absolute position from the encoder (You currently have to have precreated the DutyCycle object). Add this method (as GetAbsolutePostition) to make this easier to do.

The checks for making sure a matching set of values was read was doing direct double comparisions. This worked ok in the DutyCycle case, but has problems in the analog case. Solve this by using an epsilon comparison.

And finally, scale AnalogEncoders analog input to 0-1 instead of 0-5. This was reported a few years ago, but the issue was missed. This caused the encoder to count from 0-5, then 1-6, then 2-7 etc. This is solved and now works correctly.

Closes #3188
Closes #4046
Closes #4051

And fixes the following issue on CD
https://www.chiefdelphi.com/t/wpilib-analogencoder-java/372649
2022-02-24 22:45:15 -08:00
Dustin Spicuzza
f88c435dd0 [hal] Add mechanism to cancel all periodic callbacks (#4049) 2022-02-23 09:46:01 -08:00
Leonard Abbas
e4b91005cf [examples] Update SwerveModule constructor doc (NFC) (#4042)
Renamed "port" to "channel" for consistency.
2022-02-22 09:26:16 -08:00
Leonard Abbas
a260bfd83b [examples] Remove "this" keyword from SwerveModule (#4043) 2022-02-21 09:27:00 -08:00
Leonard Abbas
18e262a100 [examples] Fix multiple doc typos in SwerveControllerCommand example (NFC) (#4044) 2022-02-21 09:26:20 -08:00
Dustin Spicuzza
4bd1f526ab [wpilibc] Prevent StopMotor from terminating robot during MotorSafety check (#4038)
- Nothing else in that function can throw, so protecting StopMotor should be sufficient
- Fixes #4036
2022-02-19 20:42:10 -08:00
Dustin Spicuzza
27847d7eb2 [sim] Expose GUI control functions via HAL_RegisterExtension (#4034) 2022-02-19 20:40:25 -08:00
Dustin Spicuzza
b2a8d3f0f3 [wpilibc] Add mechanism to reset MotorSafety list (#4037) 2022-02-19 20:38:30 -08:00
Tyler Veness
49adac9564 [wpilib] Check for signedness in ArcadeDriveIK() (#4028)
If xSpeed == -0.0 and zRotation > 0, the algorithm assumes it's in the
third quadrant instead of the first since +0.0 == -0.0.

Also added tests for inverse kinematic functions, fixed some
MecanumDrive test bugs, and added Java MecanumDrive.driveCartesianIK()
and KilloughDrive.driveCartesianIK() overloads with defaulted gyro angle
that C++ already had.

Fixes #4022.
2022-02-17 18:03:59 -08:00
Peter Johnson
a19d1133b1 [wpiutil] libuv: Fix sign compare warnings in gcc 11.2 (#4031) 2022-02-13 16:56:53 -08:00
Peter Johnson
dde91717e4 [build] cmake: Add ability to customize target warnings (#4032) 2022-02-13 16:53:55 -08:00
Peter Johnson
e9050afd67 [sim] Update sim match time to match real robot (#4024)
The real robot has match time set to -1.0 until it's enabled, and then
counts down. Disabling the robot sets the time to -1.0.

The sim GUI has been updated to add preset buttons for auto and teleop
match times. The enable match timing checkbox has been removed as it's
no longer required.

The DS socket plugin has also been fixed to properly initialize
matchTime to -1.0 and reset it to -1.0 on disable.
2022-02-12 22:31:10 -08:00
sciencewhiz
165d2837cf [wpilib] Preferences: Set Persistent in Init methods (#4025)
Fixes #4018
2022-02-12 22:30:02 -08:00
Peter Johnson
ac7549edca [glass] Fix snprintf truncation warning (#4029) 2022-02-12 22:29:26 -08:00
Jonah Snider
4d96bc72e0 [wpilibj] Fix typos in error messages for non-null assertions (#4014) 2022-02-11 18:11:15 -08:00
Dustin Spicuzza
3411eee20f [hal] Replace hardcoded sim array sizes with constants (#4015) 2022-02-10 00:12:07 -08:00
Dustin Spicuzza
74de97eeca [wpilibc] Add mechanism to reset various global structures (#4007) 2022-02-09 22:14:12 -08:00
sciencewhiz
4e3cc25012 [examples] Fix periodic function rate comment (NFC) (#4013)
Fixes #3979
2022-02-08 13:19:31 -08:00
Dustin Spicuzza
90c1db393e [sim] Add exported functions to control the sim GUI (#3995) 2022-02-07 00:39:45 -08:00
sciencewhiz
2f43274aa4 [wpilibj] MechanismRoot2d: Add flush to setPosition (#4011)
Fixes #4010.
2022-02-06 22:47:33 -08:00
Peter Johnson
aeca09db09 [glass] Support remapping of Enter key (#3994)
This is useful for editing of values without disabling the DS.
2022-02-06 00:11:37 -08:00
Peter Johnson
c107f22c67 [sim] Sim GUI: don't force-show Timing and Other Devices (#4001)
Instead preserve their saved visible state.
2022-02-06 00:11:12 -08:00
Peter Johnson
68fe51e8da [wpigui] Update PFD to latest, fix kdialog multiselect (#4005) 2022-02-06 00:10:43 -08:00
modelmat
8d08d67cf1 [wpigui] PFD: Add console warning if file chooser unavailable (#4003)
Also remove iostream use.
2022-02-06 00:10:20 -08:00
Dustin Spicuzza
4f1782f66e [wpilibc] Only call HAL_Report when initializing SmartDashboard (#4006) 2022-02-06 00:07:55 -08:00
Tyler Veness
3f77725cd3 Remove uses of iostream (#4004)
Most of these were unused, the IMU ones were just debug messages.

The only one that wasn't removed is in portable-file-dialogs.cpp since
the replacement looks less trivial.
2022-02-05 23:00:31 -08:00
Peter Johnson
5635f33a32 [glass] Increase plot depth to 20K points (#3993)
2K was sufficient for simulation because it's possible to pause time,
but isn't quite enough for looking at real robot data. 20K points
is 400 seconds at 50 Hz which should make pausing plots much more
useful.

As every point is looped over, this does increase CPU utilization
somewhat but doesn't seem to have much of an impact for typical
use cases. Increasing this further will necessitate some greater
optimizations (e.g. an initial cull using binary search).
2022-02-04 22:20:38 -08:00
Peter Johnson
bca4b7111b [glass] Fix PlotSeries::SetSource() (#3991)
This can be called in a delayed manner, so it's possible for
m_size to already be at maximum, which results in writing past
the end of the array. Instead, just call AppendValue().
2022-02-04 20:47:11 -08:00
Oblarg
6a6366b0d6 [commands] Add until() as alias for withInterrupt() (#3981)
This is a clearer description for the functionality.
Will deprecate withInterrupt next year.
2022-02-03 22:14:52 -08:00
Thad House
16bf2c70c5 [wpilib] Fix joystick out of range error messages (#3988) 2022-02-03 22:10:44 -08:00
Thad House
4b3edb742c [wpilib] Fix ADIS16448 IMU default constructor not working in Java (#3989)
Also fixes a few related uninitialized variables in C++.
2022-02-03 22:09:12 -08:00
209 changed files with 11607 additions and 390 deletions

View File

@@ -262,6 +262,7 @@ if (WITH_GUI)
add_subdirectory(outlineviewer)
if (LIBSSH_FOUND)
add_subdirectory(roborioteamnumbersetter)
add_subdirectory(datalogtool)
endif()
endif()

View File

@@ -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
View 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
}

View 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
View 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
View 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
View 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()
}
}
}
}

View 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}";
}

View 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();
}

View 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);

View 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;
}

View 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;
};

View 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;
}
}
}

View 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;
};

View 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();
}
}

View 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;

View 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)};
}

View 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

View 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;
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

View File

@@ -0,0 +1 @@
IDI_ICON1 ICON "datalogtool.ico"

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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); });

View File

@@ -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

View File

@@ -14,7 +14,6 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <stdexcept>
#include <string>
#include <thread>

View File

@@ -48,4 +48,6 @@ int32_t HALSIM_RegisterSimPeriodicAfterCallback(
void HALSIM_CancelSimPeriodicAfterCallback(int32_t uid) {}
void HALSIM_CancelAllSimPeriodicCallbacks(void) {}
} // extern "C"

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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>

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;
};

View File

@@ -28,7 +28,9 @@ class Handle {
kLogger,
kLoggerPoller,
kRpcCall,
kRpcCallPoller
kRpcCallPoller,
kDataLogger,
kConnectionDataLogger
};
enum { kIndexMax = 0xfffff };

View File

@@ -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;
};

View File

@@ -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)

View File

@@ -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);

View File

@@ -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

View File

@@ -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
*/

View File

@@ -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

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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
* @{

View File

@@ -11,7 +11,6 @@
#include <sys/stat.h>
#include <algorithm>
#include <iostream>
#include <stdexcept>
#include <fmt/format.h>

View File

@@ -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'

View File

@@ -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);

View File

@@ -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

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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>(

View File

@@ -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

View 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

View File

@@ -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>

View File

@@ -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

View File

@@ -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.
*

View File

@@ -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");

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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");

View File

@@ -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)));

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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();

View File

@@ -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));

View File

@@ -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);

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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 */

View File

@@ -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 {

View 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();
}
}
}

View File

@@ -4,7 +4,6 @@
#include "frc/DigitalInput.h"
#include <iostream>
#include <limits>
#include <hal/DIO.h>

View File

@@ -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);
}
}
}

View File

@@ -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);

View File

@@ -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());
}
}
}

View File

@@ -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) {

Some files were not shown because too many files have changed in this diff Show More