From 08508fb4ea696b4e2fbbd5508e4ecd3c17581711 Mon Sep 17 00:00:00 2001 From: Kyle Krenzer Date: Tue, 1 Sep 2026 17:03:14 -0700 Subject: [PATCH 1/4] 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 df5193f5aae6f224ca94edf04f22454152ae1f3c Mon Sep 17 00:00:00 2001 From: Kyle Krenzer Date: Fri, 4 Sep 2026 13:05:44 -0700 Subject: [PATCH 2/4] Run the update through Moonraker's update_manager when configured With [update_manager] application: cosmos in the config, the update button asks Moonraker to update that update_manager entry (machine.update.client) instead of running a shell command, and the progress Moonraker streams through notify_update_response is shown in a dialog, the same way Mainsail and Fluidd show it. A failed update ends in the "Failed" dialog with the message from Moonraker; a refused request (for example while printing) does the same straight away. Without the config key nothing changes: the button keeps running its configured command. Co-Authored-By: Claude Fable 5.1 --- src/setting_panel.cpp | 18 ++++-- src/setting_panel.h | 2 + src/update_manager_client.cpp | 107 ++++++++++++++++++++++++++++++++++ src/update_manager_client.h | 45 ++++++++++++++ 4 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 src/update_manager_client.cpp create mode 100644 src/update_manager_client.h diff --git a/src/setting_panel.cpp b/src/setting_panel.cpp index 4fafca6..7200124 100644 --- a/src/setting_panel.cpp +++ b/src/setting_panel.cpp @@ -75,6 +75,7 @@ SettingPanel::SettingPanel(KWebSocketClient &c, std::mutex &l, lv_obj_t *parent) : ws(c) , cont(lv_obj_create(parent)) , wifi_panel(l) + , update_manager(c, l) , wifi_btn(cont, &network_img, "WIFI", &SettingPanel::_handle_callback, this) , restart_klipper_btn(cont, &refresh_img, "Restart Klipper", &SettingPanel::_handle_callback, this, "Restart Klipper", "Do you want to restart klipper?", {"Back", "Restart Klipper"}) @@ -174,12 +175,17 @@ void SettingPanel::handle_callback(lv_event_t *event) { } #ifdef UPDATE_BUTTON_CMD } 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); + if (update_manager.enabled()) { + // Moonraker runs the update and reports its progress to every UI. + update_manager.start(UPDATE_BUTTON_TITLE); + } else { + 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); + } #else } else if (btn == shutdown_host_btn.get_container()) { Config *conf = Config::get_instance(); diff --git a/src/setting_panel.h b/src/setting_panel.h index 2ea462b..2218ad0 100644 --- a/src/setting_panel.h +++ b/src/setting_panel.h @@ -5,6 +5,7 @@ #include "wifi_panel.h" #include "button_container.h" #include "websocket_client.h" +#include "update_manager_client.h" #include "lvgl/lvgl.h" #include @@ -28,6 +29,7 @@ class SettingPanel { lv_obj_t *cont; WifiPanel wifi_panel; + UpdateManagerClient update_manager; ButtonContainer wifi_btn; ButtonContainer restart_klipper_btn; diff --git a/src/update_manager_client.cpp b/src/update_manager_client.cpp new file mode 100644 index 0000000..9a215ef --- /dev/null +++ b/src/update_manager_client.cpp @@ -0,0 +1,107 @@ +#include "update_manager_client.h" +#include "config.h" +#include "logger.h" +#include "simple_dialog.h" + +UpdateManagerClient::UpdateManagerClient(KWebSocketClient &c, std::mutex &l) + : ws(c) + , lv_lock(l) +{ + Config *conf = Config::get_instance(); + app = conf->get("/update_manager/application"); + if (app.empty()) { + return; + } + ws.register_method_callback("notify_update_response", "UpdateManagerClient", + [this](json &j) { this->handle_notification(j); }); +} + +// How long the final message stays up after a successful update. A COSMOS +// update reboots the printer before this runs out. +static constexpr uint32_t UPDATE_DONE_DISMISS_MS = 8000; + +void UpdateManagerClient::show(const std::string &title, const std::string &message) { + if (dismiss_timer != nullptr) { + lv_timer_del(dismiss_timer); + dismiss_timer = nullptr; + } + if (mbox == nullptr) { + // The taller dialog variant: update messages run to two lines. + SimpleDialogOptions options{}; + options.multiline_message = true; + dialog_title = title; + mbox = create_configurable_dialog(lv_scr_act(), dialog_title.c_str(), message.c_str(), options); + } else { + lv_label_set_text(lv_msgbox_get_text(mbox), message.c_str()); + } +} + +void UpdateManagerClient::dismiss() { + dismiss_timer = nullptr; + if (mbox != nullptr) { + simple_dialog_close(mbox); + mbox = nullptr; + } +} + +void UpdateManagerClient::dismiss_cb(lv_timer_t *t) { + UpdateManagerClient *client = static_cast(t->user_data); + client->dismiss(); + lv_timer_del(t); +} + +void UpdateManagerClient::finish(const std::string &message, bool failed) { + if (failed) { + dismiss(); + std::string title = dialog_title + " Failed"; + create_simple_dialog(lv_scr_act(), title.c_str(), message.c_str(), true, true); + return; + } + // Leave the last message up for a moment, no button: the printer usually + // reboots at this point, and if it does not the dialog goes away by itself. + show(dialog_title.empty() ? "Updating " + app : dialog_title, message); + dismiss_timer = lv_timer_create(dismiss_cb, UPDATE_DONE_DISMISS_MS, this); + lv_timer_set_repeat_count(dismiss_timer, 1); +} + +void UpdateManagerClient::start(const char *title) { + if (app.empty()) { + return; + } + show(title, "Starting update..."); + + json params = {{"name", app}}; + ws.send_jsonrpc("machine.update.client", params, [this](json &j) { + // Moonraker answers straight away only when it refuses the update, for + // example while printing. Progress comes through notify_update_response. + if (j.contains("error")) { + std::string msg = j.value("/error/message"_json_pointer, std::string("Moonraker refused the update")); + LOG_ERROR("update_manager refused the update of {}: {}", app, msg); + std::lock_guard lock(lv_lock); + finish(msg, true); + } + }); +} + +void UpdateManagerClient::handle_notification(json &j) { + auto &p = j["/params/0"_json_pointer]; + if (!p.is_object()) { + return; + } + if (p.value("application", std::string()) != app) { + return; + } + std::string message = p.value("message", std::string()); + bool complete = p.value("complete", false); + LOG_DEBUG("update_manager {}: {}{}", app, message, complete ? " (complete)" : ""); + + std::lock_guard lock(lv_lock); + if (complete) { + // update_manager reports failures as "Error updating : ..." + finish(message, message.rfind("Error", 0) == 0); + return; + } + if (!message.empty()) { + show("Updating " + app, message); + } +} diff --git a/src/update_manager_client.h b/src/update_manager_client.h new file mode 100644 index 0000000..0b97777 --- /dev/null +++ b/src/update_manager_client.h @@ -0,0 +1,45 @@ +#ifndef __UPDATE_MANAGER_CLIENT_H__ +#define __UPDATE_MANAGER_CLIENT_H__ + +#include "websocket_client.h" +#include "lvgl/lvgl.h" + +#include +#include + +// Runs an update through Moonraker's update_manager and shows its progress, +// the same way Mainsail and Fluidd do. Enabled by naming the update_manager +// entry in the config: +// +// [update_manager] +// application: cosmos +// +// Without that key nothing here is active and the update button keeps +// running its configured command. +class UpdateManagerClient { + public: + UpdateManagerClient(KWebSocketClient &ws, std::mutex &lock); + + bool enabled() const { return !app.empty(); } + + // Asks Moonraker to update the configured application and shows the + // progress dialog. Call from the UI thread. + void start(const char *title); + + private: + void handle_notification(json &j); + void show(const std::string &title, const std::string &message); + void finish(const std::string &message, bool failed); + void dismiss(); + + static void dismiss_cb(lv_timer_t *t); + + KWebSocketClient &ws; + std::mutex &lv_lock; + std::string app; + std::string dialog_title; + lv_obj_t *mbox = nullptr; + lv_timer_t *dismiss_timer = nullptr; +}; + +#endif // __UPDATE_MANAGER_CLIENT_H__ From 032f54ec27c488444d5737232175d5926ee397f8 Mon Sep 17 00:00:00 2001 From: Kyle Krenzer Date: Fri, 4 Sep 2026 18:56:49 -0700 Subject: [PATCH 3/4] Build the update_manager client only when the update button is enabled Everything in update_manager_client.{h,cpp} and its use in the settings panel is now inside #ifdef UPDATE_BUTTON_CMD, which the Makefile defines only when UPDATE_CMD is set. A build without the update button, such as Simple AF, compiles none of it and the binary has no reference to the update_manager API. Co-Authored-By: Claude Fable 5.1 --- src/setting_panel.cpp | 2 ++ src/setting_panel.h | 4 ++++ src/update_manager_client.cpp | 3 +++ src/update_manager_client.h | 4 ++++ 4 files changed, 13 insertions(+) diff --git a/src/setting_panel.cpp b/src/setting_panel.cpp index 7200124..ea520d2 100644 --- a/src/setting_panel.cpp +++ b/src/setting_panel.cpp @@ -75,7 +75,9 @@ SettingPanel::SettingPanel(KWebSocketClient &c, std::mutex &l, lv_obj_t *parent) : ws(c) , cont(lv_obj_create(parent)) , wifi_panel(l) +#ifdef UPDATE_BUTTON_CMD , update_manager(c, l) +#endif , wifi_btn(cont, &network_img, "WIFI", &SettingPanel::_handle_callback, this) , restart_klipper_btn(cont, &refresh_img, "Restart Klipper", &SettingPanel::_handle_callback, this, "Restart Klipper", "Do you want to restart klipper?", {"Back", "Restart Klipper"}) diff --git a/src/setting_panel.h b/src/setting_panel.h index 2218ad0..17733d5 100644 --- a/src/setting_panel.h +++ b/src/setting_panel.h @@ -5,7 +5,9 @@ #include "wifi_panel.h" #include "button_container.h" #include "websocket_client.h" +#ifdef UPDATE_BUTTON_CMD #include "update_manager_client.h" +#endif #include "lvgl/lvgl.h" #include @@ -29,7 +31,9 @@ class SettingPanel { lv_obj_t *cont; WifiPanel wifi_panel; +#ifdef UPDATE_BUTTON_CMD UpdateManagerClient update_manager; +#endif ButtonContainer wifi_btn; ButtonContainer restart_klipper_btn; diff --git a/src/update_manager_client.cpp b/src/update_manager_client.cpp index 9a215ef..eb7c6bb 100644 --- a/src/update_manager_client.cpp +++ b/src/update_manager_client.cpp @@ -1,3 +1,5 @@ +// Only built when the update button is enabled (UPDATE_CMD in the Makefile). +#ifdef UPDATE_BUTTON_CMD #include "update_manager_client.h" #include "config.h" #include "logger.h" @@ -105,3 +107,4 @@ void UpdateManagerClient::handle_notification(json &j) { show("Updating " + app, message); } } +#endif // UPDATE_BUTTON_CMD diff --git a/src/update_manager_client.h b/src/update_manager_client.h index 0b97777..a19284c 100644 --- a/src/update_manager_client.h +++ b/src/update_manager_client.h @@ -1,6 +1,9 @@ #ifndef __UPDATE_MANAGER_CLIENT_H__ #define __UPDATE_MANAGER_CLIENT_H__ +// Only built when the update button is enabled (UPDATE_CMD in the Makefile). +#ifdef UPDATE_BUTTON_CMD + #include "websocket_client.h" #include "lvgl/lvgl.h" @@ -42,4 +45,5 @@ class UpdateManagerClient { lv_timer_t *dismiss_timer = nullptr; }; +#endif // UPDATE_BUTTON_CMD #endif // __UPDATE_MANAGER_CLIENT_H__ From c295cbbffc125168c52f33cdd254ddab228bd561 Mon Sep 17 00:00:00 2001 From: Kyle Krenzer Date: Sat, 5 Sep 2026 01:03:32 -0700 Subject: [PATCH 4/4] Use a plain title for updates started from another client When the update was started from Mainsail or Fluidd there is no button title to reuse, and the failure dialog read "Updating cosmos Failed". Use "Update " in that case, and keep the button title otherwise. Co-Authored-By: Claude Fable 5.1 --- src/update_manager_client.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/update_manager_client.cpp b/src/update_manager_client.cpp index eb7c6bb..bd3ee05 100644 --- a/src/update_manager_client.cpp +++ b/src/update_manager_client.cpp @@ -61,7 +61,7 @@ void UpdateManagerClient::finish(const std::string &message, bool failed) { } // Leave the last message up for a moment, no button: the printer usually // reboots at this point, and if it does not the dialog goes away by itself. - show(dialog_title.empty() ? "Updating " + app : dialog_title, message); + show(dialog_title.empty() ? "Update " + app : dialog_title, message); dismiss_timer = lv_timer_create(dismiss_cb, UPDATE_DONE_DISMISS_MS, this); lv_timer_set_repeat_count(dismiss_timer, 1); } @@ -104,7 +104,8 @@ void UpdateManagerClient::handle_notification(json &j) { return; } if (!message.empty()) { - show("Updating " + app, message); + // An update started from another client: no button title to reuse. + show(dialog_title.empty() ? "Update " + app : dialog_title, message); } } #endif // UPDATE_BUTTON_CMD