mirror of
https://github.com/esphome/esphome.git
synced 2026-05-23 11:16:52 +08:00
[lvgl] Implement rotation directly (#14955)
This commit is contained in:
@@ -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
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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<LvglComponent *>(lv_event_get_user_data(event));
|
||||
auto *area = static_cast<lv_area_t *>(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<lv_color_data *>(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<lv_color_data *>(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<display::Display *> 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<uint8_t *>(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_color_t *>(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_);
|
||||
}
|
||||
|
||||
@@ -156,13 +156,18 @@ template<typename... Ts> class ObjUpdateAction : public Action<Ts...> {
|
||||
#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<display::Display *> 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<> {
|
||||
|
||||
@@ -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="")
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user