diff --git a/imgui/CMakeLists.txt.in b/imgui/CMakeLists.txt.in index 4fe06e6615..998837d33d 100644 --- a/imgui/CMakeLists.txt.in +++ b/imgui/CMakeLists.txt.in @@ -33,7 +33,7 @@ ExternalProject_Add(imgui ) ExternalProject_Add(implot GIT_REPOSITORY https://github.com/epezent/implot.git - GIT_TAG db16011e7398e6d9ef062fbd59338ddb689e99c6 + GIT_TAG 90693cca1bd0ca5f0d49bc9cb8187d56b0b8f289 SOURCE_DIR "${CMAKE_CURRENT_BINARY_DIR}/implot-src" BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/implot-build" CONFIGURE_COMMAND "" diff --git a/shared/config.gradle b/shared/config.gradle index a975f342ed..78dad1fb61 100644 --- a/shared/config.gradle +++ b/shared/config.gradle @@ -11,7 +11,7 @@ nativeUtils { niLibVersion = "2020.10.1" opencvVersion = "3.4.7-3" googleTestVersion = "1.9.0-5-437e100-1" - imguiVersion = "1.76-6" + imguiVersion = "1.76-7" wpimathVersion = "-1" } } diff --git a/simulation/halsim_gui/src/main/native/cpp/AccelerometerGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AccelerometerGui.cpp index 5a8ec8b93e..fa772ad912 100644 --- a/simulation/halsim_gui/src/main/native/cpp/AccelerometerGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/AccelerometerGui.cpp @@ -8,15 +8,35 @@ #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")) { @@ -28,18 +48,21 @@ static void DisplayAccelerometers() { SimDeviceGui::DisplayValue("Range", true, &value, rangeOptions, 3); // X Accel - value = HAL_MakeDouble(HALSIM_GetAccelerometerX(0)); - if (SimDeviceGui::DisplayValue("X Accel", false, &value)) + 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(HALSIM_GetAccelerometerY(0)); - if (SimDeviceGui::DisplayValue("Y Accel", false, &value)) + 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(HALSIM_GetAccelerometerZ(0)); - if (SimDeviceGui::DisplayValue("Z Accel", false, &value)) + value = HAL_MakeDouble(gAccelZSource->GetValue()); + if (SimDeviceGui::DisplayValueSource("Z Accel", false, &value, + gAccelZSource.get())) HALSIM_SetAccelerometerZ(0, value.data.v_double); SimDeviceGui::FinishDevice(); @@ -47,5 +70,6 @@ static void DisplayAccelerometers() { } void AccelerometerGui::Initialize() { + HALSimGui::AddExecute(UpdateAccelSources); SimDeviceGui::Add(DisplayAccelerometers); } diff --git a/simulation/halsim_gui/src/main/native/cpp/AnalogGyroGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AnalogGyroGui.cpp index 37c1faddc4..5df30e6005 100644 --- a/simulation/halsim_gui/src/main/native/cpp/AnalogGyroGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/AnalogGyroGui.cpp @@ -8,38 +8,73 @@ #include "AnalogGyroGui.h" #include +#include +#include #include #include #include #include +#include "GuiDataSource.h" +#include "HALSimGui.h" #include "SimDeviceGui.h" using namespace halsimgui; -static void DisplayAnalogGyros() { - static int numAccum = HAL_GetNumAccumulators(); - for (int i = 0; i < numAccum; ++i) { - if (!HALSIM_GetAnalogGyroInitialized(i)) continue; - char name[32]; - std::snprintf(name, sizeof(name), "AnalogGyro[%d]", i); - if (SimDeviceGui::StartDevice(name)) { - HAL_Value value; +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 - // angle - value = HAL_MakeDouble(HALSIM_GetAnalogGyroAngle(i)); - if (SimDeviceGui::DisplayValue("Angle", false, &value)) - HALSIM_SetAnalogGyroAngle(i, value.data.v_double); +static std::vector> gAnalogGyroSources; - // rate - value = HAL_MakeDouble(HALSIM_GetAnalogGyroRate(i)); - if (SimDeviceGui::DisplayValue("Rate", false, &value)) - HALSIM_SetAnalogGyroRate(i, value.data.v_double); - - SimDeviceGui::FinishDevice(); +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(); } } } -void AnalogGyroGui::Initialize() { SimDeviceGui::Add(DisplayAnalogGyros); } +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/AnalogInputGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AnalogInputGui.cpp index 80cfe401ad..8ba1459b84 100644 --- a/simulation/halsim_gui/src/main/native/cpp/AnalogInputGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/AnalogInputGui.cpp @@ -7,28 +7,52 @@ #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; -static IniSaver gAnalogInputs{"AnalogInput"}; // indexed by channel +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 int numAnalog = HAL_GetNumAnalogInputs(); - static int numAccum = HAL_GetNumAccumulators(); + static const int numAccum = HAL_GetNumAccumulators(); bool first = true; - for (int i = 0; i < numAnalog; ++i) { - if (HALSIM_GetAnalogInInitialized(i)) { + for (int i = 0, iend = gAnalogInputSources.size(); i < iend; ++i) { + if (auto source = gAnalogInputSources[i].get()) { + ImGui::PushID(i); hasInputs = true; if (!first) { @@ -39,26 +63,29 @@ static void DisplayAnalogInputs() { } auto& info = gAnalogInputs[i]; - // build name - char name[128]; - info.GetName(name, sizeof(name), "In", 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(name, "AnalogGyro[%d]", i); + 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(name, "%s", HALSIM_GetSimDeviceName(simDevice)); + ImGui::LabelText(label, "%s", HALSIM_GetSimDeviceName(simDevice)); ImGui::PopStyleColor(); } else { - float val = HALSIM_GetAnalogInVoltage(i); - if (ImGui::SliderFloat(name, &val, 0.0, 5.0)) + float val = source->GetValue(); + if (source->SliderFloat(label, &val, 0.0, 5.0)) HALSIM_SetAnalogInVoltage(i, val); } // context menu to change name - info.PopupEditName(i); + if (info.PopupEditName(i)) { + source->SetName(info.GetName()); + } + ImGui::PopID(); } } if (!hasInputs) ImGui::Text("No analog inputs"); @@ -66,6 +93,9 @@ static void DisplayAnalogInputs() { 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/AnalogOutGui.cpp b/simulation/halsim_gui/src/main/native/cpp/AnalogOutGui.cpp index 89b70ce7fe..3e7f4ea6ca 100644 --- a/simulation/halsim_gui/src/main/native/cpp/AnalogOutGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/AnalogOutGui.cpp @@ -7,40 +7,66 @@ #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() { - static const int numAnalog = HAL_GetNumAnalogOutputs(); - static auto init = std::make_unique(numAnalog); - int count = 0; - for (int i = 0; i < numAnalog; ++i) { - init[i] = HALSIM_GetAnalogOutInitialized(i); - if (init[i]) ++count; + for (auto&& source : gAnalogOutSources) { + if (source) ++count; } if (count == 0) return; if (SimDeviceGui::StartDevice("Analog Outputs")) { - for (int i = 0; i < numAnalog; ++i) { - if (!init[i]) continue; + for (int i = 0, iend = gAnalogOutSources.size(); i < iend; ++i) { + if (auto source = gAnalogOutSources[i].get()) { + ImGui::PushID(i); - auto& info = gAnalogOuts[i]; - char name[128]; - info.GetName(name, sizeof(name), "Out", i); - HAL_Value value = HAL_MakeDouble(HALSIM_GetAnalogOutVoltage(i)); - SimDeviceGui::DisplayValue(name, true, &value); - info.PopupEditName(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(); @@ -49,5 +75,7 @@ static void DisplayAnalogOutputs() { 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/CompressorGui.cpp b/simulation/halsim_gui/src/main/native/cpp/CompressorGui.cpp index b2322320b2..43215fa27b 100644 --- a/simulation/halsim_gui/src/main/native/cpp/CompressorGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/CompressorGui.cpp @@ -8,56 +8,97 @@ #include "CompressorGui.h" #include +#include +#include #include #include #include #include +#include "GuiDataSource.h" #include "HALSimGui.h" #include "SimDeviceGui.h" using namespace halsimgui; -static void DisplayCompressors() { - static int numPcm = HAL_GetNumPCMModules(); - for (int i = 0; i < numPcm; ++i) { - if (!HALSIM_GetPCMCompressorInitialized(i)) continue; - char name[32]; - std::snprintf(name, sizeof(name), "Compressor[%d]", i); - if (SimDeviceGui::StartDevice(name)) { - HAL_Value value; +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 - // enabled - if (HALSimGui::AreOutputsDisabled()) - value = HAL_MakeBoolean(false); - else - value = HAL_MakeBoolean(HALSIM_GetPCMCompressorOn(i)); - if (SimDeviceGui::DisplayValue("Running", false, &value)) - HALSIM_SetPCMCompressorOn(i, value.data.v_boolean); +static std::vector> gCompressorSources; - // closed loop - value = HAL_MakeEnum(HALSIM_GetPCMClosedLoopEnabled(i) ? 1 : 0); - static const char* enabledOptions[] = {"disabled", "enabled"}; - if (SimDeviceGui::DisplayValue("Closed Loop", true, &value, - enabledOptions, 2)) - HALSIM_SetPCMClosedLoopEnabled(i, value.data.v_enum); - - // pressure switch - value = HAL_MakeEnum(HALSIM_GetPCMPressureSwitch(i) ? 1 : 0); - static const char* switchOptions[] = {"full", "low"}; - if (SimDeviceGui::DisplayValue("Pressure", false, &value, switchOptions, - 2)) - HALSIM_SetPCMPressureSwitch(i, value.data.v_enum); - - // compressor current - value = HAL_MakeDouble(HALSIM_GetPCMCompressorCurrent(i)); - if (SimDeviceGui::DisplayValue("Current (A)", false, &value)) - HALSIM_SetPCMCompressorCurrent(i, value.data.v_double); - - SimDeviceGui::FinishDevice(); +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(); } } } -void CompressorGui::Initialize() { SimDeviceGui::Add(DisplayCompressors); } +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 index f3678956da..2f82ff2930 100644 --- a/simulation/halsim_gui/src/main/native/cpp/DIOGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/DIOGui.cpp @@ -7,6 +7,9 @@ #include "DIOGui.h" +#include +#include + #include #include #include @@ -15,13 +18,23 @@ #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)); @@ -29,12 +42,62 @@ static void LabelSimDevice(const char* name, HAL_SimDeviceHandle 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; - static int numDIO = HAL_GetNumDigitalChannels(); - static int numPWM = HAL_GetNumDigitalPWMOutputs(); - static int numEncoder = HAL_GetNumEncoders(); - static int numDutyCycle = HAL_GetNumDutyCycles(); + 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); @@ -44,8 +107,8 @@ static void DisplayDIO() { std::memset(dutyCycleMap.get(), 0, numDIO * sizeof(dutyCycleMap[0])); for (int i = 0; i < numPWM; ++i) { - if (HALSIM_GetDigitalPWMInitialized(i)) { - int channel = HALSIM_GetDigitalPWMPin(i); + if (auto source = gDPWMSources[i].get()) { + int channel = source->GetChannel(); if (channel >= 0 && channel < numDIO) pwmMap[channel] = i + 1; } } @@ -61,66 +124,76 @@ static void DisplayDIO() { } for (int i = 0; i < numDutyCycle; ++i) { - if (HALSIM_GetDutyCycleInitialized(i)) { - int channel = HALSIM_GetDutyCycleDigitalChannel(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 (HALSIM_GetDIOInitialized(i)) { + if (auto dioSource = gDIOSources[i].get()) { + ImGui::PushID(i); hasAny = true; + DigitalPWMDutyCycleSource* dpwmSource = nullptr; + DutyCycleOutputSource* dutyCycleSource = nullptr; auto& info = gDIO[i]; - char name[128]; + char label[128]; if (pwmMap[i] > 0) { - info.GetName(name, sizeof(name), "PWM", i); + dpwmSource = gDPWMSources[pwmMap[i] - 1].get(); + info.GetLabel(label, sizeof(label), "PWM", i); if (auto simDevice = HALSIM_GetDIOSimDevice(i)) { - LabelSimDevice(name, simDevice); + LabelSimDevice(label, simDevice); } else { - ImGui::LabelText(name, "%0.3f", - HALSIM_GetDigitalPWMDutyCycle(pwmMap[i] - 1)); + dpwmSource->LabelText(label, "%0.3f", dpwmSource->GetValue()); } } else if (encoderMap[i] > 0) { - info.GetName(name, sizeof(name), " In", i); + info.GetLabel(label, sizeof(label), " In", i); if (auto simDevice = HALSIM_GetEncoderSimDevice(encoderMap[i] - 1)) { - LabelSimDevice(name, simDevice); + LabelSimDevice(label, simDevice); } else { ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255)); - ImGui::LabelText(name, "Encoder[%d,%d]", + ImGui::LabelText(label, "Encoder[%d,%d]", HALSIM_GetEncoderDigitalChannelA(encoderMap[i] - 1), HALSIM_GetEncoderDigitalChannelB(encoderMap[i] - 1)); ImGui::PopStyleColor(); } } else if (dutyCycleMap[i] > 0) { - info.GetName(name, sizeof(name), "Dty", i); + dutyCycleSource = gDutyCycleSources[dutyCycleMap[i] - 1].get(); + info.GetLabel(label, sizeof(label), "Dty", i); if (auto simDevice = HALSIM_GetDutyCycleSimDevice(dutyCycleMap[i] - 1)) { - LabelSimDevice(name, simDevice); + LabelSimDevice(label, simDevice); } else { - double val = HALSIM_GetDutyCycleOutput(dutyCycleMap[i] - 1); - if (ImGui::InputDouble(name, &val)) + double val = dutyCycleSource->GetValue(); + if (dutyCycleSource->InputDouble(label, &val)) HALSIM_SetDutyCycleOutput(dutyCycleMap[i] - 1, val); } } else if (!HALSIM_GetDIOIsInput(i)) { - info.GetName(name, sizeof(name), "Out", i); + info.GetLabel(label, sizeof(label), "Out", i); if (auto simDevice = HALSIM_GetDIOSimDevice(i)) { - LabelSimDevice(name, simDevice); + LabelSimDevice(label, simDevice); } else { - ImGui::LabelText(name, "%s", - HALSIM_GetDIOValue(i) ? "1 (high)" : "0 (low)"); + dioSource->LabelText( + label, "%s", dioSource->GetValue() != 0 ? "1 (high)" : "0 (low)"); } } else { - info.GetName(name, sizeof(name), " In", i); + info.GetLabel(label, sizeof(label), " In", i); if (auto simDevice = HALSIM_GetDIOSimDevice(i)) { - LabelSimDevice(name, simDevice); + LabelSimDevice(label, simDevice); } else { static const char* options[] = {"0 (low)", "1 (high)"}; - int val = HALSIM_GetDIOValue(i) ? 1 : 0; - if (ImGui::Combo(name, &val, options, 2)) HALSIM_SetDIOValue(i, val); + int val = dioSource->GetValue() != 0 ? 1 : 0; + if (dioSource->Combo(label, &val, options, 2)) + HALSIM_SetDIOValue(i, val); } } - info.PopupEditName(i); + if (info.PopupEditName(i)) { + dioSource->SetName(info.GetName()); + if (dpwmSource) dpwmSource->SetName(info.GetName()); + if (dutyCycleSource) dutyCycleSource->SetName(info.GetName()); + } + ImGui::PopID(); } } ImGui::PopItemWidth(); @@ -129,6 +202,13 @@ static void DisplayDIO() { 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/DriverStationGui.cpp b/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.cpp index 61eb04bdaa..89050279b5 100644 --- a/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/DriverStationGui.cpp @@ -8,7 +8,9 @@ #include "DriverStationGui.h" #include +#include #include +#include #include #include @@ -21,6 +23,7 @@ #include #include "ExtraGuiWidgets.h" +#include "GuiDataSource.h" #include "HALSimGui.h" #include "IniSaverInfo.h" @@ -63,6 +66,31 @@ struct RobotJoystick { bool IsButtonPressed(int i) { return (buttons.buttons & (1u << i)) != 0; } }; +class JoystickSource { + public: + explicit JoystickSource(int index); + ~JoystickSource() { + HALSIM_CancelDriverStationNewDataCallback(m_callback); + for (int i = 0; i < buttonCount; ++i) delete buttons[i]; + } + JoystickSource(const JoystickSource&) = delete; + JoystickSource& operator=(const JoystickSource&) = delete; + + int axisCount; + int buttonCount; + int povCount; + 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]; + + private: + static void CallbackFunc(const char*, void* param, const HAL_Value*); + + int m_index; + int32_t m_callback; +}; } // namespace // system joysticks @@ -71,9 +99,67 @@ static int gNumSystemJoysticks = 0; // robot joysticks static RobotJoystick gRobotJoysticks[HAL_kMaxJoysticks]; +static std::unique_ptr gJoystickSources[HAL_kMaxJoysticks]; static bool gDisableDS = false; +JoystickSource::JoystickSource(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{']'}); + } + + HAL_JoystickButtons halButtons; + HALSIM_GetJoystickButtons(index, &halButtons); + 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{']'}); + buttons[i]->SetDigital(true); + } + for (int i = buttonCount; i < 32; ++i) buttons[i] = nullptr; + + HAL_JoystickPOVs halPOVs; + 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{']'}); + } + + m_callback = + HALSIM_RegisterDriverStationNewDataCallback(CallbackFunc, this, true); +} + +void JoystickSource::CallbackFunc(const char*, void* param, const HAL_Value*) { + auto self = static_cast(param); + + HAL_JoystickAxes halAxes; + HALSIM_GetJoystickAxes(self->m_index, &halAxes); + for (int i = 0; i < halAxes.count; ++i) { + if (auto axis = self->axes[i].get()) axis->SetValue(halAxes.axes[i]); + } + + HAL_JoystickButtons halButtons; + HALSIM_GetJoystickButtons(self->m_index, &halButtons); + for (int i = 0; i < halButtons.count; ++i) { + if (auto button = self->buttons[i]) + button->SetValue((halButtons.buttons & (1u << i)) != 0 ? 1 : 0); + } + + HAL_JoystickPOVs halPOVs; + HALSIM_GetJoystickPOVs(self->m_index, &halPOVs); + for (int i = 0; i < halPOVs.count; ++i) { + if (auto pov = self->povs[i].get()) pov->SetValue(halPOVs.povs[i]); + } +} + // read/write joystick mapping to ini file static void* JoystickReadOpen(ImGuiContext* ctx, ImGuiSettingsHandler* handler, const char* name) { @@ -293,6 +379,22 @@ void RobotJoystick::GetHAL(int i) { } static void DriverStationExecute() { + // update sources + for (int i = 0; i < HAL_kMaxJoysticks; ++i) { + auto& source = gJoystickSources[i]; + int32_t axisCount, buttonCount, povCount; + HALSIM_GetJoystickCounts(i, &axisCount, &buttonCount, &povCount); + if (axisCount != 0 || buttonCount != 0 || povCount != 0) { + if (!source || source->axisCount != axisCount || + source->buttonCount != buttonCount || source->povCount != povCount) { + source.reset(); + source = std::make_unique(i); + } + } else { + source.reset(); + } + } + static bool prevDisableDS = false; if (gDisableDS && !prevDisableDS) { HALSimGui::SetWindowVisibility("System Joysticks", HALSimGui::kDisabled); @@ -464,7 +566,7 @@ static void DisplayJoysticks() { for (int i = 0; i < HAL_kMaxJoysticks; ++i) { auto& joy = gRobotJoysticks[i]; char label[128]; - joy.name.GetName(label, sizeof(label), "Joystick", i); + joy.name.GetLabel(label, sizeof(label), "Joystick", i); if (!gDisableDS && joy.sys) { ImGui::Selectable(label, false); if (ImGui::BeginDragDropSource()) { @@ -499,6 +601,7 @@ static void DisplayJoysticks() { for (int i = 0; i < HAL_kMaxJoysticks; ++i) { auto& joy = gRobotJoysticks[i]; + auto source = gJoystickSources[i].get(); if (gDisableDS) joy.GetHAL(i); @@ -515,11 +618,31 @@ static void DisplayJoysticks() { if (joy.sys->isGamepad) ImGui::Checkbox("Map gamepad", &joy.useGamepad); } - for (int j = 0; j < joy.axes.count; ++j) - ImGui::Text("Axis[%d]: %.3f", j, joy.axes.axes[j]); + for (int j = 0; j < joy.axes.count; ++j) { + if (source && source->axes[j]) { + char label[64]; + std::snprintf(label, sizeof(label), "Axis[%d]", j); + ImGui::Selectable(label); + source->axes[j]->EmitDrag(); + ImGui::SameLine(); + ImGui::Text(": %.3f", joy.axes.axes[j]); + } else { + ImGui::Text("Axis[%d]: %.3f", j, joy.axes.axes[j]); + } + } - for (int j = 0; j < joy.povs.count; ++j) - ImGui::Text("POVs[%d]: %d", j, joy.povs.povs[j]); + for (int j = 0; j < joy.povs.count; ++j) { + if (source && source->povs[j]) { + char label[64]; + std::snprintf(label, sizeof(label), "POVs[%d]", j); + ImGui::Selectable(label); + source->povs[j]->EmitDrag(); + ImGui::SameLine(); + ImGui::Text(": %d", joy.povs.povs[j]); + } else { + ImGui::Text("POVs[%d]: %d", j, joy.povs.povs[j]); + } + } // show buttons as multiple lines of LED indicators, 8 per line static const ImU32 color = IM_COL32(255, 255, 102, 255); @@ -527,7 +650,8 @@ static void DisplayJoysticks() { buttons.resize(joy.buttons.count); for (int j = 0; j < joy.buttons.count; ++j) buttons[j] = joy.IsButtonPressed(j) ? 1 : -1; - DrawLEDs(buttons.data(), buttons.size(), 8, &color); + DrawLEDSources(buttons.data(), source ? source->buttons : nullptr, + buttons.size(), 8, &color); ImGui::PopID(); } else { ImGui::Text("Unassigned"); diff --git a/simulation/halsim_gui/src/main/native/cpp/EncoderGui.cpp b/simulation/halsim_gui/src/main/native/cpp/EncoderGui.cpp index bbf3f2408d..5b169f0e24 100644 --- a/simulation/halsim_gui/src/main/native/cpp/EncoderGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/EncoderGui.cpp @@ -7,11 +7,16 @@ #include "EncoderGui.h" +#include +#include +#include + #include #include #include #include +#include "GuiDataSource.h" #include "HALSimGui.h" #include "IniSaver.h" #include "IniSaverInfo.h" @@ -19,6 +24,7 @@ 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; @@ -30,29 +36,179 @@ struct EncoderInfo : public NameInfo, public OpenInfo { 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; - static int numEncoder = HAL_GetNumEncoders(); ImGui::PushItemWidth(ImGui::GetFontSize() * 8); - for (int i = 0; i < numEncoder; ++i) { - if (HALSIM_GetEncoderInitialized(i)) { + 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 = HALSIM_GetEncoderDigitalChannelA(i); - int chB = HALSIM_GetEncoderDigitalChannelB(i); + int chA = source->GetChannelA(); + int chB = source->GetChannelB(); // build header name auto& info = gEncoders[chA]; char name[128]; - info.GetName(name, sizeof(name), "Encoder", chA, chB); + info.GetLabel(name, sizeof(name), "Encoder", chA, chB); // header bool open = ImGui::CollapsingHeader( @@ -60,28 +216,34 @@ static void DisplayEncoders() { info.SetOpen(open); // context menu to change name - info.PopupEditName(chA); + if (info.PopupEditName(chA)) { + source->SetName(info.GetName()); + } if (open) { ImGui::PushID(i); // distance per pulse - double distancePerPulse = HALSIM_GetEncoderDistancePerPulse(i); - ImGui::LabelText("Dist/Count", "%.6f", distancePerPulse); + double distancePerPulse = source->distancePerPulse.GetValue(); + source->distancePerPulse.LabelText("Dist/Count", "%.6f", + distancePerPulse); // count - int count = HALSIM_GetEncoderCount(i); - if (ImGui::InputInt("Count", &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 = HALSIM_GetEncoderPeriod(i); - if (ImGui::InputDouble("Period", &period, 0, 0, "%.6g")) + double period = source->period.GetValue(); + if (source->period.InputDouble("Period", &period, 0, 0, "%.6g")) HALSIM_SetEncoderPeriod(i, period); // reverse direction @@ -91,20 +253,21 @@ static void DisplayEncoders() { // direction static const char* options[] = {"reverse", "forward"}; - int direction = HALSIM_GetEncoderDirection(i) ? 1 : 0; - if (ImGui::Combo("Direction", &direction, options, 2)) + int direction = source->direction.GetValue() ? 1 : 0; + if (source->direction.Combo("Direction", &direction, options, 2)) HALSIM_SetEncoderDirection(i, direction); - ImGui::PopID(); // distance - double distance = HALSIM_GetEncoderDistance(i); - if (ImGui::InputDouble("Distance", &distance, 0, 0, "%.6g")) + double distance = source->distance.GetValue(); + if (source->distance.InputDouble("Distance", &distance, 0, 0, "%.6g")) HALSIM_SetEncoderDistance(i, distance); // rate - double rate = HALSIM_GetEncoderRate(i); - if (ImGui::InputDouble("Rate", &rate, 0, 0, "%.6g")) + double rate = source->rate.GetValue(); + if (source->rate.InputDouble("Rate", &rate, 0, 0, "%.6g")) HALSIM_SetEncoderRate(i, rate); + + ImGui::PopID(); } } } @@ -115,6 +278,8 @@ static void DisplayEncoders() { 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/ExtraGuiWidgets.cpp b/simulation/halsim_gui/src/main/native/cpp/ExtraGuiWidgets.cpp index 3b1ba2825a..b5bf92f384 100644 --- a/simulation/halsim_gui/src/main/native/cpp/ExtraGuiWidgets.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/ExtraGuiWidgets.cpp @@ -1,5 +1,5 @@ /*----------------------------------------------------------------------------*/ -/* Copyright (c) 2017-2019 FIRST. All Rights Reserved. */ +/* Copyright (c) 2017-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. */ @@ -7,10 +7,13 @@ #include "ExtraGuiWidgets.h" +#include "GuiDataSource.h" + namespace halsimgui { -void DrawLEDs(const int* values, int numValues, int cols, const ImU32* colors, - float size, float spacing, const LEDConfig& config) { +void DrawLEDSources(const int* values, GuiDataSource** sources, int numValues, + int cols, const ImU32* colors, float size, float spacing, + const LEDConfig& config) { if (numValues == 0 || cols < 1) return; if (size == 0) size = ImGui::GetFontSize() / 2.0; if (spacing == 0) spacing = ImGui::GetFontSize() / 3.0; @@ -21,35 +24,35 @@ void DrawLEDs(const int* values, int numValues, int cols, const ImU32* colors, ImDrawList* drawList = ImGui::GetWindowDrawList(); const ImVec2 p = ImGui::GetCursorScreenPos(); + float sized2 = size / 2; float ystart, yinc; if (config.start & 1) { // lower - ystart = p.y + size / 2 + inc * (rows - 1); + ystart = p.y + sized2 + inc * (rows - 1); yinc = -inc; } else { // upper - ystart = p.y + size / 2; + ystart = p.y + sized2; yinc = inc; } float xstart, xinc; if (config.start & 2) { // right - xstart = p.x + size / 2 + inc * (cols - 1); + xstart = p.x + sized2 + inc * (cols - 1); xinc = -inc; } else { // left - xstart = p.x + size / 2; + xstart = p.x + sized2; xinc = inc; } float x = xstart, y = ystart; - if (config.order == LEDConfig::RowMajor) { - // row major - int row = 1; - for (int i = 0; i < numValues; ++i) { - if (i >= (row * cols)) { - ++row; + int rowcol = 1; // row for row-major, column for column-major + for (int i = 0; i < numValues; ++i) { + if (config.order == LEDConfig::RowMajor) { + if (i >= (rowcol * cols)) { + ++rowcol; if (config.serpentine) { x -= xinc; xinc = -xinc; @@ -58,20 +61,9 @@ void DrawLEDs(const int* values, int numValues, int cols, const ImU32* colors, } y += yinc; } - if (values[i] > 0) - drawList->AddRectFilled(ImVec2(x, y), ImVec2(x + size, y + size), - colors[values[i] - 1]); - else if (values[i] < 0) - drawList->AddRect(ImVec2(x, y), ImVec2(x + size, y + size), - colors[-values[i] - 1], 0.0f, 0, 1.0); - x += xinc; - } - } else { - // column major - int col = 1; - for (int i = 0; i < numValues; ++i) { - if (i >= (col * rows)) { - ++col; + } else { + if (i >= (rowcol * rows)) { + ++rowcol; if (config.serpentine) { y -= yinc; yinc = -yinc; @@ -80,17 +72,38 @@ void DrawLEDs(const int* values, int numValues, int cols, const ImU32* colors, } x += xinc; } - if (values[i] > 0) - drawList->AddRectFilled(ImVec2(x, y), ImVec2(x + size, y + size), - colors[values[i] - 1]); - else if (values[i] < 0) - drawList->AddRect(ImVec2(x, y), ImVec2(x + size, y + size), - colors[-values[i] - 1], 0.0f, 0, 1.0); + } + if (values[i] > 0) + drawList->AddRectFilled(ImVec2(x, y), ImVec2(x + size, y + size), + colors[values[i] - 1]); + else if (values[i] < 0) + drawList->AddRect(ImVec2(x, y), ImVec2(x + size, y + size), + colors[-values[i] - 1], 0.0f, 0, 1.0); + if (sources) { + ImGui::SetCursorScreenPos(ImVec2(x - sized2, y - sized2)); + if (sources[i]) { + ImGui::PushID(i); + ImGui::Selectable("", false, 0, ImVec2(inc, inc)); + sources[i]->EmitDrag(); + ImGui::PopID(); + } else { + ImGui::Dummy(ImVec2(inc, inc)); + } + } + if (config.order == LEDConfig::RowMajor) { + x += xinc; + } else { y += yinc; } } - ImGui::Dummy(ImVec2(inc * cols, inc * rows)); + if (!sources) ImGui::Dummy(ImVec2(inc * cols, inc * rows)); +} + +void DrawLEDs(const int* values, int numValues, int cols, const ImU32* colors, + float size, float spacing, const LEDConfig& config) { + DrawLEDSources(values, nullptr, numValues, cols, colors, size, spacing, + config); } } // namespace halsimgui diff --git a/simulation/halsim_gui/src/main/native/cpp/GuiDataSource.cpp b/simulation/halsim_gui/src/main/native/cpp/GuiDataSource.cpp new file mode 100644 index 0000000000..df967c12f5 --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/GuiDataSource.cpp @@ -0,0 +1,116 @@ +/*----------------------------------------------------------------------------*/ +/* 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/IniSaverInfo.cpp b/simulation/halsim_gui/src/main/native/cpp/IniSaverInfo.cpp index 7baea3d875..6d01cd3e21 100644 --- a/simulation/halsim_gui/src/main/native/cpp/IniSaverInfo.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/IniSaverInfo.cpp @@ -14,7 +14,33 @@ using namespace halsimgui; -void NameInfo::GetName(char* buf, size_t size, const char* defaultName) { +void NameInfo::GetName(char* buf, size_t size, const char* defaultName) const { + if (m_name[0] != '\0') { + std::snprintf(buf, size, "%s", m_name); + } else { + std::snprintf(buf, size, "%s", defaultName); + } +} + +void NameInfo::GetName(char* buf, size_t size, const char* defaultName, + int index) const { + if (m_name[0] != '\0') { + std::snprintf(buf, size, "%s [%d]", m_name, index); + } else { + std::snprintf(buf, size, "%s[%d]", defaultName, index); + } +} + +void NameInfo::GetName(char* buf, size_t size, const char* defaultName, + int index, int index2) const { + if (m_name[0] != '\0') { + std::snprintf(buf, size, "%s [%d,%d]", m_name, index, index2); + } else { + std::snprintf(buf, size, "%s[%d,%d]", defaultName, index, index2); + } +} + +void NameInfo::GetLabel(char* buf, size_t size, const char* defaultName) const { if (m_name[0] != '\0') { std::snprintf(buf, size, "%s###Name%s", m_name, defaultName); } else { @@ -22,8 +48,8 @@ void NameInfo::GetName(char* buf, size_t size, const char* defaultName) { } } -void NameInfo::GetName(char* buf, size_t size, const char* defaultName, - int index) { +void NameInfo::GetLabel(char* buf, size_t size, const char* defaultName, + int index) const { if (m_name[0] != '\0') { std::snprintf(buf, size, "%s [%d]###Name%d", m_name, index, index); } else { @@ -31,8 +57,8 @@ void NameInfo::GetName(char* buf, size_t size, const char* defaultName, } } -void NameInfo::GetName(char* buf, size_t size, const char* defaultName, - int index, int index2) { +void NameInfo::GetLabel(char* buf, size_t size, const char* defaultName, + int index, int index2) const { if (m_name[0] != '\0') { std::snprintf(buf, size, "%s [%d,%d]###Name%d", m_name, index, index2, index); @@ -66,30 +92,40 @@ void NameInfo::PushEditNameId(const char* name) { ImGui::PushID(id); } -void NameInfo::PopupEditName(int index) { +bool NameInfo::PopupEditName(int index) { + bool rv = false; char id[64]; std::snprintf(id, sizeof(id), "Name%d", index); if (ImGui::BeginPopupContextItem(id)) { ImGui::Text("Edit name:"); - if (ImGui::InputText("##edit", m_name, sizeof(m_name), - ImGuiInputTextFlags_EnterReturnsTrue)) + if (InputTextName("##edit", ImGuiInputTextFlags_EnterReturnsTrue)) { ImGui::CloseCurrentPopup(); + rv = true; + } if (ImGui::Button("Close")) ImGui::CloseCurrentPopup(); ImGui::EndPopup(); } + return rv; } -void NameInfo::PopupEditName(const char* name) { +bool NameInfo::PopupEditName(const char* name) { + bool rv = false; char id[128]; std::snprintf(id, sizeof(id), "Name%s", name); if (ImGui::BeginPopupContextItem(id)) { ImGui::Text("Edit name:"); - if (ImGui::InputText("##edit", m_name, sizeof(m_name), - ImGuiInputTextFlags_EnterReturnsTrue)) + if (InputTextName("##edit", ImGuiInputTextFlags_EnterReturnsTrue)) { ImGui::CloseCurrentPopup(); + rv = true; + } if (ImGui::Button("Close")) ImGui::CloseCurrentPopup(); ImGui::EndPopup(); } + return rv; +} + +bool NameInfo::InputTextName(const char* label_id, ImGuiInputTextFlags flags) { + return ImGui::InputText(label_id, m_name, sizeof(m_name), flags); } bool OpenInfo::ReadIni(wpi::StringRef name, wpi::StringRef value) { diff --git a/simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.cpp b/simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.cpp index 7e6526cd41..2d442b58c4 100644 --- a/simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.cpp @@ -16,15 +16,50 @@ #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(); @@ -265,7 +300,12 @@ static void DisplayNetworkTables() { [](const auto& a, const auto& b) { return a.name < b.name; }); for (auto&& i : info) { - ImGui::Text("%s", i.name.c_str()); + 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)) { @@ -356,6 +396,13 @@ static void DisplayNetworkTables() { } 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/PDPGui.cpp b/simulation/halsim_gui/src/main/native/cpp/PDPGui.cpp index cfe1abb121..35ba49213f 100644 --- a/simulation/halsim_gui/src/main/native/cpp/PDPGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/PDPGui.cpp @@ -11,26 +11,56 @@ #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; - static int numPDP = HAL_GetNumPDPModules(); - static int numChannels = HAL_GetNumPDPChannels(); - static auto channelCurrents = std::make_unique(numChannels); - for (int i = 0; i < numPDP; ++i) { - if (HALSIM_GetPDPInitialized(i)) { + for (int i = 0, iend = gPDPSources.size(); i < iend; ++i) { + if (auto source = gPDPSources[i].get()) { hasAny = true; char name[128]; @@ -39,19 +69,19 @@ static void DisplayPDP() { ImGui::PushID(i); // temperature - double temp = HALSIM_GetPDPTemperature(i); + double temp = source->temp.GetValue(); ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4); - if (ImGui::InputDouble("Temp", &temp, 0, 0, "%.3f")) + if (source->temp.InputDouble("Temp", &temp, 0, 0, "%.3f")) HALSIM_SetPDPTemperature(i, temp); // voltage - double volts = HALSIM_GetPDPVoltage(i); + double volts = source->voltage.GetValue(); ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4); - if (ImGui::InputDouble("Voltage", &volts, 0, 0, "%.3f")) + if (source->voltage.InputDouble("Voltage", &volts, 0, 0, "%.3f")) HALSIM_SetPDPVoltage(i, volts); // channel currents; show as two columns laid out like PDP - HALSIM_GetPDPAllCurrents(i, channelCurrents.get()); + const int numChannels = source->currents.size(); ImGui::Text("Channel Current (A)"); ImGui::Columns(2, "channels", false); float maxWidth = ImGui::GetFontSize() * 13; @@ -59,27 +89,36 @@ static void DisplayPDP() { ++left, --right) { double val; + ImGui::PushID(left); auto& leftInfo = gChannels[i * numChannels + left]; - leftInfo.GetName(name, sizeof(name), "", left); - val = channelCurrents[left]; + leftInfo.GetLabel(name, sizeof(name), "", left); + val = source->currents[left]->GetValue(); ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4); - if (ImGui::InputDouble(name, &val, 0, 0, "%.3f")) + if (source->currents[left]->InputDouble(name, &val, 0, 0, "%.3f")) HALSIM_SetPDPCurrent(i, left, val); float leftWidth = ImGui::GetItemRectSize().x; - leftInfo.PopupEditName(left); + if (leftInfo.PopupEditName(left)) { + source->currents[left]->SetName(leftInfo.GetName()); + } + ImGui::PopID(); ImGui::NextColumn(); + ImGui::PushID(right); auto& rightInfo = gChannels[i * numChannels + right]; - rightInfo.GetName(name, sizeof(name), "", right); - val = channelCurrents[right]; + rightInfo.GetLabel(name, sizeof(name), "", right); + val = source->currents[right]->GetValue(); ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4); - if (ImGui::InputDouble(name, &val, 0, 0, "%.3f")) + if (source->currents[right]->InputDouble(name, &val, 0, 0, "%.3f")) HALSIM_SetPDPCurrent(i, right, val); float rightWidth = ImGui::GetItemRectSize().x; - rightInfo.PopupEditName(right); + if (rightInfo.PopupEditName(right)) { + source->currents[right]->SetName(rightInfo.GetName()); + } + ImGui::PopID(); ImGui::NextColumn(); - float width = (std::max)(leftWidth, rightWidth) * 2; + float width = + (std::max)(leftWidth, rightWidth) * 2 + ImGui::GetFontSize() * 4; if (width > maxWidth) maxWidth = width; } ImGui::Columns(1); @@ -93,7 +132,9 @@ static void DisplayPDP() { void PDPGui::Initialize() { gChannels.Initialize(); - HALSimGui::AddWindow("PDP", DisplayPDP, ImGuiWindowFlags_AlwaysAutoResize); + 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/PWMGui.cpp b/simulation/halsim_gui/src/main/native/cpp/PWMGui.cpp index d1c03c75b5..59d7a9308e 100644 --- a/simulation/halsim_gui/src/main/native/cpp/PWMGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/PWMGui.cpp @@ -10,19 +10,44 @@ #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; @@ -39,19 +64,11 @@ static void DisplayPWMs() { } } - // struct History { - // History() { std::memset(data, 0, 90 * sizeof(float)); } - // History(const History&) = delete; - // History& operator=(const History&) = delete; - // float data[90]; - // int display_offset = 0; - // int save_offset = 0; - //}; - // static std::vector> history; bool first = true; ImGui::PushItemWidth(ImGui::GetFontSize() * 4); for (int i = 0; i < numPWM; ++i) { - if (HALSIM_GetPWMInitialized(i)) { + if (auto source = gPWMSources[i].get()) { + ImGui::PushID(i); hasOutputs = true; if (!first) @@ -59,26 +76,19 @@ static void DisplayPWMs() { else first = false; - char name[128]; auto& info = gPWM[i]; - info.GetName(name, sizeof(name), "PWM", i); + char label[128]; + info.GetLabel(label, sizeof(label), "PWM", i); if (ledMap[i] > 0) { - ImGui::LabelText(name, "LED[%d]", ledMap[i] - 1); + ImGui::LabelText(label, "LED[%d]", ledMap[i] - 1); } else { float val = HALSimGui::AreOutputsDisabled() ? 0 : HALSIM_GetPWMSpeed(i); - ImGui::LabelText(name, "%0.3f", val); + source->LabelText(label, "%0.3f", val); } - info.PopupEditName(i); - - // lazily build history storage - // if (static_cast(i) > history.size()) - // history.resize(i + 1); - // if (!history[i]) history[i] = std::make_unique(); - - // save history - - // ImGui::PlotLines(labels[i].c_str(), values.data(), values.size(), - // ); + if (info.PopupEditName(i)) { + source->SetName(info.GetName()); + } + ImGui::PopID(); } } ImGui::PopItemWidth(); @@ -87,6 +97,7 @@ static void DisplayPWMs() { 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/PlotGui.cpp b/simulation/halsim_gui/src/main/native/cpp/PlotGui.cpp new file mode 100644 index 0000000000..ecaec63c76 --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/PlotGui.cpp @@ -0,0 +1,894 @@ +/*----------------------------------------------------------------------------*/ +/* 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 "PlotGui.h" + +#include +#include +#include +#include +#include +#include + +#define IMGUI_DEFINE_MATH_OPERATORS +#include +#include +#include +#include +#include +#include +#include +#include + +#include "GuiDataSource.h" +#include "HALSimGui.h" +#include "IniSaverInfo.h" +#include "IniSaverVector.h" + +using namespace halsimgui; + +namespace { +struct PlotSeriesRef { + size_t plotIndex; + size_t seriesIndex; +}; + +class PlotSeries : public NameInfo, public OpenInfo { + public: + explicit PlotSeries(wpi::StringRef id); + explicit PlotSeries(GuiDataSource* source, int yAxis = 0); + + const std::string& GetId() const { return m_id; } + + void CheckSource(); + + void SetSource(GuiDataSource* source); + GuiDataSource* GetSource() const { return m_source; } + + 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); + + void GetLabel(char* buf, size_t size) const; + + int GetYAxis() const { return m_yAxis; } + void SetYAxis(int yAxis) { m_yAxis = yAxis; } + + private: + bool IsDigital() const { + return m_digital == kDigital || + (m_digital == kAuto && m_source && m_source->IsDigital()); + } + void AppendValue(double value); + + // source linkage + GuiDataSource* m_source = nullptr; + wpi::sig::ScopedConnection m_sourceCreatedConn; + wpi::sig::ScopedConnection m_newValueConn; + std::string m_id; + + // user settings + int m_yAxis = 0; + ImVec4 m_color = IMPLOT_AUTO_COL; + int m_marker = 0; + + enum Digital { kAuto, kDigital, kAnalog }; + int m_digital = 0; + int m_digitalBitHeight = 8; + int m_digitalBitGap = 4; + + // value storage + static constexpr int kMaxSize = 2000; + static constexpr double kTimeGap = 0.05; + std::atomic m_size = 0; + std::atomic m_offset = 0; + ImPlotPoint m_data[kMaxSize]; +}; + +class Plot : public NameInfo, public OpenInfo { + 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 DragDropTarget(size_t i, bool inPlot); + void EmitPlot(double now, size_t i); + void EmitSettings(size_t i); + + std::vector> m_series; + + private: + void EmitSettingsLimits(int axis); + + bool m_visible = true; + unsigned int m_plotFlags = ImPlotFlags_Default; + bool m_lockPrevX = false; + bool m_paused = false; + float m_viewTime = 10; + int m_height = 300; + struct PlotRange { + double min = 0; + double max = 1; + bool lockMin = false; + bool lockMax = false; + bool apply = false; + }; + PlotRange m_axisRange[3]; + ImPlotRange m_xaxisRange; // read from plot, used for lockPrevX +}; +} // namespace + +static IniSaverVector gPlots{"Plot"}; + +PlotSeries::PlotSeries(wpi::StringRef id) : m_id(id) { + if (GuiDataSource* source = GuiDataSource::Find(id)) { + SetSource(source); + return; + } + CheckSource(); +} + +PlotSeries::PlotSeries(GuiDataSource* 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) { + if (m_id == id) { + SetSource(source); + m_sourceCreatedConn.disconnect(); + } + }); + } +} + +void PlotSeries::SetSource(GuiDataSource* source) { + m_source = source; + m_id = source->GetId(); + + // add initial value + m_data[m_size++] = ImPlotPoint{wpi::Now() * 1.0e-6, source->GetValue()}; + + m_newValueConn = source->valueChanged.connect_connection( + [this](double value) { AppendValue(value); }); +} + +void PlotSeries::AppendValue(double value) { + double time = wpi::Now() * 1.0e-6; + if (IsDigital()) { + if (m_size < kMaxSize) { + m_data[m_size] = ImPlotPoint{time, value}; + ++m_size; + } else { + m_data[m_offset] = ImPlotPoint{time, value}; + m_offset = (m_offset + 1) % kMaxSize; + } + } else { + // as an analog graph draws linear lines in between each value, + // insert duplicate value if "long" time between updates so it + // looks appropriately flat + if (m_size < kMaxSize) { + if (m_size > 0) { + if ((time - m_data[m_size - 1].x) > kTimeGap) { + m_data[m_size] = ImPlotPoint{time, m_data[m_size - 1].y}; + ++m_size; + } + } + m_data[m_size] = ImPlotPoint{time, value}; + ++m_size; + } else { + if (m_offset == 0) { + if ((time - m_data[kMaxSize - 1].x) > kTimeGap) { + m_data[m_offset] = ImPlotPoint{time, m_data[kMaxSize - 1].y}; + ++m_offset; + } + } else { + if ((time - m_data[m_offset - 1].x) > kTimeGap) { + m_data[m_offset] = ImPlotPoint{time, m_data[m_offset - 1].y}; + m_offset = (m_offset + 1) % kMaxSize; + } + } + m_data[m_offset] = ImPlotPoint{time, value}; + m_offset = (m_offset + 1) % kMaxSize; + } + } +} + +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 == "yAxis") { + int num; + if (value.getAsInteger(10, num)) return true; + m_yAxis = num; + return true; + } else if (name == "color") { + unsigned int num; + if (value.getAsInteger(10, num)) return true; + m_color = ImColor(num); + return true; + } else if (name == "marker") { + int num; + if (value.getAsInteger(10, num)) return true; + m_marker = num; + return true; + } else if (name == "digital") { + int num; + if (value.getAsInteger(10, num)) return true; + m_digital = num; + return true; + } else if (name == "digitalBitHeight") { + int num; + if (value.getAsInteger(10, num)) return true; + m_digitalBitHeight = num; + return true; + } else if (name == "digitalBitGap") { + int num; + if (value.getAsInteger(10, num)) return true; + m_digitalBitGap = num; + return true; + } + return false; +} + +void PlotSeries::WriteIni(ImGuiTextBuffer* out) { + NameInfo::WriteIni(out); + OpenInfo::WriteIni(out); + out->appendf( + "yAxis=%d\ncolor=%u\nmarker=%d\ndigital=%d\n" + "digitalBitHeight=%d\ndigitalBitGap=%d\n", + m_yAxis, static_cast(ImColor(m_color)), m_marker, 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()); +} + +bool PlotSeries::EmitPlot(double now, size_t i, size_t plotIndex) { + CheckSource(); + + char label[128]; + GetLabel(label, sizeof(label)); + + int size = m_size; + int offset = m_offset; + + // need to have last value at current time, so need to create fake last value + // we handle the offset logic ourselves to avoid wrap issues with size + 1 + struct GetterData { + double now; + ImPlotPoint* data; + int size; + int offset; + }; + GetterData getterData = {now, m_data, size, offset}; + auto getter = [](void* data, int idx) { + auto d = static_cast(data); + if (idx == d->size) + return ImPlotPoint{ + d->now, d->data[d->offset == 0 ? d->size - 1 : d->offset - 1].y}; + if (d->offset + idx < d->size) + return d->data[d->offset + idx]; + else + return d->data[d->offset + idx - d->size]; + }; + + if (m_color.w == IMPLOT_AUTO_COL.w) m_color = ImPlot::GetColormapColor(i); + ImPlot::PushStyleColor(ImPlotCol_Line, m_color); + if (IsDigital()) { + ImPlot::PushStyleVar(ImPlotStyleVar_DigitalBitHeight, m_digitalBitHeight); + ImPlot::PushStyleVar(ImPlotStyleVar_DigitalBitGap, m_digitalBitGap); + ImPlot::PlotDigital(label, getter, &getterData, size + 1); + ImPlot::PopStyleVar(); + ImPlot::PopStyleVar(); + } else { + ImPlot::SetPlotYAxis(m_yAxis); + 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); + ImPlot::EndLegendDragDropSource(); + } + + // Plot-specific variant of IniSaverInfo::PopupEditName() that also + // allows editing of other settings + bool rv = false; + if (ImPlot::BeginLegendPopup(label)) { + if (ImGui::Button("Close")) ImGui::CloseCurrentPopup(); + ImGui::Text("Edit name:"); + if (InputTextName("##edit", ImGuiInputTextFlags_EnterReturnsTrue)) { + ImGui::CloseCurrentPopup(); + } + rv = EmitSettingsDetail(i); + ImPlot::EndLegendPopup(); + } + + return rv; +} + +void PlotSeries::EmitDragDropPayload(size_t i, size_t plotIndex) { + PlotSeriesRef ref = {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); +} + +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; + } + + // Line color + { + ImGui::ColorEdit3("Color", &m_color.x, ImGuiColorEditFlags_NoInputs); + ImGui::SameLine(); + if (ImGui::Button("Default")) m_color = ImPlot::GetColormapColor(i); + } + + // Digital + { + static const char* const options[] = {"Auto", "Digital", "Analog"}; + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6); + ImGui::Combo("Digital", &m_digital, options, + sizeof(options) / sizeof(options[0])); + } + + if (IsDigital()) { + // Bit Height + { + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4); + ImGui::InputInt("Bit Height", &m_digitalBitHeight); + } + + // Bit Gap + { + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4); + ImGui::InputInt("Bit Gap", &m_digitalBitGap); + } + } else { + // Y-axis + { + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4); + static const char* const options[] = {"1", "2", "3"}; + ImGui::Combo("Y-Axis", &m_yAxis, options, 3); + } + + // Marker + { + static const char* const options[] = { + "None", "Circle", "Square", "Diamond", "Up", "Down", + "Left", "Right", "Cross", "Plus", "Asterisk"}; + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::Combo("Marker", &m_marker, options, + sizeof(options) / sizeof(options[0])); + } + } + + return false; +} + +Plot::Plot() { + for (int i = 0; i < 3; ++i) { + m_axisRange[i] = PlotRange{}; + } +} + +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") { + int num; + if (value.getAsInteger(10, num)) return true; + m_visible = num != 0; + return true; + } else if (name == "lockPrevX") { + int num; + if (value.getAsInteger(10, num)) return true; + m_lockPrevX = num != 0; + return true; + } else if (name == "legend") { + int num; + if (value.getAsInteger(10, num)) return true; + if (num == 0) + m_plotFlags &= ~ImPlotFlags_Legend; + else + m_plotFlags |= ImPlotFlags_Legend; + return true; + } else if (name == "yaxis2") { + int num; + if (value.getAsInteger(10, num)) return true; + if (num == 0) + m_plotFlags &= ~ImPlotFlags_YAxis2; + else + m_plotFlags |= ImPlotFlags_YAxis2; + return true; + } else if (name == "yaxis3") { + int num; + if (value.getAsInteger(10, num)) return true; + if (num == 0) + m_plotFlags &= ~ImPlotFlags_YAxis3; + else + m_plotFlags |= ImPlotFlags_YAxis3; + return true; + } else if (name == "viewTime") { + int num; + if (value.getAsInteger(10, num)) return true; + m_viewTime = num / 1000.0; + return true; + } else if (name == "height") { + int num; + if (value.getAsInteger(10, num)) return true; + m_height = num; + return true; + } else if (name.startswith("y")) { + auto [yAxisStr, yName] = name.split('_'); + int yAxis; + if (yAxisStr.substr(1).getAsInteger(10, yAxis)) return false; + if (yAxis < 0 || yAxis > 3) return false; + if (yName == "min") { + int num; + if (value.getAsInteger(10, num)) return true; + m_axisRange[yAxis].min = num / 1000.0; + return true; + } else if (yName == "max") { + int num; + if (value.getAsInteger(10, num)) return true; + m_axisRange[yAxis].max = num / 1000.0; + return true; + } else if (yName == "lockMin") { + int num; + if (value.getAsInteger(10, num)) return true; + m_axisRange[yAxis].lockMin = num != 0; + return true; + } else if (yName == "lockMax") { + int num; + if (value.getAsInteger(10, num)) return true; + m_axisRange[yAxis].lockMax = num != 0; + return true; + } + } + return false; +} + +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, + (m_plotFlags & ImPlotFlags_YAxis2) ? 1 : 0, + (m_plotFlags & ImPlotFlags_YAxis3) ? 1 : 0, + static_cast(m_viewTime * 1000), m_height); + for (int i = 0; i < 3; ++i) { + out->appendf("y%d_min=%d\ny%d_max=%d\ny%d_lockMin=%d\ny%d_lockMax=%d\n", i, + static_cast(m_axisRange[i].min * 1000), i, + static_cast(m_axisRange[i].max * 1000), i, + m_axisRange[i].lockMin ? 1 : 0, i, + m_axisRange[i].lockMax ? 1 : 0); + } +} + +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::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) { + if (!ImGui::BeginDragDropTarget()) return; + // handle dragging onto a specific Y axis + int yAxis = -1; + if (inPlot) { + for (int y = 0; y < 3; ++y) { + if (ImPlot::IsPlotYAxisHovered(y)) { + yAxis = y; + break; + } + } + } + if (const ImGuiPayload* payload = + ImGui::AcceptDragDropPayload("DataSource")) { + 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) { + return elem->GetId() == source->GetId() && + (yAxis == -1 || elem->GetYAxis() == yAxis); + }); + if (it == m_series.end()) { + m_series.emplace_back( + std::make_unique(source, yAxis == -1 ? 0 : yAxis)); + } + } else if (const ImGuiPayload* payload = + ImGui::AcceptDragDropPayload("PlotSeries")) { + auto ref = static_cast(payload->Data); + MovePlotSeries(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)); + } + } +} + +void Plot::EmitPlot(double now, size_t i) { + if (!m_visible) return; + + bool lockX = (i != 0 && m_lockPrevX); + + if (!lockX && ImGui::Button(m_paused ? "Resume" : "Pause")) + m_paused = !m_paused; + + char label[128]; + GetLabel(label, sizeof(label), i); + + if (lockX) { + ImPlot::SetNextPlotLimitsX(gPlots[i - 1].m_xaxisRange.Min, + gPlots[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); + } + + ImPlotAxisFlags yFlags[3] = {ImPlotAxisFlags_Default, + ImPlotAxisFlags_Auxiliary, + ImPlotAxisFlags_Auxiliary}; + for (int i = 0; i < 3; ++i) { + ImPlot::SetNextPlotLimitsY( + m_axisRange[i].min, m_axisRange[i].max, + m_axisRange[i].apply ? ImGuiCond_Always : ImGuiCond_Once, i); + m_axisRange[i].apply = false; + if (m_axisRange[i].lockMin) yFlags[i] |= ImPlotAxisFlags_LockMin; + if (m_axisRange[i].lockMax) yFlags[i] |= ImPlotAxisFlags_LockMax; + } + + if (ImPlot::BeginPlot(label, nullptr, nullptr, ImVec2(-1, m_height), + 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); + } + } + DragDropTarget(i, true); + m_xaxisRange = ImPlot::GetPlotLimits().X; + ImPlot::EndPlot(); + } +} + +void Plot::EmitSettingsLimits(int axis) { + ImGui::Indent(); + ImGui::PushID(axis); + + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.5); + ImGui::InputDouble("Min", &m_axisRange[axis].min, 0, 0, "%.3f"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.5); + ImGui::InputDouble("Max", &m_axisRange[axis].max, 0, 0, "%.3f"); + ImGui::SameLine(); + if (ImGui::Button("Apply")) m_axisRange[axis].apply = true; + + ImGui::TextUnformatted("Lock Axis"); + ImGui::SameLine(); + ImGui::Checkbox("Min##minlock", &m_axisRange[axis].lockMin); + ImGui::SameLine(); + ImGui::Checkbox("Max##maxlock", &m_axisRange[axis].lockMax); + + ImGui::PopID(); + 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::Checkbox("Visible", &m_visible); + 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"); + EmitSettingsLimits(0); + ImGui::CheckboxFlags("2nd Y-Axis", &m_plotFlags, ImPlotFlags_YAxis2); + if ((m_plotFlags & ImPlotFlags_YAxis2) != 0) EmitSettingsLimits(1); + ImGui::CheckboxFlags("3rd Y-Axis", &m_plotFlags, ImPlotFlags_YAxis3); + if ((m_plotFlags & ImPlotFlags_YAxis3) != 0) EmitSettingsLimits(2); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6); + ImGui::InputFloat("View Time (s)", &m_viewTime, 0.1f, 1.0f, "%.1f"); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6); + 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); + } + ImGui::PopID(); + } + ImGui::Unindent(); + + ImGui::PopID(); +} + +static void DisplayPlot() { + if (gPlots.empty()) { + ImGui::Text("No Plots"); + return; + } + double now = wpi::Now() * 1.0e-6; + for (size_t i = 0; i < gPlots.size(); ++i) { + ImGui::PushID(i); + gPlots[i].EmitPlot(now, 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); + } +} + +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(); +} + +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('='); + name = name.trim(); + value = value.trim(); + element->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 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/simulation/halsim_gui/src/main/native/cpp/PlotGui.h b/simulation/halsim_gui/src/main/native/cpp/PlotGui.h new file mode 100644 index 0000000000..6bbd337794 --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/PlotGui.h @@ -0,0 +1,17 @@ +/*----------------------------------------------------------------------------*/ +/* 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 { + +class PlotGui { + 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 index 8b961eaf46..917167835c 100644 --- a/simulation/halsim_gui/src/main/native/cpp/RelayGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/RelayGui.cpp @@ -9,29 +9,63 @@ #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; - static const int numRelay = HAL_GetNumRelayHeaders(); - for (int i = 0; i < numRelay; ++i) { - bool forwardInit = HALSIM_GetRelayInitializedForward(i); - bool reverseInit = HALSIM_GetRelayInitializedReverse(i); + for (int i = 0, iend = gRelayForwardSources.size(); i < iend; ++i) { + auto forwardSource = gRelayForwardSources[i].get(); + auto reverseSource = gRelayReverseSources[i].get(); - if (forwardInit || reverseInit) { + if (forwardSource || reverseSource) { hasOutputs = true; if (!first) @@ -42,8 +76,8 @@ static void DisplayRelays() { bool forward = false; bool reverse = false; if (!HALSimGui::AreOutputsDisabled()) { - reverse = HALSIM_GetRelayReverse(i); - forward = HALSIM_GetRelayForward(i); + if (forwardSource) forward = forwardSource->GetValue(); + if (reverseSource) reverse = reverseSource->GetValue(); } auto& info = gRelays[i]; @@ -53,16 +87,22 @@ static void DisplayRelays() { else ImGui::Text("Relay[%d]", i); ImGui::PopID(); - info.PopupEditName(i); + 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] = {reverseInit ? (reverse ? 2 : -2) : -3, - forwardInit ? (forward ? 1 : -1) : -3}; - DrawLEDs(values, 2, 2, colors); + 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"); @@ -70,6 +110,10 @@ static void DisplayRelays() { 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/RoboRioGui.cpp b/simulation/halsim_gui/src/main/native/cpp/RoboRioGui.cpp index d7435f21a5..b4f89091bc 100644 --- a/simulation/halsim_gui/src/main/native/cpp/RoboRioGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/RoboRioGui.cpp @@ -7,13 +7,55 @@ #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()); @@ -22,93 +64,96 @@ static void DisplayRoboRio() { if (ImGui::CollapsingHeader("RoboRIO Input")) { { - double val = HALSIM_GetRoboRioVInVoltage(); - if (ImGui::InputDouble("Voltage (V)", &val)) + double val = gRioSource->vInVoltage.GetValue(); + if (gRioSource->vInVoltage.InputDouble("Voltage (V)", &val)) HALSIM_SetRoboRioVInVoltage(val); } { - double val = HALSIM_GetRoboRioVInCurrent(); - if (ImGui::InputDouble("Current (A)", &val)) + double val = gRioSource->vInCurrent.GetValue(); + if (gRioSource->vInCurrent.InputDouble("Current (A)", &val)) HALSIM_SetRoboRioVInCurrent(val); } } if (ImGui::CollapsingHeader("6V Rail")) { { - double val = HALSIM_GetRoboRioUserVoltage6V(); - if (ImGui::InputDouble("Voltage (V)", &val)) + double val = gRioSource->userVoltage6V.GetValue(); + if (gRioSource->userVoltage6V.InputDouble("Voltage (V)", &val)) HALSIM_SetRoboRioUserVoltage6V(val); } { - double val = HALSIM_GetRoboRioUserCurrent6V(); - if (ImGui::InputDouble("Current (A)", &val)) + double val = gRioSource->userCurrent6V.GetValue(); + if (gRioSource->userCurrent6V.InputDouble("Current (A)", &val)) HALSIM_SetRoboRioUserCurrent6V(val); } { static const char* options[] = {"inactive", "active"}; - int val = HALSIM_GetRoboRioUserActive6V() ? 1 : 0; - if (ImGui::Combo("Active", &val, options, 2)) + int val = gRioSource->userActive6V.GetValue() ? 1 : 0; + if (gRioSource->userActive6V.Combo("Active", &val, options, 2)) HALSIM_SetRoboRioUserActive6V(val); } { - int val = HALSIM_GetRoboRioUserFaults6V(); - if (ImGui::InputInt("Faults", &val)) HALSIM_SetRoboRioUserFaults6V(val); + int val = gRioSource->userFaults6V.GetValue(); + if (gRioSource->userFaults6V.InputInt("Faults", &val)) + HALSIM_SetRoboRioUserFaults6V(val); } } if (ImGui::CollapsingHeader("5V Rail")) { { - double val = HALSIM_GetRoboRioUserVoltage5V(); - if (ImGui::InputDouble("Voltage (V)", &val)) + double val = gRioSource->userVoltage5V.GetValue(); + if (gRioSource->userVoltage5V.InputDouble("Voltage (V)", &val)) HALSIM_SetRoboRioUserVoltage5V(val); } { - double val = HALSIM_GetRoboRioUserCurrent5V(); - if (ImGui::InputDouble("Current (A)", &val)) + double val = gRioSource->userCurrent5V.GetValue(); + if (gRioSource->userCurrent5V.InputDouble("Current (A)", &val)) HALSIM_SetRoboRioUserCurrent5V(val); } { static const char* options[] = {"inactive", "active"}; - int val = HALSIM_GetRoboRioUserActive5V() ? 1 : 0; - if (ImGui::Combo("Active", &val, options, 2)) + int val = gRioSource->userActive5V.GetValue() ? 1 : 0; + if (gRioSource->userActive5V.Combo("Active", &val, options, 2)) HALSIM_SetRoboRioUserActive5V(val); } { - int val = HALSIM_GetRoboRioUserFaults5V(); - if (ImGui::InputInt("Faults", &val)) HALSIM_SetRoboRioUserFaults5V(val); + int val = gRioSource->userFaults5V.GetValue(); + if (gRioSource->userFaults5V.InputInt("Faults", &val)) + HALSIM_SetRoboRioUserFaults5V(val); } } if (ImGui::CollapsingHeader("3.3V Rail")) { { - double val = HALSIM_GetRoboRioUserVoltage3V3(); - if (ImGui::InputDouble("Voltage (V)", &val)) + double val = gRioSource->userVoltage3V3.GetValue(); + if (gRioSource->userVoltage3V3.InputDouble("Voltage (V)", &val)) HALSIM_SetRoboRioUserVoltage3V3(val); } { - double val = HALSIM_GetRoboRioUserCurrent3V3(); - if (ImGui::InputDouble("Current (A)", &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 (ImGui::Combo("Active", &val, options, 2)) + if (gRioSource->userActive3V3.Combo("Active", &val, options, 2)) HALSIM_SetRoboRioUserActive3V3(val); } { - int val = HALSIM_GetRoboRioUserFaults3V3(); - if (ImGui::InputInt("Faults", &val)) HALSIM_SetRoboRioUserFaults3V3(val); + int val = gRioSource->userFaults3V3.GetValue(); + if (gRioSource->userFaults3V3.InputInt("Faults", &val)) + HALSIM_SetRoboRioUserFaults3V3(val); } } @@ -116,6 +161,7 @@ static void DisplayRoboRio() { } void RoboRioGui::Initialize() { + HALSimGui::AddExecute(UpdateRoboRioSources); HALSimGui::AddWindow("RoboRIO", DisplayRoboRio, ImGuiWindowFlags_AlwaysAutoResize); // hide it by default diff --git a/simulation/halsim_gui/src/main/native/cpp/SimDeviceGui.cpp b/simulation/halsim_gui/src/main/native/cpp/SimDeviceGui.cpp index bc426d3fa7..2802000fcf 100644 --- a/simulation/halsim_gui/src/main/native/cpp/SimDeviceGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/SimDeviceGui.cpp @@ -9,12 +9,16 @@ #include +#include +#include #include #include #include #include +#include +#include "GuiDataSource.h" #include "HALSimGui.h" #include "IniSaverInfo.h" #include "IniSaverString.h" @@ -22,6 +26,7 @@ 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; @@ -34,10 +39,56 @@ struct ElementInfo : public NameInfo, public OpenInfo { } bool visible = true; // not saved }; + +class SimValueSource : public GuiDataSource { + public: + explicit SimValueSource(HAL_SimValueHandle handle, const char* device, + const char* name) + : GuiDataSource(wpi::Twine{device} + wpi::Twine{'-'} + name), + m_callback{HALSIM_RegisterSimValueChangedCallback( + handle, this, CallbackFunc, true)} {} + ~SimValueSource() { + if (m_callback != 0) HALSIM_CancelSimValueChangedCallback(m_callback); + } + + private: + static void CallbackFunc(const char*, void* param, HAL_SimValueHandle, + HAL_Bool, const HAL_Value* value) { + auto source = static_cast(param); + if (value->type == HAL_BOOLEAN) { + source->SetValue(value->data.v_boolean); + source->SetDigital(true); + } else if (value->type == HAL_DOUBLE) { + source->SetValue(value->data.v_double); + source->SetDigital(false); + } + } + + int32_t m_callback; +}; + } // namespace static std::vector> gDeviceExecutors; static IniSaverString gElements{"Device"}; +static wpi::DenseMap> + gSimValueSources; + +static void UpdateSimValueSources() { + HALSIM_EnumerateSimDevices( + "", nullptr, [](const char* name, void*, HAL_SimDeviceHandle handle) { + HALSIM_EnumerateSimValues( + handle, const_cast(name), + [](const char* name, void* deviceV, HAL_SimValueHandle handle, + HAL_Bool readonly, const HAL_Value* value) { + auto device = static_cast(deviceV); + auto& source = gSimValueSources[handle]; + if (!source) { + source = std::make_unique(handle, device, name); + } + }); + }); +} void SimDeviceGui::Hide(const char* name) { gElements[name].visible = false; } @@ -50,7 +101,7 @@ bool SimDeviceGui::StartDevice(const char* label, ImGuiTreeNodeFlags flags) { if (!element.visible) return false; char name[128]; - element.GetName(name, sizeof(name), label); + element.GetLabel(name, sizeof(name), label); bool open = ImGui::CollapsingHeader( name, flags | (element.IsOpen() ? ImGuiTreeNodeFlags_DefaultOpen : 0)); @@ -63,8 +114,8 @@ bool SimDeviceGui::StartDevice(const char* label, ImGuiTreeNodeFlags flags) { void SimDeviceGui::FinishDevice() { ImGui::PopID(); } -bool DisplayValueImpl(const char* name, bool readonly, HAL_Value* value, - const char** options, int32_t numOptions) { +static bool DisplayValueImpl(const char* name, bool readonly, HAL_Value* value, + const char** options, int32_t numOptions) { // read-only if (readonly) { switch (value->type) { @@ -141,11 +192,36 @@ bool DisplayValueImpl(const char* name, bool readonly, HAL_Value* value, 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 DisplayValueImpl(name, readonly, value, options, numOptions); + return DisplayValueSourceImpl(name, readonly, value, source, options, + numOptions); } static void SimDeviceDisplayValue(const char* name, void*, @@ -158,7 +234,9 @@ static void SimDeviceDisplayValue(const char* name, void*, options = HALSIM_GetSimValueEnumOptions(handle, &numOptions); HAL_Value valueCopy = *value; - if (DisplayValueImpl(name, readonly, &valueCopy, options, numOptions)) + if (DisplayValueSourceImpl(name, readonly, &valueCopy, + gSimValueSources[handle].get(), options, + numOptions)) HAL_SetSimValue(handle, valueCopy); } @@ -184,6 +262,7 @@ static void DisplayDeviceTree() { 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); diff --git a/simulation/halsim_gui/src/main/native/cpp/SolenoidGui.cpp b/simulation/halsim_gui/src/main/native/cpp/SolenoidGui.cpp index 9709a49e1e..b0f802ecc8 100644 --- a/simulation/halsim_gui/src/main/native/cpp/SolenoidGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/SolenoidGui.cpp @@ -9,6 +9,8 @@ #include #include +#include +#include #include #include @@ -16,28 +18,60 @@ #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; - static const int numPCM = HAL_GetNumPCMModules(); - static const int numChannels = HAL_GetNumSolenoidChannels(); - for (int i = 0; i < numPCM; ++i) { - bool anyInit = 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 (HALSIM_GetPCMSolenoidInitialized(i, j)) { - anyInit = true; + if (pcmSource.solenoids[j]) { channels[j] = (!HALSimGui::AreOutputsDisabled() && - HALSIM_GetPCMSolenoidOutput(i, j)) + pcmSource.solenoids[j]->GetValue()) ? 1 : -1; } else { @@ -45,9 +79,6 @@ static void DisplaySolenoids() { } } - if (!anyInit) continue; - hasOutputs = true; - char name[128]; std::snprintf(name, sizeof(name), "PCM[%d]", i); auto& pcmInfo = gPCMs[i]; @@ -66,11 +97,16 @@ static void DisplaySolenoids() { ImGui::PushID(i); ImGui::PushItemWidth(ImGui::GetFontSize() * 4); for (int j = 0; j < numChannels; ++j) { - if (channels[j] == -2) continue; + if (!pcmSource.solenoids[j]) continue; auto& info = gSolenoids[i * numChannels + j]; - info.GetName(name, sizeof(name), "Solenoid", j); - ImGui::LabelText(name, "%s", channels[j] == 1 ? "On" : "Off"); - info.PopupEditName(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(); @@ -82,6 +118,12 @@ static void DisplaySolenoids() { 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/main.cpp b/simulation/halsim_gui/src/main/native/cpp/main.cpp index 7c1541e0dc..7900ace51a 100644 --- a/simulation/halsim_gui/src/main/native/cpp/main.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/main.cpp @@ -23,6 +23,7 @@ #include "NetworkTablesGui.h" #include "PDPGui.h" #include "PWMGui.h" +#include "PlotGui.h" #include "RelayGui.h" #include "RoboRioGui.h" #include "SimDeviceGui.h" @@ -50,6 +51,7 @@ __declspec(dllexport) 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); diff --git a/simulation/halsim_gui/src/main/native/include/ExtraGuiWidgets.h b/simulation/halsim_gui/src/main/native/include/ExtraGuiWidgets.h index 7374cac95b..91d73010c4 100644 --- a/simulation/halsim_gui/src/main/native/include/ExtraGuiWidgets.h +++ b/simulation/halsim_gui/src/main/native/include/ExtraGuiWidgets.h @@ -1,5 +1,5 @@ /*----------------------------------------------------------------------------*/ -/* Copyright (c) 2017-2019 FIRST. All Rights Reserved. */ +/* Copyright (c) 2017-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. */ @@ -11,6 +11,8 @@ namespace halsimgui { +class GuiDataSource; + /** * DrawLEDs() configuration for 2D arrays. */ @@ -60,4 +62,28 @@ void DrawLEDs(const int* values, int numValues, int cols, const ImU32* colors, float size = 0.0f, float spacing = 0.0f, const LEDConfig& config = LEDConfig{}); +/** + * Draw a 2D array of LEDs. + * + * Values are indices into colors array. Positive values are filled (lit), + * negative values are unfilled (dark / border only). The actual color index + * is the absolute value of the value - 1. 0 values are not drawn at all + * (an empty space is left). + * + * @param values values array + * @param sources sources array + * @param numValues size of values and sources arrays + * @param cols number of columns + * @param colors colors array + * @param size size of each LED (both horizontal and vertical); + * if 0, defaults to 1/2 of font size + * @param spacing spacing between each LED (both horizontal and vertical); + * if 0, defaults to 1/3 of font size + * @param config 2D array configuration + */ +void DrawLEDSources(const int* values, GuiDataSource** sources, int numValues, + int cols, const ImU32* colors, float size = 0.0f, + float spacing = 0.0f, + const LEDConfig& config = LEDConfig{}); + } // namespace halsimgui diff --git a/simulation/halsim_gui/src/main/native/include/GuiDataSource.h b/simulation/halsim_gui/src/main/native/include/GuiDataSource.h new file mode 100644 index 0000000000..117d6ea340 --- /dev/null +++ b/simulation/halsim_gui/src/main/native/include/GuiDataSource.h @@ -0,0 +1,186 @@ +/*----------------------------------------------------------------------------*/ +/* 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 { + +/** + * 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 + +#define HALSIMGUI_DATASOURCE(cbname, id, TYPE, vtype) \ + class cbname##Source : public ::halsimgui::GuiDataSource { \ + public: \ + cbname##Source() \ + : GuiDataSource(id), \ + m_callback{ \ + HALSIM_Register##cbname##Callback(CallbackFunc, this, true)} { \ + SetDigital(HAL_##TYPE == HAL_BOOLEAN); \ + } \ + \ + ~cbname##Source() { \ + if (m_callback != 0) HALSIM_Cancel##cbname##Callback(m_callback); \ + } \ + \ + private: \ + static void CallbackFunc(const char*, void* param, \ + const HAL_Value* value) { \ + if (value->type == HAL_##TYPE) \ + static_cast(param)->SetValue(value->data.v_##vtype); \ + } \ + \ + int32_t m_callback; \ + } + +#define HALSIMGUI_DATASOURCE_BOOLEAN(cbname, id) \ + HALSIMGUI_DATASOURCE(cbname, id, BOOLEAN, boolean) + +#define HALSIMGUI_DATASOURCE_DOUBLE(cbname, id) \ + HALSIMGUI_DATASOURCE(cbname, id, DOUBLE, double) + +#define HALSIMGUI_DATASOURCE_INT(cbname, id) \ + HALSIMGUI_DATASOURCE(cbname, id, INT, int) + +#define HALSIMGUI_DATASOURCE_INDEXED(cbname, id, TYPE, vtype) \ + class cbname##Source : public ::halsimgui::GuiDataSource { \ + public: \ + explicit cbname##Source(int32_t index, int channel = -1) \ + : GuiDataSource(id, channel < 0 ? index : channel), \ + m_index{index}, \ + m_channel{channel < 0 ? index : channel}, \ + m_callback{HALSIM_Register##cbname##Callback(index, CallbackFunc, \ + this, true)} { \ + SetDigital(HAL_##TYPE == HAL_BOOLEAN); \ + } \ + \ + ~cbname##Source() { \ + if (m_callback != 0) \ + HALSIM_Cancel##cbname##Callback(m_index, m_callback); \ + } \ + \ + int32_t GetIndex() const { return m_index; } \ + \ + int GetChannel() const { return m_channel; } \ + \ + private: \ + static void CallbackFunc(const char*, void* param, \ + const HAL_Value* value) { \ + if (value->type == HAL_##TYPE) \ + static_cast(param)->SetValue(value->data.v_##vtype); \ + } \ + \ + int32_t m_index; \ + int m_channel; \ + int32_t m_callback; \ + } + +#define HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED(cbname, id) \ + HALSIMGUI_DATASOURCE_INDEXED(cbname, id, BOOLEAN, boolean) + +#define HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(cbname, id) \ + HALSIMGUI_DATASOURCE_INDEXED(cbname, id, DOUBLE, double) + +#define HALSIMGUI_DATASOURCE_INDEXED2(cbname, id, TYPE, vtype) \ + class cbname##Source : public ::halsimgui::GuiDataSource { \ + public: \ + explicit cbname##Source(int32_t index, int32_t channel) \ + : GuiDataSource(id, index, channel), \ + m_index{index}, \ + m_channel{channel}, \ + m_callback{HALSIM_Register##cbname##Callback( \ + index, channel, CallbackFunc, this, true)} { \ + SetDigital(HAL_##TYPE == HAL_BOOLEAN); \ + } \ + \ + ~cbname##Source() { \ + if (m_callback != 0) \ + HALSIM_Cancel##cbname##Callback(m_index, m_channel, m_callback); \ + } \ + \ + int32_t GetIndex() const { return m_index; } \ + \ + int32_t GetChannel() const { return m_channel; } \ + \ + private: \ + static void CallbackFunc(const char*, void* param, \ + const HAL_Value* value) { \ + if (value->type == HAL_##TYPE) \ + static_cast(param)->SetValue(value->data.v_##vtype); \ + } \ + \ + int32_t m_index; \ + int32_t m_channel; \ + int32_t m_callback; \ + } + +#define HALSIMGUI_DATASOURCE_BOOLEAN_INDEXED2(cbname, id) \ + HALSIMGUI_DATASOURCE_INDEXED2(cbname, id, BOOLEAN, boolean) + +#define HALSIMGUI_DATASOURCE_DOUBLE_INDEXED2(cbname, id) \ + HALSIMGUI_DATASOURCE_INDEXED2(cbname, id, DOUBLE, double) diff --git a/simulation/halsim_gui/src/main/native/include/IniSaverInfo.h b/simulation/halsim_gui/src/main/native/include/IniSaverInfo.h index 96e5621eb3..25fbe58c4f 100644 --- a/simulation/halsim_gui/src/main/native/include/IniSaverInfo.h +++ b/simulation/halsim_gui/src/main/native/include/IniSaverInfo.h @@ -18,16 +18,24 @@ class NameInfo { bool HasName() const { return m_name[0] != '\0'; } const char* GetName() const { return m_name; } - void GetName(char* buf, size_t size, const char* defaultName); - void GetName(char* buf, size_t size, const char* defaultName, int index); + void GetName(char* buf, size_t size, const char* defaultName) const; + void GetName(char* buf, size_t size, const char* defaultName, + int index) const; void GetName(char* buf, size_t size, const char* defaultName, int index, - int index2); + int index2) const; + void GetLabel(char* buf, size_t size, const char* defaultName) const; + void GetLabel(char* buf, size_t size, const char* defaultName, + int index) const; + void GetLabel(char* buf, size_t size, const char* defaultName, int index, + int index2) const; + bool ReadIni(wpi::StringRef name, wpi::StringRef value); void WriteIni(ImGuiTextBuffer* out); void PushEditNameId(int index); void PushEditNameId(const char* name); - void PopupEditName(int index); - void PopupEditName(const char* name); + bool PopupEditName(int index); + bool PopupEditName(const char* name); + bool InputTextName(const char* label_id, ImGuiInputTextFlags flags = 0); private: char m_name[64]; diff --git a/simulation/halsim_gui/src/main/native/include/IniSaverVector.h b/simulation/halsim_gui/src/main/native/include/IniSaverVector.h new file mode 100644 index 0000000000..0816933980 --- /dev/null +++ b/simulation/halsim_gui/src/main/native/include/IniSaverVector.h @@ -0,0 +1,36 @@ +/*----------------------------------------------------------------------------*/ +/* 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 halsimgui { + +template +class IniSaverVector : public std::vector { + public: + explicit IniSaverVector(const char* typeName) : m_typeName(typeName) {} + void Initialize(); + + 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; +}; + +} // namespace halsimgui + +#include "IniSaverVector.inl" diff --git a/simulation/halsim_gui/src/main/native/include/IniSaverVector.inl b/simulation/halsim_gui/src/main/native/include/IniSaverVector.inl new file mode 100644 index 0000000000..b2979bc509 --- /dev/null +++ b/simulation/halsim_gui/src/main/native/include/IniSaverVector.inl @@ -0,0 +1,60 @@ +/*----------------------------------------------------------------------------*/ +/* 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 22f29b28ad..8c24cc9f22 100644 --- a/simulation/halsim_gui/src/main/native/include/SimDeviceGui.h +++ b/simulation/halsim_gui/src/main/native/include/SimDeviceGui.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. */ @@ -35,6 +35,8 @@ void HALSIMGUI_DeviceTreeFinishDevice(void); namespace halsimgui { +class GuiDataSource; + class SimDeviceGui { public: static void Initialize(); @@ -69,6 +71,22 @@ class SimDeviceGui { 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