diff --git a/sysid/src/main/native/cpp/App.cpp b/sysid/src/main/native/cpp/App.cpp index 947ea434a65..fd74ebd8c22 100644 --- a/sysid/src/main/native/cpp/App.cpp +++ b/sysid/src/main/native/cpp/App.cpp @@ -24,21 +24,18 @@ #include #include "sysid/view/Analyzer.h" -#include "sysid/view/JSONConverter.h" -#include "sysid/view/Logger.h" +#include "sysid/view/LogLoader.h" #include "sysid/view/UILayout.h" namespace gui = wpi::gui; static std::unique_ptr gWindowManager; -glass::Window* gLoggerWindow; +glass::Window* gLogLoaderWindow; glass::Window* gAnalyzerWindow; glass::Window* gProgramLogWindow; static glass::MainMenuBar gMainMenu; -std::unique_ptr gJSONConverter; - glass::LogData gLog; wpi::Logger gLogger; @@ -103,8 +100,8 @@ void Application(std::string_view saveDir) { gWindowManager = std::make_unique(storage); gWindowManager->GlobalInit(); - gLoggerWindow = gWindowManager->AddWindow( - "Logger", std::make_unique(storage, gLogger)); + gLogLoaderWindow = gWindowManager->AddWindow( + "Log Loader", std::make_unique(storage, gLogger)); gAnalyzerWindow = gWindowManager->AddWindow( "Analyzer", std::make_unique(storage, gLogger)); @@ -115,10 +112,10 @@ void Application(std::string_view saveDir) { // Set default positions and sizes for windows. // Logger window position/size - gLoggerWindow->SetDefaultPos(sysid::kLoggerWindowPos.x, - sysid::kLoggerWindowPos.y); - gLoggerWindow->SetDefaultSize(sysid::kLoggerWindowSize.x, - sysid::kLoggerWindowSize.y); + gLogLoaderWindow->SetDefaultPos(sysid::kLogLoaderWindowPos.x, + sysid::kLogLoaderWindowPos.y); + gLogLoaderWindow->SetDefaultSize(sysid::kLogLoaderWindowSize.x, + sysid::kLogLoaderWindowSize.y); // Analyzer window position/size gAnalyzerWindow->SetDefaultPos(sysid::kAnalyzerWindowPos.x, @@ -133,8 +130,6 @@ void Application(std::string_view saveDir) { sysid::kProgramLogWindowSize.y); gProgramLogWindow->DisableRenamePopup(); - gJSONConverter = std::make_unique(gLogger); - // Configure save file. gui::ConfigurePlatformSaveFile("sysid.ini"); @@ -157,15 +152,6 @@ void Application(std::string_view saveDir) { ImGui::EndMenu(); } - bool toCSV = false; - if (ImGui::BeginMenu("JSON Converters")) { - if (ImGui::MenuItem("JSON to CSV Converter")) { - toCSV = true; - } - - ImGui::EndMenu(); - } - if (ImGui::BeginMenu("Docs")) { if (ImGui::MenuItem("Online documentation")) { wpi::gui::OpenURL( @@ -178,19 +164,6 @@ void Application(std::string_view saveDir) { ImGui::EndMainMenuBar(); - if (toCSV) { - ImGui::OpenPopup("SysId JSON to CSV Converter"); - toCSV = false; - } - - if (ImGui::BeginPopupModal("SysId JSON to CSV Converter")) { - gJSONConverter->DisplayCSVConvert(); - if (ImGui::Button("Close")) { - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); - } - if (about) { ImGui::OpenPopup("About"); about = false; diff --git a/sysid/src/main/native/cpp/analysis/AnalysisManager.cpp b/sysid/src/main/native/cpp/analysis/AnalysisManager.cpp index 8c1ffff6e0b..6b6bdd6a03a 100644 --- a/sysid/src/main/native/cpp/analysis/AnalysisManager.cpp +++ b/sysid/src/main/native/cpp/analysis/AnalysisManager.cpp @@ -18,7 +18,6 @@ #include "sysid/Util.h" #include "sysid/analysis/FilteringUtils.h" -#include "sysid/analysis/JSONConverter.h" #include "sysid/analysis/TrackWidthAnalysis.h" using namespace sysid; @@ -464,20 +463,7 @@ AnalysisManager::AnalysisManager(std::string_view path, Settings& settings, // Check that we have a sysid JSON if (m_json.find("sysid") == m_json.end()) { - // If it's not a sysid JSON, try converting it from frc-char format - std::string newPath = sysid::ConvertJSON(path, logger); - - // Read JSON from the specified path - std::error_code ec; - std::unique_ptr fileBuffer = - wpi::MemoryBuffer::GetFile(path, ec); - if (fileBuffer == nullptr || ec) { - throw FileReadingError(newPath); - } - - m_json = wpi::json::parse(fileBuffer->GetCharBuffer()); - - WPI_INFO(m_logger, "Read {}", newPath); + throw FileReadingError(path); } WPI_INFO(m_logger, "Parsing initial data of {}", path); diff --git a/sysid/src/main/native/cpp/telemetry/TelemetryManager.cpp b/sysid/src/main/native/cpp/telemetry/TelemetryManager.cpp deleted file mode 100644 index ac97cdbca4d..00000000000 --- a/sysid/src/main/native/cpp/telemetry/TelemetryManager.cpp +++ /dev/null @@ -1,275 +0,0 @@ -// Copyright (c) FIRST and other WPILib contributors. -// Open Source Software; you can modify and/or share it under the terms of -// the WPILib BSD license file in the root directory of this project. - -#include "sysid/telemetry/TelemetryManager.h" - -#include -#include // for ::tolower -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "sysid/Util.h" -#include "sysid/analysis/AnalysisType.h" - -using namespace sysid; - -TelemetryManager::TelemetryManager(const Settings& settings, - wpi::Logger& logger, - nt::NetworkTableInstance instance) - : m_settings(settings), m_logger(logger), m_inst(instance) {} - -void TelemetryManager::BeginTest(std::string_view name) { - // Create a new test params instance for this test. - m_params = - TestParameters{name.starts_with("fast"), name.ends_with("forward"), - m_settings.mechanism == analysis::kDrivetrainAngular, - State::WaitingForEnable}; - - // Add this test to the list of running tests and set the running flag. - m_tests.push_back(std::string{name}); - m_isRunningTest = true; - - // Set the Voltage Command Entry - m_voltageCommand.Set((m_params.fast ? m_settings.stepVoltage - : m_settings.quasistaticRampRate) * - (m_params.forward ? 1 : -1)); - - // Set the test type - m_testType.Set(m_params.fast ? "Dynamic" : "Quasistatic"); - - // Set the rotate entry - m_rotate.Set(m_params.rotate); - - // Set the current mechanism in NT. - m_mechanism.Set(m_settings.mechanism.name); - // Set Overflow to False - m_overflowPub.Set(false); - // Set Mechanism Error to False - m_mechErrorPub.Set(false); - m_inst.Flush(); - - // Display the warning message. - for (auto&& func : m_callbacks) { - func( - "Please enable the robot in autonomous mode, and then " - "disable it " - "before it runs out of space. \n Note: The robot will " - "continue " - "to move until you disable it - It is your " - "responsibility to " - "ensure it does not hit anything!"); - } - - WPI_INFO(m_logger, "Started {} test.", m_tests.back()); -} - -void TelemetryManager::EndTest() { - // If there is no test running, this is a no-op - if (!m_isRunningTest) { - return; - } - - // Disable the running flag and store the data in the JSON. - m_isRunningTest = false; - m_data[m_tests.back()] = m_params.data; - - // Call the cancellation callbacks. - for (auto&& func : m_callbacks) { - std::string msg; - if (m_params.mechError) { - msg += - "\nERROR: The robot indicated that you are using the wrong project " - "for characterizing your mechanism. \nThis most likely means you " - "are trying to characterize a mechanism like a Drivetrain with a " - "deployed config for a General Mechanism (e.g. Arm, Flywheel, and " - "Elevator) or vice versa. Please double check your settings and " - "try again."; - } else if (!m_params.data.empty()) { - std::string units = m_settings.units; - std::transform(m_settings.units.begin(), m_settings.units.end(), - units.begin(), ::tolower); - - if (std::string_view{m_settings.mechanism.name}.starts_with( - "Drivetrain")) { - double p = (m_params.data.back()[3] - m_params.data.front()[3]) * - m_settings.unitsPerRotation; - double s = (m_params.data.back()[4] - m_params.data.front()[4]) * - m_settings.unitsPerRotation; - double g = m_params.data.back()[7] - m_params.data.front()[7]; - - msg = fmt::format( - "The left and right encoders traveled {} {} and {} {} " - "respectively.\nThe gyro angle delta was {} degrees.", - p, units, s, units, g * 180.0 / std::numbers::pi); - } else { - double p = (m_params.data.back()[2] - m_params.data.front()[2]) * - m_settings.unitsPerRotation; - msg = fmt::format("The encoder reported traveling {} {}.", p, units); - } - - if (m_params.overflow) { - msg += - "\nNOTE: the robot stopped recording data early because the entry " - "storage was exceeded."; - } - } else { - msg = "No data was detected."; - } - func(msg); - } - - // Remove previously run test from list of tests if no data was detected. - if (m_params.data.empty()) { - m_tests.pop_back(); - } - - // Send a zero command over NT. - m_voltageCommand.Set(0.0); - m_inst.Flush(); -} - -void TelemetryManager::Update() { - // If there is no test running, these is nothing to update. - if (!m_isRunningTest) { - return; - } - - // Update the NT entries that we're reading. - - int currAckNumber = m_ackNumberSub.Get(); - std::string telemetryValue; - - // Get the FMS Control Word. - for (auto tsValue : m_fmsControlData.ReadQueue()) { - uint32_t ctrl = tsValue.value; - m_params.enabled = ctrl & 0x01; - } - - // Get the string in the data field. - for (auto tsValue : m_telemetry.ReadQueue()) { - telemetryValue = tsValue.value; - } - - // Get the overflow flag - for (auto tsValue : m_overflowSub.ReadQueue()) { - m_params.overflow = tsValue.value; - } - - // Get the mechanism error flag - for (auto tsValue : m_mechErrorSub.ReadQueue()) { - m_params.mechError = tsValue.value; - } - - // Go through our state machine. - if (m_params.state == State::WaitingForEnable) { - if (m_params.enabled) { - m_params.enableStart = wpi::Now() * 1E-6; - m_params.state = State::RunningTest; - m_ackNumber = currAckNumber; - WPI_INFO(m_logger, "{}", "Transitioned to running test state."); - } - } - - if (m_params.state == State::RunningTest) { - // If for some reason we've disconnected, end the test. - if (!m_inst.IsConnected()) { - WPI_WARNING(m_logger, "{}", - "NT connection was dropped when executing the test. The test " - "has been canceled."); - EndTest(); - } - - // If the robot has disabled, then we can move on to the next step. - if (!m_params.enabled) { - m_params.disableStart = wpi::Now() * 1E-6; - m_params.state = State::WaitingForData; - WPI_INFO(m_logger, "{}", "Transitioned to waiting for data."); - } - } - - if (m_params.state == State::WaitingForData) { - double now = wpi::Now() * 1E-6; - m_voltageCommand.Set(0.0); - m_inst.Flush(); - - // Process valid data - if (!telemetryValue.empty() && m_ackNumber < currAckNumber) { - m_params.raw = std::move(telemetryValue); - m_ackNumber = currAckNumber; - } - - // We have the data that we need, so we can parse it and end the test. - if (!m_params.raw.empty() && - wpi::starts_with(m_params.raw, m_tests.back())) { - // Remove test type from start of string - m_params.raw.erase(0, m_params.raw.find(';') + 1); - - // Clean up the string -- remove spaces if there are any. - m_params.raw.erase( - std::remove_if(m_params.raw.begin(), m_params.raw.end(), ::isspace), - m_params.raw.end()); - - // Split the string into individual components. - wpi::SmallVector res; - wpi::split(m_params.raw, res, ','); - - // Convert each string to double. - std::vector values; - values.reserve(res.size()); - for (auto&& str : res) { - values.push_back(wpi::parse_float(str).value()); - } - - // Add the values to our result vector. - for (size_t i = 0; i < values.size() - m_settings.mechanism.rawDataSize; - i += m_settings.mechanism.rawDataSize) { - std::vector d(m_settings.mechanism.rawDataSize); - - std::copy_n(std::make_move_iterator(values.begin() + i), - m_settings.mechanism.rawDataSize, d.begin()); - m_params.data.push_back(std::move(d)); - } - - WPI_INFO(m_logger, - "Received data with size: {} for the {} test in {} seconds.", - m_params.data.size(), m_tests.back(), - m_params.data.back()[0] - m_params.data.front()[0]); - m_ackNumberPub.Set(++m_ackNumber); - EndTest(); - } - - // If we timed out, end the test and let the user know. - if (now - m_params.disableStart > 5.0) { - WPI_WARNING(m_logger, "{}", - "TelemetryManager did not receieve data 5 seconds after " - "completing the test..."); - EndTest(); - } - } -} - -std::string TelemetryManager::SaveJSON(std::string_view location) { - m_data["test"] = m_settings.mechanism.name; - m_data["units"] = m_settings.units; - m_data["unitsPerRotation"] = m_settings.unitsPerRotation; - m_data["sysid"] = true; - - std::string loc = fmt::format("{}/sysid_data{:%Y%m%d-%H%M%S}.json", location, - std::chrono::system_clock::now()); - - sysid::SaveFile(m_data.dump(2), std::filesystem::path{loc}); - WPI_INFO(m_logger, "Wrote JSON to: {}", loc); - - return loc; -} diff --git a/sysid/src/main/native/cpp/view/JSONConverter.cpp b/sysid/src/main/native/cpp/view/JSONConverter.cpp deleted file mode 100644 index 88eaa6a02f0..00000000000 --- a/sysid/src/main/native/cpp/view/JSONConverter.cpp +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) FIRST and other WPILib contributors. -// Open Source Software; you can modify and/or share it under the terms of -// the WPILib BSD license file in the root directory of this project. - -#include "sysid/analysis/JSONConverter.h" -#include "sysid/view/JSONConverter.h" - -#include - -#include -#include -#include - -#include "sysid/Util.h" - -using namespace sysid; - -void JSONConverter::DisplayConverter( - const char* tooltip, - std::function converter) { - if (ImGui::Button(tooltip)) { - m_opener = std::make_unique( - tooltip, "", std::vector{"JSON File", SYSID_PFD_JSON_EXT}); - } - - if (m_opener && m_opener->ready()) { - if (!m_opener->result().empty()) { - m_location = m_opener->result()[0]; - try { - converter(m_location, m_logger); - m_timestamp = wpi::Now() * 1E-6; - } catch (const std::exception& e) { - ImGui::OpenPopup("Exception Caught!"); - m_exception = e.what(); - } - } - m_opener.reset(); - } - - if (wpi::Now() * 1E-6 - m_timestamp < 5) { - ImGui::SameLine(); - ImGui::Text("Saved!"); - } - - // Handle exceptions. - ImGui::SetNextWindowSize(ImVec2(480.f, 0.0f)); - if (ImGui::BeginPopupModal("Exception Caught!")) { - ImGui::PushTextWrapPos(0.0f); - ImGui::Text( - "An error occurred when parsing the JSON. This most likely means that " - "the JSON data is incorrectly formatted."); - ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s", - m_exception.c_str()); - ImGui::PopTextWrapPos(); - if (ImGui::Button("Close")) { - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); - } -} - -void JSONConverter::DisplayCSVConvert() { - DisplayConverter("Select SysId JSON", sysid::ToCSV); -} diff --git a/sysid/src/main/native/cpp/view/LogLoader.cpp b/sysid/src/main/native/cpp/view/LogLoader.cpp new file mode 100644 index 00000000000..66e2b22c75d --- /dev/null +++ b/sysid/src/main/native/cpp/view/LogLoader.cpp @@ -0,0 +1,192 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "sysid/view/LogLoader.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace sysid; + +void LogLoader::Display() { + if (ImGui::Button("Open data log file...")) { + m_opener = std::make_unique( + "Select Data Log", "", + std::vector{"DataLog Files", "*.wpilog"}); + } + + // Handle opening the file + if (m_opener && m_opener->ready(0)) { + if (!m_opener->result().empty()) { + m_filename = m_opener->result()[0]; + + std::error_code ec; + auto buf = wpi::MemoryBuffer::GetFile(m_filename, ec); + if (ec) { + ImGui::OpenPopup("Error"); + m_error = fmt::format("Could not open file: {}", ec.message()); + return; + } + + wpi::log::DataLogReader reader{std::move(buf)}; + if (!reader.IsValid()) { + ImGui::OpenPopup("Error"); + m_error = "Not a valid datalog file"; + return; + } + m_reader = + std::make_unique(std::move(reader)); + m_entryTree.clear(); + } + m_opener.reset(); + } + + // Handle errors + ImGui::SetNextWindowSize(ImVec2(480.f, 0.0f)); + if (ImGui::BeginPopupModal("Error")) { + ImGui::PushTextWrapPos(0.0f); + ImGui::TextUnformatted(m_error.c_str()); + ImGui::PopTextWrapPos(); + if (ImGui::Button("Close")) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + if (!m_reader) { + return; + } + + // Summary info + ImGui::TextUnformatted(fs::path{m_filename}.stem().string().c_str()); + ImGui::Text("%u records, %u entries%s", m_reader->GetNumRecords(), + m_reader->GetNumEntries(), + m_reader->IsDone() ? "" : " (working)"); + + if (!m_reader->IsDone()) { + return; + } + + // Display tree of entries + if (m_entryTree.empty()) { + RebuildEntryTree(); + } + + ImGui::BeginTable( + "Entries", 3, + ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp); + ImGui::TableSetupColumn("Name"); + ImGui::TableSetupColumn("Type"); + ImGui::TableSetupColumn("Metadata"); + ImGui::TableHeadersRow(); + DisplayEntryTree(m_entryTree); + ImGui::EndTable(); +} + +void LogLoader::RebuildEntryTree() { + wpi::SmallVector parts; + m_reader->ForEachEntryName([&](const glass::DataLogReaderThread::Entry& + entry) { + // only show double/float entries (TODO: support struct/protobuf) + if (entry.type != "double" && entry.type != "float") { + return; + } + + parts.clear(); + // split on first : if one is present + auto [prefix, mainpart] = wpi::split(entry.name, ':'); + if (mainpart.empty() || wpi::contains(prefix, '/')) { + mainpart = entry.name; + } else { + parts.emplace_back(prefix); + } + wpi::split(mainpart, parts, '/', -1, false); + + // ignore a raw "/" key + if (parts.empty()) { + return; + } + + // get to leaf + auto nodes = &m_entryTree; + for (auto part : wpi::drop_back(std::span{parts.begin(), parts.end()})) { + auto it = + std::find_if(nodes->begin(), nodes->end(), + [&](const auto& node) { return node.name == part; }); + if (it == nodes->end()) { + nodes->emplace_back(part); + // path is from the beginning of the string to the end of the current + // part; this works because part is a reference to the internals of + // entry.name + nodes->back().path.assign( + entry.name.data(), part.data() + part.size() - entry.name.data()); + it = nodes->end() - 1; + } + nodes = &it->children; + } + + auto it = std::find_if(nodes->begin(), nodes->end(), [&](const auto& node) { + return node.name == parts.back(); + }); + if (it == nodes->end()) { + nodes->emplace_back(parts.back()); + // no need to set path, as it's identical to entry.name + it = nodes->end() - 1; + } + it->entry = &entry; + }); +} + +static void EmitEntry(const std::string& name, + const glass::DataLogReaderThread::Entry& entry) { + ImGui::TableNextColumn(); + ImGui::Selectable(name.c_str()); + if (ImGui::BeginDragDropSource()) { + auto entryPtr = &entry; + ImGui::SetDragDropPayload("DataLogEntry", &entryPtr, + sizeof(entryPtr)); // NOLINT + ImGui::TextUnformatted(entry.name.data(), + entry.name.data() + entry.name.size()); + ImGui::EndDragDropSource(); + } + + ImGui::TableNextColumn(); + ImGui::TextUnformatted(entry.type.data(), + entry.type.data() + entry.type.size()); + + ImGui::TableNextColumn(); + ImGui::TextUnformatted(entry.metadata.data(), + entry.metadata.data() + entry.metadata.size()); +} + +void LogLoader::DisplayEntryTree(const std::vector& tree) { + for (auto&& node : tree) { + if (node.entry) { + EmitEntry(node.name, *node.entry); + } + + if (!node.children.empty()) { + ImGui::TableNextColumn(); + bool open = ImGui::TreeNodeEx(node.name.c_str(), + ImGuiTreeNodeFlags_SpanFullWidth); + ImGui::TableNextColumn(); + ImGui::TableNextColumn(); + if (open) { + DisplayEntryTree(node.children); + ImGui::TreePop(); + } + } + } +} diff --git a/sysid/src/main/native/cpp/view/Logger.cpp b/sysid/src/main/native/cpp/view/Logger.cpp deleted file mode 100644 index 5e7773dbd0f..00000000000 --- a/sysid/src/main/native/cpp/view/Logger.cpp +++ /dev/null @@ -1,222 +0,0 @@ -// Copyright (c) FIRST and other WPILib contributors. -// Open Source Software; you can modify and/or share it under the terms of -// the WPILib BSD license file in the root directory of this project. - -#include "sysid/view/Logger.h" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "sysid/Util.h" -#include "sysid/analysis/AnalysisType.h" -#include "sysid/view/UILayout.h" - -using namespace sysid; - -Logger::Logger(glass::Storage& storage, wpi::Logger& logger) - : m_logger{logger}, m_ntSettings{"sysid", storage} { - wpi::gui::AddEarlyExecute([&] { m_ntSettings.Update(); }); - - m_ntSettings.EnableServerOption(false); -} - -void Logger::Display() { - // Get the current width of the window. This will be used to scale - // our UI elements. - float width = ImGui::GetContentRegionAvail().x; - - // Add team number input and apply button for NT connection. - m_ntSettings.Display(); - - // Reset and clear the internal manager state. - ImGui::SameLine(); - if (ImGui::Button("Reset Telemetry")) { - m_settings = TelemetryManager::Settings{}; - m_manager = std::make_unique(m_settings, m_logger); - m_settings.mechanism = analysis::FromName(kTypes[m_selectedType]); - } - - // Add NT connection indicator. - static ImVec4 kColorDisconnected{1.0f, 0.4f, 0.4f, 1.0f}; - static ImVec4 kColorConnected{0.2f, 1.0f, 0.2f, 1.0f}; - ImGui::SameLine(); - bool ntConnected = nt::NetworkTableInstance::GetDefault().IsConnected(); - ImGui::TextColored(ntConnected ? kColorConnected : kColorDisconnected, - ntConnected ? "NT Connected" : "NT Disconnected"); - - // Create a Section for project configuration - ImGui::Separator(); - ImGui::Spacing(); - ImGui::Text("Project Parameters"); - - // Add a dropdown for mechanism type. - ImGui::SetNextItemWidth(ImGui::GetFontSize() * kTextBoxWidthMultiple); - - if (ImGui::Combo("Mechanism", &m_selectedType, kTypes, - IM_ARRAYSIZE(kTypes))) { - m_settings.mechanism = analysis::FromName(kTypes[m_selectedType]); - } - - // Add Dropdown for Units - ImGui::SetNextItemWidth(ImGui::GetFontSize() * kTextBoxWidthMultiple); - if (ImGui::Combo("Unit Type", &m_selectedUnit, kUnits, - IM_ARRAYSIZE(kUnits))) { - m_settings.units = kUnits[m_selectedUnit]; - } - - sysid::CreateTooltip( - "This is the type of units that your gains will be in. For example, if " - "you want your flywheel gains in terms of radians, then use the radians " - "unit. On the other hand, if your drivetrain will use gains in meters, " - "choose meters."); - - // Rotational units have fixed Units per rotations - m_isRotationalUnits = - (m_settings.units == "Rotations" || m_settings.units == "Degrees" || - m_settings.units == "Radians"); - if (m_settings.units == "Degrees") { - m_settings.unitsPerRotation = 360.0; - } else if (m_settings.units == "Radians") { - m_settings.unitsPerRotation = 2 * std::numbers::pi; - } else if (m_settings.units == "Rotations") { - m_settings.unitsPerRotation = 1.0; - } - - // Units Per Rotations entry - ImGui::SetNextItemWidth(ImGui::GetFontSize() * kTextBoxWidthMultiple); - ImGui::InputDouble("Units Per Rotation", &m_settings.unitsPerRotation, 0.0f, - 0.0f, "%.4f", - m_isRotationalUnits ? ImGuiInputTextFlags_ReadOnly - : ImGuiInputTextFlags_None); - sysid::CreateTooltip( - "The logger assumes that the code will be sending recorded motor shaft " - "rotations over NetworkTables. This value will then be multiplied by the " - "units per rotation to get the measurement in the units you " - "specified.\n\nFor non-rotational units (e.g. meters), this value is " - "usually the wheel diameter times pi (should not include gearing)."); - // Create a section for voltage parameters. - ImGui::Separator(); - ImGui::Spacing(); - ImGui::Text("Voltage Parameters"); - - auto CreateVoltageParameters = [this](const char* text, double* data, - float min, float max) { - ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6); - ImGui::PushItemFlag(ImGuiItemFlags_Disabled, - m_manager && m_manager->IsActive()); - float value = static_cast(*data); - if (ImGui::SliderFloat(text, &value, min, max, "%.2f")) { - *data = value; - } - ImGui::PopItemFlag(); - }; - - CreateVoltageParameters("Quasistatic Ramp Rate (V/s)", - &m_settings.quasistaticRampRate, 0.10f, 0.60f); - sysid::CreateTooltip( - "This is the rate at which the voltage will increase during the " - "quasistatic test."); - - CreateVoltageParameters("Dynamic Step Voltage (V)", &m_settings.stepVoltage, - 0.0f, 10.0f); - sysid::CreateTooltip( - "This is the voltage that will be applied for the " - "dynamic voltage (acceleration) tests."); - - // Create a section for tests. - ImGui::Separator(); - ImGui::Spacing(); - ImGui::Text("Tests"); - - auto CreateTest = [this, width](const char* text, const char* itext) { - // Display buttons if we have an NT connection. - if (nt::NetworkTableInstance::GetDefault().IsConnected()) { - // Create button to run tests. - if (ImGui::Button(text)) { - // Open the warning message. - ImGui::OpenPopup("Warning"); - m_manager->BeginTest(itext); - m_opened = text; - } - if (m_opened == text && ImGui::BeginPopupModal("Warning")) { - ImGui::TextWrapped("%s", m_popupText.c_str()); - if (ImGui::Button(m_manager->IsActive() ? "End Test" : "Close")) { - m_manager->EndTest(); - ImGui::CloseCurrentPopup(); - m_opened = ""; - } - ImGui::EndPopup(); - } - } else { - // Show disabled text when there is no connection. - ImGui::TextDisabled("%s", text); - } - - // Show whether the tests were run or not. - bool run = m_manager->HasRunTest(itext); - ImGui::SameLine(width * 0.7); - ImGui::Text(run ? "Run" : "Not Run"); - }; - - CreateTest("Quasistatic Forward", "slow-forward"); - CreateTest("Quasistatic Backward", "slow-backward"); - CreateTest("Dynamic Forward", "fast-forward"); - CreateTest("Dynamic Backward", "fast-backward"); - - m_manager->RegisterDisplayCallback( - [this](const auto& str) { m_popupText = str; }); - - // Display the path to where the JSON will be saved and a button to select the - // location. - ImGui::Separator(); - ImGui::Spacing(); - ImGui::Text("Save Location"); - if (ImGui::Button("Choose")) { - m_selector = std::make_unique("Select Folder"); - } - ImGui::SameLine(); - ImGui::InputText("##savelocation", &m_jsonLocation, - ImGuiInputTextFlags_ReadOnly); - - // Add button to save. - ImGui::SameLine(width * 0.9); - if (ImGui::Button("Save")) { - try { - m_manager->SaveJSON(m_jsonLocation); - } catch (const std::exception& e) { - ImGui::OpenPopup("Exception Caught!"); - m_exception = e.what(); - } - } - - // Handle exceptions. - if (ImGui::BeginPopupModal("Exception Caught!")) { - ImGui::Text("%s", m_exception.c_str()); - if (ImGui::Button("Close")) { - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); - } - - // Run periodic methods. - SelectDataFolder(); - m_ntSettings.Update(); - m_manager->Update(); -} - -void Logger::SelectDataFolder() { - // If the selector exists and is ready with a result, we can store it. - if (m_selector && m_selector->ready()) { - m_jsonLocation = m_selector->result(); - m_selector.reset(); - } -} diff --git a/sysid/src/main/native/include/sysid/telemetry/TelemetryManager.h b/sysid/src/main/native/include/sysid/telemetry/TelemetryManager.h deleted file mode 100644 index 85ee09e520b..00000000000 --- a/sysid/src/main/native/include/sysid/telemetry/TelemetryManager.h +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright (c) FIRST and other WPILib contributors. -// Open Source Software; you can modify and/or share it under the terms of -// the WPILib BSD license file in the root directory of this project. - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "sysid/analysis/AnalysisType.h" - -namespace sysid { -/** - * This class is responsible for collecting data from the robot and storing it - * inside a JSON. - */ -class TelemetryManager { - public: - /** - * Represents settings for an instance of the TelemetryManager class. This - * contains information about the quasistatic ramp rate for slow tests, the - * step voltage for fast tests, and the mechanism type for characterization. - */ - struct Settings { - /** - * The rate at which the voltage should increase during the quasistatic test - * (V/s). - */ - double quasistaticRampRate = 0.25; - - /** - * The voltage that the dynamic test should run at (V). - */ - double stepVoltage = 7.0; - - /** - * The units the mechanism moves per recorded rotation. The sysid project - * will be recording things in rotations of the shaft so the - * unitsPerRotation is to convert those measurements to the units the user - * wants to use. - */ - double unitsPerRotation = 1.0; - - /** - * The name of the units used. - * Valid units: "Meters", "Feet", "Inches", "Radians", "Degrees", - * "Rotations" - */ - std::string units = "Meters"; - - /** - * The type of mechanism that will be analyzed. - * Supported mechanisms: Drivetrain, Angular Drivetrain, Elevator, Arm, - * Simple motor. - */ - AnalysisType mechanism = analysis::kDrivetrain; - }; - - /** - * Constructs an instance of the telemetry manager with the provided settings - * and NT instance to collect data over. - * - * @param settings The settings for this instance of the telemetry manager. - * @param logger The logger instance to use for log data. - * @param instance The NT instance to collect data over. The default value of - * this parameter should suffice in production; it should only - * be changed during unit testing. - */ - explicit TelemetryManager(const Settings& settings, wpi::Logger& logger, - nt::NetworkTableInstance instance = - nt::NetworkTableInstance::GetDefault()); - - /** - * Begins a test with the given parameters. - * - * @param name The name of the test. - */ - void BeginTest(std::string_view name); - - /** - * Ends the currently running test. If there is no test running, this is a - * no-op. - */ - void EndTest(); - - /** - * Updates the telemetry manager -- this adds a new autospeed entry and - * collects newest data from the robot. This must be called periodically by - * the user. - */ - void Update(); - - /** - * Registers a callback that's called by the TelemetryManager when there is a - * message to display to the user. - * - * @param callback Callback function that runs based off of the message - */ - void RegisterDisplayCallback(std::function callback) { - m_callbacks.emplace_back(std::move(callback)); - } - - /** - * Saves a JSON with the stored data at the given location. - * - * @param location The location to save the JSON at (this is the folder that - * should contain the saved JSON). - * @return The full file path of the saved JSON. - */ - std::string SaveJSON(std::string_view location); - - /** - * Returns whether a test is currently running. - * - * @return Whether a test is currently running. - */ - bool IsActive() const { return m_isRunningTest; } - - /** - * Returns whether the specified test is running or has run. - * - * @param name The test to check. - * - * @return Whether the specified test is running or has run. - */ - bool HasRunTest(std::string_view name) const { - return std::find(m_tests.cbegin(), m_tests.cend(), name) != m_tests.end(); - } - - /** - * Gets the size of the stored data. - * - * @return The size of the stored data - */ - size_t GetCurrentDataSize() const { return m_params.data.size(); } - - private: - enum class State { WaitingForEnable, RunningTest, WaitingForData }; - - /** - * Stores information about a currently running test. This information - * includes whether the robot will be traveling quickly (dynamic) or slowly - * (quasistatic), the direction of movement, the start time of the test, - * whether the robot is enabled, the current speed of the robot, and the - * collected data. - */ - struct TestParameters { - bool fast = false; - bool forward = false; - bool rotate = false; - - State state = State::WaitingForEnable; - - double enableStart = 0.0; - double disableStart = 0.0; - - bool enabled = false; - double speed = 0.0; - - std::string raw; - std::vector> data{}; - bool overflow = false; - bool mechError = false; - - TestParameters() = default; - TestParameters(bool fast, bool forward, bool rotate, State state) - : fast{fast}, forward{forward}, rotate{rotate}, state{state} {} - }; - - // Settings for this instance. - const Settings& m_settings; - - // Logger. - wpi::Logger& m_logger; - - // Test parameters for the currently running test. - TestParameters m_params; - bool m_isRunningTest = false; - - // A list of running or already run tests. - std::vector m_tests; - - // Stores the test data. - wpi::json m_data; - - // Display callbacks. - wpi::SmallVector, 1> m_callbacks; - - // NetworkTables instance and entries. - nt::NetworkTableInstance m_inst; - std::shared_ptr table = m_inst.GetTable("SmartDashboard"); - nt::DoublePublisher m_voltageCommand = - table->GetDoubleTopic("SysIdVoltageCommand").Publish(); - nt::StringPublisher m_testType = - table->GetStringTopic("SysIdTestType").Publish(); - nt::BooleanPublisher m_rotate = - table->GetBooleanTopic("SysIdRotate").Publish(); - nt::StringPublisher m_mechanism = - table->GetStringTopic("SysIdTest").Publish(); - nt::BooleanPublisher m_overflowPub = - table->GetBooleanTopic("SysIdOverflow").Publish(); - nt::BooleanSubscriber m_overflowSub = - table->GetBooleanTopic("SysIdOverflow").Subscribe(false); - nt::BooleanPublisher m_mechErrorPub = - table->GetBooleanTopic("SysIdWrongMech").Publish(); - nt::BooleanSubscriber m_mechErrorSub = - table->GetBooleanTopic("SysIdWrongMech").Subscribe(false); - nt::StringSubscriber m_telemetry = - table->GetStringTopic("SysIdTelemetry").Subscribe(""); - nt::IntegerSubscriber m_fmsControlData = - m_inst.GetTable("FMSInfo") - ->GetIntegerTopic("FMSControlData") - .Subscribe(0); - nt::DoublePublisher m_ackNumberPub = - table->GetDoubleTopic("SysIdAckNumber").Publish(); - nt::DoubleSubscriber m_ackNumberSub = - table->GetDoubleTopic("SysIdAckNumber").Subscribe(0); - - int m_ackNumber; -}; -} // namespace sysid diff --git a/sysid/src/main/native/include/sysid/view/DataSelector.h b/sysid/src/main/native/include/sysid/view/DataSelector.h new file mode 100644 index 00000000000..00c22c4253a --- /dev/null +++ b/sysid/src/main/native/include/sysid/view/DataSelector.h @@ -0,0 +1,39 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include + +namespace glass { +class Storage; +} // namespace glass + +namespace wpi { +class Logger; +} // namespace wpi + +namespace sysid { +/** + * Helps with loading datalog files. + */ +class DataSelector : public glass::View { + public: + /** + * Creates a data selector widget + * + * @param logger The program logger + */ + explicit DataSelector(glass::Storage& storage, wpi::Logger& logger) + : m_logger(logger) {} + + /** + * Displays the log loader window. + */ + void Display() override; + + private: + wpi::Logger& m_logger; +}; +} // namespace sysid diff --git a/sysid/src/main/native/include/sysid/view/JSONConverter.h b/sysid/src/main/native/include/sysid/view/JSONConverter.h deleted file mode 100644 index 89bfa3290d1..00000000000 --- a/sysid/src/main/native/include/sysid/view/JSONConverter.h +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) FIRST and other WPILib contributors. -// Open Source Software; you can modify and/or share it under the terms of -// the WPILib BSD license file in the root directory of this project. - -#pragma once - -#include -#include -#include -#include - -#include -#include -#include - -namespace sysid { -/** - * Helps with converting different JSONs into different formats. Primarily - * enables users to convert an old 2020 FRC-Characterization JSON into a SysId - * JSON or a SysId JSON into a CSV file. - */ -class JSONConverter { - public: - /** - * Creates a JSONConverter widget - * - * @param logger The program logger - */ - explicit JSONConverter(wpi::Logger& logger) : m_logger(logger) {} - - /** - * Function to display the SysId JSON to CSV converter. - */ - void DisplayCSVConvert(); - - private: - /** - * Helper method to display a specific JSON converter - * - * @param tooltip The tooltip describing the JSON converter - * @param converter The function that takes a filename path and performs the - * previously specifid JSON conversion. - */ - void DisplayConverter( - const char* tooltip, - std::function converter); - - wpi::Logger& m_logger; - - std::string m_location; - std::unique_ptr m_opener; - - std::string m_exception; - - double m_timestamp = 0; -}; -} // namespace sysid diff --git a/sysid/src/main/native/include/sysid/view/LogLoader.h b/sysid/src/main/native/include/sysid/view/LogLoader.h new file mode 100644 index 00000000000..dfdd399c99e --- /dev/null +++ b/sysid/src/main/native/include/sysid/view/LogLoader.h @@ -0,0 +1,64 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +namespace glass { +class Storage; +} // namespace glass + +namespace wpi { +class Logger; +} // namespace wpi + +namespace sysid { +/** + * Helps with loading datalog files. + */ +class LogLoader : public glass::View { + public: + /** + * Creates a log loader widget + * + * @param logger The program logger + */ + explicit LogLoader(glass::Storage& storage, wpi::Logger& logger) + : m_logger(logger) {} + + /** + * Displays the log loader window. + */ + void Display() override; + + private: + wpi::Logger& m_logger; + + std::string m_filename; + std::unique_ptr m_opener; + std::unique_ptr m_reader; + + std::string m_error; + + struct EntryTreeNode { + explicit EntryTreeNode(std::string_view name) : name{name} {} + std::string name; // name of just this node + std::string path; // full path if entry is nullptr + const glass::DataLogReaderThread::Entry* entry = nullptr; + std::vector children; // children, sorted by name + }; + std::vector m_entryTree; + + void RebuildEntryTree(); + void DisplayEntryTree(const std::vector& nodes); +}; +} // namespace sysid diff --git a/sysid/src/main/native/include/sysid/view/Logger.h b/sysid/src/main/native/include/sysid/view/Logger.h deleted file mode 100644 index d06d6508165..00000000000 --- a/sysid/src/main/native/include/sysid/view/Logger.h +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) FIRST and other WPILib contributors. -// Open Source Software; you can modify and/or share it under the terms of -// the WPILib BSD license file in the root directory of this project. - -#pragma once - -#include -#include - -#include -#include -#include -#include -#include - -#include "sysid/telemetry/TelemetryManager.h" - -namespace glass { -class Storage; -} // namespace glass - -namespace sysid { -/** - * The logger GUI takes care of running the system idenfitication tests over - * NetworkTables and logging the data. This data is then stored in a JSON file - * which can be used for analysis. - */ -class Logger : public glass::View { - public: - /** - * Makes a logger widget. - * - * @param storage The glass storage object - * @param logger A logger object that keeps track of the program's logs - */ - Logger(glass::Storage& storage, wpi::Logger& logger); - - /** - * Displays the logger widget. - */ - void Display() override; - - /** - * The different mechanism / analysis types that are supported. - */ - static constexpr const char* kTypes[] = {"Drivetrain", "Drivetrain (Angular)", - "Arm", "Elevator", "Simple"}; - - /** - * The different units that are supported. - */ - static constexpr const char* kUnits[] = {"Meters", "Feet", "Inches", - "Radians", "Rotations", "Degrees"}; - - private: - /** - * Handles the logic of selecting a folder to save the SysId JSON to - */ - void SelectDataFolder(); - - wpi::Logger& m_logger; - - TelemetryManager::Settings m_settings; - int m_selectedType = 0; - int m_selectedUnit = 0; - - std::unique_ptr m_manager = - std::make_unique(m_settings, m_logger); - - std::unique_ptr m_selector; - std::string m_jsonLocation; - - glass::NetworkTablesSettings m_ntSettings; - - bool m_isRotationalUnits = false; - - std::string m_popupText; - - std::string m_opened; - std::string m_exception; -}; -} // namespace sysid diff --git a/sysid/src/main/native/include/sysid/view/UILayout.h b/sysid/src/main/native/include/sysid/view/UILayout.h index 732a1aaf658..25d4436e4f4 100644 --- a/sysid/src/main/native/include/sysid/view/UILayout.h +++ b/sysid/src/main/native/include/sysid/view/UILayout.h @@ -62,9 +62,9 @@ inline constexpr Vector2d kLeftColSize{ 310, kAppWindowSize.y - kLeftColPos.y - kWindowGap}; // Left column contents -inline constexpr Vector2d kLoggerWindowPos = kLeftColPos; -inline constexpr Vector2d kLoggerWindowSize{ - kLeftColSize.x, kAppWindowSize.y - kWindowGap - kLoggerWindowPos.y}; +inline constexpr Vector2d kLogLoaderWindowPos = kLeftColPos; +inline constexpr Vector2d kLogLoaderWindowSize{ + kLeftColSize.x, kAppWindowSize.y - kWindowGap - kLogLoaderWindowPos.y}; // Center column position and size inline constexpr Vector2d kCenterColPos =