[lvgl] Stage 4 (#7166)

This commit is contained in:
Clyde Stubbs 2024-08-05 15:07:05 +10:00 committed by GitHub
parent 87944f0c1b
commit d18bb34f87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 2002 additions and 579 deletions

View File

@ -15,44 +15,91 @@ from esphome.const import (
CONF_TRIGGER_ID,
CONF_TYPE,
)
from esphome.core import CORE, ID, Lambda
from esphome.core import CORE, ID
from esphome.cpp_generator import MockObj
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 .animimg import animimg_spec
from .arc import arc_spec
from .automation import disp_update, update_to_code
from .btn import btn_spec
from .checkbox import checkbox_spec
from .defines import CONF_SKIP
from .img import img_spec
from .label import label_spec
from .lv_validation import lv_images_used
from .lvcode import LvContext
from .led import led_spec
from .line import line_spec
from .lv_bar import bar_spec
from .lv_switch import switch_spec
from .lv_validation import lv_bool, lv_images_used
from .lvcode import LvContext, LvglComponent
from .obj import obj_spec
from .page import add_pages, page_spec
from .rotary_encoders import ROTARY_ENCODER_CONFIG, rotary_encoders_to_code
from .schemas import any_widget_schema, create_modify_schema, obj_schema
from .schemas import (
DISP_BG_SCHEMA,
FLEX_OBJ_SCHEMA,
GRID_CELL_SCHEMA,
LAYOUT_SCHEMAS,
STYLE_SCHEMA,
WIDGET_TYPES,
any_widget_schema,
container_schema,
create_modify_schema,
grid_alignments,
obj_schema,
)
from .slider import slider_spec
from .spinner import spinner_spec
from .styles import add_top_layer, styles_to_code, theme_to_code
from .touchscreens import touchscreen_schema, touchscreens_to_code
from .trigger import generate_triggers
from .types import (
WIDGET_TYPES,
FontEngine,
IdleTrigger,
LvglComponent,
ObjUpdateAction,
lv_font_t,
lv_style_t,
lvgl_ns,
)
from .widget import Widget, add_widgets, lv_scr_act, set_obj_properties
DOMAIN = "lvgl"
DEPENDENCIES = ("display",)
AUTO_LOAD = ("key_provider",)
CODEOWNERS = ("@clydebarrow",)
DEPENDENCIES = ["display"]
AUTO_LOAD = ["key_provider"]
CODEOWNERS = ["@clydebarrow"]
LOGGER = logging.getLogger(__name__)
for w_type in (label_spec, obj_spec, btn_spec):
for w_type in (
label_spec,
obj_spec,
btn_spec,
bar_spec,
slider_spec,
arc_spec,
line_spec,
spinner_spec,
led_spec,
animimg_spec,
checkbox_spec,
img_spec,
switch_spec,
):
WIDGET_TYPES[w_type.name] = w_type
WIDGET_SCHEMA = any_widget_schema()
LAYOUT_SCHEMAS[df.TYPE_GRID] = {
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(GRID_CELL_SCHEMA))
}
LAYOUT_SCHEMAS[df.TYPE_FLEX] = {
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(FLEX_OBJ_SCHEMA))
}
LAYOUT_SCHEMAS[df.TYPE_NONE] = {
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema())
}
for w_type in WIDGET_TYPES.values():
register_action(
f"lvgl.{w_type.name}.update",
@ -61,14 +108,6 @@ for w_type in WIDGET_TYPES.values():
)(update_to_code)
async def add_init_lambda(lv_component, init):
if init:
lamb = await cg.process_lambda(
Lambda(init), [(LvglComponent.operator("ptr"), "lv_component")]
)
cg.add(lv_component.add_init_lambda(lamb))
lv_defines = {} # Dict of #defines to provide as build flags
@ -100,6 +139,9 @@ def generate_lv_conf_h():
def final_validation(config):
if pages := config.get(CONF_PAGES):
if all(p[CONF_SKIP] for p in pages):
raise cv.Invalid("At least one page must not be skipped")
global_config = full_config.get()
for display_id in config[df.CONF_DISPLAYS]:
path = global_config.get_path_for_id(display_id)[:-1]
@ -193,18 +235,23 @@ async def to_code(config):
else:
add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font))
with LvContext():
async with LvContext(lv_component):
await touchscreens_to_code(lv_component, config)
await rotary_encoders_to_code(lv_component, config)
await theme_to_code(config)
await styles_to_code(config)
await set_obj_properties(lv_scr_act, config)
await add_widgets(lv_scr_act, config)
await add_pages(lv_component, config)
await add_top_layer(config)
await disp_update(f"{lv_component}->get_disp()", 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()}")
for use in helpers.lv_uses:
@ -239,6 +286,16 @@ CONFIG_SCHEMA = (
cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of(
"big_endian", "little_endian"
),
cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list(
cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)})
.extend(STYLE_SCHEMA)
.extend(
{
cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments,
cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments,
}
)
),
cv.Optional(CONF_ON_IDLE): validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger),
@ -247,10 +304,19 @@ CONFIG_SCHEMA = (
),
}
),
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(WIDGET_SCHEMA),
cv.Exclusive(df.CONF_WIDGETS, CONF_PAGES): cv.ensure_list(WIDGET_SCHEMA),
cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list(
container_schema(page_spec)
),
cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool,
cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec),
cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color,
cv.Optional(df.CONF_THEME): cv.Schema(
{cv.Optional(name): obj_schema(w) for name, w in WIDGET_TYPES.items()}
),
cv.GenerateID(df.CONF_TOUCHSCREENS): touchscreen_schema,
cv.GenerateID(df.CONF_ROTARY_ENCODERS): ROTARY_ENCODER_CONFIG,
}
)
.extend(DISP_BG_SCHEMA)
).add_extra(cv.has_at_least_one_key(CONF_PAGES, df.CONF_WIDGETS))

View File

@ -0,0 +1,117 @@
from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_DURATION, CONF_ID
from ...cpp_generator import MockObj
from .automation import action_to_code
from .defines import CONF_AUTO_START, CONF_MAIN, CONF_REPEAT_COUNT, CONF_SRC
from .helpers import lvgl_components_required
from .img import CONF_IMAGE
from .label import CONF_LABEL
from .lv_validation import lv_image, lv_milliseconds
from .lvcode import lv, lv_expr
from .types import LvType, ObjUpdateAction, void_ptr
from .widget import Widget, WidgetType, get_widgets
CONF_ANIMIMG = "animimg"
CONF_SRC_LIST_ID = "src_list_id"
def lv_repeat_count(value):
if isinstance(value, str) and value.lower() in ("forever", "infinite"):
value = 0xFFFF
return cv.int_range(min=0, max=0xFFFF)(value)
ANIMIMG_BASE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_REPEAT_COUNT, default="forever"): lv_repeat_count,
cv.Optional(CONF_AUTO_START, default=True): cv.boolean,
}
)
ANIMIMG_SCHEMA = ANIMIMG_BASE_SCHEMA.extend(
{
cv.Required(CONF_DURATION): lv_milliseconds,
cv.Required(CONF_SRC): cv.ensure_list(lv_image),
cv.GenerateID(CONF_SRC_LIST_ID): cv.declare_id(void_ptr),
}
)
ANIMIMG_MODIFY_SCHEMA = ANIMIMG_BASE_SCHEMA.extend(
{
cv.Optional(CONF_DURATION): lv_milliseconds,
}
)
lv_animimg_t = LvType("lv_animimg_t")
class AnimimgType(WidgetType):
def __init__(self):
super().__init__(
CONF_ANIMIMG,
lv_animimg_t,
(CONF_MAIN,),
ANIMIMG_SCHEMA,
ANIMIMG_MODIFY_SCHEMA,
)
async def to_code(self, w: Widget, config):
lvgl_components_required.add(CONF_IMAGE)
lvgl_components_required.add(CONF_ANIMIMG)
if CONF_SRC in config:
for x in config[CONF_SRC]:
await cg.get_variable(x)
srcs = [lv_expr.img_from(MockObj(x)) for x in config[CONF_SRC]]
src_id = cg.static_const_array(config[CONF_SRC_LIST_ID], srcs)
count = len(config[CONF_SRC])
lv.animimg_set_src(w.obj, src_id, count)
lv.animimg_set_repeat_count(w.obj, config[CONF_REPEAT_COUNT])
lv.animimg_set_duration(w.obj, config[CONF_DURATION])
if config.get(CONF_AUTO_START):
lv.animimg_start(w.obj)
def get_uses(self):
return CONF_IMAGE, CONF_LABEL
animimg_spec = AnimimgType()
@automation.register_action(
"lvgl.animimg.start",
ObjUpdateAction,
cv.maybe_simple_value(
{
cv.Required(CONF_ID): cv.use_id(lv_animimg_t),
},
key=CONF_ID,
),
)
async def animimg_start(config, action_id, template_arg, args):
widget = await get_widgets(config)
async def do_start(w: Widget):
lv.animimg_start(w.obj)
return await action_to_code(widget, do_start, action_id, template_arg, args)
@automation.register_action(
"lvgl.animimg.stop",
ObjUpdateAction,
cv.maybe_simple_value(
{
cv.Required(CONF_ID): cv.use_id(lv_animimg_t),
},
key=CONF_ID,
),
)
async def animimg_stop(config, action_id, template_arg, args):
widget = await get_widgets(config)
async def do_stop(w: Widget):
lv.animimg_stop(w.obj)
return await action_to_code(widget, do_stop, action_id, template_arg, args)

View File

@ -0,0 +1,78 @@
import esphome.config_validation as cv
from esphome.const import (
CONF_MAX_VALUE,
CONF_MIN_VALUE,
CONF_MODE,
CONF_ROTATION,
CONF_VALUE,
)
from esphome.cpp_types import nullptr
from .defines import (
ARC_MODES,
CONF_ADJUSTABLE,
CONF_CHANGE_RATE,
CONF_END_ANGLE,
CONF_INDICATOR,
CONF_KNOB,
CONF_MAIN,
CONF_START_ANGLE,
literal,
)
from .lv_validation import angle, get_start_value, lv_float
from .lvcode import lv, lv_obj
from .types import LvNumber, NumberType
from .widget import Widget
CONF_ARC = "arc"
ARC_SCHEMA = cv.Schema(
{
cv.Optional(CONF_VALUE): lv_float,
cv.Optional(CONF_MIN_VALUE, default=0): cv.int_,
cv.Optional(CONF_MAX_VALUE, default=100): cv.int_,
cv.Optional(CONF_START_ANGLE, default=135): angle,
cv.Optional(CONF_END_ANGLE, default=45): angle,
cv.Optional(CONF_ROTATION, default=0.0): angle,
cv.Optional(CONF_ADJUSTABLE, default=False): bool,
cv.Optional(CONF_MODE, default="NORMAL"): ARC_MODES.one_of,
cv.Optional(CONF_CHANGE_RATE, default=720): cv.uint16_t,
}
)
ARC_MODIFY_SCHEMA = cv.Schema(
{
cv.Optional(CONF_VALUE): lv_float,
}
)
class ArcType(NumberType):
def __init__(self):
super().__init__(
CONF_ARC,
LvNumber("lv_arc_t"),
parts=(CONF_MAIN, CONF_INDICATOR, CONF_KNOB),
schema=ARC_SCHEMA,
modify_schema=ARC_MODIFY_SCHEMA,
)
async def to_code(self, w: Widget, config):
if CONF_MIN_VALUE in config:
lv.arc_set_range(w.obj, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE])
lv.arc_set_bg_angles(
w.obj, config[CONF_START_ANGLE] // 10, config[CONF_END_ANGLE] // 10
)
lv.arc_set_rotation(w.obj, config[CONF_ROTATION] // 10)
lv.arc_set_mode(w.obj, literal(config[CONF_MODE]))
lv.arc_set_change_rate(w.obj, config[CONF_CHANGE_RATE])
if config.get(CONF_ADJUSTABLE) is False:
lv_obj.remove_style(w.obj, nullptr, literal("LV_PART_KNOB"))
w.clear_flag("LV_OBJ_FLAG_CLICKABLE")
value = await get_start_value(config)
if value is not None:
lv.arc_set_value(w.obj, value)
arc_spec = ArcType()

View File

@ -1,15 +1,26 @@
from collections.abc import Awaitable
from typing import Callable
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 .defines import (
CONF_DISP_BG_COLOR,
CONF_DISP_BG_IMAGE,
CONF_LVGL_ID,
CONF_SHOW_SNOW,
literal,
)
from .lv_validation import lv_bool, lv_color, lv_image
from .lvcode import (
LVGL_COMP_ARG,
LambdaContext,
LocalVariable,
LvConditional,
LvglComponent,
ReturnStatement,
add_line_marks,
lv,
@ -17,46 +28,46 @@ from .lvcode import (
lv_obj,
lvgl_comp,
)
from .schemas import ACTION_SCHEMA, LVGL_SCHEMA
from .schemas import DISP_BG_SCHEMA, LIST_ACTION_SCHEMA, LVGL_SCHEMA
from .types import (
LV_EVENT,
LV_STATE,
LvglAction,
LvglComponent,
LvglComponentPtr,
LvglCondition,
ObjUpdateAction,
lv_disp_t,
lv_obj_t,
)
from .widget import Widget, get_widget, lv_scr_act, set_obj_properties
from .widget import Widget, get_widgets, 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)
async def action_to_code(
widgets: list[Widget],
action: Callable[[Widget], Awaitable[None]],
action_id,
template_arg,
args,
):
async with LambdaContext(parameters=args, where=action_id) as context:
for widget in widgets:
with LvConditional(widget.obj != nullptr):
await action(widget)
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
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
)
async def do_update(widget: Widget):
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, LV_EVENT.VALUE_CHANGED, nullptr)
widgets = await get_widgets(config[CONF_ID])
return await action_to_code(widgets, do_update, action_id, template_arg, args)
@automation.register_condition(
@ -66,9 +77,7 @@ async def update_to_code(config, action_id, template_arg, args):
)
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:
async with LambdaContext(LVGL_COMP_ARG, 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)
@ -89,15 +98,23 @@ async def lvgl_is_paused(config, condition_id, template_arg, args):
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:
async with LambdaContext(LVGL_COMP_ARG, 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
async def disp_update(disp, config: dict):
if CONF_DISP_BG_COLOR not in config and CONF_DISP_BG_IMAGE not in config:
return
with LocalVariable("lv_disp_tmp", lv_disp_t, literal(disp)) as disp_temp:
if bg_color := config.get(CONF_DISP_BG_COLOR):
lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color))
if bg_image := config.get(CONF_DISP_BG_IMAGE):
lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image))
@automation.register_action(
"lvgl.widget.redraw",
ObjUpdateAction,
@ -109,14 +126,32 @@ async def lvgl_is_idle(config, condition_id, template_arg, args):
),
)
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)
widgets = await get_widgets(config) or [lv_scr_act]
async def do_invalidate(widget: Widget):
lv_obj.invalidate(widget.obj)
return await action_to_code(widgets, do_invalidate, action_id, template_arg, args)
@automation.register_action(
"lvgl.update",
LvglAction,
DISP_BG_SCHEMA.extend(
{
cv.GenerateID(): cv.use_id(LvglComponent),
}
).add_extra(cv.has_at_least_one_key(CONF_DISP_BG_COLOR, CONF_DISP_BG_IMAGE)),
)
async def lvgl_update_to_code(config, action_id, template_arg, args):
widgets = await get_widgets(config)
w = widgets[0]
disp = f"{w.obj}->get_disp()"
async with LambdaContext(parameters=args, where=action_id) as context:
await disp_update(disp, config)
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
await cg.register_parented(var, w.var)
return var
@automation.register_action(
@ -128,8 +163,8 @@ async def obj_invalidate_to_code(config, action_id, template_arg, args):
},
)
async def pause_action_to_code(config, action_id, template_arg, args):
with LambdaContext([(LvglComponentPtr, "lvgl_comp")]) as context:
add_line_marks(action_id)
async with LambdaContext(LVGL_COMP_ARG) as context:
add_line_marks(where=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])
@ -144,45 +179,48 @@ async def pause_action_to_code(config, action_id, template_arg, args):
},
)
async def resume_action_to_code(config, action_id, template_arg, args):
with LambdaContext([(LvglComponentPtr, "lvgl_comp")]) as context:
add_line_marks(action_id)
async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context:
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)
@automation.register_action("lvgl.widget.disable", ObjUpdateAction, LIST_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)
async def do_disable(widget: Widget):
widget.add_state(LV_STATE.DISABLED)
return await action_to_code(
await get_widgets(config), do_disable, action_id, template_arg, args
)
@automation.register_action("lvgl.widget.enable", ObjUpdateAction, ACTION_SCHEMA)
@automation.register_action("lvgl.widget.enable", ObjUpdateAction, LIST_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)
async def do_enable(widget: Widget):
widget.clear_state(LV_STATE.DISABLED)
return await action_to_code(
await get_widgets(config), do_enable, action_id, template_arg, args
)
@automation.register_action("lvgl.widget.hide", ObjUpdateAction, ACTION_SCHEMA)
@automation.register_action("lvgl.widget.hide", ObjUpdateAction, LIST_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)
async def do_hide(widget: Widget):
widget.add_flag("LV_OBJ_FLAG_HIDDEN")
return await action_to_code(
await get_widgets(config), do_hide, action_id, template_arg, args
)
@automation.register_action("lvgl.widget.show", ObjUpdateAction, ACTION_SCHEMA)
@automation.register_action("lvgl.widget.show", ObjUpdateAction, LIST_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)
async def do_show(widget: Widget):
widget.clear_flag("LV_OBJ_FLAG_HIDDEN")
return await action_to_code(
await get_widgets(config), do_show, action_id, template_arg, args
)

View File

@ -1,19 +1,14 @@
from esphome.const import CONF_BUTTON
from esphome.cpp_generator import MockObjClass
from .defines import CONF_MAIN
from .types import LvBoolean, WidgetType
lv_btn_t = LvBoolean("lv_btn_t")
class BtnType(WidgetType):
def __init__(self):
super().__init__(CONF_BUTTON, LvBoolean("lv_btn_t"), (CONF_MAIN,))
def obj_creator(self, parent: MockObjClass, config: dict):
"""
LVGL 8 calls buttons `btn`
"""
return f"lv_btn_create({parent})"
super().__init__(CONF_BUTTON, lv_btn_t, (CONF_MAIN,), lv_name="btn")
def get_uses(self):
return ("btn",)

View File

@ -0,0 +1,25 @@
from .defines import CONF_INDICATOR, CONF_MAIN, CONF_TEXT
from .lv_validation import lv_text
from .lvcode import lv
from .schemas import TEXT_SCHEMA
from .types import LvBoolean
from .widget import Widget, WidgetType
CONF_CHECKBOX = "checkbox"
class CheckboxType(WidgetType):
def __init__(self):
super().__init__(
CONF_CHECKBOX,
LvBoolean("lv_checkbox_t"),
(CONF_MAIN, CONF_INDICATOR),
TEXT_SCHEMA,
)
async def to_code(self, w: Widget, config):
if value := config.get(CONF_TEXT):
lv.checkbox_set_text(w.obj, await lv_text.process(value))
checkbox_spec = CheckboxType()

View File

@ -4,31 +4,20 @@ 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_generator import MockObj
from esphome.cpp_types import uint32
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
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
lvgl_ns = cg.esphome_ns.namespace("lvgl")
def literal(arg: Union[str, ConstantLiteral]):
def literal(arg):
if isinstance(arg, str):
return ConstantLiteral(arg)
return MockObj(arg)
return arg
@ -93,15 +82,23 @@ class LvConstant(LValidator):
return self.prefix + cv.one_of(*choices, upper=True)(value)
super().__init__(validator, rtype=uint32)
self.retmapper = self.mapper
self.one_of = LValidator(validator, uint32, retmapper=self.mapper)
self.several_of = LValidator(
cv.ensure_list(self.one_of), uint32, retmapper=self.mapper
)
def mapper(self, value, args=()):
if isinstance(value, list):
value = "|".join(value)
return ConstantLiteral(value)
if not isinstance(value, list):
value = [value]
return literal(
"|".join(
[
str(v) if str(v).startswith(self.prefix) else self.prefix + str(v)
for v in value
]
).upper()
)
def extend(self, *choices):
"""
@ -112,9 +109,6 @@ class LvConstant(LValidator):
return LvConstant(self.prefix, *(self.choices + choices))
# Widgets
CONF_LABEL = "label"
# Parts
CONF_MAIN = "main"
CONF_SCROLLBAR = "scrollbar"
@ -123,10 +117,15 @@ CONF_KNOB = "knob"
CONF_SELECTED = "selected"
CONF_ITEMS = "items"
CONF_TICKS = "ticks"
CONF_TICK_STYLE = "tick_style"
CONF_CURSOR = "cursor"
CONF_TEXTAREA_PLACEHOLDER = "textarea_placeholder"
# Layout types
TYPE_FLEX = "flex"
TYPE_GRID = "grid"
TYPE_NONE = "none"
LV_FONTS = list(f"montserrat_{s}" for s in range(8, 50, 2)) + [
"dejavu_16_persian_hebrew",
"simsun_16_cjk",
@ -134,7 +133,7 @@ LV_FONTS = list(f"montserrat_{s}" for s in range(8, 50, 2)) + [
"unscii_16",
]
LV_EVENT = {
LV_EVENT_MAP = {
"PRESS": "PRESSED",
"SHORT_CLICK": "SHORT_CLICKED",
"LONG_PRESS": "LONG_PRESSED",
@ -150,7 +149,7 @@ LV_EVENT = {
"CANCEL": "CANCEL",
}
LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT)
LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT_MAP)
LV_ANIM = LvConstant(
@ -305,7 +304,8 @@ OBJ_FLAGS = (
ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL")
BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE")
BTNMATRIX_CTRLS = (
BTNMATRIX_CTRLS = LvConstant(
"LV_BTNMATRIX_CTRL_",
"HIDDEN",
"NO_REPEAT",
"DISABLED",
@ -366,7 +366,6 @@ CONF_ACCEPTED_CHARS = "accepted_chars"
CONF_ADJUSTABLE = "adjustable"
CONF_ALIGN = "align"
CONF_ALIGN_TO = "align_to"
CONF_ANGLE_RANGE = "angle_range"
CONF_ANIMATED = "animated"
CONF_ANIMATION = "animation"
CONF_ANTIALIAS = "antialias"
@ -384,8 +383,6 @@ CONF_BYTE_ORDER = "byte_order"
CONF_CHANGE_RATE = "change_rate"
CONF_CLOSE_BUTTON = "close_button"
CONF_COLOR_DEPTH = "color_depth"
CONF_COLOR_END = "color_end"
CONF_COLOR_START = "color_start"
CONF_CONTROL = "control"
CONF_DEFAULT = "default"
CONF_DEFAULT_FONT = "default_font"
@ -414,9 +411,7 @@ CONF_GRID_ROW_ALIGN = "grid_row_align"
CONF_GRID_ROWS = "grid_rows"
CONF_HEADER_MODE = "header_mode"
CONF_HOME = "home"
CONF_INDICATORS = "indicators"
CONF_KEY_CODE = "key_code"
CONF_LABEL_GAP = "label_gap"
CONF_LAYOUT = "layout"
CONF_LEFT_BUTTON = "left_button"
CONF_LINE_WIDTH = "line_width"
@ -425,7 +420,6 @@ CONF_LONG_PRESS_TIME = "long_press_time"
CONF_LONG_PRESS_REPEAT_TIME = "long_press_repeat_time"
CONF_LVGL_ID = "lvgl_id"
CONF_LONG_MODE = "long_mode"
CONF_MAJOR = "major"
CONF_MSGBOXES = "msgboxes"
CONF_OBJ = "obj"
CONF_OFFSET_X = "offset_x"
@ -434,6 +428,7 @@ CONF_ONE_LINE = "one_line"
CONF_ON_SELECT = "on_select"
CONF_ONE_CHECKED = "one_checked"
CONF_NEXT = "next"
CONF_PAGE = "page"
CONF_PAGE_WRAP = "page_wrap"
CONF_PASSWORD_MODE = "password_mode"
CONF_PIVOT_X = "pivot_x"
@ -442,14 +437,12 @@ CONF_PLACEHOLDER_TEXT = "placeholder_text"
CONF_POINTS = "points"
CONF_PREVIOUS = "previous"
CONF_REPEAT_COUNT = "repeat_count"
CONF_R_MOD = "r_mod"
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"
CONF_SCROLLBAR_MODE = "scrollbar_mode"
CONF_SELECTED_INDEX = "selected_index"
@ -459,8 +452,9 @@ CONF_SRC = "src"
CONF_START_ANGLE = "start_angle"
CONF_START_VALUE = "start_value"
CONF_STATES = "states"
CONF_STRIDE = "stride"
CONF_STYLE = "style"
CONF_STYLES = "styles"
CONF_STYLE_DEFINITIONS = "style_definitions"
CONF_STYLE_ID = "style_id"
CONF_SKIP = "skip"
CONF_SYMBOL = "symbol"
@ -505,4 +499,4 @@ DEFAULT_ESPHOME_FONT = "esphome_lv_default_font"
def join_enums(enums, prefix=""):
return ConstantLiteral("|".join(f"(int){prefix}{e.upper()}" for e in enums))
return literal("|".join(f"(int){prefix}{e.upper()}" for e in enums))

View File

@ -1,10 +1,7 @@
import re
from esphome import config_validation as cv
from esphome.config import Config
from esphome.const import CONF_ARGS, CONF_FORMAT
from esphome.core import CORE, ID
from esphome.yaml_util import ESPHomeDataBase
lv_uses = {
"USER_DATA",
@ -44,23 +41,6 @@ def validate_printf(value):
return value
def get_line_marks(value) -> list:
"""
If possible, return a preprocessor directive to identify the line number where the given id was defined.
:param id: The id in question
:return: A list containing zero or more line directives
"""
path = None
if isinstance(value, ESPHomeDataBase):
path = value.esp_range
elif isinstance(value, ID) and isinstance(CORE.config, Config):
path = CORE.config.get_path_for_id(value)[:-1]
path = CORE.config.get_deepest_document_range_for_path(path)
if path is None:
return []
return [path.start_mark.as_line_directive]
def requires_component(comp):
def validator(value):
lvgl_components_required.add(comp)

View File

@ -0,0 +1,85 @@
import esphome.config_validation as cv
from esphome.const import CONF_ANGLE, CONF_MODE
from .defines import (
CONF_ANTIALIAS,
CONF_MAIN,
CONF_OFFSET_X,
CONF_OFFSET_Y,
CONF_PIVOT_X,
CONF_PIVOT_Y,
CONF_SRC,
CONF_ZOOM,
LvConstant,
)
from .label import CONF_LABEL
from .lv_validation import angle, lv_bool, lv_image, size, zoom
from .lvcode import lv
from .types import lv_img_t
from .widget import Widget, WidgetType
CONF_IMAGE = "image"
BASE_IMG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_PIVOT_X, default="50%"): size,
cv.Optional(CONF_PIVOT_Y, default="50%"): size,
cv.Optional(CONF_ANGLE): angle,
cv.Optional(CONF_ZOOM): zoom,
cv.Optional(CONF_OFFSET_X): size,
cv.Optional(CONF_OFFSET_Y): size,
cv.Optional(CONF_ANTIALIAS): lv_bool,
cv.Optional(CONF_MODE): LvConstant(
"LV_IMG_SIZE_MODE_", "VIRTUAL", "REAL"
).one_of,
}
)
IMG_SCHEMA = BASE_IMG_SCHEMA.extend(
{
cv.Required(CONF_SRC): lv_image,
}
)
IMG_MODIFY_SCHEMA = BASE_IMG_SCHEMA.extend(
{
cv.Optional(CONF_SRC): lv_image,
}
)
class ImgType(WidgetType):
def __init__(self):
super().__init__(
CONF_IMAGE,
lv_img_t,
(CONF_MAIN,),
IMG_SCHEMA,
IMG_MODIFY_SCHEMA,
lv_name="img",
)
def get_uses(self):
return "img", CONF_LABEL
async def to_code(self, w: Widget, config):
if src := config.get(CONF_SRC):
lv.img_set_src(w.obj, await lv_image.process(src))
if cf_angle := config.get(CONF_ANGLE):
pivot_x = config[CONF_PIVOT_X]
pivot_y = config[CONF_PIVOT_Y]
lv.img_set_pivot(w.obj, pivot_x, pivot_y)
lv.img_set_angle(w.obj, cf_angle)
if img_zoom := config.get(CONF_ZOOM):
lv.img_set_zoom(w.obj, img_zoom)
if offset := config.get(CONF_OFFSET_X):
lv.img_set_offset_x(w.obj, offset)
if offset := config.get(CONF_OFFSET_Y):
lv.img_set_offset_y(w.obj, offset)
if CONF_ANTIALIAS in config:
lv.img_set_antialias(w.obj, config[CONF_ANTIALIAS])
if mode := config.get(CONF_MODE):
lv.img_set_mode(w.obj, mode)
img_spec = ImgType()

View File

@ -1,7 +1,6 @@
import esphome.config_validation as cv
from .defines import (
CONF_LABEL,
CONF_LONG_MODE,
CONF_MAIN,
CONF_RECOLOR,
@ -15,6 +14,8 @@ from .schemas import TEXT_SCHEMA
from .types import LvText, WidgetType
from .widget import Widget
CONF_LABEL = "label"
class LabelType(WidgetType):
def __init__(self):
@ -33,9 +34,9 @@ class LabelType(WidgetType):
async def to_code(self, w: Widget, config):
"""For a text object, create and set text"""
if value := config.get(CONF_TEXT):
w.set_property(CONF_TEXT, await lv_text.process(value))
w.set_property(CONF_LONG_MODE, config)
w.set_property(CONF_RECOLOR, config)
await w.set_property(CONF_TEXT, await lv_text.process(value))
await w.set_property(CONF_LONG_MODE, config)
await w.set_property(CONF_RECOLOR, config)
label_spec = LabelType()

View File

@ -0,0 +1,29 @@
import esphome.config_validation as cv
from esphome.const import CONF_BRIGHTNESS, CONF_COLOR, CONF_LED
from .defines import CONF_MAIN
from .lv_validation import lv_brightness, lv_color
from .lvcode import lv
from .types import LvType
from .widget import Widget, WidgetType
LED_SCHEMA = cv.Schema(
{
cv.Optional(CONF_COLOR): lv_color,
cv.Optional(CONF_BRIGHTNESS): lv_brightness,
}
)
class LedType(WidgetType):
def __init__(self):
super().__init__(CONF_LED, LvType("lv_led_t"), (CONF_MAIN,), LED_SCHEMA)
async def to_code(self, w: Widget, config):
if color := config.get(CONF_COLOR):
lv.led_set_color(w.obj, await lv_color.process(color))
if brightness := config.get(CONF_BRIGHTNESS):
lv.led_set_brightness(w.obj, await lv_brightness.process(brightness))
led_spec = LedType()

View File

@ -0,0 +1,51 @@
import functools
import esphome.codegen as cg
import esphome.config_validation as cv
from . import defines as df
from .defines import CONF_MAIN, literal
from .lvcode import lv
from .types import LvType
from .widget import Widget, WidgetType
CONF_LINE = "line"
CONF_POINTS = "points"
CONF_POINT_LIST_ID = "point_list_id"
lv_point_t = cg.global_ns.struct("lv_point_t")
def point_list(il):
il = cv.string(il)
nl = il.replace(" ", "").split(",")
return [int(n) for n in nl]
def cv_point_list(value):
if not isinstance(value, list):
raise cv.Invalid("List of points required")
values = [point_list(v) for v in value]
if not functools.reduce(lambda f, v: f and len(v) == 2, values, True):
raise cv.Invalid("Points must be a list of x,y integer pairs")
return values
LINE_SCHEMA = {
cv.Required(df.CONF_POINTS): cv_point_list,
cv.GenerateID(CONF_POINT_LIST_ID): cv.declare_id(lv_point_t),
}
class LineType(WidgetType):
def __init__(self):
super().__init__(CONF_LINE, LvType("lv_line_t"), (CONF_MAIN,), LINE_SCHEMA)
async def to_code(self, w: Widget, config):
"""For a line object, create and add the points"""
data = literal(config[CONF_POINTS])
points = cg.static_const_array(config[CONF_POINT_LIST_ID], data)
lv.line_set_points(w.obj, points, len(data))
line_spec = LineType()

View File

@ -0,0 +1,53 @@
import esphome.config_validation as cv
from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE
from .defines import BAR_MODES, CONF_ANIMATED, CONF_INDICATOR, CONF_MAIN, literal
from .lv_validation import animated, get_start_value, lv_float
from .lvcode import lv
from .types import LvNumber, NumberType
from .widget import Widget
CONF_BAR = "bar"
BAR_MODIFY_SCHEMA = cv.Schema(
{
cv.Optional(CONF_VALUE): lv_float,
cv.Optional(CONF_ANIMATED, default=True): animated,
}
)
BAR_SCHEMA = cv.Schema(
{
cv.Optional(CONF_VALUE): lv_float,
cv.Optional(CONF_MIN_VALUE, default=0): cv.int_,
cv.Optional(CONF_MAX_VALUE, default=100): cv.int_,
cv.Optional(CONF_MODE, default="NORMAL"): BAR_MODES.one_of,
cv.Optional(CONF_ANIMATED, default=True): animated,
}
)
class BarType(NumberType):
def __init__(self):
super().__init__(
CONF_BAR,
LvNumber("lv_bar_t"),
parts=(CONF_MAIN, CONF_INDICATOR),
schema=BAR_SCHEMA,
modify_schema=BAR_MODIFY_SCHEMA,
)
async def to_code(self, w: Widget, config):
var = w.obj
if CONF_MIN_VALUE in config:
lv.bar_set_range(var, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE])
lv.bar_set_mode(var, literal(config[CONF_MODE]))
value = await get_start_value(config)
if value is not None:
lv.bar_set_value(var, value, literal(config[CONF_ANIMATED]))
@property
def animated(self):
return True
bar_spec = BarType()

View File

@ -0,0 +1,20 @@
from .defines import CONF_INDICATOR, CONF_KNOB, CONF_MAIN
from .types import LvBoolean
from .widget import WidgetType
CONF_SWITCH = "switch"
class SwitchType(WidgetType):
def __init__(self):
super().__init__(
CONF_SWITCH,
LvBoolean("lv_switch_t"),
(CONF_MAIN, CONF_INDICATOR, CONF_KNOB),
)
async def to_code(self, w, config):
return []
switch_spec = SwitchType()

View File

@ -1,3 +1,5 @@
from typing import Union
import esphome.codegen as cg
from esphome.components.binary_sensor import BinarySensor
from esphome.components.color import ColorStruct
@ -6,7 +8,7 @@ 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
from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT
from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT, CONF_VALUE
from esphome.core import HexInt
from esphome.cpp_generator import MockObj
from esphome.cpp_types import uint32
@ -14,7 +16,14 @@ 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, ConstantLiteral, LValidator, LvConstant, literal
from .defines import (
CONF_END_VALUE,
CONF_START_VALUE,
LV_FONTS,
LValidator,
LvConstant,
literal,
)
from .helpers import (
esphome_fonts_used,
lv_fonts_used,
@ -60,6 +69,13 @@ def color_retmapper(value):
return lv_expr.color_from(MockObj(value))
def option_string(value):
value = cv.string(value).strip()
if value.find("\n") != -1:
raise cv.Invalid("Options strings must not contain newlines")
return value
lv_color = LValidator(color, ty.lv_color_t, retmapper=color_retmapper)
@ -156,6 +172,12 @@ lv_bool = LValidator(
)
def lv_pct(value: Union[int, float]):
if isinstance(value, float):
value = int(value * 100)
return literal(f"lv_pct({value})")
def lvms_validator_(value):
if value == "never":
value = "2147483647ms"
@ -189,13 +211,16 @@ class TextValidator(LValidator):
args = [str(x) for x in value[CONF_ARGS]]
arg_expr = cg.RawExpression(",".join(args))
format_str = cpp_string_escape(value[CONF_FORMAT])
return f"str_sprintf({format_str}, {arg_expr}).c_str()"
return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()")
return await super().process(value, args)
lv_text = TextValidator()
lv_float = LValidator(cv.float_, cg.float_, Sensor, "get_state()")
lv_int = LValidator(cv.int_, cg.int_, Sensor, "get_state()")
lv_brightness = LValidator(
cv.percentage, cg.float_, Sensor, "get_state()", retmapper=lambda x: int(x * 255)
)
def is_lv_font(font):
@ -222,8 +247,33 @@ class LvFont(LValidator):
async def process(self, value, args=()):
if is_lv_font(value):
return ConstantLiteral(f"&lv_font_{value}")
return ConstantLiteral(f"{value}_engine->get_lv_font()")
return literal(f"&lv_font_{value}")
return literal(f"{value}_engine->get_lv_font()")
lv_font = LvFont()
def animated(value):
if isinstance(value, bool):
value = "ON" if value else "OFF"
return LvConstant("LV_ANIM_", "OFF", "ON").one_of(value)
def key_code(value):
value = cv.Any(cv.All(cv.string_strict, cv.Length(min=1, max=1)), cv.uint8_t)(value)
if isinstance(value, str):
return ord(value[0])
return value
async def get_end_value(config):
return await lv_int.process(config.get(CONF_END_VALUE))
async def get_start_value(config):
if CONF_START_VALUE in config:
value = config[CONF_START_VALUE]
else:
value = config.get(CONF_VALUE)
return await lv_int.process(value)

View File

@ -1,9 +1,9 @@
import abc
import logging
from typing import Union
from esphome import codegen as cg
from esphome.core import ID, Lambda
from esphome.config import Config
from esphome.core import CORE, ID, Lambda
from esphome.cpp_generator import (
AssignmentExpression,
CallExpression,
@ -18,12 +18,47 @@ from esphome.cpp_generator import (
VariableDeclarationExpression,
statement,
)
from esphome.yaml_util import ESPHomeDataBase
from .defines import ConstantLiteral
from .helpers import get_line_marks
from .types import lv_group_t
from .defines import literal, lvgl_ns
_LOGGER = logging.getLogger(__name__)
LVGL_COMP = "lv_component" # used as a lambda argument in lvgl_comp()
# Argument tuple for use in lambdas
LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent)
LVGL_COMP_ARG = [(LvglComponent.operator("ptr"), LVGL_COMP)]
lv_event_t_ptr = cg.global_ns.namespace("lv_event_t").operator("ptr")
EVENT_ARG = [(lv_event_t_ptr, "ev")]
CUSTOM_EVENT = literal("lvgl::lv_custom_event")
def get_line_marks(value) -> list:
"""
If possible, return a preprocessor directive to identify the line number where the given id was defined.
:param value: The id or other token to get the line number for
:return: A list containing zero or more line directives
"""
path = None
if isinstance(value, ESPHomeDataBase):
path = value.esp_range
elif isinstance(value, ID) and isinstance(CORE.config, Config):
path = CORE.config.get_path_for_id(value)[:-1]
path = CORE.config.get_deepest_document_range_for_path(path)
if path is None:
return []
return [path.start_mark.as_line_directive]
class IndentedStatement(Statement):
def __init__(self, stmt: Statement, indent: int):
self.statement = stmt
self.indent = indent
def __str__(self):
result = " " * self.indent * 4 + str(self.statement).strip()
if not isinstance(self.statement, RawStatement):
result += ";"
return result
class CodeContext(abc.ABC):
@ -39,6 +74,16 @@ class CodeContext(abc.ABC):
def add(self, expression: Union[Expression, Statement]):
pass
@staticmethod
def start_block():
CodeContext.append(RawStatement("{"))
CodeContext.code_context.indent()
@staticmethod
def end_block():
CodeContext.code_context.detent()
CodeContext.append(RawStatement("}"))
@staticmethod
def append(expression: Union[Expression, Statement]):
if CodeContext.code_context is not None:
@ -47,14 +92,25 @@ class CodeContext(abc.ABC):
def __init__(self):
self.previous: Union[CodeContext | None] = None
self.indent_level = 0
def __enter__(self):
async def __aenter__(self):
self.previous = CodeContext.code_context
CodeContext.code_context = self
return self
def __exit__(self, *args):
async def __aexit__(self, *args):
CodeContext.code_context = self.previous
def indent(self):
self.indent_level += 1
def detent(self):
self.indent_level -= 1
def indented_statement(self, stmt):
return IndentedStatement(stmt, self.indent_level)
class MainContext(CodeContext):
"""
@ -62,42 +118,7 @@ class MainContext(CodeContext):
"""
def add(self, expression: Union[Expression, Statement]):
return cg.add(expression)
class LvContext(CodeContext):
"""
Code generation into the LVGL initialisation code (called in `setup()`)
"""
lv_init_code: list["Statement"] = []
@staticmethod
def lv_add(expression: Union[Expression, Statement]):
if isinstance(expression, Expression):
expression = statement(expression)
if not isinstance(expression, Statement):
raise ValueError(
f"Add '{expression}' must be expression or statement, not {type(expression)}"
)
LvContext.lv_init_code.append(expression)
_LOGGER.debug("LV Adding: %s", expression)
return expression
@staticmethod
def get_code():
code = []
for exp in LvContext.lv_init_code:
text = str(statement(exp))
text = text.rstrip()
code.append(text)
return "\n".join(code) + "\n\n"
def add(self, expression: Union[Expression, Statement]):
return LvContext.lv_add(expression)
def set_style(self, prop):
return MockObj("lv_set_style_{prop}", "")
return cg.add(self.indented_statement(expression))
class LambdaContext(CodeContext):
@ -110,21 +131,23 @@ class LambdaContext(CodeContext):
parameters: list[tuple[SafeExpType, str]] = None,
return_type: SafeExpType = cg.void,
capture: str = "",
where=None,
):
super().__init__()
self.code_list: list[Statement] = []
self.parameters = parameters
self.parameters = parameters or []
self.return_type = return_type
self.capture = capture
self.where = where
def add(self, expression: Union[Expression, Statement]):
self.code_list.append(expression)
self.code_list.append(self.indented_statement(expression))
return expression
async def get_lambda(self) -> LambdaExpression:
code_text = self.get_code()
return await cg.process_lambda(
Lambda("\n".join(code_text) + "\n\n"),
Lambda("\n".join(code_text) + "\n"),
self.parameters,
capture=self.capture,
return_type=self.return_type,
@ -138,33 +161,59 @@ class LambdaContext(CodeContext):
code_text.append(text)
return code_text
def __enter__(self):
super().__enter__()
async def __aenter__(self):
await super().__aenter__()
add_line_marks(self.where)
return self
class LvContext(LambdaContext):
"""
Code generation into the LVGL initialisation code (called in `setup()`)
"""
def __init__(self, lv_component, args=None):
self.args = args or LVGL_COMP_ARG
super().__init__(parameters=self.args)
self.lv_component = lv_component
async def add_init_lambda(self):
cg.add(self.lv_component.add_init_lambda(await self.get_lambda()))
async def __aexit__(self, exc_type, exc_val, exc_tb):
await super().__aexit__(exc_type, exc_val, exc_tb)
await self.add_init_lambda()
def add(self, expression: Union[Expression, Statement]):
self.code_list.append(self.indented_statement(expression))
return expression
def __call__(self, *args):
return self.add(*args)
class LocalVariable(MockObj):
"""
Create a local variable and enclose the code using it within a block.
"""
def __init__(self, name, type, modifier=None, rhs=None):
base = ID(name, True, type)
def __init__(self, name, type, rhs=None, modifier="*"):
base = ID(name + "_VAR_", True, type)
super().__init__(base, "")
self.modifier = modifier
self.rhs = rhs
def __enter__(self):
CodeContext.append(RawStatement("{"))
CodeContext.start_block()
CodeContext.append(
VariableDeclarationExpression(self.base.type, self.modifier, self.base.id)
)
if self.rhs is not None:
CodeContext.append(AssignmentExpression(None, "", self.base, self.rhs))
return self.base
return MockObj(self.base)
def __exit__(self, *args):
CodeContext.append(RawStatement("}"))
CodeContext.end_block()
class MockLv:
@ -199,14 +248,27 @@ class MockLv:
self.append(result)
return result
def cond_if(self, expression: Expression):
CodeContext.append(RawStatement(f"if {expression} {{"))
def cond_else(self):
class LvConditional:
def __init__(self, condition):
self.condition = condition
def __enter__(self):
if self.condition is not None:
CodeContext.append(RawStatement(f"if ({self.condition}) {{"))
CodeContext.code_context.indent()
return self
def __exit__(self, *args):
if self.condition is not None:
CodeContext.code_context.detent()
CodeContext.append(RawStatement("}"))
def else_(self):
assert self.condition is not None
CodeContext.code_context.detent()
CodeContext.append(RawStatement("} else {"))
def cond_endif(self):
CodeContext.append(RawStatement("}"))
CodeContext.code_context.indent()
class ReturnStatement(ExpressionStatement):
@ -228,36 +290,56 @@ lv = MockLv("lv_")
lv_expr = LvExpr("lv_")
# Mock for lv_obj_ calls
lv_obj = MockLv("lv_obj_")
lvgl_comp = MockObj("lvgl_comp", "->")
# Operations on the LVGL component
lvgl_comp = MockObj(LVGL_COMP, "->")
# equivalent to cg.add() for the lvgl init context
# equivalent to cg.add() for the current code context
def lv_add(expression: Union[Expression, Statement]):
return CodeContext.append(expression)
def add_line_marks(where):
"""
Add line marks for the current code context
:param where: An object to identify the source of the line marks
:return:
"""
for mark in get_line_marks(where):
lv_add(cg.RawStatement(mark))
def lv_assign(target, expression):
lv_add(RawExpression(f"{target} = {expression}"))
lv_add(AssignmentExpression("", "", target, expression))
lv_groups = {} # Widget group names
def lv_Pvariable(type, name):
"""
Create but do not initialise a pointer variable
:param type: Type of the variable target
:param name: name of the variable, or an ID
:return: A MockObj of the variable
"""
if isinstance(name, str):
name = ID(name, True, type)
decl = VariableDeclarationExpression(type, "*", name)
CORE.add_global(decl)
var = MockObj(name, "->")
CORE.register_variable(name, var)
return var
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]
def lv_variable(type, name):
"""
Create but do not initialise a variable
:param type: Type of the variable target
:param name: name of the variable, or an ID
:return: A MockObj of the variable
"""
if isinstance(name, str):
name = ID(name, True, type)
decl = VariableDeclarationExpression(type, "", name)
CORE.add_global(decl)
var = MockObj(name, ".")
CORE.register_variable(name, var)
return var

View File

@ -9,8 +9,72 @@ namespace esphome {
namespace lvgl {
static const char *const TAG = "lvgl";
#if LV_USE_LOG
static void log_cb(const char *buf) {
esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, TAG, 0, "%.*s", (int) strlen(buf) - 1, buf);
}
#endif // LV_USE_LOG
static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) {
// make sure all coordinates are even
if (area->x1 & 1)
area->x1--;
if (!(area->x2 & 1))
area->x2++;
if (area->y1 & 1)
area->y1--;
if (!(area->y2 & 1))
area->y2++;
}
lv_event_code_t lv_custom_event; // NOLINT
void LvglComponent::dump_config() { ESP_LOGCONFIG(TAG, "LVGL:"); }
void LvglComponent::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 LvglComponent::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);
}
}
void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1,
lv_event_code_t event2) {
this->add_event_cb(obj, callback, event1);
this->add_event_cb(obj, callback, event2);
}
void LvglComponent::add_page(LvPageType *page) {
this->pages_.push_back(page);
page->setup(this->pages_.size() - 1);
}
void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) {
if (index >= this->pages_.size())
return;
this->current_page_ = index;
lv_scr_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false);
}
void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) {
if (this->pages_.empty() || (this->current_page_ == this->pages_.size() - 1 && !this->page_wrap_))
return;
do {
this->current_page_ = (this->current_page_ + 1) % this->pages_.size();
} while (this->pages_[this->current_page_]->skip); // skip empty pages()
this->show_page(this->current_page_, anim, time);
}
void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) {
if (this->pages_.empty() || (this->current_page_ == 0 && !this->page_wrap_))
return;
do {
this->current_page_ = (this->current_page_ + this->pages_.size() - 1) % this->pages_.size();
} while (this->pages_[this->current_page_]->skip); // skip empty pages()
this->show_page(this->current_page_, anim, time);
}
void LvglComponent::draw_buffer_(const lv_area_t *area, const uint8_t *ptr) {
for (auto *display : this->displays_) {
display->draw_pixels_at(area->x1, area->y1, lv_area_get_width(area), lv_area_get_height(area), ptr,
@ -27,6 +91,116 @@ void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv
}
lv_disp_flush_ready(disp_drv);
}
IdleTrigger::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;
}
});
}
#ifdef USE_LVGL_TOUCHSCREEN
LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time) {
lv_indev_drv_init(&this->drv_);
this->drv_.long_press_repeat_time = long_press_repeat_time;
this->drv_.long_press_time = long_press_time;
this->drv_.type = LV_INDEV_TYPE_POINTER;
this->drv_.user_data = this;
this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) {
auto *l = static_cast<LVTouchListener *>(d->user_data);
if (l->touch_pressed_) {
data->point.x = l->touch_point_.x;
data->point.y = l->touch_point_.y;
data->state = LV_INDEV_STATE_PRESSED;
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
};
}
void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) {
this->touch_pressed_ = !this->parent_->is_paused() && !tpoints.empty();
if (this->touch_pressed_)
this->touch_point_ = tpoints[0];
}
#endif // USE_LVGL_TOUCHSCREEN
#ifdef USE_LVGL_ROTARY_ENCODER
LVEncoderListener::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;
};
}
#endif // USE_LVGL_ROTARY_ENCODER
#ifdef USE_LVGL_BUTTONMATRIX
void LvBtnmatrixType::set_obj(lv_obj_t *lv_obj) {
LvCompound::set_obj(lv_obj);
lv_obj_add_event_cb(
lv_obj,
[](lv_event_t *event) {
auto *self = static_cast<LvBtnmatrixType *>(event->user_data);
if (self->key_callback_.size() == 0)
return;
auto key_idx = lv_btnmatrix_get_selected_btn(self->obj);
if (key_idx == LV_BTNMATRIX_BTN_NONE)
return;
if (self->key_map_.count(key_idx) != 0) {
self->send_key_(self->key_map_[key_idx]);
return;
}
const auto *str = lv_btnmatrix_get_btn_text(self->obj, key_idx);
auto len = strlen(str);
while (len--)
self->send_key_(*str++);
},
LV_EVENT_PRESSED, this);
}
#endif // USE_LVGL_BUTTONMATRIX
#ifdef USE_LVGL_KEYBOARD
static const char *const KB_SPECIAL_KEYS[] = {
"abc", "ABC", "1#",
// maybe add other special keys here
};
void LvKeyboardType::set_obj(lv_obj_t *lv_obj) {
LvCompound::set_obj(lv_obj);
lv_obj_add_event_cb(
lv_obj,
[](lv_event_t *event) {
auto *self = static_cast<LvKeyboardType *>(event->user_data);
if (self->key_callback_.size() == 0)
return;
auto key_idx = lv_btnmatrix_get_selected_btn(self->obj);
if (key_idx == LV_BTNMATRIX_BTN_NONE)
return;
const char *txt = lv_btnmatrix_get_btn_text(self->obj, key_idx);
if (txt == nullptr)
return;
for (const auto *kb_special_key : KB_SPECIAL_KEYS) {
if (strcmp(txt, kb_special_key) == 0)
return;
}
while (*txt != 0)
self->send_key_(*txt++);
},
LV_EVENT_PRESSED, this);
}
#endif // USE_LVGL_KEYBOARD
void LvglComponent::write_random_() {
// length of 2 lines in 32 bit units
@ -97,9 +271,24 @@ void LvglComponent::setup() {
this->disp_ = lv_disp_drv_register(&this->disp_drv_);
for (const auto &v : this->init_lambdas_)
v(this);
this->show_page(0, LV_SCR_LOAD_ANIM_NONE, 0);
lv_disp_trig_activity(this->disp_);
ESP_LOGCONFIG(TAG, "LVGL Setup complete");
}
void LvglComponent::update() {
// update indicators
if (this->paused_) {
return;
}
this->idle_callbacks_.call(lv_disp_get_inactive_time(this->disp_));
}
void LvglComponent::loop() {
if (this->paused_) {
if (this->show_snow_)
this->write_random_();
}
lv_timer_handler_run_in_period(5);
}
#ifdef USE_LVGL_IMAGE
lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc) {
@ -142,7 +331,20 @@ lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc) {
}
return img_dsc;
}
#endif // USE_LVGL_IMAGE
#ifdef USE_LVGL_ANIMIMG
void lv_animimg_stop(lv_obj_t *obj) {
auto *animg = (lv_animimg_t *) obj;
int32_t duration = animg->anim.time;
lv_animimg_set_duration(obj, 0);
lv_animimg_start(obj);
lv_animimg_set_duration(obj, duration);
}
#endif
void LvglComponent::static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
reinterpret_cast<LvglComponent *>(disp_drv->user_data)->flush_cb_(disp_drv, area, color_p);
}
} // namespace lvgl
} // namespace esphome

View File

@ -18,7 +18,6 @@
#include "esphome/core/component.h"
#include "esphome/core/log.h"
#include <lvgl.h>
#include <utility>
#include <vector>
#ifdef USE_LVGL_IMAGE
#include "esphome/components/image/image.h"
@ -31,6 +30,10 @@
#include "esphome/components/touchscreen/touchscreen.h"
#endif // USE_LVGL_TOUCHSCREEN
#if defined(USE_LVGL_BUTTONMATRIX) || defined(USE_LVGL_KEYBOARD)
#include "esphome/components/key_provider/key_provider.h"
#endif // USE_LVGL_BUTTONMATRIX
namespace esphome {
namespace lvgl {
@ -47,12 +50,25 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT
#endif // LV_COLOR_DEPTH
// Parent class for things that wrap an LVGL object
class LvCompound final {
class LvCompound {
public:
void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; }
virtual void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; }
lv_obj_t *obj{};
};
class LvPageType {
public:
LvPageType(bool skip) : skip(skip) {}
void setup(size_t index) {
this->index = index;
this->obj = lv_obj_create(nullptr);
}
lv_obj_t *obj{};
size_t index{};
bool skip;
};
using LvLambdaType = std::function<void(lv_obj_t *)>;
using set_value_lambda_t = std::function<void(float)>;
using event_callback_t = void(_lv_event_t *);
@ -89,48 +105,20 @@ class FontEngine {
lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc = nullptr);
#endif // USE_LVGL_IMAGE
#ifdef USE_LVGL_ANIMIMG
void lv_animimg_stop(lv_obj_t *obj);
#endif // USE_LVGL_ANIMIMG
class LvglComponent : public PollingComponent {
constexpr static const char *const TAG = "lvgl";
public:
static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
reinterpret_cast<LvglComponent *>(disp_drv->user_data)->flush_cb_(disp_drv, area, color_p);
}
static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p);
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
static void log_cb(const char *buf) {
esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, TAG, 0, "%.*s", (int) strlen(buf) - 1, buf);
}
static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) {
// make sure all coordinates are even
if (area->x1 & 1)
area->x1--;
if (!(area->x2 & 1))
area->x2++;
if (area->y1 & 1)
area->y1--;
if (!(area->y2 & 1))
area->y2++;
}
void setup() 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 update() override;
void loop() override;
void add_on_idle_callback(std::function<void(uint32_t)> &&callback) {
this->idle_callbacks_.add(std::move(callback));
}
@ -141,23 +129,15 @@ class LvglComponent : public PollingComponent {
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);
}
}
void set_paused(bool paused, bool show_snow);
void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event);
void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2);
bool is_paused() const { return this->paused_; }
void add_page(LvPageType *page);
void show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time);
void show_next_page(lv_scr_load_anim_t anim, uint32_t time);
void show_prev_page(lv_scr_load_anim_t anim, uint32_t time);
void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; }
protected:
void write_random_();
@ -168,8 +148,11 @@ class LvglComponent : public PollingComponent {
lv_disp_drv_t disp_drv_{};
lv_disp_t *disp_{};
bool paused_{};
std::vector<LvPageType *> pages_{};
size_t current_page_{0};
bool show_snow_{};
lv_coord_t snow_line_{};
bool page_wrap_{true};
std::vector<std::function<void(LvglComponent *lv_component)>> init_lambdas_;
CallbackManager<void(uint32_t)> idle_callbacks_{};
@ -179,16 +162,7 @@ class LvglComponent : public PollingComponent {
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;
}
});
}
explicit IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeout);
protected:
TemplatableValue<uint32_t> timeout_;
@ -217,28 +191,8 @@ template<typename... Ts> class LvglCondition : public Condition<Ts...>, public P
#ifdef USE_LVGL_TOUCHSCREEN
class LVTouchListener : public touchscreen::TouchListener, public Parented<LvglComponent> {
public:
LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time) {
lv_indev_drv_init(&this->drv_);
this->drv_.long_press_repeat_time = long_press_repeat_time;
this->drv_.long_press_time = long_press_time;
this->drv_.type = LV_INDEV_TYPE_POINTER;
this->drv_.user_data = this;
this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) {
auto *l = static_cast<LVTouchListener *>(d->user_data);
if (l->touch_pressed_) {
data->point.x = l->touch_point_.x;
data->point.y = l->touch_point_.y;
data->state = LV_INDEV_STATE_PRESSED;
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
};
}
void update(const touchscreen::TouchPoints_t &tpoints) override {
this->touch_pressed_ = !this->parent_->is_paused() && !tpoints.empty();
if (this->touch_pressed_)
this->touch_point_ = tpoints[0];
}
LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time);
void update(const touchscreen::TouchPoints_t &tpoints) override;
void release() override { touch_pressed_ = false; }
lv_indev_drv_t *get_drv() { return &this->drv_; }
@ -249,24 +203,10 @@ class LVTouchListener : public touchscreen::TouchListener, public Parented<LvglC
};
#endif // USE_LVGL_TOUCHSCREEN
#ifdef USE_LVGL_KEY_LISTENER
#ifdef USE_LVGL_ROTARY_ENCODER
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;
};
}
LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt);
void set_left_button(binary_sensor::BinarySensor *left_button) {
left_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_LEFT, state); });
@ -304,6 +244,24 @@ class LVEncoderListener : public Parented<LvglComponent> {
int32_t last_count_{};
int key_{};
};
#endif // USE_LVGL_KEY_LISTENER
#endif // USE_LVGL_ROTARY_ENCODER
#ifdef USE_LVGL_BUTTONMATRIX
class LvBtnmatrixType : public key_provider::KeyProvider, public LvCompound {
public:
void set_obj(lv_obj_t *lv_obj) override;
uint16_t get_selected() { return lv_btnmatrix_get_selected_btn(this->obj); }
void set_key(size_t idx, uint8_t key) { this->key_map_[idx] = key; }
protected:
std::map<size_t, uint8_t> key_map_{};
};
#endif // USE_LVGL_BUTTONMATRIX
#ifdef USE_LVGL_KEYBOARD
class LvKeyboardType : public key_provider::KeyProvider, public LvCompound {
public:
void set_obj(lv_obj_t *lv_obj) override;
};
#endif // USE_LVGL_KEYBOARD
} // namespace lvgl
} // namespace esphome

View File

@ -0,0 +1,113 @@
from esphome import automation, codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME
from .defines import (
CONF_ANIMATION,
CONF_LVGL_ID,
CONF_PAGE,
CONF_PAGE_WRAP,
CONF_SKIP,
LV_ANIM,
)
from .lv_validation import lv_bool, lv_milliseconds
from .lvcode import LVGL_COMP_ARG, LambdaContext, add_line_marks, lv_add, lvgl_comp
from .schemas import LVGL_SCHEMA
from .types import LvglAction, lv_page_t
from .widget import Widget, WidgetType, add_widgets, set_obj_properties
class PageType(WidgetType):
def __init__(self):
super().__init__(
CONF_PAGE,
lv_page_t,
(),
{
cv.Optional(CONF_SKIP, default=False): lv_bool,
},
)
async def to_code(self, w: Widget, config: dict):
return []
SHOW_SCHEMA = LVGL_SCHEMA.extend(
{
cv.Optional(CONF_ANIMATION, default="NONE"): LV_ANIM.one_of,
cv.Optional(CONF_TIME, default="50ms"): lv_milliseconds,
}
)
page_spec = PageType()
@automation.register_action(
"lvgl.page.next",
LvglAction,
SHOW_SCHEMA,
)
async def page_next_to_code(config, action_id, template_arg, args):
animation = await LV_ANIM.process(config[CONF_ANIMATION])
time = await lv_milliseconds.process(config[CONF_TIME])
async with LambdaContext(LVGL_COMP_ARG) as context:
add_line_marks(action_id)
lv_add(lvgl_comp.show_next_page(animation, time))
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
await cg.register_parented(var, config[CONF_LVGL_ID])
return var
@automation.register_action(
"lvgl.page.previous",
LvglAction,
SHOW_SCHEMA,
)
async def page_previous_to_code(config, action_id, template_arg, args):
animation = await LV_ANIM.process(config[CONF_ANIMATION])
time = await lv_milliseconds.process(config[CONF_TIME])
async with LambdaContext(LVGL_COMP_ARG) as context:
add_line_marks(action_id)
lv_add(lvgl_comp.show_prev_page(animation, time))
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
await cg.register_parented(var, config[CONF_LVGL_ID])
return var
@automation.register_action(
"lvgl.page.show",
LvglAction,
cv.maybe_simple_value(
SHOW_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.use_id(lv_page_t),
}
),
key=CONF_ID,
),
)
async def page_show_to_code(config, action_id, template_arg, args):
widget = await cg.get_variable(config[CONF_ID])
animation = await LV_ANIM.process(config[CONF_ANIMATION])
time = await lv_milliseconds.process(config[CONF_TIME])
async with LambdaContext(LVGL_COMP_ARG) as context:
add_line_marks(action_id)
lv_add(lvgl_comp.show_page(widget.index, animation, time))
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
await cg.register_parented(var, config[CONF_LVGL_ID])
return var
async def add_pages(lv_component, config):
lv_add(lv_component.set_page_wrap(config[CONF_PAGE_WRAP]))
for pconf in config.get(CONF_PAGES, ()):
id = pconf[CONF_ID]
skip = pconf[CONF_SKIP]
var = cg.new_Pvariable(id, skip)
page = Widget.create(id, var, page_spec, pconf)
lv_add(lv_component.add_page(var))
# Set outer config first
await set_obj_properties(page, config)
await set_obj_properties(page, pconf)
await add_widgets(page, pconf)

View File

@ -13,9 +13,10 @@ from .defines import (
CONF_ROTARY_ENCODERS,
)
from .helpers import lvgl_components_required
from .lvcode import add_group, lv, lv_add, lv_expr
from .lvcode import lv, lv_add, lv_expr
from .schemas import ENCODER_SCHEMA
from .types import lv_indev_type_t
from .widget import add_group
ROTARY_ENCODER_CONFIG = cv.ensure_list(
ENCODER_SCHEMA.extend(

View File

@ -15,8 +15,12 @@ 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 id_name, lv_font
from .types import WIDGET_TYPES, WidgetType
from .lv_validation import id_name, lv_color, lv_font, lv_image
from .lvcode import LvglComponent
from .types import WidgetType
# this will be populated later, in __init__.py to avoid circular imports.
WIDGET_TYPES: dict = {}
# A schema for text properties
TEXT_SCHEMA = cv.Schema(
@ -38,11 +42,13 @@ 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,
LIST_ACTION_SCHEMA = cv.ensure_list(
cv.maybe_simple_value(
{
cv.Required(CONF_ID): cv.use_id(ty.lv_pseudo_button_t),
},
key=CONF_ID,
)
)
PRESS_TIME = cv.All(
@ -154,6 +160,7 @@ STYLE_REMAP = {
# Complete object style schema
STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend(
{
cv.Optional(df.CONF_STYLES): cv.ensure_list(cv.use_id(ty.lv_style_t)),
cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant(
"LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO"
).one_of,
@ -209,7 +216,14 @@ def create_modify_schema(widget_type):
part_schema(widget_type)
.extend(
{
cv.Required(CONF_ID): cv.use_id(widget_type),
cv.Required(CONF_ID): cv.ensure_list(
cv.maybe_simple_value(
{
cv.Required(CONF_ID): cv.use_id(widget_type),
},
key=CONF_ID,
)
),
cv.Optional(CONF_STATE): SET_STATE_SCHEMA,
}
)
@ -227,6 +241,7 @@ def obj_schema(widget_type: WidgetType):
return (
part_schema(widget_type)
.extend(FLAG_SCHEMA)
.extend(LAYOUT_SCHEMA)
.extend(ALIGN_TO_SCHEMA)
.extend(automation_schema(widget_type.w_type))
.extend(
@ -240,6 +255,8 @@ def obj_schema(widget_type: WidgetType):
)
LAYOUT_SCHEMAS = {}
ALIGN_TO_SCHEMA = {
cv.Optional(df.CONF_ALIGN_TO): cv.Schema(
{
@ -252,6 +269,65 @@ ALIGN_TO_SCHEMA = {
}
def grid_free_space(value):
value = cv.Upper(value)
if value.startswith("FR(") and value.endswith(")"):
value = value.removesuffix(")").removeprefix("FR(")
return f"LV_GRID_FR({cv.positive_int(value)})"
raise cv.Invalid("must be a size in pixels, CONTENT or FR(nn)")
grid_spec = cv.Any(
lvalid.size, df.LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space
)
cell_alignments = df.LV_CELL_ALIGNMENTS.one_of
grid_alignments = df.LV_GRID_ALIGNMENTS.one_of
flex_alignments = df.LV_FLEX_ALIGNMENTS.one_of
LAYOUT_SCHEMA = {
cv.Optional(df.CONF_LAYOUT): cv.typed_schema(
{
df.TYPE_GRID: {
cv.Required(df.CONF_GRID_ROWS): [grid_spec],
cv.Required(df.CONF_GRID_COLUMNS): [grid_spec],
cv.Optional(df.CONF_GRID_COLUMN_ALIGN): grid_alignments,
cv.Optional(df.CONF_GRID_ROW_ALIGN): grid_alignments,
},
df.TYPE_FLEX: {
cv.Optional(
df.CONF_FLEX_FLOW, default="row_wrap"
): df.FLEX_FLOWS.one_of,
cv.Optional(df.CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments,
cv.Optional(df.CONF_FLEX_ALIGN_CROSS, default="start"): flex_alignments,
cv.Optional(df.CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments,
},
},
lower=True,
)
}
GRID_CELL_SCHEMA = {
cv.Required(df.CONF_GRID_CELL_ROW_POS): cv.positive_int,
cv.Required(df.CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
cv.Optional(df.CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int,
cv.Optional(df.CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int,
cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments,
cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments,
}
FLEX_OBJ_SCHEMA = {
cv.Optional(df.CONF_FLEX_GROW): cv.int_,
}
DISP_BG_SCHEMA = cv.Schema(
{
cv.Optional(df.CONF_DISP_BG_IMAGE): lv_image,
cv.Optional(df.CONF_DISP_BG_COLOR): lv_color,
}
)
# A style schema that can include text
STYLED_TEXT_SCHEMA = cv.maybe_simple_value(
STYLE_SCHEMA.extend(TEXT_SCHEMA), key=df.CONF_TEXT
@ -260,13 +336,11 @@ STYLED_TEXT_SCHEMA = cv.maybe_simple_value(
# For use by platform components
LVGL_SCHEMA = cv.Schema(
{
cv.GenerateID(df.CONF_LVGL_ID): cv.use_id(ty.LvglComponent),
cv.GenerateID(df.CONF_LVGL_ID): cv.use_id(LvglComponent),
}
)
ALL_STYLES = {
**STYLE_PROPS,
}
ALL_STYLES = {**STYLE_PROPS, **GRID_CELL_SCHEMA, **FLEX_OBJ_SCHEMA}
def container_validator(schema, widget_type: WidgetType):
@ -281,16 +355,17 @@ def container_validator(schema, widget_type: WidgetType):
result = schema
if w_sch := widget_type.schema:
result = result.extend(w_sch)
ltype = df.TYPE_NONE
if value and (layout := value.get(df.CONF_LAYOUT)):
if not isinstance(layout, dict):
raise cv.Invalid("Layout value must be a dict")
ltype = layout.get(CONF_TYPE)
if not ltype:
raise (cv.Invalid("Layout schema requires type:"))
add_lv_use(ltype)
result = result.extend(
{cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema())}
)
if value == SCHEMA_EXTRACT:
return result
result = result.extend(LAYOUT_SCHEMAS[ltype.lower()])
return result(value)
return validator

View File

@ -0,0 +1,63 @@
import esphome.config_validation as cv
from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE
from .defines import (
BAR_MODES,
CONF_ANIMATED,
CONF_INDICATOR,
CONF_KNOB,
CONF_MAIN,
literal,
)
from .helpers import add_lv_use
from .lv_bar import CONF_BAR
from .lv_validation import animated, get_start_value, lv_float
from .lvcode import lv
from .types import LvNumber, NumberType
from .widget import Widget
CONF_SLIDER = "slider"
SLIDER_MODIFY_SCHEMA = cv.Schema(
{
cv.Optional(CONF_VALUE): lv_float,
cv.Optional(CONF_ANIMATED, default=True): animated,
}
)
SLIDER_SCHEMA = cv.Schema(
{
cv.Optional(CONF_VALUE): lv_float,
cv.Optional(CONF_MIN_VALUE, default=0): cv.int_,
cv.Optional(CONF_MAX_VALUE, default=100): cv.int_,
cv.Optional(CONF_MODE, default="NORMAL"): BAR_MODES.one_of,
cv.Optional(CONF_ANIMATED, default=True): animated,
}
)
class SliderType(NumberType):
def __init__(self):
super().__init__(
CONF_SLIDER,
LvNumber("lv_slider_t"),
parts=(CONF_MAIN, CONF_INDICATOR, CONF_KNOB),
schema=SLIDER_SCHEMA,
modify_schema=SLIDER_MODIFY_SCHEMA,
)
@property
def animated(self):
return True
async def to_code(self, w: Widget, config):
add_lv_use(CONF_BAR)
if CONF_MIN_VALUE in config:
# not modify case
lv.slider_set_range(w.obj, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE])
lv.slider_set_mode(w.obj, literal(config[CONF_MODE]))
value = await get_start_value(config)
if value is not None:
lv.slider_set_value(w.obj, value, literal(config[CONF_ANIMATED]))
slider_spec = SliderType()

View File

@ -0,0 +1,43 @@
import esphome.config_validation as cv
from esphome.cpp_generator import MockObjClass
from .arc import CONF_ARC
from .defines import CONF_ARC_LENGTH, CONF_INDICATOR, CONF_MAIN, CONF_SPIN_TIME
from .lv_validation import angle
from .lvcode import lv_expr
from .types import LvType
from .widget import Widget, WidgetType
CONF_SPINNER = "spinner"
SPINNER_SCHEMA = cv.Schema(
{
cv.Required(CONF_ARC_LENGTH): angle,
cv.Required(CONF_SPIN_TIME): cv.positive_time_period_milliseconds,
}
)
class SpinnerType(WidgetType):
def __init__(self):
super().__init__(
CONF_SPINNER,
LvType("lv_spinner_t"),
(CONF_MAIN, CONF_INDICATOR),
SPINNER_SCHEMA,
{},
)
async def to_code(self, w: Widget, config):
return []
def get_uses(self):
return (CONF_ARC,)
def obj_creator(self, parent: MockObjClass, config: dict):
spin_time = config[CONF_SPIN_TIME].total_milliseconds
arc_length = config[CONF_ARC_LENGTH] // 10
return lv_expr.call("spinner_create", parent, spin_time, arc_length)
spinner_spec = SpinnerType()

View File

@ -0,0 +1,58 @@
import esphome.codegen as cg
from esphome.const import CONF_ID
from esphome.core import ID
from esphome.cpp_generator import MockObj
from .defines import (
CONF_STYLE_DEFINITIONS,
CONF_THEME,
CONF_TOP_LAYER,
LValidator,
literal,
)
from .helpers import add_lv_use
from .lvcode import LambdaContext, LocalVariable, lv, lv_assign, lv_variable
from .obj import obj_spec
from .schemas import ALL_STYLES
from .types import lv_lambda_t, lv_obj_t, lv_obj_t_ptr
from .widget import Widget, add_widgets, set_obj_properties, theme_widget_map
TOP_LAYER = literal("lv_disp_get_layer_top(lv_component->get_disp())")
async def styles_to_code(config):
"""Convert styles to C__ code."""
for style in config.get(CONF_STYLE_DEFINITIONS, ()):
svar = cg.new_Pvariable(style[CONF_ID])
lv.style_init(svar)
for prop, validator in ALL_STYLES.items():
if value := style.get(prop):
if isinstance(validator, LValidator):
value = await validator.process(value)
if isinstance(value, list):
value = "|".join(value)
lv.call(f"style_set_{prop}", svar, literal(value))
async def theme_to_code(config):
if theme := config.get(CONF_THEME):
add_lv_use(CONF_THEME)
for w_name, style in theme.items():
if not isinstance(style, dict):
continue
lname = "lv_theme_apply_" + w_name
apply = lv_variable(lv_lambda_t, lname)
theme_widget_map[w_name] = apply
ow = Widget.create("obj", MockObj(ID("obj")), obj_spec)
async with LambdaContext([(lv_obj_t_ptr, "obj")], where=w_name) as context:
await set_obj_properties(ow, style)
lv_assign(apply, await context.get_lambda())
async def add_top_layer(config):
if top_conf := config.get(CONF_TOP_LAYER):
with LocalVariable("top_layer", lv_obj_t, TOP_LAYER) as top_layer_obj:
top_w = Widget(top_layer_obj, obj_spec, top_conf)
await set_obj_properties(top_w, top_conf)
await add_widgets(top_w, top_conf)

View File

@ -7,15 +7,14 @@ from .defines import (
CONF_ALIGN_TO,
CONF_X,
CONF_Y,
LV_EVENT,
LV_EVENT_MAP,
LV_EVENT_TRIGGERS,
literal,
)
from .lvcode import LambdaContext, add_line_marks, lv, lv_add
from .lvcode import EVENT_ARG, LambdaContext, LvConditional, lv, lv_add
from .types import LV_EVENT
from .widget import widget_map
lv_event_t_ptr = cg.global_ns.namespace("lv_event_t").operator("ptr")
async def generate_triggers(lv_component):
"""
@ -34,15 +33,15 @@ async def generate_triggers(lv_component):
}.items():
conf = conf[0]
w.add_flag("LV_OBJ_FLAG_CLICKABLE")
event = "LV_EVENT_" + LV_EVENT[event[3:].upper()]
event = literal("LV_EVENT_" + LV_EVENT_MAP[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)
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]
align = literal(align_to[CONF_ALIGN])
x = align_to[CONF_X]
y = align_to[CONF_Y]
lv.obj_align_to(w.obj, target, align, x, y)
@ -50,12 +49,11 @@ async def generate_triggers(lv_component):
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)))
async with LambdaContext(EVENT_ARG, where=tid) as context:
with LvConditional(w.is_selected()):
lv_add(trigger.trigger(value))
lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), event))

View File

@ -1,8 +1,11 @@
from esphome import automation, codegen as cg
from esphome.core import ID
from esphome.cpp_generator import MockObjClass
import sys
from .defines import CONF_TEXT
from esphome import automation, codegen as cg
from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_VALUE
from esphome.cpp_generator import MockObj, MockObjClass
from .defines import CONF_TEXT, lvgl_ns
from .lvcode import lv_expr
class LvType(cg.MockObjClass):
@ -18,36 +21,48 @@ class LvType(cg.MockObjClass):
return self.args[0][0] if len(self.args) else None
class LvNumber(LvType):
def __init__(self, *args):
super().__init__(
*args,
largs=[(cg.float_, "x")],
lvalue=lambda w: w.get_number_value(),
has_on_value=True,
)
self.value_property = CONF_VALUE
uint16_t_ptr = cg.uint16.operator("ptr")
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_coord_t = cg.global_ns.namespace("lv_coord_t")
lv_event_code_t = cg.global_ns.enum("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)
lv_lambda_t = lvgl_ns.class_("LvLambdaType")
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")
# fake parent class for first class widgets and matrix buttons
lv_pseudo_button_t = lvgl_ns.class_("LvPseudoButton")
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_disp_t = cg.global_ns.struct("lv_disp_t")
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_page_t = cg.global_ns.class_("LvPageType", LvCompound)
lv_img_t = LvType("lv_img_t")
# this will be populated later, in __init__.py to avoid circular imports.
WIDGET_TYPES: dict = {}
LV_EVENT = MockObj(base="LV_EVENT_", op="")
LV_STATE = MockObj(base="LV_STATE_", op="")
LV_BTNMATRIX_CTRL = MockObj(base="LV_BTNMATRIX_CTRL_", op="")
class LvText(LvType):
@ -55,7 +70,8 @@ class LvText(LvType):
super().__init__(
*args,
largs=[(cg.std_string, "text")],
lvalue=lambda w: w.get_property("text")[0],
lvalue=lambda w: w.get_property("text"),
has_on_value=True,
**kwargs,
)
self.value_property = CONF_TEXT
@ -66,13 +82,21 @@ class LvBoolean(LvType):
super().__init__(
*args,
largs=[(cg.bool_, "x")],
lvalue=lambda w: w.has_state("LV_STATE_CHECKED"),
lvalue=lambda w: w.is_checked(),
has_on_value=True,
**kwargs,
)
CUSTOM_EVENT = ID("lv_custom_event", False, type=lv_event_code_t)
class LvSelect(LvType):
def __init__(self, *args, **kwargs):
super().__init__(
*args,
largs=[(cg.int_, "x")],
lvalue=lambda w: w.get_property("selected"),
has_on_value=True,
**kwargs,
)
class WidgetType:
@ -80,7 +104,15 @@ class WidgetType:
Describes a type of Widget, e.g. "bar" or "line"
"""
def __init__(self, name, w_type, parts, schema=None, modify_schema=None):
def __init__(
self,
name: str,
w_type: LvType,
parts: tuple,
schema=None,
modify_schema=None,
lv_name=None,
):
"""
:param name: The widget name, e.g. "bar"
:param w_type: The C type of the widget
@ -89,6 +121,7 @@ class WidgetType:
:param modify_schema: A schema to update the widget
"""
self.name = name
self.lv_name = lv_name or name
self.w_type = w_type
self.parts = parts
if schema is None:
@ -98,7 +131,8 @@ class WidgetType:
if modify_schema is None:
self.modify_schema = self.schema
else:
self.modify_schema = self.schema
self.modify_schema = modify_schema
self.mock_obj = MockObj(f"lv_{self.lv_name}", "_")
@property
def animated(self):
@ -118,7 +152,7 @@ class WidgetType:
:param config: Its configuration
:return: Generated code as a list of text lines
"""
raise NotImplementedError(f"No to_code defined for {self.name}")
return []
def obj_creator(self, parent: MockObjClass, config: dict):
"""
@ -127,7 +161,7 @@ class WidgetType:
:param config: Its configuration
:return: Generated code as a single text line
"""
return f"lv_{self.name}_create({parent})"
return lv_expr.call(f"{self.lv_name}_create", parent)
def get_uses(self):
"""
@ -135,3 +169,23 @@ class WidgetType:
:return:
"""
return ()
def get_max(self, config: dict):
return sys.maxsize
def get_min(self, config: dict):
return -sys.maxsize
def get_step(self, config: dict):
return 1
def get_scale(self, config: dict):
return 1.0
class NumberType(WidgetType):
def get_max(self, config: dict):
return int(config[CONF_MAX_VALUE] or 100)
def get_min(self, config: dict):
return int(config[CONF_MIN_VALUE] or 0)

View File

@ -1,33 +1,63 @@
import sys
from typing import Any
from typing import Any, Union
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 CORE, TimePeriod
from esphome.const import CONF_GROUP, CONF_ID, CONF_STATE, CONF_TYPE
from esphome.core import ID, TimePeriod
from esphome.coroutine import FakeAwaitable
from esphome.cpp_generator import MockObj, MockObjClass, VariableDeclarationExpression
from esphome.cpp_generator import AssignmentExpression, CallExpression, MockObj
from .defines import (
CONF_DEFAULT,
CONF_FLEX_ALIGN_CROSS,
CONF_FLEX_ALIGN_MAIN,
CONF_FLEX_ALIGN_TRACK,
CONF_FLEX_FLOW,
CONF_GRID_COLUMN_ALIGN,
CONF_GRID_COLUMNS,
CONF_GRID_ROW_ALIGN,
CONF_GRID_ROWS,
CONF_LAYOUT,
CONF_MAIN,
CONF_SCROLLBAR_MODE,
CONF_STYLES,
CONF_WIDGETS,
OBJ_FLAGS,
PARTS,
STATES,
ConstantLiteral,
TYPE_FLEX,
TYPE_GRID,
LValidator,
join_enums,
literal,
)
from .helpers import add_lv_use
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, LvType, WidgetType, lv_obj_t, lv_obj_t_ptr
from .lvcode import (
LvConditional,
add_line_marks,
lv,
lv_add,
lv_assign,
lv_expr,
lv_obj,
lv_Pvariable,
)
from .schemas import ALL_STYLES, STYLE_REMAP, WIDGET_TYPES
from .types import (
LV_STATE,
LvType,
WidgetType,
lv_coord_t,
lv_group_t,
lv_obj_t,
lv_obj_t_ptr,
)
EVENT_LAMB = "event_lamb__"
theme_widget_map = {}
class LvScrActType(WidgetType):
"""
@ -37,9 +67,6 @@ class LvScrActType(WidgetType):
def __init__(self):
super().__init__("lv_scr_act()", lv_obj_t, ())
def obj_creator(self, parent: MockObjClass, config: dict):
return []
async def to_code(self, w, config: dict):
return []
@ -55,7 +82,7 @@ class Widget:
def set_completed():
Widget.widgets_completed = True
def __init__(self, var, wtype: WidgetType, config: dict = None, parent=None):
def __init__(self, var, wtype: WidgetType, config: dict = None):
self.var = var
self.type = wtype
self.config = config
@ -63,21 +90,18 @@ class Widget:
self.step = 1.0
self.range_from = -sys.maxsize
self.range_to = sys.maxsize
self.parent = parent
if wtype.is_compound():
self.obj = MockObj(f"{self.var}->obj")
else:
self.obj = var
@staticmethod
def create(name, var, wtype: WidgetType, config: dict = None, parent=None):
w = Widget(var, wtype, config, parent)
def create(name, var, wtype: WidgetType, config: dict = None):
w = Widget(var, wtype, config)
if name is not None:
widget_map[name] = w
return w
@property
def obj(self):
if self.type.is_compound():
return f"{self.var}->obj"
return self.var
def add_state(self, state):
return lv_obj.add_state(self.obj, literal(state))
@ -85,7 +109,13 @@ class Widget:
return lv_obj.clear_state(self.obj, literal(state))
def has_state(self, state):
return lv_expr.obj_get_state(self.obj) & literal(state) != 0
return (lv_expr.obj_get_state(self.obj) & literal(state)) != 0
def is_pressed(self):
return self.has_state(LV_STATE.PRESSED)
def is_checked(self):
return self.has_state(LV_STATE.CHECKED)
def add_flag(self, flag):
return lv_obj.add_flag(self.obj, literal(flag))
@ -93,32 +123,37 @@ class Widget:
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):
async def set_property(self, prop, value, animated: bool = None):
if isinstance(value, dict):
value = value.get(prop)
if isinstance(ALL_STYLES.get(prop), LValidator):
value = await ALL_STYLES[prop].process(value)
else:
value = literal(value)
if value is None:
return
if isinstance(value, TimePeriod):
value = value.total_milliseconds
ltype = ltype or self.__type_base()
if isinstance(value, str):
value = literal(value)
if animated is None or self.type.animated is not True:
lv.call(f"{ltype}_set_{prop}", self.obj, value)
lv.call(f"{self.type.lv_name}_set_{prop}", self.obj, value)
else:
lv.call(
f"{ltype}_set_{prop}",
f"{self.type.lv_name}_set_{prop}",
self.obj,
value,
"LV_ANIM_ON" if animated else "LV_ANIM_OFF",
literal("LV_ANIM_ON" if animated else "LV_ANIM_OFF"),
)
def get_property(self, prop, ltype=None):
ltype = ltype or self.__type_base()
return f"lv_{ltype}_get_{prop}({self.obj})"
return cg.RawExpression(f"lv_{ltype}_get_{prop}({self.obj})")
def set_style(self, prop, value, state):
if value is None:
return []
return lv.call(f"obj_set_style_{prop}", self.obj, value, state)
return
lv.call(f"obj_set_style_{prop}", self.obj, value, state)
def __type_base(self):
wtype = self.type.w_type
@ -140,6 +175,32 @@ class Widget:
return self.type.w_type.value(self)
return self.obj
def get_number_value(self):
value = self.type.mock_obj.get_value(self.obj)
if self.scale == 1.0:
return value
return value / float(self.scale)
def is_selected(self):
"""
Overridable property to determine if the widget is selected. Will be None except
for matrix buttons
:return:
"""
return None
def get_max(self):
return self.type.get_max(self.config)
def get_min(self):
return self.type.get_min(self.config)
def get_step(self):
return self.type.get_step(self.config)
def get_scale(self):
return self.type.get_scale(self.config)
# Map of widgets to their config, used for trigger generation
widget_map: dict[Any, Widget] = {}
@ -161,13 +222,20 @@ def get_widget_generator(wid):
yield
async def get_widget(config: dict, id: str = CONF_ID) -> Widget:
wid = config[id]
async def get_widget_(wid: Widget):
if obj := widget_map.get(wid):
return obj
return await FakeAwaitable(get_widget_generator(wid))
async def get_widgets(config: Union[dict, list], id: str = CONF_ID) -> list[Widget]:
if not config:
return []
if not isinstance(config, list):
config = [config]
return [await get_widget_(c[id]) for c in config if id in c]
def collect_props(config):
"""
Collect all properties from a configuration
@ -175,7 +243,7 @@ def collect_props(config):
:return:
"""
props = {}
for prop in [*ALL_STYLES, *OBJ_FLAGS, CONF_GROUP]:
for prop in [*ALL_STYLES, *OBJ_FLAGS, CONF_STYLES, CONF_GROUP]:
if prop in config:
props[prop] = config[prop]
return props
@ -209,12 +277,39 @@ def collect_parts(config):
async def set_obj_properties(w: Widget, config):
"""Generate a list of C++ statements to apply properties to an lv_obj_t"""
if layout := config.get(CONF_LAYOUT):
layout_type: str = layout[CONF_TYPE]
lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}"))
if layout_type == TYPE_GRID:
wid = config[CONF_ID]
rows = "{" + ",".join(layout[CONF_GRID_ROWS]) + ", LV_GRID_TEMPLATE_LAST}"
row_id = ID(f"{wid}_row_dsc", is_declaration=True, type=lv_coord_t)
row_array = cg.static_const_array(row_id, cg.RawExpression(rows))
w.set_style("grid_row_dsc_array", row_array, 0)
columns = (
"{" + ",".join(layout[CONF_GRID_COLUMNS]) + ", LV_GRID_TEMPLATE_LAST}"
)
column_id = ID(f"{wid}_column_dsc", is_declaration=True, type=lv_coord_t)
column_array = cg.static_const_array(column_id, cg.RawExpression(columns))
w.set_style("grid_column_dsc_array", column_array, 0)
w.set_style(
CONF_GRID_COLUMN_ALIGN, literal(layout.get(CONF_GRID_COLUMN_ALIGN)), 0
)
w.set_style(
CONF_GRID_ROW_ALIGN, literal(layout.get(CONF_GRID_ROW_ALIGN)), 0
)
if layout_type == TYPE_FLEX:
lv_obj.set_flex_flow(w.obj, literal(layout[CONF_FLEX_FLOW]))
main = literal(layout[CONF_FLEX_ALIGN_MAIN])
cross = literal(layout[CONF_FLEX_ALIGN_CROSS])
track = literal(layout[CONF_FLEX_ALIGN_TRACK])
lv_obj.set_flex_align(w.obj, main, cross, track)
parts = collect_parts(config)
for part, states in parts.items():
for state, props in states.items():
lv_state = ConstantLiteral(
f"(int)LV_STATE_{state.upper()}|(int)LV_PART_{part.upper()}"
)
lv_state = join_enums((f"LV_STATE_{state}", f"LV_PART_{part}"))
for style_id in props.get(CONF_STYLES, ()):
lv_obj.add_style(w.obj, MockObj(style_id), lv_state)
for prop, value in {
k: v for k, v in props.items() if k in ALL_STYLES
}.items():
@ -258,14 +353,12 @@ 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 = f"LV_STATE_{key.upper}"
lv.cond_if(lamb)
w.add_state(state)
lv.cond_else()
w.clear_state(state)
lv.cond_endif()
if scrollbar_mode := config.get(CONF_SCROLLBAR_MODE):
lv_obj.set_scrollbar_mode(w.obj, scrollbar_mode)
state = f"LV_STATE_{key.upper()}"
with LvConditional(f"{lamb}()") as cond:
w.add_state(state)
cond.else_()
w.clear_state(state)
await w.set_property(CONF_SCROLLBAR_MODE, config)
async def add_widgets(parent: Widget, config: dict):
@ -280,7 +373,7 @@ async def add_widgets(parent: Widget, config: dict):
await widget_to_code(w_cnfig, w_type, parent.obj)
async def widget_to_code(w_cnfig, w_type, parent):
async def widget_to_code(w_cnfig, w_type: WidgetType, parent):
"""
Converts a Widget definition to C code.
:param w_cnfig: The widget configuration
@ -298,19 +391,33 @@ async def widget_to_code(w_cnfig, w_type, parent):
var = cg.new_Pvariable(wid)
lv_add(var.set_obj(creator))
else:
var = MockObj(wid, "->")
decl = VariableDeclarationExpression(lv_obj_t, "*", wid)
CORE.add_global(decl)
CORE.register_variable(wid, var)
var = lv_Pvariable(lv_obj_t, wid)
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)
w = Widget.create(wid, var, spec, w_cnfig)
if theme := theme_widget_map.get(w_type):
lv_add(CallExpression(theme, w.obj))
await set_obj_properties(w, w_cnfig)
await add_widgets(w, w_cnfig)
await spec.to_code(w, w_cnfig)
lv_scr_act_spec = LvScrActType()
lv_scr_act = Widget.create(
None, ConstantLiteral("lv_scr_act()"), lv_scr_act_spec, {}, parent=None
)
lv_scr_act = Widget.create(None, literal("lv_scr_act()"), lv_scr_act_spec, {})
lv_groups = {} # Widget group names
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] = literal(fullname)
return lv_groups[name]

View File

@ -5,142 +5,229 @@ lvgl:
- touchscreen_id: tft_touch
long_press_repeat_time: 200ms
long_press_time: 500ms
widgets:
- label:
id: hello_label
text: Hello world
text_color: 0xFF8000
align: center
text_font: montserrat_40
border_post: true
- label:
text: "Hello shiny day"
text_color: 0xFFFFFF
align: bottom_mid
text_font: space16
- obj:
align: center
arc_opa: COVER
arc_color: 0xFF0000
arc_rounded: false
arc_width: 3
anim_time: 1s
bg_color: light_blue
bg_grad_color: light_blue
bg_dither_mode: ordered
bg_grad_dir: hor
bg_grad_stop: 128
bg_image_opa: transp
bg_image_recolor: light_blue
bg_image_recolor_opa: 50%
bg_main_stop: 0
bg_opa: 20%
border_color: 0x00FF00
border_opa: cover
border_post: true
border_side: [bottom, left]
border_width: 4
clip_corner: false
height: 50%
image_recolor: light_blue
image_recolor_opa: cover
line_width: 10
line_dash_width: 10
line_dash_gap: 10
line_rounded: false
line_color: light_blue
opa: cover
opa_layered: cover
outline_color: light_blue
outline_opa: cover
outline_pad: 10px
outline_width: 10px
pad_all: 10px
pad_bottom: 10px
pad_column: 10px
pad_left: 10px
pad_right: 10px
pad_row: 10px
pad_top: 10px
shadow_color: light_blue
shadow_ofs_x: 5
shadow_ofs_y: 5
shadow_opa: cover
shadow_spread: 5
shadow_width: 10
text_align: auto
text_color: light_blue
text_decor: [underline, strikethrough]
text_font: montserrat_18
text_letter_space: 4
text_line_space: 4
text_opa: cover
transform_angle: 180
transform_height: 100
transform_pivot_x: 50%
transform_pivot_y: 50%
transform_zoom: 0.5
translate_x: 10
translate_y: 10
max_height: 100
max_width: 200
min_height: 20%
min_width: 20%
radius: circle
width: 10px
x: 100
y: 120
- button:
width: 20%
height: 10%
pressed:
bg_color: light_blue
checkable: true
checked:
bg_color: 0x000000
widgets:
- label:
text: Button
on_click:
lvgl.label.update:
pages:
- id: page1
skip: true
widgets:
- label:
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
text: Hello world
text_color: 0xFF8000
align: center
text_font: montserrat_40
border_post: true
- label:
text: "Hello shiny day"
text_color: 0xFFFFFF
align: bottom_mid
text_font: space16
- obj:
align: center
arc_opa: COVER
arc_color: 0xFF0000
arc_rounded: false
arc_width: 3
anim_time: 1s
bg_color: light_blue
bg_grad_color: light_blue
bg_dither_mode: ordered
bg_grad_dir: hor
bg_grad_stop: 128
bg_image_opa: transp
bg_image_recolor: light_blue
bg_image_recolor_opa: 50%
bg_main_stop: 0
bg_opa: 20%
border_color: 0x00FF00
border_opa: cover
border_post: true
border_side: [bottom, left]
border_width: 4
clip_corner: false
height: 50%
image_recolor: light_blue
image_recolor_opa: cover
line_width: 10
line_dash_width: 10
line_dash_gap: 10
line_rounded: false
line_color: light_blue
opa: cover
opa_layered: cover
outline_color: light_blue
outline_opa: cover
outline_pad: 10px
outline_width: 10px
pad_all: 10px
pad_bottom: 10px
pad_column: 10px
pad_left: 10px
pad_right: 10px
pad_row: 10px
pad_top: 10px
shadow_color: light_blue
shadow_ofs_x: 5
shadow_ofs_y: 5
shadow_opa: cover
shadow_spread: 5
shadow_width: 10
text_align: auto
text_color: light_blue
text_decor: [underline, strikethrough]
text_font: montserrat_18
text_letter_space: 4
text_line_space: 4
text_opa: cover
transform_angle: 180
transform_height: 100
transform_pivot_x: 50%
transform_pivot_y: 50%
transform_zoom: 0.5
translate_x: 10
translate_y: 10
max_height: 100
max_width: 200
min_height: 20%
min_width: 20%
radius: circle
width: 10px
x: 100
y: 120
- button:
width: 20%
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
- led:
color: 0x00FF00
brightness: 50%
align: right_mid
- spinner:
arc_length: 120
spin_time: 2s
align: left_mid
- image:
src: cat_image
align: top_left
y: 50
- id: page2
widgets:
- arc:
align: left_mid
id: lv_arc
adjustable: true
on_value:
then:
- logger.log:
format: "Arc value is %f"
args: [x]
group: general
scroll_on_focus: true
value: 75
min_value: 1
max_value: 100
arc_color: 0xFF0000
indicator:
arc_color: 0xF000FF
pressed:
arc_color: 0xFFFF00
focused:
arc_color: 0x808080
- bar:
id: bar_id
align: top_mid
y: 20
value: 30
max_value: 100
min_value: 10
mode: range
on_click:
then:
- lvgl.bar.update:
id: bar_id
value: !lambda return (int)((float)rand() / RAND_MAX * 100);
- logger.log:
format: "bar value %f"
args: [x]
- line:
align: center
points:
- 5, 5
- 70, 70
- 120, 10
- 180, 60
- 240, 10
on_click:
lvgl.page.next:
- switch:
align: right_mid
- checkbox:
text: Checkbox
align: bottom_right
- slider:
id: slider_id
align: top_mid
y: 40
value: 30
max_value: 100
min_value: 10
mode: normal
on_value:
then:
- logger.log:
format: "slider value %f"
args: [x]
on_click:
then:
- lvgl.slider.update:
id: slider_id
value: !lambda return (int)((float)rand() / RAND_MAX * 100);
font:
- file: "gfonts://Roboto"
id: space16
bpp: 4
image:
- id: cat_img
- id: cat_image
resize: 256x48
file: $component_dir/logo-text.svg
- id: dog_img