From 4aa6cd8ba1b308db6cd6ed4c8c49941fb636455c Mon Sep 17 00:00:00 2001 From: Blake Date: Wed, 22 Jul 2026 18:00:35 -0400 Subject: [PATCH 01/36] qt does things --- MBR_data.json | 8 ++++---- cmake/MbrDaqSuite.cmake | 17 ++++++++++++++++- cmake/VendorSetup.cmake | 11 +++++++++++ include/app/gui.hpp | 33 ++++++++++++--------------------- src/main.cpp | 41 ++++++++++++++++++++++++++++++++++------- 5 files changed, 77 insertions(+), 33 deletions(-) diff --git a/MBR_data.json b/MBR_data.json index 219ba76..d7b4b99 100644 --- a/MBR_data.json +++ b/MBR_data.json @@ -3,9 +3,9 @@ { "key": "T", "name": "Timestamp", "required": true, "plot": false }, { "key": "E", "name": "Engine RPM", "required": false, "plot": true, "unit": "RPM", "group": "rpm" }, { "key": "W", "name": "Wheel RPM", "required": false, "plot": true, "unit": "RPM", "group": "rpm" }, - { "key": "fr", "name": "Front Right", "required": false, "plot": true, "unit": "mm", "group": "shock" }, - { "key": "fl", "name": "Front Left", "required": false, "plot": true, "unit": "mm", "group": "shock" }, - { "key": "br", "name": "Back Right", "required": false, "plot": true, "unit": "mm", "group": "shock" }, - { "key": "bl", "name": "Back Left", "required": false, "plot": true, "unit": "mm", "group": "shock" } + { "key": "FR", "name": "Front Right", "required": false, "plot": true, "unit": "mm", "group": "shock" }, + { "key": "FL", "name": "Front Left", "required": false, "plot": true, "unit": "mm", "group": "shock" }, + { "key": "RR", "name": "Back Right", "required": false, "plot": true, "unit": "mm", "group": "shock" }, + { "key": "RL", "name": "Back Left", "required": false, "plot": true, "unit": "mm", "group": "shock" } ] } diff --git a/cmake/MbrDaqSuite.cmake b/cmake/MbrDaqSuite.cmake index 171f74a..1755416 100644 --- a/cmake/MbrDaqSuite.cmake +++ b/cmake/MbrDaqSuite.cmake @@ -1,5 +1,10 @@ function(create_mbrdaq_suite TARGET_NAME EXTRA_FLAGS OUT_DIR) add_library(mbrdaq_core_${TARGET_NAME} STATIC ${LIB_SOURCES}) + set_target_properties(mbrdaq_core_${TARGET_NAME} PROPERTIES + AUTOMOC ON + AUTOUIC OFF + AUTORCC OFF + ) target_link_libraries(mbrdaq_core_${TARGET_NAME} PUBLIC imgui_setup implot_setup @@ -20,6 +25,8 @@ function(create_mbrdaq_suite TARGET_NAME EXTRA_FLAGS OUT_DIR) magic_enum::magic_enum unordered_dense::unordered_dense stdx + Qt6::Core + Qt6::Widgets ) target_include_directories(mbrdaq_core_${TARGET_NAME} PUBLIC include) @@ -90,7 +97,6 @@ function(create_standard_target EXTRA_FLAGS TARGET_NAME) RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${TARGET_NAME} OUTPUT_NAME ${PROJECT_NAME}) - # Windows still depends on a custom ffmpeg dll provided by opencv if(WIN32) add_custom_command(TARGET mbrdaq_${TARGET_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory @@ -98,5 +104,14 @@ function(create_standard_target EXTRA_FLAGS TARGET_NAME) "$" COMMENT "Copy FFMPEG to output directory" ) + + if(WINDEPLOYQT_EXE) + add_custom_command(TARGET mbrdaq_${TARGET_NAME} POST_BUILD + COMMAND ${WINDEPLOYQT_EXE} + --dir "$" + "$" + COMMENT "Deploy Qt runtime libraries" + ) + endif() endif() endfunction() diff --git a/cmake/VendorSetup.cmake b/cmake/VendorSetup.cmake index 58a669b..3dd404d 100644 --- a/cmake/VendorSetup.cmake +++ b/cmake/VendorSetup.cmake @@ -230,3 +230,14 @@ option(ENABLE_STDX_PROFILE "Enable profiling/tracing in stdx" OFF) set(STDX_PROFILE ${ENABLE_STDX_PROFILE} CACHE BOOL "" FORCE) add_vendor_subdirectory(vendor/stdx) + +# Qt +if(WIN32) + list(APPEND CMAKE_PREFIX_PATH "C:/msys64/ucrt64") +endif() + +find_package(Qt6 REQUIRED COMPONENTS Core Widgets) +find_program(WINDEPLOYQT_EXE windeployqt + HINTS "${Qt6_DIR}/../../../bin" "${Qt6_DIR}/../../../share/qt6/bin" +) +message(STATUS "windeployqt found: ${WINDEPLOYQT_EXE}") diff --git a/include/app/gui.hpp b/include/app/gui.hpp index d62110a..c3b5538 100644 --- a/include/app/gui.hpp +++ b/include/app/gui.hpp @@ -1,35 +1,26 @@ #pragma once - #include -#include - +#include +#include +#include #include "app/context.hpp" #include "app/pages/page.hpp" -#include "assets/texture.hpp" - -struct sapp_event; -struct sapp_desc; namespace mbr { -class gui_t { - public: - explicit gui_t(const std::shared_ptr& ctx) : context_{ctx} {}; - - sapp_desc get_sokol_desc(); - void on_init(); - void on_frame(); - void on_event(const sapp_event* event); - void on_cleanup(); +class gui_t : public QMainWindow { + Q_OBJECT +public: + explicit gui_t(const std::shared_ptr& ctx, QWidget* parent = nullptr); - private: +private: void change_page(page_type_t type); - void draw_main_menu_bar(); + void build_menu_bar(); - private: - std::unique_ptr current_page_; +private: + QStackedWidget* pages_; + pages::page* current_page_ = nullptr; // now owned by pages_, not unique_ptr std::shared_ptr context_; - std::string command_buf_; }; } // namespace mbr diff --git a/src/main.cpp b/src/main.cpp index 2cdb0b1..d94a0d4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,10 +1,37 @@ -#include -#include +#include +#include +#include +#include +#include +#include -#include "app/app.hpp" +int main(int argc, char *argv[]) { + // 1. Initialize the Qt Application instance + QApplication app(argc, argv); -i32 main(i32 argc, char** argv) { - stdx::profiler profiler{argv[0]}; - mbr::app_t app{argc, argv}; - app.run(); + // 2. Create the main window + QMainWindow mainWindow; + mainWindow.setWindowTitle("MBR-DAQ Qt Window"); + mainWindow.resize(400, 300); + + // 3. Create a layout and widgets programmatically + auto *centralWidget = new QWidget(&mainWindow); + auto *layout = new QVBoxLayout(centralWidget); + + auto *button = new QPushButton("Click Me!", centralWidget); + layout->addWidget(button); + + // 4. Connect button click (Signal) to a pop-up dialog (Slot) + QObject::connect(button, &QPushButton::clicked, [&]() { + QMessageBox::information(&mainWindow, "Qt Signal", "Hello from Qt6 C++!"); + }); + + centralWidget->setLayout(layout); + mainWindow.setCentralWidget(centralWidget); + + // 5. Display the window + mainWindow.show(); + + // 6. Start the Qt event loop + return app.exec(); } From a6ada5b09f02201475c57258c84bc6e3697519ef Mon Sep 17 00:00:00 2001 From: Blake Date: Sun, 26 Jul 2026 15:47:15 -0400 Subject: [PATCH 02/36] I do not know what I am doing, but I am trying to restructure the gui backend --- include/app/app.hpp | 4 +- include/app/context.hpp | 6 +- include/app/gui.hpp | 6 + src/app/app.cpp | 7 +- src/app/gui.cpp | 249 +++------------------------------------- src/main.cpp | 39 +------ 6 files changed, 38 insertions(+), 273 deletions(-) diff --git a/include/app/app.hpp b/include/app/app.hpp index e6c24aa..bba5520 100644 --- a/include/app/app.hpp +++ b/include/app/app.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -12,13 +13,14 @@ class gui_t; class app_t { public: - explicit app_t(i32 arc, char** argv); + explicit app_t(i32 argc, char** argv); ~app_t(); MAKE_PINNED(app_t); void run(); private: + std::unique_ptr qt_app_; std::unique_ptr gui_; std::shared_ptr context_; }; diff --git a/include/app/context.hpp b/include/app/context.hpp index 2128e16..7f36d30 100644 --- a/include/app/context.hpp +++ b/include/app/context.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include @@ -14,9 +15,8 @@ namespace mbr { enum class page_type_t : u8 { HOME, - RPM, - SHOCK, - VIEW, + PLOT, + ANALYSIS, SERIAL, }; diff --git a/include/app/gui.hpp b/include/app/gui.hpp index c3b5538..9543669 100644 --- a/include/app/gui.hpp +++ b/include/app/gui.hpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include "app/context.hpp" #include "app/pages/page.hpp" @@ -16,10 +17,15 @@ class gui_t : public QMainWindow { private: void change_page(page_type_t type); void build_menu_bar(); + [[nodiscard]] pages::page* create_page(page_type_t type, const std::shared_ptr& ctx, QWidget* parent); private: QStackedWidget* pages_; pages::page* current_page_ = nullptr; // now owned by pages_, not unique_ptr + //homepage* home_page_; + //plot_page* plot_page; + //analysis_page* analysis_page_; + //serial_page* serial_page; std::shared_ptr context_; }; diff --git a/src/app/app.cpp b/src/app/app.cpp index 1ead367..cc6c12e 100644 --- a/src/app/app.cpp +++ b/src/app/app.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "app/context.hpp" #include "app/gui.hpp" @@ -20,7 +21,7 @@ namespace mbr { namespace { const std::filesystem::path DEFAULT_JSON_PATH{"MBR_data.json"}; } // namespace -app_t::app_t(i32, char**) : context_{std::make_shared()} /*, m_Manager{m_Context}*/ { +app_t::app_t(i32 argc, char** argv) : qt_app_{std::make_unique(argc, argv)}, context_{std::make_shared()} /*, m_Manager{m_Context}*/ { context_->log = context_->logger.get_log_fn(); ix::initNetSystem(); gui_ = std::make_unique(context_); @@ -30,8 +31,8 @@ app_t::app_t(i32, char**) : context_{std::make_shared()} /*, m_Mana app_t::~app_t() { ix::uninitNetSystem(); } void app_t::run() { - auto app_desc = gui_->get_sokol_desc(); - sapp_run(&app_desc); + gui_->show(); + qt_app_->exec(); } } // namespace mbr diff --git a/src/app/gui.cpp b/src/app/gui.cpp index 71ff090..12e63e4 100644 --- a/src/app/gui.cpp +++ b/src/app/gui.cpp @@ -6,13 +6,6 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include #include #include @@ -32,236 +25,30 @@ using namespace std::chrono; namespace mbr { -namespace { - -void sokol_init_cb(void* data) { - gsl::not_null gui = static_cast(data); - gui->on_init(); -} - -void sokol_cleanup_cb(void* data) { - gsl::not_null gui = static_cast(data); - gui->on_cleanup(); -} - -void sokol_frame_cb(void* data) { - gsl::not_null gui = static_cast(data); - gui->on_frame(); -} - -void sokol_event_cb(const sapp_event* e, void* data) { - gsl::not_null gui = static_cast(data); - gui->on_event(e); -} - -void sokol_start_frame() { - simgui_frame_desc_t frame_desc = {}; - frame_desc.width = sapp_width(); - frame_desc.height = sapp_height(); - frame_desc.delta_time = sapp_frame_duration(); - frame_desc.dpi_scale = sapp_dpi_scale(); - simgui_new_frame(&frame_desc); -} - -void sokol_end_frame() { - sg_pass_action pass_action = {}; - pass_action.colors[0].load_action = SG_LOADACTION_CLEAR; - pass_action.colors[0].clear_value = {.r = 0.1F, .g = 0.1F, .b = 0.1F, .a = 1.0F}; - - sg_pass pass = {}; - pass.action = pass_action; - pass.swapchain = sglue_swapchain(); - - sg_begin_pass(&pass); - simgui_render(); - sg_end_pass(); - sg_commit(); -} - -} // namespace - -sapp_desc gui_t::get_sokol_desc() { - sapp_desc desc = {}; - desc.init_userdata_cb = sokol_init_cb; - desc.frame_userdata_cb = sokol_frame_cb; - desc.cleanup_userdata_cb = sokol_cleanup_cb; - desc.event_userdata_cb = sokol_event_cb; - desc.width = 1'920; - desc.height = 1'080; - desc.window_title = "Michigan Baja Racing - Data Suite"; - desc.icon.sokol_default = false; - desc.user_data = this; - - // The app description takes ownership of the icon here - assets::icon_texture app_icon{assets::BAJA_LOGO_PNG}; - desc.icon.images[0].width = app_icon.width; - desc.icon.images[0].height = app_icon.height; - desc.icon.images[0].pixels = {.ptr = app_icon.pixels, .size = app_icon.size}; - - return desc; -} - -void gui_t::on_init() { - sg_desc sg_description = {}; - sg_description.environment = sglue_environment(); - sg_description.logger.func = slog_func; - sg_setup(&sg_description); - - simgui_desc_t si_desc = {}; - si_desc.no_default_font = true; - simgui_setup(&si_desc); - - auto& io = ImGui::GetIO(); - context_->style.default_fonts = load_fonts(); - io.FontDefault = context_->style.default_fonts.regular; - - ImPlot::CreateContext(); - context_->style.set_dark_theme(); - - change_page(page_type_t::HOME); -} - -void gui_t::on_frame() { - PROFILE_FUNCTION(); - sokol_start_frame(); - const auto cleanup_frame{gsl::finally(sokol_end_frame)}; - MAIN_MENU_BAR(draw_main_menu_bar()); - - if (current_page_) { - const auto* viewport = ImGui::GetMainViewport(); - ImGui::SetNextWindowPos(viewport->WorkPos); - ImGui::SetNextWindowSize(viewport->WorkSize); - - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0F); - const auto sv1{gsl::finally([] { ImGui::PopStyleVar(1); })}; - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0F); - const auto sv2{gsl::finally([] { ImGui::PopStyleVar(1); })}; - - static constexpr ImGuiWindowFlags window_flags = - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus | - ImGuiWindowFlags_NoNavFocus; - - if (ImGui::Begin("##currpage", nullptr, window_flags)) { - const auto cleanup_page{gsl::finally(ImGui::End)}; - current_page_->update(); + gui_t::gui_t() { + for (auto type : {page_type_t::HOME, page_type_t::PLOT, page_type_t::ANALYSIS, page_type_t::SERIAL}) { + pages::page* p = create_page(type, context_, pages_); + pages_->addWidget(p); + page_lookup_[type] = p; } } - if (ImGui::IsKeyPressed(ImGuiKey_F11, false)) { sapp_toggle_fullscreen(); } - if (context_->backend->get_try_connection().exchange(false)) { context_->backend->start(); } -} - -void gui_t::on_event(const sapp_event* event) { // NOLINT - PROFILE_FUNCTION(); - if (event->type == SAPP_EVENTTYPE_QUIT_REQUESTED) { sapp_quit(); } - simgui_handle_event(event); -} - -void gui_t::on_cleanup() { - context_->should_exit = true; - if (current_page_) { current_page_->on_exit(); } - current_page_.reset(); - - simgui_shutdown(); - sg_shutdown(); - ImPlot::DestroyContext(); -} - -void gui_t::change_page(page_type_t type) { - if (current_page_) { current_page_->on_exit(); } - - switch (type) { - case page_type_t::HOME: current_page_ = std::make_unique(context_); break; - case page_type_t::RPM: current_page_ = std::make_unique(context_); break; - case page_type_t::SHOCK: current_page_ = std::make_unique(context_); break; - case page_type_t::VIEW: current_page_ = std::make_unique(context_); break; - case page_type_t::SERIAL: current_page_ = std::make_unique(context_); break; - } - - if (current_page_) { current_page_->on_enter(); } - context_->current_page_type = type; -} - -void gui_t::draw_main_menu_bar() { - PROFILE_FUNCTION(); - // DO NOT MOVE THIS BEGIN CALL IT WILL BREAK - if (ImGui::BeginMainMenuBar()) { - const auto cleanup_mm{gsl::finally(ImGui::EndMainMenuBar)}; - if (ImGui::BeginMenu("Menu")) { - const auto cleanup_m{gsl::finally(ImGui::EndMenu)}; - MAIN_MENU_BAR_ITEM({ - if (ImGui::MenuItem("Home")) { change_page(page_type_t::HOME); } - if (ImGui::MenuItem("RPM")) { change_page(page_type_t::RPM); } - if (ImGui::MenuItem("Shock")) { change_page(page_type_t::SHOCK); } - if (ImGui::MenuItem("View")) { change_page(page_type_t::VIEW); } - if (ImGui::MenuItem("Serial Monitor")) { change_page(page_type_t::SERIAL); } - if (ImGui::MenuItem("Toggle Dark Mode")) { - if (context_->style.dark_mode) { - context_->style.set_light_theme(); - } else { - context_->style.set_dark_theme(); - } - } - if (ImGui::MenuItem("Exit")) { sapp_request_quit(); } - }); - } - - ImGui::Separator(); - ImGui::TextUnformatted(page_type_str(context_->current_page_type)); - ImGui::Separator(); - // ImGui::TextUnformatted(page_type_str(m_Context->CurrentPageType)); - // ImGui::Separator(); - - const local_time lt; - const auto sync_time = lt.micros_since_midnight(); - - const auto command = fmt::format("CMD SYNC {}", sync_time); - if (ImGui::Button("Sync Time")) { context_->backend->send_cmd(command); } - - ImGui::Separator(); - if (ImGui::Button("Restart Connection")) { context_->backend->get_try_connection() = true; } - ImGui::Separator(); - if (ImGui::Button("Clear Data")) { context_->backend->get_data().clear(); } - ImGui::Separator(); - - // Connection indicator - const bool connected = context_->backend->is_connected(); - const bool receiving = context_->backend->is_receiving(); - - const f32 radius = 10.0F; - const ImVec2 pos = ImGui::GetCursorScreenPos(); - const ImVec2 center = ImVec2(pos.x + radius, pos.y + (ImGui::GetFrameHeight() * 0.5F)); - - ImU32 color; - if (connected) { - color = receiving ? IM_COL32(0, 200, 0, 255) : IM_COL32(255, 255, 0, 255); - } else { - color = IM_COL32(200, 0, 0, 255); - } - - ImDrawList* draw = ImGui::GetWindowDrawList(); - draw->AddCircleFilled(center, radius, color); - - ImGui::Dummy({radius * 2.5F, radius * 2.0F}); - ImGui::SameLine(); - ImGui::TextUnformatted(connected ? "Connected" : "Not Connected"); - - ImGui::Separator(); - pages::utils::draw_input_box("##command", command_buf_); - context_->is_cmd_input_focused = ImGui::IsItemFocused(); - ImGui::Separator(); - - if (ImGui::Button("Send CMD")) { - context_->backend->send_cmd(command_buf_); - command_buf_ = {}; +namespace { + [[nodiscard]] pages::page* create_page(page_type_t type, const std::shared_ptr& ctx, QWidget* parent) { + switch (type) { + case page_type_t::HOME: return new pages::home_page(ctx, parent); + case page_type_t::PLOT: return new pages::rpm_page(ctx, parent); + case page_type_t::ANALYSIS: return new pages::shock_page(ctx, parent); + case page_type_t::SERIAL: return new pages::serial_page(ctx, parent); + default: return nullptr; } + } - ImGui::Separator(); +} // namespace - const std::string time_formatted = lt.to_string(); - ImGui::TextUnformatted(time_formatted.c_str()); + void gui_t::change_page(page_type_t type) { + pages_->setCurrentWidget(type); } -} + } // namespace mbr diff --git a/src/main.cpp b/src/main.cpp index d94a0d4..7ac15be 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,37 +1,6 @@ -#include -#include -#include -#include -#include -#include +#include "app/app.hpp" -int main(int argc, char *argv[]) { - // 1. Initialize the Qt Application instance - QApplication app(argc, argv); - - // 2. Create the main window - QMainWindow mainWindow; - mainWindow.setWindowTitle("MBR-DAQ Qt Window"); - mainWindow.resize(400, 300); - - // 3. Create a layout and widgets programmatically - auto *centralWidget = new QWidget(&mainWindow); - auto *layout = new QVBoxLayout(centralWidget); - - auto *button = new QPushButton("Click Me!", centralWidget); - layout->addWidget(button); - - // 4. Connect button click (Signal) to a pop-up dialog (Slot) - QObject::connect(button, &QPushButton::clicked, [&]() { - QMessageBox::information(&mainWindow, "Qt Signal", "Hello from Qt6 C++!"); - }); - - centralWidget->setLayout(layout); - mainWindow.setCentralWidget(centralWidget); - - // 5. Display the window - mainWindow.show(); - - // 6. Start the Qt event loop - return app.exec(); +int main(int argc, char* argv[]) { + mbr::app_t app{argc, argv}; + app.run(); } From 6e4601625ada18ca1cbc5221ef5353eab981a51f Mon Sep 17 00:00:00 2001 From: Blake Watson Date: Sun, 26 Jul 2026 20:37:08 -0400 Subject: [PATCH 03/36] Worked on updating and renaming pages, removed the multiple plotting pages in favor of new one, implementation isn't CLOSE to being done --- include/app/context.hpp | 5 +- include/app/gui.hpp | 9 ++- include/app/pages/{view.hpp => analysis.hpp} | 13 ++-- include/app/pages/home.hpp | 4 +- include/app/pages/page.hpp | 7 +- include/app/pages/{shock.hpp => plot.hpp} | 7 +- include/app/pages/rpm.hpp | 34 --------- include/app/pages/serialmon.hpp | 3 +- src/app/app.cpp | 2 +- src/app/gui.cpp | 14 ++-- src/app/pages/{view.cpp => analysis.cpp} | 0 src/app/pages/{shock.cpp => plot.cpp} | 0 src/app/pages/rpm.cpp | 75 -------------------- 13 files changed, 34 insertions(+), 139 deletions(-) rename include/app/pages/{view.hpp => analysis.hpp} (91%) rename include/app/pages/{shock.hpp => plot.hpp} (75%) delete mode 100644 include/app/pages/rpm.hpp rename src/app/pages/{view.cpp => analysis.cpp} (100%) rename src/app/pages/{shock.cpp => plot.cpp} (100%) delete mode 100644 src/app/pages/rpm.cpp diff --git a/include/app/context.hpp b/include/app/context.hpp index 7f36d30..eb8c4b3 100644 --- a/include/app/context.hpp +++ b/include/app/context.hpp @@ -23,9 +23,8 @@ enum class page_type_t : u8 { [[nodiscard]] constexpr const char* page_type_str(page_type_t page_type) { switch (page_type) { case page_type_t::HOME: return "Home"; - case page_type_t::RPM: return "RPM"; - case page_type_t::SHOCK: return "Shock"; - case page_type_t::VIEW: return "View"; + case page_type_t::PLOT: return "Plot"; + case page_type_t::ANALYSIS: return "Analysis"; case page_type_t::SERIAL: return "Serial Monitor"; default: return "Unknown"; } diff --git a/include/app/gui.hpp b/include/app/gui.hpp index 9543669..12a9db7 100644 --- a/include/app/gui.hpp +++ b/include/app/gui.hpp @@ -6,6 +6,9 @@ #include #include "app/context.hpp" #include "app/pages/page.hpp" +#include +#include +#include namespace mbr { @@ -21,12 +24,8 @@ class gui_t : public QMainWindow { private: QStackedWidget* pages_; - pages::page* current_page_ = nullptr; // now owned by pages_, not unique_ptr - //homepage* home_page_; - //plot_page* plot_page; - //analysis_page* analysis_page_; - //serial_page* serial_page; std::shared_ptr context_; + ankerl::unordered_dense::map page_lookup_; }; } // namespace mbr diff --git a/include/app/pages/view.hpp b/include/app/pages/analysis.hpp similarity index 91% rename from include/app/pages/view.hpp rename to include/app/pages/analysis.hpp index 52edbd9..f4d0977 100644 --- a/include/app/pages/view.hpp +++ b/include/app/pages/analysis.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include "app/assets/texture.hpp" #include "app/context.hpp" @@ -23,7 +24,7 @@ namespace mbr::pages { -class view_page : public page { +class analysis_page : public page { public: using selected_video_t = stdx::option>>; using selected_txt_file_t = stdx::option; @@ -37,8 +38,8 @@ class view_page : public page { [[nodiscard]] static const char* data_type_string(data_view_t type); public: - explicit view_page(const std::shared_ptr& ctx); - ~view_page() override; + explicit analysis_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : page(ctx, parent) {}; + ~analysis_page() override; void on_enter() override; void on_exit() override; @@ -118,9 +119,9 @@ class view_page : public page { std::condition_variable queue_cv_; ImVec2 button_size_{24, 24}; - assets::button_texture play_button_; - assets::button_texture pause_button_; - assets::button_texture step_button_; + //assets::button_texture play_button_; + //assets::button_texture pause_button_; + //assets::button_texture step_button_; sg_image video_texture_{SG_INVALID_ID}; sg_view video_view_{SG_INVALID_ID}; diff --git a/include/app/pages/home.hpp b/include/app/pages/home.hpp index 4ee55b8..6e037bc 100644 --- a/include/app/pages/home.hpp +++ b/include/app/pages/home.hpp @@ -3,6 +3,8 @@ #include #include +#include + #include "app/context.hpp" #include "app/pages/page.hpp" #include "core/ip.hpp" @@ -11,7 +13,7 @@ namespace mbr::pages { class home_page : public page { public: - explicit home_page(const std::shared_ptr& ctx) : page{ctx} {} + explicit home_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : page{ctx, parent} {} ~home_page() override = default; void on_enter() override; diff --git a/include/app/pages/page.hpp b/include/app/pages/page.hpp index 1b47ab8..aca1854 100644 --- a/include/app/pages/page.hpp +++ b/include/app/pages/page.hpp @@ -1,12 +1,15 @@ #pragma once #include +#include +#include #include "app/context.hpp" namespace mbr::pages { -class page { +class page : public QWidget{ + Q_OBJECT public: page() = default; virtual ~page() = default; @@ -16,7 +19,7 @@ class page { virtual void update() = 0; protected: - explicit page(const std::shared_ptr& ctx) : context_{ctx} {} + explicit page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : QWidget(parent), context_{ctx} {} std::shared_ptr context_; }; diff --git a/include/app/pages/shock.hpp b/include/app/pages/plot.hpp similarity index 75% rename from include/app/pages/shock.hpp rename to include/app/pages/plot.hpp index f31c933..bacbc9c 100644 --- a/include/app/pages/shock.hpp +++ b/include/app/pages/plot.hpp @@ -5,16 +5,17 @@ #include #include +#include #include "app/pages/page.hpp" #include "app/pages/utils.hpp" namespace mbr::pages { -class shock_page : public page { +class plot_page : public page { public: - explicit shock_page(const std::shared_ptr& ctx) : page{ctx}, text_utils_{ctx} {} - ~shock_page() override = default; + explicit plot_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : page{ctx, parent}, text_utils_{ctx} {} + ~plot_page() override = default; void on_enter() override; void on_exit() override; diff --git a/include/app/pages/rpm.hpp b/include/app/pages/rpm.hpp deleted file mode 100644 index 867061a..0000000 --- a/include/app/pages/rpm.hpp +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include -#include -#include - -#include - -#include "app/pages/page.hpp" -#include "app/pages/utils.hpp" - -namespace mbr::pages { - -class rpm_page : public page { - public: - explicit rpm_page(const std::shared_ptr& ctx) : page{ctx}, text_drawer_{ctx} {} - ~rpm_page() override = default; - - void on_enter() override; - void on_exit() override; - void update() override; - - private: - void DrawLHS(const std::vector& raw_lines); - void DrawRHS(gsl::span time, - gsl::span wheel, - gsl::span engine); - - private: - utils::text_drawers text_drawer_; - std::string download_fd_text_; -}; - -} // namespace mbr::pages diff --git a/include/app/pages/serialmon.hpp b/include/app/pages/serialmon.hpp index 3195928..15a6ac7 100644 --- a/include/app/pages/serialmon.hpp +++ b/include/app/pages/serialmon.hpp @@ -4,6 +4,7 @@ #include #include +#include #include "app/pages/page.hpp" #include "app/pages/utils.hpp" @@ -12,7 +13,7 @@ namespace mbr::pages { class serial_page : public page { public: - explicit serial_page(const std::shared_ptr& ctx) : page{ctx}, text_drawer_{ctx} {} + explicit serial_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : page{ctx, parent}, text_drawer_{ctx} {} ~serial_page() override = default; void on_enter() override; diff --git a/src/app/app.cpp b/src/app/app.cpp index cc6c12e..8270d21 100644 --- a/src/app/app.cpp +++ b/src/app/app.cpp @@ -21,7 +21,7 @@ namespace mbr { namespace { const std::filesystem::path DEFAULT_JSON_PATH{"MBR_data.json"}; } // namespace -app_t::app_t(i32 argc, char** argv) : qt_app_{std::make_unique(argc, argv)}, context_{std::make_shared()} /*, m_Manager{m_Context}*/ { +app_t::app_t(i32 argc, char** argv) : qt_app_{std::make_unique(argc, argv)}, context_{std::make_shared()} { context_->log = context_->logger.get_log_fn(); ix::initNetSystem(); gui_ = std::make_unique(context_); diff --git a/src/app/gui.cpp b/src/app/gui.cpp index 12e63e4..ac01a67 100644 --- a/src/app/gui.cpp +++ b/src/app/gui.cpp @@ -13,11 +13,10 @@ #include "app/assets/texture.hpp" #include "app/context.hpp" #include "app/pages/home.hpp" -#include "app/pages/rpm.hpp" +#include "app/pages/plot.hpp" #include "app/pages/serialmon.hpp" -#include "app/pages/shock.hpp" #include "app/pages/utils.hpp" -#include "app/pages/view.hpp" +#include "app/pages/analysis.hpp" #include "app/style.hpp" #include "core/time.hpp" @@ -33,21 +32,20 @@ namespace mbr { } } -namespace { + [[nodiscard]] pages::page* create_page(page_type_t type, const std::shared_ptr& ctx, QWidget* parent) { switch (type) { case page_type_t::HOME: return new pages::home_page(ctx, parent); - case page_type_t::PLOT: return new pages::rpm_page(ctx, parent); - case page_type_t::ANALYSIS: return new pages::shock_page(ctx, parent); + case page_type_t::PLOT: return new pages::plot_page(ctx, parent); + case page_type_t::ANALYSIS: return new pages::analysis_page(ctx, parent); case page_type_t::SERIAL: return new pages::serial_page(ctx, parent); default: return nullptr; } } -} // namespace void gui_t::change_page(page_type_t type) { - pages_->setCurrentWidget(type); + pages_->setCurrentWidget(page_lookup_[type]); } diff --git a/src/app/pages/view.cpp b/src/app/pages/analysis.cpp similarity index 100% rename from src/app/pages/view.cpp rename to src/app/pages/analysis.cpp diff --git a/src/app/pages/shock.cpp b/src/app/pages/plot.cpp similarity index 100% rename from src/app/pages/shock.cpp rename to src/app/pages/plot.cpp diff --git a/src/app/pages/rpm.cpp b/src/app/pages/rpm.cpp deleted file mode 100644 index 47dc5cd..0000000 --- a/src/app/pages/rpm.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include "app/pages/rpm.hpp" - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "app/pages/utils.hpp" -#include "app/style.hpp" -#include "core/log.hpp" -#include "core/time.hpp" - -namespace mbr::pages { - -void rpm_page::on_enter() { log_info(context_->log, "Entered RPMPage"); } -void rpm_page::on_exit() { log_info(context_->log, "Exited RPMPage"); } - -void rpm_page::update() { - PROFILE_FUNCTION(); - if (ImGui::BeginTable( - "##viewsplit", 2, ImGuiTableFlags_NoBordersInBody | ImGuiTableFlags_Resizable)) { - const auto cleanup_split{gsl::finally(ImGui::EndTable)}; - const std::shared_lock lock{context_->backend->get_data_latch()}; - const auto& data = context_->backend->get_data(); - - ImGui::TableNextColumn(); - DrawLHS(data.get_raw_lines()); - ImGui::TableNextColumn(); - DrawRHS(data.get_time(), data.get_series("W"), data.get_series("E")); - } -} - -void rpm_page::DrawLHS(const std::vector& raw_lines) { - if (ImGui::BeginChild("##datalog")) { - const auto cleanup_data{gsl::finally(ImGui::EndChild)}; - BOLD_HEADER(ImGui::Text("Data Log")); - - ImGui::Separator(); - text_drawer_.start_logging_button(); - ImGui::SameLine(); - text_drawer_.data_download_button(raw_lines, download_fd_text_); - ImGui::SameLine(); - - HEADER(utils::draw_input_box("##extra_rpm", download_fd_text_, "File descriptor")); - ImGui::Separator(); - utils::draw_data_log(raw_lines); - } -} - -void rpm_page::DrawRHS(gsl::span time, - gsl::span wheel, - gsl::span engine) { - if (ImGui::BeginChild("##graph")) { - const auto cleanup_graph{gsl::finally(ImGui::EndChild)}; - const auto sync_lt = context_->backend->get_data().get_sync_lt(); - const auto plot_title = sync_lt - ? fmt::format("RPM Data from {}", sync_lt.value().to_string()) - : "No Synced Time"; - - if (ImPlot::BeginPlot(plot_title.c_str(), {-1, -1})) { - const auto cleanup_plot{gsl::finally(ImPlot::EndPlot)}; - utils::plot_if_non_empty("Wheel Speed", time, wheel); - utils::plot_if_non_empty("Engine Speed", time, engine); - } - } -} - -} // namespace mbr::pages From c06f03d161368ff22d88e81fff7249f87a38d1f1 Mon Sep 17 00:00:00 2001 From: Blake Date: Mon, 27 Jul 2026 09:54:58 -0400 Subject: [PATCH 04/36] not compiling --- include/app/gui.hpp | 2 +- include/app/pages/analysis.hpp | 25 -- include/app/pages/home.hpp | 6 - include/app/pages/serialmon.hpp | 3 - src/app/gui.cpp | 19 +- src/app/pages/analysis.cpp | 698 +------------------------------- src/app/pages/home.cpp | 198 --------- src/app/pages/plot.cpp | 63 +-- src/app/pages/serialmon.cpp | 120 ------ 9 files changed, 27 insertions(+), 1107 deletions(-) diff --git a/include/app/gui.hpp b/include/app/gui.hpp index 12a9db7..d11dc12 100644 --- a/include/app/gui.hpp +++ b/include/app/gui.hpp @@ -15,7 +15,7 @@ namespace mbr { class gui_t : public QMainWindow { Q_OBJECT public: - explicit gui_t(const std::shared_ptr& ctx, QWidget* parent = nullptr); + explicit gui_t(const std::shared_ptr& ctx); private: void change_page(page_type_t type); diff --git a/include/app/pages/analysis.hpp b/include/app/pages/analysis.hpp index f4d0977..e6c29d6 100644 --- a/include/app/pages/analysis.hpp +++ b/include/app/pages/analysis.hpp @@ -46,31 +46,6 @@ class analysis_page : public page { void update() override; private: - void cleanup(); - - void draw_lhs(); - void draw_lhs_controls(); - void draw_open_video(); - - void draw_rhs(); - void draw_open_text(); - void draw_sync_video_buttons(); - - selected_video_t open_video_file(const std::string& previous_file); - void request_seek(i32 frame_index); - - selected_txt_file_t open_text_file(const std::string& previous_file); - void load_data(); - stdx::option sync_data_video(const std::vector& micros_times); - void delete_extra(usize erase_pos); - void dynamic_plot_start(); - void dynamic_plot_loop(); - - void start_decoding_thread(); - void stop_decoding_thread(); - void update_texture(bool is_timer_tick); - - void try_cleanup_sokol_resources(); private: std::shared_ptr is_alive_; diff --git a/include/app/pages/home.hpp b/include/app/pages/home.hpp index 6e037bc..b9e1806 100644 --- a/include/app/pages/home.hpp +++ b/include/app/pages/home.hpp @@ -21,13 +21,7 @@ class home_page : public page { void update() override; private: - void draw_top_lhs(); - void draw_top_rhs(); - void draw_bottom_lhs(); - void draw_bottom_rhs(); - void draw_ip_controls(); - void draw_credential_controls(); private: ipv4_t previous_ip_; diff --git a/include/app/pages/serialmon.hpp b/include/app/pages/serialmon.hpp index 15a6ac7..ea53b76 100644 --- a/include/app/pages/serialmon.hpp +++ b/include/app/pages/serialmon.hpp @@ -21,9 +21,6 @@ class serial_page : public page { void update() override; private: - void draw_top_lhs(); - void draw_top_rhs(); - void draw_bottom(); private: utils::text_drawers text_drawer_; diff --git a/src/app/gui.cpp b/src/app/gui.cpp index ac01a67..09f4a97 100644 --- a/src/app/gui.cpp +++ b/src/app/gui.cpp @@ -8,9 +8,14 @@ #include #include #include +#include +#include +#include +#include #include "app/assets/images/app_icon.hpp" #include "app/assets/texture.hpp" +#include "app/pages/page.hpp" #include "app/context.hpp" #include "app/pages/home.hpp" #include "app/pages/plot.hpp" @@ -24,16 +29,19 @@ using namespace std::chrono; namespace mbr { - gui_t::gui_t() { + gui_t::gui_t(const std::shared_ptr& ctx) : context_(ctx) { + pages_ = new QStackedWidget(this); + pages_->setCurrentWidget(pages_); for (auto type : {page_type_t::HOME, page_type_t::PLOT, page_type_t::ANALYSIS, page_type_t::SERIAL}) { pages::page* p = create_page(type, context_, pages_); pages_->addWidget(p); page_lookup_[type] = p; } + build_menu_bar(); } - [[nodiscard]] pages::page* create_page(page_type_t type, const std::shared_ptr& ctx, QWidget* parent) { + [[nodiscard]] pages::page* gui_t::create_page(page_type_t type, const std::shared_ptr& ctx, QWidget* parent) { switch (type) { case page_type_t::HOME: return new pages::home_page(ctx, parent); case page_type_t::PLOT: return new pages::plot_page(ctx, parent); @@ -48,5 +56,12 @@ namespace mbr { pages_->setCurrentWidget(page_lookup_[type]); } + void gui_t::build_menu_bar() { + QMenu* nav = menuBar()->addMenu("Navigate"); + nav->addAction("Home", this, [this] { change_page(page_type_t::HOME); }); + nav->addAction("Plot", this, [this] { change_page(page_type_t::PLOT); }); + } + + } // namespace mbr diff --git a/src/app/pages/analysis.cpp b/src/app/pages/analysis.cpp index 3fcfeb5..45273d0 100644 --- a/src/app/pages/analysis.cpp +++ b/src/app/pages/analysis.cpp @@ -1,4 +1,4 @@ -#include "app/pages/view.hpp" +#include "app/pages/analysis.hpp" #include #include @@ -42,698 +42,10 @@ using namespace std::chrono_literals; namespace mbr::pages { -static constexpr usize MAX_QUEUE_SIZE = 10; + void analysis_page::on_enter() { log_info(context_->log, "Entered AnalysisPage"); } + void analysis_page::on_exit() { log_info(context_->log, "Exited AnalysisPage"); } -const char* view_page::data_type_string(data_view_t type) { - switch (type) { - case data_view_t::ALL: return "All Data Shown"; - case data_view_t::RPMDATA: return "RPM Data Shown"; - case data_view_t::SHOCKDATA: return "Shock Data Shown"; - default: return "Unknown"; + void analysis_page::update() { + PROFILE_FUNCTION(); } -} - -view_page::view_page(const std::shared_ptr& ctx) - : page{ctx}, is_alive_{std::make_shared(true)}, play_button_{assets::PLAY_BUTTON_PNG}, - pause_button_{assets::PAUSE_BUTTON_PNG}, step_button_{assets::STEP_BUTTON_PNG} {} - -view_page::~view_page() { - *is_alive_ = false; - cleanup(); - log_info(context_->log, "Destroyed ViewPage"); -} - -void view_page::on_enter() { log_info(context_->log, "Entered ViewPage"); } - -void view_page::on_exit() { - cleanup(); - log_info(context_->log, "Exited ViewPage"); -} - -void view_page::update() { - PROFILE_FUNCTION(); - if (ImGui::BeginTable( - "##viewsplt", 2, ImGuiTableFlags_NoBordersInBody | ImGuiTableFlags_Resizable)) { - const auto cleanup_table{gsl::finally(ImGui::EndTable)}; - ImGui::TableNextColumn(); - draw_lhs(); - ImGui::TableNextColumn(); - draw_rhs(); - } -} - -void view_page::cleanup() { - stop_decoding_thread(); - try_cleanup_sokol_resources(); -} - -void view_page::draw_lhs() { - if (ImGui::BeginChild("##video")) { - const auto cleanup_video{gsl::finally(ImGui::EndChild)}; - draw_open_video(); - if (video_loaded_ && !video_path_.empty()) { - ImGui::SameLine(); - const std::filesystem::path path{video_path_}; - const auto filename_str = path.filename().string(); - BOLD_DEFAULT(ImGui::TextUnformatted("Current File: ")); - ImGui::SameLine(); - ImGui::TextUnformatted(filename_str.c_str()); - } - ImGui::SameLine(); - - // Clear Video button is right aligned - const char* label = "Clear Video"; - const ImVec2 button_size = ImGui::CalcTextSize(label); - const f32 padding = ImGui::GetStyle().FramePadding.x * 2.0F; - const f32 right_x = ImGui::GetWindowContentRegionMax().x - (button_size.x + padding); - - ImGui::SetCursorPosX(right_x); - if (ImGui::Button(label)) { - stop_decoding_thread(); - is_playing_ = false; - video_path_ = {}; - try_cleanup_sokol_resources(); - return; - } - - ImGui::Separator(); - - // Playback logic and frame skipping can be ignored if paused - bool is_timer_tick = false; - if (is_playing_ && video_fps_ > 0.0F) { - time_accumulator_ += ImGui::GetIO().DeltaTime; - const auto frames_to_advance = static_cast(time_accumulator_ / frame_duration_); - - // We have to handle skipped frames gracefully - if (frames_to_advance > 0) { - time_accumulator_ -= (frames_to_advance * frame_duration_); - - if (frames_to_advance > 1) { - const std::scoped_lock lock{frame_mutex_}; - - const auto recoverable_frames = - std::min(static_cast(frame_queue_.size()) - 1, frames_to_advance - 1); - for (auto i = 0; i < recoverable_frames; i++) { frame_queue_.pop_front(); } - } - - is_timer_tick = true; - } - } - - update_texture(is_timer_tick); - - if (video_texture_.id != SG_INVALID_ID && texture_width_ > 0) { - const f32 aspect = static_cast(texture_width_) / static_cast(texture_height_); - const f32 avail_w = ImGui::GetContentRegionAvail().x; - const f32 h = avail_w / aspect; - - ImGui::Image(video_texture_id_, {avail_w, h}); - video_hovered_ = ImGui::IsItemHovered(); - } - - if (thread_running_) { - ImGui::Separator(); - draw_lhs_controls(); - } - } -} - -void view_page::draw_lhs_controls() { - // Slider - const auto current_timestamp_min = - (static_cast(current_frame_ui_) / total_frames_) * video_length_minutes_; - const auto current_timestamp = local_time::from_minutes(current_timestamp_min); - const auto formatted_timestamp = - current_timestamp.value_or(local_time::zero()).to_string(false); - ImGui::Text("%s / %s", formatted_timestamp.c_str(), video_length_formatted_.c_str()); - ImGui::SameLine(); - - { - ImGui::PushItemWidth(-1); - const auto cleanup_width{gsl::finally(ImGui::PopItemWidth)}; - - i32 slider_pos = current_frame_ui_; - if (ImGui::SliderInt( - "##scrub", &slider_pos, 0, total_frames_, "", ImGuiSliderFlags_NoInput)) { - is_playing_.exchange(false); - request_seek(slider_pos); - } - } - - // Loop checkbox - bool looping = is_looping_; - if (ImGui::Checkbox("Looping", &looping)) { is_looping_ = looping; } - ImGui::SameLine(); - - // Play/Pause - const auto is_playing = is_playing_.load(); - const auto tint_color = context_->style.dark_mode ? ImVec4{1, 1, 1, 1} : ImVec4{-1, -1, -1, 1}; - ImGui::SameLine(); - if (ImGui::ImageButton("##stepback", - step_button_.get_id(), - button_size_, - {1, 0}, - {0, 1}, - {0, 0, 0, 0}, - tint_color)) { - request_seek(current_frame_ui_ - 5); - is_playing_.exchange(false); - } - ImGui::SameLine(); - if (ImGui::ImageButton("##playpause", - is_playing ? pause_button_.get_id() : play_button_.get_id(), - button_size_, - {0, 0}, - {1, 1}, - {0, 0, 0, 0}, - tint_color)) { - is_playing_.exchange(!is_playing); - time_accumulator_ = 0.0; - } - ImGui::SameLine(); - if (ImGui::ImageButton("##stepforward", - step_button_.get_id(), - button_size_, - {0, 0}, - {1, 1}, - {0, 0, 0, 0}, - tint_color)) { - request_seek(current_frame_ui_ + 5); - is_playing_.exchange(false); - } - - // Keyboard shortcuts - if (video_hovered_ && !timestamp_input_focused_ && !context_->is_cmd_input_focused) { - if (ImGui::IsKeyPressed(ImGuiKey_Space)) { - is_playing_.exchange(!is_playing); - time_accumulator_ = 0.0; - } - - if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow, false)) { - request_seek(current_frame_ui_ - 1); - is_playing_.exchange(false); - } - - if (ImGui::IsKeyPressed(ImGuiKey_DownArrow, false)) { - request_seek(current_frame_ui_ - 10); - is_playing_.exchange(false); - } - - if (ImGui::IsKeyPressed(ImGuiKey_RightArrow, false)) { - request_seek(current_frame_ui_ + 1); - is_playing_.exchange(false); - } - - if (ImGui::IsKeyPressed(ImGuiKey_UpArrow, false)) { - request_seek(current_frame_ui_ + 10); - is_playing_.exchange(false); - } - } -} - -void view_page::draw_open_video() { - // Check if the file is ready - { - const std::scoped_lock lock{video_path_mutex_}; - if (selected_video_) { - stop_decoding_thread(); - try_cleanup_sokol_resources(); - video_path_ = selected_video_.value().first; - selected_video_ = stdx::none; - video_loaded_ = true; - start_decoding_thread(); - } - } - - // File selection can happen at any point during playback - const bool is_disabled_by_video = video_dialog_running_.load(); - if (is_disabled_by_video) { ImGui::BeginDisabled(); } - if (ImGui::Button("Open Video")) { - video_dialog_running_ = true; - video_loaded_ = false; - dynamic_plotting_ = false; - - const auto alive = is_alive_; - const auto& previous_file = video_path_; - - std::thread([this, alive, previous_file]() noexcept { - try { - const auto path = open_video_file(previous_file); - if (*alive) { - const std::scoped_lock lock{video_path_mutex_}; - selected_video_ = path; - video_dialog_running_ = false; - } - } catch (const std::exception& e) { - video_dialog_running_ = false; - log_critical( - context_->log, "Fatal error occurred while opening video dialog: {}", e.what()); - } catch (...) { - video_dialog_running_ = false; - log_critical(context_->log, "Unknown fatal error occurred"); - } - }).detach(); - } - if (is_disabled_by_video) { ImGui::EndDisabled(); } -} - -void view_page::draw_rhs() { - if (ImGui::BeginChild("##data")) { - const auto cleanup_data{gsl::finally(ImGui::EndChild)}; - HEADER({ - draw_open_text(); - ImGui::SameLine(); - draw_sync_video_buttons(); - ImGui::Separator(); - }); - - if (ImGui::BeginCombo("##DataView", data_type_string(data_show_))) { - const auto cleanup_combo{gsl::finally(ImGui::EndCombo)}; - if (ImGui::Selectable("All Data", data_show_ == data_view_t::ALL)) { - data_show_ = data_view_t::ALL; - } - if (ImGui::Selectable("RPM", data_show_ == data_view_t::RPMDATA)) { - data_show_ = data_view_t::RPMDATA; - } - if (ImGui::Selectable("Shock", data_show_ == data_view_t::SHOCKDATA)) { - data_show_ = data_view_t::SHOCKDATA; - } - } - - const std::shared_lock lock{context_->backend->get_data_latch()}; - const auto& data = context_->backend->get_data(); - - const auto sync_lt = data.get_sync_lt(); - const auto plot_title = sync_lt - ? fmt::format("Data View from {}", sync_lt.value().to_string()) - : "No Synced Time"; - - view_page::dynamic_plot_loop(); - if (ImPlot::BeginPlot(plot_title.c_str(), {-1, -1})) { - const auto cleanup_plot{gsl::finally(ImPlot::EndPlot)}; - utils::plot_if_non_empty("Wheel Speed", - data.get_time(), - data.get_series("W"), - data_show_ == data_view_t::ALL || - data_show_ == data_view_t::RPMDATA, - plot_percent_); - utils::plot_if_non_empty("Engine Speed", - data.get_time(), - data.get_series("E"), - data_show_ == data_view_t::ALL || - data_show_ == data_view_t::RPMDATA, - plot_percent_); - - utils::plot_if_non_empty("Front Right Shock Travel", - data.get_time(), - data.get_series("FR"), - data_show_ == data_view_t::ALL || - data_show_ == data_view_t::SHOCKDATA, - plot_percent_); - utils::plot_if_non_empty("Front Left Shock Travel", - data.get_time(), - data.get_series("FL"), - data_show_ == data_view_t::ALL || - data_show_ == data_view_t::SHOCKDATA, - plot_percent_); - utils::plot_if_non_empty("Rear Right Shock Travel", - data.get_time(), - data.get_series("RR"), - data_show_ == data_view_t::ALL || - data_show_ == data_view_t::SHOCKDATA, - plot_percent_); - utils::plot_if_non_empty("Rear Left Shock Travel", - data.get_time(), - data.get_series("RL"), - data_show_ == data_view_t::ALL || - data_show_ == data_view_t::SHOCKDATA, - plot_percent_); - } - } -} - -void view_page::draw_open_text() { - // Check if the file is ready - { - const std::scoped_lock lock{txt_path_mutex_}; - if (selected_txt_) { - txt_path_ = selected_txt_.value(); - selected_txt_ = stdx::none; - txt_loaded_ = true; - load_data(); - } - } - - // File selection can happen at any point during playback - const bool is_disabled_by_txt = txt_dialog_running_.load(); - if (is_disabled_by_txt) { ImGui::BeginDisabled(); } - if (ImGui::Button("Open Text File")) { - txt_dialog_running_ = true; - txt_loaded_ = false; - dynamic_plotting_ = false; - - const auto alive = is_alive_; - const auto previous_path = txt_path_; - - std::thread([this, alive, previous_path]() noexcept { - try { - const auto path = open_text_file(previous_path); - if (*alive) { - const std::scoped_lock lock{txt_path_mutex_}; - selected_txt_ = path; - txt_dialog_running_ = false; - context_->backend->set_logging(false); - } - } catch (const std::exception& e) { - txt_dialog_running_ = false; - log_critical(context_->log, - "Fatal error occurred while opening text file dialog: {}", - e.what()); - } catch (...) { - txt_dialog_running_ = false; - log_critical(context_->log, "Unknown fatal error occurred"); - } - }).detach(); - } - if (is_disabled_by_txt) { ImGui::EndDisabled(); } -} - -void view_page::draw_sync_video_buttons() { - if (ImGui::Button("Sync Data/Video")) { - data_and_time_sync_ = false; - dynamic_plotting_ = false; - - load_data(); - if (!txt_loaded_ || !video_loaded_) { - log_error(context_->log, "Could not sync data with video:"); - log_error(context_->log, " Text Loaded: {}", txt_loaded_); - log_error(context_->log, " Video Loaded: {}", video_loaded_); - return; - } - - video_creation_ts_ = local_time::from_string(creation_metadata_text_buf_); - if (!video_creation_ts_) { - log_error(context_->log, "Could not parse provided timestamp, or it was not provided."); - return; - } - creation_metadata_text_buf_ = {}; - - stdx::option sync_time_pos; - { - const std::shared_lock lock{context_->backend->get_data_latch()}; - sync_time_pos = sync_data_video(context_->backend->get_data().get_time_no_normal()); - } - - if (!sync_time_pos) { - log_error(context_->log, "Could not get trim position from data/video"); - return; - } - - // This is probably mega unsafe... - delete_extra(sync_time_pos.value()); - request_seek(0); - } - - ImGui::SameLine(); - utils::draw_input_box("##extra_view", creation_metadata_text_buf_, "HH:MM:SS", 120.0F); - timestamp_input_focused_ = ImGui::IsItemFocused(); - ImGui::SameLine(); - if (ImGui::Checkbox("Dynamic Plotting", &dynamic_plotting_)) { - view_page::dynamic_plot_start(); - } -} - -view_page::selected_video_t view_page::open_video_file(const std::string& previous_file) { - const char* const filters[] = {"*.mp4", "*.mov"}; - const char* path = tinyfd_openFileDialog( - "Select a video file", previous_file.c_str(), std::size(filters), filters, nullptr, 0); - if (path == nullptr) { - log_warn(context_->log, "No file selected"); - return stdx::none; - } - - const std::string real_path{path}; - log_info(context_->log, "Selected file: {}", real_path); - - auto dt = date_time::from_video_metadata(real_path); - if (dt) { - log_info( - context_->log, "Selected video with creation timestamp: {}", dt.value().to_string()); - } else { - log_warn(context_->log, "Could not detect datetime metadata from selected video."); - } - - return std::pair{path, dt}; -} - -view_page::selected_txt_file_t view_page::open_text_file(const std::string& previous_file) { - const char* const filters[] = {"*.txt"}; - const char* path = tinyfd_openFileDialog( - "Select a text file", previous_file.c_str(), std::size(filters), filters, nullptr, 0); - if (path == nullptr) { - log_warn(context_->log, "No file selected"); - return stdx::none; - } - - log_info(context_->log, "Selected file: {}", path); - return path; -} - -void view_page::load_data() { - std::ifstream file{txt_path_}; - if (!file.is_open()) { - log_error(context_->log, "Failed to open file: {}", txt_path_); - return; - } - - const std::unique_lock lock{context_->backend->get_data_latch()}; - context_->backend->get_data().clear(); - std::string ident, value; - while (file >> ident >> value) { context_->backend->get_data().write_data(ident, value); } -} - -void view_page::request_seek(i32 frame_index) { - // Prevent stepping out of bounds, though it is recoverable - const auto clamped_frame = std::clamp(frame_index, 0, total_frames_); - - { - const std::scoped_lock lock{frame_mutex_}; - frame_queue_.clear(); - } - - seek_target_ = clamped_frame; - force_update_frame_ = true; - current_frame_ui_ = clamped_frame; - queue_cv_.notify_one(); -} - -void view_page::start_decoding_thread() { - thread_running_ = true; - is_playing_ = true; - - decode_thread_ = std::thread([this]() { - cv::VideoCapture cap{video_path_}; - if (!cap.isOpened()) { - log_error(context_->log, "Failed to open video"); - thread_running_ = false; - return; - } - - video_fps_ = cap.get(cv::CAP_PROP_FPS); - frame_duration_ = 1.0 / video_fps_; - total_frames_ = static_cast(cap.get(cv::CAP_PROP_FRAME_COUNT)); - video_length_minutes_ = (static_cast(total_frames_) / video_fps_) / 60.0; - const auto video_length_lt = local_time::from_minutes(video_length_minutes_); - if (!video_length_lt) { - log_error(context_->log, "Video length could not be determined"); - thread_running_ = false; - return; - } - video_length_formatted_ = video_length_lt.value().to_string(false); - - cv::Mat raw_frame, rgba_frame; - while (thread_running_) { - // Keep the buffer from becoming too large - { - std::unique_lock lock{frame_mutex_}; - queue_cv_.wait(lock, [this] { - return frame_queue_.size() < MAX_QUEUE_SIZE || !thread_running_; - }); - - if (!thread_running_) { break; } - } - - const i32 seek_req = seek_target_.exchange(-1); - bool just_sought = false; - - if (seek_req != -1) { - cap.set(cv::CAP_PROP_POS_FRAMES, seek_req); - const std::scoped_lock lock{frame_mutex_}; - frame_queue_.clear(); - just_sought = true; - } - - // Don't decode if seeking or paused to prevent jitters - if (!is_playing_ && !just_sought) { - std::this_thread::sleep_for(5ms); - continue; - } - - auto current_frame_index = static_cast(cap.get(cv::CAP_PROP_POS_FRAMES)); - { - PROFILE_SCOPE("view_page::decode_frame"); - if (cap.read(raw_frame)) { - cv::cvtColor(raw_frame, rgba_frame, cv::COLOR_BGR2RGBA); - const cv::Mat frame_copy = rgba_frame.clone(); - - const std::scoped_lock lock{frame_mutex_}; - frame_queue_.emplace_back(frame_copy, current_frame_index); - } else { - if (is_looping_) { - cap.set(cv::CAP_PROP_POS_FRAMES, 0); - } else { - std::this_thread::sleep_for(10ms); - } - } - } - } - }); -} - -void view_page::stop_decoding_thread() { - thread_running_ = false; - queue_cv_.notify_all(); - if (decode_thread_.joinable()) { decode_thread_.join(); } - - const std::scoped_lock lock{frame_mutex_}; - frame_queue_.clear(); -} - -void view_page::update_texture(bool is_timer_tick) { - PROFILE_FUNCTION(); - cv::Mat frame_to_upload; - bool frame_ready = false; - { - const std::scoped_lock lock{frame_mutex_}; - const bool should_consume = is_timer_tick || force_update_frame_; - - if (should_consume && !frame_queue_.empty()) { - const auto p = frame_queue_.front(); - frame_to_upload = p.first; - current_frame_ui_ = p.second; - - frame_queue_.pop_front(); - frame_ready = true; - queue_cv_.notify_one(); - force_update_frame_.exchange(false); - } - } - - if (frame_ready && !frame_to_upload.empty()) { - // Initialize Texture if resolution changed or first run - if (texture_width_ != frame_to_upload.cols || texture_height_ != frame_to_upload.rows) { - try_cleanup_sokol_resources(); - - texture_width_ = frame_to_upload.cols; - texture_height_ = frame_to_upload.rows; - - sg_image_desc desc = {}; - desc.width = texture_width_; - desc.height = texture_height_; - desc.pixel_format = SG_PIXELFORMAT_RGBA8; - desc.usage.stream_update = true; - desc.num_mipmaps = 1; - video_texture_ = sg_make_image(&desc); - ASSERT(video_texture_.id != SG_INVALID_ID, "Video texture failed to initialize"); - - sg_view_desc view_desc = {}; - view_desc.texture.image = video_texture_; - video_view_ = sg_make_view(&view_desc); - video_texture_id_ = simgui_imtextureid(video_view_); - } - - // Perform the actual upload - sg_image_data data = {}; - data.mip_levels[0].ptr = frame_to_upload.data; - data.mip_levels[0].size = frame_to_upload.total() * frame_to_upload.elemSize(); - sg_update_image(video_texture_, &data); - } -}; - -void view_page::try_cleanup_sokol_resources() { - if (video_view_.id != SG_INVALID_ID) { - sg_destroy_view(video_view_); - video_view_.id = SG_INVALID_ID; - } - - if (video_texture_.id != SG_INVALID_ID) { - sg_destroy_image(video_texture_); - video_texture_.id = SG_INVALID_ID; - video_texture_id_ = 0; - } - - texture_width_ = 0; - texture_height_ = 0; -} - -stdx::option view_page::sync_data_video(const std::vector& micros_times) { - if (!video_creation_ts_) { return stdx::none; } - - data_and_time_sync_ = true; - const auto creation_timestamp = video_creation_ts_.value(); - const u64 micros_to_sync = creation_timestamp.micros_since_midnight(); - const auto it = std::ranges::lower_bound(micros_times, micros_to_sync); - if (it != micros_times.end()) { return std::distance(micros_times.begin(), it); } - - return stdx::none; -} - -void view_page::delete_extra(usize erase_pos) { - const std::unique_lock lock{context_->backend->get_data_latch()}; - std::vector* const data[] = { - &context_->backend->get_data().time_, - &context_->backend->get_data().get_series("W"), - &context_->backend->get_data().get_series("E"), - &context_->backend->get_data().get_series("FR"), - &context_->backend->get_data().get_series("FL"), - &context_->backend->get_data().get_series("RR"), - &context_->backend->get_data().get_series("RL"), - }; - - for (const auto& datum : data) { - if (datum->size() < erase_pos) { return; } - } - - for (const auto& datum : data) { - datum->erase(datum->begin(), datum->begin() + static_cast(erase_pos)); - } -} - -void view_page::dynamic_plot_start() { - if (dynamic_plotting_) { - const std::shared_lock lock{context_->backend->get_data_latch()}; - const auto& time_vec = context_->backend->get_data().time_; - if (time_vec.empty()) { return; } - const auto begin_time_min = context_->backend->get_data().time_[0]; - - const f64 target_end_time = begin_time_min + video_length_minutes_; - const auto end_it = std::ranges::lower_bound(time_vec, target_end_time); - - const usize end_idx = std::distance(time_vec.begin(), end_it); - const usize end_idy = std::distance(end_it, time_vec.end()); - data_from_end_ = static_cast(end_idy); - data_count_ = static_cast(end_idx); - points_per_ = static_cast(end_idx) / total_frames_; - } -} - -void view_page::dynamic_plot_loop() { - if (dynamic_plotting_) { - plot_percent_ = static_cast(std::max( - data_count_ - (points_per_ * current_frame_ui_) + data_from_end_, data_from_end_)); - return; - } - plot_percent_ = 0; -} - } // namespace mbr::pages diff --git a/src/app/pages/home.cpp b/src/app/pages/home.cpp index 9097a70..dc9a4f4 100644 --- a/src/app/pages/home.cpp +++ b/src/app/pages/home.cpp @@ -22,203 +22,5 @@ void home_page::on_exit() { log_info(context_->log, "Exited HomePage"); } void home_page::update() { PROFILE_FUNCTION(); - const f32 full_height = ImGui::GetContentRegionAvail().y; - const f32 top_height = full_height * 0.66F; - const f32 bottom_height = full_height - top_height; - - if (ImGui::BeginChild("##topsec", {0, top_height})) { - const auto cleanup_top{gsl::finally(ImGui::EndChild)}; - if (ImGui::BeginTable("##topsplt", 2, ImGuiTableFlags_BordersInnerV)) { - const auto cleanup_split{gsl::finally(ImGui::EndTable)}; - ImGui::TableNextColumn(); - draw_top_lhs(); - ImGui::TableNextColumn(); - draw_top_rhs(); - } - } - ImGui::Separator(); - - if (ImGui::BeginChild("##botsec", {0, bottom_height})) { - const auto cleanup_bot{gsl::finally(ImGui::EndChild)}; - if (ImGui::BeginTable("##botsplt", 2, ImGuiTableFlags_Resizable)) { - const auto cleanup_split{gsl::finally(ImGui::EndTable)}; - ImGui::TableSetupColumn("##errors", ImGuiTableColumnFlags_WidthStretch, 0.33F); - ImGui::TableSetupColumn("##action", ImGuiTableColumnFlags_WidthStretch, 0.66F); - - ImGui::TableNextColumn(); - draw_bottom_lhs(); - ImGui::TableNextColumn(); - draw_bottom_rhs(); - } - } -} - -void home_page::draw_top_lhs() { - BOLD_HEADER(ImGui::Text("SD Card Control")); - ImGui::Separator(); - - if (ImGui::BeginChild("##sd_control", {0, 0}, false, ImGuiWindowFlags_HorizontalScrollbar)) { - const auto cleanup{gsl::finally(ImGui::EndChild)}; - local_time t; - HEADER(utils::draw_input_box( - "##sd_name", set_name_, fmt::format("Name ({})", t.to_string(false)).c_str())); - ImGui::SameLine(); - if (ImGui::Button("Create File")) { - if (set_name_.empty()) { - sd_name_ = t.to_string(false); - } else { - sd_name_ = set_name_; - } - for (char& c : sd_name_) { - if (c == ':' || c == ' ') { c = '-'; } - } - const auto command = fmt::format("SD_START /{}.txt", sd_name_); - context_->backend->send_cmd(command); - set_name_.clear(); - } - ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button, - context_->backend->is_writing() ? ImVec4(0, 0.7F, 0, 1) - : ImVec4(0.7F, 0, 0, 1)); - if (ImGui::Button(context_->backend->is_writing() ? "Write ON" : "Write OFF")) { - if (context_->backend->is_writing()) { - context_->backend->send_cmd("SD_WRITE 0"); - } else { - context_->backend->send_cmd("SD_WRITE 1"); - } - // m_SDWrite = !m_SDWrite; - } - ImGui::PopStyleColor(); - ImGui::SameLine(); - if (!sd_name_.empty()) { - if (ImGui::Button(context_->backend->is_open() ? "Close SD" : "Open SD")) { - if (context_->backend->is_open()) { - context_->backend->send_cmd("SD_CLOSE"); - } else { - const auto command = fmt::format("SD_START /{}.txt", sd_name_); - context_->backend->send_cmd(command); - } - } - } - ImGui::SameLine(); - ImGui::TextDisabled("%s", !sd_name_.empty() ? sd_name_.c_str() : "No file created"); - /* - ImGui::BulletText("Connect to Wifi on laptop"); - ImGui::BulletText( - "Make sure you see 'Connected' and a green light, that means you are receiving data"); - ImGui::BulletText("If not, restart everything: app, esp32, wifi"); - ImGui::BulletText("Once connection is established, press 'Sync Time', this will allow the " - "data to be displayed"); - ImGui::BulletText("No data will be collected unless time is synced"); - ImGui::BulletText("You must sync time everytime the esp32 is restarted"); - ImGui::BulletText("Under the 'Menu' dropdown there are various pages"); - ImGui::BulletText("Go to the page you want data from"); - ImGui::BulletText("Press 'Start Logging'"); - ImGui::BulletText("Once you've gathered the data, you can download it by just pressing " - "download, or giving it a title"); - ImGui::BulletText("No title just gives the time stamp"); - ImGui::BulletText( - "In the 'View' page, you can upload data and video to watch the data be plotted live"); - ImGui::BulletText("First upload both the video and the data file"); - ImGui::BulletText( - "Then type in the timestamp (down to the second) of when the video was created"); - ImGui::BulletText("Press 'Sync Data/Video'"); - ImGui::BulletText("Turn on dynamic plotting and watch the data be plotted"); - ImGui::BulletText( - "You can also hide data you don't want to see by pressing on them in the legend"); - ImGui::BulletText("Send CMD currently works, but there aren't any commands supported"); - */ - } -} - -void home_page::draw_top_rhs() { - BOLD_HEADER(ImGui::Text("Control Panel")); - ImGui::Separator(); - - BOLD_DEFAULT(ImGui::SeparatorText("Metrics")); - ImGui::BulletText("Backend Status: %s", - context_->backend->is_connected() ? "Online" : "Offline"); - ImGui::BulletText("Last IP Update: %s", previous_ip_.to_string().c_str()); - ImGui::BulletText("Application FPS: %.2f", ImGui::GetIO().Framerate); - - BOLD_DEFAULT(ImGui::SeparatorText("UI Settings")); - bool dark_mode = context_->style.dark_mode; - if (ImGui::Checkbox("Dark Mode", &dark_mode)) { - if (dark_mode) { - context_->style.set_dark_theme(); - } else { - context_->style.set_light_theme(); - } - } - - bool fullscreen = sapp_is_fullscreen(); - if (ImGui::Checkbox("Fullscreen", &fullscreen)) { sapp_toggle_fullscreen(); } -} - -void home_page::draw_bottom_lhs() { - BOLD_HEADER(ImGui::Text("Error Log")); - ImGui::Separator(); - - if (ImGui::BeginChild("##errscroll", {0, 0}, false, ImGuiWindowFlags_HorizontalScrollbar)) { - const auto cleanup{gsl::finally(ImGui::EndChild)}; - const std::string all_errors = context_->logger.get_streamed_logs(); - ImGui::TextUnformatted(all_errors.c_str()); - if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) { ImGui::SetScrollHereY(1.0F); } - } } - -void home_page::draw_bottom_rhs() { - BOLD_HEADER(ImGui::Text("Command Center")); - ImGui::Separator(); - - if (ImGui::BeginChild("##commandcenter", {0, 0}, false, ImGuiWindowFlags_HorizontalScrollbar)) { - const auto cleanup{gsl::finally(ImGui::EndChild)}; - draw_ip_controls(); - draw_credential_controls(); - } -} - -void home_page::draw_ip_controls() { - if (ImGui::Button("Update IP") && !ip_buf_.any_empty()) { - context_->backend->set_ip(ip_buf_); - previous_ip_ = std::exchange(ip_buf_, {}); - } - - ImGui::SameLine(); - utils::draw_input_box("##ip_FIRST", ip_buf_.first, previous_ip_.first.c_str(), 75.0F); - ImGui::SameLine(); - ImGui::TextUnformatted("."); - - ImGui::SameLine(); - utils::draw_input_box("##ip_SECOND", ip_buf_.second, previous_ip_.second.c_str(), 75.0F); - ImGui::SameLine(); - ImGui::TextUnformatted("."); - - ImGui::SameLine(); - utils::draw_input_box("##ip_THIRD", ip_buf_.third, previous_ip_.third.c_str(), 75.0F); - ImGui::SameLine(); - ImGui::TextUnformatted("."); - - ImGui::SameLine(); - utils::draw_input_box("##ip_FOURTH", ip_buf_.fourth, previous_ip_.fourth.c_str(), 75.0F); - ImGui::SameLine(); - ImGui::TextUnformatted(":"); - - ImGui::SameLine(); - utils::draw_input_box("##ip_PORT", ip_buf_.port, previous_ip_.port.c_str(), 50.0F); -} - -void home_page::draw_credential_controls() { - // TODO(blake): Do something with user/password - if (ImGui::Button("Upload Credentials") && (!username_buf_.empty() && !password_buf_.empty())) { - username_buf_ = {}; - password_buf_ = {}; - } - - ImGui::SameLine(); - utils::draw_input_box("##username", username_buf_, "Username", 150.0F); - ImGui::SameLine(); - utils::draw_input_box("##password", password_buf_, "Password", 150.0F); -} - } // namespace mbr::pages diff --git a/src/app/pages/plot.cpp b/src/app/pages/plot.cpp index 99600d1..00afd63 100644 --- a/src/app/pages/plot.cpp +++ b/src/app/pages/plot.cpp @@ -1,4 +1,4 @@ -#include "app/pages/shock.hpp" +#include "app/pages/plot.hpp" #include #include @@ -18,65 +18,10 @@ namespace mbr::pages { -void shock_page::on_enter() { log_info(context_->log, "Entered ShockPage"); } -void shock_page::on_exit() { log_info(context_->log, "Exited ShockPage"); } +void plot_page::on_enter() { log_info(context_->log, "Entered PlotPage"); } +void plot_page::on_exit() { log_info(context_->log, "Exited PlotPage"); } -void shock_page::update() { - PROFILE_FUNCTION(); - if (ImGui::BeginTable( - "##viewsplit", 2, ImGuiTableFlags_NoBordersInBody | ImGuiTableFlags_Resizable)) { - const auto cleanup_split{gsl::finally(ImGui::EndTable)}; - const std::shared_lock lock{context_->backend->get_data_latch()}; - const auto& data = context_->backend->get_data(); - - ImGui::TableNextColumn(); - draw_lhs(data.get_raw_lines()); - ImGui::TableNextColumn(); - draw_rhs(data.get_time(), - data.get_series("FR"), - data.get_series("FL"), - data.get_series("RR"), - data.get_series("RL")); - } -} - -void shock_page::draw_lhs(const std::vector& raw_lines) { - if (ImGui::BeginChild("##datalog")) { - const auto cleanup_data{gsl::finally(ImGui::EndChild)}; - BOLD_HEADER(ImGui::Text("Data Log")); - - ImGui::Separator(); - text_utils_.start_logging_button(); - ImGui::SameLine(); - text_utils_.data_download_button(raw_lines, download_fd_text_); - ImGui::SameLine(); - - HEADER(utils::draw_input_box("##extra_shock", download_fd_text_, "File descriptor")); - ImGui::Separator(); - utils::draw_data_log(raw_lines); - } -} - -void shock_page::draw_rhs(gsl::span time, - gsl::span fr, - gsl::span fl, - gsl::span br, - gsl::span bl) { - if (ImGui::BeginChild("##graph")) { - const auto cleanup_graph{gsl::finally(ImGui::EndChild)}; - const auto sync_lt = context_->backend->get_data().get_sync_lt(); - const auto plot_title = - sync_lt ? fmt::format("Shock Travel Data from {}", sync_lt.value().to_string()) - : "No Synced Time"; - - if (ImPlot::BeginPlot(plot_title.c_str(), {-1, -1})) { - const auto cleanup_plot{gsl::finally(ImPlot::EndPlot)}; - utils::plot_if_non_empty("Front Right Shock Travel", time, fr); - utils::plot_if_non_empty("Front Left Shock Travel", time, fl); - utils::plot_if_non_empty("Rear Right Shock Travel", time, br); - utils::plot_if_non_empty("Rear Left Shock Travel", time, bl); - } - } +void plot_page::update() { } } // namespace mbr::pages diff --git a/src/app/pages/serialmon.cpp b/src/app/pages/serialmon.cpp index 82d4297..3cb06a9 100644 --- a/src/app/pages/serialmon.cpp +++ b/src/app/pages/serialmon.cpp @@ -20,126 +20,6 @@ void serial_page::on_exit() { log_info(context_->log, "Exited SerialPage"); } void serial_page::update() { PROFILE_FUNCTION(); - const f32 full_height = ImGui::GetContentRegionAvail().y; - const f32 top_height = full_height * 0.66F; - const f32 bottom_height = full_height - top_height; - - if (ImGui::BeginChild("##topsec", {0, top_height})) { - const auto cleanup_top{gsl::finally(ImGui::EndChild)}; - if (ImGui::BeginTable("##topsplt", 2, ImGuiTableFlags_BordersInnerV)) { - const auto cleanup_split{gsl::finally(ImGui::EndTable)}; - ImGui::TableNextColumn(); - draw_top_lhs(); - ImGui::TableNextColumn(); - draw_top_rhs(); - } - } - ImGui::Separator(); - - if (ImGui::BeginChild("##botsec", {0, bottom_height})) { - const auto cleanup{gsl::finally(ImGui::EndChild)}; - draw_bottom(); - } -} - -void serial_page::draw_top_lhs() { - if (ImGui::BeginChild("##datalog")) { - const auto cleanup{gsl::finally(ImGui::EndChild)}; - BOLD_HEADER(ImGui::Text("Serial Monitor Settings")); - - ImGui::Separator(); - if (text_drawer_.start_serial_button()) { - if (!context_->backend->get_serial_manager().is_running()) { - context_->backend->get_serial_manager().start(); - } else { - context_->backend->get_serial_manager().stop(); - } - } - ImGui::SameLine(); - text_drawer_.send_data_button(); - if (context_->backend->get_serial_manager().should_send_data()) { - context_->backend->get_serial_manager().send_data( - context_->backend->get_data().get_current_line()); - } - ImGui::Separator(); - - auto all_ports = context_->backend->get_serial_manager().get_all_ports(); - ImGui::SetNextItemWidth(250.0F); - if (ImGui::BeginCombo("##port_dropdown", "Select Ports to Send Data")) { - const auto cleanup{gsl::finally(ImGui::EndCombo)}; - for (const auto& port : all_ports) { - const bool is_selected = - context_->backend->get_serial_manager().is_port_selected(port); - if (ImGui::Selectable( - port.c_str(), is_selected, ImGuiSelectableFlags_NoAutoClosePopups)) { - if (!is_selected) { - context_->backend->get_serial_manager().add_port(port); - log_info(context_->log, "Added port {}", port); - } else { - context_->backend->get_serial_manager().remove_port(port); - log_info(context_->log, "Removed port {}", port); - } - } - } - } - - ImGui::SameLine(); - ImGui::TextUnformatted("Chosen Ports: "); - ImGui::SameLine(); - for (const auto& port : context_->backend->get_serial_manager().get_chosen_ports()) { - ImGui::TextUnformatted(port.c_str()); - ImGui::SameLine(); - } - ImGui::Separator(); - ImGui::TextUnformatted("Baud Rate"); - ImGui::SameLine(); - - ImGui::SetNextItemWidth(100.0F); - if (ImGui::BeginCombo( - "##baud_dropdown", - baud_rate_string(context_->backend->get_serial_manager().get_baud_rate()))) { - const auto cleanup_combo{gsl::finally(ImGui::EndCombo)}; - for (const auto rate : stdx::enum_range()) { - const bool is_selected = - (context_->backend->get_serial_manager().get_baud_rate() == rate); - if (ImGui::Selectable(baud_rate_string(rate), is_selected)) { - context_->backend->get_serial_manager().change_baud_rate(rate); - } - if (is_selected) { ImGui::SetItemDefaultFocus(); } - } - } - } -} - -void serial_page::draw_top_rhs() { - BOLD_HEADER(ImGui::Text("LOG Info")); - - ImGui::Separator(); - - if (ImGui::BeginChild("##errscroll", {0, 0}, false, ImGuiWindowFlags_HorizontalScrollbar)) { - const auto cleanup{gsl::finally(ImGui::EndChild)}; - const std::string all_errors = context_->logger.get_streamed_logs(); - ImGui::TextUnformatted(all_errors.c_str()); - if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) { ImGui::SetScrollHereY(1.0F); } - } -} - -void serial_page::draw_bottom() { - if (utils::draw_input_box("##command", - serial_buffer_, - "Send Serial Data Here", - 1900.0F, - ImGuiInputTextFlags_EnterReturnsTrue)) { - context_->backend->get_serial_manager().send_data(serial_buffer_ + "\n"); - serial_buffer_ = {}; - ImGui::SetKeyboardFocusHere(-1); - } - if (ImGui::BeginChild("##datalog")) { - const auto cleanup{gsl::finally(ImGui::EndChild)}; - ImGui::Separator(); - const std::scoped_lock lock{context_->backend->get_serial_manager().get_mutex()}; - utils::draw_data_log(context_->backend->get_serial_manager().get_data_stream()); - } } } // namespace mbr::pages From 26cad3fe09520c602a31c74800691051faf3b060 Mon Sep 17 00:00:00 2001 From: Blake Date: Mon, 27 Jul 2026 16:28:42 -0400 Subject: [PATCH 05/36] Qt is here on Windows, menu bar theoretically works --- CMakeLists.txt | 2 +- cmake/CompilerFlags.cmake | 2 +- cmake/MbrDaqSuite.cmake | 30 ++++-- cmake/VendorSetup.cmake | 75 -------------- include/app/assets/texture.hpp | 53 ---------- include/app/backend_bridge.hpp | 24 +++++ include/app/context.hpp | 4 +- include/app/gui.hpp | 9 ++ include/app/pages/analysis.hpp | 63 +----------- include/app/pages/plot.hpp | 4 +- include/app/pages/serialmon.hpp | 4 +- include/app/pages/settings.hpp | 30 ++++++ include/app/pages/utils.hpp | 58 ----------- include/app/style.hpp | 48 --------- include/esp32/backend.hpp | 23 +++++ include/esp32/data.hpp | 10 +- src/app/app.cpp | 11 +-- src/app/assets/texture.cpp | 49 --------- src/app/backend_bridge.cpp | 0 src/app/gui.cpp | 84 ++++++++++++++-- src/app/pages/analysis.cpp | 13 +-- src/app/pages/home.cpp | 4 - src/app/pages/plot.cpp | 4 - src/app/pages/serialmon.cpp | 3 - src/app/pages/settings.cpp | 23 +++++ src/app/pages/utils.cpp | 103 ------------------- src/app/style.cpp | 170 -------------------------------- src/esp32/backend.cpp | 89 +++-------------- src/esp32/data.cpp | 6 +- src/vendor/sokol_impl.cpp | 14 --- 30 files changed, 245 insertions(+), 767 deletions(-) delete mode 100644 include/app/assets/texture.hpp create mode 100644 include/app/backend_bridge.hpp create mode 100644 include/app/pages/settings.hpp delete mode 100644 include/app/pages/utils.hpp delete mode 100644 include/app/style.hpp delete mode 100644 src/app/assets/texture.cpp create mode 100644 src/app/backend_bridge.cpp create mode 100644 src/app/pages/settings.cpp delete mode 100644 src/app/pages/utils.cpp delete mode 100644 src/app/style.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f99da68..dabf130 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,7 @@ file(GLOB_RECURSE ALL_SOURCES "${SRC_DIR}/*.cpp") set(INCLUDE_DIR "${CMAKE_SOURCE_DIR}/include") file(GLOB_RECURSE HEADERS "${INCLUDE_DIR}/*.hpp" "${INCLUDE_DIR}/*.h") -set(LIB_SOURCES ${ALL_SOURCES}) +set(LIB_SOURCES ${ALL_SOURCES} ${HEADERS}) list(FILTER LIB_SOURCES EXCLUDE REGEX "main\\.cpp$") set(TEST_DIR "${CMAKE_SOURCE_DIR}/tests") diff --git a/cmake/CompilerFlags.cmake b/cmake/CompilerFlags.cmake index 334d74b..74977af 100644 --- a/cmake/CompilerFlags.cmake +++ b/cmake/CompilerFlags.cmake @@ -8,7 +8,7 @@ elseif(WIN32) endif() if(MINGW) - add_link_options("-static-libgcc" "-static-libstdc++" "-static") + add_link_options(-fuse-ld=lld) endif() set(CMAKE_CXX_STANDARD 23) diff --git a/cmake/MbrDaqSuite.cmake b/cmake/MbrDaqSuite.cmake index 1755416..77d0eb7 100644 --- a/cmake/MbrDaqSuite.cmake +++ b/cmake/MbrDaqSuite.cmake @@ -5,15 +5,9 @@ function(create_mbrdaq_suite TARGET_NAME EXTRA_FLAGS OUT_DIR) AUTOUIC OFF AUTORCC OFF ) + target_link_libraries(mbrdaq_core_${TARGET_NAME} PUBLIC - imgui_setup - implot_setup tinyfiledialogs - opencv_core - opencv_imgproc - opencv_imgcodecs - opencv_videoio - opencv_highgui spdlog::spdlog ixwebsocket stb_image_setup @@ -42,11 +36,13 @@ function(create_mbrdaq_suite TARGET_NAME EXTRA_FLAGS OUT_DIR) target_compile_options(mbrdaq_core_${TARGET_NAME} PRIVATE ${BASE_FLAGS}) target_compile_options(mbrdaq_core_${TARGET_NAME} PRIVATE ${EXTRA_FLAGS}) + if(APPLE) - target_link_options(mbrdaq_core_${TARGET_NAME} PRIVATE ${EXTRA_FLAGS} "-static-libstdc++") + target_link_options(mbrdaq_core_${TARGET_NAME} PRIVATE ${EXTRA_FLAGS}) elseif(WIN32) target_link_options(mbrdaq_core_${TARGET_NAME} PRIVATE ${EXTRA_FLAGS}) endif() + set_target_properties(mbrdaq_core_${TARGET_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${OUT_DIR}) @@ -60,6 +56,7 @@ function(create_mbrdaq_suite TARGET_NAME EXTRA_FLAGS OUT_DIR) set_target_properties(catch2_${TARGET_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${OUT_DIR}) + # Test Executable add_executable(mbrdaq_tests_${TARGET_NAME} ${TEST_SOURCES}) target_link_libraries(mbrdaq_tests_${TARGET_NAME} PRIVATE catch2_${TARGET_NAME} mbrdaq_core_${TARGET_NAME}) @@ -71,6 +68,7 @@ function(create_mbrdaq_suite TARGET_NAME EXTRA_FLAGS OUT_DIR) target_compile_options(mbrdaq_tests_${TARGET_NAME} PRIVATE ${EXTRA_FLAGS}) target_link_options(mbrdaq_tests_${TARGET_NAME} PRIVATE ${EXTRA_FLAGS}) + set_target_properties(mbrdaq_tests_${TARGET_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${OUT_DIR}) endfunction() @@ -90,6 +88,7 @@ function(create_standard_target EXTRA_FLAGS TARGET_NAME) endif() target_link_libraries(mbrdaq_${TARGET_NAME} PRIVATE mbrdaq_core_${TARGET_NAME}) + add_custom_target(${TARGET_NAME} DEPENDS mbrdaq_tests_${TARGET_NAME} mbrdaq_${TARGET_NAME}) @@ -98,17 +97,30 @@ function(create_standard_target EXTRA_FLAGS TARGET_NAME) OUTPUT_NAME ${PROJECT_NAME}) if(WIN32) + # Ensure destination directory exists add_custom_command(TARGET mbrdaq_${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/bin" COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_BINARY_DIR}/bin" "$" - COMMENT "Copy FFMPEG to output directory" + COMMENT "Copy FFMPEG binaries to output directory" + ) + + # Automatically copy MSYS2 GCC/MinGW compiler runtimes + add_custom_command(TARGET mbrdaq_${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "C:/msys64/ucrt64/bin/libstdc++-6.dll" + "C:/msys64/ucrt64/bin/libgcc_s_seh-1.dll" + "C:/msys64/ucrt64/bin/libwinpthread-1.dll" + "$" + COMMENT "Copying MinGW runtime DLLs" ) if(WINDEPLOYQT_EXE) add_custom_command(TARGET mbrdaq_${TARGET_NAME} POST_BUILD COMMAND ${WINDEPLOYQT_EXE} --dir "$" + --no-translations "$" COMMENT "Deploy Qt runtime libraries" ) diff --git a/cmake/VendorSetup.cmake b/cmake/VendorSetup.cmake index 3dd404d..dff325b 100644 --- a/cmake/VendorSetup.cmake +++ b/cmake/VendorSetup.cmake @@ -45,85 +45,10 @@ add_vendor_fetch_content(gsl vendor/gsl) add_vendor_fetch_content(fmt vendor/fmt) # Sokol -add_library(sokol_setup STATIC src/vendor/sokol_impl.cpp) -target_include_directories(sokol_setup SYSTEM PUBLIC - vendor/sokol - vendor/sokol/util - vendor/imgui - vendor/imgui/backends) -target_compile_options(sokol_setup PRIVATE ${WARNING_IGNORE}) -if(APPLE) - target_compile_definitions(sokol_setup PUBLIC SOKOL_METAL) - set_source_files_properties(src/vendor/sokol_impl.cpp PROPERTIES LANGUAGE OBJCXX) - target_link_libraries(sokol_setup INTERFACE - "-framework Cocoa" - "-framework IOKit" - "-framework Metal" - "-framework MetalKit" - "-framework QuartzCore" - "-framework AVFoundation" - "-framework CoreMedia" - "-framework CoreVideo" - ) -elseif(WIN32) - target_compile_definitions(sokol_setup PUBLIC SOKOL_D3D11) - target_link_libraries(sokol_setup INTERFACE d3d11 dxgi dwmapi) -endif() - -# Dear ImGUI -add_library(imgui_setup STATIC - vendor/imgui/imgui.cpp - vendor/imgui/imgui_draw.cpp - vendor/imgui/imgui_widgets.cpp - vendor/imgui/imgui_tables.cpp - vendor/imgui/imgui_demo.cpp -) -target_include_directories(imgui_setup SYSTEM PUBLIC - vendor/imgui vendor/imgui/backends) -target_link_libraries(imgui_setup PUBLIC sokol_setup) - -# ImPlot -add_library(implot_setup STATIC - vendor/implot/implot.cpp - vendor/implot/implot_items.cpp - vendor/implot/implot_demo.cpp -) -target_include_directories(implot_setup SYSTEM PUBLIC vendor/implot) -target_link_libraries(implot_setup PUBLIC imgui_setup) -target_compile_options(implot_setup PRIVATE ${WARNING_IGNORE}) # OpenCV -set(BUILD_LIST "core,imgproc,imgcodecs,videoio,highgui" CACHE STRING "" FORCE) -set(BUILD_SHARED_LIBS OFF CACHE BOOL "Static linking" FORCE) -set(BUILD_opencv_apps OFF CACHE BOOL "Classifier training not needed" FORCE) -set(BUILD_opencv_python2 OFF CACHE BOOL "Disable python support" FORCE) -set(BUILD_opencv_python3 OFF CACHE BOOL "Disable python support" FORCE) -set(BUILD_EXAMPLES OFF CACHE BOOL "Examples not needed" FORCE) -set(BUILD_DOCS OFF CACHE BOOL "OpenCV docs are not needed" FORCE) -set(BUILD_TESTS OFF CACHE BOOL "Testing not needed for vendoring" FORCE) -set(BUILD_PERF_TESTS OFF CACHE BOOL "Testing not needed for vendoring" FORCE) -set(BUILD_CUDA_STUBS OFF CACHE BOOL "Cuda is never needed" FORCE) -set(WITH_CUDA OFF CACHE BOOL "" FORCE) -set(BUILD_JAVA OFF CACHE BOOL "" FORCE) -set(WITH_IPP OFF CACHE BOOL "Intel IPP support not needed" FORCE) -set(WITH_ITT OFF CACHE BOOL "Intel ITT support not needed" FORCE) -set(WITH_OPENCL OFF CACHE BOOL "" FORCE) -set(WITH_WEBP OFF CACHE BOOL "" FORCE) -set(WITH_OPENEXR OFF CACHE BOOL "" FORCE) - -set(OPENCV_WARNINGS_ARE_ERRORS OFF CACHE BOOL "" FORCE) -set(CMAKE_SUPPRESS_DEVELOPER_WARNINGS ON CACHE BOOL "" FORCE) - -if(APPLE) - set(WITH_FFMPEG OFF CACHE BOOL "System dependencies instead" FORCE) - set(HAVE_FFMPEG OFF CACHE BOOL "System dependencies instead" FORCE) - set(WITH_AVFOUNDATION ON CACHE BOOL "Apple framework is statically linked" FORCE) - unset(FFMPEG_LIBRARIES CACHE) - unset(FFMPEG_INCLUDE_DIRS CACHE) -endif() -add_vendor_subdirectory(vendor/opencv) # TinyFileDialogs add_library(tinyfiledialogs STATIC vendor/libtinyfiledialogs/tinyfiledialogs.c) diff --git a/include/app/assets/texture.hpp b/include/app/assets/texture.hpp deleted file mode 100644 index 978d929..0000000 --- a/include/app/assets/texture.hpp +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace mbr::assets { - -class button_texture { - public: - button_texture(gsl::span data); - ~button_texture(); - MAKE_MOVE_ONLY(button_texture); - - [[nodiscard]] ImTextureID get_id() const { return im_tex_id_; } - [[nodiscard]] bool is_valid() const { return im_tex_id_ != 0; } - - private: - sg_image image_{SG_INVALID_ID}; - sg_view view_{SG_INVALID_ID}; - ImTextureID im_tex_id_{0}; -}; - -template struct icon_texture { - icon_texture(gsl::span data) - : pixels{stbi_load_from_memory( - data.data(), static_cast(data.size()), &width, &height, &comp, 4)} { - size = static_cast(width * height) * comp; - } - ~icon_texture() { release(); }; - MAKE_MOVE_ONLY(icon_texture); - - // This is a noop if the texture is managed - void release() { - if constexpr (!Managed) { - stbi_image_free(pixels); - pixels = nullptr; - } - } - - i32 width; - i32 height; - i32 comp; - unsigned char* pixels; - usize size; -}; - -} // namespace mbr::assets diff --git a/include/app/backend_bridge.hpp b/include/app/backend_bridge.hpp new file mode 100644 index 0000000..5f7c91b --- /dev/null +++ b/include/app/backend_bridge.hpp @@ -0,0 +1,24 @@ +#pragma once +#include +#include +#include +#include + +namespace mbr { + +class backend_bridge : public QObject { + Q_OBJECT +public: + explicit backend_bridge(telemetry_backend* backend, QObject* parent = nullptr) + : QObject(parent) { + backend->set_on_data([this](const ankerl::unordered_dense::map>& d) { emit data_updated(d); }); + backend->set_on_connection_changed([this](bool c) { emit connection_changed(c); }); + backend->set_on_receiving_changed([this](bool r) { emit receiving_changed(r); }); + } +signals: + void data_updated(const ankerl::unordered_dense::map>&); + void connection_changed(bool connected); + void receiving_changed(bool receiving); +}; + +} //namespace mbr diff --git a/include/app/context.hpp b/include/app/context.hpp index eb8c4b3..718726c 100644 --- a/include/app/context.hpp +++ b/include/app/context.hpp @@ -7,7 +7,6 @@ #include -#include "app/style.hpp" #include "core/log.hpp" #include "esp32/backend.hpp" @@ -18,6 +17,7 @@ enum class page_type_t : u8 { PLOT, ANALYSIS, SERIAL, + SETTINGS, }; [[nodiscard]] constexpr const char* page_type_str(page_type_t page_type) { @@ -26,6 +26,7 @@ enum class page_type_t : u8 { case page_type_t::PLOT: return "Plot"; case page_type_t::ANALYSIS: return "Analysis"; case page_type_t::SERIAL: return "Serial Monitor"; + case page_type_t::SETTINGS: return "Settings"; default: return "Unknown"; } } @@ -34,7 +35,6 @@ class telemetry_backend; struct app_context { log_t logger; - app_style style; page_type_t current_page_type; std::atomic should_exit{false}; bool is_cmd_input_focused{false}; diff --git a/include/app/gui.hpp b/include/app/gui.hpp index d11dc12..7105613 100644 --- a/include/app/gui.hpp +++ b/include/app/gui.hpp @@ -3,9 +3,11 @@ #include #include #include +#include #include #include "app/context.hpp" #include "app/pages/page.hpp" +#include "app/backend_bridge.hpp" #include #include #include @@ -16,16 +18,23 @@ class gui_t : public QMainWindow { Q_OBJECT public: explicit gui_t(const std::shared_ptr& ctx); + ~gui_t() = default; private: void change_page(page_type_t type); void build_menu_bar(); + void update_status_dot(); [[nodiscard]] pages::page* create_page(page_type_t type, const std::shared_ptr& ctx, QWidget* parent); private: + backend_bridge bridge_; QStackedWidget* pages_; std::shared_ptr context_; ankerl::unordered_dense::map page_lookup_; + QLabel* connection_status_; + QLabel* status_dot_ = nullptr; + bool is_connected_ = false; + bool is_receiving_ = false; }; } // namespace mbr diff --git a/include/app/pages/analysis.hpp b/include/app/pages/analysis.hpp index e6c29d6..89fdcb6 100644 --- a/include/app/pages/analysis.hpp +++ b/include/app/pages/analysis.hpp @@ -10,14 +10,11 @@ #include #include -#include -#include -#include + #include #include #include -#include "app/assets/texture.hpp" #include "app/context.hpp" #include "app/pages/page.hpp" #include "core/time.hpp" @@ -39,7 +36,7 @@ class analysis_page : public page { public: explicit analysis_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : page(ctx, parent) {}; - ~analysis_page() override; + ~analysis_page() override = default; void on_enter() override; void on_exit() override; @@ -48,63 +45,7 @@ class analysis_page : public page { private: private: - std::shared_ptr is_alive_; - - std::string video_path_; - stdx::option video_creation_ts_; - std::string creation_metadata_text_buf_; - std::atomic video_dialog_running_{false}; - std::mutex video_path_mutex_; - selected_video_t selected_video_; - std::string input_time_; - bool video_loaded_{false}; - bool video_hovered_{false}; - - bool dynamic_plotting_{false}; - usize plot_percent_; - double points_per_{0.0}; - double data_count_{0.0}; - double data_from_end_{0.0}; - - std::string txt_path_; - std::atomic txt_dialog_running_{false}; - std::mutex txt_path_mutex_; - selected_txt_file_t selected_txt_; - bool txt_loaded_{false}; - bool data_and_time_sync_{false}; - - i32 total_frames_{0}; - double video_fps_{0.0}; - double video_length_minutes_{0.0}; - std::string video_length_formatted_; - double frame_duration_{0.0}; - double time_accumulator_{0.0}; - i32 current_frame_ui_{0}; - - std::atomic is_playing_{false}; - std::atomic is_looping_{false}; - std::atomic seek_target_{-1}; - std::atomic force_update_frame_{false}; - data_view_t data_show_{data_view_t::ALL}; - - std::thread decode_thread_; - std::atomic thread_running_{false}; - std::mutex frame_mutex_; - std::deque> frame_queue_; - std::condition_variable queue_cv_; - - ImVec2 button_size_{24, 24}; - //assets::button_texture play_button_; - //assets::button_texture pause_button_; - //assets::button_texture step_button_; - - sg_image video_texture_{SG_INVALID_ID}; - sg_view video_view_{SG_INVALID_ID}; - ImTextureID video_texture_id_{0}; - i32 texture_width_{0}; - i32 texture_height_{0}; - bool timestamp_input_focused_{false}; }; } // namespace mbr::pages diff --git a/include/app/pages/plot.hpp b/include/app/pages/plot.hpp index bacbc9c..32d2ef4 100644 --- a/include/app/pages/plot.hpp +++ b/include/app/pages/plot.hpp @@ -8,13 +8,12 @@ #include #include "app/pages/page.hpp" -#include "app/pages/utils.hpp" namespace mbr::pages { class plot_page : public page { public: - explicit plot_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : page{ctx, parent}, text_utils_{ctx} {} + explicit plot_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : page{ctx, parent} {} ~plot_page() override = default; void on_enter() override; @@ -29,7 +28,6 @@ class plot_page : public page { gsl::span bl); private: - utils::text_drawers text_utils_; std::string download_fd_text_; }; diff --git a/include/app/pages/serialmon.hpp b/include/app/pages/serialmon.hpp index ea53b76..f5dff1b 100644 --- a/include/app/pages/serialmon.hpp +++ b/include/app/pages/serialmon.hpp @@ -7,13 +7,12 @@ #include #include "app/pages/page.hpp" -#include "app/pages/utils.hpp" namespace mbr::pages { class serial_page : public page { public: - explicit serial_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : page{ctx, parent}, text_drawer_{ctx} {} + explicit serial_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : page{ctx, parent} {} ~serial_page() override = default; void on_enter() override; @@ -23,7 +22,6 @@ class serial_page : public page { private: private: - utils::text_drawers text_drawer_; std::string serial_buffer_; }; diff --git a/include/app/pages/settings.hpp b/include/app/pages/settings.hpp new file mode 100644 index 0000000..36b3eca --- /dev/null +++ b/include/app/pages/settings.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include + +#include "app/context.hpp" +#include "app/pages/page.hpp" +#include "core/ip.hpp" + +namespace mbr::pages { + +class settings_page : public page { + public: + explicit settings_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : page{ctx, parent} {} + ~settings_page() override = default; + + void on_enter() override; + void on_exit() override; + void update() override; + + private: + + + private: + +}; + +} // namespace mbr::pages diff --git a/include/app/pages/utils.hpp b/include/app/pages/utils.hpp deleted file mode 100644 index 457af4b..0000000 --- a/include/app/pages/utils.hpp +++ /dev/null @@ -1,58 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace mbr { - -struct app_context; - -namespace pages::utils { - -template -void plot_if_non_empty(const char* label, - gsl::span x, - gsl::span y, - bool extra_condition = true, - usize data_clip_position = 0) { - if (!x.empty() && !y.empty() && extra_condition) { - const usize n = std::min(x.size(), y.size()); - const usize count = n - std::min(n, data_clip_position); - ImPlot::PlotLine(label, x.data(), y.data(), count); - } -} - -class text_drawers { - public: - explicit text_drawers(const std::shared_ptr& ctx) : context_{ctx} {} - - void start_logging_button(); - bool start_serial_button(); - void send_data_button(); - - // Appends the buf data to the filepath, clearing the buf on success. - void data_download_button(const std::vector& raw_lines, std::string& buf); - - private: - std::shared_ptr context_; -}; - -bool draw_input_box(const char* label, - std::string& buf, - stdx::option hint = stdx::none, - float width_scale = 200.0F, - ImGuiInputTextFlags flags = 0); -void draw_data_log(gsl::span raw_lines); - -} // namespace pages::utils - -} // namespace mbr diff --git a/include/app/style.hpp b/include/app/style.hpp deleted file mode 100644 index fcafb8b..0000000 --- a/include/app/style.hpp +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -struct ImFont; - -#include "core/log.hpp" - -namespace mbr { - -struct app_fonts { - ImFont* regular; - ImFont* bold; - ImFont* italic; - ImFont* bold_italic; -}; - -app_fonts load_fonts(); - -struct app_style { - void set_dark_theme(const log_fn_t& log = nullptr); - void set_light_theme(const log_fn_t& log = nullptr); - - app_fonts default_fonts; - bool dark_mode{true}; -}; - -constexpr float DEFAULT_FONT_SIZE = 22.0F; -constexpr float HEADER_FONT_SIZE = 26.0F; -constexpr float MAIN_MENU_BAR_FONT_SIZE = 30.0F; -constexpr float MAIN_MENU_ITEM_FONT_SIZE = 28.0F; -constexpr float MENU_BAR_FONT_SIZE = 26.0F; -constexpr float MENU_ITEM_FONT_SIZE = 24.0F; - -#define WITH_FONT(font, size, code) \ - do { \ - ImGui::PushFont(font, size); \ - code; \ - ImGui::PopFont(); \ - } while (false) - -#define BOLD_DEFAULT(B) WITH_FONT(context_->style.default_fonts.bold, DEFAULT_FONT_SIZE, B) -#define HEADER(B) WITH_FONT(context_->style.default_fonts.regular, HEADER_FONT_SIZE, B) -#define BOLD_HEADER(B) WITH_FONT(context_->style.default_fonts.bold, HEADER_FONT_SIZE, B) -#define MAIN_MENU_BAR(B) \ - WITH_FONT(context_->style.default_fonts.regular, MAIN_MENU_BAR_FONT_SIZE, B) -#define MAIN_MENU_BAR_ITEM(B) \ - WITH_FONT(context_->style.default_fonts.regular, MAIN_MENU_ITEM_FONT_SIZE, B) - -} // namespace mbr diff --git a/include/esp32/backend.hpp b/include/esp32/backend.hpp index 35c9716..eb754f3 100644 --- a/include/esp32/backend.hpp +++ b/include/esp32/backend.hpp @@ -34,6 +34,29 @@ enum class response_type_t : u8 { }; class telemetry_backend { + + public: + using data_cb = std::function>&)>; + using status_cb = std::function; + using receiving_cb = std::function; + using cmd_cb = std::function; + + void set_on_data(data_cb cb) { on_data_ = std::move(cb); } + void set_on_connection_changed(status_cb cb) { on_connection_changed_ = std::move(cb); } + void set_on_receiving_changed(receiving_cb cb) { on_receiving_changed_ = std::move(cb); } + void set_on_cmd(cmd_cb cb) { on_cmd_ = std::move(cb); } + + private: + void notify_data(const ankerl::unordered_dense::map>& s) { if (on_data_) on_data_(s); } + void notify_connection_changed(bool c) { if (on_connection_changed_) on_connection_changed_(c); } + void notify_receiving_changed(bool r) { if (on_receiving_changed_) on_receiving_changed_(r); } + + data_cb on_data_; + status_cb on_connection_changed_; + receiving_cb on_receiving_changed_; + cmd_cb on_cmd_; + + public: explicit telemetry_backend(const std::filesystem::path& json_path, log_fn_t log = nullptr); ~telemetry_backend(); diff --git a/include/esp32/data.hpp b/include/esp32/data.hpp index 2ed100f..37fb6ba 100644 --- a/include/esp32/data.hpp +++ b/include/esp32/data.hpp @@ -47,8 +47,8 @@ class telemetry_data { template [[nodiscard]] auto& get_series(this Self&& self, const std::string& key) noexcept { - auto it = self.series.find(key); - if (it != self.series.end()) { return it->second; } + auto it = self.series_.find(key); + if (it != self.series_.end()) { return it->second; } if (!self.logged_missing_keys_.contains(key)) { self.logged_missing_keys_.insert(key); @@ -58,6 +58,10 @@ class telemetry_data { return dummy; } + [[nodiscard]] auto& get_series() noexcept { + return series_; + } + void set_sync_lt(local_time lt) noexcept { sync_lt_ = lt; } void write_data(const std::string& identifier, const std::string& value); void write_raw_line(const std::string& message); @@ -76,7 +80,7 @@ class telemetry_data { std::vector raw_lines_; double sync_start_; stdx::option sync_lt_; - ankerl::unordered_dense::map> series; + ankerl::unordered_dense::map> series_; mutable ankerl::unordered_dense::set logged_missing_keys_; friend class pages::view_page; diff --git a/src/app/app.cpp b/src/app/app.cpp index 8270d21..ab2348f 100644 --- a/src/app/app.cpp +++ b/src/app/app.cpp @@ -4,12 +4,11 @@ #include #include -#include -#include -#include -#include +#include +#include #include #include +#include #include "app/context.hpp" #include "app/gui.hpp" @@ -24,14 +23,14 @@ namespace { const std::filesystem::path DEFAULT_JSON_PATH{"MBR_data.json"}; } // app_t::app_t(i32 argc, char** argv) : qt_app_{std::make_unique(argc, argv)}, context_{std::make_shared()} { context_->log = context_->logger.get_log_fn(); ix::initNetSystem(); - gui_ = std::make_unique(context_); context_->backend = std::make_unique(DEFAULT_JSON_PATH, context_->log); + gui_ = std::make_unique(context_); } app_t::~app_t() { ix::uninitNetSystem(); } void app_t::run() { - gui_->show(); + gui_->showMaximized(); qt_app_->exec(); } diff --git a/src/app/assets/texture.cpp b/src/app/assets/texture.cpp deleted file mode 100644 index 1b28b7e..0000000 --- a/src/app/assets/texture.cpp +++ /dev/null @@ -1,49 +0,0 @@ -#include "app/assets/texture.hpp" - -#include -#include -#include -#include - -namespace mbr::assets { - -button_texture::button_texture(gsl::span data) { - i32 width, height, comp; - auto* pixels = stbi_load_from_memory( - data.data(), static_cast(data.size()), &width, &height, &comp, 4); - ASSERT(pixels, "Image load from memory failed"); - - sg_image_desc img_desc = {}; - img_desc.width = width; - img_desc.height = height; - img_desc.pixel_format = SG_PIXELFORMAT_RGBA8; - img_desc.num_mipmaps = 1; - img_desc.data.mip_levels[0].ptr = pixels; - img_desc.data.mip_levels[0].size = static_cast(width * height) * comp; - - image_ = sg_make_image(&img_desc); - stbi_image_free(pixels); - ASSERT(image_.id != SG_INVALID_ID, "Image allocation failed"); - - sg_view_desc view_desc = {}; - view_desc.texture.image = image_; - - view_ = sg_make_view(&view_desc); - ASSERT(view_.id != SG_INVALID_ID, "View allocation failed"); - im_tex_id_ = simgui_imtextureid(view_); -} - -button_texture::~button_texture() { - if (view_.id != SG_INVALID_ID) { - sg_destroy_view(view_); - view_.id = SG_INVALID_ID; - } - - if (image_.id != SG_INVALID_ID) { - sg_destroy_image(image_); - image_.id = SG_INVALID_ID; - im_tex_id_ = 0; - } -} - -} // namespace mbr::assets diff --git a/src/app/backend_bridge.cpp b/src/app/backend_bridge.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/app/gui.cpp b/src/app/gui.cpp index 09f4a97..a69378f 100644 --- a/src/app/gui.cpp +++ b/src/app/gui.cpp @@ -1,6 +1,9 @@ #include "app/gui.hpp" #include +#include +#include +#include #include #include @@ -8,35 +11,40 @@ #include #include #include + + #include #include #include #include +#include +#include +#include #include "app/assets/images/app_icon.hpp" -#include "app/assets/texture.hpp" #include "app/pages/page.hpp" #include "app/context.hpp" #include "app/pages/home.hpp" #include "app/pages/plot.hpp" #include "app/pages/serialmon.hpp" -#include "app/pages/utils.hpp" #include "app/pages/analysis.hpp" -#include "app/style.hpp" +#include "app/pages/settings.hpp" #include "core/time.hpp" +#include "esp32/backend.hpp" using namespace std::chrono; namespace mbr { - gui_t::gui_t(const std::shared_ptr& ctx) : context_(ctx) { + gui_t::gui_t(const std::shared_ptr& ctx) : bridge_(ctx->backend.get(), this), context_(ctx) { pages_ = new QStackedWidget(this); - pages_->setCurrentWidget(pages_); + setCentralWidget(pages_); for (auto type : {page_type_t::HOME, page_type_t::PLOT, page_type_t::ANALYSIS, page_type_t::SERIAL}) { pages::page* p = create_page(type, context_, pages_); pages_->addWidget(p); page_lookup_[type] = p; } + change_page(page_type_t::HOME); build_menu_bar(); } @@ -47,6 +55,7 @@ namespace mbr { case page_type_t::PLOT: return new pages::plot_page(ctx, parent); case page_type_t::ANALYSIS: return new pages::analysis_page(ctx, parent); case page_type_t::SERIAL: return new pages::serial_page(ctx, parent); + case page_type_t::SETTINGS: return new pages::settings_page(ctx, parent); default: return nullptr; } } @@ -57,9 +66,72 @@ namespace mbr { } void gui_t::build_menu_bar() { - QMenu* nav = menuBar()->addMenu("Navigate"); + menuBar()->setStyleSheet(R"( + QMenuBar { + background-color: #2b2b2b; + color: white; + font-size: 20px; + spacing: 2px; + } + QMenuBar::item { + background-color: transparent; + padding: 4px 6px; + border-radius: 4px; + } + QMenuBar::item:selected { + background-color: #3C3F41; + } + )"); + auto* file = menuBar()->addMenu("File"); + file->addAction("Settings"); + file->addAction("Export Log"); + file->addAction("Exit", this, [] { QApplication::quit(); }); + auto* nav = menuBar()->addMenu("Navigation"); nav->addAction("Home", this, [this] { change_page(page_type_t::HOME); }); nav->addAction("Plot", this, [this] { change_page(page_type_t::PLOT); }); + nav->addAction("Serial", this, [this] { change_page(page_type_t::SERIAL); }); + nav->addAction("Analysis", this, [this] { change_page(page_type_t::ANALYSIS); }); + + menuBar()->addAction("Sync Time", this, [this] { + local_time lt; + const auto sync_time = lt.micros_since_midnight(); + context_->backend->send_cmd(std::format("CMD SYNC {}", sync_time)); + }); + + menuBar()->addAction("Restart Connection", this, [this] { context_->backend->get_try_connection() = true; }); + + menuBar()->addAction("Clear Data", this, [this] { context_->backend->get_data().clear(); }); + + auto* rightWidget = new QWidget(menuBar()); + auto* rightLayout = new QHBoxLayout(rightWidget); + rightLayout->setContentsMargins(0, 4, 8, 4); + rightLayout->setSpacing(8); + + + connection_status_ = new QLabel("Disconnected", rightWidget); + + connect(&bridge_, &backend_bridge::connection_changed, this, [this](bool connected) { + is_connected_ = connected; + if (!connected) is_receiving_ = false; + connection_status_->setText(connected ? "Connected" : "Disconnected"); + update_status_dot(); + }); + connection_status_->setStyleSheet("font-size: 20px;"); + + status_dot_ = new QLabel(rightWidget); + status_dot_->setFixedSize(20, 20); + status_dot_->setStyleSheet("background-color: #F44336; border-radius: 10px;"); + rightLayout->addWidget(connection_status_, 0, Qt::AlignVCenter); + rightLayout->addWidget(status_dot_, 0, Qt::AlignVCenter); + + menuBar()->setCornerWidget(rightWidget, Qt::TopRightCorner); + } + + void gui_t::update_status_dot() { + QString color = !is_connected_ ? "#F44336" + : !is_receiving_ ? "#FFC107" + : "#00E676"; + status_dot_->setStyleSheet(QString("background-color: %1; border-radius: 10px;").arg(color)); } diff --git a/src/app/pages/analysis.cpp b/src/app/pages/analysis.cpp index 45273d0..5d74974 100644 --- a/src/app/pages/analysis.cpp +++ b/src/app/pages/analysis.cpp @@ -15,16 +15,7 @@ #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include + #include #include #include @@ -33,8 +24,6 @@ #include "app/assets/images/image_buttons.hpp" #include "app/pages/page.hpp" -#include "app/pages/utils.hpp" -#include "app/style.hpp" #include "core/log.hpp" #include "core/time.hpp" diff --git a/src/app/pages/home.cpp b/src/app/pages/home.cpp index dc9a4f4..6840a2c 100644 --- a/src/app/pages/home.cpp +++ b/src/app/pages/home.cpp @@ -5,13 +5,9 @@ #include #include -#include -#include #include #include -#include "app/pages/utils.hpp" -#include "app/style.hpp" #include "core/log.hpp" #include "core/time.hpp" diff --git a/src/app/pages/plot.cpp b/src/app/pages/plot.cpp index 00afd63..2071f48 100644 --- a/src/app/pages/plot.cpp +++ b/src/app/pages/plot.cpp @@ -7,13 +7,9 @@ #include #include #include -#include -#include #include #include -#include "app/pages/utils.hpp" -#include "app/style.hpp" #include "core/log.hpp" namespace mbr::pages { diff --git a/src/app/pages/serialmon.cpp b/src/app/pages/serialmon.cpp index 3cb06a9..b447da0 100644 --- a/src/app/pages/serialmon.cpp +++ b/src/app/pages/serialmon.cpp @@ -3,13 +3,10 @@ #include #include -#include #include #include #include -#include "app/pages/utils.hpp" -#include "app/style.hpp" #include "core/log.hpp" #include "esp32/serial.hpp" diff --git a/src/app/pages/settings.cpp b/src/app/pages/settings.cpp new file mode 100644 index 0000000..918688c --- /dev/null +++ b/src/app/pages/settings.cpp @@ -0,0 +1,23 @@ +#include "app/pages/settings.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "core/log.hpp" + +namespace mbr::pages { + +void settings_page::on_enter() { log_info(context_->log, "Entered SettingsPage"); } +void settings_page::on_exit() { log_info(context_->log, "Exited SettingsPage"); } + +void settings_page::update() { +} + +} // namespace mbr::pages diff --git a/src/app/pages/utils.cpp b/src/app/pages/utils.cpp deleted file mode 100644 index 47c182e..0000000 --- a/src/app/pages/utils.cpp +++ /dev/null @@ -1,103 +0,0 @@ -#include "app/pages/utils.hpp" - -#include -#include -#include -#include -#include - -// clang-format off -#include -#include -#include // NOLINT -// clang-format on - -#include -#include -#include -#include - -#include "app/context.hpp" -#include "app/style.hpp" -#include "core/log.hpp" -#include "core/time.hpp" - -namespace mbr::pages::utils { - -void text_drawers::start_logging_button() { - HEADER({ - if (ImGui::Button(context_->backend->is_logging() ? "Stop Logging" : "Start Logging")) { - context_->backend->set_logging(!context_->backend->is_logging()); - } - }); -} - -bool text_drawers::start_serial_button() { - bool clicked = false; - HEADER({ - if (ImGui::Button(context_->backend->get_serial_manager().is_serial_write() - ? "Stop Serial" - : "Start Serial")) { - context_->backend->get_serial_manager().set_serial_write( - !context_->backend->get_serial_manager().is_serial_write()); - clicked = true; - } - }); - return clicked; -} - -void text_drawers::send_data_button() { - HEADER({ - if (ImGui::Button(context_->backend->get_serial_manager().should_send_data() - ? "Stop Data Send" - : "Send Data")) { - context_->backend->get_serial_manager().set_send_data( - !context_->backend->get_serial_manager().should_send_data()); - } - }); -} - -void text_drawers::data_download_button(const std::vector& raw_lines, - std::string& buf) { - HEADER({ - if (ImGui::Button("Download Data")) { - const date_time dt; - std::string filepath; - if (!buf.empty()) { - filepath = fmt::format("{}_{}.txt", dt.to_string(date_time::fmt_t::TEXT_FILE), buf); - } else { - filepath = fmt::format("{}.txt", dt.to_string(date_time::fmt_t::TEXT_FILE)); - } - - if (raw_lines.empty()) { - log_warn(context_->log, "Cannot download data as the data buffer is empty!"); - } else { - std::ofstream out{filepath}; - if (!out.is_open()) { - log_error( - context_->log, "Failed to open output file: {}", std::strerror(errno)); - } else { - for (const auto& line : raw_lines) { out << line << "\n"; } - buf = {}; - } - } - } - }); -} - -bool draw_input_box(const char* label, - std::string& buf, - stdx::option hint, - f32 width_scale, - ImGuiInputTextFlags flags) { - ImGui::SetNextItemWidth(width_scale); - if (hint) { return ImGui::InputTextWithHint(label, *hint, &buf, flags); } - return ImGui::InputText(label, &buf, flags); -} - -void draw_data_log(gsl::span raw_lines) { - for (const auto& msg : raw_lines) { ImGui::TextUnformatted(msg.c_str()); } - if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) { ImGui::SetScrollHereY(1.0F); } -} - -} // namespace mbr::pages::utils diff --git a/src/app/style.cpp b/src/app/style.cpp deleted file mode 100644 index 19ba722..0000000 --- a/src/app/style.cpp +++ /dev/null @@ -1,170 +0,0 @@ -#include "app/style.hpp" - -#include - -#ifdef _WIN32 -# include -# include -# include -#endif - -#include -#include -#include -#include -#include - -#include "app/assets/fonts/open_sans.hpp" -#include "core/log.hpp" - -namespace mbr { - -namespace { - -constinit stdx::option> color_cache = stdx::none; - -void initialize_imgui_color_cache(ImVec4 colors[ImGuiCol_COUNT]) { - auto& cache = color_cache.emplace(); - for (auto i = 0; i < ImGuiCol_COUNT; i++) { cache[i] = colors[i]; } -} - -void refresh_imgui_color_cache(ImVec4 colors[ImGuiCol_COUNT]) { - ASSERT(color_cache, "Color cache has not yet been initialized"); - auto& cache = *color_cache; - for (auto i = 0; i < ImGuiCol_COUNT; i++) { colors[i] = cache[i]; } -} - -} // namespace - -app_fonts load_fonts() { - ImGuiIO& io = ImGui::GetIO(); - ImFontConfig cfg; - cfg.FontDataOwnedByAtlas = false; - - auto* regular = - io.Fonts->AddFontFromMemoryTTF(assets::OPEN_SANS_REGULAR_TTF.data(), - static_cast(assets::OPEN_SANS_REGULAR_TTF.size()), - DEFAULT_FONT_SIZE, - &cfg); - - auto* bold = io.Fonts->AddFontFromMemoryTTF(assets::OPEN_SANS_BOLD_TTF.data(), - static_cast(assets::OPEN_SANS_BOLD_TTF.size()), - DEFAULT_FONT_SIZE, - &cfg); - auto* italic = - io.Fonts->AddFontFromMemoryTTF(assets::OPEN_SANS_ITALIC_TTF.data(), - static_cast(assets::OPEN_SANS_ITALIC_TTF.size()), - DEFAULT_FONT_SIZE, - &cfg); - auto* bold_italic = - io.Fonts->AddFontFromMemoryTTF(assets::OPEN_SANS_BOLD_ITALIC_TTF.data(), - static_cast(assets::OPEN_SANS_BOLD_ITALIC_TTF.size()), - DEFAULT_FONT_SIZE, - &cfg); - - return {.regular = regular, .bold = bold, .italic = italic, .bold_italic = bold_italic}; -} - -void app_style::set_dark_theme(const log_fn_t& log) { - log_info(log, "Setting dark mode"); -#ifdef _WIN32 - HWND hwnd = reinterpret_cast(const_cast(sapp_win32_get_hwnd())); - BOOL use_dark_mode = TRUE; - DwmSetWindowAttribute(hwnd, 20, &use_dark_mode, sizeof(use_dark_mode)); -#endif - - // This is from my Game Engine - auto& colors = ImGui::GetStyle().Colors; - if (!color_cache) { initialize_imgui_color_cache(colors); } - refresh_imgui_color_cache(colors); - - colors[ImGuiCol_WindowBg] = {0.1F, 0.105F, 0.11F, 1.0F}; - - // Headers - colors[ImGuiCol_Header] = {0.2F, 0.205F, 0.21F, 1.0F}; - colors[ImGuiCol_HeaderHovered] = {0.3F, 0.305F, 0.31F, 1.0F}; - colors[ImGuiCol_HeaderActive] = {0.15F, 0.1505F, 0.151F, 1.0F}; - - // Buttons - colors[ImGuiCol_Button] = {0.2F, 0.205F, 0.21F, 1.0F}; - colors[ImGuiCol_ButtonHovered] = {0.3F, 0.305F, 0.31F, 1.0F}; - colors[ImGuiCol_ButtonActive] = {0.15F, 0.1505F, 0.151F, 1.0F}; - - // Frame BG - colors[ImGuiCol_FrameBg] = {0.2F, 0.205F, 0.21F, 1.0F}; - colors[ImGuiCol_FrameBgHovered] = {0.3F, 0.305F, 0.31F, 1.0F}; - colors[ImGuiCol_FrameBgActive] = {0.15F, 0.1505F, 0.151F, 1.0F}; - - // Tabs - colors[ImGuiCol_Tab] = {0.15F, 0.1505F, 0.151F, 1.0F}; - colors[ImGuiCol_TabHovered] = {0.38F, 0.3805F, 0.381F, 1.0F}; - colors[ImGuiCol_TabActive] = {0.28F, 0.2805F, 0.281F, 1.0F}; - colors[ImGuiCol_TabUnfocused] = {0.15F, 0.1505F, 0.151F, 1.0F}; - colors[ImGuiCol_TabUnfocusedActive] = {0.2F, 0.205F, 0.21F, 1.0F}; - - // Title - colors[ImGuiCol_TitleBg] = {0.15F, 0.1505F, 0.151F, 1.0F}; - colors[ImGuiCol_TitleBgActive] = {0.15F, 0.1505F, 0.151F, 1.0F}; - colors[ImGuiCol_TitleBgCollapsed] = {0.15F, 0.1505F, 0.151F, 1.0F}; - - dark_mode = true; -} - -void app_style::set_light_theme(const log_fn_t& log) { - log_info(log, "Setting light mode"); -#ifdef _WIN32 - HWND hwnd = reinterpret_cast(const_cast(sapp_win32_get_hwnd())); - BOOL use_dark_mode = FALSE; - DwmSetWindowAttribute(hwnd, 20, &use_dark_mode, sizeof(use_dark_mode)); -#endif - - // This isn't from my Game Engine - auto& colors = ImGui::GetStyle().Colors; - if (!color_cache) { initialize_imgui_color_cache(colors); } - refresh_imgui_color_cache(colors); - - colors[ImGuiCol_WindowBg] = {0.94F, 0.94F, 0.94F, 1.0F}; - colors[ImGuiCol_ChildBg] = {0.00F, 0.00F, 0.00F, 0.00F}; - colors[ImGuiCol_PopupBg] = {1.00F, 1.00F, 1.00F, 0.98F}; - - // Text - colors[ImGuiCol_Text] = {0.15F, 0.15F, 0.15F, 1.0F}; - colors[ImGuiCol_TextDisabled] = {0.50F, 0.50F, 0.50F, 1.00F}; - colors[ImGuiCol_InputTextCursor] = {0.15F, 0.15F, 0.15F, 1.0F}; - - // Headers - colors[ImGuiCol_Header] = {0.85F, 0.85F, 0.85F, 1.0F}; - colors[ImGuiCol_HeaderHovered] = {0.78F, 0.78F, 0.78F, 1.0F}; - colors[ImGuiCol_HeaderActive] = {0.70F, 0.70F, 0.70F, 1.0F}; - - // Buttons - colors[ImGuiCol_Button] = {0.85F, 0.85F, 0.85F, 1.0F}; - colors[ImGuiCol_ButtonHovered] = {0.75F, 0.75F, 0.75F, 1.0F}; - colors[ImGuiCol_ButtonActive] = {0.65F, 0.65F, 0.65F, 1.0F}; - - // Frame BG - colors[ImGuiCol_FrameBg] = {1.00F, 1.00F, 1.00F, 1.0F}; - colors[ImGuiCol_FrameBgHovered] = {0.90F, 0.90F, 0.95F, 1.0F}; - colors[ImGuiCol_FrameBgActive] = {0.85F, 0.85F, 0.90F, 1.0F}; - - // Tabs - colors[ImGuiCol_Tab] = {0.85F, 0.85F, 0.85F, 1.0F}; - colors[ImGuiCol_TabHovered] = {0.95F, 0.95F, 0.95F, 1.0F}; - colors[ImGuiCol_TabActive] = {0.94F, 0.94F, 0.94F, 1.0F}; - colors[ImGuiCol_TabUnfocused] = {0.80F, 0.80F, 0.80F, 1.0F}; - colors[ImGuiCol_TabUnfocusedActive] = {0.88F, 0.88F, 0.88F, 1.0F}; - - // Title - colors[ImGuiCol_TitleBg] = {0.90F, 0.90F, 0.90F, 1.0F}; - colors[ImGuiCol_TitleBgActive] = {0.90F, 0.90F, 0.90F, 1.0F}; - colors[ImGuiCol_TitleBgCollapsed] = {0.90F, 0.90F, 0.90F, 1.0F}; - colors[ImGuiCol_MenuBarBg] = {0.90F, 0.90F, 0.90F, 1.0F}; - - // Separators & Borders - colors[ImGuiCol_Separator] = {0.70F, 0.70F, 0.70F, 1.0F}; - colors[ImGuiCol_Border] = {0.70F, 0.70F, 0.70F, 0.5F}; - - dark_mode = false; -} - -} // namespace mbr diff --git a/src/esp32/backend.cpp b/src/esp32/backend.cpp index 0397b3b..e0a1ac9 100644 --- a/src/esp32/backend.cpp +++ b/src/esp32/backend.cpp @@ -67,7 +67,9 @@ void telemetry_backend::start() { web_sockets_.setOnMessageCallback([this](const ix::WebSocketMessagePtr& msg) { if (msg->type == ix::WebSocketMessageType::Open) { is_connected_ = true; + notify_connection_changed(true); is_receiving_ = false; + notify_receiving_changed(false); send_cmd("STATUS"); log_info(log_, "Connected to ESP32"); } @@ -76,6 +78,8 @@ void telemetry_backend::start() { msg->type == ix::WebSocketMessageType::Error) { is_connected_ = false; is_receiving_ = false; + notify_connection_changed(false); + notify_receiving_changed(false); } if (msg->type == ix::WebSocketMessageType::Close) { log_warn(log_, "WebSocket closed"); } @@ -86,6 +90,7 @@ void telemetry_backend::start() { if (msg->type == ix::WebSocketMessageType::Message) { is_receiving_ = true; + notify_receiving_changed(true); this->on_message(msg); last_data_time_ = std::chrono::steady_clock::now(); } @@ -109,6 +114,7 @@ void telemetry_backend::send_cmd(const std::string& text) { if (!info.success) { log_warn(log_, "Send failed:"); is_connected_ = false; + notify_connection_changed(false); return; } @@ -122,9 +128,13 @@ void telemetry_backend::send_cmd(const std::string& text) { void telemetry_backend::worker_loop() { while (!should_kill_) { std::this_thread::sleep_for(10ms); - if (web_sockets_.getReadyState() != ix::ReadyState::Open) { is_connected_ = false; } - if (std::chrono::steady_clock::now() - last_data_time_.load() > 500ms) { + if (web_sockets_.getReadyState() != ix::ReadyState::Open) { + is_connected_ = false; + notify_connection_changed(false); + } + if (std::chrono::steady_clock::now() - last_data_time_.load() > 250ms) { is_receiving_ = false; + notify_receiving_changed(false); } } } @@ -156,6 +166,7 @@ void telemetry_backend::on_message(const ix::WebSocketMessagePtr& msg) { for (const auto& [ident, value] : parsed.value()) { data_.write_data(std::string{ident}, std::string{value}); } + notify_data(data_.get_series()); data_.write_raw_line(line); for (const auto& [ident, value] : parsed.value()) { if (ident == "W") { @@ -328,78 +339,4 @@ void telemetry_backend::register_handlers() { }; } -/* -auto TelemetryBackend::HandleCommand( - stdx::option>> parsed) -> void { - if (!parsed || parsed->size() < 2) { - LOG_ERROR("Command Not Found"); - return; - } - auto response = parsed.value()[1]; - if (response.first == "SYNC") { - u64 micros = 0; - std::from_chars( - response.second.data(), response.second.data() + response.second.size(), micros); - LocalTime t{micros}; - LOG_INFO("Time successfully synced at: {}", t.String(false)); - } else if (response.first == "SD_START") { - if (response.second == "deadbeef") { - LOG_ERROR("SD Card Failed Initialization"); - } else { - LOG_INFO("SD Card Initialized At {}", response.second); - IsOpen = true; - } - } else if (response.first == "SD_WRITE") { - if (response.second == "1") { - LOG_INFO("SD Card Has Begun Writing"); - IsWriting = true; - } else if (response.second == "0") { - LOG_INFO("SD Card Has Stopped Writing"); - IsWriting = false; - } - } else if (response.first == "SD_CLOSE") { - if (response.second == "1") { - IsOpen = false; - LOG_INFO("SD Card Has Closed Succesfully"); - } else if (response.second == "0") { - LOG_INFO("SD Card Failed Close"); - } - } else if (parsed.value()[0].second == "1") { - LOG_ERROR("Command Not Found"); - } -} -for (const auto& field : m_PacketFields) { - const usize ident_start = pos; - while (pos < str.size() && str[pos] != ' ') { - pos += 1; - } - if (str.substr(ident_start, pos - ident_start) != field) { return stdx::none; } - - // There can be an arbitrary amount of spaces between idents/values - if (pos >= str.size() || str[pos] != ' ') { return stdx::none; } - while (pos < str.size() && str[pos] == ' ') { - pos += 1; - } - - const usize value_start = pos; - while (pos < str.size() && str[pos] != ' ') { - pos += 1; - } - if (value_start == pos) { return stdx::none; } - const auto value = str.substr(value_start, pos - value_start); - - // Uncalibrated packets or unparsable times are invalid - if (field == "T") { - u64 t = 0; - auto res = std::from_chars(value.data(), value.data() + value.size(), t); - if (res.ec != std::errc{} || t == 0) { return stdx::none; } - } - - parsed.emplace_back(field, value); - while (pos < str.size() && str[pos] == ' ') { - pos += 1; - } -} -*/ - } // namespace mbr diff --git a/src/esp32/data.cpp b/src/esp32/data.cpp index 00302ae..8c766ce 100644 --- a/src/esp32/data.cpp +++ b/src/esp32/data.cpp @@ -49,7 +49,7 @@ void telemetry_data::init_json(const std::filesystem::path& path) { } void telemetry_data::init_data() { - for (const auto& data : data_values) { series.try_emplace(data.key, std::vector{}); } + for (const auto& data : data_values) { series_.try_emplace(data.key, std::vector{}); } } void telemetry_data::write_data(const std::string& identifier, const std::string& value) { @@ -57,7 +57,7 @@ void telemetry_data::write_data(const std::string& identifier, const std::string try { // This cannot be from_chars since apple clang doesn't support it :( f64 val = std::stod(value); - series[identifier].emplace_back(val); + series_[identifier].emplace_back(val); if (identifier == "T") { u64 raw_micros = static_cast(val); time_no_normal_micros_.emplace_back(raw_micros); @@ -67,7 +67,7 @@ void telemetry_data::write_data(const std::string& identifier, const std::string } void telemetry_data::clear() { - for (auto& [key, values] : series) { values.clear(); } + for (auto& [key, values] : series_) { values.clear(); } time_.clear(); time_no_normal_micros_.clear(); raw_lines_.clear(); diff --git a/src/vendor/sokol_impl.cpp b/src/vendor/sokol_impl.cpp index a971196..e69de29 100644 --- a/src/vendor/sokol_impl.cpp +++ b/src/vendor/sokol_impl.cpp @@ -1,14 +0,0 @@ -#define SOKOL_APP_IMPL -#define SOKOL_GFX_IMPL -#define SOKOL_GLUE_IMPL -#define SOKOL_IMGUI_IMPL -#define SOKOL_LOG_IMPL -#define SOKOL_NO_ENTRY - -#include - -#include -#include -#include -#include -#include From 6b97e1e566a8269c08414c58b1315acae5347e2b Mon Sep 17 00:00:00 2001 From: Blake Watson Date: Mon, 27 Jul 2026 18:52:04 -0400 Subject: [PATCH 06/36] only qt and ffmpeg are dynamically linked now, maybe updated for mac building and compilation, linux you are out of luck --- cmake/CompilerFlags.cmake | 2 +- cmake/MbrDaqSuite.cmake | 18 +++++++++++++++++- cmake/VendorSetup.cmake | 30 ++++++++++++++++++++++++++---- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/cmake/CompilerFlags.cmake b/cmake/CompilerFlags.cmake index 74977af..a50c3ab 100644 --- a/cmake/CompilerFlags.cmake +++ b/cmake/CompilerFlags.cmake @@ -8,7 +8,7 @@ elseif(WIN32) endif() if(MINGW) - add_link_options(-fuse-ld=lld) + add_link_options("-static-libgcc" "-static-libstdc++") endif() set(CMAKE_CXX_STANDARD 23) diff --git a/cmake/MbrDaqSuite.cmake b/cmake/MbrDaqSuite.cmake index 77d0eb7..dc48288 100644 --- a/cmake/MbrDaqSuite.cmake +++ b/cmake/MbrDaqSuite.cmake @@ -83,6 +83,14 @@ function(create_standard_target EXTRA_FLAGS TARGET_NAME) if (NOT MINGW) target_link_options(mbrdaq_${TARGET_NAME} PRIVATE "LINKER:/ENTRY:mainCRTStartup") endif() + elseif(APPLE) + add_executable(mbrdaq_${TARGET_NAME} MACOSX_BUNDLE src/main.cpp) + set_target_properties(mbrdaq_${TARGET_NAME} PROPERTIES + MACOSX_BUNDLE_BUNDLE_NAME ${PROJECT_NAME} + MACOSX_BUNDLE_GUI_IDENTIFIER "com.mbr.daq.${TARGET_NAME}" + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION} + ) else() add_executable(mbrdaq_${TARGET_NAME} src/main.cpp) endif() @@ -125,5 +133,13 @@ function(create_standard_target EXTRA_FLAGS TARGET_NAME) COMMENT "Deploy Qt runtime libraries" ) endif() + elseif(APPLE) + if(MACDEPLOYQT_EXE) + add_custom_command(TARGET mbrdaq_${TARGET_NAME} POST_BUILD + COMMAND ${MACDEPLOYQT_EXE} + "$" + COMMENT "Deploy Qt frameworks into app bundle" + ) + endif() endif() -endfunction() +endfunction() \ No newline at end of file diff --git a/cmake/VendorSetup.cmake b/cmake/VendorSetup.cmake index dff325b..f36c0b5 100644 --- a/cmake/VendorSetup.cmake +++ b/cmake/VendorSetup.cmake @@ -159,10 +159,32 @@ add_vendor_subdirectory(vendor/stdx) # Qt if(WIN32) list(APPEND CMAKE_PREFIX_PATH "C:/msys64/ucrt64") +elseif(APPLE) + # Homebrew Qt6 install location differs by CPU architecture + execute_process( + COMMAND brew --prefix qt + OUTPUT_VARIABLE HOMEBREW_QT_PREFIX + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) + if(HOMEBREW_QT_PREFIX) + list(APPEND CMAKE_PREFIX_PATH "${HOMEBREW_QT_PREFIX}") + else() + # Fallback if `brew` isn't on PATH (e.g. CI) - covers both arches + list(APPEND CMAKE_PREFIX_PATH "/opt/homebrew/opt/qt" "/usr/local/opt/qt") + endif() endif() find_package(Qt6 REQUIRED COMPONENTS Core Widgets) -find_program(WINDEPLOYQT_EXE windeployqt - HINTS "${Qt6_DIR}/../../../bin" "${Qt6_DIR}/../../../share/qt6/bin" -) -message(STATUS "windeployqt found: ${WINDEPLOYQT_EXE}") + +if(WIN32) + find_program(WINDEPLOYQT_EXE windeployqt + HINTS "${Qt6_DIR}/../../../bin" "${Qt6_DIR}/../../../share/qt6/bin" + ) + message(STATUS "windeployqt found: ${WINDEPLOYQT_EXE}") +elseif(APPLE) + find_program(MACDEPLOYQT_EXE macdeployqt + HINTS "${Qt6_DIR}/../../../bin" + ) + message(STATUS "macdeployqt found: ${MACDEPLOYQT_EXE}") +endif() From e106b9986caef8dc9b3157ffb01625e0c291da04 Mon Sep 17 00:00:00 2001 From: Blake Watson Date: Mon, 27 Jul 2026 22:48:53 -0400 Subject: [PATCH 07/36] wifi works again and the brdige works! --- include/esp32/backend.hpp | 3 +-- src/app/app.cpp | 1 + src/app/gui.cpp | 2 +- src/esp32/backend.cpp | 7 ++++++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/include/esp32/backend.hpp b/include/esp32/backend.hpp index eb754f3..1e1ffd2 100644 --- a/include/esp32/backend.hpp +++ b/include/esp32/backend.hpp @@ -71,7 +71,6 @@ class telemetry_backend { [[nodiscard]] const telemetry_data& get_data() const noexcept { return data_; } [[nodiscard]] serial_manager_t& get_serial_manager() noexcept { return serial_manager_; } - [[nodiscard]] std::atomic& get_try_connection() noexcept { return try_connection_; } [[nodiscard]] bool is_connected() const noexcept { return is_connected_.load(std::memory_order_relaxed); @@ -91,6 +90,7 @@ class telemetry_backend { stdx::option>> validate_packet(std::string_view str) const; + void restart(); private: void worker_loop(); @@ -104,7 +104,6 @@ class telemetry_backend { rw_latch data_latch_; telemetry_data data_; serial_manager_t serial_manager_; - std::atomic try_connection_{false}; std::atomic is_connected_{false}; std::atomic is_logging_{false}; std::atomic is_receiving_{false}; diff --git a/src/app/app.cpp b/src/app/app.cpp index ab2348f..6b03d1f 100644 --- a/src/app/app.cpp +++ b/src/app/app.cpp @@ -25,6 +25,7 @@ app_t::app_t(i32 argc, char** argv) : qt_app_{std::make_unique(arg ix::initNetSystem(); context_->backend = std::make_unique(DEFAULT_JSON_PATH, context_->log); gui_ = std::make_unique(context_); + context_->backend->start(); } app_t::~app_t() { ix::uninitNetSystem(); } diff --git a/src/app/gui.cpp b/src/app/gui.cpp index a69378f..f92c8db 100644 --- a/src/app/gui.cpp +++ b/src/app/gui.cpp @@ -98,7 +98,7 @@ namespace mbr { context_->backend->send_cmd(std::format("CMD SYNC {}", sync_time)); }); - menuBar()->addAction("Restart Connection", this, [this] { context_->backend->get_try_connection() = true; }); + menuBar()->addAction("Restart Connection", this, [this] { context_->backend->restart(); }); menuBar()->addAction("Clear Data", this, [this] { context_->backend->get_data().clear(); }); diff --git a/src/esp32/backend.cpp b/src/esp32/backend.cpp index e0a1ac9..d065411 100644 --- a/src/esp32/backend.cpp +++ b/src/esp32/backend.cpp @@ -108,6 +108,12 @@ void telemetry_backend::kill() { } } +void telemetry_backend::restart() { + kill(); + should_kill_ = false; + start(); +} + void telemetry_backend::send_cmd(const std::string& text) { if (web_sockets_.getReadyState() == ix::ReadyState::Open) { const ix::WebSocketSendInfo info = web_sockets_.send(text); @@ -244,7 +250,6 @@ void telemetry_backend::set_ip(const ipv4_t& ipv4) { kill(); should_kill_ = false; start(); - try_connection_ = false; } namespace { From 5cd393d4c2d8fc674cc70696e105f052bfbc0c24 Mon Sep 17 00:00:00 2001 From: Blake Date: Tue, 28 Jul 2026 12:44:41 -0400 Subject: [PATCH 08/36] working on plotting, everything but the live update signals should be working. Not a huge fan of the current design but I should be able to change it by tweaking the style --- cmake/MbrDaqSuite.cmake | 15 +-- cmake/VendorSetup.cmake | 29 ++++- include/app/pages/analysis.hpp | 2 +- include/app/pages/home.hpp | 5 +- include/app/pages/page.hpp | 2 +- include/app/pages/plot.hpp | 33 +++-- include/app/pages/serialmon.hpp | 2 +- include/app/pages/settings.hpp | 2 +- src/app/gui.cpp | 42 +++++- src/app/pages/analysis.cpp | 2 +- src/app/pages/home.cpp | 12 +- src/app/pages/plot.cpp | 221 +++++++++++++++++++++++++++++++- src/app/pages/serialmon.cpp | 2 +- src/app/pages/settings.cpp | 2 +- 14 files changed, 336 insertions(+), 35 deletions(-) diff --git a/cmake/MbrDaqSuite.cmake b/cmake/MbrDaqSuite.cmake index dc48288..6c18dde 100644 --- a/cmake/MbrDaqSuite.cmake +++ b/cmake/MbrDaqSuite.cmake @@ -21,6 +21,9 @@ function(create_mbrdaq_suite TARGET_NAME EXTRA_FLAGS OUT_DIR) stdx Qt6::Core Qt6::Widgets + Qt6::Charts + Qt6::PrintSupport + QCustomPlot ) target_include_directories(mbrdaq_core_${TARGET_NAME} PUBLIC include) @@ -114,16 +117,6 @@ function(create_standard_target EXTRA_FLAGS TARGET_NAME) COMMENT "Copy FFMPEG binaries to output directory" ) - # Automatically copy MSYS2 GCC/MinGW compiler runtimes - add_custom_command(TARGET mbrdaq_${TARGET_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "C:/msys64/ucrt64/bin/libstdc++-6.dll" - "C:/msys64/ucrt64/bin/libgcc_s_seh-1.dll" - "C:/msys64/ucrt64/bin/libwinpthread-1.dll" - "$" - COMMENT "Copying MinGW runtime DLLs" - ) - if(WINDEPLOYQT_EXE) add_custom_command(TARGET mbrdaq_${TARGET_NAME} POST_BUILD COMMAND ${WINDEPLOYQT_EXE} @@ -142,4 +135,4 @@ function(create_standard_target EXTRA_FLAGS TARGET_NAME) ) endif() endif() -endfunction() \ No newline at end of file +endfunction() diff --git a/cmake/VendorSetup.cmake b/cmake/VendorSetup.cmake index f36c0b5..2d147ef 100644 --- a/cmake/VendorSetup.cmake +++ b/cmake/VendorSetup.cmake @@ -175,7 +175,7 @@ elseif(APPLE) endif() endif() -find_package(Qt6 REQUIRED COMPONENTS Core Widgets) +find_package(Qt6 REQUIRED COMPONENTS Core Widgets Charts PrintSupport) if(WIN32) find_program(WINDEPLOYQT_EXE windeployqt @@ -188,3 +188,30 @@ elseif(APPLE) ) message(STATUS "macdeployqt found: ${MACDEPLOYQT_EXE}") endif() + +# QCustomPlot +include(FetchContent) + +FetchContent_Declare( + qcustomplot + URL https://www.qcustomplot.com/release/2.1.1/QCustomPlot-source.tar.gz + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + SOURCE_SUBDIR "no-cmake" +) + +FetchContent_MakeAvailable(qcustomplot) + +add_library(QCustomPlot STATIC + ${qcustomplot_SOURCE_DIR}/qcustomplot.cpp + ${qcustomplot_SOURCE_DIR}/qcustomplot.h +) + +set_target_properties(QCustomPlot PROPERTIES + AUTOMOC ON + AUTOUIC OFF + AUTORCC OFF +) + +target_include_directories(QCustomPlot PUBLIC ${qcustomplot_SOURCE_DIR}) +target_compile_options(QCustomPlot PRIVATE ${WARNING_IGNORE}) +target_link_libraries(QCustomPlot PUBLIC Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::PrintSupport) diff --git a/include/app/pages/analysis.hpp b/include/app/pages/analysis.hpp index 89fdcb6..394e2cd 100644 --- a/include/app/pages/analysis.hpp +++ b/include/app/pages/analysis.hpp @@ -40,7 +40,7 @@ class analysis_page : public page { void on_enter() override; void on_exit() override; - void update() override; + void build_page() override; private: diff --git a/include/app/pages/home.hpp b/include/app/pages/home.hpp index b9e1806..8cce782 100644 --- a/include/app/pages/home.hpp +++ b/include/app/pages/home.hpp @@ -18,10 +18,11 @@ class home_page : public page { void on_enter() override; void on_exit() override; - void update() override; + void build_page() override; private: - + void build_rhs(); + void build_lhs(); private: ipv4_t previous_ip_; diff --git a/include/app/pages/page.hpp b/include/app/pages/page.hpp index aca1854..3a8e0fc 100644 --- a/include/app/pages/page.hpp +++ b/include/app/pages/page.hpp @@ -16,7 +16,7 @@ class page : public QWidget{ virtual void on_enter() = 0; virtual void on_exit() = 0; - virtual void update() = 0; + virtual void build_page() = 0; protected: explicit page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : QWidget(parent), context_{ctx} {} diff --git a/include/app/pages/plot.hpp b/include/app/pages/plot.hpp index 32d2ef4..007e9cc 100644 --- a/include/app/pages/plot.hpp +++ b/include/app/pages/plot.hpp @@ -1,33 +1,48 @@ #pragma once #include +#include +#include +#include +#include #include #include #include #include +#include +#include +#include +#include +#include +#include +#include #include "app/pages/page.hpp" +#include "esp32/data.hpp" +#include "esp32/data.hpp" namespace mbr::pages { class plot_page : public page { public: - explicit plot_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : page{ctx, parent} {} + explicit plot_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : page{ctx, parent} { + build_page(); + } ~plot_page() override = default; void on_enter() override; void on_exit() override; - void update() override; - - void draw_lhs(const std::vector& raw_lines); - void draw_rhs(gsl::span time, - gsl::span fr, - gsl::span fl, - gsl::span br, - gsl::span bl); + void build_page() override; private: + QWidget* build_rhs(); + QWidget* build_lhs(); + QToolButton* create_tree_dropdown(const std::vector& data, QWidget* parent); + void plot_signal(const std::string key); + QPlainTextEdit* text_log_; + QCustomPlot* plot_; + ankerl::unordered_dense::map active_graphs_; std::string download_fd_text_; }; diff --git a/include/app/pages/serialmon.hpp b/include/app/pages/serialmon.hpp index f5dff1b..08aafe6 100644 --- a/include/app/pages/serialmon.hpp +++ b/include/app/pages/serialmon.hpp @@ -17,7 +17,7 @@ class serial_page : public page { void on_enter() override; void on_exit() override; - void update() override; + void build_page() override; private: diff --git a/include/app/pages/settings.hpp b/include/app/pages/settings.hpp index 36b3eca..e1f5b9d 100644 --- a/include/app/pages/settings.hpp +++ b/include/app/pages/settings.hpp @@ -18,7 +18,7 @@ class settings_page : public page { void on_enter() override; void on_exit() override; - void update() override; + void build_page() override; private: diff --git a/src/app/gui.cpp b/src/app/gui.cpp index f92c8db..67e2af4 100644 --- a/src/app/gui.cpp +++ b/src/app/gui.cpp @@ -22,6 +22,7 @@ #include #include "app/assets/images/app_icon.hpp" +#include "app/backend_bridge.hpp" #include "app/pages/page.hpp" #include "app/context.hpp" #include "app/pages/home.hpp" @@ -66,27 +67,64 @@ namespace mbr { } void gui_t::build_menu_bar() { - menuBar()->setStyleSheet(R"( + static const QString kMenuBarStyle = R"( QMenuBar { background-color: #2b2b2b; color: white; font-size: 20px; spacing: 2px; + border-bottom: 1px solid #3C3F41; } QMenuBar::item { background-color: transparent; padding: 4px 6px; border-radius: 4px; + border: 1px solid transparent; } QMenuBar::item:selected { background-color: #3C3F41; + border: 1px solid #5A5D5F; } - )"); + )"; + + static const QString kMenuStyle = R"( + QMenu { + background-color: #2b2b2b; + color: white; + font-size: 20px; + border: 1px solid #3C3F41; + padding: 4px; + } + QMenu::item { + background-color: transparent; + padding: 4px 24px 4px 12px; + border-radius: 4px; + border: 1px solid transparent; + } + QMenu::item:selected { + background-color: #3C3F41; + border: 1px solid #5A5D5F; + } + QMenu::item:disabled { + color: #6e6e6e; + } + QMenu::separator { + height: 1px; + background-color: #3C3F41; + margin: 4px 6px; + } + )"; + + menuBar()->setStyleSheet(kMenuBarStyle); + auto* file = menuBar()->addMenu("File"); + file->setStyleSheet(kMenuStyle); file->addAction("Settings"); file->addAction("Export Log"); file->addAction("Exit", this, [] { QApplication::quit(); }); + auto* nav = menuBar()->addMenu("Navigation"); + nav->setStyleSheet(kMenuStyle); nav->addAction("Home", this, [this] { change_page(page_type_t::HOME); }); nav->addAction("Plot", this, [this] { change_page(page_type_t::PLOT); }); nav->addAction("Serial", this, [this] { change_page(page_type_t::SERIAL); }); diff --git a/src/app/pages/analysis.cpp b/src/app/pages/analysis.cpp index 5d74974..9c2dd7a 100644 --- a/src/app/pages/analysis.cpp +++ b/src/app/pages/analysis.cpp @@ -34,7 +34,7 @@ namespace mbr::pages { void analysis_page::on_enter() { log_info(context_->log, "Entered AnalysisPage"); } void analysis_page::on_exit() { log_info(context_->log, "Exited AnalysisPage"); } - void analysis_page::update() { + void analysis_page::build_page() { PROFILE_FUNCTION(); } } // namespace mbr::pages diff --git a/src/app/pages/home.cpp b/src/app/pages/home.cpp index 6840a2c..511e5f4 100644 --- a/src/app/pages/home.cpp +++ b/src/app/pages/home.cpp @@ -16,7 +16,17 @@ namespace mbr::pages { void home_page::on_enter() { log_info(context_->log, "Entered HomePage"); } void home_page::on_exit() { log_info(context_->log, "Exited HomePage"); } -void home_page::update() { +void home_page::build_page() { PROFILE_FUNCTION(); + build_rhs(); + build_lhs(); +} + +void home_page::build_rhs() { + +} + +void home_page::build_lhs() { + } } // namespace mbr::pages diff --git a/src/app/pages/plot.cpp b/src/app/pages/plot.cpp index 2071f48..923d903 100644 --- a/src/app/pages/plot.cpp +++ b/src/app/pages/plot.cpp @@ -1,6 +1,8 @@ #include "app/pages/plot.hpp" -#include +#include +#include +#include #include #include @@ -10,14 +12,229 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include "core/log.hpp" +#include "esp32/backend.hpp" +#include "esp32/data.hpp" +#include "qcustomplot.h" namespace mbr::pages { void plot_page::on_enter() { log_info(context_->log, "Entered PlotPage"); } void plot_page::on_exit() { log_info(context_->log, "Exited PlotPage"); } -void plot_page::update() { +void plot_page::build_page() { + auto* layout = new QHBoxLayout(this); + layout->setContentsMargins(8, 8, 8, 8); + layout->setSpacing(8); + + auto* lhs = build_lhs(); + auto* rhs = build_rhs(); + + layout->addWidget(lhs, 1); + layout->addWidget(rhs, 3); +} + +QWidget* plot_page::build_lhs() { + auto* container = new QWidget(this); + auto* layout = new QVBoxLayout(container); + + text_log_ = new QPlainTextEdit(container); + text_log_->setReadOnly(true); + layout->addWidget(text_log_); + return container; +} + +QWidget* plot_page::build_rhs() { + + auto* container = new QWidget(this); + auto* layout = new QVBoxLayout(container); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(8); + + QToolButton* channel_selector = create_tree_dropdown(context_->backend->get_data().data_values, container); + + layout->addWidget(channel_selector); + + plot_ = new QCustomPlot(container); + + const QColor bg_color("#1e1e1e"); + const QColor axis_color("#a0a0a0"); + const QColor grid_color("#2d2d2d"); + const QColor line_color("#00adb5"); + + plot_->setBackground(QBrush(bg_color)); + plot_->axisRect()->setBackground(QBrush(bg_color)); + + // Graph setup & trace styling + QCPGraph* graph = plot_->addGraph(); + + + QPen pen(line_color); + pen.setWidthF(2.0); + graph->setPen(pen); + + // Axes & gridline styling + auto configure_axis = [&](QCPAxis* axis, const QString& label) { + axis->setLabel(label); + axis->setBasePen(QPen(axis_color, 1.0)); + axis->setTickPen(QPen(axis_color, 1.0)); + axis->setSubTickPen(QPen(axis_color, 0.5)); + axis->setTickLabelColor(axis_color); + axis->setLabelColor(axis_color); + + // Subtle dark gridlines + axis->grid()->setPen(QPen(grid_color, 1.0, Qt::SolidLine)); + axis->grid()->setSubGridPen(QPen(grid_color, 0.5, Qt::DotLine)); + axis->grid()->setSubGridVisible(true); + }; + + configure_axis(plot_->xAxis, "Time (s)"); + configure_axis(plot_->yAxis, "Value"); + + // Padding, scaling & interactions + plot_->axisRect()->setAutoMargins(QCP::msNone); + plot_->axisRect()->setMargins(QMargins(50, 20, 20, 40)); + plot_->rescaleAxes(); + plot_->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom); + plot_->replot(); + + layout->addWidget(plot_, 1); + + QTreeWidget* tree = channel_selector->findChild(); + + if (tree) { + connect(tree, &QTreeWidget::itemSelectionChanged, this, [this, tree]() { + plot_->clearGraphs(); + active_graphs_.clear(); + for (QTreeWidgetItem* item : tree->selectedItems()) { + QString key = item->data(0, Qt::UserRole).toString(); + if (!key.isEmpty()) { + plot_signal(key.toStdString()); + } + + if (item->childCount() > 0) { + for (int i = 0; i < item->childCount(); ++i) { + QTreeWidgetItem* child = item->child(i); + std::string child_key = child->data(0, Qt::UserRole).toString().toStdString(); + if (!child_key.empty()) { + plot_signal(child_key); + } + } + } + } + }); + } + + return container; +} + +QToolButton* plot_page::create_tree_dropdown(const std::vector& data, QWidget* parent) { + auto* select = new QToolButton(parent); + + select->setText("Select Data"); + select->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + select->setPopupMode(QToolButton::InstantPopup); + select->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + select->setStyleSheet( + "QToolButton {" + " background-color: #1e1e1e; color: #d4d4d4; border: 1px solid #2d2d2d;" + " border-radius: 4px; padding: 6px 12px; text-align: left;" + "}" + "QToolButton:hover { background-color: #2a2a2a; }" + ); + + select->setPopupMode(QToolButton::InstantPopup); + select->setStyleSheet( + "QPushButton {" + " background-color: #1e1e1e; color: #d4d4d4; border: 1px solid #2d2d2d;" + " border-radius: 4px; padding: 6px 12px; text-align: left;" + "}" + "QPushButton::menu-indicator { image: none; }" + ); + + auto* tree = new QTreeWidget(select); + tree->setSelectionMode(QAbstractItemView::ExtendedSelection); + tree->setHeaderHidden(true); + tree->setColumnCount(1); + tree->setStyleSheet( + "QTreeWidget { background-color: #1e1e1e; color: #d4d4d4; border: 1px solid #2d2d2d; }" + "QTreeWidget::item:hover { background-color: #00adb5; color: white; }" + ); + + for (auto it : data) { + bool found = false; + QTreeWidgetItem* target_group = nullptr; + for (int i = 0; i < tree->topLevelItemCount(); ++i) { + if (QString::fromStdString(it.group) == tree->topLevelItem(i)->text(0)) { + found = true; + target_group = tree->topLevelItem(i); + break; + } + } + if (!found && it.plot) { + target_group = new QTreeWidgetItem; + QString group_name = QString::fromStdString(it.group); + target_group->setText(0, group_name); + tree->addTopLevelItem(target_group); + } + auto* child_item = new QTreeWidgetItem(target_group); + child_item->setText(0, QString::fromStdString(it.name)); + child_item->setData(0, Qt::UserRole, QString::fromStdString(it.key)); + } + tree->expandAll(); + + auto* menu = new QMenu(select); + auto* widget_action = new QWidgetAction(menu); + + tree->setMinimumSize(300, 200); + widget_action->setDefaultWidget(tree); + menu->addAction(widget_action); + + select->setMenu(menu); + + return select; +} + + + +void plot_page::plot_signal(const std::string key) { + if (active_graphs_.contains(key)) { + return; + } + auto series = context_->backend->get_data().get_series(key); + auto time = context_->backend->get_data().get_time(); + + QVector x_times(time.begin(), time.end()); + QVector y_values(series.begin(), series.end()); + QCPGraph* graph = plot_->addGraph(); + + static const std::vector palette = { + QColor("#00adb5"), QColor("#ff5722"), QColor("#e91e63"), + QColor("#9c27b0"), QColor("#4caf50"), QColor("#ffeb3b") + }; + QColor color = palette[active_graphs_.size() % palette.size()]; + + graph->setPen(QPen(color, 2.0)); + graph->setData(x_times, y_values); + graph->setName(QString::fromStdString(key)); + + active_graphs_[key] = graph; + + plot_->rescaleAxes(); + plot_->replot(); } } // namespace mbr::pages diff --git a/src/app/pages/serialmon.cpp b/src/app/pages/serialmon.cpp index b447da0..1627fc8 100644 --- a/src/app/pages/serialmon.cpp +++ b/src/app/pages/serialmon.cpp @@ -15,7 +15,7 @@ namespace mbr::pages { void serial_page::on_enter() { log_info(context_->log, "Entered SerialPage"); } void serial_page::on_exit() { log_info(context_->log, "Exited SerialPage"); } -void serial_page::update() { +void serial_page::build_page() { PROFILE_FUNCTION(); } diff --git a/src/app/pages/settings.cpp b/src/app/pages/settings.cpp index 918688c..4885f50 100644 --- a/src/app/pages/settings.cpp +++ b/src/app/pages/settings.cpp @@ -17,7 +17,7 @@ namespace mbr::pages { void settings_page::on_enter() { log_info(context_->log, "Entered SettingsPage"); } void settings_page::on_exit() { log_info(context_->log, "Exited SettingsPage"); } -void settings_page::update() { +void settings_page::build_page() { } } // namespace mbr::pages From 76d546a431c9c31e42a650a5266f1627af8d0118 Mon Sep 17 00:00:00 2001 From: Blake Date: Tue, 28 Jul 2026 13:09:20 -0400 Subject: [PATCH 09/36] solving problems on pr --- include/app/gui.hpp | 4 +++- src/app/gui.cpp | 5 +++-- src/main.cpp | 2 ++ src/vendor/sokol_impl.cpp | 0 4 files changed, 8 insertions(+), 3 deletions(-) delete mode 100644 src/vendor/sokol_impl.cpp diff --git a/include/app/gui.hpp b/include/app/gui.hpp index 7105613..2c2c239 100644 --- a/include/app/gui.hpp +++ b/include/app/gui.hpp @@ -8,6 +8,8 @@ #include "app/context.hpp" #include "app/pages/page.hpp" #include "app/backend_bridge.hpp" +#include "stdx/fixed/enum_map.hh" +#include #include #include #include @@ -30,7 +32,7 @@ class gui_t : public QMainWindow { backend_bridge bridge_; QStackedWidget* pages_; std::shared_ptr context_; - ankerl::unordered_dense::map page_lookup_; + stdx::fixed::enum_map page_lookup_; QLabel* connection_status_; QLabel* status_dot_ = nullptr; bool is_connected_ = false; diff --git a/src/app/gui.cpp b/src/app/gui.cpp index 67e2af4..2c801c0 100644 --- a/src/app/gui.cpp +++ b/src/app/gui.cpp @@ -32,6 +32,7 @@ #include "app/pages/settings.hpp" #include "core/time.hpp" #include "esp32/backend.hpp" +#include "stdx/enum.hh" using namespace std::chrono; @@ -40,8 +41,8 @@ namespace mbr { gui_t::gui_t(const std::shared_ptr& ctx) : bridge_(ctx->backend.get(), this), context_(ctx) { pages_ = new QStackedWidget(this); setCentralWidget(pages_); - for (auto type : {page_type_t::HOME, page_type_t::PLOT, page_type_t::ANALYSIS, page_type_t::SERIAL}) { - pages::page* p = create_page(type, context_, pages_); + for (auto type : stdx::enum_range()) { + auto* p = create_page(type, context_, pages_); pages_->addWidget(p); page_lookup_[type] = p; } diff --git a/src/main.cpp b/src/main.cpp index 7ac15be..33e324a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,8 @@ #include "app/app.hpp" +#include int main(int argc, char* argv[]) { + stdx::profiler profiler{argv[0]}; mbr::app_t app{argc, argv}; app.run(); } diff --git a/src/vendor/sokol_impl.cpp b/src/vendor/sokol_impl.cpp deleted file mode 100644 index e69de29..0000000 From 743ad36cf3714737f229b57fae8b3c3dac65c5c7 Mon Sep 17 00:00:00 2001 From: Blake Date: Tue, 28 Jul 2026 16:22:12 -0400 Subject: [PATCH 10/36] Plot page (almost) finished!!! Everything should be up to date with the ImGui app feature wise. Desperately need to make the standard theme style pages --- include/app/context.hpp | 2 + include/app/gui.hpp | 3 +- include/app/pages/plot.hpp | 25 ++++- src/app/app.cpp | 2 + src/app/gui.cpp | 4 +- src/app/pages/plot.cpp | 211 ++++++++++++++++++++++++++++++++----- 6 files changed, 212 insertions(+), 35 deletions(-) diff --git a/include/app/context.hpp b/include/app/context.hpp index 718726c..350a084 100644 --- a/include/app/context.hpp +++ b/include/app/context.hpp @@ -9,6 +9,7 @@ #include "core/log.hpp" #include "esp32/backend.hpp" +#include "app/backend_bridge.hpp" namespace mbr { @@ -40,6 +41,7 @@ struct app_context { bool is_cmd_input_focused{false}; std::unique_ptr backend; + std::unique_ptr bridge; std::string username; std::string password; log_fn_t log; diff --git a/include/app/gui.hpp b/include/app/gui.hpp index 2c2c239..2eb433c 100644 --- a/include/app/gui.hpp +++ b/include/app/gui.hpp @@ -7,7 +7,6 @@ #include #include "app/context.hpp" #include "app/pages/page.hpp" -#include "app/backend_bridge.hpp" #include "stdx/fixed/enum_map.hh" #include #include @@ -29,7 +28,7 @@ class gui_t : public QMainWindow { [[nodiscard]] pages::page* create_page(page_type_t type, const std::shared_ptr& ctx, QWidget* parent); private: - backend_bridge bridge_; + QStackedWidget* pages_; std::shared_ptr context_; stdx::fixed::enum_map page_lookup_; diff --git a/include/app/pages/plot.hpp b/include/app/pages/plot.hpp index 007e9cc..c0d163f 100644 --- a/include/app/pages/plot.hpp +++ b/include/app/pages/plot.hpp @@ -1,14 +1,17 @@ #pragma once #include -#include -#include -#include -#include #include +#include #include +#include #include + +#include +#include +#include +#include #include #include #include @@ -17,6 +20,7 @@ #include #include #include +#include #include "app/pages/page.hpp" #include "esp32/data.hpp" @@ -40,10 +44,21 @@ class plot_page : public page { QWidget* build_lhs(); QToolButton* create_tree_dropdown(const std::vector& data, QWidget* parent); void plot_signal(const std::string key); + void plot_timer(); + void update_plot(); + void update_stream(); + + private: QPlainTextEdit* text_log_; QCustomPlot* plot_; ankerl::unordered_dense::map active_graphs_; - std::string download_fd_text_; + + ankerl::unordered_dense::map plotted_counts_; + bool pending_update_ = false; + QTimer* redraw_timer_ = nullptr; + + private slots: + void on_data_updated(); }; } // namespace mbr::pages diff --git a/src/app/app.cpp b/src/app/app.cpp index 6b03d1f..fa481d2 100644 --- a/src/app/app.cpp +++ b/src/app/app.cpp @@ -15,6 +15,7 @@ #include "core/log.hpp" #include "esp32/backend.hpp" #include "esp32/data.hpp" +#include "app/backend_bridge.hpp" namespace mbr { @@ -24,6 +25,7 @@ app_t::app_t(i32 argc, char** argv) : qt_app_{std::make_unique(arg context_->log = context_->logger.get_log_fn(); ix::initNetSystem(); context_->backend = std::make_unique(DEFAULT_JSON_PATH, context_->log); + context_->bridge = std::make_unique(context_->backend.get()); gui_ = std::make_unique(context_); context_->backend->start(); } diff --git a/src/app/gui.cpp b/src/app/gui.cpp index 2c801c0..f763da8 100644 --- a/src/app/gui.cpp +++ b/src/app/gui.cpp @@ -38,7 +38,7 @@ using namespace std::chrono; namespace mbr { - gui_t::gui_t(const std::shared_ptr& ctx) : bridge_(ctx->backend.get(), this), context_(ctx) { + gui_t::gui_t(const std::shared_ptr& ctx) : context_(ctx) { pages_ = new QStackedWidget(this); setCentralWidget(pages_); for (auto type : stdx::enum_range()) { @@ -149,7 +149,7 @@ namespace mbr { connection_status_ = new QLabel("Disconnected", rightWidget); - connect(&bridge_, &backend_bridge::connection_changed, this, [this](bool connected) { + connect(context_->bridge.get(), &backend_bridge::connection_changed, this, [this](bool connected) { is_connected_ = connected; if (!connected) is_receiving_ = false; connection_status_->setText(connected ? "Connected" : "Disconnected"); diff --git a/src/app/pages/plot.cpp b/src/app/pages/plot.cpp index 923d903..d94481b 100644 --- a/src/app/pages/plot.cpp +++ b/src/app/pages/plot.cpp @@ -1,10 +1,14 @@ #include "app/pages/plot.hpp" +#include +#include +#include #include #include #include #include #include +#include #include #include @@ -23,11 +27,15 @@ #include #include #include +#include +#include #include "core/log.hpp" #include "esp32/backend.hpp" #include "esp32/data.hpp" -#include "qcustomplot.h" +#include "app/backend_bridge.hpp" + +#include namespace mbr::pages { @@ -42,17 +50,83 @@ void plot_page::build_page() { auto* lhs = build_lhs(); auto* rhs = build_rhs(); + plot_timer(); + layout->addWidget(lhs, 1); layout->addWidget(rhs, 3); } QWidget* plot_page::build_lhs() { + QString button_style = + "QToolButton {" + " background-color: #1e1e1e; color: #d4d4d4; border: 1px solid #2d2d2d;" + " border-radius: 4px; padding: 4px 16px; font-size: 16px;" + "}" + "QToolButton:hover { background-color: #2a2a2a; }"; auto* container = new QWidget(this); - auto* layout = new QVBoxLayout(container); + auto* v_layout = new QVBoxLayout(container); + auto* h_layout = new QHBoxLayout(); + + auto* log_button = new QToolButton(); + auto* log_action = new QAction(context_->backend->is_logging() ? "Stop Logging" : "Start Logging", this); + connect(log_action, &QAction::triggered, this, [this, log_action] { + context_->backend->set_logging(!context_->backend->is_logging()); + log_action->setText(context_->backend->is_logging() ? "Stop Logging" : "Start Logging"); + }); + log_button->setDefaultAction(log_action); + log_button->setMinimumHeight(32); + log_button->setStyleSheet(button_style); + h_layout->addWidget(log_button); + + auto* download_name_input = new QLineEdit(); + download_name_input->setPlaceholderText("current_time.txt"); + download_name_input->setMaximumWidth(160); + download_name_input->setMinimumHeight(32); + h_layout->addWidget(download_name_input); + + auto* download_button = new QToolButton(); + auto* download_action = new QAction("Download Data", this); + connect(download_action, &QAction::triggered, this, [this, download_name_input] { + std::string name = download_name_input->text().toStdString(); + + local_time lt; + std::string filepath; + if (!name.empty()) { + filepath = fmt::format("{}_{}.txt", lt.to_string(false), name); + } else { + filepath = fmt::format("{}.txt", lt.to_string(false)); + } + + auto raw_lines = context_->backend->get_data().get_raw_lines(); + if (raw_lines.empty()) { + log_warn(context_->log, "Cannot download data as the data buffer is empty!"); + download_name_input->clear(); + return; + } + + std::ofstream out{filepath}; + if (!out.is_open()) { + log_error(context_->log, fmt::format("Failed to open output file: {}", std::strerror(errno))); + download_name_input->clear(); + return; + } + + for (const auto& line : raw_lines) { + out << line << "\n"; + } + + download_name_input->clear(); + }); + + download_button->setMinimumHeight(32); + download_button->setStyleSheet(button_style); + download_button->setDefaultAction(download_action); + h_layout->addWidget(download_button); text_log_ = new QPlainTextEdit(container); text_log_->setReadOnly(true); - layout->addWidget(text_log_); + v_layout->addLayout(h_layout); + v_layout->addWidget(text_log_); return container; } @@ -77,15 +151,6 @@ QWidget* plot_page::build_rhs() { plot_->setBackground(QBrush(bg_color)); plot_->axisRect()->setBackground(QBrush(bg_color)); - // Graph setup & trace styling - QCPGraph* graph = plot_->addGraph(); - - - QPen pen(line_color); - pen.setWidthF(2.0); - graph->setPen(pen); - - // Axes & gridline styling auto configure_axis = [&](QCPAxis* axis, const QString& label) { axis->setLabel(label); axis->setBasePen(QPen(axis_color, 1.0)); @@ -94,7 +159,6 @@ QWidget* plot_page::build_rhs() { axis->setTickLabelColor(axis_color); axis->setLabelColor(axis_color); - // Subtle dark gridlines axis->grid()->setPen(QPen(grid_color, 1.0, Qt::SolidLine)); axis->grid()->setSubGridPen(QPen(grid_color, 0.5, Qt::DotLine)); axis->grid()->setSubGridVisible(true); @@ -103,20 +167,26 @@ QWidget* plot_page::build_rhs() { configure_axis(plot_->xAxis, "Time (s)"); configure_axis(plot_->yAxis, "Value"); - // Padding, scaling & interactions plot_->axisRect()->setAutoMargins(QCP::msNone); plot_->axisRect()->setMargins(QMargins(50, 20, 20, 40)); + plot_->setNoAntialiasingOnDrag(true); plot_->rescaleAxes(); plot_->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom); plot_->replot(); + plot_->legend->setVisible(true); + plot_->legend->setBrush(QBrush(bg_color)); + plot_->legend->setTextColor(axis_color); layout->addWidget(plot_, 1); QTreeWidget* tree = channel_selector->findChild(); if (tree) { - connect(tree, &QTreeWidget::itemSelectionChanged, this, [this, tree]() { + connect(tree, &QTreeWidget::itemSelectionChanged, this, [this, tree, channel_selector]() { plot_->clearGraphs(); + plotted_counts_.clear(); + int count = tree->selectedItems().count(); + channel_selector->setText(count == 0 ? "Select Data" : QString("%1 selected").arg(count)); active_graphs_.clear(); for (QTreeWidgetItem* item : tree->selectedItems()) { QString key = item->data(0, Qt::UserRole).toString(); @@ -137,9 +207,24 @@ QWidget* plot_page::build_rhs() { }); } + return container; } +void plot_page::plot_timer() { + + connect(context_->bridge.get(), &backend_bridge::data_updated, this, [this](const auto&) { pending_update_ = true; }); + + redraw_timer_ = new QTimer(this); + connect(redraw_timer_, &QTimer::timeout, this, [this]() { + if (pending_update_) { + on_data_updated(); + pending_update_ = false; + } + }); + redraw_timer_->start(17); +} + QToolButton* plot_page::create_tree_dropdown(const std::vector& data, QWidget* parent) { auto* select = new QToolButton(parent); @@ -156,15 +241,6 @@ QToolButton* plot_page::create_tree_dropdown(const std::vectorsetPopupMode(QToolButton::InstantPopup); - select->setStyleSheet( - "QPushButton {" - " background-color: #1e1e1e; color: #d4d4d4; border: 1px solid #2d2d2d;" - " border-radius: 4px; padding: 6px 12px; text-align: left;" - "}" - "QPushButton::menu-indicator { image: none; }" - ); - auto* tree = new QTreeWidget(select); tree->setSelectionMode(QAbstractItemView::ExtendedSelection); tree->setHeaderHidden(true); @@ -190,6 +266,9 @@ QToolButton* plot_page::create_tree_dropdown(const std::vectorsetText(0, group_name); tree->addTopLevelItem(target_group); } + if (!target_group) { + continue; + } auto* child_item = new QTreeWidgetItem(target_group); child_item->setText(0, QString::fromStdString(it.name)); child_item->setData(0, Qt::UserRole, QString::fromStdString(it.key)); @@ -199,7 +278,7 @@ QToolButton* plot_page::create_tree_dropdown(const std::vectorsetMinimumSize(300, 200); + tree->setMinimumSize(300, 0); widget_action->setDefaultWidget(tree); menu->addAction(widget_action); @@ -219,7 +298,9 @@ void plot_page::plot_signal(const std::string key) { QVector x_times(time.begin(), time.end()); QVector y_values(series.begin(), series.end()); + QCPGraph* graph = plot_->addGraph(); + graph->setAdaptiveSampling(true); static const std::vector palette = { QColor("#00adb5"), QColor("#ff5722"), QColor("#e91e63"), @@ -229,12 +310,90 @@ void plot_page::plot_signal(const std::string key) { graph->setPen(QPen(color, 2.0)); graph->setData(x_times, y_values); - graph->setName(QString::fromStdString(key)); + for (auto d : context_->backend->get_data().data_values) { + if (d.key == key) { + graph->setName(QString::fromStdString(d.name)); + break; + } + } + graph->addToLegend(); active_graphs_[key] = graph; + plotted_counts_[key] = series.size(); plot_->rescaleAxes(); plot_->replot(); } +void plot_page::on_data_updated() { + update_plot(); + update_stream(); +} + +void plot_page::update_plot() { + + if (active_graphs_.empty()) { + return; + } + + auto time = context_->backend->get_data().get_time(); + + for (auto& [key, graph] : active_graphs_) { + auto series = context_->backend->get_data().get_series(key); + + std::size_t already_plotted = plotted_counts_[key]; + std::size_t total = series.size(); + + if (total < already_plotted) { + graph->data()->clear(); + already_plotted = 0; + } + + if (total <= already_plotted) { + plotted_counts_[key] = total; + continue; + } + + QVector new_x(time.begin() + already_plotted, time.end()); + QVector new_y(series.begin() + already_plotted, series.end()); + graph->addData(new_x, new_y); + + plotted_counts_[key] = total; + } + + plot_->xAxis->rescale(); + plot_->replot(); +} + +void plot_page::update_stream() { + + if (!context_->backend->is_logging()) { + return; + } + + auto series = context_->backend->get_data().get_raw_lines(); + + std::size_t displayed = 0; + if (text_log_->document()->isEmpty()) { + displayed = 0; + } else { + displayed = static_cast(text_log_->document()->blockCount()); + } + + std::size_t total = series.size(); + + if (total < displayed) { + text_log_->clear(); + displayed = 0; + } + + if (total <= displayed) { + return; + } + + for (std::size_t i = displayed; i < total; ++i) { + text_log_->appendPlainText(QString::fromStdString(series[i])); + } +} + } // namespace mbr::pages From 046edd1529d8e56871e7b9c19c5a3a5c9982ea04 Mon Sep 17 00:00:00 2001 From: Blake Watson Date: Tue, 28 Jul 2026 18:09:51 -0400 Subject: [PATCH 11/36] time now syncs right, little weird behavior worth looking into but it works --- include/app/pages/plot.hpp | 3 +++ src/app/gui.cpp | 5 +++++ src/app/pages/plot.cpp | 20 ++++++++++++++++++-- src/esp32/data.cpp | 2 +- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/include/app/pages/plot.hpp b/include/app/pages/plot.hpp index c0d163f..f293212 100644 --- a/include/app/pages/plot.hpp +++ b/include/app/pages/plot.hpp @@ -57,6 +57,9 @@ class plot_page : public page { bool pending_update_ = false; QTimer* redraw_timer_ = nullptr; + double time_offset_ = 0.0; + bool has_offset_ = false; + private slots: void on_data_updated(); }; diff --git a/src/app/gui.cpp b/src/app/gui.cpp index f763da8..263097c 100644 --- a/src/app/gui.cpp +++ b/src/app/gui.cpp @@ -155,6 +155,11 @@ namespace mbr { connection_status_->setText(connected ? "Connected" : "Disconnected"); update_status_dot(); }); + + connect(context_->bridge.get(), &backend_bridge::receiving_changed, this, [this](bool receiving) { + is_receiving_ = receiving; + update_status_dot(); + }); connection_status_->setStyleSheet("font-size: 20px;"); status_dot_ = new QLabel(rightWidget); diff --git a/src/app/pages/plot.cpp b/src/app/pages/plot.cpp index d94481b..462b7d6 100644 --- a/src/app/pages/plot.cpp +++ b/src/app/pages/plot.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -296,7 +297,14 @@ void plot_page::plot_signal(const std::string key) { auto series = context_->backend->get_data().get_series(key); auto time = context_->backend->get_data().get_time(); - QVector x_times(time.begin(), time.end()); + if (!has_offset_ && !time.empty()) { + time_offset_ = time.front(); + has_offset_ = true; + } + + QVector x_times; + x_times.reserve(time.size()); + for (auto t : time) x_times.append(t - time_offset_); QVector y_values(series.begin(), series.end()); QCPGraph* graph = plot_->addGraph(); @@ -331,6 +339,10 @@ void plot_page::on_data_updated() { } void plot_page::update_plot() { + auto sync_opt = context_->backend->get_data().get_sync_lt(); + plot_->xAxis->setLabel(sync_opt + ? QString("Data Synced at: %1").arg(QString::fromStdString(sync_opt->to_string(false))) + : "No Time Synced"); if (active_graphs_.empty()) { return; @@ -354,7 +366,11 @@ void plot_page::update_plot() { continue; } - QVector new_x(time.begin() + already_plotted, time.end()); + QVector new_x; + new_x.reserve(total - already_plotted); + for (auto it = time.begin() + already_plotted; it != time.end(); ++it) { + new_x.append(*it - time_offset_); + } QVector new_y(series.begin() + already_plotted, series.end()); graph->addData(new_x, new_y); diff --git a/src/esp32/data.cpp b/src/esp32/data.cpp index 8c766ce..a79da75 100644 --- a/src/esp32/data.cpp +++ b/src/esp32/data.cpp @@ -72,7 +72,7 @@ void telemetry_data::clear() { time_no_normal_micros_.clear(); raw_lines_.clear(); current_line_.clear(); - sync_lt_.reset(); + //sync_lt_.reset(); logged_missing_keys_.clear(); } From e403e27d99562d22cf85e8d08fc62279c6d7ec5c Mon Sep 17 00:00:00 2001 From: Blake Watson Date: Tue, 28 Jul 2026 18:14:46 -0400 Subject: [PATCH 12/36] fixed width of text on menubar --- src/app/gui.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/app/gui.cpp b/src/app/gui.cpp index 263097c..fe7f1cc 100644 --- a/src/app/gui.cpp +++ b/src/app/gui.cpp @@ -148,12 +148,18 @@ namespace mbr { connection_status_ = new QLabel("Disconnected", rightWidget); + connection_status_->setStyleSheet("font-size: 20px;"); + connection_status_->setMinimumWidth(connection_status_->fontMetrics().horizontalAdvance("Disconnected") + 4); + connection_status_->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + int w = std::max(connection_status_->fontMetrics().horizontalAdvance("Connected"), connection_status_->fontMetrics().horizontalAdvance("Disconnected")); + connection_status_->setMinimumWidth(w + 4); connect(context_->bridge.get(), &backend_bridge::connection_changed, this, [this](bool connected) { is_connected_ = connected; if (!connected) is_receiving_ = false; connection_status_->setText(connected ? "Connected" : "Disconnected"); update_status_dot(); + }); connect(context_->bridge.get(), &backend_bridge::receiving_changed, this, [this](bool receiving) { From 6b35f4550b1a6ac9c48653fdbfc5718685580306 Mon Sep 17 00:00:00 2001 From: Blake Watson Date: Tue, 28 Jul 2026 21:07:24 -0400 Subject: [PATCH 13/36] Update format.yml --- .github/workflows/format.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 2e60a76..b0d5e0c 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -32,6 +32,7 @@ jobs: brew update brew install llvm@21 echo "/opt/homebrew/opt/llvm@21/bin" >> $GITHUB_PATH + python3 scripts/install_qt.py env: HOMEBREW_NO_AUTO_UPDATE: 1 From 084924c9c559c08e59bd5583aca678db866fbed8 Mon Sep 17 00:00:00 2001 From: Blake Watson Date: Tue, 28 Jul 2026 21:09:06 -0400 Subject: [PATCH 14/36] Update package.yml --- .github/workflows/package.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 5f53411..7eb7961 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -25,6 +25,7 @@ jobs: run: | brew update brew install llvm + python3 scripts/install_qt.py env: HOMEBREW_NO_AUTO_UPDATE: 1 @@ -40,6 +41,9 @@ jobs: mingw-w64-ucrt-x86_64-ninja zip tar + - name: Install Qt + shell: msys2 {0} + run: python3 scripts/install_qt.py - name: Build and pack macOS if: runner.os == 'macOS' From 44478f8a391d5c48e68d12aeb8c6f8a8daa98ed5 Mon Sep 17 00:00:00 2001 From: Blake Watson Date: Tue, 28 Jul 2026 21:21:39 -0400 Subject: [PATCH 15/36] holy merge conflicts --- scripts/install_qt.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 scripts/install_qt.py diff --git a/scripts/install_qt.py b/scripts/install_qt.py new file mode 100644 index 0000000..037a6b8 --- /dev/null +++ b/scripts/install_qt.py @@ -0,0 +1,18 @@ +"""scripts/install_qt.py — installs Qt6 for local dev.""" + +import platform +import subprocess +import os +import sys + +if platform.system() == "Darwin": + subprocess.run(["brew", "install", "qt@6"], check=True) +elif "MSYSTEM" in os.environ: + subprocess.run( + ["pacman", "-S", "--noconfirm", "--needed", + "mingw-w64-ucrt-x86_64-qt6-base", + "mingw-w64-ucrt-x86_64-qt6-tools"], + check=True, + ) +else: + sys.exit("Run on macOS, or on Windows from an MSYS2 UCRT64 shell.") From 4bf15ef1e5f58bf3c6c6f8041f449df4ed1324c8 Mon Sep 17 00:00:00 2001 From: Blake Watson Date: Tue, 28 Jul 2026 21:31:37 -0400 Subject: [PATCH 16/36] formatted --- include/app/app.hpp | 6 +- include/app/backend_bridge.hpp | 11 +- include/app/context.hpp | 14 +-- include/app/gui.hpp | 30 +++--- include/app/pages/analysis.hpp | 8 +- include/app/pages/home.hpp | 3 +- include/app/pages/page.hpp | 11 +- include/app/pages/plot.hpp | 43 ++++---- include/app/pages/serialmon.hpp | 8 +- include/app/pages/settings.hpp | 6 +- include/esp32/backend.hpp | 45 ++++---- include/esp32/data.hpp | 4 +- src/app/app.cpp | 10 +- src/app/gui.cpp | 180 +++++++++++++++----------------- src/app/pages/analysis.cpp | 9 +- src/app/pages/home.cpp | 7 +- src/app/pages/plot.cpp | 149 ++++++++++++-------------- src/app/pages/serialmon.cpp | 4 +- src/app/pages/settings.cpp | 3 +- src/esp32/data.cpp | 2 +- src/main.cpp | 2 +- 21 files changed, 269 insertions(+), 286 deletions(-) diff --git a/include/app/app.hpp b/include/app/app.hpp index bba5520..5bc2562 100644 --- a/include/app/app.hpp +++ b/include/app/app.hpp @@ -1,7 +1,7 @@ #pragma once -#include #include +#include #include #include @@ -21,8 +21,8 @@ class app_t { private: std::unique_ptr qt_app_; - std::unique_ptr gui_; - std::shared_ptr context_; + std::unique_ptr gui_; + std::shared_ptr context_; }; } // namespace mbr diff --git a/include/app/backend_bridge.hpp b/include/app/backend_bridge.hpp index 5f7c91b..26e57fc 100644 --- a/include/app/backend_bridge.hpp +++ b/include/app/backend_bridge.hpp @@ -8,17 +8,20 @@ namespace mbr { class backend_bridge : public QObject { Q_OBJECT -public: + public: explicit backend_bridge(telemetry_backend* backend, QObject* parent = nullptr) : QObject(parent) { - backend->set_on_data([this](const ankerl::unordered_dense::map>& d) { emit data_updated(d); }); + backend->set_on_data( + [this](const ankerl::unordered_dense::map>& d) { + emit data_updated(d); + }); backend->set_on_connection_changed([this](bool c) { emit connection_changed(c); }); backend->set_on_receiving_changed([this](bool r) { emit receiving_changed(r); }); } -signals: + signals: void data_updated(const ankerl::unordered_dense::map>&); void connection_changed(bool connected); void receiving_changed(bool receiving); }; -} //namespace mbr +} // namespace mbr diff --git a/include/app/context.hpp b/include/app/context.hpp index 350a084..2eb78cf 100644 --- a/include/app/context.hpp +++ b/include/app/context.hpp @@ -1,15 +1,15 @@ #pragma once +#include #include #include #include -#include #include +#include "app/backend_bridge.hpp" #include "core/log.hpp" #include "esp32/backend.hpp" -#include "app/backend_bridge.hpp" namespace mbr { @@ -23,12 +23,12 @@ enum class page_type_t : u8 { [[nodiscard]] constexpr const char* page_type_str(page_type_t page_type) { switch (page_type) { - case page_type_t::HOME: return "Home"; - case page_type_t::PLOT: return "Plot"; - case page_type_t::ANALYSIS: return "Analysis"; - case page_type_t::SERIAL: return "Serial Monitor"; + case page_type_t::HOME: return "Home"; + case page_type_t::PLOT: return "Plot"; + case page_type_t::ANALYSIS: return "Analysis"; + case page_type_t::SERIAL: return "Serial Monitor"; case page_type_t::SETTINGS: return "Settings"; - default: return "Unknown"; + default: return "Unknown"; } } diff --git a/include/app/gui.hpp b/include/app/gui.hpp index 2eb433c..341e416 100644 --- a/include/app/gui.hpp +++ b/include/app/gui.hpp @@ -1,13 +1,13 @@ #pragma once -#include +#include "app/context.hpp" +#include "app/pages/page.hpp" +#include "stdx/fixed/enum_map.hh" #include #include #include +#include #include #include -#include "app/context.hpp" -#include "app/pages/page.hpp" -#include "stdx/fixed/enum_map.hh" #include #include #include @@ -17,25 +17,25 @@ namespace mbr { class gui_t : public QMainWindow { Q_OBJECT -public: + public: explicit gui_t(const std::shared_ptr& ctx); ~gui_t() = default; -private: + private: void change_page(page_type_t type); void build_menu_bar(); void update_status_dot(); - [[nodiscard]] pages::page* create_page(page_type_t type, const std::shared_ptr& ctx, QWidget* parent); - -private: + [[nodiscard]] pages::page* + create_page(page_type_t type, const std::shared_ptr& ctx, QWidget* parent); - QStackedWidget* pages_; - std::shared_ptr context_; + private: + QStackedWidget* pages_; + std::shared_ptr context_; stdx::fixed::enum_map page_lookup_; - QLabel* connection_status_; - QLabel* status_dot_ = nullptr; - bool is_connected_ = false; - bool is_receiving_ = false; + QLabel* connection_status_; + QLabel* status_dot_ = nullptr; + bool is_connected_ = false; + bool is_receiving_ = false; }; } // namespace mbr diff --git a/include/app/pages/analysis.hpp b/include/app/pages/analysis.hpp index 394e2cd..92bf6f6 100644 --- a/include/app/pages/analysis.hpp +++ b/include/app/pages/analysis.hpp @@ -10,10 +10,9 @@ #include #include - +#include #include #include -#include #include "app/context.hpp" #include "app/pages/page.hpp" @@ -35,7 +34,8 @@ class analysis_page : public page { [[nodiscard]] static const char* data_type_string(data_view_t type); public: - explicit analysis_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : page(ctx, parent) {}; + explicit analysis_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) + : page(ctx, parent) {}; ~analysis_page() override = default; void on_enter() override; @@ -43,9 +43,7 @@ class analysis_page : public page { void build_page() override; private: - private: - }; } // namespace mbr::pages diff --git a/include/app/pages/home.hpp b/include/app/pages/home.hpp index 8cce782..305c21d 100644 --- a/include/app/pages/home.hpp +++ b/include/app/pages/home.hpp @@ -13,7 +13,8 @@ namespace mbr::pages { class home_page : public page { public: - explicit home_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : page{ctx, parent} {} + explicit home_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) + : page{ctx, parent} {} ~home_page() override = default; void on_enter() override; diff --git a/include/app/pages/page.hpp b/include/app/pages/page.hpp index 3a8e0fc..773d025 100644 --- a/include/app/pages/page.hpp +++ b/include/app/pages/page.hpp @@ -1,25 +1,26 @@ #pragma once -#include #include +#include #include #include "app/context.hpp" namespace mbr::pages { -class page : public QWidget{ +class page : public QWidget { Q_OBJECT public: page() = default; virtual ~page() = default; - virtual void on_enter() = 0; - virtual void on_exit() = 0; + virtual void on_enter() = 0; + virtual void on_exit() = 0; virtual void build_page() = 0; protected: - explicit page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : QWidget(parent), context_{ctx} {} + explicit page(const std::shared_ptr& ctx, QWidget* parent = nullptr) + : QWidget(parent), context_{ctx} {} std::shared_ptr context_; }; diff --git a/include/app/pages/plot.hpp b/include/app/pages/plot.hpp index f293212..d7a42ab 100644 --- a/include/app/pages/plot.hpp +++ b/include/app/pages/plot.hpp @@ -1,36 +1,36 @@ #pragma once +#include #include #include -#include #include #include #include -#include -#include -#include -#include -#include #include -#include #include +#include +#include +#include #include #include #include #include -#include +#include +#include +#include +#include #include "app/pages/page.hpp" #include "esp32/data.hpp" -#include "esp32/data.hpp" namespace mbr::pages { class plot_page : public page { public: - explicit plot_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : page{ctx, parent} { + explicit plot_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) + : page{ctx, parent} { build_page(); } ~plot_page() override = default; @@ -40,22 +40,23 @@ class plot_page : public page { void build_page() override; private: - QWidget* build_rhs(); - QWidget* build_lhs(); - QToolButton* create_tree_dropdown(const std::vector& data, QWidget* parent); - void plot_signal(const std::string key); - void plot_timer(); - void update_plot(); - void update_stream(); + QWidget* build_rhs(); + QWidget* build_lhs(); + QToolButton* create_tree_dropdown(const std::vector& data, + QWidget* parent); + void plot_signal(const std::string key); + void plot_timer(); + void update_plot(); + void update_stream(); private: - QPlainTextEdit* text_log_; - QCustomPlot* plot_; + QPlainTextEdit* text_log_; + QCustomPlot* plot_; ankerl::unordered_dense::map active_graphs_; ankerl::unordered_dense::map plotted_counts_; - bool pending_update_ = false; - QTimer* redraw_timer_ = nullptr; + bool pending_update_ = false; + QTimer* redraw_timer_ = nullptr; double time_offset_ = 0.0; bool has_offset_ = false; diff --git a/include/app/pages/serialmon.hpp b/include/app/pages/serialmon.hpp index 08aafe6..92c5f14 100644 --- a/include/app/pages/serialmon.hpp +++ b/include/app/pages/serialmon.hpp @@ -3,8 +3,8 @@ #include #include -#include #include +#include #include "app/pages/page.hpp" @@ -12,7 +12,8 @@ namespace mbr::pages { class serial_page : public page { public: - explicit serial_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : page{ctx, parent} {} + explicit serial_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) + : page{ctx, parent} {} ~serial_page() override = default; void on_enter() override; @@ -20,9 +21,8 @@ class serial_page : public page { void build_page() override; private: - private: - std::string serial_buffer_; + std::string serial_buffer_; }; } // namespace mbr::pages diff --git a/include/app/pages/settings.hpp b/include/app/pages/settings.hpp index e1f5b9d..2fa5c10 100644 --- a/include/app/pages/settings.hpp +++ b/include/app/pages/settings.hpp @@ -13,7 +13,8 @@ namespace mbr::pages { class settings_page : public page { public: - explicit settings_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : page{ctx, parent} {} + explicit settings_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) + : page{ctx, parent} {} ~settings_page() override = default; void on_enter() override; @@ -21,10 +22,7 @@ class settings_page : public page { void build_page() override; private: - - private: - }; } // namespace mbr::pages diff --git a/include/esp32/backend.hpp b/include/esp32/backend.hpp index 1e1ffd2..3835051 100644 --- a/include/esp32/backend.hpp +++ b/include/esp32/backend.hpp @@ -35,27 +35,33 @@ enum class response_type_t : u8 { class telemetry_backend { - public: - using data_cb = std::function>&)>; - using status_cb = std::function; - using receiving_cb = std::function; - using cmd_cb = std::function; - - void set_on_data(data_cb cb) { on_data_ = std::move(cb); } - void set_on_connection_changed(status_cb cb) { on_connection_changed_ = std::move(cb); } - void set_on_receiving_changed(receiving_cb cb) { on_receiving_changed_ = std::move(cb); } - void set_on_cmd(cmd_cb cb) { on_cmd_ = std::move(cb); } + public: + using data_cb = + std::function>&)>; + using status_cb = std::function; + using receiving_cb = std::function; + using cmd_cb = std::function; - private: - void notify_data(const ankerl::unordered_dense::map>& s) { if (on_data_) on_data_(s); } - void notify_connection_changed(bool c) { if (on_connection_changed_) on_connection_changed_(c); } - void notify_receiving_changed(bool r) { if (on_receiving_changed_) on_receiving_changed_(r); } + void set_on_data(data_cb cb) { on_data_ = std::move(cb); } + void set_on_connection_changed(status_cb cb) { on_connection_changed_ = std::move(cb); } + void set_on_receiving_changed(receiving_cb cb) { on_receiving_changed_ = std::move(cb); } + void set_on_cmd(cmd_cb cb) { on_cmd_ = std::move(cb); } - data_cb on_data_; - status_cb on_connection_changed_; - receiving_cb on_receiving_changed_; - cmd_cb on_cmd_; + private: + void notify_data(const ankerl::unordered_dense::map>& s) { + if (on_data_) { on_data_(s); } + } + void notify_connection_changed(bool c) { + if (on_connection_changed_) { on_connection_changed_(c); } + } + void notify_receiving_changed(bool r) { + if (on_receiving_changed_) { on_receiving_changed_(r); } + } + data_cb on_data_; + status_cb on_connection_changed_; + receiving_cb on_receiving_changed_; + cmd_cb on_cmd_; public: explicit telemetry_backend(const std::filesystem::path& json_path, log_fn_t log = nullptr); @@ -71,7 +77,6 @@ class telemetry_backend { [[nodiscard]] const telemetry_data& get_data() const noexcept { return data_; } [[nodiscard]] serial_manager_t& get_serial_manager() noexcept { return serial_manager_; } - [[nodiscard]] bool is_connected() const noexcept { return is_connected_.load(std::memory_order_relaxed); } @@ -89,7 +94,7 @@ class telemetry_backend { void set_logging(bool value) noexcept { is_logging_.store(value, std::memory_order_relaxed); } stdx::option>> - validate_packet(std::string_view str) const; + validate_packet(std::string_view str) const; void restart(); private: diff --git a/include/esp32/data.hpp b/include/esp32/data.hpp index 37fb6ba..3878816 100644 --- a/include/esp32/data.hpp +++ b/include/esp32/data.hpp @@ -58,9 +58,7 @@ class telemetry_data { return dummy; } - [[nodiscard]] auto& get_series() noexcept { - return series_; - } + [[nodiscard]] auto& get_series() noexcept { return series_; } void set_sync_lt(local_time lt) noexcept { sync_lt_ = lt; } void write_data(const std::string& identifier, const std::string& value); diff --git a/src/app/app.cpp b/src/app/app.cpp index fa481d2..9ce4e05 100644 --- a/src/app/app.cpp +++ b/src/app/app.cpp @@ -3,25 +3,27 @@ #include #include +#include +#include #include #include #include #include -#include -#include +#include "app/backend_bridge.hpp" #include "app/context.hpp" #include "app/gui.hpp" #include "core/log.hpp" #include "esp32/backend.hpp" #include "esp32/data.hpp" -#include "app/backend_bridge.hpp" namespace mbr { namespace { const std::filesystem::path DEFAULT_JSON_PATH{"MBR_data.json"}; } // namespace -app_t::app_t(i32 argc, char** argv) : qt_app_{std::make_unique(argc, argv)}, context_{std::make_shared()} { +app_t::app_t(i32 argc, char** argv) + : qt_app_{std::make_unique(argc, argv)}, + context_{std::make_shared()} { context_->log = context_->logger.get_log_fn(); ix::initNetSystem(); context_->backend = std::make_unique(DEFAULT_JSON_PATH, context_->log); diff --git a/src/app/gui.cpp b/src/app/gui.cpp index fe7f1cc..557252b 100644 --- a/src/app/gui.cpp +++ b/src/app/gui.cpp @@ -12,23 +12,22 @@ #include #include - -#include -#include -#include -#include #include #include #include +#include +#include +#include +#include #include "app/assets/images/app_icon.hpp" #include "app/backend_bridge.hpp" -#include "app/pages/page.hpp" #include "app/context.hpp" +#include "app/pages/analysis.hpp" #include "app/pages/home.hpp" +#include "app/pages/page.hpp" #include "app/pages/plot.hpp" #include "app/pages/serialmon.hpp" -#include "app/pages/analysis.hpp" #include "app/pages/settings.hpp" #include "core/time.hpp" #include "esp32/backend.hpp" @@ -38,37 +37,34 @@ using namespace std::chrono; namespace mbr { - gui_t::gui_t(const std::shared_ptr& ctx) : context_(ctx) { - pages_ = new QStackedWidget(this); - setCentralWidget(pages_); - for (auto type : stdx::enum_range()) { - auto* p = create_page(type, context_, pages_); - pages_->addWidget(p); - page_lookup_[type] = p; - } - change_page(page_type_t::HOME); - build_menu_bar(); +gui_t::gui_t(const std::shared_ptr& ctx) : context_(ctx) { + pages_ = new QStackedWidget(this); + setCentralWidget(pages_); + for (auto type : stdx::enum_range()) { + auto* p = create_page(type, context_, pages_); + pages_->addWidget(p); + page_lookup_[type] = p; } - - - [[nodiscard]] pages::page* gui_t::create_page(page_type_t type, const std::shared_ptr& ctx, QWidget* parent) { - switch (type) { - case page_type_t::HOME: return new pages::home_page(ctx, parent); - case page_type_t::PLOT: return new pages::plot_page(ctx, parent); - case page_type_t::ANALYSIS: return new pages::analysis_page(ctx, parent); - case page_type_t::SERIAL: return new pages::serial_page(ctx, parent); - case page_type_t::SETTINGS: return new pages::settings_page(ctx, parent); - default: return nullptr; - } + change_page(page_type_t::HOME); + build_menu_bar(); +} + +[[nodiscard]] pages::page* +gui_t::create_page(page_type_t type, const std::shared_ptr& ctx, QWidget* parent) { + switch (type) { + case page_type_t::HOME: return new pages::home_page(ctx, parent); + case page_type_t::PLOT: return new pages::plot_page(ctx, parent); + case page_type_t::ANALYSIS: return new pages::analysis_page(ctx, parent); + case page_type_t::SERIAL: return new pages::serial_page(ctx, parent); + case page_type_t::SETTINGS: return new pages::settings_page(ctx, parent); + default: return nullptr; } +} +void gui_t::change_page(page_type_t type) { pages_->setCurrentWidget(page_lookup_[type]); } - void gui_t::change_page(page_type_t type) { - pages_->setCurrentWidget(page_lookup_[type]); - } - - void gui_t::build_menu_bar() { - static const QString kMenuBarStyle = R"( +void gui_t::build_menu_bar() { + static const QString kMenuBarStyle = R"( QMenuBar { background-color: #2b2b2b; color: white; @@ -88,7 +84,7 @@ namespace mbr { } )"; - static const QString kMenuStyle = R"( + static const QString kMenuStyle = R"( QMenu { background-color: #2b2b2b; color: white; @@ -116,74 +112,72 @@ namespace mbr { } )"; - menuBar()->setStyleSheet(kMenuBarStyle); - - auto* file = menuBar()->addMenu("File"); - file->setStyleSheet(kMenuStyle); - file->addAction("Settings"); - file->addAction("Export Log"); - file->addAction("Exit", this, [] { QApplication::quit(); }); - - auto* nav = menuBar()->addMenu("Navigation"); - nav->setStyleSheet(kMenuStyle); - nav->addAction("Home", this, [this] { change_page(page_type_t::HOME); }); - nav->addAction("Plot", this, [this] { change_page(page_type_t::PLOT); }); - nav->addAction("Serial", this, [this] { change_page(page_type_t::SERIAL); }); - nav->addAction("Analysis", this, [this] { change_page(page_type_t::ANALYSIS); }); - - menuBar()->addAction("Sync Time", this, [this] { - local_time lt; - const auto sync_time = lt.micros_since_midnight(); - context_->backend->send_cmd(std::format("CMD SYNC {}", sync_time)); - }); - - menuBar()->addAction("Restart Connection", this, [this] { context_->backend->restart(); }); - - menuBar()->addAction("Clear Data", this, [this] { context_->backend->get_data().clear(); }); - - auto* rightWidget = new QWidget(menuBar()); - auto* rightLayout = new QHBoxLayout(rightWidget); - rightLayout->setContentsMargins(0, 4, 8, 4); - rightLayout->setSpacing(8); - - - connection_status_ = new QLabel("Disconnected", rightWidget); - connection_status_->setStyleSheet("font-size: 20px;"); - connection_status_->setMinimumWidth(connection_status_->fontMetrics().horizontalAdvance("Disconnected") + 4); - connection_status_->setAlignment(Qt::AlignRight | Qt::AlignVCenter); - int w = std::max(connection_status_->fontMetrics().horizontalAdvance("Connected"), connection_status_->fontMetrics().horizontalAdvance("Disconnected")); - connection_status_->setMinimumWidth(w + 4); - - connect(context_->bridge.get(), &backend_bridge::connection_changed, this, [this](bool connected) { + menuBar()->setStyleSheet(kMenuBarStyle); + + auto* file = menuBar()->addMenu("File"); + file->setStyleSheet(kMenuStyle); + file->addAction("Settings"); + file->addAction("Export Log"); + file->addAction("Exit", this, [] { QApplication::quit(); }); + + auto* nav = menuBar()->addMenu("Navigation"); + nav->setStyleSheet(kMenuStyle); + nav->addAction("Home", this, [this] { change_page(page_type_t::HOME); }); + nav->addAction("Plot", this, [this] { change_page(page_type_t::PLOT); }); + nav->addAction("Serial", this, [this] { change_page(page_type_t::SERIAL); }); + nav->addAction("Analysis", this, [this] { change_page(page_type_t::ANALYSIS); }); + + menuBar()->addAction("Sync Time", this, [this] { + local_time lt; + const auto sync_time = lt.micros_since_midnight(); + context_->backend->send_cmd(std::format("CMD SYNC {}", sync_time)); + }); + + menuBar()->addAction("Restart Connection", this, [this] { context_->backend->restart(); }); + + menuBar()->addAction("Clear Data", this, [this] { context_->backend->get_data().clear(); }); + + auto* rightWidget = new QWidget(menuBar()); + auto* rightLayout = new QHBoxLayout(rightWidget); + rightLayout->setContentsMargins(0, 4, 8, 4); + rightLayout->setSpacing(8); + + connection_status_ = new QLabel("Disconnected", rightWidget); + connection_status_->setStyleSheet("font-size: 20px;"); + connection_status_->setMinimumWidth( + connection_status_->fontMetrics().horizontalAdvance("Disconnected") + 4); + connection_status_->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + int w = std::max(connection_status_->fontMetrics().horizontalAdvance("Connected"), + connection_status_->fontMetrics().horizontalAdvance("Disconnected")); + connection_status_->setMinimumWidth(w + 4); + + connect( + context_->bridge.get(), &backend_bridge::connection_changed, this, [this](bool connected) { is_connected_ = connected; - if (!connected) is_receiving_ = false; + if (!connected) { is_receiving_ = false; } connection_status_->setText(connected ? "Connected" : "Disconnected"); update_status_dot(); - }); - connect(context_->bridge.get(), &backend_bridge::receiving_changed, this, [this](bool receiving) { + connect( + context_->bridge.get(), &backend_bridge::receiving_changed, this, [this](bool receiving) { is_receiving_ = receiving; update_status_dot(); }); - connection_status_->setStyleSheet("font-size: 20px;"); - - status_dot_ = new QLabel(rightWidget); - status_dot_->setFixedSize(20, 20); - status_dot_->setStyleSheet("background-color: #F44336; border-radius: 10px;"); - rightLayout->addWidget(connection_status_, 0, Qt::AlignVCenter); - rightLayout->addWidget(status_dot_, 0, Qt::AlignVCenter); + connection_status_->setStyleSheet("font-size: 20px;"); - menuBar()->setCornerWidget(rightWidget, Qt::TopRightCorner); - } - - void gui_t::update_status_dot() { - QString color = !is_connected_ ? "#F44336" - : !is_receiving_ ? "#FFC107" - : "#00E676"; - status_dot_->setStyleSheet(QString("background-color: %1; border-radius: 10px;").arg(color)); - } + status_dot_ = new QLabel(rightWidget); + status_dot_->setFixedSize(20, 20); + status_dot_->setStyleSheet("background-color: #F44336; border-radius: 10px;"); + rightLayout->addWidget(connection_status_, 0, Qt::AlignVCenter); + rightLayout->addWidget(status_dot_, 0, Qt::AlignVCenter); + menuBar()->setCornerWidget(rightWidget, Qt::TopRightCorner); +} +void gui_t::update_status_dot() { + QString color = !is_connected_ ? "#F44336" : !is_receiving_ ? "#FFC107" : "#00E676"; + status_dot_->setStyleSheet(QString("background-color: %1; border-radius: 10px;").arg(color)); +} } // namespace mbr diff --git a/src/app/pages/analysis.cpp b/src/app/pages/analysis.cpp index 9c2dd7a..38d25d4 100644 --- a/src/app/pages/analysis.cpp +++ b/src/app/pages/analysis.cpp @@ -31,10 +31,9 @@ using namespace std::chrono_literals; namespace mbr::pages { - void analysis_page::on_enter() { log_info(context_->log, "Entered AnalysisPage"); } - void analysis_page::on_exit() { log_info(context_->log, "Exited AnalysisPage"); } +void analysis_page::on_enter() { log_info(context_->log, "Entered AnalysisPage"); } +void analysis_page::on_exit() { log_info(context_->log, "Exited AnalysisPage"); } + +void analysis_page::build_page() { PROFILE_FUNCTION(); } - void analysis_page::build_page() { - PROFILE_FUNCTION(); - } } // namespace mbr::pages diff --git a/src/app/pages/home.cpp b/src/app/pages/home.cpp index 511e5f4..37ce0b1 100644 --- a/src/app/pages/home.cpp +++ b/src/app/pages/home.cpp @@ -22,11 +22,8 @@ void home_page::build_page() { build_lhs(); } -void home_page::build_rhs() { +void home_page::build_rhs() {} -} - -void home_page::build_lhs() { +void home_page::build_lhs() {} -} } // namespace mbr::pages diff --git a/src/app/pages/plot.cpp b/src/app/pages/plot.cpp index 462b7d6..d3b75e2 100644 --- a/src/app/pages/plot.cpp +++ b/src/app/pages/plot.cpp @@ -1,5 +1,6 @@ #include "app/pages/plot.hpp" +#include #include #include #include @@ -9,7 +10,6 @@ #include #include #include -#include #include #include @@ -17,24 +17,24 @@ #include #include -#include +#include #include -#include +#include #include +#include +#include +#include +#include #include #include #include #include -#include -#include -#include -#include #include +#include "app/backend_bridge.hpp" #include "core/log.hpp" #include "esp32/backend.hpp" #include "esp32/data.hpp" -#include "app/backend_bridge.hpp" #include @@ -65,11 +65,12 @@ QWidget* plot_page::build_lhs() { "}" "QToolButton:hover { background-color: #2a2a2a; }"; auto* container = new QWidget(this); - auto* v_layout = new QVBoxLayout(container); - auto* h_layout = new QHBoxLayout(); + auto* v_layout = new QVBoxLayout(container); + auto* h_layout = new QHBoxLayout(); auto* log_button = new QToolButton(); - auto* log_action = new QAction(context_->backend->is_logging() ? "Stop Logging" : "Start Logging", this); + auto* log_action = + new QAction(context_->backend->is_logging() ? "Stop Logging" : "Start Logging", this); connect(log_action, &QAction::triggered, this, [this, log_action] { context_->backend->set_logging(!context_->backend->is_logging()); log_action->setText(context_->backend->is_logging() ? "Stop Logging" : "Start Logging"); @@ -90,7 +91,7 @@ QWidget* plot_page::build_lhs() { connect(download_action, &QAction::triggered, this, [this, download_name_input] { std::string name = download_name_input->text().toStdString(); - local_time lt; + local_time lt; std::string filepath; if (!name.empty()) { filepath = fmt::format("{}_{}.txt", lt.to_string(false), name); @@ -107,14 +108,13 @@ QWidget* plot_page::build_lhs() { std::ofstream out{filepath}; if (!out.is_open()) { - log_error(context_->log, fmt::format("Failed to open output file: {}", std::strerror(errno))); + log_error(context_->log, + fmt::format("Failed to open output file: {}", std::strerror(errno))); download_name_input->clear(); return; } - for (const auto& line : raw_lines) { - out << line << "\n"; - } + for (const auto& line : raw_lines) { out << line << "\n"; } download_name_input->clear(); }); @@ -134,11 +134,12 @@ QWidget* plot_page::build_lhs() { QWidget* plot_page::build_rhs() { auto* container = new QWidget(this); - auto* layout = new QVBoxLayout(container); + auto* layout = new QVBoxLayout(container); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(8); - QToolButton* channel_selector = create_tree_dropdown(context_->backend->get_data().data_values, container); + QToolButton* channel_selector = + create_tree_dropdown(context_->backend->get_data().data_values, container); layout->addWidget(channel_selector); @@ -187,46 +188,46 @@ QWidget* plot_page::build_rhs() { plot_->clearGraphs(); plotted_counts_.clear(); int count = tree->selectedItems().count(); - channel_selector->setText(count == 0 ? "Select Data" : QString("%1 selected").arg(count)); + channel_selector->setText(count == 0 ? "Select Data" + : QString("%1 selected").arg(count)); active_graphs_.clear(); for (QTreeWidgetItem* item : tree->selectedItems()) { QString key = item->data(0, Qt::UserRole).toString(); - if (!key.isEmpty()) { - plot_signal(key.toStdString()); - } + if (!key.isEmpty()) { plot_signal(key.toStdString()); } if (item->childCount() > 0) { for (int i = 0; i < item->childCount(); ++i) { QTreeWidgetItem* child = item->child(i); - std::string child_key = child->data(0, Qt::UserRole).toString().toStdString(); - if (!child_key.empty()) { - plot_signal(child_key); - } + std::string child_key = + child->data(0, Qt::UserRole).toString().toStdString(); + if (!child_key.empty()) { plot_signal(child_key); } } } } }); } - return container; } void plot_page::plot_timer() { - connect(context_->bridge.get(), &backend_bridge::data_updated, this, [this](const auto&) { pending_update_ = true; }); + connect(context_->bridge.get(), &backend_bridge::data_updated, this, [this](const auto&) { + pending_update_ = true; + }); redraw_timer_ = new QTimer(this); - connect(redraw_timer_, &QTimer::timeout, this, [this]() { - if (pending_update_) { - on_data_updated(); - pending_update_ = false; - } - }); - redraw_timer_->start(17); + connect(redraw_timer_, &QTimer::timeout, this, [this]() { + if (pending_update_) { + on_data_updated(); + pending_update_ = false; + } + }); + redraw_timer_->start(17); } -QToolButton* plot_page::create_tree_dropdown(const std::vector& data, QWidget* parent) { +QToolButton* plot_page::create_tree_dropdown(const std::vector& data, + QWidget* parent) { auto* select = new QToolButton(parent); select->setText("Select Data"); @@ -234,49 +235,44 @@ QToolButton* plot_page::create_tree_dropdown(const std::vectorsetPopupMode(QToolButton::InstantPopup); select->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - select->setStyleSheet( - "QToolButton {" - " background-color: #1e1e1e; color: #d4d4d4; border: 1px solid #2d2d2d;" - " border-radius: 4px; padding: 6px 12px; text-align: left;" - "}" - "QToolButton:hover { background-color: #2a2a2a; }" - ); + select->setStyleSheet("QToolButton {" + " background-color: #1e1e1e; color: #d4d4d4; border: 1px solid #2d2d2d;" + " border-radius: 4px; padding: 6px 12px; text-align: left;" + "}" + "QToolButton:hover { background-color: #2a2a2a; }"); - auto* tree = new QTreeWidget(select); + auto* tree = new QTreeWidget(select); tree->setSelectionMode(QAbstractItemView::ExtendedSelection); tree->setHeaderHidden(true); tree->setColumnCount(1); tree->setStyleSheet( "QTreeWidget { background-color: #1e1e1e; color: #d4d4d4; border: 1px solid #2d2d2d; }" - "QTreeWidget::item:hover { background-color: #00adb5; color: white; }" - ); + "QTreeWidget::item:hover { background-color: #00adb5; color: white; }"); for (auto it : data) { - bool found = false; + bool found = false; QTreeWidgetItem* target_group = nullptr; for (int i = 0; i < tree->topLevelItemCount(); ++i) { if (QString::fromStdString(it.group) == tree->topLevelItem(i)->text(0)) { - found = true; + found = true; target_group = tree->topLevelItem(i); break; } } if (!found && it.plot) { - target_group = new QTreeWidgetItem; + target_group = new QTreeWidgetItem; QString group_name = QString::fromStdString(it.group); target_group->setText(0, group_name); tree->addTopLevelItem(target_group); } - if (!target_group) { - continue; - } + if (!target_group) { continue; } auto* child_item = new QTreeWidgetItem(target_group); child_item->setText(0, QString::fromStdString(it.name)); child_item->setData(0, Qt::UserRole, QString::fromStdString(it.key)); } tree->expandAll(); - auto* menu = new QMenu(select); + auto* menu = new QMenu(select); auto* widget_action = new QWidgetAction(menu); tree->setMinimumSize(300, 0); @@ -288,12 +284,8 @@ QToolButton* plot_page::create_tree_dropdown(const std::vectorbackend->get_data().get_series(key); auto time = context_->backend->get_data().get_time(); @@ -304,17 +296,19 @@ void plot_page::plot_signal(const std::string key) { QVector x_times; x_times.reserve(time.size()); - for (auto t : time) x_times.append(t - time_offset_); + for (auto t : time) { x_times.append(t - time_offset_); } QVector y_values(series.begin(), series.end()); QCPGraph* graph = plot_->addGraph(); graph->setAdaptiveSampling(true); - static const std::vector palette = { - QColor("#00adb5"), QColor("#ff5722"), QColor("#e91e63"), - QColor("#9c27b0"), QColor("#4caf50"), QColor("#ffeb3b") - }; - QColor color = palette[active_graphs_.size() % palette.size()]; + static const std::vector palette = {QColor("#00adb5"), + QColor("#ff5722"), + QColor("#e91e63"), + QColor("#9c27b0"), + QColor("#4caf50"), + QColor("#ffeb3b")}; + QColor color = palette[active_graphs_.size() % palette.size()]; graph->setPen(QPen(color, 2.0)); graph->setData(x_times, y_values); @@ -326,7 +320,7 @@ void plot_page::plot_signal(const std::string key) { } graph->addToLegend(); - active_graphs_[key] = graph; + active_graphs_[key] = graph; plotted_counts_[key] = series.size(); plot_->rescaleAxes(); @@ -340,13 +334,12 @@ void plot_page::on_data_updated() { void plot_page::update_plot() { auto sync_opt = context_->backend->get_data().get_sync_lt(); - plot_->xAxis->setLabel(sync_opt + plot_->xAxis->setLabel( + sync_opt ? QString("Data Synced at: %1").arg(QString::fromStdString(sync_opt->to_string(false))) : "No Time Synced"); - if (active_graphs_.empty()) { - return; - } + if (active_graphs_.empty()) { return; } auto time = context_->backend->get_data().get_time(); @@ -354,7 +347,7 @@ void plot_page::update_plot() { auto series = context_->backend->get_data().get_series(key); std::size_t already_plotted = plotted_counts_[key]; - std::size_t total = series.size(); + std::size_t total = series.size(); if (total < already_plotted) { graph->data()->clear(); @@ -367,10 +360,10 @@ void plot_page::update_plot() { } QVector new_x; - new_x.reserve(total - already_plotted); - for (auto it = time.begin() + already_plotted; it != time.end(); ++it) { - new_x.append(*it - time_offset_); - } + new_x.reserve(total - already_plotted); + for (auto it = time.begin() + already_plotted; it != time.end(); ++it) { + new_x.append(*it - time_offset_); + } QVector new_y(series.begin() + already_plotted, series.end()); graph->addData(new_x, new_y); @@ -383,9 +376,7 @@ void plot_page::update_plot() { void plot_page::update_stream() { - if (!context_->backend->is_logging()) { - return; - } + if (!context_->backend->is_logging()) { return; } auto series = context_->backend->get_data().get_raw_lines(); @@ -403,9 +394,7 @@ void plot_page::update_stream() { displayed = 0; } - if (total <= displayed) { - return; - } + if (total <= displayed) { return; } for (std::size_t i = displayed; i < total; ++i) { text_log_->appendPlainText(QString::fromStdString(series[i])); diff --git a/src/app/pages/serialmon.cpp b/src/app/pages/serialmon.cpp index 1627fc8..85d96d4 100644 --- a/src/app/pages/serialmon.cpp +++ b/src/app/pages/serialmon.cpp @@ -15,8 +15,6 @@ namespace mbr::pages { void serial_page::on_enter() { log_info(context_->log, "Entered SerialPage"); } void serial_page::on_exit() { log_info(context_->log, "Exited SerialPage"); } -void serial_page::build_page() { - PROFILE_FUNCTION(); -} +void serial_page::build_page() { PROFILE_FUNCTION(); } } // namespace mbr::pages diff --git a/src/app/pages/settings.cpp b/src/app/pages/settings.cpp index 4885f50..c34e036 100644 --- a/src/app/pages/settings.cpp +++ b/src/app/pages/settings.cpp @@ -17,7 +17,6 @@ namespace mbr::pages { void settings_page::on_enter() { log_info(context_->log, "Entered SettingsPage"); } void settings_page::on_exit() { log_info(context_->log, "Exited SettingsPage"); } -void settings_page::build_page() { -} +void settings_page::build_page() {} } // namespace mbr::pages diff --git a/src/esp32/data.cpp b/src/esp32/data.cpp index a79da75..abc994e 100644 --- a/src/esp32/data.cpp +++ b/src/esp32/data.cpp @@ -72,7 +72,7 @@ void telemetry_data::clear() { time_no_normal_micros_.clear(); raw_lines_.clear(); current_line_.clear(); - //sync_lt_.reset(); + // sync_lt_.reset(); logged_missing_keys_.clear(); } diff --git a/src/main.cpp b/src/main.cpp index 33e324a..21597bf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,6 @@ int main(int argc, char* argv[]) { stdx::profiler profiler{argv[0]}; - mbr::app_t app{argc, argv}; + mbr::app_t app{argc, argv}; app.run(); } From 581270613326b571e657c72bb1c27204c9fff71b Mon Sep 17 00:00:00 2001 From: Blake Watson Date: Tue, 28 Jul 2026 21:37:51 -0400 Subject: [PATCH 17/36] ci --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 198fe00..1ffcdf0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,7 @@ jobs: run: | brew update brew install llvm + run: python3 scripts/install_qt.py env: HOMEBREW_NO_AUTO_UPDATE: 1 @@ -49,6 +50,9 @@ jobs: mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-ninja + - name: Install Qt + shell: msys2 {0} + run: python3 scripts/install_qt.py - name: Cache build directory uses: actions/cache@v4 From 52cfe3044f5999dc1cd28839e9268bf545abe5c3 Mon Sep 17 00:00:00 2001 From: Blake Watson Date: Tue, 28 Jul 2026 21:41:14 -0400 Subject: [PATCH 18/36] ci --- .github/workflows/ci.yml | 1 + .github/workflows/package.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ffcdf0..74d4d5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,6 +50,7 @@ jobs: mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-ninja + mingw-w64-ucrt-x86_64-python - name: Install Qt shell: msys2 {0} run: python3 scripts/install_qt.py diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 7eb7961..f23d1af 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -39,6 +39,7 @@ jobs: mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-ninja + mingw-w64-ucrt-x86_64-python zip tar - name: Install Qt From 6abcd5920f5601b8fdf0b0fe014c49b453d74923 Mon Sep 17 00:00:00 2001 From: Blake Watson Date: Tue, 28 Jul 2026 21:49:34 -0400 Subject: [PATCH 19/36] ci --- scripts/install_qt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/install_qt.py b/scripts/install_qt.py index 037a6b8..28658f3 100644 --- a/scripts/install_qt.py +++ b/scripts/install_qt.py @@ -1,5 +1,4 @@ """scripts/install_qt.py — installs Qt6 for local dev.""" - import platform import subprocess import os @@ -11,7 +10,8 @@ subprocess.run( ["pacman", "-S", "--noconfirm", "--needed", "mingw-w64-ucrt-x86_64-qt6-base", - "mingw-w64-ucrt-x86_64-qt6-tools"], + "mingw-w64-ucrt-x86_64-qt6-tools", + "mingw-w64-ucrt-x86_64-qt6-charts"], check=True, ) else: From 9473a008198450930483c93cda100f665a026845 Mon Sep 17 00:00:00 2001 From: Blake Date: Wed, 29 Jul 2026 10:28:14 -0400 Subject: [PATCH 20/36] ci --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74d4d5e..a643753 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: run: | brew update brew install llvm - run: python3 scripts/install_qt.py + python3 scripts/install_qt.py env: HOMEBREW_NO_AUTO_UPDATE: 1 From 45abe6a01a2e1c7f04239a64ad0caba60ae7af24 Mon Sep 17 00:00:00 2001 From: Blake Date: Wed, 29 Jul 2026 10:38:00 -0400 Subject: [PATCH 21/36] ci and package --- .github/workflows/ci.yml | 4 +++- .github/workflows/package.yml | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a643753..87d495d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,9 @@ jobs: mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-ninja mingw-w64-ucrt-x86_64-python - - name: Install Qt + + - name: Install Qt (Windows) + if: runner.os == 'Windows' shell: msys2 {0} run: python3 scripts/install_qt.py diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index f23d1af..d919ec0 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -42,7 +42,8 @@ jobs: mingw-w64-ucrt-x86_64-python zip tar - - name: Install Qt + - name: Install Qt (Windows) + if: runner.os == 'Windows' shell: msys2 {0} run: python3 scripts/install_qt.py From 3509b12aadaa069350b3ecb52cec980ad88a9080 Mon Sep 17 00:00:00 2001 From: Blake Date: Wed, 29 Jul 2026 10:48:48 -0400 Subject: [PATCH 22/36] this should pass --- src/esp32/data.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/esp32/data.cpp b/src/esp32/data.cpp index abc994e..8c766ce 100644 --- a/src/esp32/data.cpp +++ b/src/esp32/data.cpp @@ -72,7 +72,7 @@ void telemetry_data::clear() { time_no_normal_micros_.clear(); raw_lines_.clear(); current_line_.clear(); - // sync_lt_.reset(); + sync_lt_.reset(); logged_missing_keys_.clear(); } From f9012080d2c38aed57604949eb3e8fa746f959d6 Mon Sep 17 00:00:00 2001 From: Blake Date: Wed, 29 Jul 2026 11:14:13 -0400 Subject: [PATCH 23/36] fixed MINOR performance hit in plot.cpp --- src/app/pages/plot.cpp | 20 ++++++++++---------- src/main.cpp | 3 ++- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/app/pages/plot.cpp b/src/app/pages/plot.cpp index d3b75e2..5f2f432 100644 --- a/src/app/pages/plot.cpp +++ b/src/app/pages/plot.cpp @@ -285,7 +285,8 @@ QToolButton* plot_page::create_tree_dropdown(const std::vectorbackend->get_data().get_series(key); auto time = context_->backend->get_data().get_time(); @@ -346,28 +347,27 @@ void plot_page::update_plot() { for (auto& [key, graph] : active_graphs_) { auto series = context_->backend->get_data().get_series(key); - std::size_t already_plotted = plotted_counts_[key]; + std::size_t* already_plotted = &plotted_counts_[key]; std::size_t total = series.size(); - if (total < already_plotted) { + if (total < *already_plotted) { graph->data()->clear(); - already_plotted = 0; + *already_plotted = 0; } - if (total <= already_plotted) { - plotted_counts_[key] = total; + if (total == *already_plotted) { continue; } QVector new_x; - new_x.reserve(total - already_plotted); - for (auto it = time.begin() + already_plotted; it != time.end(); ++it) { + new_x.reserve(total - *already_plotted); + for (auto it = time.begin() + *already_plotted; it != time.end(); ++it) { new_x.append(*it - time_offset_); } - QVector new_y(series.begin() + already_plotted, series.end()); + QVector new_y(series.begin() + *already_plotted, series.end()); graph->addData(new_x, new_y); - plotted_counts_[key] = total; + *already_plotted = total; } plot_->xAxis->rescale(); diff --git a/src/main.cpp b/src/main.cpp index 21597bf..db743ec 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,8 +1,9 @@ #include "app/app.hpp" #include -int main(int argc, char* argv[]) { +i32 main(i32 argc, char* argv[]) { stdx::profiler profiler{argv[0]}; mbr::app_t app{argc, argv}; app.run(); + return 0; } From 13a31d3ff53c37e7ff8700d16a38471c6268e3ac Mon Sep 17 00:00:00 2001 From: Blake Date: Wed, 29 Jul 2026 11:32:25 -0400 Subject: [PATCH 24/36] formatting and pr fixes --- include/app/gui.hpp | 12 ++++++---- include/app/pages/plot.hpp | 2 +- src/app/pages/plot.cpp | 46 +++++++++++++++++++------------------- src/core/time.cpp | 8 +++---- 4 files changed, 36 insertions(+), 32 deletions(-) diff --git a/include/app/gui.hpp b/include/app/gui.hpp index 341e416..af2b3d5 100644 --- a/include/app/gui.hpp +++ b/include/app/gui.hpp @@ -1,18 +1,22 @@ #pragma once -#include "app/context.hpp" -#include "app/pages/page.hpp" -#include "stdx/fixed/enum_map.hh" + +#include + #include #include #include -#include #include #include #include + #include +#include #include #include +#include "app/context.hpp" +#include "app/pages/page.hpp" + namespace mbr { class gui_t : public QMainWindow { diff --git a/include/app/pages/plot.hpp b/include/app/pages/plot.hpp index d7a42ab..a3885c0 100644 --- a/include/app/pages/plot.hpp +++ b/include/app/pages/plot.hpp @@ -44,7 +44,7 @@ class plot_page : public page { QWidget* build_lhs(); QToolButton* create_tree_dropdown(const std::vector& data, QWidget* parent); - void plot_signal(const std::string key); + void plot_signal(const std::string& key); void plot_timer(); void update_plot(); void update_stream(); diff --git a/src/app/pages/plot.cpp b/src/app/pages/plot.cpp index 5f2f432..dbb68c3 100644 --- a/src/app/pages/plot.cpp +++ b/src/app/pages/plot.cpp @@ -31,6 +31,8 @@ #include #include +#include + #include "app/backend_bridge.hpp" #include "core/log.hpp" #include "esp32/backend.hpp" @@ -114,7 +116,7 @@ QWidget* plot_page::build_lhs() { return; } - for (const auto& line : raw_lines) { out << line << "\n"; } + for (const auto& line : raw_lines) { fmt::println(out, "{}", line); } download_name_input->clear(); }); @@ -187,7 +189,7 @@ QWidget* plot_page::build_rhs() { connect(tree, &QTreeWidget::itemSelectionChanged, this, [this, tree, channel_selector]() { plot_->clearGraphs(); plotted_counts_.clear(); - int count = tree->selectedItems().count(); + usize count = tree->selectedItems().count(); channel_selector->setText(count == 0 ? "Select Data" : QString("%1 selected").arg(count)); active_graphs_.clear(); @@ -196,7 +198,7 @@ QWidget* plot_page::build_rhs() { if (!key.isEmpty()) { plot_signal(key.toStdString()); } if (item->childCount() > 0) { - for (int i = 0; i < item->childCount(); ++i) { + for (i32 i = 0; i < item->childCount(); ++i) { QTreeWidgetItem* child = item->child(i); std::string child_key = child->data(0, Qt::UserRole).toString().toStdString(); @@ -252,7 +254,7 @@ QToolButton* plot_page::create_tree_dropdown(const std::vectortopLevelItemCount(); ++i) { + for (i32 i = 0; i < tree->topLevelItemCount(); ++i) { if (QString::fromStdString(it.group) == tree->topLevelItem(i)->text(0)) { found = true; target_group = tree->topLevelItem(i); @@ -284,7 +286,7 @@ QToolButton* plot_page::create_tree_dropdown(const std::vectorbackend->get_data().get_series(key); @@ -303,17 +305,17 @@ void plot_page::plot_signal(const std::string key) { QCPGraph* graph = plot_->addGraph(); graph->setAdaptiveSampling(true); - static const std::vector palette = {QColor("#00adb5"), - QColor("#ff5722"), - QColor("#e91e63"), - QColor("#9c27b0"), - QColor("#4caf50"), - QColor("#ffeb3b")}; - QColor color = palette[active_graphs_.size() % palette.size()]; + static const std::array palette = {QColor("#00adb5"), + QColor("#ff5722"), + QColor("#e91e63"), + QColor("#9c27b0"), + QColor("#4caf50"), + QColor("#ffeb3b")}; + QColor color = palette[active_graphs_.size() % palette.size()]; graph->setPen(QPen(color, 2.0)); graph->setData(x_times, y_values); - for (auto d : context_->backend->get_data().data_values) { + for (auto& d : context_->backend->get_data().data_values) { if (d.key == key) { graph->setName(QString::fromStdString(d.name)); break; @@ -321,7 +323,7 @@ void plot_page::plot_signal(const std::string key) { } graph->addToLegend(); - active_graphs_[key] = graph; + it->second = graph; plotted_counts_[key] = series.size(); plot_->rescaleAxes(); @@ -347,17 +349,15 @@ void plot_page::update_plot() { for (auto& [key, graph] : active_graphs_) { auto series = context_->backend->get_data().get_series(key); - std::size_t* already_plotted = &plotted_counts_[key]; - std::size_t total = series.size(); + usize* already_plotted = &plotted_counts_[key]; + usize total = series.size(); if (total < *already_plotted) { graph->data()->clear(); *already_plotted = 0; } - if (total == *already_plotted) { - continue; - } + if (total == *already_plotted) { continue; } QVector new_x; new_x.reserve(total - *already_plotted); @@ -380,14 +380,14 @@ void plot_page::update_stream() { auto series = context_->backend->get_data().get_raw_lines(); - std::size_t displayed = 0; + usize displayed = 0; if (text_log_->document()->isEmpty()) { displayed = 0; } else { - displayed = static_cast(text_log_->document()->blockCount()); + displayed = static_cast(text_log_->document()->blockCount()); } - std::size_t total = series.size(); + usize total = series.size(); if (total < displayed) { text_log_->clear(); @@ -396,7 +396,7 @@ void plot_page::update_stream() { if (total <= displayed) { return; } - for (std::size_t i = displayed; i < total; ++i) { + for (usize i = displayed; i < total; ++i) { text_log_->appendPlainText(QString::fromStdString(series[i])); } } diff --git a/src/core/time.cpp b/src/core/time.cpp index 62f5c7f..2aa7bd0 100644 --- a/src/core/time.cpp +++ b/src/core/time.cpp @@ -126,10 +126,10 @@ date_time::date_time() { const auto total_us = duration_cast(duration).count(); local = local_time{static_cast(lt.tm_hour), - static_cast(lt.tm_min), - static_cast(lt.tm_sec), - static_cast((total_us / 1'000) % 1'000), - static_cast(total_us % 1'000)}; + static_cast(lt.tm_min), + static_cast(lt.tm_sec), + static_cast((total_us / 1'000) % 1'000), + static_cast(total_us % 1'000)}; } date_time::date_time(u64 creation_time_seconds) { From 4bbaa8cc5bcf139c58db23561418bc47dd85ad15 Mon Sep 17 00:00:00 2001 From: Blake Date: Wed, 29 Jul 2026 13:41:46 -0400 Subject: [PATCH 25/36] standard colors and styling, lowkey annoying but it exists --- include/app/assets/style.hpp | 50 ++++++++++++++++ include/app/pages/analysis.hpp | 2 - src/app/assets/style.cpp | 102 +++++++++++++++++++++++++++++++++ src/app/gui.cpp | 63 ++++---------------- src/app/pages/plot.cpp | 75 ++++++++++-------------- 5 files changed, 191 insertions(+), 101 deletions(-) create mode 100644 include/app/assets/style.hpp create mode 100644 src/app/assets/style.cpp diff --git a/include/app/assets/style.hpp b/include/app/assets/style.hpp new file mode 100644 index 0000000..f5d6f68 --- /dev/null +++ b/include/app/assets/style.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +namespace mbr::ui::style { + +namespace color { + // Dark Mode Theme Standards + inline const QColor bg_dark{"#1e1e1e"}; + inline const QColor bg_hover{"#2a2a2a"}; + inline const QColor border{"#2d2d2d"}; + inline const QColor text_main{"#d4d4d4"}; + inline const QColor text_muted{"#a0a0a0"}; + inline const QColor accent{"#00adb5"}; + + // Status Indicator Colors + inline const QColor status_error{"#f44336"}; + inline const QColor status_warn{"#ffc107"}; + inline const QColor status_ok{"#00e676"}; + + // Graph Line Palette + inline const std::array graph_palette = { + "#00adb5", "#ff5722", "#e91e63", "#9c27b0", "#4caf50", "#ffeb3b" + }; + + QColor get_graph_color(std::size_t index); +} // namespace color + +// QSS Generators +struct button_style_options { + int font_size = 10; + int padding_v = 6; + int padding_h = 12; + int border_radius = 4; + QString align = "center"; +}; + +QString make_button_style(const button_style_options& opts = {}); +QString make_tree_style(); +QString make_menubar_style(int font_size_px = 20); +QString make_menu_style(int font_size_px = 20); +QString make_status_dot_style(const QColor& color, int diameter_px = 20); + +} // namespace mbr::ui::style diff --git a/include/app/pages/analysis.hpp b/include/app/pages/analysis.hpp index 92bf6f6..3cc0d5e 100644 --- a/include/app/pages/analysis.hpp +++ b/include/app/pages/analysis.hpp @@ -31,8 +31,6 @@ class analysis_page : public page { SHOCKDATA, }; - [[nodiscard]] static const char* data_type_string(data_view_t type); - public: explicit analysis_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : page(ctx, parent) {}; diff --git a/src/app/assets/style.cpp b/src/app/assets/style.cpp new file mode 100644 index 0000000..22b6abc --- /dev/null +++ b/src/app/assets/style.cpp @@ -0,0 +1,102 @@ +#include + +#include +#include + +#include +#include + +#include "app/assets/style.hpp" + +namespace mbr::ui::style { + +QColor color::get_graph_color(usize index) { + return QColor(graph_palette[index % graph_palette.size()]); +} + +QString make_button_style(const button_style_options& opts) { + return QString::fromStdString(fmt::format( + "QToolButton {{" + " background-color: {0}; color: {1}; border: 1px solid {2};" + " border-radius: {3}px; padding: {4}px {5}px; font: {6}pt; text-align: {7};" + "}}" + "QToolButton:hover {{ background-color: {8}; }}", + color::bg_dark.name().toStdString(), + color::text_main.name().toStdString(), + color::border.name().toStdString(), + opts.border_radius, + opts.padding_v, + opts.padding_h, + opts.font_size, + opts.align.toStdString(), + color::bg_hover.name().toStdString() + )); +} + +QString make_tree_style() { + return QString::fromStdString(fmt::format( + "QTreeWidget {{" + " background-color: {0};" + " color: {1};" + " border: 1px solid {2};" + " font: 14px;" + "}}", + color::bg_dark.name().toStdString(), + color::text_main.name().toStdString(), + color::border.name().toStdString(), + color::accent.name().toStdString(), + color::text_main.name().toStdString() + )); +} + +QString make_menubar_style(int font_size_px) { + return QString::fromStdString(fmt::format( + "QMenuBar {{" + " background-color: {0}; color: {1}; font: {2}px; spacing: 2px;" + " border-bottom: 1px solid {3};" + "}}" + "QMenuBar::item {{" + " background-color: transparent; padding: 4px 8px; border-radius: 4px; border: 1px solid transparent;" + "}}" + "QMenuBar::item:selected {{" + " background-color: {4}; border: 1px solid {3};" + "}}", + color::bg_dark.name().toStdString(), + color::text_main.name().toStdString(), + font_size_px, + color::border.name().toStdString(), + color::bg_hover.name().toStdString() + )); +} + +QString make_menu_style(int font_size_px) { + return QString::fromStdString(fmt::format( + "QMenu {{" + " background-color: {0}; color: {1}; font-size: {2}px; border: 1px solid {3}; padding: 4px;" + "}}" + "QMenu::item {{" + " background-color: transparent; padding: 4px 24px 4px 12px; border-radius: 4px; border: 1px solid transparent;" + "}}" + "QMenu::item:selected {{" + " background-color: {4}; border: 1px solid {3};" + "}}" + "QMenu::item:disabled {{ color: {5}; }}" + "QMenu::separator {{ height: 1px; background-color: {3}; margin: 4px 6px; }}", + color::bg_dark.name().toStdString(), + color::text_main.name().toStdString(), + font_size_px, + color::border.name().toStdString(), + color::bg_hover.name().toStdString(), + color::text_muted.name().toStdString() + )); +} + +QString make_status_dot_style(const QColor& color, int diameter_px) { + return QString::fromStdString(fmt::format( + "background-color: {}; border-radius: {}px;", + color.name().toStdString(), + diameter_px / 2 + )); +} + +} // namespace mbr::ui::style diff --git a/src/app/gui.cpp b/src/app/gui.cpp index 557252b..f3f6cb3 100644 --- a/src/app/gui.cpp +++ b/src/app/gui.cpp @@ -29,6 +29,7 @@ #include "app/pages/plot.hpp" #include "app/pages/serialmon.hpp" #include "app/pages/settings.hpp" +#include "app/assets/style.hpp" #include "core/time.hpp" #include "esp32/backend.hpp" #include "stdx/enum.hh" @@ -37,6 +38,9 @@ using namespace std::chrono; namespace mbr { +namespace style = mbr::ui::style; +namespace colors = mbr::ui::style::color; + gui_t::gui_t(const std::shared_ptr& ctx) : context_(ctx) { pages_ = new QStackedWidget(this); setCentralWidget(pages_); @@ -64,64 +68,17 @@ gui_t::create_page(page_type_t type, const std::shared_ptr& ctx, QW void gui_t::change_page(page_type_t type) { pages_->setCurrentWidget(page_lookup_[type]); } void gui_t::build_menu_bar() { - static const QString kMenuBarStyle = R"( - QMenuBar { - background-color: #2b2b2b; - color: white; - font-size: 20px; - spacing: 2px; - border-bottom: 1px solid #3C3F41; - } - QMenuBar::item { - background-color: transparent; - padding: 4px 6px; - border-radius: 4px; - border: 1px solid transparent; - } - QMenuBar::item:selected { - background-color: #3C3F41; - border: 1px solid #5A5D5F; - } - )"; - - static const QString kMenuStyle = R"( - QMenu { - background-color: #2b2b2b; - color: white; - font-size: 20px; - border: 1px solid #3C3F41; - padding: 4px; - } - QMenu::item { - background-color: transparent; - padding: 4px 24px 4px 12px; - border-radius: 4px; - border: 1px solid transparent; - } - QMenu::item:selected { - background-color: #3C3F41; - border: 1px solid #5A5D5F; - } - QMenu::item:disabled { - color: #6e6e6e; - } - QMenu::separator { - height: 1px; - background-color: #3C3F41; - margin: 4px 6px; - } - )"; - - menuBar()->setStyleSheet(kMenuBarStyle); + + menuBar()->setStyleSheet(style::make_menubar_style()); auto* file = menuBar()->addMenu("File"); - file->setStyleSheet(kMenuStyle); + file->setStyleSheet(style::make_menu_style()); file->addAction("Settings"); file->addAction("Export Log"); file->addAction("Exit", this, [] { QApplication::quit(); }); auto* nav = menuBar()->addMenu("Navigation"); - nav->setStyleSheet(kMenuStyle); + nav->setStyleSheet(style::make_menu_style()); nav->addAction("Home", this, [this] { change_page(page_type_t::HOME); }); nav->addAction("Plot", this, [this] { change_page(page_type_t::PLOT); }); nav->addAction("Serial", this, [this] { change_page(page_type_t::SERIAL); }); @@ -168,7 +125,7 @@ void gui_t::build_menu_bar() { status_dot_ = new QLabel(rightWidget); status_dot_->setFixedSize(20, 20); - status_dot_->setStyleSheet("background-color: #F44336; border-radius: 10px;"); + status_dot_->setStyleSheet(QString::fromStdString(fmt::format(fmt::runtime("background-color: {0}; border-radius: 10px;"), colors::status_error.name().toStdString()))); rightLayout->addWidget(connection_status_, 0, Qt::AlignVCenter); rightLayout->addWidget(status_dot_, 0, Qt::AlignVCenter); @@ -176,7 +133,7 @@ void gui_t::build_menu_bar() { } void gui_t::update_status_dot() { - QString color = !is_connected_ ? "#F44336" : !is_receiving_ ? "#FFC107" : "#00E676"; + QString color = !is_connected_ ? colors::status_error.name() : !is_receiving_ ? colors::status_warn.name() : colors::status_ok.name(); status_dot_->setStyleSheet(QString("background-color: %1; border-radius: 10px;").arg(color)); } diff --git a/src/app/pages/plot.cpp b/src/app/pages/plot.cpp index dbb68c3..affc49b 100644 --- a/src/app/pages/plot.cpp +++ b/src/app/pages/plot.cpp @@ -34,6 +34,7 @@ #include #include "app/backend_bridge.hpp" +#include "app/assets/style.hpp" #include "core/log.hpp" #include "esp32/backend.hpp" #include "esp32/data.hpp" @@ -42,6 +43,9 @@ namespace mbr::pages { +namespace style = mbr::ui::style; +namespace colors = mbr::ui::style::color; + void plot_page::on_enter() { log_info(context_->log, "Entered PlotPage"); } void plot_page::on_exit() { log_info(context_->log, "Exited PlotPage"); } @@ -60,26 +64,21 @@ void plot_page::build_page() { } QWidget* plot_page::build_lhs() { - QString button_style = - "QToolButton {" - " background-color: #1e1e1e; color: #d4d4d4; border: 1px solid #2d2d2d;" - " border-radius: 4px; padding: 4px 16px; font-size: 16px;" - "}" - "QToolButton:hover { background-color: #2a2a2a; }"; + style::button_style_options button_style; + button_style.font_size = 10; + auto* container = new QWidget(this); auto* v_layout = new QVBoxLayout(container); auto* h_layout = new QHBoxLayout(); auto* log_button = new QToolButton(); - auto* log_action = - new QAction(context_->backend->is_logging() ? "Stop Logging" : "Start Logging", this); - connect(log_action, &QAction::triggered, this, [this, log_action] { + log_button->setText(context_->backend->is_logging() ? "Stop Logging" : "Start Logging"); + connect(log_button, &QToolButton::clicked, this, [this, log_button] { context_->backend->set_logging(!context_->backend->is_logging()); - log_action->setText(context_->backend->is_logging() ? "Stop Logging" : "Start Logging"); + log_button->setText(context_->backend->is_logging() ? "Stop Logging" : "Start Logging"); }); - log_button->setDefaultAction(log_action); log_button->setMinimumHeight(32); - log_button->setStyleSheet(button_style); + log_button->setStyleSheet(style::make_button_style(button_style)); h_layout->addWidget(log_button); auto* download_name_input = new QLineEdit(); @@ -89,8 +88,8 @@ QWidget* plot_page::build_lhs() { h_layout->addWidget(download_name_input); auto* download_button = new QToolButton(); - auto* download_action = new QAction("Download Data", this); - connect(download_action, &QAction::triggered, this, [this, download_name_input] { + download_button->setText("Download Data"); + connect(download_button, &QToolButton::clicked, this, [this, download_name_input] { std::string name = download_name_input->text().toStdString(); local_time lt; @@ -122,8 +121,7 @@ QWidget* plot_page::build_lhs() { }); download_button->setMinimumHeight(32); - download_button->setStyleSheet(button_style); - download_button->setDefaultAction(download_action); + download_button->setStyleSheet(style::make_button_style(button_style)); h_layout->addWidget(download_button); text_log_ = new QPlainTextEdit(container); @@ -147,24 +145,20 @@ QWidget* plot_page::build_rhs() { plot_ = new QCustomPlot(container); - const QColor bg_color("#1e1e1e"); - const QColor axis_color("#a0a0a0"); - const QColor grid_color("#2d2d2d"); - const QColor line_color("#00adb5"); - plot_->setBackground(QBrush(bg_color)); - plot_->axisRect()->setBackground(QBrush(bg_color)); + plot_->setBackground(QBrush(colors::bg_dark)); + plot_->axisRect()->setBackground(QBrush(colors::bg_dark)); auto configure_axis = [&](QCPAxis* axis, const QString& label) { axis->setLabel(label); - axis->setBasePen(QPen(axis_color, 1.0)); - axis->setTickPen(QPen(axis_color, 1.0)); - axis->setSubTickPen(QPen(axis_color, 0.5)); - axis->setTickLabelColor(axis_color); - axis->setLabelColor(axis_color); - - axis->grid()->setPen(QPen(grid_color, 1.0, Qt::SolidLine)); - axis->grid()->setSubGridPen(QPen(grid_color, 0.5, Qt::DotLine)); + axis->setBasePen(QPen(colors::text_muted, 1.0)); + axis->setTickPen(QPen(colors::text_muted, 1.0)); + axis->setSubTickPen(QPen(colors::text_muted, 0.5)); + axis->setTickLabelColor(colors::text_muted); + axis->setLabelColor(colors::text_muted); + + axis->grid()->setPen(QPen(colors::border, 1.0, Qt::SolidLine)); + axis->grid()->setSubGridPen(QPen(colors::border, 0.5, Qt::DotLine)); axis->grid()->setSubGridVisible(true); }; @@ -178,8 +172,8 @@ QWidget* plot_page::build_rhs() { plot_->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom); plot_->replot(); plot_->legend->setVisible(true); - plot_->legend->setBrush(QBrush(bg_color)); - plot_->legend->setTextColor(axis_color); + plot_->legend->setBrush(QBrush(colors::bg_dark)); + plot_->legend->setTextColor(colors::text_muted); layout->addWidget(plot_, 1); @@ -237,19 +231,13 @@ QToolButton* plot_page::create_tree_dropdown(const std::vectorsetPopupMode(QToolButton::InstantPopup); select->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - select->setStyleSheet("QToolButton {" - " background-color: #1e1e1e; color: #d4d4d4; border: 1px solid #2d2d2d;" - " border-radius: 4px; padding: 6px 12px; text-align: left;" - "}" - "QToolButton:hover { background-color: #2a2a2a; }"); + select->setStyleSheet(style::make_button_style()); auto* tree = new QTreeWidget(select); tree->setSelectionMode(QAbstractItemView::ExtendedSelection); tree->setHeaderHidden(true); tree->setColumnCount(1); - tree->setStyleSheet( - "QTreeWidget { background-color: #1e1e1e; color: #d4d4d4; border: 1px solid #2d2d2d; }" - "QTreeWidget::item:hover { background-color: #00adb5; color: white; }"); + tree->setStyleSheet(style::make_tree_style()); for (auto it : data) { bool found = false; @@ -305,12 +293,7 @@ void plot_page::plot_signal(const std::string& key) { QCPGraph* graph = plot_->addGraph(); graph->setAdaptiveSampling(true); - static const std::array palette = {QColor("#00adb5"), - QColor("#ff5722"), - QColor("#e91e63"), - QColor("#9c27b0"), - QColor("#4caf50"), - QColor("#ffeb3b")}; + static const std::array palette = colors::graph_palette; QColor color = palette[active_graphs_.size() % palette.size()]; graph->setPen(QPen(color, 2.0)); From 3ddf4e58b370c79908397819a8e0b4522f4467a7 Mon Sep 17 00:00:00 2001 From: Blake Date: Wed, 29 Jul 2026 13:46:22 -0400 Subject: [PATCH 26/36] more pr fixes --- include/app/assets/style.hpp | 49 ++++++------- src/app/assets/style.cpp | 135 +++++++++++++++++------------------ src/app/gui.cpp | 10 ++- src/app/pages/plot.cpp | 19 +++-- 4 files changed, 106 insertions(+), 107 deletions(-) diff --git a/include/app/assets/style.hpp b/include/app/assets/style.hpp index f5d6f68..50ad3d5 100644 --- a/include/app/assets/style.hpp +++ b/include/app/assets/style.hpp @@ -11,34 +11,35 @@ namespace mbr::ui::style { namespace color { - // Dark Mode Theme Standards - inline const QColor bg_dark{"#1e1e1e"}; - inline const QColor bg_hover{"#2a2a2a"}; - inline const QColor border{"#2d2d2d"}; - inline const QColor text_main{"#d4d4d4"}; - inline const QColor text_muted{"#a0a0a0"}; - inline const QColor accent{"#00adb5"}; - - // Status Indicator Colors - inline const QColor status_error{"#f44336"}; - inline const QColor status_warn{"#ffc107"}; - inline const QColor status_ok{"#00e676"}; - - // Graph Line Palette - inline const std::array graph_palette = { - "#00adb5", "#ff5722", "#e91e63", "#9c27b0", "#4caf50", "#ffeb3b" - }; - - QColor get_graph_color(std::size_t index); + +// Dark Mode Theme Standards +inline const QColor bg_dark{"#1e1e1e"}; +inline const QColor bg_hover{"#2a2a2a"}; +inline const QColor border{"#2d2d2d"}; +inline const QColor text_main{"#d4d4d4"}; +inline const QColor text_muted{"#a0a0a0"}; +inline const QColor accent{"#00adb5"}; + +// Status Indicator Colors +inline const QColor status_error{"#f44336"}; +inline const QColor status_warn{"#ffc107"}; +inline const QColor status_ok{"#00e676"}; + +// Graph Line Palette +inline const std::array graph_palette = { + "#00adb5", "#ff5722", "#e91e63", "#9c27b0", "#4caf50", "#ffeb3b"}; + +QColor get_graph_color(std::size_t index); + } // namespace color // QSS Generators struct button_style_options { - int font_size = 10; - int padding_v = 6; - int padding_h = 12; - int border_radius = 4; - QString align = "center"; + int font_size = 10; + int padding_v = 6; + int padding_h = 12; + int border_radius = 4; + QString align = "center"; }; QString make_button_style(const button_style_options& opts = {}); diff --git a/src/app/assets/style.cpp b/src/app/assets/style.cpp index 22b6abc..d4d9f65 100644 --- a/src/app/assets/style.cpp +++ b/src/app/assets/style.cpp @@ -3,8 +3,8 @@ #include #include -#include #include +#include #include "app/assets/style.hpp" @@ -15,88 +15,83 @@ QColor color::get_graph_color(usize index) { } QString make_button_style(const button_style_options& opts) { - return QString::fromStdString(fmt::format( - "QToolButton {{" - " background-color: {0}; color: {1}; border: 1px solid {2};" - " border-radius: {3}px; padding: {4}px {5}px; font: {6}pt; text-align: {7};" - "}}" - "QToolButton:hover {{ background-color: {8}; }}", - color::bg_dark.name().toStdString(), - color::text_main.name().toStdString(), - color::border.name().toStdString(), - opts.border_radius, - opts.padding_v, - opts.padding_h, - opts.font_size, - opts.align.toStdString(), - color::bg_hover.name().toStdString() - )); + return QString::fromStdString( + fmt::format("QToolButton {{" + " background-color: {0}; color: {1}; border: 1px solid {2};" + " border-radius: {3}px; padding: {4}px {5}px; font: {6}pt; text-align: {7};" + "}}" + "QToolButton:hover {{ background-color: {8}; }}", + color::bg_dark.name().toStdString(), + color::text_main.name().toStdString(), + color::border.name().toStdString(), + opts.border_radius, + opts.padding_v, + opts.padding_h, + opts.font_size, + opts.align.toStdString(), + color::bg_hover.name().toStdString())); } QString make_tree_style() { - return QString::fromStdString(fmt::format( - "QTreeWidget {{" - " background-color: {0};" - " color: {1};" - " border: 1px solid {2};" - " font: 14px;" - "}}", - color::bg_dark.name().toStdString(), - color::text_main.name().toStdString(), - color::border.name().toStdString(), - color::accent.name().toStdString(), - color::text_main.name().toStdString() - )); + return QString::fromStdString(fmt::format("QTreeWidget {{" + " background-color: {0};" + " color: {1};" + " border: 1px solid {2};" + " font: 14px;" + "}}", + color::bg_dark.name().toStdString(), + color::text_main.name().toStdString(), + color::border.name().toStdString(), + color::accent.name().toStdString(), + color::text_main.name().toStdString())); } QString make_menubar_style(int font_size_px) { - return QString::fromStdString(fmt::format( - "QMenuBar {{" - " background-color: {0}; color: {1}; font: {2}px; spacing: 2px;" - " border-bottom: 1px solid {3};" - "}}" - "QMenuBar::item {{" - " background-color: transparent; padding: 4px 8px; border-radius: 4px; border: 1px solid transparent;" - "}}" - "QMenuBar::item:selected {{" - " background-color: {4}; border: 1px solid {3};" - "}}", - color::bg_dark.name().toStdString(), - color::text_main.name().toStdString(), - font_size_px, - color::border.name().toStdString(), - color::bg_hover.name().toStdString() - )); + return QString::fromStdString( + fmt::format("QMenuBar {{" + " background-color: {0}; color: {1}; font: {2}px; spacing: 2px;" + " border-bottom: 1px solid {3};" + "}}" + "QMenuBar::item {{" + " background-color: transparent; padding: 4px 8px; border-radius: 4px; " + "border: 1px solid transparent;" + "}}" + "QMenuBar::item:selected {{" + " background-color: {4}; border: 1px solid {3};" + "}}", + color::bg_dark.name().toStdString(), + color::text_main.name().toStdString(), + font_size_px, + color::border.name().toStdString(), + color::bg_hover.name().toStdString())); } QString make_menu_style(int font_size_px) { - return QString::fromStdString(fmt::format( - "QMenu {{" - " background-color: {0}; color: {1}; font-size: {2}px; border: 1px solid {3}; padding: 4px;" - "}}" - "QMenu::item {{" - " background-color: transparent; padding: 4px 24px 4px 12px; border-radius: 4px; border: 1px solid transparent;" - "}}" - "QMenu::item:selected {{" - " background-color: {4}; border: 1px solid {3};" - "}}" - "QMenu::item:disabled {{ color: {5}; }}" - "QMenu::separator {{ height: 1px; background-color: {3}; margin: 4px 6px; }}", - color::bg_dark.name().toStdString(), - color::text_main.name().toStdString(), - font_size_px, - color::border.name().toStdString(), - color::bg_hover.name().toStdString(), - color::text_muted.name().toStdString() - )); + return QString::fromStdString( + fmt::format("QMenu {{" + " background-color: {0}; color: {1}; font-size: {2}px; border: 1px solid " + "{3}; padding: 4px;" + "}}" + "QMenu::item {{" + " background-color: transparent; padding: 4px 24px 4px 12px; border-radius: " + "4px; border: 1px solid transparent;" + "}}" + "QMenu::item:selected {{" + " background-color: {4}; border: 1px solid {3};" + "}}" + "QMenu::item:disabled {{ color: {5}; }}" + "QMenu::separator {{ height: 1px; background-color: {3}; margin: 4px 6px; }}", + color::bg_dark.name().toStdString(), + color::text_main.name().toStdString(), + font_size_px, + color::border.name().toStdString(), + color::bg_hover.name().toStdString(), + color::text_muted.name().toStdString())); } QString make_status_dot_style(const QColor& color, int diameter_px) { return QString::fromStdString(fmt::format( - "background-color: {}; border-radius: {}px;", - color.name().toStdString(), - diameter_px / 2 - )); + "background-color: {}; border-radius: {}px;", color.name().toStdString(), diameter_px / 2)); } } // namespace mbr::ui::style diff --git a/src/app/gui.cpp b/src/app/gui.cpp index f3f6cb3..c6dc58c 100644 --- a/src/app/gui.cpp +++ b/src/app/gui.cpp @@ -21,6 +21,7 @@ #include #include "app/assets/images/app_icon.hpp" +#include "app/assets/style.hpp" #include "app/backend_bridge.hpp" #include "app/context.hpp" #include "app/pages/analysis.hpp" @@ -29,7 +30,6 @@ #include "app/pages/plot.hpp" #include "app/pages/serialmon.hpp" #include "app/pages/settings.hpp" -#include "app/assets/style.hpp" #include "core/time.hpp" #include "esp32/backend.hpp" #include "stdx/enum.hh" @@ -125,7 +125,9 @@ void gui_t::build_menu_bar() { status_dot_ = new QLabel(rightWidget); status_dot_->setFixedSize(20, 20); - status_dot_->setStyleSheet(QString::fromStdString(fmt::format(fmt::runtime("background-color: {0}; border-radius: 10px;"), colors::status_error.name().toStdString()))); + status_dot_->setStyleSheet(QString::fromStdString( + fmt::format(fmt::runtime("background-color: {0}; border-radius: 10px;"), + colors::status_error.name().toStdString()))); rightLayout->addWidget(connection_status_, 0, Qt::AlignVCenter); rightLayout->addWidget(status_dot_, 0, Qt::AlignVCenter); @@ -133,7 +135,9 @@ void gui_t::build_menu_bar() { } void gui_t::update_status_dot() { - QString color = !is_connected_ ? colors::status_error.name() : !is_receiving_ ? colors::status_warn.name() : colors::status_ok.name(); + QString color = !is_connected_ ? colors::status_error.name() + : !is_receiving_ ? colors::status_warn.name() + : colors::status_ok.name(); status_dot_->setStyleSheet(QString("background-color: %1; border-radius: 10px;").arg(color)); } diff --git a/src/app/pages/plot.cpp b/src/app/pages/plot.cpp index affc49b..085c8e1 100644 --- a/src/app/pages/plot.cpp +++ b/src/app/pages/plot.cpp @@ -33,8 +33,8 @@ #include -#include "app/backend_bridge.hpp" #include "app/assets/style.hpp" +#include "app/backend_bridge.hpp" #include "core/log.hpp" #include "esp32/backend.hpp" #include "esp32/data.hpp" @@ -145,7 +145,6 @@ QWidget* plot_page::build_rhs() { plot_ = new QCustomPlot(container); - plot_->setBackground(QBrush(colors::bg_dark)); plot_->axisRect()->setBackground(QBrush(colors::bg_dark)); @@ -332,25 +331,25 @@ void plot_page::update_plot() { for (auto& [key, graph] : active_graphs_) { auto series = context_->backend->get_data().get_series(key); - usize* already_plotted = &plotted_counts_[key]; + usize& already_plotted = plotted_counts_[key]; usize total = series.size(); - if (total < *already_plotted) { + if (total < already_plotted) { graph->data()->clear(); - *already_plotted = 0; + already_plotted = 0; } - if (total == *already_plotted) { continue; } + if (total == already_plotted) { continue; } QVector new_x; - new_x.reserve(total - *already_plotted); - for (auto it = time.begin() + *already_plotted; it != time.end(); ++it) { + new_x.reserve(total - already_plotted); + for (auto it = time.begin() + already_plotted; it != time.end(); ++it) { new_x.append(*it - time_offset_); } - QVector new_y(series.begin() + *already_plotted, series.end()); + QVector new_y(series.begin() + already_plotted, series.end()); graph->addData(new_x, new_y); - *already_plotted = total; + already_plotted = total; } plot_->xAxis->rescale(); From 44a9931e71ca13418c217500470522411220b4d5 Mon Sep 17 00:00:00 2001 From: Blake Date: Wed, 29 Jul 2026 16:47:31 -0400 Subject: [PATCH 27/36] home page is now being worked on --- include/app/backend_bridge.hpp | 3 + include/app/pages/home.hpp | 24 +++++--- include/esp32/backend.hpp | 7 +++ src/app/gui.cpp | 5 +- src/app/pages/home.cpp | 109 +++++++++++++++++++++++++++++++-- src/esp32/backend.cpp | 25 +++++--- 6 files changed, 146 insertions(+), 27 deletions(-) diff --git a/include/app/backend_bridge.hpp b/include/app/backend_bridge.hpp index 26e57fc..caa321a 100644 --- a/include/app/backend_bridge.hpp +++ b/include/app/backend_bridge.hpp @@ -17,11 +17,14 @@ class backend_bridge : public QObject { }); backend->set_on_connection_changed([this](bool c) { emit connection_changed(c); }); backend->set_on_receiving_changed([this](bool r) { emit receiving_changed(r); }); + backend->set_on_fields_received( + [this](const std::vector& keys) { emit fields_received(keys); }); } signals: void data_updated(const ankerl::unordered_dense::map>&); void connection_changed(bool connected); void receiving_changed(bool receiving); + void fields_received(const std::vector& keys); }; } // namespace mbr diff --git a/include/app/pages/home.hpp b/include/app/pages/home.hpp index 305c21d..31f9d2b 100644 --- a/include/app/pages/home.hpp +++ b/include/app/pages/home.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include "app/context.hpp" @@ -14,7 +15,9 @@ namespace mbr::pages { class home_page : public page { public: explicit home_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) - : page{ctx, parent} {} + : page{ctx, parent} { + build_page(); + } ~home_page() override = default; void on_enter() override; @@ -22,16 +25,19 @@ class home_page : public page { void build_page() override; private: - void build_rhs(); - void build_lhs(); + QWidget* build_rhs(); + QWidget* build_lhs(); + void connect_signals(); private: - ipv4_t previous_ip_; - ipv4_t ip_buf_; - std::string username_buf_; - std::string password_buf_; - std::string sd_name_; - std::string set_name_; + ipv4_t previous_ip_; + ipv4_t ip_buf_; + std::string username_buf_; + std::string password_buf_; + std::string sd_name_; + std::string set_name_; + ankerl::unordered_dense::map labels_; + ankerl::unordered_dense::map last_updated_; }; } // namespace mbr::pages diff --git a/include/esp32/backend.hpp b/include/esp32/backend.hpp index 3835051..67a195d 100644 --- a/include/esp32/backend.hpp +++ b/include/esp32/backend.hpp @@ -41,11 +41,13 @@ class telemetry_backend { using status_cb = std::function; using receiving_cb = std::function; using cmd_cb = std::function; + using fields_cb = std::function&)>; void set_on_data(data_cb cb) { on_data_ = std::move(cb); } void set_on_connection_changed(status_cb cb) { on_connection_changed_ = std::move(cb); } void set_on_receiving_changed(receiving_cb cb) { on_receiving_changed_ = std::move(cb); } void set_on_cmd(cmd_cb cb) { on_cmd_ = std::move(cb); } + void set_on_fields_received(fields_cb cb) { on_fields_ = std::move(cb); } private: void notify_data(const ankerl::unordered_dense::map>& s) { @@ -58,10 +60,15 @@ class telemetry_backend { if (on_receiving_changed_) { on_receiving_changed_(r); } } + void notify_fields_received(const std::vector& keys) { + if (on_fields_) { on_fields_(keys); } + } + data_cb on_data_; status_cb on_connection_changed_; receiving_cb on_receiving_changed_; cmd_cb on_cmd_; + fields_cb on_fields_; public: explicit telemetry_backend(const std::filesystem::path& json_path, log_fn_t log = nullptr); diff --git a/src/app/gui.cpp b/src/app/gui.cpp index c6dc58c..9ccc379 100644 --- a/src/app/gui.cpp +++ b/src/app/gui.cpp @@ -125,9 +125,8 @@ void gui_t::build_menu_bar() { status_dot_ = new QLabel(rightWidget); status_dot_->setFixedSize(20, 20); - status_dot_->setStyleSheet(QString::fromStdString( - fmt::format(fmt::runtime("background-color: {0}; border-radius: 10px;"), - colors::status_error.name().toStdString()))); + status_dot_->setStyleSheet(QString::fromStdString(fmt::format( + "background-color: {0}; border-radius: 10px;", colors::status_error.name().toStdString()))); rightLayout->addWidget(connection_status_, 0, Qt::AlignVCenter); rightLayout->addWidget(status_dot_, 0, Qt::AlignVCenter); diff --git a/src/app/pages/home.cpp b/src/app/pages/home.cpp index 37ce0b1..3791cfa 100644 --- a/src/app/pages/home.cpp +++ b/src/app/pages/home.cpp @@ -1,29 +1,128 @@ #include "app/pages/home.hpp" +#include #include #include +#include +#include +#include +#include +#include +#include + #include #include #include #include +#include "app/assets/style.hpp" #include "core/log.hpp" #include "core/time.hpp" namespace mbr::pages { +namespace style = mbr::ui::style; +namespace colors = mbr::ui::style::color; + void home_page::on_enter() { log_info(context_->log, "Entered HomePage"); } void home_page::on_exit() { log_info(context_->log, "Exited HomePage"); } void home_page::build_page() { - PROFILE_FUNCTION(); - build_rhs(); - build_lhs(); + auto* layout = new QHBoxLayout(this); + // layout->setContentsMargins(8, 8, 8, 8); + layout->setSpacing(1); + + auto* lhs = build_lhs(); + auto* rhs = build_rhs(); + + layout->addWidget(lhs, 1); + layout->addWidget(rhs, 1); + + connect_signals(); +} + +QWidget* home_page::build_rhs() { + auto* container = new QWidget; + auto* v_layout = new QVBoxLayout(container); + auto* jason_grid = new QGridLayout(); + + auto* title = new QLabel("Data Status"); + title->setStyleSheet(QString::fromStdString( + fmt::format("color: {}; font: 12pt", colors::text_main.name().toStdString()))); + + auto* line = new QFrame; + line->setFrameShape(QFrame::HLine); + line->setFrameShadow(QFrame::Sunken); + + usize row = 0; + for (auto& d : context_->backend->get_data().data_values) { + auto* label = new QLabel(QString::fromStdString(d.name)); + label->setStyleSheet(QString::fromStdString( + fmt::format("color: {}; font: 12pt", colors::text_main.name().toStdString()))); + jason_grid->addWidget(label, row, 0); + + auto* status = new QLabel(); + status->setFixedSize(20, 20); + status->setStyleSheet( + QString::fromStdString(fmt::format("background-color: {}; border-radius: 10px;", + colors::status_error.name().toStdString()))); + jason_grid->addWidget(status, row, 1); + labels_.try_emplace(QString::fromStdString(d.key), status); + row++; + } + jason_grid->setColumnStretch(2, 1); + + v_layout->addWidget(title); + v_layout->addWidget(line); + v_layout->addLayout(jason_grid); + v_layout->addStretch(); + + return container; } -void home_page::build_rhs() {} +QWidget* home_page::build_lhs() { + auto* container = new QWidget; + auto* v_layout = new QVBoxLayout(container); + // auto* jason_grid = new QGridLayout(); + + auto* title = new QLabel("hello world"); -void home_page::build_lhs() {} + v_layout->addWidget(title); + return container; +} + +void home_page::connect_signals() { + connect(context_->bridge.get(), + &backend_bridge::fields_received, + this, + [this](const std::vector& keys) { + const auto now = std::chrono::steady_clock::now(); + for (const auto& key : keys) { + auto qkey = QString::fromStdString(key); + last_updated_[qkey] = now; + if (auto it = labels_.find(qkey); it != labels_.end()) { + it->second->setStyleSheet(QString::fromStdString( + fmt::format("background-color: {}; border-radius: 10px;", + colors::status_ok.name().toStdString()))); + } + } + }); + + auto* timer = new QTimer(this); + connect(timer, &QTimer::timeout, this, [this]() { + const auto now = std::chrono::steady_clock::now(); + for (const auto& [key, time] : last_updated_) { + if (now - time > std::chrono::milliseconds(250)) { + if (auto it = labels_.find(key); it != labels_.end()) { + it->second->setStyleSheet(QString::fromStdString( + fmt::format("background-color: {}; border-radius: 10px;", + colors::status_error.name().toStdString()))); + } + } + } + }); + timer->start(100); +} } // namespace mbr::pages diff --git a/src/esp32/backend.cpp b/src/esp32/backend.cpp index d065411..dbcb508 100644 --- a/src/esp32/backend.cpp +++ b/src/esp32/backend.cpp @@ -166,18 +166,23 @@ void telemetry_backend::on_message(const ix::WebSocketMessagePtr& msg) { // Now we can safely unpack the packet const std::unique_lock lock{data_latch_}; - // write data - if (!is_logging_) { continue; } - + // Write data and add keys received to signal + std::vector received_keys; + received_keys.reserve(parsed.value().size()); for (const auto& [ident, value] : parsed.value()) { - data_.write_data(std::string{ident}, std::string{value}); + received_keys.push_back(std::string{ident}); + if (is_logging_) { data_.write_data(std::string{ident}, std::string{value}); } } - notify_data(data_.get_series()); - data_.write_raw_line(line); - for (const auto& [ident, value] : parsed.value()) { - if (ident == "W") { - data_.save_current_line(line); - break; + notify_fields_received(received_keys); + + if (is_logging_) { + notify_data(data_.get_series()); + data_.write_raw_line(line); + for (const auto& [ident, value] : parsed.value()) { + if (ident == "W") { + data_.save_current_line(line); + break; + } } } } From f21ea064c9eccab5ed5bee81c9b22598b9c4dad5 Mon Sep 17 00:00:00 2001 From: Blake Watson Date: Wed, 29 Jul 2026 18:41:38 -0400 Subject: [PATCH 28/36] formatting --- src/core/time.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/time.cpp b/src/core/time.cpp index 2aa7bd0..62f5c7f 100644 --- a/src/core/time.cpp +++ b/src/core/time.cpp @@ -126,10 +126,10 @@ date_time::date_time() { const auto total_us = duration_cast(duration).count(); local = local_time{static_cast(lt.tm_hour), - static_cast(lt.tm_min), - static_cast(lt.tm_sec), - static_cast((total_us / 1'000) % 1'000), - static_cast(total_us % 1'000)}; + static_cast(lt.tm_min), + static_cast(lt.tm_sec), + static_cast((total_us / 1'000) % 1'000), + static_cast(total_us % 1'000)}; } date_time::date_time(u64 creation_time_seconds) { From 9f79f61c09ef87b6c55aa6c005d78fa515e49dc9 Mon Sep 17 00:00:00 2001 From: Blake Watson Date: Wed, 29 Jul 2026 22:11:46 -0400 Subject: [PATCH 29/36] pr stuff --- include/app/backend_bridge.hpp | 14 ++++---- include/app/context.hpp | 24 +++++++------- include/app/gui.hpp | 2 +- include/app/pages/analysis.hpp | 15 ++------- include/app/pages/home.hpp | 4 +-- include/app/pages/page.hpp | 4 +-- include/app/pages/plot.hpp | 12 +++---- include/app/pages/serialmon.hpp | 4 +-- include/app/pages/settings.hpp | 4 +-- src/app/app.cpp | 2 +- src/app/gui.cpp | 59 +++++++++++++++++---------------- src/app/pages/analysis.cpp | 4 +-- src/app/pages/home.cpp | 14 ++++---- src/app/pages/plot.cpp | 17 +++++----- src/app/pages/serialmon.cpp | 4 +-- src/app/pages/settings.cpp | 4 +-- 16 files changed, 89 insertions(+), 98 deletions(-) diff --git a/include/app/backend_bridge.hpp b/include/app/backend_bridge.hpp index caa321a..da06671 100644 --- a/include/app/backend_bridge.hpp +++ b/include/app/backend_bridge.hpp @@ -4,20 +4,20 @@ #include #include -namespace mbr { +namespace mbr::ui::bridge { class backend_bridge : public QObject { Q_OBJECT public: - explicit backend_bridge(telemetry_backend* backend, QObject* parent = nullptr) + explicit backend_bridge(telemetry_backend& backend, QObject* parent = nullptr) : QObject(parent) { - backend->set_on_data( + backend.set_on_data( [this](const ankerl::unordered_dense::map>& d) { emit data_updated(d); }); - backend->set_on_connection_changed([this](bool c) { emit connection_changed(c); }); - backend->set_on_receiving_changed([this](bool r) { emit receiving_changed(r); }); - backend->set_on_fields_received( + backend.set_on_connection_changed([this](bool c) { emit connection_changed(c); }); + backend.set_on_receiving_changed([this](bool r) { emit receiving_changed(r); }); + backend.set_on_fields_received( [this](const std::vector& keys) { emit fields_received(keys); }); } signals: @@ -27,4 +27,4 @@ class backend_bridge : public QObject { void fields_received(const std::vector& keys); }; -} // namespace mbr +} // namespace mbr::ui::bridge diff --git a/include/app/context.hpp b/include/app/context.hpp index 2eb78cf..4d150bb 100644 --- a/include/app/context.hpp +++ b/include/app/context.hpp @@ -17,18 +17,18 @@ enum class page_type_t : u8 { HOME, PLOT, ANALYSIS, - SERIAL, + SERIAL_MONITOR, SETTINGS, }; [[nodiscard]] constexpr const char* page_type_str(page_type_t page_type) { switch (page_type) { - case page_type_t::HOME: return "Home"; - case page_type_t::PLOT: return "Plot"; - case page_type_t::ANALYSIS: return "Analysis"; - case page_type_t::SERIAL: return "Serial Monitor"; - case page_type_t::SETTINGS: return "Settings"; - default: return "Unknown"; + case page_type_t::HOME: return "Home"; + case page_type_t::PLOT: return "Plot"; + case page_type_t::ANALYSIS: return "Analysis"; + case page_type_t::SERIAL_MONITOR: return "Serial Monitor"; + case page_type_t::SETTINGS: return "Settings"; + default: return "Unknown"; } } @@ -40,11 +40,11 @@ struct app_context { std::atomic should_exit{false}; bool is_cmd_input_focused{false}; - std::unique_ptr backend; - std::unique_ptr bridge; - std::string username; - std::string password; - log_fn_t log; + std::unique_ptr backend; + std::unique_ptr bridge; + std::string username; + std::string password; + log_fn_t log; }; } // namespace mbr diff --git a/include/app/gui.hpp b/include/app/gui.hpp index af2b3d5..26c8824 100644 --- a/include/app/gui.hpp +++ b/include/app/gui.hpp @@ -29,7 +29,7 @@ class gui_t : public QMainWindow { void change_page(page_type_t type); void build_menu_bar(); void update_status_dot(); - [[nodiscard]] pages::page* + [[nodiscard]] ui::pages::page* create_page(page_type_t type, const std::shared_ptr& ctx, QWidget* parent); private: diff --git a/include/app/pages/analysis.hpp b/include/app/pages/analysis.hpp index 3cc0d5e..9d29cc8 100644 --- a/include/app/pages/analysis.hpp +++ b/include/app/pages/analysis.hpp @@ -18,19 +18,9 @@ #include "app/pages/page.hpp" #include "core/time.hpp" -namespace mbr::pages { +namespace mbr::ui::pages { class analysis_page : public page { - public: - using selected_video_t = stdx::option>>; - using selected_txt_file_t = stdx::option; - - enum class data_view_t : u8 { - ALL, - RPMDATA, - SHOCKDATA, - }; - public: explicit analysis_page(const std::shared_ptr& ctx, QWidget* parent = nullptr) : page(ctx, parent) {}; @@ -41,7 +31,6 @@ class analysis_page : public page { void build_page() override; private: - private: }; -} // namespace mbr::pages +} // namespace mbr::ui::pages diff --git a/include/app/pages/home.hpp b/include/app/pages/home.hpp index 31f9d2b..b2802b7 100644 --- a/include/app/pages/home.hpp +++ b/include/app/pages/home.hpp @@ -10,7 +10,7 @@ #include "app/pages/page.hpp" #include "core/ip.hpp" -namespace mbr::pages { +namespace mbr::ui::pages { class home_page : public page { public: @@ -40,4 +40,4 @@ class home_page : public page { ankerl::unordered_dense::map last_updated_; }; -} // namespace mbr::pages +} // namespace mbr::ui::pages diff --git a/include/app/pages/page.hpp b/include/app/pages/page.hpp index 773d025..7916c26 100644 --- a/include/app/pages/page.hpp +++ b/include/app/pages/page.hpp @@ -6,7 +6,7 @@ #include "app/context.hpp" -namespace mbr::pages { +namespace mbr::ui::pages { class page : public QWidget { Q_OBJECT @@ -24,4 +24,4 @@ class page : public QWidget { std::shared_ptr context_; }; -} // namespace mbr::pages +} // namespace mbr::ui::pages diff --git a/include/app/pages/plot.hpp b/include/app/pages/plot.hpp index a3885c0..7fea55a 100644 --- a/include/app/pages/plot.hpp +++ b/include/app/pages/plot.hpp @@ -25,7 +25,7 @@ #include "app/pages/page.hpp" #include "esp32/data.hpp" -namespace mbr::pages { +namespace mbr::ui::pages { class plot_page : public page { public: @@ -55,14 +55,14 @@ class plot_page : public page { ankerl::unordered_dense::map active_graphs_; ankerl::unordered_dense::map plotted_counts_; - bool pending_update_ = false; - QTimer* redraw_timer_ = nullptr; + QTimer* redraw_timer_ = nullptr; - double time_offset_ = 0.0; - bool has_offset_ = false; + double time_offset_ = 0.0; + bool pending_update_ = false; + bool has_offset_ = false; private slots: void on_data_updated(); }; -} // namespace mbr::pages +} // namespace mbr::ui::pages diff --git a/include/app/pages/serialmon.hpp b/include/app/pages/serialmon.hpp index 92c5f14..49aaf76 100644 --- a/include/app/pages/serialmon.hpp +++ b/include/app/pages/serialmon.hpp @@ -8,7 +8,7 @@ #include "app/pages/page.hpp" -namespace mbr::pages { +namespace mbr::ui::pages { class serial_page : public page { public: @@ -25,4 +25,4 @@ class serial_page : public page { std::string serial_buffer_; }; -} // namespace mbr::pages +} // namespace mbr::ui::pages diff --git a/include/app/pages/settings.hpp b/include/app/pages/settings.hpp index 2fa5c10..9acb5ee 100644 --- a/include/app/pages/settings.hpp +++ b/include/app/pages/settings.hpp @@ -9,7 +9,7 @@ #include "app/pages/page.hpp" #include "core/ip.hpp" -namespace mbr::pages { +namespace mbr::ui::pages { class settings_page : public page { public: @@ -25,4 +25,4 @@ class settings_page : public page { private: }; -} // namespace mbr::pages +} // namespace mbr::ui::pages diff --git a/src/app/app.cpp b/src/app/app.cpp index 9ce4e05..4d5856e 100644 --- a/src/app/app.cpp +++ b/src/app/app.cpp @@ -27,7 +27,7 @@ app_t::app_t(i32 argc, char** argv) context_->log = context_->logger.get_log_fn(); ix::initNetSystem(); context_->backend = std::make_unique(DEFAULT_JSON_PATH, context_->log); - context_->bridge = std::make_unique(context_->backend.get()); + context_->bridge = std::make_unique(*context_->backend); gui_ = std::make_unique(context_); context_->backend->start(); } diff --git a/src/app/gui.cpp b/src/app/gui.cpp index 9ccc379..3cc28ba 100644 --- a/src/app/gui.cpp +++ b/src/app/gui.cpp @@ -38,8 +38,9 @@ using namespace std::chrono; namespace mbr { -namespace style = mbr::ui::style; -namespace colors = mbr::ui::style::color; +namespace style = ui::style; +namespace colors = ui::style::color; +namespace page = ui::pages; gui_t::gui_t(const std::shared_ptr& ctx) : context_(ctx) { pages_ = new QStackedWidget(this); @@ -53,15 +54,15 @@ gui_t::gui_t(const std::shared_ptr& ctx) : context_(ctx) { build_menu_bar(); } -[[nodiscard]] pages::page* +[[nodiscard]] page::page* gui_t::create_page(page_type_t type, const std::shared_ptr& ctx, QWidget* parent) { switch (type) { - case page_type_t::HOME: return new pages::home_page(ctx, parent); - case page_type_t::PLOT: return new pages::plot_page(ctx, parent); - case page_type_t::ANALYSIS: return new pages::analysis_page(ctx, parent); - case page_type_t::SERIAL: return new pages::serial_page(ctx, parent); - case page_type_t::SETTINGS: return new pages::settings_page(ctx, parent); - default: return nullptr; + case page_type_t::HOME: return new page::home_page(ctx, parent); + case page_type_t::PLOT: return new page::plot_page(ctx, parent); + case page_type_t::ANALYSIS: return new page::analysis_page(ctx, parent); + case page_type_t::SERIAL_MONITOR: return new page::serial_page(ctx, parent); + case page_type_t::SETTINGS: return new page::settings_page(ctx, parent); + default: return nullptr; } } @@ -81,7 +82,7 @@ void gui_t::build_menu_bar() { nav->setStyleSheet(style::make_menu_style()); nav->addAction("Home", this, [this] { change_page(page_type_t::HOME); }); nav->addAction("Plot", this, [this] { change_page(page_type_t::PLOT); }); - nav->addAction("Serial", this, [this] { change_page(page_type_t::SERIAL); }); + nav->addAction("Serial", this, [this] { change_page(page_type_t::SERIAL_MONITOR); }); nav->addAction("Analysis", this, [this] { change_page(page_type_t::ANALYSIS); }); menuBar()->addAction("Sync Time", this, [this] { @@ -101,26 +102,26 @@ void gui_t::build_menu_bar() { connection_status_ = new QLabel("Disconnected", rightWidget); connection_status_->setStyleSheet("font-size: 20px;"); - connection_status_->setMinimumWidth( - connection_status_->fontMetrics().horizontalAdvance("Disconnected") + 4); + connection_status_->setMinimumWidth(120); connection_status_->setAlignment(Qt::AlignRight | Qt::AlignVCenter); - int w = std::max(connection_status_->fontMetrics().horizontalAdvance("Connected"), - connection_status_->fontMetrics().horizontalAdvance("Disconnected")); - connection_status_->setMinimumWidth(w + 4); - - connect( - context_->bridge.get(), &backend_bridge::connection_changed, this, [this](bool connected) { - is_connected_ = connected; - if (!connected) { is_receiving_ = false; } - connection_status_->setText(connected ? "Connected" : "Disconnected"); - update_status_dot(); - }); - - connect( - context_->bridge.get(), &backend_bridge::receiving_changed, this, [this](bool receiving) { - is_receiving_ = receiving; - update_status_dot(); - }); + + connect(context_->bridge.get(), + &ui::bridge::backend_bridge::connection_changed, + this, + [this](bool connected) { + is_connected_ = connected; + if (!connected) { is_receiving_ = false; } + connection_status_->setText(connected ? "Connected" : "Disconnected"); + update_status_dot(); + }); + + connect(context_->bridge.get(), + &ui::bridge::backend_bridge::receiving_changed, + this, + [this](bool receiving) { + is_receiving_ = receiving; + update_status_dot(); + }); connection_status_->setStyleSheet("font-size: 20px;"); status_dot_ = new QLabel(rightWidget); diff --git a/src/app/pages/analysis.cpp b/src/app/pages/analysis.cpp index 38d25d4..99cc14e 100644 --- a/src/app/pages/analysis.cpp +++ b/src/app/pages/analysis.cpp @@ -29,11 +29,11 @@ using namespace std::chrono_literals; -namespace mbr::pages { +namespace mbr::ui::pages { void analysis_page::on_enter() { log_info(context_->log, "Entered AnalysisPage"); } void analysis_page::on_exit() { log_info(context_->log, "Exited AnalysisPage"); } void analysis_page::build_page() { PROFILE_FUNCTION(); } -} // namespace mbr::pages +} // namespace mbr::ui::pages diff --git a/src/app/pages/home.cpp b/src/app/pages/home.cpp index 3791cfa..589375a 100644 --- a/src/app/pages/home.cpp +++ b/src/app/pages/home.cpp @@ -20,10 +20,12 @@ #include "core/log.hpp" #include "core/time.hpp" -namespace mbr::pages { +namespace mbr::ui::pages { -namespace style = mbr::ui::style; -namespace colors = mbr::ui::style::color; +namespace style = ui::style; +namespace colors = ui::style::color; + +using namespace std::chrono_literals; void home_page::on_enter() { log_info(context_->log, "Entered HomePage"); } void home_page::on_exit() { log_info(context_->log, "Exited HomePage"); } @@ -94,7 +96,7 @@ QWidget* home_page::build_lhs() { void home_page::connect_signals() { connect(context_->bridge.get(), - &backend_bridge::fields_received, + &ui::bridge::backend_bridge::fields_received, this, [this](const std::vector& keys) { const auto now = std::chrono::steady_clock::now(); @@ -113,7 +115,7 @@ void home_page::connect_signals() { connect(timer, &QTimer::timeout, this, [this]() { const auto now = std::chrono::steady_clock::now(); for (const auto& [key, time] : last_updated_) { - if (now - time > std::chrono::milliseconds(250)) { + if (now - time > 250ms) { if (auto it = labels_.find(key); it != labels_.end()) { it->second->setStyleSheet(QString::fromStdString( fmt::format("background-color: {}; border-radius: 10px;", @@ -125,4 +127,4 @@ void home_page::connect_signals() { timer->start(100); } -} // namespace mbr::pages +} // namespace mbr::ui::pages diff --git a/src/app/pages/plot.cpp b/src/app/pages/plot.cpp index 085c8e1..7d85bb6 100644 --- a/src/app/pages/plot.cpp +++ b/src/app/pages/plot.cpp @@ -41,10 +41,10 @@ #include -namespace mbr::pages { +namespace mbr::ui::pages { -namespace style = mbr::ui::style; -namespace colors = mbr::ui::style::color; +namespace style = ui::style; +namespace colors = ui::style::color; void plot_page::on_enter() { log_info(context_->log, "Entered PlotPage"); } void plot_page::on_exit() { log_info(context_->log, "Exited PlotPage"); } @@ -103,7 +103,6 @@ QWidget* plot_page::build_lhs() { auto raw_lines = context_->backend->get_data().get_raw_lines(); if (raw_lines.empty()) { log_warn(context_->log, "Cannot download data as the data buffer is empty!"); - download_name_input->clear(); return; } @@ -111,7 +110,6 @@ QWidget* plot_page::build_lhs() { if (!out.is_open()) { log_error(context_->log, fmt::format("Failed to open output file: {}", std::strerror(errno))); - download_name_input->clear(); return; } @@ -207,9 +205,10 @@ QWidget* plot_page::build_rhs() { void plot_page::plot_timer() { - connect(context_->bridge.get(), &backend_bridge::data_updated, this, [this](const auto&) { - pending_update_ = true; - }); + connect(context_->bridge.get(), + &ui::bridge::backend_bridge::data_updated, + this, + [this](const auto&) { pending_update_ = true; }); redraw_timer_ = new QTimer(this); connect(redraw_timer_, &QTimer::timeout, this, [this]() { @@ -383,4 +382,4 @@ void plot_page::update_stream() { } } -} // namespace mbr::pages +} // namespace mbr::ui::pages diff --git a/src/app/pages/serialmon.cpp b/src/app/pages/serialmon.cpp index 85d96d4..b6cbefd 100644 --- a/src/app/pages/serialmon.cpp +++ b/src/app/pages/serialmon.cpp @@ -10,11 +10,11 @@ #include "core/log.hpp" #include "esp32/serial.hpp" -namespace mbr::pages { +namespace mbr::ui::pages { void serial_page::on_enter() { log_info(context_->log, "Entered SerialPage"); } void serial_page::on_exit() { log_info(context_->log, "Exited SerialPage"); } void serial_page::build_page() { PROFILE_FUNCTION(); } -} // namespace mbr::pages +} // namespace mbr::ui::pages diff --git a/src/app/pages/settings.cpp b/src/app/pages/settings.cpp index c34e036..5d1e84a 100644 --- a/src/app/pages/settings.cpp +++ b/src/app/pages/settings.cpp @@ -12,11 +12,11 @@ #include "core/log.hpp" -namespace mbr::pages { +namespace mbr::ui::pages { void settings_page::on_enter() { log_info(context_->log, "Entered SettingsPage"); } void settings_page::on_exit() { log_info(context_->log, "Exited SettingsPage"); } void settings_page::build_page() {} -} // namespace mbr::pages +} // namespace mbr::ui::pages From 40f174feb68bbb7b21bc262f5ef7a022290fbd74 Mon Sep 17 00:00:00 2001 From: Blake Watson Date: Wed, 29 Jul 2026 22:18:59 -0400 Subject: [PATCH 30/36] pr stuff --- src/app/pages/plot.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/pages/plot.cpp b/src/app/pages/plot.cpp index 7d85bb6..498779b 100644 --- a/src/app/pages/plot.cpp +++ b/src/app/pages/plot.cpp @@ -237,11 +237,13 @@ QToolButton* plot_page::create_tree_dropdown(const std::vectorsetColumnCount(1); tree->setStyleSheet(style::make_tree_style()); - for (auto it : data) { + QString current_group; + for (auto& it : data) { bool found = false; QTreeWidgetItem* target_group = nullptr; + current_group = QString::fromStdString(it.group); for (i32 i = 0; i < tree->topLevelItemCount(); ++i) { - if (QString::fromStdString(it.group) == tree->topLevelItem(i)->text(0)) { + if (current_group == tree->topLevelItem(i)->text(0)) { found = true; target_group = tree->topLevelItem(i); break; From a011d15e7dcaf31dddb51993131d142580800f02 Mon Sep 17 00:00:00 2001 From: Blake Date: Thu, 30 Jul 2026 09:52:42 -0400 Subject: [PATCH 31/36] more pr stuff --- .github/workflows/package.yml | 14 ++++++++++++++ include/esp32/data.hpp | 14 +++++++------- src/app/pages/plot.cpp | 10 ++++++---- tests/esp32/test_data.cpp | 10 +++++----- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index d919ec0..b540fee 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -84,3 +84,17 @@ jobs: path: | *.tar.gz *.zip + + release: + needs: package + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + - uses: softprops/action-gh-release@v2 + with: + files: artifacts/* diff --git a/include/esp32/data.hpp b/include/esp32/data.hpp index 3878816..dbc5554 100644 --- a/include/esp32/data.hpp +++ b/include/esp32/data.hpp @@ -46,19 +46,19 @@ class telemetry_data { [[nodiscard]] const std::string& get_current_line() { return current_line_; } template - [[nodiscard]] auto& get_series(this Self&& self, const std::string& key) noexcept { + [[nodiscard]] auto get_series(this Self&& self, const std::string& key) noexcept { + using ret_t = stdx::option>&>; + auto it = self.series_.find(key); - if (it != self.series_.end()) { return it->second; } + if (it != self.series_.end()) { return ret_t{it->second}; } - if (!self.logged_missing_keys_.contains(key)) { - self.logged_missing_keys_.insert(key); + if (self.logged_missing_keys_.insert(key).second) { log_error(self.log_, "Key '{}' not found in telemetry configuration!", key); } - static stdx::const_dispatch_t> dummy; - return dummy; + return ret_t{}; } - [[nodiscard]] auto& get_series() noexcept { return series_; } + [[nodiscard]] const auto& get_series() noexcept { return series_; } void set_sync_lt(local_time lt) noexcept { sync_lt_ = lt; } void write_data(const std::string& identifier, const std::string& value); diff --git a/src/app/pages/plot.cpp b/src/app/pages/plot.cpp index 498779b..08fd045 100644 --- a/src/app/pages/plot.cpp +++ b/src/app/pages/plot.cpp @@ -278,6 +278,7 @@ void plot_page::plot_signal(const std::string& key) { auto [it, inserted] = active_graphs_.try_emplace(key); if (!inserted) { return; } auto series = context_->backend->get_data().get_series(key); + if (!series) { return; } auto time = context_->backend->get_data().get_time(); if (!has_offset_ && !time.empty()) { @@ -288,7 +289,7 @@ void plot_page::plot_signal(const std::string& key) { QVector x_times; x_times.reserve(time.size()); for (auto t : time) { x_times.append(t - time_offset_); } - QVector y_values(series.begin(), series.end()); + QVector y_values(series->begin(), series->end()); QCPGraph* graph = plot_->addGraph(); graph->setAdaptiveSampling(true); @@ -307,7 +308,7 @@ void plot_page::plot_signal(const std::string& key) { graph->addToLegend(); it->second = graph; - plotted_counts_[key] = series.size(); + plotted_counts_[key] = series->size(); plot_->rescaleAxes(); plot_->replot(); @@ -331,9 +332,10 @@ void plot_page::update_plot() { for (auto& [key, graph] : active_graphs_) { auto series = context_->backend->get_data().get_series(key); + if (!series) { return; } usize& already_plotted = plotted_counts_[key]; - usize total = series.size(); + usize total = series->size(); if (total < already_plotted) { graph->data()->clear(); @@ -347,7 +349,7 @@ void plot_page::update_plot() { for (auto it = time.begin() + already_plotted; it != time.end(); ++it) { new_x.append(*it - time_offset_); } - QVector new_y(series.begin() + already_plotted, series.end()); + QVector new_y(series->begin() + already_plotted, series->end()); graph->addData(new_x, new_y); already_plotted = total; diff --git a/tests/esp32/test_data.cpp b/tests/esp32/test_data.cpp index ab389ab..5440306 100644 --- a/tests/esp32/test_data.cpp +++ b/tests/esp32/test_data.cpp @@ -41,15 +41,15 @@ TEST_CASE("telemetry_data config loading and data writing") { const auto& series_t = data.get_series("T"); const auto& series_e = data.get_series("E"); - REQUIRE(series_t.size() == 1); - CHECK(series_t[0] == Catch::Approx(60'000'000.0)); - REQUIRE(series_e.size() == 1); - CHECK(series_e[0] == Catch::Approx(3500.5)); + REQUIRE(series_t.get()->size() == 1); + CHECK(series_t.get()->at(0) == Catch::Approx(60'000'000.0)); + REQUIRE(series_e.get()->size() == 1); + CHECK(series_e.get()->at(0) == Catch::Approx(3500.5)); } SECTION("Handling missing keys") { const auto& series_missing = data.get_series("UNKNOWN_KEY"); - CHECK(series_missing.empty()); + CHECK(series_missing.get()->empty()); std::string logs = logger.get_streamed_logs(); CHECK(logs.contains("Key 'UNKNOWN_KEY' not found in telemetry configuration!")); } From a70bdb8173614a2ee194621be03e2641c2ab98f9 Mon Sep 17 00:00:00 2001 From: Blake Date: Thu, 30 Jul 2026 10:00:33 -0400 Subject: [PATCH 32/36] forgot to format --- src/app/pages/plot.cpp | 2 +- src/core/time.cpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/pages/plot.cpp b/src/app/pages/plot.cpp index 08fd045..f2d6dac 100644 --- a/src/app/pages/plot.cpp +++ b/src/app/pages/plot.cpp @@ -279,7 +279,7 @@ void plot_page::plot_signal(const std::string& key) { if (!inserted) { return; } auto series = context_->backend->get_data().get_series(key); if (!series) { return; } - auto time = context_->backend->get_data().get_time(); + auto time = context_->backend->get_data().get_time(); if (!has_offset_ && !time.empty()) { time_offset_ = time.front(); diff --git a/src/core/time.cpp b/src/core/time.cpp index 62f5c7f..2aa7bd0 100644 --- a/src/core/time.cpp +++ b/src/core/time.cpp @@ -126,10 +126,10 @@ date_time::date_time() { const auto total_us = duration_cast(duration).count(); local = local_time{static_cast(lt.tm_hour), - static_cast(lt.tm_min), - static_cast(lt.tm_sec), - static_cast((total_us / 1'000) % 1'000), - static_cast(total_us % 1'000)}; + static_cast(lt.tm_min), + static_cast(lt.tm_sec), + static_cast((total_us / 1'000) % 1'000), + static_cast(total_us % 1'000)}; } date_time::date_time(u64 creation_time_seconds) { From 081fcd56997f887186c21f01a1fec1440fcba48a Mon Sep 17 00:00:00 2001 From: Blake Date: Thu, 30 Jul 2026 10:18:33 -0400 Subject: [PATCH 33/36] colors are now known on compile time, annoying to deal with in fmt::format but whatever the marginal speed increase is worth --- include/app/assets/style.hpp | 23 +++++++++++------------ src/app/assets/style.cpp | 36 ++++++++++++++++++------------------ src/app/gui.cpp | 13 +++++++------ src/app/pages/home.cpp | 15 ++++++++------- src/app/pages/plot.cpp | 15 ++++++++------- 5 files changed, 52 insertions(+), 50 deletions(-) diff --git a/include/app/assets/style.hpp b/include/app/assets/style.hpp index 50ad3d5..342f174 100644 --- a/include/app/assets/style.hpp +++ b/include/app/assets/style.hpp @@ -13,22 +13,21 @@ namespace mbr::ui::style { namespace color { // Dark Mode Theme Standards -inline const QColor bg_dark{"#1e1e1e"}; -inline const QColor bg_hover{"#2a2a2a"}; -inline const QColor border{"#2d2d2d"}; -inline const QColor text_main{"#d4d4d4"}; -inline const QColor text_muted{"#a0a0a0"}; -inline const QColor accent{"#00adb5"}; +inline constexpr QRgb bg_dark{0xff1e1e1e}; +inline constexpr QRgb bg_hover{0xff2a2a2a}; +inline constexpr QRgb border{0xff2d2d2d}; +inline constexpr QRgb text_main{0xffd4d4d4}; +inline constexpr QRgb text_muted{0xffa0a0a0}; +inline constexpr QRgb accent{0xff00adb5}; // Status Indicator Colors -inline const QColor status_error{"#f44336"}; -inline const QColor status_warn{"#ffc107"}; -inline const QColor status_ok{"#00e676"}; +inline constexpr QRgb status_error{0xfff44336}; +inline constexpr QRgb status_warn{0xffffc107}; +inline constexpr QRgb status_ok{0xff00e676}; // Graph Line Palette -inline const std::array graph_palette = { - "#00adb5", "#ff5722", "#e91e63", "#9c27b0", "#4caf50", "#ffeb3b"}; - +inline constexpr std::array graph_palette = { + 0xff00adb5, 0xffff5722, 0xffe91e63, 0xff9c27b0, 0xff4caf50, 0xffffeb3b}; QColor get_graph_color(std::size_t index); } // namespace color diff --git a/src/app/assets/style.cpp b/src/app/assets/style.cpp index d4d9f65..c3945ee 100644 --- a/src/app/assets/style.cpp +++ b/src/app/assets/style.cpp @@ -21,15 +21,15 @@ QString make_button_style(const button_style_options& opts) { " border-radius: {3}px; padding: {4}px {5}px; font: {6}pt; text-align: {7};" "}}" "QToolButton:hover {{ background-color: {8}; }}", - color::bg_dark.name().toStdString(), - color::text_main.name().toStdString(), - color::border.name().toStdString(), + color::bg_dark, + color::text_main, + color::border, opts.border_radius, opts.padding_v, opts.padding_h, opts.font_size, opts.align.toStdString(), - color::bg_hover.name().toStdString())); + color::bg_hover)); } QString make_tree_style() { @@ -39,11 +39,11 @@ QString make_tree_style() { " border: 1px solid {2};" " font: 14px;" "}}", - color::bg_dark.name().toStdString(), - color::text_main.name().toStdString(), - color::border.name().toStdString(), - color::accent.name().toStdString(), - color::text_main.name().toStdString())); + color::bg_dark, + color::text_main, + color::border, + color::accent, + color::text_main)); } QString make_menubar_style(int font_size_px) { @@ -59,11 +59,11 @@ QString make_menubar_style(int font_size_px) { "QMenuBar::item:selected {{" " background-color: {4}; border: 1px solid {3};" "}}", - color::bg_dark.name().toStdString(), - color::text_main.name().toStdString(), + color::bg_dark, + color::text_main, font_size_px, - color::border.name().toStdString(), - color::bg_hover.name().toStdString())); + color::border, + color::bg_hover)); } QString make_menu_style(int font_size_px) { @@ -81,12 +81,12 @@ QString make_menu_style(int font_size_px) { "}}" "QMenu::item:disabled {{ color: {5}; }}" "QMenu::separator {{ height: 1px; background-color: {3}; margin: 4px 6px; }}", - color::bg_dark.name().toStdString(), - color::text_main.name().toStdString(), + color::bg_dark, + color::text_main, font_size_px, - color::border.name().toStdString(), - color::bg_hover.name().toStdString(), - color::text_muted.name().toStdString())); + color::border, + color::bg_hover, + color::text_muted)); } QString make_status_dot_style(const QColor& color, int diameter_px) { diff --git a/src/app/gui.cpp b/src/app/gui.cpp index 3cc28ba..9f9ae92 100644 --- a/src/app/gui.cpp +++ b/src/app/gui.cpp @@ -126,8 +126,8 @@ void gui_t::build_menu_bar() { status_dot_ = new QLabel(rightWidget); status_dot_->setFixedSize(20, 20); - status_dot_->setStyleSheet(QString::fromStdString(fmt::format( - "background-color: {0}; border-radius: 10px;", colors::status_error.name().toStdString()))); + status_dot_->setStyleSheet(QString::fromStdString( + fmt::format("background-color: {0}; border-radius: 10px;", colors::status_error))); rightLayout->addWidget(connection_status_, 0, Qt::AlignVCenter); rightLayout->addWidget(status_dot_, 0, Qt::AlignVCenter); @@ -135,10 +135,11 @@ void gui_t::build_menu_bar() { } void gui_t::update_status_dot() { - QString color = !is_connected_ ? colors::status_error.name() - : !is_receiving_ ? colors::status_warn.name() - : colors::status_ok.name(); - status_dot_->setStyleSheet(QString("background-color: %1; border-radius: 10px;").arg(color)); + QRgb color = !is_connected_ ? colors::status_error + : !is_receiving_ ? colors::status_warn + : colors::status_ok; + status_dot_->setStyleSheet( + QString("background-color: %1; border-radius: 10px;").arg(QColor(color).name())); } } // namespace mbr diff --git a/src/app/pages/home.cpp b/src/app/pages/home.cpp index 589375a..33bb094 100644 --- a/src/app/pages/home.cpp +++ b/src/app/pages/home.cpp @@ -1,5 +1,6 @@ #include "app/pages/home.hpp" +#include #include #include #include @@ -50,8 +51,8 @@ QWidget* home_page::build_rhs() { auto* jason_grid = new QGridLayout(); auto* title = new QLabel("Data Status"); - title->setStyleSheet(QString::fromStdString( - fmt::format("color: {}; font: 12pt", colors::text_main.name().toStdString()))); + title->setStyleSheet( + QString::fromStdString(fmt::format("color: {}; font: 12pt", colors::text_main))); auto* line = new QFrame; line->setFrameShape(QFrame::HLine); @@ -60,15 +61,15 @@ QWidget* home_page::build_rhs() { usize row = 0; for (auto& d : context_->backend->get_data().data_values) { auto* label = new QLabel(QString::fromStdString(d.name)); - label->setStyleSheet(QString::fromStdString( - fmt::format("color: {}; font: 12pt", colors::text_main.name().toStdString()))); + label->setStyleSheet( + QString::fromStdString(fmt::format("color: {}; font: 12pt", colors::text_main))); jason_grid->addWidget(label, row, 0); auto* status = new QLabel(); status->setFixedSize(20, 20); status->setStyleSheet( QString::fromStdString(fmt::format("background-color: {}; border-radius: 10px;", - colors::status_error.name().toStdString()))); + QColor(colors::status_error).name().toStdString()))); jason_grid->addWidget(status, row, 1); labels_.try_emplace(QString::fromStdString(d.key), status); row++; @@ -106,7 +107,7 @@ void home_page::connect_signals() { if (auto it = labels_.find(qkey); it != labels_.end()) { it->second->setStyleSheet(QString::fromStdString( fmt::format("background-color: {}; border-radius: 10px;", - colors::status_ok.name().toStdString()))); + QColor(colors::status_ok).name().toStdString()))); } } }); @@ -119,7 +120,7 @@ void home_page::connect_signals() { if (auto it = labels_.find(key); it != labels_.end()) { it->second->setStyleSheet(QString::fromStdString( fmt::format("background-color: {}; border-radius: 10px;", - colors::status_error.name().toStdString()))); + QColor(colors::status_error).name().toStdString()))); } } } diff --git a/src/app/pages/plot.cpp b/src/app/pages/plot.cpp index f2d6dac..8f38f3a 100644 --- a/src/app/pages/plot.cpp +++ b/src/app/pages/plot.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -148,14 +149,14 @@ QWidget* plot_page::build_rhs() { auto configure_axis = [&](QCPAxis* axis, const QString& label) { axis->setLabel(label); - axis->setBasePen(QPen(colors::text_muted, 1.0)); - axis->setTickPen(QPen(colors::text_muted, 1.0)); - axis->setSubTickPen(QPen(colors::text_muted, 0.5)); + axis->setBasePen(QPen(QColor(colors::text_muted), 1.0)); + axis->setTickPen(QPen(QColor(colors::text_muted), 1.0)); + axis->setSubTickPen(QPen(QColor(colors::text_muted), 0.5)); axis->setTickLabelColor(colors::text_muted); axis->setLabelColor(colors::text_muted); - axis->grid()->setPen(QPen(colors::border, 1.0, Qt::SolidLine)); - axis->grid()->setSubGridPen(QPen(colors::border, 0.5, Qt::DotLine)); + axis->grid()->setPen(QPen(QColor(colors::border), 1.0, Qt::SolidLine)); + axis->grid()->setSubGridPen(QPen(QColor(colors::border), 0.5, Qt::DotLine)); axis->grid()->setSubGridVisible(true); }; @@ -294,8 +295,8 @@ void plot_page::plot_signal(const std::string& key) { QCPGraph* graph = plot_->addGraph(); graph->setAdaptiveSampling(true); - static const std::array palette = colors::graph_palette; - QColor color = palette[active_graphs_.size() % palette.size()]; + static const std::array palette = colors::graph_palette; + QColor color = palette[active_graphs_.size() % palette.size()]; graph->setPen(QPen(color, 2.0)); graph->setData(x_times, y_values); From 5b89a28e51975710fad3fd3c2f57ec1d64f23783 Mon Sep 17 00:00:00 2001 From: Blake Date: Thu, 30 Jul 2026 11:27:14 -0400 Subject: [PATCH 34/36] qcolor --- include/app/assets/style.hpp | 28 ++++++++++++++++------------ src/app/assets/style.cpp | 36 ++++++++++++++++++------------------ src/app/gui.cpp | 12 ++++++------ src/app/pages/home.cpp | 10 +++++----- src/app/pages/plot.cpp | 4 ++-- 5 files changed, 47 insertions(+), 43 deletions(-) diff --git a/include/app/assets/style.hpp b/include/app/assets/style.hpp index 342f174..6a10fc1 100644 --- a/include/app/assets/style.hpp +++ b/include/app/assets/style.hpp @@ -13,22 +13,26 @@ namespace mbr::ui::style { namespace color { // Dark Mode Theme Standards -inline constexpr QRgb bg_dark{0xff1e1e1e}; -inline constexpr QRgb bg_hover{0xff2a2a2a}; -inline constexpr QRgb border{0xff2d2d2d}; -inline constexpr QRgb text_main{0xffd4d4d4}; -inline constexpr QRgb text_muted{0xffa0a0a0}; -inline constexpr QRgb accent{0xff00adb5}; +inline constexpr QColor bg_dark(0x1e, 0x1e, 0x1e); +inline constexpr QColor bg_hover(0x2a, 0x2a, 0x2a); +inline constexpr QColor border(0x2d, 0x2d, 0x2d); +inline constexpr QColor text_main(0xd4, 0xd4, 0xd4); +inline constexpr QColor text_muted(0xa0, 0xa0, 0xa0); +inline constexpr QColor accent(0x00, 0xad, 0xb5); // Status Indicator Colors -inline constexpr QRgb status_error{0xfff44336}; -inline constexpr QRgb status_warn{0xffffc107}; -inline constexpr QRgb status_ok{0xff00e676}; +inline constexpr QColor status_error(0xf4, 0x43, 0x36); +inline constexpr QColor status_warn(0xff, 0xc1, 07); +inline constexpr QColor status_ok(0x00, 0xe6, 0x76); // Graph Line Palette -inline constexpr std::array graph_palette = { - 0xff00adb5, 0xffff5722, 0xffe91e63, 0xff9c27b0, 0xff4caf50, 0xffffeb3b}; -QColor get_graph_color(std::size_t index); +inline constexpr std::array graph_palette = {QColor(0x00, 0xad, 0xb5), + QColor(0xff, 0x57, 0x22), + QColor(0xe9, 0x1e, 0x63), + QColor(0x9c, 0x27, 0xb0), + QColor(0x4c, 0xaf, 0x50), + QColor(0xff, 0xeb, 0x3b)}; +QColor get_graph_color(std::size_t index); } // namespace color diff --git a/src/app/assets/style.cpp b/src/app/assets/style.cpp index c3945ee..d4d9f65 100644 --- a/src/app/assets/style.cpp +++ b/src/app/assets/style.cpp @@ -21,15 +21,15 @@ QString make_button_style(const button_style_options& opts) { " border-radius: {3}px; padding: {4}px {5}px; font: {6}pt; text-align: {7};" "}}" "QToolButton:hover {{ background-color: {8}; }}", - color::bg_dark, - color::text_main, - color::border, + color::bg_dark.name().toStdString(), + color::text_main.name().toStdString(), + color::border.name().toStdString(), opts.border_radius, opts.padding_v, opts.padding_h, opts.font_size, opts.align.toStdString(), - color::bg_hover)); + color::bg_hover.name().toStdString())); } QString make_tree_style() { @@ -39,11 +39,11 @@ QString make_tree_style() { " border: 1px solid {2};" " font: 14px;" "}}", - color::bg_dark, - color::text_main, - color::border, - color::accent, - color::text_main)); + color::bg_dark.name().toStdString(), + color::text_main.name().toStdString(), + color::border.name().toStdString(), + color::accent.name().toStdString(), + color::text_main.name().toStdString())); } QString make_menubar_style(int font_size_px) { @@ -59,11 +59,11 @@ QString make_menubar_style(int font_size_px) { "QMenuBar::item:selected {{" " background-color: {4}; border: 1px solid {3};" "}}", - color::bg_dark, - color::text_main, + color::bg_dark.name().toStdString(), + color::text_main.name().toStdString(), font_size_px, - color::border, - color::bg_hover)); + color::border.name().toStdString(), + color::bg_hover.name().toStdString())); } QString make_menu_style(int font_size_px) { @@ -81,12 +81,12 @@ QString make_menu_style(int font_size_px) { "}}" "QMenu::item:disabled {{ color: {5}; }}" "QMenu::separator {{ height: 1px; background-color: {3}; margin: 4px 6px; }}", - color::bg_dark, - color::text_main, + color::bg_dark.name().toStdString(), + color::text_main.name().toStdString(), font_size_px, - color::border, - color::bg_hover, - color::text_muted)); + color::border.name().toStdString(), + color::bg_hover.name().toStdString(), + color::text_muted.name().toStdString())); } QString make_status_dot_style(const QColor& color, int diameter_px) { diff --git a/src/app/gui.cpp b/src/app/gui.cpp index 9f9ae92..6f3c247 100644 --- a/src/app/gui.cpp +++ b/src/app/gui.cpp @@ -126,8 +126,8 @@ void gui_t::build_menu_bar() { status_dot_ = new QLabel(rightWidget); status_dot_->setFixedSize(20, 20); - status_dot_->setStyleSheet(QString::fromStdString( - fmt::format("background-color: {0}; border-radius: 10px;", colors::status_error))); + status_dot_->setStyleSheet(QString::fromStdString(fmt::format( + "background-color: {0}; border-radius: 10px;", colors::status_error.name().toStdString()))); rightLayout->addWidget(connection_status_, 0, Qt::AlignVCenter); rightLayout->addWidget(status_dot_, 0, Qt::AlignVCenter); @@ -135,11 +135,11 @@ void gui_t::build_menu_bar() { } void gui_t::update_status_dot() { - QRgb color = !is_connected_ ? colors::status_error - : !is_receiving_ ? colors::status_warn - : colors::status_ok; + QColor color = !is_connected_ ? colors::status_error.name() + : !is_receiving_ ? colors::status_warn.name() + : colors::status_ok.name(); status_dot_->setStyleSheet( - QString("background-color: %1; border-radius: 10px;").arg(QColor(color).name())); + QString("background-color: %1; border-radius: 10px;").arg(color.name().toStdString())); } } // namespace mbr diff --git a/src/app/pages/home.cpp b/src/app/pages/home.cpp index 33bb094..93a7d0d 100644 --- a/src/app/pages/home.cpp +++ b/src/app/pages/home.cpp @@ -51,8 +51,8 @@ QWidget* home_page::build_rhs() { auto* jason_grid = new QGridLayout(); auto* title = new QLabel("Data Status"); - title->setStyleSheet( - QString::fromStdString(fmt::format("color: {}; font: 12pt", colors::text_main))); + title->setStyleSheet(QString::fromStdString( + fmt::format("color: {}; font: 12pt", colors::text_main.name().toStdString()))); auto* line = new QFrame; line->setFrameShape(QFrame::HLine); @@ -61,15 +61,15 @@ QWidget* home_page::build_rhs() { usize row = 0; for (auto& d : context_->backend->get_data().data_values) { auto* label = new QLabel(QString::fromStdString(d.name)); - label->setStyleSheet( - QString::fromStdString(fmt::format("color: {}; font: 12pt", colors::text_main))); + label->setStyleSheet(QString::fromStdString( + fmt::format("color: {}; font: 12pt", colors::text_main.name().toStdString()))); jason_grid->addWidget(label, row, 0); auto* status = new QLabel(); status->setFixedSize(20, 20); status->setStyleSheet( QString::fromStdString(fmt::format("background-color: {}; border-radius: 10px;", - QColor(colors::status_error).name().toStdString()))); + colors::status_error.name().toStdString()))); jason_grid->addWidget(status, row, 1); labels_.try_emplace(QString::fromStdString(d.key), status); row++; diff --git a/src/app/pages/plot.cpp b/src/app/pages/plot.cpp index 8f38f3a..e654b12 100644 --- a/src/app/pages/plot.cpp +++ b/src/app/pages/plot.cpp @@ -295,8 +295,8 @@ void plot_page::plot_signal(const std::string& key) { QCPGraph* graph = plot_->addGraph(); graph->setAdaptiveSampling(true); - static const std::array palette = colors::graph_palette; - QColor color = palette[active_graphs_.size() % palette.size()]; + static const std::array palette = colors::graph_palette; + QColor color = palette[active_graphs_.size() % palette.size()]; graph->setPen(QPen(color, 2.0)); graph->setData(x_times, y_values); From 44e0b871e358fed832c011bac0a11172e6f06f08 Mon Sep 17 00:00:00 2001 From: Blake Watson Date: Thu, 30 Jul 2026 17:35:48 -0400 Subject: [PATCH 35/36] bruh --- tests/esp32/test_data.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/esp32/test_data.cpp b/tests/esp32/test_data.cpp index 5440306..701533f 100644 --- a/tests/esp32/test_data.cpp +++ b/tests/esp32/test_data.cpp @@ -49,7 +49,7 @@ TEST_CASE("telemetry_data config loading and data writing") { SECTION("Handling missing keys") { const auto& series_missing = data.get_series("UNKNOWN_KEY"); - CHECK(series_missing.get()->empty()); + CHECK_FALSE(series_missing); std::string logs = logger.get_streamed_logs(); CHECK(logs.contains("Key 'UNKNOWN_KEY' not found in telemetry configuration!")); } From ce576bfb147466a3b6abe6a0e3aaf24bd9375dfd Mon Sep 17 00:00:00 2001 From: Blake Watson Date: Thu, 30 Jul 2026 17:40:31 -0400 Subject: [PATCH 36/36] formatting --- src/core/time.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/time.cpp b/src/core/time.cpp index 2aa7bd0..62f5c7f 100644 --- a/src/core/time.cpp +++ b/src/core/time.cpp @@ -126,10 +126,10 @@ date_time::date_time() { const auto total_us = duration_cast(duration).count(); local = local_time{static_cast(lt.tm_hour), - static_cast(lt.tm_min), - static_cast(lt.tm_sec), - static_cast((total_us / 1'000) % 1'000), - static_cast(total_us % 1'000)}; + static_cast(lt.tm_min), + static_cast(lt.tm_sec), + static_cast((total_us / 1'000) % 1'000), + static_cast(total_us % 1'000)}; } date_time::date_time(u64 creation_time_seconds) {