[sim] Add plotting to simulation GUI

This commit is contained in:
Peter Johnson
2020-08-14 20:02:35 -07:00
parent 70ba92f917
commit 1593eb4d47
29 changed files with 2550 additions and 301 deletions

View File

@@ -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 ""

View File

@@ -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"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

View 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();
}

View File

@@ -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) {

View File

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

View File

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

View File

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

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

View 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

View File

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

View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -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

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

View File

@@ -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];

View File

@@ -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"

View File

@@ -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

View File

@@ -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