[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_TRIGGER_ID,
CONF_TYPE, CONF_TYPE,
) )
from esphome.core import CORE, ID, Lambda from esphome.core import CORE, ID
from esphome.cpp_generator import MockObj from esphome.cpp_generator import MockObj
from esphome.final_validate import full_config from esphome.final_validate import full_config
from esphome.helpers import write_file_if_changed from esphome.helpers import write_file_if_changed
from . import defines as df, helpers, lv_validation as lvalid 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 .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 .label import label_spec
from .lv_validation import lv_images_used from .led import led_spec
from .lvcode import LvContext 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 .obj import obj_spec
from .page import add_pages, page_spec
from .rotary_encoders import ROTARY_ENCODER_CONFIG, rotary_encoders_to_code 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 .touchscreens import touchscreen_schema, touchscreens_to_code
from .trigger import generate_triggers from .trigger import generate_triggers
from .types import ( from .types import (
WIDGET_TYPES,
FontEngine, FontEngine,
IdleTrigger, IdleTrigger,
LvglComponent,
ObjUpdateAction, ObjUpdateAction,
lv_font_t, lv_font_t,
lv_style_t,
lvgl_ns, lvgl_ns,
) )
from .widget import Widget, add_widgets, lv_scr_act, set_obj_properties from .widget import Widget, add_widgets, lv_scr_act, set_obj_properties
DOMAIN = "lvgl" DOMAIN = "lvgl"
DEPENDENCIES = ("display",) DEPENDENCIES = ["display"]
AUTO_LOAD = ("key_provider",) AUTO_LOAD = ["key_provider"]
CODEOWNERS = ("@clydebarrow",) CODEOWNERS = ["@clydebarrow"]
LOGGER = logging.getLogger(__name__) 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_TYPES[w_type.name] = w_type
WIDGET_SCHEMA = any_widget_schema() 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(): for w_type in WIDGET_TYPES.values():
register_action( register_action(
f"lvgl.{w_type.name}.update", f"lvgl.{w_type.name}.update",
@ -61,14 +108,6 @@ for w_type in WIDGET_TYPES.values():
)(update_to_code) )(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 lv_defines = {} # Dict of #defines to provide as build flags
@ -100,6 +139,9 @@ def generate_lv_conf_h():
def final_validation(config): 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() global_config = full_config.get()
for display_id in config[df.CONF_DISPLAYS]: for display_id in config[df.CONF_DISPLAYS]:
path = global_config.get_path_for_id(display_id)[:-1] path = global_config.get_path_for_id(display_id)[:-1]
@ -193,18 +235,23 @@ async def to_code(config):
else: else:
add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font)) 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 touchscreens_to_code(lv_component, config)
await rotary_encoders_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 set_obj_properties(lv_scr_act, config)
await add_widgets(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() Widget.set_completed()
await generate_triggers(lv_component) await generate_triggers(lv_component)
for conf in config.get(CONF_ON_IDLE, ()): for conf in config.get(CONF_ON_IDLE, ()):
templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32) templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32)
idle_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, templ) idle_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, templ)
await build_automation(idle_trigger, [], conf) await build_automation(idle_trigger, [], conf)
await add_init_lambda(lv_component, LvContext.get_code())
for comp in helpers.lvgl_components_required: for comp in helpers.lvgl_components_required:
CORE.add_define(f"USE_LVGL_{comp.upper()}") CORE.add_define(f"USE_LVGL_{comp.upper()}")
for use in helpers.lv_uses: for use in helpers.lv_uses:
@ -239,6 +286,16 @@ CONFIG_SCHEMA = (
cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of( cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of(
"big_endian", "little_endian" "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.Optional(CONF_ON_IDLE): validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger), 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_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_TOUCHSCREENS): touchscreen_schema,
cv.GenerateID(df.CONF_ROTARY_ENCODERS): ROTARY_ENCODER_CONFIG, 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)) ).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 from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_TIMEOUT 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 esphome.cpp_types import nullptr
from .defines import CONF_LVGL_ID, CONF_SHOW_SNOW, literal from .defines import (
from .lv_validation import lv_bool 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 ( from .lvcode import (
LVGL_COMP_ARG,
LambdaContext, LambdaContext,
LocalVariable,
LvConditional,
LvglComponent,
ReturnStatement, ReturnStatement,
add_line_marks, add_line_marks,
lv, lv,
@ -17,46 +28,46 @@ from .lvcode import (
lv_obj, lv_obj,
lvgl_comp, lvgl_comp,
) )
from .schemas import ACTION_SCHEMA, LVGL_SCHEMA from .schemas import DISP_BG_SCHEMA, LIST_ACTION_SCHEMA, LVGL_SCHEMA
from .types import ( from .types import (
LV_EVENT,
LV_STATE,
LvglAction, LvglAction,
LvglComponent,
LvglComponentPtr,
LvglCondition, LvglCondition,
ObjUpdateAction, ObjUpdateAction,
lv_disp_t,
lv_obj_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): async def action_to_code(
with LambdaContext() as context: widgets: list[Widget],
lv.cond_if(widget.obj == nullptr) action: Callable[[Widget], Awaitable[None]],
lv_add(RawStatement(" return;")) action_id,
lv.cond_endif() template_arg,
code = context.get_code() args,
code.extend(action) ):
action = "\n".join(code) + "\n\n" async with LambdaContext(parameters=args, where=action_id) as context:
lamb = await cg.process_lambda(Lambda(action), args) for widget in widgets:
var = cg.new_Pvariable(action_id, template_arg, lamb) with LvConditional(widget.obj != nullptr):
await action(widget)
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
return var return var
async def update_to_code(config, action_id, template_arg, args): async def update_to_code(config, action_id, template_arg, args):
if config is not None: async def do_update(widget: Widget):
widget = await get_widget(config) await set_obj_properties(widget, config)
with LambdaContext() as context: await widget.type.to_code(widget, config)
add_line_marks(action_id) if (
await set_obj_properties(widget, config) widget.type.w_type.value_property is not None
await widget.type.to_code(widget, config) and widget.type.w_type.value_property in config
if ( ):
widget.type.w_type.value_property is not None lv.event_send(widget.obj, LV_EVENT.VALUE_CHANGED, nullptr)
and widget.type.w_type.value_property in config
): widgets = await get_widgets(config[CONF_ID])
lv.event_send(widget.obj, literal("LV_EVENT_VALUE_CHANGED"), nullptr) return await action_to_code(widgets, do_update, action_id, template_arg, args)
return await action_to_code(
context.get_code(), action_id, widget, template_arg, args
)
@automation.register_condition( @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): async def lvgl_is_paused(config, condition_id, template_arg, args):
lvgl = config[CONF_LVGL_ID] lvgl = config[CONF_LVGL_ID]
with LambdaContext( async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context:
[(LvglComponentPtr, "lvgl_comp")], return_type=cg.bool_
) as context:
lv_add(ReturnStatement(lvgl_comp.is_paused())) lv_add(ReturnStatement(lvgl_comp.is_paused()))
var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda()) var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda())
await cg.register_parented(var, lvgl) 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): async def lvgl_is_idle(config, condition_id, template_arg, args):
lvgl = config[CONF_LVGL_ID] lvgl = config[CONF_LVGL_ID]
timeout = await cg.templatable(config[CONF_TIMEOUT], [], cg.uint32) timeout = await cg.templatable(config[CONF_TIMEOUT], [], cg.uint32)
with LambdaContext( async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context:
[(LvglComponentPtr, "lvgl_comp")], return_type=cg.bool_
) as context:
lv_add(ReturnStatement(lvgl_comp.is_idle(timeout))) lv_add(ReturnStatement(lvgl_comp.is_idle(timeout)))
var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda()) var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda())
await cg.register_parented(var, lvgl) await cg.register_parented(var, lvgl)
return var 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( @automation.register_action(
"lvgl.widget.redraw", "lvgl.widget.redraw",
ObjUpdateAction, 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): async def obj_invalidate_to_code(config, action_id, template_arg, args):
if CONF_ID in config: widgets = await get_widgets(config) or [lv_scr_act]
w = await get_widget(config)
else: async def do_invalidate(widget: Widget):
w = lv_scr_act lv_obj.invalidate(widget.obj)
with LambdaContext() as context:
add_line_marks(action_id) return await action_to_code(widgets, do_invalidate, action_id, template_arg, args)
lv_obj.invalidate(w.obj)
return await action_to_code(context.get_code(), action_id, w, 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( @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): async def pause_action_to_code(config, action_id, template_arg, args):
with LambdaContext([(LvglComponentPtr, "lvgl_comp")]) as context: async with LambdaContext(LVGL_COMP_ARG) as context:
add_line_marks(action_id) add_line_marks(where=action_id)
lv_add(lvgl_comp.set_paused(True, config[CONF_SHOW_SNOW])) lv_add(lvgl_comp.set_paused(True, config[CONF_SHOW_SNOW]))
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
await cg.register_parented(var, config[CONF_ID]) 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): async def resume_action_to_code(config, action_id, template_arg, args):
with LambdaContext([(LvglComponentPtr, "lvgl_comp")]) as context: async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context:
add_line_marks(action_id)
lv_add(lvgl_comp.set_paused(False, False)) lv_add(lvgl_comp.set_paused(False, False))
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
await cg.register_parented(var, config[CONF_ID]) await cg.register_parented(var, config[CONF_ID])
return var 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): async def obj_disable_to_code(config, action_id, template_arg, args):
w = await get_widget(config) async def do_disable(widget: Widget):
with LambdaContext() as context: widget.add_state(LV_STATE.DISABLED)
add_line_marks(action_id)
w.add_state("LV_STATE_DISABLED") return await action_to_code(
return await action_to_code(context.get_code(), action_id, w, template_arg, args) 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): async def obj_enable_to_code(config, action_id, template_arg, args):
w = await get_widget(config) async def do_enable(widget: Widget):
with LambdaContext() as context: widget.clear_state(LV_STATE.DISABLED)
add_line_marks(action_id)
w.clear_state("LV_STATE_DISABLED") return await action_to_code(
return await action_to_code(context.get_code(), action_id, w, template_arg, args) 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): async def obj_hide_to_code(config, action_id, template_arg, args):
w = await get_widget(config) async def do_hide(widget: Widget):
with LambdaContext() as context: widget.add_flag("LV_OBJ_FLAG_HIDDEN")
add_line_marks(action_id)
w.add_flag("LV_OBJ_FLAG_HIDDEN") return await action_to_code(
return await action_to_code(context.get_code(), action_id, w, template_arg, args) 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): async def obj_show_to_code(config, action_id, template_arg, args):
w = await get_widget(config) async def do_show(widget: Widget):
with LambdaContext() as context: widget.clear_flag("LV_OBJ_FLAG_HIDDEN")
add_line_marks(action_id)
w.clear_flag("LV_OBJ_FLAG_HIDDEN") return await action_to_code(
return await action_to_code(context.get_code(), action_id, w, template_arg, args) 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.const import CONF_BUTTON
from esphome.cpp_generator import MockObjClass
from .defines import CONF_MAIN from .defines import CONF_MAIN
from .types import LvBoolean, WidgetType from .types import LvBoolean, WidgetType
lv_btn_t = LvBoolean("lv_btn_t")
class BtnType(WidgetType): class BtnType(WidgetType):
def __init__(self): def __init__(self):
super().__init__(CONF_BUTTON, LvBoolean("lv_btn_t"), (CONF_MAIN,)) super().__init__(CONF_BUTTON, lv_btn_t, (CONF_MAIN,), lv_name="btn")
def obj_creator(self, parent: MockObjClass, config: dict):
"""
LVGL 8 calls buttons `btn`
"""
return f"lv_btn_create({parent})"
def get_uses(self): def get_uses(self):
return ("btn",) 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 import codegen as cg, config_validation as cv
from esphome.core import ID, Lambda 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.cpp_types import uint32
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from .helpers import requires_component from .helpers import requires_component
lvgl_ns = cg.esphome_ns.namespace("lvgl")
class ConstantLiteral(Literal):
__slots__ = ("constant",)
def __init__(self, constant: str):
super().__init__()
self.constant = constant
def __str__(self):
return self.constant
def literal(arg: Union[str, ConstantLiteral]): def literal(arg):
if isinstance(arg, str): if isinstance(arg, str):
return ConstantLiteral(arg) return MockObj(arg)
return arg return arg
@ -93,15 +82,23 @@ class LvConstant(LValidator):
return self.prefix + cv.one_of(*choices, upper=True)(value) return self.prefix + cv.one_of(*choices, upper=True)(value)
super().__init__(validator, rtype=uint32) super().__init__(validator, rtype=uint32)
self.retmapper = self.mapper
self.one_of = LValidator(validator, uint32, retmapper=self.mapper) self.one_of = LValidator(validator, uint32, retmapper=self.mapper)
self.several_of = LValidator( self.several_of = LValidator(
cv.ensure_list(self.one_of), uint32, retmapper=self.mapper cv.ensure_list(self.one_of), uint32, retmapper=self.mapper
) )
def mapper(self, value, args=()): def mapper(self, value, args=()):
if isinstance(value, list): if not isinstance(value, list):
value = "|".join(value) value = [value]
return ConstantLiteral(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): def extend(self, *choices):
""" """
@ -112,9 +109,6 @@ class LvConstant(LValidator):
return LvConstant(self.prefix, *(self.choices + choices)) return LvConstant(self.prefix, *(self.choices + choices))
# Widgets
CONF_LABEL = "label"
# Parts # Parts
CONF_MAIN = "main" CONF_MAIN = "main"
CONF_SCROLLBAR = "scrollbar" CONF_SCROLLBAR = "scrollbar"
@ -123,10 +117,15 @@ CONF_KNOB = "knob"
CONF_SELECTED = "selected" CONF_SELECTED = "selected"
CONF_ITEMS = "items" CONF_ITEMS = "items"
CONF_TICKS = "ticks" CONF_TICKS = "ticks"
CONF_TICK_STYLE = "tick_style"
CONF_CURSOR = "cursor" CONF_CURSOR = "cursor"
CONF_TEXTAREA_PLACEHOLDER = "textarea_placeholder" 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)) + [ LV_FONTS = list(f"montserrat_{s}" for s in range(8, 50, 2)) + [
"dejavu_16_persian_hebrew", "dejavu_16_persian_hebrew",
"simsun_16_cjk", "simsun_16_cjk",
@ -134,7 +133,7 @@ LV_FONTS = list(f"montserrat_{s}" for s in range(8, 50, 2)) + [
"unscii_16", "unscii_16",
] ]
LV_EVENT = { LV_EVENT_MAP = {
"PRESS": "PRESSED", "PRESS": "PRESSED",
"SHORT_CLICK": "SHORT_CLICKED", "SHORT_CLICK": "SHORT_CLICKED",
"LONG_PRESS": "LONG_PRESSED", "LONG_PRESS": "LONG_PRESSED",
@ -150,7 +149,7 @@ LV_EVENT = {
"CANCEL": "CANCEL", "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( LV_ANIM = LvConstant(
@ -305,7 +304,8 @@ OBJ_FLAGS = (
ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL") ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL")
BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE") BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE")
BTNMATRIX_CTRLS = ( BTNMATRIX_CTRLS = LvConstant(
"LV_BTNMATRIX_CTRL_",
"HIDDEN", "HIDDEN",
"NO_REPEAT", "NO_REPEAT",
"DISABLED", "DISABLED",
@ -366,7 +366,6 @@ CONF_ACCEPTED_CHARS = "accepted_chars"
CONF_ADJUSTABLE = "adjustable" CONF_ADJUSTABLE = "adjustable"
CONF_ALIGN = "align" CONF_ALIGN = "align"
CONF_ALIGN_TO = "align_to" CONF_ALIGN_TO = "align_to"
CONF_ANGLE_RANGE = "angle_range"
CONF_ANIMATED = "animated" CONF_ANIMATED = "animated"
CONF_ANIMATION = "animation" CONF_ANIMATION = "animation"
CONF_ANTIALIAS = "antialias" CONF_ANTIALIAS = "antialias"
@ -384,8 +383,6 @@ CONF_BYTE_ORDER = "byte_order"
CONF_CHANGE_RATE = "change_rate" CONF_CHANGE_RATE = "change_rate"
CONF_CLOSE_BUTTON = "close_button" CONF_CLOSE_BUTTON = "close_button"
CONF_COLOR_DEPTH = "color_depth" CONF_COLOR_DEPTH = "color_depth"
CONF_COLOR_END = "color_end"
CONF_COLOR_START = "color_start"
CONF_CONTROL = "control" CONF_CONTROL = "control"
CONF_DEFAULT = "default" CONF_DEFAULT = "default"
CONF_DEFAULT_FONT = "default_font" CONF_DEFAULT_FONT = "default_font"
@ -414,9 +411,7 @@ CONF_GRID_ROW_ALIGN = "grid_row_align"
CONF_GRID_ROWS = "grid_rows" CONF_GRID_ROWS = "grid_rows"
CONF_HEADER_MODE = "header_mode" CONF_HEADER_MODE = "header_mode"
CONF_HOME = "home" CONF_HOME = "home"
CONF_INDICATORS = "indicators"
CONF_KEY_CODE = "key_code" CONF_KEY_CODE = "key_code"
CONF_LABEL_GAP = "label_gap"
CONF_LAYOUT = "layout" CONF_LAYOUT = "layout"
CONF_LEFT_BUTTON = "left_button" CONF_LEFT_BUTTON = "left_button"
CONF_LINE_WIDTH = "line_width" 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_LONG_PRESS_REPEAT_TIME = "long_press_repeat_time"
CONF_LVGL_ID = "lvgl_id" CONF_LVGL_ID = "lvgl_id"
CONF_LONG_MODE = "long_mode" CONF_LONG_MODE = "long_mode"
CONF_MAJOR = "major"
CONF_MSGBOXES = "msgboxes" CONF_MSGBOXES = "msgboxes"
CONF_OBJ = "obj" CONF_OBJ = "obj"
CONF_OFFSET_X = "offset_x" CONF_OFFSET_X = "offset_x"
@ -434,6 +428,7 @@ CONF_ONE_LINE = "one_line"
CONF_ON_SELECT = "on_select" CONF_ON_SELECT = "on_select"
CONF_ONE_CHECKED = "one_checked" CONF_ONE_CHECKED = "one_checked"
CONF_NEXT = "next" CONF_NEXT = "next"
CONF_PAGE = "page"
CONF_PAGE_WRAP = "page_wrap" CONF_PAGE_WRAP = "page_wrap"
CONF_PASSWORD_MODE = "password_mode" CONF_PASSWORD_MODE = "password_mode"
CONF_PIVOT_X = "pivot_x" CONF_PIVOT_X = "pivot_x"
@ -442,14 +437,12 @@ CONF_PLACEHOLDER_TEXT = "placeholder_text"
CONF_POINTS = "points" CONF_POINTS = "points"
CONF_PREVIOUS = "previous" CONF_PREVIOUS = "previous"
CONF_REPEAT_COUNT = "repeat_count" CONF_REPEAT_COUNT = "repeat_count"
CONF_R_MOD = "r_mod"
CONF_RECOLOR = "recolor" CONF_RECOLOR = "recolor"
CONF_RIGHT_BUTTON = "right_button" CONF_RIGHT_BUTTON = "right_button"
CONF_ROLLOVER = "rollover" CONF_ROLLOVER = "rollover"
CONF_ROOT_BACK_BTN = "root_back_btn" CONF_ROOT_BACK_BTN = "root_back_btn"
CONF_ROTARY_ENCODERS = "rotary_encoders" CONF_ROTARY_ENCODERS = "rotary_encoders"
CONF_ROWS = "rows" CONF_ROWS = "rows"
CONF_SCALES = "scales"
CONF_SCALE_LINES = "scale_lines" CONF_SCALE_LINES = "scale_lines"
CONF_SCROLLBAR_MODE = "scrollbar_mode" CONF_SCROLLBAR_MODE = "scrollbar_mode"
CONF_SELECTED_INDEX = "selected_index" CONF_SELECTED_INDEX = "selected_index"
@ -459,8 +452,9 @@ CONF_SRC = "src"
CONF_START_ANGLE = "start_angle" CONF_START_ANGLE = "start_angle"
CONF_START_VALUE = "start_value" CONF_START_VALUE = "start_value"
CONF_STATES = "states" CONF_STATES = "states"
CONF_STRIDE = "stride"
CONF_STYLE = "style" CONF_STYLE = "style"
CONF_STYLES = "styles"
CONF_STYLE_DEFINITIONS = "style_definitions"
CONF_STYLE_ID = "style_id" CONF_STYLE_ID = "style_id"
CONF_SKIP = "skip" CONF_SKIP = "skip"
CONF_SYMBOL = "symbol" CONF_SYMBOL = "symbol"
@ -505,4 +499,4 @@ DEFAULT_ESPHOME_FONT = "esphome_lv_default_font"
def join_enums(enums, prefix=""): 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 import re
from esphome import config_validation as cv from esphome import config_validation as cv
from esphome.config import Config
from esphome.const import CONF_ARGS, CONF_FORMAT from esphome.const import CONF_ARGS, CONF_FORMAT
from esphome.core import CORE, ID
from esphome.yaml_util import ESPHomeDataBase
lv_uses = { lv_uses = {
"USER_DATA", "USER_DATA",
@ -44,23 +41,6 @@ def validate_printf(value):
return 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 requires_component(comp):
def validator(value): def validator(value):
lvgl_components_required.add(comp) 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 import esphome.config_validation as cv
from .defines import ( from .defines import (
CONF_LABEL,
CONF_LONG_MODE, CONF_LONG_MODE,
CONF_MAIN, CONF_MAIN,
CONF_RECOLOR, CONF_RECOLOR,
@ -15,6 +14,8 @@ from .schemas import TEXT_SCHEMA
from .types import LvText, WidgetType from .types import LvText, WidgetType
from .widget import Widget from .widget import Widget
CONF_LABEL = "label"
class LabelType(WidgetType): class LabelType(WidgetType):
def __init__(self): def __init__(self):
@ -33,9 +34,9 @@ class LabelType(WidgetType):
async def to_code(self, w: Widget, config): async def to_code(self, w: Widget, config):
"""For a text object, create and set text""" """For a text object, create and set text"""
if value := config.get(CONF_TEXT): if value := config.get(CONF_TEXT):
w.set_property(CONF_TEXT, await lv_text.process(value)) await w.set_property(CONF_TEXT, await lv_text.process(value))
w.set_property(CONF_LONG_MODE, config) await w.set_property(CONF_LONG_MODE, config)
w.set_property(CONF_RECOLOR, config) await w.set_property(CONF_RECOLOR, config)
label_spec = LabelType() 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 import esphome.codegen as cg
from esphome.components.binary_sensor import BinarySensor from esphome.components.binary_sensor import BinarySensor
from esphome.components.color import ColorStruct 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.sensor import Sensor
from esphome.components.text_sensor import TextSensor from esphome.components.text_sensor import TextSensor
import esphome.config_validation as cv 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.core import HexInt
from esphome.cpp_generator import MockObj from esphome.cpp_generator import MockObj
from esphome.cpp_types import uint32 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 esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from . import types as ty 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 ( from .helpers import (
esphome_fonts_used, esphome_fonts_used,
lv_fonts_used, lv_fonts_used,
@ -60,6 +69,13 @@ def color_retmapper(value):
return lv_expr.color_from(MockObj(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) 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): def lvms_validator_(value):
if value == "never": if value == "never":
value = "2147483647ms" value = "2147483647ms"
@ -189,13 +211,16 @@ class TextValidator(LValidator):
args = [str(x) for x in value[CONF_ARGS]] args = [str(x) for x in value[CONF_ARGS]]
arg_expr = cg.RawExpression(",".join(args)) arg_expr = cg.RawExpression(",".join(args))
format_str = cpp_string_escape(value[CONF_FORMAT]) 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) return await super().process(value, args)
lv_text = TextValidator() lv_text = TextValidator()
lv_float = LValidator(cv.float_, cg.float_, Sensor, "get_state()") lv_float = LValidator(cv.float_, cg.float_, Sensor, "get_state()")
lv_int = LValidator(cv.int_, cg.int_, 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): def is_lv_font(font):
@ -222,8 +247,33 @@ class LvFont(LValidator):
async def process(self, value, args=()): async def process(self, value, args=()):
if is_lv_font(value): if is_lv_font(value):
return ConstantLiteral(f"&lv_font_{value}") return literal(f"&lv_font_{value}")
return ConstantLiteral(f"{value}_engine->get_lv_font()") return literal(f"{value}_engine->get_lv_font()")
lv_font = LvFont() 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 abc
import logging
from typing import Union from typing import Union
from esphome import codegen as cg 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 ( from esphome.cpp_generator import (
AssignmentExpression, AssignmentExpression,
CallExpression, CallExpression,
@ -18,12 +18,47 @@ from esphome.cpp_generator import (
VariableDeclarationExpression, VariableDeclarationExpression,
statement, statement,
) )
from esphome.yaml_util import ESPHomeDataBase
from .defines import ConstantLiteral from .defines import literal, lvgl_ns
from .helpers import get_line_marks
from .types import lv_group_t
_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): class CodeContext(abc.ABC):
@ -39,6 +74,16 @@ class CodeContext(abc.ABC):
def add(self, expression: Union[Expression, Statement]): def add(self, expression: Union[Expression, Statement]):
pass pass
@staticmethod
def start_block():
CodeContext.append(RawStatement("{"))
CodeContext.code_context.indent()
@staticmethod
def end_block():
CodeContext.code_context.detent()
CodeContext.append(RawStatement("}"))
@staticmethod @staticmethod
def append(expression: Union[Expression, Statement]): def append(expression: Union[Expression, Statement]):
if CodeContext.code_context is not None: if CodeContext.code_context is not None:
@ -47,14 +92,25 @@ class CodeContext(abc.ABC):
def __init__(self): def __init__(self):
self.previous: Union[CodeContext | None] = None self.previous: Union[CodeContext | None] = None
self.indent_level = 0
def __enter__(self): async def __aenter__(self):
self.previous = CodeContext.code_context self.previous = CodeContext.code_context
CodeContext.code_context = self CodeContext.code_context = self
return self
def __exit__(self, *args): async def __aexit__(self, *args):
CodeContext.code_context = self.previous 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): class MainContext(CodeContext):
""" """
@ -62,42 +118,7 @@ class MainContext(CodeContext):
""" """
def add(self, expression: Union[Expression, Statement]): def add(self, expression: Union[Expression, Statement]):
return cg.add(expression) return cg.add(self.indented_statement(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}", "")
class LambdaContext(CodeContext): class LambdaContext(CodeContext):
@ -110,21 +131,23 @@ class LambdaContext(CodeContext):
parameters: list[tuple[SafeExpType, str]] = None, parameters: list[tuple[SafeExpType, str]] = None,
return_type: SafeExpType = cg.void, return_type: SafeExpType = cg.void,
capture: str = "", capture: str = "",
where=None,
): ):
super().__init__() super().__init__()
self.code_list: list[Statement] = [] self.code_list: list[Statement] = []
self.parameters = parameters self.parameters = parameters or []
self.return_type = return_type self.return_type = return_type
self.capture = capture self.capture = capture
self.where = where
def add(self, expression: Union[Expression, Statement]): def add(self, expression: Union[Expression, Statement]):
self.code_list.append(expression) self.code_list.append(self.indented_statement(expression))
return expression return expression
async def get_lambda(self) -> LambdaExpression: async def get_lambda(self) -> LambdaExpression:
code_text = self.get_code() code_text = self.get_code()
return await cg.process_lambda( return await cg.process_lambda(
Lambda("\n".join(code_text) + "\n\n"), Lambda("\n".join(code_text) + "\n"),
self.parameters, self.parameters,
capture=self.capture, capture=self.capture,
return_type=self.return_type, return_type=self.return_type,
@ -138,33 +161,59 @@ class LambdaContext(CodeContext):
code_text.append(text) code_text.append(text)
return code_text return code_text
def __enter__(self): async def __aenter__(self):
super().__enter__() await super().__aenter__()
add_line_marks(self.where)
return self 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): class LocalVariable(MockObj):
""" """
Create a local variable and enclose the code using it within a block. Create a local variable and enclose the code using it within a block.
""" """
def __init__(self, name, type, modifier=None, rhs=None): def __init__(self, name, type, rhs=None, modifier="*"):
base = ID(name, True, type) base = ID(name + "_VAR_", True, type)
super().__init__(base, "") super().__init__(base, "")
self.modifier = modifier self.modifier = modifier
self.rhs = rhs self.rhs = rhs
def __enter__(self): def __enter__(self):
CodeContext.append(RawStatement("{")) CodeContext.start_block()
CodeContext.append( CodeContext.append(
VariableDeclarationExpression(self.base.type, self.modifier, self.base.id) VariableDeclarationExpression(self.base.type, self.modifier, self.base.id)
) )
if self.rhs is not None: if self.rhs is not None:
CodeContext.append(AssignmentExpression(None, "", self.base, self.rhs)) CodeContext.append(AssignmentExpression(None, "", self.base, self.rhs))
return self.base return MockObj(self.base)
def __exit__(self, *args): def __exit__(self, *args):
CodeContext.append(RawStatement("}")) CodeContext.end_block()
class MockLv: class MockLv:
@ -199,14 +248,27 @@ class MockLv:
self.append(result) self.append(result)
return 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 {")) CodeContext.append(RawStatement("} else {"))
CodeContext.code_context.indent()
def cond_endif(self):
CodeContext.append(RawStatement("}"))
class ReturnStatement(ExpressionStatement): class ReturnStatement(ExpressionStatement):
@ -228,36 +290,56 @@ lv = MockLv("lv_")
lv_expr = LvExpr("lv_") lv_expr = LvExpr("lv_")
# Mock for lv_obj_ calls # Mock for lv_obj_ calls
lv_obj = MockLv("lv_obj_") 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]): def lv_add(expression: Union[Expression, Statement]):
return CodeContext.append(expression) return CodeContext.append(expression)
def add_line_marks(where): 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): for mark in get_line_marks(where):
lv_add(cg.RawStatement(mark)) lv_add(cg.RawStatement(mark))
def lv_assign(target, expression): 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): def lv_variable(type, name):
if name is None: """
return None Create but do not initialise a variable
fullname = f"lv_esp_group_{name}" :param type: Type of the variable target
if name not in lv_groups: :param name: name of the variable, or an ID
gid = ID(fullname, True, type=lv_group_t.operator("ptr")) :return: A MockObj of the variable
lv_add( """
AssignmentExpression( if isinstance(name, str):
type_=gid.type, modifier="", name=fullname, rhs=lv_expr.group_create() name = ID(name, True, type)
) decl = VariableDeclarationExpression(type, "", name)
) CORE.add_global(decl)
lv_groups[name] = ConstantLiteral(fullname) var = MockObj(name, ".")
return lv_groups[name] CORE.register_variable(name, var)
return var

View file

@ -9,8 +9,72 @@ namespace esphome {
namespace lvgl { namespace lvgl {
static const char *const TAG = "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 lv_event_code_t lv_custom_event; // NOLINT
void LvglComponent::dump_config() { ESP_LOGCONFIG(TAG, "LVGL:"); } 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) { void LvglComponent::draw_buffer_(const lv_area_t *area, const uint8_t *ptr) {
for (auto *display : this->displays_) { for (auto *display : this->displays_) {
display->draw_pixels_at(area->x1, area->y1, lv_area_get_width(area), lv_area_get_height(area), ptr, 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); 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_() { void LvglComponent::write_random_() {
// length of 2 lines in 32 bit units // length of 2 lines in 32 bit units
@ -97,9 +271,24 @@ void LvglComponent::setup() {
this->disp_ = lv_disp_drv_register(&this->disp_drv_); this->disp_ = lv_disp_drv_register(&this->disp_drv_);
for (const auto &v : this->init_lambdas_) for (const auto &v : this->init_lambdas_)
v(this); v(this);
this->show_page(0, LV_SCR_LOAD_ANIM_NONE, 0);
lv_disp_trig_activity(this->disp_); lv_disp_trig_activity(this->disp_);
ESP_LOGCONFIG(TAG, "LVGL Setup complete"); 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 #ifdef USE_LVGL_IMAGE
lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc) { 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; 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 #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 lvgl
} // namespace esphome } // namespace esphome

View file

@ -18,7 +18,6 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <lvgl.h> #include <lvgl.h>
#include <utility>
#include <vector> #include <vector>
#ifdef USE_LVGL_IMAGE #ifdef USE_LVGL_IMAGE
#include "esphome/components/image/image.h" #include "esphome/components/image/image.h"
@ -31,6 +30,10 @@
#include "esphome/components/touchscreen/touchscreen.h" #include "esphome/components/touchscreen/touchscreen.h"
#endif // USE_LVGL_TOUCHSCREEN #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 esphome {
namespace lvgl { namespace lvgl {
@ -47,12 +50,25 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT
#endif // LV_COLOR_DEPTH #endif // LV_COLOR_DEPTH
// Parent class for things that wrap an LVGL object // Parent class for things that wrap an LVGL object
class LvCompound final { class LvCompound {
public: 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{}; 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 LvLambdaType = std::function<void(lv_obj_t *)>;
using set_value_lambda_t = std::function<void(float)>; using set_value_lambda_t = std::function<void(float)>;
using event_callback_t = void(_lv_event_t *); 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); lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc = nullptr);
#endif // USE_LVGL_IMAGE #endif // USE_LVGL_IMAGE
#ifdef USE_LVGL_ANIMIMG
void lv_animimg_stop(lv_obj_t *obj);
#endif // USE_LVGL_ANIMIMG
class LvglComponent : public PollingComponent { class LvglComponent : public PollingComponent {
constexpr static const char *const TAG = "lvgl"; constexpr static const char *const TAG = "lvgl";
public: public:
static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { 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);
}
float get_setup_priority() const override { return setup_priority::PROCESSOR; } 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 setup() override;
void update() override;
void update() override { void loop() override;
// update indicators
if (this->paused_) {
return;
}
this->idle_callbacks_.call(lv_disp_get_inactive_time(this->disp_));
}
void loop() override {
if (this->paused_) {
if (this->show_snow_)
this->write_random_();
}
lv_timer_handler_run_in_period(5);
}
void add_on_idle_callback(std::function<void(uint32_t)> &&callback) { void add_on_idle_callback(std::function<void(uint32_t)> &&callback) {
this->idle_callbacks_.add(std::move(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; } 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; } void set_buffer_frac(size_t frac) { this->buffer_frac_ = frac; }
lv_disp_t *get_disp() { return this->disp_; } lv_disp_t *get_disp() { return this->disp_; }
void set_paused(bool paused, bool show_snow) { void set_paused(bool paused, bool show_snow);
this->paused_ = paused; void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event);
this->show_snow_ = show_snow; void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2);
this->snow_line_ = 0;
if (!paused && lv_scr_act() != nullptr) {
lv_disp_trig_activity(this->disp_); // resets the inactivity time
lv_obj_invalidate(lv_scr_act());
}
}
void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) {
lv_obj_add_event_cb(obj, callback, event, this);
if (event == LV_EVENT_VALUE_CHANGED) {
lv_obj_add_event_cb(obj, callback, lv_custom_event, this);
}
}
bool is_paused() const { return this->paused_; } 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: protected:
void write_random_(); void write_random_();
@ -168,8 +148,11 @@ class LvglComponent : public PollingComponent {
lv_disp_drv_t disp_drv_{}; lv_disp_drv_t disp_drv_{};
lv_disp_t *disp_{}; lv_disp_t *disp_{};
bool paused_{}; bool paused_{};
std::vector<LvPageType *> pages_{};
size_t current_page_{0};
bool show_snow_{}; bool show_snow_{};
lv_coord_t snow_line_{}; lv_coord_t snow_line_{};
bool page_wrap_{true};
std::vector<std::function<void(LvglComponent *lv_component)>> init_lambdas_; std::vector<std::function<void(LvglComponent *lv_component)>> init_lambdas_;
CallbackManager<void(uint32_t)> idle_callbacks_{}; CallbackManager<void(uint32_t)> idle_callbacks_{};
@ -179,16 +162,7 @@ class LvglComponent : public PollingComponent {
class IdleTrigger : public Trigger<> { class IdleTrigger : public Trigger<> {
public: public:
explicit IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeout) : timeout_(std::move(timeout)) { explicit IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeout);
parent->add_on_idle_callback([this](uint32_t idle_time) {
if (!this->is_idle_ && idle_time > this->timeout_.value()) {
this->is_idle_ = true;
this->trigger();
} else if (this->is_idle_ && idle_time < this->timeout_.value()) {
this->is_idle_ = false;
}
});
}
protected: protected:
TemplatableValue<uint32_t> timeout_; TemplatableValue<uint32_t> timeout_;
@ -217,28 +191,8 @@ template<typename... Ts> class LvglCondition : public Condition<Ts...>, public P
#ifdef USE_LVGL_TOUCHSCREEN #ifdef USE_LVGL_TOUCHSCREEN
class LVTouchListener : public touchscreen::TouchListener, public Parented<LvglComponent> { class LVTouchListener : public touchscreen::TouchListener, public Parented<LvglComponent> {
public: public:
LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time) { LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time);
lv_indev_drv_init(&this->drv_); void update(const touchscreen::TouchPoints_t &tpoints) override;
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];
}
void release() override { touch_pressed_ = false; } void release() override { touch_pressed_ = false; }
lv_indev_drv_t *get_drv() { return &this->drv_; } 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 #endif // USE_LVGL_TOUCHSCREEN
#ifdef USE_LVGL_KEY_LISTENER #ifdef USE_LVGL_ROTARY_ENCODER
class LVEncoderListener : public Parented<LvglComponent> { class LVEncoderListener : public Parented<LvglComponent> {
public: public:
LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt) { LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt);
lv_indev_drv_init(&this->drv_);
this->drv_.type = type;
this->drv_.user_data = this;
this->drv_.long_press_time = lpt;
this->drv_.long_press_repeat_time = lprt;
this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) {
auto *l = static_cast<LVEncoderListener *>(d->user_data);
data->state = l->pressed_ ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
data->key = l->key_;
data->enc_diff = (int16_t) (l->count_ - l->last_count_);
l->last_count_ = l->count_;
data->continue_reading = false;
};
}
void set_left_button(binary_sensor::BinarySensor *left_button) { void set_left_button(binary_sensor::BinarySensor *left_button) {
left_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_LEFT, state); }); 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_{}; int32_t last_count_{};
int key_{}; 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 lvgl
} // namespace esphome } // 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, CONF_ROTARY_ENCODERS,
) )
from .helpers import lvgl_components_required 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 .schemas import ENCODER_SCHEMA
from .types import lv_indev_type_t from .types import lv_indev_type_t
from .widget import add_group
ROTARY_ENCODER_CONFIG = cv.ensure_list( ROTARY_ENCODER_CONFIG = cv.ensure_list(
ENCODER_SCHEMA.extend( 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 . import defines as df, lv_validation as lvalid, types as ty
from .helpers import add_lv_use, requires_component, validate_printf from .helpers import add_lv_use, requires_component, validate_printf
from .lv_validation import id_name, lv_font from .lv_validation import id_name, lv_color, lv_font, lv_image
from .types import WIDGET_TYPES, WidgetType 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 # A schema for text properties
TEXT_SCHEMA = cv.Schema( TEXT_SCHEMA = cv.Schema(
@ -38,11 +42,13 @@ TEXT_SCHEMA = cv.Schema(
} }
) )
ACTION_SCHEMA = cv.maybe_simple_value( LIST_ACTION_SCHEMA = cv.ensure_list(
{ cv.maybe_simple_value(
cv.Required(CONF_ID): cv.use_id(ty.lv_pseudo_button_t), {
}, cv.Required(CONF_ID): cv.use_id(ty.lv_pseudo_button_t),
key=CONF_ID, },
key=CONF_ID,
)
) )
PRESS_TIME = cv.All( PRESS_TIME = cv.All(
@ -154,6 +160,7 @@ STYLE_REMAP = {
# Complete object style schema # Complete object style schema
STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend( 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( cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant(
"LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO" "LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO"
).one_of, ).one_of,
@ -209,7 +216,14 @@ def create_modify_schema(widget_type):
part_schema(widget_type) part_schema(widget_type)
.extend( .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, cv.Optional(CONF_STATE): SET_STATE_SCHEMA,
} }
) )
@ -227,6 +241,7 @@ def obj_schema(widget_type: WidgetType):
return ( return (
part_schema(widget_type) part_schema(widget_type)
.extend(FLAG_SCHEMA) .extend(FLAG_SCHEMA)
.extend(LAYOUT_SCHEMA)
.extend(ALIGN_TO_SCHEMA) .extend(ALIGN_TO_SCHEMA)
.extend(automation_schema(widget_type.w_type)) .extend(automation_schema(widget_type.w_type))
.extend( .extend(
@ -240,6 +255,8 @@ def obj_schema(widget_type: WidgetType):
) )
LAYOUT_SCHEMAS = {}
ALIGN_TO_SCHEMA = { ALIGN_TO_SCHEMA = {
cv.Optional(df.CONF_ALIGN_TO): cv.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 # A style schema that can include text
STYLED_TEXT_SCHEMA = cv.maybe_simple_value( STYLED_TEXT_SCHEMA = cv.maybe_simple_value(
STYLE_SCHEMA.extend(TEXT_SCHEMA), key=df.CONF_TEXT 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 # For use by platform components
LVGL_SCHEMA = cv.Schema( 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 = { ALL_STYLES = {**STYLE_PROPS, **GRID_CELL_SCHEMA, **FLEX_OBJ_SCHEMA}
**STYLE_PROPS,
}
def container_validator(schema, widget_type: WidgetType): def container_validator(schema, widget_type: WidgetType):
@ -281,16 +355,17 @@ def container_validator(schema, widget_type: WidgetType):
result = schema result = schema
if w_sch := widget_type.schema: if w_sch := widget_type.schema:
result = result.extend(w_sch) result = result.extend(w_sch)
ltype = df.TYPE_NONE
if value and (layout := value.get(df.CONF_LAYOUT)): if value and (layout := value.get(df.CONF_LAYOUT)):
if not isinstance(layout, dict): if not isinstance(layout, dict):
raise cv.Invalid("Layout value must be a dict") raise cv.Invalid("Layout value must be a dict")
ltype = layout.get(CONF_TYPE) ltype = layout.get(CONF_TYPE)
if not ltype:
raise (cv.Invalid("Layout schema requires type:"))
add_lv_use(ltype) add_lv_use(ltype)
result = result.extend(
{cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema())}
)
if value == SCHEMA_EXTRACT: if value == SCHEMA_EXTRACT:
return result return result
result = result.extend(LAYOUT_SCHEMAS[ltype.lower()])
return result(value) return result(value)
return validator 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_ALIGN_TO,
CONF_X, CONF_X,
CONF_Y, CONF_Y,
LV_EVENT, LV_EVENT_MAP,
LV_EVENT_TRIGGERS, LV_EVENT_TRIGGERS,
literal, 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 from .widget import widget_map
lv_event_t_ptr = cg.global_ns.namespace("lv_event_t").operator("ptr")
async def generate_triggers(lv_component): async def generate_triggers(lv_component):
""" """
@ -34,15 +33,15 @@ async def generate_triggers(lv_component):
}.items(): }.items():
conf = conf[0] conf = conf[0]
w.add_flag("LV_OBJ_FLAG_CLICKABLE") 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) await add_trigger(conf, event, lv_component, w)
for conf in w.config.get(CONF_ON_VALUE, ()): 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 # Generate align to directives while we're here
if align_to := w.config.get(CONF_ALIGN_TO): if align_to := w.config.get(CONF_ALIGN_TO):
target = widget_map[align_to[CONF_ID]].obj target = widget_map[align_to[CONF_ID]].obj
align = align_to[CONF_ALIGN] align = literal(align_to[CONF_ALIGN])
x = align_to[CONF_X] x = align_to[CONF_X]
y = align_to[CONF_Y] y = align_to[CONF_Y]
lv.obj_align_to(w.obj, target, align, x, 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): async def add_trigger(conf, event, lv_component, w):
tid = conf[CONF_TRIGGER_ID] tid = conf[CONF_TRIGGER_ID]
add_line_marks(tid)
trigger = cg.new_Pvariable(tid) trigger = cg.new_Pvariable(tid)
args = w.get_args() args = w.get_args()
value = w.get_value() value = w.get_value()
await automation.build_automation(trigger, args, conf) await automation.build_automation(trigger, args, conf)
with LambdaContext([(lv_event_t_ptr, "event_data")]) as context: async with LambdaContext(EVENT_ARG, where=tid) as context:
add_line_marks(tid) with LvConditional(w.is_selected()):
lv_add(trigger.trigger(value)) lv_add(trigger.trigger(value))
lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), literal(event))) 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 import sys
from esphome.core import ID
from esphome.cpp_generator import MockObjClass
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): class LvType(cg.MockObjClass):
@ -18,36 +21,48 @@ class LvType(cg.MockObjClass):
return self.args[0][0] if len(self.args) else None 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") uint16_t_ptr = cg.uint16.operator("ptr")
lvgl_ns = cg.esphome_ns.namespace("lvgl")
char_ptr = cg.global_ns.namespace("char").operator("ptr") char_ptr = cg.global_ns.namespace("char").operator("ptr")
void_ptr = cg.void.operator("ptr") void_ptr = cg.void.operator("ptr")
LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent) lv_coord_t = cg.global_ns.namespace("lv_coord_t")
LvglComponentPtr = LvglComponent.operator("ptr") lv_event_code_t = cg.global_ns.enum("lv_event_code_t")
lv_event_code_t = cg.global_ns.namespace("lv_event_code_t")
lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t") lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t")
FontEngine = lvgl_ns.class_("FontEngine") FontEngine = lvgl_ns.class_("FontEngine")
IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template()) IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template())
ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action) ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action)
LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition) LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition)
LvglAction = lvgl_ns.class_("LvglAction", automation.Action) LvglAction = lvgl_ns.class_("LvglAction", automation.Action)
lv_lambda_t = lvgl_ns.class_("LvLambdaType")
LvCompound = lvgl_ns.class_("LvCompound") LvCompound = lvgl_ns.class_("LvCompound")
lv_font_t = cg.global_ns.class_("lv_font_t") lv_font_t = cg.global_ns.class_("lv_font_t")
lv_style_t = cg.global_ns.struct("lv_style_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_pseudo_button_t = lvgl_ns.class_("LvPseudoButton")
lv_obj_base_t = cg.global_ns.class_("lv_obj_t", lv_pseudo_button_t) 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_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_color_t = cg.global_ns.struct("lv_color_t")
lv_group_t = cg.global_ns.struct("lv_group_t") lv_group_t = cg.global_ns.struct("lv_group_t")
LVTouchListener = lvgl_ns.class_("LVTouchListener") LVTouchListener = lvgl_ns.class_("LVTouchListener")
LVEncoderListener = lvgl_ns.class_("LVEncoderListener") LVEncoderListener = lvgl_ns.class_("LVEncoderListener")
lv_obj_t = LvType("lv_obj_t") lv_obj_t = LvType("lv_obj_t")
lv_page_t = cg.global_ns.class_("LvPageType", LvCompound)
lv_img_t = LvType("lv_img_t") lv_img_t = LvType("lv_img_t")
LV_EVENT = MockObj(base="LV_EVENT_", op="")
# this will be populated later, in __init__.py to avoid circular imports. LV_STATE = MockObj(base="LV_STATE_", op="")
WIDGET_TYPES: dict = {} LV_BTNMATRIX_CTRL = MockObj(base="LV_BTNMATRIX_CTRL_", op="")
class LvText(LvType): class LvText(LvType):
@ -55,7 +70,8 @@ class LvText(LvType):
super().__init__( super().__init__(
*args, *args,
largs=[(cg.std_string, "text")], 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, **kwargs,
) )
self.value_property = CONF_TEXT self.value_property = CONF_TEXT
@ -66,13 +82,21 @@ class LvBoolean(LvType):
super().__init__( super().__init__(
*args, *args,
largs=[(cg.bool_, "x")], largs=[(cg.bool_, "x")],
lvalue=lambda w: w.has_state("LV_STATE_CHECKED"), lvalue=lambda w: w.is_checked(),
has_on_value=True, has_on_value=True,
**kwargs, **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: class WidgetType:
@ -80,7 +104,15 @@ class WidgetType:
Describes a type of Widget, e.g. "bar" or "line" 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 name: The widget name, e.g. "bar"
:param w_type: The C type of the widget :param w_type: The C type of the widget
@ -89,6 +121,7 @@ class WidgetType:
:param modify_schema: A schema to update the widget :param modify_schema: A schema to update the widget
""" """
self.name = name self.name = name
self.lv_name = lv_name or name
self.w_type = w_type self.w_type = w_type
self.parts = parts self.parts = parts
if schema is None: if schema is None:
@ -98,7 +131,8 @@ class WidgetType:
if modify_schema is None: if modify_schema is None:
self.modify_schema = self.schema self.modify_schema = self.schema
else: else:
self.modify_schema = self.schema self.modify_schema = modify_schema
self.mock_obj = MockObj(f"lv_{self.lv_name}", "_")
@property @property
def animated(self): def animated(self):
@ -118,7 +152,7 @@ class WidgetType:
:param config: Its configuration :param config: Its configuration
:return: Generated code as a list of text lines :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): def obj_creator(self, parent: MockObjClass, config: dict):
""" """
@ -127,7 +161,7 @@ class WidgetType:
:param config: Its configuration :param config: Its configuration
:return: Generated code as a single text line :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): def get_uses(self):
""" """
@ -135,3 +169,23 @@ class WidgetType:
:return: :return:
""" """
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 import sys
from typing import Any from typing import Any, Union
from esphome import codegen as cg, config_validation as cv from esphome import codegen as cg, config_validation as cv
from esphome.config_validation import Invalid from esphome.config_validation import Invalid
from esphome.const import CONF_GROUP, CONF_ID, CONF_STATE from esphome.const import CONF_GROUP, CONF_ID, CONF_STATE, CONF_TYPE
from esphome.core import CORE, TimePeriod from esphome.core import ID, TimePeriod
from esphome.coroutine import FakeAwaitable from esphome.coroutine import FakeAwaitable
from esphome.cpp_generator import MockObj, MockObjClass, VariableDeclarationExpression from esphome.cpp_generator import AssignmentExpression, CallExpression, MockObj
from .defines import ( from .defines import (
CONF_DEFAULT, 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_MAIN,
CONF_SCROLLBAR_MODE, CONF_SCROLLBAR_MODE,
CONF_STYLES,
CONF_WIDGETS, CONF_WIDGETS,
OBJ_FLAGS, OBJ_FLAGS,
PARTS, PARTS,
STATES, STATES,
ConstantLiteral, TYPE_FLEX,
TYPE_GRID,
LValidator, LValidator,
join_enums, join_enums,
literal, literal,
) )
from .helpers import add_lv_use from .helpers import add_lv_use
from .lvcode import add_group, add_line_marks, lv, lv_add, lv_assign, lv_expr, lv_obj from .lvcode import (
from .schemas import ALL_STYLES, STYLE_REMAP LvConditional,
from .types import WIDGET_TYPES, LvType, WidgetType, lv_obj_t, lv_obj_t_ptr 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__" EVENT_LAMB = "event_lamb__"
theme_widget_map = {}
class LvScrActType(WidgetType): class LvScrActType(WidgetType):
""" """
@ -37,9 +67,6 @@ class LvScrActType(WidgetType):
def __init__(self): def __init__(self):
super().__init__("lv_scr_act()", lv_obj_t, ()) 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): async def to_code(self, w, config: dict):
return [] return []
@ -55,7 +82,7 @@ class Widget:
def set_completed(): def set_completed():
Widget.widgets_completed = True 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.var = var
self.type = wtype self.type = wtype
self.config = config self.config = config
@ -63,21 +90,18 @@ class Widget:
self.step = 1.0 self.step = 1.0
self.range_from = -sys.maxsize self.range_from = -sys.maxsize
self.range_to = 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 @staticmethod
def create(name, var, wtype: WidgetType, config: dict = None, parent=None): def create(name, var, wtype: WidgetType, config: dict = None):
w = Widget(var, wtype, config, parent) w = Widget(var, wtype, config)
if name is not None: if name is not None:
widget_map[name] = w widget_map[name] = w
return 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): def add_state(self, state):
return lv_obj.add_state(self.obj, literal(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)) return lv_obj.clear_state(self.obj, literal(state))
def has_state(self, 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): def add_flag(self, flag):
return lv_obj.add_flag(self.obj, literal(flag)) return lv_obj.add_flag(self.obj, literal(flag))
@ -93,32 +123,37 @@ class Widget:
def clear_flag(self, flag): def clear_flag(self, flag):
return lv_obj.clear_flag(self.obj, literal(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): if isinstance(value, dict):
value = value.get(prop) 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: if value is None:
return return
if isinstance(value, TimePeriod): if isinstance(value, TimePeriod):
value = value.total_milliseconds 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: 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: else:
lv.call( lv.call(
f"{ltype}_set_{prop}", f"{self.type.lv_name}_set_{prop}",
self.obj, self.obj,
value, 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): def get_property(self, prop, ltype=None):
ltype = ltype or self.__type_base() 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): def set_style(self, prop, value, state):
if value is None: if value is None:
return [] return
return lv.call(f"obj_set_style_{prop}", self.obj, value, state) lv.call(f"obj_set_style_{prop}", self.obj, value, state)
def __type_base(self): def __type_base(self):
wtype = self.type.w_type wtype = self.type.w_type
@ -140,6 +175,32 @@ class Widget:
return self.type.w_type.value(self) return self.type.w_type.value(self)
return self.obj 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 # Map of widgets to their config, used for trigger generation
widget_map: dict[Any, Widget] = {} widget_map: dict[Any, Widget] = {}
@ -161,13 +222,20 @@ def get_widget_generator(wid):
yield yield
async def get_widget(config: dict, id: str = CONF_ID) -> Widget: async def get_widget_(wid: Widget):
wid = config[id]
if obj := widget_map.get(wid): if obj := widget_map.get(wid):
return obj return obj
return await FakeAwaitable(get_widget_generator(wid)) 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): def collect_props(config):
""" """
Collect all properties from a configuration Collect all properties from a configuration
@ -175,7 +243,7 @@ def collect_props(config):
:return: :return:
""" """
props = {} 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: if prop in config:
props[prop] = config[prop] props[prop] = config[prop]
return props return props
@ -209,12 +277,39 @@ def collect_parts(config):
async def set_obj_properties(w: Widget, config): async def set_obj_properties(w: Widget, config):
"""Generate a list of C++ statements to apply properties to an lv_obj_t""" """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) parts = collect_parts(config)
for part, states in parts.items(): for part, states in parts.items():
for state, props in states.items(): for state, props in states.items():
lv_state = ConstantLiteral( lv_state = join_enums((f"LV_STATE_{state}", f"LV_PART_{part}"))
f"(int)LV_STATE_{state.upper()}|(int)LV_PART_{part.upper()}" for style_id in props.get(CONF_STYLES, ()):
) lv_obj.add_style(w.obj, MockObj(style_id), lv_state)
for prop, value in { for prop, value in {
k: v for k, v in props.items() if k in ALL_STYLES k: v for k, v in props.items() if k in ALL_STYLES
}.items(): }.items():
@ -258,14 +353,12 @@ async def set_obj_properties(w: Widget, config):
w.clear_state(clears) w.clear_state(clears)
for key, value in lambs.items(): for key, value in lambs.items():
lamb = await cg.process_lambda(value, [], return_type=cg.bool_) lamb = await cg.process_lambda(value, [], return_type=cg.bool_)
state = f"LV_STATE_{key.upper}" state = f"LV_STATE_{key.upper()}"
lv.cond_if(lamb) with LvConditional(f"{lamb}()") as cond:
w.add_state(state) w.add_state(state)
lv.cond_else() cond.else_()
w.clear_state(state) w.clear_state(state)
lv.cond_endif() await w.set_property(CONF_SCROLLBAR_MODE, config)
if scrollbar_mode := config.get(CONF_SCROLLBAR_MODE):
lv_obj.set_scrollbar_mode(w.obj, scrollbar_mode)
async def add_widgets(parent: Widget, config: dict): 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) 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. Converts a Widget definition to C code.
:param w_cnfig: The widget configuration :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) var = cg.new_Pvariable(wid)
lv_add(var.set_obj(creator)) lv_add(var.set_obj(creator))
else: else:
var = MockObj(wid, "->") var = lv_Pvariable(lv_obj_t, wid)
decl = VariableDeclarationExpression(lv_obj_t, "*", wid)
CORE.add_global(decl)
CORE.register_variable(wid, var)
lv_assign(var, creator) lv_assign(var, creator)
widget = Widget.create(wid, var, spec, w_cnfig, parent) w = Widget.create(wid, var, spec, w_cnfig)
await set_obj_properties(widget, w_cnfig) if theme := theme_widget_map.get(w_type):
await add_widgets(widget, w_cnfig) lv_add(CallExpression(theme, w.obj))
await spec.to_code(widget, w_cnfig) 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_spec = LvScrActType()
lv_scr_act = Widget.create( lv_scr_act = Widget.create(None, literal("lv_scr_act()"), lv_scr_act_spec, {})
None, ConstantLiteral("lv_scr_act()"), lv_scr_act_spec, {}, parent=None
) 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 - touchscreen_id: tft_touch
long_press_repeat_time: 200ms long_press_repeat_time: 200ms
long_press_time: 500ms long_press_time: 500ms
widgets: pages:
- label: - id: page1
id: hello_label skip: true
text: Hello world widgets:
text_color: 0xFF8000 - label:
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 id: hello_label
bg_color: 0x123456 text: Hello world
text: clicked text_color: 0xFF8000
on_value: align: center
logger.log: text_font: montserrat_40
format: "state now %d" border_post: true
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
- 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: font:
- file: "gfonts://Roboto" - file: "gfonts://Roboto"
id: space16 id: space16
bpp: 4 bpp: 4
image: image:
- id: cat_img - id: cat_image
resize: 256x48 resize: 256x48
file: $component_dir/logo-text.svg file: $component_dir/logo-text.svg
- id: dog_img - id: dog_img