diff --git a/simulation/halsim_gui/CMakeLists.txt b/simulation/halsim_gui/CMakeLists.txt index 91af53c7c0..f1804e458e 100644 --- a/simulation/halsim_gui/CMakeLists.txt +++ b/simulation/halsim_gui/CMakeLists.txt @@ -7,7 +7,7 @@ file(GLOB halsim_gui_src src/main/native/cpp/*.cpp) add_library(halsim_gui MODULE ${halsim_gui_src}) wpilib_target_warnings(halsim_gui) set_target_properties(halsim_gui PROPERTIES DEBUG_POSTFIX "d") -target_link_libraries(halsim_gui PUBLIC hal PRIVATE imgui) +target_link_libraries(halsim_gui PUBLIC hal ntcore PRIVATE imgui) target_include_directories(halsim_gui PRIVATE src/main/native/include) diff --git a/simulation/halsim_gui/build.gradle b/simulation/halsim_gui/build.gradle index 5df53023dd..49a2f49d93 100644 --- a/simulation/halsim_gui/build.gradle +++ b/simulation/halsim_gui/build.gradle @@ -4,6 +4,7 @@ if (!project.hasProperty('onlylinuxathena') && !project.hasProperty('onlylinuxra ext { includeWpiutil = true + includeNtCore = true pluginName = 'halsim_gui' } diff --git a/simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.cpp b/simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.cpp new file mode 100644 index 0000000000..548b49a31c --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.cpp @@ -0,0 +1,362 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "NetworkTablesGui.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "HALSimGui.h" + +using namespace halsimgui; + +static void BooleanArrayToString(wpi::SmallVectorImpl& out, + wpi::ArrayRef in) { + out.clear(); + wpi::raw_svector_ostream os{out}; + os << '['; + bool first = true; + for (auto v : in) { + if (!first) os << ','; + first = false; + if (v) + os << "true"; + else + os << "false"; + } + os << ']'; +} + +static std::shared_ptr StringToBooleanArray(wpi::StringRef in) { + in = in.trim(); + if (in.empty()) + return nt::NetworkTableValue::MakeBooleanArray( + std::initializer_list{}); + if (in.front() == '[') in = in.drop_front(); + if (in.back() == ']') in = in.drop_back(); + in = in.trim(); + + wpi::SmallVector inSplit; + wpi::SmallVector out; + + in.split(inSplit, ',', -1, false); + for (auto val : inSplit) { + val = val.trim(); + if (val.equals_lower("true")) { + out.emplace_back(1); + } else if (val.equals_lower("false")) { + out.emplace_back(0); + } else { + wpi::errs() << "GUI: NetworkTables: Could not understand value '" << val + << "'\n"; + return nullptr; + } + } + + return nt::NetworkTableValue::MakeBooleanArray(out); +} + +static void DoubleArrayToString(wpi::SmallVectorImpl& out, + wpi::ArrayRef in) { + out.clear(); + wpi::raw_svector_ostream os{out}; + os << '['; + bool first = true; + for (auto v : in) { + if (!first) os << ','; + first = false; + os << wpi::format("%.6f", v); + } + os << ']'; +} + +static std::shared_ptr StringToDoubleArray(wpi::StringRef in) { + in = in.trim(); + if (in.empty()) + return nt::NetworkTableValue::MakeBooleanArray( + std::initializer_list{}); + if (in.front() == '[') in = in.drop_front(); + if (in.back() == ']') in = in.drop_back(); + in = in.trim(); + + wpi::SmallVector inSplit; + wpi::SmallVector out; + + in.split(inSplit, ',', -1, false); + for (auto val : inSplit) { + val = val.trim(); + wpi::SmallString<32> valStr = val; + double d; + if (std::sscanf(valStr.c_str(), "%lf", &d) == 1) { + out.emplace_back(d); + } else { + wpi::errs() << "GUI: NetworkTables: Could not understand value '" << val + << "'\n"; + return nullptr; + } + } + + return nt::NetworkTableValue::MakeDoubleArray(out); +} + +static void StringArrayToString(wpi::SmallVectorImpl& out, + wpi::ArrayRef in) { + out.clear(); + wpi::raw_svector_ostream os{out}; + os << '['; + bool first = true; + for (auto&& v : in) { + if (!first) os << ','; + first = false; + os << '"'; + os.write_escaped(v); + os << '"'; + } + os << ']'; +} + +static int fromxdigit(char ch) { + if (ch >= 'a' && ch <= 'f') + return (ch - 'a' + 10); + else if (ch >= 'A' && ch <= 'F') + return (ch - 'A' + 10); + else + return ch - '0'; +} + +static wpi::StringRef UnescapeString(wpi::StringRef source, + wpi::SmallVectorImpl& buf) { + assert(source.size() >= 2 && source.front() == '"' && source.back() == '"'); + buf.clear(); + buf.reserve(source.size() - 2); + for (auto s = source.begin() + 1, end = source.end() - 1; s != end; ++s) { + if (*s != '\\') { + buf.push_back(*s); + continue; + } + switch (*++s) { + case 't': + buf.push_back('\t'); + break; + case 'n': + buf.push_back('\n'); + break; + case 'x': { + if (!isxdigit(*(s + 1))) { + buf.push_back('x'); // treat it like a unknown escape + break; + } + int ch = fromxdigit(*++s); + if (std::isxdigit(*(s + 1))) { + ch <<= 4; + ch |= fromxdigit(*++s); + } + buf.push_back(static_cast(ch)); + break; + } + default: + buf.push_back(*s); + break; + } + } + return wpi::StringRef{buf.data(), buf.size()}; +} + +static std::shared_ptr StringToStringArray(wpi::StringRef in) { + in = in.trim(); + if (in.empty()) + return nt::NetworkTableValue::MakeStringArray( + std::initializer_list{}); + if (in.front() == '[') in = in.drop_front(); + if (in.back() == ']') in = in.drop_back(); + in = in.trim(); + + wpi::SmallVector inSplit; + std::vector out; + wpi::SmallString<32> buf; + + in.split(inSplit, ',', -1, false); + for (auto val : inSplit) { + val = val.trim(); + if (val.empty()) continue; + if (val.front() != '"' || val.back() != '"') { + wpi::errs() << "GUI: NetworkTables: Could not understand value '" << val + << "'\n"; + return nullptr; + } + out.emplace_back(UnescapeString(val, buf)); + } + + return nt::NetworkTableValue::MakeStringArray(std::move(out)); +} + +static constexpr size_t kTextBufferSize = 4096; + +static char* GetTextBuffer(wpi::StringRef in) { + static char textBuffer[kTextBufferSize]; + size_t len = (std::min)(in.size(), kTextBufferSize - 1); + std::memcpy(textBuffer, in.data(), len); + textBuffer[len] = '\0'; + return textBuffer; +} + +static void DisplayNetworkTables() { + static auto inst = nt::NetworkTableInstance::GetDefault(); + + if (ImGui::CollapsingHeader("Connections")) { + ImGui::Columns(4, "connections"); + ImGui::Text("Id"); + ImGui::NextColumn(); + ImGui::Text("Address"); + ImGui::NextColumn(); + ImGui::Text("Updated"); + ImGui::NextColumn(); + ImGui::Text("Proto"); + ImGui::NextColumn(); + ImGui::Separator(); + for (auto&& i : inst.GetConnections()) { + ImGui::Text("%s", i.remote_id.c_str()); + ImGui::NextColumn(); + ImGui::Text("%s", i.remote_ip.c_str()); + ImGui::NextColumn(); + ImGui::Text("%llu", + static_cast( // NOLINT(runtime/int) + i.last_update)); + ImGui::NextColumn(); + ImGui::Text("%d.%d", i.protocol_version >> 8, i.protocol_version & 0xff); + ImGui::NextColumn(); + } + ImGui::Columns(); + } + + if (ImGui::CollapsingHeader("Values", ImGuiTreeNodeFlags_DefaultOpen)) { + static bool first = true; + ImGui::Columns(4, "values"); + if (first) ImGui::SetColumnWidth(-1, 0.5f * ImGui::GetWindowWidth()); + ImGui::Text("Name"); + ImGui::NextColumn(); + ImGui::Text("Value"); + ImGui::NextColumn(); + if (first) ImGui::SetColumnWidth(-1, 12 * ImGui::GetFontSize()); + ImGui::Text("Flags"); + ImGui::NextColumn(); + ImGui::Text("Changed"); + ImGui::NextColumn(); + ImGui::Separator(); + first = false; + + auto info = inst.GetEntryInfo("", 0); + std::sort(info.begin(), info.end(), + [](const auto& a, const auto& b) { return a.name < b.name; }); + + for (auto&& i : info) { + ImGui::Text("%s", i.name.c_str()); + ImGui::NextColumn(); + + if (auto val = nt::GetEntryValue(i.entry)) { + ImGui::PushID(i.name.c_str()); + switch (val->type()) { + case NT_BOOLEAN: { + static const char* boolOptions[] = {"false", "true"}; + int v = val->GetBoolean() ? 1 : 0; + if (ImGui::Combo("boolean", &v, boolOptions, 2)) + nt::SetEntryValue(i.entry, nt::NetworkTableValue::MakeBoolean(v)); + break; + } + case NT_DOUBLE: { + double v = val->GetDouble(); + if (ImGui::InputDouble("double", &v, 0, 0, "%.6f", + ImGuiInputTextFlags_EnterReturnsTrue)) + nt::SetEntryValue(i.entry, nt::NetworkTableValue::MakeDouble(v)); + break; + } + case NT_STRING: { + char* v = GetTextBuffer(val->GetString()); + if (ImGui::InputText("string", v, kTextBufferSize, + ImGuiInputTextFlags_EnterReturnsTrue)) + nt::SetEntryValue(i.entry, nt::NetworkTableValue::MakeString(v)); + break; + } + case NT_BOOLEAN_ARRAY: { + wpi::SmallString<64> buf; + BooleanArrayToString(buf, val->GetBooleanArray()); + char* v = GetTextBuffer(buf); + if (ImGui::InputText("boolean[]", v, kTextBufferSize, + ImGuiInputTextFlags_EnterReturnsTrue)) { + if (auto outv = StringToBooleanArray(v)) + nt::SetEntryValue(i.entry, std::move(outv)); + } + break; + } + case NT_DOUBLE_ARRAY: { + wpi::SmallString<64> buf; + DoubleArrayToString(buf, val->GetDoubleArray()); + char* v = GetTextBuffer(buf); + if (ImGui::InputText("double[]", v, kTextBufferSize, + ImGuiInputTextFlags_EnterReturnsTrue)) { + if (auto outv = StringToDoubleArray(v)) + nt::SetEntryValue(i.entry, std::move(outv)); + } + break; + } + case NT_STRING_ARRAY: { + wpi::SmallString<64> buf; + StringArrayToString(buf, val->GetStringArray()); + char* v = GetTextBuffer(buf); + if (ImGui::InputText("string[]", v, kTextBufferSize, + ImGuiInputTextFlags_EnterReturnsTrue)) { + if (auto outv = StringToStringArray(v)) + nt::SetEntryValue(i.entry, std::move(outv)); + } + break; + } + case NT_RAW: + ImGui::LabelText("raw", "[...]"); + break; + case NT_RPC: + ImGui::LabelText("rpc", "[...]"); + break; + default: + ImGui::LabelText("other", "?"); + break; + } + ImGui::PopID(); + } + ImGui::NextColumn(); + + if ((i.flags & NT_PERSISTENT) != 0) + ImGui::Text("Persistent"); + else if (i.flags != 0) + ImGui::Text("%02x", i.flags); + ImGui::NextColumn(); + + ImGui::Text("%llu", + static_cast( // NOLINT(runtime/int) + i.last_change)); + ImGui::NextColumn(); + ImGui::Separator(); + } + ImGui::Columns(); + } +} + +void NetworkTablesGui::Initialize() { + HALSimGui::AddWindow("NetworkTables", DisplayNetworkTables); + HALSimGui::SetDefaultWindowPos("NetworkTables", 250, 260); + HALSimGui::SetDefaultWindowSize("NetworkTables", 1500, 375); +} diff --git a/simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.h b/simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.h new file mode 100644 index 0000000000..f4e5d7baeb --- /dev/null +++ b/simulation/halsim_gui/src/main/native/cpp/NetworkTablesGui.h @@ -0,0 +1,17 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2020 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#pragma once + +namespace halsimgui { + +class NetworkTablesGui { + public: + static void Initialize(); +}; + +} // namespace halsimgui diff --git a/simulation/halsim_gui/src/main/native/cpp/main.cpp b/simulation/halsim_gui/src/main/native/cpp/main.cpp index 2362d34311..acbfbd6c29 100644 --- a/simulation/halsim_gui/src/main/native/cpp/main.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/main.cpp @@ -19,6 +19,7 @@ #include "EncoderGui.h" #include "Field2D.h" #include "HALSimGui.h" +#include "NetworkTablesGui.h" #include "PDPGui.h" #include "PWMGui.h" #include "RelayGui.h" @@ -44,6 +45,7 @@ __declspec(dllexport) HALSimGui::Add(DIOGui::Initialize); HALSimGui::Add(EncoderGui::Initialize); HALSimGui::Add(Field2D::Initialize); + HALSimGui::Add(NetworkTablesGui::Initialize); HALSimGui::Add(PDPGui::Initialize); HALSimGui::Add(PWMGui::Initialize); HALSimGui::Add(RelayGui::Initialize);