[lvgl] PR stage 3 (#7160)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
Clyde Stubbs 2024-07-31 14:31:15 +10:00 committed by GitHub
parent 8849443bf6
commit 3920029aff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 895 additions and 98 deletions

View file

@ -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))

View file

@ -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)

View file

@ -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()

View file

@ -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"

View file

@ -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
)

View file

@ -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]

View file

@ -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<lv_coord_t>(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

View file

@ -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 <lvgl.h>
#include <utility>
#include <vector>
#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<void(float)>;
using event_callback_t = void(_lv_event_t *);
using text_lambda_t = std::function<const char *()>;
template<typename... Ts> class ObjUpdateAction : public Action<Ts...> {
public:
explicit ObjUpdateAction(std::function<void(Ts...)> &&lamb) : lamb_(std::move(lamb)) {}
void play(Ts... x) override { this->lamb_(x...); }
protected:
std::function<void(Ts...)> 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<void(uint32_t)> &&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<void(lv_disp_t *)> &lamb) { this->init_lambdas_.push_back(lamb); }
void add_init_lambda(const std::function<void(LvglComponent *)> &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<display::Display *> 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<std::function<void(lv_disp_t *)>> init_lambdas_;
std::vector<std::function<void(LvglComponent *lv_component)>> init_lambdas_;
CallbackManager<void(uint32_t)> idle_callbacks_{};
size_t buffer_frac_{1};
bool full_refresh_{};
};
class IdleTrigger : public Trigger<> {
public:
explicit IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> 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<uint32_t> timeout_;
bool is_idle_{};
};
template<typename... Ts> class LvglAction : public Action<Ts...>, public Parented<LvglComponent> {
public:
explicit LvglAction(std::function<void(LvglComponent *)> &&lamb) : action_(std::move(lamb)) {}
void play(Ts... x) override { this->action_(this->parent_); }
protected:
std::function<void(LvglComponent *)> action_{};
};
template<typename... Ts> class LvglCondition : public Condition<Ts...>, public Parented<LvglComponent> {
public:
LvglCondition(std::function<bool(LvglComponent *)> &&condition_lambda)
: condition_lambda_(std::move(condition_lambda)) {}
bool check(Ts... x) override { return this->condition_lambda_(this->parent_); }
protected:
std::function<bool(LvglComponent *)> condition_lambda_{};
};
#ifdef USE_LVGL_TOUCHSCREEN
class LVTouchListener : public touchscreen::TouchListener, public Parented<LvglComponent> {
public:
@ -160,7 +248,62 @@ class LVTouchListener : public touchscreen::TouchListener, public Parented<LvglC
bool touch_pressed_{};
};
#endif // USE_LVGL_TOUCHSCREEN
#ifdef USE_LVGL_KEY_LISTENER
class LVEncoderListener : public Parented<LvglComponent> {
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<LVEncoderListener *>(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

View file

@ -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)

View file

@ -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())

View file

@ -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,
}

View file

@ -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(
{

View file

@ -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)))

View file

@ -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):

View file

@ -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
)

View file

@ -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

View file

@ -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"

View file

@ -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