mirror of
https://github.com/esphome/esphome.git
synced 2024-11-21 22:48:10 +01:00
[lvgl] PR stage 3 (#7160)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
parent
8849443bf6
commit
3920029aff
18 changed files with 895 additions and 98 deletions
|
@ -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()
|
||||
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))
|
||||
|
|
188
esphome/components/lvgl/automation.py
Normal file
188
esphome/components/lvgl/automation.py
Normal 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)
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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) {
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
62
esphome/components/lvgl/rotary_encoders.py
Normal file
62
esphome/components/lvgl/rotary_encoders.py
Normal 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())
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
{
|
||||
|
|
61
esphome/components/lvgl/trigger.py
Normal file
61
esphome/components/lvgl/trigger.py
Normal 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)))
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue