Skip to content

Commit

Permalink
[lvgl] Stage 4 (esphome#7166)
Browse files Browse the repository at this point in the history
  • Loading branch information
clydebarrow authored Aug 5, 2024
1 parent 87944f0 commit d18bb34
Show file tree
Hide file tree
Showing 28 changed files with 2,002 additions and 579 deletions.
110 changes: 88 additions & 22 deletions esphome/components/lvgl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,44 +15,91 @@
CONF_TRIGGER_ID,
CONF_TYPE,
)
from esphome.core import CORE, ID, Lambda
from esphome.core import CORE, ID
from esphome.cpp_generator import MockObj
from esphome.final_validate import full_config
from esphome.helpers import write_file_if_changed

from . import defines as df, helpers, lv_validation as lvalid
from .automation import update_to_code
from .animimg import animimg_spec
from .arc import arc_spec
from .automation import disp_update, update_to_code
from .btn import btn_spec
from .checkbox import checkbox_spec
from .defines import CONF_SKIP
from .img import img_spec
from .label import label_spec
from .lv_validation import lv_images_used
from .lvcode import LvContext
from .led import led_spec
from .line import line_spec
from .lv_bar import bar_spec
from .lv_switch import switch_spec
from .lv_validation import lv_bool, lv_images_used
from .lvcode import LvContext, LvglComponent
from .obj import obj_spec
from .page import add_pages, page_spec
from .rotary_encoders import ROTARY_ENCODER_CONFIG, rotary_encoders_to_code
from .schemas import any_widget_schema, create_modify_schema, obj_schema
from .schemas import (
DISP_BG_SCHEMA,
FLEX_OBJ_SCHEMA,
GRID_CELL_SCHEMA,
LAYOUT_SCHEMAS,
STYLE_SCHEMA,
WIDGET_TYPES,
any_widget_schema,
container_schema,
create_modify_schema,
grid_alignments,
obj_schema,
)
from .slider import slider_spec
from .spinner import spinner_spec
from .styles import add_top_layer, styles_to_code, theme_to_code
from .touchscreens import touchscreen_schema, touchscreens_to_code
from .trigger import generate_triggers
from .types import (
WIDGET_TYPES,
FontEngine,
IdleTrigger,
LvglComponent,
ObjUpdateAction,
lv_font_t,
lv_style_t,
lvgl_ns,
)
from .widget import Widget, add_widgets, lv_scr_act, set_obj_properties

DOMAIN = "lvgl"
DEPENDENCIES = ("display",)
AUTO_LOAD = ("key_provider",)
CODEOWNERS = ("@clydebarrow",)
DEPENDENCIES = ["display"]
AUTO_LOAD = ["key_provider"]
CODEOWNERS = ["@clydebarrow"]
LOGGER = logging.getLogger(__name__)

for w_type in (label_spec, obj_spec, btn_spec):
for w_type in (
label_spec,
obj_spec,
btn_spec,
bar_spec,
slider_spec,
arc_spec,
line_spec,
spinner_spec,
led_spec,
animimg_spec,
checkbox_spec,
img_spec,
switch_spec,
):
WIDGET_TYPES[w_type.name] = w_type

WIDGET_SCHEMA = any_widget_schema()

LAYOUT_SCHEMAS[df.TYPE_GRID] = {
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(GRID_CELL_SCHEMA))
}
LAYOUT_SCHEMAS[df.TYPE_FLEX] = {
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(FLEX_OBJ_SCHEMA))
}
LAYOUT_SCHEMAS[df.TYPE_NONE] = {
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema())
}
for w_type in WIDGET_TYPES.values():
register_action(
f"lvgl.{w_type.name}.update",
Expand All @@ -61,14 +108,6 @@
)(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


Expand Down Expand Up @@ -100,6 +139,9 @@ def generate_lv_conf_h():


def final_validation(config):
if pages := config.get(CONF_PAGES):
if all(p[CONF_SKIP] for p in pages):
raise cv.Invalid("At least one page must not be skipped")
global_config = full_config.get()
for display_id in config[df.CONF_DISPLAYS]:
path = global_config.get_path_for_id(display_id)[:-1]
Expand Down Expand Up @@ -193,18 +235,23 @@ async def to_code(config):
else:
add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font))

with LvContext():
async with LvContext(lv_component):
await touchscreens_to_code(lv_component, config)
await rotary_encoders_to_code(lv_component, config)
await theme_to_code(config)
await styles_to_code(config)
await set_obj_properties(lv_scr_act, config)
await add_widgets(lv_scr_act, config)
await add_pages(lv_component, config)
await add_top_layer(config)
await disp_update(f"{lv_component}->get_disp()", config)
Widget.set_completed()
await generate_triggers(lv_component)
for conf in config.get(CONF_ON_IDLE, ()):
templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32)
idle_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, templ)
await build_automation(idle_trigger, [], conf)
await add_init_lambda(lv_component, LvContext.get_code())

for comp in helpers.lvgl_components_required:
CORE.add_define(f"USE_LVGL_{comp.upper()}")
for use in helpers.lv_uses:
Expand Down Expand Up @@ -239,6 +286,16 @@ def display_schema(config):
cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of(
"big_endian", "little_endian"
),
cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list(
cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)})
.extend(STYLE_SCHEMA)
.extend(
{
cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments,
cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments,
}
)
),
cv.Optional(CONF_ON_IDLE): validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger),
Expand All @@ -247,10 +304,19 @@ def display_schema(config):
),
}
),
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(WIDGET_SCHEMA),
cv.Exclusive(df.CONF_WIDGETS, CONF_PAGES): cv.ensure_list(WIDGET_SCHEMA),
cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list(
container_schema(page_spec)
),
cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool,
cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec),
cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color,
cv.Optional(df.CONF_THEME): cv.Schema(
{cv.Optional(name): obj_schema(w) for name, w in WIDGET_TYPES.items()}
),
cv.GenerateID(df.CONF_TOUCHSCREENS): touchscreen_schema,
cv.GenerateID(df.CONF_ROTARY_ENCODERS): ROTARY_ENCODER_CONFIG,
}
)
.extend(DISP_BG_SCHEMA)
).add_extra(cv.has_at_least_one_key(CONF_PAGES, df.CONF_WIDGETS))
117 changes: 117 additions & 0 deletions esphome/components/lvgl/animimg.py
Original file line number Diff line number Diff line change
@@ -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 changes: 78 additions & 0 deletions esphome/components/lvgl/arc.py
Original file line number Diff line number Diff line change
@@ -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()
Loading

0 comments on commit d18bb34

Please sign in to comment.