diff --git a/src/setting_panel.cpp b/src/setting_panel.cpp index 223fe27..ea520d2 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,10 +55,29 @@ 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)) , 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"}) @@ -151,24 +177,25 @@ 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); - auto ret = call_command(update_cmd); - if (ret == 0) { - create_simple_dialog(lv_scr_act(), UPDATE_BUTTON_TITLE " Initiated", UPDATE_BUTTON_SUCCESS, false, false); + if (update_manager.enabled()) { + // Moonraker runs the update and reports its progress to every UI. + update_manager.start(UPDATE_BUTTON_TITLE); } else { - create_simple_dialog(lv_scr_act(), UPDATE_BUTTON_TITLE " Failed", UPDATE_BUTTON_FAILURE, true, true); + 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(); 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 +211,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); } } } diff --git a/src/setting_panel.h b/src/setting_panel.h index 2ea462b..17733d5 100644 --- a/src/setting_panel.h +++ b/src/setting_panel.h @@ -5,6 +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 @@ -28,6 +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 new file mode 100644 index 0000000..bd3ee05 --- /dev/null +++ b/src/update_manager_client.cpp @@ -0,0 +1,111 @@ +// 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" +#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() ? "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); +} + +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()) { + // An update started from another client: no button title to reuse. + show(dialog_title.empty() ? "Update " + app : dialog_title, message); + } +} +#endif // UPDATE_BUTTON_CMD diff --git a/src/update_manager_client.h b/src/update_manager_client.h new file mode 100644 index 0000000..a19284c --- /dev/null +++ b/src/update_manager_client.h @@ -0,0 +1,49 @@ +#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" + +#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_BUTTON_CMD +#endif // __UPDATE_MANAGER_CLIENT_H__