mirror of
https://github.com/esphome/esphome.git
synced 2024-12-22 16:37:52 +01:00
[lvgl] Stage 4 (#7166)
This commit is contained in:
parent
87944f0c1b
commit
d18bb34f87
@ -15,44 +15,91 @@ from esphome.const import (
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_TYPE,
|
||||
)
|
||||
from esphome.core import CORE, ID, Lambda
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.final_validate import full_config
|
||||
from esphome.helpers import write_file_if_changed
|
||||
|
||||
from . import defines as df, helpers, lv_validation as lvalid
|
||||
from .automation import update_to_code
|
||||
from .animimg import animimg_spec
|
||||
from .arc import arc_spec
|
||||
from .automation import disp_update, update_to_code
|
||||
from .btn import btn_spec
|
||||
from .checkbox import checkbox_spec
|
||||
from .defines import CONF_SKIP
|
||||
from .img import img_spec
|
||||
from .label import label_spec
|
||||
from .lv_validation import lv_images_used
|
||||
from .lvcode import LvContext
|
||||
from .led import led_spec
|
||||
from .line import line_spec
|
||||
from .lv_bar import bar_spec
|
||||
from .lv_switch import switch_spec
|
||||
from .lv_validation import lv_bool, lv_images_used
|
||||
from .lvcode import LvContext, LvglComponent
|
||||
from .obj import obj_spec
|
||||
from .page import add_pages, page_spec
|
||||
from .rotary_encoders import ROTARY_ENCODER_CONFIG, rotary_encoders_to_code
|
||||
from .schemas import any_widget_schema, create_modify_schema, obj_schema
|
||||
from .schemas import (
|
||||
DISP_BG_SCHEMA,
|
||||
FLEX_OBJ_SCHEMA,
|
||||
GRID_CELL_SCHEMA,
|
||||
LAYOUT_SCHEMAS,
|
||||
STYLE_SCHEMA,
|
||||
WIDGET_TYPES,
|
||||
any_widget_schema,
|
||||
container_schema,
|
||||
create_modify_schema,
|
||||
grid_alignments,
|
||||
obj_schema,
|
||||
)
|
||||
from .slider import slider_spec
|
||||
from .spinner import spinner_spec
|
||||
from .styles import add_top_layer, styles_to_code, theme_to_code
|
||||
from .touchscreens import touchscreen_schema, touchscreens_to_code
|
||||
from .trigger import generate_triggers
|
||||
from .types import (
|
||||
WIDGET_TYPES,
|
||||
FontEngine,
|
||||
IdleTrigger,
|
||||
LvglComponent,
|
||||
ObjUpdateAction,
|
||||
lv_font_t,
|
||||
lv_style_t,
|
||||
lvgl_ns,
|
||||
)
|
||||
from .widget import Widget, add_widgets, lv_scr_act, set_obj_properties
|
||||
|
||||
DOMAIN = "lvgl"
|
||||
DEPENDENCIES = ("display",)
|
||||
AUTO_LOAD = ("key_provider",)
|
||||
CODEOWNERS = ("@clydebarrow",)
|
||||
DEPENDENCIES = ["display"]
|
||||
AUTO_LOAD = ["key_provider"]
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
for w_type in (label_spec, obj_spec, btn_spec):
|
||||
for w_type in (
|
||||
label_spec,
|
||||
obj_spec,
|
||||
btn_spec,
|
||||
bar_spec,
|
||||
slider_spec,
|
||||
arc_spec,
|
||||
line_spec,
|
||||
spinner_spec,
|
||||
led_spec,
|
||||
animimg_spec,
|
||||
checkbox_spec,
|
||||
img_spec,
|
||||
switch_spec,
|
||||
):
|
||||
WIDGET_TYPES[w_type.name] = w_type
|
||||
|
||||
WIDGET_SCHEMA = any_widget_schema()
|
||||
|
||||
LAYOUT_SCHEMAS[df.TYPE_GRID] = {
|
||||
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(GRID_CELL_SCHEMA))
|
||||
}
|
||||
LAYOUT_SCHEMAS[df.TYPE_FLEX] = {
|
||||
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(FLEX_OBJ_SCHEMA))
|
||||
}
|
||||
LAYOUT_SCHEMAS[df.TYPE_NONE] = {
|
||||
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema())
|
||||
}
|
||||
for w_type in WIDGET_TYPES.values():
|
||||
register_action(
|
||||
f"lvgl.{w_type.name}.update",
|
||||
@ -61,14 +108,6 @@ for w_type in WIDGET_TYPES.values():
|
||||
)(update_to_code)
|
||||
|
||||
|
||||
async def add_init_lambda(lv_component, init):
|
||||
if init:
|
||||
lamb = await cg.process_lambda(
|
||||
Lambda(init), [(LvglComponent.operator("ptr"), "lv_component")]
|
||||
)
|
||||
cg.add(lv_component.add_init_lambda(lamb))
|
||||
|
||||
|
||||
lv_defines = {} # Dict of #defines to provide as build flags
|
||||
|
||||
|
||||
@ -100,6 +139,9 @@ def generate_lv_conf_h():
|
||||
|
||||
|
||||
def final_validation(config):
|
||||
if pages := config.get(CONF_PAGES):
|
||||
if all(p[CONF_SKIP] for p in pages):
|
||||
raise cv.Invalid("At least one page must not be skipped")
|
||||
global_config = full_config.get()
|
||||
for display_id in config[df.CONF_DISPLAYS]:
|
||||
path = global_config.get_path_for_id(display_id)[:-1]
|
||||
@ -193,18 +235,23 @@ async def to_code(config):
|
||||
else:
|
||||
add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font))
|
||||
|
||||
with LvContext():
|
||||
async with LvContext(lv_component):
|
||||
await touchscreens_to_code(lv_component, config)
|
||||
await rotary_encoders_to_code(lv_component, config)
|
||||
await theme_to_code(config)
|
||||
await styles_to_code(config)
|
||||
await set_obj_properties(lv_scr_act, config)
|
||||
await add_widgets(lv_scr_act, config)
|
||||
await add_pages(lv_component, config)
|
||||
await add_top_layer(config)
|
||||
await disp_update(f"{lv_component}->get_disp()", config)
|
||||
Widget.set_completed()
|
||||
await generate_triggers(lv_component)
|
||||
for conf in config.get(CONF_ON_IDLE, ()):
|
||||
templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32)
|
||||
idle_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, templ)
|
||||
await build_automation(idle_trigger, [], conf)
|
||||
await add_init_lambda(lv_component, LvContext.get_code())
|
||||
|
||||
for comp in helpers.lvgl_components_required:
|
||||
CORE.add_define(f"USE_LVGL_{comp.upper()}")
|
||||
for use in helpers.lv_uses:
|
||||
@ -239,6 +286,16 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of(
|
||||
"big_endian", "little_endian"
|
||||
),
|
||||
cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list(
|
||||
cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)})
|
||||
.extend(STYLE_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments,
|
||||
cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments,
|
||||
}
|
||||
)
|
||||
),
|
||||
cv.Optional(CONF_ON_IDLE): validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger),
|
||||
@ -247,10 +304,19 @@ CONFIG_SCHEMA = (
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(WIDGET_SCHEMA),
|
||||
cv.Exclusive(df.CONF_WIDGETS, CONF_PAGES): cv.ensure_list(WIDGET_SCHEMA),
|
||||
cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list(
|
||||
container_schema(page_spec)
|
||||
),
|
||||
cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool,
|
||||
cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec),
|
||||
cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color,
|
||||
cv.Optional(df.CONF_THEME): cv.Schema(
|
||||
{cv.Optional(name): obj_schema(w) for name, w in WIDGET_TYPES.items()}
|
||||
),
|
||||
cv.GenerateID(df.CONF_TOUCHSCREENS): touchscreen_schema,
|
||||
cv.GenerateID(df.CONF_ROTARY_ENCODERS): ROTARY_ENCODER_CONFIG,
|
||||
}
|
||||
)
|
||||
.extend(DISP_BG_SCHEMA)
|
||||
).add_extra(cv.has_at_least_one_key(CONF_PAGES, df.CONF_WIDGETS))
|
||||
|
117
esphome/components/lvgl/animimg.py
Normal file
117
esphome/components/lvgl/animimg.py
Normal 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)
|
78
esphome/components/lvgl/arc.py
Normal file
78
esphome/components/lvgl/arc.py
Normal 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()
|
@ -1,15 +1,26 @@
|
||||
from collections.abc import Awaitable
|
||||
from typing import Callable
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_TIMEOUT
|
||||
from esphome.core import Lambda
|
||||
from esphome.cpp_generator import RawStatement
|
||||
from esphome.cpp_types import nullptr
|
||||
|
||||
from .defines import CONF_LVGL_ID, CONF_SHOW_SNOW, literal
|
||||
from .lv_validation import lv_bool
|
||||
from .defines import (
|
||||
CONF_DISP_BG_COLOR,
|
||||
CONF_DISP_BG_IMAGE,
|
||||
CONF_LVGL_ID,
|
||||
CONF_SHOW_SNOW,
|
||||
literal,
|
||||
)
|
||||
from .lv_validation import lv_bool, lv_color, lv_image
|
||||
from .lvcode import (
|
||||
LVGL_COMP_ARG,
|
||||
LambdaContext,
|
||||
LocalVariable,
|
||||
LvConditional,
|
||||
LvglComponent,
|
||||
ReturnStatement,
|
||||
add_line_marks,
|
||||
lv,
|
||||
@ -17,46 +28,46 @@ from .lvcode import (
|
||||
lv_obj,
|
||||
lvgl_comp,
|
||||
)
|
||||
from .schemas import ACTION_SCHEMA, LVGL_SCHEMA
|
||||
from .schemas import DISP_BG_SCHEMA, LIST_ACTION_SCHEMA, LVGL_SCHEMA
|
||||
from .types import (
|
||||
LV_EVENT,
|
||||
LV_STATE,
|
||||
LvglAction,
|
||||
LvglComponent,
|
||||
LvglComponentPtr,
|
||||
LvglCondition,
|
||||
ObjUpdateAction,
|
||||
lv_disp_t,
|
||||
lv_obj_t,
|
||||
)
|
||||
from .widget import Widget, get_widget, lv_scr_act, set_obj_properties
|
||||
from .widget import Widget, get_widgets, lv_scr_act, set_obj_properties
|
||||
|
||||
|
||||
async def action_to_code(action: list, action_id, widget: Widget, template_arg, args):
|
||||
with LambdaContext() as context:
|
||||
lv.cond_if(widget.obj == nullptr)
|
||||
lv_add(RawStatement(" return;"))
|
||||
lv.cond_endif()
|
||||
code = context.get_code()
|
||||
code.extend(action)
|
||||
action = "\n".join(code) + "\n\n"
|
||||
lamb = await cg.process_lambda(Lambda(action), args)
|
||||
var = cg.new_Pvariable(action_id, template_arg, lamb)
|
||||
async def action_to_code(
|
||||
widgets: list[Widget],
|
||||
action: Callable[[Widget], Awaitable[None]],
|
||||
action_id,
|
||||
template_arg,
|
||||
args,
|
||||
):
|
||||
async with LambdaContext(parameters=args, where=action_id) as context:
|
||||
for widget in widgets:
|
||||
with LvConditional(widget.obj != nullptr):
|
||||
await action(widget)
|
||||
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
|
||||
return var
|
||||
|
||||
|
||||
async def update_to_code(config, action_id, template_arg, args):
|
||||
if config is not None:
|
||||
widget = await get_widget(config)
|
||||
with LambdaContext() as context:
|
||||
add_line_marks(action_id)
|
||||
await set_obj_properties(widget, config)
|
||||
await widget.type.to_code(widget, config)
|
||||
if (
|
||||
widget.type.w_type.value_property is not None
|
||||
and widget.type.w_type.value_property in config
|
||||
):
|
||||
lv.event_send(widget.obj, literal("LV_EVENT_VALUE_CHANGED"), nullptr)
|
||||
return await action_to_code(
|
||||
context.get_code(), action_id, widget, template_arg, args
|
||||
)
|
||||
async def do_update(widget: Widget):
|
||||
await set_obj_properties(widget, config)
|
||||
await widget.type.to_code(widget, config)
|
||||
if (
|
||||
widget.type.w_type.value_property is not None
|
||||
and widget.type.w_type.value_property in config
|
||||
):
|
||||
lv.event_send(widget.obj, LV_EVENT.VALUE_CHANGED, nullptr)
|
||||
|
||||
widgets = await get_widgets(config[CONF_ID])
|
||||
return await action_to_code(widgets, do_update, action_id, template_arg, args)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
@ -66,9 +77,7 @@ async def update_to_code(config, action_id, template_arg, args):
|
||||
)
|
||||
async def lvgl_is_paused(config, condition_id, template_arg, args):
|
||||
lvgl = config[CONF_LVGL_ID]
|
||||
with LambdaContext(
|
||||
[(LvglComponentPtr, "lvgl_comp")], return_type=cg.bool_
|
||||
) as context:
|
||||
async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context:
|
||||
lv_add(ReturnStatement(lvgl_comp.is_paused()))
|
||||
var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda())
|
||||
await cg.register_parented(var, lvgl)
|
||||
@ -89,15 +98,23 @@ async def lvgl_is_paused(config, condition_id, template_arg, args):
|
||||
async def lvgl_is_idle(config, condition_id, template_arg, args):
|
||||
lvgl = config[CONF_LVGL_ID]
|
||||
timeout = await cg.templatable(config[CONF_TIMEOUT], [], cg.uint32)
|
||||
with LambdaContext(
|
||||
[(LvglComponentPtr, "lvgl_comp")], return_type=cg.bool_
|
||||
) as context:
|
||||
async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context:
|
||||
lv_add(ReturnStatement(lvgl_comp.is_idle(timeout)))
|
||||
var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda())
|
||||
await cg.register_parented(var, lvgl)
|
||||
return var
|
||||
|
||||
|
||||
async def disp_update(disp, config: dict):
|
||||
if CONF_DISP_BG_COLOR not in config and CONF_DISP_BG_IMAGE not in config:
|
||||
return
|
||||
with LocalVariable("lv_disp_tmp", lv_disp_t, literal(disp)) as disp_temp:
|
||||
if bg_color := config.get(CONF_DISP_BG_COLOR):
|
||||
lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color))
|
||||
if bg_image := config.get(CONF_DISP_BG_IMAGE):
|
||||
lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image))
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.widget.redraw",
|
||||
ObjUpdateAction,
|
||||
@ -109,14 +126,32 @@ async def lvgl_is_idle(config, condition_id, template_arg, args):
|
||||
),
|
||||
)
|
||||
async def obj_invalidate_to_code(config, action_id, template_arg, args):
|
||||
if CONF_ID in config:
|
||||
w = await get_widget(config)
|
||||
else:
|
||||
w = lv_scr_act
|
||||
with LambdaContext() as context:
|
||||
add_line_marks(action_id)
|
||||
lv_obj.invalidate(w.obj)
|
||||
return await action_to_code(context.get_code(), action_id, w, template_arg, args)
|
||||
widgets = await get_widgets(config) or [lv_scr_act]
|
||||
|
||||
async def do_invalidate(widget: Widget):
|
||||
lv_obj.invalidate(widget.obj)
|
||||
|
||||
return await action_to_code(widgets, do_invalidate, action_id, template_arg, args)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.update",
|
||||
LvglAction,
|
||||
DISP_BG_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(LvglComponent),
|
||||
}
|
||||
).add_extra(cv.has_at_least_one_key(CONF_DISP_BG_COLOR, CONF_DISP_BG_IMAGE)),
|
||||
)
|
||||
async def lvgl_update_to_code(config, action_id, template_arg, args):
|
||||
widgets = await get_widgets(config)
|
||||
w = widgets[0]
|
||||
disp = f"{w.obj}->get_disp()"
|
||||
async with LambdaContext(parameters=args, where=action_id) as context:
|
||||
await disp_update(disp, config)
|
||||
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
|
||||
await cg.register_parented(var, w.var)
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
@ -128,8 +163,8 @@ async def obj_invalidate_to_code(config, action_id, template_arg, args):
|
||||
},
|
||||
)
|
||||
async def pause_action_to_code(config, action_id, template_arg, args):
|
||||
with LambdaContext([(LvglComponentPtr, "lvgl_comp")]) as context:
|
||||
add_line_marks(action_id)
|
||||
async with LambdaContext(LVGL_COMP_ARG) as context:
|
||||
add_line_marks(where=action_id)
|
||||
lv_add(lvgl_comp.set_paused(True, config[CONF_SHOW_SNOW]))
|
||||
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
@ -144,45 +179,48 @@ async def pause_action_to_code(config, action_id, template_arg, args):
|
||||
},
|
||||
)
|
||||
async def resume_action_to_code(config, action_id, template_arg, args):
|
||||
with LambdaContext([(LvglComponentPtr, "lvgl_comp")]) as context:
|
||||
add_line_marks(action_id)
|
||||
async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context:
|
||||
lv_add(lvgl_comp.set_paused(False, False))
|
||||
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action("lvgl.widget.disable", ObjUpdateAction, ACTION_SCHEMA)
|
||||
@automation.register_action("lvgl.widget.disable", ObjUpdateAction, LIST_ACTION_SCHEMA)
|
||||
async def obj_disable_to_code(config, action_id, template_arg, args):
|
||||
w = await get_widget(config)
|
||||
with LambdaContext() as context:
|
||||
add_line_marks(action_id)
|
||||
w.add_state("LV_STATE_DISABLED")
|
||||
return await action_to_code(context.get_code(), action_id, w, template_arg, args)
|
||||
async def do_disable(widget: Widget):
|
||||
widget.add_state(LV_STATE.DISABLED)
|
||||
|
||||
return await action_to_code(
|
||||
await get_widgets(config), do_disable, action_id, template_arg, args
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action("lvgl.widget.enable", ObjUpdateAction, ACTION_SCHEMA)
|
||||
@automation.register_action("lvgl.widget.enable", ObjUpdateAction, LIST_ACTION_SCHEMA)
|
||||
async def obj_enable_to_code(config, action_id, template_arg, args):
|
||||
w = await get_widget(config)
|
||||
with LambdaContext() as context:
|
||||
add_line_marks(action_id)
|
||||
w.clear_state("LV_STATE_DISABLED")
|
||||
return await action_to_code(context.get_code(), action_id, w, template_arg, args)
|
||||
async def do_enable(widget: Widget):
|
||||
widget.clear_state(LV_STATE.DISABLED)
|
||||
|
||||
return await action_to_code(
|
||||
await get_widgets(config), do_enable, action_id, template_arg, args
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action("lvgl.widget.hide", ObjUpdateAction, ACTION_SCHEMA)
|
||||
@automation.register_action("lvgl.widget.hide", ObjUpdateAction, LIST_ACTION_SCHEMA)
|
||||
async def obj_hide_to_code(config, action_id, template_arg, args):
|
||||
w = await get_widget(config)
|
||||
with LambdaContext() as context:
|
||||
add_line_marks(action_id)
|
||||
w.add_flag("LV_OBJ_FLAG_HIDDEN")
|
||||
return await action_to_code(context.get_code(), action_id, w, template_arg, args)
|
||||
async def do_hide(widget: Widget):
|
||||
widget.add_flag("LV_OBJ_FLAG_HIDDEN")
|
||||
|
||||
return await action_to_code(
|
||||
await get_widgets(config), do_hide, action_id, template_arg, args
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action("lvgl.widget.show", ObjUpdateAction, ACTION_SCHEMA)
|
||||
@automation.register_action("lvgl.widget.show", ObjUpdateAction, LIST_ACTION_SCHEMA)
|
||||
async def obj_show_to_code(config, action_id, template_arg, args):
|
||||
w = await get_widget(config)
|
||||
with LambdaContext() as context:
|
||||
add_line_marks(action_id)
|
||||
w.clear_flag("LV_OBJ_FLAG_HIDDEN")
|
||||
return await action_to_code(context.get_code(), action_id, w, template_arg, args)
|
||||
async def do_show(widget: Widget):
|
||||
widget.clear_flag("LV_OBJ_FLAG_HIDDEN")
|
||||
|
||||
return await action_to_code(
|
||||
await get_widgets(config), do_show, action_id, template_arg, args
|
||||
)
|
||||
|
@ -1,19 +1,14 @@
|
||||
from esphome.const import CONF_BUTTON
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
|
||||
from .defines import CONF_MAIN
|
||||
from .types import LvBoolean, WidgetType
|
||||
|
||||
lv_btn_t = LvBoolean("lv_btn_t")
|
||||
|
||||
|
||||
class BtnType(WidgetType):
|
||||
def __init__(self):
|
||||
super().__init__(CONF_BUTTON, LvBoolean("lv_btn_t"), (CONF_MAIN,))
|
||||
|
||||
def obj_creator(self, parent: MockObjClass, config: dict):
|
||||
"""
|
||||
LVGL 8 calls buttons `btn`
|
||||
"""
|
||||
return f"lv_btn_create({parent})"
|
||||
super().__init__(CONF_BUTTON, lv_btn_t, (CONF_MAIN,), lv_name="btn")
|
||||
|
||||
def get_uses(self):
|
||||
return ("btn",)
|
||||
|
25
esphome/components/lvgl/checkbox.py
Normal file
25
esphome/components/lvgl/checkbox.py
Normal 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()
|
@ -4,31 +4,20 @@ Constants already defined in esphome.const are not duplicated here and must be i
|
||||
|
||||
"""
|
||||
|
||||
from typing import Union
|
||||
|
||||
from esphome import codegen as cg, config_validation as cv
|
||||
from esphome.core import ID, Lambda
|
||||
from esphome.cpp_generator import Literal
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.cpp_types import uint32
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
|
||||
from .helpers import requires_component
|
||||
|
||||
|
||||
class ConstantLiteral(Literal):
|
||||
__slots__ = ("constant",)
|
||||
|
||||
def __init__(self, constant: str):
|
||||
super().__init__()
|
||||
self.constant = constant
|
||||
|
||||
def __str__(self):
|
||||
return self.constant
|
||||
lvgl_ns = cg.esphome_ns.namespace("lvgl")
|
||||
|
||||
|
||||
def literal(arg: Union[str, ConstantLiteral]):
|
||||
def literal(arg):
|
||||
if isinstance(arg, str):
|
||||
return ConstantLiteral(arg)
|
||||
return MockObj(arg)
|
||||
return arg
|
||||
|
||||
|
||||
@ -93,15 +82,23 @@ class LvConstant(LValidator):
|
||||
return self.prefix + cv.one_of(*choices, upper=True)(value)
|
||||
|
||||
super().__init__(validator, rtype=uint32)
|
||||
self.retmapper = self.mapper
|
||||
self.one_of = LValidator(validator, uint32, retmapper=self.mapper)
|
||||
self.several_of = LValidator(
|
||||
cv.ensure_list(self.one_of), uint32, retmapper=self.mapper
|
||||
)
|
||||
|
||||
def mapper(self, value, args=()):
|
||||
if isinstance(value, list):
|
||||
value = "|".join(value)
|
||||
return ConstantLiteral(value)
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
return literal(
|
||||
"|".join(
|
||||
[
|
||||
str(v) if str(v).startswith(self.prefix) else self.prefix + str(v)
|
||||
for v in value
|
||||
]
|
||||
).upper()
|
||||
)
|
||||
|
||||
def extend(self, *choices):
|
||||
"""
|
||||
@ -112,9 +109,6 @@ class LvConstant(LValidator):
|
||||
return LvConstant(self.prefix, *(self.choices + choices))
|
||||
|
||||
|
||||
# Widgets
|
||||
CONF_LABEL = "label"
|
||||
|
||||
# Parts
|
||||
CONF_MAIN = "main"
|
||||
CONF_SCROLLBAR = "scrollbar"
|
||||
@ -123,10 +117,15 @@ CONF_KNOB = "knob"
|
||||
CONF_SELECTED = "selected"
|
||||
CONF_ITEMS = "items"
|
||||
CONF_TICKS = "ticks"
|
||||
CONF_TICK_STYLE = "tick_style"
|
||||
CONF_CURSOR = "cursor"
|
||||
CONF_TEXTAREA_PLACEHOLDER = "textarea_placeholder"
|
||||
|
||||
# Layout types
|
||||
|
||||
TYPE_FLEX = "flex"
|
||||
TYPE_GRID = "grid"
|
||||
TYPE_NONE = "none"
|
||||
|
||||
LV_FONTS = list(f"montserrat_{s}" for s in range(8, 50, 2)) + [
|
||||
"dejavu_16_persian_hebrew",
|
||||
"simsun_16_cjk",
|
||||
@ -134,7 +133,7 @@ LV_FONTS = list(f"montserrat_{s}" for s in range(8, 50, 2)) + [
|
||||
"unscii_16",
|
||||
]
|
||||
|
||||
LV_EVENT = {
|
||||
LV_EVENT_MAP = {
|
||||
"PRESS": "PRESSED",
|
||||
"SHORT_CLICK": "SHORT_CLICKED",
|
||||
"LONG_PRESS": "LONG_PRESSED",
|
||||
@ -150,7 +149,7 @@ LV_EVENT = {
|
||||
"CANCEL": "CANCEL",
|
||||
}
|
||||
|
||||
LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT)
|
||||
LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT_MAP)
|
||||
|
||||
|
||||
LV_ANIM = LvConstant(
|
||||
@ -305,7 +304,8 @@ OBJ_FLAGS = (
|
||||
ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL")
|
||||
BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE")
|
||||
|
||||
BTNMATRIX_CTRLS = (
|
||||
BTNMATRIX_CTRLS = LvConstant(
|
||||
"LV_BTNMATRIX_CTRL_",
|
||||
"HIDDEN",
|
||||
"NO_REPEAT",
|
||||
"DISABLED",
|
||||
@ -366,7 +366,6 @@ CONF_ACCEPTED_CHARS = "accepted_chars"
|
||||
CONF_ADJUSTABLE = "adjustable"
|
||||
CONF_ALIGN = "align"
|
||||
CONF_ALIGN_TO = "align_to"
|
||||
CONF_ANGLE_RANGE = "angle_range"
|
||||
CONF_ANIMATED = "animated"
|
||||
CONF_ANIMATION = "animation"
|
||||
CONF_ANTIALIAS = "antialias"
|
||||
@ -384,8 +383,6 @@ CONF_BYTE_ORDER = "byte_order"
|
||||
CONF_CHANGE_RATE = "change_rate"
|
||||
CONF_CLOSE_BUTTON = "close_button"
|
||||
CONF_COLOR_DEPTH = "color_depth"
|
||||
CONF_COLOR_END = "color_end"
|
||||
CONF_COLOR_START = "color_start"
|
||||
CONF_CONTROL = "control"
|
||||
CONF_DEFAULT = "default"
|
||||
CONF_DEFAULT_FONT = "default_font"
|
||||
@ -414,9 +411,7 @@ CONF_GRID_ROW_ALIGN = "grid_row_align"
|
||||
CONF_GRID_ROWS = "grid_rows"
|
||||
CONF_HEADER_MODE = "header_mode"
|
||||
CONF_HOME = "home"
|
||||
CONF_INDICATORS = "indicators"
|
||||
CONF_KEY_CODE = "key_code"
|
||||
CONF_LABEL_GAP = "label_gap"
|
||||
CONF_LAYOUT = "layout"
|
||||
CONF_LEFT_BUTTON = "left_button"
|
||||
CONF_LINE_WIDTH = "line_width"
|
||||
@ -425,7 +420,6 @@ CONF_LONG_PRESS_TIME = "long_press_time"
|
||||
CONF_LONG_PRESS_REPEAT_TIME = "long_press_repeat_time"
|
||||
CONF_LVGL_ID = "lvgl_id"
|
||||
CONF_LONG_MODE = "long_mode"
|
||||
CONF_MAJOR = "major"
|
||||
CONF_MSGBOXES = "msgboxes"
|
||||
CONF_OBJ = "obj"
|
||||
CONF_OFFSET_X = "offset_x"
|
||||
@ -434,6 +428,7 @@ CONF_ONE_LINE = "one_line"
|
||||
CONF_ON_SELECT = "on_select"
|
||||
CONF_ONE_CHECKED = "one_checked"
|
||||
CONF_NEXT = "next"
|
||||
CONF_PAGE = "page"
|
||||
CONF_PAGE_WRAP = "page_wrap"
|
||||
CONF_PASSWORD_MODE = "password_mode"
|
||||
CONF_PIVOT_X = "pivot_x"
|
||||
@ -442,14 +437,12 @@ CONF_PLACEHOLDER_TEXT = "placeholder_text"
|
||||
CONF_POINTS = "points"
|
||||
CONF_PREVIOUS = "previous"
|
||||
CONF_REPEAT_COUNT = "repeat_count"
|
||||
CONF_R_MOD = "r_mod"
|
||||
CONF_RECOLOR = "recolor"
|
||||
CONF_RIGHT_BUTTON = "right_button"
|
||||
CONF_ROLLOVER = "rollover"
|
||||
CONF_ROOT_BACK_BTN = "root_back_btn"
|
||||
CONF_ROTARY_ENCODERS = "rotary_encoders"
|
||||
CONF_ROWS = "rows"
|
||||
CONF_SCALES = "scales"
|
||||
CONF_SCALE_LINES = "scale_lines"
|
||||
CONF_SCROLLBAR_MODE = "scrollbar_mode"
|
||||
CONF_SELECTED_INDEX = "selected_index"
|
||||
@ -459,8 +452,9 @@ CONF_SRC = "src"
|
||||
CONF_START_ANGLE = "start_angle"
|
||||
CONF_START_VALUE = "start_value"
|
||||
CONF_STATES = "states"
|
||||
CONF_STRIDE = "stride"
|
||||
CONF_STYLE = "style"
|
||||
CONF_STYLES = "styles"
|
||||
CONF_STYLE_DEFINITIONS = "style_definitions"
|
||||
CONF_STYLE_ID = "style_id"
|
||||
CONF_SKIP = "skip"
|
||||
CONF_SYMBOL = "symbol"
|
||||
@ -505,4 +499,4 @@ DEFAULT_ESPHOME_FONT = "esphome_lv_default_font"
|
||||
|
||||
|
||||
def join_enums(enums, prefix=""):
|
||||
return ConstantLiteral("|".join(f"(int){prefix}{e.upper()}" for e in enums))
|
||||
return literal("|".join(f"(int){prefix}{e.upper()}" for e in enums))
|
||||
|
@ -1,10 +1,7 @@
|
||||
import re
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.config import Config
|
||||
from esphome.const import CONF_ARGS, CONF_FORMAT
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.yaml_util import ESPHomeDataBase
|
||||
|
||||
lv_uses = {
|
||||
"USER_DATA",
|
||||
@ -44,23 +41,6 @@ def validate_printf(value):
|
||||
return value
|
||||
|
||||
|
||||
def get_line_marks(value) -> list:
|
||||
"""
|
||||
If possible, return a preprocessor directive to identify the line number where the given id was defined.
|
||||
:param id: The id in question
|
||||
:return: A list containing zero or more line directives
|
||||
"""
|
||||
path = None
|
||||
if isinstance(value, ESPHomeDataBase):
|
||||
path = value.esp_range
|
||||
elif isinstance(value, ID) and isinstance(CORE.config, Config):
|
||||
path = CORE.config.get_path_for_id(value)[:-1]
|
||||
path = CORE.config.get_deepest_document_range_for_path(path)
|
||||
if path is None:
|
||||
return []
|
||||
return [path.start_mark.as_line_directive]
|
||||
|
||||
|
||||
def requires_component(comp):
|
||||
def validator(value):
|
||||
lvgl_components_required.add(comp)
|
||||
|
85
esphome/components/lvgl/img.py
Normal file
85
esphome/components/lvgl/img.py
Normal 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()
|
@ -1,7 +1,6 @@
|
||||
import esphome.config_validation as cv
|
||||
|
||||
from .defines import (
|
||||
CONF_LABEL,
|
||||
CONF_LONG_MODE,
|
||||
CONF_MAIN,
|
||||
CONF_RECOLOR,
|
||||
@ -15,6 +14,8 @@ from .schemas import TEXT_SCHEMA
|
||||
from .types import LvText, WidgetType
|
||||
from .widget import Widget
|
||||
|
||||
CONF_LABEL = "label"
|
||||
|
||||
|
||||
class LabelType(WidgetType):
|
||||
def __init__(self):
|
||||
@ -33,9 +34,9 @@ class LabelType(WidgetType):
|
||||
async def to_code(self, w: Widget, config):
|
||||
"""For a text object, create and set text"""
|
||||
if value := config.get(CONF_TEXT):
|
||||
w.set_property(CONF_TEXT, await lv_text.process(value))
|
||||
w.set_property(CONF_LONG_MODE, config)
|
||||
w.set_property(CONF_RECOLOR, config)
|
||||
await w.set_property(CONF_TEXT, await lv_text.process(value))
|
||||
await w.set_property(CONF_LONG_MODE, config)
|
||||
await w.set_property(CONF_RECOLOR, config)
|
||||
|
||||
|
||||
label_spec = LabelType()
|
||||
|
29
esphome/components/lvgl/led.py
Normal file
29
esphome/components/lvgl/led.py
Normal 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()
|
51
esphome/components/lvgl/line.py
Normal file
51
esphome/components/lvgl/line.py
Normal 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()
|
53
esphome/components/lvgl/lv_bar.py
Normal file
53
esphome/components/lvgl/lv_bar.py
Normal 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()
|
20
esphome/components/lvgl/lv_switch.py
Normal file
20
esphome/components/lvgl/lv_switch.py
Normal 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()
|
@ -1,3 +1,5 @@
|
||||
from typing import Union
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.binary_sensor import BinarySensor
|
||||
from esphome.components.color import ColorStruct
|
||||
@ -6,7 +8,7 @@ from esphome.components.image import Image_
|
||||
from esphome.components.sensor import Sensor
|
||||
from esphome.components.text_sensor import TextSensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT
|
||||
from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT, CONF_VALUE
|
||||
from esphome.core import HexInt
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.cpp_types import uint32
|
||||
@ -14,7 +16,14 @@ from esphome.helpers import cpp_string_escape
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
|
||||
from . import types as ty
|
||||
from .defines import LV_FONTS, ConstantLiteral, LValidator, LvConstant, literal
|
||||
from .defines import (
|
||||
CONF_END_VALUE,
|
||||
CONF_START_VALUE,
|
||||
LV_FONTS,
|
||||
LValidator,
|
||||
LvConstant,
|
||||
literal,
|
||||
)
|
||||
from .helpers import (
|
||||
esphome_fonts_used,
|
||||
lv_fonts_used,
|
||||
@ -60,6 +69,13 @@ def color_retmapper(value):
|
||||
return lv_expr.color_from(MockObj(value))
|
||||
|
||||
|
||||
def option_string(value):
|
||||
value = cv.string(value).strip()
|
||||
if value.find("\n") != -1:
|
||||
raise cv.Invalid("Options strings must not contain newlines")
|
||||
return value
|
||||
|
||||
|
||||
lv_color = LValidator(color, ty.lv_color_t, retmapper=color_retmapper)
|
||||
|
||||
|
||||
@ -156,6 +172,12 @@ lv_bool = LValidator(
|
||||
)
|
||||
|
||||
|
||||
def lv_pct(value: Union[int, float]):
|
||||
if isinstance(value, float):
|
||||
value = int(value * 100)
|
||||
return literal(f"lv_pct({value})")
|
||||
|
||||
|
||||
def lvms_validator_(value):
|
||||
if value == "never":
|
||||
value = "2147483647ms"
|
||||
@ -189,13 +211,16 @@ class TextValidator(LValidator):
|
||||
args = [str(x) for x in value[CONF_ARGS]]
|
||||
arg_expr = cg.RawExpression(",".join(args))
|
||||
format_str = cpp_string_escape(value[CONF_FORMAT])
|
||||
return f"str_sprintf({format_str}, {arg_expr}).c_str()"
|
||||
return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()")
|
||||
return await super().process(value, args)
|
||||
|
||||
|
||||
lv_text = TextValidator()
|
||||
lv_float = LValidator(cv.float_, cg.float_, Sensor, "get_state()")
|
||||
lv_int = LValidator(cv.int_, cg.int_, Sensor, "get_state()")
|
||||
lv_brightness = LValidator(
|
||||
cv.percentage, cg.float_, Sensor, "get_state()", retmapper=lambda x: int(x * 255)
|
||||
)
|
||||
|
||||
|
||||
def is_lv_font(font):
|
||||
@ -222,8 +247,33 @@ class LvFont(LValidator):
|
||||
|
||||
async def process(self, value, args=()):
|
||||
if is_lv_font(value):
|
||||
return ConstantLiteral(f"&lv_font_{value}")
|
||||
return ConstantLiteral(f"{value}_engine->get_lv_font()")
|
||||
return literal(f"&lv_font_{value}")
|
||||
return literal(f"{value}_engine->get_lv_font()")
|
||||
|
||||
|
||||
lv_font = LvFont()
|
||||
|
||||
|
||||
def animated(value):
|
||||
if isinstance(value, bool):
|
||||
value = "ON" if value else "OFF"
|
||||
return LvConstant("LV_ANIM_", "OFF", "ON").one_of(value)
|
||||
|
||||
|
||||
def key_code(value):
|
||||
value = cv.Any(cv.All(cv.string_strict, cv.Length(min=1, max=1)), cv.uint8_t)(value)
|
||||
if isinstance(value, str):
|
||||
return ord(value[0])
|
||||
return value
|
||||
|
||||
|
||||
async def get_end_value(config):
|
||||
return await lv_int.process(config.get(CONF_END_VALUE))
|
||||
|
||||
|
||||
async def get_start_value(config):
|
||||
if CONF_START_VALUE in config:
|
||||
value = config[CONF_START_VALUE]
|
||||
else:
|
||||
value = config.get(CONF_VALUE)
|
||||
return await lv_int.process(value)
|
||||
|
@ -1,9 +1,9 @@
|
||||
import abc
|
||||
import logging
|
||||
from typing import Union
|
||||
|
||||
from esphome import codegen as cg
|
||||
from esphome.core import ID, Lambda
|
||||
from esphome.config import Config
|
||||
from esphome.core import CORE, ID, Lambda
|
||||
from esphome.cpp_generator import (
|
||||
AssignmentExpression,
|
||||
CallExpression,
|
||||
@ -18,12 +18,47 @@ from esphome.cpp_generator import (
|
||||
VariableDeclarationExpression,
|
||||
statement,
|
||||
)
|
||||
from esphome.yaml_util import ESPHomeDataBase
|
||||
|
||||
from .defines import ConstantLiteral
|
||||
from .helpers import get_line_marks
|
||||
from .types import lv_group_t
|
||||
from .defines import literal, lvgl_ns
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
LVGL_COMP = "lv_component" # used as a lambda argument in lvgl_comp()
|
||||
|
||||
# Argument tuple for use in lambdas
|
||||
LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent)
|
||||
LVGL_COMP_ARG = [(LvglComponent.operator("ptr"), LVGL_COMP)]
|
||||
lv_event_t_ptr = cg.global_ns.namespace("lv_event_t").operator("ptr")
|
||||
EVENT_ARG = [(lv_event_t_ptr, "ev")]
|
||||
CUSTOM_EVENT = literal("lvgl::lv_custom_event")
|
||||
|
||||
|
||||
def get_line_marks(value) -> list:
|
||||
"""
|
||||
If possible, return a preprocessor directive to identify the line number where the given id was defined.
|
||||
:param value: The id or other token to get the line number for
|
||||
:return: A list containing zero or more line directives
|
||||
"""
|
||||
path = None
|
||||
if isinstance(value, ESPHomeDataBase):
|
||||
path = value.esp_range
|
||||
elif isinstance(value, ID) and isinstance(CORE.config, Config):
|
||||
path = CORE.config.get_path_for_id(value)[:-1]
|
||||
path = CORE.config.get_deepest_document_range_for_path(path)
|
||||
if path is None:
|
||||
return []
|
||||
return [path.start_mark.as_line_directive]
|
||||
|
||||
|
||||
class IndentedStatement(Statement):
|
||||
def __init__(self, stmt: Statement, indent: int):
|
||||
self.statement = stmt
|
||||
self.indent = indent
|
||||
|
||||
def __str__(self):
|
||||
result = " " * self.indent * 4 + str(self.statement).strip()
|
||||
if not isinstance(self.statement, RawStatement):
|
||||
result += ";"
|
||||
return result
|
||||
|
||||
|
||||
class CodeContext(abc.ABC):
|
||||
@ -39,6 +74,16 @@ class CodeContext(abc.ABC):
|
||||
def add(self, expression: Union[Expression, Statement]):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def start_block():
|
||||
CodeContext.append(RawStatement("{"))
|
||||
CodeContext.code_context.indent()
|
||||
|
||||
@staticmethod
|
||||
def end_block():
|
||||
CodeContext.code_context.detent()
|
||||
CodeContext.append(RawStatement("}"))
|
||||
|
||||
@staticmethod
|
||||
def append(expression: Union[Expression, Statement]):
|
||||
if CodeContext.code_context is not None:
|
||||
@ -47,14 +92,25 @@ class CodeContext(abc.ABC):
|
||||
|
||||
def __init__(self):
|
||||
self.previous: Union[CodeContext | None] = None
|
||||
self.indent_level = 0
|
||||
|
||||
def __enter__(self):
|
||||
async def __aenter__(self):
|
||||
self.previous = CodeContext.code_context
|
||||
CodeContext.code_context = self
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
async def __aexit__(self, *args):
|
||||
CodeContext.code_context = self.previous
|
||||
|
||||
def indent(self):
|
||||
self.indent_level += 1
|
||||
|
||||
def detent(self):
|
||||
self.indent_level -= 1
|
||||
|
||||
def indented_statement(self, stmt):
|
||||
return IndentedStatement(stmt, self.indent_level)
|
||||
|
||||
|
||||
class MainContext(CodeContext):
|
||||
"""
|
||||
@ -62,42 +118,7 @@ class MainContext(CodeContext):
|
||||
"""
|
||||
|
||||
def add(self, expression: Union[Expression, Statement]):
|
||||
return cg.add(expression)
|
||||
|
||||
|
||||
class LvContext(CodeContext):
|
||||
"""
|
||||
Code generation into the LVGL initialisation code (called in `setup()`)
|
||||
"""
|
||||
|
||||
lv_init_code: list["Statement"] = []
|
||||
|
||||
@staticmethod
|
||||
def lv_add(expression: Union[Expression, Statement]):
|
||||
if isinstance(expression, Expression):
|
||||
expression = statement(expression)
|
||||
if not isinstance(expression, Statement):
|
||||
raise ValueError(
|
||||
f"Add '{expression}' must be expression or statement, not {type(expression)}"
|
||||
)
|
||||
LvContext.lv_init_code.append(expression)
|
||||
_LOGGER.debug("LV Adding: %s", expression)
|
||||
return expression
|
||||
|
||||
@staticmethod
|
||||
def get_code():
|
||||
code = []
|
||||
for exp in LvContext.lv_init_code:
|
||||
text = str(statement(exp))
|
||||
text = text.rstrip()
|
||||
code.append(text)
|
||||
return "\n".join(code) + "\n\n"
|
||||
|
||||
def add(self, expression: Union[Expression, Statement]):
|
||||
return LvContext.lv_add(expression)
|
||||
|
||||
def set_style(self, prop):
|
||||
return MockObj("lv_set_style_{prop}", "")
|
||||
return cg.add(self.indented_statement(expression))
|
||||
|
||||
|
||||
class LambdaContext(CodeContext):
|
||||
@ -110,21 +131,23 @@ class LambdaContext(CodeContext):
|
||||
parameters: list[tuple[SafeExpType, str]] = None,
|
||||
return_type: SafeExpType = cg.void,
|
||||
capture: str = "",
|
||||
where=None,
|
||||
):
|
||||
super().__init__()
|
||||
self.code_list: list[Statement] = []
|
||||
self.parameters = parameters
|
||||
self.parameters = parameters or []
|
||||
self.return_type = return_type
|
||||
self.capture = capture
|
||||
self.where = where
|
||||
|
||||
def add(self, expression: Union[Expression, Statement]):
|
||||
self.code_list.append(expression)
|
||||
self.code_list.append(self.indented_statement(expression))
|
||||
return expression
|
||||
|
||||
async def get_lambda(self) -> LambdaExpression:
|
||||
code_text = self.get_code()
|
||||
return await cg.process_lambda(
|
||||
Lambda("\n".join(code_text) + "\n\n"),
|
||||
Lambda("\n".join(code_text) + "\n"),
|
||||
self.parameters,
|
||||
capture=self.capture,
|
||||
return_type=self.return_type,
|
||||
@ -138,33 +161,59 @@ class LambdaContext(CodeContext):
|
||||
code_text.append(text)
|
||||
return code_text
|
||||
|
||||
def __enter__(self):
|
||||
super().__enter__()
|
||||
async def __aenter__(self):
|
||||
await super().__aenter__()
|
||||
add_line_marks(self.where)
|
||||
return self
|
||||
|
||||
|
||||
class LvContext(LambdaContext):
|
||||
"""
|
||||
Code generation into the LVGL initialisation code (called in `setup()`)
|
||||
"""
|
||||
|
||||
def __init__(self, lv_component, args=None):
|
||||
self.args = args or LVGL_COMP_ARG
|
||||
super().__init__(parameters=self.args)
|
||||
self.lv_component = lv_component
|
||||
|
||||
async def add_init_lambda(self):
|
||||
cg.add(self.lv_component.add_init_lambda(await self.get_lambda()))
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await super().__aexit__(exc_type, exc_val, exc_tb)
|
||||
await self.add_init_lambda()
|
||||
|
||||
def add(self, expression: Union[Expression, Statement]):
|
||||
self.code_list.append(self.indented_statement(expression))
|
||||
return expression
|
||||
|
||||
def __call__(self, *args):
|
||||
return self.add(*args)
|
||||
|
||||
|
||||
class LocalVariable(MockObj):
|
||||
"""
|
||||
Create a local variable and enclose the code using it within a block.
|
||||
"""
|
||||
|
||||
def __init__(self, name, type, modifier=None, rhs=None):
|
||||
base = ID(name, True, type)
|
||||
def __init__(self, name, type, rhs=None, modifier="*"):
|
||||
base = ID(name + "_VAR_", True, type)
|
||||
super().__init__(base, "")
|
||||
self.modifier = modifier
|
||||
self.rhs = rhs
|
||||
|
||||
def __enter__(self):
|
||||
CodeContext.append(RawStatement("{"))
|
||||
CodeContext.start_block()
|
||||
CodeContext.append(
|
||||
VariableDeclarationExpression(self.base.type, self.modifier, self.base.id)
|
||||
)
|
||||
if self.rhs is not None:
|
||||
CodeContext.append(AssignmentExpression(None, "", self.base, self.rhs))
|
||||
return self.base
|
||||
return MockObj(self.base)
|
||||
|
||||
def __exit__(self, *args):
|
||||
CodeContext.append(RawStatement("}"))
|
||||
CodeContext.end_block()
|
||||
|
||||
|
||||
class MockLv:
|
||||
@ -199,14 +248,27 @@ class MockLv:
|
||||
self.append(result)
|
||||
return result
|
||||
|
||||
def cond_if(self, expression: Expression):
|
||||
CodeContext.append(RawStatement(f"if {expression} {{"))
|
||||
|
||||
def cond_else(self):
|
||||
class LvConditional:
|
||||
def __init__(self, condition):
|
||||
self.condition = condition
|
||||
|
||||
def __enter__(self):
|
||||
if self.condition is not None:
|
||||
CodeContext.append(RawStatement(f"if ({self.condition}) {{"))
|
||||
CodeContext.code_context.indent()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
if self.condition is not None:
|
||||
CodeContext.code_context.detent()
|
||||
CodeContext.append(RawStatement("}"))
|
||||
|
||||
def else_(self):
|
||||
assert self.condition is not None
|
||||
CodeContext.code_context.detent()
|
||||
CodeContext.append(RawStatement("} else {"))
|
||||
|
||||
def cond_endif(self):
|
||||
CodeContext.append(RawStatement("}"))
|
||||
CodeContext.code_context.indent()
|
||||
|
||||
|
||||
class ReturnStatement(ExpressionStatement):
|
||||
@ -228,36 +290,56 @@ lv = MockLv("lv_")
|
||||
lv_expr = LvExpr("lv_")
|
||||
# Mock for lv_obj_ calls
|
||||
lv_obj = MockLv("lv_obj_")
|
||||
lvgl_comp = MockObj("lvgl_comp", "->")
|
||||
# Operations on the LVGL component
|
||||
lvgl_comp = MockObj(LVGL_COMP, "->")
|
||||
|
||||
|
||||
# equivalent to cg.add() for the lvgl init context
|
||||
# equivalent to cg.add() for the current code context
|
||||
def lv_add(expression: Union[Expression, Statement]):
|
||||
return CodeContext.append(expression)
|
||||
|
||||
|
||||
def add_line_marks(where):
|
||||
"""
|
||||
Add line marks for the current code context
|
||||
:param where: An object to identify the source of the line marks
|
||||
:return:
|
||||
"""
|
||||
for mark in get_line_marks(where):
|
||||
lv_add(cg.RawStatement(mark))
|
||||
|
||||
|
||||
def lv_assign(target, expression):
|
||||
lv_add(RawExpression(f"{target} = {expression}"))
|
||||
lv_add(AssignmentExpression("", "", target, expression))
|
||||
|
||||
|
||||
lv_groups = {} # Widget group names
|
||||
def lv_Pvariable(type, name):
|
||||
"""
|
||||
Create but do not initialise a pointer variable
|
||||
:param type: Type of the variable target
|
||||
:param name: name of the variable, or an ID
|
||||
:return: A MockObj of the variable
|
||||
"""
|
||||
if isinstance(name, str):
|
||||
name = ID(name, True, type)
|
||||
decl = VariableDeclarationExpression(type, "*", name)
|
||||
CORE.add_global(decl)
|
||||
var = MockObj(name, "->")
|
||||
CORE.register_variable(name, var)
|
||||
return var
|
||||
|
||||
|
||||
def add_group(name):
|
||||
if name is None:
|
||||
return None
|
||||
fullname = f"lv_esp_group_{name}"
|
||||
if name not in lv_groups:
|
||||
gid = ID(fullname, True, type=lv_group_t.operator("ptr"))
|
||||
lv_add(
|
||||
AssignmentExpression(
|
||||
type_=gid.type, modifier="", name=fullname, rhs=lv_expr.group_create()
|
||||
)
|
||||
)
|
||||
lv_groups[name] = ConstantLiteral(fullname)
|
||||
return lv_groups[name]
|
||||
def lv_variable(type, name):
|
||||
"""
|
||||
Create but do not initialise a variable
|
||||
:param type: Type of the variable target
|
||||
:param name: name of the variable, or an ID
|
||||
:return: A MockObj of the variable
|
||||
"""
|
||||
if isinstance(name, str):
|
||||
name = ID(name, True, type)
|
||||
decl = VariableDeclarationExpression(type, "", name)
|
||||
CORE.add_global(decl)
|
||||
var = MockObj(name, ".")
|
||||
CORE.register_variable(name, var)
|
||||
return var
|
||||
|
@ -9,8 +9,72 @@ namespace esphome {
|
||||
namespace lvgl {
|
||||
static const char *const TAG = "lvgl";
|
||||
|
||||
#if LV_USE_LOG
|
||||
static void log_cb(const char *buf) {
|
||||
esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, TAG, 0, "%.*s", (int) strlen(buf) - 1, buf);
|
||||
}
|
||||
#endif // LV_USE_LOG
|
||||
|
||||
static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) {
|
||||
// make sure all coordinates are even
|
||||
if (area->x1 & 1)
|
||||
area->x1--;
|
||||
if (!(area->x2 & 1))
|
||||
area->x2++;
|
||||
if (area->y1 & 1)
|
||||
area->y1--;
|
||||
if (!(area->y2 & 1))
|
||||
area->y2++;
|
||||
}
|
||||
|
||||
lv_event_code_t lv_custom_event; // NOLINT
|
||||
void LvglComponent::dump_config() { ESP_LOGCONFIG(TAG, "LVGL:"); }
|
||||
void LvglComponent::set_paused(bool paused, bool show_snow) {
|
||||
this->paused_ = paused;
|
||||
this->show_snow_ = show_snow;
|
||||
this->snow_line_ = 0;
|
||||
if (!paused && lv_scr_act() != nullptr) {
|
||||
lv_disp_trig_activity(this->disp_); // resets the inactivity time
|
||||
lv_obj_invalidate(lv_scr_act());
|
||||
}
|
||||
}
|
||||
void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) {
|
||||
lv_obj_add_event_cb(obj, callback, event, this);
|
||||
if (event == LV_EVENT_VALUE_CHANGED) {
|
||||
lv_obj_add_event_cb(obj, callback, lv_custom_event, this);
|
||||
}
|
||||
}
|
||||
void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1,
|
||||
lv_event_code_t event2) {
|
||||
this->add_event_cb(obj, callback, event1);
|
||||
this->add_event_cb(obj, callback, event2);
|
||||
}
|
||||
void LvglComponent::add_page(LvPageType *page) {
|
||||
this->pages_.push_back(page);
|
||||
page->setup(this->pages_.size() - 1);
|
||||
}
|
||||
void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) {
|
||||
if (index >= this->pages_.size())
|
||||
return;
|
||||
this->current_page_ = index;
|
||||
lv_scr_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false);
|
||||
}
|
||||
void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) {
|
||||
if (this->pages_.empty() || (this->current_page_ == this->pages_.size() - 1 && !this->page_wrap_))
|
||||
return;
|
||||
do {
|
||||
this->current_page_ = (this->current_page_ + 1) % this->pages_.size();
|
||||
} while (this->pages_[this->current_page_]->skip); // skip empty pages()
|
||||
this->show_page(this->current_page_, anim, time);
|
||||
}
|
||||
void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) {
|
||||
if (this->pages_.empty() || (this->current_page_ == 0 && !this->page_wrap_))
|
||||
return;
|
||||
do {
|
||||
this->current_page_ = (this->current_page_ + this->pages_.size() - 1) % this->pages_.size();
|
||||
} while (this->pages_[this->current_page_]->skip); // skip empty pages()
|
||||
this->show_page(this->current_page_, anim, time);
|
||||
}
|
||||
void LvglComponent::draw_buffer_(const lv_area_t *area, const uint8_t *ptr) {
|
||||
for (auto *display : this->displays_) {
|
||||
display->draw_pixels_at(area->x1, area->y1, lv_area_get_width(area), lv_area_get_height(area), ptr,
|
||||
@ -27,6 +91,116 @@ void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv
|
||||
}
|
||||
lv_disp_flush_ready(disp_drv);
|
||||
}
|
||||
IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeout) : timeout_(std::move(timeout)) {
|
||||
parent->add_on_idle_callback([this](uint32_t idle_time) {
|
||||
if (!this->is_idle_ && idle_time > this->timeout_.value()) {
|
||||
this->is_idle_ = true;
|
||||
this->trigger();
|
||||
} else if (this->is_idle_ && idle_time < this->timeout_.value()) {
|
||||
this->is_idle_ = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#ifdef USE_LVGL_TOUCHSCREEN
|
||||
LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time) {
|
||||
lv_indev_drv_init(&this->drv_);
|
||||
this->drv_.long_press_repeat_time = long_press_repeat_time;
|
||||
this->drv_.long_press_time = long_press_time;
|
||||
this->drv_.type = LV_INDEV_TYPE_POINTER;
|
||||
this->drv_.user_data = this;
|
||||
this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) {
|
||||
auto *l = static_cast<LVTouchListener *>(d->user_data);
|
||||
if (l->touch_pressed_) {
|
||||
data->point.x = l->touch_point_.x;
|
||||
data->point.y = l->touch_point_.y;
|
||||
data->state = LV_INDEV_STATE_PRESSED;
|
||||
} else {
|
||||
data->state = LV_INDEV_STATE_RELEASED;
|
||||
}
|
||||
};
|
||||
}
|
||||
void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) {
|
||||
this->touch_pressed_ = !this->parent_->is_paused() && !tpoints.empty();
|
||||
if (this->touch_pressed_)
|
||||
this->touch_point_ = tpoints[0];
|
||||
}
|
||||
#endif // USE_LVGL_TOUCHSCREEN
|
||||
|
||||
#ifdef USE_LVGL_ROTARY_ENCODER
|
||||
LVEncoderListener::LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt) {
|
||||
lv_indev_drv_init(&this->drv_);
|
||||
this->drv_.type = type;
|
||||
this->drv_.user_data = this;
|
||||
this->drv_.long_press_time = lpt;
|
||||
this->drv_.long_press_repeat_time = lprt;
|
||||
this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) {
|
||||
auto *l = static_cast<LVEncoderListener *>(d->user_data);
|
||||
data->state = l->pressed_ ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
|
||||
data->key = l->key_;
|
||||
data->enc_diff = (int16_t) (l->count_ - l->last_count_);
|
||||
l->last_count_ = l->count_;
|
||||
data->continue_reading = false;
|
||||
};
|
||||
}
|
||||
#endif // USE_LVGL_ROTARY_ENCODER
|
||||
|
||||
#ifdef USE_LVGL_BUTTONMATRIX
|
||||
void LvBtnmatrixType::set_obj(lv_obj_t *lv_obj) {
|
||||
LvCompound::set_obj(lv_obj);
|
||||
lv_obj_add_event_cb(
|
||||
lv_obj,
|
||||
[](lv_event_t *event) {
|
||||
auto *self = static_cast<LvBtnmatrixType *>(event->user_data);
|
||||
if (self->key_callback_.size() == 0)
|
||||
return;
|
||||
auto key_idx = lv_btnmatrix_get_selected_btn(self->obj);
|
||||
if (key_idx == LV_BTNMATRIX_BTN_NONE)
|
||||
return;
|
||||
if (self->key_map_.count(key_idx) != 0) {
|
||||
self->send_key_(self->key_map_[key_idx]);
|
||||
return;
|
||||
}
|
||||
const auto *str = lv_btnmatrix_get_btn_text(self->obj, key_idx);
|
||||
auto len = strlen(str);
|
||||
while (len--)
|
||||
self->send_key_(*str++);
|
||||
},
|
||||
LV_EVENT_PRESSED, this);
|
||||
}
|
||||
#endif // USE_LVGL_BUTTONMATRIX
|
||||
|
||||
#ifdef USE_LVGL_KEYBOARD
|
||||
static const char *const KB_SPECIAL_KEYS[] = {
|
||||
"abc", "ABC", "1#",
|
||||
// maybe add other special keys here
|
||||
};
|
||||
|
||||
void LvKeyboardType::set_obj(lv_obj_t *lv_obj) {
|
||||
LvCompound::set_obj(lv_obj);
|
||||
lv_obj_add_event_cb(
|
||||
lv_obj,
|
||||
[](lv_event_t *event) {
|
||||
auto *self = static_cast<LvKeyboardType *>(event->user_data);
|
||||
if (self->key_callback_.size() == 0)
|
||||
return;
|
||||
|
||||
auto key_idx = lv_btnmatrix_get_selected_btn(self->obj);
|
||||
if (key_idx == LV_BTNMATRIX_BTN_NONE)
|
||||
return;
|
||||
const char *txt = lv_btnmatrix_get_btn_text(self->obj, key_idx);
|
||||
if (txt == nullptr)
|
||||
return;
|
||||
for (const auto *kb_special_key : KB_SPECIAL_KEYS) {
|
||||
if (strcmp(txt, kb_special_key) == 0)
|
||||
return;
|
||||
}
|
||||
while (*txt != 0)
|
||||
self->send_key_(*txt++);
|
||||
},
|
||||
LV_EVENT_PRESSED, this);
|
||||
}
|
||||
#endif // USE_LVGL_KEYBOARD
|
||||
|
||||
void LvglComponent::write_random_() {
|
||||
// length of 2 lines in 32 bit units
|
||||
@ -97,9 +271,24 @@ void LvglComponent::setup() {
|
||||
this->disp_ = lv_disp_drv_register(&this->disp_drv_);
|
||||
for (const auto &v : this->init_lambdas_)
|
||||
v(this);
|
||||
this->show_page(0, LV_SCR_LOAD_ANIM_NONE, 0);
|
||||
lv_disp_trig_activity(this->disp_);
|
||||
ESP_LOGCONFIG(TAG, "LVGL Setup complete");
|
||||
}
|
||||
void LvglComponent::update() {
|
||||
// update indicators
|
||||
if (this->paused_) {
|
||||
return;
|
||||
}
|
||||
this->idle_callbacks_.call(lv_disp_get_inactive_time(this->disp_));
|
||||
}
|
||||
void LvglComponent::loop() {
|
||||
if (this->paused_) {
|
||||
if (this->show_snow_)
|
||||
this->write_random_();
|
||||
}
|
||||
lv_timer_handler_run_in_period(5);
|
||||
}
|
||||
|
||||
#ifdef USE_LVGL_IMAGE
|
||||
lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc) {
|
||||
@ -142,7 +331,20 @@ lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc) {
|
||||
}
|
||||
return img_dsc;
|
||||
}
|
||||
#endif // USE_LVGL_IMAGE
|
||||
|
||||
#ifdef USE_LVGL_ANIMIMG
|
||||
void lv_animimg_stop(lv_obj_t *obj) {
|
||||
auto *animg = (lv_animimg_t *) obj;
|
||||
int32_t duration = animg->anim.time;
|
||||
lv_animimg_set_duration(obj, 0);
|
||||
lv_animimg_start(obj);
|
||||
lv_animimg_set_duration(obj, duration);
|
||||
}
|
||||
#endif
|
||||
void LvglComponent::static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
|
||||
reinterpret_cast<LvglComponent *>(disp_drv->user_data)->flush_cb_(disp_drv, area, color_p);
|
||||
}
|
||||
} // namespace lvgl
|
||||
} // namespace esphome
|
||||
|
||||
|
@ -18,7 +18,6 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <lvgl.h>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#ifdef USE_LVGL_IMAGE
|
||||
#include "esphome/components/image/image.h"
|
||||
@ -31,6 +30,10 @@
|
||||
#include "esphome/components/touchscreen/touchscreen.h"
|
||||
#endif // USE_LVGL_TOUCHSCREEN
|
||||
|
||||
#if defined(USE_LVGL_BUTTONMATRIX) || defined(USE_LVGL_KEYBOARD)
|
||||
#include "esphome/components/key_provider/key_provider.h"
|
||||
#endif // USE_LVGL_BUTTONMATRIX
|
||||
|
||||
namespace esphome {
|
||||
namespace lvgl {
|
||||
|
||||
@ -47,12 +50,25 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT
|
||||
#endif // LV_COLOR_DEPTH
|
||||
|
||||
// Parent class for things that wrap an LVGL object
|
||||
class LvCompound final {
|
||||
class LvCompound {
|
||||
public:
|
||||
void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; }
|
||||
virtual void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; }
|
||||
lv_obj_t *obj{};
|
||||
};
|
||||
|
||||
class LvPageType {
|
||||
public:
|
||||
LvPageType(bool skip) : skip(skip) {}
|
||||
|
||||
void setup(size_t index) {
|
||||
this->index = index;
|
||||
this->obj = lv_obj_create(nullptr);
|
||||
}
|
||||
lv_obj_t *obj{};
|
||||
size_t index{};
|
||||
bool skip;
|
||||
};
|
||||
|
||||
using LvLambdaType = std::function<void(lv_obj_t *)>;
|
||||
using set_value_lambda_t = std::function<void(float)>;
|
||||
using event_callback_t = void(_lv_event_t *);
|
||||
@ -89,48 +105,20 @@ class FontEngine {
|
||||
lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc = nullptr);
|
||||
#endif // USE_LVGL_IMAGE
|
||||
|
||||
#ifdef USE_LVGL_ANIMIMG
|
||||
void lv_animimg_stop(lv_obj_t *obj);
|
||||
#endif // USE_LVGL_ANIMIMG
|
||||
|
||||
class LvglComponent : public PollingComponent {
|
||||
constexpr static const char *const TAG = "lvgl";
|
||||
|
||||
public:
|
||||
static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
|
||||
reinterpret_cast<LvglComponent *>(disp_drv->user_data)->flush_cb_(disp_drv, area, color_p);
|
||||
}
|
||||
static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p);
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
static void log_cb(const char *buf) {
|
||||
esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, TAG, 0, "%.*s", (int) strlen(buf) - 1, buf);
|
||||
}
|
||||
static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) {
|
||||
// make sure all coordinates are even
|
||||
if (area->x1 & 1)
|
||||
area->x1--;
|
||||
if (!(area->x2 & 1))
|
||||
area->x2++;
|
||||
if (area->y1 & 1)
|
||||
area->y1--;
|
||||
if (!(area->y2 & 1))
|
||||
area->y2++;
|
||||
}
|
||||
|
||||
void setup() override;
|
||||
|
||||
void update() override {
|
||||
// update indicators
|
||||
if (this->paused_) {
|
||||
return;
|
||||
}
|
||||
this->idle_callbacks_.call(lv_disp_get_inactive_time(this->disp_));
|
||||
}
|
||||
|
||||
void loop() override {
|
||||
if (this->paused_) {
|
||||
if (this->show_snow_)
|
||||
this->write_random_();
|
||||
}
|
||||
lv_timer_handler_run_in_period(5);
|
||||
}
|
||||
|
||||
void update() override;
|
||||
void loop() override;
|
||||
void add_on_idle_callback(std::function<void(uint32_t)> &&callback) {
|
||||
this->idle_callbacks_.add(std::move(callback));
|
||||
}
|
||||
@ -141,23 +129,15 @@ class LvglComponent : public PollingComponent {
|
||||
bool is_idle(uint32_t idle_ms) { return lv_disp_get_inactive_time(this->disp_) > idle_ms; }
|
||||
void set_buffer_frac(size_t frac) { this->buffer_frac_ = frac; }
|
||||
lv_disp_t *get_disp() { return this->disp_; }
|
||||
void set_paused(bool paused, bool show_snow) {
|
||||
this->paused_ = paused;
|
||||
this->show_snow_ = show_snow;
|
||||
this->snow_line_ = 0;
|
||||
if (!paused && lv_scr_act() != nullptr) {
|
||||
lv_disp_trig_activity(this->disp_); // resets the inactivity time
|
||||
lv_obj_invalidate(lv_scr_act());
|
||||
}
|
||||
}
|
||||
|
||||
void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) {
|
||||
lv_obj_add_event_cb(obj, callback, event, this);
|
||||
if (event == LV_EVENT_VALUE_CHANGED) {
|
||||
lv_obj_add_event_cb(obj, callback, lv_custom_event, this);
|
||||
}
|
||||
}
|
||||
void set_paused(bool paused, bool show_snow);
|
||||
void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event);
|
||||
void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2);
|
||||
bool is_paused() const { return this->paused_; }
|
||||
void add_page(LvPageType *page);
|
||||
void show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time);
|
||||
void show_next_page(lv_scr_load_anim_t anim, uint32_t time);
|
||||
void show_prev_page(lv_scr_load_anim_t anim, uint32_t time);
|
||||
void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; }
|
||||
|
||||
protected:
|
||||
void write_random_();
|
||||
@ -168,8 +148,11 @@ class LvglComponent : public PollingComponent {
|
||||
lv_disp_drv_t disp_drv_{};
|
||||
lv_disp_t *disp_{};
|
||||
bool paused_{};
|
||||
std::vector<LvPageType *> pages_{};
|
||||
size_t current_page_{0};
|
||||
bool show_snow_{};
|
||||
lv_coord_t snow_line_{};
|
||||
bool page_wrap_{true};
|
||||
|
||||
std::vector<std::function<void(LvglComponent *lv_component)>> init_lambdas_;
|
||||
CallbackManager<void(uint32_t)> idle_callbacks_{};
|
||||
@ -179,16 +162,7 @@ class LvglComponent : public PollingComponent {
|
||||
|
||||
class IdleTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeout) : timeout_(std::move(timeout)) {
|
||||
parent->add_on_idle_callback([this](uint32_t idle_time) {
|
||||
if (!this->is_idle_ && idle_time > this->timeout_.value()) {
|
||||
this->is_idle_ = true;
|
||||
this->trigger();
|
||||
} else if (this->is_idle_ && idle_time < this->timeout_.value()) {
|
||||
this->is_idle_ = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
explicit IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeout);
|
||||
|
||||
protected:
|
||||
TemplatableValue<uint32_t> timeout_;
|
||||
@ -217,28 +191,8 @@ template<typename... Ts> class LvglCondition : public Condition<Ts...>, public P
|
||||
#ifdef USE_LVGL_TOUCHSCREEN
|
||||
class LVTouchListener : public touchscreen::TouchListener, public Parented<LvglComponent> {
|
||||
public:
|
||||
LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time) {
|
||||
lv_indev_drv_init(&this->drv_);
|
||||
this->drv_.long_press_repeat_time = long_press_repeat_time;
|
||||
this->drv_.long_press_time = long_press_time;
|
||||
this->drv_.type = LV_INDEV_TYPE_POINTER;
|
||||
this->drv_.user_data = this;
|
||||
this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) {
|
||||
auto *l = static_cast<LVTouchListener *>(d->user_data);
|
||||
if (l->touch_pressed_) {
|
||||
data->point.x = l->touch_point_.x;
|
||||
data->point.y = l->touch_point_.y;
|
||||
data->state = LV_INDEV_STATE_PRESSED;
|
||||
} else {
|
||||
data->state = LV_INDEV_STATE_RELEASED;
|
||||
}
|
||||
};
|
||||
}
|
||||
void update(const touchscreen::TouchPoints_t &tpoints) override {
|
||||
this->touch_pressed_ = !this->parent_->is_paused() && !tpoints.empty();
|
||||
if (this->touch_pressed_)
|
||||
this->touch_point_ = tpoints[0];
|
||||
}
|
||||
LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time);
|
||||
void update(const touchscreen::TouchPoints_t &tpoints) override;
|
||||
void release() override { touch_pressed_ = false; }
|
||||
lv_indev_drv_t *get_drv() { return &this->drv_; }
|
||||
|
||||
@ -249,24 +203,10 @@ class LVTouchListener : public touchscreen::TouchListener, public Parented<LvglC
|
||||
};
|
||||
#endif // USE_LVGL_TOUCHSCREEN
|
||||
|
||||
#ifdef USE_LVGL_KEY_LISTENER
|
||||
#ifdef USE_LVGL_ROTARY_ENCODER
|
||||
class LVEncoderListener : public Parented<LvglComponent> {
|
||||
public:
|
||||
LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt) {
|
||||
lv_indev_drv_init(&this->drv_);
|
||||
this->drv_.type = type;
|
||||
this->drv_.user_data = this;
|
||||
this->drv_.long_press_time = lpt;
|
||||
this->drv_.long_press_repeat_time = lprt;
|
||||
this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) {
|
||||
auto *l = static_cast<LVEncoderListener *>(d->user_data);
|
||||
data->state = l->pressed_ ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
|
||||
data->key = l->key_;
|
||||
data->enc_diff = (int16_t) (l->count_ - l->last_count_);
|
||||
l->last_count_ = l->count_;
|
||||
data->continue_reading = false;
|
||||
};
|
||||
}
|
||||
LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt);
|
||||
|
||||
void set_left_button(binary_sensor::BinarySensor *left_button) {
|
||||
left_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_LEFT, state); });
|
||||
@ -304,6 +244,24 @@ class LVEncoderListener : public Parented<LvglComponent> {
|
||||
int32_t last_count_{};
|
||||
int key_{};
|
||||
};
|
||||
#endif // USE_LVGL_KEY_LISTENER
|
||||
#endif // USE_LVGL_ROTARY_ENCODER
|
||||
#ifdef USE_LVGL_BUTTONMATRIX
|
||||
class LvBtnmatrixType : public key_provider::KeyProvider, public LvCompound {
|
||||
public:
|
||||
void set_obj(lv_obj_t *lv_obj) override;
|
||||
uint16_t get_selected() { return lv_btnmatrix_get_selected_btn(this->obj); }
|
||||
void set_key(size_t idx, uint8_t key) { this->key_map_[idx] = key; }
|
||||
|
||||
protected:
|
||||
std::map<size_t, uint8_t> key_map_{};
|
||||
};
|
||||
#endif // USE_LVGL_BUTTONMATRIX
|
||||
|
||||
#ifdef USE_LVGL_KEYBOARD
|
||||
class LvKeyboardType : public key_provider::KeyProvider, public LvCompound {
|
||||
public:
|
||||
void set_obj(lv_obj_t *lv_obj) override;
|
||||
};
|
||||
#endif // USE_LVGL_KEYBOARD
|
||||
} // namespace lvgl
|
||||
} // namespace esphome
|
||||
|
113
esphome/components/lvgl/page.py
Normal file
113
esphome/components/lvgl/page.py
Normal 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)
|
@ -13,9 +13,10 @@ from .defines import (
|
||||
CONF_ROTARY_ENCODERS,
|
||||
)
|
||||
from .helpers import lvgl_components_required
|
||||
from .lvcode import add_group, lv, lv_add, lv_expr
|
||||
from .lvcode import lv, lv_add, lv_expr
|
||||
from .schemas import ENCODER_SCHEMA
|
||||
from .types import lv_indev_type_t
|
||||
from .widget import add_group
|
||||
|
||||
ROTARY_ENCODER_CONFIG = cv.ensure_list(
|
||||
ENCODER_SCHEMA.extend(
|
||||
|
@ -15,8 +15,12 @@ from esphome.schema_extractors import SCHEMA_EXTRACT
|
||||
|
||||
from . import defines as df, lv_validation as lvalid, types as ty
|
||||
from .helpers import add_lv_use, requires_component, validate_printf
|
||||
from .lv_validation import id_name, lv_font
|
||||
from .types import WIDGET_TYPES, WidgetType
|
||||
from .lv_validation import id_name, lv_color, lv_font, lv_image
|
||||
from .lvcode import LvglComponent
|
||||
from .types import WidgetType
|
||||
|
||||
# this will be populated later, in __init__.py to avoid circular imports.
|
||||
WIDGET_TYPES: dict = {}
|
||||
|
||||
# A schema for text properties
|
||||
TEXT_SCHEMA = cv.Schema(
|
||||
@ -38,11 +42,13 @@ TEXT_SCHEMA = cv.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(ty.lv_pseudo_button_t),
|
||||
},
|
||||
key=CONF_ID,
|
||||
LIST_ACTION_SCHEMA = cv.ensure_list(
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(ty.lv_pseudo_button_t),
|
||||
},
|
||||
key=CONF_ID,
|
||||
)
|
||||
)
|
||||
|
||||
PRESS_TIME = cv.All(
|
||||
@ -154,6 +160,7 @@ STYLE_REMAP = {
|
||||
# Complete object style schema
|
||||
STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend(
|
||||
{
|
||||
cv.Optional(df.CONF_STYLES): cv.ensure_list(cv.use_id(ty.lv_style_t)),
|
||||
cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant(
|
||||
"LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO"
|
||||
).one_of,
|
||||
@ -209,7 +216,14 @@ def create_modify_schema(widget_type):
|
||||
part_schema(widget_type)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(widget_type),
|
||||
cv.Required(CONF_ID): cv.ensure_list(
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(widget_type),
|
||||
},
|
||||
key=CONF_ID,
|
||||
)
|
||||
),
|
||||
cv.Optional(CONF_STATE): SET_STATE_SCHEMA,
|
||||
}
|
||||
)
|
||||
@ -227,6 +241,7 @@ def obj_schema(widget_type: WidgetType):
|
||||
return (
|
||||
part_schema(widget_type)
|
||||
.extend(FLAG_SCHEMA)
|
||||
.extend(LAYOUT_SCHEMA)
|
||||
.extend(ALIGN_TO_SCHEMA)
|
||||
.extend(automation_schema(widget_type.w_type))
|
||||
.extend(
|
||||
@ -240,6 +255,8 @@ def obj_schema(widget_type: WidgetType):
|
||||
)
|
||||
|
||||
|
||||
LAYOUT_SCHEMAS = {}
|
||||
|
||||
ALIGN_TO_SCHEMA = {
|
||||
cv.Optional(df.CONF_ALIGN_TO): cv.Schema(
|
||||
{
|
||||
@ -252,6 +269,65 @@ ALIGN_TO_SCHEMA = {
|
||||
}
|
||||
|
||||
|
||||
def grid_free_space(value):
|
||||
value = cv.Upper(value)
|
||||
if value.startswith("FR(") and value.endswith(")"):
|
||||
value = value.removesuffix(")").removeprefix("FR(")
|
||||
return f"LV_GRID_FR({cv.positive_int(value)})"
|
||||
raise cv.Invalid("must be a size in pixels, CONTENT or FR(nn)")
|
||||
|
||||
|
||||
grid_spec = cv.Any(
|
||||
lvalid.size, df.LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space
|
||||
)
|
||||
|
||||
cell_alignments = df.LV_CELL_ALIGNMENTS.one_of
|
||||
grid_alignments = df.LV_GRID_ALIGNMENTS.one_of
|
||||
flex_alignments = df.LV_FLEX_ALIGNMENTS.one_of
|
||||
|
||||
LAYOUT_SCHEMA = {
|
||||
cv.Optional(df.CONF_LAYOUT): cv.typed_schema(
|
||||
{
|
||||
df.TYPE_GRID: {
|
||||
cv.Required(df.CONF_GRID_ROWS): [grid_spec],
|
||||
cv.Required(df.CONF_GRID_COLUMNS): [grid_spec],
|
||||
cv.Optional(df.CONF_GRID_COLUMN_ALIGN): grid_alignments,
|
||||
cv.Optional(df.CONF_GRID_ROW_ALIGN): grid_alignments,
|
||||
},
|
||||
df.TYPE_FLEX: {
|
||||
cv.Optional(
|
||||
df.CONF_FLEX_FLOW, default="row_wrap"
|
||||
): df.FLEX_FLOWS.one_of,
|
||||
cv.Optional(df.CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments,
|
||||
cv.Optional(df.CONF_FLEX_ALIGN_CROSS, default="start"): flex_alignments,
|
||||
cv.Optional(df.CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments,
|
||||
},
|
||||
},
|
||||
lower=True,
|
||||
)
|
||||
}
|
||||
|
||||
GRID_CELL_SCHEMA = {
|
||||
cv.Required(df.CONF_GRID_CELL_ROW_POS): cv.positive_int,
|
||||
cv.Required(df.CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
|
||||
cv.Optional(df.CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int,
|
||||
cv.Optional(df.CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int,
|
||||
cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments,
|
||||
cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments,
|
||||
}
|
||||
|
||||
FLEX_OBJ_SCHEMA = {
|
||||
cv.Optional(df.CONF_FLEX_GROW): cv.int_,
|
||||
}
|
||||
|
||||
DISP_BG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(df.CONF_DISP_BG_IMAGE): lv_image,
|
||||
cv.Optional(df.CONF_DISP_BG_COLOR): lv_color,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# A style schema that can include text
|
||||
STYLED_TEXT_SCHEMA = cv.maybe_simple_value(
|
||||
STYLE_SCHEMA.extend(TEXT_SCHEMA), key=df.CONF_TEXT
|
||||
@ -260,13 +336,11 @@ STYLED_TEXT_SCHEMA = cv.maybe_simple_value(
|
||||
# For use by platform components
|
||||
LVGL_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(df.CONF_LVGL_ID): cv.use_id(ty.LvglComponent),
|
||||
cv.GenerateID(df.CONF_LVGL_ID): cv.use_id(LvglComponent),
|
||||
}
|
||||
)
|
||||
|
||||
ALL_STYLES = {
|
||||
**STYLE_PROPS,
|
||||
}
|
||||
ALL_STYLES = {**STYLE_PROPS, **GRID_CELL_SCHEMA, **FLEX_OBJ_SCHEMA}
|
||||
|
||||
|
||||
def container_validator(schema, widget_type: WidgetType):
|
||||
@ -281,16 +355,17 @@ def container_validator(schema, widget_type: WidgetType):
|
||||
result = schema
|
||||
if w_sch := widget_type.schema:
|
||||
result = result.extend(w_sch)
|
||||
ltype = df.TYPE_NONE
|
||||
if value and (layout := value.get(df.CONF_LAYOUT)):
|
||||
if not isinstance(layout, dict):
|
||||
raise cv.Invalid("Layout value must be a dict")
|
||||
ltype = layout.get(CONF_TYPE)
|
||||
if not ltype:
|
||||
raise (cv.Invalid("Layout schema requires type:"))
|
||||
add_lv_use(ltype)
|
||||
result = result.extend(
|
||||
{cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema())}
|
||||
)
|
||||
if value == SCHEMA_EXTRACT:
|
||||
return result
|
||||
result = result.extend(LAYOUT_SCHEMAS[ltype.lower()])
|
||||
return result(value)
|
||||
|
||||
return validator
|
||||
|
63
esphome/components/lvgl/slider.py
Normal file
63
esphome/components/lvgl/slider.py
Normal 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()
|
43
esphome/components/lvgl/spinner.py
Normal file
43
esphome/components/lvgl/spinner.py
Normal 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()
|
58
esphome/components/lvgl/styles.py
Normal file
58
esphome/components/lvgl/styles.py
Normal 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)
|
@ -7,15 +7,14 @@ from .defines import (
|
||||
CONF_ALIGN_TO,
|
||||
CONF_X,
|
||||
CONF_Y,
|
||||
LV_EVENT,
|
||||
LV_EVENT_MAP,
|
||||
LV_EVENT_TRIGGERS,
|
||||
literal,
|
||||
)
|
||||
from .lvcode import LambdaContext, add_line_marks, lv, lv_add
|
||||
from .lvcode import EVENT_ARG, LambdaContext, LvConditional, lv, lv_add
|
||||
from .types import LV_EVENT
|
||||
from .widget import widget_map
|
||||
|
||||
lv_event_t_ptr = cg.global_ns.namespace("lv_event_t").operator("ptr")
|
||||
|
||||
|
||||
async def generate_triggers(lv_component):
|
||||
"""
|
||||
@ -34,15 +33,15 @@ async def generate_triggers(lv_component):
|
||||
}.items():
|
||||
conf = conf[0]
|
||||
w.add_flag("LV_OBJ_FLAG_CLICKABLE")
|
||||
event = "LV_EVENT_" + LV_EVENT[event[3:].upper()]
|
||||
event = literal("LV_EVENT_" + LV_EVENT_MAP[event[3:].upper()])
|
||||
await add_trigger(conf, event, lv_component, w)
|
||||
for conf in w.config.get(CONF_ON_VALUE, ()):
|
||||
await add_trigger(conf, "LV_EVENT_VALUE_CHANGED", lv_component, w)
|
||||
await add_trigger(conf, LV_EVENT.VALUE_CHANGED, lv_component, w)
|
||||
|
||||
# Generate align to directives while we're here
|
||||
if align_to := w.config.get(CONF_ALIGN_TO):
|
||||
target = widget_map[align_to[CONF_ID]].obj
|
||||
align = align_to[CONF_ALIGN]
|
||||
align = literal(align_to[CONF_ALIGN])
|
||||
x = align_to[CONF_X]
|
||||
y = align_to[CONF_Y]
|
||||
lv.obj_align_to(w.obj, target, align, x, y)
|
||||
@ -50,12 +49,11 @@ async def generate_triggers(lv_component):
|
||||
|
||||
async def add_trigger(conf, event, lv_component, w):
|
||||
tid = conf[CONF_TRIGGER_ID]
|
||||
add_line_marks(tid)
|
||||
trigger = cg.new_Pvariable(tid)
|
||||
args = w.get_args()
|
||||
value = w.get_value()
|
||||
await automation.build_automation(trigger, args, conf)
|
||||
with LambdaContext([(lv_event_t_ptr, "event_data")]) as context:
|
||||
add_line_marks(tid)
|
||||
lv_add(trigger.trigger(value))
|
||||
lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), literal(event)))
|
||||
async with LambdaContext(EVENT_ARG, where=tid) as context:
|
||||
with LvConditional(w.is_selected()):
|
||||
lv_add(trigger.trigger(value))
|
||||
lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), event))
|
||||
|
@ -1,8 +1,11 @@
|
||||
from esphome import automation, codegen as cg
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
import sys
|
||||
|
||||
from .defines import CONF_TEXT
|
||||
from esphome import automation, codegen as cg
|
||||
from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_VALUE
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
|
||||
from .defines import CONF_TEXT, lvgl_ns
|
||||
from .lvcode import lv_expr
|
||||
|
||||
|
||||
class LvType(cg.MockObjClass):
|
||||
@ -18,36 +21,48 @@ class LvType(cg.MockObjClass):
|
||||
return self.args[0][0] if len(self.args) else None
|
||||
|
||||
|
||||
class LvNumber(LvType):
|
||||
def __init__(self, *args):
|
||||
super().__init__(
|
||||
*args,
|
||||
largs=[(cg.float_, "x")],
|
||||
lvalue=lambda w: w.get_number_value(),
|
||||
has_on_value=True,
|
||||
)
|
||||
self.value_property = CONF_VALUE
|
||||
|
||||
|
||||
uint16_t_ptr = cg.uint16.operator("ptr")
|
||||
lvgl_ns = cg.esphome_ns.namespace("lvgl")
|
||||
char_ptr = cg.global_ns.namespace("char").operator("ptr")
|
||||
void_ptr = cg.void.operator("ptr")
|
||||
LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent)
|
||||
LvglComponentPtr = LvglComponent.operator("ptr")
|
||||
lv_event_code_t = cg.global_ns.namespace("lv_event_code_t")
|
||||
lv_coord_t = cg.global_ns.namespace("lv_coord_t")
|
||||
lv_event_code_t = cg.global_ns.enum("lv_event_code_t")
|
||||
lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t")
|
||||
FontEngine = lvgl_ns.class_("FontEngine")
|
||||
IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template())
|
||||
ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action)
|
||||
LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition)
|
||||
LvglAction = lvgl_ns.class_("LvglAction", automation.Action)
|
||||
lv_lambda_t = lvgl_ns.class_("LvLambdaType")
|
||||
LvCompound = lvgl_ns.class_("LvCompound")
|
||||
lv_font_t = cg.global_ns.class_("lv_font_t")
|
||||
lv_style_t = cg.global_ns.struct("lv_style_t")
|
||||
# fake parent class for first class widgets and matrix buttons
|
||||
lv_pseudo_button_t = lvgl_ns.class_("LvPseudoButton")
|
||||
lv_obj_base_t = cg.global_ns.class_("lv_obj_t", lv_pseudo_button_t)
|
||||
lv_obj_t_ptr = lv_obj_base_t.operator("ptr")
|
||||
lv_disp_t_ptr = cg.global_ns.struct("lv_disp_t").operator("ptr")
|
||||
lv_disp_t = cg.global_ns.struct("lv_disp_t")
|
||||
lv_color_t = cg.global_ns.struct("lv_color_t")
|
||||
lv_group_t = cg.global_ns.struct("lv_group_t")
|
||||
LVTouchListener = lvgl_ns.class_("LVTouchListener")
|
||||
LVEncoderListener = lvgl_ns.class_("LVEncoderListener")
|
||||
lv_obj_t = LvType("lv_obj_t")
|
||||
lv_page_t = cg.global_ns.class_("LvPageType", LvCompound)
|
||||
lv_img_t = LvType("lv_img_t")
|
||||
|
||||
|
||||
# this will be populated later, in __init__.py to avoid circular imports.
|
||||
WIDGET_TYPES: dict = {}
|
||||
LV_EVENT = MockObj(base="LV_EVENT_", op="")
|
||||
LV_STATE = MockObj(base="LV_STATE_", op="")
|
||||
LV_BTNMATRIX_CTRL = MockObj(base="LV_BTNMATRIX_CTRL_", op="")
|
||||
|
||||
|
||||
class LvText(LvType):
|
||||
@ -55,7 +70,8 @@ class LvText(LvType):
|
||||
super().__init__(
|
||||
*args,
|
||||
largs=[(cg.std_string, "text")],
|
||||
lvalue=lambda w: w.get_property("text")[0],
|
||||
lvalue=lambda w: w.get_property("text"),
|
||||
has_on_value=True,
|
||||
**kwargs,
|
||||
)
|
||||
self.value_property = CONF_TEXT
|
||||
@ -66,13 +82,21 @@ class LvBoolean(LvType):
|
||||
super().__init__(
|
||||
*args,
|
||||
largs=[(cg.bool_, "x")],
|
||||
lvalue=lambda w: w.has_state("LV_STATE_CHECKED"),
|
||||
lvalue=lambda w: w.is_checked(),
|
||||
has_on_value=True,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
CUSTOM_EVENT = ID("lv_custom_event", False, type=lv_event_code_t)
|
||||
class LvSelect(LvType):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(
|
||||
*args,
|
||||
largs=[(cg.int_, "x")],
|
||||
lvalue=lambda w: w.get_property("selected"),
|
||||
has_on_value=True,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class WidgetType:
|
||||
@ -80,7 +104,15 @@ class WidgetType:
|
||||
Describes a type of Widget, e.g. "bar" or "line"
|
||||
"""
|
||||
|
||||
def __init__(self, name, w_type, parts, schema=None, modify_schema=None):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
w_type: LvType,
|
||||
parts: tuple,
|
||||
schema=None,
|
||||
modify_schema=None,
|
||||
lv_name=None,
|
||||
):
|
||||
"""
|
||||
:param name: The widget name, e.g. "bar"
|
||||
:param w_type: The C type of the widget
|
||||
@ -89,6 +121,7 @@ class WidgetType:
|
||||
:param modify_schema: A schema to update the widget
|
||||
"""
|
||||
self.name = name
|
||||
self.lv_name = lv_name or name
|
||||
self.w_type = w_type
|
||||
self.parts = parts
|
||||
if schema is None:
|
||||
@ -98,7 +131,8 @@ class WidgetType:
|
||||
if modify_schema is None:
|
||||
self.modify_schema = self.schema
|
||||
else:
|
||||
self.modify_schema = self.schema
|
||||
self.modify_schema = modify_schema
|
||||
self.mock_obj = MockObj(f"lv_{self.lv_name}", "_")
|
||||
|
||||
@property
|
||||
def animated(self):
|
||||
@ -118,7 +152,7 @@ class WidgetType:
|
||||
:param config: Its configuration
|
||||
:return: Generated code as a list of text lines
|
||||
"""
|
||||
raise NotImplementedError(f"No to_code defined for {self.name}")
|
||||
return []
|
||||
|
||||
def obj_creator(self, parent: MockObjClass, config: dict):
|
||||
"""
|
||||
@ -127,7 +161,7 @@ class WidgetType:
|
||||
:param config: Its configuration
|
||||
:return: Generated code as a single text line
|
||||
"""
|
||||
return f"lv_{self.name}_create({parent})"
|
||||
return lv_expr.call(f"{self.lv_name}_create", parent)
|
||||
|
||||
def get_uses(self):
|
||||
"""
|
||||
@ -135,3 +169,23 @@ class WidgetType:
|
||||
:return:
|
||||
"""
|
||||
return ()
|
||||
|
||||
def get_max(self, config: dict):
|
||||
return sys.maxsize
|
||||
|
||||
def get_min(self, config: dict):
|
||||
return -sys.maxsize
|
||||
|
||||
def get_step(self, config: dict):
|
||||
return 1
|
||||
|
||||
def get_scale(self, config: dict):
|
||||
return 1.0
|
||||
|
||||
|
||||
class NumberType(WidgetType):
|
||||
def get_max(self, config: dict):
|
||||
return int(config[CONF_MAX_VALUE] or 100)
|
||||
|
||||
def get_min(self, config: dict):
|
||||
return int(config[CONF_MIN_VALUE] or 0)
|
||||
|
@ -1,33 +1,63 @@
|
||||
import sys
|
||||
from typing import Any
|
||||
from typing import Any, Union
|
||||
|
||||
from esphome import codegen as cg, config_validation as cv
|
||||
from esphome.config_validation import Invalid
|
||||
from esphome.const import CONF_GROUP, CONF_ID, CONF_STATE
|
||||
from esphome.core import CORE, TimePeriod
|
||||
from esphome.const import CONF_GROUP, CONF_ID, CONF_STATE, CONF_TYPE
|
||||
from esphome.core import ID, TimePeriod
|
||||
from esphome.coroutine import FakeAwaitable
|
||||
from esphome.cpp_generator import MockObj, MockObjClass, VariableDeclarationExpression
|
||||
from esphome.cpp_generator import AssignmentExpression, CallExpression, MockObj
|
||||
|
||||
from .defines import (
|
||||
CONF_DEFAULT,
|
||||
CONF_FLEX_ALIGN_CROSS,
|
||||
CONF_FLEX_ALIGN_MAIN,
|
||||
CONF_FLEX_ALIGN_TRACK,
|
||||
CONF_FLEX_FLOW,
|
||||
CONF_GRID_COLUMN_ALIGN,
|
||||
CONF_GRID_COLUMNS,
|
||||
CONF_GRID_ROW_ALIGN,
|
||||
CONF_GRID_ROWS,
|
||||
CONF_LAYOUT,
|
||||
CONF_MAIN,
|
||||
CONF_SCROLLBAR_MODE,
|
||||
CONF_STYLES,
|
||||
CONF_WIDGETS,
|
||||
OBJ_FLAGS,
|
||||
PARTS,
|
||||
STATES,
|
||||
ConstantLiteral,
|
||||
TYPE_FLEX,
|
||||
TYPE_GRID,
|
||||
LValidator,
|
||||
join_enums,
|
||||
literal,
|
||||
)
|
||||
from .helpers import add_lv_use
|
||||
from .lvcode import add_group, add_line_marks, lv, lv_add, lv_assign, lv_expr, lv_obj
|
||||
from .schemas import ALL_STYLES, STYLE_REMAP
|
||||
from .types import WIDGET_TYPES, LvType, WidgetType, lv_obj_t, lv_obj_t_ptr
|
||||
from .lvcode import (
|
||||
LvConditional,
|
||||
add_line_marks,
|
||||
lv,
|
||||
lv_add,
|
||||
lv_assign,
|
||||
lv_expr,
|
||||
lv_obj,
|
||||
lv_Pvariable,
|
||||
)
|
||||
from .schemas import ALL_STYLES, STYLE_REMAP, WIDGET_TYPES
|
||||
from .types import (
|
||||
LV_STATE,
|
||||
LvType,
|
||||
WidgetType,
|
||||
lv_coord_t,
|
||||
lv_group_t,
|
||||
lv_obj_t,
|
||||
lv_obj_t_ptr,
|
||||
)
|
||||
|
||||
EVENT_LAMB = "event_lamb__"
|
||||
|
||||
theme_widget_map = {}
|
||||
|
||||
|
||||
class LvScrActType(WidgetType):
|
||||
"""
|
||||
@ -37,9 +67,6 @@ class LvScrActType(WidgetType):
|
||||
def __init__(self):
|
||||
super().__init__("lv_scr_act()", lv_obj_t, ())
|
||||
|
||||
def obj_creator(self, parent: MockObjClass, config: dict):
|
||||
return []
|
||||
|
||||
async def to_code(self, w, config: dict):
|
||||
return []
|
||||
|
||||
@ -55,7 +82,7 @@ class Widget:
|
||||
def set_completed():
|
||||
Widget.widgets_completed = True
|
||||
|
||||
def __init__(self, var, wtype: WidgetType, config: dict = None, parent=None):
|
||||
def __init__(self, var, wtype: WidgetType, config: dict = None):
|
||||
self.var = var
|
||||
self.type = wtype
|
||||
self.config = config
|
||||
@ -63,21 +90,18 @@ class Widget:
|
||||
self.step = 1.0
|
||||
self.range_from = -sys.maxsize
|
||||
self.range_to = sys.maxsize
|
||||
self.parent = parent
|
||||
if wtype.is_compound():
|
||||
self.obj = MockObj(f"{self.var}->obj")
|
||||
else:
|
||||
self.obj = var
|
||||
|
||||
@staticmethod
|
||||
def create(name, var, wtype: WidgetType, config: dict = None, parent=None):
|
||||
w = Widget(var, wtype, config, parent)
|
||||
def create(name, var, wtype: WidgetType, config: dict = None):
|
||||
w = Widget(var, wtype, config)
|
||||
if name is not None:
|
||||
widget_map[name] = w
|
||||
return w
|
||||
|
||||
@property
|
||||
def obj(self):
|
||||
if self.type.is_compound():
|
||||
return f"{self.var}->obj"
|
||||
return self.var
|
||||
|
||||
def add_state(self, state):
|
||||
return lv_obj.add_state(self.obj, literal(state))
|
||||
|
||||
@ -85,7 +109,13 @@ class Widget:
|
||||
return lv_obj.clear_state(self.obj, literal(state))
|
||||
|
||||
def has_state(self, state):
|
||||
return lv_expr.obj_get_state(self.obj) & literal(state) != 0
|
||||
return (lv_expr.obj_get_state(self.obj) & literal(state)) != 0
|
||||
|
||||
def is_pressed(self):
|
||||
return self.has_state(LV_STATE.PRESSED)
|
||||
|
||||
def is_checked(self):
|
||||
return self.has_state(LV_STATE.CHECKED)
|
||||
|
||||
def add_flag(self, flag):
|
||||
return lv_obj.add_flag(self.obj, literal(flag))
|
||||
@ -93,32 +123,37 @@ class Widget:
|
||||
def clear_flag(self, flag):
|
||||
return lv_obj.clear_flag(self.obj, literal(flag))
|
||||
|
||||
def set_property(self, prop, value, animated: bool = None, ltype=None):
|
||||
async def set_property(self, prop, value, animated: bool = None):
|
||||
if isinstance(value, dict):
|
||||
value = value.get(prop)
|
||||
if isinstance(ALL_STYLES.get(prop), LValidator):
|
||||
value = await ALL_STYLES[prop].process(value)
|
||||
else:
|
||||
value = literal(value)
|
||||
if value is None:
|
||||
return
|
||||
if isinstance(value, TimePeriod):
|
||||
value = value.total_milliseconds
|
||||
ltype = ltype or self.__type_base()
|
||||
if isinstance(value, str):
|
||||
value = literal(value)
|
||||
if animated is None or self.type.animated is not True:
|
||||
lv.call(f"{ltype}_set_{prop}", self.obj, value)
|
||||
lv.call(f"{self.type.lv_name}_set_{prop}", self.obj, value)
|
||||
else:
|
||||
lv.call(
|
||||
f"{ltype}_set_{prop}",
|
||||
f"{self.type.lv_name}_set_{prop}",
|
||||
self.obj,
|
||||
value,
|
||||
"LV_ANIM_ON" if animated else "LV_ANIM_OFF",
|
||||
literal("LV_ANIM_ON" if animated else "LV_ANIM_OFF"),
|
||||
)
|
||||
|
||||
def get_property(self, prop, ltype=None):
|
||||
ltype = ltype or self.__type_base()
|
||||
return f"lv_{ltype}_get_{prop}({self.obj})"
|
||||
return cg.RawExpression(f"lv_{ltype}_get_{prop}({self.obj})")
|
||||
|
||||
def set_style(self, prop, value, state):
|
||||
if value is None:
|
||||
return []
|
||||
return lv.call(f"obj_set_style_{prop}", self.obj, value, state)
|
||||
return
|
||||
lv.call(f"obj_set_style_{prop}", self.obj, value, state)
|
||||
|
||||
def __type_base(self):
|
||||
wtype = self.type.w_type
|
||||
@ -140,6 +175,32 @@ class Widget:
|
||||
return self.type.w_type.value(self)
|
||||
return self.obj
|
||||
|
||||
def get_number_value(self):
|
||||
value = self.type.mock_obj.get_value(self.obj)
|
||||
if self.scale == 1.0:
|
||||
return value
|
||||
return value / float(self.scale)
|
||||
|
||||
def is_selected(self):
|
||||
"""
|
||||
Overridable property to determine if the widget is selected. Will be None except
|
||||
for matrix buttons
|
||||
:return:
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_max(self):
|
||||
return self.type.get_max(self.config)
|
||||
|
||||
def get_min(self):
|
||||
return self.type.get_min(self.config)
|
||||
|
||||
def get_step(self):
|
||||
return self.type.get_step(self.config)
|
||||
|
||||
def get_scale(self):
|
||||
return self.type.get_scale(self.config)
|
||||
|
||||
|
||||
# Map of widgets to their config, used for trigger generation
|
||||
widget_map: dict[Any, Widget] = {}
|
||||
@ -161,13 +222,20 @@ def get_widget_generator(wid):
|
||||
yield
|
||||
|
||||
|
||||
async def get_widget(config: dict, id: str = CONF_ID) -> Widget:
|
||||
wid = config[id]
|
||||
async def get_widget_(wid: Widget):
|
||||
if obj := widget_map.get(wid):
|
||||
return obj
|
||||
return await FakeAwaitable(get_widget_generator(wid))
|
||||
|
||||
|
||||
async def get_widgets(config: Union[dict, list], id: str = CONF_ID) -> list[Widget]:
|
||||
if not config:
|
||||
return []
|
||||
if not isinstance(config, list):
|
||||
config = [config]
|
||||
return [await get_widget_(c[id]) for c in config if id in c]
|
||||
|
||||
|
||||
def collect_props(config):
|
||||
"""
|
||||
Collect all properties from a configuration
|
||||
@ -175,7 +243,7 @@ def collect_props(config):
|
||||
:return:
|
||||
"""
|
||||
props = {}
|
||||
for prop in [*ALL_STYLES, *OBJ_FLAGS, CONF_GROUP]:
|
||||
for prop in [*ALL_STYLES, *OBJ_FLAGS, CONF_STYLES, CONF_GROUP]:
|
||||
if prop in config:
|
||||
props[prop] = config[prop]
|
||||
return props
|
||||
@ -209,12 +277,39 @@ def collect_parts(config):
|
||||
|
||||
async def set_obj_properties(w: Widget, config):
|
||||
"""Generate a list of C++ statements to apply properties to an lv_obj_t"""
|
||||
if layout := config.get(CONF_LAYOUT):
|
||||
layout_type: str = layout[CONF_TYPE]
|
||||
lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}"))
|
||||
if layout_type == TYPE_GRID:
|
||||
wid = config[CONF_ID]
|
||||
rows = "{" + ",".join(layout[CONF_GRID_ROWS]) + ", LV_GRID_TEMPLATE_LAST}"
|
||||
row_id = ID(f"{wid}_row_dsc", is_declaration=True, type=lv_coord_t)
|
||||
row_array = cg.static_const_array(row_id, cg.RawExpression(rows))
|
||||
w.set_style("grid_row_dsc_array", row_array, 0)
|
||||
columns = (
|
||||
"{" + ",".join(layout[CONF_GRID_COLUMNS]) + ", LV_GRID_TEMPLATE_LAST}"
|
||||
)
|
||||
column_id = ID(f"{wid}_column_dsc", is_declaration=True, type=lv_coord_t)
|
||||
column_array = cg.static_const_array(column_id, cg.RawExpression(columns))
|
||||
w.set_style("grid_column_dsc_array", column_array, 0)
|
||||
w.set_style(
|
||||
CONF_GRID_COLUMN_ALIGN, literal(layout.get(CONF_GRID_COLUMN_ALIGN)), 0
|
||||
)
|
||||
w.set_style(
|
||||
CONF_GRID_ROW_ALIGN, literal(layout.get(CONF_GRID_ROW_ALIGN)), 0
|
||||
)
|
||||
if layout_type == TYPE_FLEX:
|
||||
lv_obj.set_flex_flow(w.obj, literal(layout[CONF_FLEX_FLOW]))
|
||||
main = literal(layout[CONF_FLEX_ALIGN_MAIN])
|
||||
cross = literal(layout[CONF_FLEX_ALIGN_CROSS])
|
||||
track = literal(layout[CONF_FLEX_ALIGN_TRACK])
|
||||
lv_obj.set_flex_align(w.obj, main, cross, track)
|
||||
parts = collect_parts(config)
|
||||
for part, states in parts.items():
|
||||
for state, props in states.items():
|
||||
lv_state = ConstantLiteral(
|
||||
f"(int)LV_STATE_{state.upper()}|(int)LV_PART_{part.upper()}"
|
||||
)
|
||||
lv_state = join_enums((f"LV_STATE_{state}", f"LV_PART_{part}"))
|
||||
for style_id in props.get(CONF_STYLES, ()):
|
||||
lv_obj.add_style(w.obj, MockObj(style_id), lv_state)
|
||||
for prop, value in {
|
||||
k: v for k, v in props.items() if k in ALL_STYLES
|
||||
}.items():
|
||||
@ -258,14 +353,12 @@ async def set_obj_properties(w: Widget, config):
|
||||
w.clear_state(clears)
|
||||
for key, value in lambs.items():
|
||||
lamb = await cg.process_lambda(value, [], return_type=cg.bool_)
|
||||
state = f"LV_STATE_{key.upper}"
|
||||
lv.cond_if(lamb)
|
||||
w.add_state(state)
|
||||
lv.cond_else()
|
||||
w.clear_state(state)
|
||||
lv.cond_endif()
|
||||
if scrollbar_mode := config.get(CONF_SCROLLBAR_MODE):
|
||||
lv_obj.set_scrollbar_mode(w.obj, scrollbar_mode)
|
||||
state = f"LV_STATE_{key.upper()}"
|
||||
with LvConditional(f"{lamb}()") as cond:
|
||||
w.add_state(state)
|
||||
cond.else_()
|
||||
w.clear_state(state)
|
||||
await w.set_property(CONF_SCROLLBAR_MODE, config)
|
||||
|
||||
|
||||
async def add_widgets(parent: Widget, config: dict):
|
||||
@ -280,7 +373,7 @@ async def add_widgets(parent: Widget, config: dict):
|
||||
await widget_to_code(w_cnfig, w_type, parent.obj)
|
||||
|
||||
|
||||
async def widget_to_code(w_cnfig, w_type, parent):
|
||||
async def widget_to_code(w_cnfig, w_type: WidgetType, parent):
|
||||
"""
|
||||
Converts a Widget definition to C code.
|
||||
:param w_cnfig: The widget configuration
|
||||
@ -298,19 +391,33 @@ async def widget_to_code(w_cnfig, w_type, parent):
|
||||
var = cg.new_Pvariable(wid)
|
||||
lv_add(var.set_obj(creator))
|
||||
else:
|
||||
var = MockObj(wid, "->")
|
||||
decl = VariableDeclarationExpression(lv_obj_t, "*", wid)
|
||||
CORE.add_global(decl)
|
||||
CORE.register_variable(wid, var)
|
||||
var = lv_Pvariable(lv_obj_t, wid)
|
||||
lv_assign(var, creator)
|
||||
|
||||
widget = Widget.create(wid, var, spec, w_cnfig, parent)
|
||||
await set_obj_properties(widget, w_cnfig)
|
||||
await add_widgets(widget, w_cnfig)
|
||||
await spec.to_code(widget, w_cnfig)
|
||||
w = Widget.create(wid, var, spec, w_cnfig)
|
||||
if theme := theme_widget_map.get(w_type):
|
||||
lv_add(CallExpression(theme, w.obj))
|
||||
await set_obj_properties(w, w_cnfig)
|
||||
await add_widgets(w, w_cnfig)
|
||||
await spec.to_code(w, w_cnfig)
|
||||
|
||||
|
||||
lv_scr_act_spec = LvScrActType()
|
||||
lv_scr_act = Widget.create(
|
||||
None, ConstantLiteral("lv_scr_act()"), lv_scr_act_spec, {}, parent=None
|
||||
)
|
||||
lv_scr_act = Widget.create(None, literal("lv_scr_act()"), lv_scr_act_spec, {})
|
||||
|
||||
lv_groups = {} # Widget group names
|
||||
|
||||
|
||||
def add_group(name):
|
||||
if name is None:
|
||||
return None
|
||||
fullname = f"lv_esp_group_{name}"
|
||||
if name not in lv_groups:
|
||||
gid = ID(fullname, True, type=lv_group_t.operator("ptr"))
|
||||
lv_add(
|
||||
AssignmentExpression(
|
||||
type_=gid.type, modifier="", name=fullname, rhs=lv_expr.group_create()
|
||||
)
|
||||
)
|
||||
lv_groups[name] = literal(fullname)
|
||||
return lv_groups[name]
|
||||
|
@ -5,142 +5,229 @@ lvgl:
|
||||
- touchscreen_id: tft_touch
|
||||
long_press_repeat_time: 200ms
|
||||
long_press_time: 500ms
|
||||
widgets:
|
||||
- label:
|
||||
id: hello_label
|
||||
text: Hello world
|
||||
text_color: 0xFF8000
|
||||
align: center
|
||||
text_font: montserrat_40
|
||||
border_post: true
|
||||
|
||||
- label:
|
||||
text: "Hello shiny day"
|
||||
text_color: 0xFFFFFF
|
||||
align: bottom_mid
|
||||
text_font: space16
|
||||
- obj:
|
||||
align: center
|
||||
arc_opa: COVER
|
||||
arc_color: 0xFF0000
|
||||
arc_rounded: false
|
||||
arc_width: 3
|
||||
anim_time: 1s
|
||||
bg_color: light_blue
|
||||
bg_grad_color: light_blue
|
||||
bg_dither_mode: ordered
|
||||
bg_grad_dir: hor
|
||||
bg_grad_stop: 128
|
||||
bg_image_opa: transp
|
||||
bg_image_recolor: light_blue
|
||||
bg_image_recolor_opa: 50%
|
||||
bg_main_stop: 0
|
||||
bg_opa: 20%
|
||||
border_color: 0x00FF00
|
||||
border_opa: cover
|
||||
border_post: true
|
||||
border_side: [bottom, left]
|
||||
border_width: 4
|
||||
clip_corner: false
|
||||
height: 50%
|
||||
image_recolor: light_blue
|
||||
image_recolor_opa: cover
|
||||
line_width: 10
|
||||
line_dash_width: 10
|
||||
line_dash_gap: 10
|
||||
line_rounded: false
|
||||
line_color: light_blue
|
||||
opa: cover
|
||||
opa_layered: cover
|
||||
outline_color: light_blue
|
||||
outline_opa: cover
|
||||
outline_pad: 10px
|
||||
outline_width: 10px
|
||||
pad_all: 10px
|
||||
pad_bottom: 10px
|
||||
pad_column: 10px
|
||||
pad_left: 10px
|
||||
pad_right: 10px
|
||||
pad_row: 10px
|
||||
pad_top: 10px
|
||||
shadow_color: light_blue
|
||||
shadow_ofs_x: 5
|
||||
shadow_ofs_y: 5
|
||||
shadow_opa: cover
|
||||
shadow_spread: 5
|
||||
shadow_width: 10
|
||||
text_align: auto
|
||||
text_color: light_blue
|
||||
text_decor: [underline, strikethrough]
|
||||
text_font: montserrat_18
|
||||
text_letter_space: 4
|
||||
text_line_space: 4
|
||||
text_opa: cover
|
||||
transform_angle: 180
|
||||
transform_height: 100
|
||||
transform_pivot_x: 50%
|
||||
transform_pivot_y: 50%
|
||||
transform_zoom: 0.5
|
||||
translate_x: 10
|
||||
translate_y: 10
|
||||
max_height: 100
|
||||
max_width: 200
|
||||
min_height: 20%
|
||||
min_width: 20%
|
||||
radius: circle
|
||||
width: 10px
|
||||
x: 100
|
||||
y: 120
|
||||
- button:
|
||||
width: 20%
|
||||
height: 10%
|
||||
pressed:
|
||||
bg_color: light_blue
|
||||
checkable: true
|
||||
checked:
|
||||
bg_color: 0x000000
|
||||
widgets:
|
||||
- label:
|
||||
text: Button
|
||||
on_click:
|
||||
lvgl.label.update:
|
||||
pages:
|
||||
- id: page1
|
||||
skip: true
|
||||
widgets:
|
||||
- label:
|
||||
id: hello_label
|
||||
bg_color: 0x123456
|
||||
text: clicked
|
||||
on_value:
|
||||
logger.log:
|
||||
format: "state now %d"
|
||||
args: [x]
|
||||
on_short_click:
|
||||
lvgl.widget.hide: hello_label
|
||||
on_long_press:
|
||||
lvgl.widget.show: hello_label
|
||||
on_cancel:
|
||||
lvgl.widget.enable: hello_label
|
||||
on_ready:
|
||||
lvgl.widget.disable: hello_label
|
||||
on_defocus:
|
||||
lvgl.widget.hide: hello_label
|
||||
on_focus:
|
||||
logger.log: Button clicked
|
||||
on_scroll:
|
||||
logger.log: Button clicked
|
||||
on_scroll_end:
|
||||
logger.log: Button clicked
|
||||
on_scroll_begin:
|
||||
logger.log: Button clicked
|
||||
on_release:
|
||||
logger.log: Button clicked
|
||||
on_long_press_repeat:
|
||||
logger.log: Button clicked
|
||||
text: Hello world
|
||||
text_color: 0xFF8000
|
||||
align: center
|
||||
text_font: montserrat_40
|
||||
border_post: true
|
||||
|
||||
- label:
|
||||
text: "Hello shiny day"
|
||||
text_color: 0xFFFFFF
|
||||
align: bottom_mid
|
||||
text_font: space16
|
||||
- obj:
|
||||
align: center
|
||||
arc_opa: COVER
|
||||
arc_color: 0xFF0000
|
||||
arc_rounded: false
|
||||
arc_width: 3
|
||||
anim_time: 1s
|
||||
bg_color: light_blue
|
||||
bg_grad_color: light_blue
|
||||
bg_dither_mode: ordered
|
||||
bg_grad_dir: hor
|
||||
bg_grad_stop: 128
|
||||
bg_image_opa: transp
|
||||
bg_image_recolor: light_blue
|
||||
bg_image_recolor_opa: 50%
|
||||
bg_main_stop: 0
|
||||
bg_opa: 20%
|
||||
border_color: 0x00FF00
|
||||
border_opa: cover
|
||||
border_post: true
|
||||
border_side: [bottom, left]
|
||||
border_width: 4
|
||||
clip_corner: false
|
||||
height: 50%
|
||||
image_recolor: light_blue
|
||||
image_recolor_opa: cover
|
||||
line_width: 10
|
||||
line_dash_width: 10
|
||||
line_dash_gap: 10
|
||||
line_rounded: false
|
||||
line_color: light_blue
|
||||
opa: cover
|
||||
opa_layered: cover
|
||||
outline_color: light_blue
|
||||
outline_opa: cover
|
||||
outline_pad: 10px
|
||||
outline_width: 10px
|
||||
pad_all: 10px
|
||||
pad_bottom: 10px
|
||||
pad_column: 10px
|
||||
pad_left: 10px
|
||||
pad_right: 10px
|
||||
pad_row: 10px
|
||||
pad_top: 10px
|
||||
shadow_color: light_blue
|
||||
shadow_ofs_x: 5
|
||||
shadow_ofs_y: 5
|
||||
shadow_opa: cover
|
||||
shadow_spread: 5
|
||||
shadow_width: 10
|
||||
text_align: auto
|
||||
text_color: light_blue
|
||||
text_decor: [underline, strikethrough]
|
||||
text_font: montserrat_18
|
||||
text_letter_space: 4
|
||||
text_line_space: 4
|
||||
text_opa: cover
|
||||
transform_angle: 180
|
||||
transform_height: 100
|
||||
transform_pivot_x: 50%
|
||||
transform_pivot_y: 50%
|
||||
transform_zoom: 0.5
|
||||
translate_x: 10
|
||||
translate_y: 10
|
||||
max_height: 100
|
||||
max_width: 200
|
||||
min_height: 20%
|
||||
min_width: 20%
|
||||
radius: circle
|
||||
width: 10px
|
||||
x: 100
|
||||
y: 120
|
||||
- button:
|
||||
width: 20%
|
||||
height: 10%
|
||||
pressed:
|
||||
bg_color: light_blue
|
||||
checkable: true
|
||||
checked:
|
||||
bg_color: 0x000000
|
||||
widgets:
|
||||
- label:
|
||||
text: Button
|
||||
on_click:
|
||||
lvgl.label.update:
|
||||
id: hello_label
|
||||
bg_color: 0x123456
|
||||
text: clicked
|
||||
on_value:
|
||||
logger.log:
|
||||
format: "state now %d"
|
||||
args: [x]
|
||||
on_short_click:
|
||||
lvgl.widget.hide: hello_label
|
||||
on_long_press:
|
||||
lvgl.widget.show: hello_label
|
||||
on_cancel:
|
||||
lvgl.widget.enable: hello_label
|
||||
on_ready:
|
||||
lvgl.widget.disable: hello_label
|
||||
on_defocus:
|
||||
lvgl.widget.hide: hello_label
|
||||
on_focus:
|
||||
logger.log: Button clicked
|
||||
on_scroll:
|
||||
logger.log: Button clicked
|
||||
on_scroll_end:
|
||||
logger.log: Button clicked
|
||||
on_scroll_begin:
|
||||
logger.log: Button clicked
|
||||
on_release:
|
||||
logger.log: Button clicked
|
||||
on_long_press_repeat:
|
||||
logger.log: Button clicked
|
||||
- led:
|
||||
color: 0x00FF00
|
||||
brightness: 50%
|
||||
align: right_mid
|
||||
- spinner:
|
||||
arc_length: 120
|
||||
spin_time: 2s
|
||||
align: left_mid
|
||||
- image:
|
||||
src: cat_image
|
||||
align: top_left
|
||||
y: 50
|
||||
|
||||
- id: page2
|
||||
widgets:
|
||||
- arc:
|
||||
align: left_mid
|
||||
id: lv_arc
|
||||
adjustable: true
|
||||
on_value:
|
||||
then:
|
||||
- logger.log:
|
||||
format: "Arc value is %f"
|
||||
args: [x]
|
||||
group: general
|
||||
scroll_on_focus: true
|
||||
value: 75
|
||||
min_value: 1
|
||||
max_value: 100
|
||||
arc_color: 0xFF0000
|
||||
indicator:
|
||||
arc_color: 0xF000FF
|
||||
pressed:
|
||||
arc_color: 0xFFFF00
|
||||
focused:
|
||||
arc_color: 0x808080
|
||||
- bar:
|
||||
id: bar_id
|
||||
align: top_mid
|
||||
y: 20
|
||||
value: 30
|
||||
max_value: 100
|
||||
min_value: 10
|
||||
mode: range
|
||||
on_click:
|
||||
then:
|
||||
- lvgl.bar.update:
|
||||
id: bar_id
|
||||
value: !lambda return (int)((float)rand() / RAND_MAX * 100);
|
||||
- logger.log:
|
||||
format: "bar value %f"
|
||||
args: [x]
|
||||
- line:
|
||||
align: center
|
||||
points:
|
||||
- 5, 5
|
||||
- 70, 70
|
||||
- 120, 10
|
||||
- 180, 60
|
||||
- 240, 10
|
||||
on_click:
|
||||
lvgl.page.next:
|
||||
- switch:
|
||||
align: right_mid
|
||||
- checkbox:
|
||||
text: Checkbox
|
||||
align: bottom_right
|
||||
- slider:
|
||||
id: slider_id
|
||||
align: top_mid
|
||||
y: 40
|
||||
value: 30
|
||||
max_value: 100
|
||||
min_value: 10
|
||||
mode: normal
|
||||
on_value:
|
||||
then:
|
||||
- logger.log:
|
||||
format: "slider value %f"
|
||||
args: [x]
|
||||
on_click:
|
||||
then:
|
||||
- lvgl.slider.update:
|
||||
id: slider_id
|
||||
value: !lambda return (int)((float)rand() / RAND_MAX * 100);
|
||||
font:
|
||||
- file: "gfonts://Roboto"
|
||||
id: space16
|
||||
bpp: 4
|
||||
|
||||
image:
|
||||
- id: cat_img
|
||||
- id: cat_image
|
||||
resize: 256x48
|
||||
file: $component_dir/logo-text.svg
|
||||
- id: dog_img
|
||||
|
Loading…
Reference in New Issue
Block a user