Skip to content
Open
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
77 changes: 51 additions & 26 deletions src/setting_panel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
Expand All @@ -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"})
Expand Down Expand Up @@ -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>(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>(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<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 +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<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);
}
}
}
6 changes: 6 additions & 0 deletions src/setting_panel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <mutex>
Expand All @@ -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;
Expand Down
111 changes: 111 additions & 0 deletions src/update_manager_client.cpp
Original file line number Diff line number Diff line change
@@ -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<std::string>("/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<UpdateManagerClient *>(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<std::mutex> 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<std::mutex> lock(lv_lock);
if (complete) {
// update_manager reports failures as "Error updating <app>: ..."
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
49 changes: 49 additions & 0 deletions src/update_manager_client.h
Original file line number Diff line number Diff line change
@@ -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 <mutex>
#include <string>

// 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__