[lvgl] Implement better software rotation (#7595)

This commit is contained in:
Clyde Stubbs 2024-10-16 14:26:06 +11:00 committed by GitHub
parent b274d6901a
commit 6a86d92781
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 201 additions and 58 deletions

View file

@ -48,6 +48,7 @@ from .types import (
FontEngine,
IdleTrigger,
ObjUpdateAction,
PauseTrigger,
lv_font_t,
lv_group_t,
lv_style_t,
@ -233,6 +234,8 @@ async def to_code(config):
frac = 8
cg.add(lv_component.set_buffer_frac(int(frac)))
cg.add(lv_component.set_full_refresh(config[df.CONF_FULL_REFRESH]))
cg.add(lv_component.set_draw_rounding(config[df.CONF_DRAW_ROUNDING]))
cg.add(lv_component.set_resume_on_input(config[df.CONF_RESUME_ON_INPUT]))
for font in helpers.esphome_fonts_used:
await cg.get_variable(font)
@ -272,11 +275,19 @@ async def to_code(config):
async with LvContext(lv_component):
await generate_triggers(lv_component)
await generate_page_triggers(lv_component, config)
await initial_focus_to_code(config)
for conf in config.get(CONF_ON_IDLE, ()):
templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32)
idle_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, templ)
await build_automation(idle_trigger, [], conf)
await initial_focus_to_code(config)
for conf in config.get(df.CONF_ON_PAUSE, ()):
pause_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, True)
await build_automation(pause_trigger, [], conf)
for conf in config.get(df.CONF_ON_RESUME, ()):
resume_trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], lv_component, False
)
await build_automation(resume_trigger, [], conf)
for comp in helpers.lvgl_components_required:
CORE.add_define(f"USE_LVGL_{comp.upper()}")
@ -314,6 +325,7 @@ CONFIG_SCHEMA = (
cv.Optional(df.CONF_COLOR_DEPTH, default=16): cv.one_of(16),
cv.Optional(df.CONF_DEFAULT_FONT, default="montserrat_14"): lvalid.lv_font,
cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean,
cv.Optional(df.CONF_DRAW_ROUNDING, default=2): cv.positive_int,
cv.Optional(CONF_BUFFER_SIZE, default="100%"): cv.percentage,
cv.Optional(df.CONF_LOG_LEVEL, default="WARN"): cv.one_of(
*df.LOG_LEVELS, upper=True
@ -341,6 +353,16 @@ CONFIG_SCHEMA = (
),
}
),
cv.Optional(df.CONF_ON_PAUSE): validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger),
}
),
cv.Optional(df.CONF_ON_RESUME): validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger),
}
),
cv.Exclusive(df.CONF_WIDGETS, CONF_PAGES): cv.ensure_list(WIDGET_SCHEMA),
cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list(
container_schema(page_spec)
@ -356,6 +378,7 @@ CONFIG_SCHEMA = (
cv.Optional(df.CONF_TOUCHSCREENS, default=None): touchscreen_schema,
cv.Optional(df.CONF_ENCODERS, default=None): ENCODERS_CONFIG,
cv.GenerateID(df.CONF_DEFAULT_GROUP): cv.declare_id(lv_group_t),
cv.Optional(df.CONF_RESUME_ON_INPUT, default=True): cv.boolean,
}
)
.extend(DISP_BG_SCHEMA)

View file

@ -408,6 +408,7 @@ CONF_DEFAULT_FONT = "default_font"
CONF_DEFAULT_GROUP = "default_group"
CONF_DIR = "dir"
CONF_DISPLAYS = "displays"
CONF_DRAW_ROUNDING = "draw_rounding"
CONF_EDITING = "editing"
CONF_ENCODERS = "encoders"
CONF_END_ANGLE = "end_angle"
@ -451,6 +452,8 @@ CONF_OFFSET_X = "offset_x"
CONF_OFFSET_Y = "offset_y"
CONF_ONE_CHECKED = "one_checked"
CONF_ONE_LINE = "one_line"
CONF_ON_PAUSE = "on_pause"
CONF_ON_RESUME = "on_resume"
CONF_ON_SELECT = "on_select"
CONF_OPA = "opa"
CONF_NEXT = "next"
@ -466,6 +469,7 @@ CONF_POINTS = "points"
CONF_PREVIOUS = "previous"
CONF_REPEAT_COUNT = "repeat_count"
CONF_RECOLOR = "recolor"
CONF_RESUME_ON_INPUT = "resume_on_input"
CONF_RIGHT_BUTTON = "right_button"
CONF_ROLLOVER = "rollover"
CONF_ROOT_BACK_BTN = "root_back_btn"

View file

@ -69,30 +69,38 @@ std::string lv_event_code_name_for(uint8_t event_code) {
}
return str_sprintf("%2d", event_code);
}
static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) {
// make sure all coordinates are even
if (area->x1 & 1)
area->x1--;
if (!(area->x2 & 1))
area->x2++;
if (area->y1 & 1)
area->y1--;
if (!(area->y2 & 1))
area->y2++;
// cater for display driver chips with special requirements for bounds of partial
// draw areas. Extend the draw area to satisfy:
// * Coordinates must be a multiple of draw_rounding
auto *comp = static_cast<LvglComponent *>(disp_drv->user_data);
auto draw_rounding = comp->draw_rounding;
// round down the start coordinates
area->x1 = area->x1 / draw_rounding * draw_rounding;
area->y1 = area->y1 / draw_rounding * draw_rounding;
// round up the end coordinates
area->x2 = (area->x2 + draw_rounding) / draw_rounding * draw_rounding - 1;
area->y2 = (area->y2 + draw_rounding) / draw_rounding * draw_rounding - 1;
}
lv_event_code_t lv_api_event; // NOLINT
lv_event_code_t lv_update_event; // NOLINT
void LvglComponent::dump_config() { ESP_LOGCONFIG(TAG, "LVGL:"); }
void LvglComponent::dump_config() {
ESP_LOGCONFIG(TAG, "LVGL:");
ESP_LOGCONFIG(TAG, " Rotation: %d", this->rotation);
ESP_LOGCONFIG(TAG, " Draw rounding: %d", (int) this->draw_rounding);
}
void LvglComponent::set_paused(bool paused, bool show_snow) {
this->paused_ = paused;
this->show_snow_ = show_snow;
this->snow_line_ = 0;
if (!paused && lv_scr_act() != nullptr) {
lv_disp_trig_activity(this->disp_); // resets the inactivity time
lv_obj_invalidate(lv_scr_act());
}
this->pause_callbacks_.call(paused);
}
void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) {
lv_obj_add_event_cb(obj, callback, event, this);
}
@ -133,19 +141,64 @@ void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) {
} while (this->pages_[this->current_page_]->skip); // skip empty pages()
this->show_page(this->current_page_, anim, time);
}
void LvglComponent::draw_buffer_(const lv_area_t *area, const uint8_t *ptr) {
void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
auto width = lv_area_get_width(area);
auto height = lv_area_get_height(area);
auto x1 = area->x1;
auto y1 = area->y1;
lv_color_t *dst = this->rotate_buf_;
switch (this->rotation) {
case display::DISPLAY_ROTATION_90_DEGREES:
for (lv_coord_t x = height - 1; x-- != 0;) {
for (lv_coord_t y = 0; y != width; y++) {
dst[y * height + x] = *ptr++;
}
}
y1 = x1;
x1 = this->disp_drv_.ver_res - area->y1 - height;
width = height;
height = lv_area_get_width(area);
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++;
}
}
x1 = this->disp_drv_.hor_res - x1 - width;
y1 = this->disp_drv_.ver_res - 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 + x] = *ptr++;
}
}
x1 = y1;
y1 = this->disp_drv_.hor_res - area->x1 - width;
width = height;
height = lv_area_get_width(area);
break;
default:
dst = ptr;
break;
}
for (auto *display : this->displays_) {
display->draw_pixels_at(area->x1, area->y1, lv_area_get_width(area), lv_area_get_height(area), ptr,
display::COLOR_ORDER_RGB, LV_BITNESS, LV_COLOR_16_SWAP);
ESP_LOGV(TAG, "draw buffer x1=%d, y1=%d, width=%d, height=%d", x1, y1, width, height);
display->draw_pixels_at(x1, y1, width, height, (const uint8_t *) dst, display::COLOR_ORDER_RGB, LV_BITNESS,
LV_COLOR_16_SWAP);
}
}
void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
if (!this->paused_) {
auto now = millis();
this->draw_buffer_(area, (const uint8_t *) color_p);
ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area),
lv_area_get_height(area), (int) (millis() - now));
this->draw_buffer_(area, color_p);
ESP_LOGVV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area),
lv_area_get_height(area), (int) (millis() - now));
}
lv_disp_flush_ready(disp_drv);
}
@ -160,6 +213,13 @@ IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeo
});
}
PauseTrigger::PauseTrigger(LvglComponent *parent, TemplatableValue<bool> paused) : paused_(std::move(paused)) {
parent->add_on_pause_callback([this](bool pausing) {
if (this->paused_.value() == pausing)
this->trigger();
});
}
#ifdef USE_LVGL_TOUCHSCREEN
LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time) {
lv_indev_drv_init(&this->drv_);
@ -261,23 +321,31 @@ void LvKeyboardType::set_obj(lv_obj_t *lv_obj) {
#endif // USE_LVGL_KEYBOARD
void LvglComponent::write_random_() {
// length of 2 lines in 32 bit units
// we write 2 lines for the benefit of displays that won't write one line at a time.
size_t line_len = this->disp_drv_.hor_res * LV_COLOR_DEPTH / 8 / 4 * 2;
for (size_t i = 0; i != line_len; i++) {
((uint32_t *) (this->draw_buf_.buf1))[i] = random_uint32();
int iterations = 6 - lv_disp_get_inactive_time(this->disp_) / 60000;
if (iterations <= 0)
iterations = 1;
while (iterations-- != 0) {
auto col = random_uint32() % this->disp_drv_.hor_res;
col = col / this->draw_rounding * this->draw_rounding;
auto row = random_uint32() % this->disp_drv_.ver_res;
row = row / this->draw_rounding * this->draw_rounding;
auto size = (random_uint32() % 32) / this->draw_rounding * this->draw_rounding - 1;
lv_area_t area;
area.x1 = col;
area.y1 = row;
area.x2 = col + size;
area.y2 = row + size;
if (area.x2 >= this->disp_drv_.hor_res)
area.x2 = this->disp_drv_.hor_res - 1;
if (area.y2 >= this->disp_drv_.ver_res)
area.y2 = this->disp_drv_.ver_res - 1;
size_t line_len = lv_area_get_width(&area) * lv_area_get_height(&area) / 2;
for (size_t i = 0; i != line_len; i++) {
((uint32_t *) (this->draw_buf_.buf1))[i] = random_uint32();
}
this->draw_buffer_(&area, (lv_color_t *) this->draw_buf_.buf1);
}
lv_area_t area;
area.x1 = 0;
area.x2 = this->disp_drv_.hor_res - 1;
if (this->snow_line_ == this->disp_drv_.ver_res / 2) {
area.y1 = static_cast<lv_coord_t>(random_uint32() % (this->disp_drv_.ver_res / 2) * 2);
} else {
area.y1 = this->snow_line_++ * 2;
}
// write 2 lines
area.y2 = area.y1 + 1;
this->draw_buffer_(&area, (const uint8_t *) this->draw_buf_.buf1);
}
void LvglComponent::setup() {
@ -291,7 +359,7 @@ void LvglComponent::setup() {
auto *display = this->displays_[0];
size_t buffer_pixels = display->get_width() * display->get_height() / this->buffer_frac_;
auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8;
auto *buf = lv_custom_mem_alloc(buf_bytes);
auto *buf = lv_custom_mem_alloc(buf_bytes); // NOLINT
if (buf == nullptr) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
ESP_LOGE(TAG, "Malloc failed to allocate %zu bytes", buf_bytes);
@ -307,26 +375,30 @@ void LvglComponent::setup() {
this->disp_drv_.full_refresh = this->full_refresh_;
this->disp_drv_.flush_cb = static_flush_cb;
this->disp_drv_.rounder_cb = rounder_cb;
switch (display->get_rotation()) {
case display::DISPLAY_ROTATION_0_DEGREES:
break;
case display::DISPLAY_ROTATION_90_DEGREES:
this->disp_drv_.sw_rotate = true;
this->disp_drv_.rotated = LV_DISP_ROT_90;
break;
case display::DISPLAY_ROTATION_180_DEGREES:
this->disp_drv_.sw_rotate = true;
this->disp_drv_.rotated = LV_DISP_ROT_180;
break;
case display::DISPLAY_ROTATION_270_DEGREES:
this->disp_drv_.sw_rotate = true;
this->disp_drv_.rotated = LV_DISP_ROT_270;
break;
this->rotation = display->get_rotation();
if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) {
this->rotate_buf_ = static_cast<lv_color_t *>(lv_custom_mem_alloc(buf_bytes)); // NOLINT
if (this->rotate_buf_ == nullptr) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
ESP_LOGE(TAG, "Malloc failed to allocate %zu bytes", buf_bytes);
#endif
this->mark_failed();
this->status_set_error("Memory allocation failure");
return;
}
}
display->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);
this->disp_drv_.hor_res = (lv_coord_t) display->get_width();
this->disp_drv_.ver_res = (lv_coord_t) display->get_height();
ESP_LOGV(TAG, "sw_rotate = %d, rotated=%d", this->disp_drv_.sw_rotate, this->disp_drv_.rotated);
switch (this->rotation) {
default:
this->disp_drv_.hor_res = (lv_coord_t) display->get_width();
this->disp_drv_.ver_res = (lv_coord_t) display->get_height();
break;
case display::DISPLAY_ROTATION_90_DEGREES:
case display::DISPLAY_ROTATION_270_DEGREES:
this->disp_drv_.ver_res = (lv_coord_t) display->get_width();
this->disp_drv_.hor_res = (lv_coord_t) display->get_height();
break;
}
this->disp_ = lv_disp_drv_register(&this->disp_drv_);
for (const auto &v : this->init_lambdas_)
v(this);

View file

@ -119,6 +119,7 @@ class LvglComponent : public PollingComponent {
void add_on_idle_callback(std::function<void(uint32_t)> &&callback) {
this->idle_callbacks_.add(std::move(callback));
}
void add_on_pause_callback(std::function<void(bool)> &&callback) { this->pause_callbacks_.add(std::move(callback)); }
void add_display(display::Display *display) { this->displays_.push_back(display); }
void add_init_lambda(const std::function<void(LvglComponent *)> &lamb) { this->init_lambdas_.push_back(lamb); }
void dump_config() override;
@ -126,12 +127,22 @@ class LvglComponent : public PollingComponent {
bool is_idle(uint32_t idle_ms) { return lv_disp_get_inactive_time(this->disp_) > idle_ms; }
void set_buffer_frac(size_t frac) { this->buffer_frac_ = frac; }
lv_disp_t *get_disp() { return this->disp_; }
// Pause or resume the display.
// @param paused If true, pause the display. If false, resume the display.
// @param show_snow If true, show the snow effect when paused.
void set_paused(bool paused, bool show_snow);
bool is_paused() const { return this->paused_; }
// If the display is paused and we have resume_on_input_ set to true, resume the display.
void maybe_wakeup() {
if (this->paused_ && this->resume_on_input_) {
this->set_paused(false, false);
}
}
void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event);
void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2);
void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2,
lv_event_code_t event3);
bool is_paused() const { return this->paused_; }
void add_page(LvPageType *page);
void show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time);
void show_next_page(lv_scr_load_anim_t anim, uint32_t time);
@ -144,10 +155,17 @@ class LvglComponent : public PollingComponent {
lv_group_focus_obj(mark);
}
}
// rounding factor to align bounds of update area when drawing
size_t draw_rounding{2};
void set_draw_rounding(size_t rounding) { this->draw_rounding = rounding; }
void set_resume_on_input(bool resume_on_input) { this->resume_on_input_ = resume_on_input; }
// if set to true, the bounds of the update area will always start at 0,0
display::DisplayRotation rotation{display::DISPLAY_ROTATION_0_DEGREES};
protected:
void write_random_();
void draw_buffer_(const lv_area_t *area, const uint8_t *ptr);
void draw_buffer_(const lv_area_t *area, lv_color_t *ptr);
void flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p);
std::vector<display::Display *> displays_{};
lv_disp_draw_buf_t draw_buf_{};
@ -157,14 +175,16 @@ class LvglComponent : public PollingComponent {
std::vector<LvPageType *> pages_{};
size_t current_page_{0};
bool show_snow_{};
lv_coord_t snow_line_{};
bool page_wrap_{true};
bool resume_on_input_{};
std::map<lv_group_t *, lv_obj_t *> focus_marks_{};
std::vector<std::function<void(LvglComponent *lv_component)>> init_lambdas_;
CallbackManager<void(uint32_t)> idle_callbacks_{};
CallbackManager<void(bool)> pause_callbacks_{};
size_t buffer_frac_{1};
bool full_refresh_{};
lv_color_t *rotate_buf_{};
};
class IdleTrigger : public Trigger<> {
@ -176,6 +196,14 @@ class IdleTrigger : public Trigger<> {
bool is_idle_{};
};
class PauseTrigger : public Trigger<> {
public:
explicit PauseTrigger(LvglComponent *parent, TemplatableValue<bool> paused);
protected:
TemplatableValue<bool> paused_;
};
template<typename... Ts> class LvglAction : public Action<Ts...>, public Parented<LvglComponent> {
public:
explicit LvglAction(std::function<void(LvglComponent *)> &&lamb) : action_(std::move(lamb)) {}
@ -200,7 +228,10 @@ class LVTouchListener : public touchscreen::TouchListener, public Parented<LvglC
public:
LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time);
void update(const touchscreen::TouchPoints_t &tpoints) override;
void release() override { touch_pressed_ = false; }
void release() override {
touch_pressed_ = false;
this->parent_->maybe_wakeup();
}
lv_indev_drv_t *get_drv() { return &this->drv_; }
protected:
@ -236,12 +267,18 @@ class LVEncoderListener : public Parented<LvglComponent> {
if (!this->parent_->is_paused()) {
this->pressed_ = pressed;
this->key_ = key;
} else if (!pressed) {
// maybe wakeup on release if paused
this->parent_->maybe_wakeup();
}
}
void set_count(int32_t count) {
if (!this->parent_->is_paused())
if (!this->parent_->is_paused()) {
this->count_ = count;
} else {
this->parent_->maybe_wakeup();
}
}
lv_indev_drv_t *get_drv() { return &this->drv_; }

View file

@ -40,6 +40,7 @@ lv_event_code_t = cg.global_ns.enum("lv_event_code_t")
lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t")
FontEngine = lvgl_ns.class_("FontEngine")
IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template())
PauseTrigger = lvgl_ns.class_("PauseTrigger", automation.Trigger.template())
ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action)
LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition)
LvglAction = lvgl_ns.class_("LvglAction", automation.Action)

View file

@ -12,6 +12,11 @@ substitutions:
arrow_down: "\U000F004B"
lvgl:
resume_on_input: true
on_pause:
logger.log: LVGL is Paused
on_resume:
logger.log: LVGL has resumed
log_level: TRACE
bg_color: light_blue
disp_bg_color: color_id

View file

@ -44,6 +44,7 @@ binary_sensor:
number: GPIO39
inverted: true
lvgl:
draw_rounding: 8
encoders:
group: switches
initial_focus: button_button