Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 43 additions & 26 deletions src/setting_panel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
#include "logger.h"
#include "subprocess.hpp"
#include "simple_dialog.h"
#ifdef UPDATE_BUTTON_CMD
#include "update_progress.h"

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be guarded with UPDATE_BUTTON_STATUS_FILE too.

#endif

#include <experimental/filesystem>

Expand All @@ -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);
Expand All @@ -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);
}
Expand All @@ -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))
Expand Down Expand Up @@ -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>(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<std::string>("/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();
Expand All @@ -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<std::string>("/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<std::string>("/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);
}
}
}
165 changes: 165 additions & 0 deletions src/update_progress.h
Original file line number Diff line number Diff line change
@@ -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 <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <string>

// 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<subprocess::Popen> proc;
bool saw_reboot = false;
};

// Reads the "<state>|<percent>" 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<UpdateProgressCtx *>(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