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 223fe27..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 @@ -16,11 +19,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 +40,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 +58,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 +179,15 @@ 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); - } + start_update_with_progress(update_cmd); #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); } } } 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