diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 66aaac5301..9b2f41e063 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -17,6 +17,7 @@ from esphome.components.esp32 import ( add_idf_component, add_idf_sdkconfig_option, get_esp32_variant, + idf_version, ) from esphome.components.image import ( CONF_OPAQUE, @@ -158,7 +159,7 @@ def as_macro(macro, value): return f"#define {macro} {value}" -LVGL_VERSION = "9.5.0" +LVGL_VERSION = "9.6.0" LV_CONF_FILENAME = "lv_conf.h" LV_CONF_H_FORMAT = """\ #pragma once @@ -324,10 +325,16 @@ async def to_code(configs): # Skip compiling lvgl examples add_idf_sdkconfig_option("CONFIG_LV_BUILD_EXAMPLES", False) add_idf_sdkconfig_option("CONFIG_LV_BUILD_DEMOS", False) + # Match LV_USE_STDLIB_MALLOC in lv_conf.h. This also drops the builtin malloc + # options, which LVGL 9.5 builds leave set to deprecated values. + add_idf_sdkconfig_option("CONFIG_LV_USE_CUSTOM_MALLOC", True) if get_esp32_variant() == VARIANT_ESP32P4: add_idf_sdkconfig_option("CONFIG_LV_DRAW_BUF_ALIGN", 64) - # disable use of PPA for fills until upstream bugs fixed - df.add_define("LV_USE_PPA", "0") + # The LVGL PPA driver uses cache functions added in ESP-IDF 6.0 + if idf_version() >= cv.Version(6, 0, 0): + df.add_define("LV_USE_PPA", "1") + else: + df.add_define("LV_USE_PPA", "0") df.add_define("LV_DRAW_BUF_ALIGN", "64") else: df.add_define("LV_DRAW_BUF_ALIGN", "32") @@ -337,6 +344,8 @@ async def to_code(configs): cg.add_library("lvgl/lvgl", LVGL_VERSION) df.add_define("LV_DRAW_BUF_STRIDE_ALIGN", "1") df.add_define("LV_USE_DRAW_SW", "1") + # LVGL 9.6 rejects 0 here unless tiled rendering is disabled + df.add_define("LV_DRAW_SW_DRAW_UNIT_CNT", "1") df.add_define("LV_USE_STDLIB_SPRINTF", "LV_STDLIB_CLIB") df.add_define("LV_USE_STDLIB_STRING", "LV_STDLIB_CLIB") df.add_define("LV_USE_STDLIB_MALLOC", "LV_STDLIB_CUSTOM") @@ -356,15 +365,22 @@ async def to_code(configs): "LVGL_LOG_LEVEL", cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[CONF_LOG_LEVEL]}"), ) - df.add_define("LV_COLOR_DEPTH", config_0[CONF_COLOR_DEPTH]) + # Big-endian displays get LVGL to render directly in swapped byte order + display_format = ( + "RGB565_SWAPPED" if config_0[CONF_BYTE_ORDER] == BYTE_ORDER_BIG else "RGB565" + ) + df.add_define("LV_COLOR_FORMAT_DEFAULT", f"LV_COLOR_FORMAT_{display_format}") for font in df.get_lv_fonts_used(): df.add_define(f"LV_FONT_{font.upper()}") - - if config_0[CONF_COLOR_DEPTH] == 16: + df.add_define("LV_OBJ_STYLE_CACHE", "1") + if any(config[df.CONF_CHECK_ARGS] for config in configs): + df.add_define("LV_USE_CHECK_ARG", "1") df.add_define( - "LV_COLOR_16_SWAP", - "1" if config_0[CONF_BYTE_ORDER] == "big_endian" else "0", + "LV_CHECK_ARG_LOG_MODE", + f"LV_CHECK_ARG_LOG_MODE_{df.LV_CHECK_ARG_LOG_MODES[config_0[CONF_LOG_LEVEL]]}", ) + else: + df.add_define("LV_USE_CHECK_ARG", "0") df.add_define( "LV_COLOR_CHROMA_KEY", await lvalid.lv_color.process(config_0[df.CONF_TRANSPARENCY_KEY]), @@ -504,8 +520,9 @@ async def to_code(configs): if configs[0].get(df.CONF_THEME, {}).get(df.CONF_DARK_MODE): df.add_define("LV_THEME_DEFAULT_DARK", "1") - # Currently always need RGB565 for the display buffer, and ARGB8888 is used for layer blending - lv_image_formats = {"RGB565", "ARGB8888"} + # The display buffer format is always needed, RGB565 is used by canvas buffers and + # ARGB8888 for layer blending + lv_image_formats = {"RGB565", display_format, "ARGB8888"} for image_id in get_lv_images_used(): await cg.get_variable(image_id) @@ -519,7 +536,7 @@ async def to_code(configs): if image_type == ImageRGB565: lv_image_formats.add("RGB565A8" if transparent else "RGB565") if image_type == ImageRGB: - lv_image_formats.add("ARGB8888" if transparent else "RGB8888") + lv_image_formats.add("ARGB8888" if transparent else "RGB888") if df.is_defined("LV_GRADIENT_MAX_STOPS"): lv_image_formats.add("RGB888") for fmt in lv_image_formats: @@ -595,6 +612,7 @@ LVGL_TOP_LEVEL_SCHEMA = ( cv.Optional(CONF_LOG_LEVEL, default="WARN"): cv.one_of( *df.LV_LOG_LEVELS, upper=True ), + cv.Optional(df.CONF_CHECK_ARGS, default=False): cv.boolean, cv.Optional(CONF_BYTE_ORDER): cv.one_of( "big_endian", "little_endian", lower=True ), diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index d797688a7e..4e8698f33c 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -8,6 +8,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_ACTION, CONF_GROUP, + CONF_HIDDEN, CONF_ID, CONF_POSITION, CONF_ROTATION, @@ -354,7 +355,7 @@ async def obj_enable_to_code(config, action_id, template_arg, args): ) async def obj_hide_to_code(config, action_id, template_arg, args): async def do_hide(widget: Widget): - widget.add_flag("LV_OBJ_FLAG_HIDDEN") + widget.set_flag(CONF_HIDDEN, True) widgets = [widget.outer or widget for widget in await get_widgets(config)] return await action_to_code(widgets, do_hide, action_id, template_arg, args) @@ -365,7 +366,7 @@ async def obj_hide_to_code(config, action_id, template_arg, args): ) async def obj_show_to_code(config, action_id, template_arg, args): async def do_show(widget: Widget): - widget.clear_flag("LV_OBJ_FLAG_HIDDEN") + widget.set_flag(CONF_HIDDEN, False) if widget.move_to_foreground: lv_obj.move_foreground(widget.obj) diff --git a/esphome/components/lvgl/binary_sensor/__init__.py b/esphome/components/lvgl/binary_sensor/__init__.py index fc7317f8b9..e658735c75 100644 --- a/esphome/components/lvgl/binary_sensor/__init__.py +++ b/esphome/components/lvgl/binary_sensor/__init__.py @@ -6,7 +6,7 @@ from esphome.components.binary_sensor import ( import esphome.config_validation as cv from esphome.const import CONF_STATE -from ..defines import CONF_WIDGET, LV_OBJ_FLAG, LvConstant +from ..defines import CONF_CLICKABLE, CONF_WIDGET, LvConstant from ..lvcode import EVENT_ARG, UPDATE_EVENT, LambdaContext, LvContext, lvgl_static from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t from ..widgets import Widget, get_widgets @@ -41,7 +41,7 @@ async def to_code(config): ctx.add(sensor.publish_initial_state(test_expr)) if is_pressed: events = [LV_EVENT.PRESSED, LV_EVENT.RELEASED] - widget.add_flag(LV_OBJ_FLAG.CLICKABLE) + widget.set_flag(CONF_CLICKABLE, True) else: events = [LV_EVENT.VALUE_CHANGED, UPDATE_EVENT] ctx.add( diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index c019dff96e..854ef0fb49 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -477,6 +477,16 @@ LV_LOG_LEVELS = { "NONE": "NONE", } +# Log output for failed LVGL argument checks, chosen from the LVGL log level +LV_CHECK_ARG_LOG_MODES = { + "VERBOSE": "VERBOSE", + "DEBUG": "VERBOSE", + "INFO": "MINIMAL", + "WARN": "MINIMAL", + "ERROR": "NONE", + "NONE": "NONE", +} + LV_LONG_MODES = LvConstant( "LV_LABEL_LONG_", "WRAP", @@ -606,13 +616,20 @@ OBJ_FLAGS = ( "ignore_layout", "floating", "overflow_visible", + "send_draw_task_events", + "event_trickle", + "state_trickle", + "radio_button", + "flex_in_new_track", "layout_1", "layout_2", - "send_draw_task_events", "widget_1", "widget_2", ) -LV_OBJ_FLAG = LvConstant("LV_OBJ_FLAG_", *OBJ_FLAGS) +# Flags whose lv_obj_set_() setter has a different name +FLAG_SETTER_ALIASES = {"layout_1": "flex_in_new_track"} +# Custom flags that have no lv_obj_set_() setter +CUSTOM_FLAGS = ("layout_2", "widget_1", "widget_2") ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL") BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE") @@ -703,6 +720,7 @@ CONF_DISP_BG_OPA = "disp_bg_opa" CONF_BODY = "body" CONF_BUTTONS = "buttons" CONF_CHANGE_RATE = "change_rate" +CONF_CLICKABLE = "clickable" CONF_CLOSE_BUTTON = "close_button" CONF_COLOR_END = "color_end" CONF_COLOR_START = "color_start" @@ -727,6 +745,7 @@ CONF_FLEX_ALIGN_CROSS = "flex_align_cross" CONF_FLEX_ALIGN_TRACK = "flex_align_track" CONF_FLEX_GROW = "flex_grow" CONF_FREEZE = "freeze" +CONF_CHECK_ARGS = "check_args" CONF_DARK_MODE = "dark_mode" CONF_FULL_REFRESH = "full_refresh" CONF_GRADIENTS = "gradients" @@ -795,6 +814,7 @@ CONF_SCALE = "scale" CONF_SCALE_LINES = "scale_lines" CONF_SCROLLBAR_MODE = "scrollbar_mode" CONF_SCROLL_DIR = "scroll_dir" +CONF_SCROLLABLE = "scrollable" CONF_SCROLL_SNAP_X = "scroll_snap_x" CONF_SCROLL_SNAP_Y = "scroll_snap_y" CONF_SELECTED_INDEX = "selected_index" @@ -891,7 +911,8 @@ def get_part_state_selector(part: str, state: str) -> MockObj: # fmt: off LV_COLOR_FORMATS = ( - "RGB565", "SWAPPED", "RGB565A8", "RGB888", "XRGB8888", "ARGB8888", "PREMULTIPLIED", "L8", "AL88", "A8", "I1", + "RGB565", "RGB565_SWAPPED", "RGB565A8", "RGB888", "XRGB8888", "ARGB8888", "ARGB8888_PREMULTIPLIED", "L8", "AL88", + "A8", "I1", ) LV_DEFINES = ( diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index e35cc0c48b..0b375404d7 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -6,6 +6,7 @@ #include "core/lv_global.h" #include "core/lv_obj_class_private.h" +#include "core/lv_obj_private.h" #include @@ -225,6 +226,36 @@ void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_ev add_event_cb(obj, callback, event3, user_data); } +void LvglComponent::lv_obj_set_custom_flag(lv_obj_t *obj, lv_obj_flag_t flag, bool value) { + // LVGL 9.6 keeps these flags in spec_attr->user_flags, in the same bits as its deprecated lv_obj_add_flag() + uint8_t mask; + switch (flag) { + case LV_OBJ_FLAG_LAYOUT_2: + mask = 1 << 1; + break; + case LV_OBJ_FLAG_WIDGET_1: + mask = 1 << 2; + break; + case LV_OBJ_FLAG_WIDGET_2: + mask = 1 << 3; + break; + default: + return; + } + if (value) { + lv_obj_spec_attr_t *spec_attr = lv_obj_allocate_spec_attr(obj); + if (spec_attr == nullptr) + return; + spec_attr->user_flags |= mask; + } else if (obj->spec_attr != nullptr) { + obj->spec_attr->user_flags &= ~mask; + } + if (flag == LV_OBJ_FLAG_LAYOUT_2) { + lv_obj_mark_layout_as_dirty(lv_obj_get_parent(obj)); + lv_obj_mark_layout_as_dirty(obj); + } +} + void LvglComponent::add_page(LvPageType *page) { this->pages_.push_back(page); page->set_parent(this); @@ -822,7 +853,7 @@ void LvglComponent::setup() { } this->draw_buf_ = static_cast(buffer); this->set_resolution_(); - lv_display_set_color_format(this->disp_, LV_COLOR_FORMAT_RGB565); + lv_display_set_color_format(this->disp_, LV_COLOR_FORMAT_DEFAULT); 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); @@ -994,7 +1025,7 @@ lv_point_t LvglComponent::get_touch_relative_to_obj(lv_obj_t *obj) { static void lv_container_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj) { LV_TRACE_OBJ_CREATE("begin"); - lv_obj_remove_flag(obj, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_clickable(obj, false); LV_UNUSED(class_p); } diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 8b7397c4cd..4da8a6a1a3 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -269,6 +269,9 @@ class LvglComponent final : public PollingComponent { static 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, void *user_data = nullptr); + // Set or clear LV_OBJ_FLAG_LAYOUT_2, LV_OBJ_FLAG_WIDGET_1 or LV_OBJ_FLAG_WIDGET_2, which have no setter in LVGL 9.6 + static void lv_obj_set_custom_flag(lv_obj_t *obj, lv_obj_flag_t flag, bool value); + // change the state of a widget and fire an event if changed (only needed for CHECKED) static void lv_obj_set_state_value(lv_obj_t *obj, lv_state_t state, bool value) { diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 924c17fb54..2d879e8d43 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -391,7 +391,6 @@ SET_STATE_SCHEMA = cv.Schema( ) # Setting object flags FLAG_SCHEMA = cv.Schema({cv.Optional(flag): lvalid.lv_bool for flag in df.OBJ_FLAGS}) -FLAG_LIST = cv.ensure_list(df.LV_OBJ_FLAG.one_of) VALUE_TRIGGER_SCHEMA = { cv.Optional(CONF_TRIGGER, default=CONF_ON_VALUE): cv.one_of( diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py index 56dcf81a79..cd118595f1 100644 --- a/esphome/components/lvgl/trigger.py +++ b/esphome/components/lvgl/trigger.py @@ -17,6 +17,8 @@ from .defines import ( CONF_ALIGN, CONF_ALIGN_TO, CONF_ALIGN_TO_LAMBDA_ID, + CONF_CLICKABLE, + CONF_SCROLLABLE, DIRECTIONS, LV_DISPLAY_EVENT_MAP, LV_DISPLAY_EVENT_TRIGGERS, @@ -72,7 +74,7 @@ async def generate_triggers(): event: conf for event, conf in config.items() if event in all_triggers }.items(): conf = conf[0] - w.add_flag("LV_OBJ_FLAG_CLICKABLE") + w.set_flag(CONF_CLICKABLE, True) await add_trigger(conf, w, event) for event, conf in { @@ -82,7 +84,7 @@ async def generate_triggers(): dir = event[9:].upper() dir = {"UP": "TOP", "DOWN": "BOTTOM"}.get(dir, dir) dir = DIRECTIONS.mapper(dir) - w.clear_flag("LV_OBJ_FLAG_SCROLLABLE") + w.set_flag(CONF_SCROLLABLE, False) selected = literal( f"lv_indev_get_gesture_dir(lv_indev_active()) == {dir}" ) diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index d69e90056f..b48123b902 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -1,4 +1,4 @@ -from collections.abc import Callable +from collections.abc import Awaitable, Callable import sys from typing import Any @@ -37,6 +37,8 @@ from ..defines import ( CONF_SCALE, CONF_STYLES, CONF_WIDGETS, + CUSTOM_FLAGS, + FLAG_SETTER_ALIASES, LOGGER, OBJ_FLAGS, PARTS, @@ -47,13 +49,11 @@ from ..defines import ( get_part_state_selector, get_styles_used, get_widget_map, - join_enums, literal, next_debug_outline_color, ) from ..lv_validation import lv_int from ..lvcode import ( - LvConditional, add_line_marks, lv, lv_add, @@ -295,7 +295,9 @@ class Widget: This class has a lot of methods. Adding any more runs foul of lint checks ("too many public methods"). """ - def __init__(self, var, wtype: WidgetType, config: dict = None): + def __init__( + self, var: MockObj | ID, wtype: WidgetType, config: dict | None = None + ) -> None: self.var = var self.type = wtype self.config = config @@ -314,39 +316,50 @@ class Widget: self.parent = None @staticmethod - def create(name, var, wtype: WidgetType, config: dict = None): + def create( + name: ID | str, var: MockObj, wtype: WidgetType, config: dict | None = None + ) -> "Widget": w = Widget(var, wtype, config) get_widget_map()[name] = w return w - def set_state(self, state: MockObj, value: bool | Expression): + def set_state(self, state: MockObj, value: bool | Expression) -> None: lv_add(lvgl_static.lv_obj_set_state_value(self.obj, state, value)) - def has_state(self, state: MockObj): + def has_state(self, state: MockObj) -> MockObj: return lv_expr.obj_has_state(self.obj, state) - def is_pressed(self): + def is_pressed(self) -> MockObj: return self.has_state(LV_STATE.PRESSED) - def is_checked(self): + def is_checked(self) -> MockObj: return self.has_state(LV_STATE.CHECKED) - def add_flag(self, flag): - if "|" in flag: - flag = f"(lv_obj_flag_t)({flag})" - return lv_obj.add_flag(self.obj, literal(flag)) + def set_flag(self, flag: str, value: bool | Expression) -> None: + """ + Set or clear an object flag, given as the lower case name without the LV_OBJ_FLAG_ prefix + """ + flag = FLAG_SETTER_ALIASES.get(flag, flag) + if flag in CUSTOM_FLAGS: + lv_add( + lvgl_static.lv_obj_set_custom_flag( + self.obj, literal(f"LV_OBJ_FLAG_{flag.upper()}"), value + ) + ) + else: + lv_obj.call(f"set_{flag}", self.obj, value) - def clear_flag(self, flag): - if "|" in flag: - flag = f"(lv_obj_flag_t)({flag})" - return lv_obj.remove_flag(self.obj, literal(flag)) - - def add_style(self, style_id, state=LV_STATE.DEFAULT): + def add_style(self, style_id: ID, state: MockObj | str = LV_STATE.DEFAULT) -> None: lv_obj.add_style(self.obj, MockObj(style_id), literal(state)) async def set_property( - self, prop, value, animated: bool = None, lv_name=None, processor=None - ): + self, + prop: str, + value: Any, + animated: bool | None = None, + lv_name: str | None = None, + processor: LValidator | Callable[[Any], Awaitable[Any]] | None = None, + ) -> None: """ Set a property of the widget. :param prop: The property name @@ -387,11 +400,13 @@ class Widget: literal("LV_ANIM_ON" if animated else "LV_ANIM_OFF"), ) - def get_property(self, prop, ltype=None): + def get_property(self, prop: str, ltype: str | None = None) -> cg.RawExpression: ltype = ltype or self.__type_base() return cg.RawExpression(f"lv_{ltype}_get_{prop}({self.obj})") - def set_style(self, prop: str, value, state=LV_STATE.DEFAULT): + def set_style( + self, prop: str, value: Any, state: MockObj | str = LV_STATE.DEFAULT + ) -> None: if value is None: return get_styles_used().add(prop) @@ -399,22 +414,22 @@ class Widget: value = literal(value) lv.call(f"obj_set_style_{prop}", self.obj, value, state) - def __type_base(self): + def __type_base(self) -> str: wtype = self.type.w_type base = str(wtype) if base.startswith("Lv"): return f"{wtype}".removeprefix("Lv").removesuffix("Type").lower() return f"{wtype}".removeprefix("lv_").removesuffix("_t") - def __str__(self): + def __str__(self) -> str: return f"({self.var}, {self.type})" - def get_args(self): + def get_args(self) -> list[tuple[MockObj, str]]: if isinstance(self.type.w_type, LvType): return self.type.w_type.args.copy() return [(lv_obj_t_ptr, "obj")] - def get_value(self): + def get_value(self) -> Expression: if isinstance(self.type.w_type, LvType): result = self.type.w_type.value(self) if isinstance(result, list): @@ -422,7 +437,7 @@ class Widget: return result return self.obj - def get_values(self): + def get_values(self) -> list[Expression]: if isinstance(self.type.w_type, LvType): result = self.type.w_type.value(self) if isinstance(result, list): @@ -430,13 +445,13 @@ class Widget: return [result] return [self.obj] - def get_number_value(self): + def get_number_value(self) -> Expression: value = self.type.mock_obj.get_value(self.obj) if self.scale == 1.0: return value return value / float(self.scale) - def is_selected(self): + def is_selected(self) -> MockObj | None: """ Overridable property to determine if the widget is selected. Will be None except for matrix buttons @@ -444,7 +459,7 @@ class Widget: """ return - def get_scale(self): + def get_scale(self) -> float: return self.type.get_scale(self.config) @@ -630,29 +645,14 @@ async def set_obj_properties(w: Widget, config): group = await cg.get_variable(group) lv.group_add_obj(group, w.obj) props = parts[CONF_MAIN][CONF_DEFAULT] - lambs = {} - flag_set = set() - flag_clr = set() - for prop, value in {k: v for k, v in props.items() if k in OBJ_FLAGS}.items(): + for flag, value in props.items(): + if flag not in OBJ_FLAGS: + continue if isinstance(value, cv.Lambda): - lambs[prop] = value - elif value: - flag_set.add(prop) - else: - flag_clr.add(prop) - if flag_set: - adds = join_enums(flag_set, "LV_OBJ_FLAG_") - w.add_flag(adds) - if flag_clr: - clrs = join_enums(flag_clr, "LV_OBJ_FLAG_") - w.clear_flag(clrs) - for key, value in lambs.items(): - lamb = await cg.process_lambda(value, [], capture="=", return_type=cg.bool_) - flag = f"LV_OBJ_FLAG_{key.upper()}" - with LvConditional(call_lambda(lamb)) as cond: - w.add_flag(flag) - cond.else_() - w.clear_flag(flag) + value = call_lambda( + await cg.process_lambda(value, [], capture="=", return_type=cg.bool_) + ) + w.set_flag(flag, value) for key, value in config.get(CONF_STATE, {}).items(): if isinstance(value, cv.Lambda): diff --git a/esphome/components/lvgl/widgets/arc.py b/esphome/components/lvgl/widgets/arc.py index ac993cc382..4739412e09 100644 --- a/esphome/components/lvgl/widgets/arc.py +++ b/esphome/components/lvgl/widgets/arc.py @@ -13,12 +13,12 @@ from ..defines import ( ARC_MODES, CONF_ADJUSTABLE, CONF_CHANGE_RATE, + CONF_CLICKABLE, CONF_END_ANGLE, CONF_INDICATOR, CONF_KNOB, CONF_MAIN, CONF_START_ANGLE, - LV_OBJ_FLAG, LV_PART, ) from ..lv_validation import ( @@ -85,7 +85,7 @@ class ArcType(NumberType): if CONF_ADJUSTABLE in config: if not config[CONF_ADJUSTABLE]: lv_obj.remove_style(w.obj, nullptr, LV_PART.KNOB) - w.clear_flag(LV_OBJ_FLAG.CLICKABLE) + w.set_flag(CONF_CLICKABLE, False) elif CONF_GROUP not in config: # For some reason arc does not get automatically added to the default group lv.group_add_obj(lv_expr.group_get_default(), w.obj) diff --git a/esphome/components/lvgl/widgets/canvas.py b/esphome/components/lvgl/widgets/canvas.py index 2137acb3f6..9dd5e84419 100644 --- a/esphome/components/lvgl/widgets/canvas.py +++ b/esphome/components/lvgl/widgets/canvas.py @@ -99,7 +99,7 @@ class CanvasType(WidgetType): if config[CONF_TRANSPARENT]: color_format = "LV_COLOR_FORMAT_ARGB8888" else: - color_format = "LV_COLOR_FORMAT_NATIVE" + color_format = "LV_COLOR_FORMAT_RGB565" # LVGL 9.4: LV_CANVAS_BUF_SIZE(width, height, bits_per_pixel, stride) # stride is 0 for default (width * bytes_per_pixel) diff --git a/esphome/components/lvgl/widgets/lv_list.py b/esphome/components/lvgl/widgets/lv_list.py index f41cb8e4e9..98e1838f0f 100644 --- a/esphome/components/lvgl/widgets/lv_list.py +++ b/esphome/components/lvgl/widgets/lv_list.py @@ -20,6 +20,7 @@ from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from ..automation import action_to_code from ..defines import ( CONF_ALIGN_TO, + CONF_CLICKABLE, CONF_MAIN, CONF_PAD_ROW, CONF_SCROLLBAR, @@ -441,7 +442,7 @@ async def _wire_dynamic_triggers(w: Widget, config: dict) -> None: for event, conf in { event: conf for event, conf in config.items() if event in LV_EVENT_TRIGGERS }.items(): - w.add_flag("LV_OBJ_FLAG_CLICKABLE") + w.set_flag(CONF_CLICKABLE, True) await add_trigger( conf[0], event_target, event, attach_obj=w.obj, user_data=user_data ) diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index fedc79383d..a49d58dc4d 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -38,7 +38,6 @@ from ..defines import ( CONF_SRC, CONF_START_VALUE, CONF_TICKS, - LV_OBJ_FLAG, LV_PART, LV_SCALE_MODE, add_lv_use, @@ -457,7 +456,7 @@ class MeterType(WidgetType): LV_EVENT.DRAW_TASK_ADDED, nullptr, ) - lv.obj_add_flag(scale_var, LV_OBJ_FLAG.SEND_DRAW_TASK_EVENTS) + lv_obj.set_send_draw_task_events(scale_var, True) if t == CONF_LINE: # Needle represented by a line diff --git a/esphome/components/lvgl/widgets/msgbox.py b/esphome/components/lvgl/widgets/msgbox.py index 29087009cb..44d16bf614 100644 --- a/esphome/components/lvgl/widgets/msgbox.py +++ b/esphome/components/lvgl/widgets/msgbox.py @@ -1,5 +1,5 @@ from esphome import codegen as cg, config_validation as cv -from esphome.const import CONF_BUTTON, CONF_ID, CONF_TEXT +from esphome.const import CONF_BUTTON, CONF_HIDDEN, CONF_ID, CONF_TEXT from esphome.core import ID from esphome.cpp_generator import MockObjClass from esphome.cpp_types import nullptr @@ -14,7 +14,6 @@ from ..defines import ( CONF_MSGBOXES, CONF_SRC, CONF_TITLE, - LV_OBJ_FLAG, TYPE_FLEX, add_lv_use, add_warning, @@ -141,7 +140,7 @@ async def msgbox_to_code(top_layer, conf): percent100 = await pixels_or_percent.process(1.0) lv_obj.set_size(outer, percent100, percent100) outer_widget.add_style(await OUTER_STYLE.get_var()) - outer_widget.add_flag(LV_OBJ_FLAG.HIDDEN) + outer_widget.set_flag(CONF_HIDDEN, True) lv.msgbox_add_title(msgbox, title) lv.msgbox_add_text(msgbox, text) lv_obj.set_style_align(msgbox, literal("LV_ALIGN_CENTER"), 0) @@ -153,7 +152,7 @@ async def msgbox_to_code(top_layer, conf): await widget_to_code(button, header_button_spec, msgbox) async with LambdaContext(EVENT_ARG, where=messagebox_id) as close_action: - outer_widget.add_flag(LV_OBJ_FLAG.HIDDEN) + outer_widget.set_flag(CONF_HIDDEN, True) if close_button: with LocalVariable( "close_btn_", lv_obj_t, lv_expr.msgbox_add_close_button(msgbox) diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 8ac76cd41f..670145cfe0 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -107,7 +107,7 @@ dependencies: rules: - if: "$ESPHOME_ARDUINO_COMPONENT == 0" lvgl/lvgl: - version: 9.5.0 + version: 9.6.0 fastled/FastLED: git: https://github.com/FastLED/FastLED.git version: d44c800a9e876a8394caefc2ce4915dd96dac77b diff --git a/platformio.ini b/platformio.ini index 7534b748d1..7b3414a32a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -108,7 +108,7 @@ build_unflags = lib_deps = esphome/dlms_parser@2.2.0 ; dlms_meter bblanchon/ArduinoJson@7.4.3 ; json - lvgl/lvgl@9.5.0 ; lvgl + lvgl/lvgl@9.6.0 ; lvgl ; This are common settings for the ESP8266 using Arduino. [common:esp8266-arduino] @@ -646,7 +646,7 @@ extends = common platform = platformio/native lib_deps = esphome/noise-c@0.1.31 ; used by noise (api, ota) - lvgl/lvgl@9.5.0 ; lvgl + lvgl/lvgl@9.6.0 ; lvgl build_flags = ${common.build_flags} -DUSE_HOST diff --git a/tests/component_tests/lvgl/config/lv_conf_check_second.yaml b/tests/component_tests/lvgl/config/lv_conf_check_second.yaml new file mode 100644 index 0000000000..a5ccd0e68d --- /dev/null +++ b/tests/component_tests/lvgl/config/lv_conf_check_second.yaml @@ -0,0 +1,41 @@ +esphome: + name: test-lv-conf-check-second + +esp32: + board: lolin_c3_mini + +spi: + mosi_pin: + number: GPIO2 + ignore_strapping_warning: true + clk_pin: GPIO1 + +display: + - platform: mipi_spi + id: display_a + data_rate: 20MHz + model: st7735 + cs_pin: + number: GPIO8 + ignore_strapping_warning: true + dc_pin: + number: GPIO3 + - platform: mipi_spi + id: display_b + data_rate: 20MHz + model: st7735 + cs_pin: GPIO4 + dc_pin: GPIO5 + +lvgl: + - id: lvgl_a + displays: display_a + widgets: + - label: + text: Hello + - id: lvgl_b + displays: display_b + check_args: true + widgets: + - label: + text: Hello diff --git a/tests/component_tests/lvgl/config/lv_conf_check_warn.yaml b/tests/component_tests/lvgl/config/lv_conf_check_warn.yaml new file mode 100644 index 0000000000..769032f839 --- /dev/null +++ b/tests/component_tests/lvgl/config/lv_conf_check_warn.yaml @@ -0,0 +1,27 @@ +esphome: + name: test-lv-conf-check-warn + +esp32: + board: lolin_c3_mini + +spi: + mosi_pin: + number: GPIO2 + ignore_strapping_warning: true + clk_pin: GPIO1 + +display: + - platform: mipi_spi + data_rate: 20MHz + model: st7735 + cs_pin: + number: GPIO8 + ignore_strapping_warning: true + dc_pin: + number: GPIO3 + +lvgl: + check_args: true + widgets: + - label: + text: Hello diff --git a/tests/component_tests/lvgl/config/lv_conf_defaults.yaml b/tests/component_tests/lvgl/config/lv_conf_defaults.yaml new file mode 100644 index 0000000000..94950a3b43 --- /dev/null +++ b/tests/component_tests/lvgl/config/lv_conf_defaults.yaml @@ -0,0 +1,26 @@ +esphome: + name: test-lv-conf-defaults + +esp32: + board: lolin_c3_mini + +spi: + mosi_pin: + number: GPIO2 + ignore_strapping_warning: true + clk_pin: GPIO1 + +display: + - platform: mipi_spi + data_rate: 20MHz + model: st7735 + cs_pin: + number: GPIO8 + ignore_strapping_warning: true + dc_pin: + number: GPIO3 + +lvgl: + widgets: + - label: + text: Hello diff --git a/tests/component_tests/lvgl/config/lv_conf_options.yaml b/tests/component_tests/lvgl/config/lv_conf_options.yaml new file mode 100644 index 0000000000..969091c086 --- /dev/null +++ b/tests/component_tests/lvgl/config/lv_conf_options.yaml @@ -0,0 +1,29 @@ +esphome: + name: test-lv-conf-options + +esp32: + board: lolin_c3_mini + +spi: + mosi_pin: + number: GPIO2 + ignore_strapping_warning: true + clk_pin: GPIO1 + +display: + - platform: mipi_spi + data_rate: 20MHz + model: st7735 + byte_order: little_endian + cs_pin: + number: GPIO8 + ignore_strapping_warning: true + dc_pin: + number: GPIO3 + +lvgl: + check_args: true + log_level: DEBUG + widgets: + - label: + text: Hello diff --git a/tests/component_tests/lvgl/test_list.py b/tests/component_tests/lvgl/test_list.py index 368a3a4990..e93d858ad5 100644 --- a/tests/component_tests/lvgl/test_list.py +++ b/tests/component_tests/lvgl/test_list.py @@ -287,10 +287,7 @@ def test_add_text_with_index_moves_before_firing_on_add(main_cpp: str) -> None: def test_add_button_with_checkable_flag(main_cpp: str) -> None: assert "lv_obj_t *dyn_button_VAR_ = lv_button_create(test_list);" in main_cpp - assert ( - "lv_obj_add_flag(dyn_button_VAR_, (lv_obj_flag_t)(LV_OBJ_FLAG_CHECKABLE));" - in main_cpp - ) + assert "lv_obj_set_checkable(dyn_button_VAR_, true);" in main_cpp assert ( 'lv_label_set_text(lv_obj_get_child(dyn_button_VAR_, 0), "Entry");' in main_cpp ) diff --git a/tests/component_tests/lvgl/test_lv_conf.py b/tests/component_tests/lvgl/test_lv_conf.py new file mode 100644 index 0000000000..8b93c01166 --- /dev/null +++ b/tests/component_tests/lvgl/test_lv_conf.py @@ -0,0 +1,80 @@ +"""Tests for the LVGL configuration defines written to lv_conf.h.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + +import pytest + +from esphome.components.lvgl import defines as df +from esphome.components.lvgl.schemas import WIDGET_TYPES + +# LVGL 9.6 stops the build with #error when an enabled option "selects" another +# option that is disabled. ESPHome sets every unused LV_USE_* to 0, so each widget +# must list these in get_uses(). +LVGL_SELECTS = ( + ("SLIDER", "BAR"), + ("BARCODE", "CANVAS"), + ("QRCODE", "CANVAS"), + ("CANVAS", "IMAGE"), + ("DROPDOWN", "LABEL"), + ("MSGBOX", "LABEL"), + ("ROLLER", "LABEL"), + ("TEXTAREA", "LABEL"), +) + + +def test_default_lv_conf_defines( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + generate_main(component_config_path("lv_conf_defaults.yaml")) + defines = df.get_defines() + assert defines["LV_COLOR_FORMAT_DEFAULT"] == "LV_COLOR_FORMAT_RGB565_SWAPPED" + assert defines["LV_DRAW_SW_SUPPORT_RGB565_SWAPPED"] == "1" + assert defines["LV_OBJ_STYLE_CACHE"] == "1" + assert defines["LV_USE_CHECK_ARG"] == "0" + assert "LV_CHECK_ARG_LOG_MODE" not in defines + # Both are deprecated in LVGL 9.6 and produce build warnings + assert "LV_COLOR_DEPTH" not in defines + assert "LV_COLOR_16_SWAP" not in defines + + +@pytest.mark.parametrize( + ("yaml_file", "color_format", "log_mode"), + [ + ("lv_conf_options.yaml", "RGB565", "VERBOSE"), + ("lv_conf_check_warn.yaml", "RGB565_SWAPPED", "MINIMAL"), + # Only the second LVGL instance sets check_args + ("lv_conf_check_second.yaml", "RGB565_SWAPPED", "MINIMAL"), + ], +) +def test_lv_conf_options( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + yaml_file: str, + color_format: str, + log_mode: str, +) -> None: + generate_main(component_config_path(yaml_file)) + defines = df.get_defines() + assert defines["LV_COLOR_FORMAT_DEFAULT"] == f"LV_COLOR_FORMAT_{color_format}" + assert defines["LV_USE_CHECK_ARG"] == "1" + assert defines["LV_CHECK_ARG_LOG_MODE"] == f"LV_CHECK_ARG_LOG_MODE_{log_mode}" + + +def test_check_arg_log_modes_cover_all_log_levels() -> None: + assert df.LV_CHECK_ARG_LOG_MODES.keys() == df.LV_LOG_LEVELS.keys() + + +@pytest.mark.parametrize("widget_name", sorted(WIDGET_TYPES)) +def test_widget_uses_satisfy_lvgl_selects(widget_name: str) -> None: + widget_type = WIDGET_TYPES[widget_name] + df.add_lv_use(widget_type.name, *widget_type.get_uses()) + enabled = {use.upper() for use in df.get_lv_uses()} + for option, required in LVGL_SELECTS: + if option in enabled: + assert required in enabled, ( + f"'{widget_name}' enables LV_USE_{option}, which needs LV_USE_{required}" + ) diff --git a/tests/component_tests/lvgl/test_multi_conf_validate.py b/tests/component_tests/lvgl/test_multi_conf_validate.py index c44b40317e..9ec73ca618 100644 --- a/tests/component_tests/lvgl/test_multi_conf_validate.py +++ b/tests/component_tests/lvgl/test_multi_conf_validate.py @@ -10,11 +10,15 @@ from esphome.config_validation import Invalid def _config( - displays: list[str], theme: dict | None = None, debug_outline: bool = False + displays: list[str], + theme: dict | None = None, + debug_outline: bool = False, + check_args: bool = False, ) -> dict: config = { df.CONF_DISPLAYS: displays, "log_level": "WARN", + df.CONF_CHECK_ARGS: check_args, "color_depth": 16, "byte_order": "big_endian", df.CONF_TRANSPARENCY_KEY: 0x000400, @@ -73,3 +77,8 @@ class TestDebugOutlineOnMultipleInstances: _config(["disp_b"], debug_outline=True), ] multi_conf_validate(configs) + + +def test_check_args_may_differ_between_instances() -> None: + configs = [_config(["disp_a"], check_args=True), _config(["disp_b"])] + multi_conf_validate(configs) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 18a7fd0913..681eb41b5b 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -53,6 +53,7 @@ lvgl: id: lvgl_id rotation: 90 log_level: debug + check_args: true resume_on_input: true paused: true update_when_display_idle: true @@ -976,6 +977,16 @@ lvgl: - spinner: align: right_mid send_draw_task_events: true + event_trickle: true + state_trickle: true + radio_button: true + flex_in_new_track: !lambda return true; + - spinner: + align: top_mid + layout_1: true + layout_2: true + widget_1: !lambda return false; + widget_2: false - image: id: lv_image src: cat_image diff --git a/tests/components/lvgl/test-idf6.esp32-p4-idf.yaml b/tests/components/lvgl/test-idf6.esp32-p4-idf.yaml new file mode 100644 index 0000000000..e0332caaed --- /dev/null +++ b/tests/components/lvgl/test-idf6.esp32-p4-idf.yaml @@ -0,0 +1,22 @@ +# LVGL only uses the PPA to draw on the ESP32-P4 with ESP-IDF 6.0 or later +esp32: + framework: + version: 6.0.1 + +display: + - platform: mipi_dsi + model: WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-3.4C + +lvgl: + widgets: + - obj: + bg_color: 0x2040C0 + widgets: + - label: + text: PPA + +psram: + +esp_ldo: + - channel: 3 + voltage: 2.5V diff --git a/tests/components/lvgl/test.esp32-p4-idf.yaml b/tests/components/lvgl/test.esp32-p4-idf.yaml index 5fd9370255..a7a8d13ffc 100644 --- a/tests/components/lvgl/test.esp32-p4-idf.yaml +++ b/tests/components/lvgl/test.esp32-p4-idf.yaml @@ -3,6 +3,7 @@ display: model: WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-3.4C lvgl: byte_order: little_endian + check_args: true rotation: 90 psram: diff --git a/tests/components/lvgl/test.host.yaml b/tests/components/lvgl/test.host.yaml index 22d051da8e..1df27d82f7 100644 --- a/tests/components/lvgl/test.host.yaml +++ b/tests/components/lvgl/test.host.yaml @@ -1,5 +1,5 @@ display: - - platform: sdl + - platform: snapshot id: sdl0 auto_clear_enabled: false dimensions: @@ -11,11 +11,6 @@ display: width: 480 height: 480 -touchscreen: - - platform: sdl - display: sdl0 - sdl_id: sdl0 - lvgl: - id: lvgl_0 default_font: space16 diff --git a/tests/integration/test_lvgl_headless_render.py b/tests/integration/test_lvgl_headless_render.py index 1c60e49604..5a668a2302 100644 --- a/tests/integration/test_lvgl_headless_render.py +++ b/tests/integration/test_lvgl_headless_render.py @@ -30,7 +30,7 @@ EXPECTED_SHA256 = "a995b002dd1d183c47514da15ab9a60a3e7d788c2e24386a02fddd4865509 # Bundled LVGL version (esphome/components/lvgl/__init__.py, LVGL_VERSION) the hash above was # generated against. A version bump can shift anti-aliasing enough to change the hash even though # nothing is actually wrong -- if this test fails, check that first before regenerating the hash. -EXPECTED_LVGL_VERSION = "9.5.0" +EXPECTED_LVGL_VERSION = "9.6.0" @pytest.mark.asyncio