mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Merge remote-tracking branch 'origin/dev' into nrf52_core
This commit is contained in:
		
							
								
								
									
										7
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							| @@ -13,6 +13,13 @@ updates: | ||||
|     schedule: | ||||
|       interval: daily | ||||
|     open-pull-requests-limit: 10 | ||||
|     groups: | ||||
|       docker-actions: | ||||
|         applies-to: version-updates | ||||
|         patterns: | ||||
|           - "docker/setup-qemu-action" | ||||
|           - "docker/login-action" | ||||
|           - "docker/setup-buildx-action" | ||||
|   - package-ecosystem: github-actions | ||||
|     directory: "/.github/actions/build-image" | ||||
|     schedule: | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										212
									
								
								esphome/components/lvgl/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								esphome/components/lvgl/__init__.py
									
									
									
									
									
										Normal 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)) | ||||
							
								
								
									
										487
									
								
								esphome/components/lvgl/defines.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										487
									
								
								esphome/components/lvgl/defines.py
									
									
									
									
									
										Normal 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) | ||||
							
								
								
									
										76
									
								
								esphome/components/lvgl/font.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								esphome/components/lvgl/font.cpp
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										70
									
								
								esphome/components/lvgl/helpers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								esphome/components/lvgl/helpers.py
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										34
									
								
								esphome/components/lvgl/label.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								esphome/components/lvgl/label.py
									
									
									
									
									
										Normal 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() | ||||
							
								
								
									
										170
									
								
								esphome/components/lvgl/lv_validation.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								esphome/components/lvgl/lv_validation.py
									
									
									
									
									
										Normal 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() | ||||
							
								
								
									
										237
									
								
								esphome/components/lvgl/lvcode.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								esphome/components/lvgl/lvcode.py
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										129
									
								
								esphome/components/lvgl/lvgl_esphome.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								esphome/components/lvgl/lvgl_esphome.cpp
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										119
									
								
								esphome/components/lvgl/lvgl_esphome.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								esphome/components/lvgl/lvgl_esphome.h
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										21
									
								
								esphome/components/lvgl/lvgl_hal.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								esphome/components/lvgl/lvgl_hal.h
									
									
									
									
									
										Normal 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); | ||||
							
								
								
									
										22
									
								
								esphome/components/lvgl/obj.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								esphome/components/lvgl/obj.py
									
									
									
									
									
										Normal 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() | ||||
							
								
								
									
										260
									
								
								esphome/components/lvgl/schemas.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										260
									
								
								esphome/components/lvgl/schemas.py
									
									
									
									
									
										Normal 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)) | ||||
							
								
								
									
										64
									
								
								esphome/components/lvgl/types.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								esphome/components/lvgl/types.py
									
									
									
									
									
										Normal 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) | ||||
							
								
								
									
										347
									
								
								esphome/components/lvgl/widget.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										347
									
								
								esphome/components/lvgl/widget.py
									
									
									
									
									
										Normal 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) | ||||
| @@ -72,43 +72,44 @@ void PMWCS3Component::dump_config() { | ||||
|   LOG_SENSOR("  ", "vwc", this->vwc_sensor_); | ||||
| } | ||||
| void PMWCS3Component::read_data_() { | ||||
|   uint8_t data[8]; | ||||
|   float e25, ec, temperature, vwc; | ||||
|  | ||||
|   /////// Super important !!!! first activate reading PMWCS3_REG_READ_START (if not, return always the same values) //// | ||||
|  | ||||
|   if (!this->write_bytes(PMWCS3_REG_READ_START, nullptr, 0)) { | ||||
|     this->status_set_warning(); | ||||
|     ESP_LOGVV(TAG, "Failed to write into REG_READ_START register !!!"); | ||||
|     return; | ||||
|   } | ||||
|   // NOLINT  delay(100); | ||||
|  | ||||
|   if (!this->read_bytes(PMWCS3_REG_GET_DATA, (uint8_t *) &data, 8)) { | ||||
|     ESP_LOGVV(TAG, "Error reading PMWCS3_REG_GET_DATA registers"); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   if (this->e25_sensor_ != nullptr) { | ||||
|     e25 = ((data[1] << 8) | data[0]) / 100.0; | ||||
|     this->e25_sensor_->publish_state(e25); | ||||
|     ESP_LOGVV(TAG, "e25: data[0]=%d, data[1]=%d, result=%f", data[0], data[1], e25); | ||||
|   } | ||||
|   if (this->ec_sensor_ != nullptr) { | ||||
|     ec = ((data[3] << 8) | data[2]) / 10.0; | ||||
|     this->ec_sensor_->publish_state(ec); | ||||
|     ESP_LOGVV(TAG, "ec: data[2]=%d, data[3]=%d, result=%f", data[2], data[3], ec); | ||||
|   } | ||||
|   if (this->temperature_sensor_ != nullptr) { | ||||
|     temperature = ((data[5] << 8) | data[4]) / 100.0; | ||||
|     this->temperature_sensor_->publish_state(temperature); | ||||
|     ESP_LOGVV(TAG, "temp: data[4]=%d, data[5]=%d, result=%f", data[4], data[5], temperature); | ||||
|   } | ||||
|   if (this->vwc_sensor_ != nullptr) { | ||||
|     vwc = ((data[7] << 8) | data[6]) / 10.0; | ||||
|     this->vwc_sensor_->publish_state(vwc); | ||||
|     ESP_LOGVV(TAG, "vwc: data[6]=%d, data[7]=%d, result=%f", data[6], data[7], vwc); | ||||
|   } | ||||
|   // Wait for the sensor to be ready. | ||||
|   // 80ms empirically determined (conservative). | ||||
|   this->set_timeout(80, [this] { | ||||
|     uint8_t data[8]; | ||||
|     float e25, ec, temperature, vwc; | ||||
|     if (!this->read_bytes(PMWCS3_REG_GET_DATA, (uint8_t *) &data, 8)) { | ||||
|       ESP_LOGVV(TAG, "Error reading PMWCS3_REG_GET_DATA registers"); | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
|     if (this->e25_sensor_ != nullptr) { | ||||
|       e25 = ((data[1] << 8) | data[0]) / 100.0; | ||||
|       this->e25_sensor_->publish_state(e25); | ||||
|       ESP_LOGVV(TAG, "e25: data[0]=%d, data[1]=%d, result=%f", data[0], data[1], e25); | ||||
|     } | ||||
|     if (this->ec_sensor_ != nullptr) { | ||||
|       ec = ((data[3] << 8) | data[2]) / 10.0; | ||||
|       this->ec_sensor_->publish_state(ec); | ||||
|       ESP_LOGVV(TAG, "ec: data[2]=%d, data[3]=%d, result=%f", data[2], data[3], ec); | ||||
|     } | ||||
|     if (this->temperature_sensor_ != nullptr) { | ||||
|       temperature = ((data[5] << 8) | data[4]) / 100.0; | ||||
|       this->temperature_sensor_->publish_state(temperature); | ||||
|       ESP_LOGVV(TAG, "temp: data[4]=%d, data[5]=%d, result=%f", data[4], data[5], temperature); | ||||
|     } | ||||
|     if (this->vwc_sensor_ != nullptr) { | ||||
|       vwc = ((data[7] << 8) | data[6]) / 10.0; | ||||
|       this->vwc_sensor_->publish_state(vwc); | ||||
|       ESP_LOGVV(TAG, "vwc: data[6]=%d, data[7]=%d, result=%f", data[6], data[7], vwc); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| }  // namespace pmwcs3 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 = | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| async_timeout==4.0.3; python_version <= "3.10" | ||||
| cryptography==42.0.2 | ||||
| cryptography==43.0.0 | ||||
| voluptuous==0.14.2 | ||||
| PyYAML==6.0.1 | ||||
| paho-mqtt==1.6.1 | ||||
| @@ -13,7 +13,7 @@ platformio==6.1.15  # When updating platformio, also update Dockerfile | ||||
| esptool==4.7.0 | ||||
| click==8.1.7 | ||||
| esphome-dashboard==20240620.0 | ||||
| aioesphomeapi==24.3.0 | ||||
| aioesphomeapi==24.6.2 | ||||
| zeroconf==0.132.2 | ||||
| python-magic==0.4.27 | ||||
| ruamel.yaml==0.18.6 # dashboard_import | ||||
|   | ||||
							
								
								
									
										0
									
								
								tests/components/lvgl/common.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/components/lvgl/common.yaml
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										24
									
								
								tests/components/lvgl/lvgl-package.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								tests/components/lvgl/lvgl-package.yaml
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										30
									
								
								tests/components/lvgl/test.esp32-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								tests/components/lvgl/test.esp32-ard.yaml
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										52
									
								
								tests/components/lvgl/test.esp32-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								tests/components/lvgl/test.esp32-idf.yaml
									
									
									
									
									
										Normal 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 | ||||
		Reference in New Issue
	
	Block a user