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