mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-19 00:41:43 +00:00
[sim] Add plotting to simulation GUI
This commit is contained 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 ""
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,15 +8,35 @@
|
||||
#include "AccelerometerGui.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
|
||||
#include <hal/Value.h>
|
||||
#include <hal/simulation/AccelerometerData.h>
|
||||
#include <imgui.h>
|
||||
|
||||
#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<AccelerometerXSource> gAccelXSource;
|
||||
static std::unique_ptr<AccelerometerYSource> gAccelYSource;
|
||||
static std::unique_ptr<AccelerometerZSource> gAccelZSource;
|
||||
|
||||
static void UpdateAccelSources() {
|
||||
if (!HALSIM_GetAccelerometerActive(0)) return;
|
||||
if (!gAccelXSource) gAccelXSource = std::make_unique<AccelerometerXSource>(0);
|
||||
if (!gAccelYSource) gAccelYSource = std::make_unique<AccelerometerYSource>(0);
|
||||
if (!gAccelZSource) gAccelZSource = std::make_unique<AccelerometerZSource>(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);
|
||||
}
|
||||
|
||||
@@ -8,38 +8,73 @@
|
||||
#include "AnalogGyroGui.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <hal/Ports.h>
|
||||
#include <hal/Value.h>
|
||||
#include <hal/simulation/AnalogGyroData.h>
|
||||
#include <imgui.h>
|
||||
|
||||
#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<std::unique_ptr<AnalogGyroSource>> 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<AnalogGyroSource>(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);
|
||||
}
|
||||
|
||||
@@ -7,28 +7,52 @@
|
||||
|
||||
#include "AnalogInputGui.h"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <hal/Ports.h>
|
||||
#include <hal/simulation/AnalogGyroData.h>
|
||||
#include <hal/simulation/AnalogInData.h>
|
||||
#include <hal/simulation/SimDeviceData.h>
|
||||
#include <imgui.h>
|
||||
|
||||
#include "GuiDataSource.h"
|
||||
#include "HALSimGui.h"
|
||||
#include "IniSaver.h"
|
||||
#include "IniSaverInfo.h"
|
||||
|
||||
using namespace halsimgui;
|
||||
|
||||
static IniSaver<NameInfo> gAnalogInputs{"AnalogInput"}; // indexed by channel
|
||||
namespace {
|
||||
HALSIMGUI_DATASOURCE_DOUBLE_INDEXED(AnalogInVoltage, "AIn");
|
||||
} // namespace
|
||||
|
||||
// indexed by channel
|
||||
static IniSaver<NameInfo> gAnalogInputs{"AnalogInput"};
|
||||
static std::vector<std::unique_ptr<AnalogInVoltageSource>> 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<AnalogInVoltageSource>(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);
|
||||
|
||||
@@ -7,40 +7,66 @@
|
||||
|
||||
#include "AnalogOutGui.h"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <hal/Ports.h>
|
||||
#include <hal/simulation/AnalogOutData.h>
|
||||
#include <imgui.h>
|
||||
|
||||
#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<NameInfo> gAnalogOuts{"AnalogOut"}; // indexed by channel
|
||||
static std::vector<std::unique_ptr<AnalogOutVoltageSource>> 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<AnalogOutVoltageSource>(i);
|
||||
source->SetName(gAnalogOuts[i].GetName());
|
||||
}
|
||||
} else {
|
||||
source.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void DisplayAnalogOutputs() {
|
||||
static const int numAnalog = HAL_GetNumAnalogOutputs();
|
||||
static auto init = std::make_unique<bool[]>(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);
|
||||
}
|
||||
|
||||
@@ -8,56 +8,97 @@
|
||||
#include "CompressorGui.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <hal/Ports.h>
|
||||
#include <hal/Value.h>
|
||||
#include <hal/simulation/PCMData.h>
|
||||
#include <imgui.h>
|
||||
|
||||
#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<std::unique_ptr<CompressorSource>> 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<CompressorSource>(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);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
|
||||
#include "DIOGui.h"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <hal/Ports.h>
|
||||
#include <hal/simulation/DIOData.h>
|
||||
#include <hal/simulation/DigitalPWMData.h>
|
||||
@@ -15,13 +18,23 @@
|
||||
#include <hal/simulation/SimDeviceData.h>
|
||||
#include <imgui.h>
|
||||
|
||||
#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<NameInfo> gDIO{"DIO"};
|
||||
static std::vector<std::unique_ptr<DIOValueSource>> gDIOSources;
|
||||
static std::vector<std::unique_ptr<DigitalPWMDutyCycleSource>> gDPWMSources;
|
||||
static std::vector<std::unique_ptr<DutyCycleOutputSource>> 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<DIOValueSource>(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<DigitalPWMDutyCycleSource>(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<DutyCycleOutputSource>(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<int[]>(numDIO);
|
||||
static auto encoderMap = std::make_unique<int[]>(numDIO);
|
||||
static auto dutyCycleMap = std::make_unique<int[]>(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);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
#include "DriverStationGui.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <GLFW/glfw3.h>
|
||||
#include <hal/simulation/DriverStationData.h>
|
||||
@@ -21,6 +23,7 @@
|
||||
#include <wpi/raw_ostream.h>
|
||||
|
||||
#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<GuiDataSource> axes[HAL_kMaxJoystickAxes];
|
||||
// use pointer instead of unique_ptr to allow it to be passed directly
|
||||
// to DrawLEDSources()
|
||||
GuiDataSource* buttons[32];
|
||||
std::unique_ptr<GuiDataSource> 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<JoystickSource> 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<GuiDataSource>("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<GuiDataSource>("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<JoystickSource*>(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<JoystickSource>(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");
|
||||
|
||||
@@ -7,11 +7,16 @@
|
||||
|
||||
#include "EncoderGui.h"
|
||||
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <hal/Ports.h>
|
||||
#include <hal/simulation/EncoderData.h>
|
||||
#include <hal/simulation/SimDeviceData.h>
|
||||
#include <imgui.h>
|
||||
|
||||
#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<EncoderSource*>(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<double>::infinity());
|
||||
else if (period == std::numeric_limits<double>::infinity())
|
||||
self->rate.SetValue(0);
|
||||
else
|
||||
self->rate.SetValue(static_cast<float>(distPerPulse / period));
|
||||
}
|
||||
}
|
||||
|
||||
static void CountCallbackFunc(const char*, void* param,
|
||||
const HAL_Value* value) {
|
||||
if (value->type == HAL_INT) {
|
||||
auto self = static_cast<EncoderSource*>(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<EncoderSource*>(param);
|
||||
double period = value->data.v_double;
|
||||
self->period.SetValue(period);
|
||||
if (period == 0)
|
||||
self->rate.SetValue(std::numeric_limits<double>::infinity());
|
||||
else if (period == std::numeric_limits<double>::infinity())
|
||||
self->rate.SetValue(0);
|
||||
else
|
||||
self->rate.SetValue(
|
||||
static_cast<float>(self->distancePerPulse.GetValue() / period));
|
||||
}
|
||||
}
|
||||
|
||||
static void DirectionCallbackFunc(const char*, void* param,
|
||||
const HAL_Value* value) {
|
||||
if (value->type == HAL_BOOLEAN) {
|
||||
static_cast<EncoderSource*>(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<EncoderInfo> gEncoders{"Encoder"}; // indexed by channel A
|
||||
static std::vector<std::unique_ptr<EncoderSource>> 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<EncoderSource>(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);
|
||||
|
||||
@@ -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
|
||||
|
||||
116
simulation/halsim_gui/src/main/native/cpp/GuiDataSource.cpp
Normal file
116
simulation/halsim_gui/src/main/native/cpp/GuiDataSource.cpp
Normal file
@@ -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 <wpi/StringMap.h>
|
||||
|
||||
using namespace halsimgui;
|
||||
|
||||
wpi::sig::Signal<const char*, GuiDataSource*> GuiDataSource::sourceCreated;
|
||||
|
||||
static wpi::StringMap<GuiDataSource*> 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();
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -16,15 +16,50 @@
|
||||
#include <imgui.h>
|
||||
#include <networktables/NetworkTableInstance.h>
|
||||
#include <networktables/NetworkTableValue.h>
|
||||
#include <ntcore_cpp.h>
|
||||
#include <wpi/DenseMap.h>
|
||||
#include <wpi/Format.h>
|
||||
#include <wpi/SmallString.h>
|
||||
#include <wpi/StringRef.h>
|
||||
#include <wpi/raw_ostream.h>
|
||||
|
||||
#include "GuiDataSource.h"
|
||||
#include "HALSimGui.h"
|
||||
|
||||
using namespace halsimgui;
|
||||
|
||||
static NT_EntryListenerPoller gNetworkTablesPoller;
|
||||
static wpi::DenseMap<NT_Entry, std::unique_ptr<GuiDataSource>>
|
||||
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<GuiDataSource>(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<char>& out,
|
||||
wpi::ArrayRef<int> 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);
|
||||
|
||||
@@ -11,26 +11,56 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <hal/Ports.h>
|
||||
#include <hal/simulation/PDPData.h>
|
||||
#include <imgui.h>
|
||||
|
||||
#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<PDPCurrentSource>(index, i));
|
||||
}
|
||||
PDPTemperatureSource temp;
|
||||
PDPVoltageSource voltage;
|
||||
std::vector<std::unique_ptr<PDPCurrentSource>> currents;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
static IniSaver<NameInfo> gChannels{"PDP"};
|
||||
static std::vector<std::unique_ptr<PDPSource>> 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<PDPSource>(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<double[]>(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);
|
||||
|
||||
@@ -10,19 +10,44 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <hal/Ports.h>
|
||||
#include <hal/simulation/AddressableLEDData.h>
|
||||
#include <hal/simulation/PWMData.h>
|
||||
#include <imgui.h>
|
||||
|
||||
#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<NameInfo> gPWM{"PWM"};
|
||||
static std::vector<std::unique_ptr<PWMSpeedSource>> gPWMSources;
|
||||
|
||||
static void UpdatePWMSources() {
|
||||
static const int numPWM = HAL_GetNumPWMChannels();
|
||||
if (static_cast<size_t>(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<PWMSpeedSource>(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<std::unique_ptr<History>> 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<unsigned int>(i) > history.size())
|
||||
// history.resize(i + 1);
|
||||
// if (!history[i]) history[i] = std::make_unique<History>();
|
||||
|
||||
// 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);
|
||||
|
||||
894
simulation/halsim_gui/src/main/native/cpp/PlotGui.cpp
Normal file
894
simulation/halsim_gui/src/main/native/cpp/PlotGui.cpp
Normal file
@@ -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 <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#include <hal/simulation/MockHooks.h>
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h>
|
||||
#include <implot.h>
|
||||
#include <wpi/Signal.h>
|
||||
#include <wpi/SmallVector.h>
|
||||
#include <wpi/timestamp.h>
|
||||
#include <wpi/raw_ostream.h>
|
||||
|
||||
#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<int> m_size = 0;
|
||||
std::atomic<int> 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<std::unique_ptr<PlotSeries>> 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<Plot> 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<ImU32>(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<GetterData*>(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<const PlotSeriesRef*>(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<int>(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<int>(m_axisRange[i].min * 1000), i,
|
||||
static_cast<int>(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<GuiDataSource**>(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<PlotSeries>(source, yAxis == -1 ? 0 : yAxis));
|
||||
}
|
||||
} else if (const ImGuiPayload* payload =
|
||||
ImGui::AcceptDragDropPayload("PlotSeries")) {
|
||||
auto ref = static_cast<const PlotSeriesRef*>(payload->Data);
|
||||
MovePlotSeries(ref->plotIndex, ref->seriesIndex, i, m_series.size(), yAxis);
|
||||
} else if (const ImGuiPayload* payload =
|
||||
ImGui::AcceptDragDropPayload("Plot")) {
|
||||
auto fromPlotIndex = *static_cast<const size_t*>(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<void*>(
|
||||
static_cast<intptr_t>(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<PlotSeries>(id)).get();
|
||||
}
|
||||
|
||||
static void PlotSeries_ReadLine(ImGuiContext* ctx,
|
||||
ImGuiSettingsHandler* handler, void* entry,
|
||||
const char* lineStr) {
|
||||
auto element = static_cast<PlotSeries*>(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<int>(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);
|
||||
}
|
||||
17
simulation/halsim_gui/src/main/native/cpp/PlotGui.h
Normal file
17
simulation/halsim_gui/src/main/native/cpp/PlotGui.h
Normal file
@@ -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
|
||||
@@ -9,29 +9,63 @@
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <hal/Ports.h>
|
||||
#include <hal/simulation/RelayData.h>
|
||||
#include <imgui.h>
|
||||
|
||||
#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<NameInfo> gRelays{"Relay"};
|
||||
static std::vector<std::unique_ptr<RelayForwardSource>> gRelayForwardSources;
|
||||
static std::vector<std::unique_ptr<RelayReverseSource>> 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<RelayForwardSource>(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<RelayReverseSource>(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);
|
||||
|
||||
@@ -7,13 +7,55 @@
|
||||
|
||||
#include "RoboRioGui.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <hal/simulation/RoboRioData.h>
|
||||
#include <imgui.h>
|
||||
|
||||
#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<RoboRioSource> gRioSource;
|
||||
|
||||
static void UpdateRoboRioSources() {
|
||||
if (!gRioSource) gRioSource = std::make_unique<RoboRioSource>();
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -9,12 +9,16 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <hal/SimDevice.h>
|
||||
#include <hal/simulation/SimDeviceData.h>
|
||||
#include <imgui.h>
|
||||
#include <wpi/DenseMap.h>
|
||||
|
||||
#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<SimValueSource*>(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<std::function<void()>> gDeviceExecutors;
|
||||
static IniSaverString<ElementInfo> gElements{"Device"};
|
||||
static wpi::DenseMap<HAL_SimValueHandle, std::unique_ptr<SimValueSource>>
|
||||
gSimValueSources;
|
||||
|
||||
static void UpdateSimValueSources() {
|
||||
HALSIM_EnumerateSimDevices(
|
||||
"", nullptr, [](const char* name, void*, HAL_SimDeviceHandle handle) {
|
||||
HALSIM_EnumerateSimValues(
|
||||
handle, const_cast<char*>(name),
|
||||
[](const char* name, void* deviceV, HAL_SimValueHandle handle,
|
||||
HAL_Bool readonly, const HAL_Value* value) {
|
||||
auto device = static_cast<const char*>(deviceV);
|
||||
auto& source = gSimValueSources[handle];
|
||||
if (!source) {
|
||||
source = std::make_unique<SimValueSource>(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);
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <hal/Ports.h>
|
||||
#include <hal/simulation/PCMData.h>
|
||||
@@ -16,28 +18,60 @@
|
||||
#include <wpi/SmallVector.h>
|
||||
|
||||
#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<std::unique_ptr<PCMSolenoidOutputSource>> solenoids;
|
||||
int initCount = 0;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
static IniSaver<OpenInfo> gPCMs{"PCM"};
|
||||
static IniSaver<NameInfo> gSolenoids{"Solenoid"};
|
||||
static std::vector<PCMSource> 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<PCMSolenoidOutputSource>(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<int, 16> 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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
186
simulation/halsim_gui/src/main/native/include/GuiDataSource.h
Normal file
186
simulation/halsim_gui/src/main/native/include/GuiDataSource.h
Normal file
@@ -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 <atomic>
|
||||
#include <string>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <wpi/Signal.h>
|
||||
#include <wpi/StringRef.h>
|
||||
#include <wpi/Twine.h>
|
||||
#include <wpi/spinlock.h>
|
||||
|
||||
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<wpi::spinlock, double> valueChanged;
|
||||
|
||||
static GuiDataSource* Find(wpi::StringRef id);
|
||||
|
||||
static wpi::sig::Signal<const char*, GuiDataSource*> sourceCreated;
|
||||
|
||||
private:
|
||||
std::string m_id;
|
||||
std::string m_name;
|
||||
bool m_digital = false;
|
||||
std::atomic<double> 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<cbname##Source*>(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<cbname##Source*>(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<cbname##Source*>(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)
|
||||
@@ -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];
|
||||
|
||||
@@ -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 <vector>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h>
|
||||
|
||||
namespace halsimgui {
|
||||
|
||||
template <typename Info>
|
||||
class IniSaverVector : public std::vector<Info> {
|
||||
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"
|
||||
@@ -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 <typename Info>
|
||||
void IniSaverVector<Info>::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 <typename Info>
|
||||
void* IniSaverVector<Info>::ReadOpen(ImGuiContext* ctx,
|
||||
ImGuiSettingsHandler* handler,
|
||||
const char* name) {
|
||||
auto self = static_cast<IniSaverVector*>(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 <typename Info>
|
||||
void IniSaverVector<Info>::ReadLine(ImGuiContext* ctx,
|
||||
ImGuiSettingsHandler* handler, void* entry,
|
||||
const char* lineStr) {
|
||||
auto element = static_cast<Info*>(entry);
|
||||
wpi::StringRef line{lineStr};
|
||||
auto [name, value] = line.split('=');
|
||||
name = name.trim();
|
||||
value = value.trim();
|
||||
element->ReadIni(name, value);
|
||||
}
|
||||
|
||||
template <typename Info>
|
||||
void IniSaverVector<Info>::WriteAll(ImGuiContext* ctx,
|
||||
ImGuiSettingsHandler* handler,
|
||||
ImGuiTextBuffer* out_buf) {
|
||||
auto self = static_cast<IniSaverVector*>(handler->UserData);
|
||||
for (size_t i = 0; i < self->size(); ++i) {
|
||||
out_buf->appendf("[%s][%d]\n", self->m_typeName, static_cast<int>(i));
|
||||
(*self)[i].WriteIni(out_buf);
|
||||
out_buf->append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace halsimgui
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user