[lvgl] Migrate to 9.6.0 (#20257)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Clyde Stubbs
2026-10-12 07:36:32 +11:00
committed by GitHub
co-authored by Claude
parent 153c51e39f
commit bea0ef264a
28 changed files with 413 additions and 101 deletions
+29 -11
View File
@@ -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
),
+3 -2
View File
@@ -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)
@@ -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(
+24 -3
View File
@@ -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_<flag>() setter has a different name
FLAG_SETTER_ALIASES = {"layout_1": "flex_in_new_track"}
# Custom flags that have no lv_obj_set_<flag>() 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 = (
+33 -2
View File
@@ -6,6 +6,7 @@
#include "core/lv_global.h"
#include "core/lv_obj_class_private.h"
#include "core/lv_obj_private.h"
#include <numeric>
@@ -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<uint8_t *>(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);
}
+3
View File
@@ -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) {
-1
View File
@@ -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(
+4 -2
View File
@@ -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}"
)
+53 -53
View File
@@ -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):
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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)
+2 -1
View File
@@ -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
)
+1 -2
View File
@@ -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
+3 -4
View File
@@ -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)
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+1 -4
View File
@@ -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
)
@@ -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}"
)
@@ -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)
+11
View File
@@ -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
@@ -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
@@ -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:
+1 -6
View File
@@ -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
@@ -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