diff --git a/firmware/src/display_task.cpp b/firmware/src/display_task.cpp index b4eb9f5..9fe2e80 100644 --- a/firmware/src/display_task.cpp +++ b/firmware/src/display_task.cpp @@ -6,7 +6,16 @@ static const uint8_t LEDC_CHANNEL_LCD_BACKLIGHT = 0; -DisplayTask::DisplayTask(const uint8_t task_core) : Task{"Display", 2048, 1, task_core} { +TFT_eSPI tft_ = TFT_eSPI(); + +static lv_disp_draw_buf_t disp_buf; +uint32_t size_in_px = DISP_BUF_SIZE; +static lv_color_t buf[DISP_BUF_SIZE]; + +static lv_point_t points_left_bound[2] = {{0,0},{0,0}}; +static lv_point_t points_right_bound[2] = {{0,0},{0,0}}; + +DisplayTask::DisplayTask(const uint8_t task_core) : Task{"Display", 4096, 1, task_core} { knob_state_queue_ = xQueueCreate(1, sizeof(PB_SmartKnobState)); assert(knob_state_queue_ != NULL); @@ -19,6 +28,20 @@ DisplayTask::~DisplayTask() { vSemaphoreDelete(mutex_); } +void disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) +{ + uint32_t w = (area->x2 - area->x1 + 1); + uint32_t h = (area->y2 - area->y1 + 1); + uint32_t wh = w*h; + + tft_.startWrite(); + tft_.setAddrWindow(area->x1, area->y1, w, h); + tft_.pushColors(&color_p->full, w * h, true); + tft_.endWrite(); + + lv_disp_flush_ready(disp); +} + void DisplayTask::run() { tft_.begin(); tft_.invertDisplay(1); @@ -29,54 +52,147 @@ void DisplayTask::run() { ledcAttachPin(PIN_LCD_BACKLIGHT, LEDC_CHANNEL_LCD_BACKLIGHT); ledcWrite(LEDC_CHANNEL_LCD_BACKLIGHT, UINT16_MAX); - spr_.setColorDepth(8); + lv_init(); + + lv_disp_draw_buf_init(&disp_buf, buf, NULL, size_in_px); + + lv_disp_drv_t disp_drv; + lv_disp_drv_init(&disp_drv); + + log("Display Driver Init"); + disp_drv.hor_res = 240; + disp_drv.ver_res = 240; + disp_drv.flush_cb = disp_flush; + disp_drv.draw_buf = &disp_buf; + lv_disp_drv_register(&disp_drv); + + log("Display Driver Register"); + + + + //Create lvgl objects + screen = lv_scr_act(); + + lv_obj_set_style_bg_color(screen,lv_color_hex(0x383838),0); + lv_obj_set_style_bg_opa( screen,LV_OPA_COVER,0); + lv_obj_set_style_bg_grad_color(screen,lv_color_hex(0x7e4dfa),0); + lv_obj_set_style_bg_grad_dir(screen,LV_GRAD_DIR_VER,0); + lv_obj_set_style_bg_grad_stop(screen,TFT_HEIGHT,0); + lv_obj_set_style_bg_main_stop(screen,TFT_HEIGHT,0); //Same value for gradient and main gives sharp line on Gradient + + + //Testing with gauge. Not really suitable after a short test. Maybe create own lvgl Widget that is more usable with the smartknob + /**gauge = lv_gauge_create(screen,NULL); + lv_obj_clean_style_list(gauge,LV_GAUGE_PART_MAIN); + lv_obj_set_size(gauge,210,210); + lv_obj_align(gauge,screen,LV_ALIGN_CENTER,0,0); + lv_gauge_set_range(gauge,0,10); + lv_gauge_set_scale(gauge,360,10,10); + lv_gauge_set_value(gauge,0,4);**/ + + /*Create line style*/ + static lv_style_t style_line; + lv_style_init(&style_line); + lv_style_set_line_width(&style_line, 4); + lv_style_set_line_color(&style_line, lv_color_white()); + lv_style_set_line_rounded(&style_line, true); + + line_left_bound = lv_line_create(screen); + lv_obj_add_style(line_left_bound,&style_line, 0); + lv_obj_set_pos(line_left_bound,0,0); + + + line_right_bound = lv_line_create(screen); + lv_obj_add_style(line_right_bound,&style_line, 0); + lv_obj_set_pos(line_right_bound,0,0); + + arc_dot = lv_arc_create(screen);//use this arc only for the knob + //lv_style_reset(arc_dot,LV_PART_MAIN);//we dont need a background + //lv_obj_set_style_bg_opa(); + lv_obj_remove_style(arc_dot,NULL,0); + lv_arc_set_bg_angles(arc_dot,0,0); + lv_obj_set_style_line_width(arc_dot,20,0); + lv_obj_set_style_pad_all(arc_dot,10,0);//line width and padding are used in calculation for the knob and arc so we have to set them + //lv_obj_set_style_bg_opa(arc_dot,LV_ARC_PART_BG,LV_STATE_DEFAULT,LV_OPA_TRANSP); + //lv_obj_set_style_bg_opa(arc_dot,LV_ARC_PART_INDIC,LV_STATE_DEFAULT,LV_OPA_TRANSP); + lv_obj_remove_style(arc_dot,NULL,LV_PART_INDICATOR | LV_STATE_DEFAULT); + + lv_obj_set_size(arc_dot,TFT_WIDTH,TFT_HEIGHT); + lv_obj_align(arc_dot,LV_ALIGN_CENTER,0,0); + //lv_arc_set_adjustable(arc_dot,true);//knob showing + lv_obj_add_flag(arc_dot,LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(arc_dot, LV_OBJ_FLAG_CLICKABLE); + + lv_arc_set_angles(arc_dot,0,0); + //lv_arc_set_bg_angles(arc_dot,150,260); + //lv_arc_set_value(arc_dot,20); + lv_obj_set_style_line_width(arc_dot,10,LV_PART_INDICATOR|LV_STATE_DEFAULT); + lv_obj_set_style_pad_all(arc_dot,5,LV_PART_KNOB|LV_STATE_DEFAULT); + lv_arc_set_rotation(arc_dot, 270); + + arc = lv_arc_create(screen); + //lv_obj_clean_style_list(arc,LV_ARC_PART_BG); + lv_obj_remove_style(arc,NULL,LV_PART_MAIN | LV_STATE_DEFAULT); + lv_arc_set_bg_angles(arc,0,0); + lv_obj_set_style_line_width(arc,20,0); + lv_obj_set_style_pad_all(arc,10,0); + lv_obj_set_size(arc,TFT_WIDTH,TFT_HEIGHT); + lv_obj_align(arc,LV_ALIGN_CENTER,0,0); + //lv_arc_set_adjustable(arc,false);//knob hidden + lv_obj_add_flag(arc,LV_OBJ_FLAG_SCROLLABLE); + lv_arc_set_angles(arc,0,0); + lv_obj_set_style_line_width(arc,10,LV_PART_INDICATOR|LV_STATE_DEFAULT); + //lv_obj_set_style_pad_all(arc,LV_ARC_PART_KNOB,LV_STATE_DEFAULT,5); + lv_arc_set_rotation(arc, 270); + + //lv_obj_set_style_line_color(arc_dot,LV_ARC_PART_INDIC,LV_STATE_DEFAULT,LV_COLOR_TEAL); + //lv_obj_set_style_line_color(arc_dot,LV_ARC_PART_KNOB,LV_STATE_DEFAULT,LV_COLOR_CYAN); + //lv_obj_set_style_pad_all(arc_dot,LV_ARC_PART_INDIC,LV_STATE_DEFAULT,8); + //lv_obj_set_style_size(arc_dot,LV_ARC_PART_KNOB,LV_STATE_DEFAULT,10); + + + + label_cur_pos = lv_label_create(screen); + //lv_obj_set_pos(label_cur_pos,TFT_WIDTH/2,TFT_HEIGHT/2-VALUE_OFFSET); + //lv_label_set_align(label_cur_pos,LV_LABEL_ALIGN_CENTER); + lv_obj_set_style_text_align(label_cur_pos,LV_ALIGN_CENTER,LV_PART_MAIN); + lv_obj_align(label_cur_pos,LV_ALIGN_CENTER,0,-VALUE_OFFSET); + lv_label_set_text(label_cur_pos,"0"); + lv_obj_set_style_text_color(label_cur_pos,lv_color_white(),0); + lv_obj_set_style_text_font(label_cur_pos,&lv_font_montserrat_28,0); + + label_desc = lv_label_create(screen); + //lv_obj_set_pos(label_desc,TFT_WIDTH/2,TFT_HEIGHT / 2 + DESCRIPTION_Y_OFFSET); + lv_obj_set_style_text_align(label_desc,LV_ALIGN_CENTER,LV_PART_MAIN); + lv_obj_align(label_desc,LV_ALIGN_CENTER,0,DESCRIPTION_Y_OFFSET); + lv_label_set_text(label_desc,""); + lv_obj_set_style_text_color(label_desc,lv_color_white(),0); + lv_obj_set_style_text_font(label_desc,&lv_font_montserrat_16,0); - if (spr_.createSprite(TFT_WIDTH, TFT_HEIGHT) == nullptr) { - log("ERROR: sprite allocation failed!"); - tft_.fillScreen(TFT_RED); - } else { - log("Sprite created!"); - tft_.fillScreen(TFT_PURPLE); - } - spr_.setTextColor(0xFFFF, TFT_BLACK); PB_SmartKnobState state; const int RADIUS = TFT_WIDTH / 2; - const uint16_t FILL_COLOR = spr_.color565(90, 18, 151); - const uint16_t DOT_COLOR = spr_.color565(80, 100, 200); - spr_.setTextDatum(CC_DATUM); - spr_.setTextColor(TFT_WHITE); + while(1) { if (xQueueReceive(knob_state_queue_, &state, portMAX_DELAY) == pdFALSE) { continue; } - spr_.fillSprite(TFT_BLACK); if (state.config.num_positions > 1) { - int32_t height = state.current_position * TFT_HEIGHT / (state.config.num_positions - 1); - spr_.fillRect(0, TFT_HEIGHT - height, TFT_WIDTH, height, FILL_COLOR); + int32_t height = state.current_position * TFT_HEIGHT / (state.config.num_positions-1); + lv_obj_set_style_bg_grad_stop(screen,TFT_HEIGHT-height,0); + lv_obj_set_style_bg_main_stop(screen,TFT_HEIGHT-height,0); //Same value for gradient and main gives sharp line on Gradient } - spr_.setFreeFont(&Roboto_Light_60); - spr_.drawString(String() + state.current_position, TFT_WIDTH / 2, TFT_HEIGHT / 2 - VALUE_OFFSET, 1); - spr_.setFreeFont(&DESCRIPTION_FONT); - int32_t line_y = TFT_HEIGHT / 2 + DESCRIPTION_Y_OFFSET; - char* start = state.config.text; - char* end = start + strlen(state.config.text); - while (start < end) { - char* newline = strchr(start, '\n'); - if (newline == nullptr) { - newline = end; - } - - char buf[sizeof(state.config.text)] = {}; - strncat(buf, start, min(sizeof(buf) - 1, (size_t)(newline - start))); - spr_.drawString(String(buf), TFT_WIDTH / 2, line_y, 1); - start = newline + 1; - line_y += spr_.fontHeight(1); - } + lv_label_set_text_fmt(label_cur_pos,"%d" , state.current_position); + lv_obj_set_style_text_align(label_cur_pos,LV_ALIGN_CENTER,LV_PART_MAIN); + lv_obj_align(label_cur_pos,LV_ALIGN_CENTER,0,-VALUE_OFFSET); + + lv_label_set_text(label_desc,state.config.text); + lv_obj_set_style_text_align(label_desc,LV_ALIGN_CENTER,LV_PART_MAIN); + lv_obj_align(label_desc,LV_ALIGN_CENTER,0,DESCRIPTION_Y_OFFSET); float left_bound = PI / 2; @@ -84,11 +200,32 @@ void DisplayTask::run() { float range_radians = (state.config.num_positions - 1) * state.config.position_width_radians; left_bound = PI / 2 + range_radians / 2; float right_bound = PI / 2 - range_radians / 2; - spr_.drawLine(TFT_WIDTH/2 + RADIUS * cosf(left_bound), TFT_HEIGHT/2 - RADIUS * sinf(left_bound), TFT_WIDTH/2 + (RADIUS - 10) * cosf(left_bound), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(left_bound), TFT_WHITE); - spr_.drawLine(TFT_WIDTH/2 + RADIUS * cosf(right_bound), TFT_HEIGHT/2 - RADIUS * sinf(right_bound), TFT_WIDTH/2 + (RADIUS - 10) * cosf(right_bound), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(right_bound), TFT_WHITE); + points_left_bound[0] = { + (lv_coord_t)(TFT_WIDTH/2 + RADIUS * cosf(left_bound)), + (lv_coord_t)(TFT_HEIGHT/2 - RADIUS * sinf(left_bound)) + }; + points_left_bound[1] = { + (lv_coord_t)(TFT_WIDTH/2 + (RADIUS - 10) * cosf(left_bound)), + (lv_coord_t)(TFT_HEIGHT/2 - (RADIUS - 10) * sinf(left_bound)) + }; + lv_line_set_points(line_left_bound,points_left_bound,2); + points_right_bound[0] = { + (lv_coord_t)(TFT_WIDTH/2 + RADIUS * cosf(right_bound)), + (lv_coord_t)(TFT_HEIGHT/2 - RADIUS * sinf(right_bound)) + }; + points_right_bound[1] = { + (lv_coord_t)(TFT_WIDTH/2 + (RADIUS - 10) * cosf(right_bound)), + (lv_coord_t)(TFT_HEIGHT/2 - (RADIUS - 10) * sinf(right_bound)) + }; + lv_line_set_points(line_right_bound,points_right_bound,2); } - if (DRAW_ARC) { - spr_.drawCircle(TFT_WIDTH/2, TFT_HEIGHT/2, RADIUS, TFT_DARKGREY); + else{ + points_left_bound[0] = {0,0}; + points_left_bound[1] = {0,0}; + lv_line_set_points(line_left_bound,points_left_bound,2); + points_right_bound[0] = {0,0}; + points_right_bound[1] = {0,0}; + lv_line_set_points(line_right_bound,points_right_bound,2); } float adjusted_sub_position = state.sub_position_unit * state.config.position_width_radians; @@ -102,26 +239,24 @@ void DisplayTask::run() { float raw_angle = left_bound - state.current_position * state.config.position_width_radians; float adjusted_angle = raw_angle - adjusted_sub_position; + int32_t raw_angle_offset = (int)(-((raw_angle*(180/PI))-90))%360; + int32_t adjusted_angle_offset = (int)(-((adjusted_angle*(180/PI))-90))%360; - if (state.config.num_positions > 0 && ((state.current_position == 0 && state.sub_position_unit < 0) || (state.current_position == state.config.num_positions - 1 && state.sub_position_unit > 0))) { + lv_arc_set_angles(arc_dot,raw_angle_offset,raw_angle_offset); - spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(raw_angle), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(raw_angle), 5, DOT_COLOR); - if (raw_angle < adjusted_angle) { - for (float r = raw_angle; r <= adjusted_angle; r += 2 * PI / 180) { - spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(r), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(r), 2, DOT_COLOR); - } - spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(adjusted_angle), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(adjusted_angle), 2, DOT_COLOR); + if(adjusted_sub_position!=0){ + lv_obj_add_flag(arc,LV_OBJ_FLAG_HIDDEN); + if (raw_angle_offset < adjusted_angle_offset) { + lv_arc_set_angles(arc,raw_angle_offset,adjusted_angle_offset); } else { - for (float r = raw_angle; r >= adjusted_angle; r -= 2 * PI / 180) { - spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(r), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(r), 2, DOT_COLOR); - } - spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(adjusted_angle), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(adjusted_angle), 2, DOT_COLOR); + lv_arc_set_angles(arc,adjusted_angle_offset,raw_angle_offset); } - } else { - spr_.fillCircle(TFT_WIDTH/2 + (RADIUS - 10) * cosf(adjusted_angle), TFT_HEIGHT/2 - (RADIUS - 10) * sinf(adjusted_angle), 5, DOT_COLOR); + } + else{ + lv_obj_clear_flag(arc,LV_OBJ_FLAG_HIDDEN); } - spr_.pushSprite(0, 0); + lv_task_handler(); { SemaphoreGuard lock(mutex_); diff --git a/firmware/src/display_task.h b/firmware/src/display_task.h index 1ecfe27..b6934c9 100644 --- a/firmware/src/display_task.h +++ b/firmware/src/display_task.h @@ -4,11 +4,14 @@ #include #include +#include "lvgl.h" #include "logger.h" #include "proto_gen/smartknob.pb.h" #include "task.h" +#define DISP_BUF_SIZE (TFT_WIDTH * 10) + class DisplayTask : public Task { friend class Task; // Allow base Task to invoke protected run() @@ -25,10 +28,16 @@ class DisplayTask : public Task { void run(); private: - TFT_eSPI tft_ = TFT_eSPI(); - /** Full-size sprite used as a framebuffer */ - TFT_eSprite spr_ = TFT_eSprite(&tft_); + lv_obj_t * screen; + lv_obj_t * label_cur_pos; + lv_obj_t * label_desc; + lv_obj_t * arc; + lv_obj_t * gauge; + lv_obj_t * roller; + lv_obj_t * line_left_bound; + lv_obj_t * line_right_bound; + lv_obj_t * arc_dot; QueueHandle_t knob_state_queue_; diff --git a/platformio.ini b/platformio.ini index 3b0fc90..871953c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -47,6 +47,7 @@ lib_deps = ; registry for the runtime. The submodule is available for manually updating ; the pre-compiled (checked in) .pb.h/c files when proto files change, but is ; otherwise not used during application firmware compilation. + lvgl/lvgl@^8.3.0 build_flags = ${base_config.build_flags} @@ -109,3 +110,10 @@ build_flags = ; GPIO >= 34 are input only ; (SOC_GPIO_VALID_GPIO_MASK & ~(0ULL | _FL_BIT(34) | _FL_BIT(35) | _FL_BIT(36) | _FL_BIT(37) | _FL_BIT(38) | _FL_BIT(39))) -DSOC_GPIO_VALID_OUTPUT_GPIO_MASK=0x30EFFFFFF + -DLV_LVGL_H_INCLUDE_SIMPLE=1 + -DLV_CONF_SKIP=1 + -DLV_COLOR_DEPTH=16 + -DLV_TICK_CUSTOM=1 + -DLV_THEME_DEFAULT_DARK=1 + -DLV_FONT_MONTSERRAT_16=1 + -DLV_FONT_MONTSERRAT_28=1