From 08508fb4ea696b4e2fbbd5508e4ecd3c17581711 Mon Sep 17 00:00:00 2001 From: Kyle Krenzer Date: Tue, 1 Sep 2026 17:03:14 -0700 Subject: [PATCH 1/2] Show the dialog before running update, switch to stock and shutdown commands The update, switch-to-stock and shutdown-host handlers ran their command synchronously on the LVGL thread from inside the click dispatch and only created the "Initiated" dialog after the command returned. Commands that end in a reboot never return, so the screen showed nothing at all until the printer restarted, not even the button's pressed state. Factory reset already avoids this by creating the dialog first and deferring the command to a one-shot lv_timer. Generalise that into run_command_deferred() and use it for all four commands. Factory reset keeps its 5 s delay; the others wait 500 ms, enough for LVGL to render the dialog before the command blocks the thread. Failure handling is unchanged: a non-zero exit replaces the dialog with the "Failed" one. Co-Authored-By: Claude Fable 5.1 --- src/setting_panel.cpp | 69 +++++++++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/src/setting_panel.cpp b/src/setting_panel.cpp index 223fe27..4fafca6 100644 --- a/src/setting_panel.cpp +++ b/src/setting_panel.cpp @@ -16,11 +16,18 @@ LV_IMG_DECLARE(emergency); LV_IMG_DECLARE(print); LV_IMG_DECLARE(sd_img); -struct reset_ctx { +struct deferred_cmd_ctx { lv_obj_t * mbox; std::string cmd; + const char * failure_title; + const char * failure_message; }; +// Long enough for LVGL to render the dialog before the command blocks the UI thread. +static constexpr uint32_t DEFERRED_COMMAND_DELAY_MS = 500; +// The factory reset dialog is deliberately left up longer before we block. +static constexpr uint32_t FACTORY_RESET_DELAY_MS = 5000; + static int call_command(const std::string &cmd) { try { return sp::call(cmd); @@ -30,16 +37,16 @@ static int call_command(const std::string &cmd) { } } -static void run_factory_reset_cb(lv_timer_t * t) { - reset_ctx * ctx = (reset_ctx *)t->user_data; +static void run_deferred_command_cb(lv_timer_t * t) { + deferred_cmd_ctx * ctx = (deferred_cmd_ctx *)t->user_data; int ret = call_command(ctx->cmd); if (ret != 0) { simple_dialog_close(ctx->mbox); create_simple_dialog(lv_scr_act(), - FACTORY_RESET_BUTTON_TITLE " Failed", - FACTORY_RESET_BUTTON_FAILURE, + ctx->failure_title, + ctx->failure_message, true, true); } @@ -48,6 +55,22 @@ static void run_factory_reset_cb(lv_timer_t * t) { lv_timer_del(t); } +// Commands such as update, switch to stock and factory reset block for a long +// time and then reboot the printer. Calling them straight from the click +// handler freezes the UI thread before LVGL gets a chance to draw, so the press +// appears to do nothing at all until the machine reboots. Put the dialog up +// first and defer the command to a one shot timer so the screen is rendered +// before we block on it. +static void run_command_deferred(lv_obj_t * mbox, + const std::string &cmd, + const char * failure_title, + const char * failure_message, + uint32_t delay_ms) { + deferred_cmd_ctx * ctx = new deferred_cmd_ctx{ mbox, cmd, failure_title, failure_message }; + lv_timer_t * timer = lv_timer_create(run_deferred_command_cb, delay_ms, ctx); + lv_timer_set_repeat_count(timer, 1); +} + SettingPanel::SettingPanel(KWebSocketClient &c, std::mutex &l, lv_obj_t *parent) : ws(c) , cont(lv_obj_create(parent)) @@ -153,22 +176,18 @@ void SettingPanel::handle_callback(lv_event_t *event) { } else if (btn == update_btn.get_container()) { Config *conf = Config::get_instance(); auto update_cmd = conf->get(std::string("/commands/") + UPDATE_BUTTON_CMD); - auto ret = call_command(update_cmd); - if (ret == 0) { - create_simple_dialog(lv_scr_act(), UPDATE_BUTTON_TITLE " Initiated", UPDATE_BUTTON_SUCCESS, false, false); - } else { - create_simple_dialog(lv_scr_act(), UPDATE_BUTTON_TITLE " Failed", UPDATE_BUTTON_FAILURE, true, true); - } + lv_obj_t *mbox = create_simple_dialog(lv_scr_act(), UPDATE_BUTTON_TITLE " Initiated", UPDATE_BUTTON_SUCCESS, false, false); + run_command_deferred(mbox, update_cmd, + UPDATE_BUTTON_TITLE " Failed", UPDATE_BUTTON_FAILURE, + DEFERRED_COMMAND_DELAY_MS); #else } else if (btn == shutdown_host_btn.get_container()) { Config *conf = Config::get_instance(); auto shutdown_host_cmd = conf->get("/commands/shutdown_host_cmd"); - auto ret = call_command(shutdown_host_cmd); - if (ret == 0) { - create_simple_dialog(lv_scr_act(), "Shutdown Host Initiated", "Shutdown of host has been initiated", false, false); - } else { - create_simple_dialog(lv_scr_act(), "Shutdown Host Failed", "Failed to shutdown host!", true, true); - } + lv_obj_t *mbox = create_simple_dialog(lv_scr_act(), "Shutdown Host Initiated", "Shutdown of host has been initiated", false, false); + run_command_deferred(mbox, shutdown_host_cmd, + "Shutdown Host Failed", "Failed to shutdown host!", + DEFERRED_COMMAND_DELAY_MS); #endif } else if (btn == support_zip_btn.get_container()) { Config *conf = Config::get_instance(); @@ -184,20 +203,18 @@ void SettingPanel::handle_callback(lv_event_t *event) { } else if (btn == switch_to_stock_btn.get_container()) { Config *conf = Config::get_instance(); auto switch_to_stock_cmd = conf->get("/commands/switch_to_stock_cmd"); - auto ret = call_command(switch_to_stock_cmd); - if (ret == 0) { - create_simple_dialog(lv_scr_act(), SWITCH_TO_STOCK_BUTTON_TITLE " Initiated", SWITCH_TO_STOCK_BUTTON_SUCCESS, false, false); - } else { - create_simple_dialog(lv_scr_act(), SWITCH_TO_STOCK_BUTTON_TITLE " Failed", SWITCH_TO_STOCK_BUTTON_FAILURE, true, true); - } + lv_obj_t *mbox = create_simple_dialog(lv_scr_act(), SWITCH_TO_STOCK_BUTTON_TITLE " Initiated", SWITCH_TO_STOCK_BUTTON_SUCCESS, false, false); + run_command_deferred(mbox, switch_to_stock_cmd, + SWITCH_TO_STOCK_BUTTON_TITLE " Failed", SWITCH_TO_STOCK_BUTTON_FAILURE, + DEFERRED_COMMAND_DELAY_MS); } else if (btn == factory_reset_btn.get_container()) { lv_obj_t *mbox = create_simple_dialog(lv_scr_act(), FACTORY_RESET_BUTTON_TITLE " Initiated", FACTORY_RESET_BUTTON_SUCCESS, false, false); Config *conf = Config::get_instance(); auto cmd = conf->get("/commands/factory_reset_cmd"); - reset_ctx * ctx = new reset_ctx{ mbox, cmd }; - lv_timer_t * timer = lv_timer_create(run_factory_reset_cb, 5000, ctx); - lv_timer_set_repeat_count(timer, 1); + run_command_deferred(mbox, cmd, + FACTORY_RESET_BUTTON_TITLE " Failed", FACTORY_RESET_BUTTON_FAILURE, + FACTORY_RESET_DELAY_MS); } } } From 4bd84fe15f19b6537875c81e2291723bb83b710e Mon Sep 17 00:00:00 2001 From: Kyle Krenzer Date: Tue, 1 Sep 2026 17:03:14 -0700 Subject: [PATCH 2/2] Show a progress bar while the update command runs When the update button is enabled (UPDATE_CMD) the update command is now spawned detached with sp::Popen instead of blocking the UI thread, and the dialog shows a bar fed by the "|" line the command publishes to UPDATE_STATUS_FILE (default /run/cosmos-update.status): downloading, flashing, rebooting, failed. The file is polled every 500 ms from an lv_timer; the command's exit status stays authoritative, so exiting before it reports "rebooting" shows the "Failed" dialog. The dialog uses the multiline_message variant because the default no-button dialog on a 272 px high screen is 95 px tall and clips the bar. Co-Authored-By: Claude Fable 5.1 --- Makefile | 3 + src/setting_panel.cpp | 8 +- src/update_progress.h | 165 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 src/update_progress.h diff --git a/Makefile b/Makefile index 4f9294c..09597ca 100644 --- a/Makefile +++ b/Makefile @@ -92,6 +92,9 @@ endif ifdef UPDATE_CMD DEFINES += -D UPDATE_BUTTON_CMD='"$(UPDATE_CMD)"' +# The update command reports its progress here, see src/update_progress.h +UPDATE_STATUS_FILE ?= /run/cosmos-update.status +DEFINES += -D UPDATE_BUTTON_STATUS_FILE='"$(UPDATE_STATUS_FILE)"' endif ifdef UPDATE_TEXT diff --git a/src/setting_panel.cpp b/src/setting_panel.cpp index 4fafca6..9b3860c 100644 --- a/src/setting_panel.cpp +++ b/src/setting_panel.cpp @@ -3,6 +3,9 @@ #include "logger.h" #include "subprocess.hpp" #include "simple_dialog.h" +#ifdef UPDATE_BUTTON_CMD +#include "update_progress.h" +#endif #include @@ -176,10 +179,7 @@ void SettingPanel::handle_callback(lv_event_t *event) { } else if (btn == update_btn.get_container()) { Config *conf = Config::get_instance(); auto update_cmd = conf->get(std::string("/commands/") + UPDATE_BUTTON_CMD); - lv_obj_t *mbox = create_simple_dialog(lv_scr_act(), UPDATE_BUTTON_TITLE " Initiated", UPDATE_BUTTON_SUCCESS, false, false); - run_command_deferred(mbox, update_cmd, - UPDATE_BUTTON_TITLE " Failed", UPDATE_BUTTON_FAILURE, - DEFERRED_COMMAND_DELAY_MS); + start_update_with_progress(update_cmd); #else } else if (btn == shutdown_host_btn.get_container()) { Config *conf = Config::get_instance(); diff --git a/src/update_progress.h b/src/update_progress.h new file mode 100644 index 0000000..6a1bd6b --- /dev/null +++ b/src/update_progress.h @@ -0,0 +1,165 @@ +#ifndef UPDATE_PROGRESS_H +#define UPDATE_PROGRESS_H + +#ifdef UPDATE_BUTTON_CMD + +#include "lvgl.h" +#include "logger.h" +#include "simple_dialog.h" +#include "subprocess.hpp" + +#include +#include +#include +#include +#include + +// How often the status file written by the update command is re-read. +static constexpr uint32_t UPDATE_PROGRESS_POLL_MS = 500; + +struct UpdateProgressCtx { + lv_obj_t *mbox = nullptr; + lv_obj_t *bar = nullptr; + lv_obj_t *label = nullptr; + std::unique_ptr proc; + bool saw_reboot = false; +}; + +// Reads the "|" line the update command publishes. Returns +// false while the file does not exist yet, which is the normal case for the +// first poll or two. +static inline bool update_progress_read(std::string &state, int &percent) { + FILE *f = fopen(UPDATE_BUTTON_STATUS_FILE, "r"); + if (f == nullptr) return false; + + char buf[64] = {0}; + char *line = fgets(buf, sizeof(buf), f); + fclose(f); + if (line == nullptr) return false; + + char *sep = strchr(buf, '|'); + if (sep == nullptr) return false; + *sep = '\0'; + + state = buf; + percent = atoi(sep + 1); + if (percent < 0) percent = 0; + if (percent > 100) percent = 100; + return true; +} + +static inline void update_progress_fail(UpdateProgressCtx *ctx, lv_timer_t *t) { + simple_dialog_close(ctx->mbox); + create_simple_dialog(lv_scr_act(), + UPDATE_BUTTON_TITLE " Failed", + UPDATE_BUTTON_FAILURE, + true, + true); + delete ctx; + lv_timer_del(t); +} + +static inline void update_progress_timer_cb(lv_timer_t *t) { + UpdateProgressCtx *ctx = static_cast(t->user_data); + + std::string state; + int percent = 0; + if (update_progress_read(state, percent)) { + if (state == "downloading") { + lv_bar_set_value(ctx->bar, percent, LV_ANIM_OFF); + if (percent > 0) { + lv_label_set_text_fmt(ctx->label, "Downloading update... %d%%", percent); + } else { + // Nothing counted yet, or the size of the download is unknown. + lv_label_set_text(ctx->label, "Downloading update..."); + } + } else if (state == "flashing") { + lv_bar_set_value(ctx->bar, 100, LV_ANIM_OFF); + lv_label_set_text(ctx->label, "Installing update, do not power off!"); + } else if (state == "rebooting") { + ctx->saw_reboot = true; + lv_bar_set_value(ctx->bar, 100, LV_ANIM_OFF); + lv_label_set_text(ctx->label, UPDATE_BUTTON_SUCCESS); + } + // "failed" needs no handling of its own: the command exits straight + // after writing it, and the exit status below is what we act on. + } + + // The update command ends in a reboot, so observing it exit before it + // reported "rebooting" means the update did not get that far. + int ret; + try { + ret = ctx->proc->poll(); + } catch (const std::exception &e) { + LOG_ERROR("Failed to poll update process: {}", e.what()); + update_progress_fail(ctx, t); + return; + } + + if (ret >= 0) { + if (ctx->saw_reboot) { + // Leave the dialog up and wait for the machine to go down. + delete ctx; + lv_timer_del(t); + } else { + update_progress_fail(ctx, t); + } + } +} + +// Runs the update command detached and shows a dialog with a progress bar fed +// by the status file the command writes (UPDATE_BUTTON_STATUS_FILE, see the +// Makefile). Unlike a blocking call from the click handler this leaves the UI +// thread free, so the dialog is drawn immediately and keeps updating for the +// length of the download. +static inline void start_update_with_progress(const std::string &cmd) { + // Discard any status left behind by a previous attempt. + remove(UPDATE_BUTTON_STATUS_FILE); + + // The taller dialog variant: with the bar under it the message needs more + // room than the one line dialog offers on a 272 pixel high screen. + SimpleDialogOptions options{}; + options.multiline_message = true; + lv_obj_t *mbox = create_configurable_dialog(lv_scr_act(), + UPDATE_BUTTON_TITLE " Initiated", + "Starting update...", + options); + + lv_obj_t *content = lv_msgbox_get_content(mbox); + lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(content, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + // Gap between the message text and the bar below it. + lv_obj_set_style_pad_row(content, 12, 0); + + lv_obj_t *bar = lv_bar_create(content); + lv_obj_set_width(bar, LV_PCT(90)); + lv_obj_set_height(bar, 18); + lv_bar_set_range(bar, 0, 100); + lv_bar_set_value(bar, 0, LV_ANIM_OFF); + + UpdateProgressCtx *ctx = new UpdateProgressCtx(); + ctx->mbox = mbox; + ctx->bar = bar; + ctx->label = lv_msgbox_get_text(mbox); + + try { + // Same invocation as call_command(): the configured command is split + // on whitespace and executed directly, we just do not wait for it. + ctx->proc.reset(new subprocess::Popen(cmd)); + } catch (const std::exception &e) { + LOG_ERROR("Failed to start update command '{}': {}", cmd, e.what()); + simple_dialog_close(mbox); + create_simple_dialog(lv_scr_act(), + UPDATE_BUTTON_TITLE " Failed", + UPDATE_BUTTON_FAILURE, + true, + true); + delete ctx; + return; + } + + lv_timer_create(update_progress_timer_cb, UPDATE_PROGRESS_POLL_MS, ctx); +} + +#endif // UPDATE_BUTTON_CMD +#endif // UPDATE_PROGRESS_H