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