[lvgl] Continue activity while display busy (#17374)

This commit is contained in:
Clyde Stubbs
2026-07-07 03:16:29 +00:00
committed by GitHub
parent e8d37e5bd3
commit ad7c980c4b
6 changed files with 64 additions and 41 deletions
+3
View File
@@ -414,6 +414,8 @@ async def to_code(configs):
await cg.register_component(lv_component, config)
if rotation := config.get(CONF_ROTATION):
cg.add(lv_component.set_rotation(rotation))
if refr_time := config.get(df.CONF_REFRESH_INTERVAL):
cg.add(lv_component.set_refresh_interval(refr_time.total_milliseconds))
Widget.create(config[CONF_ID], lv_component, LvScrActType(), config)
lv_scr_act = get_screen_active(lv_component)
@@ -598,6 +600,7 @@ LVGL_TOP_LEVEL_SCHEMA = (
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_UPDATE_WHEN_DISPLAY_IDLE, default=False): cv.boolean,
cv.Optional(df.CONF_REFRESH_INTERVAL): cv.positive_time_period_milliseconds,
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,
+1
View File
@@ -764,6 +764,7 @@ CONF_PLACEHOLDER_TEXT = "placeholder_text"
CONF_POINTS = "points"
CONF_PREVIOUS = "previous"
CONF_RADIUS = "radius"
CONF_REFRESH_INTERVAL = "refresh_interval"
CONF_REPEAT_COUNT = "repeat_count"
CONF_RECOLOR = "recolor"
CONF_RESUME_ON_INPUT = "resume_on_input"
+38 -22
View File
@@ -401,7 +401,10 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) {
}
void LvglComponent::flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p) {
if (!this->is_paused()) {
// no guard here for display busy, since LVGL will not call flush_cb until the refresh timer fires,
// and while the display is busy this is reset to 5 minutes. If that expires and the display is still
// busy there are bigger problems.
if (!this->paused_) {
auto now = millis();
this->draw_buffer_(area, reinterpret_cast<lv_color_data *>(color_p));
ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", (int) area->x1, (int) area->y1,
@@ -620,20 +623,20 @@ void LvKeyboardType::set_obj(lv_obj_t *lv_obj) {
void LvglComponent::draw_end_() {
if (this->draw_end_callback_ != nullptr)
this->draw_end_callback_->trigger();
// Only reachable once the display is idle again: while busy, the display's refr_timer_ is
// paused (see loop()), so LVGL never renders/flushes and this event never fires.
if (this->update_when_display_idle_) {
for (auto *disp : this->displays_)
disp->update();
}
}
bool LvglComponent::is_paused() const {
if (this->paused_)
return true;
if (this->update_when_display_idle_) {
for (auto *disp : this->displays_) {
if (!disp->is_idle())
return true;
}
bool LvglComponent::displays_busy_() const {
if (!this->update_when_display_idle_)
return false;
for (auto *disp : this->displays_) {
if (!disp->is_idle())
return true;
}
return false;
}
@@ -777,6 +780,8 @@ void LvglComponent::setup() {
if (this->draw_end_callback_ != nullptr || this->update_when_display_idle_) {
lv_display_add_event_cb(this->disp_, render_end_cb, LV_EVENT_REFR_READY, this);
}
this->refr_timer_ = lv_display_get_refr_timer(this->disp_);
lv_timer_set_period(this->refr_timer_, this->refr_timer_period_);
#if LV_USE_LOG
lv_log_register_print_cb([](lv_log_level_t level, const char *buf) {
auto next = strchr(buf, ')');
@@ -802,21 +807,32 @@ void LvglComponent::update() {
}
void LvglComponent::loop() {
if (this->is_paused()) {
if (this->paused_ && this->show_snow_)
if (this->paused_) {
if (this->show_snow_)
this->write_random_();
} else {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
auto now = millis();
lv_timer_handler();
auto elapsed = millis() - now;
if (elapsed > 15) {
ESP_LOGV(TAG, "lv_timer_handler took %dms", (int) (millis() - now));
}
#else
lv_timer_handler();
#endif
return;
}
// Pause/resume the display's own refresh timer to track its busy state. While paused, LVGL
// still keeps track of invalidated areas but won't render or flush them, so nothing needs to
// be discarded or replayed: once resumed, the accumulated areas are simply drawn as normal.
// Input events and other timers keep being processed below regardless of this state.
if (this->update_when_display_idle_) {
bool busy = this->displays_busy_();
if (busy && !this->refr_timer_paused_) {
this->refr_timer_paused_ = true;
// calling lv_timer_pause() here would be ineffective; LVGL pauses and resumes the timer based on its own internal
// state, which is not aware of the display's busy state. Instead, we extend the timer period to avoid it firing
// while the display is busy.
lv_timer_set_period(this->refr_timer_, 5 * 60 * 1000);
} else if (!busy && this->refr_timer_paused_) {
this->refr_timer_paused_ = false;
lv_timer_set_period(this->refr_timer_, this->refr_timer_period_);
// Don't wait for the timer's next natural period: refresh right away now that the
// display is idle again.
lv_timer_ready(this->refr_timer_);
}
}
lv_timer_handler();
}
#ifdef USE_LVGL_ANIMIMG
+18 -2
View File
@@ -214,9 +214,14 @@ class LvglComponent final : public PollingComponent {
// @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);
void set_refresh_interval(uint32_t period) {
this->refr_timer_period_ = period;
if (this->refr_timer_ != nullptr)
lv_timer_set_period(this->refr_timer_, period);
}
// Returns true if the display is explicitly paused, or a blocking display update is in progress.
bool is_paused() const;
// Returns true if the display has been explicitly paused via set_paused().
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_) {
@@ -299,6 +304,9 @@ class LvglComponent final : public PollingComponent {
// Not checking for non-null callback since the
// LVGL callback that calls it is not set in that case
void draw_start_() const { this->draw_start_callback_->trigger(); }
// Returns true if update_when_display_idle is enabled and at least one underlying display
// component is currently busy (e.g. mid-refresh).
bool displays_busy_() const;
void write_random_();
void draw_buffer_(const lv_area_t *area, lv_color_data *ptr);
@@ -316,6 +324,14 @@ class LvglComponent final : public PollingComponent {
uint8_t *draw_buf_{};
lv_display_t *disp_{};
// The display's own periodic refresh timer, effectively paused while the display is busy (see
// displays_busy_()) so LVGL neither renders nor flushes to it, without losing track of
// invalidated areas. Other timers (indev reading, animations, ...) keep running as normal.
lv_timer_t *refr_timer_{};
// Tracks whether refr_timer_ is currently paused, so loop() can detect the busy -> idle edge
// and kick off an immediate refresh instead of waiting for the timer's next natural period.
bool refr_timer_paused_{};
uint32_t refr_timer_period_{16};
uint16_t width_{};
uint16_t height_{};
bool paused_{};
+2 -14
View File
@@ -24,20 +24,6 @@ binary_sensor:
name: Button A checked
widget: button_a
state: checked
- platform: lvgl
id: button_checker
name: LVGL button
widget: button_button
state: checked
on_state:
then:
- lvgl.checkbox.update:
id: checkbox_id
state:
checked: !lambda |-
auto y = x; // block inlining of one line return
return y;
- platform: lvgl
id: button_presser
name: Button pressed
@@ -49,6 +35,8 @@ lvgl:
rotation: 90
log_level: debug
resume_on_input: true
update_when_display_idle: true
refresh_interval: 30ms
on_pause:
- logger.log: LVGL is Paused
- lvgl.display.set_rotation: 90
+2 -3
View File
@@ -1,7 +1,8 @@
packages:
lvgl: !include lvgl-package.yaml
lvgl_package: !include lvgl-package.yaml
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
lvgl: !include common.yaml
sensor:
- platform: rotary_encoder
@@ -77,5 +78,3 @@ lvgl:
- component.update: tft_display
- delay: 60s
- lvgl.resume:
<<: !include common.yaml