Implement settings, temp control, extrude/retract, untested status display

This commit is contained in:
suchmememanyskill
2023-11-12 21:49:51 +01:00
parent ef1afa6560
commit 86fca4ca16
16 changed files with 497 additions and 16 deletions

View File

@@ -1,8 +1,19 @@
#include <Preferences.h>
#include "global_config.h"
#include "lvgl.h"
GLOBAL_CONFIG global_config = {0};
COLOR_DEF color_defs[] = {
{LV_PALETTE_BLUE, LV_PALETTE_RED},
{LV_PALETTE_GREEN, LV_PALETTE_PURPLE},
{LV_PALETTE_GREY, LV_PALETTE_CYAN},
{LV_PALETTE_YELLOW, LV_PALETTE_PINK},
{LV_PALETTE_ORANGE, LV_PALETTE_BLUE},
{LV_PALETTE_RED, LV_PALETTE_GREEN},
{LV_PALETTE_PURPLE, LV_PALETTE_GREY},
};
void WriteGlobalConfig() {
Preferences preferences;
preferences.begin("global_config", false);

View File

@@ -1,6 +1,8 @@
#ifndef _GLOBAL_CONFIG_INIT
#define _GLOBAL_CONFIG_INIT
#include "lvgl.h"
#define CONFIG_VERSION 80
typedef struct _GLOBAL_CONFIG {
@@ -11,6 +13,8 @@ typedef struct _GLOBAL_CONFIG {
bool screenCalibrated : 1;
bool wifiConfigured : 1;
bool ipConfigured : 1;
bool lightMode : 1;
bool invertColors : 1;
};
};
float screenCalXOffset;
@@ -23,9 +27,17 @@ typedef struct _GLOBAL_CONFIG {
char klipperHost[64];
unsigned short klipperPort;
unsigned char color_scheme;
} GLOBAL_CONFIG;
typedef struct _COLOR_DEF {
lv_palette_t primary_color;
lv_palette_t secondary_color;
} COLOR_DEF;
extern GLOBAL_CONFIG global_config;
extern COLOR_DEF color_defs[];
void WriteGlobalConfig();
void VerifyVersion();

View File

@@ -31,6 +31,7 @@ void send_gcode(bool wait, const char* gcode){
}
}
// TODO: Merge with other request, see https://moonraker.readthedocs.io/en/latest/printer_objects/#webhooks
void fetch_printer_state(){
char buff[91] = {};
sprintf(buff, "http://%s:%d/printer/info", global_config.klipperHost, global_config.klipperPort);
@@ -74,15 +75,17 @@ void fetch_printer_state(){
}
}
char filename_buff[512] = {0};
void fetch_printer_data(){
char buff[256] = {};
sprintf(buff, "http://%s:%d/printer/objects/query?extruder&heater_bed&toolhead&gcode_move", global_config.klipperHost, global_config.klipperPort);
sprintf(buff, "http://%s:%d/printer/objects/query?extruder&heater_bed&toolhead&gcode_move&virtual_sdcard&print_stats", global_config.klipperHost, global_config.klipperPort);
HTTPClient client;
client.begin(buff);
int httpCode = client.GET();
if (httpCode == 200) {
String payload = client.getString();
DynamicJsonDocument doc(2048);
DynamicJsonDocument doc(4096);
deserializeJson(doc, payload);
auto status = doc["result"]["status"];
if (status.containsKey("extruder")){
@@ -110,7 +113,45 @@ void fetch_printer_data(){
printer.absolute_coords = absolute_coords == true;
}
if (status.containsKey("virtual_sdcard")){
printer.print_progress = status["virtual_sdcard"]["progress"];
}
int printer_state = printer.state;
if (status.containsKey("print_stats")){
const char* filename = status["print_stats"]["filename"];
strcpy(filename_buff, filename);
printer.print_filename = filename_buff;
printer.elapsed_time_s = status["print_stats"]["print_duration"];
printer.filament_used_mm = status["print_stats"]["filament_used"];
const char* state = status["print_stats"]["state"];
if (strcmp(state, "printing") == 0){
printer_state = PRINTER_STATE_PRINTING;
}
else if (strcmp(state, "paused") == 0){
printer_state = PRINTER_STATE_PAUSED;
}
else if (strcmp(state, "complete") == 0 || strcmp(state, "cancelled") == 0 || strcmp(state, "standby") == 0){
printer_state = PRINTER_STATE_IDLE;
}
}
// TODO: make a call to /server/files/metadata to get more accurate time estimates
// https://moonraker.readthedocs.io/en/latest/web_api/#server-administration
if (printer.state == PRINTER_STATE_PRINTING && printer.print_progress > 0){
printer.remaining_time_s = (printer.elapsed_time_s / printer.print_progress) - printer.elapsed_time_s;
}
lv_msg_send(DATA_PRINTER_DATA, &printer);
if (printer.state != printer_state){
printer.state = printer_state;
lv_msg_send(DATA_PRINTER_STATE, &printer);
}
}
else {
Serial.printf("Failed to fetch printer data: %d\n", httpCode);
@@ -132,6 +173,7 @@ void data_loop(){
}
void data_setup(){
printer.print_filename = filename_buff;
fetch_printer_state();
fetch_printer_data();
}

View File

@@ -4,11 +4,12 @@ enum {
PRINTER_STATE_ERROR = 0,
PRINTER_STATE_IDLE = 1,
PRINTER_STATE_PRINTING = 2,
PRINTER_STATE_PAUSED = 3,
};
extern const char* printer_state_messages[];
typedef struct Printer {
typedef struct _Printer {
unsigned char state;
char* state_message;
float extruder_temp;
@@ -19,7 +20,12 @@ typedef struct Printer {
unsigned char can_extrude;
unsigned char homed_axis;
unsigned char absolute_coords;
} _Printer;
float elapsed_time_s;
float remaining_time_s;
float filament_used_mm;
char* print_filename; // 0 -> 1
float print_progress;
} Printer;
extern Printer printer;

View File

@@ -158,6 +158,16 @@ void screen_lv_touchRead(lv_indev_drv_t *indev_driver, lv_indev_data_t *data)
}
}
void set_color_scheme(){
lv_disp_t *dispp = lv_disp_get_default();
lv_theme_t *theme = lv_theme_default_init(dispp, lv_palette_main(color_defs[global_config.color_scheme].primary_color), lv_palette_main(color_defs[global_config.color_scheme].secondary_color), !global_config.lightMode, LV_FONT_DEFAULT);
lv_disp_set_theme(dispp, theme);
}
void set_invert_display(){
tft.invertDisplay(global_config.invertColors);
}
void screen_setup()
{
touchscreen_spi.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS);
@@ -169,6 +179,7 @@ void screen_setup()
tft.init();
tft.setRotation(1);
tft.fillScreen(TFT_BLACK);
set_invert_display();
touchscreen_spi.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS);
touchscreen.begin(touchscreen_spi);
@@ -199,8 +210,5 @@ void screen_setup()
/*Initialize the graphics library */
LV_EVENT_GET_COMP_CHILD = lv_event_register_id();
lv_disp_t *dispp = lv_disp_get_default();
lv_theme_t *theme = lv_theme_default_init(dispp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED), true, LV_FONT_DEFAULT);
lv_disp_set_theme(dispp, theme);
set_color_scheme();
}

View File

@@ -5,6 +5,7 @@
#define _SCREEN_DRIVER_INIT
#include <XPT2046_Touchscreen.h>
#include <TFT_eSPI.h>
#define XPT2046_IRQ 36
#define XPT2046_MOSI 32
@@ -19,6 +20,10 @@ void screen_timer_setup();
void screen_timer_start();
void screen_timer_stop();
void screen_timer_period(uint32_t period);
void set_color_scheme();
void screen_setup();
void set_invert_display();
extern TFT_eSPI tft;
#endif // _SCREEN_DRIVER_INIT

View File

@@ -17,7 +17,6 @@ static void event_handler(lv_event_t * e){
}
}
void setup() {
Serial.begin(115200);
Serial.println("Hello World");

View File

@@ -98,7 +98,7 @@ static void on_state_change(void * s, lv_msg_t * m){
error_ui();
}
else {
nav_buttons_setup(1);
nav_buttons_setup(0);
}
}

View File

@@ -8,6 +8,7 @@ static lv_style_t nav_button_style;
static char temp_buffer[10];
static char z_pos_buffer[10];
static char time_buffer[10];
static lv_style_t nav_button_text_style;
@@ -25,6 +26,39 @@ static void update_printer_data_temp(lv_event_t * e) {
lv_label_set_text(label, temp_buffer);
}
static void update_printer_data_time(lv_event_t * e){
lv_obj_t * label = lv_event_get_target(e);
if (printer.state == PRINTER_STATE_IDLE){
lv_label_set_text(label, "Idle");
return;
}
if (printer.state == PRINTER_STATE_PAUSED){
lv_label_set_text(label, "Paused");
return;
}
unsigned long time = printer.remaining_time_s;
unsigned long hours = time / 3600;
unsigned long minutes = (time % 3600) / 60;
unsigned long seconds = (time % 3600) % 60;
if (hours > 99){
lv_label_set_text(label, ">99h");
return;
}
if (hours >= 1){
sprintf(time_buffer, "%02luh%02lum", hours, minutes);
} else {
sprintf(time_buffer, "%02lum%02lus", minutes, seconds);
}
lv_label_set_text(label, time_buffer);
}
static void btn_click_files(lv_event_t * e){
nav_buttons_setup(0);
}
@@ -66,6 +100,8 @@ void nav_buttons_setup(unsigned char active_panel){
lv_label_set_text(label, "Idle");
lv_obj_align(label, LV_ALIGN_CENTER, 0, icon_text_spacing);
lv_obj_add_style(label, &nav_button_text_style, 0);
lv_obj_add_event_cb(label, update_printer_data_time, LV_EVENT_MSG_RECEIVED, NULL);
lv_msg_subsribe_obj(DATA_PRINTER_STATE, label, NULL);
// Move
btn = lv_btn_create(lv_scr_act());

View File

@@ -18,7 +18,7 @@ static void move_printer(const char* axis, float amount) {
const char * space = "%20";
sprintf(gcode, "G1%s%s%s%.1f", space, axis, extra, amount);
sprintf(gcode, "G1%s%s%s%.1f%sF6000", space, axis, extra, amount, space);
send_gcode(true, gcode);
if (absolute_coords) {

View File

@@ -3,4 +3,5 @@
void settings_panel_init(lv_obj_t* panel);
void temp_panel_init(lv_obj_t* panel);
void print_panel_init(lv_obj_t* panel);
void move_panel_init(lv_obj_t* panel);
void move_panel_init(lv_obj_t* panel);
void progress_panel_init(lv_obj_t* panel);

View File

@@ -1,6 +1,10 @@
#include "lvgl.h"
#include "panel.h"
#include "../../core/data_setup.h"
void print_panel_init(lv_obj_t* panel){
if (printer.state == PRINTER_STATE_PRINTING || printer.state == PRINTER_STATE_PAUSED || true){
progress_panel_init(panel);
return;
}
}

View File

@@ -0,0 +1,120 @@
#include "panel.h"
#include "../../core/data_setup.h"
#include <stdio.h>
char time_buffer[12];
char* time_display(unsigned long time){
unsigned long hours = time / 3600;
unsigned long minutes = (time % 3600) / 60;
unsigned long seconds = (time % 3600) % 60;
sprintf(time_buffer, "%02lu:%02lu:%02lu", hours, minutes, seconds);
return time_buffer;
}
static void progress_bar_update(lv_event_t* e){
lv_obj_t * bar = lv_event_get_target(e);
lv_bar_set_value(bar, printer.print_progress * 100, LV_ANIM_ON);
}
static void update_printer_data_elapsed_time(lv_event_t * e){
lv_obj_t * label = lv_event_get_target(e);
lv_label_set_text(label, time_display(printer.elapsed_time_s));
}
static void update_printer_data_remaining_time(lv_event_t * e){
lv_obj_t * label = lv_event_get_target(e);
lv_label_set_text(label, time_display(printer.remaining_time_s));
}
static void update_printer_data_percentage(lv_event_t * e){
lv_obj_t * label = lv_event_get_target(e);
char percentage_buffer[12];
sprintf(percentage_buffer, "%.2f%%", printer.print_progress * 100);
lv_label_set_text(label, percentage_buffer);
}
static void btn_click_stop(lv_event_t * e){
// TODO: Implement
}
static void btn_click_pause(lv_event_t * e){
// TODO: Implement
}
static void btn_click_resume(lv_event_t * e){
// TODO: Implement
}
void progress_panel_init(lv_obj_t* panel){
auto panel_width = TFT_HEIGHT - 40;
auto panel_width_margin = panel_width - 30;
// Filename
lv_obj_t * label = lv_label_create(panel);
lv_label_set_text(label, printer.print_filename);
lv_obj_align(label, LV_ALIGN_CENTER, 0, -40);
lv_label_set_long_mode(label, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_set_width(label, panel_width_margin);
// Progress Bar
lv_obj_t * bar = lv_bar_create(panel);
lv_obj_align(bar, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_size(bar, panel_width_margin, 20);
lv_obj_add_event_cb(bar, progress_bar_update, LV_EVENT_MSG_RECEIVED, NULL);
lv_msg_subsribe_obj(DATA_PRINTER_DATA, bar, NULL);
// Elapsed Time
label = lv_label_create(panel);
lv_label_set_text(label, "???");
lv_obj_align(label, LV_ALIGN_LEFT_MID, 10, 20);
lv_obj_add_event_cb(label, update_printer_data_elapsed_time, LV_EVENT_MSG_RECEIVED, NULL);
lv_msg_subsribe_obj(DATA_PRINTER_DATA, label, NULL);
// Remaining Time
label = lv_label_create(panel);
lv_label_set_text(label, "???");
lv_obj_align(label, LV_ALIGN_RIGHT_MID, -10, 20);
lv_obj_add_event_cb(label, update_printer_data_remaining_time, LV_EVENT_MSG_RECEIVED, NULL);
lv_msg_subsribe_obj(DATA_PRINTER_DATA, label, NULL);
// Percentage
label = lv_label_create(panel);
lv_label_set_text(label, "???");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 20);
lv_obj_add_event_cb(label, update_printer_data_percentage, LV_EVENT_MSG_RECEIVED, NULL);
lv_msg_subsribe_obj(DATA_PRINTER_DATA, label, NULL);
// Stop Button
lv_obj_t * btn = lv_btn_create(panel);
lv_obj_align(btn, LV_ALIGN_BOTTOM_RIGHT, -10, -10);
lv_obj_set_size(btn, 40, 40);
lv_obj_add_event_cb(btn, btn_click_stop, LV_EVENT_CLICKED, NULL);
label = lv_label_create(btn);
lv_label_set_text(label, LV_SYMBOL_STOP);
lv_obj_center(label);
// Resume Button
if (printer.state == PRINTER_STATE_PAUSED){
btn = lv_btn_create(panel);
lv_obj_align(btn, LV_ALIGN_BOTTOM_RIGHT, -60, -10);
lv_obj_set_size(btn, 40, 40);
lv_obj_add_event_cb(btn, btn_click_resume, LV_EVENT_CLICKED, NULL);
label = lv_label_create(btn);
lv_label_set_text(label, LV_SYMBOL_PLAY);
lv_obj_center(label);
}
// Pause Button
else {
btn = lv_btn_create(panel);
lv_obj_align(btn, LV_ALIGN_BOTTOM_RIGHT, -60, -10);
lv_obj_set_size(btn, 40, 40);
lv_obj_add_event_cb(btn, btn_click_pause, LV_EVENT_CLICKED, NULL);
label = lv_label_create(btn);
lv_label_set_text(label, LV_SYMBOL_PAUSE);
lv_obj_center(label);
}
}

View File

@@ -1,6 +1,99 @@
#include "lvgl.h"
#include "panel.h"
#include "../../core/screen_driver.h"
#include "../../conf/global_config.h"
static void invert_color_switch(lv_event_t * e){
auto state = lv_obj_get_state(lv_event_get_target(e));
bool checked = (state & LV_STATE_CHECKED == LV_STATE_CHECKED);
global_config.invertColors = checked;
WriteGlobalConfig();
set_invert_display();
}
static void reset_calibration_click(lv_event_t * e){
global_config.screenCalibrated = false;
WriteGlobalConfig();
ESP.restart();
}
static void reset_wifi_click(lv_event_t * e){
global_config.wifiConfigured = false;
global_config.ipConfigured = false;
WriteGlobalConfig();
ESP.restart();
}
static void light_mode_switch(lv_event_t * e){
auto state = lv_obj_get_state(lv_event_get_target(e));
bool checked = (state & LV_STATE_CHECKED == LV_STATE_CHECKED);
global_config.lightMode = checked;
WriteGlobalConfig();
set_color_scheme();
}
static void theme_dropdown(lv_event_t * e){
lv_obj_t * dropdown = lv_event_get_target(e);
auto selected = lv_dropdown_get_selected(dropdown);
global_config.color_scheme = selected;
WriteGlobalConfig();
set_color_scheme();
}
void settings_panel_init(lv_obj_t* panel){
auto panel_width = TFT_HEIGHT - 40;
lv_obj_t * btn = lv_btn_create(panel);
lv_obj_align(btn, LV_ALIGN_TOP_LEFT, 10, 5);
lv_obj_add_event_cb(btn, reset_wifi_click, LV_EVENT_CLICKED, NULL);
lv_obj_set_size(btn, panel_width / 2 - 15, 30);
lv_obj_t * label = lv_label_create(btn);
lv_label_set_text(label, "WiFi Setup");
lv_obj_center(label);
btn = lv_btn_create(panel);
lv_obj_align(btn, LV_ALIGN_TOP_RIGHT, -10, 5);
lv_obj_add_event_cb(btn, reset_calibration_click, LV_EVENT_CLICKED, NULL);
lv_obj_set_size(btn, panel_width / 2 - 15, 30);
label = lv_label_create(btn);
lv_label_set_text(label, "Touch Cal");
lv_obj_center(label);
lv_obj_t * toggle = lv_switch_create(panel);
lv_obj_align(toggle, LV_ALIGN_TOP_RIGHT, -14, 57);
lv_obj_add_event_cb(toggle, invert_color_switch, LV_EVENT_VALUE_CHANGED, NULL);
if (global_config.invertColors)
lv_obj_add_state(toggle, LV_STATE_CHECKED);
label = lv_label_create(panel);
lv_label_set_text(label, "Invert Colors");
lv_obj_align(label, LV_ALIGN_TOP_RIGHT, -10, 40);
lv_obj_set_style_text_font(label, &lv_font_montserrat_10, 0);
toggle = lv_switch_create(panel);
lv_obj_align(toggle, LV_ALIGN_TOP_LEFT, 13, 57);
lv_obj_add_event_cb(toggle, light_mode_switch, LV_EVENT_VALUE_CHANGED, NULL);
if (global_config.lightMode)
lv_obj_add_state(toggle, LV_STATE_CHECKED);
label = lv_label_create(panel);
lv_label_set_text(label, "Light Mode");
lv_obj_align(label, LV_ALIGN_TOP_LEFT, 10, 40);
lv_obj_set_style_text_font(label, &lv_font_montserrat_10, 0);
lv_obj_t * dropdown = lv_dropdown_create(panel);
lv_dropdown_set_options(dropdown, "Blue\nGreen\nGrey\nYellow\nOrange\nRed\nPurple");
lv_dropdown_set_selected(dropdown, global_config.color_scheme);
lv_obj_align(dropdown, LV_ALIGN_TOP_MID, 0, 55);
lv_obj_add_event_cb(dropdown, theme_dropdown, LV_EVENT_VALUE_CHANGED, NULL);
label = lv_label_create(panel);
lv_label_set_text(label, "Theme");
lv_obj_align(label, LV_ALIGN_TOP_MID, 0, 40);
lv_obj_set_style_text_font(label, &lv_font_montserrat_10, 0);
}

View File

@@ -1,6 +1,151 @@
#include "lvgl.h"
#include "panel.h"
#include "../../core/data_setup.h"
#include <HardwareSerial.h>
// False: Hotend, True: Bed
static bool hotend_or_bed = true;
static char hotend_buff[40];
static char bed_buff[40];
static void update_printer_data_hotend_temp(lv_event_t * e){
lv_obj_t * label = lv_event_get_target(e);
sprintf(hotend_buff, "Hotend: %.0f C\nTarget: %.0f C", printer.extruder_temp, printer.extruder_target_temp);
lv_label_set_text(label, hotend_buff);
}
static void update_printer_data_bed_temp(lv_event_t * e){
lv_obj_t * label = lv_event_get_target(e);
sprintf(bed_buff, "Bed: %.0f C\nTarget: %.0f C", printer.bed_temp, printer.bed_target_temp);
lv_label_set_text(label, bed_buff);
}
static void keyboard_callback(lv_event_t * e){
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t * ta = lv_event_get_target(e);
lv_obj_t * kb = (lv_obj_t *)lv_event_get_user_data(e);
if (code == LV_EVENT_READY) {
const char * text = lv_textarea_get_text(ta);
int temp = atoi(text);
if (temp < 0 || temp > 500){
return;
}
Serial.printf("%d %s %d\n", hotend_or_bed, text, temp);
char gcode[64];
const char* space = "%20";
if (hotend_or_bed){
sprintf(gcode, "M140%sS%d", space, temp);
} else {
sprintf(gcode, "M104%sS%d", space, temp);
}
send_gcode(true, gcode);
}
if(code == LV_EVENT_DEFOCUSED || code == LV_EVENT_CANCEL || code == LV_EVENT_READY) {
lv_keyboard_set_textarea(kb, NULL);
lv_obj_del(kb);
lv_obj_del(ta);
}
}
static void show_keyboard(lv_event_t * e){
lv_obj_t * panel = (lv_obj_t *)lv_event_get_user_data(e);
lv_obj_t * keyboard = lv_keyboard_create(panel);
lv_obj_t * ta = lv_textarea_create(panel);
lv_obj_set_size(ta, 100, 30);
lv_obj_align(ta, LV_ALIGN_TOP_MID, 0, 40);
lv_textarea_set_max_length(ta, 3);
lv_textarea_set_one_line(ta, true);
lv_textarea_set_text(ta, "");
lv_obj_add_event_cb(ta, keyboard_callback, LV_EVENT_ALL, keyboard);
lv_keyboard_set_mode(keyboard, LV_KEYBOARD_MODE_NUMBER);
lv_keyboard_set_textarea(keyboard, ta);
}
static void show_keyboard_with_hotend(lv_event_t * e){
hotend_or_bed = false;
show_keyboard(e);
}
static void show_keyboard_with_bed(lv_event_t * e){
hotend_or_bed = true;
show_keyboard(e);
}
static void cooldown_temp(lv_event_t * e){
send_gcode(true, "M104%20S0");
send_gcode(true, "M140%20S0");
}
static void btn_extrude(lv_event_t * e){
send_gcode(true, "M83");
send_gcode(true, "G1%20E25%20F300");
}
static void btn_retract(lv_event_t * e){
send_gcode(true, "M83");
send_gcode(true, "G1%20E-25%20F300");
}
void temp_panel_init(lv_obj_t* panel){
auto panel_width = TFT_HEIGHT - 40;
lv_obj_t * label = lv_label_create(panel);
lv_label_set_text(label, "Hotend");
lv_obj_align(label, LV_ALIGN_TOP_LEFT, 10, 10);
lv_obj_add_event_cb(label, update_printer_data_hotend_temp, LV_EVENT_MSG_RECEIVED, NULL);
lv_msg_subscribe_obj(DATA_PRINTER_DATA, label, NULL);
label = lv_label_create(panel);
lv_label_set_text(label, "Bed");
lv_obj_align(label, LV_ALIGN_TOP_LEFT, 10, 50);
lv_obj_add_event_cb(label, update_printer_data_bed_temp, LV_EVENT_MSG_RECEIVED, NULL);
lv_msg_subscribe_obj(DATA_PRINTER_DATA, label, NULL);
lv_obj_t * btn = lv_btn_create(panel);
lv_obj_align(btn, LV_ALIGN_TOP_RIGHT, -10, 10);
lv_obj_add_event_cb(btn, show_keyboard_with_hotend, LV_EVENT_CLICKED, panel);
label = lv_label_create(btn);
lv_label_set_text(label, "Set");
lv_obj_center(label);
btn = lv_btn_create(panel);
lv_obj_align(btn, LV_ALIGN_TOP_RIGHT, -10, 50);
lv_obj_add_event_cb(btn, show_keyboard_with_bed, LV_EVENT_CLICKED, panel);
label = lv_label_create(btn);
lv_label_set_text(label, "Set");
lv_obj_center(label);
btn = lv_btn_create(panel);
lv_obj_align(btn, LV_ALIGN_TOP_MID, 0, 90);
lv_obj_add_event_cb(btn, cooldown_temp, LV_EVENT_CLICKED, panel);
label = lv_label_create(btn);
lv_label_set_text(label, "Cooldown");
lv_obj_center(label);
btn = lv_btn_create(panel);
lv_obj_align(btn, LV_ALIGN_BOTTOM_LEFT, 10, -5);
lv_obj_add_event_cb(btn, btn_extrude, LV_EVENT_CLICKED, NULL);
lv_obj_set_size(btn, panel_width / 2 - 15, 30);
label = lv_label_create(btn);
lv_label_set_text(label, LV_SYMBOL_DOWN " Extrude");
lv_obj_center(label);
btn = lv_btn_create(panel);
lv_obj_align(btn, LV_ALIGN_BOTTOM_RIGHT, -10, -5);
lv_obj_add_event_cb(btn, btn_retract, LV_EVENT_CLICKED, NULL);
lv_obj_set_size(btn, panel_width / 2 - 15, 30);
label = lv_label_create(btn);
lv_label_set_text(label, LV_SYMBOL_UP " Retract");
lv_obj_center(label);
}