diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index a9d31d42d8b..b429e1e322a 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -9,7 +9,7 @@ from esphome.components.const import ( CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING, ) -from esphome.components.display import Display +from esphome.components.display import Display, get_display_metadata, validate_rotation from esphome.components.esp32 import ( VARIANT_ESP32P4, add_idf_component, @@ -37,6 +37,7 @@ from esphome.const import ( CONF_ON_BOOT, CONF_ON_IDLE, CONF_PAGES, + CONF_ROTATION, CONF_TIMEOUT, CONF_TRIGGER_ID, ) @@ -74,6 +75,7 @@ from .trigger import add_on_boot_triggers, generate_align_tos, generate_triggers from .types import ( IdleTrigger, PlainTrigger, + RotationType, lv_font_t, lv_group_t, lv_lambda_t, @@ -185,6 +187,7 @@ def final_validation(config_list): for config in config_list: if (pages := config.get(CONF_PAGES)) and all(p[df.CONF_SKIP] for p in pages): raise cv.Invalid("At least one page must not be skipped") + uses_rotation = CONF_ROTATION in config for display_id in config[df.CONF_DISPLAYS]: path = global_config.get_path_for_id(display_id)[:-1] display = global_config.get_config_for_path(path) @@ -192,6 +195,11 @@ def final_validation(config_list): raise cv.Invalid( "Using lambda: or pages: in display config is not compatible with LVGL" ) + # treating 0 as false is intended here. + if uses_rotation and display.get(CONF_ROTATION): + df.LOGGER.warning( + "use of 'rotation' in both LVGL and the display config is not recommended" + ) if display.get(CONF_AUTO_CLEAR_ENABLED) is True: raise cv.Invalid( "Using auto_clear_enabled: true in display config not compatible with LVGL" @@ -322,6 +330,18 @@ async def to_code(configs): displays = [ await cg.get_variable(display) for display in config[df.CONF_DISPLAYS] ] + rotation_type = RotationType.ROTATION_UNUSED + # options will have CONF_ROTATION true if rotation is changed in an automation. + if CONF_ROTATION in config or df.get_options().get(CONF_ROTATION) is True: + if all( + get_display_metadata(str(disp)).has_hardware_rotation + for disp in displays + ): + rotation_type = RotationType.ROTATION_HARDWARE + df.LOGGER.info("LVGL will use hardware rotation via display driver") + else: + rotation_type = RotationType.ROTATION_SOFTWARE + df.LOGGER.info("LVGL will use software rotation") lv_component = cg.new_Pvariable( config[CONF_ID], displays, @@ -330,8 +350,11 @@ async def to_code(configs): config[CONF_DRAW_ROUNDING], config[df.CONF_RESUME_ON_INPUT], config[df.CONF_UPDATE_WHEN_DISPLAY_IDLE], + rotation_type, ) await cg.register_component(lv_component, config) + if rotation := config.get(CONF_ROTATION): + cg.add(lv_component.set_rotation(rotation)) Widget.create(config[CONF_ID], lv_component, LvScrActType(), config) lv_scr_act = get_screen_active(lv_component) @@ -492,6 +515,7 @@ LVGL_SCHEMA = cv.All( ): cv.boolean, cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int, cv.Optional(CONF_BUFFER_SIZE, default=0): cv.percentage, + cv.Optional(CONF_ROTATION): validate_rotation, cv.Optional(CONF_LOG_LEVEL, default="WARN"): cv.one_of( *df.LV_LOG_LEVELS, upper=True ), diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index 50e6db74b8f..b825320a407 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -3,8 +3,9 @@ from typing import Any from esphome import automation import esphome.codegen as cg +from esphome.components.display import validate_rotation import esphome.config_validation as cv -from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_TIMEOUT +from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_ROTATION, CONF_TIMEOUT from esphome.core import Lambda from esphome.cpp_generator import TemplateArguments, get_variable from esphome.cpp_types import nullptr @@ -23,6 +24,7 @@ from .defines import ( PARTS, StaticCastExpression, add_warning, + get_options, ) from .lv_validation import lv_bool, lv_milliseconds from .lvcode import ( @@ -191,6 +193,33 @@ async def lvgl_is_idle(config, condition_id, template_arg, args): return var +def _validate_rotation(value): + # Note that we need rotation + get_options()[CONF_ROTATION] = True + return validate_rotation(value) + + +@automation.register_action( + "lvgl.display.set_rotation", + ObjUpdateAction, + cv.maybe_simple_value( + LVGL_SCHEMA.extend( + { + cv.Required(CONF_ROTATION): _validate_rotation, + } + ), + key=CONF_ROTATION, + ), + synchronous=True, +) +async def lvgl_set_rotation(config, action_id, template_arg, args): + lv_comp = await cg.get_variable(config[CONF_LVGL_ID]) + async with LambdaContext() as context: + add_line_marks(where=action_id) + lv_add(lv_comp.set_rotation(config[CONF_ROTATION])) + return cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + + @automation.register_action( "lvgl.widget.redraw", ObjUpdateAction, diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 668bb465151..ae8387bccac 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -29,6 +29,7 @@ KEY_COLOR_FORMATS = "color_formats" KEY_LV_DEFINES = "lv_defines" KEY_REMAPPED_USES = "remapped_uses" KEY_UPDATED_WIDGETS = "updated_widgets" +KEY_OPTIONS = "options" KEY_WARNINGS = "warnings" @@ -56,6 +57,10 @@ def add_warning(msg: str): get_warnings().add(msg) +def get_options(): + return get_data(KEY_OPTIONS) + + class StaticCastExpression(Expression): __slots__ = ("type", "exp") diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index a5075cb6148..0ab49d0a101 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -83,6 +83,44 @@ std::string lv_event_code_name_for(lv_event_t *event) { return buf; } +void LvglComponent::set_rotation(display::DisplayRotation rotation) { + if (this->rotation_type_ == RotationType::ROTATION_UNUSED) { + ESP_LOGW(TAG, "Display rotation cannot be changed unless rotation was enabled during setup."); + return; + } + this->rotation_ = rotation; + if (this->is_ready()) { + this->set_resolution_(); + lv_obj_update_layout(this->get_screen_active()); + lv_obj_invalidate(this->get_screen_active()); + } +} + +void LvglComponent::rotate_coordinates(int32_t &x, int32_t &y) const { + switch (this->rotation_) { + default: + break; + + case display::DISPLAY_ROTATION_180_DEGREES: { + x = this->width_ - x - 1; + y = this->height_ - y - 1; + break; + } + case display::DISPLAY_ROTATION_270_DEGREES: { + auto tmp = x; + x = this->height_ - y - 1; + y = tmp; + break; + } + case display::DISPLAY_ROTATION_90_DEGREES: { + auto tmp = y; + y = this->width_ - x - 1; + x = tmp; + break; + } + } +} + static void rounder_cb(lv_event_t *event) { auto *comp = static_cast(lv_event_get_user_data(event)); auto *area = static_cast(lv_event_get_param(event)); @@ -118,7 +156,11 @@ void LvglComponent::dump_config() { " Buffer size: %zu%%\n" " Rotation: %d\n" " Draw rounding: %d", - this->width_, this->height_, 100 / this->buffer_frac_, this->rotation, (int) this->draw_rounding); + this->width_, this->height_, 100 / this->buffer_frac_, this->rotation_, (int) this->draw_rounding); + if (this->rotation_type_ != ROTATION_UNUSED) { + ESP_LOGCONFIG(TAG, " Rotation type: %s", + this->rotation_type_ == RotationType::ROTATION_SOFTWARE ? "software" : "hardware via display driver"); + } } void LvglComponent::set_paused(bool paused, bool show_snow) { @@ -216,48 +258,51 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) { auto height_rounded = (height + this->draw_rounding - 1) / this->draw_rounding * this->draw_rounding; auto x1 = area->x1; auto y1 = area->y1; - lv_color_data *dst = reinterpret_cast(this->rotate_buf_); - switch (this->rotation) { - case display::DISPLAY_ROTATION_90_DEGREES: - for (lv_coord_t x = height; x-- != 0;) { - for (lv_coord_t y = 0; y != width; y++) { - dst[y * height_rounded + x] = *ptr++; + if (this->rotation_type_ == RotationType::ROTATION_SOFTWARE) { + lv_color_data *dst = reinterpret_cast(this->rotate_buf_); + switch (this->rotation_) { + case display::DISPLAY_ROTATION_90_DEGREES: + for (lv_coord_t x = height; x-- != 0;) { + for (lv_coord_t y = 0; y != width; y++) { + dst[y * height_rounded + x] = *ptr++; + } } - } - y1 = x1; - x1 = this->height_ - area->y1 - height; - height = width; - width = height_rounded; - break; + y1 = x1; + x1 = this->width_ - area->y1 - height; + height = width; + width = height_rounded; + break; - case display::DISPLAY_ROTATION_180_DEGREES: - for (lv_coord_t y = height; y-- != 0;) { - for (lv_coord_t x = width; x-- != 0;) { - dst[y * width + x] = *ptr++; + case display::DISPLAY_ROTATION_180_DEGREES: + for (lv_coord_t y = height; y-- != 0;) { + for (lv_coord_t x = width; x-- != 0;) { + dst[y * width + x] = *ptr++; + } } - } - x1 = this->width_ - x1 - width; - y1 = this->height_ - y1 - height; - break; + x1 = this->width_ - x1 - width; + y1 = this->height_ - y1 - height; + break; - case display::DISPLAY_ROTATION_270_DEGREES: - for (lv_coord_t x = 0; x != height; x++) { - for (lv_coord_t y = width; y-- != 0;) { - dst[y * height_rounded + x] = *ptr++; + case display::DISPLAY_ROTATION_270_DEGREES: + for (lv_coord_t x = 0; x != height; x++) { + for (lv_coord_t y = width; y-- != 0;) { + dst[y * height_rounded + x] = *ptr++; + } } - } - x1 = y1; - y1 = this->width_ - area->x1 - width; - height = width; - width = height_rounded; - break; + x1 = y1; + y1 = this->height_ - area->x1 - width; + height = width; + width = height_rounded; + break; - default: - dst = ptr; - break; + default: + dst = ptr; + break; + } + ptr = dst; } for (auto *display : this->displays_) { - display->draw_pixels_at(x1, y1, width, height, (const uint8_t *) dst, display::COLOR_ORDER_RGB, LV_BITNESS, + display->draw_pixels_at(x1, y1, width, height, (const uint8_t *) ptr, display::COLOR_ORDER_RGB, LV_BITNESS, this->big_endian_); } } @@ -297,6 +342,7 @@ LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_r if (l->touch_pressed_) { data->point.x = l->touch_point_.x; data->point.y = l->touch_point_.y; + l->parent_->rotate_coordinates(data->point.x, data->point.y); data->state = LV_INDEV_STATE_PRESSED; } else { data->state = LV_INDEV_STATE_RELEASED; @@ -543,26 +589,44 @@ void LvglComponent::write_random_() { * multiple of 2, and so on. * @param resume_on_input if true, this component will resume rendering when the user * presses a key or clicks on the screen. + * @param rotation_type What rotation type to use, if any */ LvglComponent::LvglComponent(std::vector displays, float buffer_frac, bool full_refresh, - int draw_rounding, bool resume_on_input, bool update_when_display_idle) + int draw_rounding, bool resume_on_input, bool update_when_display_idle, + RotationType rotation_type) : draw_rounding(draw_rounding), displays_(std::move(displays)), buffer_frac_(buffer_frac), full_refresh_(full_refresh), resume_on_input_(resume_on_input), - update_when_display_idle_(update_when_display_idle) { + update_when_display_idle_(update_when_display_idle), + rotation_type_(rotation_type) { this->disp_ = lv_display_create(240, 240); } +void LvglComponent::set_resolution_() const { + int32_t width = this->width_; + int32_t height = this->height_; + if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES || + this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) { + std::swap(width, height); + } + ESP_LOGD(TAG, "Setting resolution to %u x %u (rotation %d)", (unsigned) width, (unsigned) height, + (int) this->rotation_); + if (this->rotation_type_ == RotationType::ROTATION_HARDWARE) { + for (auto *display : this->displays_) + display->set_rotation(this->rotation_); + } + lv_display_set_resolution(this->disp_, width, height); +} void LvglComponent::setup() { auto *display = this->displays_[0]; auto rounding = this->draw_rounding; + this->width_ = display->get_native_width(); + this->height_ = display->get_native_height(); // cater for displays with dimensions that don't divide by the required rounding - this->width_ = display->get_width(); - this->height_ = display->get_height(); - auto width = (display->get_width() + rounding - 1) / rounding * rounding; - auto height = (display->get_height() + rounding - 1) / rounding * rounding; + auto width = (this->width_ + rounding - 1) / rounding * rounding; + auto height = (this->height_ + rounding - 1) / rounding * rounding; auto frac = this->buffer_frac_; if (frac == 0) frac = 1; @@ -586,15 +650,14 @@ void LvglComponent::setup() { return; } this->draw_buf_ = static_cast(buffer); - lv_display_set_resolution(this->disp_, this->width_, this->height_); + this->set_resolution_(); lv_display_set_color_format(this->disp_, LV_COLOR_FORMAT_RGB565); lv_display_set_flush_cb(this->disp_, static_flush_cb); lv_display_set_user_data(this->disp_, this); lv_display_add_event_cb(this->disp_, rounder_cb, LV_EVENT_INVALIDATE_AREA, this); lv_display_set_buffers(this->disp_, this->draw_buf_, nullptr, buf_bytes, this->full_refresh_ ? LV_DISPLAY_RENDER_MODE_FULL : LV_DISPLAY_RENDER_MODE_PARTIAL); - this->rotation = display->get_rotation(); - if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) { + if (this->rotation_type_ == RotationType::ROTATION_SOFTWARE) { this->rotate_buf_ = static_cast(lv_alloc_draw_buf(buf_bytes, false)); // NOLINT if (this->rotate_buf_ == nullptr) { this->status_set_error(LOG_STR("Memory allocation failure")); @@ -620,9 +683,6 @@ void LvglComponent::setup() { esp_log_printf_(LOG_LEVEL_MAP[level], TAG, 0, "%.*s", (int) strlen(buf) - 1, buf); }); #endif - // Rotation will be handled by our drawing function, so reset the display rotation. - for (auto *disp : this->displays_) - disp->set_rotation(display::DISPLAY_ROTATION_0_DEGREES); this->show_page(0, LV_SCREEN_LOAD_ANIM_NONE, 0); lv_display_trigger_activity(this->disp_); } diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 8d139b23cb1..4a4c11d3834 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -156,13 +156,18 @@ template class ObjUpdateAction : public Action { #ifdef USE_LVGL_ANIMIMG void lv_animimg_stop(lv_obj_t *obj); #endif // USE_LVGL_ANIMIMG +enum RotationType : uint8_t { + ROTATION_UNUSED, + ROTATION_SOFTWARE, + ROTATION_HARDWARE, +}; class LvglComponent : public PollingComponent { constexpr static const char *const TAG = "lvgl"; public: LvglComponent(std::vector displays, float buffer_frac, bool full_refresh, int draw_rounding, - bool resume_on_input, bool update_when_display_idle); + bool resume_on_input, bool update_when_display_idle, RotationType rotation_type); static void static_flush_cb(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p); float get_setup_priority() const override { return setup_priority::PROCESSOR; } @@ -216,13 +221,16 @@ class LvglComponent : public PollingComponent { // rounding factor to align bounds of update area when drawing size_t draw_rounding{2}; - display::DisplayRotation rotation{display::DISPLAY_ROTATION_0_DEGREES}; void set_pause_trigger(Trigger<> *trigger) { this->pause_callback_ = trigger; } void set_resume_trigger(Trigger<> *trigger) { this->resume_callback_ = trigger; } void set_draw_start_trigger(Trigger<> *trigger) { this->draw_start_callback_ = trigger; } void set_draw_end_trigger(Trigger<> *trigger) { this->draw_end_callback_ = trigger; } + void set_rotation(display::DisplayRotation rotation); + display::DisplayRotation get_rotation() const { return this->rotation_; } + void rotate_coordinates(int32_t &x, int32_t &y) const; protected: + void set_resolution_() const; void draw_end_(); // Not checking for non-null callback since the // LVGL callback that calls it is not set in that case @@ -256,6 +264,8 @@ class LvglComponent : public PollingComponent { Trigger<> *draw_start_callback_{}; Trigger<> *draw_end_callback_{}; void *rotate_buf_{}; + display::DisplayRotation rotation_{display::DISPLAY_ROTATION_0_DEGREES}; + RotationType rotation_type_; }; class IdleTrigger : public Trigger<> { diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 686e4292679..0c8ddfbfbd0 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -69,6 +69,7 @@ lv_page_t = LvType("LvPageType", parents=(LvCompound,)) lv_image_t = LvType("lv_image_t") lv_gradient_t = LvType("lv_grad_dsc_t") lv_event_t = LvType("lv_event_t") +RotationType = lvgl_ns.enum("RotationType") LV_EVENT = MockObj(base="LV_EVENT_", op="") LV_STATE = MockObj(base="LV_STATE_", op="") diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 3c5c730e6cb..4d44c62000c 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -30,12 +30,19 @@ binary_sensor: return y; lvgl: + id: lvgl_id + rotation: 90 log_level: debug resume_on_input: true on_pause: - logger.log: LVGL is Paused + - logger.log: LVGL is Paused + - lvgl.display.set_rotation: 90 on_resume: - logger.log: LVGL has resumed + - logger.log: LVGL has resumed + - lvgl.display.set_rotation: + rotation: 0 + lvgl_id: lvgl_id + on_boot: - logger.log: LVGL has started - lvgl.indicator.update: