From 2a5ca77454cbd528e32c23af6378969b9a1f2060 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Sat, 12 Sep 2020 10:55:46 -0700 Subject: [PATCH] [glass] Add glass: an application for display of robot data This reuses many pieces of the current simulation GUI. The common pieces have been refactored into the libglass library. The libglass library is designed to be usable for other standalone data visualization applications (e.g. viewing data logs). The name "glass" comes from "glass cockpit", as the application features several multi-function displays that can be adjusted to display robot information as needed. --- CMakeLists.txt | 1 + glass/CMakeLists.txt | 69 ++ glass/Info.plist | 30 + glass/build.gradle | 131 ++++ glass/publish.gradle | 55 ++ .../app/native/cpp/NetworkTablesSettings.cpp | 71 ++ .../app/native/cpp/NetworkTablesSettings.h | 35 + glass/src/app/native/cpp/main.cpp | 137 ++++ glass/src/lib/native/cpp/Context.cpp | 422 ++++++++++ glass/src/lib/native/cpp/DataSource.cpp | 142 ++++ glass/src/lib/native/cpp/MainMenuBar.cpp | 52 ++ .../src/lib/native/cpp/Model.cpp | 11 +- glass/src/lib/native/cpp/View.cpp | 30 + glass/src/lib/native/cpp/Window.cpp | 102 +++ glass/src/lib/native/cpp/WindowManager.cpp | 107 +++ .../lib/native/cpp/hardware/Accelerometer.cpp | 51 ++ .../lib/native/cpp/hardware/AnalogGyro.cpp | 41 + .../lib/native/cpp/hardware/AnalogInput.cpp | 66 ++ .../lib/native/cpp/hardware/AnalogOutput.cpp | 47 ++ glass/src/lib/native/cpp/hardware/DIO.cpp | 118 +++ glass/src/lib/native/cpp/hardware/Encoder.cpp | 165 ++++ .../lib/native/cpp/hardware/LEDDisplay.cpp | 91 +++ glass/src/lib/native/cpp/hardware/PCM.cpp | 150 ++++ glass/src/lib/native/cpp/hardware/PDP.cpp | 92 +++ glass/src/lib/native/cpp/hardware/PWM.cpp | 62 ++ glass/src/lib/native/cpp/hardware/Relay.cpp | 74 ++ glass/src/lib/native/cpp/hardware/RoboRio.cpp | 87 +++ glass/src/lib/native/cpp/other/DeviceTree.cpp | 161 ++++ glass/src/lib/native/cpp/other/FMS.cpp | 140 ++++ glass/src/lib/native/cpp/other/Field2D.cpp | 617 +++++++++++++++ .../src/lib/native/cpp/other/Plot.cpp | 734 +++++++++--------- .../lib/native/cpp/other/StringChooser.cpp | 44 ++ .../native/cpp/support}/ExtraGuiWidgets.cpp | 69 +- .../lib/native/cpp/support/IniSaverBase.cpp | 64 ++ .../lib/native/cpp/support}/IniSaverInfo.cpp | 40 +- glass/src/lib/native/include/glass/Context.h | 147 ++++ .../native/include/glass/ContextInternal.h | 49 ++ .../src/lib/native/include/glass/DataSource.h | 86 ++ .../lib/native/include/glass/MainMenuBar.h | 51 ++ .../src/lib/native/include/glass/Model.h | 16 +- glass/src/lib/native/include/glass/Provider.h | 171 ++++ .../src/lib/native/include/glass/Provider.inc | 90 +++ glass/src/lib/native/include/glass/View.h | 51 ++ glass/src/lib/native/include/glass/Window.h | 134 ++++ .../lib/native/include/glass/WindowManager.h | 141 ++++ .../include/glass/hardware/Accelerometer.h | 32 + .../include/glass/hardware/AnalogGyro.h | 36 + .../include/glass/hardware/AnalogInput.h | 39 + .../include/glass/hardware/AnalogOutput.h | 33 + .../lib/native/include/glass/hardware/DIO.h | 65 ++ .../native/include/glass/hardware/Encoder.h | 59 ++ .../include/glass/hardware/LEDDisplay.h | 47 ++ .../lib/native/include/glass/hardware/PCM.h | 62 ++ .../lib/native/include/glass/hardware/PDP.h | 41 + .../lib/native/include/glass/hardware/PWM.h | 39 + .../lib/native/include/glass/hardware/Relay.h | 38 + .../native/include/glass/hardware/RoboRio.h | 46 ++ .../native/include/glass/other/DeviceTree.h | 143 ++++ .../src/lib/native/include/glass/other/FMS.h | 56 ++ .../lib/native/include/glass/other/Field2D.h | 58 ++ .../src/lib/native/include/glass/other/Plot.h | 67 ++ .../include/glass/other/StringChooser.h | 35 + .../include/glass/support}/ExtraGuiWidgets.h | 18 +- .../native/include/glass/support}/IniSaver.h | 25 +- .../native/include/glass/support/IniSaver.inl | 40 + .../include/glass/support/IniSaverBase.h | 49 ++ .../include/glass/support}/IniSaverInfo.h | 12 +- .../include/glass/support}/IniSaverString.h | 39 +- .../include/glass/support/IniSaverString.inl | 38 + .../include/glass/support}/IniSaverVector.h | 26 +- .../include/glass/support/IniSaverVector.inl | 41 + glass/src/libnt/native/cpp/NTDigitalInput.cpp | 46 ++ .../src/libnt/native/cpp/NTDigitalOutput.cpp | 55 ++ glass/src/libnt/native/cpp/NTFMS.cpp | 93 +++ glass/src/libnt/native/cpp/NTField2D.cpp | 236 ++++++ .../src/libnt/native/cpp/NTStringChooser.cpp | 64 ++ glass/src/libnt/native/cpp/NetworkTables.cpp | 610 +++++++++++++++ .../libnt/native/cpp/NetworkTablesHelper.cpp | 22 + .../native/cpp/NetworkTablesProvider.cpp | 177 +++++ .../native/cpp/StandardNetworkTables.cpp | 73 ++ .../glass/networktables/NTDigitalInput.h | 56 ++ .../glass/networktables/NTDigitalOutput.h | 58 ++ .../include/glass/networktables/NTFMS.h | 72 ++ .../include/glass/networktables/NTField2D.h | 52 ++ .../glass/networktables/NTStringChooser.h | 57 ++ .../glass/networktables/NetworkTables.h | 123 +++ .../glass/networktables/NetworkTablesHelper.h | 58 ++ .../networktables/NetworkTablesProvider.h | 118 +++ settings.gradle | 1 + simulation/halsim_gui/CMakeLists.txt | 2 +- simulation/halsim_gui/build.gradle | 2 + .../src/main/native/cpp/AccelerometerGui.cpp | 75 -- .../main/native/cpp/AccelerometerSimGui.cpp | 67 ++ .../src/main/native/cpp/AccelerometerSimGui.h | 17 + .../src/main/native/cpp/AddressableLEDGui.cpp | 175 ++--- .../src/main/native/cpp/AnalogGyroGui.cpp | 80 -- .../src/main/native/cpp/AnalogGyroSimGui.cpp | 101 +++ ...{AccelerometerGui.h => AnalogGyroSimGui.h} | 4 +- .../src/main/native/cpp/AnalogInputGui.cpp | 102 --- .../src/main/native/cpp/AnalogInputGui.h | 17 - .../src/main/native/cpp/AnalogInputSimGui.cpp | 123 +++ .../{AnalogOutGui.h => AnalogInputSimGui.h} | 4 +- .../src/main/native/cpp/AnalogOutGui.cpp | 81 -- .../main/native/cpp/AnalogOutputSimGui.cpp | 96 +++ .../{AnalogGyroGui.h => AnalogOutputSimGui.h} | 4 +- .../src/main/native/cpp/CompressorGui.cpp | 104 --- .../halsim_gui/src/main/native/cpp/DIOGui.cpp | 214 ----- .../src/main/native/cpp/DIOSimGui.cpp | 244 ++++++ .../native/cpp/{EncoderGui.h => DIOSimGui.h} | 2 +- .../src/main/native/cpp/DriverStationGui.cpp | 299 ++++--- .../src/main/native/cpp/DriverStationGui.h | 13 +- .../src/main/native/cpp/EncoderGui.cpp | 286 ------- .../src/main/native/cpp/EncoderSimGui.cpp | 257 ++++++ .../src/main/native/cpp/EncoderSimGui.h | 22 + .../src/main/native/cpp/Field2D.cpp | 652 ---------------- .../src/main/native/cpp/GuiDataSource.cpp | 116 --- .../src/main/native/cpp/HALProvider.cpp | 91 +++ .../src/main/native/cpp/HALSimGui.cpp | 335 +------- .../src/main/native/cpp/Mechanism2D.cpp | 23 +- .../src/main/native/cpp/NetworkTablesGui.cpp | 409 ---------- .../main/native/cpp/NetworkTablesSimGui.cpp | 40 + .../cpp/{Field2D.h => NetworkTablesSimGui.h} | 3 +- .../src/main/native/cpp/PCMSimGui.cpp | 234 ++++++ .../main/native/cpp/{DIOGui.h => PCMSimGui.h} | 4 +- .../halsim_gui/src/main/native/cpp/PDPGui.cpp | 141 ---- .../src/main/native/cpp/PDPSimGui.cpp | 124 +++ .../main/native/cpp/{PDPGui.h => PDPSimGui.h} | 4 +- .../halsim_gui/src/main/native/cpp/PWMGui.cpp | 104 --- .../halsim_gui/src/main/native/cpp/PWMGui.h | 17 - .../src/main/native/cpp/PWMSimGui.cpp | 119 +++ .../src/main/native/cpp/PWMSimGui.h | 17 + .../src/main/native/cpp/RelayGui.cpp | 120 --- .../halsim_gui/src/main/native/cpp/RelayGui.h | 17 - .../src/main/native/cpp/RelaySimGui.cpp | 120 +++ .../src/main/native/cpp/RelaySimGui.h | 17 + .../src/main/native/cpp/RoboRioGui.cpp | 170 ---- .../src/main/native/cpp/RoboRioGui.h | 17 - .../src/main/native/cpp/RoboRioSimGui.cpp | 139 ++++ .../cpp/{CompressorGui.h => RoboRioSimGui.h} | 4 +- .../src/main/native/cpp/SimDeviceGui.cpp | 292 ++----- .../src/main/native/cpp/SolenoidGui.cpp | 130 ---- .../src/main/native/cpp/SolenoidGui.h | 17 - .../src/main/native/cpp/TimingGui.cpp | 24 +- .../halsim_gui/src/main/native/cpp/main.cpp | 125 ++- .../{GuiDataSource.h => HALDataSource.h} | 79 +- .../src/main/native/include/HALProvider.h | 50 ++ .../src/main/native/include/HALSimGui.h | 137 +--- .../src/main/native/include/IniSaver.inl | 56 -- .../main/native/include/IniSaverString.inl | 57 -- .../main/native/include/IniSaverVector.inl | 60 -- .../src/main/native/include/SimDeviceGui.h | 97 +-- 151 files changed, 10386 insertions(+), 4565 deletions(-) create mode 100644 glass/CMakeLists.txt create mode 100644 glass/Info.plist create mode 100644 glass/build.gradle create mode 100644 glass/publish.gradle create mode 100644 glass/src/app/native/cpp/NetworkTablesSettings.cpp create mode 100644 glass/src/app/native/cpp/NetworkTablesSettings.h create mode 100644 glass/src/app/native/cpp/main.cpp create mode 100644 glass/src/lib/native/cpp/Context.cpp create mode 100644 glass/src/lib/native/cpp/DataSource.cpp create mode 100644 glass/src/lib/native/cpp/MainMenuBar.cpp rename simulation/halsim_gui/src/main/native/cpp/PlotGui.h => glass/src/lib/native/cpp/Model.cpp (80%) create mode 100644 glass/src/lib/native/cpp/View.cpp create mode 100644 glass/src/lib/native/cpp/Window.cpp create mode 100644 glass/src/lib/native/cpp/WindowManager.cpp create mode 100644 glass/src/lib/native/cpp/hardware/Accelerometer.cpp create mode 100644 glass/src/lib/native/cpp/hardware/AnalogGyro.cpp create mode 100644 glass/src/lib/native/cpp/hardware/AnalogInput.cpp create mode 100644 glass/src/lib/native/cpp/hardware/AnalogOutput.cpp create mode 100644 glass/src/lib/native/cpp/hardware/DIO.cpp create mode 100644 glass/src/lib/native/cpp/hardware/Encoder.cpp create mode 100644 glass/src/lib/native/cpp/hardware/LEDDisplay.cpp create mode 100644 glass/src/lib/native/cpp/hardware/PCM.cpp create mode 100644 glass/src/lib/native/cpp/hardware/PDP.cpp create mode 100644 glass/src/lib/native/cpp/hardware/PWM.cpp create mode 100644 glass/src/lib/native/cpp/hardware/Relay.cpp create mode 100644 glass/src/lib/native/cpp/hardware/RoboRio.cpp create mode 100644 glass/src/lib/native/cpp/other/DeviceTree.cpp create mode 100644 glass/src/lib/native/cpp/other/FMS.cpp create mode 100644 glass/src/lib/native/cpp/other/Field2D.cpp rename simulation/halsim_gui/src/main/native/cpp/PlotGui.cpp => glass/src/lib/native/cpp/other/Plot.cpp (56%) create mode 100644 glass/src/lib/native/cpp/other/StringChooser.cpp rename {simulation/halsim_gui/src/main/native/cpp => glass/src/lib/native/cpp/support}/ExtraGuiWidgets.cpp (54%) create mode 100644 glass/src/lib/native/cpp/support/IniSaverBase.cpp rename {simulation/halsim_gui/src/main/native/cpp => glass/src/lib/native/cpp/support}/IniSaverInfo.cpp (79%) create mode 100644 glass/src/lib/native/include/glass/Context.h create mode 100644 glass/src/lib/native/include/glass/ContextInternal.h create mode 100644 glass/src/lib/native/include/glass/DataSource.h create mode 100644 glass/src/lib/native/include/glass/MainMenuBar.h rename simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.h => glass/src/lib/native/include/glass/Model.h (65%) create mode 100644 glass/src/lib/native/include/glass/Provider.h create mode 100644 glass/src/lib/native/include/glass/Provider.inc create mode 100644 glass/src/lib/native/include/glass/View.h create mode 100644 glass/src/lib/native/include/glass/Window.h create mode 100644 glass/src/lib/native/include/glass/WindowManager.h create mode 100644 glass/src/lib/native/include/glass/hardware/Accelerometer.h create mode 100644 glass/src/lib/native/include/glass/hardware/AnalogGyro.h create mode 100644 glass/src/lib/native/include/glass/hardware/AnalogInput.h create mode 100644 glass/src/lib/native/include/glass/hardware/AnalogOutput.h create mode 100644 glass/src/lib/native/include/glass/hardware/DIO.h create mode 100644 glass/src/lib/native/include/glass/hardware/Encoder.h create mode 100644 glass/src/lib/native/include/glass/hardware/LEDDisplay.h create mode 100644 glass/src/lib/native/include/glass/hardware/PCM.h create mode 100644 glass/src/lib/native/include/glass/hardware/PDP.h create mode 100644 glass/src/lib/native/include/glass/hardware/PWM.h create mode 100644 glass/src/lib/native/include/glass/hardware/Relay.h create mode 100644 glass/src/lib/native/include/glass/hardware/RoboRio.h create mode 100644 glass/src/lib/native/include/glass/other/DeviceTree.h create mode 100644 glass/src/lib/native/include/glass/other/FMS.h create mode 100644 glass/src/lib/native/include/glass/other/Field2D.h create mode 100644 glass/src/lib/native/include/glass/other/Plot.h create mode 100644 glass/src/lib/native/include/glass/other/StringChooser.h rename {simulation/halsim_gui/src/main/native/include => glass/src/lib/native/include/glass/support}/ExtraGuiWidgets.h (88%) rename {simulation/halsim_gui/src/main/native/include => glass/src/lib/native/include/glass/support}/IniSaver.h (64%) create mode 100644 glass/src/lib/native/include/glass/support/IniSaver.inl create mode 100644 glass/src/lib/native/include/glass/support/IniSaverBase.h rename {simulation/halsim_gui/src/main/native/include => glass/src/lib/native/include/glass/support}/IniSaverInfo.h (87%) rename {simulation/halsim_gui/src/main/native/include => glass/src/lib/native/include/glass/support}/IniSaverString.h (56%) create mode 100644 glass/src/lib/native/include/glass/support/IniSaverString.inl rename {simulation/halsim_gui/src/main/native/include => glass/src/lib/native/include/glass/support}/IniSaverVector.h (50%) create mode 100644 glass/src/lib/native/include/glass/support/IniSaverVector.inl create mode 100644 glass/src/libnt/native/cpp/NTDigitalInput.cpp create mode 100644 glass/src/libnt/native/cpp/NTDigitalOutput.cpp create mode 100644 glass/src/libnt/native/cpp/NTFMS.cpp create mode 100644 glass/src/libnt/native/cpp/NTField2D.cpp create mode 100644 glass/src/libnt/native/cpp/NTStringChooser.cpp create mode 100644 glass/src/libnt/native/cpp/NetworkTables.cpp create mode 100644 glass/src/libnt/native/cpp/NetworkTablesHelper.cpp create mode 100644 glass/src/libnt/native/cpp/NetworkTablesProvider.cpp create mode 100644 glass/src/libnt/native/cpp/StandardNetworkTables.cpp create mode 100644 glass/src/libnt/native/include/glass/networktables/NTDigitalInput.h create mode 100644 glass/src/libnt/native/include/glass/networktables/NTDigitalOutput.h create mode 100644 glass/src/libnt/native/include/glass/networktables/NTFMS.h create mode 100644 glass/src/libnt/native/include/glass/networktables/NTField2D.h create mode 100644 glass/src/libnt/native/include/glass/networktables/NTStringChooser.h create mode 100644 glass/src/libnt/native/include/glass/networktables/NetworkTables.h create mode 100644 glass/src/libnt/native/include/glass/networktables/NetworkTablesHelper.h create mode 100644 glass/src/libnt/native/include/glass/networktables/NetworkTablesProvider.h delete mode 100644 simulation/halsim_gui/src/main/native/cpp/AccelerometerGui.cpp create mode 100644 simulation/halsim_gui/src/main/native/cpp/AccelerometerSimGui.cpp create mode 100644 simulation/halsim_gui/src/main/native/cpp/AccelerometerSimGui.h delete mode 100644 simulation/halsim_gui/src/main/native/cpp/AnalogGyroGui.cpp create mode 100644 simulation/halsim_gui/src/main/native/cpp/AnalogGyroSimGui.cpp rename simulation/halsim_gui/src/main/native/cpp/{AccelerometerGui.h => AnalogGyroSimGui.h} (85%) delete mode 100644 simulation/halsim_gui/src/main/native/cpp/AnalogInputGui.cpp delete mode 100644 simulation/halsim_gui/src/main/native/cpp/AnalogInputGui.h create mode 100644 simulation/halsim_gui/src/main/native/cpp/AnalogInputSimGui.cpp rename simulation/halsim_gui/src/main/native/cpp/{AnalogOutGui.h => AnalogInputSimGui.h} (85%) delete mode 100644 simulation/halsim_gui/src/main/native/cpp/AnalogOutGui.cpp create mode 100644 simulation/halsim_gui/src/main/native/cpp/AnalogOutputSimGui.cpp rename simulation/halsim_gui/src/main/native/cpp/{AnalogGyroGui.h => AnalogOutputSimGui.h} (85%) delete mode 100644 simulation/halsim_gui/src/main/native/cpp/CompressorGui.cpp delete mode 100644 simulation/halsim_gui/src/main/native/cpp/DIOGui.cpp create mode 100644 simulation/halsim_gui/src/main/native/cpp/DIOSimGui.cpp rename simulation/halsim_gui/src/main/native/cpp/{EncoderGui.h => DIOSimGui.h} (96%) delete mode 100644 simulation/halsim_gui/src/main/native/cpp/EncoderGui.cpp create mode 100644 simulation/halsim_gui/src/main/native/cpp/EncoderSimGui.cpp create mode 100644 simulation/halsim_gui/src/main/native/cpp/EncoderSimGui.h delete mode 100644 simulation/halsim_gui/src/main/native/cpp/Field2D.cpp delete mode 100644 simulation/halsim_gui/src/main/native/cpp/GuiDataSource.cpp create mode 100644 simulation/halsim_gui/src/main/native/cpp/HALProvider.cpp delete mode 100644 simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.cpp create mode 100644 simulation/halsim_gui/src/main/native/cpp/NetworkTablesSimGui.cpp rename simulation/halsim_gui/src/main/native/cpp/{Field2D.h => NetworkTablesSimGui.h} (91%) create mode 100644 simulation/halsim_gui/src/main/native/cpp/PCMSimGui.cpp rename simulation/halsim_gui/src/main/native/cpp/{DIOGui.h => PCMSimGui.h} (86%) delete mode 100644 simulation/halsim_gui/src/main/native/cpp/PDPGui.cpp create mode 100644 simulation/halsim_gui/src/main/native/cpp/PDPSimGui.cpp rename simulation/halsim_gui/src/main/native/cpp/{PDPGui.h => PDPSimGui.h} (86%) delete mode 100644 simulation/halsim_gui/src/main/native/cpp/PWMGui.cpp delete mode 100644 simulation/halsim_gui/src/main/native/cpp/PWMGui.h create mode 100644 simulation/halsim_gui/src/main/native/cpp/PWMSimGui.cpp create mode 100644 simulation/halsim_gui/src/main/native/cpp/PWMSimGui.h delete mode 100644 simulation/halsim_gui/src/main/native/cpp/RelayGui.cpp delete mode 100644 simulation/halsim_gui/src/main/native/cpp/RelayGui.h create mode 100644 simulation/halsim_gui/src/main/native/cpp/RelaySimGui.cpp create mode 100644 simulation/halsim_gui/src/main/native/cpp/RelaySimGui.h delete mode 100644 simulation/halsim_gui/src/main/native/cpp/RoboRioGui.cpp delete mode 100644 simulation/halsim_gui/src/main/native/cpp/RoboRioGui.h create mode 100644 simulation/halsim_gui/src/main/native/cpp/RoboRioSimGui.cpp rename simulation/halsim_gui/src/main/native/cpp/{CompressorGui.h => RoboRioSimGui.h} (85%) delete mode 100644 simulation/halsim_gui/src/main/native/cpp/SolenoidGui.cpp delete mode 100644 simulation/halsim_gui/src/main/native/cpp/SolenoidGui.h rename simulation/halsim_gui/src/main/native/include/{GuiDataSource.h => HALDataSource.h} (75%) create mode 100644 simulation/halsim_gui/src/main/native/include/HALProvider.h delete mode 100644 simulation/halsim_gui/src/main/native/include/IniSaver.inl delete mode 100644 simulation/halsim_gui/src/main/native/include/IniSaverString.inl delete mode 100644 simulation/halsim_gui/src/main/native/include/IniSaverVector.inl diff --git a/CMakeLists.txt b/CMakeLists.txt index 86f8d30041..f0e6bc0884 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -186,6 +186,7 @@ endif() if (WITH_GUI) add_subdirectory(imgui) add_subdirectory(wpigui) + add_subdirectory(glass) endif() if (WITH_CSCORE) diff --git a/glass/CMakeLists.txt b/glass/CMakeLists.txt new file mode 100644 index 0000000000..f9ab9fae93 --- /dev/null +++ b/glass/CMakeLists.txt @@ -0,0 +1,69 @@ +project(glass) + +include(CompileWarnings) +include(LinkMacOSGUI) + +# +# libglass +# +file(GLOB_RECURSE libglass_src src/lib/native/cpp/*.cpp) + +add_library(libglass STATIC ${libglass_src}) +set_target_properties(libglass PROPERTIES DEBUG_POSTFIX "d" OUTPUT_NAME "glass") +set_property(TARGET libglass PROPERTY POSITION_INDEPENDENT_CODE ON) + +set_property(TARGET libglass PROPERTY FOLDER "libraries") + +wpilib_target_warnings(libglass) +target_link_libraries(libglass PUBLIC wpigui wpimath wpiutil) + +target_include_directories(libglass PUBLIC + $ + $) + +install(TARGETS libglass EXPORT libglass DESTINATION "${main_lib_dest}") +install(DIRECTORY src/lib/native/include/ DESTINATION "${include_dest}/glass") + +# +# libglassnt +# +file(GLOB_RECURSE libglassnt_src src/libnt/native/cpp/*.cpp) + +add_library(libglassnt STATIC ${libglassnt_src}) +set_target_properties(libglassnt PROPERTIES DEBUG_POSTFIX "d" OUTPUT_NAME "glassnt") +set_property(TARGET libglassnt PROPERTY POSITION_INDEPENDENT_CODE ON) + +set_property(TARGET libglassnt PROPERTY FOLDER "libraries") + +wpilib_target_warnings(libglassnt) +target_link_libraries(libglassnt PUBLIC ntcore libglass) + +target_include_directories(libglassnt PUBLIC + $ + $) + +install(TARGETS libglassnt EXPORT libglassnt DESTINATION "${main_lib_dest}") +install(DIRECTORY src/libnt/native/include/ DESTINATION "${include_dest}/glass") + +# +# glass application +# + +file(GLOB glass_src src/app/native/cpp/*.cpp) + +add_executable(glass ${glass_src}) +wpilib_link_macos_gui(glass) +target_link_libraries(glass libglassnt libglass) +if (WIN32) + set_target_properties(glass PROPERTIES WIN32_EXECUTABLE YES) +endif() + +#if (MSVC OR FLAT_INSTALL_WPILIB) +# set (wpigui_config_dir ${wpilib_dest}) +#else() +# set (wpigui_config_dir share/wpigui) +#endif() + +#configure_file(wpigui-config.cmake.in ${CMAKE_BINARY_DIR}/wpigui-config.cmake ) +#install(FILES ${CMAKE_BINARY_DIR}/wpigui-config.cmake DESTINATION ${wpigui_config_dir}) +#install(EXPORT wpigui DESTINATION ${wpigui_config_dir}) diff --git a/glass/Info.plist b/glass/Info.plist new file mode 100644 index 0000000000..0107192d0a --- /dev/null +++ b/glass/Info.plist @@ -0,0 +1,30 @@ + + + + + CFBundleName + Glass + CFBundleExecutable + glassApp + CFBundleDisplayName + Glass + CFBundleIdentifier + edu.wpi.first.tools.Glass + CFBundlePackageType + APPL + CFBundleSupportedPlatforms + + MacOSX + + CFBundleInfoDictionaryVersion + 6.0 + CFBundleShortVersionString + 2021 + CFBundleVersion + 2021 + LSMinimumSystemVersion + 10.11 + NSHighResolutionCapable + + + diff --git a/glass/build.gradle b/glass/build.gradle new file mode 100644 index 0000000000..c2bd46418f --- /dev/null +++ b/glass/build.gradle @@ -0,0 +1,131 @@ +if (!project.hasProperty('onlylinuxathena') && !project.hasProperty('onlylinuxraspbian') && !project.hasProperty('onlylinuxaarch64bionic')) { + + description = "A different kind of dashboard" + + apply plugin: 'cpp' + apply plugin: 'c' + apply plugin: 'google-test-test-suite' + apply plugin: 'visual-studio' + apply plugin: 'edu.wpi.first.NativeUtils' + + ext { + nativeName = 'glass' + } + + apply from: "${rootDir}/shared/config.gradle" + + project(':').libraryBuild.dependsOn build + + nativeUtils.exportsConfigs { + glass { + x86ExcludeSymbols = ['_CT??_R0?AV_System_error', '_CT??_R0?AVexception', '_CT??_R0?AVfailure', + '_CT??_R0?AVruntime_error', '_CT??_R0?AVsystem_error', '_CTA5?AVfailure', + '_TI5?AVfailure', '_CT??_R0?AVout_of_range', '_CTA3?AVout_of_range', + '_TI3?AVout_of_range', '_CT??_R0?AVbad_cast'] + x64ExcludeSymbols = ['_CT??_R0?AV_System_error', '_CT??_R0?AVexception', '_CT??_R0?AVfailure', + '_CT??_R0?AVruntime_error', '_CT??_R0?AVsystem_error', '_CTA5?AVfailure', + '_TI5?AVfailure', '_CT??_R0?AVout_of_range', '_CTA3?AVout_of_range', + '_TI3?AVout_of_range', '_CT??_R0?AVbad_cast'] + } + } + + model { + components { + "${nativeName}"(NativeLibrarySpec) { + sources { + cpp { + source { + srcDirs = ['src/lib/native/cpp'] + include '**/*.cpp' + } + exportedHeaders { + srcDirs 'src/lib/native/include' + } + } + } + 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 + } + if (it instanceof SharedLibraryBinarySpec) { + it.buildable = false + return + } + lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' + lib project: ':wpimath', library: 'wpimath', linkage: 'shared' + lib project: ':wpigui', library: 'wpigui', linkage: 'static' + nativeUtils.useRequiredLibrary(it, 'imgui_static') + } + appendDebugPathToBinaries(binaries) + } + "${nativeName}nt"(NativeLibrarySpec) { + sources { + cpp { + source { + srcDirs = ['src/libnt/native/cpp'] + include '**/*.cpp' + } + exportedHeaders { + srcDirs 'src/libnt/native/include' + } + } + } + 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 + } + if (it instanceof SharedLibraryBinarySpec) { + it.buildable = false + return + } + lib library: nativeName, linkage: 'static' + lib project: ':ntcore', library: 'ntcore', linkage: 'shared' + lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' + lib project: ':wpimath', library: 'wpimath', linkage: 'shared' + lib project: ':wpigui', library: 'wpigui', linkage: 'static' + nativeUtils.useRequiredLibrary(it, 'imgui_static') + } + appendDebugPathToBinaries(binaries) + } + // By default, a development executable will be generated. This is to help the case of + // testing specific functionality of the library. + "${nativeName}App"(NativeExecutableSpec) { + sources { + cpp { + source { + srcDirs 'src/app/native/cpp' + include '**/*.cpp' + } + exportedHeaders { + srcDirs 'src/app/native/include' + } + } + } + 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 + } + lib library: 'glassnt', linkage: 'static' + lib library: nativeName, linkage: 'static' + lib project: ':ntcore', library: 'ntcore', linkage: 'static' + lib project: ':wpiutil', library: 'wpiutil', linkage: 'static' + lib project: ':wpimath', library: 'wpimath', linkage: 'static' + lib project: ':wpigui', library: 'wpigui', linkage: 'static' + nativeUtils.useRequiredLibrary(it, 'imgui_static') + if (it.targetPlatform.operatingSystem.isWindows()) { + it.linker.args << 'Gdi32.lib' << 'Shell32.lib' << 'd3d11.lib' << 'd3dcompiler.lib' + } else if (it.targetPlatform.operatingSystem.isMacOsX()) { + it.linker.args << '-framework' << 'Metal' << '-framework' << 'MetalKit' << '-framework' << 'Cocoa' << '-framework' << 'IOKit' << '-framework' << 'CoreFoundation' << '-framework' << 'CoreVideo' << '-framework' << 'QuartzCore' + } else { + it.linker.args << '-lX11' + } + } + } + } + } + + apply from: 'publish.gradle' +} diff --git a/glass/publish.gradle b/glass/publish.gradle new file mode 100644 index 0000000000..e7a81715ca --- /dev/null +++ b/glass/publish.gradle @@ -0,0 +1,55 @@ +apply plugin: 'maven-publish' + +def baseArtifactId = 'Glass' +def artifactGroupId = 'edu.wpi.first.tools' +def zipBaseName = '_GROUP_edu_wpi_first_tools_ID_Glass_CLS' + +model { + publishing { + def tasks = [] + $.components.each { component -> + component.binaries.each { binary -> + if (binary in NativeExecutableBinarySpec && binary.application.name.contains("glassApp")) { + 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 + + // Create the ZIP. + def outputsFolder = file("$project.buildDir/outputs") + def task = project.tasks.create("copyGlassExecutable", Zip) { + description("Copies the Glass executable to the outputs directory.") + destinationDir(outputsFolder) + + archiveBaseName = '_M_' + zipBaseName + duplicatesStrategy = 'exclude' + classifier = binary.targetPlatform.name + binary.buildType.name + + from(licenseFile) { + into '/' + } + + from(applicationPath) + into(nativeUtils.getPlatformPath(binary)) + } + + task.dependsOn binary.tasks.link + tasks.add(task) + project.build.dependsOn task + project.artifacts { task } + addTaskToCopyAllOutputs(task) + } + } + } + } + + publications { + cpp(MavenPublication) { + tasks.each { artifact it } + artifactId = baseArtifactId + groupId = artifactGroupId + version wpilibVersioning.version.get() + } + } + } +} diff --git a/glass/src/app/native/cpp/NetworkTablesSettings.cpp b/glass/src/app/native/cpp/NetworkTablesSettings.cpp new file mode 100644 index 0000000000..7b2d4d205a --- /dev/null +++ b/glass/src/app/native/cpp/NetworkTablesSettings.cpp @@ -0,0 +1,71 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "NetworkTablesSettings.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "glass/Context.h" + +NetworkTablesSettings::NetworkTablesSettings(NT_Inst inst, + const char* storageName) + : m_inst{inst} { + auto& storage = glass::GetStorage(storageName); + m_pMode = storage.GetIntRef("mode"); + m_pIniName = storage.GetStringRef("iniName", "networktables.ini"); + m_pServerTeam = storage.GetStringRef("serverTeam"); + m_pListenAddress = storage.GetStringRef("listenAddress"); +} + +void NetworkTablesSettings::Update() { + if (!m_restart) return; + m_restart = false; + nt::StopClient(m_inst); + nt::StopServer(m_inst); + nt::StopLocal(m_inst); + if (*m_pMode == 1) { + wpi::StringRef serverTeam{*m_pServerTeam}; + unsigned int team; + if (!serverTeam.contains('.') && !serverTeam.getAsInteger(10, team)) { + nt::StartClientTeam(m_inst, team, NT_DEFAULT_PORT); + } else { + wpi::SmallVector serverNames; + wpi::SmallVector, 4> servers; + serverTeam.split(serverNames, ',', -1, false); + for (auto&& serverName : serverNames) + servers.emplace_back(serverName, NT_DEFAULT_PORT); + nt::StartClient(m_inst, servers); + } + } else if (*m_pMode == 2) { + nt::StartServer(m_inst, m_pIniName->c_str(), m_pListenAddress->c_str(), + NT_DEFAULT_PORT); + } +} + +void NetworkTablesSettings::Display() { + static const char* modeOptions[] = {"Disabled", "Client", "Server"}; + ImGui::Combo("Mode", m_pMode, modeOptions, 3); + switch (*m_pMode) { + case 1: + ImGui::InputText("Team/IP", m_pServerTeam); + break; + case 2: + ImGui::InputText("Listen Address", m_pListenAddress); + ImGui::InputText("ini Filename", m_pIniName); + break; + default: + break; + } + if (ImGui::Button("Apply")) m_restart = true; +} diff --git a/glass/src/app/native/cpp/NetworkTablesSettings.h b/glass/src/app/native/cpp/NetworkTablesSettings.h new file mode 100644 index 0000000000..08f2054f2b --- /dev/null +++ b/glass/src/app/native/cpp/NetworkTablesSettings.h @@ -0,0 +1,35 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +#include + +namespace wpi { +template +class SmallVectorImpl; +} // namespace wpi + +class NetworkTablesSettings { + public: + explicit NetworkTablesSettings( + NT_Inst inst = nt::GetDefaultInstance(), + const char* storageName = "NetworkTables Settings"); + + void Update(); + void Display(); + + private: + NT_Inst m_inst; + bool m_restart = true; + int* m_pMode; + std::string* m_pIniName; + std::string* m_pServerTeam; + std::string* m_pListenAddress; +}; diff --git a/glass/src/app/native/cpp/main.cpp b/glass/src/app/native/cpp/main.cpp new file mode 100644 index 0000000000..4a0f1b790d --- /dev/null +++ b/glass/src/app/native/cpp/main.cpp @@ -0,0 +1,137 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include + +#include +#include +#include +#include +#include + +#include "NetworkTablesSettings.h" +#include "glass/Context.h" +#include "glass/Model.h" +#include "glass/View.h" +#include "glass/networktables/NetworkTables.h" +#include "glass/networktables/NetworkTablesProvider.h" +#include "glass/other/Plot.h" + +namespace gui = wpi::gui; + +static std::unique_ptr gPlotProvider; +static std::unique_ptr gNtProvider; + +static std::unique_ptr gNetworkTablesModel; +static std::unique_ptr gNetworkTablesSettings; +static glass::Window* gNetworkTablesWindow; +static glass::Window* gNetworkTablesSettingsWindow; + +static void NtInitialize() { + // update window title when connection status changes + auto inst = nt::GetDefaultInstance(); + auto poller = nt::CreateConnectionListenerPoller(inst); + nt::AddPolledConnectionListener(poller, true); + gui::AddEarlyExecute([poller] { + auto win = gui::GetSystemWindow(); + if (!win) return; + bool timedOut; + for (auto&& event : nt::PollConnectionListener(poller, 0, &timedOut)) { + if (event.connected) { + wpi::SmallString<64> title; + title = "Glass - Connected ("; + title += event.conn.remote_ip; + title += ')'; + glfwSetWindowTitle(win, title.c_str()); + } else { + glfwSetWindowTitle(win, "Glass - DISCONNECTED"); + } + } + }); + + // NetworkTables table window + gNetworkTablesModel = std::make_unique(); + gui::AddEarlyExecute([] { gNetworkTablesModel->Update(); }); + + gNetworkTablesWindow = gNtProvider->AddWindow( + "NetworkTables", + std::make_unique(gNetworkTablesModel.get())); + if (gNetworkTablesWindow) { + gNetworkTablesWindow->SetDefaultPos(250, 277); + gNetworkTablesWindow->SetDefaultSize(750, 185); + gNetworkTablesWindow->DisableRenamePopup(); + } + + // NetworkTables settings window + gNetworkTablesSettings = std::make_unique(); + gui::AddEarlyExecute([] { gNetworkTablesSettings->Update(); }); + + gNetworkTablesSettingsWindow = gNtProvider->AddWindow( + "NetworkTables Settings", [] { gNetworkTablesSettings->Display(); }); + if (gNetworkTablesSettingsWindow) { + gNetworkTablesSettingsWindow->SetDefaultPos(30, 30); + gNetworkTablesSettingsWindow->SetFlags(ImGuiWindowFlags_AlwaysAutoResize); + gNetworkTablesSettingsWindow->DisableRenamePopup(); + } +} + +#ifdef _WIN32 +int __stdcall WinMain(void* hInstance, void* hPrevInstance, char* pCmdLine, + int nCmdShow) { +#else +int main() { +#endif + gui::CreateContext(); + glass::CreateContext(); + + gPlotProvider = std::make_unique("Plot"); + gNtProvider = std::make_unique("NTProvider"); + + gui::AddInit([] { ImGui::GetIO().IniFilename = "glass.ini"; }); + gPlotProvider->GlobalInit(); + gui::AddInit([] { gPlotProvider->ResetTime(); }); + gNtProvider->GlobalInit(); + gui::AddInit(NtInitialize); + + glass::AddStandardNetworkTablesViews(*gNtProvider); + + gui::AddLateExecute([] { + ImGui::BeginMainMenuBar(); + gui::EmitViewMenu(); + if (ImGui::BeginMenu("NetworkTables")) { + if (gNetworkTablesSettingsWindow) + gNetworkTablesSettingsWindow->DisplayMenuItem("NetworkTables Settings"); + if (gNetworkTablesWindow) + gNetworkTablesWindow->DisplayMenuItem("NetworkTables View"); + ImGui::Separator(); + gNtProvider->DisplayMenu(); + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Plot")) { + bool paused = gPlotProvider->IsPaused(); + if (ImGui::MenuItem("Pause All Plots", nullptr, &paused)) { + gPlotProvider->SetPaused(paused); + } + if (ImGui::MenuItem("Reset Plot Time")) gPlotProvider->ResetTime(); + ImGui::Separator(); + gPlotProvider->DisplayMenu(); + ImGui::EndMenu(); + } + ImGui::EndMainMenuBar(); + }); + + gui::Initialize("Glass - DISCONNECTED", 1024, 768); + gui::Main(); + + gNetworkTablesModel.reset(); + gNetworkTablesSettings.reset(); + gNtProvider.reset(); + gPlotProvider.reset(); + + glass::DestroyContext(); + gui::DestroyContext(); +} diff --git a/glass/src/lib/native/cpp/Context.cpp b/glass/src/lib/native/cpp/Context.cpp new file mode 100644 index 0000000000..cac280d84b --- /dev/null +++ b/glass/src/lib/native/cpp/Context.cpp @@ -0,0 +1,422 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/Context.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "glass/ContextInternal.h" + +using namespace glass; + +Context* glass::gContext; + +static bool ConvertInt(Storage::Value* value) { + value->type = Storage::Value::kInt; + if (value->stringVal.empty()) { + return false; + } else { + if (wpi::StringRef{value->stringVal}.getAsInteger(10, value->intVal)) + return false; + } + return true; +} + +static bool ConvertInt64(Storage::Value* value) { + value->type = Storage::Value::kInt64; + if (value->stringVal.empty()) { + return false; + } else { + if (wpi::StringRef{value->stringVal}.getAsInteger(10, value->int64Val)) + return false; + } + return true; +} + +static bool ConvertBool(Storage::Value* value) { + value->type = Storage::Value::kBool; + if (value->stringVal.empty()) { + return false; + } else { + int val; + if (wpi::StringRef{value->stringVal}.getAsInteger(10, val)) { + return false; + } + value->boolVal = (val != 0); + } + return true; +} + +static bool ConvertFloat(Storage::Value* value) { + value->type = Storage::Value::kFloat; + if (value->stringVal.empty()) { + return false; + } else { + if (std::sscanf(value->stringVal.c_str(), "%f", &value->floatVal) != 1) + return false; + } + return true; +} + +static bool ConvertDouble(Storage::Value* value) { + value->type = Storage::Value::kDouble; + if (value->stringVal.empty()) { + return false; + } else { + if (std::sscanf(value->stringVal.c_str(), "%lf", &value->doubleVal) != 1) + return false; + } + return true; +} + +static void* GlassStorageReadOpen(ImGuiContext*, ImGuiSettingsHandler* handler, + const char* name) { + auto ctx = static_cast(handler->UserData); + auto& storage = ctx->storage[name]; + if (!storage) storage = std::make_unique(); + return storage.get(); +} + +static void GlassStorageReadLine(ImGuiContext*, ImGuiSettingsHandler*, + void* entry, const char* line) { + auto storage = static_cast(entry); + auto [key, val] = wpi::StringRef{line}.split('='); + auto& keys = storage->GetKeys(); + auto& values = storage->GetValues(); + auto it = std::find(keys.begin(), keys.end(), key); + if (it == keys.end()) { + keys.emplace_back(key); + values.emplace_back(std::make_unique(val)); + } else { + auto& value = *values[it - keys.begin()]; + value.stringVal = val; + switch (value.type) { + case Storage::Value::kInt: + ConvertInt(&value); + break; + case Storage::Value::kInt64: + ConvertInt64(&value); + break; + case Storage::Value::kBool: + ConvertBool(&value); + break; + case Storage::Value::kFloat: + ConvertFloat(&value); + break; + case Storage::Value::kDouble: + ConvertDouble(&value); + break; + default: + break; + } + } +} + +static void GlassStorageWriteAll(ImGuiContext*, ImGuiSettingsHandler* handler, + ImGuiTextBuffer* out_buf) { + auto ctx = static_cast(handler->UserData); + + // sort for output + std::vector>> sorted; + for (auto it = ctx->storage.begin(); it != ctx->storage.end(); ++it) { + sorted.emplace_back(it); + } + std::sort(sorted.begin(), sorted.end(), [](const auto& a, const auto& b) { + return a->getKey() < b->getKey(); + }); + + for (auto&& entryIt : sorted) { + auto& entry = *entryIt; + out_buf->append("[GlassStorage]["); + out_buf->append(entry.first().begin(), entry.first().end()); + out_buf->append("]\n"); + auto& keys = entry.second->GetKeys(); + auto& values = entry.second->GetValues(); + for (size_t i = 0; i < keys.size(); ++i) { + out_buf->append(keys[i].data(), keys[i].data() + keys[i].size()); + out_buf->append("="); + auto& value = *values[i]; + switch (value.type) { + case Storage::Value::kInt: + out_buf->appendf("%d\n", value.intVal); + break; + case Storage::Value::kInt64: + out_buf->appendf("%" PRId64 "\n", value.int64Val); + break; + case Storage::Value::kBool: + out_buf->appendf("%d\n", value.boolVal ? 1 : 0); + break; + case Storage::Value::kFloat: + out_buf->appendf("%f\n", value.floatVal); + break; + case Storage::Value::kDouble: + out_buf->appendf("%f\n", value.doubleVal); + break; + case Storage::Value::kNone: + case Storage::Value::kString: + out_buf->append(value.stringVal.data(), + value.stringVal.data() + value.stringVal.size()); + out_buf->append("\n"); + break; + } + } + out_buf->append("\n"); + } +} + +static void Initialize(Context* ctx) { + wpi::gui::AddInit([=] { + ImGuiSettingsHandler ini_handler; + ini_handler.TypeName = "GlassStorage"; + ini_handler.TypeHash = ImHashStr("GlassStorage"); + ini_handler.ReadOpenFn = GlassStorageReadOpen; + ini_handler.ReadLineFn = GlassStorageReadLine; + ini_handler.WriteAllFn = GlassStorageWriteAll; + ini_handler.UserData = ctx; + ImGui::GetCurrentContext()->SettingsHandlers.push_back(ini_handler); + + ctx->sources.Initialize(); + }); +} + +static void Shutdown(Context* ctx) {} + +Context* glass::CreateContext() { + Context* ctx = new Context; + if (!gContext) SetCurrentContext(ctx); + Initialize(ctx); + return ctx; +} + +void glass::DestroyContext(Context* ctx) { + if (!ctx) ctx = gContext; + Shutdown(ctx); + if (gContext == ctx) SetCurrentContext(nullptr); + delete ctx; +} + +Context* glass::GetCurrentContext() { return gContext; } + +void glass::SetCurrentContext(Context* ctx) { gContext = ctx; } + +Storage::Value& Storage::GetValue(wpi::StringRef key) { + auto it = std::find(m_keys.begin(), m_keys.end(), key); + if (it == m_keys.end()) { + m_keys.emplace_back(key); + m_values.emplace_back(std::make_unique()); + return *m_values.back(); + } else { + return *m_values[it - m_keys.begin()]; + } +} + +#define DEFUN(CapsName, LowerName, CType) \ + CType Storage::Get##CapsName(wpi::StringRef key, CType defaultVal) const { \ + auto it = std::find(m_keys.begin(), m_keys.end(), key); \ + if (it == m_keys.end()) return defaultVal; \ + Value& value = *m_values[it - m_keys.begin()]; \ + if (value.type != Value::k##CapsName) { \ + if (!Convert##CapsName(&value)) value.LowerName##Val = defaultVal; \ + } \ + return value.LowerName##Val; \ + } \ + \ + void Storage::Set##CapsName(wpi::StringRef key, CType val) { \ + auto it = std::find(m_keys.begin(), m_keys.end(), key); \ + if (it == m_keys.end()) { \ + m_keys.emplace_back(key); \ + m_values.emplace_back(std::make_unique()); \ + m_values.back()->type = Value::k##CapsName; \ + m_values.back()->LowerName##Val = val; \ + } else { \ + Value& value = *m_values[it - m_keys.begin()]; \ + value.type = Value::k##CapsName; \ + value.LowerName##Val = val; \ + } \ + } \ + \ + CType* Storage::Get##CapsName##Ref(wpi::StringRef key, CType defaultVal) { \ + auto it = std::find(m_keys.begin(), m_keys.end(), key); \ + if (it == m_keys.end()) { \ + m_keys.emplace_back(key); \ + m_values.emplace_back(std::make_unique()); \ + m_values.back()->type = Value::k##CapsName; \ + m_values.back()->LowerName##Val = defaultVal; \ + return &m_values.back()->LowerName##Val; \ + } else { \ + Value& value = *m_values[it - m_keys.begin()]; \ + if (value.type != Value::k##CapsName) { \ + if (!Convert##CapsName(&value)) value.LowerName##Val = defaultVal; \ + } \ + return &value.LowerName##Val; \ + } \ + } + +DEFUN(Int, int, int) +DEFUN(Int64, int64, int64_t) +DEFUN(Bool, bool, bool) +DEFUN(Float, float, float) +DEFUN(Double, double, double) + +std::string Storage::GetString(wpi::StringRef key, + const std::string& defaultVal) const { + auto it = std::find(m_keys.begin(), m_keys.end(), key); + if (it == m_keys.end()) return defaultVal; + Value& value = *m_values[it - m_keys.begin()]; + value.type = Value::kString; + return value.stringVal; +} + +void Storage::SetString(wpi::StringRef key, const wpi::Twine& val) { + auto it = std::find(m_keys.begin(), m_keys.end(), key); + if (it == m_keys.end()) { + m_keys.emplace_back(key); + m_values.emplace_back(std::make_unique(val)); + m_values.back()->type = Value::kString; + } else { + Value& value = *m_values[it - m_keys.begin()]; + value.type = Value::kString; + value.stringVal = val.str(); + } +} + +std::string* Storage::GetStringRef(wpi::StringRef key, + wpi::StringRef defaultVal) { + auto it = std::find(m_keys.begin(), m_keys.end(), key); + if (it == m_keys.end()) { + m_keys.emplace_back(key); + m_values.emplace_back(std::make_unique(defaultVal)); + m_values.back()->type = Value::kString; + return &m_values.back()->stringVal; + } else { + Value& value = *m_values[it - m_keys.begin()]; + value.type = Value::kString; + return &value.stringVal; + } +} + +Storage& glass::GetStorage() { + auto& storage = gContext->storage[gContext->curId]; + if (!storage) storage = std::make_unique(); + return *storage; +} + +Storage& glass::GetStorage(wpi::StringRef id) { + auto& storage = gContext->storage[id]; + if (!storage) storage = std::make_unique(); + return *storage; +} + +static void PushIDStack(wpi::StringRef label_id) { + gContext->idStack.emplace_back(gContext->curId.size()); + + auto [label, id] = wpi::StringRef{label_id}.split("###"); + // if no ###id, use label as id + if (id.empty()) id = label; + if (!gContext->curId.empty()) gContext->curId += "###"; + gContext->curId += id; +} + +static void PopIDStack() { + gContext->curId.resize(gContext->idStack.back()); + gContext->idStack.pop_back(); +} + +bool glass::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) { + PushIDStack(name); + return ImGui::Begin(name, p_open, flags); +} + +void glass::End() { + ImGui::End(); + PopIDStack(); +} + +bool glass::BeginChild(const char* str_id, const ImVec2& size, bool border, + ImGuiWindowFlags flags) { + PushIDStack(str_id); + return ImGui::BeginChild(str_id, size, border, flags); +} + +void glass::EndChild() { + ImGui::EndChild(); + PopIDStack(); +} + +bool glass::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags) { + wpi::SmallString<64> openKey; + auto [name, id] = wpi::StringRef{label}.split("###"); + // if no ###id, use name as id + if (id.empty()) id = name; + openKey = id; + openKey += "###open"; + + bool* open = GetStorage().GetBoolRef(openKey); + *open = ImGui::CollapsingHeader( + label, flags | (*open ? ImGuiTreeNodeFlags_DefaultOpen : 0)); + return *open; +} + +bool glass::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags) { + PushIDStack(label); + bool* open = GetStorage().GetBoolRef("open"); + *open = ImGui::TreeNodeEx( + label, flags | (*open ? ImGuiTreeNodeFlags_DefaultOpen : 0)); + if (!*open) PopIDStack(); + return *open; +} + +void glass::TreePop() { + ImGui::TreePop(); + PopIDStack(); +} + +void glass::PushID(const char* str_id) { + PushIDStack(str_id); + ImGui::PushID(str_id); +} + +void glass::PushID(const char* str_id_begin, const char* str_id_end) { + PushIDStack(wpi::StringRef(str_id_begin, str_id_end - str_id_begin)); + ImGui::PushID(str_id_begin, str_id_end); +} + +void glass::PushID(int int_id) { + char buf[16]; + std::snprintf(buf, sizeof(buf), "%d", int_id); + PushIDStack(buf); + ImGui::PushID(int_id); +} + +void glass::PopID() { + ImGui::PopID(); + PopIDStack(); +} + +bool glass::PopupEditName(const char* label, std::string* name) { + bool rv = false; + if (ImGui::BeginPopupContextItem(label)) { + ImGui::Text("Edit name:"); + if (ImGui::InputText("##editname", name)) { + rv = true; + } + if (ImGui::Button("Close") || ImGui::IsKeyPressedMap(ImGuiKey_Enter) || + ImGui::IsKeyPressedMap(ImGuiKey_KeyPadEnter)) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + return rv; +} diff --git a/glass/src/lib/native/cpp/DataSource.cpp b/glass/src/lib/native/cpp/DataSource.cpp new file mode 100644 index 0000000000..8c9361ca48 --- /dev/null +++ b/glass/src/lib/native/cpp/DataSource.cpp @@ -0,0 +1,142 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/DataSource.h" + +#include "glass/ContextInternal.h" + +using namespace glass; + +wpi::sig::Signal DataSource::sourceCreated; + +DataSource::DataSource(const wpi::Twine& id) : m_id{id.str()} { + auto it = gContext->sources.try_emplace(m_id, this); + auto& srcName = it.first->getValue(); + m_name = srcName.name.get(); + if (!srcName.source) srcName.source = this; + sourceCreated(m_id.c_str(), this); +} + +DataSource::DataSource(const wpi::Twine& id, int index) + : DataSource{id + wpi::Twine('[') + wpi::Twine(index) + wpi::Twine(']')} {} + +DataSource::DataSource(const wpi::Twine& id, int index, int index2) + : DataSource{id + wpi::Twine('[') + wpi::Twine(index) + wpi::Twine(',') + + wpi::Twine(index2) + wpi::Twine(']')} {} + +DataSource::~DataSource() { + if (!gContext) return; + auto it = gContext->sources.find(m_id); + if (it == gContext->sources.end()) return; + auto& srcName = it->getValue(); + if (srcName.source == this) srcName.source = nullptr; +} + +void DataSource::SetName(const wpi::Twine& name) { m_name->SetName(name); } + +const char* DataSource::GetName() const { return m_name->GetName(); } + +void DataSource::PushEditNameId(int index) { m_name->PushEditNameId(index); } + +void DataSource::PushEditNameId(const char* name) { + m_name->PushEditNameId(name); +} + +bool DataSource::PopupEditName(int index) { + return m_name->PopupEditName(index); +} + +bool DataSource::PopupEditName(const char* name) { + return m_name->PopupEditName(name); +} + +bool DataSource::InputTextName(const char* label_id, + ImGuiInputTextFlags flags) { + return m_name->InputTextName(label_id, flags); +} + +void DataSource::LabelText(const char* label, const char* fmt, ...) const { + va_list args; + va_start(args, fmt); + LabelTextV(label, fmt, args); + va_end(args); +} + +// Add a label+text combo aligned to other label+value widgets +void DataSource::LabelTextV(const char* label, const char* fmt, + va_list args) const { + ImGui::PushID(label); + ImGui::LabelTextV("##input", fmt, args); + ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::Selectable(label); + ImGui::PopID(); + EmitDrag(); +} + +bool DataSource::Combo(const char* label, int* current_item, + const char* const items[], int items_count, + int popup_max_height_in_items) const { + ImGui::PushID(label); + bool rv = ImGui::Combo("##input", current_item, items, items_count, + popup_max_height_in_items); + ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::Selectable(label); + EmitDrag(); + ImGui::PopID(); + return rv; +} + +bool DataSource::SliderFloat(const char* label, float* v, float v_min, + float v_max, const char* format, + float power) const { + ImGui::PushID(label); + bool rv = ImGui::SliderFloat("##input", v, v_min, v_max, format, power); + ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::Selectable(label); + EmitDrag(); + ImGui::PopID(); + return rv; +} + +bool DataSource::InputDouble(const char* label, double* v, double step, + double step_fast, const char* format, + ImGuiInputTextFlags flags) const { + ImGui::PushID(label); + bool rv = ImGui::InputDouble("##input", v, step, step_fast, format, flags); + ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::Selectable(label); + EmitDrag(); + ImGui::PopID(); + return rv; +} + +bool DataSource::InputInt(const char* label, int* v, int step, int step_fast, + ImGuiInputTextFlags flags) const { + ImGui::PushID(label); + bool rv = ImGui::InputInt("##input", v, step, step_fast, flags); + ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::Selectable(label); + EmitDrag(); + ImGui::PopID(); + return rv; +} + +void DataSource::EmitDrag(ImGuiDragDropFlags flags) const { + if (ImGui::BeginDragDropSource(flags)) { + auto self = this; + ImGui::SetDragDropPayload("DataSource", &self, sizeof(self)); + const char* name = GetName(); + ImGui::TextUnformatted(name[0] == '\0' ? m_id.c_str() : name); + ImGui::EndDragDropSource(); + } +} + +DataSource* DataSource::Find(wpi::StringRef id) { + auto it = gContext->sources.find(id); + if (it == gContext->sources.end()) return nullptr; + return it->getValue().source; +} diff --git a/glass/src/lib/native/cpp/MainMenuBar.cpp b/glass/src/lib/native/cpp/MainMenuBar.cpp new file mode 100644 index 0000000000..406519cabf --- /dev/null +++ b/glass/src/lib/native/cpp/MainMenuBar.cpp @@ -0,0 +1,52 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/MainMenuBar.h" + +#include + +#include + +using namespace glass; + +void MainMenuBar::AddMainMenu(std::function menu) { + if (menu) m_menus.emplace_back(std::move(menu)); +} + +void MainMenuBar::AddOptionMenu(std::function menu) { + if (menu) m_optionMenus.emplace_back(std::move(menu)); +} + +void MainMenuBar::Display() { + ImGui::BeginMainMenuBar(); + + if (!m_optionMenus.empty()) { + if (ImGui::BeginMenu("Options")) { + for (auto&& menu : m_optionMenus) { + if (menu) menu(); + } + ImGui::EndMenu(); + } + } + + wpi::gui::EmitViewMenu(); + + for (auto&& menu : m_menus) { + if (menu) menu(); + } + +#if 0 + char str[64]; + std::snprintf(str, sizeof(str), "%.3f ms/frame (%.1f FPS)", + 1000.0f / ImGui::GetIO().Framerate, + ImGui::GetIO().Framerate); + ImGui::SameLine(ImGui::GetWindowWidth() - ImGui::CalcTextSize(str).x - + 10); + ImGui::Text("%s", str); +#endif + ImGui::EndMainMenuBar(); +} diff --git a/simulation/halsim_gui/src/main/native/cpp/PlotGui.h b/glass/src/lib/native/cpp/Model.cpp similarity index 80% rename from simulation/halsim_gui/src/main/native/cpp/PlotGui.h rename to glass/src/lib/native/cpp/Model.cpp index 6bbd337794..77b57598c6 100644 --- a/simulation/halsim_gui/src/main/native/cpp/PlotGui.h +++ b/glass/src/lib/native/cpp/Model.cpp @@ -5,13 +5,8 @@ /* the project. */ /*----------------------------------------------------------------------------*/ -#pragma once +#include "glass/Model.h" -namespace halsimgui { +using namespace glass; -class PlotGui { - public: - static void Initialize(); -}; - -} // namespace halsimgui +bool Model::IsReadOnly() { return false; } diff --git a/glass/src/lib/native/cpp/View.cpp b/glass/src/lib/native/cpp/View.cpp new file mode 100644 index 0000000000..dee9a88dc0 --- /dev/null +++ b/glass/src/lib/native/cpp/View.cpp @@ -0,0 +1,30 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/View.h" + +using namespace glass; + +namespace { +class FunctionView : public View { + public: + explicit FunctionView(wpi::unique_function display) + : m_display(std::move(display)) {} + + void Display() override { m_display(); } + + private: + wpi::unique_function m_display; +}; +} // namespace + +std::unique_ptr glass::MakeFunctionView( + wpi::unique_function display) { + return std::make_unique(std::move(display)); +} + +void View::Hidden() {} diff --git a/glass/src/lib/native/cpp/Window.cpp b/glass/src/lib/native/cpp/Window.cpp new file mode 100644 index 0000000000..9bfc0608bd --- /dev/null +++ b/glass/src/lib/native/cpp/Window.cpp @@ -0,0 +1,102 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/Window.h" + +#include +#include + +#include "glass/Context.h" + +using namespace glass; + +void Window::SetVisibility(Visibility visibility) { + switch (visibility) { + case kHide: + m_visible = false; + m_enabled = true; + break; + case kShow: + m_visible = true; + m_enabled = true; + break; + case kDisabled: + m_enabled = false; + break; + } +} + +void Window::Display() { + if (!m_view) return; + if (!m_visible || !m_enabled) { + PushID(m_id); + m_view->Hidden(); + PopID(); + return; + } + + if (m_posCond != 0) ImGui::SetNextWindowPos(m_pos, m_posCond); + if (m_sizeCond != 0) ImGui::SetNextWindowSize(m_size, m_sizeCond); + if (m_setPadding) ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, m_padding); + + char label[128]; + std::snprintf(label, sizeof(label), "%s###%s", + m_name.empty() ? m_id.c_str() : m_name.c_str(), m_id.c_str()); + + if (Begin(label, &m_visible, m_flags)) { + if (m_renamePopupEnabled) PopupEditName(nullptr, &m_name); + m_view->Display(); + } else { + m_view->Hidden(); + } + End(); + if (m_setPadding) ImGui::PopStyleVar(); +} + +bool Window::DisplayMenuItem(const char* label) { + bool wasVisible = m_visible; + ImGui::MenuItem( + label ? label : (m_name.empty() ? m_id.c_str() : m_name.c_str()), nullptr, + &m_visible, m_enabled); + return !wasVisible && m_visible; +} + +void Window::ScaleDefault(float scale) { + if ((m_posCond & ImGuiCond_FirstUseEver) != 0) { + m_pos.x *= scale; + m_pos.y *= scale; + } + if ((m_sizeCond & ImGuiCond_FirstUseEver) != 0) { + m_size.x *= scale; + m_size.y *= scale; + } +} + +void Window::IniReadLine(const char* lineStr) { + wpi::StringRef line{lineStr}; + auto [name, value] = line.split('='); + name = name.trim(); + value = value.trim(); + + if (name == "name") { + m_name = value; + } else if (name == "visible") { + int num; + if (value.getAsInteger(10, num)) return; + m_visible = num; + } else if (name == "enabled") { + int num; + if (value.getAsInteger(10, num)) return; + m_enabled = num; + } +} + +void Window::IniWriteAll(const char* typeName, ImGuiTextBuffer* out_buf) { + out_buf->appendf("[%s][%s]\nname=%s\nvisible=%d\nenabled=%d\n\n", typeName, + m_id.c_str(), m_name.c_str(), m_visible ? 1 : 0, + m_enabled ? 1 : 0); +} diff --git a/glass/src/lib/native/cpp/WindowManager.cpp b/glass/src/lib/native/cpp/WindowManager.cpp new file mode 100644 index 0000000000..8cbc5a5224 --- /dev/null +++ b/glass/src/lib/native/cpp/WindowManager.cpp @@ -0,0 +1,107 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/WindowManager.h" + +#include + +#include +#include +#include + +using namespace glass; + +WindowManager::WindowManager(const wpi::Twine& iniName) + : m_iniSaver{iniName, this} {} + +// read/write open state to ini file +void* WindowManager::IniSaver::IniReadOpen(const char* name) { + return m_manager->GetOrAddWindow(name, true); +} + +void WindowManager::IniSaver::IniReadLine(void* entry, const char* lineStr) { + static_cast(entry)->IniReadLine(lineStr); +} + +void WindowManager::IniSaver::IniWriteAll(ImGuiTextBuffer* out_buf) { + const char* typeName = GetTypeName(); + for (auto&& window : m_manager->m_windows) { + window->IniWriteAll(typeName, out_buf); + } +} + +Window* WindowManager::AddWindow(wpi::StringRef id, + wpi::unique_function display) { + auto win = GetOrAddWindow(id, false); + if (!win) return nullptr; + if (win->HasView()) { + wpi::errs() << "GUI: ignoring duplicate window '" << id << "'\n"; + return nullptr; + } + win->SetView(MakeFunctionView(std::move(display))); + return win; +} + +Window* WindowManager::AddWindow(wpi::StringRef id, + std::unique_ptr view) { + auto win = GetOrAddWindow(id, false); + if (!win) return nullptr; + if (win->HasView()) { + wpi::errs() << "GUI: ignoring duplicate window '" << id << "'\n"; + return nullptr; + } + win->SetView(std::move(view)); + return win; +} + +Window* WindowManager::GetOrAddWindow(wpi::StringRef id, bool duplicateOk) { + // binary search + auto it = std::lower_bound( + m_windows.begin(), m_windows.end(), id, + [](const auto& elem, wpi::StringRef s) { return elem->GetId() < s; }); + if (it != m_windows.end() && (*it)->GetId() == id) { + if (!duplicateOk) { + wpi::errs() << "GUI: ignoring duplicate window '" << id << "'\n"; + return nullptr; + } + return it->get(); + } + // insert before (keeps sort) + return m_windows.emplace(it, std::make_unique(id))->get(); +} + +Window* WindowManager::GetWindow(wpi::StringRef id) { + // binary search + auto it = std::lower_bound( + m_windows.begin(), m_windows.end(), id, + [](const auto& elem, wpi::StringRef s) { return elem->GetId() < s; }); + if (it == m_windows.end() || (*it)->GetId() != id) return nullptr; + return it->get(); +} + +void WindowManager::GlobalInit() { + wpi::gui::AddInit([this] { m_iniSaver.Initialize(); }); + wpi::gui::AddWindowScaler([this](float scale) { + // scale default window positions + for (auto&& window : m_windows) { + window->ScaleDefault(scale); + } + }); + wpi::gui::AddLateExecute([this] { DisplayWindows(); }); +} + +void WindowManager::DisplayMenu() { + for (auto&& window : m_windows) { + window->DisplayMenuItem(); + } +} + +void WindowManager::DisplayWindows() { + for (auto&& window : m_windows) { + window->Display(); + } +} diff --git a/glass/src/lib/native/cpp/hardware/Accelerometer.cpp b/glass/src/lib/native/cpp/hardware/Accelerometer.cpp new file mode 100644 index 0000000000..d39c270e4f --- /dev/null +++ b/glass/src/lib/native/cpp/hardware/Accelerometer.cpp @@ -0,0 +1,51 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/hardware/Accelerometer.h" + +#include "glass/DataSource.h" +#include "glass/other/DeviceTree.h" + +using namespace glass; + +void glass::DisplayAccelerometerDevice(AccelerometerModel* model) { + if (!model->Exists()) return; + if (BeginDevice("BuiltInAccel")) { + // Range + { + int value = model->GetRange(); + static const char* rangeOptions[] = {"2G", "4G", "8G"}; + DeviceEnum("Range", true, &value, rangeOptions, 3); + } + + // X Accel + if (auto xData = model->GetXData()) { + double value = xData->GetValue(); + if (DeviceDouble("X Accel", false, &value, xData)) { + model->SetX(value); + } + } + + // Y Accel + if (auto yData = model->GetYData()) { + double value = yData->GetValue(); + if (DeviceDouble("Y Accel", false, &value, yData)) { + model->SetY(value); + } + } + + // Z Accel + if (auto zData = model->GetZData()) { + double value = zData->GetValue(); + if (DeviceDouble("Z Accel", false, &value, zData)) { + model->SetZ(value); + } + } + + EndDevice(); + } +} diff --git a/glass/src/lib/native/cpp/hardware/AnalogGyro.cpp b/glass/src/lib/native/cpp/hardware/AnalogGyro.cpp new file mode 100644 index 0000000000..1d8fb44e3c --- /dev/null +++ b/glass/src/lib/native/cpp/hardware/AnalogGyro.cpp @@ -0,0 +1,41 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/hardware/AnalogGyro.h" + +#include "glass/DataSource.h" +#include "glass/other/DeviceTree.h" + +using namespace glass; + +void glass::DisplayAnalogGyroDevice(AnalogGyroModel* model, int index) { + char name[32]; + std::snprintf(name, sizeof(name), "AnalogGyro[%d]", index); + if (BeginDevice(name)) { + // angle + if (auto angleData = model->GetAngleData()) { + double value = angleData->GetValue(); + if (DeviceDouble("Angle", false, &value, angleData)) { + model->SetAngle(value); + } + } + + // rate + if (auto rateData = model->GetRateData()) { + double value = rateData->GetValue(); + if (DeviceDouble("Rate", false, &value, rateData)) { + model->SetRate(value); + } + } + EndDevice(); + } +} + +void glass::DisplayAnalogGyrosDevice(AnalogGyrosModel* model) { + model->ForEachAnalogGyro( + [&](AnalogGyroModel& gyro, int i) { DisplayAnalogGyroDevice(&gyro, i); }); +} diff --git a/glass/src/lib/native/cpp/hardware/AnalogInput.cpp b/glass/src/lib/native/cpp/hardware/AnalogInput.cpp new file mode 100644 index 0000000000..cb3117694a --- /dev/null +++ b/glass/src/lib/native/cpp/hardware/AnalogInput.cpp @@ -0,0 +1,66 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/hardware/AnalogInput.h" + +#include + +#include "glass/Context.h" +#include "glass/DataSource.h" + +using namespace glass; + +void glass::DisplayAnalogInput(AnalogInputModel* model, int index) { + auto voltageData = model->GetVoltageData(); + if (!voltageData) return; + + // build label + std::string* name = GetStorage().GetStringRef("name"); + char label[128]; + if (!name->empty()) { + std::snprintf(label, sizeof(label), "%s [%d]###name", name->c_str(), index); + } else { + std::snprintf(label, sizeof(label), "In[%d]###name", index); + } + + if (model->IsGyro()) { + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255)); + ImGui::LabelText(label, "AnalogGyro[%d]", index); + ImGui::PopStyleColor(); + } else if (auto simDevice = model->GetSimDevice()) { + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255)); + ImGui::LabelText(label, "%s", simDevice); + ImGui::PopStyleColor(); + } else { + float val = voltageData->GetValue(); + if (voltageData->SliderFloat(label, &val, 0.0, 5.0)) model->SetVoltage(val); + } + + // context menu to change name + if (PopupEditName("name", name)) voltageData->SetName(name->c_str()); +} + +void glass::DisplayAnalogInputs(AnalogInputsModel* model, + wpi::StringRef noneMsg) { + ImGui::Text("(Use Ctrl+Click to edit value)"); + bool hasAny = false; + bool first = true; + model->ForEachAnalogInput([&](AnalogInputModel& input, int i) { + if (!first) { + ImGui::Spacing(); + ImGui::Spacing(); + } else { + first = false; + } + PushID(i); + DisplayAnalogInput(&input, i); + PopID(); + hasAny = true; + }); + if (!hasAny && !noneMsg.empty()) + ImGui::TextUnformatted(noneMsg.begin(), noneMsg.end()); +} diff --git a/glass/src/lib/native/cpp/hardware/AnalogOutput.cpp b/glass/src/lib/native/cpp/hardware/AnalogOutput.cpp new file mode 100644 index 0000000000..88a3e99385 --- /dev/null +++ b/glass/src/lib/native/cpp/hardware/AnalogOutput.cpp @@ -0,0 +1,47 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/hardware/AnalogOutput.h" + +#include "glass/Context.h" +#include "glass/DataSource.h" +#include "glass/other/DeviceTree.h" + +using namespace glass; + +void glass::DisplayAnalogOutputsDevice(AnalogOutputsModel* model) { + int count = 0; + model->ForEachAnalogOutput([&](auto&, int) { ++count; }); + if (count == 0) return; + + if (BeginDevice("Analog Outputs")) { + model->ForEachAnalogOutput([&](auto& analogOut, int i) { + auto analogOutData = analogOut.GetVoltageData(); + if (!analogOutData) return; + PushID(i); + + // build label + std::string* name = GetStorage().GetStringRef("name"); + char label[128]; + if (!name->empty()) { + std::snprintf(label, sizeof(label), "%s [%d]###name", name->c_str(), i); + } else { + std::snprintf(label, sizeof(label), "Out[%d]###name", i); + } + + double value = analogOutData->GetValue(); + DeviceDouble(label, true, &value, analogOutData); + + if (PopupEditName("name", name)) { + if (analogOutData) analogOutData->SetName(name->c_str()); + } + PopID(); + }); + + EndDevice(); + } +} diff --git a/glass/src/lib/native/cpp/hardware/DIO.cpp b/glass/src/lib/native/cpp/hardware/DIO.cpp new file mode 100644 index 0000000000..c4636dbaf7 --- /dev/null +++ b/glass/src/lib/native/cpp/hardware/DIO.cpp @@ -0,0 +1,118 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/hardware/DIO.h" + +#include + +#include "glass/DataSource.h" +#include "glass/hardware/Encoder.h" +#include "glass/support/IniSaverInfo.h" + +using namespace glass; + +static void LabelSimDevice(const char* name, const char* simDeviceName) { + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255)); + ImGui::LabelText(name, "%s", simDeviceName); + ImGui::PopStyleColor(); +} + +void DisplayDIOImpl(DIOModel* model, int index, bool outputsEnabled) { + auto dpwm = model->GetDPWM(); + auto dutyCycle = model->GetDutyCycle(); + auto encoder = model->GetEncoder(); + + auto dioData = model->GetValueData(); + auto dpwmData = dpwm ? dpwm->GetValueData() : nullptr; + auto dutyCycleData = dutyCycle ? dutyCycle->GetValueData() : nullptr; + + bool exists = model->Exists(); + auto& info = dioData->GetNameInfo(); + char label[128]; + if (exists && dpwmData) { + dpwmData->GetNameInfo().GetLabel(label, sizeof(label), "PWM", index); + if (auto simDevice = dpwm->GetSimDevice()) { + LabelSimDevice(label, simDevice); + } else { + dpwmData->LabelText(label, "%0.3f", dpwmData->GetValue()); + } + } else if (exists && encoder) { + info.GetLabel(label, sizeof(label), " In", index); + if (auto simDevice = encoder->GetSimDevice()) { + LabelSimDevice(label, simDevice); + } else { + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255)); + ImGui::LabelText(label, "Encoder[%d,%d]", encoder->GetChannelA(), + encoder->GetChannelB()); + ImGui::PopStyleColor(); + } + } else if (exists && dutyCycleData) { + dutyCycleData->GetNameInfo().GetLabel(label, sizeof(label), "Dty", index); + if (auto simDevice = dutyCycle->GetSimDevice()) { + LabelSimDevice(label, simDevice); + } else { + double val = dutyCycleData->GetValue(); + if (dutyCycleData->InputDouble(label, &val)) { + dutyCycle->SetValue(val); + } + } + } else { + const char* name = model->GetName(); + if (name[0] != '\0') + info.GetLabel(label, sizeof(label), name); + else + info.GetLabel(label, sizeof(label), model->IsInput() ? " In" : "Out", + index); + if (auto simDevice = model->GetSimDevice()) { + LabelSimDevice(label, simDevice); + } else { + if (!exists) { + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255)); + dioData->LabelText(label, "unknown"); + ImGui::PopStyleColor(); + } else if (model->IsReadOnly()) { + dioData->LabelText( + label, "%s", + outputsEnabled ? (dioData->GetValue() != 0 ? "1 (high)" : "0 (low)") + : "1 (disabled)"); + + } else { + static const char* options[] = {"0 (low)", "1 (high)"}; + int val = dioData->GetValue() != 0 ? 1 : 0; + if (dioData->Combo(label, &val, options, 2)) { + model->SetValue(val); + } + } + } + } + if (info.PopupEditName(index)) { + if (dpwmData) dpwmData->SetName(info.GetName()); + if (dutyCycleData) dutyCycleData->SetName(info.GetName()); + } +} + +void glass::DisplayDIO(DIOModel* model, int index, bool outputsEnabled) { + ImGui::PushItemWidth(ImGui::GetFontSize() * 8); + DisplayDIOImpl(model, index, outputsEnabled); + ImGui::PopItemWidth(); +} + +void glass::DisplayDIOs(DIOsModel* model, bool outputsEnabled, + wpi::StringRef noneMsg) { + bool hasAny = false; + + ImGui::PushItemWidth(ImGui::GetFontSize() * 8); + model->ForEachDIO([&](DIOModel& dio, int i) { + hasAny = true; + ImGui::PushID(i); + DisplayDIOImpl(&dio, i, outputsEnabled); + ImGui::PopID(); + }); + ImGui::PopItemWidth(); + if (!hasAny && !noneMsg.empty()) + ImGui::TextUnformatted(noneMsg.begin(), noneMsg.end()); +} diff --git a/glass/src/lib/native/cpp/hardware/Encoder.cpp b/glass/src/lib/native/cpp/hardware/Encoder.cpp new file mode 100644 index 0000000000..7695aac1df --- /dev/null +++ b/glass/src/lib/native/cpp/hardware/Encoder.cpp @@ -0,0 +1,165 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/hardware/Encoder.h" + +#include + +#include "glass/Context.h" +#include "glass/DataSource.h" + +using namespace glass; + +void EncoderModel::SetName(const wpi::Twine& name) { + if (name.str().empty()) { + if (auto distancePerPulse = GetDistancePerPulseData()) { + distancePerPulse->SetName(""); + } + if (auto count = GetCountData()) { + count->SetName(""); + } + if (auto period = GetPeriodData()) { + period->SetName(""); + } + if (auto direction = GetDirectionData()) { + direction->SetName(""); + } + if (auto distance = GetDistanceData()) { + distance->SetName(""); + } + if (auto rate = GetRateData()) { + rate->SetName(""); + } + } else { + if (auto distancePerPulse = GetDistancePerPulseData()) { + distancePerPulse->SetName(name + " Distance/Count"); + } + if (auto count = GetCountData()) { + count->SetName(name + " Count"); + } + if (auto period = GetPeriodData()) { + period->SetName(name + " Period"); + } + if (auto direction = GetDirectionData()) { + direction->SetName(name + " Direction"); + } + if (auto distance = GetDistanceData()) { + distance->SetName(name + " Distance"); + } + if (auto rate = GetRateData()) { + rate->SetName(name + " Rate"); + } + } +} + +void glass::DisplayEncoder(EncoderModel* model) { + if (auto simDevice = model->GetSimDevice()) { + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255)); + ImGui::TextUnformatted(simDevice); + ImGui::PopStyleColor(); + return; + } + + int chA = model->GetChannelA(); + int chB = model->GetChannelB(); + + // build header label + std::string* name = GetStorage().GetStringRef("name"); + char label[128]; + if (!name->empty()) { + std::snprintf(label, sizeof(label), "%s [%d,%d]###name", name->c_str(), chA, + chB); + } else { + std::snprintf(label, sizeof(label), "Encoder[%d,%d]###name", chA, chB); + } + + // header + bool open = CollapsingHeader(label); + + // context menu to change name + if (PopupEditName("name", name)) { + model->SetName(name->c_str()); + } + + if (!open) return; + + ImGui::PushItemWidth(ImGui::GetFontSize() * 8); + // distance per pulse + if (auto distancePerPulseData = model->GetDistancePerPulseData()) { + double value = distancePerPulseData->GetValue(); + distancePerPulseData->LabelText("Dist/Count", "%.6f", value); + } + + // count + if (auto countData = model->GetCountData()) { + int value = countData->GetValue(); + if (ImGui::InputInt("##input", &value)) model->SetCount(value); + ImGui::SameLine(); + if (ImGui::Button("Reset")) { + model->SetCount(0); + } + ImGui::SameLine(); + ImGui::Selectable("Count"); + countData->EmitDrag(); + } + + // max period + { + double maxPeriod = model->GetMaxPeriod(); + ImGui::LabelText("Max Period", "%.6f", maxPeriod); + } + + // period + if (auto periodData = model->GetPeriodData()) { + double value = periodData->GetValue(); + if (periodData->InputDouble("Period", &value, 0, 0, "%.6g")) { + model->SetPeriod(value); + } + } + + // reverse direction + ImGui::LabelText("Reverse Direction", "%s", + model->GetReverseDirection() ? "true" : "false"); + + // direction + if (auto directionData = model->GetDirectionData()) { + static const char* options[] = {"reverse", "forward"}; + int value = directionData->GetValue() ? 1 : 0; + if (directionData->Combo("Direction", &value, options, 2)) { + model->SetDirection(value != 0); + } + } + + // distance + if (auto distanceData = model->GetDistanceData()) { + double value = distanceData->GetValue(); + if (distanceData->InputDouble("Distance", &value, 0, 0, "%.6g")) { + model->SetDistance(value); + } + } + + // rate + if (auto rateData = model->GetRateData()) { + double value = rateData->GetValue(); + if (rateData->InputDouble("Rate", &value, 0, 0, "%.6g")) { + model->SetRate(value); + } + } + ImGui::PopItemWidth(); +} + +void glass::DisplayEncoders(EncodersModel* model, wpi::StringRef noneMsg) { + bool hasAny = false; + model->ForEachEncoder([&](EncoderModel& encoder, int i) { + hasAny = true; + PushID(i); + DisplayEncoder(&encoder); + PopID(); + }); + if (!hasAny && !noneMsg.empty()) + ImGui::TextUnformatted(noneMsg.begin(), noneMsg.end()); +} diff --git a/glass/src/lib/native/cpp/hardware/LEDDisplay.cpp b/glass/src/lib/native/cpp/hardware/LEDDisplay.cpp new file mode 100644 index 0000000000..711b097701 --- /dev/null +++ b/glass/src/lib/native/cpp/hardware/LEDDisplay.cpp @@ -0,0 +1,91 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/hardware/LEDDisplay.h" + +#include "glass/Context.h" +#include "glass/support/ExtraGuiWidgets.h" + +using namespace glass; + +namespace { +struct IndicatorData { + std::vector values; + std::vector colors; +}; +} // namespace + +void glass::DisplayLEDDisplay(LEDDisplayModel* model, int index) { + wpi::SmallVector dataBuf; + auto data = model->GetData(dataBuf); + int length = data.size(); + bool running = model->IsRunning(); + auto& storage = GetStorage(); + + int* numColumns = storage.GetIntRef("columns", 10); + bool* serpentine = storage.GetBoolRef("serpentine", false); + int* order = storage.GetIntRef("order", LEDConfig::RowMajor); + int* start = storage.GetIntRef("start", LEDConfig::UpperLeft); + + ImGui::PushItemWidth(ImGui::GetFontSize() * 6); + ImGui::LabelText("Length", "%d", length); + ImGui::LabelText("Running", "%s", running ? "Yes" : "No"); + ImGui::InputInt("Columns", numColumns); + { + static const char* options[] = {"Row Major", "Column Major"}; + ImGui::Combo("Order", order, options, 2); + } + { + static const char* options[] = {"Upper Left", "Lower Left", "Upper Right", + "Lower Right"}; + ImGui::Combo("Start", start, options, 4); + } + ImGui::Checkbox("Serpentine", serpentine); + if (*numColumns < 1) *numColumns = 1; + ImGui::PopItemWidth(); + + // show as LED indicators + auto iData = storage.GetData(); + if (!iData) { + storage.SetData(std::make_shared()); + iData = storage.GetData(); + } + if (length > static_cast(iData->values.size())) + iData->values.resize(length); + if (length > static_cast(iData->colors.size())) + iData->colors.resize(length); + if (!running) { + iData->colors[0] = IM_COL32(128, 128, 128, 255); + for (int j = 0; j < length; ++j) iData->values[j] = -1; + } else { + for (int j = 0; j < length; ++j) { + iData->values[j] = j + 1; + iData->colors[j] = IM_COL32(data[j].r, data[j].g, data[j].b, 255); + } + } + + LEDConfig config; + config.serpentine = *serpentine; + config.order = static_cast(*order); + config.start = static_cast(*start); + + DrawLEDs(iData->values.data(), length, *numColumns, iData->colors.data(), 0, + 0, config); +} + +void glass::DisplayLEDDisplays(LEDDisplaysModel* model) { + bool hasAny = false; + + model->ForEachLEDDisplay([&](LEDDisplayModel& display, int i) { + hasAny = true; + if (model->GetNumLEDDisplays() > 1) ImGui::Text("LEDs[%d]", i); + PushID(i); + DisplayLEDDisplay(&display, i); + PopID(); + }); + if (!hasAny) ImGui::Text("No addressable LEDs"); +} diff --git a/glass/src/lib/native/cpp/hardware/PCM.cpp b/glass/src/lib/native/cpp/hardware/PCM.cpp new file mode 100644 index 0000000000..608172ad71 --- /dev/null +++ b/glass/src/lib/native/cpp/hardware/PCM.cpp @@ -0,0 +1,150 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/hardware/PCM.h" + +#include +#include + +#include +#include + +#include "glass/Context.h" +#include "glass/DataSource.h" +#include "glass/other/DeviceTree.h" +#include "glass/support/ExtraGuiWidgets.h" +#include "glass/support/IniSaverInfo.h" + +using namespace glass; + +bool glass::DisplayPCMSolenoids(PCMModel* model, int index, + bool outputsEnabled) { + wpi::SmallVector channels; + model->ForEachSolenoid([&](SolenoidModel& solenoid, int j) { + if (auto data = solenoid.GetOutputData()) { + if (j >= static_cast(channels.size())) channels.resize(j + 1); + channels[j] = (outputsEnabled && data->GetValue()) ? 1 : -1; + } + }); + + if (channels.empty()) return false; + + // show nonexistent channels as empty + for (auto&& ch : channels) { + if (ch == 0) ch = -2; + } + + // build header label + std::string* name = GetStorage().GetStringRef("name"); + char label[128]; + if (!name->empty()) { + std::snprintf(label, sizeof(label), "%s [%d]###name", name->c_str(), index); + } else { + std::snprintf(label, sizeof(label), "PCM[%d]###name", index); + } + + // header + bool open = CollapsingHeader(label); + + PopupEditName("name", name); + + ImGui::SetItemAllowOverlap(); + ImGui::SameLine(); + + // show channels as LED indicators + static const ImU32 colors[] = {IM_COL32(255, 255, 102, 255), + IM_COL32(128, 128, 128, 255)}; + DrawLEDs(channels.data(), channels.size(), channels.size(), colors); + + if (open) { + ImGui::PushItemWidth(ImGui::GetFontSize() * 4); + model->ForEachSolenoid([&](SolenoidModel& solenoid, int j) { + if (auto data = solenoid.GetOutputData()) { + PushID(j); + char solenoidName[64]; + auto& info = data->GetNameInfo(); + info.GetLabel(solenoidName, sizeof(solenoidName), "Solenoid", j); + data->LabelText(solenoidName, "%s", channels[j] == 1 ? "On" : "Off"); + info.PopupEditName(j); + PopID(); + } + }); + ImGui::PopItemWidth(); + } + + return true; +} + +void glass::DisplayPCMsSolenoids(PCMsModel* model, bool outputsEnabled, + wpi::StringRef noneMsg) { + bool hasAny = false; + model->ForEachPCM([&](PCMModel& pcm, int i) { + PushID(i); + if (DisplayPCMSolenoids(&pcm, i, outputsEnabled)) hasAny = true; + PopID(); + }); + if (!hasAny && !noneMsg.empty()) + ImGui::TextUnformatted(noneMsg.begin(), noneMsg.end()); +} + +void glass::DisplayCompressorDevice(PCMModel* model, int index, + bool outputsEnabled) { + auto compressor = model->GetCompressor(); + if (!compressor || !compressor->Exists()) return; + DisplayCompressorDevice(compressor, index, outputsEnabled); +} + +void glass::DisplayCompressorDevice(CompressorModel* model, int index, + bool outputsEnabled) { + char name[32]; + std::snprintf(name, sizeof(name), "Compressor[%d]", index); + if (BeginDevice(name)) { + // output enabled + if (auto runningData = model->GetRunningData()) { + bool value = outputsEnabled && runningData->GetValue(); + if (DeviceBoolean("Running", false, &value, runningData)) { + model->SetRunning(value); + } + } + + // closed loop enabled + if (auto enabledData = model->GetEnabledData()) { + int value = enabledData->GetValue() ? 1 : 0; + static const char* enabledOptions[] = {"disabled", "enabled"}; + if (DeviceEnum("Closed Loop", true, &value, enabledOptions, 2, + enabledData)) { + model->SetEnabled(value != 0); + } + } + + // pressure switch + if (auto pressureSwitchData = model->GetPressureSwitchData()) { + int value = pressureSwitchData->GetValue() ? 1 : 0; + static const char* switchOptions[] = {"full", "low"}; + if (DeviceEnum("Pressure", false, &value, switchOptions, 2, + pressureSwitchData)) { + model->SetPressureSwitch(value != 0); + } + } + + // compressor current + if (auto currentData = model->GetCurrentData()) { + double value = currentData->GetValue(); + if (DeviceDouble("Current (A)", false, &value, currentData)) { + model->SetCurrent(value); + } + } + + EndDevice(); + } +} + +void glass::DisplayCompressorsDevice(PCMsModel* model, bool outputsEnabled) { + model->ForEachPCM([&](PCMModel& pcm, int i) { + DisplayCompressorDevice(&pcm, i, outputsEnabled); + }); +} diff --git a/glass/src/lib/native/cpp/hardware/PDP.cpp b/glass/src/lib/native/cpp/hardware/PDP.cpp new file mode 100644 index 0000000000..1b978b9f73 --- /dev/null +++ b/glass/src/lib/native/cpp/hardware/PDP.cpp @@ -0,0 +1,92 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/hardware/PDP.h" + +#include +#include + +#include + +#include "glass/Context.h" +#include "glass/DataSource.h" +#include "glass/support/IniSaverInfo.h" + +using namespace glass; + +static float DisplayChannel(PDPModel& pdp, int channel) { + float width = 0; + if (auto currentData = pdp.GetCurrentData(channel)) { + ImGui::PushID(channel); + auto& leftInfo = currentData->GetNameInfo(); + char name[64]; + leftInfo.GetLabel(name, sizeof(name), "", channel); + double val = currentData->GetValue(); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4); + if (currentData->InputDouble(name, &val, 0, 0, "%.3f")) + pdp.SetCurrent(channel, val); + width = ImGui::GetItemRectSize().x; + leftInfo.PopupEditName(channel); + ImGui::PopID(); + } + return width; +} + +void glass::DisplayPDP(PDPModel* model, int index) { + char name[128]; + std::snprintf(name, sizeof(name), "PDP[%d]", index); + if (CollapsingHeader(name)) { + // temperature + if (auto tempData = model->GetTemperatureData()) { + double value = tempData->GetValue(); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4); + if (tempData->InputDouble("Temp", &value, 0, 0, "%.3f")) { + model->SetTemperature(value); + } + } + + // voltage + if (auto voltageData = model->GetVoltageData()) { + double value = voltageData->GetValue(); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4); + if (voltageData->InputDouble("Voltage", &value, 0, 0, "%.3f")) { + model->SetVoltage(value); + } + } + + // channel currents; show as two columns laid out like PDP + const int numChannels = model->GetNumChannels(); + ImGui::Text("Channel Current (A)"); + ImGui::Columns(2, "channels", false); + float maxWidth = ImGui::GetFontSize() * 13; + for (int left = 0, right = numChannels - 1; left < right; ++left, --right) { + float leftWidth = DisplayChannel(*model, left); + ImGui::NextColumn(); + + float rightWidth = DisplayChannel(*model, right); + ImGui::NextColumn(); + + float width = + (std::max)(leftWidth, rightWidth) * 2 + ImGui::GetFontSize() * 4; + if (width > maxWidth) maxWidth = width; + } + ImGui::Columns(1); + ImGui::Dummy(ImVec2(maxWidth, 0)); + } +} + +void glass::DisplayPDPs(PDPsModel* model, wpi::StringRef noneMsg) { + bool hasAny = false; + model->ForEachPDP([&](PDPModel& pdp, int i) { + hasAny = true; + PushID(i); + DisplayPDP(&pdp, i); + PopID(); + }); + if (!hasAny && !noneMsg.empty()) + ImGui::TextUnformatted(noneMsg.begin(), noneMsg.end()); +} diff --git a/glass/src/lib/native/cpp/hardware/PWM.cpp b/glass/src/lib/native/cpp/hardware/PWM.cpp new file mode 100644 index 0000000000..df20c68e2a --- /dev/null +++ b/glass/src/lib/native/cpp/hardware/PWM.cpp @@ -0,0 +1,62 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/hardware/PWM.h" + +#include + +#include "glass/Context.h" +#include "glass/DataSource.h" + +using namespace glass; + +void glass::DisplayPWM(PWMModel* model, int index, bool outputsEnabled) { + auto data = model->GetSpeedData(); + if (!data) return; + + // build label + std::string* name = GetStorage().GetStringRef("name"); + char label[128]; + if (!name->empty()) { + std::snprintf(label, sizeof(label), "%s [%d]###name", name->c_str(), index); + } else { + std::snprintf(label, sizeof(label), "PWM[%d]###name", index); + } + + int led = model->GetAddressableLED(); + + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4); + if (led >= 0) { + ImGui::LabelText(label, "LED[%d]", led); + } else { + float val = outputsEnabled ? data->GetValue() : 0; + data->LabelText(label, "%0.3f", val); + } + if (PopupEditName("name", name)) { + data->SetName(name->c_str()); + } +} + +void glass::DisplayPWMs(PWMsModel* model, bool outputsEnabled, + wpi::StringRef noneMsg) { + bool hasAny = false; + bool first = true; + model->ForEachPWM([&](PWMModel& pwm, int i) { + hasAny = true; + PushID(i); + + if (!first) + ImGui::Separator(); + else + first = false; + + DisplayPWM(&pwm, i, outputsEnabled); + PopID(); + }); + if (!hasAny && !noneMsg.empty()) + ImGui::TextUnformatted(noneMsg.begin(), noneMsg.end()); +} diff --git a/glass/src/lib/native/cpp/hardware/Relay.cpp b/glass/src/lib/native/cpp/hardware/Relay.cpp new file mode 100644 index 0000000000..e45fee4314 --- /dev/null +++ b/glass/src/lib/native/cpp/hardware/Relay.cpp @@ -0,0 +1,74 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/hardware/Relay.h" + +#include + +#include "glass/Context.h" +#include "glass/DataSource.h" +#include "glass/support/ExtraGuiWidgets.h" + +using namespace glass; + +void glass::DisplayRelay(RelayModel* model, int index, bool outputsEnabled) { + auto forwardData = model->GetForwardData(); + auto reverseData = model->GetReverseData(); + + if (!forwardData && !reverseData) { + return; + } + + bool forward = false; + bool reverse = false; + if (outputsEnabled) { + if (forwardData) forward = forwardData->GetValue(); + if (reverseData) reverse = reverseData->GetValue(); + } + + std::string* name = GetStorage().GetStringRef("name"); + ImGui::PushID("name"); + if (!name->empty()) + ImGui::Text("%s [%d]", name->c_str(), index); + else + ImGui::Text("Relay[%d]", index); + ImGui::PopID(); + if (PopupEditName("name", name)) { + if (forwardData) forwardData->SetName(name->c_str()); + if (reverseData) reverseData->SetName(name->c_str()); + } + ImGui::SameLine(); + + // show forward and reverse as LED indicators + static const ImU32 colors[] = {IM_COL32(255, 255, 102, 255), + IM_COL32(255, 0, 0, 255), + IM_COL32(128, 128, 128, 255)}; + int values[2] = {reverseData ? (reverse ? 2 : -2) : -3, + forwardData ? (forward ? 1 : -1) : -3}; + DataSource* sources[2] = {reverseData, forwardData}; + DrawLEDSources(values, sources, 2, 2, colors); +} + +void glass::DisplayRelays(RelaysModel* model, bool outputsEnabled, + wpi::StringRef noneMsg) { + bool hasAny = false; + bool first = true; + model->ForEachRelay([&](RelayModel& relay, int i) { + hasAny = true; + + if (!first) + ImGui::Separator(); + else + first = false; + + PushID(i); + DisplayRelay(&relay, i, outputsEnabled); + PopID(); + }); + if (!hasAny && !noneMsg.empty()) + ImGui::TextUnformatted(noneMsg.begin(), noneMsg.end()); +} diff --git a/glass/src/lib/native/cpp/hardware/RoboRio.cpp b/glass/src/lib/native/cpp/hardware/RoboRio.cpp new file mode 100644 index 0000000000..c874167cde --- /dev/null +++ b/glass/src/lib/native/cpp/hardware/RoboRio.cpp @@ -0,0 +1,87 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/hardware/RoboRio.h" + +#include + +#include "glass/Context.h" +#include "glass/DataSource.h" + +using namespace glass; + +static void DisplayRail(RoboRioRailModel& rail, const char* name) { + if (CollapsingHeader(name)) { + ImGui::PushID(name); + if (auto data = rail.GetVoltageData()) { + double val = data->GetValue(); + if (data->InputDouble("Voltage (V)", &val)) { + rail.SetVoltage(val); + } + } + + if (auto data = rail.GetCurrentData()) { + double val = data->GetValue(); + if (data->InputDouble("Current (A)", &val)) { + rail.SetCurrent(val); + } + } + + if (auto data = rail.GetActiveData()) { + static const char* options[] = {"inactive", "active"}; + int val = data->GetValue() ? 1 : 0; + if (data->Combo("Active", &val, options, 2)) { + rail.SetActive(val); + } + } + + if (auto data = rail.GetFaultsData()) { + int val = data->GetValue(); + if (data->InputInt("Faults", &val)) { + rail.SetFaults(val); + } + } + ImGui::PopID(); + } +} + +void glass::DisplayRoboRio(RoboRioModel* model) { + ImGui::Button("User Button"); + model->SetUserButton(ImGui::IsItemActive()); + + ImGui::PushItemWidth(ImGui::GetFontSize() * 8); + + if (CollapsingHeader("RoboRIO Input")) { + ImGui::PushID("RoboRIO Input"); + if (auto data = model->GetVInVoltageData()) { + double val = data->GetValue(); + if (data->InputDouble("Voltage (V)", &val)) { + model->SetVInVoltage(val); + } + } + + if (auto data = model->GetVInCurrentData()) { + double val = data->GetValue(); + if (data->InputDouble("Current (A)", &val)) { + model->SetVInCurrent(val); + } + } + ImGui::PopID(); + } + + if (auto rail = model->GetUser6VRail()) { + DisplayRail(*rail, "6V Rail"); + } + if (auto rail = model->GetUser5VRail()) { + DisplayRail(*rail, "5V Rail"); + } + if (auto rail = model->GetUser3V3Rail()) { + DisplayRail(*rail, "3.3V Rail"); + } + + ImGui::PopItemWidth(); +} diff --git a/glass/src/lib/native/cpp/other/DeviceTree.cpp b/glass/src/lib/native/cpp/other/DeviceTree.cpp new file mode 100644 index 0000000000..9d24d11f4e --- /dev/null +++ b/glass/src/lib/native/cpp/other/DeviceTree.cpp @@ -0,0 +1,161 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/other/DeviceTree.h" + +#include + +#include + +#include "glass/Context.h" +#include "glass/ContextInternal.h" +#include "glass/DataSource.h" + +using namespace glass; + +void DeviceTreeModel::Update() { + for (auto&& display : m_displays) { + if (display.first) display.first->Update(); + } +} + +bool DeviceTreeModel::Exists() { + for (auto&& display : m_displays) { + if (display.first && display.first->Exists()) return true; + } + return false; +} + +void DeviceTreeModel::Display() { + for (auto&& display : m_displays) { + if (display.second) display.second(display.first); + } +} + +void glass::HideDevice(const char* id) { gContext->deviceHidden[id] = true; } + +bool glass::BeginDevice(const char* id, ImGuiTreeNodeFlags flags) { + if (gContext->deviceHidden[id]) return false; + + PushID(id); + + // build label + std::string* name = GetStorage().GetStringRef("name"); + char label[128]; + std::snprintf(label, sizeof(label), "%s###name", + name->empty() ? id : name->c_str()); + + bool open = CollapsingHeader(label, flags); + PopupEditName("name", name); + + if (!open) PopID(); + return open; +} + +void glass::EndDevice() { PopID(); } + +static bool DeviceBooleanImpl(const char* name, bool readonly, bool* value) { + if (readonly) { + ImGui::LabelText(name, "%s", *value ? "true" : "false"); + } else { + static const char* boolOptions[] = {"false", "true"}; + int val = *value ? 1 : 0; + if (ImGui::Combo(name, &val, boolOptions, 2)) { + *value = val; + return true; + } + } + return false; +} + +static bool DeviceDoubleImpl(const char* name, bool readonly, double* value) { + if (readonly) { + ImGui::LabelText(name, "%.6f", *value); + return false; + } else { + return ImGui::InputDouble(name, value, 0, 0, "%.6f", + ImGuiInputTextFlags_EnterReturnsTrue); + } +} + +static bool DeviceEnumImpl(const char* name, bool readonly, int* value, + const char** options, int32_t numOptions) { + if (readonly) { + if (*value < 0 || *value >= numOptions) + ImGui::LabelText(name, "%d (unknown)", *value); + else + ImGui::LabelText(name, "%s", options[*value]); + return false; + } else { + return ImGui::Combo(name, value, options, numOptions); + } +} + +static bool DeviceIntImpl(const char* name, bool readonly, int32_t* value) { + if (readonly) { + ImGui::LabelText(name, "%" PRId32, *value); + return false; + } else { + return ImGui::InputScalar(name, ImGuiDataType_S32, value, nullptr, nullptr, + nullptr, ImGuiInputTextFlags_EnterReturnsTrue); + } +} + +static bool DeviceLongImpl(const char* name, bool readonly, int64_t* value) { + if (readonly) { + ImGui::LabelText(name, "%" PRId64, *value); + return false; + } else { + return ImGui::InputScalar(name, ImGuiDataType_S64, value, nullptr, nullptr, + nullptr, ImGuiInputTextFlags_EnterReturnsTrue); + } +} + +template +static inline bool DeviceValueImpl(const char* name, bool readonly, + const DataSource* source, F&& func, + Args... args) { + ImGui::SetNextItemWidth(ImGui::GetWindowWidth() * 0.5f); + if (!source) { + return func(name, readonly, args...); + } else { + ImGui::PushID(name); + bool rv = func("", readonly, args...); + ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::Selectable(name); + source->EmitDrag(); + ImGui::PopID(); + return rv; + } +} + +bool glass::DeviceBoolean(const char* name, bool readonly, bool* value, + const DataSource* source) { + return DeviceValueImpl(name, readonly, source, DeviceBooleanImpl, value); +} + +bool glass::DeviceDouble(const char* name, bool readonly, double* value, + const DataSource* source) { + return DeviceValueImpl(name, readonly, source, DeviceDoubleImpl, value); +} + +bool glass::DeviceEnum(const char* name, bool readonly, int* value, + const char** options, int32_t numOptions, + const DataSource* source) { + return DeviceValueImpl(name, readonly, source, DeviceEnumImpl, value, options, + numOptions); +} + +bool glass::DeviceInt(const char* name, bool readonly, int32_t* value, + const DataSource* source) { + return DeviceValueImpl(name, readonly, source, DeviceIntImpl, value); +} + +bool glass::DeviceLong(const char* name, bool readonly, int64_t* value, + const DataSource* source) { + return DeviceValueImpl(name, readonly, source, DeviceLongImpl, value); +} diff --git a/glass/src/lib/native/cpp/other/FMS.cpp b/glass/src/lib/native/cpp/other/FMS.cpp new file mode 100644 index 0000000000..8c6a26a030 --- /dev/null +++ b/glass/src/lib/native/cpp/other/FMS.cpp @@ -0,0 +1,140 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/other/FMS.h" + +#include +#include + +#include "glass/DataSource.h" + +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) { + if (!model->Exists() || model->IsReadOnly()) return DisplayFMSReadOnly(model); + + // FMS Attached + if (auto data = model->GetFmsAttachedData()) { + bool val = data->GetValue(); + if (ImGui::Checkbox("FMS Attached", &val)) model->SetFmsAttached(val); + data->EmitDrag(); + } + + // DS Attached + if (auto data = model->GetDsAttachedData()) { + bool val = data->GetValue(); + if (ImGui::Checkbox("DS Attached", &val)) model->SetDsAttached(val); + data->EmitDrag(); + } + + // Alliance Station + if (auto data = model->GetAllianceStationIdData()) { + int val = data->GetValue(); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + if (ImGui::Combo("Alliance Station", &val, stations, 6)) + model->SetAllianceStationId(val); + data->EmitDrag(); + } + + // 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", + ImGuiInputTextFlags_EnterReturnsTrue)) { + model->SetMatchTime(val); + } + data->EmitDrag(); + ImGui::SameLine(); + if (ImGui::Button("Reset")) { + model->SetMatchTime(0.0); + } + } + + // Game Specific Message + // make buffer full 64 width, null terminated, for editability + wpi::SmallString<64> gameSpecificMessage; + model->GetGameSpecificMessage(gameSpecificMessage); + gameSpecificMessage.resize(63); + gameSpecificMessage.push_back('\0'); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + if (ImGui::InputText("Game Specific", gameSpecificMessage.data(), + gameSpecificMessage.size(), + ImGuiInputTextFlags_EnterReturnsTrue)) { + model->SetGameSpecificMessage(gameSpecificMessage.data()); + } +} + +void glass::DisplayFMSReadOnly(FMSModel* model) { + bool exists = model->Exists(); + if (!exists) ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255)); + + if (auto data = model->GetEStopData()) { + ImGui::Selectable("E-Stopped: "); + data->EmitDrag(); + ImGui::SameLine(); + ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?"); + } + if (auto data = model->GetEnabledData()) { + ImGui::Selectable("Robot Enabled: "); + data->EmitDrag(); + ImGui::SameLine(); + ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?"); + } + if (auto data = model->GetTestData()) { + ImGui::Selectable("Test Mode: "); + data->EmitDrag(); + ImGui::SameLine(); + ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?"); + } + if (auto data = model->GetAutonomousData()) { + ImGui::Selectable("Autonomous Mode: "); + data->EmitDrag(); + ImGui::SameLine(); + ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?"); + } + if (auto data = model->GetFmsAttachedData()) { + ImGui::Selectable("FMS Attached: "); + data->EmitDrag(); + ImGui::SameLine(); + ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?"); + } + if (auto data = model->GetDsAttachedData()) { + ImGui::Selectable("DS Attached: "); + data->EmitDrag(); + ImGui::SameLine(); + ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?"); + } + if (auto data = model->GetAllianceStationIdData()) { + ImGui::Selectable("Alliance Station: "); + data->EmitDrag(); + ImGui::SameLine(); + ImGui::TextUnformatted(exists ? stations[static_cast(data->GetValue())] + : "?"); + } + if (auto data = model->GetMatchTimeData()) { + ImGui::Selectable("Match Time: "); + data->EmitDrag(); + ImGui::SameLine(); + if (exists) + ImGui::Text("%.1f", data->GetValue()); + else + ImGui::TextUnformatted("?"); + } + + wpi::SmallString<64> gameSpecificMessage; + model->GetGameSpecificMessage(gameSpecificMessage); + ImGui::Text("Game Specific: %s", exists ? gameSpecificMessage.c_str() : "?"); + + if (!exists) ImGui::PopStyleColor(); +} diff --git a/glass/src/lib/native/cpp/other/Field2D.cpp b/glass/src/lib/native/cpp/other/Field2D.cpp new file mode 100644 index 0000000000..5bb722ad26 --- /dev/null +++ b/glass/src/lib/native/cpp/other/Field2D.cpp @@ -0,0 +1,617 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/other/Field2D.h" + +#include +#include + +#define IMGUI_DEFINE_MATH_OPERATORS +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "glass/Context.h" +#include "glass/DataSource.h" + +using namespace glass; + +namespace gui = wpi::gui; + +namespace { + +// Per-frame field data (not persistent) +struct FieldFrameData { + // in screen coordinates + ImVec2 imageMin; + ImVec2 imageMax; + ImVec2 min; + ImVec2 max; + + float scale; // scaling from field units to screen units +}; + +// Object drag state +struct ObjectDragState { + int object = 0; + int corner = 0; + ImVec2 initialOffset; + double initialAngle = 0; +}; + +// Per-frame object data (not persistent) +class ObjectFrameData { + public: + explicit ObjectFrameData(FieldObjectModel& model, const FieldFrameData& ffd, + float width, float length); + void SetPosition(double x, double y); + // set and get rotation in radians + void SetRotation(double rot); + double GetRotation() const { + return units::convert(m_rot); + } + void UpdateFrameData(); + int IsHovered(const ImVec2& cursor) const; + bool HandleDrag(const ImVec2& cursor, int hitCorner, ObjectDragState* drag); + void Draw(ImDrawList* drawList, const gui::Texture& texture, + int hitCorner) const; + + // in window coordinates + ImVec2 m_center; + ImVec2 m_corners[4]; + ImVec2 m_arrow[3]; + + private: + FieldObjectModel& m_model; + const FieldFrameData& m_ffd; + + // scaled width/2 and length/2, in screen units + float m_width2; + float m_length2; + + float m_hitRadius; + + double m_x = 0; + double m_y = 0; + double m_rot = 0; +}; + +class ObjectGroupInfo { + public: + static constexpr float kDefaultWidth = 0.6858f; + static constexpr float kDefaultLength = 0.8204f; + + ObjectGroupInfo(); + + std::unique_ptr m_fileOpener; + float* m_pWidth; + float* m_pLength; + ObjectDragState m_dragState; + + void Reset(); + void LoadImage(); + const gui::Texture& GetTexture() const { return m_texture; } + + private: + bool LoadImageImpl(const char* fn); + + std::string* m_pFilename; + gui::Texture m_texture; +}; + +class FieldInfo { + public: + static constexpr float kDefaultWidth = 15.98f; + static constexpr float kDefaultHeight = 8.21f; + + FieldInfo(); + + std::unique_ptr m_fileOpener; + float* m_pWidth; + float* m_pHeight; + + void Reset(); + void LoadImage(); + void LoadJson(const wpi::Twine& jsonfile); + FieldFrameData GetFrameData(ImVec2 min, ImVec2 max) const; + void Draw(ImDrawList* drawList, const FieldFrameData& frameData) const; + + wpi::StringMap> m_objectGroups; + + private: + bool LoadImageImpl(const char* fn); + + std::string* m_pFilename; + gui::Texture m_texture; + int m_imageWidth; + int m_imageHeight; + int* m_pTop; + int* m_pLeft; + int* m_pBottom; + int* m_pRight; +}; + +} // namespace + +FieldInfo::FieldInfo() { + auto& storage = GetStorage(); + m_pFilename = storage.GetStringRef("image"); + m_pTop = storage.GetIntRef("top", 0); + m_pLeft = storage.GetIntRef("left", 0); + m_pBottom = storage.GetIntRef("bottom", -1); + m_pRight = storage.GetIntRef("right", -1); + m_pWidth = storage.GetFloatRef("width", kDefaultWidth); + m_pHeight = storage.GetFloatRef("height", kDefaultHeight); +} + +void FieldInfo::Reset() { + m_texture = gui::Texture{}; + m_pFilename->clear(); + m_imageWidth = 0; + m_imageHeight = 0; + *m_pTop = 0; + *m_pLeft = 0; + *m_pBottom = -1; + *m_pRight = -1; +} + +void FieldInfo::LoadImage() { + if (m_fileOpener && m_fileOpener->ready(0)) { + auto result = m_fileOpener->result(); + if (!result.empty()) { + if (wpi::StringRef(result[0]).endswith(".json")) { + LoadJson(result[0]); + } else { + LoadImageImpl(result[0].c_str()); + *m_pTop = 0; + *m_pLeft = 0; + *m_pBottom = -1; + *m_pRight = -1; + } + } + m_fileOpener.reset(); + } + if (!m_texture && !m_pFilename->empty()) { + if (!LoadImageImpl(m_pFilename->c_str())) m_pFilename->clear(); + } +} + +void FieldInfo::LoadJson(const wpi::Twine& jsonfile) { + std::error_code ec; + wpi::raw_fd_istream f(jsonfile, ec); + if (ec) { + wpi::errs() << "GUI: could not open field JSON file\n"; + return; + } + + // parse file + wpi::json j; + try { + j = wpi::json::parse(f); + } catch (const wpi::json::parse_error& e) { + wpi::errs() << "GUI: JSON: could not parse: " << e.what() << '\n'; + } + + // top level must be an object + if (!j.is_object()) { + wpi::errs() << "GUI: JSON: does not contain a top object\n"; + return; + } + + // image filename + std::string image; + try { + image = j.at("field-image").get(); + } catch (const wpi::json::exception& e) { + wpi::errs() << "GUI: JSON: could not read field-image: " << e.what() + << '\n'; + return; + } + + // corners + int top, left, bottom, right; + try { + top = j.at("field-corners").at("top-left").at(1).get(); + left = j.at("field-corners").at("top-left").at(0).get(); + bottom = j.at("field-corners").at("bottom-right").at(1).get(); + right = j.at("field-corners").at("bottom-right").at(0).get(); + } catch (const wpi::json::exception& e) { + wpi::errs() << "GUI: JSON: could not read field-corners: " << e.what() + << '\n'; + return; + } + + // size + float width; + float height; + try { + width = j.at("field-size").at(0).get(); + height = j.at("field-size").at(1).get(); + } catch (const wpi::json::exception& e) { + wpi::errs() << "GUI: JSON: could not read field-size: " << e.what() << '\n'; + return; + } + + // units for size + std::string unit; + try { + unit = j.at("field-unit").get(); + } catch (const wpi::json::exception& e) { + wpi::errs() << "GUI: JSON: could not read field-unit: " << e.what() << '\n'; + return; + } + + // convert size units to meters + if (unit == "foot" || unit == "feet") { + width = units::convert(width); + height = units::convert(height); + } + + // the image filename is relative to the json file + wpi::SmallString<128> pathname; + jsonfile.toVector(pathname); + wpi::sys::path::remove_filename(pathname); + wpi::sys::path::append(pathname, image); + + // load field image + if (!LoadImageImpl(pathname.c_str())) return; + + // save to field info + *m_pFilename = pathname.str(); + *m_pTop = top; + *m_pLeft = left; + *m_pBottom = bottom; + *m_pRight = right; + *m_pWidth = width; + *m_pHeight = height; +} + +bool FieldInfo::LoadImageImpl(const char* fn) { + wpi::outs() << "GUI: loading field image '" << fn << "'\n"; + auto texture = gui::Texture::CreateFromFile(fn); + if (!texture) { + wpi::errs() << "GUI: could not read field image\n"; + return false; + } + m_texture = std::move(texture); + m_imageWidth = m_texture.GetWidth(); + m_imageHeight = m_texture.GetHeight(); + *m_pFilename = fn; + return true; +} + +FieldFrameData FieldInfo::GetFrameData(ImVec2 min, ImVec2 max) const { + // fit the image into the window + if (m_texture && m_imageHeight != 0 && m_imageWidth != 0) + gui::MaxFit(&min, &max, m_imageWidth, m_imageHeight); + + FieldFrameData ffd; + ffd.imageMin = min; + ffd.imageMax = max; + + // size down the box by the image corners (if any) + if (*m_pBottom > 0 && *m_pRight > 0) { + min.x += *m_pLeft * (max.x - min.x) / m_imageWidth; + min.y += *m_pTop * (max.y - min.y) / m_imageHeight; + max.x -= (m_imageWidth - *m_pRight) * (max.x - min.x) / m_imageWidth; + max.y -= (m_imageHeight - *m_pBottom) * (max.y - min.y) / m_imageHeight; + } + + // draw the field "active area" as a yellow boundary box + gui::MaxFit(&min, &max, *m_pWidth, *m_pHeight); + + ffd.min = min; + ffd.max = max; + ffd.scale = (max.x - min.x) / *m_pWidth; + return ffd; +} + +void FieldInfo::Draw(ImDrawList* drawList, const FieldFrameData& ffd) const { + if (m_texture && m_imageHeight != 0 && m_imageWidth != 0) { + drawList->AddImage(m_texture, ffd.imageMin, ffd.imageMax); + } + + // draw the field "active area" as a yellow boundary box + drawList->AddRect(ffd.min, ffd.max, IM_COL32(255, 255, 0, 255)); +} + +ObjectGroupInfo::ObjectGroupInfo() { + auto& storage = GetStorage(); + m_pFilename = storage.GetStringRef("image"); + m_pWidth = storage.GetFloatRef("width", kDefaultWidth); + m_pLength = storage.GetFloatRef("length", kDefaultLength); +} + +void ObjectGroupInfo::Reset() { + m_texture = gui::Texture{}; + m_pFilename->clear(); +} + +void ObjectGroupInfo::LoadImage() { + if (m_fileOpener && m_fileOpener->ready(0)) { + auto result = m_fileOpener->result(); + if (!result.empty()) LoadImageImpl(result[0].c_str()); + m_fileOpener.reset(); + } + if (!m_texture && !m_pFilename->empty()) { + if (!LoadImageImpl(m_pFilename->c_str())) m_pFilename->clear(); + } +} + +bool ObjectGroupInfo::LoadImageImpl(const char* fn) { + wpi::outs() << "GUI: loading object image '" << fn << "'\n"; + auto texture = gui::Texture::CreateFromFile(fn); + if (!texture) { + wpi::errs() << "GUI: could not read object image\n"; + return false; + } + m_texture = std::move(texture); + *m_pFilename = fn; + return true; +} + +ObjectFrameData::ObjectFrameData(FieldObjectModel& model, + const FieldFrameData& ffd, float width, + float length) + : m_model{model}, + m_ffd{ffd}, + m_width2(ffd.scale * width / 2), + m_length2(ffd.scale * length / 2), + m_hitRadius((std::min)(m_width2, m_length2) / 2) { + if (auto xData = model.GetXData()) m_x = xData->GetValue(); + if (auto yData = model.GetYData()) m_y = yData->GetValue(); + if (auto rotationData = model.GetRotationData()) + m_rot = rotationData->GetValue(); + UpdateFrameData(); +} + +void ObjectFrameData::SetPosition(double x, double y) { + m_x = x; + m_y = y; + m_model.SetPosition(x, y); +} + +void ObjectFrameData::SetRotation(double rot) { + double rotDegrees = units::convert(rot); + // force to -180 to +180 range + rotDegrees = rotDegrees + std::ceil((-rotDegrees - 180) / 360) * 360; + m_rot = rotDegrees; + m_model.SetRotation(rotDegrees); +} + +void ObjectFrameData::UpdateFrameData() { + // (0,0) origin is bottom left + ImVec2 center(m_ffd.min.x + m_ffd.scale * m_x, + m_ffd.max.y - m_ffd.scale * m_y); + + // build rotated points around center + float length2 = m_length2; + float width2 = m_width2; + double rot = GetRotation(); + float cos_a = std::cos(-rot); + float sin_a = std::sin(-rot); + + m_corners[0] = center + ImRotate(ImVec2(-length2, -width2), cos_a, sin_a); + m_corners[1] = center + ImRotate(ImVec2(length2, -width2), cos_a, sin_a); + m_corners[2] = center + ImRotate(ImVec2(length2, width2), cos_a, sin_a); + m_corners[3] = center + ImRotate(ImVec2(-length2, width2), cos_a, sin_a); + m_arrow[0] = + center + ImRotate(ImVec2(-length2 / 2, -width2 / 2), cos_a, sin_a); + m_arrow[1] = center + ImRotate(ImVec2(length2 / 2, 0), cos_a, sin_a); + m_arrow[2] = + center + ImRotate(ImVec2(-length2 / 2, width2 / 2), cos_a, sin_a); + + m_center = center; +} + +int ObjectFrameData::IsHovered(const ImVec2& cursor) const { + // only allow initiation of dragging when invisible button is hovered; + // this prevents the window resize handles from simultaneously activating + // the drag functionality + if (!ImGui::IsItemHovered()) return 0; + + float hitRadiusSquared = m_hitRadius * m_hitRadius; + // it's within the hit radius of the center? + if (gui::GetDistSquared(cursor, m_center) < hitRadiusSquared) + return 1; + else if (gui::GetDistSquared(cursor, m_corners[0]) < hitRadiusSquared) + return 2; + else if (gui::GetDistSquared(cursor, m_corners[1]) < hitRadiusSquared) + return 3; + else if (gui::GetDistSquared(cursor, m_corners[2]) < hitRadiusSquared) + return 4; + else if (gui::GetDistSquared(cursor, m_corners[3]) < hitRadiusSquared) + return 5; + else + return 0; +} + +bool ObjectFrameData::HandleDrag(const ImVec2& cursor, int hitCorner, + ObjectDragState* drag) { + bool rv = false; + if (hitCorner > 0 && ImGui::IsMouseClicked(0)) { + if (hitCorner == 1) { + drag->corner = hitCorner; + drag->initialOffset = cursor - m_center; + } else { + drag->corner = hitCorner; + ImVec2 off = cursor - m_center; + drag->initialAngle = std::atan2(off.y, off.x) + GetRotation(); + } + rv = true; + } + + if (drag->corner > 0 && ImGui::IsMouseDown(0)) { + if (drag->corner == 1) { + ImVec2 newPos = cursor - drag->initialOffset; + SetPosition( + (std::clamp(newPos.x, m_ffd.min.x, m_ffd.max.x) - m_ffd.min.x) / + m_ffd.scale, + (m_ffd.max.y - std::clamp(newPos.y, m_ffd.min.y, m_ffd.max.y)) / + m_ffd.scale); + UpdateFrameData(); + } else { + ImVec2 off = cursor - m_center; + SetRotation(drag->initialAngle - std::atan2(off.y, off.x)); + } + } else { + drag->corner = 0; + } + + return rv; +} + +void ObjectFrameData::Draw(ImDrawList* drawList, const gui::Texture& texture, + int hitCorner) const { + if (texture) { + drawList->AddImageQuad(texture, m_corners[0], m_corners[1], m_corners[2], + m_corners[3]); + } else { + drawList->AddQuad(m_corners[0], m_corners[1], m_corners[2], m_corners[3], + IM_COL32(255, 0, 0, 255), 4.0); + drawList->AddTriangle(m_arrow[0], m_arrow[1], m_arrow[2], + IM_COL32(0, 255, 0, 255), 4.0); + } + + if (hitCorner > 0) { + if (hitCorner == 1) { + drawList->AddCircle(m_center, m_hitRadius, IM_COL32(0, 255, 0, 255)); + } else { + drawList->AddCircle(m_corners[hitCorner - 2], m_hitRadius, + IM_COL32(0, 255, 0, 255)); + } + } +} + +void glass::DisplayField2DSettings(Field2DModel* model) { + auto& storage = GetStorage(); + auto field = storage.GetData(); + if (!field) { + storage.SetData(std::make_shared()); + field = storage.GetData(); + } + + ImGui::PushItemWidth(ImGui::GetFontSize() * 4); + if (ImGui::CollapsingHeader("Field")) { + ImGui::PushID("Field"); + if (ImGui::Button("Choose image...")) { + field->m_fileOpener = std::make_unique( + "Choose field image", "", + std::vector{"Image File", + "*.jpg *.jpeg *.png *.bmp *.psd *.tga *.gif " + "*.hdr *.pic *.ppm *.pgm", + "PathWeaver JSON File", "*.json"}); + } + if (ImGui::Button("Reset image")) { + field->Reset(); + } + ImGui::InputFloat("Field Width", field->m_pWidth); + ImGui::InputFloat("Field Height", field->m_pHeight); + // ImGui::InputInt("Field Top", field->m_pTop); + // ImGui::InputInt("Field Left", field->m_pLeft); + // ImGui::InputInt("Field Right", field->m_pRight); + // ImGui::InputInt("Field Bottom", field->m_pBottom); + ImGui::PopID(); + } + + model->ForEachFieldObjectGroup([&](auto& groupModel, auto name) { + if (!groupModel.Exists()) return; + PushID(name); + auto& objGroupRef = field->m_objectGroups[name]; + if (!objGroupRef) objGroupRef = std::make_unique(); + auto objGroup = objGroupRef.get(); + + wpi::SmallString<64> nameBuf = name; + if (ImGui::CollapsingHeader(nameBuf.c_str())) { + if (ImGui::Button("Choose image...")) { + objGroup->m_fileOpener = std::make_unique( + "Choose object image", "", + std::vector{ + "Image File", + "*.jpg *.jpeg *.png *.bmp *.psd *.tga *.gif " + "*.hdr *.pic *.ppm *.pgm"}); + } + if (ImGui::Button("Reset image")) { + objGroup->Reset(); + } + ImGui::InputFloat("Width", objGroup->m_pWidth); + ImGui::InputFloat("Length", objGroup->m_pLength); + } + PopID(); + }); + ImGui::PopItemWidth(); +} + +void glass::DisplayField2D(Field2DModel* model, const ImVec2& contentSize) { + auto& storage = GetStorage(); + auto field = storage.GetData(); + if (!field) { + storage.SetData(std::make_shared()); + field = storage.GetData(); + } + + ImVec2 windowPos = ImGui::GetWindowPos(); + ImVec2 mousePos = ImGui::GetIO().MousePos; + + // for dragging to work, there needs to be a button (otherwise the window is + // dragged) + if (contentSize.x <= 0 || contentSize.y <= 0) return; + ImVec2 cursorPos = windowPos + ImGui::GetCursorPos(); // screen coords + ImGui::InvisibleButton("field", contentSize); + + // field + field->LoadImage(); + FieldFrameData ffd = field->GetFrameData(cursorPos, cursorPos + contentSize); + auto drawList = ImGui::GetWindowDrawList(); + field->Draw(drawList, ffd); + + model->ForEachFieldObjectGroup([&](auto& groupModel, auto name) { + if (!groupModel.Exists()) return; + PushID(name); + auto& objGroupRef = field->m_objectGroups[name]; + if (!objGroupRef) objGroupRef = std::make_unique(); + auto objGroup = objGroupRef.get(); + objGroup->LoadImage(); + + int i = 0; + groupModel.ForEachFieldObject([&](auto& objModel) { + ++i; + ObjectFrameData ofd{objModel, ffd, *objGroup->m_pWidth, + *objGroup->m_pLength}; + + int hitCorner = 0; + if (objGroup->m_dragState.object == 0 || + objGroup->m_dragState.object == i) { + hitCorner = ofd.IsHovered(mousePos); + if (ofd.HandleDrag(mousePos, hitCorner, &objGroup->m_dragState)) + objGroup->m_dragState.object = i; + } + + // draw + ofd.Draw(drawList, objGroup->GetTexture(), hitCorner); + }); + PopID(); + }); +} + +void Field2DView::Display() { + if (ImGui::BeginPopupContextItem()) { + DisplayField2DSettings(m_model); + ImGui::EndPopup(); + } + DisplayField2D(m_model, ImGui::GetWindowContentRegionMax() - + ImGui::GetWindowContentRegionMin()); +} diff --git a/simulation/halsim_gui/src/main/native/cpp/PlotGui.cpp b/glass/src/lib/native/cpp/other/Plot.cpp similarity index 56% rename from simulation/halsim_gui/src/main/native/cpp/PlotGui.cpp rename to glass/src/lib/native/cpp/other/Plot.cpp index ecaec63c76..0b3a20eadc 100644 --- a/simulation/halsim_gui/src/main/native/cpp/PlotGui.cpp +++ b/glass/src/lib/native/cpp/other/Plot.cpp @@ -5,59 +5,67 @@ /* the project. */ /*----------------------------------------------------------------------------*/ -#include "PlotGui.h" +#include "glass/other/Plot.h" + +#include #include #include #include #include #include +#include #include #define IMGUI_DEFINE_MATH_OPERATORS -#include #include #include +#include #include +#include #include +#include #include #include -#include -#include "GuiDataSource.h" -#include "HALSimGui.h" -#include "IniSaverInfo.h" -#include "IniSaverVector.h" +#include "glass/Context.h" +#include "glass/DataSource.h" +#include "glass/support/ExtraGuiWidgets.h" -using namespace halsimgui; +using namespace glass; namespace { +class PlotView; + struct PlotSeriesRef { + PlotView* view; size_t plotIndex; size_t seriesIndex; }; -class PlotSeries : public NameInfo, public OpenInfo { +class PlotSeries { public: explicit PlotSeries(wpi::StringRef id); - explicit PlotSeries(GuiDataSource* source, int yAxis = 0); + explicit PlotSeries(DataSource* source, int yAxis = 0); const std::string& GetId() const { return m_id; } void CheckSource(); - void SetSource(GuiDataSource* source); - GuiDataSource* GetSource() const { return m_source; } + void SetSource(DataSource* source); + DataSource* GetSource() const { return m_source; } + + void Clear() { m_size = 0; } bool ReadIni(wpi::StringRef name, wpi::StringRef value); void WriteIni(ImGuiTextBuffer* out); - bool EmitPlot(double now, size_t i, size_t plotIndex); - bool EmitSettings(size_t i, size_t plotIndex); - bool EmitSettingsDetail(size_t i); - void EmitDragDropPayload(size_t i, size_t plotIndex); + enum Action { kNone, kMoveUp, kMoveDown, kDelete }; + Action EmitPlot(PlotView& view, double now, size_t i, size_t plotIndex); + void EmitSettings(size_t i); + void EmitDragDropPayload(PlotView& view, size_t i, size_t plotIndex); - void GetLabel(char* buf, size_t size) const; + const char* GetName() const; int GetYAxis() const { return m_yAxis; } void SetYAxis(int yAxis) { m_yAxis = yAxis; } @@ -67,18 +75,20 @@ class PlotSeries : public NameInfo, public OpenInfo { return m_digital == kDigital || (m_digital == kAuto && m_source && m_source->IsDigital()); } - void AppendValue(double value); + void AppendValue(double value, uint64_t time); // source linkage - GuiDataSource* m_source = nullptr; + DataSource* m_source = nullptr; wpi::sig::ScopedConnection m_sourceCreatedConn; wpi::sig::ScopedConnection m_newValueConn; std::string m_id; // user settings + std::string m_name; int m_yAxis = 0; ImVec4 m_color = IMPLOT_AUTO_COL; int m_marker = 0; + float m_weight = IMPLOT_AUTO; enum Digital { kAuto, kDigital, kAnalog }; int m_digital = 0; @@ -93,25 +103,29 @@ class PlotSeries : public NameInfo, public OpenInfo { ImPlotPoint m_data[kMaxSize]; }; -class Plot : public NameInfo, public OpenInfo { +class Plot { public: Plot(); + bool ReadIni(wpi::StringRef name, wpi::StringRef value); void WriteIni(ImGuiTextBuffer* out); - void GetLabel(char* buf, size_t size, int index) const; - void GetName(char* buf, size_t size, int index) const; + void Clear(); - void DragDropTarget(size_t i, bool inPlot); - void EmitPlot(double now, size_t i); + void DragDropTarget(PlotView& view, size_t i, bool inPlot); + void EmitPlot(PlotView& view, double now, bool paused, size_t i); void EmitSettings(size_t i); + const std::string& GetName() const { return m_name; } + std::vector> m_series; private: void EmitSettingsLimits(int axis); + std::string m_name; bool m_visible = true; + bool m_showPause = true; unsigned int m_plotFlags = ImPlotFlags_Default; bool m_lockPrevX = false; bool m_paused = false; @@ -127,27 +141,44 @@ class Plot : public NameInfo, public OpenInfo { PlotRange m_axisRange[3]; ImPlotRange m_xaxisRange; // read from plot, used for lockPrevX }; + +class PlotView : public View { + public: + explicit PlotView(PlotProvider* provider) : m_provider{provider} {} + + void Clear(); + + void Display() override; + + void MovePlot(PlotView* fromView, size_t fromIndex, size_t toIndex); + + void MovePlotSeries(PlotView* fromView, size_t fromPlotIndex, + size_t fromSeriesIndex, size_t toPlotIndex, + size_t toSeriesIndex, int yAxis = -1); + + PlotProvider* m_provider; + std::vector> m_plots; +}; + } // namespace -static IniSaverVector gPlots{"Plot"}; - PlotSeries::PlotSeries(wpi::StringRef id) : m_id(id) { - if (GuiDataSource* source = GuiDataSource::Find(id)) { + if (DataSource* source = DataSource::Find(id)) { SetSource(source); return; } CheckSource(); } -PlotSeries::PlotSeries(GuiDataSource* source, int yAxis) : m_yAxis(yAxis) { +PlotSeries::PlotSeries(DataSource* source, int yAxis) : m_yAxis(yAxis) { SetSource(source); } void PlotSeries::CheckSource() { if (!m_newValueConn.connected() && !m_sourceCreatedConn.connected()) { m_source = nullptr; - m_sourceCreatedConn = GuiDataSource::sourceCreated.connect_connection( - [this](const char* id, GuiDataSource* source) { + m_sourceCreatedConn = DataSource::sourceCreated.connect_connection( + [this](const char* id, DataSource* source) { if (m_id == id) { SetSource(source); m_sourceCreatedConn.disconnect(); @@ -156,7 +187,7 @@ void PlotSeries::CheckSource() { } } -void PlotSeries::SetSource(GuiDataSource* source) { +void PlotSeries::SetSource(DataSource* source) { m_source = source; m_id = source->GetId(); @@ -164,11 +195,11 @@ void PlotSeries::SetSource(GuiDataSource* source) { m_data[m_size++] = ImPlotPoint{wpi::Now() * 1.0e-6, source->GetValue()}; m_newValueConn = source->valueChanged.connect_connection( - [this](double value) { AppendValue(value); }); + [this](double value, uint64_t time) { AppendValue(value, time); }); } -void PlotSeries::AppendValue(double value) { - double time = wpi::Now() * 1.0e-6; +void PlotSeries::AppendValue(double value, uint64_t timeUs) { + double time = (timeUs != 0 ? timeUs : wpi::Now()) * 1.0e-6; if (IsDigital()) { if (m_size < kMaxSize) { m_data[m_size] = ImPlotPoint{time, value}; @@ -209,8 +240,10 @@ void PlotSeries::AppendValue(double value) { } bool PlotSeries::ReadIni(wpi::StringRef name, wpi::StringRef value) { - if (NameInfo::ReadIni(name, value)) return true; - if (OpenInfo::ReadIni(name, value)) return true; + if (name == "name") { + m_name = value; + return true; + } if (name == "yAxis") { int num; if (value.getAsInteger(10, num)) return true; @@ -226,6 +259,9 @@ bool PlotSeries::ReadIni(wpi::StringRef name, wpi::StringRef value) { if (value.getAsInteger(10, num)) return true; m_marker = num; return true; + } else if (name == "weight") { + std::sscanf(value.data(), "%f", &m_weight); + return true; } else if (name == "digital") { int num; if (value.getAsInteger(10, num)) return true; @@ -246,27 +282,28 @@ bool PlotSeries::ReadIni(wpi::StringRef name, wpi::StringRef value) { } void PlotSeries::WriteIni(ImGuiTextBuffer* out) { - NameInfo::WriteIni(out); - OpenInfo::WriteIni(out); out->appendf( - "yAxis=%d\ncolor=%u\nmarker=%d\ndigital=%d\n" + "name=%s\nyAxis=%d\ncolor=%u\nmarker=%d\nweight=%f\ndigital=%d\n" "digitalBitHeight=%d\ndigitalBitGap=%d\n", - m_yAxis, static_cast(ImColor(m_color)), m_marker, m_digital, - m_digitalBitHeight, m_digitalBitGap); + m_name.c_str(), m_yAxis, static_cast(ImColor(m_color)), m_marker, + m_weight, m_digital, m_digitalBitHeight, m_digitalBitGap); } -void PlotSeries::GetLabel(char* buf, size_t size) const { - const char* name = GetName(); - if (name[0] == '\0' && m_newValueConn.connected()) name = m_source->GetName(); - if (name[0] == '\0') name = m_id.c_str(); - std::snprintf(buf, size, "%s###%s", name, m_id.c_str()); +const char* PlotSeries::GetName() const { + if (!m_name.empty()) return m_name.c_str(); + if (m_newValueConn.connected()) { + auto sourceName = m_source->GetName(); + if (sourceName[0] != '\0') return sourceName; + } + return m_id.c_str(); } -bool PlotSeries::EmitPlot(double now, size_t i, size_t plotIndex) { +PlotSeries::Action PlotSeries::EmitPlot(PlotView& view, double now, size_t i, + size_t plotIndex) { CheckSource(); char label[128]; - GetLabel(label, sizeof(label)); + std::snprintf(label, sizeof(label), "%s###name", GetName()); int size = m_size; int offset = m_offset; @@ -292,7 +329,7 @@ bool PlotSeries::EmitPlot(double now, size_t i, size_t plotIndex) { }; if (m_color.w == IMPLOT_AUTO_COL.w) m_color = ImPlot::GetColormapColor(i); - ImPlot::PushStyleColor(ImPlotCol_Line, m_color); + ImPlot::SetNextLineStyle(m_color, m_weight); if (IsDigital()) { ImPlot::PushStyleVar(ImPlotStyleVar_DigitalBitHeight, m_digitalBitHeight); ImPlot::PushStyleVar(ImPlotStyleVar_DigitalBitGap, m_digitalBitGap); @@ -304,98 +341,48 @@ bool PlotSeries::EmitPlot(double now, size_t i, size_t plotIndex) { ImPlot::SetNextMarkerStyle(m_marker - 1); ImPlot::PlotLine(label, getter, &getterData, size + 1); } - ImPlot::PopStyleColor(); // DND source for PlotSeries if (ImPlot::BeginLegendDragDropSource(label)) { - EmitDragDropPayload(i, plotIndex); + EmitDragDropPayload(view, i, plotIndex); ImPlot::EndLegendDragDropSource(); } - // Plot-specific variant of IniSaverInfo::PopupEditName() that also - // allows editing of other settings - bool rv = false; + // Edit settings via popup + Action rv = kNone; if (ImPlot::BeginLegendPopup(label)) { if (ImGui::Button("Close")) ImGui::CloseCurrentPopup(); - ImGui::Text("Edit name:"); - if (InputTextName("##edit", ImGuiInputTextFlags_EnterReturnsTrue)) { + ImGui::Text("Edit series name:"); + ImGui::InputText("##editname", &m_name); + if (ImGui::Button("Move Up")) { ImGui::CloseCurrentPopup(); + rv = kMoveUp; } - rv = EmitSettingsDetail(i); + ImGui::SameLine(); + if (ImGui::Button("Move Down")) { + ImGui::CloseCurrentPopup(); + rv = kMoveDown; + } + ImGui::SameLine(); + if (ImGui::Button("Delete")) { + ImGui::CloseCurrentPopup(); + rv = kDelete; + } + EmitSettings(i); ImPlot::EndLegendPopup(); } return rv; } -void PlotSeries::EmitDragDropPayload(size_t i, size_t plotIndex) { - PlotSeriesRef ref = {plotIndex, i}; +void PlotSeries::EmitDragDropPayload(PlotView& view, size_t i, + size_t plotIndex) { + PlotSeriesRef ref = {&view, plotIndex, i}; ImGui::SetDragDropPayload("PlotSeries", &ref, sizeof(ref)); - const char* name = GetName(); - if (name[0] == '\0' && m_newValueConn.connected()) name = m_source->GetName(); - if (name[0] == '\0') name = m_id.c_str(); - ImGui::TextUnformatted(name); + ImGui::TextUnformatted(GetName()); } -static void MovePlotSeries(size_t fromPlotIndex, size_t fromSeriesIndex, - size_t toPlotIndex, size_t toSeriesIndex, - int yAxis = -1) { - if (fromPlotIndex == toPlotIndex) { - // need to handle this specially as the index of the old location changes - if (fromSeriesIndex != toSeriesIndex) { - auto& plotSeries = gPlots[fromPlotIndex].m_series; - auto val = std::move(plotSeries[fromSeriesIndex]); - // only set Y-axis if actually set - if (yAxis != -1) val->SetYAxis(yAxis); - plotSeries.insert(plotSeries.begin() + toSeriesIndex, std::move(val)); - plotSeries.erase(plotSeries.begin() + fromSeriesIndex + - (fromSeriesIndex > toSeriesIndex ? 1 : 0)); - } - } else { - auto& fromPlot = gPlots[fromPlotIndex]; - auto& toPlot = gPlots[toPlotIndex]; - // always set Y-axis if moving plots - fromPlot.m_series[fromSeriesIndex]->SetYAxis(yAxis == -1 ? 0 : yAxis); - toPlot.m_series.insert(toPlot.m_series.begin() + toSeriesIndex, - std::move(fromPlot.m_series[fromSeriesIndex])); - fromPlot.m_series.erase(fromPlot.m_series.begin() + fromSeriesIndex); - } -} - -bool PlotSeries::EmitSettings(size_t i, size_t plotIndex) { - char label[128]; - GetLabel(label, sizeof(label)); - - bool open = ImGui::CollapsingHeader( - label, IsOpen() ? ImGuiTreeNodeFlags_DefaultOpen : 0); - - // DND source for PlotSeries - if (ImGui::BeginDragDropSource()) { - EmitDragDropPayload(i, plotIndex); - ImGui::EndDragDropSource(); - } - - // If another PlotSeries is dropped, move it before this series - if (ImGui::BeginDragDropTarget()) { - if (const ImGuiPayload* payload = - ImGui::AcceptDragDropPayload("PlotSeries")) { - auto ref = static_cast(payload->Data); - MovePlotSeries(ref->plotIndex, ref->seriesIndex, plotIndex, i); - } - } - - SetOpen(open); - PopupEditName(i); - if (!open) return false; - - return EmitSettingsDetail(i); -} - -bool PlotSeries::EmitSettingsDetail(size_t i) { - if (ImGui::Button("Delete")) { - return true; - } - +void PlotSeries::EmitSettings(size_t i) { // Line color { ImGui::ColorEdit3("Color", &m_color.x, ImGuiColorEditFlags_NoInputs); @@ -403,6 +390,12 @@ bool PlotSeries::EmitSettingsDetail(size_t i) { if (ImGui::Button("Default")) m_color = ImPlot::GetColormapColor(i); } + // Line weight + { + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6); + ImGui::InputFloat("Weight", &m_weight, 0.1f, 1.0f, "%.1f"); + } + // Digital { static const char* const options[] = {"Auto", "Digital", "Analog"}; @@ -441,8 +434,6 @@ bool PlotSeries::EmitSettingsDetail(size_t i) { sizeof(options) / sizeof(options[0])); } } - - return false; } Plot::Plot() { @@ -452,13 +443,19 @@ Plot::Plot() { } bool Plot::ReadIni(wpi::StringRef name, wpi::StringRef value) { - if (NameInfo::ReadIni(name, value)) return true; - if (OpenInfo::ReadIni(name, value)) return true; - if (name == "visible") { + if (name == "name") { + m_name = value; + return true; + } else if (name == "visible") { int num; if (value.getAsInteger(10, num)) return true; m_visible = num != 0; return true; + } else if (name == "showPause") { + int num; + if (value.getAsInteger(10, num)) return true; + m_showPause = num != 0; + return true; } else if (name == "lockPrevX") { int num; if (value.getAsInteger(10, num)) return true; @@ -529,13 +526,11 @@ bool Plot::ReadIni(wpi::StringRef name, wpi::StringRef value) { } void Plot::WriteIni(ImGuiTextBuffer* out) { - NameInfo::WriteIni(out); - OpenInfo::WriteIni(out); out->appendf( - "visible=%d\nlockPrevX=%d\nlegend=%d\nyaxis2=%d\nyaxis3=%d\n" - "viewTime=%d\nheight=%d\n", - m_visible ? 1 : 0, m_lockPrevX ? 1 : 0, - (m_plotFlags & ImPlotFlags_Legend) ? 1 : 0, + "name=%s\nvisible=%d\nshowPause=%d\nlockPrevX=%d\nlegend=%d\n" + "yaxis2=%d\nyaxis3=%d\nviewTime=%d\nheight=%d\n", + m_name.c_str(), m_visible ? 1 : 0, m_showPause ? 1 : 0, + m_lockPrevX ? 1 : 0, (m_plotFlags & ImPlotFlags_Legend) ? 1 : 0, (m_plotFlags & ImPlotFlags_YAxis2) ? 1 : 0, (m_plotFlags & ImPlotFlags_YAxis3) ? 1 : 0, static_cast(m_viewTime * 1000), m_height); @@ -548,25 +543,11 @@ void Plot::WriteIni(ImGuiTextBuffer* out) { } } -void Plot::GetLabel(char* buf, size_t size, int index) const { - const char* name = NameInfo::GetName(); - if (name[0] != '\0') { - std::snprintf(buf, size, "%s##Plot%d", name, index); - } else { - std::snprintf(buf, size, "Plot %d##Plot%d", index, index); - } +void Plot::Clear() { + for (auto&& series : m_series) series->Clear(); } -void Plot::GetName(char* buf, size_t size, int index) const { - const char* name = NameInfo::GetName(); - if (name[0] != '\0') { - std::snprintf(buf, size, "%s", name); - } else { - std::snprintf(buf, size, "Plot %d", index); - } -} - -void Plot::DragDropTarget(size_t i, bool inPlot) { +void Plot::DragDropTarget(PlotView& view, size_t i, bool inPlot) { if (!ImGui::BeginDragDropTarget()) return; // handle dragging onto a specific Y axis int yAxis = -1; @@ -580,7 +561,7 @@ void Plot::DragDropTarget(size_t i, bool inPlot) { } if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("DataSource")) { - auto source = *static_cast(payload->Data); + auto source = *static_cast(payload->Data); // don't add duplicates unless it's onto a different Y axis auto it = std::find_if(m_series.begin(), m_series.end(), [=](const auto& elem) { @@ -594,40 +575,35 @@ void Plot::DragDropTarget(size_t i, bool inPlot) { } else if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("PlotSeries")) { auto ref = static_cast(payload->Data); - MovePlotSeries(ref->plotIndex, ref->seriesIndex, i, m_series.size(), yAxis); + view.MovePlotSeries(ref->view, ref->plotIndex, ref->seriesIndex, i, + m_series.size(), yAxis); } else if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("Plot")) { - auto fromPlotIndex = *static_cast(payload->Data); - if (i != fromPlotIndex) { - auto val = std::move(gPlots[fromPlotIndex]); - gPlots.insert(gPlots.begin() + i, std::move(val)); - gPlots.erase(gPlots.begin() + fromPlotIndex + - (fromPlotIndex > i ? 1 : 0)); - } + auto ref = static_cast(payload->Data); + view.MovePlot(ref->view, ref->plotIndex, i); } } -void Plot::EmitPlot(double now, size_t i) { +void Plot::EmitPlot(PlotView& view, double now, bool paused, size_t i) { if (!m_visible) return; bool lockX = (i != 0 && m_lockPrevX); - if (!lockX && ImGui::Button(m_paused ? "Resume" : "Pause")) + if (!lockX && m_showPause && ImGui::Button(m_paused ? "Resume" : "Pause")) m_paused = !m_paused; char label[128]; - GetLabel(label, sizeof(label), i); + std::snprintf(label, sizeof(label), "%s##plot", m_name.c_str()); if (lockX) { - ImPlot::SetNextPlotLimitsX(gPlots[i - 1].m_xaxisRange.Min, - gPlots[i - 1].m_xaxisRange.Max, + ImPlot::SetNextPlotLimitsX(view.m_plots[i - 1]->m_xaxisRange.Min, + view.m_plots[i - 1]->m_xaxisRange.Max, ImGuiCond_Always); } else { // also force-pause plots if overall timing is paused - ImPlot::SetNextPlotLimitsX(now - m_viewTime, now, - (m_paused || HALSIM_IsTimingPaused()) - ? ImGuiCond_Once - : ImGuiCond_Always); + ImPlot::SetNextPlotLimitsX( + now - m_viewTime, now, + (paused || m_paused) ? ImGuiCond_Once : ImGuiCond_Always); } ImPlotAxisFlags yFlags[3] = {ImPlotAxisFlags_Default, @@ -646,11 +622,24 @@ void Plot::EmitPlot(double now, size_t i) { m_plotFlags, ImPlotAxisFlags_Default, yFlags[0], yFlags[1], yFlags[2])) { for (size_t j = 0; j < m_series.size(); ++j) { - if (m_series[j]->EmitPlot(now, j, i)) { - m_series.erase(m_series.begin() + j); + ImGui::PushID(j); + switch (m_series[j]->EmitPlot(view, now, j, i)) { + case PlotSeries::kMoveUp: + if (j > 0) std::swap(m_series[j - 1], m_series[j]); + break; + case PlotSeries::kMoveDown: + if (j < (m_series.size() - 1)) + std::swap(m_series[j], m_series[j + 1]); + break; + case PlotSeries::kDelete: + m_series.erase(m_series.begin() + j); + break; + default: + break; } + ImGui::PopID(); } - DragDropTarget(i, true); + DragDropTarget(view, i, true); m_xaxisRange = ImPlot::GetPlotLimits().X; ImPlot::EndPlot(); } @@ -678,108 +667,11 @@ void Plot::EmitSettingsLimits(int axis) { ImGui::Unindent(); } -// Delete button (X in circle), based on ImGui::CloseButton() -static bool DeleteButton(ImGuiID id, const ImVec2& pos) { - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - - // We intentionally allow interaction when clipped so that a mechanical - // Alt,Right,Validate sequence close a window. (this isn't the regular - // behavior of buttons, but it doesn't affect the user much because navigation - // tends to keep items visible). - const ImRect bb( - pos, pos + ImVec2(g.FontSize, g.FontSize) + g.Style.FramePadding * 2.0f); - bool is_clipped = !ImGui::ItemAdd(bb, id); - - bool hovered, held; - bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held); - if (is_clipped) return pressed; - - // Render - ImU32 col = - ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered); - ImVec2 center = bb.GetCenter(); - if (hovered) - window->DrawList->AddCircleFilled( - center, ImMax(2.0f, g.FontSize * 0.5f + 1.0f), col, 12); - - ImU32 cross_col = ImGui::GetColorU32(ImGuiCol_Text); - window->DrawList->AddCircle(center, ImMax(2.0f, g.FontSize * 0.5f + 1.0f), - cross_col, 12); - float cross_extent = g.FontSize * 0.5f * 0.5f - 1.0f; - center -= ImVec2(0.5f, 0.5f); - window->DrawList->AddLine(center + ImVec2(+cross_extent, +cross_extent), - center + ImVec2(-cross_extent, -cross_extent), - cross_col, 1.0f); - window->DrawList->AddLine(center + ImVec2(+cross_extent, -cross_extent), - center + ImVec2(-cross_extent, +cross_extent), - cross_col, 1.0f); - - return pressed; -} - void Plot::EmitSettings(size_t i) { - char label[128]; - GetLabel(label, sizeof(label), i); - - bool open = ImGui::CollapsingHeader( - label, ImGuiTreeNodeFlags_AllowItemOverlap | - ImGuiTreeNodeFlags_ClipLabelForTrailingButton | - (IsOpen() ? ImGuiTreeNodeFlags_DefaultOpen : 0)); - - { - // Create a small overlapping delete button - ImGuiWindow* window = ImGui::GetCurrentWindow(); - ImGuiContext& g = *GImGui; - ImGuiItemHoveredDataBackup last_item_backup; - ImGuiID id = window->GetID(label); - float button_size = g.FontSize; - float button_x = ImMax(window->DC.LastItemRect.Min.x, - window->DC.LastItemRect.Max.x - - g.Style.FramePadding.x * 2.0f - button_size); - float button_y = window->DC.LastItemRect.Min.y; - if (DeleteButton(window->GetID(reinterpret_cast( - static_cast(id) + 1)), - ImVec2(button_x, button_y))) { - gPlots.erase(gPlots.begin() + i); - return; - } - last_item_backup.Restore(); - } - - // DND source for Plot - if (ImGui::BeginDragDropSource()) { - ImGui::SetDragDropPayload("Plot", &i, sizeof(i)); - char name[64]; - GetName(name, sizeof(name), i); - ImGui::TextUnformatted(name); - ImGui::EndDragDropSource(); - } - DragDropTarget(i, false); - SetOpen(open); - PopupEditName(i); - if (!open) return; - ImGui::PushID(i); -#if 0 - if (ImGui::Button("Move Up") && i > 0) { - std::swap(gPlots[i - 1], gPlots[i]); - ImGui::PopID(); - return; - } - ImGui::SameLine(); - if (ImGui::Button("Move Down") && i < (gPlots.size() - 1)) { - std::swap(gPlots[i], gPlots[i + 1]); - ImGui::PopID(); - return; - } - ImGui::SameLine(); - if (ImGui::Button("Delete")) { - gPlots.erase(gPlots.begin() + i); - ImGui::PopID(); - return; - } -#endif + ImGui::Text("Edit plot name:"); + ImGui::InputText("##editname", &m_name); ImGui::Checkbox("Visible", &m_visible); + ImGui::Checkbox("Show Pause Button", &m_showPause); ImGui::CheckboxFlags("Show Legend", &m_plotFlags, ImPlotFlags_Legend); if (i != 0) ImGui::Checkbox("Lock X-axis to previous plot", &m_lockPrevX); ImGui::TextUnformatted("Primary Y-Axis"); @@ -794,101 +686,247 @@ void Plot::EmitSettings(size_t i) { if (ImGui::InputInt("Height", &m_height, 10)) { if (m_height < 0) m_height = 0; } +} - ImGui::Indent(); - for (size_t j = 0; j < m_series.size(); ++j) { - ImGui::PushID(j); - if (m_series[j]->EmitSettings(j, i)) { - m_series.erase(m_series.begin() + j); +void PlotView::Clear() { + for (auto&& plot : m_plots) plot->Clear(); +} + +void PlotView::Display() { + if (ImGui::BeginPopupContextItem()) { + if (ImGui::Button("Add plot")) + m_plots.emplace_back(std::make_unique()); + + for (size_t i = 0; i < m_plots.size(); ++i) { + auto& plot = m_plots[i]; + ImGui::PushID(i); + + char name[64]; + if (!plot->GetName().empty()) + std::snprintf(name, sizeof(name), "%s", plot->GetName().c_str()); + else + std::snprintf(name, sizeof(name), "Plot %d", static_cast(i)); + + char label[90]; + std::snprintf(label, sizeof(label), "%s###header%d", name, + static_cast(i)); + + bool open = ImGui::CollapsingHeader(label); + + // DND source and target for Plot + if (ImGui::BeginDragDropSource()) { + PlotSeriesRef ref = {this, i, 0}; + ImGui::SetDragDropPayload("Plot", &ref, sizeof(ref)); + ImGui::TextUnformatted(name); + ImGui::EndDragDropSource(); + } + plot->DragDropTarget(*this, i, false); + + if (open) { + if (ImGui::Button("Move Up")) { + if (i > 0) std::swap(m_plots[i - 1], plot); + } + + ImGui::SameLine(); + if (ImGui::Button("Move Down")) { + if (i < (m_plots.size() - 1)) std::swap(plot, m_plots[i + 1]); + } + + ImGui::SameLine(); + if (ImGui::Button("Delete")) { + m_plots.erase(m_plots.begin() + i); + ImGui::PopID(); + continue; + } + + plot->EmitSettings(i); + } + + ImGui::PopID(); } - ImGui::PopID(); - } - ImGui::Unindent(); - ImGui::PopID(); -} - -static void DisplayPlot() { - if (gPlots.empty()) { - ImGui::Text("No Plots"); - return; + ImGui::EndPopup(); } - double now = wpi::Now() * 1.0e-6; - for (size_t i = 0; i < gPlots.size(); ++i) { + + if (m_plots.empty()) { + if (ImGui::Button("Add plot")) + m_plots.emplace_back(std::make_unique()); + + // Make "add plot" button a DND target for Plot + if (!ImGui::BeginDragDropTarget()) return; + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("Plot")) { + auto ref = static_cast(payload->Data); + MovePlot(ref->view, ref->plotIndex, 0); + } + } + + double now = (wpi::Now() - m_provider->GetStartTime()) * 1.0e-6; + for (size_t i = 0; i < m_plots.size(); ++i) { ImGui::PushID(i); - gPlots[i].EmitPlot(now, i); + m_plots[i]->EmitPlot(*this, now, m_provider->IsPaused(), i); ImGui::PopID(); } - ImGui::Text("(Right double click for more settings)"); } -static void DisplayPlotSettings() { - if (ImGui::Button("Add new plot")) { - gPlots.emplace_back(); - } - for (size_t i = 0; i < gPlots.size(); ++i) { - gPlots[i].EmitSettings(i); +void PlotView::MovePlot(PlotView* fromView, size_t fromIndex, size_t toIndex) { + if (fromView == this) { + if (fromIndex == toIndex) return; + auto val = std::move(m_plots[fromIndex]); + m_plots.insert(m_plots.begin() + toIndex, std::move(val)); + m_plots.erase(m_plots.begin() + fromIndex + (fromIndex > toIndex ? 1 : 0)); + } else { + auto val = std::move(fromView->m_plots[fromIndex]); + m_plots.insert(m_plots.begin() + toIndex, std::move(val)); + fromView->m_plots.erase(fromView->m_plots.begin() + fromIndex); } } -static void* PlotSeries_ReadOpen(ImGuiContext* ctx, - ImGuiSettingsHandler* handler, - const char* name) { - wpi::StringRef plotIndexStr, id; - std::tie(plotIndexStr, id) = wpi::StringRef{name}.split(','); - unsigned int plotIndex; - if (plotIndexStr.getAsInteger(10, plotIndex)) return nullptr; - if (plotIndex >= gPlots.size()) gPlots.resize(plotIndex + 1); - auto& plot = gPlots[plotIndex]; - auto it = std::find_if( - plot.m_series.begin(), plot.m_series.end(), - [&](const auto& elem) { return elem && elem->GetId() == id; }); - if (it != plot.m_series.end()) return it->get(); - return plot.m_series.emplace_back(std::make_unique(id)).get(); +void PlotView::MovePlotSeries(PlotView* fromView, size_t fromPlotIndex, + size_t fromSeriesIndex, size_t toPlotIndex, + size_t toSeriesIndex, int yAxis) { + if (fromView == this && fromPlotIndex == toPlotIndex) { + // need to handle this specially as the index of the old location changes + if (fromSeriesIndex != toSeriesIndex) { + auto& plotSeries = m_plots[fromPlotIndex]->m_series; + auto val = std::move(plotSeries[fromSeriesIndex]); + // only set Y-axis if actually set + if (yAxis != -1) val->SetYAxis(yAxis); + plotSeries.insert(plotSeries.begin() + toSeriesIndex, std::move(val)); + plotSeries.erase(plotSeries.begin() + fromSeriesIndex + + (fromSeriesIndex > toSeriesIndex ? 1 : 0)); + } + } else { + auto& fromPlot = *fromView->m_plots[fromPlotIndex]; + auto& toPlot = *m_plots[toPlotIndex]; + // always set Y-axis if moving plots + fromPlot.m_series[fromSeriesIndex]->SetYAxis(yAxis == -1 ? 0 : yAxis); + toPlot.m_series.insert(toPlot.m_series.begin() + toSeriesIndex, + std::move(fromPlot.m_series[fromSeriesIndex])); + fromPlot.m_series.erase(fromPlot.m_series.begin() + fromSeriesIndex); + } } -static void PlotSeries_ReadLine(ImGuiContext* ctx, - ImGuiSettingsHandler* handler, void* entry, - const char* lineStr) { - auto element = static_cast(entry); - wpi::StringRef line{lineStr}; - auto [name, value] = line.split('='); +PlotProvider::PlotProvider(const wpi::Twine& iniName) + : WindowManager{iniName + "Window"}, + m_plotSaver{iniName, this, false}, + m_seriesSaver{iniName + "Series", this, true} {} + +PlotProvider::~PlotProvider() {} + +void PlotProvider::GlobalInit() { + WindowManager::GlobalInit(); + wpi::gui::AddInit([this] { + m_plotSaver.Initialize(); + m_seriesSaver.Initialize(); + }); +} + +void PlotProvider::ResetTime() { + m_startTime = wpi::Now(); + for (auto&& window : m_windows) { + if (auto view = static_cast(window->GetView())) { + view->Clear(); + } + } +} + +void PlotProvider::DisplayMenu() { + for (size_t i = 0; i < m_windows.size(); ++i) { + m_windows[i]->DisplayMenuItem(); + // provide method to destroy the plot window + if (ImGui::BeginPopupContextItem()) { + if (ImGui::Selectable("Destroy Plot Window")) { + m_windows.erase(m_windows.begin() + i); + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + } + + if (ImGui::MenuItem("New Plot Window")) { + char id[32]; + std::snprintf(id, sizeof(id), "Plot <%d>", + static_cast(m_windows.size())); + AddWindow(id, std::make_unique(this)); + } +} + +void PlotProvider::DisplayWindows() { + // create views if not already created + for (auto&& window : m_windows) { + if (!window->HasView()) window->SetView(std::make_unique(this)); + } + WindowManager::DisplayWindows(); +} + +PlotProvider::IniSaver::IniSaver(const wpi::Twine& typeName, + PlotProvider* provider, bool forSeries) + : IniSaverBase{typeName}, m_provider{provider}, m_forSeries{forSeries} {} + +void* PlotProvider::IniSaver::IniReadOpen(const char* name) { + auto [viewId, plotNumStr] = wpi::StringRef{name}.split('#'); + wpi::StringRef seriesId; + if (m_forSeries) { + std::tie(plotNumStr, seriesId) = plotNumStr.split('#'); + if (seriesId.empty()) return nullptr; + } + unsigned int plotNum; + if (plotNumStr.getAsInteger(10, plotNum)) return nullptr; + + // get or create window + auto win = m_provider->GetOrAddWindow(viewId, true); + if (!win) return nullptr; + + // get or create view + auto view = static_cast(win->GetView()); + if (!view) { + win->SetView(std::make_unique(m_provider)); + view = static_cast(win->GetView()); + } + + // get or create plot + if (view->m_plots.size() <= plotNum) view->m_plots.resize(plotNum + 1); + auto& plot = view->m_plots[plotNum]; + if (!plot) plot = std::make_unique(); + + // early exit for plot data + if (!m_forSeries) return plot.get(); + + // get or create series + return plot->m_series.emplace_back(std::make_unique(seriesId)) + .get(); +} + +void PlotProvider::IniSaver::IniReadLine(void* entry, const char* lineStr) { + auto [name, value] = wpi::StringRef{lineStr}.split('='); name = name.trim(); value = value.trim(); - element->ReadIni(name, value); + if (m_forSeries) + static_cast(entry)->ReadIni(name, value); + else + static_cast(entry)->ReadIni(name, value); } -static void PlotSeries_WriteAll(ImGuiContext* ctx, - ImGuiSettingsHandler* handler, - ImGuiTextBuffer* out_buf) { - for (size_t i = 0; i < gPlots.size(); ++i) { - for (const auto& series : gPlots[i].m_series) { - out_buf->appendf("[PlotSeries][%d,%s]\n", static_cast(i), - series->GetId().c_str()); - series->WriteIni(out_buf); - out_buf->append("\n"); +void PlotProvider::IniSaver::IniWriteAll(ImGuiTextBuffer* out_buf) { + for (auto&& win : m_provider->m_windows) { + auto view = static_cast(win->GetView()); + auto id = win->GetId(); + for (size_t i = 0; i < view->m_plots.size(); ++i) { + if (m_forSeries) { + // Loop over series + for (auto&& series : view->m_plots[i]->m_series) { + out_buf->appendf("[%s][%s#%d#%s]\n", GetTypeName(), id.data(), + static_cast(i), series->GetId().c_str()); + series->WriteIni(out_buf); + out_buf->append("\n"); + } + } else { + // Just the plot + out_buf->appendf("[%s][%s#%d]\n", GetTypeName(), id.data(), + static_cast(i)); + view->m_plots[i]->WriteIni(out_buf); + out_buf->append("\n"); + } } } } - -void PlotGui::Initialize() { - gPlots.Initialize(); - - // hook ini handler for PlotSeries to save settings - ImGuiSettingsHandler iniHandler; - iniHandler.TypeName = "PlotSeries"; - iniHandler.TypeHash = ImHashStr("PlotSeries"); - iniHandler.ReadOpenFn = PlotSeries_ReadOpen; - iniHandler.ReadLineFn = PlotSeries_ReadLine; - iniHandler.WriteAllFn = PlotSeries_WriteAll; - ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler); - - // HALSimGui::AddExecute([] { ImPlot::ShowDemoWindow(); }); - HALSimGui::AddWindow("Plot", DisplayPlot); - HALSimGui::SetDefaultWindowPos("Plot", 600, 75); - HALSimGui::SetDefaultWindowSize("Plot", 300, 200); - - HALSimGui::AddWindow("Plot Settings", DisplayPlotSettings); - HALSimGui::SetDefaultWindowPos("Plot Settings", 902, 75); - HALSimGui::SetDefaultWindowSize("Plot Settings", 120, 200); -} diff --git a/glass/src/lib/native/cpp/other/StringChooser.cpp b/glass/src/lib/native/cpp/other/StringChooser.cpp new file mode 100644 index 0000000000..160068d60c --- /dev/null +++ b/glass/src/lib/native/cpp/other/StringChooser.cpp @@ -0,0 +1,44 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/other/StringChooser.h" + +#include + +using namespace glass; + +void glass::DisplayStringChooser(StringChooserModel* model) { + auto& defaultValue = model->GetDefault(); + auto& selected = model->GetSelected(); + auto& active = model->GetActive(); + auto& options = model->GetOptions(); + + const char* preview = + selected.empty() ? defaultValue.c_str() : selected.c_str(); + + const char* label; + if (active == preview) { + label = "GOOD##select"; + } else { + label = "BAD ##select"; + } + + if (ImGui::BeginCombo(label, preview)) { + for (auto&& option : options) { + ImGui::PushID(option.c_str()); + bool isSelected = (option == selected); + if (ImGui::Selectable(option.c_str(), isSelected)) { + model->SetSelected(option); + } + if (isSelected) ImGui::SetItemDefaultFocus(); + ImGui::PopID(); + } + ImGui::EndCombo(); + } + + ImGui::SameLine(); +} diff --git a/simulation/halsim_gui/src/main/native/cpp/ExtraGuiWidgets.cpp b/glass/src/lib/native/cpp/support/ExtraGuiWidgets.cpp similarity index 54% rename from simulation/halsim_gui/src/main/native/cpp/ExtraGuiWidgets.cpp rename to glass/src/lib/native/cpp/support/ExtraGuiWidgets.cpp index b5bf92f384..3e2c57ad30 100644 --- a/simulation/halsim_gui/src/main/native/cpp/ExtraGuiWidgets.cpp +++ b/glass/src/lib/native/cpp/support/ExtraGuiWidgets.cpp @@ -5,13 +5,16 @@ /* the project. */ /*----------------------------------------------------------------------------*/ -#include "ExtraGuiWidgets.h" +#include "glass/support/ExtraGuiWidgets.h" -#include "GuiDataSource.h" +#define IMGUI_DEFINE_MATH_OPERATORS +#include -namespace halsimgui { +#include "glass/DataSource.h" -void DrawLEDSources(const int* values, GuiDataSource** sources, int numValues, +namespace glass { + +void DrawLEDSources(const int* values, DataSource** sources, int numValues, int cols, const ImU32* colors, float size, float spacing, const LEDConfig& config) { if (numValues == 0 || cols < 1) return; @@ -106,4 +109,60 @@ void DrawLEDs(const int* values, int numValues, int cols, const ImU32* colors, config); } -} // namespace halsimgui +bool DeleteButton(ImGuiID id, const ImVec2& pos) { + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // We intentionally allow interaction when clipped so that a mechanical + // Alt,Right,Validate sequence close a window. (this isn't the regular + // behavior of buttons, but it doesn't affect the user much because navigation + // tends to keep items visible). + const ImRect bb( + pos, pos + ImVec2(g.FontSize, g.FontSize) + g.Style.FramePadding * 2.0f); + bool is_clipped = !ImGui::ItemAdd(bb, id); + + bool hovered, held; + bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held); + if (is_clipped) return pressed; + + // Render + ImU32 col = + ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered); + ImVec2 center = bb.GetCenter(); + if (hovered) + window->DrawList->AddCircleFilled( + center, ImMax(2.0f, g.FontSize * 0.5f + 1.0f), col, 12); + + ImU32 cross_col = ImGui::GetColorU32(ImGuiCol_Text); + window->DrawList->AddCircle(center, ImMax(2.0f, g.FontSize * 0.5f + 1.0f), + cross_col, 12); + float cross_extent = g.FontSize * 0.5f * 0.5f - 1.0f; + center -= ImVec2(0.5f, 0.5f); + window->DrawList->AddLine(center + ImVec2(+cross_extent, +cross_extent), + center + ImVec2(-cross_extent, -cross_extent), + cross_col, 1.0f); + window->DrawList->AddLine(center + ImVec2(+cross_extent, -cross_extent), + center + ImVec2(-cross_extent, +cross_extent), + cross_col, 1.0f); + + return pressed; +} + +bool HeaderDeleteButton(const char* label) { + ImGuiWindow* window = ImGui::GetCurrentWindow(); + ImGuiContext& g = *GImGui; + ImGuiItemHoveredDataBackup last_item_backup; + ImGuiID id = window->GetID(label); + float button_size = g.FontSize; + float button_x = ImMax(window->DC.LastItemRect.Min.x, + window->DC.LastItemRect.Max.x - + g.Style.FramePadding.x * 2.0f - button_size); + float button_y = window->DC.LastItemRect.Min.y; + bool rv = DeleteButton( + window->GetID(reinterpret_cast(static_cast(id) + 1)), + ImVec2(button_x, button_y)); + last_item_backup.Restore(); + return rv; +} + +} // namespace glass diff --git a/glass/src/lib/native/cpp/support/IniSaverBase.cpp b/glass/src/lib/native/cpp/support/IniSaverBase.cpp new file mode 100644 index 0000000000..f17d9a40bc --- /dev/null +++ b/glass/src/lib/native/cpp/support/IniSaverBase.cpp @@ -0,0 +1,64 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/support/IniSaverBase.h" + +#include + +using namespace glass; + +namespace { +class ImGuiSaver : public IniSaverBackend { + public: + void Register(IniSaverBase* iniSaver) override; + void Unregister(IniSaverBase* iniSaver) override; +}; +} // namespace + +void ImGuiSaver::Register(IniSaverBase* iniSaver) { + // hook ini handler to save settings + ImGuiSettingsHandler iniHandler; + iniHandler.TypeName = iniSaver->GetTypeName(); + iniHandler.TypeHash = ImHashStr(iniHandler.TypeName); + iniHandler.ReadOpenFn = [](ImGuiContext* ctx, ImGuiSettingsHandler* handler, + const char* name) { + return static_cast(handler->UserData)->IniReadOpen(name); + }; + iniHandler.ReadLineFn = [](ImGuiContext* ctx, ImGuiSettingsHandler* handler, + void* entry, const char* line) { + static_cast(handler->UserData)->IniReadLine(entry, line); + }; + iniHandler.WriteAllFn = [](ImGuiContext* ctx, ImGuiSettingsHandler* handler, + ImGuiTextBuffer* out_buf) { + static_cast(handler->UserData)->IniWriteAll(out_buf); + }; + iniHandler.UserData = iniSaver; + ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler); +} + +void ImGuiSaver::Unregister(IniSaverBase* iniSaver) { + if (auto ctx = ImGui::GetCurrentContext()) { + auto& handlers = ctx->SettingsHandlers; + for (auto it = handlers.begin(), end = handlers.end(); it != end; ++it) { + if (it->UserData == iniSaver) { + handlers.erase(it); + return; + } + } + } +} + +static ImGuiSaver* GetSaverInstance() { + static ImGuiSaver* inst = new ImGuiSaver; + return inst; +} + +IniSaverBase::IniSaverBase(const wpi::Twine& typeName, IniSaverBackend* backend) + : m_typeName(typeName.str()), + m_backend{backend ? backend : GetSaverInstance()} {} + +IniSaverBase::~IniSaverBase() { m_backend->Unregister(this); } diff --git a/simulation/halsim_gui/src/main/native/cpp/IniSaverInfo.cpp b/glass/src/lib/native/cpp/support/IniSaverInfo.cpp similarity index 79% rename from simulation/halsim_gui/src/main/native/cpp/IniSaverInfo.cpp rename to glass/src/lib/native/cpp/support/IniSaverInfo.cpp index 6d01cd3e21..2ca034bed4 100644 --- a/simulation/halsim_gui/src/main/native/cpp/IniSaverInfo.cpp +++ b/glass/src/lib/native/cpp/support/IniSaverInfo.cpp @@ -5,14 +5,23 @@ /* the project. */ /*----------------------------------------------------------------------------*/ -#include "IniSaverInfo.h" +#include "glass/support/IniSaverInfo.h" #include #include #include +#include -using namespace halsimgui; +using namespace glass; + +void NameInfo::SetName(const wpi::Twine& name) { + wpi::SmallString<64> nameBuf; + auto nameStr = name.toStringRef(nameBuf); + size_t len = (std::min)(nameStr.size(), sizeof(m_name) - 1); + std::memcpy(m_name, nameStr.data(), len); + m_name[len] = '\0'; +} void NameInfo::GetName(char* buf, size_t size, const char* defaultName) const { if (m_name[0] != '\0') { @@ -98,11 +107,13 @@ bool NameInfo::PopupEditName(int index) { std::snprintf(id, sizeof(id), "Name%d", index); if (ImGui::BeginPopupContextItem(id)) { ImGui::Text("Edit name:"); - if (InputTextName("##edit", ImGuiInputTextFlags_EnterReturnsTrue)) { - ImGui::CloseCurrentPopup(); + if (InputTextName("##edit")) { rv = true; } - if (ImGui::Button("Close")) ImGui::CloseCurrentPopup(); + if (ImGui::Button("Close") || ImGui::IsKeyPressedMap(ImGuiKey_Enter) || + ImGui::IsKeyPressedMap(ImGuiKey_KeyPadEnter)) { + ImGui::CloseCurrentPopup(); + } ImGui::EndPopup(); } return rv; @@ -114,11 +125,13 @@ bool NameInfo::PopupEditName(const char* name) { std::snprintf(id, sizeof(id), "Name%s", name); if (ImGui::BeginPopupContextItem(id)) { ImGui::Text("Edit name:"); - if (InputTextName("##edit", ImGuiInputTextFlags_EnterReturnsTrue)) { - ImGui::CloseCurrentPopup(); + if (InputTextName("##edit")) { rv = true; } - if (ImGui::Button("Close")) ImGui::CloseCurrentPopup(); + if (ImGui::Button("Close") || ImGui::IsKeyPressedMap(ImGuiKey_Enter) || + ImGui::IsKeyPressedMap(ImGuiKey_KeyPadEnter)) { + ImGui::CloseCurrentPopup(); + } ImGui::EndPopup(); } return rv; @@ -139,3 +152,14 @@ bool OpenInfo::ReadIni(wpi::StringRef name, wpi::StringRef value) { void OpenInfo::WriteIni(ImGuiTextBuffer* out) { out->appendf("open=%d\n", m_open ? 1 : 0); } + +bool NameOpenInfo::ReadIni(wpi::StringRef name, wpi::StringRef value) { + if (NameInfo::ReadIni(name, value)) return true; + if (OpenInfo::ReadIni(name, value)) return true; + return false; +} + +void NameOpenInfo::WriteIni(ImGuiTextBuffer* out) { + NameInfo::WriteIni(out); + OpenInfo::WriteIni(out); +} diff --git a/glass/src/lib/native/include/glass/Context.h b/glass/src/lib/native/include/glass/Context.h new file mode 100644 index 0000000000..64ead2d8bd --- /dev/null +++ b/glass/src/lib/native/include/glass/Context.h @@ -0,0 +1,147 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +namespace glass { + +struct Context; + +Context* CreateContext(); +void DestroyContext(Context* ctx = nullptr); +Context* GetCurrentContext(); +void SetCurrentContext(Context* ctx); + +/** + * Storage provides both persistent and non-persistent key/value storage for + * widgets. + * + * Keys are always strings. The storage also provides non-persistent arbitrary + * data storage (via std::shared_ptr). + * + * Storage is automatically indexed internally by the ID stack. Note it is + * necessary to use the glass wrappers for PushID et al to preserve naming in + * the save file (unnamed values are still stored, but this is non-ideal for + * users trying to hand-edit the save file). + */ +class Storage { + public: + struct Value { + Value() = default; + explicit Value(const wpi::Twine& str) : stringVal{str.str()} {} + + enum Type { kNone, kInt, kInt64, kBool, kFloat, kDouble, kString }; + Type type = kNone; + union { + int intVal; + int64_t int64Val; + bool boolVal; + float floatVal; + double doubleVal; + }; + std::string stringVal; + }; + + int GetInt(wpi::StringRef key, int defaultVal = 0) const; + int64_t GetInt64(wpi::StringRef key, int64_t defaultVal = 0) const; + bool GetBool(wpi::StringRef key, bool defaultVal = false) const; + float GetFloat(wpi::StringRef key, float defaultVal = 0.0f) const; + double GetDouble(wpi::StringRef key, double defaultVal = 0.0) const; + std::string GetString(wpi::StringRef key, + const std::string& defaultVal = {}) const; + + void SetInt(wpi::StringRef key, int val); + void SetInt64(wpi::StringRef key, int64_t val); + void SetBool(wpi::StringRef key, bool val); + void SetFloat(wpi::StringRef key, float val); + void SetDouble(wpi::StringRef key, double val); + void SetString(wpi::StringRef key, const wpi::Twine& val); + + int* GetIntRef(wpi::StringRef key, int defaultVal = 0); + int64_t* GetInt64Ref(wpi::StringRef key, int64_t defaultVal = 0); + bool* GetBoolRef(wpi::StringRef key, bool defaultVal = false); + float* GetFloatRef(wpi::StringRef key, float defaultVal = 0.0f); + double* GetDoubleRef(wpi::StringRef key, double defaultVal = 0.0); + std::string* GetStringRef(wpi::StringRef key, wpi::StringRef defaultVal = {}); + + Value& GetValue(wpi::StringRef key); + + void SetData(std::shared_ptr&& data) { m_data = std::move(data); } + + template + T* GetData() const { + return static_cast(m_data.get()); + } + + Storage() = default; + Storage(const Storage&) = delete; + Storage& operator=(const Storage&) = delete; + + std::vector& GetKeys() { return m_keys; } + const std::vector& GetKeys() const { return m_keys; } + std::vector>& GetValues() { return m_values; } + const std::vector>& GetValues() const { + return m_values; + } + + private: + mutable std::vector m_keys; + mutable std::vector> m_values; + std::shared_ptr m_data; +}; + +Storage& GetStorage(); +Storage& GetStorage(wpi::StringRef id); + +bool Begin(const char* name, bool* p_open = nullptr, + ImGuiWindowFlags flags = 0); + +void End(); + +bool BeginChild(const char* str_id, const ImVec2& size = ImVec2(0, 0), + bool border = false, ImGuiWindowFlags flags = 0); + +void EndChild(); + +/** + * Saves open status to storage "open" key. + * If returning 'true' the header is open. doesn't indent nor push on ID stack. + * user doesn't have to call TreePop(). + */ +bool CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags = 0); + +bool TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags = 0); + +void TreePop(); + +// push string into the ID stack (will hash string). +void PushID(const char* str_id); + +// push string into the ID stack (will hash string). +void PushID(const char* str_id_begin, const char* str_id_end); + +// push string into the ID stack (will hash string). +inline void PushID(wpi::StringRef str) { PushID(str.begin(), str.end()); } + +// push integer into the ID stack (will hash integer). +void PushID(int int_id); + +// pop from the ID stack. +void PopID(); + +bool PopupEditName(const char* label, std::string* name); + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/ContextInternal.h b/glass/src/lib/native/include/glass/ContextInternal.h new file mode 100644 index 0000000000..c187ffd75f --- /dev/null +++ b/glass/src/lib/native/include/glass/ContextInternal.h @@ -0,0 +1,49 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +#include +#include +#include +#include + +#include "glass/Context.h" +#include "glass/support/IniSaverInfo.h" +#include "glass/support/IniSaverString.h" + +namespace glass { + +class DataSource; + +class DataSourceName { + public: + DataSourceName() = default; + explicit DataSourceName(DataSource* source) : source{source} {} + + bool ReadIni(wpi::StringRef name_, wpi::StringRef value) { + return name->ReadIni(name_, value); + } + void WriteIni(ImGuiTextBuffer* out) { name->WriteIni(out); } + + std::unique_ptr name{new NameInfo}; + DataSource* source = nullptr; +}; + +struct Context { + wpi::SmallString<128> curId; + wpi::SmallVector idStack; + wpi::StringMap> storage; + wpi::StringMap deviceHidden; + IniSaverString sources{"Data Sources"}; +}; + +extern Context* gContext; + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/DataSource.h b/glass/src/lib/native/include/glass/DataSource.h new file mode 100644 index 0000000000..78e764acb6 --- /dev/null +++ b/glass/src/lib/native/include/glass/DataSource.h @@ -0,0 +1,86 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace glass { + +class NameInfo; + +/** + * A data source for numeric/boolean data. + */ +class DataSource { + public: + explicit DataSource(const wpi::Twine& id); + DataSource(const wpi::Twine& id, int index); + DataSource(const wpi::Twine& id, int index, int index2); + virtual ~DataSource(); + + DataSource(const DataSource&) = delete; + DataSource& operator=(const DataSource&) = delete; + + const char* GetId() const { return m_id.c_str(); } + + void SetName(const wpi::Twine& name); + const char* GetName() const; + NameInfo& GetNameInfo() { return *m_name; } + + void PushEditNameId(int index); + void PushEditNameId(const char* name); + bool PopupEditName(int index); + bool PopupEditName(const char* name); + bool InputTextName(const char* label_id, ImGuiInputTextFlags flags = 0); + + void SetDigital(bool digital) { m_digital = digital; } + bool IsDigital() const { return m_digital; } + + void SetValue(double value, uint64_t time = 0) { + m_value = value; + valueChanged(value, time); + } + double GetValue() const { return m_value; } + + // drag source helpers + void LabelText(const char* label, const char* fmt, ...) const; + void LabelTextV(const char* label, const char* fmt, va_list args) const; + bool Combo(const char* label, int* current_item, const char* const items[], + int items_count, int popup_max_height_in_items = -1) const; + bool SliderFloat(const char* label, float* v, float v_min, float v_max, + const char* format = "%.3f", float power = 1.0f) const; + bool InputDouble(const char* label, double* v, double step = 0.0, + double step_fast = 0.0, const char* format = "%.6f", + ImGuiInputTextFlags flags = 0) const; + bool InputInt(const char* label, int* v, int step = 1, int step_fast = 100, + ImGuiInputTextFlags flags = 0) const; + void EmitDrag(ImGuiDragDropFlags flags = 0) const; + + wpi::sig::SignalBase valueChanged; + + static DataSource* Find(wpi::StringRef id); + + static wpi::sig::Signal sourceCreated; + + private: + std::string m_id; + NameInfo* m_name; + bool m_digital = false; + std::atomic m_value = 0; +}; + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/MainMenuBar.h b/glass/src/lib/native/include/glass/MainMenuBar.h new file mode 100644 index 0000000000..5d36c264f3 --- /dev/null +++ b/glass/src/lib/native/include/glass/MainMenuBar.h @@ -0,0 +1,51 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include + +namespace glass { + +class WindowManager; + +/** + * GUI main menu bar. + */ +class MainMenuBar { + public: + /** + * Displays the main menu bar. Should be added to GUI LateExecute. + */ + void Display(); + + /** + * Adds to GUI's main menu bar. The menu function is called from within a + * ImGui::BeginMainMenuBar()/EndMainMenuBar() block. Usually it's only + * appropriate to create a menu with ImGui::BeginMenu()/EndMenu() inside of + * this function. + * + * @param menu menu display function + */ + void AddMainMenu(std::function menu); + + /** + * Adds to GUI's option menu. The menu function is called from within a + * ImGui::BeginMenu()/EndMenu() block. Usually it's only appropriate to + * create menu items inside of this function. + * + * @param menu menu display function + */ + void AddOptionMenu(std::function menu); + + private: + std::vector> m_optionMenus; + std::vector> m_menus; +}; + +} // namespace glass diff --git a/simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.h b/glass/src/lib/native/include/glass/Model.h similarity index 65% rename from simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.h rename to glass/src/lib/native/include/glass/Model.h index f4e5d7baeb..f9d9b6dc31 100644 --- a/simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.h +++ b/glass/src/lib/native/include/glass/Model.h @@ -7,11 +7,19 @@ #pragma once -namespace halsimgui { +namespace glass { -class NetworkTablesGui { +class Model { public: - static void Initialize(); + Model() = default; + virtual ~Model() = default; + + Model(const Model&) = delete; + Model& operator=(const Model&) = delete; + + virtual void Update() = 0; + virtual bool Exists() = 0; + virtual bool IsReadOnly(); }; -} // namespace halsimgui +} // namespace glass diff --git a/glass/src/lib/native/include/glass/Provider.h b/glass/src/lib/native/include/glass/Provider.h new file mode 100644 index 0000000000..c3550e1dd9 --- /dev/null +++ b/glass/src/lib/native/include/glass/Provider.h @@ -0,0 +1,171 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "glass/Model.h" +#include "glass/WindowManager.h" + +namespace glass { + +namespace detail { +struct ProviderFunctions { + using Exists = std::function; + using CreateModel = std::function()>; + using ViewExists = std::function; + using CreateView = std::function(Window*, Model*)>; +}; +} // namespace detail + +/** + * Providers are registries of models and views. They have ownership over + * their created Models, Windows, and Views. + * + * GlobalInit() configures Update() to be called during EarlyExecute. + * Calling Update() calls Update() on all created models (Provider + * implementations must ensure this occurs). + * + * @tparam Functions defines functor interface types + */ +template +class Provider : public WindowManager { + public: + using ExistsFunc = typename Functions::Exists; + using CreateModelFunc = typename Functions::CreateModel; + using ViewExistsFunc = typename Functions::ViewExists; + using CreateViewFunc = typename Functions::CreateView; + + /** + * Constructor. + * + * @param iniName Group name to use in ini file + */ + explicit Provider(const wpi::Twine& iniName) : WindowManager{iniName} {} + + Provider(const Provider&) = delete; + Provider& operator=(const Provider&) = delete; + + /** + * Perform global initialization. This should be called prior to + * wpi::gui::Initialize(). + */ + void GlobalInit() override; + + /** + * Show the specified view by default on first load. Has no effect if + * the user previously hid the window (e.g. in a saved prior execution). + * + * @param name View name + */ + void ShowDefault(wpi::StringRef name); + + /** + * Register a model and view combination. Equivalent to calling both + * RegisterModel() and RegisterView() with no ViewExistsFunc. + * + * @param name View/model name + * @param exists Functor, returns true if model can be created + * @param createModel Functor for creating model + * @param createView Functor for creating view + */ + void Register(wpi::StringRef name, ExistsFunc exists, + CreateModelFunc createModel, CreateViewFunc createView); + + /** + * Register a model. + * + * @param name Model name + * @param exists Functor, returns true if model can be created + * @param createModel Functor for creating model + */ + void RegisterModel(wpi::StringRef name, ExistsFunc exists, + CreateModelFunc createModel); + + /** + * Register a view. + * + * @param name View name + * @param modelName Model name + * @param exists Functor, returns true if view can be created + * @param createView Functor for creating view + */ + void RegisterView(wpi::StringRef name, wpi::StringRef modelName, + ViewExistsFunc exists, CreateViewFunc createView); + + protected: + virtual void Update(); + + struct ModelEntry { + ModelEntry(wpi::StringRef name, ExistsFunc exists, + CreateModelFunc createModel) + : name{name}, + exists{std::move(exists)}, + createModel{std::move(createModel)} {} + virtual ~ModelEntry() = default; + + std::string name; + ExistsFunc exists; + CreateModelFunc createModel; + std::unique_ptr model; + }; + + struct ViewEntry { + ViewEntry(wpi::StringRef name, ModelEntry* modelEntry, + ViewExistsFunc exists, CreateViewFunc createView) + : name{name}, + modelEntry{modelEntry}, + exists{std::move(exists)}, + createView{std::move(createView)} {} + virtual ~ViewEntry() = default; + + std::string name; + ModelEntry* modelEntry; + ViewExistsFunc exists; + CreateViewFunc createView; + Window* window = nullptr; + }; + + // sorted by name + using ModelEntries = std::vector>; + ModelEntries m_modelEntries; + using ViewEntries = std::vector>; + ViewEntries m_viewEntries; + + typename ModelEntries::iterator FindModelEntry(wpi::StringRef name); + typename ViewEntries::iterator FindViewEntry(wpi::StringRef name); + + virtual std::unique_ptr MakeModelEntry( + wpi::StringRef name, ExistsFunc exists, CreateModelFunc createModel) { + return std::make_unique(name, std::move(exists), + std::move(createModel)); + } + + virtual std::unique_ptr MakeViewEntry(wpi::StringRef name, + ModelEntry* modelEntry, + ViewExistsFunc exists, + CreateViewFunc createView) { + return std::make_unique(name, modelEntry, std::move(exists), + std::move(createView)); + } + + virtual void Show(ViewEntry* entry, Window* window) = 0; +}; + +} // namespace glass + +#include "Provider.inc" diff --git a/glass/src/lib/native/include/glass/Provider.inc b/glass/src/lib/native/include/glass/Provider.inc new file mode 100644 index 0000000000..122ecb0d44 --- /dev/null +++ b/glass/src/lib/native/include/glass/Provider.inc @@ -0,0 +1,90 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +namespace glass { + +template +void Provider::GlobalInit() { + WindowManager::GlobalInit(); + wpi::gui::AddEarlyExecute([this] { Update(); }); +} + +template +void Provider::ShowDefault(wpi::StringRef name) { + auto win = GetWindow(name); + if (win) return; + auto it = FindViewEntry(name); + if (it == m_viewEntries.end() || (*it)->name != name) return; + this->Show(it->get(), (*it)->window); +} + +template +void Provider::Register(wpi::StringRef name, ExistsFunc exists, + CreateModelFunc createModel, + CreateViewFunc createView) { + RegisterModel(name, std::move(exists), std::move(createModel)); + RegisterView(name, name, nullptr, std::move(createView)); +} + +template +void Provider::RegisterModel(wpi::StringRef name, ExistsFunc exists, + CreateModelFunc createModel) { + auto it = FindModelEntry(name); + // ignore if exists + if (it != m_modelEntries.end() && (*it)->name == name) return; + // insert in sorted location + m_modelEntries.emplace( + it, MakeModelEntry(name, std::move(exists), std::move(createModel))); +} + +template +void Provider::RegisterView(wpi::StringRef name, + wpi::StringRef modelName, + ViewExistsFunc exists, + CreateViewFunc createView) { + // find model; if model doesn't exist, ignore + auto modelIt = FindModelEntry(modelName); + if (modelIt == m_modelEntries.end() || (*modelIt)->name != modelName) return; + + auto viewIt = FindViewEntry(name); + // ignore if exists + if (viewIt != m_viewEntries.end() && (*viewIt)->name == name) return; + // insert in sorted location + m_viewEntries.emplace(viewIt, + MakeViewEntry(name, modelIt->get(), std::move(exists), + std::move(createView))); +} + +template +void Provider::Update() { + // update entries + for (auto&& entry : m_modelEntries) { + if (entry->model) entry->model->Update(); + } +} + +template +typename Provider::ModelEntries::iterator +Provider::FindModelEntry(wpi::StringRef name) { + return std::lower_bound( + m_modelEntries.begin(), m_modelEntries.end(), name, + [](const auto& elem, wpi::StringRef s) { return elem->name < s; }); +} + +template +typename Provider::ViewEntries::iterator +Provider::FindViewEntry(wpi::StringRef name) { + return std::lower_bound( + m_viewEntries.begin(), m_viewEntries.end(), name, + [](const auto& elem, wpi::StringRef s) { return elem->name < s; }); +} + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/View.h b/glass/src/lib/native/include/glass/View.h new file mode 100644 index 0000000000..59bbc011fa --- /dev/null +++ b/glass/src/lib/native/include/glass/View.h @@ -0,0 +1,51 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +#include + +namespace glass { + +/** + * A view is the contents of a window (1:1 mapping). + * It may reference multiple models. + * + * Typically a view is constructed by a Provider and the View's constructor + * is given the corresponding Model(s). + * + * A view may retain a reference to its parent window for dynamic + * window configuration. + */ +class View { + public: + virtual ~View() = default; + + /** + * Displays the window contents. Called by Window::Display() from within an + * ImGui::Begin() / ImGui::End() block. + */ + virtual void Display() = 0; + + /** + * Called instead of Display() when the window is hidden (e.g. when + * ImGui::Begin() returns false). + */ + virtual void Hidden(); +}; + +/** + * Make a View for a display functor. + * + * @param display Display function + * @return unique_ptr to View + */ +std::unique_ptr MakeFunctionView(wpi::unique_function display); + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/Window.h b/glass/src/lib/native/include/glass/Window.h new file mode 100644 index 0000000000..2ab31be60e --- /dev/null +++ b/glass/src/lib/native/include/glass/Window.h @@ -0,0 +1,134 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include "glass/View.h" + +namespace glass { + +/** + * Managed window information. + * A Window owns the View that displays the window's contents. + */ +class Window { + public: + Window() = default; + explicit Window(wpi::StringRef id) : m_id{id} {} + + wpi::StringRef GetId() const { return m_id; } + + enum Visibility { kHide = 0, kShow, kDisabled }; + + bool HasView() { return static_cast(m_view); } + + void SetView(std::unique_ptr view) { m_view = std::move(view); } + + View* GetView() { return m_view.get(); } + const View* GetView() const { return m_view.get(); } + + bool IsVisible() const { return m_visible; } + void SetVisible(bool visible) { m_visible = visible; } + bool IsEnabled() const { return m_enabled; } + void SetEnabled(bool enabled) { m_enabled = enabled; } + + void SetFlags(ImGuiWindowFlags flags) { m_flags = flags; } + + void SetName(const wpi::Twine& name) { m_name = name.str(); } + + /** + * Normally windows provide a right-click popup menu on the title bar to + * rename the window. Calling this disables that functionality so the + * view can provide its own popup. + */ + void DisableRenamePopup() { m_renamePopupEnabled = false; } + + /** + * Sets visibility of window. + * + * @param visibility 0=hide, 1=show, 2=disabled (force-hide) + */ + void SetVisibility(Visibility visibility); + + /** + * Sets default position of window. + * + * @param x x location of upper left corner + * @param y y location of upper left corner + */ + void SetDefaultPos(float x, float y) { + m_posCond = ImGuiCond_FirstUseEver; + m_pos = ImVec2{x, y}; + } + + /** + * Sets default size of window. + * + * @param width width + * @param height height + */ + void SetDefaultSize(float width, float height) { + m_sizeCond = ImGuiCond_FirstUseEver; + m_size = ImVec2{width, height}; + } + + /** + * Sets internal padding of window. + * @param x horizontal padding + * @param y vertical padding + */ + void SetPadding(float x, float y) { + m_setPadding = true; + m_padding = ImVec2{x, y}; + } + + /** + * Displays window. + */ + void Display(); + + /** + * Displays menu item for the window. + * @param label what to display as the menu item label; defaults to + * window ID if nullptr + * @return True if window went from invisible to visible. + */ + bool DisplayMenuItem(const char* label = nullptr); + + /** + * Scale default window position and size. + */ + void ScaleDefault(float scale); + + void IniReadLine(const char* lineStr); + void IniWriteAll(const char* typeName, ImGuiTextBuffer* out_buf); + + private: + std::string m_id; + std::string m_name; + std::unique_ptr m_view; + ImGuiWindowFlags m_flags = 0; + bool m_visible = true; + bool m_enabled = true; + bool m_renamePopupEnabled = true; + ImGuiCond m_posCond = 0; + ImGuiCond m_sizeCond = 0; + ImVec2 m_pos; + ImVec2 m_size; + bool m_setPadding = false; + ImVec2 m_padding; +}; + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/WindowManager.h b/glass/src/lib/native/include/glass/WindowManager.h new file mode 100644 index 0000000000..5297a1a7f2 --- /dev/null +++ b/glass/src/lib/native/include/glass/WindowManager.h @@ -0,0 +1,141 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "glass/Window.h" +#include "glass/support/IniSaverBase.h" + +namespace glass { + +/** + * Window manager. + * + * To properly integrate into an application: + * - Call GlobalInit() from the application main, after calling + * wpi::gui::CreateContext(), but before calling wpi::gui::Initialize(). + * - Add DisplayMenu() to the application's MainMenuBar. + */ +class WindowManager { + public: + /** + * Constructor. + * + * @param iniName Group name to use in ini file + */ + explicit WindowManager(const wpi::Twine& iniName); + virtual ~WindowManager() = default; + + WindowManager(const WindowManager&) = delete; + WindowManager& operator=(const WindowManager&) = delete; + + /** + * Perform global initialization. This should be called prior to + * wpi::gui::Initialize(). + */ + virtual void GlobalInit(); + + /** + * Displays menu contents, one item for each window. + * See Window::DisplayMenuItem(). + */ + virtual void DisplayMenu(); + + /** + * Adds window to GUI. The display function is called from within a + * ImGui::Begin()/End() block. While windows can be created within the + * execute function passed to gui::AddExecute(), using this function ensures + * the windows are consistently integrated with the rest of the GUI. + * + * On each Dear ImGui frame, gui::AddExecute() functions are always called + * prior to AddWindow display functions. Note that windows may be shaded or + * completely hidden, in which case this function will not be called. + * It's important to perform any processing steps that must be performed + * every frame in the gui::AddExecute() function. + * + * @param id unique identifier of the window (title bar) + * @param display window contents display function + */ + Window* AddWindow(wpi::StringRef id, wpi::unique_function display); + + /** + * Adds window to GUI. The view's display function is called from within a + * ImGui::Begin()/End() block. While windows can be created within the + * execute function passed to gui::AddExecute(), using this function ensures + * the windows are consistently integrated with the rest of the GUI. + * + * On each Dear ImGui frame, gui::AddExecute() functions are always called + * prior to AddWindow display functions. Note that windows may be shaded or + * completely hidden, in which case this function will not be called. + * It's important to perform any processing steps that must be performed + * every frame in the gui::AddExecute() function. + * + * @param id unique identifier of the window (title bar) + * @param view view object + * @return Window, or nullptr on duplicate window + */ + Window* AddWindow(wpi::StringRef id, std::unique_ptr view); + + /** + * Adds window to GUI. A View must be assigned to the returned Window + * to display the window contents. While windows can be created within the + * execute function passed to gui::AddExecute(), using this function ensures + * the windows are consistently integrated with the rest of the GUI. + * + * On each Dear ImGui frame, gui::AddExecute() functions are always called + * prior to AddWindow display functions. Note that windows may be shaded or + * completely hidden, in which case this function will not be called. + * It's important to perform any processing steps that must be performed + * every frame in the gui::AddExecute() function. + * + * @param id unique identifier of the window (default title bar) + * @return Window, or nullptr on duplicate window + */ + Window* GetOrAddWindow(wpi::StringRef id, bool duplicateOk = false); + + /** + * Gets existing window. If none exists, returns nullptr. + * + * @param id unique identifier of the window (default title bar) + * @return Window, or nullptr if window does not exist + */ + Window* GetWindow(wpi::StringRef id); + + protected: + virtual void DisplayWindows(); + + // kept sorted by id + std::vector> m_windows; + + private: + class IniSaver : public IniSaverBase { + public: + explicit IniSaver(const wpi::Twine& typeName, WindowManager* manager) + : IniSaverBase{typeName}, m_manager{manager} {} + + void* IniReadOpen(const char* name) override; + void IniReadLine(void* entry, const char* lineStr) override; + void IniWriteAll(ImGuiTextBuffer* out_buf) override; + + private: + WindowManager* m_manager; + }; + + IniSaver m_iniSaver; +}; + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/hardware/Accelerometer.h b/glass/src/lib/native/include/glass/hardware/Accelerometer.h new file mode 100644 index 0000000000..118eca4801 --- /dev/null +++ b/glass/src/lib/native/include/glass/hardware/Accelerometer.h @@ -0,0 +1,32 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include "glass/Model.h" + +namespace glass { + +class DataSource; + +class AccelerometerModel : public Model { + public: + virtual DataSource* GetXData() = 0; + virtual DataSource* GetYData() = 0; + virtual DataSource* GetZData() = 0; + + virtual int GetRange() = 0; + + virtual void SetX(double val) = 0; + virtual void SetY(double val) = 0; + virtual void SetZ(double val) = 0; + virtual void SetRange(int val) = 0; +}; + +void DisplayAccelerometerDevice(AccelerometerModel* model); + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/hardware/AnalogGyro.h b/glass/src/lib/native/include/glass/hardware/AnalogGyro.h new file mode 100644 index 0000000000..fff76ae82b --- /dev/null +++ b/glass/src/lib/native/include/glass/hardware/AnalogGyro.h @@ -0,0 +1,36 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +#include "glass/Model.h" + +namespace glass { + +class DataSource; + +class AnalogGyroModel : public Model { + public: + virtual DataSource* GetAngleData() = 0; + virtual DataSource* GetRateData() = 0; + + virtual void SetAngle(double val) = 0; + virtual void SetRate(double val) = 0; +}; + +class AnalogGyrosModel : public Model { + public: + virtual void ForEachAnalogGyro( + wpi::function_ref func) = 0; +}; + +void DisplayAnalogGyroDevice(AnalogGyroModel* model, int index); +void DisplayAnalogGyrosDevice(AnalogGyrosModel* model); + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/hardware/AnalogInput.h b/glass/src/lib/native/include/glass/hardware/AnalogInput.h new file mode 100644 index 0000000000..4c9bbe2886 --- /dev/null +++ b/glass/src/lib/native/include/glass/hardware/AnalogInput.h @@ -0,0 +1,39 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include + +#include "glass/Model.h" + +namespace glass { + +class DataSource; + +class AnalogInputModel : public Model { + public: + virtual bool IsGyro() const = 0; + virtual const char* GetSimDevice() const = 0; + + virtual DataSource* GetVoltageData() = 0; + + virtual void SetVoltage(double val) = 0; +}; + +class AnalogInputsModel : public Model { + public: + virtual void ForEachAnalogInput( + wpi::function_ref func) = 0; +}; + +void DisplayAnalogInput(AnalogInputModel* model, int index); +void DisplayAnalogInputs(AnalogInputsModel* model, + wpi::StringRef noneMsg = "No analog inputs"); + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/hardware/AnalogOutput.h b/glass/src/lib/native/include/glass/hardware/AnalogOutput.h new file mode 100644 index 0000000000..1238a80087 --- /dev/null +++ b/glass/src/lib/native/include/glass/hardware/AnalogOutput.h @@ -0,0 +1,33 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +#include "glass/Model.h" + +namespace glass { + +class DataSource; + +class AnalogOutputModel : public Model { + public: + virtual DataSource* GetVoltageData() = 0; + + virtual void SetVoltage(double val) = 0; +}; + +class AnalogOutputsModel : public Model { + public: + virtual void ForEachAnalogOutput( + wpi::function_ref func) = 0; +}; + +void DisplayAnalogOutputsDevice(AnalogOutputsModel* model); + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/hardware/DIO.h b/glass/src/lib/native/include/glass/hardware/DIO.h new file mode 100644 index 0000000000..3cdc3e6e7f --- /dev/null +++ b/glass/src/lib/native/include/glass/hardware/DIO.h @@ -0,0 +1,65 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include + +#include "glass/Model.h" + +namespace glass { + +class EncoderModel; +class DataSource; + +class DPWMModel : public Model { + public: + virtual const char* GetSimDevice() const = 0; + + virtual DataSource* GetValueData() = 0; + + virtual void SetValue(double val) = 0; +}; + +class DutyCycleModel : public Model { + public: + virtual const char* GetSimDevice() const = 0; + + virtual DataSource* GetValueData() = 0; + + virtual void SetValue(double val) = 0; +}; + +class DIOModel : public Model { + public: + virtual const char* GetName() const = 0; + + virtual const char* GetSimDevice() const = 0; + + virtual DPWMModel* GetDPWM() = 0; + virtual DutyCycleModel* GetDutyCycle() = 0; + virtual EncoderModel* GetEncoder() = 0; + + virtual bool IsInput() const = 0; + + virtual DataSource* GetValueData() = 0; + + virtual void SetValue(bool val) = 0; +}; + +class DIOsModel : public Model { + public: + virtual void ForEachDIO( + wpi::function_ref func) = 0; +}; + +void DisplayDIO(DIOModel* model, int index, bool outputsEnabled); +void DisplayDIOs(DIOsModel* model, bool outputsEnabled, + wpi::StringRef noneMsg = "No Digital I/O"); + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/hardware/Encoder.h b/glass/src/lib/native/include/glass/hardware/Encoder.h new file mode 100644 index 0000000000..0509c6f73c --- /dev/null +++ b/glass/src/lib/native/include/glass/hardware/Encoder.h @@ -0,0 +1,59 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include + +#include "glass/Model.h" + +namespace glass { + +class DataSource; + +class EncoderModel : public Model { + public: + virtual void SetName(const wpi::Twine& name); + + virtual const char* GetSimDevice() const = 0; + + virtual int GetChannelA() const = 0; + virtual int GetChannelB() const = 0; + + virtual DataSource* GetDistancePerPulseData() = 0; + virtual DataSource* GetCountData() = 0; + virtual DataSource* GetPeriodData() = 0; + virtual DataSource* GetDirectionData() = 0; + virtual DataSource* GetDistanceData() = 0; + virtual DataSource* GetRateData() = 0; + + virtual double GetMaxPeriod() = 0; + virtual bool GetReverseDirection() = 0; + + virtual void SetDistancePerPulse(double val) = 0; + virtual void SetCount(int val) = 0; + virtual void SetPeriod(double val) = 0; + virtual void SetDirection(bool val) = 0; + virtual void SetDistance(double val) = 0; + virtual void SetRate(double val) = 0; + + virtual void SetMaxPeriod(double val) = 0; + virtual void SetReverseDirection(bool val) = 0; +}; + +class EncodersModel : public Model { + public: + virtual void ForEachEncoder( + wpi::function_ref func) = 0; +}; + +void DisplayEncoder(EncoderModel* model); +void DisplayEncoders(EncodersModel* model, + wpi::StringRef noneMsg = "No encoders"); + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/hardware/LEDDisplay.h b/glass/src/lib/native/include/glass/hardware/LEDDisplay.h new file mode 100644 index 0000000000..de3f3a3959 --- /dev/null +++ b/glass/src/lib/native/include/glass/hardware/LEDDisplay.h @@ -0,0 +1,47 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include + +#include "glass/Model.h" + +namespace wpi { +template +class SmallVectorImpl; +} // namespace wpi + +namespace glass { + +class LEDDisplayModel : public glass::Model { + public: + struct Data { + uint8_t b; + uint8_t g; + uint8_t r; + uint8_t padding; + }; + + virtual bool IsRunning() = 0; + + virtual wpi::ArrayRef GetData(wpi::SmallVectorImpl& buf) = 0; +}; + +class LEDDisplaysModel : public glass::Model { + public: + virtual size_t GetNumLEDDisplays() = 0; + + virtual void ForEachLEDDisplay( + wpi::function_ref func) = 0; +}; + +void DisplayLEDDisplay(LEDDisplayModel* model, int index); +void DisplayLEDDisplays(LEDDisplaysModel* model); + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/hardware/PCM.h b/glass/src/lib/native/include/glass/hardware/PCM.h new file mode 100644 index 0000000000..cc0276273b --- /dev/null +++ b/glass/src/lib/native/include/glass/hardware/PCM.h @@ -0,0 +1,62 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include + +#include "glass/Model.h" + +namespace glass { + +class DataSource; + +class CompressorModel : public Model { + public: + virtual DataSource* GetRunningData() = 0; + virtual DataSource* GetEnabledData() = 0; + virtual DataSource* GetPressureSwitchData() = 0; + virtual DataSource* GetCurrentData() = 0; + + virtual void SetRunning(bool val) = 0; + virtual void SetEnabled(bool val) = 0; + virtual void SetPressureSwitch(bool val) = 0; + virtual void SetCurrent(double val) = 0; +}; + +class SolenoidModel : public Model { + public: + virtual DataSource* GetOutputData() = 0; + + virtual void SetOutput(bool val) = 0; +}; + +class PCMModel : public Model { + public: + virtual CompressorModel* GetCompressor() = 0; + + virtual void ForEachSolenoid( + wpi::function_ref func) = 0; +}; + +class PCMsModel : public Model { + public: + virtual void ForEachPCM( + wpi::function_ref func) = 0; +}; + +bool DisplayPCMSolenoids(PCMModel* model, int index, bool outputsEnabled); +void DisplayPCMsSolenoids(PCMsModel* model, bool outputsEnabled, + wpi::StringRef noneMsg = "No solenoids"); + +void DisplayCompressorDevice(PCMModel* model, int index, bool outputsEnabled); +void DisplayCompressorDevice(CompressorModel* model, int index, + bool outputsEnabled); +void DisplayCompressorsDevice(PCMsModel* model, bool outputsEnabled); + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/hardware/PDP.h b/glass/src/lib/native/include/glass/hardware/PDP.h new file mode 100644 index 0000000000..ce1edf3219 --- /dev/null +++ b/glass/src/lib/native/include/glass/hardware/PDP.h @@ -0,0 +1,41 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include + +#include "glass/Model.h" + +namespace glass { + +class DataSource; + +class PDPModel : public Model { + public: + virtual int GetNumChannels() const = 0; + + virtual DataSource* GetTemperatureData() = 0; + virtual DataSource* GetVoltageData() = 0; + virtual DataSource* GetCurrentData(int channel) = 0; + + virtual void SetTemperature(double val) = 0; + virtual void SetVoltage(double val) = 0; + virtual void SetCurrent(int channel, double val) = 0; +}; + +class PDPsModel : public Model { + public: + virtual void ForEachPDP( + wpi::function_ref func) = 0; +}; + +void DisplayPDP(PDPModel* model, int index); +void DisplayPDPs(PDPsModel* model, wpi::StringRef noneMsg = "No PDPs"); + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/hardware/PWM.h b/glass/src/lib/native/include/glass/hardware/PWM.h new file mode 100644 index 0000000000..140b78db12 --- /dev/null +++ b/glass/src/lib/native/include/glass/hardware/PWM.h @@ -0,0 +1,39 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include + +#include "glass/Model.h" + +namespace glass { + +class DataSource; + +class PWMModel : public Model { + public: + // returns -1 if not an addressable LED + virtual int GetAddressableLED() const = 0; + + virtual DataSource* GetSpeedData() = 0; + + virtual void SetSpeed(double val) = 0; +}; + +class PWMsModel : public Model { + public: + virtual void ForEachPWM( + wpi::function_ref func) = 0; +}; + +void DisplayPWM(PWMModel* model, int index, bool outputsEnabled); +void DisplayPWMs(PWMsModel* model, bool outputsEnabled, + wpi::StringRef noneMsg = "No PWM outputs"); + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/hardware/Relay.h b/glass/src/lib/native/include/glass/hardware/Relay.h new file mode 100644 index 0000000000..532f6a8c2a --- /dev/null +++ b/glass/src/lib/native/include/glass/hardware/Relay.h @@ -0,0 +1,38 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include + +#include "glass/Model.h" + +namespace glass { + +class DataSource; + +class RelayModel : public Model { + public: + virtual DataSource* GetForwardData() = 0; + virtual DataSource* GetReverseData() = 0; + + virtual void SetForward(bool val) = 0; + virtual void SetReverse(bool val) = 0; +}; + +class RelaysModel : public Model { + public: + virtual void ForEachRelay( + wpi::function_ref func) = 0; +}; + +void DisplayRelay(RelayModel* model, int index, bool outputsEnabled); +void DisplayRelays(RelaysModel* model, bool outputsEnabled, + wpi::StringRef noneMsg = "No relays"); + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/hardware/RoboRio.h b/glass/src/lib/native/include/glass/hardware/RoboRio.h new file mode 100644 index 0000000000..d0fcdb018e --- /dev/null +++ b/glass/src/lib/native/include/glass/hardware/RoboRio.h @@ -0,0 +1,46 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include "glass/Model.h" + +namespace glass { + +class DataSource; + +class RoboRioRailModel : public Model { + public: + virtual DataSource* GetVoltageData() = 0; + virtual DataSource* GetCurrentData() = 0; + virtual DataSource* GetActiveData() = 0; + virtual DataSource* GetFaultsData() = 0; + + virtual void SetVoltage(double val) = 0; + virtual void SetCurrent(double val) = 0; + virtual void SetActive(bool val) = 0; + virtual void SetFaults(int val) = 0; +}; + +class RoboRioModel : public Model { + public: + virtual RoboRioRailModel* GetUser6VRail() = 0; + virtual RoboRioRailModel* GetUser5VRail() = 0; + virtual RoboRioRailModel* GetUser3V3Rail() = 0; + + virtual DataSource* GetUserButton() = 0; + virtual DataSource* GetVInVoltageData() = 0; + virtual DataSource* GetVInCurrentData() = 0; + + virtual void SetUserButton(bool val) = 0; + virtual void SetVInVoltage(double val) = 0; + virtual void SetVInCurrent(double val) = 0; +}; + +void DisplayRoboRio(RoboRioModel* model); + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/other/DeviceTree.h b/glass/src/lib/native/include/glass/other/DeviceTree.h new file mode 100644 index 0000000000..775c18107e --- /dev/null +++ b/glass/src/lib/native/include/glass/other/DeviceTree.h @@ -0,0 +1,143 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include +#include + +#include +#include + +#include "glass/Model.h" + +namespace glass { + +class DataSource; + +/** + * Model for device tree. + */ +class DeviceTreeModel : public Model { + public: + using DisplayFunc = wpi::unique_function; + + /** + * Add a display to the device tree. + * + * @param model Model to keep updated (may be nullptr) + * @param display Display function + */ + void Add(std::unique_ptr model, DisplayFunc display) { + m_displays.emplace_back(model.get(), std::move(display)); + m_ownedModels.emplace_back(std::move(model)); + } + + void Add(Model* model, DisplayFunc display) { + m_displays.emplace_back(model, std::move(display)); + } + + void Update() override; + + bool Exists() override; + + void Display(); + + private: + std::vector> m_displays; + std::vector> m_ownedModels; +}; + +/** + * Hides device on tree. + * + * @param id device name + */ +void HideDevice(const char* id); + +/** + * Wraps CollapsingHeader() to provide both hiding functionality and open + * persistence. As with the ImGui function, returns true if the tree node + * is visible and expanded. If returns true, call EndDevice() to finish + * the block. + * + * @param id label + * @param flags ImGuiTreeNodeFlags flags + * @return True if expanded + */ +bool BeginDevice(const char* id, ImGuiTreeNodeFlags flags = 0); + +/** + * Finish a device block started with BeginDevice(). + */ +void EndDevice(); + +/** + * Displays device value. + * + * @param name value name + * @param readonly prevent value from being modified by the user + * @param value value contents (modified in place) + * @param source data source for drag source (may be nullptr) + * @return True if value was modified by the user + */ +bool DeviceBoolean(const char* name, bool readonly, bool* value, + const DataSource* source = nullptr); + +/** + * Displays device value. + * + * @param name value name + * @param readonly prevent value from being modified by the user + * @param value value contents (modified in place) + * @param source data source for drag source (may be nullptr) + * @return True if value was modified by the user + */ +bool DeviceDouble(const char* name, bool readonly, double* value, + const DataSource* source = nullptr); + +/** + * Displays device value. + * + * @param name value name + * @param readonly prevent value from being modified by the user + * @param value value contents (modified in place) + * @param options options array + * @param numOptions size of options array + * @param source data source for drag source (may be nullptr) + * @return True if value was modified by the user + */ +bool DeviceEnum(const char* name, bool readonly, int* value, + const char** options, int32_t numOptions, + const DataSource* source = nullptr); + +/** + * Displays device value. + * + * @param name value name + * @param readonly prevent value from being modified by the user + * @param value value contents (modified in place) + * @param source data source for drag source (may be nullptr) + * @return True if value was modified by the user + */ +bool DeviceInt(const char* name, bool readonly, int32_t* value, + const DataSource* source = nullptr); + +/** + * Displays device value. + * + * @param name value name + * @param readonly prevent value from being modified by the user + * @param value value contents (modified in place) + * @param source data source for drag source (may be nullptr) + * @return True if value was modified by the user + */ +bool DeviceLong(const char* name, bool readonly, int64_t* value, + const DataSource* source = nullptr); + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/other/FMS.h b/glass/src/lib/native/include/glass/other/FMS.h new file mode 100644 index 0000000000..ebdaab7119 --- /dev/null +++ b/glass/src/lib/native/include/glass/other/FMS.h @@ -0,0 +1,56 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +#include "glass/Model.h" + +namespace wpi { +template +class SmallVectorImpl; +} // namespace wpi + +namespace glass { + +class DataSource; + +class FMSModel : public Model { + public: + virtual DataSource* GetFmsAttachedData() = 0; + virtual DataSource* GetDsAttachedData() = 0; + virtual DataSource* GetAllianceStationIdData() = 0; + virtual DataSource* GetMatchTimeData() = 0; + virtual DataSource* GetEStopData() = 0; + virtual DataSource* GetEnabledData() = 0; + virtual DataSource* GetTestData() = 0; + virtual DataSource* GetAutonomousData() = 0; + virtual wpi::StringRef GetGameSpecificMessage( + wpi::SmallVectorImpl& buf) = 0; + + virtual void SetFmsAttached(bool val) = 0; + virtual void SetDsAttached(bool val) = 0; + virtual void SetAllianceStationId(int val) = 0; + virtual void SetMatchTime(double val) = 0; + virtual void SetEStop(bool val) = 0; + virtual void SetEnabled(bool val) = 0; + virtual void SetTest(bool val) = 0; + virtual void SetAutonomous(bool val) = 0; + virtual void SetGameSpecificMessage(const char* val) = 0; +}; + +/** + * Displays FMS view. + * + * @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 DisplayFMSReadOnly(FMSModel* model); + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/other/Field2D.h b/glass/src/lib/native/include/glass/other/Field2D.h new file mode 100644 index 0000000000..0fdc8baf56 --- /dev/null +++ b/glass/src/lib/native/include/glass/other/Field2D.h @@ -0,0 +1,58 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include +#include + +#include "glass/Model.h" +#include "glass/View.h" + +namespace glass { + +class DataSource; + +class FieldObjectModel : public Model { + public: + virtual DataSource* GetXData() = 0; + virtual DataSource* GetYData() = 0; + virtual DataSource* GetRotationData() = 0; + + virtual void SetPose(double x, double y, double rot) = 0; + virtual void SetPosition(double x, double y) = 0; + virtual void SetRotation(double rot) = 0; +}; + +class FieldObjectGroupModel : public Model { + public: + virtual void ForEachFieldObject( + wpi::function_ref func) = 0; +}; + +class Field2DModel : public Model { + public: + virtual void ForEachFieldObjectGroup( + wpi::function_ref + func) = 0; +}; + +void DisplayField2D(Field2DModel* model, const ImVec2& contentSize); +void DisplayField2DSettings(Field2DModel* model); + +class Field2DView : public View { + public: + explicit Field2DView(Field2DModel* model) : m_model{model} {} + + void Display() override; + + private: + Field2DModel* m_model; +}; + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/other/Plot.h b/glass/src/lib/native/include/glass/other/Plot.h new file mode 100644 index 0000000000..590c222836 --- /dev/null +++ b/glass/src/lib/native/include/glass/other/Plot.h @@ -0,0 +1,67 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include "glass/WindowManager.h" +#include "glass/support/IniSaverBase.h" + +namespace glass { + +class PlotProvider : private WindowManager { + public: + explicit PlotProvider(const wpi::Twine& iniName); + ~PlotProvider() override; + + void GlobalInit() override; + + /** + * Pauses or unpauses all plots. + * + * @param paused true to pause, false to unpause + */ + void SetPaused(bool paused) { m_paused = paused; } + + /** + * Returns true if all plots are paused. + */ + bool IsPaused() { return m_paused; } + + /** + * Resets time on all plots such that 0 = time when this function is called. + * Also clears the plot data. + */ + void ResetTime(); + + uint64_t GetStartTime() const { return m_startTime; } + + void DisplayMenu() override; + + private: + void DisplayWindows() override; + + class IniSaver : public IniSaverBase { + public: + explicit IniSaver(const wpi::Twine& typeName, PlotProvider* provider, + bool forSeries); + + void* IniReadOpen(const char* name) override; + void IniReadLine(void* entry, const char* lineStr) override; + void IniWriteAll(ImGuiTextBuffer* out_buf) override; + + private: + PlotProvider* m_provider; + bool m_forSeries; + }; + + IniSaver m_plotSaver; + IniSaver m_seriesSaver; + uint64_t m_startTime = 0; + bool m_paused = false; +}; + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/other/StringChooser.h b/glass/src/lib/native/include/glass/other/StringChooser.h new file mode 100644 index 0000000000..a905528850 --- /dev/null +++ b/glass/src/lib/native/include/glass/other/StringChooser.h @@ -0,0 +1,35 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include + +#include +#include + +#include "glass/Model.h" + +namespace glass { + +class StringChooserModel : public Model { + public: + virtual const std::string& GetDefault() = 0; + virtual const std::string& GetSelected() = 0; + virtual const std::string& GetActive() = 0; + virtual const std::vector& GetOptions() = 0; + + virtual void SetDefault(wpi::StringRef val) = 0; + virtual void SetSelected(wpi::StringRef val) = 0; + virtual void SetActive(wpi::StringRef val) = 0; + virtual void SetOptions(wpi::ArrayRef val) = 0; +}; + +void DisplayStringChooser(StringChooserModel* model); + +} // namespace glass diff --git a/simulation/halsim_gui/src/main/native/include/ExtraGuiWidgets.h b/glass/src/lib/native/include/glass/support/ExtraGuiWidgets.h similarity index 88% rename from simulation/halsim_gui/src/main/native/include/ExtraGuiWidgets.h rename to glass/src/lib/native/include/glass/support/ExtraGuiWidgets.h index 91d73010c4..ed83902c51 100644 --- a/simulation/halsim_gui/src/main/native/include/ExtraGuiWidgets.h +++ b/glass/src/lib/native/include/glass/support/ExtraGuiWidgets.h @@ -9,9 +9,9 @@ #include -namespace halsimgui { +namespace glass { -class GuiDataSource; +class DataSource; /** * DrawLEDs() configuration for 2D arrays. @@ -81,9 +81,19 @@ void DrawLEDs(const int* values, int numValues, int cols, const ImU32* colors, * if 0, defaults to 1/3 of font size * @param config 2D array configuration */ -void DrawLEDSources(const int* values, GuiDataSource** sources, int numValues, +void DrawLEDSources(const int* values, DataSource** sources, int numValues, int cols, const ImU32* colors, float size = 0.0f, float spacing = 0.0f, const LEDConfig& config = LEDConfig{}); -} // namespace halsimgui +/** + * Delete button (X in circle), based on ImGui::CloseButton(). + */ +bool DeleteButton(ImGuiID id, const ImVec2& pos); + +/** + * Create a small overlapping delete button for collapsing headers. + */ +bool HeaderDeleteButton(const char* label); + +} // namespace glass diff --git a/simulation/halsim_gui/src/main/native/include/IniSaver.h b/glass/src/lib/native/include/glass/support/IniSaver.h similarity index 64% rename from simulation/halsim_gui/src/main/native/include/IniSaver.h rename to glass/src/lib/native/include/glass/support/IniSaver.h index a36fa8b1b7..857791e471 100644 --- a/simulation/halsim_gui/src/main/native/include/IniSaver.h +++ b/glass/src/lib/native/include/glass/support/IniSaver.h @@ -8,16 +8,19 @@ #pragma once #include -#include #include +#include -namespace halsimgui { +#include "glass/support/IniSaverBase.h" + +namespace glass { template -class IniSaver { +class IniSaver : public IniSaverBase { public: - explicit IniSaver(const char* typeName) : m_typeName(typeName) {} - void Initialize(); + explicit IniSaver(const wpi::Twine& typeName, + IniSaverBackend* backend = nullptr) + : IniSaverBase(typeName, backend) {} // pass through useful functions to map Info& operator[](int index) { return m_map[index]; } @@ -31,17 +34,13 @@ class IniSaver { auto find(int index) const { return m_map.find(index); } private: - static void* ReadOpen(ImGuiContext* ctx, ImGuiSettingsHandler* handler, - const char* name); - static void ReadLine(ImGuiContext* ctx, ImGuiSettingsHandler* handler, - void* entry, const char* lineStr); - static void WriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, - ImGuiTextBuffer* out_buf); + void* IniReadOpen(const char* name) override; + void IniReadLine(void* entry, const char* lineStr) override; + void IniWriteAll(ImGuiTextBuffer* out_buf) override; - const char* m_typeName; wpi::DenseMap m_map; }; -} // namespace halsimgui +} // namespace glass #include "IniSaver.inl" diff --git a/glass/src/lib/native/include/glass/support/IniSaver.inl b/glass/src/lib/native/include/glass/support/IniSaver.inl new file mode 100644 index 0000000000..5324dfd41a --- /dev/null +++ b/glass/src/lib/native/include/glass/support/IniSaver.inl @@ -0,0 +1,40 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +namespace glass { + +template +void* IniSaver::IniReadOpen(const char* name) { + int num; + if (wpi::StringRef{name}.getAsInteger(10, num)) return nullptr; + return &m_map[num]; +} + +template +void IniSaver::IniReadLine(void* entry, const char* lineStr) { + auto element = static_cast(entry); + wpi::StringRef line{lineStr}; + auto [name, value] = line.split('='); + name = name.trim(); + value = value.trim(); + element->ReadIni(name, value); +} + +template +void IniSaver::IniWriteAll(ImGuiTextBuffer* out_buf) { + for (auto&& it : m_map) { + out_buf->appendf("[%s][%d]\n", GetTypeName(), it.first); + it.second.WriteIni(out_buf); + out_buf->append("\n"); + } +} + +} // namespace glass diff --git a/glass/src/lib/native/include/glass/support/IniSaverBase.h b/glass/src/lib/native/include/glass/support/IniSaverBase.h new file mode 100644 index 0000000000..e869d5500e --- /dev/null +++ b/glass/src/lib/native/include/glass/support/IniSaverBase.h @@ -0,0 +1,49 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +#include +#include + +namespace glass { + +class IniSaverBase; + +class IniSaverBackend { + public: + virtual ~IniSaverBackend() = default; + virtual void Register(IniSaverBase* iniSaver) = 0; + virtual void Unregister(IniSaverBase* iniSaver) = 0; +}; + +class IniSaverBase { + public: + explicit IniSaverBase(const wpi::Twine& typeName, + IniSaverBackend* backend = nullptr); + virtual ~IniSaverBase(); + + void Initialize() { m_backend->Register(this); } + + const char* GetTypeName() const { return m_typeName.c_str(); } + IniSaverBackend* GetBackend() const { return m_backend; } + + IniSaverBase(const IniSaverBase&) = delete; + IniSaverBase& operator=(const IniSaverBase&) = delete; + + virtual void* IniReadOpen(const char* name) = 0; + virtual void IniReadLine(void* entry, const char* lineStr) = 0; + virtual void IniWriteAll(ImGuiTextBuffer* out_buf) = 0; + + private: + std::string m_typeName; + IniSaverBackend* m_backend; +}; + +} // namespace glass diff --git a/simulation/halsim_gui/src/main/native/include/IniSaverInfo.h b/glass/src/lib/native/include/glass/support/IniSaverInfo.h similarity index 87% rename from simulation/halsim_gui/src/main/native/include/IniSaverInfo.h rename to glass/src/lib/native/include/glass/support/IniSaverInfo.h index 25fbe58c4f..ffff53959e 100644 --- a/simulation/halsim_gui/src/main/native/include/IniSaverInfo.h +++ b/glass/src/lib/native/include/glass/support/IniSaverInfo.h @@ -9,14 +9,16 @@ #include #include +#include -namespace halsimgui { +namespace glass { class NameInfo { public: NameInfo() { m_name[0] = '\0'; } bool HasName() const { return m_name[0] != '\0'; } + void SetName(const wpi::Twine& name); const char* GetName() const { return m_name; } void GetName(char* buf, size_t size, const char* defaultName) const; void GetName(char* buf, size_t size, const char* defaultName, @@ -55,4 +57,10 @@ class OpenInfo { bool m_open = false; }; -} // namespace halsimgui +class NameOpenInfo : public NameInfo, public OpenInfo { + public: + bool ReadIni(wpi::StringRef name, wpi::StringRef value); + void WriteIni(ImGuiTextBuffer* out); +}; + +} // namespace glass diff --git a/simulation/halsim_gui/src/main/native/include/IniSaverString.h b/glass/src/lib/native/include/glass/support/IniSaverString.h similarity index 56% rename from simulation/halsim_gui/src/main/native/include/IniSaverString.h rename to glass/src/lib/native/include/glass/support/IniSaverString.h index 206d695482..5d329c3e5a 100644 --- a/simulation/halsim_gui/src/main/native/include/IniSaverString.h +++ b/glass/src/lib/native/include/glass/support/IniSaverString.h @@ -7,22 +7,35 @@ #pragma once +#include + #include -#include #include #include +#include -namespace halsimgui { +#include "glass/support/IniSaverBase.h" + +namespace glass { template -class IniSaverString { +class IniSaverString : public IniSaverBase { public: - explicit IniSaverString(const char* typeName) : m_typeName(typeName) {} - void Initialize(); + explicit IniSaverString(const wpi::Twine& typeName, + IniSaverBackend* backend = nullptr) + : IniSaverBase(typeName, backend) {} // pass through useful functions to map Info& operator[](wpi::StringRef key) { return m_map[key]; } + template + auto try_emplace(wpi::StringRef key, ArgsTy&&... args) { + return m_map.try_emplace(key, std::forward(args)...); + } + + void erase(typename wpi::StringMap::iterator it) { m_map.erase(it); } + auto erase(wpi::StringRef key) { return m_map.erase(key); } + auto begin() { return m_map.begin(); } auto end() { return m_map.end(); } auto find(wpi::StringRef key) { return m_map.find(key); } @@ -31,18 +44,16 @@ class IniSaverString { auto end() const { return m_map.end(); } auto find(wpi::StringRef key) const { return m_map.find(key); } - private: - static void* ReadOpen(ImGuiContext* ctx, ImGuiSettingsHandler* handler, - const char* name); - static void ReadLine(ImGuiContext* ctx, ImGuiSettingsHandler* handler, - void* entry, const char* lineStr); - static void WriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, - ImGuiTextBuffer* out_buf); + bool empty() const { return m_map.empty(); } + + private: + void* IniReadOpen(const char* name) override; + void IniReadLine(void* entry, const char* lineStr) override; + void IniWriteAll(ImGuiTextBuffer* out_buf) override; - const char* m_typeName; wpi::StringMap m_map; }; -} // namespace halsimgui +} // namespace glass #include "IniSaverString.inl" diff --git a/glass/src/lib/native/include/glass/support/IniSaverString.inl b/glass/src/lib/native/include/glass/support/IniSaverString.inl new file mode 100644 index 0000000000..e29cdfef5f --- /dev/null +++ b/glass/src/lib/native/include/glass/support/IniSaverString.inl @@ -0,0 +1,38 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +namespace glass { + +template +void* IniSaverString::IniReadOpen(const char* name) { + return &m_map[name]; +} + +template +void IniSaverString::IniReadLine(void* entry, const char* lineStr) { + auto element = static_cast(entry); + wpi::StringRef line{lineStr}; + auto [name, value] = line.split('='); + name = name.trim(); + value = value.trim(); + element->ReadIni(name, value); +} + +template +void IniSaverString::IniWriteAll(ImGuiTextBuffer* out_buf) { + for (auto&& it : m_map) { + out_buf->appendf("[%s][%s]\n", GetTypeName(), it.getKey().data()); + it.second.WriteIni(out_buf); + out_buf->append("\n"); + } +} + +} // namespace glass diff --git a/simulation/halsim_gui/src/main/native/include/IniSaverVector.h b/glass/src/lib/native/include/glass/support/IniSaverVector.h similarity index 50% rename from simulation/halsim_gui/src/main/native/include/IniSaverVector.h rename to glass/src/lib/native/include/glass/support/IniSaverVector.h index 0816933980..f4244d889f 100644 --- a/simulation/halsim_gui/src/main/native/include/IniSaverVector.h +++ b/glass/src/lib/native/include/glass/support/IniSaverVector.h @@ -10,27 +10,25 @@ #include #include -#include +#include -namespace halsimgui { +#include "glass/support/IniSaverBase.h" + +namespace glass { template -class IniSaverVector : public std::vector { +class IniSaverVector : public std::vector, public IniSaverBase { public: - explicit IniSaverVector(const char* typeName) : m_typeName(typeName) {} - void Initialize(); + explicit IniSaverVector(const wpi::Twine& typeName, + IniSaverBackend* backend = nullptr) + : IniSaverBase(typeName, backend) {} private: - static void* ReadOpen(ImGuiContext* ctx, ImGuiSettingsHandler* handler, - const char* name); - static void ReadLine(ImGuiContext* ctx, ImGuiSettingsHandler* handler, - void* entry, const char* lineStr); - static void WriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, - ImGuiTextBuffer* out_buf); - - const char* m_typeName; + void* IniReadOpen(const char* name) override; + void IniReadLine(void* entry, const char* lineStr) override; + void IniWriteAll(ImGuiTextBuffer* out_buf) override; }; -} // namespace halsimgui +} // namespace glass #include "IniSaverVector.inl" diff --git a/glass/src/lib/native/include/glass/support/IniSaverVector.inl b/glass/src/lib/native/include/glass/support/IniSaverVector.inl new file mode 100644 index 0000000000..d87a0ac188 --- /dev/null +++ b/glass/src/lib/native/include/glass/support/IniSaverVector.inl @@ -0,0 +1,41 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +namespace glass { + +template +void* IniSaverVector::IniReadOpen(const char* name) { + unsigned int num; + if (wpi::StringRef{name}.getAsInteger(10, num)) return nullptr; + if (num >= this->size()) this->resize(num + 1); + return &(*this)[num]; +} + +template +void IniSaverVector::IniReadLine(void* entry, const char* lineStr) { + auto element = static_cast(entry); + wpi::StringRef line{lineStr}; + auto [name, value] = line.split('='); + name = name.trim(); + value = value.trim(); + element->ReadIni(name, value); +} + +template +void IniSaverVector::IniWriteAll(ImGuiTextBuffer* out_buf) { + for (size_t i = 0; i < this->size(); ++i) { + out_buf->appendf("[%s][%d]\n", GetTypeName(), static_cast(i)); + (*this)[i].WriteIni(out_buf); + out_buf->append("\n"); + } +} + +} // namespace glass diff --git a/glass/src/libnt/native/cpp/NTDigitalInput.cpp b/glass/src/libnt/native/cpp/NTDigitalInput.cpp new file mode 100644 index 0000000000..c2d2747b08 --- /dev/null +++ b/glass/src/libnt/native/cpp/NTDigitalInput.cpp @@ -0,0 +1,46 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/networktables/NTDigitalInput.h" + +#include + +using namespace glass; + +NTDigitalInputModel::NTDigitalInputModel(wpi::StringRef path) + : NTDigitalInputModel{nt::GetDefaultInstance(), path} {} + +NTDigitalInputModel::NTDigitalInputModel(NT_Inst inst, wpi::StringRef path) + : m_nt{inst}, + m_value{m_nt.GetEntry(path + "/Value")}, + m_name{m_nt.GetEntry(path + "/.name")}, + m_valueData{"NT_DIn:" + path}, + m_nameValue{path.rsplit('/').second} { + m_nt.AddListener(m_value); + m_nt.AddListener(m_name); + + m_valueData.SetDigital(true); + Update(); +} + +void NTDigitalInputModel::Update() { + for (auto&& event : m_nt.PollListener()) { + if (event.entry == m_value) { + if (event.value && event.value->IsBoolean()) { + m_valueData.SetValue(event.value->GetBoolean()); + } + } else if (event.entry == m_name) { + if (event.value && event.value->IsString()) { + m_nameValue = event.value->GetString(); + } + } + } +} + +bool NTDigitalInputModel::Exists() { + return m_nt.IsConnected() && nt::GetEntryType(m_value) != NT_UNASSIGNED; +} diff --git a/glass/src/libnt/native/cpp/NTDigitalOutput.cpp b/glass/src/libnt/native/cpp/NTDigitalOutput.cpp new file mode 100644 index 0000000000..e7df09c5a7 --- /dev/null +++ b/glass/src/libnt/native/cpp/NTDigitalOutput.cpp @@ -0,0 +1,55 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/networktables/NTDigitalOutput.h" + +#include + +using namespace glass; + +NTDigitalOutputModel::NTDigitalOutputModel(wpi::StringRef path) + : NTDigitalOutputModel{nt::GetDefaultInstance(), path} {} + +NTDigitalOutputModel::NTDigitalOutputModel(NT_Inst inst, wpi::StringRef path) + : m_nt{inst}, + m_value{m_nt.GetEntry(path + "/Value")}, + m_name{m_nt.GetEntry(path + "/.name")}, + m_controllable{m_nt.GetEntry(path + "/.controllable")}, + m_valueData{"NT_DOut:" + path} { + m_nt.AddListener(m_value); + m_nt.AddListener(m_name); + m_nt.AddListener(m_controllable); + + m_valueData.SetDigital(true); + Update(); +} + +void NTDigitalOutputModel::SetValue(bool val) { + nt::SetEntryValue(m_value, nt::Value::MakeBoolean(val)); +} + +void NTDigitalOutputModel::Update() { + for (auto&& event : m_nt.PollListener()) { + if (event.entry == m_value) { + if (event.value && event.value->IsBoolean()) { + m_valueData.SetValue(event.value->GetBoolean()); + } + } else if (event.entry == m_name) { + if (event.value && event.value->IsString()) { + m_nameValue = event.value->GetString(); + } + } else if (event.entry == m_controllable) { + if (event.value && event.value->IsBoolean()) { + m_controllableValue = event.value->GetBoolean(); + } + } + } +} + +bool NTDigitalOutputModel::Exists() { + return m_nt.IsConnected() && nt::GetEntryType(m_value) != NT_UNASSIGNED; +} diff --git a/glass/src/libnt/native/cpp/NTFMS.cpp b/glass/src/libnt/native/cpp/NTFMS.cpp new file mode 100644 index 0000000000..048db261bb --- /dev/null +++ b/glass/src/libnt/native/cpp/NTFMS.cpp @@ -0,0 +1,93 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/networktables/NTFMS.h" + +#include + +#include +#include + +using namespace glass; + +NTFMSModel::NTFMSModel(wpi::StringRef path) + : NTFMSModel{nt::GetDefaultInstance(), path} {} + +NTFMSModel::NTFMSModel(NT_Inst inst, wpi::StringRef path) + : m_nt{inst}, + m_gameSpecificMessage{m_nt.GetEntry(path + "/GameSpecificMessage")}, + m_alliance{m_nt.GetEntry(path + "/IsRedAlliance")}, + m_station{m_nt.GetEntry(path + "/StationNumber")}, + m_controlWord{m_nt.GetEntry(path + "/FMSControlData")}, + m_fmsAttached{"NT_FMS:FMSAttached:" + path}, + m_dsAttached{"NT_FMS:DSAttached:" + path}, + m_allianceStationId{"NT_FMS:AllianceStationID:" + path}, + m_estop{"NT_FMS:EStop:" + path}, + m_enabled{"NT_FMS:RobotEnabled:" + path}, + m_test{"NT_FMS:TestMode:" + path}, + m_autonomous{"NT_FMS:AutonomousMode:" + path} { + m_nt.AddListener(m_alliance); + m_nt.AddListener(m_station); + m_nt.AddListener(m_controlWord); + + m_fmsAttached.SetDigital(true); + m_dsAttached.SetDigital(true); + m_estop.SetDigital(true); + m_enabled.SetDigital(true); + m_test.SetDigital(true); + m_autonomous.SetDigital(true); + Update(); +} + +wpi::StringRef NTFMSModel::GetGameSpecificMessage( + wpi::SmallVectorImpl& buf) { + buf.clear(); + auto value = nt::GetEntryValue(m_gameSpecificMessage); + if (value && value->IsString()) { + auto str = value->GetString(); + buf.append(str.begin(), str.end()); + } + return wpi::StringRef{buf.data(), buf.size()}; +} + +void NTFMSModel::Update() { + for (auto&& event : m_nt.PollListener()) { + if (event.entry == m_alliance) { + if (event.value && event.value->IsBoolean()) { + int allianceStationId = m_allianceStationId.GetValue(); + allianceStationId %= 3; + // true if red + allianceStationId += 3 * (event.value->GetBoolean() ? 0 : 1); + m_allianceStationId.SetValue(allianceStationId); + } + } else if (event.entry == m_station) { + if (event.value && event.value->IsDouble()) { + int allianceStationId = m_allianceStationId.GetValue(); + bool isRed = (allianceStationId < 3); + // the NT value is 1-indexed + m_allianceStationId.SetValue(event.value->GetDouble() - 1 + + 3 * (isRed ? 0 : 1)); + } + } else if (event.entry == m_controlWord) { + if (event.value && event.value->IsDouble()) { + uint32_t controlWord = event.value->GetDouble(); + // See HAL_ControlWord definition + auto time = wpi::Now(); + m_enabled.SetValue(((controlWord & 0x01) != 0) ? 1 : 0, time); + m_autonomous.SetValue(((controlWord & 0x02) != 0) ? 1 : 0, time); + m_test.SetValue(((controlWord & 0x04) != 0) ? 1 : 0, time); + m_estop.SetValue(((controlWord & 0x08) != 0) ? 1 : 0, time); + m_fmsAttached.SetValue(((controlWord & 0x10) != 0) ? 1 : 0, time); + m_dsAttached.SetValue(((controlWord & 0x20) != 0) ? 1 : 0, time); + } + } + } +} + +bool NTFMSModel::Exists() { + return m_nt.IsConnected() && nt::GetEntryType(m_controlWord) != NT_UNASSIGNED; +} diff --git a/glass/src/libnt/native/cpp/NTField2D.cpp b/glass/src/libnt/native/cpp/NTField2D.cpp new file mode 100644 index 0000000000..1f87813d03 --- /dev/null +++ b/glass/src/libnt/native/cpp/NTField2D.cpp @@ -0,0 +1,236 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/networktables/NTField2D.h" + +#include + +#include +#include + +#include "glass/DataSource.h" + +using namespace glass; + +class NTField2DModel::GroupModel : public FieldObjectGroupModel { + public: + GroupModel(wpi::StringRef name, NT_Entry entry) + : m_name{name}, m_entry{entry} {} + + wpi::StringRef GetName() const { return m_name; } + NT_Entry GetEntry() const { return m_entry; } + + void NTUpdate(const nt::Value& value); + + void Update() override { + if (auto value = nt::GetEntryValue(m_entry)) NTUpdate(*value); + } + bool Exists() override { return nt::GetEntryType(m_entry) != NT_UNASSIGNED; } + bool IsReadOnly() override { return false; } + + void ForEachFieldObject( + wpi::function_ref func) override; + + private: + std::string m_name; + NT_Entry m_entry; + + // keep count of objects rather than resizing vector, as there is a fair + // amount of overhead associated with the latter (DataSource record keeping) + size_t m_count = 0; + class ObjectModel; + std::vector> m_objects; +}; + +class NTField2DModel::GroupModel::ObjectModel : public FieldObjectModel { + public: + ObjectModel(wpi::StringRef name, NT_Entry entry, int index) + : m_entry{entry}, + m_index{index}, + m_x{name + "[" + wpi::Twine{index} + "]/x"}, + m_y{name + "[" + wpi::Twine{index} + "]/y"}, + m_rot{name + "[" + wpi::Twine{index} + "]/rot"} {} + + void SetExists(bool exists) { m_exists = exists; } + + void Update() override {} + bool Exists() override { return m_exists; } + bool IsReadOnly() override { return false; } + + DataSource* GetXData() override { return &m_x; } + DataSource* GetYData() override { return &m_y; } + DataSource* GetRotationData() override { return &m_rot; } + + void SetPose(double x, double y, double rot) override; + void SetPosition(double x, double y) override; + void SetRotation(double rot) override; + + private: + void SetPoseImpl(double x, double y, double rot, bool setX, bool setY, + bool setRot); + + NT_Entry m_entry; + int m_index; + bool m_exists = true; + + public: + DataSource m_x; + DataSource m_y; + DataSource m_rot; +}; + +void NTField2DModel::GroupModel::NTUpdate(const nt::Value& value) { + if (!value.IsDoubleArray()) { + m_count = 0; + return; + } + + auto arr = value.GetDoubleArray(); + // must be triples + if ((arr.size() % 3) != 0) { + m_count = 0; + return; + } + + m_count = arr.size() / 3; + if (m_count > m_objects.size()) { + m_objects.reserve(m_count); + for (size_t i = m_objects.size(); i < m_count; ++i) + m_objects.emplace_back(std::make_unique(m_name, m_entry, i)); + } + if (m_count < m_objects.size()) { + for (size_t i = m_count; i < m_objects.size(); ++i) + m_objects[i]->SetExists(false); + } + + for (size_t i = 0; i < m_count; ++i) { + auto& obj = m_objects[i]; + obj->SetExists(true); + obj->m_x.SetValue(arr[i * 3], value.last_change()); + obj->m_y.SetValue(arr[i * 3 + 1], value.last_change()); + obj->m_rot.SetValue(arr[i * 3 + 2], value.last_change()); + } +} + +void NTField2DModel::GroupModel::ForEachFieldObject( + wpi::function_ref func) { + for (size_t i = 0; i < m_count; ++i) { + func(*m_objects[i]); + } +} + +void NTField2DModel::GroupModel::ObjectModel::SetPose(double x, double y, + double rot) { + SetPoseImpl(x, y, rot, true, true, true); +} + +void NTField2DModel::GroupModel::ObjectModel::SetPosition(double x, double y) { + SetPoseImpl(x, y, 0, true, true, false); +} + +void NTField2DModel::GroupModel::ObjectModel::SetRotation(double rot) { + SetPoseImpl(0, 0, rot, false, false, true); +} + +void NTField2DModel::GroupModel::ObjectModel::SetPoseImpl(double x, double y, + double rot, bool setX, + bool setY, + bool setRot) { + // get from NT, validate type and size + auto value = nt::GetEntryValue(m_entry); + if (!value || !value->IsDoubleArray()) return; + auto origArr = value->GetDoubleArray(); + if (origArr.size() < static_cast((m_index + 1) * 3)) return; + + // copy existing array + wpi::SmallVector arr; + arr.reserve(origArr.size()); + for (auto&& elem : origArr) arr.emplace_back(elem); + + // update value + if (setX) arr[m_index * 3 + 0] = x; + if (setY) arr[m_index * 3 + 1] = y; + if (setRot) arr[m_index * 3 + 2] = rot; + + // set back to NT + nt::SetEntryValue(m_entry, nt::Value::MakeDoubleArray(arr)); +} + +NTField2DModel::NTField2DModel(wpi::StringRef path) + : NTField2DModel{nt::GetDefaultInstance(), path} {} + +NTField2DModel::NTField2DModel(NT_Inst inst, wpi::StringRef path) + : m_nt{inst}, + m_path{(path + "/").str()}, + m_name{m_nt.GetEntry(path + "/.name")} { + m_nt.AddListener(m_path, NT_NOTIFY_LOCAL | NT_NOTIFY_NEW | NT_NOTIFY_DELETE | + NT_NOTIFY_UPDATE | NT_NOTIFY_IMMEDIATE); +} + +NTField2DModel::~NTField2DModel() {} + +void NTField2DModel::Update() { + for (auto&& event : m_nt.PollListener()) { + // .name + if (event.entry == m_name) { + if (event.value && event.value->IsString()) { + m_nameValue = event.value->GetString(); + } + continue; + } + + // common case: update of existing entry; search by entry + if (event.flags & NT_NOTIFY_UPDATE) { + auto it = std::find_if( + m_groups.begin(), m_groups.end(), + [&](const auto& e) { return e->GetEntry() == event.entry; }); + if (it != m_groups.end()) { + (*it)->NTUpdate(*event.value); + continue; + } + } + + // handle create/delete + if (wpi::StringRef{event.name}.startswith(m_path)) { + auto name = wpi::StringRef{event.name}.drop_front(m_path.size()); + if (name.empty() || name[0] == '.') continue; + auto it = std::lower_bound(m_groups.begin(), m_groups.end(), name, + [](const auto& e, wpi::StringRef name) { + return e->GetName() < name; + }); + bool match = (it != m_groups.end() && (*it)->GetName() == name); + if (event.flags & NT_NOTIFY_DELETE) { + if (match) m_groups.erase(it); + continue; + } else if (event.flags & NT_NOTIFY_NEW) { + if (!match) + it = m_groups.emplace( + it, std::make_unique(event.name, event.entry)); + } else if (!match) { + continue; + } + if (event.flags & (NT_NOTIFY_NEW | NT_NOTIFY_UPDATE)) { + (*it)->NTUpdate(*event.value); + } + } + } +} + +bool NTField2DModel::Exists() { + return m_nt.IsConnected() && nt::GetEntryType(m_name) != NT_UNASSIGNED; +} + +bool NTField2DModel::IsReadOnly() { return false; } + +void NTField2DModel::ForEachFieldObjectGroup( + wpi::function_ref + func) { + for (auto&& group : m_groups) { + if (group->Exists()) + func(*group, wpi::StringRef{group->GetName()}.drop_front(m_path.size())); + } +} diff --git a/glass/src/libnt/native/cpp/NTStringChooser.cpp b/glass/src/libnt/native/cpp/NTStringChooser.cpp new file mode 100644 index 0000000000..d9dc71445b --- /dev/null +++ b/glass/src/libnt/native/cpp/NTStringChooser.cpp @@ -0,0 +1,64 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/networktables/NTStringChooser.h" + +using namespace glass; + +NTStringChooserModel::NTStringChooserModel(wpi::StringRef path) + : NTStringChooserModel{nt::GetDefaultInstance(), path} {} + +NTStringChooserModel::NTStringChooserModel(NT_Inst inst, wpi::StringRef path) + : m_nt{inst}, + m_default{m_nt.GetEntry(path + "/default")}, + m_selected{m_nt.GetEntry(path + "/selected")}, + m_active{m_nt.GetEntry(path + "/active")}, + m_options{m_nt.GetEntry(path + "/options")} { + m_nt.AddListener(m_default); + m_nt.AddListener(m_selected); + m_nt.AddListener(m_active); + m_nt.AddListener(m_options); + + Update(); +} + +void NTStringChooserModel::SetDefault(wpi::StringRef val) { + nt::SetEntryValue(m_default, nt::Value::MakeString(val)); +} + +void NTStringChooserModel::SetSelected(wpi::StringRef val) { + nt::SetEntryValue(m_selected, nt::Value::MakeString(val)); +} + +void NTStringChooserModel::SetActive(wpi::StringRef val) { + nt::SetEntryValue(m_active, nt::Value::MakeString(val)); +} + +void NTStringChooserModel::SetOptions(wpi::ArrayRef val) { + nt::SetEntryValue(m_options, nt::Value::MakeStringArray(val)); +} + +void NTStringChooserModel::Update() { + for (auto&& event : m_nt.PollListener()) { + if (event.entry == m_default && event.value && event.value->IsString()) { + m_defaultValue = event.value->GetString(); + } else if (event.entry == m_selected && event.value && + event.value->IsString()) { + m_selectedValue = event.value->GetString(); + } else if (event.entry == m_active && event.value && + event.value->IsString()) { + m_activeValue = event.value->GetString(); + } else if (event.entry == m_options && event.value && + event.value->IsStringArray()) { + m_optionsValue = event.value->GetStringArray(); + } + } +} + +bool NTStringChooserModel::Exists() { + return m_nt.IsConnected() && nt::GetEntryType(m_options) != NT_UNASSIGNED; +} diff --git a/glass/src/libnt/native/cpp/NetworkTables.cpp b/glass/src/libnt/native/cpp/NetworkTables.cpp new file mode 100644 index 0000000000..4bbd270435 --- /dev/null +++ b/glass/src/libnt/native/cpp/NetworkTables.cpp @@ -0,0 +1,610 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/networktables/NetworkTables.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "glass/Context.h" +#include "glass/DataSource.h" + +using namespace glass; + +static std::string BooleanArrayToString(wpi::ArrayRef in) { + std::string rv; + wpi::raw_string_ostream os{rv}; + os << '['; + bool first = true; + for (auto v : in) { + if (!first) os << ','; + first = false; + if (v) + os << "true"; + else + os << "false"; + } + os << ']'; + return rv; +} + +static std::string DoubleArrayToString(wpi::ArrayRef in) { + std::string rv; + wpi::raw_string_ostream os{rv}; + os << '['; + bool first = true; + for (auto v : in) { + if (!first) os << ','; + first = false; + os << wpi::format("%.6f", v); + } + os << ']'; + return rv; +} + +static std::string StringArrayToString(wpi::ArrayRef in) { + std::string rv; + wpi::raw_string_ostream os{rv}; + os << '['; + bool first = true; + for (auto&& v : in) { + if (!first) os << ','; + first = false; + os << '"'; + os.write_escaped(v); + os << '"'; + } + os << ']'; + return rv; +} + +NetworkTablesModel::NetworkTablesModel() + : NetworkTablesModel{nt::GetDefaultInstance()} {} + +NetworkTablesModel::NetworkTablesModel(NT_Inst inst) + : m_inst{inst}, m_poller{nt::CreateEntryListenerPoller(inst)} { + nt::AddPolledEntryListener(m_poller, "", + NT_NOTIFY_LOCAL | NT_NOTIFY_NEW | + NT_NOTIFY_UPDATE | NT_NOTIFY_DELETE | + NT_NOTIFY_FLAGS | NT_NOTIFY_IMMEDIATE); +} + +NetworkTablesModel::~NetworkTablesModel() { + nt::DestroyEntryListenerPoller(m_poller); +} + +NetworkTablesModel::Entry::Entry(nt::EntryNotification&& event) + : entry{event.entry}, + name{std::move(event.name)}, + value{std::move(event.value)}, + flags{nt::GetEntryFlags(event.entry)} { + UpdateValue(); +} + +void NetworkTablesModel::Entry::UpdateValue() { + switch (value->type()) { + case NT_BOOLEAN: + if (!source) + source = std::make_unique(wpi::Twine{"NT:"} + name); + source->SetValue(value->GetBoolean() ? 1 : 0); + source->SetDigital(true); + break; + case NT_DOUBLE: + if (!source) + source = std::make_unique(wpi::Twine{"NT:"} + name); + source->SetValue(value->GetDouble()); + source->SetDigital(false); + break; + case NT_BOOLEAN_ARRAY: + valueStr = BooleanArrayToString(value->GetBooleanArray()); + break; + case NT_DOUBLE_ARRAY: + valueStr = DoubleArrayToString(value->GetDoubleArray()); + break; + case NT_STRING_ARRAY: + valueStr = StringArrayToString(value->GetStringArray()); + break; + default: + break; + } +} + +void NetworkTablesModel::Update() { + bool timedOut = false; + bool updateTree = false; + for (auto&& event : nt::PollEntryListener(m_poller, 0, &timedOut)) { + auto& entry = m_entries[event.entry]; + if (event.flags & NT_NOTIFY_NEW) { + if (!entry) { + entry = std::make_unique(std::move(event)); + m_sortedEntries.emplace_back(entry.get()); + updateTree = true; + } + } + if (!entry) continue; + if (event.flags & NT_NOTIFY_DELETE) { + auto it = std::find(m_sortedEntries.begin(), m_sortedEntries.end(), + entry.get()); + // will be removed completely below + if (it != m_sortedEntries.end()) *it = nullptr; + m_entries.erase(event.entry); + updateTree = true; + continue; + } + if (event.flags & NT_NOTIFY_UPDATE) { + entry->value = std::move(event.value); + entry->UpdateValue(); + } + if (event.flags & NT_NOTIFY_FLAGS) { + entry->flags = nt::GetEntryFlags(event.entry); + } + } + + // shortcut common case (updates) + if (!updateTree) return; + + // remove deleted entries + m_sortedEntries.erase( + std::remove(m_sortedEntries.begin(), m_sortedEntries.end(), nullptr), + m_sortedEntries.end()); + + // sort by name + std::sort(m_sortedEntries.begin(), m_sortedEntries.end(), + [](const auto& a, const auto& b) { return a->name < b->name; }); + + // rebuild tree + m_root.clear(); + wpi::SmallVector parts; + for (auto& entry : m_sortedEntries) { + parts.clear(); + wpi::StringRef{entry->name}.split(parts, '/', -1, false); + + // get to leaf + auto nodes = &m_root; + for (auto part : wpi::ArrayRef(parts.begin(), parts.end()).drop_back()) { + 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 + // entry->name + nodes->back().path.assign(entry->name.data(), + part.end() - entry->name.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 entry->name + it = nodes->end() - 1; + } + it->entry = entry; + } +} + +bool NetworkTablesModel::Exists() { return nt::IsConnected(m_inst); } + +static std::shared_ptr StringToBooleanArray(wpi::StringRef in) { + in = in.trim(); + if (in.empty()) + return nt::NetworkTableValue::MakeBooleanArray( + std::initializer_list{}); + if (in.front() == '[') in = in.drop_front(); + if (in.back() == ']') in = in.drop_back(); + in = in.trim(); + + wpi::SmallVector inSplit; + wpi::SmallVector out; + + in.split(inSplit, ',', -1, false); + for (auto val : inSplit) { + val = val.trim(); + if (val.equals_lower("true")) { + out.emplace_back(1); + } else if (val.equals_lower("false")) { + out.emplace_back(0); + } else { + wpi::errs() << "GUI: NetworkTables: Could not understand value '" << val + << "'\n"; + return nullptr; + } + } + + return nt::NetworkTableValue::MakeBooleanArray(out); +} + +static std::shared_ptr StringToDoubleArray(wpi::StringRef in) { + in = in.trim(); + if (in.empty()) + return nt::NetworkTableValue::MakeBooleanArray( + std::initializer_list{}); + if (in.front() == '[') in = in.drop_front(); + if (in.back() == ']') in = in.drop_back(); + in = in.trim(); + + wpi::SmallVector inSplit; + wpi::SmallVector out; + + in.split(inSplit, ',', -1, false); + for (auto val : inSplit) { + val = val.trim(); + wpi::SmallString<32> valStr = val; + double d; + if (std::sscanf(valStr.c_str(), "%lf", &d) == 1) { + out.emplace_back(d); + } else { + wpi::errs() << "GUI: NetworkTables: Could not understand value '" << val + << "'\n"; + return nullptr; + } + } + + return nt::NetworkTableValue::MakeDoubleArray(out); +} + +static int fromxdigit(char ch) { + if (ch >= 'a' && ch <= 'f') + return (ch - 'a' + 10); + else if (ch >= 'A' && ch <= 'F') + return (ch - 'A' + 10); + else + return ch - '0'; +} + +static wpi::StringRef UnescapeString(wpi::StringRef source, + wpi::SmallVectorImpl& buf) { + assert(source.size() >= 2 && source.front() == '"' && source.back() == '"'); + buf.clear(); + buf.reserve(source.size() - 2); + for (auto s = source.begin() + 1, end = source.end() - 1; s != end; ++s) { + if (*s != '\\') { + buf.push_back(*s); + continue; + } + switch (*++s) { + case 't': + buf.push_back('\t'); + break; + case 'n': + buf.push_back('\n'); + break; + case 'x': { + if (!isxdigit(*(s + 1))) { + buf.push_back('x'); // treat it like a unknown escape + break; + } + int ch = fromxdigit(*++s); + if (std::isxdigit(*(s + 1))) { + ch <<= 4; + ch |= fromxdigit(*++s); + } + buf.push_back(static_cast(ch)); + break; + } + default: + buf.push_back(*s); + break; + } + } + return wpi::StringRef{buf.data(), buf.size()}; +} + +static std::shared_ptr StringToStringArray(wpi::StringRef in) { + in = in.trim(); + if (in.empty()) + return nt::NetworkTableValue::MakeStringArray( + std::initializer_list{}); + if (in.front() == '[') in = in.drop_front(); + if (in.back() == ']') in = in.drop_back(); + in = in.trim(); + + wpi::SmallVector inSplit; + std::vector out; + wpi::SmallString<32> buf; + + in.split(inSplit, ',', -1, false); + for (auto val : inSplit) { + val = val.trim(); + if (val.empty()) continue; + if (val.front() != '"' || val.back() != '"') { + wpi::errs() << "GUI: NetworkTables: Could not understand value '" << val + << "'\n"; + return nullptr; + } + out.emplace_back(UnescapeString(val, buf)); + } + + return nt::NetworkTableValue::MakeStringArray(std::move(out)); +} + +static void EmitEntryValueReadonly(NetworkTablesModel::Entry& entry) { + auto& val = entry.value; + if (!val) return; + + switch (val->type()) { + case NT_BOOLEAN: + ImGui::LabelText("boolean", "%s", val->GetBoolean() ? "true" : "false"); + break; + case NT_DOUBLE: + ImGui::LabelText("double", "%.6f", val->GetDouble()); + break; + case NT_STRING: { + // GetString() comes from a std::string, so it's null terminated + ImGui::LabelText("string", "%s", val->GetString().data()); + break; + } + case NT_BOOLEAN_ARRAY: + ImGui::LabelText("boolean[]", "%s", entry.valueStr.c_str()); + break; + case NT_DOUBLE_ARRAY: + ImGui::LabelText("double[]", "%s", entry.valueStr.c_str()); + break; + case NT_STRING_ARRAY: + ImGui::LabelText("string[]", "%s", entry.valueStr.c_str()); + break; + case NT_RAW: + ImGui::LabelText("raw", "[...]"); + break; + case NT_RPC: + ImGui::LabelText("rpc", "[...]"); + break; + default: + ImGui::LabelText("other", "?"); + break; + } +} + +static constexpr size_t kTextBufferSize = 4096; + +static char* GetTextBuffer(wpi::StringRef in) { + static char textBuffer[kTextBufferSize]; + size_t len = (std::min)(in.size(), kTextBufferSize - 1); + std::memcpy(textBuffer, in.data(), len); + textBuffer[len] = '\0'; + return textBuffer; +} + +static void EmitEntryValueEditable(NetworkTablesModel::Entry& entry) { + auto& val = entry.value; + if (!val) return; + + ImGui::PushID(entry.name.c_str()); + switch (val->type()) { + case NT_BOOLEAN: { + static const char* boolOptions[] = {"false", "true"}; + int v = val->GetBoolean() ? 1 : 0; + if (ImGui::Combo("boolean", &v, boolOptions, 2)) + nt::SetEntryValue(entry.entry, nt::NetworkTableValue::MakeBoolean(v)); + break; + } + case NT_DOUBLE: { + double v = val->GetDouble(); + if (ImGui::InputDouble("double", &v, 0, 0, "%.6f", + ImGuiInputTextFlags_EnterReturnsTrue)) + nt::SetEntryValue(entry.entry, nt::NetworkTableValue::MakeDouble(v)); + break; + } + case NT_STRING: { + char* v = GetTextBuffer(val->GetString()); + if (ImGui::InputText("string", v, kTextBufferSize, + ImGuiInputTextFlags_EnterReturnsTrue)) + nt::SetEntryValue(entry.entry, nt::NetworkTableValue::MakeString(v)); + break; + } + case NT_BOOLEAN_ARRAY: { + char* v = GetTextBuffer(entry.valueStr); + if (ImGui::InputText("boolean[]", v, kTextBufferSize, + ImGuiInputTextFlags_EnterReturnsTrue)) { + if (auto outv = StringToBooleanArray(v)) + nt::SetEntryValue(entry.entry, std::move(outv)); + } + break; + } + case NT_DOUBLE_ARRAY: { + char* v = GetTextBuffer(entry.valueStr); + if (ImGui::InputText("double[]", v, kTextBufferSize, + ImGuiInputTextFlags_EnterReturnsTrue)) { + if (auto outv = StringToDoubleArray(v)) + nt::SetEntryValue(entry.entry, std::move(outv)); + } + break; + } + case NT_STRING_ARRAY: { + char* v = GetTextBuffer(entry.valueStr); + if (ImGui::InputText("string[]", v, kTextBufferSize, + ImGuiInputTextFlags_EnterReturnsTrue)) { + if (auto outv = StringToStringArray(v)) + nt::SetEntryValue(entry.entry, std::move(outv)); + } + break; + } + case NT_RAW: + ImGui::LabelText("raw", "[...]"); + break; + case NT_RPC: + ImGui::LabelText("rpc", "[...]"); + break; + default: + ImGui::LabelText("other", "?"); + break; + } + ImGui::PopID(); +} + +static void EmitEntry(NetworkTablesModel::Entry& entry, const char* name, + NetworkTablesFlags flags) { + if (entry.source) { + ImGui::Selectable(name); + entry.source->EmitDrag(); + } else { + ImGui::Text("%s", name); + } + ImGui::NextColumn(); + + if (flags & NetworkTablesFlags_ReadOnly) + EmitEntryValueReadonly(entry); + else + EmitEntryValueEditable(entry); + ImGui::NextColumn(); + + if (flags & NetworkTablesFlags_ShowFlags) { + if ((entry.flags & NT_PERSISTENT) != 0) + ImGui::Text("Persistent"); + else if (entry.flags != 0) + ImGui::Text("%02x", entry.flags); + ImGui::NextColumn(); + } + + if (flags & NetworkTablesFlags_ShowTimestamp) { + if (entry.value) + ImGui::Text("%" PRIu64, entry.value->last_change()); + else + ImGui::TextUnformatted(""); + ImGui::NextColumn(); + } + ImGui::Separator(); +} + +static void EmitTree(const std::vector& tree, + NetworkTablesFlags flags) { + for (auto&& node : tree) { + if (node.entry) { + EmitEntry(*node.entry, node.name.c_str(), flags); + } + + if (!node.children.empty()) { + bool open = + TreeNodeEx(node.name.c_str(), ImGuiTreeNodeFlags_SpanFullWidth); + ImGui::NextColumn(); + ImGui::NextColumn(); + if (flags & NetworkTablesFlags_ShowFlags) ImGui::NextColumn(); + if (flags & NetworkTablesFlags_ShowTimestamp) ImGui::NextColumn(); + ImGui::Separator(); + if (open) { + EmitTree(node.children, flags); + TreePop(); + } + } + } +} + +void glass::DisplayNetworkTables(NetworkTablesModel* model, + NetworkTablesFlags flags) { + auto inst = model->GetInstance(); + + if (flags & NetworkTablesFlags_ShowConnections) { + if (CollapsingHeader("Connections")) { + ImGui::Columns(4, "connections"); + ImGui::Text("Id"); + ImGui::NextColumn(); + ImGui::Text("Address"); + ImGui::NextColumn(); + ImGui::Text("Updated"); + ImGui::NextColumn(); + ImGui::Text("Proto"); + ImGui::NextColumn(); + ImGui::Separator(); + for (auto&& i : nt::GetConnections(inst)) { + ImGui::Text("%s", i.remote_id.c_str()); + ImGui::NextColumn(); + ImGui::Text("%s", i.remote_ip.c_str()); + ImGui::NextColumn(); + ImGui::Text("%llu", + static_cast( // NOLINT(runtime/int) + i.last_update)); + ImGui::NextColumn(); + ImGui::Text("%d.%d", i.protocol_version >> 8, + i.protocol_version & 0xff); + ImGui::NextColumn(); + } + ImGui::Columns(); + } + + if (!CollapsingHeader("Values", ImGuiTreeNodeFlags_DefaultOpen)) return; + } + + const bool showFlags = (flags & NetworkTablesFlags_ShowFlags); + const bool showTimestamp = (flags & NetworkTablesFlags_ShowTimestamp); + + static bool first = true; + ImGui::Columns(2 + (showFlags ? 1 : 0) + (showTimestamp ? 1 : 0), "values"); + if (first) ImGui::SetColumnWidth(-1, 0.5f * ImGui::GetWindowWidth()); + ImGui::Text("Name"); + ImGui::NextColumn(); + ImGui::Text("Value"); + ImGui::NextColumn(); + if (showFlags) { + if (first) ImGui::SetColumnWidth(-1, 12 * ImGui::GetFontSize()); + ImGui::Text("Flags"); + ImGui::NextColumn(); + } + if (showTimestamp) { + ImGui::Text("Changed"); + ImGui::NextColumn(); + } + ImGui::Separator(); + first = false; + + if (flags & NetworkTablesFlags_TreeView) { + EmitTree(model->GetTreeRoot(), flags); + } else { + for (auto entry : model->GetEntries()) { + EmitEntry(*entry, entry->name.c_str(), flags); + } + } + ImGui::Columns(); +} + +void NetworkTablesView::Display() { + if (ImGui::BeginPopupContextItem()) { + auto& storage = GetStorage(); + auto pTreeView = storage.GetBoolRef( + "tree", m_defaultFlags & NetworkTablesFlags_TreeView); + auto pShowConnections = storage.GetBoolRef( + "connections", m_defaultFlags & NetworkTablesFlags_ShowConnections); + auto pShowFlags = storage.GetBoolRef( + "flags", m_defaultFlags & NetworkTablesFlags_ShowFlags); + auto pShowTimestamp = storage.GetBoolRef( + "timestamp", m_defaultFlags & NetworkTablesFlags_ShowTimestamp); + + ImGui::MenuItem("Tree View", "", pTreeView); + ImGui::MenuItem("Show Connections", "", pShowConnections); + ImGui::MenuItem("Show Flags", "", pShowFlags); + ImGui::MenuItem("Show Timestamp", "", pShowTimestamp); + + m_flags &= + ~(NetworkTablesFlags_TreeView | NetworkTablesFlags_ShowConnections | + NetworkTablesFlags_ShowFlags | NetworkTablesFlags_ShowTimestamp); + m_flags |= (*pTreeView ? NetworkTablesFlags_TreeView : 0) | + (*pShowConnections ? NetworkTablesFlags_ShowConnections : 0) | + (*pShowFlags ? NetworkTablesFlags_ShowFlags : 0) | + (*pShowTimestamp ? NetworkTablesFlags_ShowTimestamp : 0); + ImGui::EndPopup(); + } + + DisplayNetworkTables(m_model, m_flags); +} diff --git a/glass/src/libnt/native/cpp/NetworkTablesHelper.cpp b/glass/src/libnt/native/cpp/NetworkTablesHelper.cpp new file mode 100644 index 0000000000..78fc521ef9 --- /dev/null +++ b/glass/src/libnt/native/cpp/NetworkTablesHelper.cpp @@ -0,0 +1,22 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/networktables/NetworkTablesHelper.h" + +using namespace glass; + +NetworkTablesHelper::NetworkTablesHelper(NT_Inst inst) + : m_inst{inst}, m_poller{nt::CreateEntryListenerPoller(inst)} {} + +NetworkTablesHelper::~NetworkTablesHelper() { + nt::DestroyEntryListenerPoller(m_poller); +} + +bool NetworkTablesHelper::IsConnected() const { + return nt::GetNetworkMode(m_inst) == NT_NET_MODE_SERVER || + nt::IsConnected(m_inst); +} diff --git a/glass/src/libnt/native/cpp/NetworkTablesProvider.cpp b/glass/src/libnt/native/cpp/NetworkTablesProvider.cpp new file mode 100644 index 0000000000..cc8430fcf6 --- /dev/null +++ b/glass/src/libnt/native/cpp/NetworkTablesProvider.cpp @@ -0,0 +1,177 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/networktables/NetworkTablesProvider.h" + +#include + +#include +#include +#include + +using namespace glass; + +NetworkTablesProvider::NetworkTablesProvider(const wpi::Twine& iniName) + : NetworkTablesProvider{iniName, nt::GetDefaultInstance()} {} + +NetworkTablesProvider::NetworkTablesProvider(const wpi::Twine& iniName, + NT_Inst inst) + : Provider{iniName + "Window"}, m_nt{inst}, m_typeCache{iniName} { + m_nt.AddListener("", NT_NOTIFY_LOCAL | NT_NOTIFY_NEW | NT_NOTIFY_DELETE | + NT_NOTIFY_IMMEDIATE); +} + +void NetworkTablesProvider::GlobalInit() { + Provider::GlobalInit(); + wpi::gui::AddInit([this] { m_typeCache.Initialize(); }); +} + +void NetworkTablesProvider::DisplayMenu() { + wpi::SmallVector path; + wpi::SmallString<64> name; + for (auto&& entry : m_viewEntries) { + path.clear(); + wpi::StringRef{entry->name}.split(path, '/', -1, false); + + bool fullDepth = true; + int depth = 0; + for (; depth < (static_cast(path.size()) - 1); ++depth) { + name = path[depth]; + if (!ImGui::BeginMenu(name.c_str())) { + fullDepth = false; + break; + } + } + + if (fullDepth) { + bool visible = entry->window && entry->window->IsVisible(); + bool wasVisible = visible; + // FIXME: enabled? + // data is the last item, so is guaranteed to be null-terminated + ImGui::MenuItem(path.back().data(), nullptr, &visible, true); + if (!wasVisible && visible) { + Show(entry.get(), entry->window); + } else if (wasVisible && !visible && entry->window) { + entry->window->SetVisible(false); + } + } + + for (; depth > 0; --depth) ImGui::EndMenu(); + } +} + +void NetworkTablesProvider::Update() { + Provider::Update(); + + // add/remove entries from NT changes + for (auto&& event : m_nt.PollListener()) { + // look for .type fields + wpi::StringRef eventName{event.name}; + if (!eventName.endswith("/.type") || !event.value || + !event.value->IsString()) + continue; + auto tableName = eventName.drop_back(6); + + // only handle ones where we have a builder + auto builderIt = m_typeMap.find(event.value->GetString()); + if (builderIt == m_typeMap.end()) continue; + + if (event.flags & NT_NOTIFY_DELETE) { + auto it = std::find_if( + m_viewEntries.begin(), m_viewEntries.end(), [&](const auto& elem) { + return static_cast(elem->modelEntry)->typeEntry == + event.entry; + }); + if (it != m_viewEntries.end()) { + m_viewEntries.erase(it); + } + } else if (event.flags & NT_NOTIFY_NEW) { + GetOrCreateView(builderIt->second, event.entry, tableName); + // cache the type + m_typeCache[tableName].SetName(event.value->GetString()); + } + } + + // check for visible windows that need displays (typically this is due to + // file loading) + for (auto&& window : m_windows) { + if (!window->IsVisible() || window->HasView()) continue; + auto id = window->GetId(); + auto typeIt = m_typeCache.find(id); + if (typeIt == m_typeCache.end()) continue; + + // only handle ones where we have a builder + auto builderIt = m_typeMap.find(typeIt->second.GetName()); + if (builderIt == m_typeMap.end()) continue; + + auto entry = GetOrCreateView( + builderIt->second, nt::GetEntry(m_nt.GetInstance(), id + "/.type"), id); + if (entry) Show(entry, window.get()); + } +} + +void NetworkTablesProvider::Register(wpi::StringRef typeName, + CreateModelFunc createModel, + CreateViewFunc createView) { + m_typeMap[typeName] = Builder{std::move(createModel), std::move(createView)}; +} + +void NetworkTablesProvider::Show(ViewEntry* entry, Window* window) { + // if there's already a window, just show it + if (entry->window) { + entry->window->SetVisible(true); + return; + } + + // get or create model + if (!entry->modelEntry->model) + entry->modelEntry->model = + entry->modelEntry->createModel(m_nt.GetInstance(), entry->name.c_str()); + if (!entry->modelEntry->model) return; + + // the window might exist and we're just not associated to it yet + if (!window) window = GetOrAddWindow(entry->name, true); + if (!window) return; + entry->window = window; + + // create view + auto view = entry->createView(window, entry->modelEntry->model.get(), + entry->name.c_str()); + if (!view) return; + window->SetView(std::move(view)); + + entry->window->SetVisible(true); +} + +NetworkTablesProvider::ViewEntry* NetworkTablesProvider::GetOrCreateView( + const Builder& builder, NT_Entry typeEntry, wpi::StringRef name) { + // get view entry if it already exists + auto viewIt = FindViewEntry(name); + if (viewIt != m_viewEntries.end() && (*viewIt)->name == name) { + // make sure typeEntry is set in model + static_cast((*viewIt)->modelEntry)->typeEntry = typeEntry; + return viewIt->get(); + } + + // get or create model entry + auto modelIt = FindModelEntry(name); + if (modelIt != m_modelEntries.end() && (*modelIt)->name == name) { + static_cast(modelIt->get())->typeEntry = typeEntry; + } else { + modelIt = m_modelEntries.emplace( + modelIt, std::make_unique(typeEntry, name, builder)); + } + + // create new view entry + viewIt = m_viewEntries.emplace( + viewIt, + std::make_unique( + name, modelIt->get(), [](Model*, const char*) { return true; }, + builder.createView)); + + return viewIt->get(); +} diff --git a/glass/src/libnt/native/cpp/StandardNetworkTables.cpp b/glass/src/libnt/native/cpp/StandardNetworkTables.cpp new file mode 100644 index 0000000000..55f937d759 --- /dev/null +++ b/glass/src/libnt/native/cpp/StandardNetworkTables.cpp @@ -0,0 +1,73 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "glass/networktables/NTDigitalInput.h" +#include "glass/networktables/NTDigitalOutput.h" +#include "glass/networktables/NTFMS.h" +#include "glass/networktables/NTField2D.h" +#include "glass/networktables/NTStringChooser.h" +#include "glass/networktables/NetworkTablesProvider.h" + +using namespace glass; + +void glass::AddStandardNetworkTablesViews(NetworkTablesProvider& provider) { + provider.Register( + NTFMSModel::kType, + [](NT_Inst inst, const char* path) { + return std::make_unique(inst, path); + }, + [](Window* win, Model* model, const char*) { + win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize); + return MakeFunctionView( + [=] { DisplayFMS(static_cast(model)); }); + }); + provider.Register( + NTDigitalInputModel::kType, + [](NT_Inst inst, const char* path) { + return std::make_unique(inst, path); + }, + [](Window* win, Model* model, const char*) { + win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize); + return MakeFunctionView([=] { + DisplayDIO(static_cast(model), 0, true); + }); + }); + provider.Register( + NTDigitalOutputModel::kType, + [](NT_Inst inst, const char* path) { + return std::make_unique(inst, path); + }, + [](Window* win, Model* model, const char*) { + win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize); + return MakeFunctionView([=] { + DisplayDIO(static_cast(model), 0, true); + }); + }); + provider.Register( + NTField2DModel::kType, + [](NT_Inst inst, const char* path) { + return std::make_unique(inst, path); + }, + [=](Window* win, Model* model, const char* path) { + win->SetDefaultPos(200, 200); + win->SetDefaultSize(400, 200); + win->SetPadding(0, 0); + return std::make_unique( + static_cast(model)); + }); + provider.Register( + NTStringChooserModel::kType, + [](NT_Inst inst, const char* path) { + return std::make_unique(inst, path); + }, + [](Window* win, Model* model, const char*) { + win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize); + return MakeFunctionView([=] { + DisplayStringChooser(static_cast(model)); + }); + }); +} diff --git a/glass/src/libnt/native/include/glass/networktables/NTDigitalInput.h b/glass/src/libnt/native/include/glass/networktables/NTDigitalInput.h new file mode 100644 index 0000000000..6a40c3eebc --- /dev/null +++ b/glass/src/libnt/native/include/glass/networktables/NTDigitalInput.h @@ -0,0 +1,56 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +#include +#include + +#include "glass/DataSource.h" +#include "glass/hardware/DIO.h" +#include "glass/networktables/NetworkTablesHelper.h" + +namespace glass { + +class NTDigitalInputModel : public DIOModel { + public: + static constexpr const char* kType = "Digital Input"; + + // path is to the table containing ".type", excluding the trailing / + explicit NTDigitalInputModel(wpi::StringRef path); + NTDigitalInputModel(NT_Inst inst, wpi::StringRef path); + + const char* GetName() const override { return m_nameValue.c_str(); } + + const char* GetSimDevice() const override { return nullptr; } + + DPWMModel* GetDPWM() override { return nullptr; } + DutyCycleModel* GetDutyCycle() override { return nullptr; } + EncoderModel* GetEncoder() override { return nullptr; } + + bool IsInput() const override { return true; } + + DataSource* GetValueData() override { return &m_valueData; } + + void SetValue(bool val) override {} + + void Update() override; + bool Exists() override; + bool IsReadOnly() override { return true; } + + private: + NetworkTablesHelper m_nt; + NT_Entry m_value; + NT_Entry m_name; + + DataSource m_valueData; + std::string m_nameValue; +}; + +} // namespace glass diff --git a/glass/src/libnt/native/include/glass/networktables/NTDigitalOutput.h b/glass/src/libnt/native/include/glass/networktables/NTDigitalOutput.h new file mode 100644 index 0000000000..b2a89e2985 --- /dev/null +++ b/glass/src/libnt/native/include/glass/networktables/NTDigitalOutput.h @@ -0,0 +1,58 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +#include +#include + +#include "glass/DataSource.h" +#include "glass/hardware/DIO.h" +#include "glass/networktables/NetworkTablesHelper.h" + +namespace glass { + +class NTDigitalOutputModel : public DIOModel { + public: + static constexpr const char* kType = "Digital Output"; + + // path is to the table containing ".type", excluding the trailing / + explicit NTDigitalOutputModel(wpi::StringRef path); + NTDigitalOutputModel(NT_Inst inst, wpi::StringRef path); + + const char* GetName() const override { return m_nameValue.c_str(); } + + const char* GetSimDevice() const override { return nullptr; } + + DPWMModel* GetDPWM() override { return nullptr; } + DutyCycleModel* GetDutyCycle() override { return nullptr; } + EncoderModel* GetEncoder() override { return nullptr; } + + bool IsInput() const override { return true; } + + DataSource* GetValueData() override { return &m_valueData; } + + void SetValue(bool val) override; + + void Update() override; + bool Exists() override; + bool IsReadOnly() override { return !m_controllableValue; } + + private: + NetworkTablesHelper m_nt; + NT_Entry m_value; + NT_Entry m_name; + NT_Entry m_controllable; + + DataSource m_valueData; + std::string m_nameValue; + bool m_controllableValue = false; +}; + +} // namespace glass diff --git a/glass/src/libnt/native/include/glass/networktables/NTFMS.h b/glass/src/libnt/native/include/glass/networktables/NTFMS.h new file mode 100644 index 0000000000..fc4c479832 --- /dev/null +++ b/glass/src/libnt/native/include/glass/networktables/NTFMS.h @@ -0,0 +1,72 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include + +#include "glass/DataSource.h" +#include "glass/networktables/NetworkTablesHelper.h" +#include "glass/other/FMS.h" + +namespace glass { + +class NTFMSModel : public FMSModel { + public: + static constexpr const char* kType = "FMSInfo"; + + // path is to the table containing ".type", excluding the trailing / + explicit NTFMSModel(wpi::StringRef path); + NTFMSModel(NT_Inst inst, wpi::StringRef path); + + DataSource* GetFmsAttachedData() override { return &m_fmsAttached; } + DataSource* GetDsAttachedData() override { return &m_dsAttached; } + DataSource* GetAllianceStationIdData() override { + return &m_allianceStationId; + } + // NT does not provide match time + DataSource* GetMatchTimeData() override { return nullptr; } + DataSource* GetEStopData() override { return &m_estop; } + DataSource* GetEnabledData() override { return &m_enabled; } + DataSource* GetTestData() override { return &m_test; } + DataSource* GetAutonomousData() override { return &m_autonomous; } + wpi::StringRef GetGameSpecificMessage( + wpi::SmallVectorImpl& buf) override; + + // NT is read-only (it's continually set by robot code) + void SetFmsAttached(bool val) override {} + void SetDsAttached(bool val) override {} + void SetAllianceStationId(int val) override {} + void SetMatchTime(double val) override {} + void SetEStop(bool val) override {} + void SetEnabled(bool val) override {} + void SetTest(bool val) override {} + void SetAutonomous(bool val) override {} + void SetGameSpecificMessage(const char* val) override {} + + void Update() override; + bool Exists() override; + bool IsReadOnly() override { return true; } + + private: + NetworkTablesHelper m_nt; + NT_Entry m_gameSpecificMessage; + NT_Entry m_alliance; + NT_Entry m_station; + NT_Entry m_controlWord; + + DataSource m_fmsAttached; + DataSource m_dsAttached; + DataSource m_allianceStationId; + DataSource m_estop; + DataSource m_enabled; + DataSource m_test; + DataSource m_autonomous; +}; + +} // namespace glass diff --git a/glass/src/libnt/native/include/glass/networktables/NTField2D.h b/glass/src/libnt/native/include/glass/networktables/NTField2D.h new file mode 100644 index 0000000000..92d5c07ac2 --- /dev/null +++ b/glass/src/libnt/native/include/glass/networktables/NTField2D.h @@ -0,0 +1,52 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include +#include + +#include +#include + +#include "glass/networktables/NetworkTablesHelper.h" +#include "glass/other/Field2D.h" + +namespace glass { + +class NTField2DModel : public Field2DModel { + public: + static constexpr const char* kType = "Field2d"; + + // path is to the table containing ".type", excluding the trailing / + explicit NTField2DModel(wpi::StringRef path); + NTField2DModel(NT_Inst inst, wpi::StringRef path); + ~NTField2DModel() override; + + const char* GetPath() const { return m_path.c_str(); } + const char* GetName() const { return m_nameValue.c_str(); } + + void Update() override; + bool Exists() override; + bool IsReadOnly() override; + + void ForEachFieldObjectGroup( + wpi::function_ref + func) override; + + private: + NetworkTablesHelper m_nt; + std::string m_path; + NT_Entry m_name; + std::string m_nameValue; + + class GroupModel; + std::vector> m_groups; +}; + +} // namespace glass diff --git a/glass/src/libnt/native/include/glass/networktables/NTStringChooser.h b/glass/src/libnt/native/include/glass/networktables/NTStringChooser.h new file mode 100644 index 0000000000..d2e1f86f12 --- /dev/null +++ b/glass/src/libnt/native/include/glass/networktables/NTStringChooser.h @@ -0,0 +1,57 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include + +#include + +#include "glass/networktables/NetworkTablesHelper.h" +#include "glass/other/StringChooser.h" + +namespace glass { + +class NTStringChooserModel : public StringChooserModel { + public: + static constexpr const char* kType = "String Chooser"; + + // path is to the table containing ".type", excluding the trailing / + explicit NTStringChooserModel(wpi::StringRef path); + NTStringChooserModel(NT_Inst inst, wpi::StringRef path); + + const std::string& GetDefault() override { return m_defaultValue; } + const std::string& GetSelected() override { return m_selectedValue; } + const std::string& GetActive() override { return m_activeValue; } + const std::vector& GetOptions() override { + return m_optionsValue; + } + + void SetDefault(wpi::StringRef val) override; + void SetSelected(wpi::StringRef val) override; + void SetActive(wpi::StringRef val) override; + void SetOptions(wpi::ArrayRef val) override; + + void Update() override; + bool Exists() override; + bool IsReadOnly() override { return false; } + + private: + NetworkTablesHelper m_nt; + NT_Entry m_default; + NT_Entry m_selected; + NT_Entry m_active; + NT_Entry m_options; + + std::string m_defaultValue; + std::string m_selectedValue; + std::string m_activeValue; + std::vector m_optionsValue; +}; + +} // namespace glass diff --git a/glass/src/libnt/native/include/glass/networktables/NetworkTables.h b/glass/src/libnt/native/include/glass/networktables/NetworkTables.h new file mode 100644 index 0000000000..6668ff0fd2 --- /dev/null +++ b/glass/src/libnt/native/include/glass/networktables/NetworkTables.h @@ -0,0 +1,123 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +#include "glass/Model.h" +#include "glass/View.h" + +namespace glass { + +class DataSource; + +class NetworkTablesModel : public Model { + public: + struct Entry { + explicit Entry(nt::EntryNotification&& event); + + void UpdateValue(); + + /** Entry handle. */ + NT_Entry entry; + + /** Entry name. */ + std::string name; + + /** The value. */ + std::shared_ptr value; + + /** Flags. */ + unsigned int flags = 0; + + /** String representation of the value (for arrays / complex values). */ + std::string valueStr; + + /** Data source (for numeric values). */ + std::unique_ptr source; + }; + + struct TreeNode { + explicit TreeNode(wpi::StringRef name) : name{name} {} + + /** Short name (e.g. of just this node) */ + std::string name; + + /** + * Full path if entry is null (otherwise use entry->name), + * excluding trailing / + */ + std::string path; + + /** Null if no value at this node */ + Entry* entry = nullptr; + + /** Children of node, sorted by name */ + std::vector children; + }; + + NetworkTablesModel(); + explicit NetworkTablesModel(NT_Inst inst); + ~NetworkTablesModel() override; + + void Update() override; + bool Exists() override; + + NT_Inst GetInstance() { return m_inst; } + const std::vector& GetEntries() { return m_sortedEntries; } + const std::vector& GetTreeRoot() { return m_root; } + + private: + NT_Inst m_inst; + NT_EntryListenerPoller m_poller; + wpi::DenseMap> m_entries; + + // sorted by name + std::vector m_sortedEntries; + + std::vector m_root; +}; + +using NetworkTablesFlags = int; + +enum NetworkTablesFlags_ { + NetworkTablesFlags_TreeView = 1 << 0, + NetworkTablesFlags_ReadOnly = 1 << 1, + NetworkTablesFlags_ShowConnections = 1 << 2, + NetworkTablesFlags_ShowFlags = 1 << 3, + NetworkTablesFlags_ShowTimestamp = 1 << 4, + NetworkTablesFlags_Default = 0x1D // all except readonly +}; + +void DisplayNetworkTables( + NetworkTablesModel* model, + NetworkTablesFlags flags = NetworkTablesFlags_Default); + +class NetworkTablesView : public View { + public: + explicit NetworkTablesView( + NetworkTablesModel* model, + NetworkTablesFlags defaultFlags = NetworkTablesFlags_Default) + : m_model{model}, m_defaultFlags{defaultFlags}, m_flags{defaultFlags} {} + + void Display() override; + + private: + NetworkTablesModel* m_model; + NetworkTablesFlags m_defaultFlags; + NetworkTablesFlags m_flags; +}; + +} // namespace glass diff --git a/glass/src/libnt/native/include/glass/networktables/NetworkTablesHelper.h b/glass/src/libnt/native/include/glass/networktables/NetworkTablesHelper.h new file mode 100644 index 0000000000..fc127a33cb --- /dev/null +++ b/glass/src/libnt/native/include/glass/networktables/NetworkTablesHelper.h @@ -0,0 +1,58 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include + +#include +#include +#include + +namespace glass { + +class NetworkTablesHelper { + public: + explicit NetworkTablesHelper(NT_Inst inst); + ~NetworkTablesHelper(); + + NetworkTablesHelper(const NetworkTablesHelper&) = delete; + NetworkTablesHelper& operator=(const NetworkTablesHelper&) = delete; + + NT_Inst GetInstance() const { return m_inst; } + NT_EntryListenerPoller GetPoller() const { return m_poller; } + + NT_Entry GetEntry(const wpi::Twine& name) const { + return nt::GetEntry(m_inst, name); + } + + static constexpr int kDefaultListenerFlags = + NT_NOTIFY_LOCAL | NT_NOTIFY_NEW | NT_NOTIFY_UPDATE | NT_NOTIFY_IMMEDIATE; + + NT_EntryListener AddListener(NT_Entry entry, + unsigned int flags = kDefaultListenerFlags) { + return nt::AddPolledEntryListener(m_poller, entry, flags); + } + + NT_EntryListener AddListener(const wpi::Twine& prefix, + unsigned int flags = kDefaultListenerFlags) { + return nt::AddPolledEntryListener(m_poller, prefix, flags); + } + + std::vector PollListener() { + bool timedOut = false; + return nt::PollEntryListener(m_poller, 0, &timedOut); + } + + bool IsConnected() const; + + private: + NT_Inst m_inst; + NT_EntryListenerPoller m_poller; +}; + +} // namespace glass diff --git a/glass/src/libnt/native/include/glass/networktables/NetworkTablesProvider.h b/glass/src/libnt/native/include/glass/networktables/NetworkTablesProvider.h new file mode 100644 index 0000000000..95900cc114 --- /dev/null +++ b/glass/src/libnt/native/include/glass/networktables/NetworkTablesProvider.h @@ -0,0 +1,118 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +#include "glass/Model.h" +#include "glass/Provider.h" +#include "glass/networktables/NetworkTablesHelper.h" +#include "glass/support/IniSaverInfo.h" +#include "glass/support/IniSaverString.h" + +namespace glass { + +class Window; + +namespace detail { +struct NTProviderFunctions { + using Exists = std::function; + using CreateModel = + std::function(NT_Inst inst, const char* path)>; + using ViewExists = std::function; + using CreateView = + std::function(Window*, Model*, const char* path)>; +}; +} // namespace detail + +/** + * A provider for NetworkTables (SmartDashboard style) models and views. + */ +class NetworkTablesProvider : private Provider { + public: + using Provider::CreateModelFunc; + using Provider::CreateViewFunc; + + explicit NetworkTablesProvider(const wpi::Twine& iniName); + NetworkTablesProvider(const wpi::Twine& iniName, NT_Inst inst); + + /** + * Get the NetworkTables instance being used for this provider. + * + * @return NetworkTables instance + */ + NT_Inst GetInstance() const { return m_nt.GetInstance(); } + + /** + * Perform global initialization. This should be called prior to + * wpi::gui::Initialize(). + */ + void GlobalInit() override; + + /** + * Displays menu contents as a tree of available NetworkTables views. + */ + void DisplayMenu() override; + + /** + * Registers a NetworkTables model and view. + * + * @param typeName SmartDashboard .type value to match + * @param createModel functor to create model + * @param createView functor to create view + */ + void Register(wpi::StringRef typeName, CreateModelFunc createModel, + CreateViewFunc createView); + + using WindowManager::AddWindow; + + private: + void Update() override; + + NetworkTablesHelper m_nt; + + // cached mapping from table name to type string + IniSaverString m_typeCache; + + struct Builder { + CreateModelFunc createModel; + CreateViewFunc createView; + }; + + // mapping from .type string to model/view creators + wpi::StringMap m_typeMap; + + struct Entry : public ModelEntry { + Entry(NT_Entry typeEntry, wpi::StringRef name, const Builder& builder) + : ModelEntry{name, [](NT_Inst, const char*) { return true; }, + builder.createModel}, + typeEntry{typeEntry} {} + NT_Entry typeEntry; + }; + + void Show(ViewEntry* entry, Window* window) override; + + ViewEntry* GetOrCreateView(const Builder& builder, NT_Entry typeEntry, + wpi::StringRef name); +}; + +/** + * Add "standard" set of NetworkTables models/views. + * + * @param provider NetworkTables provider + */ +void AddStandardNetworkTablesViews(NetworkTablesProvider& provider); + +} // namespace glass diff --git a/settings.gradle b/settings.gradle index 2d59b6efe7..0a00c2cc85 100644 --- a/settings.gradle +++ b/settings.gradle @@ -28,6 +28,7 @@ include 'wpilibcIntegrationTests' include 'wpilibjExamples' include 'wpilibjIntegrationTests' include 'wpilibj' +include 'glass' include 'simulation:gz_msgs' include 'simulation:frc_gazebo_plugins' include 'simulation:halsim_gazebo' diff --git a/simulation/halsim_gui/CMakeLists.txt b/simulation/halsim_gui/CMakeLists.txt index bd45b62789..949f9f1e81 100644 --- a/simulation/halsim_gui/CMakeLists.txt +++ b/simulation/halsim_gui/CMakeLists.txt @@ -10,7 +10,7 @@ wpilib_target_warnings(halsim_gui) set_target_properties(halsim_gui PROPERTIES DEBUG_POSTFIX "d") wpilib_link_macos_gui(halsim_gui) -target_link_libraries(halsim_gui PUBLIC hal ntcore wpimath PRIVATE wpigui) +target_link_libraries(halsim_gui PUBLIC hal wpimath PRIVATE libglassnt libglass) target_include_directories(halsim_gui PRIVATE src/main/native/include) diff --git a/simulation/halsim_gui/build.gradle b/simulation/halsim_gui/build.gradle index 0fd5c2c76b..6486a7d9e4 100644 --- a/simulation/halsim_gui/build.gradle +++ b/simulation/halsim_gui/build.gradle @@ -24,6 +24,8 @@ if (!project.hasProperty('onlylinuxathena') && !project.hasProperty('onlylinuxra binaries { all { lib project: ':wpimath', library: 'wpimath', linkage: 'shared' + lib project: ':glass', library: 'glassnt', linkage: 'static' + lib project: ':glass', library: 'glass', linkage: 'static' lib project: ':wpigui', library: 'wpigui', linkage: 'static' nativeUtils.useRequiredLibrary(it, 'imgui_static') if (it.targetPlatform.name == nativeUtils.wpi.platforms.roborio || it.targetPlatform.name == nativeUtils.wpi.platforms.raspbian || it.targetPlatform.name == nativeUtils.wpi.platforms.aarch64bionic) { diff --git a/simulation/halsim_gui/src/main/native/cpp/AccelerometerGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AccelerometerGui.cpp deleted file mode 100644 index fa772ad912..0000000000 --- a/simulation/halsim_gui/src/main/native/cpp/AccelerometerGui.cpp +++ /dev/null @@ -1,75 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#include "AccelerometerGui.h" - -#include -#include - -#include -#include -#include - -#include "GuiDataSource.h" -#include "HALSimGui.h" -#include "SimDeviceGui.h" - -using namespace halsimgui; - -namespace { -HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AccelerometerX, "X Accel"); -HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AccelerometerY, "Y Accel"); -HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AccelerometerZ, "Z Accel"); -} // namespace - -static std::unique_ptr gAccelXSource; -static std::unique_ptr gAccelYSource; -static std::unique_ptr gAccelZSource; - -static void UpdateAccelSources() { - if (!HALSIM_GetAccelerometerActive(0)) return; - if (!gAccelXSource) gAccelXSource = std::make_unique(0); - if (!gAccelYSource) gAccelYSource = std::make_unique(0); - if (!gAccelZSource) gAccelZSource = std::make_unique(0); -} - -static void DisplayAccelerometers() { - if (!HALSIM_GetAccelerometerActive(0)) return; - if (SimDeviceGui::StartDevice("BuiltInAccel")) { - HAL_Value value; - - // Range - value = HAL_MakeEnum(HALSIM_GetAccelerometerRange(0)); - static const char* rangeOptions[] = {"2G", "4G", "8G"}; - SimDeviceGui::DisplayValue("Range", true, &value, rangeOptions, 3); - - // X Accel - value = HAL_MakeDouble(gAccelXSource->GetValue()); - if (SimDeviceGui::DisplayValueSource("X Accel", false, &value, - gAccelXSource.get())) - HALSIM_SetAccelerometerX(0, value.data.v_double); - - // Y Accel - value = HAL_MakeDouble(gAccelYSource->GetValue()); - if (SimDeviceGui::DisplayValueSource("Y Accel", false, &value, - gAccelYSource.get())) - HALSIM_SetAccelerometerY(0, value.data.v_double); - - // Z Accel - value = HAL_MakeDouble(gAccelZSource->GetValue()); - if (SimDeviceGui::DisplayValueSource("Z Accel", false, &value, - gAccelZSource.get())) - HALSIM_SetAccelerometerZ(0, value.data.v_double); - - SimDeviceGui::FinishDevice(); - } -} - -void AccelerometerGui::Initialize() { - HALSimGui::AddExecute(UpdateAccelSources); - SimDeviceGui::Add(DisplayAccelerometers); -} diff --git a/simulation/halsim_gui/src/main/native/cpp/AccelerometerSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AccelerometerSimGui.cpp new file mode 100644 index 0000000000..488285f199 --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/AccelerometerSimGui.cpp @@ -0,0 +1,67 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "AccelerometerSimGui.h" + +#include +#include + +#include + +#include +#include + +#include "HALDataSource.h" +#include "HALSimGui.h" +#include "SimDeviceGui.h" + +using namespace glass; +using namespace halsimgui; + +namespace { +HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AccelerometerX, "X Accel"); +HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AccelerometerY, "Y Accel"); +HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AccelerometerZ, "Z Accel"); + +class AccelerometerSimModel : public glass::AccelerometerModel { + public: + explicit AccelerometerSimModel(int32_t index) + : m_index{index}, m_xData{m_index}, m_yData{m_index}, m_zData{m_index} {} + + void Update() override {} + + bool Exists() override { return HALSIM_GetAccelerometerActive(m_index); } + + glass::DataSource* GetXData() override { return &m_xData; } + glass::DataSource* GetYData() override { return &m_yData; } + glass::DataSource* GetZData() override { return &m_zData; } + + int GetRange() override { return HALSIM_GetAccelerometerRange(m_index); } + + void SetX(double val) override { HALSIM_SetAccelerometerX(m_index, val); } + void SetY(double val) override { HALSIM_SetAccelerometerY(m_index, val); } + void SetZ(double val) override { HALSIM_SetAccelerometerZ(m_index, val); } + void SetRange(int val) override { + HALSIM_SetAccelerometerRange(m_index, + static_cast(val)); + } + + private: + int32_t m_index; + AccelerometerXSource m_xData; + AccelerometerYSource m_yData; + AccelerometerZSource m_zData; +}; +} // namespace + +void AccelerometerSimGui::Initialize() { + SimDeviceGui::GetDeviceTree().Add( + std::make_unique(0), [](glass::Model* model) { + glass::DisplayAccelerometerDevice( + static_cast(model)); + }); +} diff --git a/simulation/halsim_gui/src/main/native/cpp/AccelerometerSimGui.h b/simulation/halsim_gui/src/main/native/cpp/AccelerometerSimGui.h new file mode 100644 index 0000000000..b76c57e254 --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/AccelerometerSimGui.h @@ -0,0 +1,17 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +namespace halsimgui { + +class AccelerometerSimGui { + public: + static void Initialize(); +}; + +} // namespace halsimgui diff --git a/simulation/halsim_gui/src/main/native/cpp/AddressableLEDGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AddressableLEDGui.cpp index 36b085c23e..9990be4cbe 100644 --- a/simulation/halsim_gui/src/main/native/cpp/AddressableLEDGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/AddressableLEDGui.cpp @@ -7,120 +7,103 @@ #include "AddressableLEDGui.h" +#include + #include #include #include -#include -#include #include -#include "ExtraGuiWidgets.h" #include "HALSimGui.h" -#include "IniSaver.h" -#include "IniSaverInfo.h" using namespace halsimgui; namespace { -struct LEDDisplayInfo { - int numColumns = 10; - LEDConfig config; +class AddressableLEDModel : public glass::LEDDisplayModel { + public: + explicit AddressableLEDModel(int32_t index) : m_index{index} {} - bool ReadIni(wpi::StringRef name, wpi::StringRef value); - void WriteIni(ImGuiTextBuffer* out); + void Update() override {} + bool Exists() override { + return HALSIM_GetAddressableLEDInitialized(m_index); + } + + bool IsRunning() override { return HALSIM_GetAddressableLEDRunning(m_index); } + + wpi::ArrayRef GetData(wpi::SmallVectorImpl&) override { + size_t length = HALSIM_GetAddressableLEDData(m_index, m_data); + return {reinterpret_cast(m_data), length}; + } + + private: + int32_t m_index; + + HAL_AddressableLEDData m_data[HAL_kAddressableLEDMaxLength]; +}; + +class AddressableLEDsModel : public glass::LEDDisplaysModel { + public: + AddressableLEDsModel() : m_models(HAL_GetNumAddressableLEDs()) {} + + void Update() override; + bool Exists() override; + + size_t GetNumLEDDisplays() override { return m_models.size(); } + + void ForEachLEDDisplay( + wpi::function_ref func) + override; + + private: + std::vector> m_models; }; } // namespace -static IniSaver gDisplaySettings{"AddressableLED"}; - -bool LEDDisplayInfo::ReadIni(wpi::StringRef name, wpi::StringRef value) { - if (name == "columns") { - int num; - if (value.getAsInteger(10, num)) return true; - numColumns = num; - } else if (name == "serpentine") { - int num; - if (value.getAsInteger(10, num)) return true; - config.serpentine = num != 0; - } else if (name == "order") { - int num; - if (value.getAsInteger(10, num)) return true; - config.order = static_cast(num); - } else if (name == "start") { - int num; - if (value.getAsInteger(10, num)) return true; - config.start = static_cast(num); - } else { - return false; - } - return true; -} - -void LEDDisplayInfo::WriteIni(ImGuiTextBuffer* out) { - out->appendf("columns=%d\nserpentine=%d\norder=%d\nstart=%d\n", numColumns, - config.serpentine ? 1 : 0, static_cast(config.order), - static_cast(config.start)); -} - -static void DisplayAddressableLEDs() { - bool hasAny = false; - static const int numLED = HAL_GetNumAddressableLEDs(); - - for (int i = 0; i < numLED; ++i) { - if (!HALSIM_GetAddressableLEDInitialized(i)) continue; - hasAny = true; - - if (numLED > 1) ImGui::Text("LEDs[%d]", i); - - static HAL_AddressableLEDData data[HAL_kAddressableLEDMaxLength]; - int length = HALSIM_GetAddressableLEDData(i, data); - bool running = HALSIM_GetAddressableLEDRunning(i); - auto& info = gDisplaySettings[i]; - - ImGui::PushItemWidth(ImGui::GetFontSize() * 6); - ImGui::LabelText("Length", "%d", length); - ImGui::LabelText("Running", "%s", running ? "Yes" : "No"); - ImGui::InputInt("Columns", &info.numColumns); - { - static const char* options[] = {"Row Major", "Column Major"}; - int val = info.config.order; - if (ImGui::Combo("Order", &val, options, 2)) - info.config.order = static_cast(val); - } - { - static const char* options[] = {"Upper Left", "Lower Left", "Upper Right", - "Lower Right"}; - int val = info.config.start; - if (ImGui::Combo("Start", &val, options, 4)) - info.config.start = static_cast(val); - } - ImGui::Checkbox("Serpentine", &info.config.serpentine); - if (info.numColumns < 1) info.numColumns = 1; - ImGui::PopItemWidth(); - - // show as LED indicators - static int values[HAL_kAddressableLEDMaxLength]; - static ImU32 colors[HAL_kAddressableLEDMaxLength]; - - if (!running) { - colors[0] = IM_COL32(128, 128, 128, 255); - for (int j = 0; j < length; ++j) values[j] = -1; - } else { - for (int j = 0; j < length; ++j) { - values[j] = j + 1; - colors[j] = IM_COL32(data[j].r, data[j].g, data[j].b, 255); +void AddressableLEDsModel::Update() { + for (int i = 0; i < static_cast(m_models.size()); ++i) { + auto& model = m_models[i]; + if (HALSIM_GetAddressableLEDInitialized(i)) { + if (!model) { + model = std::make_unique(i); } + if (model) model->Update(); + } else { + model.reset(); } - - DrawLEDs(values, length, info.numColumns, colors, 0, 0, info.config); } - if (!hasAny) ImGui::Text("No addressable LEDs"); +} + +bool AddressableLEDsModel::Exists() { + for (auto&& model : m_models) { + if (model && model->Exists()) return true; + } + return false; +} + +void AddressableLEDsModel::ForEachLEDDisplay( + wpi::function_ref func) { + for (int i = 0; i < static_cast(m_models.size()); ++i) { + if (m_models[i]) func(*m_models[i], i); + } +} + +static bool AddressableLEDsExists() { + static const int numLED = HAL_GetNumAddressableLEDs(); + for (int i = 0; i < numLED; ++i) { + if (HALSIM_GetAddressableLEDInitialized(i)) return true; + } + return false; } void AddressableLEDGui::Initialize() { - gDisplaySettings.Initialize(); - HALSimGui::AddWindow("Addressable LEDs", DisplayAddressableLEDs, - ImGuiWindowFlags_AlwaysAutoResize); - HALSimGui::SetWindowVisibility("Addressable LEDs", HALSimGui::kHide); - HALSimGui::SetDefaultWindowPos("Addressable LEDs", 290, 100); + HALSimGui::halProvider.Register( + "Addressable LEDs", [] { return AddressableLEDsExists(); }, + [] { return std::make_unique(); }, + [](glass::Window* win, glass::Model* model) { + win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize); + win->SetDefaultPos(290, 100); + return glass::MakeFunctionView([=] { + glass::DisplayLEDDisplays(static_cast(model)); + }); + }); } diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogGyroGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AnalogGyroGui.cpp deleted file mode 100644 index 5df30e6005..0000000000 --- a/simulation/halsim_gui/src/main/native/cpp/AnalogGyroGui.cpp +++ /dev/null @@ -1,80 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#include "AnalogGyroGui.h" - -#include -#include -#include - -#include -#include -#include -#include - -#include "GuiDataSource.h" -#include "HALSimGui.h" -#include "SimDeviceGui.h" - -using namespace halsimgui; - -namespace { -HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AnalogGyroAngle, "AGyro Angle"); -HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AnalogGyroRate, "AGyro Rate"); -struct AnalogGyroSource { - explicit AnalogGyroSource(int32_t index) : angle{index}, rate{index} {} - AnalogGyroAngleSource angle; - AnalogGyroRateSource rate; -}; -} // namespace - -static std::vector> gAnalogGyroSources; - -static void UpdateAnalogGyroSources() { - for (int i = 0, iend = gAnalogGyroSources.size(); i < iend; ++i) { - auto& source = gAnalogGyroSources[i]; - if (HALSIM_GetAnalogGyroInitialized(i)) { - if (!source) { - source = std::make_unique(i); - } - } else { - source.reset(); - } - } -} - -static void DisplayAnalogGyros() { - for (int i = 0, iend = gAnalogGyroSources.size(); i < iend; ++i) { - if (auto source = gAnalogGyroSources[i].get()) { - char name[32]; - std::snprintf(name, sizeof(name), "AnalogGyro[%d]", i); - if (SimDeviceGui::StartDevice(name)) { - HAL_Value value; - - // angle - value = HAL_MakeDouble(source->angle.GetValue()); - if (SimDeviceGui::DisplayValueSource("Angle", false, &value, - &source->angle)) - HALSIM_SetAnalogGyroAngle(i, value.data.v_double); - - // rate - value = HAL_MakeDouble(source->rate.GetValue()); - if (SimDeviceGui::DisplayValueSource("Rate", false, &value, - &source->rate)) - HALSIM_SetAnalogGyroRate(i, value.data.v_double); - - SimDeviceGui::FinishDevice(); - } - } - } -} - -void AnalogGyroGui::Initialize() { - gAnalogGyroSources.resize(HAL_GetNumAccumulators()); - HALSimGui::AddExecute(UpdateAnalogGyroSources); - SimDeviceGui::Add(DisplayAnalogGyros); -} diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogGyroSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AnalogGyroSimGui.cpp new file mode 100644 index 0000000000..beb516dd65 --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/AnalogGyroSimGui.cpp @@ -0,0 +1,101 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "AnalogGyroSimGui.h" + +#include +#include + +#include +#include + +#include +#include +#include + +#include "HALDataSource.h" +#include "HALSimGui.h" +#include "SimDeviceGui.h" + +using namespace halsimgui; + +namespace { +HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AnalogGyroAngle, "AGyro Angle"); +HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AnalogGyroRate, "AGyro Rate"); + +class AnalogGyroSimModel : public glass::AnalogGyroModel { + public: + explicit AnalogGyroSimModel(int32_t index) + : m_index{index}, m_angle{index}, m_rate{index} {} + + void Update() override {} + + bool Exists() override { return HALSIM_GetAnalogGyroInitialized(m_index); } + + glass::DataSource* GetAngleData() override { return &m_angle; } + glass::DataSource* GetRateData() override { return &m_rate; } + + void SetAngle(double val) override { + HALSIM_SetAnalogGyroAngle(m_index, val); + } + void SetRate(double val) override { HALSIM_SetAnalogGyroRate(m_index, val); } + + private: + int32_t m_index; + AnalogGyroAngleSource m_angle; + AnalogGyroRateSource m_rate; +}; + +class AnalogGyrosSimModel : public glass::AnalogGyrosModel { + public: + AnalogGyrosSimModel() : m_models(HAL_GetNumAccumulators()) {} + + void Update() override; + + bool Exists() override { return true; } + + void ForEachAnalogGyro( + wpi::function_ref func) + override; + + private: + // indexed by channel + std::vector> m_models; +}; +} // namespace + +void AnalogGyrosSimModel::Update() { + for (int32_t i = 0, iend = static_cast(m_models.size()); i < iend; + ++i) { + auto& model = m_models[i]; + if (HALSIM_GetAnalogGyroInitialized(i)) { + if (!model) { + model = std::make_unique(i); + } + } else { + model.reset(); + } + } +} + +void AnalogGyrosSimModel::ForEachAnalogGyro( + wpi::function_ref func) { + for (int32_t i = 0, iend = static_cast(m_models.size()); i < iend; + ++i) { + if (auto model = m_models[i].get()) { + func(*model, i); + } + } +} + +void AnalogGyroSimGui::Initialize() { + SimDeviceGui::GetDeviceTree().Add( + std::make_unique(), [](glass::Model* model) { + glass::DisplayAnalogGyrosDevice( + static_cast(model)); + }); +} diff --git a/simulation/halsim_gui/src/main/native/cpp/AccelerometerGui.h b/simulation/halsim_gui/src/main/native/cpp/AnalogGyroSimGui.h similarity index 85% rename from simulation/halsim_gui/src/main/native/cpp/AccelerometerGui.h rename to simulation/halsim_gui/src/main/native/cpp/AnalogGyroSimGui.h index e1fd81b6c7..33fb612fa4 100644 --- a/simulation/halsim_gui/src/main/native/cpp/AccelerometerGui.h +++ b/simulation/halsim_gui/src/main/native/cpp/AnalogGyroSimGui.h @@ -1,5 +1,5 @@ /*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019 FIRST. All Rights Reserved. */ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ /* Open Source Software - may be modified and shared by FRC teams. The code */ /* must be accompanied by the FIRST BSD license file in the root directory of */ /* the project. */ @@ -9,7 +9,7 @@ namespace halsimgui { -class AccelerometerGui { +class AnalogGyroSimGui { public: static void Initialize(); }; diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogInputGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AnalogInputGui.cpp deleted file mode 100644 index 8ba1459b84..0000000000 --- a/simulation/halsim_gui/src/main/native/cpp/AnalogInputGui.cpp +++ /dev/null @@ -1,102 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#include "AnalogInputGui.h" - -#include -#include - -#include -#include -#include -#include -#include - -#include "GuiDataSource.h" -#include "HALSimGui.h" -#include "IniSaver.h" -#include "IniSaverInfo.h" - -using namespace halsimgui; - -namespace { -HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AnalogInVoltage, "AIn"); -} // namespace - -// indexed by channel -static IniSaver gAnalogInputs{"AnalogInput"}; -static std::vector> gAnalogInputSources; - -static void UpdateAnalogInputSources() { - for (int i = 0, iend = gAnalogInputSources.size(); i < iend; ++i) { - auto& source = gAnalogInputSources[i]; - if (HALSIM_GetAnalogInInitialized(i)) { - if (!source) { - source = std::make_unique(i); - source->SetName(gAnalogInputs[i].GetName()); - } - } else { - source.reset(); - } - } -} - -static void DisplayAnalogInputs() { - ImGui::Text("(Use Ctrl+Click to edit value)"); - bool hasInputs = false; - static const int numAccum = HAL_GetNumAccumulators(); - bool first = true; - for (int i = 0, iend = gAnalogInputSources.size(); i < iend; ++i) { - if (auto source = gAnalogInputSources[i].get()) { - ImGui::PushID(i); - hasInputs = true; - - if (!first) { - ImGui::Spacing(); - ImGui::Spacing(); - } else { - first = false; - } - - auto& info = gAnalogInputs[i]; - // build label - char label[128]; - info.GetLabel(label, sizeof(label), "In", i); - - if (i < numAccum && HALSIM_GetAnalogGyroInitialized(i)) { - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255)); - ImGui::LabelText(label, "AnalogGyro[%d]", i); - ImGui::PopStyleColor(); - } else if (auto simDevice = HALSIM_GetAnalogInSimDevice(i)) { - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255)); - ImGui::LabelText(label, "%s", HALSIM_GetSimDeviceName(simDevice)); - ImGui::PopStyleColor(); - } else { - float val = source->GetValue(); - if (source->SliderFloat(label, &val, 0.0, 5.0)) - HALSIM_SetAnalogInVoltage(i, val); - } - - // context menu to change name - if (info.PopupEditName(i)) { - source->SetName(info.GetName()); - } - ImGui::PopID(); - } - } - if (!hasInputs) ImGui::Text("No analog inputs"); -} - -void AnalogInputGui::Initialize() { - gAnalogInputs.Initialize(); - gAnalogInputSources.resize(HAL_GetNumAnalogInputs()); - - HALSimGui::AddExecute(UpdateAnalogInputSources); - HALSimGui::AddWindow("Analog Inputs", DisplayAnalogInputs, - ImGuiWindowFlags_AlwaysAutoResize); - HALSimGui::SetDefaultWindowPos("Analog Inputs", 640, 20); -} diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogInputGui.h b/simulation/halsim_gui/src/main/native/cpp/AnalogInputGui.h deleted file mode 100644 index 74c3e6389f..0000000000 --- a/simulation/halsim_gui/src/main/native/cpp/AnalogInputGui.h +++ /dev/null @@ -1,17 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#pragma once - -namespace halsimgui { - -class AnalogInputGui { - public: - static void Initialize(); -}; - -} // namespace halsimgui diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogInputSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AnalogInputSimGui.cpp new file mode 100644 index 0000000000..fe02a0fcb7 --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/AnalogInputSimGui.cpp @@ -0,0 +1,123 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "AnalogInputSimGui.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include "HALDataSource.h" +#include "HALSimGui.h" + +using namespace halsimgui; + +namespace { +HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AnalogInVoltage, "AIn"); + +class AnalogInputSimModel : public glass::AnalogInputModel { + public: + explicit AnalogInputSimModel(int32_t index) + : m_index{index}, m_voltageData{m_index} {} + + void Update() override {} + + bool Exists() override { return HALSIM_GetAnalogInInitialized(m_index); } + + bool IsGyro() const override { + return m_index < HAL_GetNumAccumulators() && + HALSIM_GetAnalogGyroInitialized(m_index); + } + + const char* GetSimDevice() const override { + if (auto simDevice = HALSIM_GetAnalogInSimDevice(m_index)) { + return HALSIM_GetSimDeviceName(simDevice); + } else { + return nullptr; + } + } + + glass::DataSource* GetVoltageData() override { return &m_voltageData; } + + void SetVoltage(double val) override { + HALSIM_SetAnalogInVoltage(m_index, val); + } + + private: + int32_t m_index; + AnalogInVoltageSource m_voltageData; +}; + +class AnalogInputsSimModel : public glass::AnalogInputsModel { + public: + AnalogInputsSimModel() : m_models(HAL_GetNumAnalogInputs()) {} + + void Update() override; + + bool Exists() override { return true; } + + void ForEachAnalogInput( + wpi::function_ref func) + override; + + private: + // indexed by channel + std::vector> m_models; +}; +} // namespace + +void AnalogInputsSimModel::Update() { + for (int32_t i = 0, iend = static_cast(m_models.size()); i < iend; + ++i) { + auto& model = m_models[i]; + if (HALSIM_GetAnalogInInitialized(i)) { + if (!model) { + model = std::make_unique(i); + } + } else { + model.reset(); + } + } +} + +void AnalogInputsSimModel::ForEachAnalogInput( + wpi::function_ref func) { + for (int32_t i = 0, iend = static_cast(m_models.size()); i < iend; + ++i) { + if (auto model = m_models[i].get()) { + func(*model, i); + } + } +} + +static bool AnalogInputsAnyInitialized() { + static const int32_t num = HAL_GetNumAnalogInputs(); + for (int32_t i = 0; i < num; ++i) { + if (HALSIM_GetAnalogInInitialized(i)) return true; + } + return false; +} + +void AnalogInputSimGui::Initialize() { + HALSimGui::halProvider.Register( + "Analog Inputs", AnalogInputsAnyInitialized, + [] { return std::make_unique(); }, + [](glass::Window* win, glass::Model* model) { + win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize); + win->SetDefaultPos(640, 20); + return glass::MakeFunctionView([=] { + glass::DisplayAnalogInputs(static_cast(model)); + }); + }); +} diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogOutGui.h b/simulation/halsim_gui/src/main/native/cpp/AnalogInputSimGui.h similarity index 85% rename from simulation/halsim_gui/src/main/native/cpp/AnalogOutGui.h rename to simulation/halsim_gui/src/main/native/cpp/AnalogInputSimGui.h index dd01699146..c92a395534 100644 --- a/simulation/halsim_gui/src/main/native/cpp/AnalogOutGui.h +++ b/simulation/halsim_gui/src/main/native/cpp/AnalogInputSimGui.h @@ -1,5 +1,5 @@ /*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019 FIRST. All Rights Reserved. */ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ /* Open Source Software - may be modified and shared by FRC teams. The code */ /* must be accompanied by the FIRST BSD license file in the root directory of */ /* the project. */ @@ -9,7 +9,7 @@ namespace halsimgui { -class AnalogOutGui { +class AnalogInputSimGui { public: static void Initialize(); }; diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogOutGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AnalogOutGui.cpp deleted file mode 100644 index 3e7f4ea6ca..0000000000 --- a/simulation/halsim_gui/src/main/native/cpp/AnalogOutGui.cpp +++ /dev/null @@ -1,81 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#include "AnalogOutGui.h" - -#include -#include - -#include -#include -#include - -#include "GuiDataSource.h" -#include "HALSimGui.h" -#include "IniSaver.h" -#include "IniSaverInfo.h" -#include "SimDeviceGui.h" - -using namespace halsimgui; - -namespace { -HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AnalogOutVoltage, "AOut"); -} // namespace - -static IniSaver gAnalogOuts{"AnalogOut"}; // indexed by channel -static std::vector> gAnalogOutSources; - -static void UpdateAnalogOutSources() { - for (int i = 0, iend = gAnalogOutSources.size(); i < iend; ++i) { - auto& source = gAnalogOutSources[i]; - if (HALSIM_GetAnalogOutInitialized(i)) { - if (!source) { - source = std::make_unique(i); - source->SetName(gAnalogOuts[i].GetName()); - } - } else { - source.reset(); - } - } -} - -static void DisplayAnalogOutputs() { - int count = 0; - for (auto&& source : gAnalogOutSources) { - if (source) ++count; - } - - if (count == 0) return; - - if (SimDeviceGui::StartDevice("Analog Outputs")) { - for (int i = 0, iend = gAnalogOutSources.size(); i < iend; ++i) { - if (auto source = gAnalogOutSources[i].get()) { - ImGui::PushID(i); - - auto& info = gAnalogOuts[i]; - char label[128]; - info.GetLabel(label, sizeof(label), "Out", i); - HAL_Value value = HAL_MakeDouble(source->GetValue()); - SimDeviceGui::DisplayValueSource(label, true, &value, source); - - if (info.PopupEditName(i)) { - if (source) source->SetName(info.GetName()); - } - ImGui::PopID(); - } - } - - SimDeviceGui::FinishDevice(); - } -} - -void AnalogOutGui::Initialize() { - gAnalogOuts.Initialize(); - gAnalogOutSources.resize(HAL_GetNumAnalogOutputs()); - HALSimGui::AddExecute(UpdateAnalogOutSources); - SimDeviceGui::Add(DisplayAnalogOutputs); -} diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogOutputSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AnalogOutputSimGui.cpp new file mode 100644 index 0000000000..01e229ea9f --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/AnalogOutputSimGui.cpp @@ -0,0 +1,96 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "AnalogOutputSimGui.h" + +#include +#include + +#include +#include + +#include +#include + +#include "HALDataSource.h" +#include "HALSimGui.h" +#include "SimDeviceGui.h" + +using namespace halsimgui; + +namespace { +HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AnalogOutVoltage, "AOut"); + +class AnalogOutputSimModel : public glass::AnalogOutputModel { + public: + explicit AnalogOutputSimModel(int32_t index) + : m_index{index}, m_voltageData{m_index} {} + + void Update() override {} + + bool Exists() override { return HALSIM_GetAnalogOutInitialized(m_index); } + + glass::DataSource* GetVoltageData() override { return &m_voltageData; } + + void SetVoltage(double val) override { + HALSIM_SetAnalogOutVoltage(m_index, val); + } + + private: + int32_t m_index; + AnalogOutVoltageSource m_voltageData; +}; + +class AnalogOutputsSimModel : public glass::AnalogOutputsModel { + public: + AnalogOutputsSimModel() : m_models(HAL_GetNumAnalogOutputs()) {} + + void Update() override; + + bool Exists() override { return true; } + + void ForEachAnalogOutput( + wpi::function_ref func) + override; + + private: + // indexed by channel + std::vector> m_models; +}; +} // namespace + +void AnalogOutputsSimModel::Update() { + for (int32_t i = 0, iend = static_cast(m_models.size()); i < iend; + ++i) { + auto& model = m_models[i]; + if (HALSIM_GetAnalogOutInitialized(i)) { + if (!model) { + model = std::make_unique(i); + } + } else { + model.reset(); + } + } +} + +void AnalogOutputsSimModel::ForEachAnalogOutput( + wpi::function_ref func) { + for (int32_t i = 0, iend = static_cast(m_models.size()); i < iend; + ++i) { + if (auto model = m_models[i].get()) { + func(*model, i); + } + } +} + +void AnalogOutputSimGui::Initialize() { + SimDeviceGui::GetDeviceTree().Add( + std::make_unique(), [](glass::Model* model) { + glass::DisplayAnalogOutputsDevice( + static_cast(model)); + }); +} diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogGyroGui.h b/simulation/halsim_gui/src/main/native/cpp/AnalogOutputSimGui.h similarity index 85% rename from simulation/halsim_gui/src/main/native/cpp/AnalogGyroGui.h rename to simulation/halsim_gui/src/main/native/cpp/AnalogOutputSimGui.h index e528279de1..6092f7e0d4 100644 --- a/simulation/halsim_gui/src/main/native/cpp/AnalogGyroGui.h +++ b/simulation/halsim_gui/src/main/native/cpp/AnalogOutputSimGui.h @@ -1,5 +1,5 @@ /*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019 FIRST. All Rights Reserved. */ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ /* Open Source Software - may be modified and shared by FRC teams. The code */ /* must be accompanied by the FIRST BSD license file in the root directory of */ /* the project. */ @@ -9,7 +9,7 @@ namespace halsimgui { -class AnalogGyroGui { +class AnalogOutputSimGui { public: static void Initialize(); }; diff --git a/simulation/halsim_gui/src/main/native/cpp/CompressorGui.cpp b/simulation/halsim_gui/src/main/native/cpp/CompressorGui.cpp deleted file mode 100644 index 43215fa27b..0000000000 --- a/simulation/halsim_gui/src/main/native/cpp/CompressorGui.cpp +++ /dev/null @@ -1,104 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#include "CompressorGui.h" - -#include -#include -#include - -#include -#include -#include -#include - -#include "GuiDataSource.h" -#include "HALSimGui.h" -#include "SimDeviceGui.h" - -using namespace halsimgui; - -namespace { -HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(PCMCompressorOn, "Compressor On"); -HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(PCMClosedLoopEnabled, "Closed Loop"); -HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(PCMPressureSwitch, "Pressure Switch"); -HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(PCMCompressorCurrent, "Comp Current"); -struct CompressorSource { - explicit CompressorSource(int32_t index) - : running{index}, enabled{index}, pressureSwitch{index}, current{index} {} - PCMCompressorOnSource running; - PCMClosedLoopEnabledSource enabled; - PCMPressureSwitchSource pressureSwitch; - PCMCompressorCurrentSource current; -}; -} // namespace - -static std::vector> gCompressorSources; - -static void UpdateCompressorSources() { - for (int i = 0, iend = gCompressorSources.size(); i < iend; ++i) { - auto& source = gCompressorSources[i]; - if (HALSIM_GetPCMCompressorInitialized(i)) { - if (!source) { - source = std::make_unique(i); - } - } else { - source.reset(); - } - } -} - -static void DisplayCompressors() { - for (int i = 0, iend = gCompressorSources.size(); i < iend; ++i) { - if (auto source = gCompressorSources[i].get()) { - char name[32]; - std::snprintf(name, sizeof(name), "Compressor[%d]", i); - if (SimDeviceGui::StartDevice(name)) { - HAL_Value value; - - // enabled - if (HALSimGui::AreOutputsDisabled()) - value = HAL_MakeBoolean(false); - else - value = HAL_MakeBoolean(source->running.GetValue()); - if (SimDeviceGui::DisplayValueSource("Running", false, &value, - &source->running)) - HALSIM_SetPCMCompressorOn(i, value.data.v_boolean); - - // closed loop - value = HAL_MakeEnum(source->enabled.GetValue() ? 1 : 0); - static const char* enabledOptions[] = {"disabled", "enabled"}; - if (SimDeviceGui::DisplayValueSource("Closed Loop", true, &value, - &source->enabled, enabledOptions, - 2)) - HALSIM_SetPCMClosedLoopEnabled(i, value.data.v_enum); - - // pressure switch - value = HAL_MakeEnum(source->pressureSwitch.GetValue() ? 1 : 0); - static const char* switchOptions[] = {"full", "low"}; - if (SimDeviceGui::DisplayValueSource("Pressure", false, &value, - &source->pressureSwitch, - switchOptions, 2)) - HALSIM_SetPCMPressureSwitch(i, value.data.v_enum); - - // compressor current - value = HAL_MakeDouble(source->current.GetValue()); - if (SimDeviceGui::DisplayValueSource("Current (A)", false, &value, - &source->current)) - HALSIM_SetPCMCompressorCurrent(i, value.data.v_double); - - SimDeviceGui::FinishDevice(); - } - } - } -} - -void CompressorGui::Initialize() { - gCompressorSources.resize(HAL_GetNumPCMModules()); - HALSimGui::AddExecute(UpdateCompressorSources); - SimDeviceGui::Add(DisplayCompressors); -} diff --git a/simulation/halsim_gui/src/main/native/cpp/DIOGui.cpp b/simulation/halsim_gui/src/main/native/cpp/DIOGui.cpp deleted file mode 100644 index 2f82ff2930..0000000000 --- a/simulation/halsim_gui/src/main/native/cpp/DIOGui.cpp +++ /dev/null @@ -1,214 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#include "DIOGui.h" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "GuiDataSource.h" -#include "HALSimGui.h" -#include "IniSaver.h" -#include "IniSaverInfo.h" - -using namespace halsimgui; - -namespace { -HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(DIOValue, "DIO"); -HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(DigitalPWMDutyCycle, "DPWM"); -HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(DutyCycleOutput, "DutyCycle"); -} // namespace - -static IniSaver gDIO{"DIO"}; -static std::vector> gDIOSources; -static std::vector> gDPWMSources; -static std::vector> gDutyCycleSources; - -static void LabelSimDevice(const char* name, HAL_SimDeviceHandle simDevice) { - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255)); - ImGui::LabelText(name, "%s", HALSIM_GetSimDeviceName(simDevice)); - ImGui::PopStyleColor(); -} - -static void UpdateDIOSources() { - for (int i = 0, iend = gDIOSources.size(); i < iend; ++i) { - auto& source = gDIOSources[i]; - if (HALSIM_GetDIOInitialized(i)) { - if (!source) { - source = std::make_unique(i); - source->SetName(gDIO[i].GetName()); - } - } else { - source.reset(); - } - } -} - -static void UpdateDPWMSources() { - const int numDIO = gDIOSources.size(); - for (int i = 0, iend = gDPWMSources.size(); i < iend; ++i) { - auto& source = gDPWMSources[i]; - if (HALSIM_GetDigitalPWMInitialized(i)) { - if (!source) { - int channel = HALSIM_GetDigitalPWMPin(i); - if (channel >= 0 && channel < numDIO) { - source = std::make_unique(i, channel); - source->SetName(gDIO[channel].GetName()); - } - } - } else { - source.reset(); - } - } -} - -static void UpdateDutyCycleSources() { - const int numDIO = gDIOSources.size(); - for (int i = 0, iend = gDutyCycleSources.size(); i < iend; ++i) { - auto& source = gDutyCycleSources[i]; - if (HALSIM_GetDutyCycleInitialized(i)) { - if (!source) { - int channel = HALSIM_GetDutyCycleDigitalChannel(i); - if (channel >= 0 && channel < numDIO) { - source = std::make_unique(i, channel); - source->SetName(gDIO[channel].GetName()); - } - } - } else { - source.reset(); - } - } -} - -static void DisplayDIO() { - bool hasAny = false; - const int numDIO = gDIOSources.size(); - const int numPWM = gDPWMSources.size(); - static const int numEncoder = HAL_GetNumEncoders(); - const int numDutyCycle = gDutyCycleSources.size(); - static auto pwmMap = std::make_unique(numDIO); - static auto encoderMap = std::make_unique(numDIO); - static auto dutyCycleMap = std::make_unique(numDIO); - - std::memset(pwmMap.get(), 0, numDIO * sizeof(pwmMap[0])); - std::memset(encoderMap.get(), 0, numDIO * sizeof(encoderMap[0])); - std::memset(dutyCycleMap.get(), 0, numDIO * sizeof(dutyCycleMap[0])); - - for (int i = 0; i < numPWM; ++i) { - if (auto source = gDPWMSources[i].get()) { - int channel = source->GetChannel(); - if (channel >= 0 && channel < numDIO) pwmMap[channel] = i + 1; - } - } - - for (int i = 0; i < numEncoder; ++i) { - if (HALSIM_GetEncoderInitialized(i)) { - int channel; - channel = HALSIM_GetEncoderDigitalChannelA(i); - if (channel >= 0 && channel < numDIO) encoderMap[channel] = i + 1; - channel = HALSIM_GetEncoderDigitalChannelB(i); - if (channel >= 0 && channel < numDIO) encoderMap[channel] = i + 1; - } - } - - for (int i = 0; i < numDutyCycle; ++i) { - if (auto source = gDutyCycleSources[i].get()) { - int channel = source->GetChannel(); - if (channel >= 0 && channel < numDIO) dutyCycleMap[channel] = i + 1; - } - } - - ImGui::PushItemWidth(ImGui::GetFontSize() * 8); - for (int i = 0; i < numDIO; ++i) { - if (auto dioSource = gDIOSources[i].get()) { - ImGui::PushID(i); - hasAny = true; - DigitalPWMDutyCycleSource* dpwmSource = nullptr; - DutyCycleOutputSource* dutyCycleSource = nullptr; - auto& info = gDIO[i]; - char label[128]; - if (pwmMap[i] > 0) { - dpwmSource = gDPWMSources[pwmMap[i] - 1].get(); - info.GetLabel(label, sizeof(label), "PWM", i); - if (auto simDevice = HALSIM_GetDIOSimDevice(i)) { - LabelSimDevice(label, simDevice); - } else { - dpwmSource->LabelText(label, "%0.3f", dpwmSource->GetValue()); - } - } else if (encoderMap[i] > 0) { - info.GetLabel(label, sizeof(label), " In", i); - if (auto simDevice = HALSIM_GetEncoderSimDevice(encoderMap[i] - 1)) { - LabelSimDevice(label, simDevice); - } else { - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255)); - ImGui::LabelText(label, "Encoder[%d,%d]", - HALSIM_GetEncoderDigitalChannelA(encoderMap[i] - 1), - HALSIM_GetEncoderDigitalChannelB(encoderMap[i] - 1)); - ImGui::PopStyleColor(); - } - } else if (dutyCycleMap[i] > 0) { - dutyCycleSource = gDutyCycleSources[dutyCycleMap[i] - 1].get(); - info.GetLabel(label, sizeof(label), "Dty", i); - if (auto simDevice = - HALSIM_GetDutyCycleSimDevice(dutyCycleMap[i] - 1)) { - LabelSimDevice(label, simDevice); - } else { - double val = dutyCycleSource->GetValue(); - if (dutyCycleSource->InputDouble(label, &val)) - HALSIM_SetDutyCycleOutput(dutyCycleMap[i] - 1, val); - } - } else if (!HALSIM_GetDIOIsInput(i)) { - info.GetLabel(label, sizeof(label), "Out", i); - if (auto simDevice = HALSIM_GetDIOSimDevice(i)) { - LabelSimDevice(label, simDevice); - } else { - dioSource->LabelText( - label, "%s", dioSource->GetValue() != 0 ? "1 (high)" : "0 (low)"); - } - } else { - info.GetLabel(label, sizeof(label), " In", i); - if (auto simDevice = HALSIM_GetDIOSimDevice(i)) { - LabelSimDevice(label, simDevice); - } else { - static const char* options[] = {"0 (low)", "1 (high)"}; - int val = dioSource->GetValue() != 0 ? 1 : 0; - if (dioSource->Combo(label, &val, options, 2)) - HALSIM_SetDIOValue(i, val); - } - } - if (info.PopupEditName(i)) { - dioSource->SetName(info.GetName()); - if (dpwmSource) dpwmSource->SetName(info.GetName()); - if (dutyCycleSource) dutyCycleSource->SetName(info.GetName()); - } - ImGui::PopID(); - } - } - ImGui::PopItemWidth(); - if (!hasAny) ImGui::Text("No Digital I/O"); -} - -void DIOGui::Initialize() { - gDIO.Initialize(); - gDIOSources.resize(HAL_GetNumDigitalChannels()); - gDPWMSources.resize(HAL_GetNumDigitalPWMOutputs()); - gDutyCycleSources.resize(HAL_GetNumDutyCycles()); - - HALSimGui::AddExecute(UpdateDIOSources); - HALSimGui::AddExecute(UpdateDPWMSources); - HALSimGui::AddExecute(UpdateDutyCycleSources); - HALSimGui::AddWindow("DIO", DisplayDIO, ImGuiWindowFlags_AlwaysAutoResize); - HALSimGui::SetDefaultWindowPos("DIO", 470, 20); -} diff --git a/simulation/halsim_gui/src/main/native/cpp/DIOSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/DIOSimGui.cpp new file mode 100644 index 0000000000..8a2c816de6 --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/DIOSimGui.cpp @@ -0,0 +1,244 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "DIOSimGui.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "EncoderSimGui.h" +#include "HALDataSource.h" +#include "HALSimGui.h" + +using namespace halsimgui; + +namespace { +HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(DIOValue, "DIO"); +HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(DigitalPWMDutyCycle, "DPWM"); +HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(DutyCycleOutput, "DutyCycle"); + +class DPWMSimModel : public glass::DPWMModel { + public: + DPWMSimModel(int32_t index, int32_t dioChannel) + : m_dioChannel{dioChannel}, m_index{index}, m_valueData{index} {} + + void Update() override {} + + bool Exists() override { return HALSIM_GetDigitalPWMInitialized(m_index); } + + const char* GetSimDevice() const override { + if (auto simDevice = HALSIM_GetDIOSimDevice(m_dioChannel)) { + return HALSIM_GetSimDeviceName(simDevice); + } else { + return nullptr; + } + } + + glass::DataSource* GetValueData() override { return &m_valueData; } + + void SetValue(double val) override { + HALSIM_SetDigitalPWMDutyCycle(m_index, val); + } + + private: + int32_t m_dioChannel; + int32_t m_index; + DigitalPWMDutyCycleSource m_valueData; +}; + +class DutyCycleSimModel : public glass::DutyCycleModel { + public: + explicit DutyCycleSimModel(int32_t index) + : m_index{index}, m_valueData{index} {} + + void Update() override {} + + bool Exists() override { return HALSIM_GetDutyCycleInitialized(m_index); } + + const char* GetSimDevice() const override { + if (auto simDevice = HALSIM_GetDutyCycleSimDevice(m_index)) { + return HALSIM_GetSimDeviceName(simDevice); + } else { + return nullptr; + } + } + + glass::DataSource* GetValueData() override { return &m_valueData; } + + void SetValue(double val) override { + HALSIM_SetDutyCycleOutput(m_index, val); + } + + private: + int32_t m_index; + DutyCycleOutputSource m_valueData; +}; + +class DIOSimModel : public glass::DIOModel { + public: + explicit DIOSimModel(int32_t channel) + : m_channel{channel}, m_valueData{channel} {} + + void Update() override {} + + bool Exists() override { return HALSIM_GetDIOInitialized(m_channel); } + + bool IsReadOnly() override { return !IsInput(); } + + const char* GetName() const override { return ""; } + + const char* GetSimDevice() const override { + if (auto simDevice = HALSIM_GetDIOSimDevice(m_channel)) { + return HALSIM_GetSimDeviceName(simDevice); + } else { + return nullptr; + } + } + + DPWMSimModel* GetDPWM() override { return m_dpwmSource; } + DutyCycleSimModel* GetDutyCycle() override { return m_dutyCycleSource; } + glass::EncoderModel* GetEncoder() override { return m_encoderSource; } + + void SetDPWM(DPWMSimModel* model) { m_dpwmSource = model; } + void SetDutyCycle(DutyCycleSimModel* model) { m_dutyCycleSource = model; } + void SetEncoder(glass::EncoderModel* model) { m_encoderSource = model; } + + bool IsInput() const override { return HALSIM_GetDIOIsInput(m_channel); } + + glass::DataSource* GetValueData() override { return &m_valueData; } + + void SetValue(bool val) override { HALSIM_SetDIOValue(m_channel, val); } + + private: + int32_t m_channel; + DIOValueSource m_valueData; + DPWMSimModel* m_dpwmSource = nullptr; + DutyCycleSimModel* m_dutyCycleSource = nullptr; + glass::EncoderModel* m_encoderSource = nullptr; +}; + +class DIOsSimModel : public glass::DIOsModel { + public: + DIOsSimModel() + : m_dioModels(HAL_GetNumDigitalChannels()), + m_dpwmModels(HAL_GetNumDigitalPWMOutputs()), + m_dutyCycleModels(HAL_GetNumDutyCycles()) {} + + void Update() override; + + bool Exists() override { return true; } + + void ForEachDIO( + wpi::function_ref func) override; + + private: + // indexed by channel + std::vector> m_dioModels; + // indexed by index + std::vector> m_dpwmModels; + std::vector> m_dutyCycleModels; +}; +} // namespace + +void DIOsSimModel::Update() { + const int32_t numDIO = m_dioModels.size(); + for (int i = 0; i < numDIO; ++i) { + auto& model = m_dioModels[i]; + if (HALSIM_GetDIOInitialized(i)) { + if (!model) { + model = std::make_unique(i); + } + model->SetDPWM(nullptr); + model->SetDutyCycle(nullptr); + model->SetEncoder(nullptr); + } else { + model.reset(); + } + } + + const int32_t numPWM = m_dpwmModels.size(); + for (int32_t i = 0; i < numPWM; ++i) { + auto& model = m_dpwmModels[i]; + if (HALSIM_GetDigitalPWMInitialized(i)) { + if (!model) { + int channel = HALSIM_GetDigitalPWMPin(i); + if (channel >= 0 && channel < numDIO && m_dioModels[channel]) { + model = std::make_unique(i, channel); + m_dioModels[channel]->SetDPWM(model.get()); + } + } + } else { + model.reset(); + } + } + + const int32_t numDutyCycle = m_dutyCycleModels.size(); + for (int32_t i = 0; i < numDutyCycle; ++i) { + auto& model = m_dutyCycleModels[i]; + if (HALSIM_GetDutyCycleInitialized(i)) { + if (!model) { + int channel = HALSIM_GetDutyCycleDigitalChannel(i); + if (channel >= 0 && channel < numDIO && m_dioModels[channel]) { + model = std::make_unique(i); + m_dioModels[channel]->SetDutyCycle(model.get()); + } + } + } else { + model.reset(); + } + } + + EncoderSimGui::GetEncodersModel().ForEachEncoder([&](auto& encoder, int i) { + int channel = encoder.GetChannelA(); + if (channel >= 0 && channel < numDIO && m_dioModels[channel]) + m_dioModels[channel]->SetEncoder(&encoder); + channel = encoder.GetChannelB(); + if (channel >= 0 && channel < numDIO && m_dioModels[channel]) + m_dioModels[channel]->SetEncoder(&encoder); + }); +} + +void DIOsSimModel::ForEachDIO( + wpi::function_ref func) { + const int32_t numDIO = m_dioModels.size(); + for (int32_t i = 0; i < numDIO; ++i) { + if (auto model = m_dioModels[i].get()) { + func(*model, i); + } + } +} + +static bool DIOAnyInitialized() { + static const int32_t num = HAL_GetNumDigitalChannels(); + for (int32_t i = 0; i < num; ++i) { + if (HALSIM_GetDIOInitialized(i)) return true; + } + return false; +} + +void DIOSimGui::Initialize() { + HALSimGui::halProvider.Register( + "DIO", DIOAnyInitialized, [] { return std::make_unique(); }, + [](glass::Window* win, glass::Model* model) { + win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize); + win->SetDefaultPos(470, 20); + return glass::MakeFunctionView([=] { + glass::DisplayDIOs(static_cast(model), + HALSimGui::halProvider.AreOutputsEnabled()); + }); + }); +} diff --git a/simulation/halsim_gui/src/main/native/cpp/EncoderGui.h b/simulation/halsim_gui/src/main/native/cpp/DIOSimGui.h similarity index 96% rename from simulation/halsim_gui/src/main/native/cpp/EncoderGui.h rename to simulation/halsim_gui/src/main/native/cpp/DIOSimGui.h index b7c9dbf6ae..0dc1d5566a 100644 --- a/simulation/halsim_gui/src/main/native/cpp/EncoderGui.h +++ b/simulation/halsim_gui/src/main/native/cpp/DIOSimGui.h @@ -9,7 +9,7 @@ namespace halsimgui { -class EncoderGui { +class DIOSimGui { public: static void Initialize(); }; diff --git a/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.cpp b/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.cpp index 67774ca54f..716e65bfaa 100644 --- a/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.cpp @@ -7,6 +7,10 @@ #include "DriverStationGui.h" +#include +#include +#include + #include #include #include @@ -22,11 +26,10 @@ #include #include #include +#include -#include "ExtraGuiWidgets.h" -#include "GuiDataSource.h" +#include "HALDataSource.h" #include "HALSimGui.h" -#include "IniSaverInfo.h" using namespace halsimgui; @@ -50,7 +53,7 @@ struct SystemJoystick { }; struct RobotJoystick { - NameInfo name; + glass::NameInfo name; std::string guid; const SystemJoystick* sys = nullptr; bool useGamepad = false; @@ -67,24 +70,24 @@ struct RobotJoystick { bool IsButtonPressed(int i) { return (buttons.buttons & (1u << i)) != 0; } }; -class JoystickSource { +class JoystickModel { public: - explicit JoystickSource(int index); - ~JoystickSource() { + explicit JoystickModel(int index); + ~JoystickModel() { HALSIM_CancelDriverStationNewDataCallback(m_callback); for (int i = 0; i < buttonCount; ++i) delete buttons[i]; } - JoystickSource(const JoystickSource&) = delete; - JoystickSource& operator=(const JoystickSource&) = delete; + JoystickModel(const JoystickModel&) = delete; + JoystickModel& operator=(const JoystickModel&) = delete; int axisCount; int buttonCount; int povCount; - std::unique_ptr axes[HAL_kMaxJoystickAxes]; + std::unique_ptr axes[HAL_kMaxJoystickAxes]; // use pointer instead of unique_ptr to allow it to be passed directly // to DrawLEDSources() - GuiDataSource* buttons[32]; - std::unique_ptr povs[HAL_kMaxJoystickPOVs]; + glass::DataSource* buttons[32]; + std::unique_ptr povs[HAL_kMaxJoystickPOVs]; private: static void CallbackFunc(const char*, void* param, const HAL_Value*); @@ -92,6 +95,77 @@ class JoystickSource { int m_index; int32_t m_callback; }; + +class FMSSimModel : public glass::FMSModel { + public: + FMSSimModel(); + + glass::DataSource* GetFmsAttachedData() override { return &m_fmsAttached; } + glass::DataSource* GetDsAttachedData() override { return &m_dsAttached; } + glass::DataSource* GetAllianceStationIdData() override { + return &m_allianceStationId; + } + glass::DataSource* GetMatchTimeData() override { return &m_matchTime; } + glass::DataSource* GetEStopData() override { return &m_estop; } + glass::DataSource* GetEnabledData() override { return &m_enabled; } + glass::DataSource* GetTestData() override { return &m_test; } + glass::DataSource* GetAutonomousData() override { return &m_autonomous; } + wpi::StringRef GetGameSpecificMessage( + wpi::SmallVectorImpl& buf) override { + HAL_MatchInfo info; + HALSIM_GetMatchInfo(&info); + buf.clear(); + buf.append(info.gameSpecificMessage, + info.gameSpecificMessage + info.gameSpecificMessageSize); + return wpi::StringRef(buf.begin(), buf.size()); + } + + void SetFmsAttached(bool val) override { + HALSIM_SetDriverStationFmsAttached(val); + } + void SetDsAttached(bool val) override { + HALSIM_SetDriverStationDsAttached(val); + } + void SetAllianceStationId(int val) override { + HALSIM_SetDriverStationAllianceStationId( + static_cast(val)); + } + 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); } + void SetTest(bool val) override { HALSIM_SetDriverStationTest(val); } + void SetAutonomous(bool val) override { + HALSIM_SetDriverStationAutonomous(val); + } + void SetGameSpecificMessage(const char* val) override { + HALSIM_SetGameSpecificMessage(val); + } + + void Update() override; + + bool Exists() override { return true; } + + bool IsReadOnly() override; + + bool m_matchTimeEnabled = true; + + private: + glass::DataSource m_fmsAttached{"FMS:FMSAttached"}; + glass::DataSource m_dsAttached{"FMS:DSAttached"}; + glass::DataSource m_allianceStationId{"FMS:AllianceStationID"}; + glass::DataSource m_matchTime{"FMS:MatchTime"}; + glass::DataSource m_estop{"FMS:EStop"}; + 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; +}; + } // namespace // system joysticks @@ -100,7 +174,13 @@ static int gNumSystemJoysticks = 0; // robot joysticks static RobotJoystick gRobotJoysticks[HAL_kMaxJoysticks]; -static std::unique_ptr gJoystickSources[HAL_kMaxJoysticks]; +static std::unique_ptr gJoystickSources[HAL_kMaxJoysticks]; + +// FMS +static std::unique_ptr gFMSModel; + +// Window management +DSManager DriverStationGui::dsManager{"DSManager"}; static bool gDisableDS = false; static bool gZeroDisconnectedJoysticks = true; @@ -110,14 +190,14 @@ static inline bool IsDSDisabled() { return gDisableDS || (gDSSocketConnected && *gDSSocketConnected); } -JoystickSource::JoystickSource(int index) : m_index{index} { +JoystickModel::JoystickModel(int index) : m_index{index} { HAL_JoystickAxes halAxes; HALSIM_GetJoystickAxes(index, &halAxes); axisCount = halAxes.count; for (int i = 0; i < axisCount; ++i) { - axes[i] = std::make_unique("Joystick[" + wpi::Twine{index} + - "] Axis[" + wpi::Twine{i} + - wpi::Twine{']'}); + axes[i] = std::make_unique( + "Joystick[" + wpi::Twine{index} + "] Axis[" + wpi::Twine{i} + + wpi::Twine{']'}); } HAL_JoystickButtons halButtons; @@ -125,8 +205,8 @@ JoystickSource::JoystickSource(int index) : m_index{index} { buttonCount = halButtons.count; for (int i = 0; i < buttonCount; ++i) { buttons[i] = - new GuiDataSource("Joystick[" + wpi::Twine{index} + "] Button[" + - wpi::Twine{i + 1} + wpi::Twine{']'}); + new glass::DataSource("Joystick[" + wpi::Twine{index} + "] Button[" + + wpi::Twine{i + 1} + wpi::Twine{']'}); buttons[i]->SetDigital(true); } for (int i = buttonCount; i < 32; ++i) buttons[i] = nullptr; @@ -135,17 +215,17 @@ JoystickSource::JoystickSource(int index) : m_index{index} { HALSIM_GetJoystickPOVs(index, &halPOVs); povCount = halPOVs.count; for (int i = 0; i < povCount; ++i) { - povs[i] = std::make_unique("Joystick[" + wpi::Twine{index} + - "] POV[" + wpi::Twine{i} + - wpi::Twine{']'}); + povs[i] = std::make_unique( + "Joystick[" + wpi::Twine{index} + "] POV[" + wpi::Twine{i} + + wpi::Twine{']'}); } m_callback = HALSIM_RegisterDriverStationNewDataCallback(CallbackFunc, this, true); } -void JoystickSource::CallbackFunc(const char*, void* param, const HAL_Value*) { - auto self = static_cast(param); +void JoystickModel::CallbackFunc(const char*, void* param, const HAL_Value*) { + auto self = static_cast(param); HAL_JoystickAxes halAxes; HALSIM_GetJoystickAxes(self->m_index, &halAxes); @@ -402,7 +482,7 @@ static void DriverStationExecute() { if (!source || source->axisCount != axisCount || source->buttonCount != buttonCount || source->povCount != povCount) { source.reset(); - source = std::make_unique(i); + source = std::make_unique(i); } } else { source.reset(); @@ -413,9 +493,13 @@ static void DriverStationExecute() { bool disableDS = IsDSDisabled(); if (disableDS && !prevDisableDS) { - HALSimGui::SetWindowVisibility("System Joysticks", HALSimGui::kDisabled); + if (auto win = HALSimGui::manager.GetWindow("System Joysticks")) { + win->SetVisibility(glass::Window::kDisabled); + } } else if (!disableDS && prevDisableDS) { - HALSimGui::SetWindowVisibility("System Joysticks", HALSimGui::kShow); + if (auto win = HALSimGui::manager.GetWindow("System Joysticks")) { + win->SetVisibility(glass::Window::kShow); + } } prevDisableDS = disableDS; if (disableDS) return; @@ -470,83 +554,48 @@ static void DriverStationExecute() { } } -static void DisplayFMS() { - bool fmsAttached = HALSIM_GetDriverStationFmsAttached(); - bool dsAttached = HALSIM_GetDriverStationDsAttached(); - static const char* stations[] = {"Red 1", "Red 2", "Red 3", - "Blue 1", "Blue 2", "Blue 3"}; - int allianceStationId = HALSIM_GetDriverStationAllianceStationId(); - double matchTime = HALSIM_GetDriverStationMatchTime(); - HAL_MatchInfo matchInfo; - HALSIM_GetMatchInfo(&matchInfo); - - if (IsDSDisabled()) { - if (!HALSIM_GetDriverStationEnabled()) - ImGui::Text("Robot State: Disabled"); - else if (HALSIM_GetDriverStationTest()) - ImGui::Text("Robot State: Test"); - else if (HALSIM_GetDriverStationAutonomous()) - ImGui::Text("Robot State: Autonomous"); - else - ImGui::Text("Robot State: Teleoperated"); - - ImGui::Text("FMS Attached: %s", fmsAttached ? "Yes" : "No"); - ImGui::Text("DS Attached: %s", dsAttached ? "Yes" : "No"); - ImGui::Text("Alliance Station: %s", stations[allianceStationId]); - ImGui::Text("Match Time: %.1f", matchTime); - ImGui::Text("Game Specific: %s", matchInfo.gameSpecificMessage); - return; - } - - double curTime = glfwGetTime(); - - // FMS Attached - if (ImGui::Checkbox("FMS Attached", &fmsAttached)) - HALSIM_SetDriverStationFmsAttached(fmsAttached); - - // DS Attached - if (ImGui::Checkbox("DS Attached", &dsAttached)) - HALSIM_SetDriverStationDsAttached(dsAttached); - - // Alliance Station - ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); - if (ImGui::Combo("Alliance Station", &allianceStationId, stations, 6)) - HALSIM_SetDriverStationAllianceStationId( - static_cast(allianceStationId)); - - // Match Time - static bool matchTimeEnabled = true; - ImGui::Checkbox("Match Time Enabled", &matchTimeEnabled); - - static double startMatchTime = 0.0; - ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); - if (ImGui::InputDouble("Match Time", &matchTime, 0, 0, "%.1f", - ImGuiInputTextFlags_EnterReturnsTrue)) { - HALSIM_SetDriverStationMatchTime(matchTime); - startMatchTime = curTime - matchTime; - } else if (!HALSIM_GetDriverStationEnabled() || HALSIM_IsTimingPaused()) { - startMatchTime = curTime - matchTime; - } else if (matchTimeEnabled) { - HALSIM_SetDriverStationMatchTime(curTime - startMatchTime); - } - ImGui::SameLine(); - if (ImGui::Button("Reset")) { - HALSIM_SetDriverStationMatchTime(0.0); - startMatchTime = curTime; - } - - // Game Specific Message - ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); - if (ImGui::InputText("Game Specific", - reinterpret_cast(matchInfo.gameSpecificMessage), - sizeof(matchInfo.gameSpecificMessage), - ImGuiInputTextFlags_EnterReturnsTrue)) { - matchInfo.gameSpecificMessageSize = - std::strlen(reinterpret_cast(matchInfo.gameSpecificMessage)); - HALSIM_SetMatchInfo(&matchInfo); - } +FMSSimModel::FMSSimModel() { + m_fmsAttached.SetDigital(true); + m_dsAttached.SetDigital(true); + m_estop.SetDigital(true); + m_enabled.SetDigital(true); + m_test.SetDigital(true); + m_autonomous.SetDigital(true); + Update(); } +void FMSSimModel::Update() { + bool enabled = HALSIM_GetDriverStationEnabled(); + m_fmsAttached.SetValue(HALSIM_GetDriverStationFmsAttached()); + m_dsAttached.SetValue(HALSIM_GetDriverStationDsAttached()); + m_allianceStationId.SetValue(HALSIM_GetDriverStationAllianceStationId()); + m_estop.SetValue(HALSIM_GetDriverStationEStop()); + m_enabled.SetValue(enabled); + m_test.SetValue(HALSIM_GetDriverStationTest()); + m_autonomous.SetValue(HALSIM_GetDriverStationAutonomous()); + + double matchTime = HALSIM_GetDriverStationMatchTime(); + if (m_matchTimeEnabled && !IsDSDisabled()) { + int32_t status = 0; + double curTime = HAL_GetFPGATime(&status) * 1.0e-6; + if (m_startMatchTime == 0.0) m_startMatchTime = curTime; + if (enabled) { + matchTime = curTime - m_startMatchTime; + HALSIM_SetDriverStationMatchTime(matchTime); + } else { + if (m_prevTime == 0.0) m_prevTime = curTime; + m_startMatchTime += (curTime - m_prevTime); + } + m_prevTime = curTime; + } else { + m_startMatchTime = 0.0; + m_prevTime = 0.0; + } + m_matchTime.SetValue(matchTime); +} + +bool FMSSimModel::IsReadOnly() { return IsDSDisabled(); } + static void DisplaySystemJoysticks() { ImGui::Text("(Drag and drop to Joysticks)"); int numShowJoysticks = gNumSystemJoysticks < 6 ? 6 : gNumSystemJoysticks; @@ -678,7 +727,7 @@ static void DisplayJoysticks() { ImGui::Columns(1); } -static void DriverStationOptionMenu() { +void DSManager::DisplayMenu() { if (gDSSocketConnected && *gDSSocketConnected) { ImGui::MenuItem("Turn off DS (real DS connected)", nullptr, true, false); } else { @@ -686,9 +735,14 @@ static void DriverStationOptionMenu() { ImGui::MenuItem("Zero disconnected joysticks", nullptr, &gZeroDisconnectedJoysticks, true); } + ImGui::Separator(); + + for (auto&& window : m_windows) { + window->DisplayMenuItem(); + } } -void DriverStationGui::Initialize() { +static void DriverStationInitialize() { // hook ini handler to save joystick settings ImGuiSettingsHandler iniHandler; iniHandler.TypeName = "Joystick"; @@ -705,18 +759,35 @@ void DriverStationGui::Initialize() { iniHandler.ReadLineFn = DriverStationReadLine; iniHandler.WriteAllFn = DriverStationWriteAll; ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler); +} - HALSimGui::AddExecute(DriverStationExecute); - HALSimGui::AddWindow("FMS", DisplayFMS, ImGuiWindowFlags_AlwaysAutoResize); - HALSimGui::AddWindow("System Joysticks", DisplaySystemJoysticks, - ImGuiWindowFlags_AlwaysAutoResize); - HALSimGui::AddWindow("Joysticks", DisplayJoysticks, - ImGuiWindowFlags_AlwaysAutoResize); - HALSimGui::AddOptionMenu(DriverStationOptionMenu); +void DriverStationGui::GlobalInit() { + dsManager.GlobalInit(); - HALSimGui::SetDefaultWindowPos("FMS", 5, 540); - HALSimGui::SetDefaultWindowPos("System Joysticks", 5, 385); - HALSimGui::SetDefaultWindowPos("Joysticks", 250, 465); + wpi::gui::AddInit(DriverStationInitialize); + + gFMSModel = std::make_unique(); + + wpi::gui::AddEarlyExecute(DriverStationExecute); + wpi::gui::AddEarlyExecute([] { gFMSModel->Update(); }); + if (auto win = dsManager.AddWindow("FMS", [] { + DisplayFMS(gFMSModel.get(), &gFMSModel->m_matchTimeEnabled); + })) { + win->DisableRenamePopup(); + win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize); + win->SetDefaultPos(5, 540); + } + if (auto win = + dsManager.AddWindow("System Joysticks", DisplaySystemJoysticks)) { + win->DisableRenamePopup(); + win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize); + win->SetDefaultPos(5, 385); + } + if (auto win = dsManager.AddWindow("Joysticks", DisplayJoysticks)) { + win->DisableRenamePopup(); + win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize); + win->SetDefaultPos(250, 465); + } } void DriverStationGui::SetDSSocketExtension(void* data) { diff --git a/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.h b/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.h index 5bf25b5375..c8dc422584 100644 --- a/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.h +++ b/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.h @@ -7,12 +7,23 @@ #pragma once +#include + namespace halsimgui { +class DSManager : public glass::WindowManager { + public: + explicit DSManager(const wpi::Twine& iniName) : WindowManager{iniName} {} + + void DisplayMenu() override; +}; + class DriverStationGui { public: - static void Initialize(); + static void GlobalInit(); static void SetDSSocketExtension(void* data); + + static DSManager dsManager; }; } // namespace halsimgui diff --git a/simulation/halsim_gui/src/main/native/cpp/EncoderGui.cpp b/simulation/halsim_gui/src/main/native/cpp/EncoderGui.cpp deleted file mode 100644 index 5b169f0e24..0000000000 --- a/simulation/halsim_gui/src/main/native/cpp/EncoderGui.cpp +++ /dev/null @@ -1,286 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#include "EncoderGui.h" - -#include -#include -#include - -#include -#include -#include -#include - -#include "GuiDataSource.h" -#include "HALSimGui.h" -#include "IniSaver.h" -#include "IniSaverInfo.h" - -using namespace halsimgui; - -namespace { - -struct EncoderInfo : public NameInfo, public OpenInfo { - bool ReadIni(wpi::StringRef name, wpi::StringRef value) { - if (NameInfo::ReadIni(name, value)) return true; - if (OpenInfo::ReadIni(name, value)) return true; - return false; - } - void WriteIni(ImGuiTextBuffer* out) { - NameInfo::WriteIni(out); - OpenInfo::WriteIni(out); - } -}; - -class EncoderSource { - public: - EncoderSource(const wpi::Twine& id, int32_t index, int channelA, int channelB) - : distancePerPulse(id + " Dist/Count"), - count(id + " Count"), - period(id + " Period"), - direction(id + " Direction"), - distance(id + " Distance"), - rate(id + " Rate"), - m_index{index}, - m_channelA{channelA}, - m_channelB{channelB}, - m_distancePerPulseCallback{ - HALSIM_RegisterEncoderDistancePerPulseCallback( - index, DistancePerPulseCallbackFunc, this, true)}, - m_countCallback{HALSIM_RegisterEncoderCountCallback( - index, CountCallbackFunc, this, true)}, - m_periodCallback{HALSIM_RegisterEncoderPeriodCallback( - index, PeriodCallbackFunc, this, true)}, - m_directionCallback{HALSIM_RegisterEncoderDirectionCallback( - index, DirectionCallbackFunc, this, true)} { - direction.SetDigital(true); - } - - EncoderSource(int32_t index, int channelA, int channelB) - : EncoderSource("Encoder[" + wpi::Twine(channelA) + wpi::Twine(',') + - wpi::Twine(channelB) + wpi::Twine(']'), - index, channelA, channelB) {} - - explicit EncoderSource(int32_t index) - : EncoderSource(index, HALSIM_GetEncoderDigitalChannelA(index), - HALSIM_GetEncoderDigitalChannelB(index)) {} - - ~EncoderSource() { - if (m_distancePerPulseCallback != 0) - HALSIM_CancelEncoderDistancePerPulseCallback(m_index, - m_distancePerPulseCallback); - if (m_countCallback != 0) - HALSIM_CancelEncoderCountCallback(m_index, m_countCallback); - if (m_periodCallback != 0) - HALSIM_CancelEncoderCountCallback(m_index, m_periodCallback); - if (m_directionCallback != 0) - HALSIM_CancelEncoderCountCallback(m_index, m_directionCallback); - } - - void SetName(const wpi::Twine& name) { - if (name.str().empty()) { - distancePerPulse.SetName(""); - count.SetName(""); - period.SetName(""); - direction.SetName(""); - distance.SetName(""); - rate.SetName(""); - } else { - distancePerPulse.SetName(name + " Distance/Count"); - count.SetName(name + " Count"); - period.SetName(name + " Period"); - direction.SetName(name + " Direction"); - distance.SetName(name + " Distance"); - rate.SetName(name + " Rate"); - } - } - - int32_t GetIndex() const { return m_index; } - int GetChannelA() const { return m_channelA; } - int GetChannelB() const { return m_channelB; } - - GuiDataSource distancePerPulse; - GuiDataSource count; - GuiDataSource period; - GuiDataSource direction; - GuiDataSource distance; - GuiDataSource rate; - - private: - static void DistancePerPulseCallbackFunc(const char*, void* param, - const HAL_Value* value) { - if (value->type == HAL_DOUBLE) { - auto self = static_cast(param); - double distPerPulse = value->data.v_double; - self->distancePerPulse.SetValue(distPerPulse); - self->distance.SetValue(self->count.GetValue() * distPerPulse); - double period = self->period.GetValue(); - if (period == 0) - self->rate.SetValue(std::numeric_limits::infinity()); - else if (period == std::numeric_limits::infinity()) - self->rate.SetValue(0); - else - self->rate.SetValue(static_cast(distPerPulse / period)); - } - } - - static void CountCallbackFunc(const char*, void* param, - const HAL_Value* value) { - if (value->type == HAL_INT) { - auto self = static_cast(param); - double count = value->data.v_int; - self->count.SetValue(count); - self->distance.SetValue(count * self->distancePerPulse.GetValue()); - } - } - - static void PeriodCallbackFunc(const char*, void* param, - const HAL_Value* value) { - if (value->type == HAL_DOUBLE) { - auto self = static_cast(param); - double period = value->data.v_double; - self->period.SetValue(period); - if (period == 0) - self->rate.SetValue(std::numeric_limits::infinity()); - else if (period == std::numeric_limits::infinity()) - self->rate.SetValue(0); - else - self->rate.SetValue( - static_cast(self->distancePerPulse.GetValue() / period)); - } - } - - static void DirectionCallbackFunc(const char*, void* param, - const HAL_Value* value) { - if (value->type == HAL_BOOLEAN) { - static_cast(param)->direction.SetValue( - value->data.v_boolean); - } - } - - int32_t m_index; - int m_channelA; - int m_channelB; - int32_t m_distancePerPulseCallback; - int32_t m_countCallback; - int32_t m_periodCallback; - int32_t m_directionCallback; -}; - -} // namespace - -static IniSaver gEncoders{"Encoder"}; // indexed by channel A -static std::vector> gEncoderSources; - -static void UpdateEncoderSources() { - for (int i = 0, iend = gEncoderSources.size(); i < iend; ++i) { - auto& source = gEncoderSources[i]; - if (HALSIM_GetEncoderInitialized(i)) { - if (!source) { - source = std::make_unique(i); - source->SetName(gEncoders[source->GetChannelA()].GetName()); - } - } else { - source.reset(); - } - } -} - -static void DisplayEncoders() { - bool hasAny = false; - ImGui::PushItemWidth(ImGui::GetFontSize() * 8); - for (int i = 0, iend = gEncoderSources.size(); i < iend; ++i) { - if (auto source = gEncoderSources[i].get()) { - hasAny = true; - if (auto simDevice = HALSIM_GetEncoderSimDevice(i)) { - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255)); - ImGui::Text("%s", HALSIM_GetSimDeviceName(simDevice)); - ImGui::PopStyleColor(); - } else { - int chA = source->GetChannelA(); - int chB = source->GetChannelB(); - - // build header name - auto& info = gEncoders[chA]; - char name[128]; - info.GetLabel(name, sizeof(name), "Encoder", chA, chB); - - // header - bool open = ImGui::CollapsingHeader( - name, gEncoders[chA].IsOpen() ? ImGuiTreeNodeFlags_DefaultOpen : 0); - info.SetOpen(open); - - // context menu to change name - if (info.PopupEditName(chA)) { - source->SetName(info.GetName()); - } - - if (open) { - ImGui::PushID(i); - // distance per pulse - double distancePerPulse = source->distancePerPulse.GetValue(); - source->distancePerPulse.LabelText("Dist/Count", "%.6f", - distancePerPulse); - - // count - int count = source->count.GetValue(); - if (ImGui::InputInt("##input", &count)) - HALSIM_SetEncoderCount(i, count); - ImGui::SameLine(); - if (ImGui::Button("Reset")) HALSIM_SetEncoderCount(i, 0); - ImGui::SameLine(); - ImGui::Selectable("Count"); - source->count.EmitDrag(); - - // max period - double maxPeriod = HALSIM_GetEncoderMaxPeriod(i); - ImGui::LabelText("Max Period", "%.6f", maxPeriod); - - // period - double period = source->period.GetValue(); - if (source->period.InputDouble("Period", &period, 0, 0, "%.6g")) - HALSIM_SetEncoderPeriod(i, period); - - // reverse direction - ImGui::LabelText( - "Reverse Direction", "%s", - HALSIM_GetEncoderReverseDirection(i) ? "true" : "false"); - - // direction - static const char* options[] = {"reverse", "forward"}; - int direction = source->direction.GetValue() ? 1 : 0; - if (source->direction.Combo("Direction", &direction, options, 2)) - HALSIM_SetEncoderDirection(i, direction); - - // distance - double distance = source->distance.GetValue(); - if (source->distance.InputDouble("Distance", &distance, 0, 0, "%.6g")) - HALSIM_SetEncoderDistance(i, distance); - - // rate - double rate = source->rate.GetValue(); - if (source->rate.InputDouble("Rate", &rate, 0, 0, "%.6g")) - HALSIM_SetEncoderRate(i, rate); - - ImGui::PopID(); - } - } - } - } - ImGui::PopItemWidth(); - if (!hasAny) ImGui::Text("No encoders"); -} - -void EncoderGui::Initialize() { - gEncoders.Initialize(); - gEncoderSources.resize(HAL_GetNumEncoders()); - HALSimGui::AddExecute(UpdateEncoderSources); - HALSimGui::AddWindow("Encoders", DisplayEncoders, - ImGuiWindowFlags_AlwaysAutoResize); - HALSimGui::SetDefaultWindowPos("Encoders", 5, 250); -} diff --git a/simulation/halsim_gui/src/main/native/cpp/EncoderSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/EncoderSimGui.cpp new file mode 100644 index 0000000000..6be4c39327 --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/EncoderSimGui.cpp @@ -0,0 +1,257 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "EncoderSimGui.h" + +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include "HALDataSource.h" +#include "HALSimGui.h" + +using namespace halsimgui; + +namespace { + +class EncoderSimModel : public glass::EncoderModel { + public: + EncoderSimModel(const wpi::Twine& id, int32_t index, int channelA, + int channelB) + : m_distancePerPulse(id + " Dist/Count"), + m_count(id + " Count"), + m_period(id + " Period"), + m_direction(id + " Direction"), + m_distance(id + " Distance"), + m_rate(id + " Rate"), + m_index{index}, + m_channelA{channelA}, + m_channelB{channelB}, + m_distancePerPulseCallback{ + HALSIM_RegisterEncoderDistancePerPulseCallback( + index, DistancePerPulseCallbackFunc, this, true)}, + m_countCallback{HALSIM_RegisterEncoderCountCallback( + index, CountCallbackFunc, this, true)}, + m_periodCallback{HALSIM_RegisterEncoderPeriodCallback( + index, PeriodCallbackFunc, this, true)}, + m_directionCallback{HALSIM_RegisterEncoderDirectionCallback( + index, DirectionCallbackFunc, this, true)} { + m_direction.SetDigital(true); + } + + EncoderSimModel(int32_t index, int channelA, int channelB) + : EncoderSimModel("Encoder[" + wpi::Twine(channelA) + wpi::Twine(',') + + wpi::Twine(channelB) + wpi::Twine(']'), + index, channelA, channelB) {} + + explicit EncoderSimModel(int32_t index) + : EncoderSimModel(index, HALSIM_GetEncoderDigitalChannelA(index), + HALSIM_GetEncoderDigitalChannelB(index)) {} + + ~EncoderSimModel() { + if (m_distancePerPulseCallback != 0) + HALSIM_CancelEncoderDistancePerPulseCallback(m_index, + m_distancePerPulseCallback); + if (m_countCallback != 0) + HALSIM_CancelEncoderCountCallback(m_index, m_countCallback); + if (m_periodCallback != 0) + HALSIM_CancelEncoderCountCallback(m_index, m_periodCallback); + if (m_directionCallback != 0) + HALSIM_CancelEncoderCountCallback(m_index, m_directionCallback); + } + + void Update() override {} + + bool Exists() override { return HALSIM_GetEncoderInitialized(m_index); } + + int32_t GetIndex() const { return m_index; } + + const char* GetSimDevice() const override { + if (auto simDevice = HALSIM_GetEncoderSimDevice(m_index)) + return HALSIM_GetSimDeviceName(simDevice); + else + return nullptr; + } + + int GetChannelA() const override { return m_channelA; } + int GetChannelB() const override { return m_channelB; } + + glass::DataSource* GetDistancePerPulseData() override { + return &m_distancePerPulse; + } + glass::DataSource* GetCountData() override { return &m_count; } + glass::DataSource* GetPeriodData() override { return &m_period; } + glass::DataSource* GetDirectionData() override { return &m_direction; } + glass::DataSource* GetDistanceData() override { return &m_distance; } + glass::DataSource* GetRateData() override { return &m_rate; } + + double GetMaxPeriod() override { return HALSIM_GetEncoderMaxPeriod(m_index); } + bool GetReverseDirection() override { + return HALSIM_GetEncoderReverseDirection(m_index); + } + + void SetDistancePerPulse(double val) override { + HALSIM_SetEncoderDistancePerPulse(m_index, val); + } + void SetCount(int val) override { HALSIM_SetEncoderCount(m_index, val); } + void SetPeriod(double val) override { HALSIM_SetEncoderPeriod(m_index, val); } + void SetDirection(bool val) override { + HALSIM_SetEncoderDirection(m_index, val); + } + void SetDistance(double val) override { + HALSIM_SetEncoderDistance(m_index, val); + } + void SetRate(double val) override { HALSIM_SetEncoderRate(m_index, val); } + + void SetMaxPeriod(double val) override { + HALSIM_SetEncoderMaxPeriod(m_index, val); + } + void SetReverseDirection(bool val) override { + HALSIM_SetEncoderReverseDirection(m_index, val); + } + + private: + static void DistancePerPulseCallbackFunc(const char*, void* param, + const HAL_Value* value) { + if (value->type == HAL_DOUBLE) { + auto self = static_cast(param); + double distPerPulse = value->data.v_double; + self->m_distancePerPulse.SetValue(distPerPulse); + self->m_distance.SetValue(self->m_count.GetValue() * distPerPulse); + double period = self->m_period.GetValue(); + if (period == 0) + self->m_rate.SetValue(std::numeric_limits::infinity()); + else if (period == std::numeric_limits::infinity()) + self->m_rate.SetValue(0); + else + self->m_rate.SetValue(static_cast(distPerPulse / period)); + } + } + + static void CountCallbackFunc(const char*, void* param, + const HAL_Value* value) { + if (value->type == HAL_INT) { + auto self = static_cast(param); + double count = value->data.v_int; + self->m_count.SetValue(count); + self->m_distance.SetValue(count * self->m_distancePerPulse.GetValue()); + } + } + + static void PeriodCallbackFunc(const char*, void* param, + const HAL_Value* value) { + if (value->type == HAL_DOUBLE) { + auto self = static_cast(param); + double period = value->data.v_double; + self->m_period.SetValue(period); + if (period == 0) + self->m_rate.SetValue(std::numeric_limits::infinity()); + else if (period == std::numeric_limits::infinity()) + self->m_rate.SetValue(0); + else + self->m_rate.SetValue( + static_cast(self->m_distancePerPulse.GetValue() / period)); + } + } + + static void DirectionCallbackFunc(const char*, void* param, + const HAL_Value* value) { + if (value->type == HAL_BOOLEAN) { + static_cast(param)->m_direction.SetValue( + value->data.v_boolean); + } + } + + glass::DataSource m_distancePerPulse; + glass::DataSource m_count; + glass::DataSource m_period; + glass::DataSource m_direction; + glass::DataSource m_distance; + glass::DataSource m_rate; + + int32_t m_index; + int m_channelA; + int m_channelB; + int32_t m_distancePerPulseCallback; + int32_t m_countCallback; + int32_t m_periodCallback; + int32_t m_directionCallback; +}; + +class EncodersSimModel : public glass::EncodersModel { + public: + EncodersSimModel() : m_models(HAL_GetNumEncoders()) {} + + void Update() override; + + bool Exists() override { return true; } + + void ForEachEncoder( + wpi::function_ref func) + override; + + private: + std::vector> m_models; +}; +} // namespace + +void EncodersSimModel::Update() { + for (int32_t i = 0, iend = static_cast(m_models.size()); i < iend; + ++i) { + auto& model = m_models[i]; + if (HALSIM_GetEncoderInitialized(i)) { + if (!model) { + model = std::make_unique(i); + } + } else { + model.reset(); + } + } +} + +void EncodersSimModel::ForEachEncoder( + wpi::function_ref func) { + for (int32_t i = 0, iend = static_cast(m_models.size()); i < iend; + ++i) { + if (auto model = m_models[i].get()) { + func(*model, i); + } + } +} + +static bool EncodersAnyInitialized() { + static const int32_t num = HAL_GetNumEncoders(); + for (int32_t i = 0; i < num; ++i) { + if (HALSIM_GetEncoderInitialized(i)) return true; + } + return false; +} + +void EncoderSimGui::Initialize() { + HALSimGui::halProvider.Register( + "Encoders", EncodersAnyInitialized, + [] { return std::make_unique(); }, + [](glass::Window* win, glass::Model* model) { + win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize); + win->SetDefaultPos(5, 250); + return glass::MakeFunctionView( + [=] { DisplayEncoders(static_cast(model)); }); + }); +} + +glass::EncodersModel& EncoderSimGui::GetEncodersModel() { + static auto model = HALSimGui::halProvider.GetModel("Encoders"); + assert(model); + return *static_cast(model); +} diff --git a/simulation/halsim_gui/src/main/native/cpp/EncoderSimGui.h b/simulation/halsim_gui/src/main/native/cpp/EncoderSimGui.h new file mode 100644 index 0000000000..25c3f7163b --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/EncoderSimGui.h @@ -0,0 +1,22 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +namespace glass { +class EncodersModel; +} // namespace glass + +namespace halsimgui { + +class EncoderSimGui { + public: + static void Initialize(); + static glass::EncodersModel& GetEncodersModel(); +}; + +} // namespace halsimgui diff --git a/simulation/halsim_gui/src/main/native/cpp/Field2D.cpp b/simulation/halsim_gui/src/main/native/cpp/Field2D.cpp deleted file mode 100644 index b1e4f0c35f..0000000000 --- a/simulation/halsim_gui/src/main/native/cpp/Field2D.cpp +++ /dev/null @@ -1,652 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2020 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#include "Field2D.h" - -#include - -#include - -#define IMGUI_DEFINE_MATH_OPERATORS -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "HALSimGui.h" -#include "SimDeviceGui.h" -#include "portable-file-dialogs.h" - -using namespace halsimgui; - -namespace gui = wpi::gui; - -namespace { - -// Per-frame field data (not persistent) -struct FieldFrameData { - // in window coordinates - ImVec2 imageMin; - ImVec2 imageMax; - ImVec2 min; - ImVec2 max; - - float scale; // scaling from field units to screen units -}; - -class FieldInfo { - public: - static constexpr float kDefaultWidth = 15.98f; - static constexpr float kDefaultHeight = 8.21f; - - std::unique_ptr m_fileOpener; - float m_width = kDefaultWidth; - float m_height = kDefaultHeight; - - void Reset(); - void LoadImage(); - void LoadJson(const wpi::Twine& jsonfile); - FieldFrameData GetFrameData() const; - void Draw(ImDrawList* drawList, const ImVec2& windowPos, - const FieldFrameData& frameData) const; - - bool ReadIni(wpi::StringRef name, wpi::StringRef value); - void WriteIni(ImGuiTextBuffer* out) const; - - private: - bool LoadImageImpl(const char* fn); - - std::string m_filename; - gui::Texture m_texture; - int m_imageWidth = 0; - int m_imageHeight = 0; - int m_top = 0; - int m_left = 0; - int m_bottom = -1; - int m_right = -1; -}; - -// Per-frame robot data (not persistent) -struct RobotFrameData { - // in window coordinates - ImVec2 center; - ImVec2 corners[4]; - ImVec2 arrow[3]; - - // scaled width/2 and length/2, in screen units - float width2; - float length2; -}; - -class RobotInfo { - public: - static constexpr float kDefaultWidth = 0.6858f; - static constexpr float kDefaultLength = 0.8204f; - - std::unique_ptr m_fileOpener; - float m_width = kDefaultWidth; - float m_length = kDefaultLength; - - void Reset(); - void LoadImage(); - void UpdateFromSimDevice(); - void SetPosition(double x, double y); - // set and get rotation in radians - void SetRotation(double rot); - double GetRotation() const { - return units::convert(m_rot); - } - RobotFrameData GetFrameData(const FieldFrameData& ffd) const; - void Draw(ImDrawList* drawList, const ImVec2& windowPos, - const RobotFrameData& frameData, int hit, float hitRadius) const; - - bool ReadIni(wpi::StringRef name, wpi::StringRef value); - void WriteIni(ImGuiTextBuffer* out) const; - - private: - bool LoadImageImpl(const char* fn); - - std::string m_filename; - gui::Texture m_texture; - - HAL_SimDeviceHandle m_devHandle = 0; - hal::SimDouble m_xHandle; - hal::SimDouble m_yHandle; - hal::SimDouble m_rotHandle; - - double m_x = 0; - double m_y = 0; - double m_rot = 0; -}; - -} // namespace - -static FieldInfo gField; -static RobotInfo gRobot; -static int gDragRobot = 0; -static ImVec2 gDragInitialOffset; -static double gDragInitialAngle; - -// read/write settings to ini file -static void* Field2DReadOpen(ImGuiContext* ctx, ImGuiSettingsHandler* handler, - const char* name) { - if (name == wpi::StringRef{"Field"}) return &gField; - if (name == wpi::StringRef{"Robot"}) return &gRobot; - return nullptr; -} - -static void Field2DReadLine(ImGuiContext* ctx, ImGuiSettingsHandler* handler, - void* entry, const char* lineStr) { - wpi::StringRef line{lineStr}; - auto [name, value] = line.split('='); - name = name.trim(); - value = value.trim(); - if (entry == &gField) - gField.ReadIni(name, value); - else if (entry == &gRobot) - gRobot.ReadIni(name, value); -} - -static void Field2DWriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, - ImGuiTextBuffer* out_buf) { - gField.WriteIni(out_buf); - gRobot.WriteIni(out_buf); -} - -void FieldInfo::Reset() { - m_texture = gui::Texture{}; - m_filename.clear(); - m_imageWidth = 0; - m_imageHeight = 0; - m_top = 0; - m_left = 0; - m_bottom = -1; - m_right = -1; -} - -void FieldInfo::LoadImage() { - if (m_fileOpener && m_fileOpener->ready(0)) { - auto result = m_fileOpener->result(); - if (!result.empty()) { - if (wpi::StringRef(result[0]).endswith(".json")) { - LoadJson(result[0]); - } else { - LoadImageImpl(result[0].c_str()); - m_top = 0; - m_left = 0; - m_bottom = -1; - m_right = -1; - } - } - m_fileOpener.reset(); - } - if (!m_texture && !m_filename.empty()) { - if (!LoadImageImpl(m_filename.c_str())) m_filename.clear(); - } -} - -void FieldInfo::LoadJson(const wpi::Twine& jsonfile) { - std::error_code ec; - wpi::raw_fd_istream f(jsonfile, ec); - if (ec) { - wpi::errs() << "GUI: could not open field JSON file\n"; - return; - } - - // parse file - wpi::json j; - try { - j = wpi::json::parse(f); - } catch (const wpi::json::parse_error& e) { - wpi::errs() << "GUI: JSON: could not parse: " << e.what() << '\n'; - } - - // top level must be an object - if (!j.is_object()) { - wpi::errs() << "GUI: JSON: does not contain a top object\n"; - return; - } - - // image filename - std::string image; - try { - image = j.at("field-image").get(); - } catch (const wpi::json::exception& e) { - wpi::errs() << "GUI: JSON: could not read field-image: " << e.what() - << '\n'; - return; - } - - // corners - int top, left, bottom, right; - try { - top = j.at("field-corners").at("top-left").at(1).get(); - left = j.at("field-corners").at("top-left").at(0).get(); - bottom = j.at("field-corners").at("bottom-right").at(1).get(); - right = j.at("field-corners").at("bottom-right").at(0).get(); - } catch (const wpi::json::exception& e) { - wpi::errs() << "GUI: JSON: could not read field-corners: " << e.what() - << '\n'; - return; - } - - // size - float width; - float height; - try { - width = j.at("field-size").at(0).get(); - height = j.at("field-size").at(1).get(); - } catch (const wpi::json::exception& e) { - wpi::errs() << "GUI: JSON: could not read field-size: " << e.what() << '\n'; - return; - } - - // units for size - std::string unit; - try { - unit = j.at("field-unit").get(); - } catch (const wpi::json::exception& e) { - wpi::errs() << "GUI: JSON: could not read field-unit: " << e.what() << '\n'; - return; - } - - // convert size units to meters - if (unit == "foot" || unit == "feet") { - width = units::convert(width); - height = units::convert(height); - } - - // the image filename is relative to the json file - wpi::SmallString<128> pathname; - jsonfile.toVector(pathname); - wpi::sys::path::remove_filename(pathname); - wpi::sys::path::append(pathname, image); - - // load field image - if (!LoadImageImpl(pathname.c_str())) return; - - // save to field info - m_filename = pathname.str(); - m_top = top; - m_left = left; - m_bottom = bottom; - m_right = right; - m_width = width; - m_height = height; -} - -bool FieldInfo::LoadImageImpl(const char* fn) { - wpi::outs() << "GUI: loading field image '" << fn << "'\n"; - auto texture = gui::Texture::CreateFromFile(fn); - if (!texture) { - wpi::errs() << "GUI: could not read field image\n"; - return false; - } - m_texture = std::move(texture); - m_imageWidth = m_texture.GetWidth(); - m_imageHeight = m_texture.GetHeight(); - m_filename = fn; - return true; -} - -FieldFrameData FieldInfo::GetFrameData() const { - FieldFrameData ffd; - - // get window content region - ffd.imageMin = ImGui::GetWindowContentRegionMin(); - ffd.imageMax = ImGui::GetWindowContentRegionMax(); - - // fit the image into the window - if (m_texture && m_imageHeight != 0 && m_imageWidth != 0) - gui::MaxFit(&ffd.imageMin, &ffd.imageMax, m_imageWidth, m_imageHeight); - - ImVec2 min = ffd.imageMin; - ImVec2 max = ffd.imageMax; - - // size down the box by the image corners (if any) - if (m_bottom > 0 && m_right > 0) { - min.x += m_left * (max.x - min.x) / m_imageWidth; - min.y += m_top * (max.y - min.y) / m_imageHeight; - max.x -= (m_imageWidth - m_right) * (max.x - min.x) / m_imageWidth; - max.y -= (m_imageHeight - m_bottom) * (max.y - min.y) / m_imageHeight; - } - - // draw the field "active area" as a yellow boundary box - gui::MaxFit(&min, &max, m_width, m_height); - - ffd.min = min; - ffd.max = max; - ffd.scale = (max.x - min.x) / m_width; - return ffd; -} - -void FieldInfo::Draw(ImDrawList* drawList, const ImVec2& windowPos, - const FieldFrameData& ffd) const { - if (m_texture && m_imageHeight != 0 && m_imageWidth != 0) { - drawList->AddImage(m_texture, windowPos + ffd.imageMin, - windowPos + ffd.imageMax); - } - - // draw the field "active area" as a yellow boundary box - drawList->AddRect(windowPos + ffd.min, windowPos + ffd.max, - IM_COL32(255, 255, 0, 255)); -} - -bool FieldInfo::ReadIni(wpi::StringRef name, wpi::StringRef value) { - if (name == "image") { - m_filename = value; - } else if (name == "top") { - int num; - if (value.getAsInteger(10, num)) return true; - m_top = num; - } else if (name == "left") { - int num; - if (value.getAsInteger(10, num)) return true; - m_left = num; - } else if (name == "bottom") { - int num; - if (value.getAsInteger(10, num)) return true; - m_bottom = num; - } else if (name == "right") { - int num; - if (value.getAsInteger(10, num)) return true; - m_right = num; - } else if (name == "width") { - std::sscanf(value.data(), "%f", &m_width); - } else if (name == "height") { - std::sscanf(value.data(), "%f", &m_height); - } else { - return false; - } - return true; -} - -void FieldInfo::WriteIni(ImGuiTextBuffer* out) const { - out->appendf( - "[Field2D][Field]\nimage=%s\ntop=%d\nleft=%d\nbottom=%d\nright=%d\nwidth=" - "%f\nheight=%f\n\n", - m_filename.c_str(), m_top, m_left, m_bottom, m_right, m_width, m_height); -} - -void RobotInfo::Reset() { - m_texture = gui::Texture{}; - m_filename.clear(); -} - -void RobotInfo::LoadImage() { - if (m_fileOpener && m_fileOpener->ready(0)) { - auto result = m_fileOpener->result(); - if (!result.empty()) LoadImageImpl(result[0].c_str()); - m_fileOpener.reset(); - } - if (!m_texture && !m_filename.empty()) { - if (!LoadImageImpl(m_filename.c_str())) m_filename.clear(); - } -} - -bool RobotInfo::LoadImageImpl(const char* fn) { - wpi::outs() << "GUI: loading robot image '" << fn << "'\n"; - auto texture = gui::Texture::CreateFromFile(fn); - if (!texture) { - wpi::errs() << "GUI: could not read robot image\n"; - return false; - } - m_texture = std::move(texture); - m_filename = fn; - return true; -} - -void RobotInfo::UpdateFromSimDevice() { - if (m_devHandle == 0) m_devHandle = HALSIM_GetSimDeviceHandle("Field2D"); - if (m_devHandle == 0) return; - - if (!m_xHandle) m_xHandle = HALSIM_GetSimValueHandle(m_devHandle, "x"); - if (m_xHandle) m_x = m_xHandle.Get(); - - if (!m_yHandle) m_yHandle = HALSIM_GetSimValueHandle(m_devHandle, "y"); - if (m_yHandle) m_y = m_yHandle.Get(); - - if (!m_rotHandle) m_rotHandle = HALSIM_GetSimValueHandle(m_devHandle, "rot"); - if (m_rotHandle) m_rot = m_rotHandle.Get(); -} - -void RobotInfo::SetPosition(double x, double y) { - m_x = x; - m_y = y; - if (m_xHandle) m_xHandle.Set(x); - if (m_yHandle) m_yHandle.Set(y); -} - -void RobotInfo::SetRotation(double rot) { - double rotDegrees = units::convert(rot); - // force to -180 to +180 range - rotDegrees = rotDegrees + std::ceil((-rotDegrees - 180) / 360) * 360; - m_rot = rotDegrees; - if (m_rotHandle) m_rotHandle.Set(rotDegrees); -} - -RobotFrameData RobotInfo::GetFrameData(const FieldFrameData& ffd) const { - RobotFrameData rfd; - float width2 = ffd.scale * m_width / 2; - float length2 = ffd.scale * m_length / 2; - - // (0,0) origin is bottom left - ImVec2 center(ffd.min.x + ffd.scale * m_x, ffd.max.y - ffd.scale * m_y); - - // build rotated points around center - double rot = GetRotation(); - float cos_a = std::cos(-rot); - float sin_a = std::sin(-rot); - - rfd.corners[0] = center + ImRotate(ImVec2(-length2, -width2), cos_a, sin_a); - rfd.corners[1] = center + ImRotate(ImVec2(length2, -width2), cos_a, sin_a); - rfd.corners[2] = center + ImRotate(ImVec2(length2, width2), cos_a, sin_a); - rfd.corners[3] = center + ImRotate(ImVec2(-length2, width2), cos_a, sin_a); - rfd.arrow[0] = - center + ImRotate(ImVec2(-length2 / 2, -width2 / 2), cos_a, sin_a); - rfd.arrow[1] = center + ImRotate(ImVec2(length2 / 2, 0), cos_a, sin_a); - rfd.arrow[2] = - center + ImRotate(ImVec2(-length2 / 2, width2 / 2), cos_a, sin_a); - - rfd.center = center; - rfd.width2 = width2; - rfd.length2 = length2; - return rfd; -} - -void RobotInfo::Draw(ImDrawList* drawList, const ImVec2& windowPos, - const RobotFrameData& rfd, int hit, - float hitRadius) const { - if (m_texture) { - drawList->AddImageQuad( - m_texture, windowPos + rfd.corners[0], windowPos + rfd.corners[1], - windowPos + rfd.corners[2], windowPos + rfd.corners[3]); - } else { - drawList->AddQuad(windowPos + rfd.corners[0], windowPos + rfd.corners[1], - windowPos + rfd.corners[2], windowPos + rfd.corners[3], - IM_COL32(255, 0, 0, 255), 4.0); - drawList->AddTriangle(windowPos + rfd.arrow[0], windowPos + rfd.arrow[1], - windowPos + rfd.arrow[2], IM_COL32(0, 255, 0, 255), - 4.0); - } - - if (hit > 0) { - if (hit == 1) { - drawList->AddCircle(windowPos + rfd.center, hitRadius, - IM_COL32(0, 255, 0, 255)); - } else { - drawList->AddCircle(windowPos + rfd.corners[hit - 2], hitRadius, - IM_COL32(0, 255, 0, 255)); - } - } -} - -bool RobotInfo::ReadIni(wpi::StringRef name, wpi::StringRef value) { - if (name == "image") { - m_filename = value; - } else if (name == "width") { - std::sscanf(value.data(), "%f", &m_width); - } else if (name == "length") { - std::sscanf(value.data(), "%f", &m_length); - } else { - return false; - } - return true; -} - -void RobotInfo::WriteIni(ImGuiTextBuffer* out) const { - out->appendf("[Field2D][Robot]\nimage=%s\nwidth=%f\nlength=%f\n\n", - m_filename.c_str(), m_width, m_length); -} - -static void OptionMenuField2D() { - if (ImGui::BeginMenu("2D Field View")) { - if (ImGui::MenuItem("Choose field image...")) { - gField.m_fileOpener = std::make_unique( - "Choose field image", "", - std::vector{"Image File", - "*.jpg *.jpeg *.png *.bmp *.psd *.tga *.gif " - "*.hdr *.pic *.ppm *.pgm", - "PathWeaver JSON File", "*.json"}); - } - if (ImGui::MenuItem("Reset field image")) { - gField.Reset(); - } - if (ImGui::MenuItem("Choose robot image...")) { - gRobot.m_fileOpener = std::make_unique( - "Choose robot image", "", - std::vector{"Image File", - "*.jpg *.jpeg *.png *.bmp *.psd *.tga *.gif " - "*.hdr *.pic *.ppm *.pgm"}); - } - if (ImGui::MenuItem("Reset robot image")) { - gRobot.Reset(); - } - ImGui::EndMenu(); - } -} - -static void DisplayField2DSettings() { - ImGui::PushItemWidth(ImGui::GetFontSize() * 4); - ImGui::InputFloat("Field Width", &gField.m_width); - ImGui::InputFloat("Field Height", &gField.m_height); - // ImGui::InputInt("Field Top", &gField.m_top); - // ImGui::InputInt("Field Left", &gField.m_left); - // ImGui::InputInt("Field Right", &gField.m_right); - // ImGui::InputInt("Field Bottom", &gField.m_bottom); - ImGui::InputFloat("Robot Width", &gRobot.m_width); - ImGui::InputFloat("Robot Length", &gRobot.m_length); - ImGui::PopItemWidth(); -} - -static void DisplayField2D() { - // load images - gField.LoadImage(); - gRobot.LoadImage(); - - // get robot coordinates from SimDevice - gRobot.UpdateFromSimDevice(); - - FieldFrameData ffd = gField.GetFrameData(); - RobotFrameData rfd = gRobot.GetFrameData(ffd); - - ImVec2 windowPos = ImGui::GetWindowPos(); - - // for dragging to work, there needs to be a button (otherwise the window is - // dragged) - ImVec2 contentSize = - ImGui::GetWindowContentRegionMax() - ImGui::GetWindowContentRegionMin(); - if (contentSize.x <= 0 || contentSize.y <= 0) return; - ImGui::InvisibleButton("field", contentSize); - - // allow dragging the robot around - ImVec2 cursor = ImGui::GetIO().MousePos - windowPos; - - int hit = 0; - float hitRadius = (std::min)(rfd.width2, rfd.length2) / 2; - // only allow initiation of dragging when invisible button is hovered; this - // prevents the window resize handles from simultaneously activating the drag - // functionality - if (ImGui::IsItemHovered()) { - float hitRadiusSquared = hitRadius * hitRadius; - // it's within the hit radius of the center? - if (gui::GetDistSquared(cursor, rfd.center) < hitRadiusSquared) - hit = 1; - else if (gui::GetDistSquared(cursor, rfd.corners[0]) < hitRadiusSquared) - hit = 2; - else if (gui::GetDistSquared(cursor, rfd.corners[1]) < hitRadiusSquared) - hit = 3; - else if (gui::GetDistSquared(cursor, rfd.corners[2]) < hitRadiusSquared) - hit = 4; - else if (gui::GetDistSquared(cursor, rfd.corners[3]) < hitRadiusSquared) - hit = 5; - if (hit > 0 && ImGui::IsMouseClicked(0)) { - if (hit == 1) { - gDragRobot = hit; - gDragInitialOffset = cursor - rfd.center; - } else { - gDragRobot = hit; - ImVec2 off = cursor - rfd.center; - gDragInitialAngle = std::atan2(off.y, off.x) + gRobot.GetRotation(); - } - } - } - - if (gDragRobot > 0 && ImGui::IsMouseDown(0)) { - if (gDragRobot == 1) { - ImVec2 newPos = cursor - gDragInitialOffset; - gRobot.SetPosition( - (std::clamp(newPos.x, ffd.min.x, ffd.max.x) - ffd.min.x) / ffd.scale, - (ffd.max.y - std::clamp(newPos.y, ffd.min.y, ffd.max.y)) / ffd.scale); - rfd = gRobot.GetFrameData(ffd); - } else { - ImVec2 off = cursor - rfd.center; - gRobot.SetRotation(gDragInitialAngle - std::atan2(off.y, off.x)); - } - hit = gDragRobot; // keep it highlighted - } else { - gDragRobot = 0; - } - - // draw - auto drawList = ImGui::GetWindowDrawList(); - gField.Draw(drawList, windowPos, ffd); - gRobot.Draw(drawList, windowPos, rfd, hit, hitRadius); -} - -void Field2D::Initialize() { - // hook ini handler to save settings - ImGuiSettingsHandler iniHandler; - iniHandler.TypeName = "Field2D"; - iniHandler.TypeHash = ImHashStr(iniHandler.TypeName); - iniHandler.ReadOpenFn = Field2DReadOpen; - iniHandler.ReadLineFn = Field2DReadLine; - iniHandler.WriteAllFn = Field2DWriteAll; - ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler); - - HALSimGui::AddOptionMenu(OptionMenuField2D); - - HALSimGui::AddWindow("2D Field Settings", DisplayField2DSettings, - ImGuiWindowFlags_AlwaysAutoResize); - HALSimGui::SetWindowVisibility("2D Field Settings", HALSimGui::kHide); - HALSimGui::SetDefaultWindowPos("2D Field Settings", 200, 150); - - HALSimGui::AddWindow("2D Field View", DisplayField2D); - HALSimGui::SetWindowVisibility("2D Field View", HALSimGui::kHide); - HALSimGui::SetDefaultWindowPos("2D Field View", 200, 200); - HALSimGui::SetDefaultWindowSize("2D Field View", 400, 200); - HALSimGui::SetWindowPadding("2D Field View", 0, 0); - - // SimDeviceGui::Hide("Field2D"); -} diff --git a/simulation/halsim_gui/src/main/native/cpp/GuiDataSource.cpp b/simulation/halsim_gui/src/main/native/cpp/GuiDataSource.cpp deleted file mode 100644 index df967c12f5..0000000000 --- a/simulation/halsim_gui/src/main/native/cpp/GuiDataSource.cpp +++ /dev/null @@ -1,116 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2020 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#include "GuiDataSource.h" - -#include - -using namespace halsimgui; - -wpi::sig::Signal GuiDataSource::sourceCreated; - -static wpi::StringMap gSources; - -GuiDataSource::GuiDataSource(const wpi::Twine& id) : m_id{id.str()} { - gSources.try_emplace(m_id, this); - sourceCreated(m_id.c_str(), this); -} - -GuiDataSource::GuiDataSource(const wpi::Twine& id, int index) - : GuiDataSource{id + wpi::Twine('[') + wpi::Twine(index) + - wpi::Twine(']')} {} - -GuiDataSource::GuiDataSource(const wpi::Twine& id, int index, int index2) - : GuiDataSource{id + wpi::Twine('[') + wpi::Twine(index) + wpi::Twine(',') + - wpi::Twine(index2) + wpi::Twine(']')} {} - -GuiDataSource::~GuiDataSource() { - auto it = gSources.find(m_id); - if (it == gSources.end()) return; - if (it->getValue() == this) gSources.erase(it); -} - -void GuiDataSource::LabelText(const char* label, const char* fmt, ...) const { - va_list args; - va_start(args, fmt); - LabelTextV(label, fmt, args); - va_end(args); -} - -// Add a label+text combo aligned to other label+value widgets -void GuiDataSource::LabelTextV(const char* label, const char* fmt, - va_list args) const { - ImGui::PushID(label); - ImGui::LabelTextV("##input", fmt, args); - ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x); - ImGui::Selectable(label); - ImGui::PopID(); - EmitDrag(); -} - -bool GuiDataSource::Combo(const char* label, int* current_item, - const char* const items[], int items_count, - int popup_max_height_in_items) const { - ImGui::PushID(label); - bool rv = ImGui::Combo("##input", current_item, items, items_count, - popup_max_height_in_items); - ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x); - ImGui::Selectable(label); - EmitDrag(); - ImGui::PopID(); - return rv; -} - -bool GuiDataSource::SliderFloat(const char* label, float* v, float v_min, - float v_max, const char* format, - float power) const { - ImGui::PushID(label); - bool rv = ImGui::SliderFloat("##input", v, v_min, v_max, format, power); - ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x); - ImGui::Selectable(label); - EmitDrag(); - ImGui::PopID(); - return rv; -} - -bool GuiDataSource::InputDouble(const char* label, double* v, double step, - double step_fast, const char* format, - ImGuiInputTextFlags flags) const { - ImGui::PushID(label); - bool rv = ImGui::InputDouble("##input", v, step, step_fast, format, flags); - ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x); - ImGui::Selectable(label); - EmitDrag(); - ImGui::PopID(); - return rv; -} - -bool GuiDataSource::InputInt(const char* label, int* v, int step, int step_fast, - ImGuiInputTextFlags flags) const { - ImGui::PushID(label); - bool rv = ImGui::InputInt("##input", v, step, step_fast, flags); - ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x); - ImGui::Selectable(label); - EmitDrag(); - ImGui::PopID(); - return rv; -} - -void GuiDataSource::EmitDrag(ImGuiDragDropFlags flags) const { - if (ImGui::BeginDragDropSource(flags)) { - auto self = this; - ImGui::SetDragDropPayload("DataSource", &self, sizeof(self)); - ImGui::TextUnformatted(m_name.empty() ? m_id.c_str() : m_name.c_str()); - ImGui::EndDragDropSource(); - } -} - -GuiDataSource* GuiDataSource::Find(wpi::StringRef id) { - auto it = gSources.find(id); - if (it == gSources.end()) return nullptr; - return it->getValue(); -} diff --git a/simulation/halsim_gui/src/main/native/cpp/HALProvider.cpp b/simulation/halsim_gui/src/main/native/cpp/HALProvider.cpp new file mode 100644 index 0000000000..ebda8938fb --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/HALProvider.cpp @@ -0,0 +1,91 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "HALProvider.h" + +#include + +#include +#include + +#include + +using namespace halsimgui; + +static bool gDisableOutputsOnDSDisable = true; + +bool HALProvider::AreOutputsDisabled() { + return gDisableOutputsOnDSDisable && !HALSIM_GetDriverStationEnabled(); +} + +void HALProvider::DisplayMenu() { + ImGui::MenuItem("Disable outputs on DS disable", nullptr, + &gDisableOutputsOnDSDisable, true); + ImGui::Separator(); + + for (auto&& viewEntry : m_viewEntries) { + bool visible = viewEntry->window && viewEntry->window->IsVisible(); + bool wasVisible = visible; + bool exists = viewEntry->modelEntry->exists(); + ImGui::MenuItem(viewEntry->name.c_str(), nullptr, &visible, + visible || exists); + if (!wasVisible && visible) { + Show(viewEntry.get(), viewEntry->window); + } else if (wasVisible && !visible && viewEntry->window) { + viewEntry->window->SetVisible(false); + } + } +} + +void HALProvider::Update() { + Provider::Update(); + + // check for visible windows that need displays (typically this is due to + // file loading) + for (auto&& window : m_windows) { + if (!window->IsVisible() || window->HasView()) continue; + auto id = window->GetId(); + auto it = FindViewEntry(id); + if (it == m_viewEntries.end() || (*it)->name != id) continue; + Show(it->get(), window.get()); + } +} + +glass::Model* HALProvider::GetModel(wpi::StringRef name) { + auto it = FindModelEntry(name); + if (it == m_modelEntries.end() || (*it)->name != name) return nullptr; + auto entry = it->get(); + + // get or create model + if (!entry->model) entry->model = entry->createModel(); + return entry->model.get(); +} + +void HALProvider::Show(ViewEntry* entry, glass::Window* window) { + // if there's already a window, just show it + if (entry->window) { + entry->window->SetVisible(true); + return; + } + + // get or create model + if (!entry->modelEntry->model) + entry->modelEntry->model = entry->modelEntry->createModel(); + if (!entry->modelEntry->model) return; + + // the window might exist and we're just not associated to it yet + if (!window) window = GetOrAddWindow(entry->name, true); + if (!window) return; + entry->window = window; + + // create view + auto view = entry->createView(window, entry->modelEntry->model.get()); + if (!view) return; + window->SetView(std::move(view)); + + entry->window->SetVisible(true); +} diff --git a/simulation/halsim_gui/src/main/native/cpp/HALSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/HALSimGui.cpp index 3d481b39d2..d02462bb5c 100644 --- a/simulation/halsim_gui/src/main/native/cpp/HALSimGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/HALSimGui.cpp @@ -7,337 +7,22 @@ #include "HALSimGui.h" -#include - -#include #include -#include -#include -#include #include using namespace halsimgui; -namespace gui = wpi::gui; +glass::MainMenuBar HALSimGui::mainMenu; +glass::WindowManager HALSimGui::manager{"SimWindow"}; +HALProvider HALSimGui::halProvider{"HALProvider"}; +glass::NetworkTablesProvider HALSimGui::ntProvider{"NTProvider"}; -namespace { -struct WindowInfo { - WindowInfo() = default; - explicit WindowInfo(const char* name_) : name{name_} {} - WindowInfo(const char* name_, std::function display_, - ImGuiWindowFlags flags_) - : name{name_}, display{std::move(display_)}, flags{flags_} {} +void HALSimGui::GlobalInit() { + manager.GlobalInit(); + halProvider.GlobalInit(); + ntProvider.GlobalInit(); - std::string name; - std::function display; - ImGuiWindowFlags flags = 0; - bool visible = true; - bool enabled = true; - ImGuiCond posCond = 0; - ImGuiCond sizeCond = 0; - ImVec2 pos; - ImVec2 size; - bool setPadding = false; - ImVec2 padding; -}; -} // namespace + wpi::gui::AddLateExecute([] { mainMenu.Display(); }); -static std::vector gWindows; -static wpi::StringMap gWindowMap; // index into gWindows -static std::vector gSortedWindows; // index into gWindows -static std::vector> gOptionMenus; -static std::vector> gMenus; -static bool gDisableOutputsOnDSDisable = true; - -// read/write open state to ini file -static void* SimWindowsReadOpen(ImGuiContext* ctx, - ImGuiSettingsHandler* handler, - const char* name) { - if (wpi::StringRef{name} == "GLOBAL") return &gDisableOutputsOnDSDisable; - - int index = gWindowMap.try_emplace(name, gWindows.size()).first->second; - if (index == static_cast(gWindows.size())) { - gSortedWindows.push_back(index); - gWindows.emplace_back(name); - std::sort(gSortedWindows.begin(), gSortedWindows.end(), - [](int a, int b) { return gWindows[a].name < gWindows[b].name; }); - } - return &gWindows[index]; + glass::AddStandardNetworkTablesViews(ntProvider); } - -static void SimWindowsReadLine(ImGuiContext* ctx, ImGuiSettingsHandler* handler, - void* entry, const char* lineStr) { - wpi::StringRef line{lineStr}; - auto [name, value] = line.split('='); - name = name.trim(); - value = value.trim(); - - if (entry == &gDisableOutputsOnDSDisable) { - int num; - if (value.getAsInteger(10, num)) return; - if (name == "disableOutputsOnDS") { - gDisableOutputsOnDSDisable = num; - } - return; - } - - auto element = static_cast(entry); - if (name == "visible") { - int num; - if (value.getAsInteger(10, num)) return; - element->visible = num; - } else if (name == "enabled") { - int num; - if (value.getAsInteger(10, num)) return; - element->enabled = num; - } -} - -static void SimWindowsWriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, - ImGuiTextBuffer* out_buf) { - out_buf->appendf("[SimWindow][GLOBAL]\ndisableOutputsOnDS=%d\n\n", - gDisableOutputsOnDSDisable ? 1 : 0); - for (auto&& window : gWindows) - out_buf->appendf("[SimWindow][%s]\nvisible=%d\nenabled=%d\n\n", - window.name.c_str(), window.visible ? 1 : 0, - window.enabled ? 1 : 0); -} - -void HALSimGui::Add(std::function initialize) { - gui::AddInit(std::move(initialize)); -} - -void HALSimGui::AddExecute(std::function execute) { - gui::AddEarlyExecute(std::move(execute)); -} - -void HALSimGui::AddWindow(const char* name, std::function display, - int flags) { - if (display) { - int index = gWindowMap.try_emplace(name, gWindows.size()).first->second; - if (index < static_cast(gWindows.size())) { - if (gWindows[index].display) { - wpi::errs() << "halsim_gui: ignoring duplicate window '" << name - << "'\n"; - } else { - gWindows[index].display = display; - gWindows[index].flags = flags; - } - return; - } - gSortedWindows.push_back(index); - gWindows.emplace_back(name, std::move(display), - static_cast(flags)); - std::sort(gSortedWindows.begin(), gSortedWindows.end(), - [](int a, int b) { return gWindows[a].name < gWindows[b].name; }); - } -} - -void HALSimGui::AddMainMenu(std::function menu) { - if (menu) gMenus.emplace_back(std::move(menu)); -} - -void HALSimGui::AddOptionMenu(std::function menu) { - if (menu) gOptionMenus.emplace_back(std::move(menu)); -} - -void HALSimGui::SetWindowVisibility(const char* name, - WindowVisibility visibility) { - auto it = gWindowMap.find(name); - if (it == gWindowMap.end()) return; - auto& window = gWindows[it->second]; - switch (visibility) { - case kHide: - window.visible = false; - window.enabled = true; - break; - case kShow: - window.visible = true; - window.enabled = true; - break; - case kDisabled: - window.enabled = false; - break; - } -} - -void HALSimGui::SetDefaultWindowPos(const char* name, float x, float y) { - auto it = gWindowMap.find(name); - if (it == gWindowMap.end()) return; - auto& window = gWindows[it->second]; - window.posCond = ImGuiCond_FirstUseEver; - window.pos = ImVec2{x, y}; -} - -void HALSimGui::SetDefaultWindowSize(const char* name, float width, - float height) { - auto it = gWindowMap.find(name); - if (it == gWindowMap.end()) return; - auto& window = gWindows[it->second]; - window.sizeCond = ImGuiCond_FirstUseEver; - window.size = ImVec2{width, height}; -} - -void HALSimGui::SetWindowPadding(const char* name, float x, float y) { - auto it = gWindowMap.find(name); - if (it == gWindowMap.end()) return; - auto& window = gWindows[it->second]; - window.setPadding = true; - window.padding = ImVec2{x, y}; -} - -bool HALSimGui::AreOutputsDisabled() { - return gDisableOutputsOnDSDisable && !HALSIM_GetDriverStationEnabled(); -} - -void HALSimGui::GlobalInit() { gui::CreateContext(); } - -bool HALSimGui::Initialize() { - gui::AddInit([] { - // Hook ini handler to save settings - ImGuiSettingsHandler iniHandler; - iniHandler.TypeName = "SimWindow"; - iniHandler.TypeHash = ImHashStr(iniHandler.TypeName); - iniHandler.ReadOpenFn = SimWindowsReadOpen; - iniHandler.ReadLineFn = SimWindowsReadLine; - iniHandler.WriteAllFn = SimWindowsWriteAll; - ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler); - }); - - gui::AddWindowScaler([](float windowScale) { - // scale default window positions - for (auto&& window : gWindows) { - if ((window.posCond & ImGuiCond_FirstUseEver) != 0) { - window.pos.x *= windowScale; - window.pos.y *= windowScale; - window.size.x *= windowScale; - window.size.y *= windowScale; - } - } - }); - - gui::AddLateExecute([] { - { - ImGui::BeginMainMenuBar(); - - if (ImGui::BeginMenu("Options")) { - ImGui::MenuItem("Disable outputs on DS disable", nullptr, - &gDisableOutputsOnDSDisable, true); - for (auto&& menu : gOptionMenus) { - if (menu) menu(); - } - ImGui::EndMenu(); - } - - gui::EmitViewMenu(); - - if (ImGui::BeginMenu("Window")) { - for (auto&& windowIndex : gSortedWindows) { - auto& window = gWindows[windowIndex]; - ImGui::MenuItem(window.name.c_str(), nullptr, &window.visible, - window.enabled); - } - ImGui::EndMenu(); - } - - for (auto&& menu : gMenus) { - if (menu) menu(); - } - -#if 0 - char str[64]; - std::snprintf(str, sizeof(str), "%.3f ms/frame (%.1f FPS)", - 1000.0f / ImGui::GetIO().Framerate, - ImGui::GetIO().Framerate); - ImGui::SameLine(ImGui::GetWindowWidth() - ImGui::CalcTextSize(str).x - - 10); - ImGui::Text("%s", str); -#endif - ImGui::EndMainMenuBar(); - } - - for (auto&& window : gWindows) { - if (window.display && window.visible && window.enabled) { - if (window.posCond != 0) - ImGui::SetNextWindowPos(window.pos, window.posCond); - if (window.sizeCond != 0) - ImGui::SetNextWindowSize(window.size, window.sizeCond); - if (window.setPadding) - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, window.padding); - if (ImGui::Begin(window.name.c_str(), &window.visible, window.flags)) - window.display(); - ImGui::End(); - if (window.setPadding) ImGui::PopStyleVar(); - } - } - }); - - if (!gui::Initialize("Simulation GUI", 1280, 720)) return false; - - return true; -} - -void HALSimGui::Main(void*) { - gui::Main(); - gui::DestroyContext(); -} - -void HALSimGui::Exit(void*) { gui::Exit(); } - -extern "C" { - -void HALSIMGUI_Add(void* param, void (*initialize)(void*)) { - if (initialize) { - HALSimGui::Add([=] { initialize(param); }); - } -} - -void HALSIMGUI_AddExecute(void* param, void (*execute)(void*)) { - if (execute) { - HALSimGui::AddExecute([=] { execute(param); }); - } -} - -void HALSIMGUI_AddWindow(const char* name, void* param, void (*display)(void*), - int32_t flags) { - if (display) { - HALSimGui::AddWindow( - name, [=] { display(param); }, flags); - } -} - -void HALSIMGUI_AddMainMenu(void* param, void (*menu)(void*)) { - if (menu) { - HALSimGui::AddMainMenu([=] { menu(param); }); - } -} - -void HALSIMGUI_AddOptionMenu(void* param, void (*menu)(void*)) { - if (menu) { - HALSimGui::AddOptionMenu([=] { menu(param); }); - } -} - -void HALSIMGUI_SetWindowVisibility(const char* name, int32_t visibility) { - HALSimGui::SetWindowVisibility( - name, static_cast(visibility)); -} - -void HALSIMGUI_SetDefaultWindowPos(const char* name, float x, float y) { - HALSimGui::SetDefaultWindowPos(name, x, y); -} - -void HALSIMGUI_SetDefaultWindowSize(const char* name, float width, - float height) { - HALSimGui::SetDefaultWindowSize(name, width, height); -} - -void HALSIMGUI_SetWindowPadding(const char* name, float x, float y) { - HALSimGui::SetDefaultWindowSize(name, x, y); -} - -int HALSIMGUI_AreOutputsDisabled(void) { - return HALSimGui::AreOutputsDisabled(); -} - -} // extern "C" diff --git a/simulation/halsim_gui/src/main/native/cpp/Mechanism2D.cpp b/simulation/halsim_gui/src/main/native/cpp/Mechanism2D.cpp index 8203325a0d..fb132d745c 100644 --- a/simulation/halsim_gui/src/main/native/cpp/Mechanism2D.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/Mechanism2D.cpp @@ -16,13 +16,13 @@ #define IMGUI_DEFINE_MATH_OPERATORS #include +#include #include #include #include #include #include "HALSimGui.h" -#include "portable-file-dialogs.h" using namespace halsimgui; @@ -276,17 +276,15 @@ static void readJson(std::string jFile) { } } -static void OptionMenuLocateJson() { - if (ImGui::BeginMenu("Mechanism2D")) { +static void DisplayAssembly2D() { + if (ImGui::BeginPopupContextItem()) { if (ImGui::MenuItem("Load Json")) { m_fileOpener = std::make_unique( "Choose Mechanism2D json", "", std::vector{"*.json"}); } - ImGui::EndMenu(); + ImGui::EndPopup(); } -} -static void DisplayAssembly2D() { GetJsonFileLocation(); if (!mechanism2DInfo.jsonLocation.empty()) { // Only read the json file if it changed @@ -313,10 +311,11 @@ void Mechanism2D::Initialize() { ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler); buildColorTable(); - HALSimGui::AddWindow("Mechanism 2D", DisplayAssembly2D); - HALSimGui::SetWindowVisibility("Mechanism 2D", HALSimGui::kHide); - HALSimGui::AddOptionMenu(OptionMenuLocateJson); - HALSimGui::SetDefaultWindowPos("Mechanism 2D", 200, 200); - HALSimGui::SetDefaultWindowSize("Mechanism 2D", 600, 600); - HALSimGui::SetWindowPadding("Mechanism 2D", 0, 0); + if (auto win = + HALSimGui::manager.AddWindow("Mechanism 2D", DisplayAssembly2D)) { + win->SetVisibility(glass::Window::kHide); + win->SetDefaultPos(200, 200); + win->SetDefaultSize(600, 600); + win->SetPadding(0, 0); + } } diff --git a/simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.cpp b/simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.cpp deleted file mode 100644 index 2d442b58c4..0000000000 --- a/simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.cpp +++ /dev/null @@ -1,409 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2020 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#include "NetworkTablesGui.h" - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "GuiDataSource.h" -#include "HALSimGui.h" - -using namespace halsimgui; - -static NT_EntryListenerPoller gNetworkTablesPoller; -static wpi::DenseMap> - gNetworkTableSources; - -static void UpdateNetworkTableSources() { - bool timedOut = false; - for (auto&& event : - nt::PollEntryListener(gNetworkTablesPoller, 0, &timedOut)) { - if (!event.value->IsBoolean() && !event.value->IsDouble()) continue; - if (event.flags & NT_NOTIFY_NEW) { - auto& source = gNetworkTableSources[event.entry]; - if (!source) - source = - std::make_unique(wpi::Twine{"NT:"} + event.name); - } - if (event.flags & NT_NOTIFY_DELETE) { - if (auto& source = gNetworkTableSources[event.entry]) source.reset(); - } - if (event.flags & (NT_NOTIFY_NEW | NT_NOTIFY_UPDATE)) { - if (auto& source = gNetworkTableSources[event.entry]) { - if (event.value->IsBoolean()) { - source->SetValue(event.value->GetBoolean() ? 1 : 0); - source->SetDigital(true); - } else if (event.value->IsDouble()) { - source->SetValue(event.value->GetDouble()); - source->SetDigital(false); - } - } - } - } -} - -static void BooleanArrayToString(wpi::SmallVectorImpl& out, - wpi::ArrayRef in) { - out.clear(); - wpi::raw_svector_ostream os{out}; - os << '['; - bool first = true; - for (auto v : in) { - if (!first) os << ','; - first = false; - if (v) - os << "true"; - else - os << "false"; - } - os << ']'; -} - -static std::shared_ptr StringToBooleanArray(wpi::StringRef in) { - in = in.trim(); - if (in.empty()) - return nt::NetworkTableValue::MakeBooleanArray( - std::initializer_list{}); - if (in.front() == '[') in = in.drop_front(); - if (in.back() == ']') in = in.drop_back(); - in = in.trim(); - - wpi::SmallVector inSplit; - wpi::SmallVector out; - - in.split(inSplit, ',', -1, false); - for (auto val : inSplit) { - val = val.trim(); - if (val.equals_lower("true")) { - out.emplace_back(1); - } else if (val.equals_lower("false")) { - out.emplace_back(0); - } else { - wpi::errs() << "GUI: NetworkTables: Could not understand value '" << val - << "'\n"; - return nullptr; - } - } - - return nt::NetworkTableValue::MakeBooleanArray(out); -} - -static void DoubleArrayToString(wpi::SmallVectorImpl& out, - wpi::ArrayRef in) { - out.clear(); - wpi::raw_svector_ostream os{out}; - os << '['; - bool first = true; - for (auto v : in) { - if (!first) os << ','; - first = false; - os << wpi::format("%.6f", v); - } - os << ']'; -} - -static std::shared_ptr StringToDoubleArray(wpi::StringRef in) { - in = in.trim(); - if (in.empty()) - return nt::NetworkTableValue::MakeBooleanArray( - std::initializer_list{}); - if (in.front() == '[') in = in.drop_front(); - if (in.back() == ']') in = in.drop_back(); - in = in.trim(); - - wpi::SmallVector inSplit; - wpi::SmallVector out; - - in.split(inSplit, ',', -1, false); - for (auto val : inSplit) { - val = val.trim(); - wpi::SmallString<32> valStr = val; - double d; - if (std::sscanf(valStr.c_str(), "%lf", &d) == 1) { - out.emplace_back(d); - } else { - wpi::errs() << "GUI: NetworkTables: Could not understand value '" << val - << "'\n"; - return nullptr; - } - } - - return nt::NetworkTableValue::MakeDoubleArray(out); -} - -static void StringArrayToString(wpi::SmallVectorImpl& out, - wpi::ArrayRef in) { - out.clear(); - wpi::raw_svector_ostream os{out}; - os << '['; - bool first = true; - for (auto&& v : in) { - if (!first) os << ','; - first = false; - os << '"'; - os.write_escaped(v); - os << '"'; - } - os << ']'; -} - -static int fromxdigit(char ch) { - if (ch >= 'a' && ch <= 'f') - return (ch - 'a' + 10); - else if (ch >= 'A' && ch <= 'F') - return (ch - 'A' + 10); - else - return ch - '0'; -} - -static wpi::StringRef UnescapeString(wpi::StringRef source, - wpi::SmallVectorImpl& buf) { - assert(source.size() >= 2 && source.front() == '"' && source.back() == '"'); - buf.clear(); - buf.reserve(source.size() - 2); - for (auto s = source.begin() + 1, end = source.end() - 1; s != end; ++s) { - if (*s != '\\') { - buf.push_back(*s); - continue; - } - switch (*++s) { - case 't': - buf.push_back('\t'); - break; - case 'n': - buf.push_back('\n'); - break; - case 'x': { - if (!isxdigit(*(s + 1))) { - buf.push_back('x'); // treat it like a unknown escape - break; - } - int ch = fromxdigit(*++s); - if (std::isxdigit(*(s + 1))) { - ch <<= 4; - ch |= fromxdigit(*++s); - } - buf.push_back(static_cast(ch)); - break; - } - default: - buf.push_back(*s); - break; - } - } - return wpi::StringRef{buf.data(), buf.size()}; -} - -static std::shared_ptr StringToStringArray(wpi::StringRef in) { - in = in.trim(); - if (in.empty()) - return nt::NetworkTableValue::MakeStringArray( - std::initializer_list{}); - if (in.front() == '[') in = in.drop_front(); - if (in.back() == ']') in = in.drop_back(); - in = in.trim(); - - wpi::SmallVector inSplit; - std::vector out; - wpi::SmallString<32> buf; - - in.split(inSplit, ',', -1, false); - for (auto val : inSplit) { - val = val.trim(); - if (val.empty()) continue; - if (val.front() != '"' || val.back() != '"') { - wpi::errs() << "GUI: NetworkTables: Could not understand value '" << val - << "'\n"; - return nullptr; - } - out.emplace_back(UnescapeString(val, buf)); - } - - return nt::NetworkTableValue::MakeStringArray(std::move(out)); -} - -static constexpr size_t kTextBufferSize = 4096; - -static char* GetTextBuffer(wpi::StringRef in) { - static char textBuffer[kTextBufferSize]; - size_t len = (std::min)(in.size(), kTextBufferSize - 1); - std::memcpy(textBuffer, in.data(), len); - textBuffer[len] = '\0'; - return textBuffer; -} - -static void DisplayNetworkTables() { - static auto inst = nt::NetworkTableInstance::GetDefault(); - - if (ImGui::CollapsingHeader("Connections")) { - ImGui::Columns(4, "connections"); - ImGui::Text("Id"); - ImGui::NextColumn(); - ImGui::Text("Address"); - ImGui::NextColumn(); - ImGui::Text("Updated"); - ImGui::NextColumn(); - ImGui::Text("Proto"); - ImGui::NextColumn(); - ImGui::Separator(); - for (auto&& i : inst.GetConnections()) { - ImGui::Text("%s", i.remote_id.c_str()); - ImGui::NextColumn(); - ImGui::Text("%s", i.remote_ip.c_str()); - ImGui::NextColumn(); - ImGui::Text("%llu", - static_cast( // NOLINT(runtime/int) - i.last_update)); - ImGui::NextColumn(); - ImGui::Text("%d.%d", i.protocol_version >> 8, i.protocol_version & 0xff); - ImGui::NextColumn(); - } - ImGui::Columns(); - } - - if (ImGui::CollapsingHeader("Values", ImGuiTreeNodeFlags_DefaultOpen)) { - static bool first = true; - ImGui::Columns(4, "values"); - if (first) ImGui::SetColumnWidth(-1, 0.5f * ImGui::GetWindowWidth()); - ImGui::Text("Name"); - ImGui::NextColumn(); - ImGui::Text("Value"); - ImGui::NextColumn(); - if (first) ImGui::SetColumnWidth(-1, 12 * ImGui::GetFontSize()); - ImGui::Text("Flags"); - ImGui::NextColumn(); - ImGui::Text("Changed"); - ImGui::NextColumn(); - ImGui::Separator(); - first = false; - - auto info = inst.GetEntryInfo("", 0); - std::sort(info.begin(), info.end(), - [](const auto& a, const auto& b) { return a.name < b.name; }); - - for (auto&& i : info) { - if (auto source = gNetworkTableSources[i.entry].get()) { - ImGui::Selectable(i.name.c_str()); - source->EmitDrag(); - } else { - ImGui::Text("%s", i.name.c_str()); - } - ImGui::NextColumn(); - - if (auto val = nt::GetEntryValue(i.entry)) { - ImGui::PushID(i.name.c_str()); - switch (val->type()) { - case NT_BOOLEAN: { - static const char* boolOptions[] = {"false", "true"}; - int v = val->GetBoolean() ? 1 : 0; - if (ImGui::Combo("boolean", &v, boolOptions, 2)) - nt::SetEntryValue(i.entry, nt::NetworkTableValue::MakeBoolean(v)); - break; - } - case NT_DOUBLE: { - double v = val->GetDouble(); - if (ImGui::InputDouble("double", &v, 0, 0, "%.6f", - ImGuiInputTextFlags_EnterReturnsTrue)) - nt::SetEntryValue(i.entry, nt::NetworkTableValue::MakeDouble(v)); - break; - } - case NT_STRING: { - char* v = GetTextBuffer(val->GetString()); - if (ImGui::InputText("string", v, kTextBufferSize, - ImGuiInputTextFlags_EnterReturnsTrue)) - nt::SetEntryValue(i.entry, nt::NetworkTableValue::MakeString(v)); - break; - } - case NT_BOOLEAN_ARRAY: { - wpi::SmallString<64> buf; - BooleanArrayToString(buf, val->GetBooleanArray()); - char* v = GetTextBuffer(buf); - if (ImGui::InputText("boolean[]", v, kTextBufferSize, - ImGuiInputTextFlags_EnterReturnsTrue)) { - if (auto outv = StringToBooleanArray(v)) - nt::SetEntryValue(i.entry, std::move(outv)); - } - break; - } - case NT_DOUBLE_ARRAY: { - wpi::SmallString<64> buf; - DoubleArrayToString(buf, val->GetDoubleArray()); - char* v = GetTextBuffer(buf); - if (ImGui::InputText("double[]", v, kTextBufferSize, - ImGuiInputTextFlags_EnterReturnsTrue)) { - if (auto outv = StringToDoubleArray(v)) - nt::SetEntryValue(i.entry, std::move(outv)); - } - break; - } - case NT_STRING_ARRAY: { - wpi::SmallString<64> buf; - StringArrayToString(buf, val->GetStringArray()); - char* v = GetTextBuffer(buf); - if (ImGui::InputText("string[]", v, kTextBufferSize, - ImGuiInputTextFlags_EnterReturnsTrue)) { - if (auto outv = StringToStringArray(v)) - nt::SetEntryValue(i.entry, std::move(outv)); - } - break; - } - case NT_RAW: - ImGui::LabelText("raw", "[...]"); - break; - case NT_RPC: - ImGui::LabelText("rpc", "[...]"); - break; - default: - ImGui::LabelText("other", "?"); - break; - } - ImGui::PopID(); - } - ImGui::NextColumn(); - - if ((i.flags & NT_PERSISTENT) != 0) - ImGui::Text("Persistent"); - else if (i.flags != 0) - ImGui::Text("%02x", i.flags); - ImGui::NextColumn(); - - ImGui::Text("%llu", - static_cast( // NOLINT(runtime/int) - i.last_change)); - ImGui::NextColumn(); - ImGui::Separator(); - } - ImGui::Columns(); - } -} - -void NetworkTablesGui::Initialize() { - gNetworkTablesPoller = - nt::CreateEntryListenerPoller(nt::GetDefaultInstance()); - nt::AddPolledEntryListener(gNetworkTablesPoller, "", - NT_NOTIFY_LOCAL | NT_NOTIFY_NEW | - NT_NOTIFY_UPDATE | NT_NOTIFY_DELETE | - NT_NOTIFY_IMMEDIATE); - HALSimGui::AddExecute(UpdateNetworkTableSources); - HALSimGui::AddWindow("NetworkTables", DisplayNetworkTables); - HALSimGui::SetDefaultWindowPos("NetworkTables", 250, 277); - HALSimGui::SetDefaultWindowSize("NetworkTables", 750, 185); -} diff --git a/simulation/halsim_gui/src/main/native/cpp/NetworkTablesSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/NetworkTablesSimGui.cpp new file mode 100644 index 0000000000..28c7f8f13c --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/NetworkTablesSimGui.cpp @@ -0,0 +1,40 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "NetworkTablesSimGui.h" + +#include + +#include + +#include "HALSimGui.h" + +using namespace halsimgui; + +static std::unique_ptr gNetworkTablesModel; +static std::unique_ptr gNetworkTablesView; +static glass::Window* gNetworkTablesWindow; + +void NetworkTablesSimGui::Initialize() { + gNetworkTablesModel = std::make_unique(); + gNetworkTablesView = + std::make_unique(gNetworkTablesModel.get()); + wpi::gui::AddEarlyExecute([] { gNetworkTablesModel->Update(); }); + gNetworkTablesWindow = HALSimGui::ntProvider.AddWindow( + "NetworkTables", [] { gNetworkTablesView->Display(); }); + if (gNetworkTablesWindow) { + gNetworkTablesWindow->SetDefaultPos(250, 277); + gNetworkTablesWindow->SetDefaultSize(750, 185); + gNetworkTablesWindow->DisableRenamePopup(); + } +} + +void NetworkTablesSimGui::DisplayMenu() { + if (gNetworkTablesWindow) { + gNetworkTablesWindow->DisplayMenuItem("NetworkTables View"); + } +} diff --git a/simulation/halsim_gui/src/main/native/cpp/Field2D.h b/simulation/halsim_gui/src/main/native/cpp/NetworkTablesSimGui.h similarity index 91% rename from simulation/halsim_gui/src/main/native/cpp/Field2D.h rename to simulation/halsim_gui/src/main/native/cpp/NetworkTablesSimGui.h index 52218f3878..b4553fe0c1 100644 --- a/simulation/halsim_gui/src/main/native/cpp/Field2D.h +++ b/simulation/halsim_gui/src/main/native/cpp/NetworkTablesSimGui.h @@ -9,9 +9,10 @@ namespace halsimgui { -class Field2D { +class NetworkTablesSimGui { public: static void Initialize(); + static void DisplayMenu(); }; } // namespace halsimgui diff --git a/simulation/halsim_gui/src/main/native/cpp/PCMSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/PCMSimGui.cpp new file mode 100644 index 0000000000..2d9c80afe1 --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/PCMSimGui.cpp @@ -0,0 +1,234 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "PCMSimGui.h" + +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include "HALDataSource.h" +#include "HALSimGui.h" +#include "SimDeviceGui.h" + +using namespace halsimgui; + +namespace { +HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(PCMCompressorOn, "Compressor On"); +HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(PCMClosedLoopEnabled, "Closed Loop"); +HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(PCMPressureSwitch, "Pressure Switch"); +HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(PCMCompressorCurrent, "Comp Current"); +HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED2(PCMSolenoidOutput, "Solenoid"); + +class CompressorSimModel : public glass::CompressorModel { + public: + explicit CompressorSimModel(int32_t index) + : m_index{index}, + m_running{index}, + m_enabled{index}, + m_pressureSwitch{index}, + m_current{index} {} + + void Update() override {} + + bool Exists() override { return HALSIM_GetPCMCompressorInitialized(m_index); } + + glass::DataSource* GetRunningData() override { return &m_running; } + glass::DataSource* GetEnabledData() override { return &m_enabled; } + glass::DataSource* GetPressureSwitchData() override { + return &m_pressureSwitch; + } + glass::DataSource* GetCurrentData() override { return &m_current; } + + void SetRunning(bool val) override { + HALSIM_SetPCMCompressorOn(m_index, val); + } + void SetEnabled(bool val) override { + HALSIM_SetPCMClosedLoopEnabled(m_index, val); + } + void SetPressureSwitch(bool val) override { + HALSIM_SetPCMPressureSwitch(m_index, val); + } + void SetCurrent(double val) override { + HALSIM_SetPCMCompressorCurrent(m_index, val); + } + + private: + int32_t m_index; + PCMCompressorOnSource m_running; + PCMClosedLoopEnabledSource m_enabled; + PCMPressureSwitchSource m_pressureSwitch; + PCMCompressorCurrentSource m_current; +}; + +class SolenoidSimModel : public glass::SolenoidModel { + public: + SolenoidSimModel(int32_t index, int32_t channel) + : m_index{index}, m_channel{channel}, m_output{index, channel} {} + + void Update() override {} + + bool Exists() override { + return HALSIM_GetPCMSolenoidInitialized(m_index, m_channel); + } + + glass::DataSource* GetOutputData() override { return &m_output; } + + void SetOutput(bool val) override { + HALSIM_SetPCMSolenoidOutput(m_index, m_channel, val); + } + + private: + int32_t m_index; + int32_t m_channel; + PCMSolenoidOutputSource m_output; +}; + +class PCMSimModel : public glass::PCMModel { + public: + explicit PCMSimModel(int32_t index) + : m_index{index}, + m_compressor{index}, + m_solenoids(HAL_GetNumSolenoidChannels()) {} + + void Update() override; + + bool Exists() override { return true; } + + CompressorSimModel* GetCompressor() override { return &m_compressor; } + + void ForEachSolenoid( + wpi::function_ref func) + override; + + int GetNumSolenoids() const { return m_solenoidInitCount; } + + private: + int32_t m_index; + CompressorSimModel m_compressor; + std::vector> m_solenoids; + int m_solenoidInitCount = 0; +}; + +class PCMsSimModel : public glass::PCMsModel { + public: + PCMsSimModel() : m_models(HAL_GetNumPCMModules()) {} + + void Update() override; + + bool Exists() override { return true; } + + void ForEachPCM( + wpi::function_ref func) override; + + private: + std::vector> m_models; +}; +} // namespace + +void PCMSimModel::Update() { + int32_t numChannels = m_solenoids.size(); + m_solenoidInitCount = 0; + for (int32_t i = 0; i < numChannels; ++i) { + auto& model = m_solenoids[i]; + if (HALSIM_GetPCMSolenoidInitialized(m_index, i)) { + if (!model) { + model = std::make_unique(m_index, i); + } + ++m_solenoidInitCount; + } else { + model.reset(); + } + } +} + +void PCMSimModel::ForEachSolenoid( + wpi::function_ref func) { + if (m_solenoidInitCount == 0) return; + int32_t numSolenoids = m_solenoids.size(); + for (int32_t i = 0; i < numSolenoids; ++i) { + if (auto model = m_solenoids[i].get()) { + func(*model, i); + } + } +} + +void PCMsSimModel::Update() { + for (int32_t i = 0, iend = static_cast(m_models.size()); i < iend; + ++i) { + auto& model = m_models[i]; + if (HALSIM_GetPCMCompressorInitialized(i) || + HALSIM_GetPCMAnySolenoidInitialized(i)) { + if (!model) { + model = std::make_unique(i); + } + model->Update(); + } else { + model.reset(); + } + } +} + +void PCMsSimModel::ForEachPCM( + wpi::function_ref func) { + int32_t numPCMs = m_models.size(); + for (int32_t i = 0; i < numPCMs; ++i) { + if (auto model = m_models[i].get()) { + func(*model, i); + } + } +} + +static bool PCMsAnyInitialized() { + static const int32_t num = HAL_GetNumPCMModules(); + for (int32_t i = 0; i < num; ++i) { + if (HALSIM_GetPCMCompressorInitialized(i) || + HALSIM_GetPCMAnySolenoidInitialized(i)) + return true; + } + return false; +} + +void PCMSimGui::Initialize() { + HALSimGui::halProvider.RegisterModel("PCMs", PCMsAnyInitialized, [] { + return std::make_unique(); + }); + HALSimGui::halProvider.RegisterView( + "Solenoids", "PCMs", + [](glass::Model* model) { + bool any = false; + static_cast(model)->ForEachPCM( + [&](glass::PCMModel& pcm, int) { + if (static_cast(&pcm)->GetNumSolenoids() > 0) + any = true; + }); + return any; + }, + [](glass::Window* win, glass::Model* model) { + win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize); + win->SetDefaultPos(290, 20); + return glass::MakeFunctionView([=] { + glass::DisplayPCMsSolenoids( + static_cast(model), + HALSimGui::halProvider.AreOutputsEnabled()); + }); + }); + + SimDeviceGui::GetDeviceTree().Add( + HALSimGui::halProvider.GetModel("PCMs"), [](glass::Model* model) { + glass::DisplayCompressorsDevice( + static_cast(model), + HALSimGui::halProvider.AreOutputsEnabled()); + }); +} diff --git a/simulation/halsim_gui/src/main/native/cpp/DIOGui.h b/simulation/halsim_gui/src/main/native/cpp/PCMSimGui.h similarity index 86% rename from simulation/halsim_gui/src/main/native/cpp/DIOGui.h rename to simulation/halsim_gui/src/main/native/cpp/PCMSimGui.h index 70181e37a0..9c6ddeed9f 100644 --- a/simulation/halsim_gui/src/main/native/cpp/DIOGui.h +++ b/simulation/halsim_gui/src/main/native/cpp/PCMSimGui.h @@ -1,5 +1,5 @@ /*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019 FIRST. All Rights Reserved. */ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ /* Open Source Software - may be modified and shared by FRC teams. The code */ /* must be accompanied by the FIRST BSD license file in the root directory of */ /* the project. */ @@ -9,7 +9,7 @@ namespace halsimgui { -class DIOGui { +class PCMSimGui { public: static void Initialize(); }; diff --git a/simulation/halsim_gui/src/main/native/cpp/PDPGui.cpp b/simulation/halsim_gui/src/main/native/cpp/PDPGui.cpp deleted file mode 100644 index 35ba49213f..0000000000 --- a/simulation/halsim_gui/src/main/native/cpp/PDPGui.cpp +++ /dev/null @@ -1,141 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#include "PDPGui.h" - -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "GuiDataSource.h" -#include "HALSimGui.h" -#include "IniSaver.h" -#include "IniSaverInfo.h" - -using namespace halsimgui; - -namespace { -HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(PDPTemperature, "PDP Temp"); -HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(PDPVoltage, "PDP Voltage"); -HALSIMGUI_DATASOURCE_DOUBLE_INDEXED2(PDPCurrent, "PDP Current"); -struct PDPSource { - explicit PDPSource(int32_t index) : temp{index}, voltage{index} { - const int numChannels = HAL_GetNumPDPChannels(); - currents.reserve(numChannels); - for (int i = 0; i < numChannels; ++i) - currents.emplace_back(std::make_unique(index, i)); - } - PDPTemperatureSource temp; - PDPVoltageSource voltage; - std::vector> currents; -}; -} // namespace - -static IniSaver gChannels{"PDP"}; -static std::vector> gPDPSources; - -static void UpdatePDPSources() { - for (int i = 0, iend = gPDPSources.size(); i < iend; ++i) { - auto& source = gPDPSources[i]; - if (HALSIM_GetPDPInitialized(i)) { - if (!source) { - source = std::make_unique(i); - } - } else { - source.reset(); - } - } -} - -static void DisplayPDP() { - bool hasAny = false; - for (int i = 0, iend = gPDPSources.size(); i < iend; ++i) { - if (auto source = gPDPSources[i].get()) { - hasAny = true; - - char name[128]; - std::snprintf(name, sizeof(name), "PDP[%d]", i); - if (ImGui::CollapsingHeader(name, ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::PushID(i); - - // temperature - double temp = source->temp.GetValue(); - ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4); - if (source->temp.InputDouble("Temp", &temp, 0, 0, "%.3f")) - HALSIM_SetPDPTemperature(i, temp); - - // voltage - double volts = source->voltage.GetValue(); - ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4); - if (source->voltage.InputDouble("Voltage", &volts, 0, 0, "%.3f")) - HALSIM_SetPDPVoltage(i, volts); - - // channel currents; show as two columns laid out like PDP - const int numChannels = source->currents.size(); - ImGui::Text("Channel Current (A)"); - ImGui::Columns(2, "channels", false); - float maxWidth = ImGui::GetFontSize() * 13; - for (int left = 0, right = numChannels - 1; left < right; - ++left, --right) { - double val; - - ImGui::PushID(left); - auto& leftInfo = gChannels[i * numChannels + left]; - leftInfo.GetLabel(name, sizeof(name), "", left); - val = source->currents[left]->GetValue(); - ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4); - if (source->currents[left]->InputDouble(name, &val, 0, 0, "%.3f")) - HALSIM_SetPDPCurrent(i, left, val); - float leftWidth = ImGui::GetItemRectSize().x; - if (leftInfo.PopupEditName(left)) { - source->currents[left]->SetName(leftInfo.GetName()); - } - ImGui::PopID(); - ImGui::NextColumn(); - - ImGui::PushID(right); - auto& rightInfo = gChannels[i * numChannels + right]; - rightInfo.GetLabel(name, sizeof(name), "", right); - val = source->currents[right]->GetValue(); - ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4); - if (source->currents[right]->InputDouble(name, &val, 0, 0, "%.3f")) - HALSIM_SetPDPCurrent(i, right, val); - float rightWidth = ImGui::GetItemRectSize().x; - if (rightInfo.PopupEditName(right)) { - source->currents[right]->SetName(rightInfo.GetName()); - } - ImGui::PopID(); - ImGui::NextColumn(); - - float width = - (std::max)(leftWidth, rightWidth) * 2 + ImGui::GetFontSize() * 4; - if (width > maxWidth) maxWidth = width; - } - ImGui::Columns(1); - ImGui::Dummy(ImVec2(maxWidth, 0)); - ImGui::PopID(); - } - } - } - if (!hasAny) ImGui::Text("No PDPs"); -} - -void PDPGui::Initialize() { - gChannels.Initialize(); - gPDPSources.resize(HAL_GetNumPDPModules()); - HALSimGui::AddExecute(UpdatePDPSources); - HALSimGui::AddWindow("PDP", DisplayPDP); - // hide it by default - HALSimGui::SetWindowVisibility("PDP", HALSimGui::kHide); - HALSimGui::SetDefaultWindowPos("PDP", 245, 155); -} diff --git a/simulation/halsim_gui/src/main/native/cpp/PDPSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/PDPSimGui.cpp new file mode 100644 index 0000000000..fe4480984e --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/PDPSimGui.cpp @@ -0,0 +1,124 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "PDPSimGui.h" + +#include + +#include +#include +#include +#include + +#include +#include + +#include "HALDataSource.h" +#include "HALSimGui.h" + +using namespace halsimgui; + +namespace { +HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(PDPTemperature, "PDP Temp"); +HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(PDPVoltage, "PDP Voltage"); +HALSIMGUI_DATASOURCE_DOUBLE_INDEXED2(PDPCurrent, "PDP Current"); + +class PDPSimModel : public glass::PDPModel { + public: + explicit PDPSimModel(int32_t index) + : m_index{index}, m_temp{index}, m_voltage{index} { + const int numChannels = HAL_GetNumPDPChannels(); + m_currents.reserve(numChannels); + for (int i = 0; i < numChannels; ++i) + m_currents.emplace_back(std::make_unique(index, i)); + } + + void Update() override {} + + bool Exists() override { return HALSIM_GetPDPInitialized(m_index); } + + int GetNumChannels() const override { return m_currents.size(); } + + glass::DataSource* GetTemperatureData() override { return &m_temp; } + glass::DataSource* GetVoltageData() override { return &m_voltage; } + glass::DataSource* GetCurrentData(int channel) override { + return m_currents[channel].get(); + } + + void SetTemperature(double val) override { + HALSIM_SetPDPTemperature(m_index, val); + } + void SetVoltage(double val) override { HALSIM_SetPDPVoltage(m_index, val); } + void SetCurrent(int channel, double val) override { + HALSIM_SetPDPCurrent(m_index, channel, val); + } + + private: + int32_t m_index; + PDPTemperatureSource m_temp; + PDPVoltageSource m_voltage; + std::vector> m_currents; +}; + +class PDPsSimModel : public glass::PDPsModel { + public: + PDPsSimModel() : m_models(HAL_GetNumPDPModules()) {} + + void Update() override; + + bool Exists() override { return true; } + + void ForEachPDP( + wpi::function_ref func) override; + + private: + std::vector> m_models; +}; +} // namespace + +void PDPsSimModel::Update() { + for (int32_t i = 0, iend = static_cast(m_models.size()); i < iend; + ++i) { + auto& model = m_models[i]; + if (HALSIM_GetPDPInitialized(i)) { + if (!model) { + model = std::make_unique(i); + } + } else { + model.reset(); + } + } +} + +void PDPsSimModel::ForEachPDP( + wpi::function_ref func) { + for (int32_t i = 0, iend = static_cast(m_models.size()); i < iend; + ++i) { + if (auto model = m_models[i].get()) { + func(*model, i); + } + } +} + +static bool PDPsAnyInitialized() { + static const int32_t num = HAL_GetNumPDPModules(); + for (int32_t i = 0; i < num; ++i) { + if (HALSIM_GetPDPInitialized(i)) return true; + } + return false; +} + +void PDPSimGui::Initialize() { + HALSimGui::halProvider.Register( + "PDPs", PDPsAnyInitialized, + [] { return std::make_unique(); }, + [](glass::Window* win, glass::Model* model) { + win->SetDefaultPos(245, 155); + return glass::MakeFunctionView( + [=] { DisplayPDPs(static_cast(model)); }); + }); +} diff --git a/simulation/halsim_gui/src/main/native/cpp/PDPGui.h b/simulation/halsim_gui/src/main/native/cpp/PDPSimGui.h similarity index 86% rename from simulation/halsim_gui/src/main/native/cpp/PDPGui.h rename to simulation/halsim_gui/src/main/native/cpp/PDPSimGui.h index aa53a3cb1c..14686620d5 100644 --- a/simulation/halsim_gui/src/main/native/cpp/PDPGui.h +++ b/simulation/halsim_gui/src/main/native/cpp/PDPSimGui.h @@ -1,5 +1,5 @@ /*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019 FIRST. All Rights Reserved. */ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ /* Open Source Software - may be modified and shared by FRC teams. The code */ /* must be accompanied by the FIRST BSD license file in the root directory of */ /* the project. */ @@ -9,7 +9,7 @@ namespace halsimgui { -class PDPGui { +class PDPSimGui { public: static void Initialize(); }; diff --git a/simulation/halsim_gui/src/main/native/cpp/PWMGui.cpp b/simulation/halsim_gui/src/main/native/cpp/PWMGui.cpp deleted file mode 100644 index 59d7a9308e..0000000000 --- a/simulation/halsim_gui/src/main/native/cpp/PWMGui.cpp +++ /dev/null @@ -1,104 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#include "PWMGui.h" - -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "GuiDataSource.h" -#include "HALSimGui.h" -#include "IniSaver.h" -#include "IniSaverInfo.h" - -using namespace halsimgui; - -namespace { -HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(PWMSpeed, "PWM"); -} // namespace - -static IniSaver gPWM{"PWM"}; -static std::vector> gPWMSources; - -static void UpdatePWMSources() { - static const int numPWM = HAL_GetNumPWMChannels(); - if (static_cast(numPWM) != gPWMSources.size()) - gPWMSources.resize(numPWM); - - for (int i = 0; i < numPWM; ++i) { - auto& source = gPWMSources[i]; - if (HALSIM_GetPWMInitialized(i)) { - if (!source) { - source = std::make_unique(i); - source->SetName(gPWM[i].GetName()); - } - } else { - source.reset(); - } - } -} - -static void DisplayPWMs() { - bool hasOutputs = false; - static const int numPWM = HAL_GetNumPWMChannels(); - static const int numLED = HAL_GetNumAddressableLEDs(); - static auto ledMap = std::make_unique(numPWM); - - std::memset(ledMap.get(), 0, numPWM * sizeof(ledMap[0])); - - for (int i = 0; i < numLED; ++i) { - if (HALSIM_GetAddressableLEDInitialized(i)) { - int channel = HALSIM_GetAddressableLEDOutputPort(i); - if (channel >= 0 && channel < numPWM) ledMap[channel] = i + 1; - } - } - - bool first = true; - ImGui::PushItemWidth(ImGui::GetFontSize() * 4); - for (int i = 0; i < numPWM; ++i) { - if (auto source = gPWMSources[i].get()) { - ImGui::PushID(i); - hasOutputs = true; - - if (!first) - ImGui::Separator(); - else - first = false; - - auto& info = gPWM[i]; - char label[128]; - info.GetLabel(label, sizeof(label), "PWM", i); - if (ledMap[i] > 0) { - ImGui::LabelText(label, "LED[%d]", ledMap[i] - 1); - } else { - float val = HALSimGui::AreOutputsDisabled() ? 0 : HALSIM_GetPWMSpeed(i); - source->LabelText(label, "%0.3f", val); - } - if (info.PopupEditName(i)) { - source->SetName(info.GetName()); - } - ImGui::PopID(); - } - } - ImGui::PopItemWidth(); - if (!hasOutputs) ImGui::Text("No PWM outputs"); -} - -void PWMGui::Initialize() { - gPWM.Initialize(); - HALSimGui::AddExecute(UpdatePWMSources); - HALSimGui::AddWindow("PWM Outputs", DisplayPWMs, - ImGuiWindowFlags_AlwaysAutoResize); - HALSimGui::SetDefaultWindowPos("PWM Outputs", 910, 20); -} diff --git a/simulation/halsim_gui/src/main/native/cpp/PWMGui.h b/simulation/halsim_gui/src/main/native/cpp/PWMGui.h deleted file mode 100644 index 211eabaa14..0000000000 --- a/simulation/halsim_gui/src/main/native/cpp/PWMGui.h +++ /dev/null @@ -1,17 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#pragma once - -namespace halsimgui { - -class PWMGui { - public: - static void Initialize(); -}; - -} // namespace halsimgui diff --git a/simulation/halsim_gui/src/main/native/cpp/PWMSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/PWMSimGui.cpp new file mode 100644 index 0000000000..a31148b0ac --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/PWMSimGui.cpp @@ -0,0 +1,119 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "PWMSimGui.h" + +#include + +#include +#include + +#include +#include +#include + +#include "HALDataSource.h" +#include "HALSimGui.h" + +using namespace halsimgui; + +namespace { +HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(PWMSpeed, "PWM"); + +class PWMSimModel : public glass::PWMModel { + public: + explicit PWMSimModel(int32_t index) : m_index{index}, m_speed{m_index} {} + + void Update() override {} + + bool Exists() override { return HALSIM_GetPWMInitialized(m_index); } + + void SetAddressableLED(int led) { m_led = led; } + int GetAddressableLED() const override { return m_led; } + + glass::DataSource* GetSpeedData() override { return &m_speed; } + + void SetSpeed(double val) override { HALSIM_SetPWMSpeed(m_index, val); } + + private: + int32_t m_index; + int m_led = -1; + PWMSpeedSource m_speed; +}; + +class PWMsSimModel : public glass::PWMsModel { + public: + PWMsSimModel() : m_sources(HAL_GetNumPWMChannels()) {} + + void Update() override; + + bool Exists() override { return true; } + + void ForEachPWM( + wpi::function_ref func) override; + + private: + // indexed by channel + std::vector> m_sources; +}; +} // namespace + +void PWMsSimModel::Update() { + const int32_t numPWM = m_sources.size(); + for (int32_t i = 0; i < numPWM; ++i) { + auto& model = m_sources[i]; + if (HALSIM_GetPWMInitialized(i)) { + if (!model) { + model = std::make_unique(i); + } + model->SetAddressableLED(-1); + } else { + model.reset(); + } + } + + static const int32_t numLED = HAL_GetNumAddressableLEDs(); + for (int32_t i = 0; i < numLED; ++i) { + if (HALSIM_GetAddressableLEDInitialized(i)) { + int32_t channel = HALSIM_GetAddressableLEDOutputPort(i); + if (channel >= 0 && channel < numPWM && m_sources[channel]) + m_sources[channel]->SetAddressableLED(i); + } + } +} + +void PWMsSimModel::ForEachPWM( + wpi::function_ref func) { + const int32_t numPWM = m_sources.size(); + for (int32_t i = 0; i < numPWM; ++i) { + if (auto model = m_sources[i].get()) { + func(*model, i); + } + } +} + +static bool PWMsAnyInitialized() { + static const int32_t num = HAL_GetNumPWMChannels(); + for (int32_t i = 0; i < num; ++i) { + if (HALSIM_GetPWMInitialized(i)) return true; + } + return false; +} + +void PWMSimGui::Initialize() { + HALSimGui::halProvider.Register( + "PWM Outputs", PWMsAnyInitialized, + [] { return std::make_unique(); }, + [](glass::Window* win, glass::Model* model) { + win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize); + win->SetDefaultPos(910, 20); + return glass::MakeFunctionView([=] { + glass::DisplayPWMs(static_cast(model), + HALSimGui::halProvider.AreOutputsEnabled()); + }); + }); +} diff --git a/simulation/halsim_gui/src/main/native/cpp/PWMSimGui.h b/simulation/halsim_gui/src/main/native/cpp/PWMSimGui.h new file mode 100644 index 0000000000..845e0607f1 --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/PWMSimGui.h @@ -0,0 +1,17 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +namespace halsimgui { + +class PWMSimGui { + public: + static void Initialize(); +}; + +} // namespace halsimgui diff --git a/simulation/halsim_gui/src/main/native/cpp/RelayGui.cpp b/simulation/halsim_gui/src/main/native/cpp/RelayGui.cpp deleted file mode 100644 index 917167835c..0000000000 --- a/simulation/halsim_gui/src/main/native/cpp/RelayGui.cpp +++ /dev/null @@ -1,120 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#include "RelayGui.h" - -#include -#include -#include -#include - -#include -#include -#include - -#include "ExtraGuiWidgets.h" -#include "GuiDataSource.h" -#include "HALSimGui.h" -#include "IniSaver.h" -#include "IniSaverInfo.h" - -using namespace halsimgui; - -namespace { -HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(RelayForward, "RelayFwd"); -HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(RelayReverse, "RelayRev"); -} // namespace - -static IniSaver gRelays{"Relay"}; -static std::vector> gRelayForwardSources; -static std::vector> gRelayReverseSources; - -static void UpdateRelaySources() { - for (int i = 0, iend = gRelayForwardSources.size(); i < iend; ++i) { - auto& source = gRelayForwardSources[i]; - if (HALSIM_GetRelayInitializedForward(i)) { - if (!source) { - source = std::make_unique(i); - source->SetName(gRelays[i].GetName()); - } - } else { - source.reset(); - } - } - for (int i = 0, iend = gRelayReverseSources.size(); i < iend; ++i) { - auto& source = gRelayReverseSources[i]; - if (HALSIM_GetRelayInitializedReverse(i)) { - if (!source) { - source = std::make_unique(i); - source->SetName(gRelays[i].GetName()); - } - } else { - source.reset(); - } - } -} - -static void DisplayRelays() { - bool hasOutputs = false; - bool first = true; - for (int i = 0, iend = gRelayForwardSources.size(); i < iend; ++i) { - auto forwardSource = gRelayForwardSources[i].get(); - auto reverseSource = gRelayReverseSources[i].get(); - - if (forwardSource || reverseSource) { - hasOutputs = true; - - if (!first) - ImGui::Separator(); - else - first = false; - - bool forward = false; - bool reverse = false; - if (!HALSimGui::AreOutputsDisabled()) { - if (forwardSource) forward = forwardSource->GetValue(); - if (reverseSource) reverse = reverseSource->GetValue(); - } - - auto& info = gRelays[i]; - info.PushEditNameId(i); - if (info.HasName()) - ImGui::Text("%s [%d]", info.GetName(), i); - else - ImGui::Text("Relay[%d]", i); - ImGui::PopID(); - if (info.PopupEditName(i)) { - if (forwardSource) forwardSource->SetName(info.GetName()); - if (reverseSource) reverseSource->SetName(info.GetName()); - } - ImGui::SameLine(); - - // show forward and reverse as LED indicators - static const ImU32 colors[] = {IM_COL32(255, 255, 102, 255), - IM_COL32(255, 0, 0, 255), - IM_COL32(128, 128, 128, 255)}; - int values[2] = {reverseSource ? (reverse ? 2 : -2) : -3, - forwardSource ? (forward ? 1 : -1) : -3}; - GuiDataSource* sources[2] = {reverseSource, forwardSource}; - ImGui::PushID(i); - DrawLEDSources(values, sources, 2, 2, colors); - ImGui::PopID(); - } - } - if (!hasOutputs) ImGui::Text("No relays"); -} - -void RelayGui::Initialize() { - gRelays.Initialize(); - int numRelays = HAL_GetNumRelayHeaders(); - gRelayForwardSources.resize(numRelays); - gRelayReverseSources.resize(numRelays); - HALSimGui::AddExecute(UpdateRelaySources); - HALSimGui::AddWindow("Relays", DisplayRelays, - ImGuiWindowFlags_AlwaysAutoResize); - HALSimGui::SetDefaultWindowPos("Relays", 180, 20); -} diff --git a/simulation/halsim_gui/src/main/native/cpp/RelayGui.h b/simulation/halsim_gui/src/main/native/cpp/RelayGui.h deleted file mode 100644 index ccc2fb6e06..0000000000 --- a/simulation/halsim_gui/src/main/native/cpp/RelayGui.h +++ /dev/null @@ -1,17 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#pragma once - -namespace halsimgui { - -class RelayGui { - public: - static void Initialize(); -}; - -} // namespace halsimgui diff --git a/simulation/halsim_gui/src/main/native/cpp/RelaySimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/RelaySimGui.cpp new file mode 100644 index 0000000000..9b6d5dbcaa --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/RelaySimGui.cpp @@ -0,0 +1,120 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "RelaySimGui.h" + +#include + +#include +#include + +#include +#include +#include + +#include "HALDataSource.h" +#include "HALSimGui.h" + +using namespace halsimgui; + +namespace { +HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(RelayForward, "RelayFwd"); +HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(RelayReverse, "RelayRev"); + +class RelaySimModel : public glass::RelayModel { + public: + explicit RelaySimModel(int32_t index) + : m_index{index}, m_forward{index}, m_reverse{index} {} + + void Update() override {} + + bool Exists() override { + return HALSIM_GetRelayInitializedForward(m_index) || + HALSIM_GetRelayInitializedReverse(m_index); + } + + glass::DataSource* GetForwardData() override { + return HALSIM_GetRelayInitializedForward(m_index) ? &m_forward : nullptr; + } + glass::DataSource* GetReverseData() override { + return HALSIM_GetRelayInitializedReverse(m_index) ? &m_reverse : nullptr; + } + + void SetForward(bool val) override { HALSIM_SetRelayForward(m_index, val); } + void SetReverse(bool val) override { HALSIM_SetRelayReverse(m_index, val); } + + private: + int32_t m_index; + RelayForwardSource m_forward; + RelayReverseSource m_reverse; +}; + +class RelaysSimModel : public glass::RelaysModel { + public: + RelaysSimModel() : m_models(HAL_GetNumRelayHeaders()) {} + + void Update() override; + + bool Exists() override { return true; } + + void ForEachRelay(wpi::function_ref + func) override; + + private: + // indexed by channel + std::vector> m_models; +}; +} // namespace + +void RelaysSimModel::Update() { + for (int32_t i = 0, iend = static_cast(m_models.size()); i < iend; + ++i) { + auto& model = m_models[i]; + if (HALSIM_GetRelayInitializedForward(i) || + HALSIM_GetRelayInitializedReverse(i)) { + if (!model) { + model = std::make_unique(i); + } + } else { + model.reset(); + } + } +} + +void RelaysSimModel::ForEachRelay( + wpi::function_ref func) { + for (int32_t i = 0, iend = static_cast(m_models.size()); i < iend; + ++i) { + if (auto model = m_models[i].get()) { + func(*model, i); + } + } +} + +static bool RelayAnyInitialized() { + static const int32_t num = HAL_GetNumRelayHeaders(); + for (int32_t i = 0; i < num; ++i) { + if (HALSIM_GetRelayInitializedForward(i) || + HALSIM_GetRelayInitializedReverse(i)) + return true; + } + return false; +} + +void RelaySimGui::Initialize() { + HALSimGui::halProvider.Register( + "Relays", RelayAnyInitialized, + [] { return std::make_unique(); }, + [](glass::Window* win, glass::Model* model) { + win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize); + win->SetDefaultPos(180, 20); + return glass::MakeFunctionView([=] { + glass::DisplayRelays(static_cast(model), + HALSimGui::halProvider.AreOutputsEnabled()); + }); + }); +} diff --git a/simulation/halsim_gui/src/main/native/cpp/RelaySimGui.h b/simulation/halsim_gui/src/main/native/cpp/RelaySimGui.h new file mode 100644 index 0000000000..389bf0d34f --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/RelaySimGui.h @@ -0,0 +1,17 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +namespace halsimgui { + +class RelaySimGui { + public: + static void Initialize(); +}; + +} // namespace halsimgui diff --git a/simulation/halsim_gui/src/main/native/cpp/RoboRioGui.cpp b/simulation/halsim_gui/src/main/native/cpp/RoboRioGui.cpp deleted file mode 100644 index b4f89091bc..0000000000 --- a/simulation/halsim_gui/src/main/native/cpp/RoboRioGui.cpp +++ /dev/null @@ -1,170 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#include "RoboRioGui.h" - -#include - -#include -#include - -#include "GuiDataSource.h" -#include "HALSimGui.h" - -using namespace halsimgui; - -namespace { -HALSIMGUI_DATASOURCE_DOUBLE(RoboRioVInVoltage, "Rio Input Voltage"); -HALSIMGUI_DATASOURCE_DOUBLE(RoboRioVInCurrent, "Rio Input Current"); -HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserVoltage6V, "Rio 6V Voltage"); -HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserCurrent6V, "Rio 6V Current"); -HALSIMGUI_DATASOURCE_BOOLEAN(RoboRioUserActive6V, "Rio 6V Active"); -HALSIMGUI_DATASOURCE_INT(RoboRioUserFaults6V, "Rio 6V Faults"); -HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserVoltage5V, "Rio 5V Voltage"); -HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserCurrent5V, "Rio 5V Current"); -HALSIMGUI_DATASOURCE_BOOLEAN(RoboRioUserActive5V, "Rio 5V Active"); -HALSIMGUI_DATASOURCE_INT(RoboRioUserFaults5V, "Rio 5V Faults"); -HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserVoltage3V3, "Rio 3.3V Voltage"); -HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserCurrent3V3, "Rio 3.3V Current"); -HALSIMGUI_DATASOURCE_BOOLEAN(RoboRioUserActive3V3, "Rio 3.3V Active"); -HALSIMGUI_DATASOURCE_INT(RoboRioUserFaults3V3, "Rio 3.3V Faults"); -struct RoboRioSource { - RoboRioVInVoltageSource vInVoltage; - RoboRioVInCurrentSource vInCurrent; - RoboRioUserVoltage6VSource userVoltage6V; - RoboRioUserCurrent6VSource userCurrent6V; - RoboRioUserActive6VSource userActive6V; - RoboRioUserFaults6VSource userFaults6V; - RoboRioUserVoltage5VSource userVoltage5V; - RoboRioUserCurrent5VSource userCurrent5V; - RoboRioUserActive5VSource userActive5V; - RoboRioUserFaults5VSource userFaults5V; - RoboRioUserVoltage3V3Source userVoltage3V3; - RoboRioUserCurrent3V3Source userCurrent3V3; - RoboRioUserActive3V3Source userActive3V3; - RoboRioUserFaults3V3Source userFaults3V3; -}; -} // namespace - -static std::unique_ptr gRioSource; - -static void UpdateRoboRioSources() { - if (!gRioSource) gRioSource = std::make_unique(); -} - -static void DisplayRoboRio() { - ImGui::Button("User Button"); - HALSIM_SetRoboRioFPGAButton(ImGui::IsItemActive()); - - ImGui::PushItemWidth(ImGui::GetFontSize() * 8); - - if (ImGui::CollapsingHeader("RoboRIO Input")) { - { - double val = gRioSource->vInVoltage.GetValue(); - if (gRioSource->vInVoltage.InputDouble("Voltage (V)", &val)) - HALSIM_SetRoboRioVInVoltage(val); - } - - { - double val = gRioSource->vInCurrent.GetValue(); - if (gRioSource->vInCurrent.InputDouble("Current (A)", &val)) - HALSIM_SetRoboRioVInCurrent(val); - } - } - - if (ImGui::CollapsingHeader("6V Rail")) { - { - double val = gRioSource->userVoltage6V.GetValue(); - if (gRioSource->userVoltage6V.InputDouble("Voltage (V)", &val)) - HALSIM_SetRoboRioUserVoltage6V(val); - } - - { - double val = gRioSource->userCurrent6V.GetValue(); - if (gRioSource->userCurrent6V.InputDouble("Current (A)", &val)) - HALSIM_SetRoboRioUserCurrent6V(val); - } - - { - static const char* options[] = {"inactive", "active"}; - int val = gRioSource->userActive6V.GetValue() ? 1 : 0; - if (gRioSource->userActive6V.Combo("Active", &val, options, 2)) - HALSIM_SetRoboRioUserActive6V(val); - } - - { - int val = gRioSource->userFaults6V.GetValue(); - if (gRioSource->userFaults6V.InputInt("Faults", &val)) - HALSIM_SetRoboRioUserFaults6V(val); - } - } - - if (ImGui::CollapsingHeader("5V Rail")) { - { - double val = gRioSource->userVoltage5V.GetValue(); - if (gRioSource->userVoltage5V.InputDouble("Voltage (V)", &val)) - HALSIM_SetRoboRioUserVoltage5V(val); - } - - { - double val = gRioSource->userCurrent5V.GetValue(); - if (gRioSource->userCurrent5V.InputDouble("Current (A)", &val)) - HALSIM_SetRoboRioUserCurrent5V(val); - } - - { - static const char* options[] = {"inactive", "active"}; - int val = gRioSource->userActive5V.GetValue() ? 1 : 0; - if (gRioSource->userActive5V.Combo("Active", &val, options, 2)) - HALSIM_SetRoboRioUserActive5V(val); - } - - { - int val = gRioSource->userFaults5V.GetValue(); - if (gRioSource->userFaults5V.InputInt("Faults", &val)) - HALSIM_SetRoboRioUserFaults5V(val); - } - } - - if (ImGui::CollapsingHeader("3.3V Rail")) { - { - double val = gRioSource->userVoltage3V3.GetValue(); - if (gRioSource->userVoltage3V3.InputDouble("Voltage (V)", &val)) - HALSIM_SetRoboRioUserVoltage3V3(val); - } - - { - double val = gRioSource->userCurrent3V3.GetValue(); - if (gRioSource->userCurrent3V3.InputDouble("Current (A)", &val)) - HALSIM_SetRoboRioUserCurrent3V3(val); - } - - { - static const char* options[] = {"inactive", "active"}; - int val = HALSIM_GetRoboRioUserActive3V3() ? 1 : 0; - if (gRioSource->userActive3V3.Combo("Active", &val, options, 2)) - HALSIM_SetRoboRioUserActive3V3(val); - } - - { - int val = gRioSource->userFaults3V3.GetValue(); - if (gRioSource->userFaults3V3.InputInt("Faults", &val)) - HALSIM_SetRoboRioUserFaults3V3(val); - } - } - - ImGui::PopItemWidth(); -} - -void RoboRioGui::Initialize() { - HALSimGui::AddExecute(UpdateRoboRioSources); - HALSimGui::AddWindow("RoboRIO", DisplayRoboRio, - ImGuiWindowFlags_AlwaysAutoResize); - // hide it by default - HALSimGui::SetWindowVisibility("RoboRIO", HALSimGui::kHide); - HALSimGui::SetDefaultWindowPos("RoboRIO", 5, 125); -} diff --git a/simulation/halsim_gui/src/main/native/cpp/RoboRioGui.h b/simulation/halsim_gui/src/main/native/cpp/RoboRioGui.h deleted file mode 100644 index 603abf0f90..0000000000 --- a/simulation/halsim_gui/src/main/native/cpp/RoboRioGui.h +++ /dev/null @@ -1,17 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#pragma once - -namespace halsimgui { - -class RoboRioGui { - public: - static void Initialize(); -}; - -} // namespace halsimgui diff --git a/simulation/halsim_gui/src/main/native/cpp/RoboRioSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/RoboRioSimGui.cpp new file mode 100644 index 0000000000..8c6351fbbd --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/RoboRioSimGui.cpp @@ -0,0 +1,139 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "RoboRioSimGui.h" + +#include + +#include + +#include + +#include "HALDataSource.h" +#include "HALSimGui.h" + +using namespace halsimgui; + +namespace { +HALSIMGUI_DATASOURCE_BOOLEAN(RoboRioFPGAButton, "Rio User Button"); +HALSIMGUI_DATASOURCE_DOUBLE(RoboRioVInVoltage, "Rio Input Voltage"); +HALSIMGUI_DATASOURCE_DOUBLE(RoboRioVInCurrent, "Rio Input Current"); +HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserVoltage6V, "Rio 6V Voltage"); +HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserCurrent6V, "Rio 6V Current"); +HALSIMGUI_DATASOURCE_BOOLEAN(RoboRioUserActive6V, "Rio 6V Active"); +HALSIMGUI_DATASOURCE_INT(RoboRioUserFaults6V, "Rio 6V Faults"); +HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserVoltage5V, "Rio 5V Voltage"); +HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserCurrent5V, "Rio 5V Current"); +HALSIMGUI_DATASOURCE_BOOLEAN(RoboRioUserActive5V, "Rio 5V Active"); +HALSIMGUI_DATASOURCE_INT(RoboRioUserFaults5V, "Rio 5V Faults"); +HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserVoltage3V3, "Rio 3.3V Voltage"); +HALSIMGUI_DATASOURCE_DOUBLE(RoboRioUserCurrent3V3, "Rio 3.3V Current"); +HALSIMGUI_DATASOURCE_BOOLEAN(RoboRioUserActive3V3, "Rio 3.3V Active"); +HALSIMGUI_DATASOURCE_INT(RoboRioUserFaults3V3, "Rio 3.3V Faults"); + +class RoboRioUser6VRailSimModel : public glass::RoboRioRailModel { + public: + void Update() override {} + bool Exists() override { return true; } + glass::DataSource* GetVoltageData() override { return &m_voltage; } + glass::DataSource* GetCurrentData() override { return &m_current; } + glass::DataSource* GetActiveData() override { return &m_active; } + glass::DataSource* GetFaultsData() override { return &m_faults; } + + void SetVoltage(double val) override { HALSIM_SetRoboRioUserVoltage6V(val); } + void SetCurrent(double val) override { HALSIM_SetRoboRioUserCurrent6V(val); } + void SetActive(bool val) override { HALSIM_SetRoboRioUserActive6V(val); } + void SetFaults(int val) override { HALSIM_SetRoboRioUserFaults6V(val); } + + private: + RoboRioUserVoltage6VSource m_voltage; + RoboRioUserCurrent6VSource m_current; + RoboRioUserActive6VSource m_active; + RoboRioUserFaults6VSource m_faults; +}; + +class RoboRioUser5VRailSimModel : public glass::RoboRioRailModel { + public: + void Update() override {} + bool Exists() override { return true; } + glass::DataSource* GetVoltageData() override { return &m_voltage; } + glass::DataSource* GetCurrentData() override { return &m_current; } + glass::DataSource* GetActiveData() override { return &m_active; } + glass::DataSource* GetFaultsData() override { return &m_faults; } + + void SetVoltage(double val) override { HALSIM_SetRoboRioUserVoltage5V(val); } + void SetCurrent(double val) override { HALSIM_SetRoboRioUserCurrent5V(val); } + void SetActive(bool val) override { HALSIM_SetRoboRioUserActive5V(val); } + void SetFaults(int val) override { HALSIM_SetRoboRioUserFaults5V(val); } + + private: + RoboRioUserVoltage5VSource m_voltage; + RoboRioUserCurrent5VSource m_current; + RoboRioUserActive5VSource m_active; + RoboRioUserFaults5VSource m_faults; +}; + +class RoboRioUser3V3RailSimModel : public glass::RoboRioRailModel { + public: + void Update() override {} + bool Exists() override { return true; } + glass::DataSource* GetVoltageData() override { return &m_voltage; } + glass::DataSource* GetCurrentData() override { return &m_current; } + glass::DataSource* GetActiveData() override { return &m_active; } + glass::DataSource* GetFaultsData() override { return &m_faults; } + + void SetVoltage(double val) override { HALSIM_SetRoboRioUserVoltage3V3(val); } + void SetCurrent(double val) override { HALSIM_SetRoboRioUserCurrent3V3(val); } + void SetActive(bool val) override { HALSIM_SetRoboRioUserActive3V3(val); } + void SetFaults(int val) override { HALSIM_SetRoboRioUserFaults3V3(val); } + + private: + RoboRioUserVoltage3V3Source m_voltage; + RoboRioUserCurrent3V3Source m_current; + RoboRioUserActive3V3Source m_active; + RoboRioUserFaults3V3Source m_faults; +}; + +class RoboRioSimModel : public glass::RoboRioModel { + public: + void Update() override {} + + bool Exists() override { return true; } + + glass::RoboRioRailModel* GetUser6VRail() override { return &m_user6VRail; } + glass::RoboRioRailModel* GetUser5VRail() override { return &m_user5VRail; } + glass::RoboRioRailModel* GetUser3V3Rail() override { return &m_user3V3Rail; } + + glass::DataSource* GetUserButton() override { return &m_userButton; } + glass::DataSource* GetVInVoltageData() override { return &m_vInVoltage; } + glass::DataSource* GetVInCurrentData() override { return &m_vInCurrent; } + + void SetUserButton(bool val) override { HALSIM_SetRoboRioFPGAButton(val); } + void SetVInVoltage(double val) override { HALSIM_SetRoboRioVInVoltage(val); } + void SetVInCurrent(double val) override { HALSIM_SetRoboRioVInCurrent(val); } + + private: + RoboRioFPGAButtonSource m_userButton; + RoboRioVInVoltageSource m_vInVoltage; + RoboRioVInCurrentSource m_vInCurrent; + RoboRioUser6VRailSimModel m_user6VRail; + RoboRioUser5VRailSimModel m_user5VRail; + RoboRioUser3V3RailSimModel m_user3V3Rail; +}; +} // namespace + +void RoboRioSimGui::Initialize() { + HALSimGui::halProvider.Register( + "RoboRIO", [] { return true; }, + [] { return std::make_unique(); }, + [](glass::Window* win, glass::Model* model) { + win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize); + win->SetDefaultPos(5, 125); + return glass::MakeFunctionView( + [=] { DisplayRoboRio(static_cast(model)); }); + }); +} diff --git a/simulation/halsim_gui/src/main/native/cpp/CompressorGui.h b/simulation/halsim_gui/src/main/native/cpp/RoboRioSimGui.h similarity index 85% rename from simulation/halsim_gui/src/main/native/cpp/CompressorGui.h rename to simulation/halsim_gui/src/main/native/cpp/RoboRioSimGui.h index f403ece9c7..cc1ea29627 100644 --- a/simulation/halsim_gui/src/main/native/cpp/CompressorGui.h +++ b/simulation/halsim_gui/src/main/native/cpp/RoboRioSimGui.h @@ -1,5 +1,5 @@ /*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019 FIRST. All Rights Reserved. */ +/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ /* Open Source Software - may be modified and shared by FRC teams. The code */ /* must be accompanied by the FIRST BSD license file in the root directory of */ /* the project. */ @@ -9,7 +9,7 @@ namespace halsimgui { -class CompressorGui { +class RoboRioSimGui { public: static void Initialize(); }; diff --git a/simulation/halsim_gui/src/main/native/cpp/SimDeviceGui.cpp b/simulation/halsim_gui/src/main/native/cpp/SimDeviceGui.cpp index 2802000fcf..ab23703918 100644 --- a/simulation/halsim_gui/src/main/native/cpp/SimDeviceGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/SimDeviceGui.cpp @@ -7,44 +7,24 @@ #include "SimDeviceGui.h" +#include #include -#include -#include -#include - #include #include -#include #include -#include "GuiDataSource.h" +#include "HALDataSource.h" #include "HALSimGui.h" -#include "IniSaverInfo.h" -#include "IniSaverString.h" using namespace halsimgui; namespace { - -struct ElementInfo : public NameInfo, public OpenInfo { - bool ReadIni(wpi::StringRef name, wpi::StringRef value) { - if (NameInfo::ReadIni(name, value)) return true; - if (OpenInfo::ReadIni(name, value)) return true; - return false; - } - void WriteIni(ImGuiTextBuffer* out) { - NameInfo::WriteIni(out); - OpenInfo::WriteIni(out); - } - bool visible = true; // not saved -}; - -class SimValueSource : public GuiDataSource { +class SimValueSource : public glass::DataSource { public: explicit SimValueSource(HAL_SimValueHandle handle, const char* device, const char* name) - : GuiDataSource(wpi::Twine{device} + wpi::Twine{'-'} + name), + : DataSource(wpi::Twine{device} + wpi::Twine{'-'} + name), m_callback{HALSIM_RegisterSimValueChangedCallback( handle, this, CallbackFunc, true)} {} ~SimValueSource() { @@ -67,226 +47,124 @@ class SimValueSource : public GuiDataSource { int32_t m_callback; }; +class SimDevicesModel : public glass::Model { + public: + void Update() override; + bool Exists() override { return true; } + + glass::DataSource* GetSource(HAL_SimValueHandle handle) { + return m_sources[handle].get(); + } + + private: + wpi::DenseMap> m_sources; +}; } // namespace -static std::vector> gDeviceExecutors; -static IniSaverString gElements{"Device"}; -static wpi::DenseMap> - gSimValueSources; +static SimDevicesModel* gSimDevicesModel; -static void UpdateSimValueSources() { +void SimDevicesModel::Update() { HALSIM_EnumerateSimDevices( - "", nullptr, [](const char* name, void*, HAL_SimDeviceHandle handle) { + "", this, [](const char* name, void* self, HAL_SimDeviceHandle handle) { + struct Data { + SimDevicesModel* self; + const char* device; + } data = {static_cast(self), name}; HALSIM_EnumerateSimValues( - handle, const_cast(name), - [](const char* name, void* deviceV, HAL_SimValueHandle handle, + handle, &data, + [](const char* name, void* dataV, HAL_SimValueHandle handle, HAL_Bool readonly, const HAL_Value* value) { - auto device = static_cast(deviceV); - auto& source = gSimValueSources[handle]; + auto data = static_cast(dataV); + auto& source = data->self->m_sources[handle]; if (!source) { - source = std::make_unique(handle, device, name); + source = std::make_unique(handle, data->device, + name); } }); }); } -void SimDeviceGui::Hide(const char* name) { gElements[name].visible = false; } +static void DisplaySimValue(const char* name, void* data, + HAL_SimValueHandle handle, HAL_Bool readonly, + const HAL_Value* value) { + auto model = static_cast(data); -void SimDeviceGui::Add(std::function execute) { - if (execute) gDeviceExecutors.emplace_back(std::move(execute)); -} + HAL_Value valueCopy = *value; -bool SimDeviceGui::StartDevice(const char* label, ImGuiTreeNodeFlags flags) { - auto& element = gElements[label]; - if (!element.visible) return false; - - char name[128]; - element.GetLabel(name, sizeof(name), label); - - bool open = ImGui::CollapsingHeader( - name, flags | (element.IsOpen() ? ImGuiTreeNodeFlags_DefaultOpen : 0)); - element.SetOpen(open); - element.PopupEditName(label); - - if (open) ImGui::PushID(label); - return open; -} - -void SimDeviceGui::FinishDevice() { ImGui::PopID(); } - -static bool DisplayValueImpl(const char* name, bool readonly, HAL_Value* value, - const char** options, int32_t numOptions) { - // read-only - if (readonly) { - switch (value->type) { - case HAL_BOOLEAN: - ImGui::LabelText(name, "%s", value->data.v_boolean ? "true" : "false"); - break; - case HAL_DOUBLE: - ImGui::LabelText(name, "%.6f", value->data.v_double); - break; - case HAL_ENUM: { - int current = value->data.v_enum; - if (current < 0 || current >= numOptions) - ImGui::LabelText(name, "%d (unknown)", current); - else - ImGui::LabelText(name, "%s", options[current]); - break; - } - case HAL_INT: - ImGui::LabelText(name, "%d", static_cast(value->data.v_int)); - break; - case HAL_LONG: - ImGui::LabelText(name, "%lld", - static_cast( // NOLINT(runtime/int) - value->data.v_long)); - break; - default: - break; - } - return false; - } - - // writable switch (value->type) { case HAL_BOOLEAN: { - static const char* boolOptions[] = {"false", "true"}; - int val = value->data.v_boolean ? 1 : 0; - if (ImGui::Combo(name, &val, boolOptions, 2)) { - value->data.v_boolean = val; - return true; + bool v = value->data.v_boolean; + if (glass::DeviceBoolean(name, readonly, &v, model->GetSource(handle))) { + valueCopy.data.v_boolean = v ? 1 : 0; + HAL_SetSimValue(handle, valueCopy); } break; } - case HAL_DOUBLE: { - if (ImGui::InputDouble(name, &value->data.v_double, 0, 0, "%.6f", - ImGuiInputTextFlags_EnterReturnsTrue)) - return true; + case HAL_DOUBLE: + if (glass::DeviceDouble(name, readonly, &valueCopy.data.v_double, + model->GetSource(handle))) { + HAL_SetSimValue(handle, valueCopy); + } break; - } case HAL_ENUM: { - int current = value->data.v_enum; - if (ImGui::Combo(name, ¤t, options, numOptions)) { - value->data.v_enum = current; - return true; + int32_t numOptions = 0; + const char** options = HALSIM_GetSimValueEnumOptions(handle, &numOptions); + if (glass::DeviceEnum(name, readonly, &valueCopy.data.v_enum, options, + numOptions, model->GetSource(handle))) { + HAL_SetSimValue(handle, valueCopy); } break; } - case HAL_INT: { - if (ImGui::InputScalar(name, ImGuiDataType_S32, &value->data.v_int, - nullptr, nullptr, nullptr, - ImGuiInputTextFlags_EnterReturnsTrue)) - return true; + case HAL_INT: + if (glass::DeviceInt(name, readonly, &valueCopy.data.v_int, + model->GetSource(handle))) { + HAL_SetSimValue(handle, valueCopy); + } break; - } - case HAL_LONG: { - if (ImGui::InputScalar(name, ImGuiDataType_S64, &value->data.v_long, - nullptr, nullptr, nullptr, - ImGuiInputTextFlags_EnterReturnsTrue)) - return true; + case HAL_LONG: + if (glass::DeviceLong(name, readonly, &valueCopy.data.v_long, + model->GetSource(handle))) { + HAL_SetSimValue(handle, valueCopy); + } break; - } default: break; } - return false; } -static bool DisplayValueSourceImpl(const char* name, bool readonly, - HAL_Value* value, - const GuiDataSource* source, - const char** options, int32_t numOptions) { - if (!source) - return DisplayValueImpl(name, readonly, value, options, numOptions); - ImGui::PushID(name); - bool rv = DisplayValueImpl("", readonly, value, options, numOptions); - ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x); - ImGui::Selectable(name); - source->EmitDrag(); - ImGui::PopID(); - return rv; -} - -bool SimDeviceGui::DisplayValue(const char* name, bool readonly, - HAL_Value* value, const char** options, - int32_t numOptions) { - return DisplayValueSource(name, readonly, value, nullptr, options, - numOptions); -} - -bool SimDeviceGui::DisplayValueSource(const char* name, bool readonly, - HAL_Value* value, - const GuiDataSource* source, - const char** options, - int32_t numOptions) { - ImGui::SetNextItemWidth(ImGui::GetWindowWidth() * 0.5f); - return DisplayValueSourceImpl(name, readonly, value, source, options, - numOptions); -} - -static void SimDeviceDisplayValue(const char* name, void*, - HAL_SimValueHandle handle, HAL_Bool readonly, - const HAL_Value* value) { - int32_t numOptions = 0; - const char** options = nullptr; - - if (value->type == HAL_ENUM) - options = HALSIM_GetSimValueEnumOptions(handle, &numOptions); - - HAL_Value valueCopy = *value; - if (DisplayValueSourceImpl(name, readonly, &valueCopy, - gSimValueSources[handle].get(), options, - numOptions)) - HAL_SetSimValue(handle, valueCopy); -} - -static void SimDeviceDisplayDevice(const char* name, void*, - HAL_SimDeviceHandle handle) { - auto it = gElements.find(name); - if (it != gElements.end() && !it->second.visible) return; - - if (SimDeviceGui::StartDevice(name)) { - ImGui::PushItemWidth(ImGui::GetWindowWidth() * 0.5f); - HALSIM_EnumerateSimValues(handle, nullptr, SimDeviceDisplayValue); - ImGui::PopItemWidth(); - SimDeviceGui::FinishDevice(); +static void DisplaySimDevice(const char* name, void* data, + HAL_SimDeviceHandle handle) { + if (glass::BeginDevice(name)) { + HALSIM_EnumerateSimValues(handle, data, DisplaySimValue); + glass::EndDevice(); } } -static void DisplayDeviceTree() { - for (auto&& execute : gDeviceExecutors) { - if (execute) execute(); - } - HALSIM_EnumerateSimDevices("", nullptr, SimDeviceDisplayDevice); -} - void SimDeviceGui::Initialize() { - gElements.Initialize(); - HALSimGui::AddExecute(UpdateSimValueSources); - HALSimGui::AddWindow("Other Devices", DisplayDeviceTree); - HALSimGui::SetDefaultWindowPos("Other Devices", 1025, 20); - HALSimGui::SetDefaultWindowSize("Other Devices", 250, 695); + HALSimGui::halProvider.Register( + "Other Devices", [] { return true; }, + [] { return std::make_unique(); }, + [](glass::Window* win, glass::Model* model) { + win->SetDefaultPos(1025, 20); + win->SetDefaultSize(250, 695); + return glass::MakeFunctionView( + [=] { static_cast(model)->Display(); }); + }); + + auto model = std::make_unique(); + gSimDevicesModel = model.get(); + GetDeviceTree().Add(std::move(model), [](glass::Model* model) { + HALSIM_EnumerateSimDevices("", static_cast(model), + DisplaySimDevice); + }); } -extern "C" { - -void HALSIMGUI_DeviceTreeAdd(void* param, void (*execute)(void*)) { - if (execute) SimDeviceGui::Add([=] { execute(param); }); +glass::DataSource* SimDeviceGui::GetValueSource(HAL_SimValueHandle handle) { + return gSimDevicesModel->GetSource(handle); } -void HALSIMGUI_DeviceTreeHide(const char* name) { SimDeviceGui::Hide(name); } - -HAL_Bool HALSIMGUI_DeviceTreeDisplayValue(const char* name, HAL_Bool readonly, - struct HAL_Value* value, - const char** options, - int32_t numOptions) { - return SimDeviceGui::DisplayValue(name, readonly, value, options, numOptions); +glass::DeviceTreeModel& SimDeviceGui::GetDeviceTree() { + static auto model = HALSimGui::halProvider.GetModel("Other Devices"); + assert(model); + return *static_cast(model); } - -HAL_Bool HALSIMGUI_DeviceTreeStartDevice(const char* label, int32_t flags) { - return SimDeviceGui::StartDevice(label, flags); -} - -void HALSIMGUI_DeviceTreeFinishDevice(void) { SimDeviceGui::FinishDevice(); } - -} // extern "C" diff --git a/simulation/halsim_gui/src/main/native/cpp/SolenoidGui.cpp b/simulation/halsim_gui/src/main/native/cpp/SolenoidGui.cpp deleted file mode 100644 index b0f802ecc8..0000000000 --- a/simulation/halsim_gui/src/main/native/cpp/SolenoidGui.cpp +++ /dev/null @@ -1,130 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#include "SolenoidGui.h" - -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "ExtraGuiWidgets.h" -#include "GuiDataSource.h" -#include "HALSimGui.h" -#include "IniSaver.h" -#include "IniSaverInfo.h" - -using namespace halsimgui; - -namespace { -HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED2(PCMSolenoidOutput, "Solenoid"); -struct PCMSource { - explicit PCMSource(int numChannels) : solenoids(numChannels) {} - std::vector> solenoids; - int initCount = 0; -}; -} // namespace - -static IniSaver gPCMs{"PCM"}; -static IniSaver gSolenoids{"Solenoid"}; -static std::vector gPCMSources; - -static void UpdateSolenoidSources() { - for (int i = 0, iend = gPCMSources.size(); i < iend; ++i) { - auto& pcmSource = gPCMSources[i]; - int numChannels = pcmSource.solenoids.size(); - pcmSource.initCount = 0; - for (int j = 0; j < numChannels; ++j) { - auto& source = pcmSource.solenoids[j]; - if (HALSIM_GetPCMSolenoidInitialized(i, j)) { - if (!source) { - source = std::make_unique(i, j); - source->SetName(gSolenoids[i * numChannels + j].GetName()); - } - ++pcmSource.initCount; - } else { - source.reset(); - } - } - } -} - -static void DisplaySolenoids() { - bool hasOutputs = false; - for (int i = 0, iend = gPCMSources.size(); i < iend; ++i) { - auto& pcmSource = gPCMSources[i]; - if (pcmSource.initCount == 0) continue; - hasOutputs = true; - - int numChannels = pcmSource.solenoids.size(); - wpi::SmallVector channels; - channels.resize(numChannels); - for (int j = 0; j < numChannels; ++j) { - if (pcmSource.solenoids[j]) { - channels[j] = (!HALSimGui::AreOutputsDisabled() && - pcmSource.solenoids[j]->GetValue()) - ? 1 - : -1; - } else { - channels[j] = -2; - } - } - - char name[128]; - std::snprintf(name, sizeof(name), "PCM[%d]", i); - auto& pcmInfo = gPCMs[i]; - bool open = ImGui::CollapsingHeader( - name, pcmInfo.IsOpen() ? ImGuiTreeNodeFlags_DefaultOpen : 0); - pcmInfo.SetOpen(open); - ImGui::SetItemAllowOverlap(); - ImGui::SameLine(); - - // show channels as LED indicators - static const ImU32 colors[] = {IM_COL32(255, 255, 102, 255), - IM_COL32(128, 128, 128, 255)}; - DrawLEDs(channels.data(), channels.size(), channels.size(), colors); - - if (open) { - ImGui::PushID(i); - ImGui::PushItemWidth(ImGui::GetFontSize() * 4); - for (int j = 0; j < numChannels; ++j) { - if (!pcmSource.solenoids[j]) continue; - auto& info = gSolenoids[i * numChannels + j]; - info.GetLabel(name, sizeof(name), "Solenoid", j); - ImGui::PushID(j); - pcmSource.solenoids[j]->LabelText(name, "%s", - channels[j] == 1 ? "On" : "Off"); - if (info.PopupEditName(j)) { - pcmSource.solenoids[j]->SetName(info.GetName()); - } - ImGui::PopID(); - } - ImGui::PopItemWidth(); - ImGui::PopID(); - } - } - if (!hasOutputs) ImGui::Text("No solenoids"); -} - -void SolenoidGui::Initialize() { - gPCMs.Initialize(); - gSolenoids.Initialize(); - const int numModules = HAL_GetNumPCMModules(); - const int numChannels = HAL_GetNumSolenoidChannels(); - gPCMSources.reserve(numModules); - for (int i = 0; i < numModules; ++i) gPCMSources.emplace_back(numChannels); - - HALSimGui::AddExecute(UpdateSolenoidSources); - HALSimGui::AddWindow("Solenoids", DisplaySolenoids, - ImGuiWindowFlags_AlwaysAutoResize); - HALSimGui::SetDefaultWindowPos("Solenoids", 290, 20); -} diff --git a/simulation/halsim_gui/src/main/native/cpp/SolenoidGui.h b/simulation/halsim_gui/src/main/native/cpp/SolenoidGui.h deleted file mode 100644 index 35905cf1dd..0000000000 --- a/simulation/halsim_gui/src/main/native/cpp/SolenoidGui.h +++ /dev/null @@ -1,17 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2019 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#pragma once - -namespace halsimgui { - -class SolenoidGui { - public: - static void Initialize(); -}; - -} // namespace halsimgui diff --git a/simulation/halsim_gui/src/main/native/cpp/TimingGui.cpp b/simulation/halsim_gui/src/main/native/cpp/TimingGui.cpp index 18c76b0caa..4ba9de9499 100644 --- a/simulation/halsim_gui/src/main/native/cpp/TimingGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/TimingGui.cpp @@ -7,6 +7,9 @@ #include "TimingGui.h" +#include +#include + #include #include #include @@ -20,6 +23,14 @@ using namespace halsimgui; +namespace { +class TimingModel : public glass::Model { + public: + void Update() override {} + bool Exists() override { return true; } +}; +} // namespace + static void DisplayTiming() { int32_t status = 0; uint64_t curTime = HAL_GetFPGATime(&status); @@ -55,7 +66,14 @@ static void DisplayTiming() { } void TimingGui::Initialize() { - HALSimGui::AddWindow("Timing", DisplayTiming, - ImGuiWindowFlags_AlwaysAutoResize); - HALSimGui::SetDefaultWindowPos("Timing", 5, 150); + HALSimGui::halProvider.Register( + "Timing", [] { return true; }, + [] { return std::make_unique(); }, + [](glass::Window* win, glass::Model* model) { + win->DisableRenamePopup(); + win->SetFlags(ImGuiWindowFlags_AlwaysAutoResize); + win->SetDefaultPos(5, 150); + return glass::MakeFunctionView(DisplayTiming); + }); + HALSimGui::halProvider.ShowDefault("Timing"); } diff --git a/simulation/halsim_gui/src/main/native/cpp/main.cpp b/simulation/halsim_gui/src/main/native/cpp/main.cpp index a904580c5f..a75464c21d 100644 --- a/simulation/halsim_gui/src/main/native/cpp/main.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/main.cpp @@ -5,71 +5,118 @@ /* the project. */ /*----------------------------------------------------------------------------*/ +#include +#include + #include #include +#include #include #include +#include -#include "AccelerometerGui.h" +#include "AccelerometerSimGui.h" #include "AddressableLEDGui.h" -#include "AnalogGyroGui.h" -#include "AnalogInputGui.h" -#include "AnalogOutGui.h" -#include "CompressorGui.h" -#include "DIOGui.h" +#include "AnalogGyroSimGui.h" +#include "AnalogInputSimGui.h" +#include "AnalogOutputSimGui.h" +#include "DIOSimGui.h" #include "DriverStationGui.h" -#include "EncoderGui.h" -#include "Field2D.h" +#include "EncoderSimGui.h" #include "HALSimGui.h" #include "Mechanism2D.h" -#include "NetworkTablesGui.h" -#include "PDPGui.h" -#include "PWMGui.h" -#include "PlotGui.h" -#include "RelayGui.h" -#include "RoboRioGui.h" +#include "NetworkTablesSimGui.h" +#include "PCMSimGui.h" +#include "PDPSimGui.h" +#include "PWMSimGui.h" +#include "RelaySimGui.h" +#include "RoboRioSimGui.h" #include "SimDeviceGui.h" -#include "SolenoidGui.h" #include "TimingGui.h" using namespace halsimgui; +namespace gui = wpi::gui; + +static glass::PlotProvider gPlotProvider{"Plot"}; + extern "C" { #if defined(WIN32) || defined(_WIN32) __declspec(dllexport) #endif int HALSIM_InitExtension(void) { - HALSimGui::GlobalInit(); - HALSimGui::Add(AccelerometerGui::Initialize); - HALSimGui::Add(AddressableLEDGui::Initialize); - HALSimGui::Add(AnalogGyroGui::Initialize); - HALSimGui::Add(AnalogInputGui::Initialize); - HALSimGui::Add(AnalogOutGui::Initialize); - HALSimGui::Add(CompressorGui::Initialize); - HALSimGui::Add(DriverStationGui::Initialize); - HALSimGui::Add(DIOGui::Initialize); - HALSimGui::Add(EncoderGui::Initialize); - HALSimGui::Add(Field2D::Initialize); - HALSimGui::Add(Mechanism2D::Initialize); - HALSimGui::Add(NetworkTablesGui::Initialize); - HALSimGui::Add(PDPGui::Initialize); - HALSimGui::Add(PlotGui::Initialize); - HALSimGui::Add(PWMGui::Initialize); - HALSimGui::Add(RelayGui::Initialize); - HALSimGui::Add(RoboRioGui::Initialize); - HALSimGui::Add(SimDeviceGui::Initialize); - HALSimGui::Add(SolenoidGui::Initialize); - HALSimGui::Add(TimingGui::Initialize); - wpi::outs() << "Simulator GUI Initializing.\n"; - if (!HALSimGui::Initialize()) return 0; + + gui::CreateContext(); + glass::CreateContext(); + HALSimGui::GlobalInit(); + DriverStationGui::GlobalInit(); + gPlotProvider.GlobalInit(); + + // These need to initialize first + gui::AddInit(EncoderSimGui::Initialize); + gui::AddInit(SimDeviceGui::Initialize); + + gui::AddInit(AccelerometerSimGui::Initialize); + gui::AddInit(AddressableLEDGui::Initialize); + gui::AddInit(AnalogGyroSimGui::Initialize); + gui::AddInit(AnalogInputSimGui::Initialize); + gui::AddInit(AnalogOutputSimGui::Initialize); + gui::AddInit(DIOSimGui::Initialize); + gui::AddInit(Mechanism2D::Initialize); + gui::AddInit(NetworkTablesSimGui::Initialize); + gui::AddInit(PCMSimGui::Initialize); + gui::AddInit(PDPSimGui::Initialize); + gui::AddInit(PWMSimGui::Initialize); + gui::AddInit(RelaySimGui::Initialize); + gui::AddInit(RoboRioSimGui::Initialize); + gui::AddInit(TimingGui::Initialize); + + HALSimGui::mainMenu.AddMainMenu([] { + if (ImGui::BeginMenu("Hardware")) { + HALSimGui::halProvider.DisplayMenu(); + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("NetworkTables")) { + NetworkTablesSimGui::DisplayMenu(); + ImGui::Separator(); + HALSimGui::ntProvider.DisplayMenu(); + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("DS")) { + DriverStationGui::dsManager.DisplayMenu(); + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Plot")) { + bool paused = gPlotProvider.IsPaused(); + if (ImGui::MenuItem("Pause All Plots", nullptr, &paused)) { + gPlotProvider.SetPaused(paused); + } + ImGui::Separator(); + gPlotProvider.DisplayMenu(); + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Window")) { + HALSimGui::manager.DisplayMenu(); + ImGui::EndMenu(); + } + }); + + if (!gui::Initialize("Robot Simulation", 1280, 720)) return 0; HAL_RegisterExtensionListener( nullptr, [](void*, const char* name, void* data) { if (wpi::StringRef{name} == "ds_socket") { DriverStationGui::SetDSSocketExtension(data); } }); - HAL_SetMain(nullptr, HALSimGui::Main, HALSimGui::Exit); + HAL_SetMain( + nullptr, + [](void*) { + gui::Main(); + glass::DestroyContext(); + gui::DestroyContext(); + }, + [](void*) { gui::Exit(); }); wpi::outs() << "Simulator GUI Initialized!\n"; return 0; diff --git a/simulation/halsim_gui/src/main/native/include/GuiDataSource.h b/simulation/halsim_gui/src/main/native/include/HALDataSource.h similarity index 75% rename from simulation/halsim_gui/src/main/native/include/GuiDataSource.h rename to simulation/halsim_gui/src/main/native/include/HALDataSource.h index 117d6ea340..e523325e67 100644 --- a/simulation/halsim_gui/src/main/native/include/GuiDataSource.h +++ b/simulation/halsim_gui/src/main/native/include/HALDataSource.h @@ -7,78 +7,13 @@ #pragma once -#include -#include - -#include -#include -#include -#include -#include - -namespace halsimgui { - -/** - * A data source. - */ -class GuiDataSource { - public: - explicit GuiDataSource(const wpi::Twine& id); - GuiDataSource(const wpi::Twine& id, int index); - GuiDataSource(const wpi::Twine& id, int index, int index2); - ~GuiDataSource(); - - GuiDataSource(const GuiDataSource&) = delete; - GuiDataSource& operator=(const GuiDataSource&) = delete; - - const char* GetId() const { return m_id.c_str(); } - - void SetName(const wpi::Twine& name) { m_name = name.str(); } - const char* GetName() const { return m_name.c_str(); } - - void SetDigital(bool digital) { m_digital = digital; } - bool IsDigital() const { return m_digital; } - - void SetValue(double value) { - m_value = value; - valueChanged(value); - } - double GetValue() const { return m_value; } - - // drag source helpers - void LabelText(const char* label, const char* fmt, ...) const; - void LabelTextV(const char* label, const char* fmt, va_list args) const; - bool Combo(const char* label, int* current_item, const char* const items[], - int items_count, int popup_max_height_in_items = -1) const; - bool SliderFloat(const char* label, float* v, float v_min, float v_max, - const char* format = "%.3f", float power = 1.0f) const; - bool InputDouble(const char* label, double* v, double step = 0.0, - double step_fast = 0.0, const char* format = "%.6f", - ImGuiInputTextFlags flags = 0) const; - bool InputInt(const char* label, int* v, int step = 1, int step_fast = 100, - ImGuiInputTextFlags flags = 0) const; - void EmitDrag(ImGuiDragDropFlags flags = 0) const; - - wpi::sig::SignalBase valueChanged; - - static GuiDataSource* Find(wpi::StringRef id); - - static wpi::sig::Signal sourceCreated; - - private: - std::string m_id; - std::string m_name; - bool m_digital = false; - std::atomic m_value = 0; -}; - -} // namespace halsimgui +#include #define HALSIMGUI_DATASOURCE(cbname, id, TYPE, vtype) \ - class cbname##Source : public ::halsimgui::GuiDataSource { \ + class cbname##Source : public ::glass::DataSource { \ public: \ cbname##Source() \ - : GuiDataSource(id), \ + : DataSource(id), \ m_callback{ \ HALSIM_Register##cbname##Callback(CallbackFunc, this, true)} { \ SetDigital(HAL_##TYPE == HAL_BOOLEAN); \ @@ -108,10 +43,10 @@ class GuiDataSource { HALSIMGUI_DATASOURCE(cbname, id, INT, int) #define HALSIMGUI_DATASOURCE_INDEXED(cbname, id, TYPE, vtype) \ - class cbname##Source : public ::halsimgui::GuiDataSource { \ + class cbname##Source : public ::glass::DataSource { \ public: \ explicit cbname##Source(int32_t index, int channel = -1) \ - : GuiDataSource(id, channel < 0 ? index : channel), \ + : DataSource(id, channel < 0 ? index : channel), \ m_index{index}, \ m_channel{channel < 0 ? index : channel}, \ m_callback{HALSIM_Register##cbname##Callback(index, CallbackFunc, \ @@ -147,10 +82,10 @@ class GuiDataSource { HALSIMGUI_DATASOURCE_INDEXED(cbname, id, DOUBLE, double) #define HALSIMGUI_DATASOURCE_INDEXED2(cbname, id, TYPE, vtype) \ - class cbname##Source : public ::halsimgui::GuiDataSource { \ + class cbname##Source : public ::glass::DataSource { \ public: \ explicit cbname##Source(int32_t index, int32_t channel) \ - : GuiDataSource(id, index, channel), \ + : DataSource(id, index, channel), \ m_index{index}, \ m_channel{channel}, \ m_callback{HALSIM_Register##cbname##Callback( \ diff --git a/simulation/halsim_gui/src/main/native/include/HALProvider.h b/simulation/halsim_gui/src/main/native/include/HALProvider.h new file mode 100644 index 0000000000..80ec614656 --- /dev/null +++ b/simulation/halsim_gui/src/main/native/include/HALProvider.h @@ -0,0 +1,50 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include + +namespace halsimgui { + +class HALProvider : public glass::Provider<> { + public: + explicit HALProvider(const wpi::Twine& iniName) : Provider{iniName} {} + + void DisplayMenu() override; + + glass::Model* GetModel(wpi::StringRef name); + + /** + * Returns true if outputs are disabled. + * + * @return true if outputs are disabled, false otherwise. + */ + static bool AreOutputsDisabled(); + + /** + * Returns true if outputs are enabled. + * + * @return true if outputs are enabled, false otherwise. + */ + static bool AreOutputsEnabled() { return !AreOutputsDisabled(); } + + private: + void Update() override; + + void Show(ViewEntry* entry, glass::Window* window) override; +}; + +} // namespace halsimgui diff --git a/simulation/halsim_gui/src/main/native/include/HALSimGui.h b/simulation/halsim_gui/src/main/native/include/HALSimGui.h index 4a4c912002..342988afef 100644 --- a/simulation/halsim_gui/src/main/native/include/HALSimGui.h +++ b/simulation/halsim_gui/src/main/native/include/HALSimGui.h @@ -7,144 +7,23 @@ #pragma once -#ifdef __cplusplus -#include -#endif +#include +#include +#include -extern "C" { - -void HALSIMGUI_Add(void* param, void (*initialize)(void*)); -void HALSIMGUI_AddExecute(void* param, void (*execute)(void*)); -void HALSIMGUI_AddWindow(const char* name, void* param, void (*display)(void*), - int32_t flags); -void HALSIMGUI_AddMainMenu(void* param, void (*menu)(void*)); -void HALSIMGUI_AddOptionMenu(void* param, void (*menu)(void*)); -void HALSIMGUI_SetWindowVisibility(const char* name, int32_t visibility); -void HALSIMGUI_SetDefaultWindowPos(const char* name, float x, float y); -void HALSIMGUI_SetDefaultWindowSize(const char* name, float width, - float height); -void HALSIMGUI_SetWindowPadding(const char* name, float x, float y); -int HALSIMGUI_AreOutputsDisabled(void); - -} // extern "C" - -#ifdef __cplusplus +#include "HALProvider.h" namespace halsimgui { class HALSimGui { public: static void GlobalInit(); - static bool Initialize(); - static void Main(void*); - static void Exit(void*); - /** - * Adds feature to GUI. The initialize function is called once, immediately - * after the GUI (both GLFW and Dear ImGui) are initialized. - * - * @param initialize initialization function - * @param execute frame execution function - */ - static void Add(std::function initialize); + static glass::MainMenuBar mainMenu; + static glass::WindowManager manager; - /** - * Adds per-frame executor to GUI. The passed function is called on each - * Dear ImGui frame prior to window and menu functions. - * - * @param execute frame execution function - */ - static void AddExecute(std::function execute); - - /** - * Adds window to GUI. The display function is called from within a - * ImGui::Begin()/End() block. While windows can be created within the - * execute function passed to AddExecute(), using this function ensures the - * windows are consistently integrated with the rest of the GUI. - * - * On each Dear ImGui frame, AddExecute() functions are always called prior - * to AddWindow display functions. Note that windows may be shaded or - * completely hidden, in which case this function will not be called. - * It's important to perform any processing steps that must be performed - * every frame in the AddExecute() function. - * - * @param name name of the window (title bar) - * @param display window contents display function - * @param flags Dear ImGui window flags - */ - static void AddWindow(const char* name, std::function display, - int flags = 0); - - /** - * Adds to GUI's main menu bar. The menu function is called from within a - * ImGui::BeginMainMenuBar()/EndMainMenuBar() block. Usually it's only - * appropriate to create a menu with ImGui::BeginMenu()/EndMenu() inside of - * this function. - * - * On each Dear ImGui frame, AddExecute() functions are always called prior - * to AddMainMenu menu functions. - * - * @param menu menu display function - */ - static void AddMainMenu(std::function menu); - - /** - * Adds to GUI's option menu. The menu function is called from within a - * ImGui::BeginMenu()/EndMenu() block. Usually it's only appropriate to - * create menu items inside of this function. - * - * On each Dear ImGui frame, AddExecute() functions are always called prior - * to AddMainMenu menu functions. - * - * @param menu menu display function - */ - static void AddOptionMenu(std::function menu); - - enum WindowVisibility { kHide = 0, kShow, kDisabled }; - - /** - * Sets visibility of window added with AddWindow(). - * - * @param name window name (same as name passed to AddWindow()) - * @param visibility 0=hide, 1=show, 2=disabled (force-hide) - */ - static void SetWindowVisibility(const char* name, - WindowVisibility visibility); - - /** - * Sets default position of window added with AddWindow(). - * - * @param name window name (same as name passed to AddWindow()) - * @param x x location of upper left corner - * @param y y location of upper left corner - */ - static void SetDefaultWindowPos(const char* name, float x, float y); - - /** - * Sets default size of window added with AddWindow(). - * - * @param name window name (same as name passed to AddWindow()) - * @param width width - * @param height height - */ - static void SetDefaultWindowSize(const char* name, float width, float height); - - /** - * Sets internal padding of window added with AddWindow(). - * @param name window name (same as name passed to AddWindow()) - * @param x horizontal padding - * @param y vertical padding - */ - static void SetWindowPadding(const char* name, float x, float y); - - /** - * Returns true if outputs are disabled. - * - * @return true if outputs are disabled, false otherwise. - */ - static bool AreOutputsDisabled(); + static HALProvider halProvider; + static glass::NetworkTablesProvider ntProvider; }; } // namespace halsimgui - -#endif // __cplusplus diff --git a/simulation/halsim_gui/src/main/native/include/IniSaver.inl b/simulation/halsim_gui/src/main/native/include/IniSaver.inl deleted file mode 100644 index 007ad5a8f1..0000000000 --- a/simulation/halsim_gui/src/main/native/include/IniSaver.inl +++ /dev/null @@ -1,56 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2020 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#pragma once - -namespace halsimgui { - -template -void IniSaver::Initialize() { - // hook ini handler to save settings - ImGuiSettingsHandler iniHandler; - iniHandler.TypeName = m_typeName; - iniHandler.TypeHash = ImHashStr(m_typeName); - iniHandler.ReadOpenFn = ReadOpen; - iniHandler.ReadLineFn = ReadLine; - iniHandler.WriteAllFn = WriteAll; - iniHandler.UserData = this; - ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler); -} - -template -void* IniSaver::ReadOpen(ImGuiContext* ctx, ImGuiSettingsHandler* handler, - const char* name) { - auto self = static_cast(handler->UserData); - int num; - if (wpi::StringRef{name}.getAsInteger(10, num)) return nullptr; - return &self->m_map[num]; -} - -template -void IniSaver::ReadLine(ImGuiContext* ctx, ImGuiSettingsHandler* handler, - void* entry, const char* lineStr) { - auto element = static_cast(entry); - wpi::StringRef line{lineStr}; - auto [name, value] = line.split('='); - name = name.trim(); - value = value.trim(); - element->ReadIni(name, value); -} - -template -void IniSaver::WriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, - ImGuiTextBuffer* out_buf) { - auto self = static_cast(handler->UserData); - for (auto&& it : self->m_map) { - out_buf->appendf("[%s][%d]\n", self->m_typeName, it.first); - it.second.WriteIni(out_buf); - out_buf->append("\n"); - } -} - -} // namespace halsimgui diff --git a/simulation/halsim_gui/src/main/native/include/IniSaverString.inl b/simulation/halsim_gui/src/main/native/include/IniSaverString.inl deleted file mode 100644 index 5ac7dc29b3..0000000000 --- a/simulation/halsim_gui/src/main/native/include/IniSaverString.inl +++ /dev/null @@ -1,57 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2020 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#pragma once - -namespace halsimgui { - -template -void IniSaverString::Initialize() { - // hook ini handler to save settings - ImGuiSettingsHandler iniHandler; - iniHandler.TypeName = m_typeName; - iniHandler.TypeHash = ImHashStr(m_typeName); - iniHandler.ReadOpenFn = ReadOpen; - iniHandler.ReadLineFn = ReadLine; - iniHandler.WriteAllFn = WriteAll; - iniHandler.UserData = this; - ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler); -} - -template -void* IniSaverString::ReadOpen(ImGuiContext* ctx, - ImGuiSettingsHandler* handler, - const char* name) { - auto self = static_cast(handler->UserData); - return &self->m_map[name]; -} - -template -void IniSaverString::ReadLine(ImGuiContext* ctx, - ImGuiSettingsHandler* handler, void* entry, - const char* lineStr) { - auto element = static_cast(entry); - wpi::StringRef line{lineStr}; - auto [name, value] = line.split('='); - name = name.trim(); - value = value.trim(); - element->ReadIni(name, value); -} - -template -void IniSaverString::WriteAll(ImGuiContext* ctx, - ImGuiSettingsHandler* handler, - ImGuiTextBuffer* out_buf) { - auto self = static_cast(handler->UserData); - for (auto&& it : self->m_map) { - out_buf->appendf("[%s][%s]\n", self->m_typeName, it.getKey().data()); - it.second.WriteIni(out_buf); - out_buf->append("\n"); - } -} - -} // namespace halsimgui diff --git a/simulation/halsim_gui/src/main/native/include/IniSaverVector.inl b/simulation/halsim_gui/src/main/native/include/IniSaverVector.inl deleted file mode 100644 index b2979bc509..0000000000 --- a/simulation/halsim_gui/src/main/native/include/IniSaverVector.inl +++ /dev/null @@ -1,60 +0,0 @@ -/*----------------------------------------------------------------------------*/ -/* Copyright (c) 2020 FIRST. All Rights Reserved. */ -/* Open Source Software - may be modified and shared by FRC teams. The code */ -/* must be accompanied by the FIRST BSD license file in the root directory of */ -/* the project. */ -/*----------------------------------------------------------------------------*/ - -#pragma once - -namespace halsimgui { - -template -void IniSaverVector::Initialize() { - // hook ini handler to save settings - ImGuiSettingsHandler iniHandler; - iniHandler.TypeName = m_typeName; - iniHandler.TypeHash = ImHashStr(m_typeName); - iniHandler.ReadOpenFn = ReadOpen; - iniHandler.ReadLineFn = ReadLine; - iniHandler.WriteAllFn = WriteAll; - iniHandler.UserData = this; - ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler); -} - -template -void* IniSaverVector::ReadOpen(ImGuiContext* ctx, - ImGuiSettingsHandler* handler, - const char* name) { - auto self = static_cast(handler->UserData); - unsigned int num; - if (wpi::StringRef{name}.getAsInteger(10, num)) return nullptr; - if (num >= self->size()) self->resize(num + 1); - return &(*self)[num]; -} - -template -void IniSaverVector::ReadLine(ImGuiContext* ctx, - ImGuiSettingsHandler* handler, void* entry, - const char* lineStr) { - auto element = static_cast(entry); - wpi::StringRef line{lineStr}; - auto [name, value] = line.split('='); - name = name.trim(); - value = value.trim(); - element->ReadIni(name, value); -} - -template -void IniSaverVector::WriteAll(ImGuiContext* ctx, - ImGuiSettingsHandler* handler, - ImGuiTextBuffer* out_buf) { - auto self = static_cast(handler->UserData); - for (size_t i = 0; i < self->size(); ++i) { - out_buf->appendf("[%s][%d]\n", self->m_typeName, static_cast(i)); - (*self)[i].WriteIni(out_buf); - out_buf->append("\n"); - } -} - -} // namespace halsimgui diff --git a/simulation/halsim_gui/src/main/native/include/SimDeviceGui.h b/simulation/halsim_gui/src/main/native/include/SimDeviceGui.h index 8c24cc9f22..52e550efdc 100644 --- a/simulation/halsim_gui/src/main/native/include/SimDeviceGui.h +++ b/simulation/halsim_gui/src/main/native/include/SimDeviceGui.h @@ -7,103 +7,20 @@ #pragma once -#include +#include -#include -#include - -#ifdef __cplusplus -#include - -#include -#endif - -extern "C" { - -void HALSIMGUI_DeviceTreeHide(const char* name); -void HALSIMGUI_DeviceTreeAdd(void* param, void (*execute)(void*)); -HAL_Bool HALSIMGUI_DeviceTreeDisplayValue(const char* name, HAL_Bool readonly, - struct HAL_Value* value, - const char** options, - int32_t numOptions); -HAL_Bool HALSIMGUI_DeviceTreeStartDevice(const char* label, int32_t flags); -void HALSIMGUI_DeviceTreeFinishDevice(void); - -} // extern "C" - -#ifdef __cplusplus +namespace glass { +class DataSource; +class DeviceTreeModel; +} // namespace glass namespace halsimgui { -class GuiDataSource; - class SimDeviceGui { public: static void Initialize(); - - /** - * Hides device on tree. - * - * @param name device name - */ - static void Hide(const char* name); - - /** - * Adds device to tree. The execute function is called from within the - * device tree window context on every frame, so it should implement an - * TreeNodeEx() block for each device to display. - * - * @param execute execute function - */ - static void Add(std::function execute); - - /** - * Displays device value formatted the same way as SimDevice device values. - * - * @param name value name - * @param readonly prevent value from being modified by the user - * @param value value contents (modified in place) - * @param options options array for enum values - * @param numOptions size of options array for enum values - * @return True if value was modified by the user - */ - static bool DisplayValue(const char* name, bool readonly, HAL_Value* value, - const char** options = nullptr, - int32_t numOptions = 0); - - /** - * Displays device value formatted the same way as SimDevice device values. - * - * @param name value name - * @param readonly prevent value from being modified by the user - * @param value value contents (modified in place) - * @param source data source (may be nullptr) - * @param options options array for enum values - * @param numOptions size of options array for enum values - * @return True if value was modified by the user - */ - static bool DisplayValueSource(const char* name, bool readonly, - HAL_Value* value, const GuiDataSource* source, - const char** options = nullptr, - int32_t numOptions = 0); - - /** - * Wraps ImGui::CollapsingHeader() to provide consistency and open - * persistence. As with the ImGui function, returns true if the tree node - * is expanded. If returns true, call StopDevice() to finish the block. - * - * @param label label - * @param flags ImGuiTreeNodeFlags flags - * @return True if expanded - */ - static bool StartDevice(const char* label, ImGuiTreeNodeFlags flags = 0); - - /** - * Finish a device block started with StartDevice(). - */ - static void FinishDevice(); + static glass::DataSource* GetValueSource(HAL_SimValueHandle handle); + static glass::DeviceTreeModel& GetDeviceTree(); }; } // namespace halsimgui - -#endif // __cplusplus