[lvgl] base implementation (#7116)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
Clyde Stubbs 2024-07-25 09:12:04 +10:00 committed by GitHub
parent 75635956cd
commit 23ffc3ddfb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 2359 additions and 0 deletions

View file

@ -217,6 +217,7 @@ esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core
esphome/components/ltr390/* @latonita @sjtrny
esphome/components/ltr_als_ps/* @latonita
esphome/components/lvgl/* @clydebarrow
esphome/components/m5stack_8angle/* @rnauber
esphome/components/matrix_keypad/* @ssieb
esphome/components/max31865/* @DAVe3283

View file

@ -0,0 +1,212 @@
import logging
import esphome.codegen as cg
from esphome.components.display import Display
import esphome.config_validation as cv
from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_BUFFER_SIZE,
CONF_ID,
CONF_LAMBDA,
CONF_PAGES,
)
from esphome.core import CORE, ID, Lambda
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 .label import label_spec
from .lvcode import ConstantLiteral, LvContext
# from .menu import menu_spec
from .obj import obj_spec
from .schemas import WIDGET_TYPES, any_widget_schema, obj_schema
from .types import FontEngine, LvglComponent, lv_disp_t_ptr, lv_font_t, lvgl_ns
from .widget import LvScrActType, Widget, add_widgets, set_obj_properties
DOMAIN = "lvgl"
DEPENDENCIES = ("display",)
AUTO_LOAD = ("key_provider",)
CODEOWNERS = ("@clydebarrow",)
LOGGER = logging.getLogger(__name__)
for widg in (
label_spec,
obj_spec,
):
WIDGET_TYPES[widg.name] = widg
lv_scr_act_spec = LvScrActType()
lv_scr_act = Widget.create(
None, ConstantLiteral("lv_scr_act()"), lv_scr_act_spec, {}, parent=None
)
WIDGET_SCHEMA = any_widget_schema()
async def add_init_lambda(lv_component, init):
if init:
lamb = await cg.process_lambda(Lambda(init), [(lv_disp_t_ptr, "lv_disp")])
cg.add(lv_component.add_init_lambda(lamb))
lv_defines = {} # Dict of #defines to provide as build flags
def add_define(macro, value="1"):
if macro in lv_defines and lv_defines[macro] != value:
LOGGER.error(
"Redefinition of %s - was %s now %s", macro, lv_defines[macro], value
)
lv_defines[macro] = value
def as_macro(macro, value):
if value is None:
return f"#define {macro}"
return f"#define {macro} {value}"
LV_CONF_FILENAME = "lv_conf.h"
LV_CONF_H_FORMAT = """\
#pragma once
{}
"""
def generate_lv_conf_h():
definitions = [as_macro(m, v) for m, v in lv_defines.items()]
definitions.sort()
return LV_CONF_H_FORMAT.format("\n".join(definitions))
def final_validation(config):
global_config = full_config.get()
for display_id in config[df.CONF_DISPLAYS]:
path = global_config.get_path_for_id(display_id)[:-1]
display = global_config.get_config_for_path(path)
if CONF_LAMBDA in display:
raise cv.Invalid("Using lambda: in display config not compatible with LVGL")
if display[CONF_AUTO_CLEAR_ENABLED]:
raise cv.Invalid(
"Using auto_clear_enabled: true in display config not compatible with LVGL"
)
buffer_frac = config[CONF_BUFFER_SIZE]
if not CORE.is_host and buffer_frac > 0.5 and "psram" not in global_config:
LOGGER.warning("buffer_size: may need to be reduced without PSRAM")
async def to_code(config):
cg.add_library("lvgl/lvgl", "8.4.0")
CORE.add_define("USE_LVGL")
# suppress default enabling of extra widgets
add_define("_LV_KCONFIG_PRESENT")
# Always enable - lots of things use it.
add_define("LV_DRAW_COMPLEX", "1")
add_define("LV_TICK_CUSTOM", "1")
add_define("LV_TICK_CUSTOM_INCLUDE", '"esphome/components/lvgl/lvgl_hal.h"')
add_define("LV_TICK_CUSTOM_SYS_TIME_EXPR", "(lv_millis())")
add_define("LV_MEM_CUSTOM", "1")
add_define("LV_MEM_CUSTOM_ALLOC", "lv_custom_mem_alloc")
add_define("LV_MEM_CUSTOM_FREE", "lv_custom_mem_free")
add_define("LV_MEM_CUSTOM_REALLOC", "lv_custom_mem_realloc")
add_define("LV_MEM_CUSTOM_INCLUDE", '"esphome/components/lvgl/lvgl_hal.h"')
add_define("LV_LOG_LEVEL", f"LV_LOG_LEVEL_{config[df.CONF_LOG_LEVEL]}")
add_define("LV_COLOR_DEPTH", config[df.CONF_COLOR_DEPTH])
for font in helpers.lv_fonts_used:
add_define(f"LV_FONT_{font.upper()}")
if config[df.CONF_COLOR_DEPTH] == 16:
add_define(
"LV_COLOR_16_SWAP",
"1" if config[df.CONF_BYTE_ORDER] == "big_endian" else "0",
)
add_define(
"LV_COLOR_CHROMA_KEY",
await lvalid.lv_color.process(config[df.CONF_TRANSPARENCY_KEY]),
)
CORE.add_build_flag("-Isrc")
cg.add_global(lvgl_ns.using)
lv_component = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(lv_component, config)
Widget.create(config[CONF_ID], lv_component, WIDGET_TYPES[df.CONF_OBJ], config)
for display in config[df.CONF_DISPLAYS]:
cg.add(lv_component.add_display(await cg.get_variable(display)))
frac = config[CONF_BUFFER_SIZE]
if frac >= 0.75:
frac = 1
elif frac >= 0.375:
frac = 2
elif frac > 0.19:
frac = 4
else:
frac = 8
cg.add(lv_component.set_buffer_frac(int(frac)))
cg.add(lv_component.set_full_refresh(config[df.CONF_FULL_REFRESH]))
for font in helpers.esphome_fonts_used:
await cg.get_variable(font)
cg.new_Pvariable(ID(f"{font}_engine", True, type=FontEngine), MockObj(font))
default_font = config[df.CONF_DEFAULT_FONT]
if default_font not in helpers.lv_fonts_used:
add_define(
"LV_FONT_CUSTOM_DECLARE", f"LV_FONT_DECLARE(*{df.DEFAULT_ESPHOME_FONT})"
)
globfont_id = ID(
df.DEFAULT_ESPHOME_FONT,
True,
type=lv_font_t.operator("ptr").operator("const"),
)
cg.new_variable(globfont_id, MockObj(default_font))
add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT)
else:
add_define("LV_FONT_DEFAULT", default_font)
with LvContext():
await set_obj_properties(lv_scr_act, config)
await add_widgets(lv_scr_act, config)
Widget.set_completed()
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:
add_define(f"LV_USE_{use.upper()}")
lv_conf_h_file = CORE.relative_src_path(LV_CONF_FILENAME)
write_file_if_changed(lv_conf_h_file, generate_lv_conf_h())
CORE.add_build_flag("-DLV_CONF_H=1")
CORE.add_build_flag(f'-DLV_CONF_PATH="{LV_CONF_FILENAME}"')
def display_schema(config):
value = cv.ensure_list(cv.use_id(Display))(config)
return value or [cv.use_id(Display)(config)]
FINAL_VALIDATE_SCHEMA = final_validation
CONFIG_SCHEMA = (
cv.polling_component_schema("1s")
.extend(obj_schema("obj"))
.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(LvglComponent),
cv.GenerateID(df.CONF_DISPLAYS): display_schema,
cv.Optional(df.CONF_COLOR_DEPTH, default=16): cv.one_of(16),
cv.Optional(df.CONF_DEFAULT_FONT, default="montserrat_14"): lvalid.lv_font,
cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean,
cv.Optional(CONF_BUFFER_SIZE, default="100%"): cv.percentage,
cv.Optional(df.CONF_LOG_LEVEL, default="WARN"): cv.one_of(
*df.LOG_LEVELS, upper=True
),
cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of(
"big_endian", "little_endian"
),
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(WIDGET_SCHEMA),
cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color,
}
)
).add_extra(cv.has_at_least_one_key(CONF_PAGES, df.CONF_WIDGETS))

View file

@ -0,0 +1,487 @@
"""
This is the base of the import tree for LVGL. It contains constant definitions used elsewhere.
Constants already defined in esphome.const are not duplicated here and must be imported where used.
"""
from esphome import codegen as cg, config_validation as cv
from esphome.core import ID, Lambda
from esphome.cpp_types import uint32
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from .lvcode import ConstantLiteral
class LValidator:
"""
A validator for a particular type used in LVGL. Usable in configs as a validator, also
has `process()` to convert a value during code generation
"""
def __init__(self, validator, rtype, idtype=None, idexpr=None, retmapper=None):
self.validator = validator
self.rtype = rtype
self.idtype = idtype
self.idexpr = idexpr
self.retmapper = retmapper
def __call__(self, value):
if isinstance(value, cv.Lambda):
return cv.returning_lambda(value)
if self.idtype is not None and isinstance(value, ID):
return cv.use_id(self.idtype)(value)
return self.validator(value)
async def process(self, value, args=()):
if value is None:
return None
if isinstance(value, Lambda):
return cg.RawExpression(
f"{await cg.process_lambda(value, args, return_type=self.rtype)}()"
)
if self.idtype is not None and isinstance(value, ID):
return cg.RawExpression(f"{value}->{self.idexpr}")
if self.retmapper is not None:
return self.retmapper(value)
return cg.safe_exp(value)
class LvConstant(LValidator):
"""
Allow one of a list of choices, mapped to upper case, and prepend the choice with the prefix.
It's also permitted to include the prefix in the value
The property `one_of` has the single case validator, and `several_of` allows a list of constants.
"""
def __init__(self, prefix: str, *choices):
self.prefix = prefix
self.choices = choices
prefixed_choices = [prefix + v for v in choices]
prefixed_validator = cv.one_of(*prefixed_choices, upper=True)
@schema_extractor("one_of")
def validator(value):
if value == SCHEMA_EXTRACT:
return self.choices
if isinstance(value, str) and value.startswith(self.prefix):
return prefixed_validator(value)
return self.prefix + cv.one_of(*choices, upper=True)(value)
super().__init__(validator, rtype=uint32)
self.one_of = LValidator(validator, uint32, retmapper=self.mapper)
self.several_of = LValidator(
cv.ensure_list(self.one_of), uint32, retmapper=self.mapper
)
def mapper(self, value, args=()):
if isinstance(value, list):
value = "|".join(value)
return ConstantLiteral(value)
def extend(self, *choices):
"""
Extend an LVCconstant with additional choices.
:param choices: The extra choices
:return: A new LVConstant instance
"""
return LvConstant(self.prefix, *(self.choices + choices))
# Widgets
CONF_LABEL = "label"
# Parts
CONF_MAIN = "main"
CONF_SCROLLBAR = "scrollbar"
CONF_INDICATOR = "indicator"
CONF_KNOB = "knob"
CONF_SELECTED = "selected"
CONF_ITEMS = "items"
CONF_TICKS = "ticks"
CONF_TICK_STYLE = "tick_style"
CONF_CURSOR = "cursor"
CONF_TEXTAREA_PLACEHOLDER = "textarea_placeholder"
LV_FONTS = list(f"montserrat_{s}" for s in range(8, 50, 2)) + [
"dejavu_16_persian_hebrew",
"simsun_16_cjk",
"unscii_8",
"unscii_16",
]
LV_EVENT = {
"PRESS": "PRESSED",
"SHORT_CLICK": "SHORT_CLICKED",
"LONG_PRESS": "LONG_PRESSED",
"LONG_PRESS_REPEAT": "LONG_PRESSED_REPEAT",
"CLICK": "CLICKED",
"RELEASE": "RELEASED",
"SCROLL_BEGIN": "SCROLL_BEGIN",
"SCROLL_END": "SCROLL_END",
"SCROLL": "SCROLL",
"FOCUS": "FOCUSED",
"DEFOCUS": "DEFOCUSED",
"READY": "READY",
"CANCEL": "CANCEL",
}
LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT)
LV_ANIM = LvConstant(
"LV_SCR_LOAD_ANIM_",
"NONE",
"OVER_LEFT",
"OVER_RIGHT",
"OVER_TOP",
"OVER_BOTTOM",
"MOVE_LEFT",
"MOVE_RIGHT",
"MOVE_TOP",
"MOVE_BOTTOM",
"FADE_IN",
"FADE_OUT",
"OUT_LEFT",
"OUT_RIGHT",
"OUT_TOP",
"OUT_BOTTOM",
)
LOG_LEVELS = (
"TRACE",
"INFO",
"WARN",
"ERROR",
"USER",
"NONE",
)
LV_LONG_MODES = LvConstant(
"LV_LABEL_LONG_",
"WRAP",
"DOT",
"SCROLL",
"SCROLL_CIRCULAR",
"CLIP",
)
STATES = (
"default",
"checked",
"focused",
"focus_key",
"edited",
"hovered",
"pressed",
"scrolled",
"disabled",
"user_1",
"user_2",
"user_3",
"user_4",
)
PARTS = (
CONF_MAIN,
CONF_SCROLLBAR,
CONF_INDICATOR,
CONF_KNOB,
CONF_SELECTED,
CONF_ITEMS,
CONF_TICKS,
CONF_CURSOR,
CONF_TEXTAREA_PLACEHOLDER,
)
KEYBOARD_MODES = LvConstant(
"LV_KEYBOARD_MODE_",
"TEXT_LOWER",
"TEXT_UPPER",
"SPECIAL",
"NUMBER",
)
ROLLER_MODES = LvConstant("LV_ROLLER_MODE_", "NORMAL", "INFINITE")
DIRECTIONS = LvConstant("LV_DIR_", "LEFT", "RIGHT", "BOTTOM", "TOP")
TILE_DIRECTIONS = DIRECTIONS.extend("HOR", "VER", "ALL")
CHILD_ALIGNMENTS = LvConstant(
"LV_ALIGN_",
"TOP_LEFT",
"TOP_MID",
"TOP_RIGHT",
"LEFT_MID",
"CENTER",
"RIGHT_MID",
"BOTTOM_LEFT",
"BOTTOM_MID",
"BOTTOM_RIGHT",
)
SIBLING_ALIGNMENTS = LvConstant(
"LV_ALIGN_",
"OUT_LEFT_TOP",
"OUT_TOP_LEFT",
"OUT_TOP_MID",
"OUT_TOP_RIGHT",
"OUT_RIGHT_TOP",
"OUT_LEFT_MID",
"OUT_RIGHT_MID",
"OUT_LEFT_BOTTOM",
"OUT_BOTTOM_LEFT",
"OUT_BOTTOM_MID",
"OUT_BOTTOM_RIGHT",
"OUT_RIGHT_BOTTOM",
)
ALIGN_ALIGNMENTS = CHILD_ALIGNMENTS.extend(*SIBLING_ALIGNMENTS.choices)
FLEX_FLOWS = LvConstant(
"LV_FLEX_FLOW_",
"ROW",
"COLUMN",
"ROW_WRAP",
"COLUMN_WRAP",
"ROW_REVERSE",
"COLUMN_REVERSE",
"ROW_WRAP_REVERSE",
"COLUMN_WRAP_REVERSE",
)
OBJ_FLAGS = (
"hidden",
"clickable",
"click_focusable",
"checkable",
"scrollable",
"scroll_elastic",
"scroll_momentum",
"scroll_one",
"scroll_chain_hor",
"scroll_chain_ver",
"scroll_chain",
"scroll_on_focus",
"scroll_with_arrow",
"snappable",
"press_lock",
"event_bubble",
"gesture_bubble",
"adv_hittest",
"ignore_layout",
"floating",
"overflow_visible",
"layout_1",
"layout_2",
"widget_1",
"widget_2",
"user_1",
"user_2",
"user_3",
"user_4",
)
ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL")
BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE")
BTNMATRIX_CTRLS = (
"HIDDEN",
"NO_REPEAT",
"DISABLED",
"CHECKABLE",
"CHECKED",
"CLICK_TRIG",
"POPOVER",
"RECOLOR",
"CUSTOM_1",
"CUSTOM_2",
)
LV_BASE_ALIGNMENTS = (
"START",
"CENTER",
"END",
)
LV_CELL_ALIGNMENTS = LvConstant(
"LV_GRID_ALIGN_",
*LV_BASE_ALIGNMENTS,
)
LV_GRID_ALIGNMENTS = LV_CELL_ALIGNMENTS.extend(
"STRETCH",
"SPACE_EVENLY",
"SPACE_AROUND",
"SPACE_BETWEEN",
)
LV_FLEX_ALIGNMENTS = LvConstant(
"LV_FLEX_ALIGN_",
*LV_BASE_ALIGNMENTS,
"SPACE_EVENLY",
"SPACE_AROUND",
"SPACE_BETWEEN",
)
LV_MENU_MODES = LvConstant(
"LV_MENU_HEADER_",
"TOP_FIXED",
"TOP_UNFIXED",
"BOTTOM_FIXED",
)
LV_CHART_TYPES = (
"NONE",
"LINE",
"BAR",
"SCATTER",
)
LV_CHART_AXES = (
"PRIMARY_Y",
"SECONDARY_Y",
"PRIMARY_X",
"SECONDARY_X",
)
CONF_ACCEPTED_CHARS = "accepted_chars"
CONF_ADJUSTABLE = "adjustable"
CONF_ALIGN = "align"
CONF_ALIGN_TO = "align_to"
CONF_ANGLE_RANGE = "angle_range"
CONF_ANIMATED = "animated"
CONF_ANIMATION = "animation"
CONF_ANTIALIAS = "antialias"
CONF_ARC_LENGTH = "arc_length"
CONF_AUTO_START = "auto_start"
CONF_BACKGROUND_STYLE = "background_style"
CONF_DECIMAL_PLACES = "decimal_places"
CONF_COLUMN = "column"
CONF_DIGITS = "digits"
CONF_DISP_BG_COLOR = "disp_bg_color"
CONF_DISP_BG_IMAGE = "disp_bg_image"
CONF_BODY = "body"
CONF_BUTTONS = "buttons"
CONF_BYTE_ORDER = "byte_order"
CONF_CHANGE_RATE = "change_rate"
CONF_CLOSE_BUTTON = "close_button"
CONF_COLOR_DEPTH = "color_depth"
CONF_COLOR_END = "color_end"
CONF_COLOR_START = "color_start"
CONF_CONTROL = "control"
CONF_DEFAULT = "default"
CONF_DEFAULT_FONT = "default_font"
CONF_DIR = "dir"
CONF_DISPLAYS = "displays"
CONF_END_ANGLE = "end_angle"
CONF_END_VALUE = "end_value"
CONF_ENTER_BUTTON = "enter_button"
CONF_ENTRIES = "entries"
CONF_FLAGS = "flags"
CONF_FLEX_FLOW = "flex_flow"
CONF_FLEX_ALIGN_MAIN = "flex_align_main"
CONF_FLEX_ALIGN_CROSS = "flex_align_cross"
CONF_FLEX_ALIGN_TRACK = "flex_align_track"
CONF_FLEX_GROW = "flex_grow"
CONF_FULL_REFRESH = "full_refresh"
CONF_GRID_CELL_ROW_POS = "grid_cell_row_pos"
CONF_GRID_CELL_COLUMN_POS = "grid_cell_column_pos"
CONF_GRID_CELL_ROW_SPAN = "grid_cell_row_span"
CONF_GRID_CELL_COLUMN_SPAN = "grid_cell_column_span"
CONF_GRID_CELL_X_ALIGN = "grid_cell_x_align"
CONF_GRID_CELL_Y_ALIGN = "grid_cell_y_align"
CONF_GRID_COLUMN_ALIGN = "grid_column_align"
CONF_GRID_COLUMNS = "grid_columns"
CONF_GRID_ROW_ALIGN = "grid_row_align"
CONF_GRID_ROWS = "grid_rows"
CONF_HEADER_MODE = "header_mode"
CONF_HOME = "home"
CONF_INDICATORS = "indicators"
CONF_KEY_CODE = "key_code"
CONF_LABEL_GAP = "label_gap"
CONF_LAYOUT = "layout"
CONF_LEFT_BUTTON = "left_button"
CONF_LINE_WIDTH = "line_width"
CONF_LOG_LEVEL = "log_level"
CONF_LONG_PRESS_TIME = "long_press_time"
CONF_LONG_PRESS_REPEAT_TIME = "long_press_repeat_time"
CONF_LVGL_ID = "lvgl_id"
CONF_LONG_MODE = "long_mode"
CONF_MAJOR = "major"
CONF_MSGBOXES = "msgboxes"
CONF_OBJ = "obj"
CONF_OFFSET_X = "offset_x"
CONF_OFFSET_Y = "offset_y"
CONF_ONE_LINE = "one_line"
CONF_ON_SELECT = "on_select"
CONF_ONE_CHECKED = "one_checked"
CONF_NEXT = "next"
CONF_PAGE_WRAP = "page_wrap"
CONF_PASSWORD_MODE = "password_mode"
CONF_PIVOT_X = "pivot_x"
CONF_PIVOT_Y = "pivot_y"
CONF_PLACEHOLDER_TEXT = "placeholder_text"
CONF_POINTS = "points"
CONF_PREVIOUS = "previous"
CONF_REPEAT_COUNT = "repeat_count"
CONF_R_MOD = "r_mod"
CONF_RECOLOR = "recolor"
CONF_RIGHT_BUTTON = "right_button"
CONF_ROLLOVER = "rollover"
CONF_ROOT_BACK_BTN = "root_back_btn"
CONF_ROWS = "rows"
CONF_SCALES = "scales"
CONF_SCALE_LINES = "scale_lines"
CONF_SCROLLBAR_MODE = "scrollbar_mode"
CONF_SELECTED_INDEX = "selected_index"
CONF_SHOW_SNOW = "show_snow"
CONF_SPIN_TIME = "spin_time"
CONF_SRC = "src"
CONF_START_ANGLE = "start_angle"
CONF_START_VALUE = "start_value"
CONF_STATES = "states"
CONF_STRIDE = "stride"
CONF_STYLE = "style"
CONF_STYLE_ID = "style_id"
CONF_SKIP = "skip"
CONF_SYMBOL = "symbol"
CONF_TAB_ID = "tab_id"
CONF_TABS = "tabs"
CONF_TEXT = "text"
CONF_TILE = "tile"
CONF_TILE_ID = "tile_id"
CONF_TILES = "tiles"
CONF_TITLE = "title"
CONF_TOP_LAYER = "top_layer"
CONF_TRANSPARENCY_KEY = "transparency_key"
CONF_THEME = "theme"
CONF_VISIBLE_ROW_COUNT = "visible_row_count"
CONF_WIDGET = "widget"
CONF_WIDGETS = "widgets"
CONF_X = "x"
CONF_Y = "y"
CONF_ZOOM = "zoom"
# Keypad keys
LV_KEYS = LvConstant(
"LV_KEY_",
"UP",
"DOWN",
"RIGHT",
"LEFT",
"ESC",
"DEL",
"BACKSPACE",
"ENTER",
"NEXT",
"PREV",
"HOME",
"END",
)
# list of widgets and the parts allowed
WIDGET_PARTS = {
CONF_LABEL: (CONF_MAIN, CONF_SCROLLBAR, CONF_SELECTED),
CONF_OBJ: (CONF_MAIN,),
}
DEFAULT_ESPHOME_FONT = "esphome_lv_default_font"
def join_enums(enums, prefix=""):
return "|".join(f"(int){prefix}{e.upper()}" for e in enums)

View file

@ -0,0 +1,76 @@
#include "lvgl_esphome.h"
#ifdef USE_LVGL_FONT
namespace esphome {
namespace lvgl {
static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) {
auto *fe = (FontEngine *) font->dsc;
const auto *gd = fe->get_glyph_data(unicode_letter);
if (gd == nullptr)
return nullptr;
// esph_log_d(TAG, "Returning bitmap @ %X", (uint32_t)gd->data);
return gd->data;
}
static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) {
auto *fe = (FontEngine *) font->dsc;
const auto *gd = fe->get_glyph_data(unicode_letter);
if (gd == nullptr)
return false;
dsc->adv_w = gd->offset_x + gd->width;
dsc->ofs_x = gd->offset_x;
dsc->ofs_y = fe->height - gd->height - gd->offset_y - fe->baseline;
dsc->box_w = gd->width;
dsc->box_h = gd->height;
dsc->is_placeholder = 0;
dsc->bpp = fe->bpp;
return true;
}
FontEngine::FontEngine(font::Font *esp_font) : font_(esp_font) {
this->bpp = esp_font->get_bpp();
this->lv_font_.dsc = this;
this->lv_font_.line_height = this->height = esp_font->get_height();
this->lv_font_.base_line = this->baseline = this->lv_font_.line_height - esp_font->get_baseline();
this->lv_font_.get_glyph_dsc = get_glyph_dsc_cb;
this->lv_font_.get_glyph_bitmap = get_glyph_bitmap;
this->lv_font_.subpx = LV_FONT_SUBPX_NONE;
this->lv_font_.underline_position = -1;
this->lv_font_.underline_thickness = 1;
}
const lv_font_t *FontEngine::get_lv_font() { return &this->lv_font_; }
const font::GlyphData *FontEngine::get_glyph_data(uint32_t unicode_letter) {
if (unicode_letter == last_letter_)
return this->last_data_;
uint8_t unicode[5];
memset(unicode, 0, sizeof unicode);
if (unicode_letter > 0xFFFF) {
unicode[0] = 0xF0 + ((unicode_letter >> 18) & 0x7);
unicode[1] = 0x80 + ((unicode_letter >> 12) & 0x3F);
unicode[2] = 0x80 + ((unicode_letter >> 6) & 0x3F);
unicode[3] = 0x80 + (unicode_letter & 0x3F);
} else if (unicode_letter > 0x7FF) {
unicode[0] = 0xE0 + ((unicode_letter >> 12) & 0xF);
unicode[1] = 0x80 + ((unicode_letter >> 6) & 0x3F);
unicode[2] = 0x80 + (unicode_letter & 0x3F);
} else if (unicode_letter > 0x7F) {
unicode[0] = 0xC0 + ((unicode_letter >> 6) & 0x1F);
unicode[1] = 0x80 + (unicode_letter & 0x3F);
} else {
unicode[0] = unicode_letter;
}
int match_length;
int glyph_n = this->font_->match_next_glyph(unicode, &match_length);
if (glyph_n < 0)
return nullptr;
this->last_data_ = this->font_->get_glyphs()[glyph_n].get_glyph_data();
this->last_letter_ = unicode_letter;
return this->last_data_;
}
} // namespace lvgl
} // namespace esphome
#endif // USES_LVGL_FONT

View file

@ -0,0 +1,70 @@
import re
from esphome import config_validation as cv
from esphome.config import Config
from esphome.const import CONF_ARGS, CONF_FORMAT
from esphome.core import CORE, ID
from esphome.yaml_util import ESPHomeDataBase
lv_uses = {
"USER_DATA",
"LOG",
"STYLE",
"FONT_PLACEHOLDER",
"THEME_DEFAULT",
}
def add_lv_use(*names):
for name in names:
lv_uses.add(name)
lv_fonts_used = set()
esphome_fonts_used = set()
REQUIRED_COMPONENTS = {}
lvgl_components_required = set()
def validate_printf(value):
cfmt = r"""
( # start of capture group 1
% # literal "%"
(?:[-+0 #]{0,5}) # optional flags
(?:\d+|\*)? # width
(?:\.(?:\d+|\*))? # precision
(?:h|l|ll|w|I|I32|I64)? # size
[cCdiouxXeEfgGaAnpsSZ] # type
)
""" # noqa
matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.X)
if len(matches) != len(value[CONF_ARGS]):
raise cv.Invalid(
f"Found {len(matches)} printf-patterns ({', '.join(matches)}), but {len(value[CONF_ARGS])} args were given!"
)
return value
def get_line_marks(value) -> list:
"""
If possible, return a preprocessor directive to identify the line number where the given id was defined.
:param id: The id in question
:return: A list containing zero or more line directives
"""
path = None
if isinstance(value, ESPHomeDataBase):
path = value.esp_range
elif isinstance(value, ID) and isinstance(CORE.config, Config):
path = CORE.config.get_path_for_id(value)[:-1]
path = CORE.config.get_deepest_document_range_for_path(path)
if path is None:
return []
return [path.start_mark.as_line_directive]
def requires_component(comp):
def validator(value):
lvgl_components_required.add(comp)
return cv.requires_component(comp)(value)
return validator

View file

@ -0,0 +1,34 @@
import esphome.config_validation as cv
from .defines import CONF_LABEL, CONF_LONG_MODE, CONF_RECOLOR, CONF_TEXT, LV_LONG_MODES
from .lv_validation import lv_bool, lv_text
from .schemas import TEXT_SCHEMA
from .types import lv_label_t
from .widget import Widget, WidgetType
class LabelType(WidgetType):
def __init__(self):
super().__init__(
CONF_LABEL,
TEXT_SCHEMA.extend(
{
cv.Optional(CONF_RECOLOR): lv_bool,
cv.Optional(CONF_LONG_MODE): LV_LONG_MODES.one_of,
}
),
)
@property
def w_type(self):
return lv_label_t
async def to_code(self, w: Widget, config):
"""For a text object, create and set text"""
if value := config.get(CONF_TEXT):
w.set_property(CONF_TEXT, await lv_text.process(value))
w.set_property(CONF_LONG_MODE, config)
w.set_property(CONF_RECOLOR, config)
label_spec = LabelType()

View file

@ -0,0 +1,170 @@
import esphome.codegen as cg
from esphome.components.binary_sensor import BinarySensor
from esphome.components.color import ColorStruct
from esphome.components.font import Font
from esphome.components.sensor import Sensor
from esphome.components.text_sensor import TextSensor
import esphome.config_validation as cv
from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT
from esphome.core import HexInt
from esphome.cpp_generator import MockObj
from esphome.helpers import cpp_string_escape
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from . import types as ty
from .defines import LV_FONTS, LValidator, LvConstant
from .helpers import (
esphome_fonts_used,
lv_fonts_used,
lvgl_components_required,
requires_component,
)
from .lvcode import ConstantLiteral, lv_expr
from .types import lv_font_t
@schema_extractor("one_of")
def color(value):
if value == SCHEMA_EXTRACT:
return ["hex color value", "color ID"]
if isinstance(value, int):
return value
return cv.use_id(ColorStruct)(value)
def color_retmapper(value):
if isinstance(value, cv.Lambda):
return cv.returning_lambda(value)
if isinstance(value, int):
hexval = HexInt(value)
return lv_expr.color_hex(hexval)
# Must be an id
lvgl_components_required.add(CONF_COLOR)
return lv_expr.color_from(MockObj(value))
def pixels_or_percent(value):
"""A length in one axis - either a number (pixels) or a percentage"""
if value == SCHEMA_EXTRACT:
return ["pixels", "..%"]
if isinstance(value, int):
return str(cv.int_(value))
# Will throw an exception if not a percentage.
return f"lv_pct({int(cv.percentage(value) * 100)})"
def zoom(value):
value = cv.float_range(0.1, 10.0)(value)
return int(value * 256)
def angle(value):
"""
Validation for an angle in degrees, converted to an integer representing 0.1deg units
:param value: The input in the range 0..360
:return: An angle in 1/10 degree units.
"""
return int(cv.float_range(0.0, 360.0)(cv.angle(value)) * 10)
@schema_extractor("one_of")
def size(value):
"""A size in one axis - one of "size_content", a number (pixels) or a percentage"""
if value == SCHEMA_EXTRACT:
return ["size_content", "pixels", "..%"]
if isinstance(value, str) and value.lower().endswith("px"):
value = cv.int_(value[:-2])
if isinstance(value, str) and not value.endswith("%"):
if value.upper() == "SIZE_CONTENT":
return "LV_SIZE_CONTENT"
raise cv.Invalid("must be 'size_content', a pixel position or a percentage")
if isinstance(value, int):
return str(cv.int_(value))
# Will throw an exception if not a percentage.
return f"lv_pct({int(cv.percentage(value) * 100)})"
@schema_extractor("one_of")
def opacity(value):
consts = LvConstant("LV_OPA_", "TRANSP", "COVER")
if value == SCHEMA_EXTRACT:
return consts.choices
value = cv.Any(cv.percentage, consts.one_of)(value)
if isinstance(value, float):
return int(value * 255)
return value
def stop_value(value):
return cv.int_range(0, 255)(value)
lv_color = LValidator(color, ty.lv_color_t, retmapper=color_retmapper)
lv_bool = LValidator(cv.boolean, cg.bool_, BinarySensor, "get_state()")
def lvms_validator_(value):
if value == "never":
value = "2147483647ms"
return cv.positive_time_period_milliseconds(value)
lv_milliseconds = LValidator(
lvms_validator_,
cg.int32,
retmapper=lambda x: x.total_milliseconds,
)
class TextValidator(LValidator):
def __init__(self):
super().__init__(
cv.string,
cg.const_char_ptr,
TextSensor,
"get_state().c_str()",
lambda s: cg.safe_exp(f"{s}"),
)
def __call__(self, value):
if isinstance(value, dict):
return value
return super().__call__(value)
async def process(self, value, args=()):
if isinstance(value, dict):
args = [str(x) for x in value[CONF_ARGS]]
arg_expr = cg.RawExpression(",".join(args))
format_str = cpp_string_escape(value[CONF_FORMAT])
return f"str_sprintf({format_str}, {arg_expr}).c_str()"
return await super().process(value, args)
lv_text = TextValidator()
lv_float = LValidator(cv.float_, cg.float_, Sensor, "get_state()")
lv_int = LValidator(cv.int_, cg.int_, Sensor, "get_state()")
class LvFont(LValidator):
def __init__(self):
def lv_builtin_font(value):
fontval = cv.one_of(*LV_FONTS, lower=True)(value)
lv_fonts_used.add(fontval)
return "&lv_font_" + fontval
def validator(value):
if value == SCHEMA_EXTRACT:
return LV_FONTS
if isinstance(value, str) and value.lower() in LV_FONTS:
return lv_builtin_font(value)
fontval = cv.use_id(Font)(value)
esphome_fonts_used.add(fontval)
return requires_component("font")(f"{fontval}_engine->get_lv_font()")
super().__init__(validator, lv_font_t)
async def process(self, value, args=()):
return ConstantLiteral(value)
lv_font = LvFont()

View file

@ -0,0 +1,237 @@
import abc
import logging
from typing import Union
from esphome import codegen as cg
from esphome.core import ID, Lambda
from esphome.cpp_generator import (
AssignmentExpression,
CallExpression,
Expression,
LambdaExpression,
Literal,
MockObj,
RawExpression,
RawStatement,
SafeExpType,
Statement,
VariableDeclarationExpression,
statement,
)
from .helpers import get_line_marks
_LOGGER = logging.getLogger(__name__)
class CodeContext(abc.ABC):
"""
A class providing a context for code generation. Generated code will be added to the
current context. A new context will stack on the current context, and restore it
when done. Used with the `with` statement.
"""
code_context = None
@abc.abstractmethod
def add(self, expression: Union[Expression, Statement]):
pass
@staticmethod
def append(expression: Union[Expression, Statement]):
if CodeContext.code_context is not None:
CodeContext.code_context.add(expression)
return expression
def __init__(self):
self.previous: Union[CodeContext | None] = None
def __enter__(self):
self.previous = CodeContext.code_context
CodeContext.code_context = self
def __exit__(self, *args):
CodeContext.code_context = self.previous
class MainContext(CodeContext):
"""
Code generation into the main() function
"""
def add(self, expression: Union[Expression, Statement]):
return cg.add(expression)
class LvContext(CodeContext):
"""
Code generation into the LVGL initialisation code (called in `setup()`)
"""
lv_init_code: list["Statement"] = []
@staticmethod
def lv_add(expression: Union[Expression, Statement]):
if isinstance(expression, Expression):
expression = statement(expression)
if not isinstance(expression, Statement):
raise ValueError(
f"Add '{expression}' must be expression or statement, not {type(expression)}"
)
LvContext.lv_init_code.append(expression)
_LOGGER.debug("LV Adding: %s", expression)
return expression
@staticmethod
def get_code():
code = []
for exp in LvContext.lv_init_code:
text = str(statement(exp))
text = text.rstrip()
code.append(text)
return "\n".join(code) + "\n\n"
def add(self, expression: Union[Expression, Statement]):
return LvContext.lv_add(expression)
def set_style(self, prop):
return MockObj("lv_set_style_{prop}", "")
class LambdaContext(CodeContext):
"""
A context that will accumlate code for use in a lambda.
"""
def __init__(
self,
parameters: list[tuple[SafeExpType, str]],
return_type: SafeExpType = None,
):
super().__init__()
self.code_list: list[Statement] = []
self.parameters = parameters
self.return_type = return_type
def add(self, expression: Union[Expression, Statement]):
self.code_list.append(expression)
return expression
async def code(self) -> LambdaExpression:
code_text = []
for exp in self.code_list:
text = str(statement(exp))
text = text.rstrip()
code_text.append(text)
return await cg.process_lambda(
Lambda("\n".join(code_text) + "\n\n"),
self.parameters,
return_type=self.return_type,
)
class LocalVariable(MockObj):
"""
Create a local variable and enclose the code using it within a block.
"""
def __init__(self, name, type, modifier=None, rhs=None):
base = ID(name, True, type)
super().__init__(base, "")
self.modifier = modifier
self.rhs = rhs
def __enter__(self):
CodeContext.append(RawStatement("{"))
CodeContext.append(
VariableDeclarationExpression(self.base.type, self.modifier, self.base.id)
)
if self.rhs is not None:
CodeContext.append(AssignmentExpression(None, "", self.base, self.rhs))
return self.base
def __exit__(self, *args):
CodeContext.append(RawStatement("}"))
class MockLv:
"""
A mock object that can be used to generate LVGL calls.
"""
def __init__(self, base):
self.base = base
def __getattr__(self, attr: str) -> "MockLv":
return MockLv(f"{self.base}{attr}")
def append(self, expression):
CodeContext.append(expression)
def __call__(self, *args: SafeExpType) -> "MockObj":
call = CallExpression(self.base, *args)
result = MockObj(call, "")
self.append(result)
return result
def __str__(self):
return str(self.base)
def __repr__(self):
return f"MockLv<{str(self.base)}>"
def call(self, prop, *args):
call = CallExpression(RawExpression(f"{self.base}{prop}"), *args)
result = MockObj(call, "")
self.append(result)
return result
def cond_if(self, expression: Expression):
CodeContext.append(RawExpression(f"if({expression}) {{"))
def cond_else(self):
CodeContext.append(RawExpression("} else {"))
def cond_endif(self):
CodeContext.append(RawExpression("}"))
class LvExpr(MockLv):
def __getattr__(self, attr: str) -> "MockLv":
return LvExpr(f"{self.base}{attr}")
def append(self, expression):
pass
# Top level mock for generic lv_ calls to be recorded
lv = MockLv("lv_")
# Just generate an expression
lv_expr = LvExpr("lv_")
# Mock for lv_obj_ calls
lv_obj = MockLv("lv_obj_")
# equivalent to cg.add() for the lvgl init context
def lv_add(expression: Union[Expression, Statement]):
return CodeContext.append(expression)
def add_line_marks(where):
for mark in get_line_marks(where):
lv_add(cg.RawStatement(mark))
def lv_assign(target, expression):
lv_add(RawExpression(f"{target} = {expression}"))
class ConstantLiteral(Literal):
__slots__ = ("constant",)
def __init__(self, constant: str):
super().__init__()
self.constant = constant
def __str__(self):
return self.constant

View file

@ -0,0 +1,129 @@
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include "esphome/core/hal.h"
#include "lvgl_hal.h"
#include "lvgl_esphome.h"
namespace esphome {
namespace lvgl {
static const char *const TAG = "lvgl";
lv_event_code_t lv_custom_event; // NOLINT
void LvglComponent::dump_config() { ESP_LOGCONFIG(TAG, "LVGL:"); }
void LvglComponent::draw_buffer_(const lv_area_t *area, const uint8_t *ptr) {
for (auto *display : this->displays_) {
display->draw_pixels_at(area->x1, area->y1, lv_area_get_width(area), lv_area_get_height(area), ptr,
display::COLOR_ORDER_RGB, LV_BITNESS, LV_COLOR_16_SWAP);
}
}
void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
auto now = millis();
this->draw_buffer_(area, (const uint8_t *) color_p);
ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area),
lv_area_get_height(area), (int) (millis() - now));
lv_disp_flush_ready(disp_drv);
}
void LvglComponent::setup() {
ESP_LOGCONFIG(TAG, "LVGL Setup starts");
#if LV_USE_LOG
lv_log_register_print_cb(log_cb);
#endif
lv_init();
lv_custom_event = static_cast<lv_event_code_t>(lv_event_register_id());
auto *display = this->displays_[0];
size_t buffer_pixels = display->get_width() * display->get_height() / this->buffer_frac_;
auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8;
auto *buf = lv_custom_mem_alloc(buf_bytes);
if (buf == nullptr) {
ESP_LOGE(TAG, "Malloc failed to allocate %zu bytes", buf_bytes);
this->mark_failed();
this->status_set_error("Memory allocation failure");
return;
}
lv_disp_draw_buf_init(&this->draw_buf_, buf, nullptr, buffer_pixels);
lv_disp_drv_init(&this->disp_drv_);
this->disp_drv_.draw_buf = &this->draw_buf_;
this->disp_drv_.user_data = this;
this->disp_drv_.full_refresh = this->full_refresh_;
this->disp_drv_.flush_cb = static_flush_cb;
this->disp_drv_.rounder_cb = rounder_cb;
switch (display->get_rotation()) {
case display::DISPLAY_ROTATION_0_DEGREES:
break;
case display::DISPLAY_ROTATION_90_DEGREES:
this->disp_drv_.sw_rotate = true;
this->disp_drv_.rotated = LV_DISP_ROT_90;
break;
case display::DISPLAY_ROTATION_180_DEGREES:
this->disp_drv_.sw_rotate = true;
this->disp_drv_.rotated = LV_DISP_ROT_180;
break;
case display::DISPLAY_ROTATION_270_DEGREES:
this->disp_drv_.sw_rotate = true;
this->disp_drv_.rotated = LV_DISP_ROT_270;
break;
}
display->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);
this->disp_drv_.hor_res = (lv_coord_t) display->get_width();
this->disp_drv_.ver_res = (lv_coord_t) display->get_height();
ESP_LOGV(TAG, "sw_rotate = %d, rotated=%d", this->disp_drv_.sw_rotate, this->disp_drv_.rotated);
this->disp_ = lv_disp_drv_register(&this->disp_drv_);
for (const auto &v : this->init_lambdas_)
v(this->disp_);
lv_disp_trig_activity(this->disp_);
ESP_LOGCONFIG(TAG, "LVGL Setup complete");
}
} // namespace lvgl
} // namespace esphome
size_t lv_millis(void) { return esphome::millis(); }
#if defined(USE_HOST) || defined(USE_RP2040) || defined(USE_ESP8266)
void *lv_custom_mem_alloc(size_t size) {
auto *ptr = malloc(size); // NOLINT
if (ptr == nullptr) {
esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
}
return ptr;
}
void lv_custom_mem_free(void *ptr) { return free(ptr); } // NOLINT
void *lv_custom_mem_realloc(void *ptr, size_t size) { return realloc(ptr, size); } // NOLINT
#else
static unsigned cap_bits = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; // NOLINT
void *lv_custom_mem_alloc(size_t size) {
void *ptr;
ptr = heap_caps_malloc(size, cap_bits);
if (ptr == nullptr) {
cap_bits = MALLOC_CAP_8BIT;
ptr = heap_caps_malloc(size, cap_bits);
}
if (ptr == nullptr) {
esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
return nullptr;
}
#ifdef ESPHOME_LOG_HAS_VERBOSE
esphome::ESP_LOGV(esphome::lvgl::TAG, "allocate %zu - > %p", size, ptr);
#endif
return ptr;
}
void lv_custom_mem_free(void *ptr) {
#ifdef ESPHOME_LOG_HAS_VERBOSE
esphome::ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr);
#endif
if (ptr == nullptr)
return;
heap_caps_free(ptr);
}
void *lv_custom_mem_realloc(void *ptr, size_t size) {
#ifdef ESPHOME_LOG_HAS_VERBOSE
esphome::ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size);
#endif
return heap_caps_realloc(ptr, size, cap_bits);
}
#endif

View file

@ -0,0 +1,119 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_LVGL
// required for clang-tidy
#ifndef LV_CONF_H
#define LV_CONF_SKIP 1 // NOLINT
#endif
#include "esphome/components/display/display.h"
#include "esphome/components/display/display_color_utils.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <lvgl.h>
#include <vector>
#ifdef USE_LVGL_FONT
#include "esphome/components/font/font.h"
#endif
namespace esphome {
namespace lvgl {
extern lv_event_code_t lv_custom_event; // NOLINT
#ifdef USE_LVGL_COLOR
static lv_color_t lv_color_from(Color color) { return lv_color_make(color.red, color.green, color.blue); }
#endif
#if LV_COLOR_DEPTH == 16
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_565;
#elif LV_COLOR_DEPTH == 32
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_888;
#else
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_332;
#endif
// Parent class for things that wrap an LVGL object
class LvCompound {
public:
virtual void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; }
lv_obj_t *obj{};
};
using LvLambdaType = std::function<void(lv_obj_t *)>;
using set_value_lambda_t = std::function<void(float)>;
using event_callback_t = void(_lv_event_t *);
using text_lambda_t = std::function<const char *()>;
#ifdef USE_LVGL_FONT
class FontEngine {
public:
FontEngine(font::Font *esp_font);
const lv_font_t *get_lv_font();
const font::GlyphData *get_glyph_data(uint32_t unicode_letter);
uint16_t baseline{};
uint16_t height{};
uint8_t bpp{};
protected:
font::Font *font_{};
uint32_t last_letter_{};
const font::GlyphData *last_data_{};
lv_font_t lv_font_{};
};
#endif // USE_LVGL_FONT
class LvglComponent : public PollingComponent {
constexpr static const char *const TAG = "lvgl";
public:
static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
reinterpret_cast<LvglComponent *>(disp_drv->user_data)->flush_cb_(disp_drv, area, color_p);
}
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 loop() override { lv_timer_handler_run_in_period(5); }
void setup() override;
void update() override {}
void add_display(display::Display *display) { this->displays_.push_back(display); }
void add_init_lambda(const std::function<void(lv_disp_t *)> &lamb) { this->init_lambdas_.push_back(lamb); }
void dump_config() override;
void set_full_refresh(bool full_refresh) { this->full_refresh_ = full_refresh; }
void set_buffer_frac(size_t frac) { this->buffer_frac_ = frac; }
lv_disp_t *get_disp() { return this->disp_; }
protected:
void draw_buffer_(const lv_area_t *area, const uint8_t *ptr);
void flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p);
std::vector<display::Display *> displays_{};
lv_disp_draw_buf_t draw_buf_{};
lv_disp_drv_t disp_drv_{};
lv_disp_t *disp_{};
std::vector<std::function<void(lv_disp_t *)>> init_lambdas_;
size_t buffer_frac_{1};
bool full_refresh_{};
};
} // namespace lvgl
} // namespace esphome
#endif

View file

@ -0,0 +1,21 @@
//
// Created by Clyde Stubbs on 20/9/2023.
//
#pragma once
#ifdef __cplusplus
#define EXTERNC extern "C"
#include <cstddef>
namespace esphome {
namespace lvgl {}
} // namespace esphome
#else
#define EXTERNC extern
#include <stddef.h>
#endif
EXTERNC size_t lv_millis(void);
EXTERNC void *lv_custom_mem_alloc(size_t size);
EXTERNC void lv_custom_mem_free(void *ptr);
EXTERNC void *lv_custom_mem_realloc(void *ptr, size_t size);

View file

@ -0,0 +1,22 @@
from .defines import CONF_OBJ
from .types import lv_obj_t
from .widget import WidgetType
class ObjType(WidgetType):
"""
The base LVGL object. All other widgets inherit from this.
"""
def __init__(self):
super().__init__(CONF_OBJ, schema={}, modify_schema={})
@property
def w_type(self):
return lv_obj_t
async def to_code(self, w, config):
return []
obj_spec = ObjType()

View file

@ -0,0 +1,260 @@
from esphome import config_validation as cv
from esphome.const import CONF_ARGS, CONF_FORMAT, CONF_ID, CONF_STATE, CONF_TYPE
from esphome.schema_extractors import SCHEMA_EXTRACT
from . import defines as df, lv_validation as lvalid, types as ty
from .defines import WIDGET_PARTS
from .helpers import (
REQUIRED_COMPONENTS,
add_lv_use,
requires_component,
validate_printf,
)
from .lv_validation import lv_font
from .types import WIDGET_TYPES, get_widget_type
# A schema for text properties
TEXT_SCHEMA = cv.Schema(
{
cv.Optional(df.CONF_TEXT): cv.Any(
cv.All(
cv.Schema(
{
cv.Required(CONF_FORMAT): cv.string,
cv.Optional(CONF_ARGS, default=list): cv.ensure_list(
cv.lambda_
),
},
),
validate_printf,
),
lvalid.lv_text,
)
}
)
# All LVGL styles and their validators
STYLE_PROPS = {
"align": df.CHILD_ALIGNMENTS.one_of,
"arc_opa": lvalid.opacity,
"arc_color": lvalid.lv_color,
"arc_rounded": lvalid.lv_bool,
"arc_width": cv.positive_int,
"anim_time": lvalid.lv_milliseconds,
"bg_color": lvalid.lv_color,
"bg_grad_color": lvalid.lv_color,
"bg_dither_mode": df.LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF").one_of,
"bg_grad_dir": df.LvConstant("LV_GRAD_DIR_", "NONE", "HOR", "VER").one_of,
"bg_grad_stop": lvalid.stop_value,
"bg_img_opa": lvalid.opacity,
"bg_img_recolor": lvalid.lv_color,
"bg_img_recolor_opa": lvalid.opacity,
"bg_main_stop": lvalid.stop_value,
"bg_opa": lvalid.opacity,
"border_color": lvalid.lv_color,
"border_opa": lvalid.opacity,
"border_post": lvalid.lv_bool,
"border_side": df.LvConstant(
"LV_BORDER_SIDE_", "NONE", "TOP", "BOTTOM", "LEFT", "RIGHT", "INTERNAL"
).several_of,
"border_width": cv.positive_int,
"clip_corner": lvalid.lv_bool,
"height": lvalid.size,
"img_recolor": lvalid.lv_color,
"img_recolor_opa": lvalid.opacity,
"line_width": cv.positive_int,
"line_dash_width": cv.positive_int,
"line_dash_gap": cv.positive_int,
"line_rounded": lvalid.lv_bool,
"line_color": lvalid.lv_color,
"opa": lvalid.opacity,
"opa_layered": lvalid.opacity,
"outline_color": lvalid.lv_color,
"outline_opa": lvalid.opacity,
"outline_pad": lvalid.size,
"outline_width": lvalid.size,
"pad_all": lvalid.size,
"pad_bottom": lvalid.size,
"pad_column": lvalid.size,
"pad_left": lvalid.size,
"pad_right": lvalid.size,
"pad_row": lvalid.size,
"pad_top": lvalid.size,
"shadow_color": lvalid.lv_color,
"shadow_ofs_x": cv.int_,
"shadow_ofs_y": cv.int_,
"shadow_opa": lvalid.opacity,
"shadow_spread": cv.int_,
"shadow_width": cv.positive_int,
"text_align": df.LvConstant(
"LV_TEXT_ALIGN_", "LEFT", "CENTER", "RIGHT", "AUTO"
).one_of,
"text_color": lvalid.lv_color,
"text_decor": df.LvConstant(
"LV_TEXT_DECOR_", "NONE", "UNDERLINE", "STRIKETHROUGH"
).several_of,
"text_font": lv_font,
"text_letter_space": cv.positive_int,
"text_line_space": cv.positive_int,
"text_opa": lvalid.opacity,
"transform_angle": lvalid.angle,
"transform_height": lvalid.pixels_or_percent,
"transform_pivot_x": lvalid.pixels_or_percent,
"transform_pivot_y": lvalid.pixels_or_percent,
"transform_zoom": lvalid.zoom,
"translate_x": lvalid.pixels_or_percent,
"translate_y": lvalid.pixels_or_percent,
"max_height": lvalid.pixels_or_percent,
"max_width": lvalid.pixels_or_percent,
"min_height": lvalid.pixels_or_percent,
"min_width": lvalid.pixels_or_percent,
"radius": cv.Any(lvalid.size, df.LvConstant("LV_RADIUS_", "CIRCLE").one_of),
"width": lvalid.size,
"x": lvalid.pixels_or_percent,
"y": lvalid.pixels_or_percent,
}
# Complete object style schema
STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend(
{
cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant(
"LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO"
).one_of,
}
)
# Object states. Top level properties apply to MAIN
STATE_SCHEMA = cv.Schema(
{cv.Optional(state): STYLE_SCHEMA for state in df.STATES}
).extend(STYLE_SCHEMA)
# Setting object states
SET_STATE_SCHEMA = cv.Schema(
{cv.Optional(state): lvalid.lv_bool for state in df.STATES}
)
# Setting object flags
FLAG_SCHEMA = cv.Schema({cv.Optional(flag): cv.boolean for flag in df.OBJ_FLAGS})
FLAG_LIST = cv.ensure_list(df.LvConstant("LV_OBJ_FLAG_", *df.OBJ_FLAGS).one_of)
def part_schema(widget_type):
"""
Generate a schema for the various parts (e.g. main:, indicator:) of a widget type
:param widget_type: The type of widget to generate for
:return:
"""
parts = WIDGET_PARTS.get(widget_type)
if parts is None:
parts = (df.CONF_MAIN,)
return cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}).extend(
STATE_SCHEMA
)
def obj_schema(widget_type: str):
"""
Create a schema for a widget type itself i.e. no allowance for children
:param widget_type:
:return:
"""
return (
part_schema(widget_type)
.extend(FLAG_SCHEMA)
.extend(ALIGN_TO_SCHEMA)
.extend(
cv.Schema(
{
cv.Optional(CONF_STATE): SET_STATE_SCHEMA,
}
)
)
)
ALIGN_TO_SCHEMA = {
cv.Optional(df.CONF_ALIGN_TO): cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(ty.lv_obj_t),
cv.Required(df.CONF_ALIGN): df.ALIGN_ALIGNMENTS.one_of,
cv.Optional(df.CONF_X, default=0): lvalid.pixels_or_percent,
cv.Optional(df.CONF_Y, default=0): lvalid.pixels_or_percent,
}
)
}
# A style schema that can include text
STYLED_TEXT_SCHEMA = cv.maybe_simple_value(
STYLE_SCHEMA.extend(TEXT_SCHEMA), key=df.CONF_TEXT
)
ALL_STYLES = {
**STYLE_PROPS,
}
def container_validator(schema, widget_type):
"""
Create a validator for a container given the widget type
:param schema: Base schema to extend
:param widget_type:
:return:
"""
def validator(value):
result = schema
if w_sch := WIDGET_TYPES[widget_type].schema:
result = result.extend(w_sch)
if value and (layout := value.get(df.CONF_LAYOUT)):
if not isinstance(layout, dict):
raise cv.Invalid("Layout value must be a dict")
ltype = layout.get(CONF_TYPE)
add_lv_use(ltype)
if value == SCHEMA_EXTRACT:
return result
return result(value)
return validator
def container_schema(widget_type, extras=None):
"""
Create a schema for a container widget of a given type. All obj properties are available, plus
the extras passed in, plus any defined for the specific widget being specified.
:param widget_type: The widget type, e.g. "img"
:param extras: Additional options to be made available, e.g. layout properties for children
:return: The schema for this type of widget.
"""
lv_type = get_widget_type(widget_type)
schema = obj_schema(widget_type).extend({cv.GenerateID(): cv.declare_id(lv_type)})
if extras:
schema = schema.extend(extras)
# Delayed evaluation for recursion
return container_validator(schema, widget_type)
def widget_schema(widget_type, extras=None):
"""
Create a schema for a given widget type
:param widget_type: The name of the widget
:param extras:
:return:
"""
validator = container_schema(widget_type, extras=extras)
if required := REQUIRED_COMPONENTS.get(widget_type):
validator = cv.All(validator, requires_component(required))
return cv.Exclusive(widget_type, df.CONF_WIDGETS), validator
# All widget schemas must be defined before this is called.
def any_widget_schema(extras=None):
"""
Generate schemas for all possible LVGL widgets. This is what implements the ability to have a list of any kind of
widget under the widgets: key.
:param extras: Additional schema to be applied to each generated one
:return:
"""
return cv.Any(dict(widget_schema(wt, extras) for wt in WIDGET_PARTS))

View file

@ -0,0 +1,64 @@
from esphome import codegen as cg
from esphome.core import ID
from .defines import CONF_LABEL, CONF_OBJ, CONF_TEXT
uint16_t_ptr = cg.uint16.operator("ptr")
lvgl_ns = cg.esphome_ns.namespace("lvgl")
char_ptr = cg.global_ns.namespace("char").operator("ptr")
void_ptr = cg.void.operator("ptr")
LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent)
lv_event_code_t = cg.global_ns.namespace("lv_event_code_t")
FontEngine = lvgl_ns.class_("FontEngine")
LvCompound = lvgl_ns.class_("LvCompound")
lv_font_t = cg.global_ns.class_("lv_font_t")
lv_style_t = cg.global_ns.struct("lv_style_t")
lv_pseudo_button_t = lvgl_ns.class_("LvPseudoButton")
lv_obj_base_t = cg.global_ns.class_("lv_obj_t", lv_pseudo_button_t)
lv_obj_t_ptr = lv_obj_base_t.operator("ptr")
lv_disp_t_ptr = cg.global_ns.struct("lv_disp_t").operator("ptr")
lv_color_t = cg.global_ns.struct("lv_color_t")
# this will be populated later, in __init__.py to avoid circular imports.
WIDGET_TYPES: dict = {}
class LvType(cg.MockObjClass):
def __init__(self, *args, **kwargs):
parens = kwargs.pop("parents", ())
super().__init__(*args, parents=parens + (lv_obj_base_t,))
self.args = kwargs.pop("largs", [(lv_obj_t_ptr, "obj")])
self.value = kwargs.pop("lvalue", lambda w: w.obj)
self.has_on_value = kwargs.pop("has_on_value", False)
self.value_property = None
def get_arg_type(self):
return self.args[0][0] if len(self.args) else None
class LvText(LvType):
def __init__(self, *args, **kwargs):
super().__init__(
*args,
largs=[(cg.std_string, "text")],
lvalue=lambda w: w.get_property("text")[0],
**kwargs,
)
self.value_property = CONF_TEXT
lv_obj_t = LvType("lv_obj_t")
lv_label_t = LvText("lv_label_t")
LV_TYPES = {
CONF_LABEL: lv_label_t,
CONF_OBJ: lv_obj_t,
}
def get_widget_type(typestr: str) -> LvType:
return LV_TYPES[typestr]
CUSTOM_EVENT = ID("lv_custom_event", False, type=lv_event_code_t)

View file

@ -0,0 +1,347 @@
import sys
from typing import Any
from esphome import codegen as cg, config_validation as cv
from esphome.config_validation import Invalid
from esphome.const import CONF_GROUP, CONF_ID, CONF_STATE
from esphome.core import ID, TimePeriod
from esphome.coroutine import FakeAwaitable
from esphome.cpp_generator import MockObjClass
from .defines import (
CONF_DEFAULT,
CONF_MAIN,
CONF_SCROLLBAR_MODE,
CONF_WIDGETS,
OBJ_FLAGS,
PARTS,
STATES,
LValidator,
join_enums,
)
from .helpers import add_lv_use
from .lvcode import ConstantLiteral, add_line_marks, lv, lv_add, lv_assign, lv_obj
from .schemas import ALL_STYLES
from .types import WIDGET_TYPES, LvCompound, lv_obj_t
EVENT_LAMB = "event_lamb__"
class WidgetType:
"""
Describes a type of Widget, e.g. "bar" or "line"
"""
def __init__(self, name, schema=None, modify_schema=None):
"""
:param name: The widget name, e.g. "bar"
:param schema: The config schema for defining a widget
:param modify_schema: A schema to update the widget
"""
self.name = name
self.schema = schema or {}
if modify_schema is None:
self.modify_schema = schema
else:
self.modify_schema = modify_schema
@property
def animated(self):
return False
@property
def w_type(self):
"""
Get the type associated with this widget
:return:
"""
return lv_obj_t
def is_compound(self):
return self.w_type.inherits_from(LvCompound)
async def to_code(self, w, config: dict):
"""
Generate code for a given widget
:param w: The widget
:param config: Its configuration
:return: Generated code as a list of text lines
"""
raise NotImplementedError(f"No to_code defined for {self.name}")
def obj_creator(self, parent: MockObjClass, config: dict):
"""
Create an instance of the widget type
:param parent: The parent to which it should be attached
:param config: Its configuration
:return: Generated code as a single text line
"""
return f"lv_{self.name}_create({parent})"
def get_uses(self):
"""
Get a list of other widgets used by this one
:return:
"""
return ()
class LvScrActType(WidgetType):
"""
A "widget" representing the active screen.
"""
def __init__(self):
super().__init__("lv_scr_act()")
def obj_creator(self, parent: MockObjClass, config: dict):
return []
async def to_code(self, w, config: dict):
return []
class Widget:
"""
Represents a Widget.
"""
widgets_completed = False
@staticmethod
def set_completed():
Widget.widgets_completed = True
def __init__(self, var, wtype: WidgetType, config: dict = None, parent=None):
self.var = var
self.type = wtype
self.config = config
self.scale = 1.0
self.step = 1.0
self.range_from = -sys.maxsize
self.range_to = sys.maxsize
self.parent = parent
@staticmethod
def create(name, var, wtype: WidgetType, config: dict = None, parent=None):
w = Widget(var, wtype, config, parent)
if name is not None:
widget_map[name] = w
return w
@property
def obj(self):
if self.type.is_compound():
return f"{self.var}->obj"
return self.var
def add_state(self, *args):
return lv_obj.add_state(self.obj, *args)
def clear_state(self, *args):
return lv_obj.clear_state(self.obj, *args)
def add_flag(self, *args):
return lv_obj.add_flag(self.obj, *args)
def clear_flag(self, *args):
return lv_obj.clear_flag(self.obj, *args)
def set_property(self, prop, value, animated: bool = None, ltype=None):
if isinstance(value, dict):
value = value.get(prop)
if value is None:
return
if isinstance(value, TimePeriod):
value = value.total_milliseconds
ltype = ltype or self.__type_base()
if animated is None or self.type.animated is not True:
lv.call(f"{ltype}_set_{prop}", self.obj, value)
else:
lv.call(
f"{ltype}_set_{prop}",
self.obj,
value,
"LV_ANIM_ON" if animated else "LV_ANIM_OFF",
)
def get_property(self, prop, ltype=None):
ltype = ltype or self.__type_base()
return f"lv_{ltype}_get_{prop}({self.obj})"
def set_style(self, prop, value, state):
if value is None:
return []
return lv.call(f"obj_set_style_{prop}", self.obj, value, state)
def __type_base(self):
wtype = self.type.w_type
base = str(wtype)
if base.startswith("Lv"):
return f"{wtype}".removeprefix("Lv").removesuffix("Type").lower()
return f"{wtype}".removeprefix("lv_").removesuffix("_t")
def __str__(self):
return f"({self.var}, {self.type})"
# Map of widgets to their config, used for trigger generation
widget_map: dict[Any, Widget] = {}
def get_widget_generator(wid):
"""
Used to wait for a widget during code generation.
:param wid:
:return:
"""
while True:
if obj := widget_map.get(wid):
return obj
if Widget.widgets_completed:
raise Invalid(
f"Widget {wid} not found, yet all widgets should be defined by now"
)
yield
async def get_widget(wid: ID) -> Widget:
if obj := widget_map.get(wid):
return obj
return await FakeAwaitable(get_widget_generator(wid))
def collect_props(config):
"""
Collect all properties from a configuration
:param config:
:return:
"""
props = {}
for prop in [*ALL_STYLES, *OBJ_FLAGS, CONF_GROUP]:
if prop in config:
props[prop] = config[prop]
return props
def collect_states(config):
"""
Collect prperties for each state of a widget
:param config:
:return:
"""
states = {CONF_DEFAULT: collect_props(config)}
for state in STATES:
if state in config:
states[state] = collect_props(config[state])
return states
def collect_parts(config):
"""
Collect properties and states for all widget parts
:param config:
:return:
"""
parts = {CONF_MAIN: collect_states(config)}
for part in PARTS:
if part in config:
parts[part] = collect_states(config[part])
return parts
async def set_obj_properties(w: Widget, config):
"""Generate a list of C++ statements to apply properties to an lv_obj_t"""
parts = collect_parts(config)
for part, states in parts.items():
for state, props in states.items():
lv_state = ConstantLiteral(
f"(int)LV_STATE_{state.upper()}|(int)LV_PART_{part.upper()}"
)
for prop, value in {
k: v for k, v in props.items() if k in ALL_STYLES
}.items():
if isinstance(ALL_STYLES[prop], LValidator):
value = await ALL_STYLES[prop].process(value)
w.set_style(prop, value, lv_state)
flag_clr = set()
flag_set = set()
props = parts[CONF_MAIN][CONF_DEFAULT]
for prop, value in {k: v for k, v in props.items() if k in OBJ_FLAGS}.items():
if value:
flag_set.add(prop)
else:
flag_clr.add(prop)
if flag_set:
adds = join_enums(flag_set, "LV_OBJ_FLAG_")
w.add_flag(adds)
if flag_clr:
clrs = join_enums(flag_clr, "LV_OBJ_FLAG_")
w.clear_flag(clrs)
if states := config.get(CONF_STATE):
adds = set()
clears = set()
lambs = {}
for key, value in states.items():
if isinstance(value, cv.Lambda):
lambs[key] = value
elif value == "true":
adds.add(key)
else:
clears.add(key)
if adds:
adds = ConstantLiteral(join_enums(adds, "LV_STATE_"))
w.add_state(adds)
if clears:
clears = ConstantLiteral(join_enums(clears, "LV_STATE_"))
w.clear_state(clears)
for key, value in lambs.items():
lamb = await cg.process_lambda(value, [], return_type=cg.bool_)
state = ConstantLiteral(f"LV_STATE_{key.upper}")
lv.cond_if(lamb)
w.add_state(state)
lv.cond_else()
w.clear_state(state)
lv.cond_endif()
if scrollbar_mode := config.get(CONF_SCROLLBAR_MODE):
lv_obj.set_scrollbar_mode(w.obj, scrollbar_mode)
async def add_widgets(parent: Widget, config: dict):
"""
Add all widgets to an object
:param parent: The enclosing obj
:param config: The configuration
:return:
"""
for w in config.get(CONF_WIDGETS) or ():
w_type, w_cnfig = next(iter(w.items()))
await widget_to_code(w_cnfig, w_type, parent.obj)
async def widget_to_code(w_cnfig, w_type, parent):
"""
Converts a Widget definition to C code.
:param w_cnfig: The widget configuration
:param w_type: The Widget type
:param parent: The parent to which the widget should be added
:return:
"""
spec: WidgetType = WIDGET_TYPES[w_type]
creator = spec.obj_creator(parent, w_cnfig)
add_lv_use(spec.name)
add_lv_use(*spec.get_uses())
wid = w_cnfig[CONF_ID]
add_line_marks(wid)
if spec.is_compound():
var = cg.new_Pvariable(wid)
lv_add(var.set_obj(creator))
else:
var = cg.Pvariable(wid, cg.nullptr, type_=lv_obj_t)
lv_assign(var, creator)
widget = Widget.create(wid, var, spec, w_cnfig, parent)
await set_obj_properties(widget, w_cnfig)
await add_widgets(widget, w_cnfig)
await spec.to_code(widget, w_cnfig)

View file

@ -38,6 +38,9 @@
#define USE_LIGHT
#define USE_LOCK
#define USE_LOGGER
#define USE_LVGL
#define USE_LVGL_FONT
#define USE_LVGL_IMAGE
#define USE_MDNS
#define USE_MEDIA_PLAYER
#define USE_MQTT

View file

@ -42,6 +42,7 @@ lib_deps =
pavlodn/HaierProtocol@0.9.31 ; haier
; This is using the repository until a new release is published to PlatformIO
https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library
lvgl/lvgl@8.4.0 ; lvgl
build_flags =
-DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE
src_filter =

View file

View file

@ -0,0 +1,24 @@
color:
- id: light_blue
hex: "3340FF"
lvgl:
bg_color: light_blue
widgets:
- label:
text: Hello world
text_color: 0xFF8000
align: center
text_font: montserrat_40
border_post: true
- label:
text: "Hello shiny day"
text_color: 0xFFFFFF
align: bottom_mid
text_font: space16
font:
- file: "gfonts://Roboto"
id: space16
bpp: 4

View file

@ -0,0 +1,30 @@
spi:
clk_pin: 14
mosi_pin: 13
i2c:
sda: GPIO18
scl: GPIO19
display:
- platform: ili9xxx
model: st7789v
id: tft_display
dimensions:
width: 240
height: 320
transform:
swap_xy: false
mirror_x: true
mirror_y: true
data_rate: 80MHz
cs_pin: GPIO22
dc_pin: GPIO21
auto_clear_enabled: false
invert_colors: false
update_interval: never
packages:
lvgl: !include lvgl-package.yaml
<<: !include common.yaml

View file

@ -0,0 +1,52 @@
spi:
clk_pin: 14
mosi_pin: 13
i2c:
sda: GPIO18
scl: GPIO19
display:
- platform: ili9xxx
model: st7789v
id: second_display
dimensions:
width: 240
height: 320
transform:
swap_xy: false
mirror_x: true
mirror_y: true
data_rate: 80MHz
cs_pin: GPIO20
dc_pin: GPIO15
auto_clear_enabled: false
invert_colors: false
update_interval: never
- platform: ili9xxx
model: st7789v
id: tft_display
dimensions:
width: 240
height: 320
transform:
swap_xy: false
mirror_x: true
mirror_y: true
data_rate: 80MHz
cs_pin: GPIO22
dc_pin: GPIO21
auto_clear_enabled: false
invert_colors: false
update_interval: never
packages:
lvgl: !include lvgl-package.yaml
lvgl:
displays:
- tft_display
- second_display
<<: !include common.yaml