ESPHome 2026.5.0b1
Loading...
Searching...
No Matches
lvgl_esphome.cpp
Go to the documentation of this file.
2#include "esphome/core/hal.h"
4#include "esphome/core/log.h"
5#include "lvgl_esphome.h"
6
7#include "core/lv_global.h"
8#include "core/lv_obj_class_private.h"
9
10#include <numeric>
11
12static void *lv_alloc_draw_buf(size_t size, bool internal);
13static void *draw_buf_alloc_cb(size_t size, lv_color_format_t color_format) { return lv_alloc_draw_buf(size, false); };
14
15namespace esphome::lvgl {
16static const char *const TAG = "lvgl";
17
18static const size_t MIN_BUFFER_FRAC = 8; // buffer must be at least 1/8 of the display size
19static const size_t MIN_BUFFER_SIZE = 2048; // Sensible minimum buffer size
20
21static const char *const EVENT_NAMES[] = {
22 "NONE",
23 "PRESSED",
24 "PRESSING",
25 "PRESS_LOST",
26 "SHORT_CLICKED",
27 "LONG_PRESSED",
28 "LONG_PRESSED_REPEAT",
29 "CLICKED",
30 "RELEASED",
31 "SCROLL_BEGIN",
32 "SCROLL_END",
33 "SCROLL",
34 "GESTURE",
35 "KEY",
36 "FOCUSED",
37 "DEFOCUSED",
38 "LEAVE",
39 "HIT_TEST",
40 "COVER_CHECK",
41 "REFR_EXT_DRAW_SIZE",
42 "DRAW_MAIN_BEGIN",
43 "DRAW_MAIN",
44 "DRAW_MAIN_END",
45 "DRAW_POST_BEGIN",
46 "DRAW_POST",
47 "DRAW_POST_END",
48 "DRAW_PART_BEGIN",
49 "DRAW_PART_END",
50 "VALUE_CHANGED",
51 "INSERT",
52 "REFRESH",
53 "READY",
54 "CANCEL",
55 "DELETE",
56 "CHILD_CHANGED",
57 "CHILD_CREATED",
58 "CHILD_DELETED",
59 "SCREEN_UNLOAD_START",
60 "SCREEN_LOAD_START",
61 "SCREEN_LOADED",
62 "SCREEN_UNLOADED",
63 "SIZE_CHANGED",
64 "STYLE_CHANGED",
65 "LAYOUT_CHANGED",
66 "GET_SELF_SIZE",
67};
68
69static const unsigned LOG_LEVEL_MAP[] = {
70 ESPHOME_LOG_LEVEL_DEBUG, ESPHOME_LOG_LEVEL_INFO, ESPHOME_LOG_LEVEL_WARN,
71 ESPHOME_LOG_LEVEL_ERROR, ESPHOME_LOG_LEVEL_ERROR, ESPHOME_LOG_LEVEL_NONE,
72
73};
74
75std::string lv_event_code_name_for(lv_event_t *event) {
76 auto event_code = lv_event_get_code(event);
77 if (event_code < sizeof(EVENT_NAMES) / sizeof(EVENT_NAMES[0])) {
78 return EVENT_NAMES[event_code];
79 }
80 // max 4 bytes: "%u" with uint8_t (max 255, 3 digits) + null
81 char buf[4];
82 snprintf(buf, sizeof(buf), "%u", event_code);
83 return buf;
84}
85
88 ESP_LOGW(TAG, "Display rotation cannot be changed unless rotation was enabled during setup.");
89 return;
90 }
91 this->rotation_ = rotation;
92 if (this->is_ready()) {
93 this->set_resolution_();
94 lv_obj_update_layout(this->get_screen_active());
95 lv_obj_invalidate(this->get_screen_active());
96 }
97}
98
99void LvglComponent::rotate_coordinates(int32_t &x, int32_t &y) const {
100 switch (this->rotation_) {
101 default:
102 break;
103
105 x = this->width_ - x - 1;
106 y = this->height_ - y - 1;
107 break;
108 }
110 auto tmp = x;
111 x = this->height_ - y - 1;
112 y = tmp;
113 break;
114 }
116 auto tmp = y;
117 y = this->width_ - x - 1;
118 x = tmp;
119 break;
120 }
121 }
122}
123
124static void rounder_cb(lv_event_t *event) {
125 auto *comp = static_cast<LvglComponent *>(lv_event_get_user_data(event));
126 auto *area = static_cast<lv_area_t *>(lv_event_get_param(event));
127 // cater for display driver chips with special requirements for bounds of partial
128 // draw areas. Extend the draw area to satisfy:
129 // * Coordinates must be a multiple of draw_rounding
130 auto draw_rounding = comp->draw_rounding;
131 // round down the start coordinates
132 area->x1 = area->x1 / draw_rounding * draw_rounding;
133 area->y1 = area->y1 / draw_rounding * draw_rounding;
134 // round up the end coordinates
135 area->x2 = (area->x2 + draw_rounding) / draw_rounding * draw_rounding - 1;
136 area->y2 = (area->y2 + draw_rounding) / draw_rounding * draw_rounding - 1;
137}
138
139void LvglComponent::render_end_cb(lv_event_t *event) {
140 auto *comp = static_cast<LvglComponent *>(lv_event_get_user_data(event));
141 comp->draw_end_();
142}
143
144void LvglComponent::render_start_cb(lv_event_t *event) {
145 ESP_LOGVV(TAG, "Draw start");
146 auto *comp = static_cast<LvglComponent *>(lv_event_get_user_data(event));
147 comp->draw_start_();
148}
149
150lv_event_code_t lv_update_event; // NOLINT
152 ESP_LOGCONFIG(TAG,
153 "LVGL:\n"
154 " Display width/height: %d x %d\n"
155 " Buffer size: %zu%%\n"
156 " Rotation: %d\n"
157 " Draw rounding: %d",
158 this->width_, this->height_, 100 / this->buffer_frac_, this->rotation_, (int) this->draw_rounding);
159 if (this->rotation_type_ != ROTATION_UNUSED) {
160 const char *rot_type = "hardware via display driver";
162#ifdef USE_ESP32_VARIANT_ESP32P4
163 rot_type = this->ppa_client_ != nullptr ? "software (PPA accelerated)" : "software";
164#else
165 rot_type = "software";
166#endif
167 }
168 ESP_LOGCONFIG(TAG, " Rotation type: %s", rot_type);
169 }
170}
171
172void LvglComponent::set_paused(bool paused, bool show_snow) {
173 this->paused_ = paused;
174 this->show_snow_ = show_snow;
175 if (!paused && lv_screen_active() != nullptr) {
176 lv_display_trigger_activity(this->disp_); // resets the inactivity time
177 lv_obj_invalidate(lv_screen_active());
178 }
179 if (paused && this->pause_callback_ != nullptr)
180 this->pause_callback_->trigger();
181 if (!paused && this->resume_callback_ != nullptr)
182 this->resume_callback_->trigger();
183}
184
186 lv_init();
187 // override draw buf alloc to ensure proper alignment for PPA
188 LV_GLOBAL_DEFAULT()->draw_buf_handlers.buf_malloc_cb = draw_buf_alloc_cb;
189 LV_GLOBAL_DEFAULT()->draw_buf_handlers.buf_free_cb = lv_free_core;
190 LV_GLOBAL_DEFAULT()->image_cache_draw_buf_handlers.buf_malloc_cb = draw_buf_alloc_cb;
191 LV_GLOBAL_DEFAULT()->image_cache_draw_buf_handlers.buf_free_cb = lv_free_core;
192 LV_GLOBAL_DEFAULT()->font_draw_buf_handlers.buf_malloc_cb = draw_buf_alloc_cb;
193 LV_GLOBAL_DEFAULT()->font_draw_buf_handlers.buf_free_cb = lv_free_core;
194 lv_tick_set_cb([] { return millis(); });
195 lv_update_event = static_cast<lv_event_code_t>(lv_event_register_id());
196}
197
198void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) {
199 lv_obj_add_event_cb(obj, callback, event, nullptr);
200}
201
202void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1,
203 lv_event_code_t event2) {
204 add_event_cb(obj, callback, event1);
205 add_event_cb(obj, callback, event2);
206}
207
208void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1,
209 lv_event_code_t event2, lv_event_code_t event3) {
210 add_event_cb(obj, callback, event1);
211 add_event_cb(obj, callback, event2);
212 add_event_cb(obj, callback, event3);
213}
214
216 this->pages_.push_back(page);
217 page->set_parent(this);
218 lv_display_set_default(this->disp_);
219 page->setup(this->pages_.size() - 1);
220}
221
222void LvglComponent::show_page(size_t index, lv_screen_load_anim_t anim, uint32_t time) {
223 if (index >= this->pages_.size())
224 return;
225 this->current_page_ = index;
226 if (anim == LV_SCREEN_LOAD_ANIM_NONE) {
227 lv_screen_load(this->pages_[this->current_page_]->obj);
228 } else {
229 lv_screen_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false);
230 }
231}
232
233void LvglComponent::show_next_page(lv_screen_load_anim_t anim, uint32_t time) {
234 if (this->pages_.empty() || (this->current_page_ == this->pages_.size() - 1 && !this->page_wrap_))
235 return;
236 size_t start = this->current_page_;
237 do {
238 this->current_page_ = (this->current_page_ + 1) % this->pages_.size();
239 if (this->current_page_ == start)
240 return; // all pages have skip=true (guaranteed not to happen by YAML validation)
241 } while (this->pages_[this->current_page_]->skip); // skip empty pages()
242 this->show_page(this->current_page_, anim, time);
243}
244
245void LvglComponent::show_prev_page(lv_screen_load_anim_t anim, uint32_t time) {
246 if (this->pages_.empty() || (this->current_page_ == 0 && !this->page_wrap_))
247 return;
248 size_t start = this->current_page_;
249 do {
250 this->current_page_ = (this->current_page_ + this->pages_.size() - 1) % this->pages_.size();
251 if (this->current_page_ == start)
252 return; // all pages have skip=true (guaranteed not to happen by YAML validation)
253 } while (this->pages_[this->current_page_]->skip); // skip empty pages()
254 this->show_page(this->current_page_, anim, time);
255}
256
257size_t LvglComponent::get_current_page() const { return this->current_page_; }
258bool LvPageType::is_showing() const { return this->parent_->get_current_page() == this->index; }
259
260#ifdef USE_ESP32_VARIANT_ESP32P4
261bool LvglComponent::ppa_rotate_(const lv_color_data *src, lv_color_data *dst, uint16_t width, uint16_t height,
262 uint32_t height_rounded) {
263 ppa_srm_rotation_angle_t angle;
264 uint16_t out_w, out_h;
265
266 // Map ESPHome clockwise display rotation to PPA counter-clockwise angles
267 switch (this->rotation_) {
269 angle = PPA_SRM_ROTATION_ANGLE_270; // 270° CCW = 90° CW
270 out_w = height_rounded;
271 out_h = width;
272 break;
274 angle = PPA_SRM_ROTATION_ANGLE_180;
275 out_w = width;
276 out_h = height;
277 break;
279 angle = PPA_SRM_ROTATION_ANGLE_90; // 90° CCW = 270° CW
280 out_w = height_rounded;
281 out_h = width;
282 break;
283 default:
284 return false; // No rotation needed
285 }
286
287 // Align buffer size to cache line (LV_DRAW_BUF_ALIGN) as required by PPA DMA
288 // the underlying buffer will be large enough as the size is also padded when allocating.
289 size_t out_buf_size = out_w * out_h * sizeof(lv_color_data);
290 out_buf_size = LV_ROUND_UP(out_buf_size, LV_DRAW_BUF_ALIGN);
291
292 ppa_srm_oper_config_t srm_config{};
293 srm_config.in.buffer = src;
294 srm_config.in.pic_w = width;
295 srm_config.in.pic_h = height;
296 srm_config.in.block_w = width;
297 srm_config.in.block_h = height;
298#if LV_COLOR_DEPTH == 16
299 srm_config.in.srm_cm = PPA_SRM_COLOR_MODE_RGB565;
300#elif LV_COLOR_DEPTH == 32
301 srm_config.in.srm_cm = PPA_SRM_COLOR_MODE_ARGB8888;
302#endif
303 srm_config.out.buffer = dst;
304 srm_config.out.buffer_size = out_buf_size;
305 srm_config.out.pic_w = out_w;
306 srm_config.out.pic_h = out_h;
307#if LV_COLOR_DEPTH == 16
308 srm_config.out.srm_cm = PPA_SRM_COLOR_MODE_RGB565;
309#elif LV_COLOR_DEPTH == 32
310 srm_config.out.srm_cm = PPA_SRM_COLOR_MODE_ARGB8888;
311#endif
312 srm_config.rotation_angle = angle;
313 srm_config.scale_x = 1.0f;
314 srm_config.scale_y = 1.0f;
315 srm_config.mode = PPA_TRANS_MODE_BLOCKING;
316
317 esp_err_t ret = ppa_do_scale_rotate_mirror(this->ppa_client_, &srm_config);
318 if (ret != ESP_OK) {
319 ESP_LOGW(TAG, "PPA rotation failed: %s", esp_err_to_name(ret));
320 ESP_LOGW(TAG, "PPA SRM: in=%ux%u src=%p, out=%ux%u dst=%p size=%zu, angle=%d", width, height, src, out_w, out_h,
321 dst, out_buf_size, (int) angle);
322 return false;
323 }
324 return true;
325}
326#endif // USE_ESP32_VARIANT_ESP32P4
327
328void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) {
329 auto width = lv_area_get_width(area);
330 auto height = lv_area_get_height(area);
331 auto height_rounded = (height + this->draw_rounding - 1) / this->draw_rounding * this->draw_rounding;
332 auto x1 = area->x1;
333 auto y1 = area->y1;
334 if (this->rotation_type_ == ROTATION_SOFTWARE) {
335 lv_color_data *dst = reinterpret_cast<lv_color_data *>(this->rotate_buf_);
336#ifdef USE_ESP32_VARIANT_ESP32P4
337 bool ppa_done = this->ppa_client_ != nullptr && this->ppa_rotate_(ptr, dst, width, height, height_rounded);
338 if (!ppa_done)
339#endif
340 {
341 switch (this->rotation_) {
343 for (lv_coord_t x = height; x-- != 0;) {
344 for (lv_coord_t y = 0; y != width; y++) {
345 dst[y * height_rounded + x] = *ptr++;
346 }
347 }
348 break;
349
351 for (lv_coord_t y = height; y-- != 0;) {
352 for (lv_coord_t x = width; x-- != 0;) {
353 dst[y * width + x] = *ptr++;
354 }
355 }
356 break;
357
359 for (lv_coord_t x = 0; x != height; x++) {
360 for (lv_coord_t y = width; y-- != 0;) {
361 dst[y * height_rounded + x] = *ptr++;
362 }
363 }
364 break;
365
366 default:
367 dst = ptr;
368 break;
369 }
370 }
371 // Coordinate adjustments apply regardless of PPA or SW rotation
372 switch (this->rotation_) {
374 y1 = x1;
375 x1 = this->width_ - area->y1 - height;
376 height = width;
377 width = height_rounded;
378 break;
379
381 x1 = this->width_ - x1 - width;
382 y1 = this->height_ - y1 - height;
383 break;
384
386 x1 = y1;
387 y1 = this->height_ - area->x1 - width;
388 height = width;
389 width = height_rounded;
390 break;
391
392 default:
393 break;
394 }
395 ptr = dst;
396 }
397 for (auto *display : this->displays_) {
398 display->draw_pixels_at(x1, y1, width, height, (const uint8_t *) ptr, display::COLOR_ORDER_RGB, LV_BITNESS,
399 this->big_endian_);
400 }
401}
402
403void LvglComponent::flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p) {
404 if (!this->is_paused()) {
405 auto now = millis();
406 this->draw_buffer_(area, reinterpret_cast<lv_color_data *>(color_p));
407 ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", (int) area->x1, (int) area->y1,
408 (int) lv_area_get_width(area), (int) lv_area_get_height(area), (int) (millis() - now));
409 }
410 lv_display_flush_ready(disp_drv);
411}
412
414 parent->add_on_idle_callback([this](uint32_t idle_time) {
415 if (!this->is_idle_ && idle_time > this->timeout_.value()) {
416 this->is_idle_ = true;
417 this->trigger();
418 } else if (this->is_idle_ && idle_time < this->timeout_.value()) {
419 this->is_idle_ = false;
420 }
421 });
422}
423
424#ifdef USE_LVGL_TOUCHSCREEN
425LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time, LvglComponent *parent) {
426 this->set_parent(parent);
427 this->drv_ = lv_indev_create();
428 lv_indev_set_type(this->drv_, LV_INDEV_TYPE_POINTER);
429 lv_indev_set_disp(this->drv_, parent->get_disp());
430 lv_indev_set_long_press_time(this->drv_, long_press_time);
431 // long press repeat time TBD
432 lv_indev_set_user_data(this->drv_, this);
433 lv_indev_set_read_cb(this->drv_, [](lv_indev_t *d, lv_indev_data_t *data) {
434 auto *l = static_cast<LVTouchListener *>(lv_indev_get_user_data(d));
435 if (l->touch_pressed_) {
436 data->point.x = l->touch_point_.x;
437 data->point.y = l->touch_point_.y;
438 l->parent_->rotate_coordinates(data->point.x, data->point.y);
439 data->state = LV_INDEV_STATE_PRESSED;
440 } else {
441 data->state = LV_INDEV_STATE_RELEASED;
442 }
443 });
444}
445
447 this->touch_pressed_ = !this->parent_->is_paused() && !tpoints.empty();
448 if (this->touch_pressed_)
449 this->touch_point_ = tpoints[0];
450}
451#endif // USE_LVGL_TOUCHSCREEN
452
453#ifdef USE_LVGL_METER
454
455int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int32_t value) {
456 auto *scale = lv_obj_get_parent(obj);
457 auto min_value = lv_scale_get_range_min_value(scale);
458 auto max_value = lv_scale_get_range_max_value(scale);
459 value = clamp(value, min_value, max_value);
460 return ((value - min_value) * lv_scale_get_angle_range(scale) / (max_value - min_value) +
461 lv_scale_get_rotation((scale))) %
462 360;
463}
464
465void IndicatorLine::set_obj(lv_obj_t *lv_obj) {
466 LvCompound::set_obj(lv_obj);
467 lv_line_set_points(lv_obj, this->points_, 2);
468 lv_obj_add_event_cb(
469 lv_obj_get_parent(obj),
470 [](lv_event_t *e) {
471 auto *indicator = static_cast<IndicatorLine *>(lv_event_get_user_data(e));
472 indicator->update_length_();
473 ESP_LOGV(TAG, "Updated length, value = %d", indicator->angle_);
474 },
475 LV_EVENT_SIZE_CHANGED, this);
476}
477
479 auto angle = lv_get_needle_angle_for_value(this->obj, value);
480 if (angle != this->angle_) {
481 this->angle_ = angle;
482 this->update_length_();
483 }
484}
485
486void IndicatorLine::update_length_() {
487 auto cx = lv_obj_get_width(lv_obj_get_parent(this->obj)) / 2;
488 auto cy = lv_obj_get_height(lv_obj_get_parent(this->obj)) / 2;
489 auto radius = clamp_at_most(cx, cy);
490 auto length = lv_obj_get_style_length(this->obj, LV_PART_MAIN);
491 auto radial_offset = lv_obj_get_style_radial_offset(this->obj, LV_PART_MAIN);
492 if (LV_COORD_IS_PCT(radial_offset)) {
493 radial_offset = radius * LV_COORD_GET_PCT(radial_offset) / 100;
494 }
495 if (LV_COORD_IS_PCT(length)) {
496 length = radius * LV_COORD_GET_PCT(length) / 100;
497 } else if (length < 0) {
498 length += radius;
499 }
500 auto x = lv_trigo_cos(this->angle_) / 32768.0f;
501 auto y = lv_trigo_sin(this->angle_) / 32768.0f;
502 // radius here also represents the offset of the scale center from top left
503 this->points_[0].x = radius + radial_offset * x;
504 this->points_[0].y = radius + radial_offset * y;
505 this->points_[1].x = radius + x * (radial_offset + length);
506 this->points_[1].y = radius + y * (radial_offset + length);
507 lv_obj_refresh_self_size(this->obj);
508 lv_obj_invalidate(this->obj);
509}
510#endif
511
512#ifdef USE_LVGL_KEY_LISTENER
513LVEncoderListener::LVEncoderListener(lv_indev_type_t type, uint16_t long_press_time, uint16_t long_press_repeat_time) {
514 this->drv_ = lv_indev_create();
515 lv_indev_set_type(this->drv_, type);
516 lv_indev_set_long_press_time(this->drv_, long_press_time);
517 lv_indev_set_long_press_repeat_time(this->drv_, long_press_repeat_time);
518 lv_indev_set_user_data(this->drv_, this);
519 lv_indev_set_read_cb(this->drv_, [](lv_indev_t *d, lv_indev_data_t *data) {
520 auto *l = static_cast<LVEncoderListener *>(lv_indev_get_user_data(d));
521 data->state = l->pressed_ ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
522 data->key = l->key_;
523 data->enc_diff = (int16_t) (l->count_ - l->last_count_);
524 l->last_count_ = l->count_;
525 data->continue_reading = false;
526 });
527}
528#endif // USE_LVGL_KEY_LISTENER
529
530#if defined(USE_LVGL_DROPDOWN) || defined(LV_USE_ROLLER)
532 auto selected = this->get_selected_index();
533 if (selected >= this->options_.size())
534 return "";
535 return this->options_[selected];
536}
537
538static std::string join_string(std::vector<std::string> options) {
539 return std::accumulate(
540 options.begin(), options.end(), std::string(),
541 [](const std::string &a, const std::string &b) -> std::string { return a + (!a.empty() ? "\n" : "") + b; });
542}
543
544void LvSelectable::set_selected_text(const std::string &text, lv_anim_enable_t anim) {
545 auto index = std::find(this->options_.begin(), this->options_.end(), text);
546 if (index != this->options_.end()) {
547 this->set_selected_index(index - this->options_.begin(), anim);
548 lv_obj_send_event(this->obj, lv_update_event, nullptr);
549 }
550}
551
552void LvSelectable::set_options(std::vector<std::string> options) {
553 auto index = this->get_selected_index();
554 if (index >= options.size())
555 index = options.size() - 1;
556 this->options_ = std::move(options);
557 this->set_option_string(join_string(this->options_).c_str());
558 lv_obj_send_event(this->obj, LV_EVENT_REFRESH, nullptr);
559 this->set_selected_index(index, LV_ANIM_OFF);
560}
561#endif // USE_LVGL_DROPDOWN || LV_USE_ROLLER
562
563#ifdef USE_LVGL_BUTTONMATRIX
564void LvButtonMatrixType::set_obj(lv_obj_t *lv_obj) {
565 LvCompound::set_obj(lv_obj);
566 lv_obj_add_event_cb(
567 lv_obj,
568 [](lv_event_t *event) {
569 auto *self = static_cast<LvButtonMatrixType *>(lv_event_get_user_data(event));
570 if (self->key_callback_.size() == 0)
571 return;
572 auto key_idx = lv_buttonmatrix_get_selected_button(self->obj);
573 if (key_idx == LV_BUTTONMATRIX_BUTTON_NONE)
574 return;
575 if (self->key_map_.count(key_idx) != 0) {
576 self->send_key_(self->key_map_[key_idx]);
577 return;
578 }
579 const auto *str = lv_buttonmatrix_get_button_text(self->obj, key_idx);
580 auto len = strlen(str);
581 while (len--)
582 self->send_key_(*str++);
583 },
584 LV_EVENT_PRESSED, this);
585}
586#endif // USE_LVGL_BUTTONMATRIX
587
588#ifdef USE_LVGL_KEYBOARD
589static const char *const KB_SPECIAL_KEYS[] = {
590 "abc", "ABC", "1#",
591 // maybe add other special keys here
592};
593
594void LvKeyboardType::set_obj(lv_obj_t *lv_obj) {
595 LvCompound::set_obj(lv_obj);
596 lv_obj_add_event_cb(
597 lv_obj,
598 [](lv_event_t *event) {
599 auto *self = static_cast<LvKeyboardType *>(lv_event_get_user_data(event));
600 if (self->key_callback_.size() == 0)
601 return;
602
603 auto key_idx = lv_buttonmatrix_get_selected_button(self->obj);
604 if (key_idx == LV_BUTTONMATRIX_BUTTON_NONE)
605 return;
606 const char *txt = lv_buttonmatrix_get_button_text(self->obj, key_idx);
607 if (txt == nullptr)
608 return;
609 for (const auto *kb_special_key : KB_SPECIAL_KEYS) {
610 if (strcmp(txt, kb_special_key) == 0)
611 return;
612 }
613 while (*txt != 0)
614 self->send_key_(*txt++);
615 },
616 LV_EVENT_PRESSED, this);
617}
618#endif // USE_LVGL_KEYBOARD
619
621 if (this->draw_end_callback_ != nullptr)
623 if (this->update_when_display_idle_) {
624 for (auto *disp : this->displays_)
625 disp->update();
626 }
627}
628
630 if (this->paused_)
631 return true;
632 if (this->update_when_display_idle_) {
633 for (auto *disp : this->displays_) {
634 if (!disp->is_idle())
635 return true;
636 }
637 }
638 return false;
639}
640
642 int iterations = 6 - lv_display_get_inactive_time(this->disp_) / 60000;
643 if (iterations <= 0)
644 iterations = 1;
645 int16_t width = lv_display_get_horizontal_resolution(this->disp_);
646 int16_t height = lv_display_get_vertical_resolution(this->disp_);
647 while (iterations-- != 0) {
648 int32_t col = random_uint32() % width;
649 col = col / this->draw_rounding * this->draw_rounding;
650 int32_t row = random_uint32() % height;
651 row = row / this->draw_rounding * this->draw_rounding;
652 // size will be between 8 and 32, and a multiple of draw_rounding
653 int32_t size = (random_uint32() % 25 + 8) / this->draw_rounding * this->draw_rounding;
654 lv_area_t area{.x1 = col, .y1 = row, .x2 = col + size - 1, .y2 = row + size - 1};
655 // clip to display bounds just in case
656 if (area.x2 >= width)
657 area.x2 = width - 1;
658 if (area.y2 >= height)
659 area.y2 = height - 1;
660
661 // line_len can't exceed 1024, and minimum buffer size is 2048, so this won't overflow the buffer
662 size_t line_len = lv_area_get_width(&area) * lv_area_get_height(&area) / 2;
663 for (size_t i = 0; i != line_len; i++) {
664 reinterpret_cast<uint32_t *>(this->draw_buf_)[i] = random_uint32();
665 }
666 this->draw_buffer_(&area, reinterpret_cast<lv_color_data *>(this->draw_buf_));
667 }
668}
669
691LvglComponent::LvglComponent(std::vector<display::Display *> displays, float buffer_frac, bool full_refresh,
692 int draw_rounding, bool resume_on_input, bool update_when_display_idle,
693 RotationType rotation_type)
694 : draw_rounding(draw_rounding),
695 displays_(std::move(displays)),
696 buffer_frac_(buffer_frac),
697 full_refresh_(full_refresh),
698 resume_on_input_(resume_on_input),
699 update_when_display_idle_(update_when_display_idle),
700 rotation_type_(rotation_type) {
701 this->disp_ = lv_display_create(240, 240);
702}
703
705 int32_t width = this->width_;
706 int32_t height = this->height_;
709 std::swap(width, height);
710 }
711 ESP_LOGD(TAG, "Setting resolution to %u x %u (rotation %d)", (unsigned) width, (unsigned) height,
712 (int) this->rotation_);
714 for (auto *display : this->displays_)
715 display->set_rotation(this->rotation_);
716 }
717 lv_display_set_resolution(this->disp_, width, height);
718}
720 auto *display = this->displays_[0];
721 auto rounding = this->draw_rounding;
722 this->width_ = display->get_native_width();
723 this->height_ = display->get_native_height();
724 // cater for displays with dimensions that don't divide by the required rounding
725 auto width = (this->width_ + rounding - 1) / rounding * rounding;
726 auto height = (this->height_ + rounding - 1) / rounding * rounding;
727 auto frac = this->buffer_frac_;
728 if (frac == 0)
729 frac = 1;
730 auto buf_bytes = clamp_at_least(width * height / frac * LV_COLOR_DEPTH / 8, MIN_BUFFER_SIZE);
731 void *buffer = nullptr;
732 // for small buffers, try to allocate in internal memory first to improve performance
733 if (this->buffer_frac_ >= MIN_BUFFER_FRAC / 2)
734 buffer = lv_alloc_draw_buf(buf_bytes, true); // NOLINT
735 if (buffer == nullptr)
736 buffer = lv_alloc_draw_buf(buf_bytes, false); // NOLINT
737 // if specific buffer size not set and can't get 100%, try for a smaller one
738 if (buffer == nullptr && this->buffer_frac_ == 0) {
739 frac = MIN_BUFFER_FRAC;
740 buf_bytes /= MIN_BUFFER_FRAC;
741 buffer = lv_alloc_draw_buf(buf_bytes, false); // NOLINT
742 }
743 this->buffer_frac_ = frac;
744 if (buffer == nullptr) {
745 this->status_set_error(LOG_STR("Memory allocation failure"));
746 this->mark_failed();
747 return;
748 }
749 this->draw_buf_ = static_cast<uint8_t *>(buffer);
750 this->set_resolution_();
751 lv_display_set_color_format(this->disp_, LV_COLOR_FORMAT_RGB565);
752 lv_display_set_flush_cb(this->disp_, static_flush_cb);
753 lv_display_set_user_data(this->disp_, this);
754 lv_display_add_event_cb(this->disp_, rounder_cb, LV_EVENT_INVALIDATE_AREA, this);
755 lv_display_set_buffers(this->disp_, this->draw_buf_, nullptr, buf_bytes,
756 this->full_refresh_ ? LV_DISPLAY_RENDER_MODE_FULL : LV_DISPLAY_RENDER_MODE_PARTIAL);
758 this->rotate_buf_ = static_cast<lv_color_t *>(lv_alloc_draw_buf(buf_bytes, false)); // NOLINT
759 if (this->rotate_buf_ == nullptr) {
760 this->status_set_error(LOG_STR("Memory allocation failure"));
761 this->mark_failed();
762 return;
763 }
764#ifdef USE_ESP32_VARIANT_ESP32P4
765 ppa_client_config_t ppa_config{};
766 ppa_config.oper_type = PPA_OPERATION_SRM;
767 ppa_config.max_pending_trans_num = 1;
768 if (ppa_register_client(&ppa_config, &this->ppa_client_) != ESP_OK) {
769 ESP_LOGW(TAG, "PPA client registration failed, using software rotation");
770 this->ppa_client_ = nullptr;
771 }
772#endif
773 }
774 if (this->draw_start_callback_ != nullptr) {
775 lv_display_add_event_cb(this->disp_, render_start_cb, LV_EVENT_RENDER_START, this);
776 }
777 if (this->draw_end_callback_ != nullptr || this->update_when_display_idle_) {
778 lv_display_add_event_cb(this->disp_, render_end_cb, LV_EVENT_REFR_READY, this);
779 }
780#if LV_USE_LOG
781 lv_log_register_print_cb([](lv_log_level_t level, const char *buf) {
782 auto next = strchr(buf, ')');
783 if (next != nullptr)
784 buf = next + 1;
785 while (isspace(*buf))
786 buf++;
787 if (level >= sizeof(LOG_LEVEL_MAP) / sizeof(LOG_LEVEL_MAP[0]))
788 level = sizeof(LOG_LEVEL_MAP) / sizeof(LOG_LEVEL_MAP[0]) - 1;
789 esp_log_printf_(LOG_LEVEL_MAP[level], TAG, 0, "%.*s", (int) strlen(buf) - 1, buf);
790 });
791#endif
792 this->show_page(0, LV_SCREEN_LOAD_ANIM_NONE, 0);
793 lv_display_trigger_activity(this->disp_);
794}
795
797 // update indicators
798 if (this->is_paused()) {
799 return;
800 }
801 this->idle_callbacks_.call(lv_display_get_inactive_time(this->disp_));
802}
803
805 if (this->is_paused()) {
806 if (this->paused_ && this->show_snow_)
807 this->write_random_();
808 } else {
809#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
810 auto now = millis();
811 lv_timer_handler();
812 auto elapsed = millis() - now;
813 if (elapsed > 15) {
814 ESP_LOGV(TAG, "lv_timer_handler took %dms", (int) (millis() - now));
815 }
816#else
817 lv_timer_handler();
818#endif
819 }
820}
821
822#ifdef USE_LVGL_ANIMIMG
823void lv_animimg_stop(lv_obj_t *obj) {
824 int32_t duration = lv_animimg_get_duration(obj);
825 lv_animimg_set_duration(obj, 0);
826 lv_animimg_start(obj);
827 lv_animimg_set_duration(obj, duration);
828}
829#endif
830void LvglComponent::static_flush_cb(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p) {
831 reinterpret_cast<LvglComponent *>(lv_display_get_user_data(disp_drv))->flush_cb_(disp_drv, area, color_p);
832}
833
834#ifdef USE_LVGL_SCALE
842void lv_scale_draw_event_cb(lv_event_t *e, int16_t range_start, int16_t range_end, lv_color_t color_start,
843 lv_color_t color_end, int width, bool local) {
844 auto *scale = static_cast<lv_obj_t *>(lv_event_get_target(e));
845 lv_draw_task_t *task = lv_event_get_draw_task(e);
846
847 if (lv_draw_task_get_type(task) == LV_DRAW_TASK_TYPE_LINE) {
848 auto *line_dsc = static_cast<lv_draw_line_dsc_t *>(lv_draw_task_get_draw_dsc(task));
849 int tick = line_dsc->base.id2;
850 if (tick >= range_start && tick <= range_end) {
851 int ratio;
852 if (local) {
853 int range = range_end - range_start;
854 tick -= range_start;
855 ratio = range == 0 ? 0 : (tick * 255) / range;
856 } else {
857 // total tick count is guaranteed to be at least 2.
858 ratio = (line_dsc->base.id1 * 255) / (lv_scale_get_total_tick_count(scale) - 1);
859 }
860 line_dsc->color = lv_color_mix(color_end, color_start, ratio);
861 line_dsc->width += width;
862 }
863 }
864}
865#endif // USE_LVGL_SCALE
866
867#ifdef USE_LVGL_GRADIENT
875lv_color_t lv_grad_calculate_color(const lv_grad_dsc_t *dsc, int32_t pos) {
876 if (dsc->stops_count == 0)
877 return lv_color_black();
878 if (dsc->stops_count == 1 || pos <= dsc->stops[0].frac)
879 return dsc->stops[0].color;
880 if (pos >= dsc->stops[dsc->stops_count - 1].frac)
881 return dsc->stops[dsc->stops_count - 1].color;
882 int i = 1;
883 while (i < dsc->stops_count && dsc->stops[i].frac < pos)
884 i++;
885 auto *stop1 = &dsc->stops[i - 1];
886 auto *stop2 = &dsc->stops[i];
887 int32_t range = stop2->frac - stop1->frac;
888 int32_t offset = pos - stop1->frac;
889 return lv_color_mix(stop2->color, stop1->color, range == 0 ? 0 : (offset * 255) / range);
890}
891#endif // USE_LVGL_GRADIENT
892
894 auto *indev = lv_indev_get_act();
895 if (indev == nullptr) {
896 return {INT32_MAX, INT32_MAX};
897 }
898 lv_point_t point;
899 lv_indev_get_point(indev, &point);
900 lv_area_t coords;
901 lv_obj_get_coords(obj, &coords);
902 point.x -= coords.x1;
903 point.y -= coords.y1;
904 return point;
905}
906
907static void lv_container_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj) {
908 LV_TRACE_OBJ_CREATE("begin");
909 LV_UNUSED(class_p);
910}
911
912// Container class. Name is based on LVGL naming convention but upper case to keep ESPHome clang-tidy happy
913const lv_obj_class_t LV_CONTAINER_CLASS = {
914 .base_class = &lv_obj_class,
915 .constructor_cb = lv_container_constructor,
916 .name = "lv_container",
917};
918
919lv_obj_t *lv_container_create(lv_obj_t *parent) {
920 lv_obj_t *obj = lv_obj_class_create_obj(&LV_CONTAINER_CLASS, parent);
921 lv_obj_class_init_obj(obj);
922 return obj;
923}
924} // namespace esphome::lvgl
925
926lv_result_t lv_mem_test_core() { return LV_RESULT_OK; }
927
928void lv_mem_init() {}
929
931
932#if defined(USE_HOST) || defined(USE_RP2040) || defined(USE_ESP8266)
933void *lv_malloc_core(size_t size) {
934 auto *ptr = malloc(size); // NOLINT
935 if (ptr == nullptr) {
936 ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
937 }
938 return ptr;
939}
940void lv_free_core(void *ptr) { return free(ptr); } // NOLINT
941void *lv_realloc_core(void *ptr, size_t size) { return realloc(ptr, size); } // NOLINT
942
943void lv_mem_monitor_core(lv_mem_monitor_t *mon_p) { memset(mon_p, 0, sizeof(lv_mem_monitor_t)); }
944static void *lv_alloc_draw_buf(size_t size, bool internal) {
945 return malloc(size); // NOLINT
946}
947
948#elif defined(USE_ESP32)
949static unsigned cap_bits = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; // NOLINT
950
951static void *lv_alloc_draw_buf(size_t size, bool internal) {
952 void *buffer;
953 size = LV_ROUND_UP(size, LV_DRAW_BUF_ALIGN);
954 buffer = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size, internal ? MALLOC_CAP_8BIT : cap_bits); // NOLINT
955 if (buffer == nullptr)
956 ESP_LOGW(esphome::lvgl::TAG, "Failed to allocate %zu bytes for %sdraw buffer", size, internal ? "internal " : "");
957 return buffer;
958}
959
960void lv_mem_monitor_core(lv_mem_monitor_t *mon_p) {
961 multi_heap_info_t heap_info;
962 heap_caps_get_info(&heap_info, cap_bits);
963 mon_p->total_size = heap_info.total_allocated_bytes + heap_info.total_free_bytes;
964 mon_p->free_size = heap_info.total_free_bytes;
965 mon_p->max_used = heap_info.total_allocated_bytes;
966 mon_p->free_biggest_size = heap_info.largest_free_block;
967 mon_p->used_cnt = heap_info.allocated_blocks;
968 mon_p->free_cnt = heap_info.free_blocks;
969 mon_p->used_pct = heap_info.allocated_blocks * 100 / (heap_info.allocated_blocks + heap_info.free_blocks);
970 mon_p->frag_pct = 0;
971}
972
973void *lv_malloc_core(size_t size) {
974 void *ptr;
975 ptr = heap_caps_malloc(size, cap_bits);
976 if (ptr == nullptr) {
977 cap_bits = MALLOC_CAP_8BIT;
978 ptr = heap_caps_malloc(size, cap_bits);
979 }
980 if (ptr == nullptr) {
981 ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
982 return nullptr;
983 }
984 ESP_LOGV(esphome::lvgl::TAG, "allocate %zu - > %p", size, ptr);
985 return ptr;
986}
987
988void lv_free_core(void *ptr) {
989 ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr);
990 if (ptr == nullptr)
991 return;
992 heap_caps_free(ptr);
993}
994
995void *lv_realloc_core(void *ptr, size_t size) {
996 ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size);
997 return heap_caps_realloc(ptr, size, cap_bits);
998}
999#endif
uint8_t l
Definition bl0906.h:0
void mark_failed()
Mark this component as failed.
bool is_ready() const
void set_parent(T *parent)
Set the parent of this object.
Definition helpers.h:1869
Function-pointer-only templatable storage (4 bytes on 32-bit).
Definition automation.h:40
T value(X... x) const
Definition automation.h:79
void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE
Inform the parent automation that the event has triggered.
Definition automation.h:482
TemplatableFn< uint32_t > timeout_
IdleTrigger(LvglComponent *parent, TemplatableFn< uint32_t > timeout)
void set_obj(lv_obj_t *lv_obj) override
LVEncoderListener(lv_indev_type_t type, uint16_t long_press_time, uint16_t long_press_repeat_time)
LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time, LvglComponent *parent)
touchscreen::TouchPoint touch_point_
void update(const touchscreen::TouchPoints_t &tpoints) override
void set_obj(lv_obj_t *lv_obj) override
virtual void set_obj(lv_obj_t *lv_obj)
void set_obj(lv_obj_t *lv_obj) override
void setup(size_t index)
void set_selected_text(const std::string &text, lv_anim_enable_t anim)
void set_options(std::vector< std::string > options)
std::vector< std::string > options_
virtual void set_selected_index(size_t index, lv_anim_enable_t anim)=0
virtual size_t get_selected_index()=0
virtual void set_option_string(const char *options)=0
Component for rendering LVGL.
void set_paused(bool paused, bool show_snow)
display::DisplayRotation rotation_
void rotate_coordinates(int32_t &x, int32_t &y) const
std::vector< LvPageType * > pages_
void set_rotation(display::DisplayRotation rotation)
static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event)
bool ppa_rotate_(const lv_color_data *src, lv_color_data *dst, uint16_t width, uint16_t height, uint32_t height_rounded)
CallbackManager< void(uint32_t)> idle_callbacks_
void show_next_page(lv_screen_load_anim_t anim, uint32_t time)
std::vector< display::Display * > displays_
static void esphome_lvgl_init()
Initialize the LVGL library and register custom events.
void show_prev_page(lv_screen_load_anim_t anim, uint32_t time)
ppa_client_handle_t ppa_client_
LvglComponent(std::vector< display::Display * > displays, float buffer_frac, bool full_refresh, int draw_rounding, bool resume_on_input, bool update_when_display_idle, RotationType rotation_type)
static void render_start_cb(lv_event_t *event)
static lv_point_t get_touch_relative_to_obj(lv_obj_t *obj)
void add_on_idle_callback(F &&callback)
void add_page(LvPageType *page)
static void static_flush_cb(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p)
static void render_end_cb(lv_event_t *event)
void draw_buffer_(const lv_area_t *area, lv_color_data *ptr)
void show_page(size_t index, lv_screen_load_anim_t anim, uint32_t time)
void flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p)
uint16_t type
uint8_t options
void lv_mem_deinit()
void lv_mem_init()
void lv_free_core(void *ptr)
void * lv_malloc_core(size_t size)
void lv_mem_monitor_core(lv_mem_monitor_t *mon_p)
void * lv_realloc_core(void *ptr, size_t size)
lv_result_t lv_mem_test_core()
Range range
Definition msa3xx.h:0
uint8_t duration
Definition msa3xx.h:0
@ DISPLAY_ROTATION_270_DEGREES
Definition display.h:137
@ DISPLAY_ROTATION_180_DEGREES
Definition display.h:136
@ DISPLAY_ROTATION_90_DEGREES
Definition display.h:135
lv_color_t lv_grad_calculate_color(const lv_grad_dsc_t *dsc, int32_t pos)
void lv_animimg_stop(lv_obj_t *obj)
uint16_t lv_color_data
void(lv_event_t *) event_callback_t
const lv_obj_class_t LV_CONTAINER_CLASS
lv_event_code_t lv_update_event
int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int32_t value)
lv_obj_t * lv_container_create(lv_obj_t *parent)
std::string lv_event_code_name_for(lv_event_t *event)
void lv_scale_draw_event_cb(lv_event_t *e, int16_t range_start, int16_t range_end, lv_color_t color_start, lv_color_t color_end, int width, bool local)
Function to apply colors to ticks based on position.
std::vector< TouchPoint > TouchPoints_t
Definition touchscreen.h:29
T clamp_at_most(T value, U max)
Definition helpers.h:2191
void HOT esp_log_printf_(int level, const char *tag, int line, const char *format,...)
Definition log.cpp:21
std::string size_t len
uint16_t size
Definition helpers.cpp:25
T clamp_at_least(T value, U min)
Definition helpers.h:2186
uint32_t random_uint32()
Return a random 32-bit unsigned integer.
Definition helpers.cpp:12
size_t size_t pos
Definition helpers.h:1038
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
static void uint32_t
uint16_t length
Definition tt21100.cpp:0
uint16_t x
Definition tt21100.cpp:5
uint16_t y
Definition tt21100.cpp:6