Compare commits

...

75 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
Thad House
fcf23fc9e9 [hal] Fix potential gamedata out of bounds read (#3983)
The size was uninitialized.  If the size is smaller than the data,
NetComm just updates the size and does not initialize the data.
2022-02-01 22:22:48 -08:00
Jan-Felix Abellera
af5ef510c5 [wpilibc] Fix REV PH pulse duration units (#3982) 2022-02-01 20:28:48 -08:00
Jan-Felix Abellera
05401e2b81 [wpilib] Write REV PH firmware version to roboRIO to display on driver station (#3977) 2022-02-01 20:27:43 -08:00
Thad House
9fde0110b6 Update to 2022 v4.0 image (#3944) 2022-01-31 23:26:05 -08:00
sciencewhiz
b03f8ddb2e [examples] fix incorrect variable in Arm Simulation Pref (#3980) 2022-01-31 22:01:31 -08:00
sciencewhiz
a26df2a022 [examples] Update ArmSimulation example to use Preferences (#3976)
This shows more real world usage then hardcoding the setpoint and PID
gains. There were no current examples using Preferences. This will also
be used to update frc-docs article for Preferences.
2022-01-31 00:17:04 -08:00
Oblarg
d68d6674e8 [examples] Armbot: rename kCos to kG (#3975) 2022-01-31 00:16:26 -08:00
sciencewhiz
a8f0f6bb90 [wpilibj] Fix ADIS16448 getRate to return rate instead of angle (#3974) 2022-01-29 20:17:57 -08:00
Thad House
dd9c92d5bf [build] Remove debug info from examples (#3971)
They take up a LOT of disk space.
2022-01-27 20:59:13 -08:00
Thad House
84df14dd70 [rtns] Fix icons (#3972) 2022-01-27 20:58:07 -08:00
sciencewhiz
560094ad92 [examples] Correct Mecanum example axes (#3955) 2022-01-27 20:57:41 -08:00
Jan-Felix Abellera
7ea1be9c01 [wpilibc] Fix typo in hardware version for REV PDH (#3969) 2022-01-27 17:54:38 -08:00
Jan-Felix Abellera
700f13bffd [wpilibj] Make methods public for Java REV PDH (#3970) 2022-01-27 17:54:14 -08:00
Jan-Felix Abellera
b6aa7c1aa9 [wpilibj] Make methods public for Java REVPH (#3968) 2022-01-27 17:53:45 -08:00
Tyler Veness
eb4d183e48 [wpimath] Fix clang-tidy bugprone-integer-division warning (#3966)
The integer conversion is deliberate.
2022-01-26 18:38:45 -08:00
Thad House
77e4e81e1e [wpilib] Add Field widget to BuiltInWidgets in shuffleboard (#3961) 2022-01-24 20:33:11 -08:00
Thad House
88f5cb6eb0 [build] Publish PDBs with C++ tools (#3960) 2022-01-24 20:32:17 -08:00
Tyler Veness
efae552f3e [wpimath] Remove DifferentialDriveKinematics include from odometry (#3958) 2022-01-24 16:02:00 -08:00
sciencewhiz
46b277421a [glass] Update Speed Controller Type name for 2022 WPILib (#3952) 2022-01-21 21:30:44 -08:00
modelmat
42908126b9 [wpilib] Add DCMotorSim (#3910) 2022-01-21 20:42:06 -08:00
Peter Johnson
a467392cbd [wpiutil] StackTrace: Add ability to override default implementation (#3951) 2022-01-21 17:22:41 -08:00
modelmat
78d0bcf49d [templates] Add SimulationInit()/SimulationPeriodic() to robot templates (#3943) 2022-01-21 16:23:46 -08:00
sciencewhiz
02a0ced9b0 [wpilib] MecanumDrive: update docs for axis to match implementation (NFC) (#3942)
Added note that implementation may change in the future, #3930.
2022-01-21 16:22:17 -08:00
shueja-personal
4ccfe1c9f2 [wpilib] Added docs clarification on units for drive class WheelSpeeds (NFC) (#3939) 2022-01-21 15:51:28 -08:00
Peter Johnson
830c0c5c2f [wpilib] MechanismLigament2d: Add getters for color and line weight (#3947)
Also add missing locking in C++.
2022-01-21 15:47:44 -08:00
Peter Johnson
5548a37465 [wpilib] PowerDistribution: Add module type getter (#3948) 2022-01-21 15:46:44 -08:00
Thad House
2f9a600de2 [hal] Fix PCM one shot (#3949) 2022-01-21 15:46:08 -08:00
Thad House
559db11a20 [myRobot] Skip deploying debug libraries for myRobot deploys (#3950) 2022-01-21 15:45:47 -08:00
Lenny Abbas
76c78e295b [examples] Reorder SwerveModules in SwerveControllerCommand example odometry update (#3934) 2022-01-21 11:04:43 -08:00
sciencewhiz
debbd5ff4b [wpilib] Improve PowerDistribution docs (NFC) (#3925)
Add docs for switchable channel.
Use PDP/PDH appropriately and clarify differences.
Fix typos.
2022-01-20 23:33:01 -08:00
Tyler Veness
841174f302 [commands] Change command vendordep JSON version number to 1.0.0 (#3938)
While the number doesn't matter, it being old is confusing a lot of
people.  We never increment the internal vendordep versions, so using a year
version number was a poor choice.

Closes #3921.
2022-01-20 23:32:02 -08:00
sciencewhiz
8c55844f91 [wpilib] Remove comment about Mecanum right side inverted (NFC) (#3929) 2022-01-18 01:00:55 -08:00
Thad House
0b990bf0f5 [hal] Fix PCM sticky faults clear function crashing (#3932)
A call to the PCM clear function was using the wrong handle passed down to the CAN layer, causing an error.
2022-01-18 00:59:51 -08:00
Thad House
104d7e2abc [hal] Don't throw exceptions in PCM JNI (#3933)
Since the CAN bus can easily become disconnected, we don't want this to crash. Instead, we just want this to report errors. This matches previous behavior.
2022-01-18 00:58:26 -08:00
Lenny Abbas
5ba69e1af1 [examples] Updated type in Java SwerveModule (#3928)
Changed turnOutput from var to double in SwerveModule. It doesn't make sense for driveOutput and turnOutput to have different types so they should both be doubles.
2022-01-17 12:20:55 -08:00
Chirag Kaushik
f3a0b5c7d7 [wpimath] Fix Java SimpleMotorFeedforward Docs (NFC) (#3926) 2022-01-17 09:59:04 -08:00
276 changed files with 12741 additions and 560 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

@@ -130,6 +130,14 @@ model {
}
from(applicationPath)
if (binary.targetPlatform.operatingSystem.isWindows()) {
def exePath = binary.executable.file.absolutePath
exePath = exePath.substring(0, exePath.length() - 4)
def pdbPath = new File(exePath + '.pdb')
from(pdbPath)
}
into(nativeUtils.getPlatformPath(binary))
}

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

@@ -16,7 +16,7 @@
namespace glass {
class NTSpeedControllerModel : public SpeedControllerModel {
public:
static constexpr const char* kType = "Speed Controller";
static constexpr const char* kType = "Motor Controller";
explicit NTSpeedControllerModel(std::string_view path);
NTSpeedControllerModel(NT_Inst instance, std::string_view path);

View File

@@ -335,8 +335,13 @@ HAL_Bool HAL_GetCTREPCMSolenoidVoltageFault(HAL_CTREPCMHandle handle,
void HAL_ClearAllCTREPCMStickyFaults(HAL_CTREPCMHandle handle,
int32_t* status) {
auto pcm = pcmHandles->Get(handle);
if (pcm == nullptr) {
*status = HAL_HANDLE_ERROR;
return;
}
uint8_t controlData[] = {0, 0, 0, 0x80};
HAL_WriteCANPacket(handle, controlData, sizeof(controlData), Control2,
HAL_WriteCANPacket(pcm->canHandle, controlData, sizeof(controlData), Control2,
status);
}
@@ -393,7 +398,7 @@ void HAL_SetCTREPCMOneShotDuration(HAL_CTREPCMHandle handle, int32_t index,
(std::min)(static_cast<uint32_t>(durMs) / 10,
static_cast<uint32_t>(0xFF));
HAL_WriteCANPacketRepeating(pcm->canHandle, pcm->oneShot.sol10MsPerUnit, 8,
Control2, SendPeriod, status);
Control3, SendPeriod, status);
}
} // extern "C"

View File

@@ -110,10 +110,15 @@ static int32_t HAL_GetControlWordInternal(HAL_ControlWord* controlWord) {
static int32_t HAL_GetMatchInfoInternal(HAL_MatchInfo* info) {
MatchType_t matchType = MatchType_t::kMatchType_none;
info->gameSpecificMessageSize = sizeof(info->gameSpecificMessage);
int status = FRC_NetworkCommunication_getMatchInfo(
info->eventName, &matchType, &info->matchNumber, &info->replayNumber,
info->gameSpecificMessage, &info->gameSpecificMessageSize);
if (info->gameSpecificMessageSize > sizeof(info->gameSpecificMessage)) {
info->gameSpecificMessageSize = 0;
}
info->matchType = static_cast<HAL_MatchType>(matchType);
*(std::end(info->eventName) - 1) = '\0';

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

@@ -86,6 +86,9 @@ deploy {
myRobotCpp(NativeExecutableArtifact) {
libraryDirectory = '/usr/local/frc/third-party/lib'
def excludes = getLibraryFilter().getExcludes()
excludes.add('**/*.so.debug')
excludes.add('**/*.so.*.debug')
postdeploy << { ctx ->
ctx.execute('chmod +x myRobotCpp')
}

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

@@ -76,6 +76,14 @@ model {
}
from(applicationPath)
if (binary.targetPlatform.operatingSystem.isWindows()) {
def exePath = binary.executable.file.absolutePath
exePath = exePath.substring(0, exePath.length() - 4)
def pdbPath = new File(exePath + '.pdb')
from(pdbPath)
}
into(nativeUtils.getPlatformPath(binary))
}

View File

@@ -12,7 +12,7 @@ file(GLOB rtns_src src/main/native/cpp/*.cpp ${CMAKE_CURRENT_BINARY_DIR}/WPILibV
if (WIN32)
set(rtns_rc src/main/native/win/roborioteamnumbersetter.rc)
elseif(APPLE)
set(MACOSX_BUNDLE_ICON_FILE glass.icns)
set(MACOSX_BUNDLE_ICON_FILE rtns.icns)
set(APP_ICON_MACOSX src/main/native/mac/rtns.icns)
set_source_files_properties(${APP_ICON_MACOSX} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
endif()

View File

@@ -76,6 +76,14 @@ model {
}
from(applicationPath)
if (binary.targetPlatform.operatingSystem.isWindows()) {
def exePath = binary.executable.file.absolutePath
exePath = exePath.substring(0, exePath.length() - 4)
def pdbPath = new File(exePath + '.pdb')
from(pdbPath)
}
into(nativeUtils.getPlatformPath(binary))
}

View File

@@ -31,6 +31,16 @@ namespace gui = wpi::gui;
const char* GetWPILibVersion();
namespace rtns {
std::string_view GetResource_rtns_16_png();
std::string_view GetResource_rtns_32_png();
std::string_view GetResource_rtns_48_png();
std::string_view GetResource_rtns_64_png();
std::string_view GetResource_rtns_128_png();
std::string_view GetResource_rtns_256_png();
std::string_view GetResource_rtns_512_png();
} // namespace rtns
#define GLFWAPI extern "C"
GLFWAPI void glfwGetWindowSize(GLFWwindow* window, int* width, int* height);
#define GLFW_DONT_CARE -1
@@ -240,6 +250,15 @@ void Application(std::string_view saveDir) {
gui::CreateContext();
glass::CreateContext();
// Add icons
gui::AddIcon(rtns::GetResource_rtns_16_png());
gui::AddIcon(rtns::GetResource_rtns_32_png());
gui::AddIcon(rtns::GetResource_rtns_48_png());
gui::AddIcon(rtns::GetResource_rtns_64_png());
gui::AddIcon(rtns::GetResource_rtns_128_png());
gui::AddIcon(rtns::GetResource_rtns_256_png());
gui::AddIcon(rtns::GetResource_rtns_512_png());
glass::SetStorageName("roborioteamnumbersetter");
glass::SetStorageDir(saveDir.empty() ? gui::GetPlatformSaveFileDir()
: saveDir);

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

@@ -10,7 +10,7 @@ nativeUtils {
wpi {
configureDependencies {
wpiVersion = "-1"
niLibVersion = "2022.2.3"
niLibVersion = "2022.4.0"
opencvVersion = "4.5.2-1"
googleTestVersion = "1.9.0-5-437e100-1"
imguiVersion = "1.86-1"
@@ -21,7 +21,10 @@ nativeUtils {
nativeUtils.wpi.addWarnings()
nativeUtils.wpi.addWarningsAsErrors()
nativeUtils.wpi.addReleaseSymbolGeneration()
if (project.name != 'wpilibcExamples') {
nativeUtils.wpi.addReleaseSymbolGeneration()
}
nativeUtils.setSinglePrintPerPlatform()
nativeUtils.enableSourceLink()

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

@@ -1,7 +1,7 @@
{
"fileName": "WPILibNewCommands.json",
"name": "WPILib-New-Commands",
"version": "2020.0.0",
"version": "1.0.0",
"uuid": "111e20f7-815e-48f8-9dd6-e675ce75b266",
"mavenUrls": [],
"jsonUrl": "",

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

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