mirror of
https://github.com/esphome/esphome.git
synced 2026-08-17 10:52:56 +08:00
[lvgl] Continue activity while display busy (#17374)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_{};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user