From 3920029aff9dd85fd5f0775945b8426c03da471c Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 31 Jul 2024 14:31:15 +1000 Subject: [PATCH] [lvgl] PR stage 3 (#7160) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/lvgl/__init__.py | 58 +++++-- esphome/components/lvgl/automation.py | 188 +++++++++++++++++++++ esphome/components/lvgl/btn.py | 6 +- esphome/components/lvgl/defines.py | 30 +++- esphome/components/lvgl/lv_validation.py | 46 ++--- esphome/components/lvgl/lvcode.py | 64 ++++--- esphome/components/lvgl/lvgl_esphome.cpp | 75 +++++++- esphome/components/lvgl/lvgl_esphome.h | 165 ++++++++++++++++-- esphome/components/lvgl/obj.py | 13 +- esphome/components/lvgl/rotary_encoders.py | 62 +++++++ esphome/components/lvgl/schemas.py | 80 ++++++++- esphome/components/lvgl/touchscreens.py | 5 +- esphome/components/lvgl/trigger.py | 61 +++++++ esphome/components/lvgl/types.py | 23 ++- esphome/components/lvgl/widget.py | 58 +++++-- esphome/core/defines.h | 3 + tests/components/lvgl/lvgl-package.yaml | 35 ++++ tests/components/lvgl/test.esp32-idf.yaml | 21 +++ 18 files changed, 895 insertions(+), 98 deletions(-) create mode 100644 esphome/components/lvgl/automation.py create mode 100644 esphome/components/lvgl/rotary_encoders.py create mode 100644 esphome/components/lvgl/trigger.py diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index c454a61957..182d04e038 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -1,5 +1,6 @@ import logging +from esphome.automation import build_automation, register_action, validate_automation import esphome.codegen as cg from esphome.components.display import Display import esphome.config_validation as cv @@ -8,7 +9,11 @@ from esphome.const import ( CONF_BUFFER_SIZE, CONF_ID, CONF_LAMBDA, + CONF_ON_IDLE, CONF_PAGES, + CONF_TIMEOUT, + CONF_TRIGGER_ID, + CONF_TYPE, ) from esphome.core import CORE, ID, Lambda from esphome.cpp_generator import MockObj @@ -16,21 +21,26 @@ from esphome.final_validate import full_config from esphome.helpers import write_file_if_changed from . import defines as df, helpers, lv_validation as lvalid +from .automation import update_to_code from .btn import btn_spec from .label import label_spec -from .lvcode import ConstantLiteral, LvContext +from .lv_validation import lv_images_used +from .lvcode import LvContext from .obj import obj_spec -from .schemas import any_widget_schema, obj_schema +from .rotary_encoders import ROTARY_ENCODER_CONFIG, rotary_encoders_to_code +from .schemas import any_widget_schema, create_modify_schema, obj_schema from .touchscreens import touchscreen_schema, touchscreens_to_code +from .trigger import generate_triggers from .types import ( WIDGET_TYPES, FontEngine, + IdleTrigger, LvglComponent, - lv_disp_t_ptr, + ObjUpdateAction, lv_font_t, lvgl_ns, ) -from .widget import LvScrActType, Widget, add_widgets, set_obj_properties +from .widget import Widget, add_widgets, lv_scr_act, set_obj_properties DOMAIN = "lvgl" DEPENDENCIES = ("display",) @@ -41,17 +51,21 @@ LOGGER = logging.getLogger(__name__) for w_type in (label_spec, obj_spec, btn_spec): WIDGET_TYPES[w_type.name] = w_type -lv_scr_act_spec = LvScrActType() -lv_scr_act = Widget.create( - None, ConstantLiteral("lv_scr_act()"), lv_scr_act_spec, {}, parent=None -) - WIDGET_SCHEMA = any_widget_schema() +for w_type in WIDGET_TYPES.values(): + register_action( + f"lvgl.{w_type.name}.update", + ObjUpdateAction, + create_modify_schema(w_type), + )(update_to_code) + async def add_init_lambda(lv_component, init): if init: - lamb = await cg.process_lambda(Lambda(init), [(lv_disp_t_ptr, "lv_disp")]) + lamb = await cg.process_lambda( + Lambda(init), [(LvglComponent.operator("ptr"), "lv_component")] + ) cg.add(lv_component.add_init_lambda(lamb)) @@ -99,6 +113,13 @@ def final_validation(config): buffer_frac = config[CONF_BUFFER_SIZE] if CORE.is_esp32 and buffer_frac > 0.5 and "psram" not in global_config: LOGGER.warning("buffer_size: may need to be reduced without PSRAM") + for image_id in lv_images_used: + path = global_config.get_path_for_id(image_id)[:-1] + image_conf = global_config.get_config_for_path(path) + if image_conf[CONF_TYPE] in ("RGBA", "RGB24"): + raise cv.Invalid( + "Using RGBA or RGB24 in image config not compatible with LVGL", path + ) async def to_code(config): @@ -174,9 +195,15 @@ async def to_code(config): with LvContext(): await touchscreens_to_code(lv_component, config) + await rotary_encoders_to_code(lv_component, config) await set_obj_properties(lv_scr_act, config) await add_widgets(lv_scr_act, config) - Widget.set_completed() + Widget.set_completed() + await generate_triggers(lv_component) + for conf in config.get(CONF_ON_IDLE, ()): + templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32) + idle_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, templ) + await build_automation(idle_trigger, [], conf) await add_init_lambda(lv_component, LvContext.get_code()) for comp in helpers.lvgl_components_required: CORE.add_define(f"USE_LVGL_{comp.upper()}") @@ -212,9 +239,18 @@ CONFIG_SCHEMA = ( cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of( "big_endian", "little_endian" ), + cv.Optional(CONF_ON_IDLE): validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger), + cv.Required(CONF_TIMEOUT): cv.templatable( + cv.positive_time_period_milliseconds + ), + } + ), cv.Optional(df.CONF_WIDGETS): cv.ensure_list(WIDGET_SCHEMA), cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color, cv.GenerateID(df.CONF_TOUCHSCREENS): touchscreen_schema, + cv.GenerateID(df.CONF_ROTARY_ENCODERS): ROTARY_ENCODER_CONFIG, } ) ).add_extra(cv.has_at_least_one_key(CONF_PAGES, df.CONF_WIDGETS)) diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py new file mode 100644 index 0000000000..4fd0be185e --- /dev/null +++ b/esphome/components/lvgl/automation.py @@ -0,0 +1,188 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_TIMEOUT +from esphome.core import Lambda +from esphome.cpp_generator import RawStatement +from esphome.cpp_types import nullptr + +from .defines import CONF_LVGL_ID, CONF_SHOW_SNOW, literal +from .lv_validation import lv_bool +from .lvcode import ( + LambdaContext, + ReturnStatement, + add_line_marks, + lv, + lv_add, + lv_obj, + lvgl_comp, +) +from .schemas import ACTION_SCHEMA, LVGL_SCHEMA +from .types import ( + LvglAction, + LvglComponent, + LvglComponentPtr, + LvglCondition, + ObjUpdateAction, + lv_obj_t, +) +from .widget import Widget, get_widget, lv_scr_act, set_obj_properties + + +async def action_to_code(action: list, action_id, widget: Widget, template_arg, args): + with LambdaContext() as context: + lv.cond_if(widget.obj == nullptr) + lv_add(RawStatement(" return;")) + lv.cond_endif() + code = context.get_code() + code.extend(action) + action = "\n".join(code) + "\n\n" + lamb = await cg.process_lambda(Lambda(action), args) + var = cg.new_Pvariable(action_id, template_arg, lamb) + return var + + +async def update_to_code(config, action_id, template_arg, args): + if config is not None: + widget = await get_widget(config) + with LambdaContext() as context: + add_line_marks(action_id) + await set_obj_properties(widget, config) + await widget.type.to_code(widget, config) + if ( + widget.type.w_type.value_property is not None + and widget.type.w_type.value_property in config + ): + lv.event_send(widget.obj, literal("LV_EVENT_VALUE_CHANGED"), nullptr) + return await action_to_code( + context.get_code(), action_id, widget, template_arg, args + ) + + +@automation.register_condition( + "lvgl.is_paused", + LvglCondition, + LVGL_SCHEMA, +) +async def lvgl_is_paused(config, condition_id, template_arg, args): + lvgl = config[CONF_LVGL_ID] + with LambdaContext( + [(LvglComponentPtr, "lvgl_comp")], return_type=cg.bool_ + ) as context: + lv_add(ReturnStatement(lvgl_comp.is_paused())) + var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, lvgl) + return var + + +@automation.register_condition( + "lvgl.is_idle", + LvglCondition, + LVGL_SCHEMA.extend( + { + cv.Required(CONF_TIMEOUT): cv.templatable( + cv.positive_time_period_milliseconds + ) + } + ), +) +async def lvgl_is_idle(config, condition_id, template_arg, args): + lvgl = config[CONF_LVGL_ID] + timeout = await cg.templatable(config[CONF_TIMEOUT], [], cg.uint32) + with LambdaContext( + [(LvglComponentPtr, "lvgl_comp")], return_type=cg.bool_ + ) as context: + lv_add(ReturnStatement(lvgl_comp.is_idle(timeout))) + var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, lvgl) + return var + + +@automation.register_action( + "lvgl.widget.redraw", + ObjUpdateAction, + cv.Schema( + { + cv.Optional(CONF_ID): cv.use_id(lv_obj_t), + cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent), + } + ), +) +async def obj_invalidate_to_code(config, action_id, template_arg, args): + if CONF_ID in config: + w = await get_widget(config) + else: + w = lv_scr_act + with LambdaContext() as context: + add_line_marks(action_id) + lv_obj.invalidate(w.obj) + return await action_to_code(context.get_code(), action_id, w, template_arg, args) + + +@automation.register_action( + "lvgl.pause", + LvglAction, + { + cv.GenerateID(): cv.use_id(LvglComponent), + cv.Optional(CONF_SHOW_SNOW, default=False): lv_bool, + }, +) +async def pause_action_to_code(config, action_id, template_arg, args): + with LambdaContext([(LvglComponentPtr, "lvgl_comp")]) as context: + add_line_marks(action_id) + lv_add(lvgl_comp.set_paused(True, config[CONF_SHOW_SNOW])) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "lvgl.resume", + LvglAction, + { + cv.GenerateID(): cv.use_id(LvglComponent), + }, +) +async def resume_action_to_code(config, action_id, template_arg, args): + with LambdaContext([(LvglComponentPtr, "lvgl_comp")]) as context: + add_line_marks(action_id) + lv_add(lvgl_comp.set_paused(False, False)) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action("lvgl.widget.disable", ObjUpdateAction, ACTION_SCHEMA) +async def obj_disable_to_code(config, action_id, template_arg, args): + w = await get_widget(config) + with LambdaContext() as context: + add_line_marks(action_id) + w.add_state("LV_STATE_DISABLED") + return await action_to_code(context.get_code(), action_id, w, template_arg, args) + + +@automation.register_action("lvgl.widget.enable", ObjUpdateAction, ACTION_SCHEMA) +async def obj_enable_to_code(config, action_id, template_arg, args): + w = await get_widget(config) + with LambdaContext() as context: + add_line_marks(action_id) + w.clear_state("LV_STATE_DISABLED") + return await action_to_code(context.get_code(), action_id, w, template_arg, args) + + +@automation.register_action("lvgl.widget.hide", ObjUpdateAction, ACTION_SCHEMA) +async def obj_hide_to_code(config, action_id, template_arg, args): + w = await get_widget(config) + with LambdaContext() as context: + add_line_marks(action_id) + w.add_flag("LV_OBJ_FLAG_HIDDEN") + return await action_to_code(context.get_code(), action_id, w, template_arg, args) + + +@automation.register_action("lvgl.widget.show", ObjUpdateAction, ACTION_SCHEMA) +async def obj_show_to_code(config, action_id, template_arg, args): + w = await get_widget(config) + with LambdaContext() as context: + add_line_marks(action_id) + w.clear_flag("LV_OBJ_FLAG_HIDDEN") + return await action_to_code(context.get_code(), action_id, w, template_arg, args) diff --git a/esphome/components/lvgl/btn.py b/esphome/components/lvgl/btn.py index 4f5f88d9e6..064d886d47 100644 --- a/esphome/components/lvgl/btn.py +++ b/esphome/components/lvgl/btn.py @@ -9,9 +9,6 @@ class BtnType(WidgetType): def __init__(self): super().__init__(CONF_BUTTON, LvBoolean("lv_btn_t"), (CONF_MAIN,)) - async def to_code(self, w, config): - return [] - def obj_creator(self, parent: MockObjClass, config: dict): """ LVGL 8 calls buttons `btn` @@ -21,5 +18,8 @@ class BtnType(WidgetType): def get_uses(self): return ("btn",) + async def to_code(self, w, config): + return [] + btn_spec = BtnType() diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index a2b4ac13fb..9f349e3943 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -4,12 +4,32 @@ Constants already defined in esphome.const are not duplicated here and must be i """ +from typing import Union + from esphome import codegen as cg, config_validation as cv from esphome.core import ID, Lambda +from esphome.cpp_generator import Literal from esphome.cpp_types import uint32 from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor -from .lvcode import ConstantLiteral +from .helpers import requires_component + + +class ConstantLiteral(Literal): + __slots__ = ("constant",) + + def __init__(self, constant: str): + super().__init__() + self.constant = constant + + def __str__(self): + return self.constant + + +def literal(arg: Union[str, ConstantLiteral]): + if isinstance(arg, str): + return ConstantLiteral(arg) + return arg class LValidator: @@ -18,14 +38,19 @@ class LValidator: has `process()` to convert a value during code generation """ - def __init__(self, validator, rtype, idtype=None, idexpr=None, retmapper=None): + def __init__( + self, validator, rtype, idtype=None, idexpr=None, retmapper=None, requires=None + ): self.validator = validator self.rtype = rtype self.idtype = idtype self.idexpr = idexpr self.retmapper = retmapper + self.requires = requires def __call__(self, value): + if self.requires: + value = requires_component(self.requires)(value) if isinstance(value, cv.Lambda): return cv.returning_lambda(value) if self.idtype is not None and isinstance(value, ID): @@ -422,6 +447,7 @@ CONF_RECOLOR = "recolor" CONF_RIGHT_BUTTON = "right_button" CONF_ROLLOVER = "rollover" CONF_ROOT_BACK_BTN = "root_back_btn" +CONF_ROTARY_ENCODERS = "rotary_encoders" CONF_ROWS = "rows" CONF_SCALES = "scales" CONF_SCALE_LINES = "scale_lines" diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 533dc582f0..818bde6aed 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components.binary_sensor import BinarySensor from esphome.components.color import ColorStruct from esphome.components.font import Font +from esphome.components.image import Image_ from esphome.components.sensor import Sensor from esphome.components.text_sensor import TextSensor import esphome.config_validation as cv @@ -13,22 +14,15 @@ from esphome.helpers import cpp_string_escape from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from . import types as ty -from .defines import LV_FONTS, LValidator, LvConstant +from .defines import LV_FONTS, ConstantLiteral, LValidator, LvConstant, literal from .helpers import ( esphome_fonts_used, lv_fonts_used, lvgl_components_required, requires_component, ) -from .lvcode import ConstantLiteral, lv_expr -from .types import lv_font_t - - -def literal_mapper(value, args=()): - if isinstance(value, str): - return ConstantLiteral(value) - return value - +from .lvcode import lv_expr +from .types import lv_font_t, lv_img_t opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER") @@ -43,7 +37,7 @@ def opacity_validator(value): return value -opacity = LValidator(opacity_validator, uint32, retmapper=literal_mapper) +opacity = LValidator(opacity_validator, uint32, retmapper=literal) @schema_extractor("one_of") @@ -79,9 +73,7 @@ def pixels_or_percent_validator(value): return f"lv_pct({int(cv.percentage(value) * 100)})" -pixels_or_percent = LValidator( - pixels_or_percent_validator, uint32, retmapper=literal_mapper -) +pixels_or_percent = LValidator(pixels_or_percent_validator, uint32, retmapper=literal) def zoom(value): @@ -115,7 +107,7 @@ def size_validator(value): return f"lv_pct({int(cv.percentage(value) * 100)})" -size = LValidator(size_validator, uint32, retmapper=literal_mapper) +size = LValidator(size_validator, uint32, retmapper=literal) radius_consts = LvConstant("LV_RADIUS_", "CIRCLE") @@ -130,21 +122,37 @@ def radius_validator(value): return value +radius = LValidator(radius_validator, uint32, retmapper=literal) + + def id_name(value): if value == SCHEMA_EXTRACT: return "id" return cv.validate_id_name(value) -radius = LValidator(radius_validator, uint32, retmapper=literal_mapper) - - def stop_value(value): return cv.int_range(0, 255)(value) +lv_images_used = set() + + +def image_validator(value): + value = requires_component("image")(value) + value = cv.use_id(Image_)(value) + lv_images_used.add(value) + return value + + +lv_image = LValidator( + image_validator, + lv_img_t, + retmapper=lambda x: lv_expr.img_from(MockObj(x)), + requires="image", +) lv_bool = LValidator( - cv.boolean, cg.bool_, BinarySensor, "get_state()", retmapper=literal_mapper + cv.boolean, cg.bool_, BinarySensor, "get_state()", retmapper=literal ) diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index 13b4862b4d..3a8a958f2e 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -8,8 +8,8 @@ from esphome.cpp_generator import ( AssignmentExpression, CallExpression, Expression, + ExpressionStatement, LambdaExpression, - Literal, MockObj, RawExpression, RawStatement, @@ -19,7 +19,9 @@ from esphome.cpp_generator import ( statement, ) +from .defines import ConstantLiteral from .helpers import get_line_marks +from .types import lv_group_t _LOGGER = logging.getLogger(__name__) @@ -105,29 +107,40 @@ class LambdaContext(CodeContext): def __init__( self, - parameters: list[tuple[SafeExpType, str]], - return_type: SafeExpType = None, + parameters: list[tuple[SafeExpType, str]] = None, + return_type: SafeExpType = cg.void, + capture: str = "", ): super().__init__() self.code_list: list[Statement] = [] self.parameters = parameters self.return_type = return_type + self.capture = capture def add(self, expression: Union[Expression, Statement]): self.code_list.append(expression) return expression - async def code(self) -> LambdaExpression: + async def get_lambda(self) -> LambdaExpression: + code_text = self.get_code() + return await cg.process_lambda( + Lambda("\n".join(code_text) + "\n\n"), + self.parameters, + capture=self.capture, + return_type=self.return_type, + ) + + def get_code(self): code_text = [] for exp in self.code_list: text = str(statement(exp)) text = text.rstrip() code_text.append(text) - return await cg.process_lambda( - Lambda("\n".join(code_text) + "\n\n"), - self.parameters, - return_type=self.return_type, - ) + return code_text + + def __enter__(self): + super().__enter__() + return self class LocalVariable(MockObj): @@ -187,13 +200,18 @@ class MockLv: return result def cond_if(self, expression: Expression): - CodeContext.append(RawExpression(f"if({expression}) {{")) + CodeContext.append(RawStatement(f"if {expression} {{")) def cond_else(self): - CodeContext.append(RawExpression("} else {")) + CodeContext.append(RawStatement("} else {")) def cond_endif(self): - CodeContext.append(RawExpression("}")) + CodeContext.append(RawStatement("}")) + + +class ReturnStatement(ExpressionStatement): + def __str__(self): + return f"return {self.expression};" class LvExpr(MockLv): @@ -210,6 +228,7 @@ lv = MockLv("lv_") lv_expr = LvExpr("lv_") # Mock for lv_obj_ calls lv_obj = MockLv("lv_obj_") +lvgl_comp = MockObj("lvgl_comp", "->") # equivalent to cg.add() for the lvgl init context @@ -226,12 +245,19 @@ def lv_assign(target, expression): lv_add(RawExpression(f"{target} = {expression}")) -class ConstantLiteral(Literal): - __slots__ = ("constant",) +lv_groups = {} # Widget group names - def __init__(self, constant: str): - super().__init__() - self.constant = constant - def __str__(self): - return self.constant +def add_group(name): + if name is None: + return None + fullname = f"lv_esp_group_{name}" + if name not in lv_groups: + gid = ID(fullname, True, type=lv_group_t.operator("ptr")) + lv_add( + AssignmentExpression( + type_=gid.type, modifier="", name=fullname, rhs=lv_expr.group_create() + ) + ) + lv_groups[name] = ConstantLiteral(fullname) + return lv_groups[name] diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 74a1b0e7af..34f8eaf21f 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -19,13 +19,35 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, const uint8_t *ptr) { } void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { - auto now = millis(); - this->draw_buffer_(area, (const uint8_t *) color_p); - ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area), - lv_area_get_height(area), (int) (millis() - now)); + if (!this->paused_) { + auto now = millis(); + this->draw_buffer_(area, (const uint8_t *) color_p); + ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area), + lv_area_get_height(area), (int) (millis() - now)); + } lv_disp_flush_ready(disp_drv); } +void LvglComponent::write_random_() { + // length of 2 lines in 32 bit units + // we write 2 lines for the benefit of displays that won't write one line at a time. + size_t line_len = this->disp_drv_.hor_res * LV_COLOR_DEPTH / 8 / 4 * 2; + for (size_t i = 0; i != line_len; i++) { + ((uint32_t *) (this->draw_buf_.buf1))[i] = random_uint32(); + } + lv_area_t area; + area.x1 = 0; + area.x2 = this->disp_drv_.hor_res - 1; + if (this->snow_line_ == this->disp_drv_.ver_res / 2) { + area.y1 = static_cast(random_uint32() % (this->disp_drv_.ver_res / 2) * 2); + } else { + area.y1 = this->snow_line_++ * 2; + } + // write 2 lines + area.y2 = area.y1 + 1; + this->draw_buffer_(&area, (const uint8_t *) this->draw_buf_.buf1); +} + void LvglComponent::setup() { ESP_LOGCONFIG(TAG, "LVGL Setup starts"); #if LV_USE_LOG @@ -74,10 +96,53 @@ void LvglComponent::setup() { ESP_LOGV(TAG, "sw_rotate = %d, rotated=%d", this->disp_drv_.sw_rotate, this->disp_drv_.rotated); this->disp_ = lv_disp_drv_register(&this->disp_drv_); for (const auto &v : this->init_lambdas_) - v(this->disp_); + v(this); lv_disp_trig_activity(this->disp_); ESP_LOGCONFIG(TAG, "LVGL Setup complete"); } + +#ifdef USE_LVGL_IMAGE +lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc) { + if (img_dsc == nullptr) + img_dsc = new lv_img_dsc_t(); // NOLINT + img_dsc->header.always_zero = 0; + img_dsc->header.reserved = 0; + img_dsc->header.w = src->get_width(); + img_dsc->header.h = src->get_height(); + img_dsc->data = src->get_data_start(); + img_dsc->data_size = image_type_to_width_stride(img_dsc->header.w * img_dsc->header.h, src->get_type()); + switch (src->get_type()) { + case image::IMAGE_TYPE_BINARY: + img_dsc->header.cf = LV_IMG_CF_ALPHA_1BIT; + break; + + case image::IMAGE_TYPE_GRAYSCALE: + img_dsc->header.cf = LV_IMG_CF_ALPHA_8BIT; + break; + + case image::IMAGE_TYPE_RGB24: + img_dsc->header.cf = LV_IMG_CF_RGB888; + break; + + case image::IMAGE_TYPE_RGB565: +#if LV_COLOR_DEPTH == 16 + img_dsc->header.cf = src->has_transparency() ? LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED : LV_IMG_CF_TRUE_COLOR; +#else + img_dsc->header.cf = LV_IMG_CF_RGB565; +#endif + break; + + case image::IMAGE_TYPE_RGBA: +#if LV_COLOR_DEPTH == 32 + img_dsc->header.cf = LV_IMG_CF_TRUE_COLOR; +#else + img_dsc->header.cf = LV_IMG_CF_RGBA8888; +#endif + break; + } + return img_dsc; +} +#endif } // namespace lvgl } // namespace esphome diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index a884a27042..a0d3d226ce 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -1,23 +1,32 @@ #pragma once #include "esphome/core/defines.h" -#ifdef USE_LVGL + +#ifdef USE_LVGL_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif // USE_LVGL_BINARY_SENSOR +#ifdef USE_LVGL_ROTARY_ENCODER +#include "esphome/components/rotary_encoder/rotary_encoder.h" +#endif // USE_LVGL_ROTARY_ENCODER // required for clang-tidy #ifndef LV_CONF_H #define LV_CONF_SKIP 1 // NOLINT -#endif +#endif // LV_CONF_H #include "esphome/components/display/display.h" #include "esphome/components/display/display_color_utils.h" #include "esphome/core/component.h" -#include "esphome/core/hal.h" #include "esphome/core/log.h" #include +#include #include +#ifdef USE_LVGL_IMAGE +#include "esphome/components/image/image.h" +#endif // USE_LVGL_IMAGE #ifdef USE_LVGL_FONT #include "esphome/components/font/font.h" -#endif +#endif // USE_LVGL_FONT #ifdef USE_LVGL_TOUCHSCREEN #include "esphome/components/touchscreen/touchscreen.h" #endif // USE_LVGL_TOUCHSCREEN @@ -40,7 +49,7 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT // Parent class for things that wrap an LVGL object class LvCompound final { public: - virtual void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; } + void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; } lv_obj_t *obj{}; }; @@ -49,6 +58,15 @@ using set_value_lambda_t = std::function; using event_callback_t = void(_lv_event_t *); using text_lambda_t = std::function; +template class ObjUpdateAction : public Action { + public: + explicit ObjUpdateAction(std::function &&lamb) : lamb_(std::move(lamb)) {} + + void play(Ts... x) override { this->lamb_(x...); } + + protected: + std::function lamb_; +}; #ifdef USE_LVGL_FONT class FontEngine { public: @@ -67,6 +85,9 @@ class FontEngine { lv_font_t lv_font_{}; }; #endif // USE_LVGL_FONT +#ifdef USE_LVGL_IMAGE +lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc = nullptr); +#endif // USE_LVGL_IMAGE class LvglComponent : public PollingComponent { constexpr static const char *const TAG = "lvgl"; @@ -92,27 +113,54 @@ class LvglComponent : public PollingComponent { area->y2++; } - void loop() override { lv_timer_handler_run_in_period(5); } void setup() override; - void update() override {} + void update() override { + // update indicators + if (this->paused_) { + return; + } + this->idle_callbacks_.call(lv_disp_get_inactive_time(this->disp_)); + } + void loop() override { + if (this->paused_) { + if (this->show_snow_) + this->write_random_(); + } + lv_timer_handler_run_in_period(5); + } + + void add_on_idle_callback(std::function &&callback) { + this->idle_callbacks_.add(std::move(callback)); + } void add_display(display::Display *display) { this->displays_.push_back(display); } - void add_init_lambda(const std::function &lamb) { this->init_lambdas_.push_back(lamb); } + void add_init_lambda(const std::function &lamb) { this->init_lambdas_.push_back(lamb); } void dump_config() override; void set_full_refresh(bool full_refresh) { this->full_refresh_ = full_refresh; } + bool is_idle(uint32_t idle_ms) { return lv_disp_get_inactive_time(this->disp_) > idle_ms; } void set_buffer_frac(size_t frac) { this->buffer_frac_ = frac; } lv_disp_t *get_disp() { return this->disp_; } void set_paused(bool paused, bool show_snow) { this->paused_ = paused; + this->show_snow_ = show_snow; + this->snow_line_ = 0; if (!paused && lv_scr_act() != nullptr) { lv_disp_trig_activity(this->disp_); // resets the inactivity time lv_obj_invalidate(lv_scr_act()); } } + + void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) { + lv_obj_add_event_cb(obj, callback, event, this); + if (event == LV_EVENT_VALUE_CHANGED) { + lv_obj_add_event_cb(obj, callback, lv_custom_event, this); + } + } bool is_paused() const { return this->paused_; } protected: + void write_random_(); void draw_buffer_(const lv_area_t *area, const uint8_t *ptr); void flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p); std::vector displays_{}; @@ -120,12 +168,52 @@ class LvglComponent : public PollingComponent { lv_disp_drv_t disp_drv_{}; lv_disp_t *disp_{}; bool paused_{}; + bool show_snow_{}; + lv_coord_t snow_line_{}; - std::vector> init_lambdas_; + std::vector> init_lambdas_; + CallbackManager idle_callbacks_{}; size_t buffer_frac_{1}; bool full_refresh_{}; }; +class IdleTrigger : public Trigger<> { + public: + explicit IdleTrigger(LvglComponent *parent, TemplatableValue timeout) : timeout_(std::move(timeout)) { + parent->add_on_idle_callback([this](uint32_t idle_time) { + if (!this->is_idle_ && idle_time > this->timeout_.value()) { + this->is_idle_ = true; + this->trigger(); + } else if (this->is_idle_ && idle_time < this->timeout_.value()) { + this->is_idle_ = false; + } + }); + } + + protected: + TemplatableValue timeout_; + bool is_idle_{}; +}; + +template class LvglAction : public Action, public Parented { + public: + explicit LvglAction(std::function &&lamb) : action_(std::move(lamb)) {} + void play(Ts... x) override { this->action_(this->parent_); } + + protected: + std::function action_{}; +}; + +template class LvglCondition : public Condition, public Parented { + public: + LvglCondition(std::function &&condition_lambda) + : condition_lambda_(std::move(condition_lambda)) {} + bool check(Ts... x) override { return this->condition_lambda_(this->parent_); } + + protected: + std::function condition_lambda_{}; +}; + #ifdef USE_LVGL_TOUCHSCREEN class LVTouchListener : public touchscreen::TouchListener, public Parented { public: @@ -160,7 +248,62 @@ class LVTouchListener : public touchscreen::TouchListener, public Parented { + public: + LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt) { + lv_indev_drv_init(&this->drv_); + this->drv_.type = type; + this->drv_.user_data = this; + this->drv_.long_press_time = lpt; + this->drv_.long_press_repeat_time = lprt; + this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) { + auto *l = static_cast(d->user_data); + data->state = l->pressed_ ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; + data->key = l->key_; + data->enc_diff = (int16_t) (l->count_ - l->last_count_); + l->last_count_ = l->count_; + data->continue_reading = false; + }; + } + + void set_left_button(binary_sensor::BinarySensor *left_button) { + left_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_LEFT, state); }); + } + void set_right_button(binary_sensor::BinarySensor *right_button) { + right_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_RIGHT, state); }); + } + + void set_enter_button(binary_sensor::BinarySensor *enter_button) { + enter_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_ENTER, state); }); + } + + void set_sensor(rotary_encoder::RotaryEncoderSensor *sensor) { + sensor->register_listener([this](int32_t count) { this->set_count(count); }); + } + + void event(int key, bool pressed) { + if (!this->parent_->is_paused()) { + this->pressed_ = pressed; + this->key_ = key; + } + } + + void set_count(int32_t count) { + if (!this->parent_->is_paused()) + this->count_ = count; + } + + lv_indev_drv_t *get_drv() { return &this->drv_; } + + protected: + lv_indev_drv_t drv_{}; + bool pressed_{}; + int32_t count_{}; + int32_t last_count_{}; + int key_{}; +}; +#endif // USE_LVGL_KEY_LISTENER } // namespace lvgl } // namespace esphome - -#endif // USE_LVGL diff --git a/esphome/components/lvgl/obj.py b/esphome/components/lvgl/obj.py index 92c4f63d2d..40d7e55381 100644 --- a/esphome/components/lvgl/obj.py +++ b/esphome/components/lvgl/obj.py @@ -1,5 +1,9 @@ +from esphome import automation + +from .automation import update_to_code from .defines import CONF_MAIN, CONF_OBJ -from .types import WidgetType, lv_obj_t +from .schemas import create_modify_schema +from .types import ObjUpdateAction, WidgetType, lv_obj_t class ObjType(WidgetType): @@ -15,3 +19,10 @@ class ObjType(WidgetType): obj_spec = ObjType() + + +@automation.register_action( + "lvgl.widget.update", ObjUpdateAction, create_modify_schema(obj_spec) +) +async def obj_update_to_code(config, action_id, template_arg, args): + return await update_to_code(config, action_id, template_arg, args) diff --git a/esphome/components/lvgl/rotary_encoders.py b/esphome/components/lvgl/rotary_encoders.py new file mode 100644 index 0000000000..77dc397c3e --- /dev/null +++ b/esphome/components/lvgl/rotary_encoders.py @@ -0,0 +1,62 @@ +import esphome.codegen as cg +from esphome.components.binary_sensor import BinarySensor +from esphome.components.rotary_encoder.sensor import RotaryEncoderSensor +import esphome.config_validation as cv +from esphome.const import CONF_GROUP, CONF_ID, CONF_SENSOR + +from .defines import ( + CONF_ENTER_BUTTON, + CONF_LEFT_BUTTON, + CONF_LONG_PRESS_REPEAT_TIME, + CONF_LONG_PRESS_TIME, + CONF_RIGHT_BUTTON, + CONF_ROTARY_ENCODERS, +) +from .helpers import lvgl_components_required +from .lvcode import add_group, lv, lv_add, lv_expr +from .schemas import ENCODER_SCHEMA +from .types import lv_indev_type_t + +ROTARY_ENCODER_CONFIG = cv.ensure_list( + ENCODER_SCHEMA.extend( + { + cv.Required(CONF_ENTER_BUTTON): cv.use_id(BinarySensor), + cv.Required(CONF_SENSOR): cv.Any( + cv.use_id(RotaryEncoderSensor), + cv.Schema( + { + cv.Required(CONF_LEFT_BUTTON): cv.use_id(BinarySensor), + cv.Required(CONF_RIGHT_BUTTON): cv.use_id(BinarySensor), + } + ), + ), + } + ) +) + + +async def rotary_encoders_to_code(var, config): + for enc_conf in config.get(CONF_ROTARY_ENCODERS, ()): + lvgl_components_required.add("KEY_LISTENER") + lvgl_components_required.add("ROTARY_ENCODER") + lpt = enc_conf[CONF_LONG_PRESS_TIME].total_milliseconds + lprt = enc_conf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds + listener = cg.new_Pvariable( + enc_conf[CONF_ID], lv_indev_type_t.LV_INDEV_TYPE_ENCODER, lpt, lprt + ) + await cg.register_parented(listener, var) + if sensor_config := enc_conf.get(CONF_SENSOR): + if isinstance(sensor_config, dict): + b_sensor = await cg.get_variable(sensor_config[CONF_LEFT_BUTTON]) + cg.add(listener.set_left_button(b_sensor)) + b_sensor = await cg.get_variable(sensor_config[CONF_RIGHT_BUTTON]) + cg.add(listener.set_right_button(b_sensor)) + else: + sensor_config = await cg.get_variable(sensor_config) + lv_add(listener.set_sensor(sensor_config)) + b_sensor = await cg.get_variable(enc_conf[CONF_ENTER_BUTTON]) + cg.add(listener.set_enter_button(b_sensor)) + if group := add_group(enc_conf.get(CONF_GROUP)): + lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group) + else: + lv.indev_drv_register(listener.get_drv()) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 9f6d984545..ebef56a882 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -1,10 +1,21 @@ from esphome import config_validation as cv -from esphome.const import CONF_ARGS, CONF_FORMAT, CONF_ID, CONF_STATE, CONF_TYPE +from esphome.automation import Trigger, validate_automation +from esphome.const import ( + CONF_ARGS, + CONF_FORMAT, + CONF_GROUP, + CONF_ID, + CONF_ON_VALUE, + CONF_STATE, + CONF_TRIGGER_ID, + CONF_TYPE, +) +from esphome.core import TimePeriod from esphome.schema_extractors import SCHEMA_EXTRACT from . import defines as df, lv_validation as lvalid, types as ty from .helpers import add_lv_use, requires_component, validate_printf -from .lv_validation import lv_font +from .lv_validation import id_name, lv_font from .types import WIDGET_TYPES, WidgetType # A schema for text properties @@ -27,6 +38,28 @@ TEXT_SCHEMA = cv.Schema( } ) +ACTION_SCHEMA = cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(ty.lv_pseudo_button_t), + }, + key=CONF_ID, +) + +PRESS_TIME = cv.All( + lvalid.lv_milliseconds, cv.Range(max=TimePeriod(milliseconds=65535)) +) + +ENCODER_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.All( + cv.declare_id(ty.LVEncoderListener), requires_component("binary_sensor") + ), + cv.Optional(CONF_GROUP): lvalid.id_name, + cv.Optional(df.CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME, + cv.Optional(df.CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME, + } +) + # All LVGL styles and their validators STYLE_PROPS = { "align": df.CHILD_ALIGNMENTS.one_of, @@ -43,6 +76,7 @@ STYLE_PROPS = { "bg_image_opa": lvalid.opacity, "bg_image_recolor": lvalid.lv_color, "bg_image_recolor_opa": lvalid.opacity, + "bg_image_src": lvalid.lv_image, "bg_main_stop": lvalid.stop_value, "bg_opa": lvalid.opacity, "border_color": lvalid.lv_color, @@ -151,6 +185,39 @@ def part_schema(widget_type: WidgetType): ) +def automation_schema(typ: ty.LvType): + if typ.has_on_value: + events = df.LV_EVENT_TRIGGERS + (CONF_ON_VALUE,) + else: + events = df.LV_EVENT_TRIGGERS + if isinstance(typ, ty.LvType): + template = Trigger.template(typ.get_arg_type()) + else: + template = Trigger.template() + return { + cv.Optional(event): validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(template), + } + ) + for event in events + } + + +def create_modify_schema(widget_type): + return ( + part_schema(widget_type) + .extend( + { + cv.Required(CONF_ID): cv.use_id(widget_type), + cv.Optional(CONF_STATE): SET_STATE_SCHEMA, + } + ) + .extend(FLAG_SCHEMA) + .extend(widget_type.modify_schema) + ) + + def obj_schema(widget_type: WidgetType): """ Create a schema for a widget type itself i.e. no allowance for children @@ -161,10 +228,12 @@ def obj_schema(widget_type: WidgetType): part_schema(widget_type) .extend(FLAG_SCHEMA) .extend(ALIGN_TO_SCHEMA) + .extend(automation_schema(widget_type.w_type)) .extend( cv.Schema( { cv.Optional(CONF_STATE): SET_STATE_SCHEMA, + cv.Optional(CONF_GROUP): id_name, } ) ) @@ -188,6 +257,13 @@ STYLED_TEXT_SCHEMA = cv.maybe_simple_value( STYLE_SCHEMA.extend(TEXT_SCHEMA), key=df.CONF_TEXT ) +# For use by platform components +LVGL_SCHEMA = cv.Schema( + { + cv.GenerateID(df.CONF_LVGL_ID): cv.use_id(ty.LvglComponent), + } +) + ALL_STYLES = { **STYLE_PROPS, } diff --git a/esphome/components/lvgl/touchscreens.py b/esphome/components/lvgl/touchscreens.py index a0d4a3e4ad..499b33aa02 100644 --- a/esphome/components/lvgl/touchscreens.py +++ b/esphome/components/lvgl/touchscreens.py @@ -2,7 +2,7 @@ import esphome.codegen as cg from esphome.components.touchscreen import CONF_TOUCHSCREEN_ID, Touchscreen import esphome.config_validation as cv from esphome.const import CONF_ID -from esphome.core import CORE, TimePeriod +from esphome.core import CORE from .defines import ( CONF_LONG_PRESS_REPEAT_TIME, @@ -10,11 +10,10 @@ from .defines import ( CONF_TOUCHSCREENS, ) from .helpers import lvgl_components_required -from .lv_validation import lv_milliseconds from .lvcode import lv +from .schemas import PRESS_TIME from .types import LVTouchListener -PRESS_TIME = cv.All(lv_milliseconds, cv.Range(max=TimePeriod(milliseconds=65535))) CONF_TOUCHSCREEN = "touchscreen" TOUCHSCREENS_CONFIG = cv.maybe_simple_value( { diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py new file mode 100644 index 0000000000..bf92bda5b0 --- /dev/null +++ b/esphome/components/lvgl/trigger.py @@ -0,0 +1,61 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.const import CONF_ID, CONF_ON_VALUE, CONF_TRIGGER_ID + +from .defines import ( + CONF_ALIGN, + CONF_ALIGN_TO, + CONF_X, + CONF_Y, + LV_EVENT, + LV_EVENT_TRIGGERS, + literal, +) +from .lvcode import LambdaContext, add_line_marks, lv, lv_add +from .widget import widget_map + +lv_event_t_ptr = cg.global_ns.namespace("lv_event_t").operator("ptr") + + +async def generate_triggers(lv_component): + """ + Generate LVGL triggers for all defined widgets + Must be done after all widgets completed + :param lv_component: The parent component + :return: + """ + + for w in widget_map.values(): + if w.config: + for event, conf in { + event: conf + for event, conf in w.config.items() + if event in LV_EVENT_TRIGGERS + }.items(): + conf = conf[0] + w.add_flag("LV_OBJ_FLAG_CLICKABLE") + event = "LV_EVENT_" + LV_EVENT[event[3:].upper()] + await add_trigger(conf, event, lv_component, w) + for conf in w.config.get(CONF_ON_VALUE, ()): + await add_trigger(conf, "LV_EVENT_VALUE_CHANGED", lv_component, w) + + # Generate align to directives while we're here + if align_to := w.config.get(CONF_ALIGN_TO): + target = widget_map[align_to[CONF_ID]].obj + align = align_to[CONF_ALIGN] + x = align_to[CONF_X] + y = align_to[CONF_Y] + lv.obj_align_to(w.obj, target, align, x, y) + + +async def add_trigger(conf, event, lv_component, w): + tid = conf[CONF_TRIGGER_ID] + add_line_marks(tid) + trigger = cg.new_Pvariable(tid) + args = w.get_args() + value = w.get_value() + await automation.build_automation(trigger, args, conf) + with LambdaContext([(lv_event_t_ptr, "event_data")]) as context: + add_line_marks(tid) + lv_add(trigger.trigger(value)) + lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), literal(event))) diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 60291ea54a..6997207dac 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -1,4 +1,4 @@ -from esphome import codegen as cg +from esphome import automation, codegen as cg from esphome.core import ID from esphome.cpp_generator import MockObjClass @@ -23,8 +23,14 @@ lvgl_ns = cg.esphome_ns.namespace("lvgl") char_ptr = cg.global_ns.namespace("char").operator("ptr") void_ptr = cg.void.operator("ptr") LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent) +LvglComponentPtr = LvglComponent.operator("ptr") lv_event_code_t = cg.global_ns.namespace("lv_event_code_t") +lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t") FontEngine = lvgl_ns.class_("FontEngine") +IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template()) +ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action) +LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition) +LvglAction = lvgl_ns.class_("LvglAction", automation.Action) LvCompound = lvgl_ns.class_("LvCompound") lv_font_t = cg.global_ns.class_("lv_font_t") lv_style_t = cg.global_ns.struct("lv_style_t") @@ -33,9 +39,11 @@ lv_obj_base_t = cg.global_ns.class_("lv_obj_t", lv_pseudo_button_t) lv_obj_t_ptr = lv_obj_base_t.operator("ptr") lv_disp_t_ptr = cg.global_ns.struct("lv_disp_t").operator("ptr") lv_color_t = cg.global_ns.struct("lv_color_t") +lv_group_t = cg.global_ns.struct("lv_group_t") LVTouchListener = lvgl_ns.class_("LVTouchListener") LVEncoderListener = lvgl_ns.class_("LVEncoderListener") lv_obj_t = LvType("lv_obj_t") +lv_img_t = LvType("lv_img_t") # this will be populated later, in __init__.py to avoid circular imports. @@ -58,7 +66,7 @@ class LvBoolean(LvType): super().__init__( *args, largs=[(cg.bool_, "x")], - lvalue=lambda w: w.is_checked(), + lvalue=lambda w: w.has_state("LV_STATE_CHECKED"), has_on_value=True, **kwargs, ) @@ -83,11 +91,14 @@ class WidgetType: self.name = name self.w_type = w_type self.parts = parts - self.schema = schema or {} - if modify_schema is None: - self.modify_schema = schema + if schema is None: + self.schema = {} else: - self.modify_schema = modify_schema + self.schema = schema + if modify_schema is None: + self.modify_schema = self.schema + else: + self.modify_schema = self.schema @property def animated(self): diff --git a/esphome/components/lvgl/widget.py b/esphome/components/lvgl/widget.py index 4755d8b21d..83aed341e7 100644 --- a/esphome/components/lvgl/widget.py +++ b/esphome/components/lvgl/widget.py @@ -4,9 +4,9 @@ from typing import Any from esphome import codegen as cg, config_validation as cv from esphome.config_validation import Invalid from esphome.const import CONF_GROUP, CONF_ID, CONF_STATE -from esphome.core import ID, TimePeriod +from esphome.core import CORE, TimePeriod from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import MockObjClass +from esphome.cpp_generator import MockObj, MockObjClass, VariableDeclarationExpression from .defines import ( CONF_DEFAULT, @@ -16,13 +16,15 @@ from .defines import ( OBJ_FLAGS, PARTS, STATES, + ConstantLiteral, LValidator, join_enums, + literal, ) from .helpers import add_lv_use -from .lvcode import ConstantLiteral, add_line_marks, lv, lv_add, lv_assign, lv_obj +from .lvcode import add_group, add_line_marks, lv, lv_add, lv_assign, lv_expr, lv_obj from .schemas import ALL_STYLES, STYLE_REMAP -from .types import WIDGET_TYPES, WidgetType, lv_obj_t +from .types import WIDGET_TYPES, LvType, WidgetType, lv_obj_t, lv_obj_t_ptr EVENT_LAMB = "event_lamb__" @@ -76,17 +78,20 @@ class Widget: return f"{self.var}->obj" return self.var - def add_state(self, *args): - return lv_obj.add_state(self.obj, *args) + def add_state(self, state): + return lv_obj.add_state(self.obj, literal(state)) - def clear_state(self, *args): - return lv_obj.clear_state(self.obj, *args) + def clear_state(self, state): + return lv_obj.clear_state(self.obj, literal(state)) - def add_flag(self, *args): - return lv_obj.add_flag(self.obj, *args) + def has_state(self, state): + return lv_expr.obj_get_state(self.obj) & literal(state) != 0 - def clear_flag(self, *args): - return lv_obj.clear_flag(self.obj, *args) + def add_flag(self, flag): + return lv_obj.add_flag(self.obj, literal(flag)) + + def clear_flag(self, flag): + return lv_obj.clear_flag(self.obj, literal(flag)) def set_property(self, prop, value, animated: bool = None, ltype=None): if isinstance(value, dict): @@ -125,6 +130,16 @@ class Widget: def __str__(self): return f"({self.var}, {self.type})" + def get_args(self): + if isinstance(self.type.w_type, LvType): + return self.type.w_type.args + return [(lv_obj_t_ptr, "obj")] + + def get_value(self): + if isinstance(self.type.w_type, LvType): + return self.type.w_type.value(self) + return self.obj + # Map of widgets to their config, used for trigger generation widget_map: dict[Any, Widget] = {} @@ -146,7 +161,8 @@ def get_widget_generator(wid): yield -async def get_widget(wid: ID) -> Widget: +async def get_widget(config: dict, id: str = CONF_ID) -> Widget: + wid = config[id] if obj := widget_map.get(wid): return obj return await FakeAwaitable(get_widget_generator(wid)) @@ -204,9 +220,10 @@ async def set_obj_properties(w: Widget, config): }.items(): if isinstance(ALL_STYLES[prop], LValidator): value = await ALL_STYLES[prop].process(value) - # Remapping for backwards compatibility of style names prop_r = STYLE_REMAP.get(prop, prop) w.set_style(prop_r, value, lv_state) + if group := add_group(config.get(CONF_GROUP)): + lv.group_add_obj(group, w.obj) flag_clr = set() flag_set = set() props = parts[CONF_MAIN][CONF_DEFAULT] @@ -241,7 +258,7 @@ async def set_obj_properties(w: Widget, config): w.clear_state(clears) for key, value in lambs.items(): lamb = await cg.process_lambda(value, [], return_type=cg.bool_) - state = ConstantLiteral(f"LV_STATE_{key.upper}") + state = f"LV_STATE_{key.upper}" lv.cond_if(lamb) w.add_state(state) lv.cond_else() @@ -281,10 +298,19 @@ async def widget_to_code(w_cnfig, w_type, parent): var = cg.new_Pvariable(wid) lv_add(var.set_obj(creator)) else: - var = cg.Pvariable(wid, cg.nullptr, type_=lv_obj_t) + var = MockObj(wid, "->") + decl = VariableDeclarationExpression(lv_obj_t, "*", wid) + CORE.add_global(decl) + CORE.register_variable(wid, var) lv_assign(var, creator) widget = Widget.create(wid, var, spec, w_cnfig, parent) await set_obj_properties(widget, w_cnfig) await add_widgets(widget, w_cnfig) await spec.to_code(widget, w_cnfig) + + +lv_scr_act_spec = LvScrActType() +lv_scr_act = Widget.create( + None, ConstantLiteral("lv_scr_act()"), lv_scr_act_spec, {}, parent=None +) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 6ba5b64761..726db24592 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -39,9 +39,12 @@ #define USE_LOCK #define USE_LOGGER #define USE_LVGL +#define USE_LVGL_BINARY_SENSOR #define USE_LVGL_FONT #define USE_LVGL_IMAGE +#define USE_LVGL_KEY_LISTENER #define USE_LVGL_TOUCHSCREEN +#define USE_LVGL_ROTARY_ENCODER #define USE_MDNS #define USE_MEDIA_PLAYER #define USE_MQTT diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 696c749876..fde700e0bd 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -7,6 +7,7 @@ lvgl: long_press_time: 500ms widgets: - label: + id: hello_label text: Hello world text_color: 0xFF8000 align: center @@ -95,9 +96,43 @@ lvgl: height: 10% pressed: bg_color: light_blue + checkable: true + checked: + bg_color: 0x000000 widgets: - label: text: Button + on_click: + lvgl.label.update: + id: hello_label + bg_color: 0x123456 + text: clicked + on_value: + logger.log: + format: "state now %d" + args: [x] + on_short_click: + lvgl.widget.hide: hello_label + on_long_press: + lvgl.widget.show: hello_label + on_cancel: + lvgl.widget.enable: hello_label + on_ready: + lvgl.widget.disable: hello_label + on_defocus: + lvgl.widget.hide: hello_label + on_focus: + logger.log: Button clicked + on_scroll: + logger.log: Button clicked + on_scroll_end: + logger.log: Button clicked + on_scroll_begin: + logger.log: Button clicked + on_release: + logger.log: Button clicked + on_long_press_repeat: + logger.log: Button clicked font: - file: "gfonts://Roboto" diff --git a/tests/components/lvgl/test.esp32-idf.yaml b/tests/components/lvgl/test.esp32-idf.yaml index eab75b05f3..0f740db980 100644 --- a/tests/components/lvgl/test.esp32-idf.yaml +++ b/tests/components/lvgl/test.esp32-idf.yaml @@ -6,6 +6,23 @@ i2c: sda: GPIO18 scl: GPIO19 +sensor: + - platform: rotary_encoder + name: "Rotary Encoder" + id: encoder + pin_a: 2 + pin_b: 1 + internal: true + +binary_sensor: + - platform: gpio + id: pushbutton + name: Pushbutton + pin: + number: 0 + inverted: true + ignore_strapping_warning: true + display: - platform: ili9xxx model: st7789v @@ -50,5 +67,9 @@ lvgl: displays: - tft_display - second_display + rotary_encoders: + sensor: encoder + enter_button: pushbutton + group: general <<: !include common.yaml