Merge remote-tracking branch 'upstream/dev' into integration

This commit is contained in:
J. Nick Koston
2026-03-18 23:22:56 -10:00
59 changed files with 2536 additions and 1215 deletions
+1 -1
View File
@@ -1 +1 @@
8e48e836c6fc196d3da000d46eb09db243b87fe33518a74e49c8e009d756074a
44c877ff43765562ac8298902bf2208799643b77facf09c1c0c3c8c4e17187eb
+1
View File
@@ -552,6 +552,7 @@ async def to_code(config):
"""
# get the codepoints from glyphsets and flatten to a set of chrs.
cg.add_define("USE_FONT")
point_set: set[str] = {
chr(x)
for x in flatten(
+80 -5
View File
@@ -9,13 +9,87 @@ namespace font {
static const char *const TAG = "font";
#ifdef USE_LVGL_FONT
const uint8_t *Font::get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) {
auto *fe = (Font *) font->dsc;
const auto *gd = fe->get_glyph_data_(unicode_letter);
static const uint8_t OPA4_TABLE[16] = {0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255};
static const uint8_t OPA2_TABLE[4] = {0, 85, 170, 255};
const void *Font::get_glyph_bitmap(lv_font_glyph_dsc_t *dsc, lv_draw_buf_t *draw_buf) {
const auto *font = dsc->resolved_font;
auto *const fe = (Font *) font->dsc;
const auto *gd = fe->get_glyph_data_(dsc->gid.index);
if (gd == nullptr) {
return nullptr;
}
return gd->data;
const uint8_t *bitmap_in = gd->data;
uint8_t *bitmap_out_tmp = draw_buf->data;
int32_t i = 0;
int32_t x, y;
uint32_t stride = lv_draw_buf_width_to_stride(gd->width, LV_COLOR_FORMAT_A8);
switch (fe->get_bpp()) {
case 1: {
uint8_t mask = 0;
uint8_t byte = 0;
for (y = 0; y != gd->height; y++) {
for (x = 0; x != gd->width; x++) {
if (mask == 0) {
mask = 0x80;
byte = *bitmap_in++;
}
bitmap_out_tmp[x] = byte & mask ? 255 : 0;
mask >>= 1;
}
bitmap_out_tmp += stride;
}
} break;
case 2:
for (y = 0; y != gd->height; y++) {
for (x = 0; x != gd->width; x++, i++) {
switch (i & 0x3) {
default:
bitmap_out_tmp[x] = OPA2_TABLE[(*bitmap_in) >> 6];
break;
case 1:
bitmap_out_tmp[x] = OPA2_TABLE[((*bitmap_in) >> 4) & 0x3];
break;
case 2:
bitmap_out_tmp[x] = OPA2_TABLE[((*bitmap_in) >> 2) & 0x3];
break;
case 3:
bitmap_out_tmp[x] = OPA2_TABLE[((*bitmap_in) >> 0) & 0x3];
bitmap_in++;
}
}
bitmap_out_tmp += stride;
}
break;
case 4:
for (y = 0; y != gd->height; y++) {
for (x = 0; x != gd->width; x++, i++) {
i = i & 0x1;
if (i == 0) {
bitmap_out_tmp[x] = OPA4_TABLE[(*bitmap_in) >> 4];
} else if (i == 1) {
bitmap_out_tmp[x] = OPA4_TABLE[(*bitmap_in) & 0xF];
bitmap_in++;
}
}
bitmap_out_tmp += stride;
}
break;
case 8:
memcpy(bitmap_out_tmp, bitmap_in, gd->width * gd->height);
break;
default:
ESP_LOGD(TAG, "Unknown bpp: %d", fe->get_bpp());
break;
}
return draw_buf;
}
bool Font::get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) {
@@ -30,7 +104,8 @@ bool Font::get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uin
dsc->box_w = gd->width;
dsc->box_h = gd->height;
dsc->is_placeholder = 0;
dsc->bpp = fe->get_bpp();
dsc->format = (lv_font_glyph_format_t) fe->get_bpp();
dsc->gid.index = unicode_letter;
return true;
}
+1 -1
View File
@@ -90,7 +90,7 @@ class Font
uint8_t bpp_; // bits per pixel
#ifdef USE_LVGL_FONT
lv_font_t lv_font_{};
static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter);
static const void *get_glyph_bitmap(lv_font_glyph_dsc_t *dsc, lv_draw_buf_t *draw_buf);
static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next);
const Glyph *get_glyph_data_(uint32_t unicode_letter);
uint32_t last_letter_{};
+61 -33
View File
@@ -28,6 +28,7 @@ from esphome.const import (
CONF_URL,
)
from esphome.core import CORE, HexInt
from esphome.final_validate import full_config
_LOGGER = logging.getLogger(__name__)
@@ -84,7 +85,7 @@ class ImageEncoder:
def __init__(self, width, height, transparency, dither, invert_alpha):
"""
:param width: The image width in pixels
:param width: The image width in pixels (or bytes)
:param height: The image height in pixels
:param transparency: Transparency type
:param dither: Dither method
@@ -93,11 +94,12 @@ class ImageEncoder:
self.transparency = transparency
self.width = width
self.height = height
self.data = [0 for _ in range(width * height)]
self.data = [0] * width * height
self.dither = dither
self.index = 0
self.invert_alpha = invert_alpha
self.path = ""
self.big_endian = False
def convert(self, image, path):
"""
@@ -119,12 +121,21 @@ class ImageEncoder:
:return:
"""
def end_image(self):
"""
Called at the end of the image.
:return:
"""
def set_big_endian(self, big_endian: bool) -> None:
self.big_endian = big_endian
@classmethod
def is_endian(cls) -> bool:
"""
Check if the image encoder supports endianness configuration
"""
return getattr(cls, "set_big_endian", None) is not None
return False
@classmethod
def get_options(cls) -> list[str]:
@@ -212,18 +223,21 @@ class ImageGrayscale(ImageEncoder):
class ImageRGB565(ImageEncoder):
def __init__(self, width, height, transparency, dither, invert_alpha):
stride = 3 if transparency == CONF_ALPHA_CHANNEL else 2
super().__init__(
width * stride,
width * 2,
height,
transparency,
dither,
invert_alpha,
)
self.big_endian = True
self.alpha = [0] * width * height
def set_big_endian(self, big_endian: bool) -> None:
self.big_endian = big_endian
@classmethod
def is_endian(cls) -> bool:
"""
Check if the image encoder supports endianness configuration
"""
return True
def convert(self, image, path):
return image.convert("RGBA")
@@ -233,6 +247,9 @@ class ImageRGB565(ImageEncoder):
r = r >> 3
g = g >> 2
b = b >> 3
if self.invert_alpha:
a ^= 0xFF
self.alpha[self.index // 2] = a
if self.transparency == CONF_CHROMA_KEY:
if r == 0 and g == 1 and b == 0:
g = 0
@@ -251,11 +268,10 @@ class ImageRGB565(ImageEncoder):
self.index += 1
self.data[self.index] = rgb >> 8
self.index += 1
def end_image(self):
if self.transparency == CONF_ALPHA_CHANNEL:
if self.invert_alpha:
a ^= 0xFF
self.data[self.index] = a
self.index += 1
self.data.extend(self.alpha)
class ImageRGB(ImageEncoder):
@@ -281,11 +297,11 @@ class ImageRGB(ImageEncoder):
r = 0
g = 1
b = 0
self.data[self.index] = r
self.data[self.index] = b
self.index += 1
self.data[self.index] = g
self.index += 1
self.data[self.index] = b
self.data[self.index] = r
self.index += 1
if self.transparency == CONF_ALPHA_CHANNEL:
if self.invert_alpha:
@@ -655,6 +671,24 @@ def _config_schema(value):
CONFIG_SCHEMA = _config_schema
def _final_validate(config):
"""
For LVGL 9 the default byte order for RGB565 images is little-endian
:param config:
:return:
"""
fv = full_config.get()
if "lvgl" in fv and not all(CONF_BYTE_ORDER in x for x in config):
config = config.copy()
for c in config:
if not c.get(CONF_BYTE_ORDER):
c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN"
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def write_image(config, all_frames=False):
path = Path(config[CONF_FILE])
if not path.is_file():
@@ -720,6 +754,7 @@ async def write_image(config, all_frames=False):
for col in range(width):
encoder.encode(pixels[row * width + col])
encoder.end_row()
encoder.end_image()
rhs = [HexInt(x) for x in encoder.data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
@@ -729,31 +764,24 @@ async def write_image(config, all_frames=False):
return prog_arr, width, height, image_type, trans_value, frame_count
async def _image_to_code(entry):
"""
Convert a single image entry to code and return its metadata.
:param entry: The config entry for the image.
:return: An ImageMetaData object
"""
prog_arr, width, height, image_type, trans_value, _ = await write_image(entry)
cg.new_Pvariable(entry[CONF_ID], prog_arr, width, height, image_type, trans_value)
return ImageMetaData(
width,
height,
entry[CONF_TYPE],
entry[CONF_TRANSPARENCY],
def add_metadata(id: str, width: int, height: int, image_type: str, transparency):
all_metadata = CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {})
all_metadata[str(id)] = ImageMetaData(
width=width, height=height, image_type=image_type, transparency=transparency
)
async def to_code(config):
cg.add_define("USE_IMAGE")
# By now the config will be a simple list.
# Use a subkey to allow for other data in the future
CORE.data[DOMAIN] = {
KEY_METADATA: {
entry[CONF_ID].id: await _image_to_code(entry) for entry in config
}
}
for entry in config:
prog_arr, width, height, image_type, trans_value, _ = await write_image(entry)
cg.new_Pvariable(
entry[CONF_ID], prog_arr, width, height, image_type, trans_value
)
add_metadata(
entry[CONF_ID], width, height, entry[CONF_TYPE], entry[CONF_TRANSPARENCY]
)
def get_all_image_metadata() -> dict[str, ImageMetaData]:
+12 -16
View File
@@ -105,22 +105,22 @@ Color Image::get_pixel(int x, int y, const Color color_on, const Color color_off
}
}
#ifdef USE_LVGL
lv_img_dsc_t *Image::get_lv_img_dsc() {
lv_image_dsc_t *Image::get_lv_image_dsc() {
// lazily construct lvgl image_dsc.
if (this->dsc_.data != this->data_start_) {
this->dsc_.data = this->data_start_;
this->dsc_.header.always_zero = 0;
this->dsc_.header.reserved = 0;
this->dsc_.header.reserved_2 = 0;
this->dsc_.header.stride = this->get_width_stride();
this->dsc_.header.w = this->width_;
this->dsc_.header.h = this->height_;
this->dsc_.data_size = this->get_width_stride() * this->get_height();
switch (this->get_type()) {
case IMAGE_TYPE_BINARY:
this->dsc_.header.cf = LV_IMG_CF_ALPHA_1BIT;
this->dsc_.header.cf = LV_COLOR_FORMAT_A1;
break;
case IMAGE_TYPE_GRAYSCALE:
this->dsc_.header.cf = LV_IMG_CF_ALPHA_8BIT;
this->dsc_.header.cf = LV_COLOR_FORMAT_A8;
break;
case IMAGE_TYPE_RGB:
@@ -138,7 +138,7 @@ lv_img_dsc_t *Image::get_lv_img_dsc() {
}
#else
this->dsc_.header.cf =
this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGBA8888 : LV_IMG_CF_RGB888;
this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_COLOR_FORMAT_ARGB8888 : LV_COLOR_FORMAT_RGB888;
#endif
break;
@@ -146,14 +146,10 @@ lv_img_dsc_t *Image::get_lv_img_dsc() {
#if LV_COLOR_DEPTH == 16
switch (this->transparency_) {
case TRANSPARENCY_ALPHA_CHANNEL:
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;
break;
case TRANSPARENCY_CHROMA_KEY:
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED;
this->dsc_.header.cf = LV_COLOR_FORMAT_RGB565A8;
break;
default:
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR;
break;
this->dsc_.header.cf = LV_COLOR_FORMAT_RGB565;
}
#else
this->dsc_.header.cf =
@@ -173,8 +169,8 @@ bool Image::get_binary_pixel_(int x, int y) const {
}
Color Image::get_rgb_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_) * this->bpp_ / 8;
Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1),
progmem_read_byte(this->data_start_ + pos + 2), 0xFF);
Color color = Color(progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 1),
progmem_read_byte(this->data_start_ + pos + 0), 0xFF);
switch (this->transparency_) {
case TRANSPARENCY_CHROMA_KEY:
@@ -200,7 +196,7 @@ Color Image::get_rgb565_pixel_(int x, int y) const {
auto a = 0xFF;
switch (this->transparency_) {
case TRANSPARENCY_ALPHA_CHANNEL:
a = progmem_read_byte(pos + 2);
a = progmem_read_byte(this->data_start_ + this->width_ * this->height_ * 2 + (x + y * this->width_));
break;
case TRANSPARENCY_CHROMA_KEY:
if (rgb565 == 0x0020)
@@ -239,7 +235,7 @@ Image::Image(const uint8_t *data_start, int width, int height, ImageType type, T
this->bpp_ = 8;
break;
case IMAGE_TYPE_RGB565:
this->bpp_ = transparency == TRANSPARENCY_ALPHA_CHANNEL ? 24 : 16;
this->bpp_ = 16;
break;
case IMAGE_TYPE_RGB:
this->bpp_ = this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? 32 : 24;
+1 -1
View File
@@ -41,7 +41,7 @@ class Image : public display::BaseImage {
bool has_transparency() const { return this->transparency_ != TRANSPARENCY_OPAQUE; }
#ifdef USE_LVGL
lv_img_dsc_t *get_lv_img_dsc();
lv_image_dsc_t *get_lv_image_dsc();
#endif
protected:
bool get_binary_pixel_(int x, int y) const;
+120 -48
View File
@@ -1,12 +1,30 @@
import importlib
import logging
from pathlib import Path
import pkgutil
from esphome.automation import build_automation, validate_automation
import esphome.codegen as cg
from esphome.components.const import CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING
from esphome.components.const import (
CONF_BYTE_ORDER,
CONF_COLOR_DEPTH,
CONF_DRAW_ROUNDING,
)
from esphome.components.display import Display
from esphome.components.esp32 import (
VARIANT_ESP32P4,
add_idf_component,
add_idf_sdkconfig_option,
get_esp32_variant,
)
from esphome.components.image import (
CONF_OPAQUE,
IMAGE_TYPE,
ImageBinary,
ImageGrayscale,
ImageRGB,
ImageRGB565,
get_image_metadata,
)
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
import esphome.config_validation as cv
from esphome.const import (
@@ -21,7 +39,6 @@ from esphome.const import (
CONF_PAGES,
CONF_TIMEOUT,
CONF_TRIGGER_ID,
CONF_TYPE,
)
from esphome.core import CORE, ID, Lambda
from esphome.cpp_generator import MockObj
@@ -30,8 +47,7 @@ from esphome.helpers import write_file_if_changed
from esphome.yaml_util import load_yaml
from . import defines as df, helpers, lv_validation as lvalid, widgets
from .automation import disp_update, focused_widgets, refreshed_widgets
from .defines import add_define
from .automation import focused_widgets, layers_to_code, lvgl_update, refreshed_widgets
from .encoders import (
ENCODERS_CONFIG,
encoders_to_code,
@@ -45,12 +61,13 @@ from .lvcode import LvContext, LvglComponent, lvgl_static
from .schemas import (
DISP_BG_SCHEMA,
FULL_STYLE_SCHEMA,
STYLE_REMAP,
WIDGET_TYPES,
any_widget_schema,
container_schema,
obj_schema,
)
from .styles import add_top_layer, styles_to_code, theme_to_code
from .styles import styles_to_code, theme_to_code
from .touchscreens import touchscreen_schema, touchscreens_to_code
from .trigger import add_on_boot_triggers, generate_triggers
from .types import IdleTrigger, PlainTrigger, lv_font_t, lv_group_t, lv_style_t, lvgl_ns
@@ -58,7 +75,7 @@ from .widgets import (
LvScrActType,
Widget,
add_widgets,
get_scr_act,
get_screen_active,
set_obj_properties,
styles_used,
)
@@ -84,7 +101,6 @@ DOMAIN = "lvgl"
DEPENDENCIES = ["display"]
AUTO_LOAD = ["key_provider"]
CODEOWNERS = ["@clydebarrow"]
LOGGER = logging.getLogger(__name__)
HELLO_WORLD_FILE = "hello_world.yaml"
@@ -102,6 +118,7 @@ def as_macro(macro, value):
return f"#define {macro} {value}"
LVGL_VERSION = "9.5.0"
LV_CONF_FILENAME = "lv_conf.h"
LV_CONF_H_FORMAT = """\
#pragma once
@@ -110,7 +127,17 @@ LV_CONF_H_FORMAT = """\
def generate_lv_conf_h():
definitions = [as_macro(m, v) for m, v in df.get_data(df.KEY_LV_DEFINES).items()]
# Get all possible LV_ config defines based on the widgets used in the config, and the standard LVGL options
all_defines = set(
df.LV_DEFINES + tuple(f"LV_USE_{w.upper()}" for w in WIDGET_TYPES)
)
# Get the defines that are actually used based on the config
lv_defines = df.get_data(df.KEY_LV_DEFINES)
unused_defines = all_defines - set(lv_defines)
# Create the content of lv_conf.h with the used defines set to their value, and the unused defines disabled
definitions = [as_macro(m, v) for m, v in lv_defines.items()] + [
as_macro(m, "0") for m in unused_defines
]
definitions.sort()
return LV_CONF_H_FORMAT.format("\n".join(definitions))
@@ -133,7 +160,7 @@ def multi_conf_validate(configs: list[dict]):
for item in (
CONF_LOG_LEVEL,
CONF_COLOR_DEPTH,
df.CONF_BYTE_ORDER,
CONF_BYTE_ORDER,
df.CONF_TRANSPARENCY_KEY,
):
if base_config[item] != config[item]:
@@ -166,14 +193,7 @@ def final_validation(config_list):
)
buffer_frac = config[CONF_BUFFER_SIZE]
if CORE.is_esp32 and buffer_frac > 0.5 and PSRAM_DOMAIN not in global_config:
LOGGER.warning("buffer_size: may need to be reduced without PSRAM")
for image_id in lv_images_used:
path = global_config.get_path_for_id(image_id)[:-1]
image_conf = global_config.get_config_for_path(path)
if image_conf[CONF_TYPE] in ("RGBA", "RGB24"):
raise cv.Invalid(
"Using RGBA or RGB24 in image config not compatible with LVGL", path
)
df.LOGGER.warning("buffer_size: may need to be reduced without PSRAM")
for w in focused_widgets:
path = global_config.get_path_for_id(w)
widget_conf = global_config.get_config_for_path(path[:-1])
@@ -205,39 +225,48 @@ def final_validation(config_list):
async def to_code(configs):
config_0 = configs[0]
# Global configuration
cg.add_library("lvgl/lvgl", "8.4.0")
if CORE.is_esp32:
if get_esp32_variant() == VARIANT_ESP32P4:
add_idf_sdkconfig_option("CONFIG_LV_DRAW_BUF_ALIGN", 64)
# disable use of PPA for fills until upstream bugs fixed
df.add_define("LV_USE_PPA", "0")
df.add_define("LV_DRAW_BUF_ALIGN", "64")
else:
df.add_define("LV_DRAW_BUF_ALIGN", "32")
add_idf_component(name="lvgl/lvgl", ref=LVGL_VERSION)
else:
df.add_define("LV_DRAW_BUF_ALIGN", "1")
cg.add_library("lvgl/lvgl", LVGL_VERSION)
df.add_define("LV_DRAW_BUF_STRIDE_ALIGN", "1")
df.add_define("LV_USE_DRAW_SW", "1")
df.add_define("LV_USE_STDLIB_SPRINTF", "LV_STDLIB_CLIB")
df.add_define("LV_USE_STDLIB_STRING", "LV_STDLIB_CLIB")
df.add_define("LV_USE_STDLIB_MALLOC", "LV_STDLIB_CUSTOM")
cg.add_define("USE_LVGL")
# suppress default enabling of extra widgets
add_define("_LV_KCONFIG_PRESENT")
# cg.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"')
df.add_define("LV_DRAW_SW_COMPLEX", "1")
add_define(
df.add_define(
"LV_LOG_LEVEL",
f"LV_LOG_LEVEL_{df.LV_LOG_LEVELS[config_0[CONF_LOG_LEVEL]]}",
)
df.add_define("LV_USE_LOG", "1")
cg.add_define(
"LVGL_LOG_LEVEL",
cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[CONF_LOG_LEVEL]}"),
)
add_define("LV_COLOR_DEPTH", config_0[CONF_COLOR_DEPTH])
df.add_define("LV_COLOR_DEPTH", config_0[CONF_COLOR_DEPTH])
for font in helpers.lv_fonts_used:
add_define(f"LV_FONT_{font.upper()}")
df.add_define(f"LV_FONT_{font.upper()}")
if config_0[CONF_COLOR_DEPTH] == 16:
add_define(
df.add_define(
"LV_COLOR_16_SWAP",
"1" if config_0[df.CONF_BYTE_ORDER] == "big_endian" else "0",
"1" if config_0[CONF_BYTE_ORDER] == "big_endian" else "0",
)
add_define(
df.add_define(
"LV_COLOR_CHROMA_KEY",
await lvalid.lv_color.process(config_0[df.CONF_TRANSPARENCY_KEY]),
)
@@ -248,7 +277,7 @@ async def to_code(configs):
await cg.get_variable(font)
default_font = config_0[df.CONF_DEFAULT_FONT]
if not lvalid.is_lv_font(default_font):
add_define(
df.add_define(
"LV_FONT_CUSTOM_DECLARE", f"LV_FONT_DECLARE(*{df.DEFAULT_ESPHOME_FONT})"
)
globfont_id = ID(
@@ -262,9 +291,9 @@ async def to_code(configs):
MockObj(await lvalid.lv_font.process(default_font), "->").get_lv_font(),
static=False,
)
add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT)
df.add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT)
else:
add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font))
df.add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font))
cg.add(lvgl_static.esphome_lvgl_init())
default_group = get_default_group(config_0)
@@ -293,8 +322,9 @@ async def to_code(configs):
await cg.register_component(lv_component, config)
Widget.create(config[CONF_ID], lv_component, LvScrActType(), config)
lv_scr_act = get_scr_act(lv_component)
lv_scr_act = get_screen_active(lv_component)
async with LvContext():
cg.add(lv_component.set_big_endian(config[CONF_BYTE_ORDER] == "big_endian"))
await touchscreens_to_code(lv_component, config)
await encoders_to_code(lv_component, config, default_group)
await keypads_to_code(lv_component, config, default_group)
@@ -304,9 +334,10 @@ async def to_code(configs):
await set_obj_properties(lv_scr_act, config)
await add_widgets(lv_scr_act, config)
await add_pages(lv_component, config)
await add_top_layer(lv_component, config)
await layers_to_code(lv_component, config)
await lvgl_update(lv_component, config)
await msgboxes_to_code(lv_component, config)
await disp_update(lv_component.get_disp(), config)
# await disp_update(lv_component.get_disp(), config)
# Set this directly since we are limited in how many methods can be added to the Widget class.
Widget.widgets_completed = True
async with LvContext():
@@ -336,15 +367,53 @@ async def to_code(configs):
# This must be done after all widgets are created
for comp in helpers.lvgl_components_required:
cg.add_define(f"USE_LVGL_{comp.upper()}")
if {"transform_angle", "transform_zoom"} & styles_used:
add_define("LV_COLOR_SCREEN_TRANSP", "1")
lv_image_formats = df.get_color_formats().copy()
if {
"transform_rotation",
"transform_scale",
"transform_scale_x",
"transform_scale_y",
} & styles_used:
df.add_define("LV_COLOR_SCREEN_TRANSP", "1")
lv_image_formats.add("ARGB8888")
lv_image_formats.add(
"RGB565"
) # Currently always need RGB565 for the display buffer
for use in helpers.lv_uses:
add_define(f"LV_USE_{use.upper()}")
df.add_define(f"LV_USE_{use.upper()}")
cg.add_define(f"USE_LVGL_{use.upper()}")
for image_id in lv_images_used:
await cg.get_variable(image_id)
metadata = get_image_metadata(image_id.id)
image_type = IMAGE_TYPE[metadata.image_type]
transparent = metadata.transparency != CONF_OPAQUE
if transparent:
# Internal draw layer will use ARGB8888
lv_image_formats.add("ARGB8888")
if image_type == ImageBinary:
lv_image_formats.add("I1")
if image_type == ImageGrayscale:
lv_image_formats.add("A8")
if image_type == ImageRGB565:
lv_image_formats.add("RGB565A8" if transparent else "RGB565")
if image_type == ImageRGB:
lv_image_formats.add("ARGB8888" if transparent else "RGB8888")
if df.is_defined("LV_GRADIENT_MAX_STOPS"):
lv_image_formats.add("RGB888")
for fmt in lv_image_formats:
df.add_define(f"LV_DRAW_SW_SUPPORT_{fmt}", "1")
lv_conf_h_file = CORE.relative_src_path(LV_CONF_FILENAME)
write_file_if_changed(lv_conf_h_file, generate_lv_conf_h())
cg.add_build_flag("-DLV_CONF_H=1")
cg.add_build_flag(f'-DLV_CONF_PATH="{LV_CONF_FILENAME}"')
cg.add_build_flag(f'-DLV_CONF_PATH=\\"{LV_CONF_FILENAME}\\"')
for prop in df.get_remapped_uses():
df.LOGGER.warning(
"Property '%s' is deprecated, use '%s' instead", prop, STYLE_REMAP[prop]
)
for warning in df.get_warnings():
df.LOGGER.warning(warning)
def display_schema(config):
@@ -357,7 +426,9 @@ def display_schema(config):
def add_hello_world(config):
if df.CONF_WIDGETS not in config and CONF_PAGES not in config:
LOGGER.info("No pages or widgets configured, creating default hello_world page")
df.LOGGER.info(
"No pages or widgets configured, creating default hello_world page"
)
hello_world_path = Path(__file__).parent / HELLO_WORLD_FILE
config[df.CONF_WIDGETS] = any_widget_schema()(load_yaml(hello_world_path))
return config
@@ -395,8 +466,8 @@ LVGL_SCHEMA = cv.All(
cv.Optional(CONF_LOG_LEVEL, default="WARN"): cv.one_of(
*df.LV_LOG_LEVELS, upper=True
),
cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of(
"big_endian", "little_endian"
cv.Optional(CONF_BYTE_ORDER, default="big_endian"): cv.one_of(
"big_endian", "little_endian", lower=True
),
cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list(
cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)}).extend(
@@ -424,6 +495,7 @@ LVGL_SCHEMA = cv.All(
cv.Optional(df.CONF_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA),
cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool,
cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec),
cv.Optional(df.CONF_BOTTOM_LAYER): container_schema(obj_spec),
cv.Optional(
df.CONF_TRANSPARENCY_KEY, default=0x000400
): lvalid.lv_color,
+64 -33
View File
@@ -10,18 +10,21 @@ from esphome.cpp_generator import TemplateArguments, get_variable
from esphome.cpp_types import nullptr
from .defines import (
CONF_DISP_BG_COLOR,
CONF_DISP_BG_IMAGE,
CONF_DISP_BG_OPA,
CONF_BG_OPA,
CONF_BOTTOM_LAYER,
CONF_EDITING,
CONF_FREEZE,
CONF_LVGL_ID,
CONF_MAIN,
CONF_OBJ,
CONF_SCROLLBAR,
CONF_SHOW_SNOW,
CONF_TOP_LAYER,
PARTS,
literal,
static_cast,
StaticCastExpression,
add_warning,
)
from .lv_validation import lv_bool, lv_color, lv_image, lv_milliseconds, opacity
from .lv_validation import lv_bool, lv_milliseconds
from .lvcode import (
LVGL_COMP_ARG,
UPDATE_EVENT,
@@ -42,13 +45,13 @@ from .schemas import (
LIST_ACTION_SCHEMA,
LVGL_SCHEMA,
base_update_schema,
part_schema,
)
from .types import (
LV_STATE,
LvglAction,
LvglCondition,
ObjUpdateAction,
lv_disp_t,
lv_group_t,
lv_obj_base_t,
lv_obj_t,
@@ -56,7 +59,9 @@ from .types import (
)
from .widgets import (
Widget,
get_scr_act,
WidgetType,
add_widgets,
get_screen_active,
get_widgets,
set_obj_properties,
wait_for_widgets,
@@ -67,6 +72,41 @@ focused_widgets = set()
refreshed_widgets = set()
async def layers_to_code(lv_component, config):
if top_conf := config.get(CONF_TOP_LAYER):
top_layer = lv_expr.display_get_layer_top(lv_component.get_disp())
with LocalVariable("top_layer", lv_obj_t, top_layer) as top_layer_obj:
top_w = Widget(top_layer_obj, layer_spec, top_conf)
await set_obj_properties(top_w, top_conf)
await add_widgets(top_w, top_conf)
if bottom_conf := config.get(CONF_BOTTOM_LAYER):
bottom_layer = lv_expr.display_get_layer_bottom(lv_component.get_disp())
with LocalVariable("bottom_layer", lv_obj_t, bottom_layer) as bottom_layer_obj:
bottom_w = Widget(bottom_layer_obj, layer_spec, bottom_conf)
await set_obj_properties(bottom_w, bottom_conf)
await add_widgets(bottom_w, bottom_conf)
async def lvgl_update(lv_component, config):
bottom = {k.removeprefix("disp_"): v for k, v in config.items() if k in DISP_PROPS}
if not bottom:
return
plural = len(bottom) != 1
add_warning(
"The propert"
+ ("ies " if plural else "y ")
+ "'"
+ "','".join(k for k in config if k in DISP_PROPS)
+ "'"
+ (" are " if plural else " is ")
+ "deprecated, use 'bottom_layer' instead."
)
# Preserve default opacity from 8.x
if CONF_BG_OPA not in bottom:
bottom[CONF_BG_OPA] = 1.0
await layers_to_code(lv_component, {CONF_BOTTOM_LAYER: bottom})
async def action_to_code(
widgets: list[Widget],
action: Callable[[Widget], Any],
@@ -151,25 +191,6 @@ async def lvgl_is_idle(config, condition_id, template_arg, args):
return var
async def disp_update(disp, config: dict):
if (
CONF_DISP_BG_COLOR not in config
and CONF_DISP_BG_IMAGE not in config
and CONF_DISP_BG_OPA not in config
):
return
with LocalVariable("lv_disp_tmp", lv_disp_t, disp) as disp_temp:
if (bg_color := config.get(CONF_DISP_BG_COLOR)) is not None:
lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color))
if bg_image := config.get(CONF_DISP_BG_IMAGE):
if bg_image == "none":
lv.disp_set_bg_image(disp_temp, static_cast("void *", "nullptr"))
else:
lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image))
if (bg_opa := config.get(CONF_DISP_BG_OPA)) is not None:
lv.disp_set_bg_opa(disp_temp, await opacity.process(bg_opa))
@automation.register_action(
"lvgl.widget.redraw",
ObjUpdateAction,
@@ -187,7 +208,7 @@ async def disp_update(disp, config: dict):
async def obj_invalidate_to_code(config, action_id, template_arg, args):
if CONF_LVGL_ID in config:
lv_comp = await cg.get_variable(config[CONF_LVGL_ID])
widgets = [get_scr_act(lv_comp)]
widgets = [get_screen_active(lv_comp)]
else:
widgets = await get_widgets(config)
@@ -197,20 +218,30 @@ async def obj_invalidate_to_code(config, action_id, template_arg, args):
return await action_to_code(widgets, do_invalidate, action_id, template_arg, args)
layer_spec = WidgetType(CONF_OBJ, lv_obj_t, (CONF_MAIN, CONF_SCROLLBAR), is_mock=True)
DISP_PROPS = {str(x) for x in DISP_BG_SCHEMA.schema}
@automation.register_action(
"lvgl.update",
LvglAction,
DISP_BG_SCHEMA.extend(LVGL_SCHEMA).add_extra(
cv.has_at_least_one_key(CONF_DISP_BG_COLOR, CONF_DISP_BG_IMAGE)
part_schema(layer_spec.parts)
.extend(LVGL_SCHEMA)
.extend(DISP_BG_SCHEMA)
.extend(
{
cv.Optional(CONF_TOP_LAYER): part_schema(layer_spec.parts),
cv.Optional(CONF_BOTTOM_LAYER): part_schema(layer_spec.parts),
}
),
synchronous=True,
)
async def lvgl_update_to_code(config, action_id, template_arg, args):
widgets = await get_widgets(config, CONF_LVGL_ID)
w = widgets[0]
disp = literal(f"{w.obj}->get_disp()")
async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context:
await disp_update(disp, config)
await lvgl_update(w.var, config)
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
await cg.register_parented(var, w.var)
return var
@@ -336,7 +367,7 @@ async def widget_focus(config, action_id, template_arg, args):
widget = await get_widgets(config)
if widget:
widget = widget[0]
group = static_cast(
group = StaticCastExpression(
lv_group_t.operator("ptr"), lv_expr.obj_get_group(widget.obj)
)
elif group := config.get(CONF_GROUP):
+155 -33
View File
@@ -10,8 +10,12 @@ from typing import Any
from esphome import codegen as cg, config_validation as cv
from esphome.const import CONF_ITEMS
from esphome.core import CORE, ID, Lambda
from esphome.cpp_generator import LambdaExpression, MockObj
from esphome.cpp_types import uint32
from esphome.cpp_generator import (
CallExpression,
LambdaExpression,
MockObj,
MockObjClass,
)
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import Expression, SafeExpType
@@ -21,8 +25,11 @@ LOGGER = logging.getLogger(__name__)
lvgl_ns = cg.esphome_ns.namespace("lvgl")
DOMAIN = "lvgl"
KEY_COLOR_FORMATS = "color_formats"
KEY_LV_DEFINES = "lv_defines"
KEY_REMAPPED_USES = "remapped_uses"
KEY_UPDATED_WIDGETS = "updated_widgets"
KEY_WARNINGS = "warnings"
def get_data(key, default=None):
@@ -33,10 +40,37 @@ def get_data(key, default=None):
:return:
"""
return CORE.data.setdefault(DOMAIN, {}).setdefault(
key, default if default is not None else {}
key, {} if default is None else default
)
def get_warnings():
return get_data(KEY_WARNINGS, set())
def get_remapped_uses():
return get_data(KEY_REMAPPED_USES, set())
def get_color_formats():
return get_data(KEY_COLOR_FORMATS, set())
def add_warning(msg: str):
get_warnings().add(msg)
class StaticCastExpression(Expression):
__slots__ = ("type", "exp")
def __init__(self, type: Any, exp: SafeExpType):
self.type = str(type)
self.exp = cg.safe_exp(exp)
def __str__(self):
return f"static_cast<{self.type}>({self.exp})"
def add_define(macro, value="1"):
lv_defines = get_data(KEY_LV_DEFINES)
value = str(value)
@@ -47,27 +81,43 @@ def add_define(macro, value="1"):
lv_defines[macro] = value
def is_defined(macro):
return macro in get_data(KEY_LV_DEFINES)
def literal(arg) -> MockObj:
if isinstance(arg, str):
return MockObj(arg)
return arg
def static_cast(type, value):
return literal(f"static_cast<{type}>({value})")
def addr(arg) -> MockObj:
return MockObj(f"&{arg}")
def call_lambda(lamb: LambdaExpression):
"""
Given a lambda, either reduce to a simple expression or call it, possibly with parameters
from the surrounding context
:param lamb:
:return:
"""
expr = lamb.content.strip()
if expr.startswith("return") and expr.endswith(";"):
return expr[6:-1].strip()
# If lambda has parameters, call it with those parameter names
# Convert a lambda returning a simple expression to just that expression
expr = cg.RawExpression(expr[6:-1].strip())
# Don't cast if the return type is a class
if isinstance(lamb.return_type, MockObjClass):
return expr
return StaticCastExpression(lamb.return_type, expr)
# If lambda has parameters, call it with their names
# Parameter names come from hardcoded component code (like "x", "it", "event")
# not from user input, so they're safe to use directly
if lamb.parameters and lamb.parameters.parameters:
param_names = ", ".join(str(param.id) for param in lamb.parameters.parameters)
return f"{lamb}({param_names})"
return f"{lamb}()"
return CallExpression(
lamb, *[MockObj(x.id) for x in lamb.parameters.parameters]
)
return CallExpression(lamb)
class LValidator:
@@ -76,7 +126,7 @@ class LValidator:
has `process()` to convert a value during code generation
"""
def __init__(self, validator, rtype, retmapper=None, requires=None):
def __init__(self, validator, rtype: MockObj, retmapper=None, requires=None):
self.validator = validator
self.rtype = rtype
self.retmapper = retmapper
@@ -99,10 +149,9 @@ class LValidator:
from .lvcode import get_lambda_context_args
args = args or get_lambda_context_args()
return cg.RawExpression(
call_lambda(
await cg.process_lambda(value, args, return_type=self.rtype)
)
return call_lambda(
await cg.process_lambda(value, args, return_type=self.rtype)
)
if self.retmapper is not None:
return self.retmapper(value)
@@ -112,6 +161,8 @@ class LValidator:
value = [
await cg.get_variable(x) if isinstance(x, ID) else x for x in value
]
if self.rtype is cg.int_:
value = int(value)
return cg.safe_exp(value)
@@ -122,10 +173,11 @@ class LvConstant(LValidator):
The property `one_of` has the single case validator, and `several_of` allows a list of constants.
"""
def __init__(self, prefix: str, *choices):
def __init__(self, prefix: str, *choices, typename=None):
self.prefix = prefix
self.choices = choices
prefixed_choices = [prefix + v for v in choices]
self.choices = tuple(x.upper() for x in choices)
self.typename = typename or prefix.lower() + "t"
prefixed_choices = [prefix + v.upper() for v in choices]
prefixed_validator = cv.one_of(*prefixed_choices, upper=True)
@schema_extractor("one_of")
@@ -136,24 +188,30 @@ class LvConstant(LValidator):
return prefixed_validator(value)
return self.prefix + cv.one_of(*choices, upper=True)(value)
super().__init__(validator, rtype=uint32)
super().__init__(validator, rtype=cg.uint32)
self.retmapper = self.mapper
self.one_of = LValidator(validator, uint32, retmapper=self.mapper)
self.one_of = LValidator(validator, cg.uint32, retmapper=self.mapper)
self.several_of = LValidator(
cv.ensure_list(self.one_of), uint32, retmapper=self.mapper
cv.ensure_list(self.one_of), cg.uint32, retmapper=self.mapper
)
def mapper(self, value):
if not isinstance(value, list):
value = [value]
return literal(
"|".join(
[
str(v) if str(v).startswith(self.prefix) else self.prefix + str(v)
for v in value
]
).upper()
)
value = [
(
str(v).upper()
if str(v).startswith(self.prefix)
else self.prefix + str(v).upper()
)
for v in value
]
if len(value) == 1:
return literal(value[0])
value = literal("|".join(value))
if self.typename is None:
return value
return StaticCastExpression(self.typename, value)
def extend(self, *choices):
"""
@@ -161,7 +219,14 @@ class LvConstant(LValidator):
:param choices: The extra choices
:return: A new LVConstant instance
"""
return LvConstant(self.prefix, *(self.choices + choices))
return LvConstant(
self.prefix, *(self.choices + choices), typename=self.typename
)
def __getattr__(self, item):
if item.upper() not in self.choices:
raise AttributeError(f"{item} not one of {self.choices}")
return self.mapper(item)
# Parts
@@ -277,11 +342,13 @@ PARTS = (
CONF_KNOB,
CONF_SELECTED,
CONF_ITEMS,
CONF_TICKS,
# CONF_TICKS,
CONF_CURSOR,
CONF_TEXTAREA_PLACEHOLDER,
)
LV_PART = LvConstant("LV_PART_", *(p.upper() for p in PARTS))
KEYBOARD_MODES = LvConstant(
"LV_KEYBOARD_MODE_",
"TEXT_LOWER",
@@ -359,6 +426,7 @@ OBJ_FLAGS = (
"overflow_visible",
"layout_1",
"layout_2",
"send_draw_task_events",
"widget_1",
"widget_2",
"user_1",
@@ -366,12 +434,14 @@ OBJ_FLAGS = (
"user_3",
"user_4",
)
LV_OBJ_FLAG = LvConstant("LV_OBJ_FLAG_", *OBJ_FLAGS)
ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL")
BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE")
SLIDER_MODES = LvConstant("LV_SLIDER_MODE_", "NORMAL", "SYMMETRICAL", "RANGE")
BUTTONMATRIX_CTRLS = LvConstant(
"LV_BTNMATRIX_CTRL_",
"LV_BUTTONMATRIX_CTRL_",
"HIDDEN",
"NO_REPEAT",
"DISABLED",
@@ -434,12 +504,16 @@ 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_ANIMATIONS = "animations"
CONF_ANTIALIAS = "antialias"
CONF_ARC_LENGTH = "arc_length"
CONF_AUTO_START = "auto_start"
CONF_BACKGROUND_STYLE = "background_style"
CONF_BG_OPA = "bg_opa"
CONF_BOTTOM_LAYER = "bottom_layer"
CONF_BUTTON_STYLE = "button_style"
CONF_DECIMAL_PLACES = "decimal_places"
CONF_COLUMN = "column"
@@ -449,9 +523,11 @@ CONF_DISP_BG_IMAGE = "disp_bg_image"
CONF_DISP_BG_OPA = "disp_bg_opa"
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_CONTAINER = "container"
CONF_CONTROL = "control"
CONF_DEFAULT_FONT = "default_font"
@@ -483,8 +559,10 @@ 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_BUTTONS = "header_buttons"
CONF_HEADER_MODE = "header_mode"
CONF_HOME = "home"
CONF_INDICATORS = "indicators"
CONF_INITIAL_FOCUS = "initial_focus"
CONF_SELECTED_DIGIT = "selected_digit"
CONF_KEY_CODE = "key_code"
@@ -496,6 +574,7 @@ 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_TICKS_STYLE = "major_ticks_style"
CONF_MSGBOXES = "msgboxes"
CONF_OBJ = "obj"
CONF_ONE_CHECKED = "one_checked"
@@ -517,12 +596,15 @@ CONF_PIVOT_Y = "pivot_y"
CONF_PLACEHOLDER_TEXT = "placeholder_text"
CONF_POINTS = "points"
CONF_PREVIOUS = "previous"
CONF_RADIUS = "radius"
CONF_REPEAT_COUNT = "repeat_count"
CONF_RECOLOR = "recolor"
CONF_RESUME_ON_INPUT = "resume_on_input"
CONF_RIGHT_BUTTON = "right_button"
CONF_ROLLOVER = "rollover"
CONF_ROOT_BACK_BTN = "root_back_btn"
CONF_ROWS = "rows"
CONF_SCALE = "scale"
CONF_SCALE_LINES = "scale_lines"
CONF_SCROLLBAR_MODE = "scrollbar_mode"
CONF_SCROLL_DIR = "scroll_dir"
@@ -536,6 +618,7 @@ CONF_SRC = "src"
CONF_START_ANGLE = "start_angle"
CONF_START_VALUE = "start_value"
CONF_STATES = "states"
CONF_STRIDE = "stride"
CONF_STYLE = "style"
CONF_STYLES = "styles"
CONF_STYLE_DEFINITIONS = "style_definitions"
@@ -544,6 +627,7 @@ CONF_SKIP = "skip"
CONF_SYMBOL = "symbol"
CONF_TAB_ID = "tab_id"
CONF_TABS = "tabs"
CONF_TICK_STYLE = "tick_style"
CONF_TIME_FORMAT = "time_format"
CONF_TILE = "tile"
CONF_TILE_ID = "tile_id"
@@ -551,6 +635,8 @@ CONF_TILES = "tiles"
CONF_TITLE = "title"
CONF_TOP_LAYER = "top_layer"
CONF_TOUCHSCREENS = "touchscreens"
CONF_TRANSFORM_ROTATION = "transform_rotation"
CONF_TRANSFORM_SCALE = "transform_scale"
CONF_TRANSPARENCY_KEY = "transparency_key"
CONF_THEME = "theme"
CONF_UPDATE_ON_RELEASE = "update_on_release"
@@ -578,6 +664,16 @@ LV_KEYS = LvConstant(
"END",
)
LV_SCALE_MODE = LvConstant(
"LV_SCALE_MODE_",
"HORIZONTAL_TOP",
"HORIZONTAL_BOTTOM",
"VERTICAL_LEFT",
"VERTICAL_RIGHT",
"ROUND_INNER",
"ROUND_OUTER",
)
DEFAULT_ESPHOME_FONT = "esphome_lv_default_font"
@@ -590,3 +686,29 @@ def join_enums(enums, prefix=""):
if prefix:
return literal("|".join(f"{prefix}{e.upper()}" for e in enums))
return literal("|".join(f"(int){e.upper()}" for e in enums))
# fmt: off
LV_COLOR_FORMATS = (
"RGB565", "SWAPPED", "RGB565A8", "RGB888", "XRGB8888", "ARGB8888", "PREMULTIPLIED", "L8", "AL88", "A8", "I1",
)
LV_DEFINES = (
"LV_USE_FREERTOS_TASK_NOTIFY", "LV_DRAW_BUF_STRIDE_ALIGN", "LV_USE_DRAW_SW", "LV_DRAW_SW_DRAW_UNIT_CNT",
"LV_DRAW_SW_COMPLEX", "LV_USE_DRAW_PXP", "LV_USE_PXP_DRAW_THREAD", "LV_USE_DRAW_G2D",
"LV_USE_G2D_DRAW_THREAD", "LV_VG_LITE_USE_BOX_SHADOW", "LV_VG_LITE_THORVG_16PIXELS_ALIGN", "LV_LOG_USE_TIMESTAMP",
"LV_LOG_USE_FILE_LINE", "LV_USE_OBJ_ID_BUILTIN", "LV_USE_OBJ_PROPERTY_NAME", "LV_ATTRIBUTE_MEM_ALIGN_SIZE",
"LV_FONT_MONTSERRAT_14", "LV_USE_FONT_PLACEHOLDER", "LV_WIDGETS_HAS_DEFAULT_VALUE", "LV_USE_ARCLABEL",
"LV_USE_CALENDAR", "LV_USE_CALENDAR_HEADER_ARROW", "LV_USE_CALENDAR_HEADER_DROPDOWN", "LV_USE_CHART",
"LV_USE_LIST", "LV_USE_MENU", "LV_USE_MSGBOX", "LV_USE_SCALE",
"LV_USE_TABLE", "LV_USE_SPAN", "LV_USE_WIN", "LV_USE_THEME_DEFAULT",
"LV_THEME_DEFAULT_GROW", "LV_USE_THEME_SIMPLE", "LV_USE_THEME_MONO", "LV_USE_FLEX",
"LV_USE_GRID", "LV_USE_PROFILER_BUILTIN", "LV_PROFILER_BUILTIN_DEFAULT_ENABLE", "LV_PROFILER_LAYOUT",
"LV_PROFILER_REFR", "LV_PROFILER_DRAW", "LV_PROFILER_INDEV", "LV_PROFILER_DECODER",
"LV_PROFILER_FONT", "LV_PROFILER_FS", "LV_PROFILER_TIMER", "LV_PROFILER_CACHE",
"LV_PROFILER_EVENT", "LV_USE_OBSERVER", "LV_IME_PINYIN_USE_DEFAULT_DICT", "LV_IME_PINYIN_USE_K9_MODE",
"LV_FILE_EXPLORER_QUICK_ACCESS", "LV_TEST_SCREENSHOT_CREATE_REFERENCE_IMAGE", "LV_LINUX_FBDEV_MMAP",
"LV_USE_NUTTX_MOUSE_MOVE_STEP", "LV_USE_GENERIC_MIPI", "LV_BUILD_EXAMPLES", "LV_BUILD_DEMOS",
"LV_WAYLAND_USE_EGL", "LV_WAYLAND_USE_G2D", "LV_WAYLAND_USE_SHM", "LV_LINUX_DRM_USE_EGL",
"LV_USE_LZ4", "LV_USE_THORVG", "LV_SDL_USE_EGL", "LV_USE_EGL", "LV_LABEL_LONG_TXT_HINT", "LV_LABEL_TEXT_SELECTION",
) + tuple(f"LV_DRAW_SW_SUPPORT_{f}" for f in LV_COLOR_FORMATS)
+1 -1
View File
@@ -71,7 +71,7 @@ async def encoders_to_code(var, config, default_group):
lv_assign(group, lv_expr.group_create())
else:
group = default_group
lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group)
lv.indev_set_group(listener.get_drv(), group)
async def initial_focus_to_code(config):
+35 -15
View File
@@ -7,12 +7,13 @@ from esphome.const import (
CONF_ID,
CONF_POSITION,
)
from esphome.core import ID
from esphome.cpp_generator import MockObj
from .defines import CONF_GRADIENTS, LV_DITHER, LV_GRAD_DIR, add_define
from .lv_validation import lv_color, lv_fraction
from .lvcode import lv_assign
from .types import lv_gradient_t
from .defines import CONF_GRADIENTS, CONF_OPA, LV_DITHER, add_define, add_warning
from .lv_validation import lv_color, lv_percentage, opacity
from .lvcode import lv
from .types import lv_color_t, lv_gradient_t, lv_opa_t
CONF_STOPS = "stops"
@@ -27,14 +28,17 @@ GRADIENT_SCHEMA = cv.ensure_list(
cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(lv_gradient_t),
cv.Optional(CONF_DIRECTION, default="NONE"): LV_GRAD_DIR.one_of,
cv.Required(CONF_DIRECTION): cv.one_of(
"HOR", "HORIZONTAL", "VER", "VERTICAL", upper=True
),
cv.Optional(CONF_DITHER, default="NONE"): LV_DITHER.one_of,
cv.Required(CONF_STOPS): cv.All(
[
cv.Schema(
{
cv.Required(CONF_COLOR): lv_color,
cv.Required(CONF_POSITION): lv_fraction,
cv.Optional(CONF_OPA, default=1.0): opacity,
cv.Required(CONF_POSITION): lv_percentage,
}
)
],
@@ -47,15 +51,31 @@ GRADIENT_SCHEMA = cv.ensure_list(
async def gradients_to_code(config):
max_stops = 2
if any(CONF_DITHER in x for x in config.get(CONF_GRADIENTS, ())):
add_warning(
"The 'dither' option for gradients is not supported by LVGL 9.x and will be ignored"
)
for gradient in config.get(CONF_GRADIENTS, ()):
var = MockObj(cg.new_Pvariable(gradient[CONF_ID]), "->")
max_stops = max(max_stops, len(gradient[CONF_STOPS]))
lv_assign(var.dir, await LV_GRAD_DIR.process(gradient[CONF_DIRECTION]))
lv_assign(var.dither, await LV_DITHER.process(gradient[CONF_DITHER]))
lv_assign(var.stops_count, len(gradient[CONF_STOPS]))
for index, stop in enumerate(gradient[CONF_STOPS]):
lv_assign(var.stops[index].color, await lv_color.process(stop[CONF_COLOR]))
lv_assign(
var.stops[index].frac, await lv_fraction.process(stop[CONF_POSITION])
)
idbase = gradient[CONF_ID].id
stops = gradient[CONF_STOPS]
max_stops = max(max_stops, len(stops))
if gradient[CONF_DIRECTION].startswith("VER"):
lv.grad_vertical_init(var)
else:
lv.grad_horizontal_init(var)
stop_colors = cg.static_const_array(
ID(idbase + "_colors_", type=lv_color_t),
[await lv_color.process(x[CONF_COLOR]) for x in stops],
)
stop_opacities = cg.static_const_array(
ID(idbase + "_opacities_", type=lv_opa_t),
[await opacity.process(x[CONF_OPA]) for x in stops],
)
stop_positions = cg.static_const_array(
ID(idbase + "_positions_", type=cg.uint8),
[await lv_percentage.process(x[CONF_POSITION]) for x in stops],
)
lv.grad_init_stops(var, stop_colors, stop_opacities, stop_positions, len(stops))
add_define("LV_GRADIENT_MAX_STOPS", max_stops)
+1 -1
View File
@@ -67,7 +67,7 @@ async def keypads_to_code(var, config, default_group):
lv_assign(group, lv_expr.group_create())
else:
group = default_group
lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group)
lv.indev_set_group(listener.get_drv(), group)
async def initial_focus_to_code(config):
+11 -13
View File
@@ -88,8 +88,8 @@ grid_spec = cv.Any(size, LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_spa
GRID_CELL_SCHEMA = {
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
cv.Optional(CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int,
cv.Optional(CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int,
cv.Optional(CONF_GRID_CELL_ROW_SPAN): cv.int_range(min=1),
cv.Optional(CONF_GRID_CELL_COLUMN_SPAN): cv.int_range(min=1),
cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments,
cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments,
}
@@ -198,12 +198,8 @@ class GridLayout(Layout):
{
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
cv.Optional(
CONF_GRID_CELL_ROW_SPAN, default=1
): cv.positive_int,
cv.Optional(
CONF_GRID_CELL_COLUMN_SPAN, default=1
): cv.positive_int,
cv.Optional(CONF_GRID_CELL_ROW_SPAN): cv.int_range(min=1),
cv.Optional(CONF_GRID_CELL_COLUMN_SPAN): cv.int_range(min=1),
cv.Optional(
CONF_GRID_CELL_X_ALIGN, default="center"
): grid_alignments,
@@ -231,8 +227,8 @@ class GridLayout(Layout):
{
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
cv.Optional(CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int,
cv.Optional(CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int,
cv.Optional(CONF_GRID_CELL_ROW_SPAN): cv.int_range(min=1),
cv.Optional(CONF_GRID_CELL_COLUMN_SPAN): cv.int_range(min=1),
cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments,
cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments,
},
@@ -299,11 +295,13 @@ class GridLayout(Layout):
w[CONF_GRID_CELL_ROW_POS] = row
w[CONF_GRID_CELL_COLUMN_POS] = column
for i in range(w[CONF_GRID_CELL_ROW_SPAN]):
for j in range(w[CONF_GRID_CELL_COLUMN_SPAN]):
row_span = w.get(CONF_GRID_CELL_ROW_SPAN, 1)
column_span = w.get(CONF_GRID_CELL_COLUMN_SPAN, 1)
for i in range(row_span):
for j in range(column_span):
if row + i >= rows or column + j >= columns:
raise cv.Invalid(
f"Cell at {row}/{column} span {w[CONF_GRID_CELL_ROW_SPAN]}x{w[CONF_GRID_CELL_COLUMN_SPAN]} "
f"Cell at {row}/{column} span {row_span}x{column_span} "
f"exceeds grid size {rows}x{columns}",
[CONF_WIDGETS, index],
)
+1 -1
View File
@@ -38,7 +38,7 @@ class LVLight : public light::LightOutput {
void set_value_(lv_color_t value) {
lv_led_set_color(this->obj_, value);
lv_led_on(this->obj_);
lv_event_send(this->obj_, lv_api_event, nullptr);
lv_obj_send_event(this->obj_, lv_api_event, nullptr);
}
lv_obj_t *obj_{};
optional<lv_color_t> initial_value_{};
+66 -51
View File
@@ -30,6 +30,7 @@ from .defines import (
LV_FONTS,
LValidator,
LvConstant,
StaticCastExpression,
call_lambda,
literal,
)
@@ -40,22 +41,28 @@ from .helpers import (
lv_fonts_used,
requires_component,
)
from .types import lv_gradient_t
from .types import lv_gradient_t, lv_opa_t
opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER")
LV_OPA = LvConstant("LV_OPA_", "TRANSP", "COVER")
@schema_extractor("one_of")
def opacity_validator(value):
if value == SCHEMA_EXTRACT:
return opacity_consts.choices
value = cv.Any(cv.percentage, opacity_consts.one_of)(value)
if isinstance(value, float):
return int(value * 255)
return value
return LV_OPA.choices
value = cv.Any(cv.percentage, LV_OPA.one_of)(value)
if value == str(LV_OPA.COVER):
value = 1.0
if value == str(LV_OPA.TRANSP):
value = 0.0
return cv.float_range(0.0, 1.0)(value)
opacity = LValidator(opacity_validator, uint32, retmapper=literal)
opacity = LValidator(
opacity_validator,
lv_opa_t,
retmapper=lambda opa: StaticCastExpression(cg.uint8, opa * 255.0),
)
COLOR_NAMES = {
"aliceblue": 0xF0F8FF,
@@ -244,7 +251,17 @@ def option_string(value):
return value
lv_color = LValidator(color, ty.lv_color_t, retmapper=color_retmapper)
class LvColor(LValidator):
def __init__(self):
super().__init__(color, ty.lv_color_t, retmapper=color_retmapper)
def __getattr__(self, item):
if item in COLOR_NAMES:
return color_retmapper(COLOR_NAMES[item])
raise AttributeError(item)
lv_color = LvColor()
def pixels_or_percent_validator(value):
@@ -252,16 +269,17 @@ def pixels_or_percent_validator(value):
if value == SCHEMA_EXTRACT:
return ["pixels", "..%"]
if isinstance(value, str) and value.lower().endswith("px"):
value = cv.int_(value[:-2])
return cv.int_(value[:-2])
if isinstance(value, str) and re.match(r"^lv_pct\((\d+)\)$", value):
return value
value = cv.Any(cv.int_, cv.percentage)(value)
if isinstance(value, int):
return value
return f"lv_pct({int(value * 100)})"
return int(value[6:-1]) / 100.0
return cv.Any(cv.int_, cv.possibly_negative_percentage)(value)
pixels_or_percent = LValidator(pixels_or_percent_validator, uint32, retmapper=literal)
pixels_or_percent = LValidator(
pixels_or_percent_validator,
uint32,
retmapper=lambda x: x if isinstance(x, int) else literal(f"lv_pct({int(x * 100)})"),
)
def pixels_validator(value):
@@ -282,15 +300,11 @@ def padding_validator(value):
padding = LValidator(padding_validator, int32, retmapper=literal)
def zoom_validator(value):
def scale_validator(value):
return cv.float_range(0.1, 10.0)(value)
def zoom_retmapper(value):
return int(value * 256)
zoom = LValidator(zoom_validator, uint32, retmapper=zoom_retmapper)
scale = LValidator(scale_validator, uint32, retmapper=lambda x: int(x * 256))
def angle(value):
@@ -321,17 +335,23 @@ def size_validator(value):
return pixels_or_percent_validator(value)
size = LValidator(size_validator, uint32, retmapper=literal)
size = LValidator(
size_validator,
uint32,
retmapper=lambda x: (
literal(x) if isinstance(x, str) else pixels_or_percent.retmapper(x)
),
)
radius_consts = LvConstant("LV_RADIUS_", "CIRCLE")
LV_RADIUS = LvConstant("LV_RADIUS_", "CIRCLE")
@schema_extractor("one_of")
def fraction_validator(value):
if value == SCHEMA_EXTRACT:
return radius_consts.choices
value = cv.Any(size, cv.percentage, radius_consts.one_of)(value)
return LV_RADIUS.choices
value = cv.Any(size, cv.percentage, LV_RADIUS.one_of)(value)
if isinstance(value, float):
return int(value * 255)
return value
@@ -374,12 +394,6 @@ lv_image_list = LValidator(
lv_bool = LValidator(cv.boolean, cg.bool_, retmapper=literal)
def lv_pct(value: int | float):
if isinstance(value, float):
value = int(value * 100)
return literal(f"lv_pct({value})")
def lvms_validator_(value):
if value == "never":
value = "2147483647ms"
@@ -424,30 +438,28 @@ class TextValidator(LValidator):
if time_format := value.get(CONF_TIME_FORMAT):
source = value[CONF_TIME]
if isinstance(source, Lambda):
time_format = cpp_string_escape(time_format)
return cg.RawExpression(
source = MockObj(
call_lambda(
await cg.process_lambda(source, args, return_type=ESPTime)
)
+ f".strftime({time_format}).c_str()"
)
# must be an ID
source = await cg.get_variable(source)
return source.now().strftime(time_format).c_str()
else:
source = (await cg.get_variable(source)).now()
return source.strftime(time_format).c_str()
if isinstance(value, Lambda):
value = call_lambda(
await cg.process_lambda(value, args, return_type=self.rtype)
)
textvalue = str(value)
# Was the lambda call reduced to a string?
if value.endswith("c_str()") or (
value.endswith('"') and value.startswith('"')
if textvalue.endswith("c_str()") or (
textvalue.endswith('"') and textvalue.startswith('"')
):
pass
else:
# Either a std::string or a lambda call returning that. We need const char*
value = f"({value}).c_str()"
return cg.RawExpression(value)
return value
# Either a std::string or a lambda call returning that. We need const char*
return MockObj(f"({value}).c_str()")
return await super().process(value, args)
@@ -455,21 +467,24 @@ lv_text = TextValidator()
lv_float = LValidator(cv.float_, cg.float_)
lv_int = LValidator(cv.int_, cg.int_)
lv_positive_int = LValidator(cv.positive_int, cg.int_)
lv_brightness = LValidator(cv.percentage, cg.float_, retmapper=lambda x: int(x * 255))
def gradient_mapper(value):
return MockObj(value)
def _percentage_validator(value):
value = cv.Any(cv.percentage, cv.float_range(0.0, 1.0), cv.int_range(0, 255))(value)
if isinstance(value, int):
return value / 255.0
return value
def gradient_validator(value):
return cv.use_id(lv_gradient_t)(value)
lv_percentage = LValidator(
_percentage_validator, cg.float_, retmapper=lambda x: int(x * 255)
)
lv_gradient = LValidator(
validator=gradient_validator,
validator=cv.use_id(lv_gradient_t),
rtype=lv_gradient_t,
retmapper=gradient_mapper,
retmapper=MockObj,
)
+8
View File
@@ -168,6 +168,9 @@ class LambdaContext(CodeContext):
def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]:
return self.parameters
def get_parameter(self, index: int):
return literal(self.parameters[index][1])
async def __aenter__(self):
await super().__aenter__()
add_line_marks(self.where)
@@ -250,10 +253,14 @@ class MockLv:
A mock object that can be used to generate LVGL calls.
"""
# Mapping for LVGL 9
ATTR_MAP = {"event_send": "obj_send_event", "dither": "bg_dither_mode"}
def __init__(self, base):
self.base = base
def __getattr__(self, attr: str) -> "MockLv":
attr = MockLv.ATTR_MAP.get(attr, attr)
return MockLv(f"{self.base}{attr}")
def append(self, expression):
@@ -307,6 +314,7 @@ class ReturnStatement(ExpressionStatement):
class LvExpr(MockLv):
def __getattr__(self, attr: str) -> "MockLv":
attr = MockLv.ATTR_MAP.get(attr, attr)
return LvExpr(f"{self.base}{attr}")
def append(self, expression):
+276 -117
View File
@@ -2,16 +2,21 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "lvgl_hal.h"
#include "lvgl_esphome.h"
#include "core/lv_global.h"
#include "core/lv_obj_class_private.h"
#include <numeric>
namespace esphome {
namespace lvgl {
static void *lv_alloc_draw_buf(size_t size, bool internal);
static void *draw_buf_alloc_cb(size_t size, lv_color_format_t color_format) { return lv_alloc_draw_buf(size, false); };
namespace esphome::lvgl {
static const char *const TAG = "lvgl";
static const size_t MIN_BUFFER_FRAC = 8;
static const size_t MIN_BUFFER_FRAC = 8; // buffer must be at least 1/8 of the display size
static const size_t MIN_BUFFER_SIZE = 2048; // Sensible minimum buffer size
static const char *const EVENT_NAMES[] = {
"NONE",
@@ -61,7 +66,14 @@ static const char *const EVENT_NAMES[] = {
"GET_SELF_SIZE",
};
std::string lv_event_code_name_for(uint8_t event_code) {
static const unsigned LOG_LEVEL_MAP[] = {
ESPHOME_LOG_LEVEL_DEBUG, ESPHOME_LOG_LEVEL_INFO, ESPHOME_LOG_LEVEL_WARN,
ESPHOME_LOG_LEVEL_ERROR, ESPHOME_LOG_LEVEL_ERROR, ESPHOME_LOG_LEVEL_NONE,
};
std::string lv_event_code_name_for(lv_event_t *event) {
auto event_code = lv_event_get_code(event);
if (event_code < sizeof(EVENT_NAMES) / sizeof(EVENT_NAMES[0])) {
return EVENT_NAMES[event_code];
}
@@ -71,11 +83,12 @@ std::string lv_event_code_name_for(uint8_t event_code) {
return buf;
}
static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) {
static void rounder_cb(lv_event_t *event) {
auto *comp = static_cast<LvglComponent *>(lv_event_get_user_data(event));
auto *area = static_cast<lv_area_t *>(lv_event_get_param(event));
// cater for display driver chips with special requirements for bounds of partial
// draw areas. Extend the draw area to satisfy:
// * Coordinates must be a multiple of draw_rounding
auto *comp = static_cast<LvglComponent *>(disp_drv->user_data);
auto draw_rounding = comp->draw_rounding;
// round down the start coordinates
area->x1 = area->x1 / draw_rounding * draw_rounding;
@@ -85,15 +98,14 @@ static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) {
area->y2 = (area->y2 + draw_rounding) / draw_rounding * draw_rounding - 1;
}
void LvglComponent::monitor_cb(lv_disp_drv_t *disp_drv, uint32_t time, uint32_t px) {
ESP_LOGVV(TAG, "Draw end: %" PRIu32 " pixels in %" PRIu32 " ms", px, time);
auto *comp = static_cast<LvglComponent *>(disp_drv->user_data);
void LvglComponent::render_end_cb(lv_event_t *event) {
auto *comp = static_cast<LvglComponent *>(lv_event_get_user_data(event));
comp->draw_end_();
}
void LvglComponent::render_start_cb(lv_disp_drv_t *disp_drv) {
void LvglComponent::render_start_cb(lv_event_t *event) {
ESP_LOGVV(TAG, "Draw start");
auto *comp = static_cast<LvglComponent *>(disp_drv->user_data);
auto *comp = static_cast<LvglComponent *>(lv_event_get_user_data(event));
comp->draw_start_();
}
@@ -106,16 +118,15 @@ void LvglComponent::dump_config() {
" Buffer size: %zu%%\n"
" Rotation: %d\n"
" Draw rounding: %d",
this->disp_drv_.hor_res, this->disp_drv_.ver_res, 100 / this->buffer_frac_, this->rotation,
(int) this->draw_rounding);
this->width_, this->height_, 100 / this->buffer_frac_, this->rotation, (int) this->draw_rounding);
}
void LvglComponent::set_paused(bool paused, bool show_snow) {
this->paused_ = paused;
this->show_snow_ = show_snow;
if (!paused && lv_scr_act() != nullptr) {
lv_disp_trig_activity(this->disp_); // resets the inactivity time
lv_obj_invalidate(lv_scr_act());
if (!paused && lv_screen_active() != nullptr) {
lv_display_trigger_activity(this->disp_); // resets the inactivity time
lv_obj_invalidate(lv_screen_active());
}
if (paused && this->pause_callback_ != nullptr)
this->pause_callback_->trigger();
@@ -125,6 +136,14 @@ void LvglComponent::set_paused(bool paused, bool show_snow) {
void LvglComponent::esphome_lvgl_init() {
lv_init();
// override draw buf alloc to ensure proper alignment for PPA
LV_GLOBAL_DEFAULT()->draw_buf_handlers.buf_malloc_cb = draw_buf_alloc_cb;
LV_GLOBAL_DEFAULT()->draw_buf_handlers.buf_free_cb = lv_free_core;
LV_GLOBAL_DEFAULT()->image_cache_draw_buf_handlers.buf_malloc_cb = draw_buf_alloc_cb;
LV_GLOBAL_DEFAULT()->image_cache_draw_buf_handlers.buf_free_cb = lv_free_core;
LV_GLOBAL_DEFAULT()->font_draw_buf_handlers.buf_malloc_cb = draw_buf_alloc_cb;
LV_GLOBAL_DEFAULT()->font_draw_buf_handlers.buf_free_cb = lv_free_core;
lv_tick_set_cb([] { return millis(); });
lv_update_event = static_cast<lv_event_code_t>(lv_event_register_id());
lv_api_event = static_cast<lv_event_code_t>(lv_event_register_id());
}
@@ -149,7 +168,7 @@ void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_ev
void LvglComponent::add_page(LvPageType *page) {
this->pages_.push_back(page);
page->set_parent(this);
lv_disp_set_default(this->disp_);
lv_display_set_default(this->disp_);
page->setup(this->pages_.size() - 1);
}
@@ -187,13 +206,13 @@ void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) {
size_t LvglComponent::get_current_page() const { return this->current_page_; }
bool LvPageType::is_showing() const { return this->parent_->get_current_page() == this->index; }
void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) {
auto width = lv_area_get_width(area);
auto height = lv_area_get_height(area);
auto height_rounded = (height + this->draw_rounding - 1) / this->draw_rounding * this->draw_rounding;
auto x1 = area->x1;
auto y1 = area->y1;
lv_color_t *dst = this->rotate_buf_;
lv_color_data *dst = reinterpret_cast<lv_color_data *>(this->rotate_buf_);
switch (this->rotation) {
case display::DISPLAY_ROTATION_90_DEGREES:
for (lv_coord_t x = height; x-- != 0;) {
@@ -202,7 +221,7 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
}
}
y1 = x1;
x1 = this->disp_drv_.ver_res - area->y1 - height;
x1 = this->height_ - area->y1 - height;
height = width;
width = height_rounded;
break;
@@ -213,8 +232,8 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
dst[y * width + x] = *ptr++;
}
}
x1 = this->disp_drv_.hor_res - x1 - width;
y1 = this->disp_drv_.ver_res - y1 - height;
x1 = this->width_ - x1 - width;
y1 = this->height_ - y1 - height;
break;
case display::DISPLAY_ROTATION_270_DEGREES:
@@ -224,7 +243,7 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
}
}
x1 = y1;
y1 = this->disp_drv_.hor_res - area->x1 - width;
y1 = this->width_ - area->x1 - width;
height = width;
width = height_rounded;
break;
@@ -234,20 +253,19 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
break;
}
for (auto *display : this->displays_) {
ESP_LOGV(TAG, "draw buffer x1=%d, y1=%d, width=%d, height=%d", x1, y1, width, height);
display->draw_pixels_at(x1, y1, width, height, (const uint8_t *) dst, display::COLOR_ORDER_RGB, LV_BITNESS,
LV_COLOR_16_SWAP);
this->big_endian_);
}
}
void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
void LvglComponent::flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p) {
if (!this->is_paused()) {
auto now = millis();
this->draw_buffer_(area, color_p);
ESP_LOGVV(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));
this->draw_buffer_(area, reinterpret_cast<lv_color_data *>(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);
lv_display_flush_ready(disp_drv);
}
IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeout) : timeout_(std::move(timeout)) {
@@ -264,14 +282,14 @@ IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeo
#ifdef USE_LVGL_TOUCHSCREEN
LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time, LvglComponent *parent) {
this->set_parent(parent);
lv_indev_drv_init(&this->drv_);
this->drv_.disp = parent->get_disp();
this->drv_.long_press_repeat_time = long_press_repeat_time;
this->drv_.long_press_time = long_press_time;
this->drv_.type = LV_INDEV_TYPE_POINTER;
this->drv_.user_data = this;
this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) {
auto *l = static_cast<LVTouchListener *>(d->user_data);
this->drv_ = lv_indev_create();
lv_indev_set_type(this->drv_, LV_INDEV_TYPE_POINTER);
lv_indev_set_disp(this->drv_, parent->get_disp());
lv_indev_set_long_press_time(this->drv_, long_press_time);
// long press repeat time TBD
lv_indev_set_user_data(this->drv_, this);
lv_indev_set_read_cb(this->drv_, [](lv_indev_t *d, lv_indev_data_t *data) {
auto *l = static_cast<LVTouchListener *>(lv_indev_get_user_data(d));
if (l->touch_pressed_) {
data->point.x = l->touch_point_.x;
data->point.y = l->touch_point_.y;
@@ -279,7 +297,7 @@ LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_r
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
};
});
}
void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) {
@@ -289,21 +307,78 @@ void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) {
}
#endif // USE_LVGL_TOUCHSCREEN
#ifdef USE_LVGL_METER
int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int value) {
auto *scale = lv_obj_get_parent(obj);
auto min_value = lv_scale_get_range_min_value(scale);
return ((value - min_value) * lv_scale_get_angle_range(scale) / (lv_scale_get_range_max_value(scale) - min_value) +
lv_scale_get_rotation((scale))) %
360;
}
void IndicatorLine::set_obj(lv_obj_t *lv_obj) {
LvCompound::set_obj(lv_obj);
lv_line_set_points(lv_obj, this->points_, 2);
lv_obj_add_event_cb(
lv_obj_get_parent(obj),
[](lv_event_t *e) {
auto *indicator = static_cast<IndicatorLine *>(lv_event_get_user_data(e));
indicator->update_length_();
ESP_LOGV(TAG, "Updated length, value = %d", indicator->angle_);
},
LV_EVENT_SIZE_CHANGED, this);
}
void IndicatorLine::set_value(int value) {
auto angle = lv_get_needle_angle_for_value(this->obj, value);
if (angle != this->angle_) {
this->angle_ = angle;
this->update_length_();
}
}
void IndicatorLine::update_length_() {
uint32_t actual_needle_length;
auto radius = lv_obj_get_width(lv_obj_get_parent(this->obj)) / 2;
auto length = lv_obj_get_style_length(this->obj, LV_PART_MAIN);
auto radial_offset = lv_obj_get_style_radial_offset(this->obj, LV_PART_MAIN);
if (LV_COORD_IS_PCT(radial_offset)) {
radial_offset = radius * LV_COORD_GET_PCT(radial_offset) / 100;
}
if (LV_COORD_IS_PCT(length)) {
actual_needle_length = radius * LV_COORD_GET_PCT(length) / 100;
} else if (length < 0) {
actual_needle_length = radius + length;
} else {
actual_needle_length = length;
}
auto x = lv_trigo_cos(this->angle_) / 32768.0f;
auto y = lv_trigo_sin(this->angle_) / 32768.0f;
this->points_[0].x = radius + radial_offset * x;
this->points_[0].y = radius + radial_offset * y;
this->points_[1].x = x * actual_needle_length + radius;
this->points_[1].y = y * actual_needle_length + radius;
lv_obj_refresh_self_size(this->obj);
lv_obj_invalidate(this->obj);
}
#endif
#ifdef USE_LVGL_KEY_LISTENER
LVEncoderListener::LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt) {
lv_indev_drv_init(&this->drv_);
this->drv_.type = type;
this->drv_.user_data = this;
this->drv_.long_press_time = lpt;
this->drv_.long_press_repeat_time = lprt;
this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) {
auto *l = static_cast<LVEncoderListener *>(d->user_data);
LVEncoderListener::LVEncoderListener(lv_indev_type_t type, uint16_t long_press_time, uint16_t long_press_repeat_time) {
this->drv_ = lv_indev_create();
lv_indev_set_type(this->drv_, type);
lv_indev_set_long_press_time(this->drv_, long_press_time);
lv_indev_set_long_press_repeat_time(this->drv_, long_press_repeat_time);
lv_indev_set_user_data(this->drv_, this);
lv_indev_set_read_cb(this->drv_, [](lv_indev_t *d, lv_indev_data_t *data) {
auto *l = static_cast<LVEncoderListener *>(lv_indev_get_user_data(d));
data->state = l->pressed_ ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
data->key = l->key_;
data->enc_diff = (int16_t) (l->count_ - l->last_count_);
l->last_count_ = l->count_;
data->continue_reading = false;
};
});
}
#endif // USE_LVGL_KEY_LISTENER
@@ -325,7 +400,7 @@ void LvSelectable::set_selected_text(const std::string &text, lv_anim_enable_t a
auto index = std::find(this->options_.begin(), this->options_.end(), text);
if (index != this->options_.end()) {
this->set_selected_index(index - this->options_.begin(), anim);
lv_event_send(this->obj, lv_api_event, nullptr);
lv_obj_send_event(this->obj, lv_api_event, nullptr);
}
}
@@ -335,7 +410,7 @@ void LvSelectable::set_options(std::vector<std::string> options) {
index = options.size() - 1;
this->options_ = std::move(options);
this->set_option_string(join_string(this->options_).c_str());
lv_event_send(this->obj, LV_EVENT_REFRESH, nullptr);
lv_obj_send_event(this->obj, LV_EVENT_REFRESH, nullptr);
this->set_selected_index(index, LV_ANIM_OFF);
}
#endif // USE_LVGL_DROPDOWN || LV_USE_ROLLER
@@ -346,17 +421,17 @@ void LvButtonMatrixType::set_obj(lv_obj_t *lv_obj) {
lv_obj_add_event_cb(
lv_obj,
[](lv_event_t *event) {
auto *self = static_cast<LvButtonMatrixType *>(event->user_data);
auto *self = static_cast<LvButtonMatrixType *>(lv_event_get_user_data(event));
if (self->key_callback_.size() == 0)
return;
auto key_idx = lv_btnmatrix_get_selected_btn(self->obj);
if (key_idx == LV_BTNMATRIX_BTN_NONE)
auto key_idx = lv_buttonmatrix_get_selected_button(self->obj);
if (key_idx == LV_BUTTONMATRIX_BUTTON_NONE)
return;
if (self->key_map_.count(key_idx) != 0) {
self->send_key_(self->key_map_[key_idx]);
return;
}
const auto *str = lv_btnmatrix_get_btn_text(self->obj, key_idx);
const auto *str = lv_buttonmatrix_get_button_text(self->obj, key_idx);
auto len = strlen(str);
while (len--)
self->send_key_(*str++);
@@ -376,14 +451,14 @@ void LvKeyboardType::set_obj(lv_obj_t *lv_obj) {
lv_obj_add_event_cb(
lv_obj,
[](lv_event_t *event) {
auto *self = static_cast<LvKeyboardType *>(event->user_data);
auto *self = static_cast<LvKeyboardType *>(lv_event_get_user_data(event));
if (self->key_callback_.size() == 0)
return;
auto key_idx = lv_btnmatrix_get_selected_btn(self->obj);
if (key_idx == LV_BTNMATRIX_BTN_NONE)
auto key_idx = lv_buttonmatrix_get_selected_button(self->obj);
if (key_idx == LV_BUTTONMATRIX_BUTTON_NONE)
return;
const char *txt = lv_btnmatrix_get_btn_text(self->obj, key_idx);
const char *txt = lv_buttonmatrix_get_button_text(self->obj, key_idx);
if (txt == nullptr)
return;
for (const auto *kb_special_key : KB_SPECIAL_KEYS) {
@@ -419,33 +494,29 @@ bool LvglComponent::is_paused() const {
}
void LvglComponent::write_random_() {
int iterations = 6 - lv_disp_get_inactive_time(this->disp_) / 60000;
int iterations = 6 - lv_display_get_inactive_time(this->disp_) / 60000;
if (iterations <= 0)
iterations = 1;
while (iterations-- != 0) {
auto col = random_uint32() % this->disp_drv_.hor_res;
int32_t col = random_uint32() % this->width_;
col = col / this->draw_rounding * this->draw_rounding;
auto row = random_uint32() % this->disp_drv_.ver_res;
int32_t row = random_uint32() % this->height_;
row = row / this->draw_rounding * this->draw_rounding;
auto size = ((random_uint32() % 32) / this->draw_rounding + 2) * this->draw_rounding - 1;
// clamp size so the square fits within the draw buffer
if ((size + 1) * (size + 1) > this->draw_buf_.size)
size = static_cast<decltype(size)>(sqrtf(this->draw_buf_.size)) - 1;
lv_area_t area;
area.x1 = col;
area.y1 = row;
area.x2 = col + size;
area.y2 = row + size;
if (area.x2 >= this->disp_drv_.hor_res)
area.x2 = this->disp_drv_.hor_res - 1;
if (area.y2 >= this->disp_drv_.ver_res)
area.y2 = this->disp_drv_.ver_res - 1;
// size will be between 8 and 32, and a multiple of draw_rounding
int32_t size = (random_uint32() % 25 + 8) / this->draw_rounding * this->draw_rounding;
lv_area_t area{col, row, col + size - 1, row + size - 1};
// clip to display bounds just in case
if (area.x2 >= this->width_)
area.x2 = this->width_ - 1;
if (area.y2 >= this->height_)
area.y2 = this->height_ - 1;
// line_len can't exceed 1024, and minimum buffer size is 2048, so this won't overflow the buffer
size_t line_len = lv_area_get_width(&area) * lv_area_get_height(&area) / 2;
for (size_t i = 0; i != line_len; i++) {
((uint32_t *) (this->draw_buf_.buf1))[i] = random_uint32();
((uint32_t *) (this->draw_buf_))[i] = random_uint32();
}
this->draw_buffer_(&area, (lv_color_t *) this->draw_buf_.buf1);
this->draw_buffer_(&area, (lv_color_data *) this->draw_buf_);
}
}
@@ -477,38 +548,32 @@ LvglComponent::LvglComponent(std::vector<display::Display *> displays, float buf
full_refresh_(full_refresh),
resume_on_input_(resume_on_input),
update_when_display_idle_(update_when_display_idle) {
lv_disp_draw_buf_init(&this->draw_buf_, nullptr, nullptr, 0);
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;
this->disp_ = lv_disp_drv_register(&this->disp_drv_);
this->disp_ = lv_display_create(240, 240);
}
void LvglComponent::setup() {
auto *display = this->displays_[0];
auto rounding = this->draw_rounding;
// cater for displays with dimensions that don't divide by the required rounding
this->width_ = display->get_width();
this->height_ = display->get_height();
auto width = (display->get_width() + rounding - 1) / rounding * rounding;
auto height = (display->get_height() + rounding - 1) / rounding * rounding;
auto frac = this->buffer_frac_;
if (frac == 0)
frac = 1;
size_t buffer_pixels = width * height / frac;
auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8;
auto buf_bytes = clamp_at_least(width * height / frac * LV_COLOR_DEPTH / 8, MIN_BUFFER_SIZE);
void *buffer = nullptr;
// for small buffers, try to allocate in internal memory first to improve performance
if (this->buffer_frac_ >= MIN_BUFFER_FRAC / 2)
buffer = malloc(buf_bytes); // NOLINT
buffer = lv_alloc_draw_buf(buf_bytes, true); // NOLINT
if (buffer == nullptr)
buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT
buffer = lv_alloc_draw_buf(buf_bytes, false); // NOLINT
// if specific buffer size not set and can't get 100%, try for a smaller one
if (buffer == nullptr && this->buffer_frac_ == 0) {
frac = MIN_BUFFER_FRAC;
buffer_pixels /= MIN_BUFFER_FRAC;
buf_bytes /= MIN_BUFFER_FRAC;
buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT
buffer = lv_alloc_draw_buf(buf_bytes, false); // NOLINT
}
this->buffer_frac_ = frac;
if (buffer == nullptr) {
@@ -516,13 +581,17 @@ void LvglComponent::setup() {
this->mark_failed();
return;
}
lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buffer_pixels);
this->disp_drv_.hor_res = display->get_width();
this->disp_drv_.ver_res = display->get_height();
lv_disp_drv_update(this->disp_, &this->disp_drv_);
this->draw_buf_ = static_cast<uint8_t *>(buffer);
lv_display_set_resolution(this->disp_, this->width_, this->height_);
lv_display_set_color_format(this->disp_, LV_COLOR_FORMAT_RGB565);
lv_display_set_flush_cb(this->disp_, static_flush_cb);
lv_display_set_user_data(this->disp_, this);
lv_display_add_event_cb(this->disp_, rounder_cb, LV_EVENT_INVALIDATE_AREA, this);
lv_display_set_buffers(this->disp_, this->draw_buf_, nullptr, buf_bytes,
this->full_refresh_ ? LV_DISPLAY_RENDER_MODE_FULL : LV_DISPLAY_RENDER_MODE_PARTIAL);
this->rotation = display->get_rotation();
if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) {
this->rotate_buf_ = static_cast<lv_color_t *>(lv_custom_mem_alloc(buf_bytes)); // NOLINT
this->rotate_buf_ = static_cast<lv_color_t *>(lv_alloc_draw_buf(buf_bytes, false)); // NOLINT
if (this->rotate_buf_ == nullptr) {
this->status_set_error(LOG_STR("Memory allocation failure"));
this->mark_failed();
@@ -530,26 +599,28 @@ void LvglComponent::setup() {
}
}
if (this->draw_start_callback_ != nullptr) {
this->disp_drv_.render_start_cb = render_start_cb;
lv_display_add_event_cb(this->disp_, render_start_cb, LV_EVENT_RENDER_START, this);
}
if (this->draw_end_callback_ != nullptr || this->update_when_display_idle_) {
this->disp_drv_.monitor_cb = monitor_cb;
lv_display_add_event_cb(this->disp_, render_end_cb, LV_EVENT_REFR_READY, this);
}
#if LV_USE_LOG
lv_log_register_print_cb([](const char *buf) {
lv_log_register_print_cb([](lv_log_level_t level, const char *buf) {
auto next = strchr(buf, ')');
if (next != nullptr)
buf = next + 1;
while (isspace(*buf))
buf++;
esp_log_printf_(LVGL_LOG_LEVEL, TAG, 0, "%.*s", (int) strlen(buf) - 1, buf);
if (level >= sizeof(LOG_LEVEL_MAP) / sizeof(LOG_LEVEL_MAP[0]))
level = sizeof(LOG_LEVEL_MAP) / sizeof(LOG_LEVEL_MAP[0]) - 1;
esp_log_printf_(LOG_LEVEL_MAP[level], TAG, 0, "%.*s", (int) strlen(buf) - 1, buf);
});
#endif
// Rotation will be handled by our drawing function, so reset the display rotation.
for (auto *disp : this->displays_)
disp->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);
this->show_page(0, LV_SCR_LOAD_ANIM_NONE, 0);
lv_disp_trig_activity(this->disp_);
lv_display_trigger_activity(this->disp_);
}
void LvglComponent::update() {
@@ -557,7 +628,7 @@ void LvglComponent::update() {
if (this->is_paused()) {
return;
}
this->idle_callbacks_.call(lv_disp_get_inactive_time(this->disp_));
this->idle_callbacks_.call(lv_display_get_inactive_time(this->disp_));
}
void LvglComponent::loop() {
@@ -565,41 +636,129 @@ void LvglComponent::loop() {
if (this->paused_ && this->show_snow_)
this->write_random_();
} else {
lv_timer_handler_run_in_period(5);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
auto now = millis();
lv_timer_handler();
auto elapsed = millis() - now;
if (elapsed > 15) {
ESP_LOGV(TAG, "lv_timer_handler took %dms", (int) (millis() - now));
}
#else
lv_timer_handler();
#endif
}
}
#ifdef USE_LVGL_ANIMIMG
void lv_animimg_stop(lv_obj_t *obj) {
auto *animg = (lv_animimg_t *) obj;
int32_t duration = animg->anim.time;
int32_t duration = lv_animimg_get_duration(obj);
lv_animimg_set_duration(obj, 0);
lv_animimg_start(obj);
lv_animimg_set_duration(obj, duration);
}
#endif
void LvglComponent::static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
reinterpret_cast<LvglComponent *>(disp_drv->user_data)->flush_cb_(disp_drv, area, color_p);
void LvglComponent::static_flush_cb(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p) {
reinterpret_cast<LvglComponent *>(lv_display_get_user_data(disp_drv))->flush_cb_(disp_drv, area, color_p);
}
} // namespace lvgl
} // namespace esphome
size_t lv_millis(void) { return esphome::millis(); }
#ifdef USE_LVGL_SCALE
/**
* Function to apply colors to ticks based on position
* @param e The event data
* @param color_start The color to apply to the first tick
* @param color_end The color to apply to the last tick
*/
void lv_scale_draw_event_cb(lv_event_t *e, uint16_t range_start, uint16_t range_end, lv_color_t color_start,
lv_color_t color_end, bool local) {
auto *scale = static_cast<lv_obj_t *>(lv_event_get_target(e));
lv_draw_task_t *task = lv_event_get_draw_task(e);
if (lv_draw_task_get_type(task) == LV_DRAW_TASK_TYPE_LINE) {
auto *line_dsc = static_cast<lv_draw_line_dsc_t *>(lv_draw_task_get_draw_dsc(task));
auto tick = line_dsc->base.id1;
if (tick >= range_start && tick <= range_end) {
unsigned range = range_end - range_start;
if (local) {
tick -= range_start;
} else {
range = lv_scale_get_total_tick_count(scale) - 1;
}
if (range == 0)
range = 1;
auto ratio = (tick * 255) / range;
line_dsc->color = lv_color_mix(color_end, color_start, ratio);
}
}
}
#endif // USE_LVGL_SCALE
static void lv_container_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj) {
LV_TRACE_OBJ_CREATE("begin");
LV_UNUSED(class_p);
}
// Container class. Name is based on LVGL naming convention but upper case to keep ESPHome clang-tidy happy
const lv_obj_class_t LV_CONTAINER_CLASS = {
.base_class = &lv_obj_class,
.constructor_cb = lv_container_constructor,
.name = "lv_container",
};
lv_obj_t *lv_container_create(lv_obj_t *parent) {
lv_obj_t *obj = lv_obj_class_create_obj(&LV_CONTAINER_CLASS, parent);
lv_obj_class_init_obj(obj);
return obj;
}
} // namespace esphome::lvgl
lv_result_t lv_mem_test_core() { return LV_RESULT_OK; }
void lv_mem_init() {}
void lv_mem_deinit() {}
#if defined(USE_HOST) || defined(USE_RP2040) || defined(USE_ESP8266)
void *lv_custom_mem_alloc(size_t size) {
void *lv_malloc_core(size_t size) {
auto *ptr = malloc(size); // NOLINT
if (ptr == nullptr) {
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
void lv_free_core(void *ptr) { return free(ptr); } // NOLINT
void *lv_realloc_core(void *ptr, size_t size) { return realloc(ptr, size); } // NOLINT
void lv_mem_monitor_core(lv_mem_monitor_t *mon_p) { memset(mon_p, 0, sizeof(lv_mem_monitor_t)); }
static void *lv_alloc_draw_buf(size_t size, bool internal) {
return malloc(size); // NOLINT
}
#elif defined(USE_ESP32)
static unsigned cap_bits = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; // NOLINT
void *lv_custom_mem_alloc(size_t size) {
static void *lv_alloc_draw_buf(size_t size, bool internal) {
void *buffer;
size = ((size + LV_DRAW_BUF_ALIGN - 1) / LV_DRAW_BUF_ALIGN) * LV_DRAW_BUF_ALIGN;
buffer = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size, internal ? MALLOC_CAP_8BIT : cap_bits); // NOLINT
if (buffer == nullptr)
ESP_LOGW(esphome::lvgl::TAG, "Failed to allocate %zu bytes for %sdraw buffer", size, internal ? "internal " : "");
return buffer;
}
void lv_mem_monitor_core(lv_mem_monitor_t *mon_p) {
multi_heap_info_t heap_info;
heap_caps_get_info(&heap_info, cap_bits);
mon_p->total_size = heap_info.total_allocated_bytes + heap_info.total_free_bytes;
mon_p->free_size = heap_info.total_free_bytes;
mon_p->max_used = heap_info.total_allocated_bytes;
mon_p->free_biggest_size = heap_info.largest_free_block;
mon_p->used_cnt = heap_info.allocated_blocks;
mon_p->free_cnt = heap_info.free_blocks;
mon_p->used_pct = heap_info.allocated_blocks * 100 / (heap_info.allocated_blocks + heap_info.free_blocks);
mon_p->frag_pct = 0;
}
void *lv_malloc_core(size_t size) {
void *ptr;
ptr = heap_caps_malloc(size, cap_bits);
if (ptr == nullptr) {
@@ -614,14 +773,14 @@ void *lv_custom_mem_alloc(size_t size) {
return ptr;
}
void lv_custom_mem_free(void *ptr) {
void lv_free_core(void *ptr) {
ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr);
if (ptr == nullptr)
return;
heap_caps_free(ptr);
}
void *lv_custom_mem_realloc(void *ptr, size_t size) {
void *lv_realloc_core(void *ptr, size_t size) {
ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size);
return heap_caps_realloc(ptr, size, cap_bits);
}
+77 -56
View File
@@ -4,7 +4,7 @@
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif // USE_BINARY_SENSOR
#ifdef USE_LVGL_IMAGE
#ifdef USE_IMAGE
#include "esphome/components/image/image.h"
#endif // USE_LVGL_IMAGE
#ifdef USE_LVGL_ROTARY_ENCODER
@@ -19,16 +19,17 @@
#include "esphome/components/display/display.h"
#include "esphome/components/display/display_color_utils.h"
#include "esphome/core/component.h"
#include "esphome/core/log.h"
#include <list>
#include <lvgl.h>
#include <map>
#include <utility>
#include <vector>
#ifdef USE_LVGL_FONT
#ifdef USE_FONT
#include "esphome/components/font/font.h"
#endif // USE_LVGL_FONT
#ifdef USE_LVGL_TOUCHSCREEN
#ifdef USE_TOUCHSCREEN
#include "esphome/components/touchscreen/touchscreen.h"
#endif // USE_LVGL_TOUCHSCREEN
@@ -36,12 +37,24 @@
#include "esphome/components/key_provider/key_provider.h"
#endif // USE_LVGL_BUTTONMATRIX
namespace esphome {
namespace lvgl {
namespace esphome::lvgl {
#if LV_COLOR_DEPTH == 16
using lv_color_data = uint16_t;
#endif
#if LV_COLOR_DEPTH == 32
using lv_color_data = uint32_t;
#endif
extern lv_event_code_t lv_api_event; // NOLINT
extern lv_event_code_t lv_update_event; // NOLINT
extern std::string lv_event_code_name_for(uint8_t event_code);
extern std::string lv_event_code_name_for(lv_event_t *event);
lv_obj_t *lv_container_create(lv_obj_t *parent);
#ifdef USE_LVGL_SCALE
void lv_scale_draw_event_cb(lv_event_t *e, uint16_t range_start, uint16_t range_end, lv_color_t color_start,
lv_color_t color_end, bool local);
#endif
#if LV_COLOR_DEPTH == 16
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_565;
#elif LV_COLOR_DEPTH == 32
@@ -50,7 +63,7 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_332;
#endif // LV_COLOR_DEPTH
#ifdef USE_LVGL_FONT
#if defined(USE_FONT) && defined(USE_LVGL_FONT)
inline void lv_obj_set_style_text_font(lv_obj_t *obj, const font::Font *font, lv_style_selector_t part) {
lv_obj_set_style_text_font(obj, font->get_lv_font(), part);
}
@@ -58,50 +71,37 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) {
lv_style_set_text_font(style, font->get_lv_font());
}
#endif
#ifdef USE_LVGL_IMAGE
#ifdef USE_IMAGE
// Shortcut / overload, so that the source of an image can easily be updated
// from within a lambda.
inline void lv_img_set_src(lv_obj_t *obj, esphome::image::Image *image) {
lv_img_set_src(obj, image->get_lv_img_dsc());
}
inline void lv_disp_set_bg_image(lv_disp_t *disp, esphome::image::Image *image) {
lv_disp_set_bg_image(disp, image->get_lv_img_dsc());
inline void lv_image_set_src(lv_obj_t *obj, esphome::image::Image *image) {
lv_image_set_src(obj, image->get_lv_image_dsc());
}
inline void lv_obj_set_style_bg_img_src(lv_obj_t *obj, esphome::image::Image *image, lv_style_selector_t selector) {
lv_obj_set_style_bg_img_src(obj, image->get_lv_img_dsc(), selector);
inline void lv_obj_set_style_bg_image_src(lv_obj_t *obj, esphome::image::Image *image, lv_style_selector_t selector) {
lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector);
}
#ifdef USE_LVGL_CANVAS
inline void lv_canvas_draw_img(lv_obj_t *canvas, lv_coord_t x, lv_coord_t y, image::Image *image,
lv_draw_img_dsc_t *dsc) {
lv_canvas_draw_img(canvas, x, y, image->get_lv_img_dsc(), dsc);
}
#endif
#ifdef USE_LVGL_METER
inline lv_meter_indicator_t *lv_meter_add_needle_img(lv_obj_t *obj, lv_meter_scale_t *scale, esphome::image::Image *src,
lv_coord_t pivot_x, lv_coord_t pivot_y) {
return lv_meter_add_needle_img(obj, scale, src->get_lv_img_dsc(), pivot_x, pivot_y);
}
#endif // USE_LVGL_METER
#endif // USE_LVGL_IMAGE
#ifdef USE_LVGL_ANIMIMG
inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images) {
auto *dsc = static_cast<std::vector<lv_img_dsc_t *> *>(lv_obj_get_user_data(img));
auto *dsc = static_cast<std::vector<lv_image_dsc_t *> *>(lv_obj_get_user_data(img));
if (dsc == nullptr) {
// object will be lazily allocated but never freed.
dsc = new std::vector<lv_img_dsc_t *>(images.size()); // NOLINT
dsc = new std::vector<lv_image_dsc_t *>(images.size()); // NOLINT
lv_obj_set_user_data(img, dsc);
}
dsc->clear();
for (auto &image : images) {
dsc->push_back(image->get_lv_img_dsc());
dsc->push_back(image->get_lv_image_dsc());
}
lv_animimg_set_src(img, (const void **) dsc->data(), dsc->size());
}
#endif // USE_LVGL_ANIMIMG
#ifdef USE_LVGL_METER
int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int value);
#endif
// Parent class for things that wrap an LVGL object
class LvCompound {
public:
@@ -130,7 +130,7 @@ class LvPageType : public Parented<LvglComponent> {
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 event_callback_t = void(lv_event_t *);
using text_lambda_t = std::function<const char *()>;
template<typename... Ts> class ObjUpdateAction : public Action<Ts...> {
@@ -152,7 +152,7 @@ class LvglComponent : public PollingComponent {
public:
LvglComponent(std::vector<display::Display *> displays, float buffer_frac, bool full_refresh, int draw_rounding,
bool resume_on_input, bool update_when_display_idle);
static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p);
static void static_flush_cb(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p);
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void setup() override;
@@ -160,11 +160,11 @@ class LvglComponent : public PollingComponent {
void loop() override;
template<typename F> void add_on_idle_callback(F &&callback) { this->idle_callbacks_.add(std::forward<F>(callback)); }
static void monitor_cb(lv_disp_drv_t *disp_drv, uint32_t time, uint32_t px);
static void render_start_cb(lv_disp_drv_t *disp_drv);
static void render_end_cb(lv_event_t *event);
static void render_start_cb(lv_event_t *event);
void dump_config() override;
lv_disp_t *get_disp() { return this->disp_; }
lv_obj_t *get_scr_act() { return lv_disp_get_scr_act(this->disp_); }
lv_obj_t *get_screen_active() { return lv_display_get_screen_active(this->disp_); }
// Pause or resume the display.
// @param paused If true, pause the display. If false, resume the display.
// @param show_snow If true, show the snow effect when paused.
@@ -187,11 +187,13 @@ class LvglComponent : public PollingComponent {
static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2);
static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2,
lv_event_code_t event3);
void add_page(LvPageType *page);
void show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time);
void show_next_page(lv_scr_load_anim_t anim, uint32_t time);
void show_prev_page(lv_scr_load_anim_t anim, uint32_t time);
void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; }
void set_big_endian(bool big_endian) { this->big_endian_ = big_endian; }
size_t get_current_page() const;
void set_focus_mark(lv_group_t *group) { this->focus_marks_[group] = lv_group_get_focused(group); }
void restore_focus_mark(lv_group_t *group) {
@@ -216,22 +218,25 @@ class LvglComponent : public PollingComponent {
void draw_start_() const { this->draw_start_callback_->trigger(); }
void write_random_();
void draw_buffer_(const lv_area_t *area, lv_color_t *ptr);
void flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p);
void draw_buffer_(const lv_area_t *area, lv_color_data *ptr);
void flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p);
std::vector<display::Display *> displays_{};
size_t buffer_frac_{1};
bool full_refresh_{};
bool resume_on_input_{};
bool update_when_display_idle_{};
lv_disp_draw_buf_t draw_buf_{};
lv_disp_drv_t disp_drv_{};
lv_disp_t *disp_{};
uint8_t *draw_buf_{};
lv_display_t *disp_{};
uint16_t width_{};
uint16_t height_{};
bool paused_{};
std::vector<LvPageType *> pages_{};
size_t current_page_{0};
bool show_snow_{};
bool page_wrap_{true};
bool big_endian_{};
std::map<lv_group_t *, lv_obj_t *> focus_marks_{};
CallbackManager<void(uint32_t)> idle_callbacks_{};
@@ -239,7 +244,7 @@ class LvglComponent : public PollingComponent {
Trigger<> *resume_callback_{};
Trigger<> *draw_start_callback_{};
Trigger<> *draw_end_callback_{};
lv_color_t *rotate_buf_{};
void *rotate_buf_{};
};
class IdleTrigger : public Trigger<> {
@@ -278,19 +283,37 @@ class LVTouchListener : public touchscreen::TouchListener, public Parented<LvglC
touch_pressed_ = false;
this->parent_->maybe_wakeup();
}
lv_indev_drv_t *get_drv() { return &this->drv_; }
lv_indev_t *get_drv() { return this->drv_; }
protected:
lv_indev_drv_t drv_{};
lv_indev_t *drv_{};
touchscreen::TouchPoint touch_point_{};
bool touch_pressed_{};
};
#endif // USE_LVGL_TOUCHSCREEN
#ifdef USE_LVGL_METER
class IndicatorLine : public LvCompound {
public:
IndicatorLine() = default;
void set_obj(lv_obj_t *lv_obj) override;
void set_value(int value);
private:
void update_length_();
int16_t angle_{};
lv_point_precise_t points_[2]{};
};
#endif
#ifdef USE_LVGL_KEY_LISTENER
class LVEncoderListener : public Parented<LvglComponent> {
public:
LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt);
LVEncoderListener(lv_indev_type_t type, uint16_t long_press_time, uint16_t long_press_repeat_time);
#ifdef USE_BINARY_SENSOR
void add_button(binary_sensor::BinarySensor *button, lv_key_t key) {
@@ -322,10 +345,10 @@ class LVEncoderListener : public Parented<LvglComponent> {
}
}
lv_indev_drv_t *get_drv() { return &this->drv_; }
lv_indev_t *get_drv() { return this->drv_; }
protected:
lv_indev_drv_t drv_{};
lv_indev_t *drv_{};
bool pressed_{};
int32_t count_{};
int32_t last_count_{};
@@ -336,14 +359,13 @@ class LVEncoderListener : public Parented<LvglComponent> {
#ifdef USE_LVGL_LINE
class LvLineType : public LvCompound {
public:
std::vector<lv_point_t> get_points() { return this->points_; }
void set_points(std::vector<lv_point_t> points) {
void set_points(FixedVector<lv_point_precise_t> points) {
this->points_ = std::move(points);
lv_line_set_points(this->obj, this->points_.data(), this->points_.size());
lv_line_set_points(this->obj, this->points_.begin(), this->points_.size());
}
protected:
std::vector<lv_point_t> points_{};
FixedVector<lv_point_precise_t> points_{};
};
#endif
#if defined(USE_LVGL_DROPDOWN) || defined(LV_USE_ROLLER)
@@ -392,7 +414,7 @@ class LvRollerType : public LvSelectable {
class LvButtonMatrixType : public key_provider::KeyProvider, public LvCompound {
public:
void set_obj(lv_obj_t *lv_obj) override;
uint16_t get_selected() { return lv_btnmatrix_get_selected_btn(this->obj); }
uint16_t get_selected() { return lv_buttonmatrix_get_selected_button(this->obj); }
void set_key(size_t idx, uint8_t key) { this->key_map_[idx] = key; }
protected:
@@ -406,5 +428,4 @@ class LvKeyboardType : public key_provider::KeyProvider, public LvCompound {
void set_obj(lv_obj_t *lv_obj) override;
};
#endif // USE_LVGL_KEYBOARD
} // namespace lvgl
} // namespace esphome
} // namespace esphome::lvgl
-21
View File
@@ -1,21 +0,0 @@
//
// 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);
+3 -3
View File
@@ -52,9 +52,9 @@ async def to_code(config):
await value.get_lambda(),
event_code,
config[CONF_RESTORE_VALUE],
max_value=widget.get_max(),
min_value=widget.get_min(),
step=widget.get_step(),
max_value=widget.type.get_max(widget.config),
min_value=widget.type.get_min(widget.config),
step=widget.type.get_step(widget.config),
)
async with LambdaContext(EVENT_ARG) as event:
event.add(var.on_value())
+47 -15
View File
@@ -29,6 +29,7 @@ from .defines import (
CONF_SCROLLBAR_MODE,
CONF_TIME_FORMAT,
LV_GRAD_DIR,
get_remapped_uses,
)
from .helpers import CONF_IF_NAN, requires_component, validate_printf
from .layout import (
@@ -42,16 +43,17 @@ from .lvcode import LvglComponent, lv_event_t_ptr
from .types import (
LVEncoderListener,
LvType,
WidgetType,
lv_group_t,
lv_obj_t,
lv_pseudo_button_t,
lv_style_t,
)
from .widgets import WidgetType
# this will be populated later, in __init__.py to avoid circular imports.
WIDGET_TYPES: dict = {}
TIME_TEXT_SCHEMA = cv.Schema(
{
cv.Required(CONF_TIME_FORMAT): cv.string,
@@ -176,23 +178,28 @@ STYLE_PROPS = {
"height": lvalid.size,
"image_recolor": lvalid.lv_color,
"image_recolor_opa": lvalid.opacity,
"line_width": lvalid.lv_positive_int,
"line_dash_width": lvalid.lv_positive_int,
"line_dash_gap": lvalid.lv_positive_int,
"line_rounded": lvalid.lv_bool,
"line_color": lvalid.lv_color,
"line_dash_gap": lvalid.lv_positive_int,
"line_dash_width": lvalid.lv_positive_int,
"line_opa": lvalid.opacity,
"line_rounded": lvalid.lv_bool,
"line_width": lvalid.lv_positive_int,
"opa": lvalid.opacity,
"opa_layered": lvalid.opacity,
"outline_color": lvalid.lv_color,
"outline_opa": lvalid.opacity,
"outline_pad": lvalid.padding,
"outline_width": lvalid.pixels,
"length": lvalid.pixels_or_percent,
"pad_all": lvalid.padding,
"pad_bottom": lvalid.padding,
"pad_left": lvalid.padding,
"pad_right": lvalid.padding,
"pad_top": lvalid.padding,
"radial_offset": lvalid.size,
"shadow_color": lvalid.lv_color,
"shadow_offset_x": lvalid.lv_int,
"shadow_offset_y": lvalid.lv_int,
"shadow_ofs_x": lvalid.lv_int,
"shadow_ofs_y": lvalid.lv_int,
"shadow_opa": lvalid.opacity,
@@ -213,7 +220,13 @@ STYLE_PROPS = {
"transform_height": lvalid.pixels_or_percent,
"transform_pivot_x": lvalid.pixels_or_percent,
"transform_pivot_y": lvalid.pixels_or_percent,
"transform_zoom": lvalid.zoom,
"transform_rotation": lvalid.lv_angle,
"transform_scale": lvalid.scale,
"transform_scale_x": lvalid.scale,
"transform_scale_y": lvalid.scale,
"transform_skew_x": lvalid.lv_angle,
"transform_skew_y": lvalid.lv_angle,
"transform_zoom": lvalid.scale,
"translate_x": lvalid.pixels_or_percent,
"translate_y": lvalid.pixels_or_percent,
"max_height": lvalid.pixels_or_percent,
@@ -227,15 +240,23 @@ STYLE_PROPS = {
}
STYLE_REMAP = {
"bg_image_opa": "bg_img_opa",
"bg_image_recolor": "bg_img_recolor",
"bg_image_recolor_opa": "bg_img_recolor_opa",
"bg_image_src": "bg_img_src",
"bg_image_tiled": "bg_img_tiled",
"image_recolor": "img_recolor",
"image_recolor_opa": "img_recolor_opa",
"transform_angle": "transform_rotation",
"transform_zoom": "transform_scale",
"zoom": "scale",
"angle": "rotation",
"shadow_ofs_x": "shadow_offset_x",
"shadow_ofs_y": "shadow_offset_y",
"r_mod": "length",
}
def remap_property(prop):
if prop in STYLE_REMAP:
get_remapped_uses().add(prop)
return STYLE_REMAP[prop]
return prop
# Complete object style schema
STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend(
{
@@ -276,7 +297,7 @@ SET_STATE_SCHEMA = cv.Schema(
)
# Setting object flags
FLAG_SCHEMA = cv.Schema({cv.Optional(flag): lvalid.lv_bool for flag in df.OBJ_FLAGS})
FLAG_LIST = cv.ensure_list(df.LvConstant("LV_OBJ_FLAG_", *df.OBJ_FLAGS).one_of)
FLAG_LIST = cv.ensure_list(df.LV_OBJ_FLAG.one_of)
def part_schema(parts):
@@ -418,6 +439,17 @@ ALL_STYLES = {
}
def strip_defaults(schema: cv.Schema):
"""
Take a schema and remove any default values, also convert Required to Optional.
Useful for converting an object schema to a modify schema
:param schema: The original Schema
:return: A new schema with no defaults and all items optional
"""
return cv.Schema({cv.Optional(k): v for k, v in schema.schema.items()})
def container_schema(widget_type: WidgetType, extras=None):
"""
Create a schema for a container widget of a given type. All obj properties are available, plus
@@ -481,9 +513,9 @@ def any_widget_schema(extras=None):
container_validator, requires_component(required)
)
# Apply custom validation
value = widget_type.validate(value or {})
path = [key] if is_dict else [index, key]
with prepend_path(path):
value = widget_type.validate(value or {})
result.append({key: container_validator(value)})
return result
+3 -3
View File
@@ -6,7 +6,7 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/preferences.h"
#include "../lvgl.h"
#include "esphome/components/lvgl/lvgl_esphome.h"
namespace esphome {
namespace lvgl {
@@ -28,12 +28,12 @@ class LVGLSelect : public select::Select, public Component {
lv_obj_add_event_cb(
this->widget_->obj,
[](lv_event_t *e) {
auto *it = static_cast<LVGLSelect *>(e->user_data);
auto *it = static_cast<LVGLSelect *>(lv_event_get_user_data(e));
it->set_options_();
},
LV_EVENT_REFRESH, this);
auto lamb = [](lv_event_t *e) {
auto *self = static_cast<LVGLSelect *>(e->user_data);
auto *self = static_cast<LVGLSelect *>(lv_event_get_user_data(e));
self->publish();
};
lv_obj_add_event_cb(this->widget_->obj, lamb, LV_EVENT_VALUE_CHANGED, this);
+34 -9
View File
@@ -13,7 +13,7 @@ from .defines import (
)
from .helpers import add_lv_use
from .lvcode import LambdaContext, LocalVariable, lv
from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, STYLE_REMAP
from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, remap_property
from .types import ObjUpdateAction, lv_obj_t, lv_style_t
from .widgets import (
Widget,
@@ -26,6 +26,10 @@ from .widgets import (
from .widgets.obj import obj_spec
def has_style_props(config) -> bool:
return any(prop in config for prop in ALL_STYLES)
async def style_set(svar, style):
for prop, validator in ALL_STYLES.items():
if (value := style.get(prop)) is not None:
@@ -33,22 +37,44 @@ async def style_set(svar, style):
value = await validator.process(value)
if isinstance(value, list):
value = "|".join(value)
remapped_prop = STYLE_REMAP.get(prop, prop)
lv.call(f"style_set_{remapped_prop}", svar, literal(value))
lv.call(f"style_set_{remap_property(prop)}", svar, literal(value))
async def create_style(style, id_name):
async def create_style(id_name, style=None):
style_id = ID(id_name, True, lv_style_t)
svar = cg.new_Pvariable(style_id)
lv.style_init(svar)
await style_set(svar, style)
if style:
await style_set(svar, style)
return svar
class LVStyle:
"""
A class to lazily create a named style
"""
named_styles = {}
def __init__(self, id_name, style=None):
self.id_name = id_name
self.style = style
self._style_var = None
async def get_var(self):
if self._style_var is None:
self._style_var = await create_style(self.id_name + "_style", self.style)
return self._style_var
@classmethod
def get_style(cls, id_name):
return cls.named_styles.setdefault(id_name, LVStyle(id_name))
async def styles_to_code(config):
"""Convert styles to C__ code."""
for style in config.get(CONF_STYLE_DEFINITIONS, ()):
await create_style(style, style[CONF_ID].id)
await create_style(style[CONF_ID].id, style)
@automation.register_action(
@@ -81,8 +107,7 @@ async def theme_to_code(config):
for part, states in collect_parts(style).items():
styles[part] = {
state: await create_style(
props,
"_lv_theme_style_" + w_name + "_" + part + "_" + state,
"_lv_theme_style_" + w_name + "_" + part + "_" + state, props
)
for state, props in states.items()
}
@@ -90,7 +115,7 @@ async def theme_to_code(config):
async def add_top_layer(lv_component, config):
top_layer = lv.disp_get_layer_top(lv_component.get_disp())
top_layer = lv.disp_get_layer_top(lv_component.var.get_disp())
if top_conf := config.get(CONF_TOP_LAYER):
with LocalVariable("top_layer", lv_obj_t, top_layer) as top_layer_obj:
top_w = Widget(top_layer_obj, obj_spec, top_conf)
+3 -6
View File
@@ -1,19 +1,16 @@
#pragma once
#include <utility>
#include "esphome/components/text/text.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/preferences.h"
namespace esphome {
namespace lvgl {
class LVGLText : public text::Text {
public:
void set_control_lambda(std::function<void(const std::string)> control_lambda) {
this->control_lambda_ = std::move(control_lambda);
void set_control_lambda(const std::function<void(std::string)> &control_lambda) {
this->control_lambda_ = control_lambda;
if (this->initial_state_.has_value()) {
this->control_lambda_(this->initial_state_.value());
this->initial_state_.reset();
@@ -28,7 +25,7 @@ class LVGLText : public text::Text {
this->initial_state_ = value;
}
}
std::function<void(const std::string)> control_lambda_{};
std::function<void(std::string)> control_lambda_{};
optional<std::string> initial_state_{};
};
-2
View File
@@ -10,7 +10,6 @@ from .defines import (
CONF_TOUCHSCREENS,
)
from .helpers import lvgl_components_required
from .lvcode import lv
from .schemas import PRESS_TIME
from .types import LVTouchListener
@@ -40,5 +39,4 @@ async def touchscreens_to_code(lv_component, config):
lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds
lprt = tconf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds
listener = cg.new_Pvariable(tconf[CONF_ID], lpt, lprt, lv_component)
lv.indev_drv_register(listener.get_drv())
cg.add(touchscreen.register_listener(listener))
+2 -2
View File
@@ -30,7 +30,7 @@ from .lvcode import (
lvgl_static,
)
from .types import LV_EVENT
from .widgets import LvScrActType, get_scr_act, widget_map
from .widgets import LvScrActType, get_screen_active, widget_map
async def add_on_boot_triggers(triggers):
@@ -48,7 +48,7 @@ async def generate_triggers():
for w in widget_map.values():
if isinstance(w.type, LvScrActType):
w = get_scr_act(w.var)
w = get_screen_active(w.var)
if w.config:
for event, conf in {
+10 -144
View File
@@ -1,15 +1,9 @@
import sys
from esphome import automation, codegen as cg
from esphome.automation import register_action
from esphome.config_validation import Schema
from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE
from esphome.core import EsphomeError
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.const import CONF_TEXT, CONF_VALUE
from esphome.cpp_generator import MockObj
from esphome.cpp_types import esphome_ns
from .defines import lvgl_ns
from .lvcode import lv_expr
class LvType(cg.MockObjClass):
@@ -26,6 +20,10 @@ class LvType(cg.MockObjClass):
return None
return [arg[0] for arg in self.args]
@property
def name(self):
return self.base.removeprefix("lv_").removesuffix("_t")
class LvNumber(LvType):
def __init__(self, *args):
@@ -63,6 +61,7 @@ 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 = cg.global_ns.struct("lv_disp_t")
lv_color_t = cg.global_ns.struct("lv_color_t")
lv_opa_t = cg.global_ns.struct("lv_opa_t")
lv_group_t = cg.global_ns.struct("lv_group_t")
LVTouchListener = lvgl_ns.class_("LVTouchListener")
LVEncoderListener = lvgl_ns.class_("LVEncoderListener")
@@ -70,10 +69,11 @@ lv_obj_t = LvType("lv_obj_t")
lv_page_t = LvType("LvPageType", parents=(LvCompound,))
lv_img_t = LvType("lv_img_t")
lv_gradient_t = LvType("lv_grad_dsc_t")
lv_event_t = LvType("lv_event_t")
LV_EVENT = MockObj(base="LV_EVENT_", op="")
LV_STATE = MockObj(base="LV_STATE_", op="")
LV_BTNMATRIX_CTRL = MockObj(base="LV_BTNMATRIX_CTRL_", op="")
LV_BTNMATRIX_CTRL = MockObj(base="LV_BUTTONMATRIX_CTRL_", op="")
class LvText(LvType):
@@ -93,7 +93,7 @@ class LvBoolean(LvType):
super().__init__(
*args,
largs=[(cg.bool_, "x")],
lvalue=lambda w: w.is_checked(),
lvalue=lambda w: w.has_state(LV_STATE.CHECKED),
has_on_value=True,
**kwargs,
)
@@ -110,137 +110,3 @@ class LvSelect(LvType):
parents=parens,
**kwargs,
)
class WidgetType:
"""
Describes a type of Widget, e.g. "bar" or "line"
"""
def __init__(
self,
name: str,
w_type: LvType,
parts: tuple,
schema=None,
modify_schema=None,
lv_name=None,
is_mock: bool = False,
):
"""
:param name: The widget name, e.g. "bar"
:param w_type: The C type of the widget
:param parts: What parts this widget supports
:param schema: The config schema for defining a widget
:param modify_schema: A schema to update the widget, defaults to the same as the schema
:param lv_name: The name of the LVGL widget in the LVGL library, if different from the name
:param is_mock: Whether this widget is a mock widget, i.e. not a real LVGL widget
"""
self.name = name
self.lv_name = lv_name or name
self.w_type = w_type
self.parts = parts
if not isinstance(schema, Schema):
schema = Schema(schema or {})
self.schema = schema
if modify_schema is None:
modify_schema = schema
if not isinstance(modify_schema, Schema):
modify_schema = Schema(modify_schema)
self.modify_schema = modify_schema
self.mock_obj = MockObj(f"lv_{self.lv_name}", "_")
# Local import to avoid circular import
from .automation import update_to_code
from .schemas import WIDGET_TYPES, base_update_schema
if not is_mock:
if self.name in WIDGET_TYPES:
raise EsphomeError(f"Duplicate definition of widget type '{self.name}'")
WIDGET_TYPES[self.name] = self
# Register the update action automatically, adding widget-specific properties
register_action(
f"lvgl.{self.name}.update",
ObjUpdateAction,
base_update_schema(self, self.parts).extend(self.modify_schema),
synchronous=True,
)(update_to_code)
@property
def animated(self):
return False
@property
def required_component(self):
return None
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
"""
async 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 lv_expr.call(f"{self.lv_name}_create", parent)
def on_create(self, var: MockObj, config: dict):
"""
Called from to_code when the widget is created, to set up any initial properties
:param var: The variable representing the widget
:param config: Its configuration
"""
def get_uses(self):
"""
Get a list of other widgets used by this one
:return:
"""
return ()
def get_max(self, config: dict):
return sys.maxsize
def get_min(self, config: dict):
return -sys.maxsize
def get_step(self, config: dict):
return 1
def get_scale(self, config: dict):
return 1.0
def validate(self, value):
"""
Provides an opportunity for custom validation for a given widget type
:param value:
:return:
"""
return value
def final_validate(self, widget, update_config, widget_config, path):
"""
Allow final validation for a given widget type update action
:param widget: A widget
:param update_config: The configuration for the update action
:param widget_config: The configuration for the widget itself
:param path: The path to the widget, for error reporting
"""
class NumberType(WidgetType):
def get_max(self, config: dict):
return int(config.get(CONF_MAX_VALUE, 100))
def get_min(self, config: dict):
return int(config.get(CONF_MIN_VALUE, 0))
+274 -74
View File
@@ -2,9 +2,18 @@ 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_DEFAULT, CONF_GROUP, CONF_ID, CONF_STATE, CONF_TYPE
from esphome.core import ID, TimePeriod
from esphome.automation import register_action
from esphome.config_validation import Invalid, Schema
from esphome.const import (
CONF_DEFAULT,
CONF_GROUP,
CONF_ID,
CONF_MAX_VALUE,
CONF_MIN_VALUE,
CONF_STATE,
CONF_TYPE,
)
from esphome.core import ID, EsphomeError, TimePeriod
from esphome.coroutine import FakeAwaitable
from esphome.cpp_generator import MockObj
@@ -21,6 +30,7 @@ from ..defines import (
CONF_MAIN,
CONF_PAD_COLUMN,
CONF_PAD_ROW,
CONF_SCALE,
CONF_STYLES,
CONF_WIDGETS,
OBJ_FLAGS,
@@ -44,8 +54,15 @@ from ..lvcode import (
lv_obj,
lv_Pvariable,
)
from ..schemas import ALL_STYLES, OBJ_PROPERTIES, STYLE_REMAP, WIDGET_TYPES
from ..types import LV_STATE, LvType, WidgetType, lv_coord_t, lv_obj_t, lv_obj_t_ptr
from ..types import (
LV_STATE,
LvCompound,
LvType,
ObjUpdateAction,
lv_coord_t,
lv_obj_t,
lv_obj_t_ptr,
)
EVENT_LAMB = "event_lamb__"
@@ -53,6 +70,171 @@ theme_widget_map = {}
styles_used = set()
class WidgetType:
"""
Describes a type of Widget, e.g. "bar" or "line"
"""
def __init__(
self,
name: str,
w_type: LvType,
parts: tuple,
schema=None,
modify_schema=None,
lv_name=None,
is_mock: bool = False,
):
"""
:param name: The widget name, e.g. "bar"
:param w_type: The C type of the widget
:param parts: What parts this widget supports
:param schema: The config schema for defining a widget
:param modify_schema: A schema to update the widget, defaults to the same as the schema
:param lv_name: The name of the LVGL widget in the LVGL library, if different from the name
:param is_mock: Whether this widget is a mock widget, i.e. not a real LVGL widget
"""
self.name = name
self.lv_name = lv_name or name
self.w_type = w_type
self.parts = parts
if not isinstance(schema, Schema):
schema = Schema(schema or {})
self.schema = schema
if modify_schema is None:
modify_schema = schema
if not isinstance(modify_schema, Schema):
modify_schema = Schema(modify_schema)
self.modify_schema = modify_schema
self.mock_obj = MockObj(f"lv_{self.lv_name}", "_")
# Local import to avoid circular import
from ..automation import update_to_code
from ..schemas import WIDGET_TYPES, base_update_schema
if not is_mock:
if self.name in WIDGET_TYPES:
raise EsphomeError(f"Duplicate definition of widget type '{self.name}'")
WIDGET_TYPES[self.name] = self
# Register the update action automatically, adding widget-specific properties
register_action(
f"lvgl.{self.name}.update",
ObjUpdateAction,
base_update_schema(self, self.parts).extend(self.modify_schema),
synchronous=True,
)(update_to_code)
@property
def animated(self):
return False
@property
def required_component(self):
return None
def is_compound(self):
return self.w_type.inherits_from(LvCompound)
async def create_to_code(self, config: dict, parent: MockObj) -> "Widget":
"""
Generate code for a widget creation.
:param config: The configuration for the widget
:param parent: The parent to which it should be attached
"""
creator = await self.obj_creator(parent, config)
add_lv_use(self.name)
add_lv_use(*self.get_uses())
wid = config[CONF_ID]
add_line_marks(wid)
if self.is_compound():
var = cg.new_Pvariable(wid)
lv_add(var.set_obj(creator))
await self.on_create(var.obj, config)
else:
var = lv_Pvariable(lv_obj_t, wid)
lv_assign(var, creator)
await self.on_create(var, config)
w = Widget.create(wid, var, self, config)
if theme := theme_widget_map.get(self.w_type.name):
for part, states in theme.items():
part = "LV_PART_" + part.upper()
for state, style in states.items():
state = "LV_STATE_" + state.upper()
if state == "LV_STATE_DEFAULT":
lv_state = literal(part)
elif part == "LV_PART_MAIN":
lv_state = literal(state)
else:
lv_state = join_enums((state, part))
w.add_style(style, lv_state)
await set_obj_properties(w, config)
await add_widgets(w, config)
await self.to_code(w, config)
return w
async def to_code(self, w: "Widget", config: dict):
"""
Update a widget, also called when creating
:param config:
:return:
"""
async def obj_creator(self, parent: MockObj, 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 lv_expr.call(f"{self.lv_name}_create", parent)
async def on_create(self, var: MockObj, config: dict):
"""
Called from to_code when the widget is created, to set up any initial properties
:param var: The variable representing the widget
:param config: Its configuration
"""
def get_uses(self):
"""
Get a list of other widgets used by this one
:return:
"""
return ()
def get_max(self, config: dict):
return sys.maxsize
def get_min(self, config: dict):
return -sys.maxsize
def get_step(self, config: dict):
return 1
def get_scale(self, config: dict):
return 1.0
def validate(self, value):
"""
Provides an opportunity for custom validation for a given widget type
:param value:
:return:
"""
return value
def final_validate(self, widget, update_config, widget_config, path):
"""
Allow final validation for a given widget type update action
:param widget: A widget
:param update_config: The configuration for the update action
:param widget_config: The configuration for the widget itself
:param path: The path to the widget, for error reporting
"""
class Widget:
"""
Represents a Widget.
@@ -74,19 +256,25 @@ class Widget:
self.obj = var
self.outer = None
self.move_to_foreground = False
# Properties for linear equations
self.slope = None
self.y_int = None
@staticmethod
def create(name, var, wtype: WidgetType, config: dict = None):
w = Widget(var, wtype, config)
if name is not None:
widget_map[name] = w
widget_map[name] = w
return w
def add_state(self, state):
if "|" in state:
state = f"(lv_state_t)({state})"
return lv_obj.add_state(self.obj, literal(state))
def clear_state(self, state):
return lv_obj.clear_state(self.obj, literal(state))
if "|" in state:
state = f"(lv_state_t)({state})"
return lv_obj.remove_state(self.obj, literal(state))
def has_state(self, state):
return (lv_expr.obj_get_state(self.obj) & literal(state)) != 0
@@ -98,12 +286,23 @@ class Widget:
return self.has_state(LV_STATE.CHECKED)
def add_flag(self, flag):
if "|" in flag:
flag = f"(lv_obj_flag_t)({flag})"
return lv_obj.add_flag(self.obj, literal(flag))
def clear_flag(self, flag):
return lv_obj.clear_flag(self.obj, literal(flag))
if "|" in flag:
flag = f"(lv_obj_flag_t)({flag})"
return lv_obj.remove_flag(self.obj, literal(flag))
async def set_property(self, prop, value, animated: bool = None, lv_name=None):
def add_style(self, style_id, state=LV_STATE.DEFAULT):
if "|" in state:
state = f"(lv_state_t)({state})"
lv_obj.add_style(self.obj, MockObj(style_id), literal(state))
async def set_property(
self, prop, value, animated: bool = None, lv_name=None, processor=None
):
"""
Set a property of the widget.
:param prop: The property name
@@ -111,18 +310,28 @@ class Widget:
:param animated: If the change should be animated
:param lv_name: The base type of the widget e.g. "obj"
"""
from ..schemas import ALL_STYLES, remap_property
if isinstance(value, dict):
value = value.get(prop)
if isinstance(ALL_STYLES.get(prop), LValidator):
value = await ALL_STYLES[prop].process(value)
else:
value = literal(value)
if value is None:
if value is None:
return
if not processor and isinstance(ALL_STYLES.get(prop), LValidator):
processor = ALL_STYLES[prop]
if isinstance(processor, LValidator):
processor = processor.process
if processor:
value = await processor(value)
elif value is None:
return
prop = remap_property(prop)
if isinstance(value, TimePeriod):
value = value.total_milliseconds
if isinstance(value, str):
elif isinstance(value, str):
value = literal(value)
elif isinstance(value, ID):
value = MockObj(value)
lv_name = lv_name or self.type.lv_name
if animated is None or self.type.animated is not True:
lv.call(f"{lv_name}_set_{prop}", self.obj, value)
@@ -138,10 +347,12 @@ class Widget:
ltype = ltype or self.__type_base()
return cg.RawExpression(f"lv_{ltype}_get_{prop}({self.obj})")
def set_style(self, prop, value, state):
def set_style(self, prop, value, state=LV_STATE.DEFAULT):
if value is None:
return
styles_used.add(prop)
if isinstance(value, str):
value = literal(value)
lv.call(f"obj_set_style_{prop}", self.obj, value, state)
def __type_base(self):
@@ -189,15 +400,6 @@ class Widget:
"""
return
def get_max(self):
return self.type.get_max(self.config)
def get_min(self):
return self.type.get_min(self.config)
def get_step(self):
return self.type.get_step(self.config)
def get_scale(self):
return self.type.get_scale(self.config)
@@ -212,14 +414,14 @@ class LvScrActType(WidgetType):
"""
def __init__(self):
super().__init__("lv_scr_act()", lv_obj_t, (), is_mock=True)
super().__init__("lv_screen_active()", lv_obj_t, (), is_mock=True)
async def to_code(self, w, config: dict):
return []
pass
def get_scr_act(lv_comp: MockObj) -> Widget:
return Widget.create(None, lv_comp.get_scr_act(), LvScrActType(), {})
def get_screen_active(lv_comp: MockObj) -> Widget:
return Widget(lv_comp.get_screen_active(), LvScrActType(), {})
def get_widget_generator(wid):
@@ -271,10 +473,17 @@ def collect_props(config):
:param config:
:return:
"""
from ..schemas import ALL_STYLES
props = {}
for prop in [*ALL_STYLES, *OBJ_FLAGS, CONF_STYLES, CONF_GROUP]:
if prop in config:
props[prop] = config[prop]
if prop == CONF_SCALE:
props[CONF_SCALE + "_x"] = config[prop]
props[CONF_SCALE + "_y"] = config[prop]
else:
props[prop] = config[prop]
return props
@@ -304,34 +513,41 @@ def collect_parts(config):
return parts
def _size_to_str(value):
if isinstance(value, float):
return f"lv_pct({int(value * 100)})"
return str(value)
async def set_obj_properties(w: Widget, config):
"""Generate a list of C++ statements to apply properties to an lv_obj_t"""
from ..schemas import ALL_STYLES, OBJ_PROPERTIES, remap_property
if layout := config.get(CONF_LAYOUT):
layout_type: str = layout[CONF_TYPE]
add_lv_use(layout_type)
lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}"))
if (pad_row := layout.get(CONF_PAD_ROW)) is not None:
w.set_style(CONF_PAD_ROW, pad_row, 0)
w.set_style(CONF_PAD_ROW, pad_row)
if (pad_column := layout.get(CONF_PAD_COLUMN)) is not None:
w.set_style(CONF_PAD_COLUMN, pad_column, 0)
w.set_style(CONF_PAD_COLUMN, pad_column)
if layout_type == TYPE_GRID:
wid = config[CONF_ID]
rows = [str(x) for x in layout[CONF_GRID_ROWS]]
rows = [_size_to_str(x) for x in layout[CONF_GRID_ROWS]]
rows = "{" + ",".join(rows) + ", LV_GRID_TEMPLATE_LAST}"
row_id = ID(f"{wid}_row_dsc", is_declaration=True, type=lv_coord_t)
row_array = cg.static_const_array(row_id, cg.RawExpression(rows))
w.set_style("grid_row_dsc_array", row_array, 0)
columns = [str(x) for x in layout[CONF_GRID_COLUMNS]]
w.set_style("grid_row_dsc_array", row_array)
columns = [_size_to_str(x) for x in layout[CONF_GRID_COLUMNS]]
columns = "{" + ",".join(columns) + ", LV_GRID_TEMPLATE_LAST}"
column_id = ID(f"{wid}_column_dsc", is_declaration=True, type=lv_coord_t)
column_array = cg.static_const_array(column_id, cg.RawExpression(columns))
w.set_style("grid_column_dsc_array", column_array, 0)
w.set_style("grid_column_dsc_array", column_array)
w.set_style(
CONF_GRID_COLUMN_ALIGN, literal(layout.get(CONF_GRID_COLUMN_ALIGN)), 0
)
w.set_style(
CONF_GRID_ROW_ALIGN, literal(layout.get(CONF_GRID_ROW_ALIGN)), 0
CONF_GRID_COLUMN_ALIGN, literal(layout.get(CONF_GRID_COLUMN_ALIGN))
)
w.set_style(CONF_GRID_ROW_ALIGN, literal(layout.get(CONF_GRID_ROW_ALIGN)))
if layout_type == TYPE_FLEX:
lv_obj.set_flex_flow(w.obj, literal(layout[CONF_FLEX_FLOW]))
main = literal(layout[CONF_FLEX_ALIGN_MAIN])
@@ -353,13 +569,13 @@ async def set_obj_properties(w: Widget, config):
else:
lv_state = join_enums((state, part))
for style_id in props.get(CONF_STYLES, ()):
lv_obj.add_style(w.obj, MockObj(style_id), lv_state)
w.add_style(style_id, lv_state)
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)
prop_r = STYLE_REMAP.get(prop, prop)
prop_r = remap_property(prop)
w.set_style(prop_r, value, lv_state)
if group := config.get(CONF_GROUP):
group = await cg.get_variable(group)
@@ -429,7 +645,7 @@ async def add_widgets(parent: Widget, config: dict):
await widget_to_code(w_cnfig, w_type, parent.obj)
async def widget_to_code(w_cnfig, w_type: WidgetType, parent):
async def widget_to_code(w_cnfig, w_type: WidgetType | str, parent) -> Widget:
"""
Converts a Widget definition to C code.
:param w_cnfig: The widget configuration
@@ -437,34 +653,18 @@ async def widget_to_code(w_cnfig, w_type: WidgetType, parent):
:param parent: The parent to which the widget should be added
:return:
"""
spec: WidgetType = WIDGET_TYPES[w_type]
creator = await 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))
spec.on_create(var.obj, w_cnfig)
else:
var = lv_Pvariable(lv_obj_t, wid)
lv_assign(var, creator)
spec.on_create(var, w_cnfig)
w = Widget.create(wid, var, spec, w_cnfig)
if theme := theme_widget_map.get(w_type):
for part, states in theme.items():
part = "LV_PART_" + part.upper()
for state, style in states.items():
state = "LV_STATE_" + state.upper()
if state == "LV_STATE_DEFAULT":
lv_state = literal(part)
elif part == "LV_PART_MAIN":
lv_state = literal(state)
else:
lv_state = join_enums((state, part))
lv.obj_add_style(w.obj, style, lv_state)
await set_obj_properties(w, w_cnfig)
await add_widgets(w, w_cnfig)
await spec.to_code(w, w_cnfig)
from ..schemas import WIDGET_TYPES
spec: WidgetType = (
w_type if isinstance(w_type, WidgetType) else WIDGET_TYPES[w_type]
)
return await spec.create_to_code(w_cnfig, parent)
class NumberType(WidgetType):
def get_max(self, config: dict):
return int(config.get(CONF_MAX_VALUE, 100))
def get_min(self, config: dict):
return int(config.get(CONF_MIN_VALUE, 0))
+13 -34
View File
@@ -18,7 +18,8 @@ from ..defines import (
CONF_KNOB,
CONF_MAIN,
CONF_START_ANGLE,
literal,
LV_OBJ_FLAG,
LV_PART,
)
from ..lv_validation import (
get_start_value,
@@ -28,8 +29,8 @@ from ..lv_validation import (
lv_positive_int,
)
from ..lvcode import lv, lv_expr, lv_obj
from ..types import LvNumber, NumberType
from . import Widget
from ..types import LvNumber
from . import NumberType, Widget
CONF_ARC = "arc"
ARC_SCHEMA = cv.Schema(
@@ -71,39 +72,17 @@ class ArcType(NumberType):
)
async def to_code(self, w: Widget, config):
if CONF_MIN_VALUE in config and CONF_MAX_VALUE in config:
max_value = await lv_int.process(config[CONF_MAX_VALUE])
min_value = await lv_int.process(config[CONF_MIN_VALUE])
lv.arc_set_range(w.obj, min_value, max_value)
elif CONF_MIN_VALUE in config:
max_value = w.get_property(CONF_MAX_VALUE)
min_value = await lv_int.process(config[CONF_MIN_VALUE])
lv.arc_set_range(w.obj, min_value, max_value)
elif CONF_MAX_VALUE in config:
max_value = await lv_int.process(config[CONF_MAX_VALUE])
min_value = w.get_property(CONF_MIN_VALUE)
lv.arc_set_range(w.obj, min_value, max_value)
await w.set_property(
"bg_start_angle",
await lv_angle_degrees.process(config.get(CONF_START_ANGLE)),
)
await w.set_property(
"bg_end_angle", await lv_angle_degrees.process(config.get(CONF_END_ANGLE))
)
await w.set_property(
CONF_ROTATION, await lv_angle_degrees.process(config.get(CONF_ROTATION))
)
await w.set_property(CONF_MODE, config)
await w.set_property(
CONF_CHANGE_RATE,
await lv_positive_int.process(config.get(CONF_CHANGE_RATE)),
)
for prop, validator in ARC_MODIFY_SCHEMA.schema.items():
if prop != CONF_VALUE:
# start_angle and end_angle are mapped to bg_start_angle and bg_end_angle
prop = str(prop)
if prop.endswith("_angle"):
prop = "bg_" + prop
await w.set_property(prop, config, processor=validator)
if CONF_ADJUSTABLE in config:
if not config[CONF_ADJUSTABLE]:
lv_obj.remove_style(w.obj, nullptr, literal("LV_PART_KNOB"))
w.clear_flag("LV_OBJ_FLAG_CLICKABLE")
lv_obj.remove_style(w.obj, nullptr, LV_PART.KNOB)
w.clear_flag(LV_OBJ_FLAG.CLICKABLE)
elif CONF_GROUP not in config:
# For some reason arc does not get automatically added to the default group
lv.group_add_obj(lv_expr.group_get_default(), w.obj)
+4 -4
View File
@@ -7,11 +7,11 @@ from ..helpers import add_lv_use
from ..lv_validation import lv_text
from ..lvcode import lv, lv_expr
from ..schemas import TEXT_SCHEMA
from ..types import LvBoolean, WidgetType
from . import Widget
from ..types import LvBoolean
from . import Widget, WidgetType
from .label import label_spec
lv_button_t = LvBoolean("lv_btn_t")
lv_button_t = LvBoolean("lv_button_t")
class ButtonType(WidgetType):
@@ -30,7 +30,7 @@ class ButtonType(WidgetType):
def get_uses(self):
return ("btn",)
def on_create(self, var: MockObj, config: dict):
async def on_create(self, var: MockObj, config: dict):
if CONF_TEXT in config:
lv.label_create(var)
return var
+16 -20
View File
@@ -118,15 +118,15 @@ class MatrixButton(Widget):
def has_state(self, state):
state = self.map_ctrls(state)
return lv_expr.btnmatrix_has_btn_ctrl(self.obj, self.index, state)
return lv_expr.buttonmatrix_has_button_ctrl(self.obj, self.index, state)
def add_state(self, state):
state = self.map_ctrls(state)
return lv.btnmatrix_set_btn_ctrl(self.obj, self.index, state)
return lv.buttonmatrix_set_button_ctrl(self.obj, self.index, state)
def clear_state(self, state):
state = self.map_ctrls(state)
return lv.btnmatrix_clear_btn_ctrl(self.obj, self.index, state)
return lv.buttonmatrix_clear_button_ctrl(self.obj, self.index, state)
def is_pressed(self):
return self.is_selected() & self.parent.has_state(LV_STATE.PRESSED)
@@ -161,7 +161,7 @@ async def get_button_data(config, buttonmatrix: Widget):
text_list.append(button_conf.get(CONF_TEXT) or "")
key_list.append(button_conf.get(CONF_KEY_CODE) or 0)
width_list.append(button_conf[CONF_WIDTH])
ctrl = ["LV_BTNMATRIX_CTRL_CLICK_TRIG"]
ctrl = ["CLICK_TRIG"]
for item in button_conf.get(CONF_CONTROL, ()):
ctrl.extend([k for k, v in item.items() if v])
ctrl_list.append(await BUTTONMATRIX_CTRLS.process(ctrl))
@@ -187,7 +187,7 @@ class ButtonMatrixType(WidgetType):
(CONF_MAIN, CONF_ITEMS),
BUTTONMATRIX_SCHEMA,
{},
lv_name="btnmatrix",
lv_name="buttonmatrix",
)
async def to_code(self, w: Widget, config):
@@ -199,22 +199,22 @@ class ButtonMatrixType(WidgetType):
)
text_id = config[CONF_BUTTON_TEXT_LIST_ID]
text_id = cg.static_const_array(text_id, text_list)
lv.btnmatrix_set_map(w.obj, text_id)
lv.buttonmatrix_set_map(w.obj, text_id)
set_btn_data(w.obj, ctrl_list, width_list)
lv.btnmatrix_set_one_checked(w.obj, config[CONF_ONE_CHECKED])
lv.buttonmatrix_set_one_checked(w.obj, config[CONF_ONE_CHECKED])
for index, key in enumerate(key_list):
if key != 0:
lv_add(w.var.set_key(index, key))
def get_uses(self):
return ("btnmatrix",)
return ("buttonmatrix",)
def set_btn_data(obj, ctrl_list, width_list):
for index, ctrl in enumerate(ctrl_list):
lv.btnmatrix_set_btn_ctrl(obj, index, ctrl)
lv.buttonmatrix_set_button_ctrl(obj, index, ctrl)
for index, width in enumerate(width_list):
lv.btnmatrix_set_btn_width(obj, index, width)
lv.buttonmatrix_set_button_width(obj, index, width)
buttonmatrix_spec = ButtonMatrixType()
@@ -253,25 +253,21 @@ async def button_update_to_code(config, action_id, template_arg, args):
async def do_button_update(w):
if (width := config.get(CONF_WIDTH)) is not None:
lv.btnmatrix_set_btn_width(w.obj, w.index, width)
lv.buttonmatrix_set_button_width(w.obj, w.index, width)
if config.get(CONF_SELECTED):
lv.btnmatrix_set_selected_btn(w.obj, w.index)
lv.buttonmatrix_set_selected_button(w.obj, w.index)
if controls := config.get(CONF_CONTROL):
adds = []
clrs = []
for item in controls:
adds.extend(
[f"LV_BTNMATRIX_CTRL_{k.upper()}" for k, v in item.items() if v]
)
clrs.extend(
[f"LV_BTNMATRIX_CTRL_{k.upper()}" for k, v in item.items() if not v]
)
adds.extend([f"{k.upper()}" for k, v in item.items() if v])
clrs.extend([f"{k.upper()}" for k, v in item.items() if not v])
if adds:
lv.btnmatrix_set_btn_ctrl(
lv.buttonmatrix_set_button_ctrl(
w.obj, w.index, await BUTTONMATRIX_CTRLS.process(adds)
)
if clrs:
lv.btnmatrix_clear_btn_ctrl(
lv.buttonmatrix_clear_button_ctrl(
w.obj, w.index, await BUTTONMATRIX_CTRLS.process(clrs)
)
+193 -85
View File
@@ -1,3 +1,22 @@
"""
LVGL 9.4 Canvas Widget Implementation
This module implements the canvas widget for LVGL 9.4. Key changes from LVGL 8.4:
1. Buffer allocation:
- LV_IMG_CF_TRUE_COLOR LV_COLOR_FORMAT_RGB565
- LV_IMG_CF_TRUE_COLOR_ALPHA LV_COLOR_FORMAT_ARGB8888
- LV_CANVAS_BUF_SIZE_TRUE_COLOR LV_CANVAS_BUF_SIZE(w, h, bpp, stride)
2. Drawing API:
- All lv_canvas_draw_* functions removed
- Use layer-based drawing: lv_canvas_init_layer() / lv_canvas_finish_layer()
- Draw using low-level lv_draw_* functions (rect, line, arc, image, label)
3. Pixel operations:
- lv_canvas_set_px_color + lv_canvas_set_px_opa lv_canvas_set_px(color, opa)
"""
from esphome import automation, codegen as cg, config_validation as cv
from esphome.components.display_menu_base import CONF_LABEL
from esphome.const import (
@@ -9,7 +28,7 @@ from esphome.const import (
CONF_X,
CONF_Y,
)
from esphome.cpp_generator import Literal, MockObj
from esphome.cpp_types import FixedVector
from ..automation import action_to_code
from ..defines import (
@@ -19,8 +38,11 @@ from ..defines import (
CONF_PIVOT_X,
CONF_PIVOT_Y,
CONF_POINTS,
CONF_RADIUS,
CONF_SRC,
CONF_START_ANGLE,
addr,
get_color_formats,
literal,
)
from ..lv_validation import (
@@ -34,18 +56,20 @@ from ..lv_validation import (
size,
)
from ..lvcode import LocalVariable, lv, lv_assign, lv_expr
from ..schemas import STYLE_PROPS, STYLE_REMAP, TEXT_SCHEMA, point_schema
from ..types import LvType, ObjUpdateAction, WidgetType
from . import Widget, get_widgets
from .line import lv_point_t, process_coord
from ..schemas import STYLE_PROPS, TEXT_SCHEMA, point_schema, remap_property
from ..types import LvType, ObjUpdateAction
from . import Widget, WidgetType, get_widgets
from .img import CONF_IMAGE
from .line import lv_point_precise_t, process_coord
CONF_CANVAS = "canvas"
CONF_BUFFER_ID = "buffer_id"
CONF_MAX_WIDTH = "max_width"
CONF_TRANSPARENT = "transparent"
CONF_RADIUS = "radius"
CONF_DRAW_BUF_ID = "draw_buf_id"
lv_canvas_t = LvType("lv_canvas_t")
lv_draw_buf_t = LvType("lv_draw_buf_t")
class CanvasType(WidgetType):
@@ -59,32 +83,44 @@ class CanvasType(WidgetType):
cv.Required(CONF_WIDTH): size,
cv.Required(CONF_HEIGHT): size,
cv.Optional(CONF_TRANSPARENT, default=False): cv.boolean,
cv.GenerateID(CONF_DRAW_BUF_ID): cv.declare_id(lv_draw_buf_t),
}
),
modify_schema={},
)
def get_uses(self):
return "img", CONF_LABEL
return CONF_IMAGE, CONF_LABEL
async def to_code(self, w: Widget, config):
width = config[CONF_WIDTH]
height = config[CONF_HEIGHT]
use_alpha = "_ALPHA" if config[CONF_TRANSPARENT] else ""
buf_size = literal(
f"LV_CANVAS_BUF_SIZE_TRUE_COLOR{use_alpha}({width}, {height})"
# LVGL 9.4: Use LV_COLOR_FORMAT instead of LV_IMG_CF
# RGB565 is 16-bit (2 bytes per pixel), ARGB8888 is 32-bit (4 bytes per pixel)
if config[CONF_TRANSPARENT]:
color_format = "LV_COLOR_FORMAT_ARGB8888"
get_color_formats().add("ARGB8888")
else:
color_format = "LV_COLOR_FORMAT_NATIVE"
# LVGL 9.4: LV_CANVAS_BUF_SIZE(width, height, bits_per_pixel, stride)
# stride is 0 for default (width * bytes_per_pixel)
draw_buf = cg.new_Pvariable(config[CONF_DRAW_BUF_ID])
buf_size = literal(f"LV_DRAW_BUF_SIZE({width}, {height}, {color_format})")
lv.draw_buf_init(
draw_buf,
width,
height,
literal(color_format),
0,
lv_expr.malloc_core(buf_size),
literal(buf_size),
)
with LocalVariable("buf", cg.void, lv_expr.custom_mem_alloc(buf_size)) as buf:
cg.add(cg.RawExpression(f"memset({buf}, 0, {buf_size});"))
lv.canvas_set_buffer(
w.obj,
buf,
width,
height,
literal(f"LV_IMG_CF_TRUE_COLOR{use_alpha}"),
)
lv.draw_buf_set_flag(draw_buf, literal("LV_IMAGE_FLAGS_MODIFIABLE"))
lv.canvas_set_draw_buf(w.obj, draw_buf)
canvas_spec = CanvasType()
CanvasType()
@automation.register_action(
@@ -117,7 +153,7 @@ async def canvas_fill(config, action_id, template_arg, args):
{
cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t),
cv.Required(CONF_COLOR): lv_color,
cv.Optional(CONF_OPA): opacity,
cv.Optional(CONF_OPA, default="COVER"): opacity,
cv.Required(CONF_POINTS): cv.ensure_list(point_schema),
},
),
@@ -126,7 +162,7 @@ async def canvas_fill(config, action_id, template_arg, args):
async def canvas_set_pixel(config, action_id, template_arg, args):
widget = await get_widgets(config)
color = await lv_color.process(config[CONF_COLOR])
opa = await opacity.process(config.get(CONF_OPA))
opa = await opacity.process(config.get(CONF_OPA), "COVER")
points = [
(
await pixels.process(p[CONF_X]),
@@ -136,25 +172,11 @@ async def canvas_set_pixel(config, action_id, template_arg, args):
]
async def do_set_pixels(w: Widget):
if isinstance(color, MockObj):
for point in points:
x, y = point
lv.canvas_set_px_color(w.obj, x, y, color)
else:
with LocalVariable("color", "lv_color_t", color, modifier="") as color_var:
for point in points:
x, y = point
lv.canvas_set_px_color(w.obj, x, y, color_var)
if opa:
if isinstance(opa, Literal):
for point in points:
x, y = point
lv.canvas_set_px_opa(w.obj, x, y, opa)
else:
with LocalVariable("opa", "lv_opa_t", opa, modifier="") as opa_var:
for point in points:
x, y = point
lv.canvas_set_px_opa(w.obj, x, y, opa_var)
# LVGL 9.4: lv_canvas_set_px combines color and opacity
# Could optimize this for lambda values
for point in points:
x, y = point
lv.canvas_set_px(w.obj, x, y, color, opa)
return await action_to_code(
widget, do_set_pixels, action_id, template_arg, args, config
@@ -178,18 +200,21 @@ async def draw_to_code(config, dsc_type, props, do_draw, action_id, template_arg
y = await pixels.process(config.get(CONF_Y))
async def action_func(w: Widget):
with LocalVariable("dsc", f"lv_draw_{dsc_type}_dsc_t", modifier="") as dsc:
dsc_addr = literal(f"&{dsc}")
lv.call(f"draw_{dsc_type}_dsc_init", dsc_addr)
if CONF_OPA in config:
opa = await opacity.process(config[CONF_OPA])
lv_assign(dsc.opa, opa)
for prop, validator in props.items():
if prop in config:
value = await validator.process(config[prop])
mapped_prop = STYLE_REMAP.get(prop, prop)
lv_assign(getattr(dsc, mapped_prop), value)
await do_draw(w, x, y, dsc_addr)
# LVGL 9.4: Create a layer for drawing on canvas
with LocalVariable("layer", "lv_layer_t", modifier="") as layer:
lv.canvas_init_layer(w.obj, addr(layer))
with LocalVariable("dsc", f"lv_draw_{dsc_type}_dsc_t", modifier="") as dsc:
lv.call(f"draw_{dsc_type}_dsc_init", addr(dsc))
if CONF_OPA in config:
opa = await opacity.process(config[CONF_OPA])
lv_assign(dsc.opa, opa)
for prop, validator in props.items():
if prop in config:
value = await validator.process(config[prop])
mapped_prop = remap_property(prop)
lv_assign(getattr(dsc, mapped_prop), value)
await do_draw(addr(layer), x, y, dsc)
lv.canvas_finish_layer(w.obj, addr(layer))
return await action_to_code(
widget, action_func, action_id, template_arg, args, config
@@ -212,6 +237,8 @@ RECT_PROPS = {
"outline_opa",
"shadow_color",
"shadow_width",
"shadow_offset_x",
"shadow_offset_y",
"shadow_ofs_x",
"shadow_ofs_y",
"shadow_spread",
@@ -220,6 +247,24 @@ RECT_PROPS = {
}
def _draw_line(layer, dsc, points):
# LVGL 9.4: Use lv_draw_line for each line segment
with (
LocalVariable(
"points", FixedVector.template(lv_point_precise_t), points, modifier=""
) as points_var,
LocalVariable("i", "uint32_t", literal("0"), modifier="") as i,
):
# Draw lines between consecutive points
lv.append(
cg.RawStatement(f"for ({i} = 0; {i} != {points_var}.size() - 1; {i}++) {{")
)
lv_assign(dsc.p1, points_var[i])
lv_assign(dsc.p2, points_var[i + 1])
lv.draw_line(layer, addr(dsc))
lv.append(cg.RawStatement("}"))
@automation.register_action(
"lvgl.canvas.draw_rectangle",
ObjUpdateAction,
@@ -237,8 +282,14 @@ async def canvas_draw_rect(config, action_id, template_arg, args):
width = await pixels.process(config[CONF_WIDTH])
height = await pixels.process(config[CONF_HEIGHT])
async def do_draw_rect(w: Widget, x, y, dsc_addr):
lv.canvas_draw_rect(w.obj, x, y, width, height, dsc_addr)
async def do_draw_rect(layer, x, y, dsc):
# LVGL 9.4: Use lv_draw_rect with area
with LocalVariable("area", "lv_area_t", modifier="") as area:
lv_assign(area.x1, x)
lv_assign(area.y1, y)
lv_assign(area.x2, literal(f"{x} + {width} - 1"))
lv_assign(area.y2, literal(f"{y} + {height} - 1"))
lv.draw_rect(layer, addr(dsc), addr(area))
return await draw_to_code(
config, "rect", RECT_PROPS, do_draw_rect, action_id, template_arg, args
@@ -277,21 +328,55 @@ async def canvas_draw_text(config, action_id, template_arg, args):
text = await lv_text.process(config[CONF_TEXT])
max_width = await pixels.process(config[CONF_MAX_WIDTH])
async def do_draw_text(w: Widget, x, y, dsc_addr):
lv.canvas_draw_text(w.obj, x, y, max_width, dsc_addr, text)
async def do_draw_text(layer, x, y, dsc):
# LVGL 9.4: Use lv_draw_label with area and hint
with LocalVariable("area", "lv_area_t", modifier="") as area:
lv_assign(area.x1, x)
lv_assign(area.y1, y)
lv_assign(area.x2, literal(f"{x} + {max_width} - 1"))
lv_assign(area.y2, literal(f"{y} + LV_COORD_MAX"))
lv_assign(dsc.text, text)
lv.draw_label(layer, addr(dsc), addr(area))
return await draw_to_code(
config, "label", TEXT_PROPS, do_draw_text, action_id, template_arg, args
)
IMG_PROPS = {
"angle": STYLE_PROPS["transform_angle"],
"zoom": STYLE_PROPS["transform_zoom"],
"recolor": STYLE_PROPS["image_recolor"],
"recolor_opa": STYLE_PROPS["image_recolor_opa"],
"opa": STYLE_PROPS["opa"],
}
IMG_PROPS = (
"angle",
"rotation",
"scale_x",
"scale_y",
"skew_x",
"skew_y",
"scale",
"zoom",
"recolor",
"recolor_opa",
"opa",
)
def _scale_map(config):
config = {remap_property(p): v for p, v in config.items()}
if "scale" in config and {"scale_x", "scale_y"} & config.keys():
raise cv.Invalid("Cannot specify both scale and scale_x/scale_y")
if "scale" in config:
config.update({"scale_x": config["scale"], "scale_y": config["scale"]})
del config["scale"]
return config
def _get_prop_validator(prop):
return STYLE_PROPS.get(f"transform_{remap_property(prop)}") or STYLE_PROPS.get(prop)
def _prop_validator(prop):
def validator(value):
return _get_prop_validator(prop)(value)
return validator
@automation.register_action(
@@ -303,9 +388,9 @@ IMG_PROPS = {
cv.Required(CONF_SRC): lv_image,
cv.Optional(CONF_PIVOT_X, default=0): pixels,
cv.Optional(CONF_PIVOT_Y, default=0): pixels,
**{cv.Optional(prop): validator for prop, validator in IMG_PROPS.items()},
**{cv.Optional(prop): _prop_validator(prop) for prop in IMG_PROPS},
}
),
).add_extra(_scale_map),
synchronous=True,
)
async def canvas_draw_image(config, action_id, template_arg, args):
@@ -313,15 +398,29 @@ async def canvas_draw_image(config, action_id, template_arg, args):
pivot_x = await pixels.process(config[CONF_PIVOT_X])
pivot_y = await pixels.process(config[CONF_PIVOT_Y])
async def do_draw_image(w: Widget, x, y, dsc_addr):
dsc = MockObj(f"(*{dsc_addr})")
async def do_draw_image(layer, x, y, dsc):
# LVGL 9.4: Use lv_draw_image with area
lv_assign(dsc.src, src.get_lv_image_dsc())
if pivot_x or pivot_y:
# pylint :disable=no-member
lv_assign(dsc.pivot, literal(f"{{{pivot_x}, {pivot_y}}}"))
lv.canvas_draw_img(w.obj, x, y, src, dsc_addr)
lv_assign(dsc.pivot.x, pivot_x)
lv_assign(dsc.pivot.y, pivot_y)
with LocalVariable("area", "lv_area_t", modifier="") as area:
lv_assign(area.x1, x)
lv_assign(area.y1, y)
# Image size will be determined from the image descriptor
lv_assign(area.x2, x)
lv_assign(area.y2, y)
lv.draw_image(layer, addr(dsc), addr(area))
return await draw_to_code(
config, "img", IMG_PROPS, do_draw_image, action_id, template_arg, args
config,
"image",
{prop: _get_prop_validator(prop) for prop in IMG_PROPS},
do_draw_image,
action_id,
template_arg,
args,
)
@@ -354,11 +453,8 @@ async def canvas_draw_line(config, action_id, template_arg, args):
for p in config[CONF_POINTS]
]
async def do_draw_line(w: Widget, x, y, dsc_addr):
with LocalVariable(
"points", cg.std_vector.template(lv_point_t), points, modifier=""
) as points_var:
lv.canvas_draw_line(w.obj, points_var.data(), points_var.size(), dsc_addr)
async def do_draw_line(layer, _x, _y, dsc):
_draw_line(layer, dsc, points)
return await draw_to_code(
config, "line", LINE_PROPS, do_draw_line, action_id, template_arg, args
@@ -382,14 +478,20 @@ async def canvas_draw_polygon(config, action_id, template_arg, args):
[await process_coord(p[CONF_X]), await process_coord(p[CONF_Y])]
for p in config[CONF_POINTS]
]
# Close the polygon
points.append(points[0])
async def do_draw_polygon(w: Widget, x, y, dsc_addr):
with LocalVariable(
"points", cg.std_vector.template(lv_point_t), points, modifier=""
) as points_var:
lv.canvas_draw_polygon(
w.obj, points_var.data(), points_var.size(), dsc_addr
)
async def do_draw_polygon(layer, x, y, dsc):
# LVGL 9.4: Draw polygon using line drawing in a closed path
# Note: This draws outline only. For filled polygons, would need different approach
# Convert rect descriptor to line descriptor for polygon outline
with LocalVariable("line_dsc", "lv_draw_line_dsc_t", modifier="") as line_dsc:
lv.draw_line_dsc_init(addr(line_dsc))
# Copy border properties from rect descriptor to line descriptor
lv_assign(line_dsc.color, dsc.border_color)
lv_assign(line_dsc.width, dsc.border_width)
lv_assign(line_dsc.opa, dsc.border_opa)
_draw_line(layer, line_dsc, points)
return await draw_to_code(
config, "rect", RECT_PROPS, do_draw_polygon, action_id, template_arg, args
@@ -422,8 +524,14 @@ async def canvas_draw_arc(config, action_id, template_arg, args):
start_angle = await lv_angle_degrees.process(config[CONF_START_ANGLE])
end_angle = await lv_angle_degrees.process(config[CONF_END_ANGLE])
async def do_draw_arc(w: Widget, x, y, dsc_addr):
lv.canvas_draw_arc(w.obj, x, y, radius, start_angle, end_angle, dsc_addr)
async def do_draw_arc(layer, x, y, dsc):
# LVGL 9.4: Use lv_draw_arc with center point
lv_assign(dsc.center.x, x)
lv_assign(dsc.center.y, y)
lv_assign(dsc.start_angle, start_angle)
lv_assign(dsc.end_angle, end_angle)
lv_assign(dsc.radius, radius)
lv.draw_arc(layer, addr(dsc))
return await draw_to_code(
config, "arc", ARC_PROPS, do_draw_arc, action_id, template_arg, args
+4 -8
View File
@@ -1,11 +1,10 @@
import esphome.config_validation as cv
from esphome.const import CONF_HEIGHT, CONF_WIDTH
from esphome.cpp_generator import MockObj
from ..defines import CONF_CONTAINER, CONF_MAIN, CONF_OBJ, CONF_SCROLLBAR
from ..defines import CONF_CONTAINER, CONF_MAIN, CONF_SCROLLBAR
from ..lv_validation import size
from ..lvcode import lv
from ..types import WidgetType, lv_obj_t
from ..types import lv_obj_t
from . import WidgetType
CONTAINER_SCHEMA = cv.Schema(
{
@@ -28,12 +27,9 @@ class ContainerType(WidgetType):
(CONF_MAIN, CONF_SCROLLBAR),
schema=CONTAINER_SCHEMA,
modify_schema={},
lv_name=CONF_OBJ,
lv_name=CONF_CONTAINER,
)
self.styles = {}
def on_create(self, var: MockObj, config: dict):
lv.obj_remove_style_all(var)
container_spec = ContainerType()
+18 -31
View File
@@ -1,17 +1,22 @@
import esphome.config_validation as cv
from esphome.const import CONF_ANGLE, CONF_MODE, CONF_OFFSET_X, CONF_OFFSET_Y
from esphome.const import (
CONF_ANGLE,
CONF_MODE,
CONF_OFFSET_X,
CONF_OFFSET_Y,
CONF_ROTATION,
)
from ..defines import (
CONF_ANTIALIAS,
CONF_MAIN,
CONF_PIVOT_X,
CONF_PIVOT_Y,
CONF_SCALE,
CONF_SRC,
CONF_ZOOM,
LvConstant,
)
from ..lv_validation import lv_angle, lv_bool, lv_image, size, zoom
from ..lvcode import lv
from ..lv_validation import lv_angle, lv_bool, lv_image, scale, size
from ..types import lv_img_t
from . import Widget, WidgetType
from .label import CONF_LABEL
@@ -22,14 +27,14 @@ BASE_IMG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_PIVOT_X): size,
cv.Optional(CONF_PIVOT_Y): size,
cv.Optional(CONF_ANGLE): lv_angle,
cv.Optional(CONF_ZOOM): zoom,
cv.Exclusive(CONF_ANGLE, CONF_ROTATION): lv_angle,
cv.Exclusive(CONF_ROTATION, CONF_ROTATION): lv_angle,
cv.Exclusive(CONF_ZOOM, CONF_SCALE): scale,
cv.Exclusive(CONF_SCALE, CONF_SCALE): scale,
cv.Optional(CONF_OFFSET_X): size,
cv.Optional(CONF_OFFSET_Y): size,
cv.Optional(CONF_ANTIALIAS): lv_bool,
cv.Optional(CONF_MODE): LvConstant(
"LV_IMG_SIZE_MODE_", "VIRTUAL", "REAL"
).one_of,
cv.Optional(CONF_MODE): cv.invalid(f"{CONF_MODE} is not supported in LVGL 9.x"),
}
)
@@ -54,33 +59,15 @@ class ImgType(WidgetType):
(CONF_MAIN,),
IMG_SCHEMA,
IMG_MODIFY_SCHEMA,
lv_name="img",
)
def get_uses(self):
return "img", CONF_LABEL
return CONF_IMAGE, CONF_LABEL
async def to_code(self, w: Widget, config):
if src := config.get(CONF_SRC):
lv.img_set_src(w.obj, await lv_image.process(src))
if (pivot_x := config.get(CONF_PIVOT_X)) and (
pivot_y := config.get(CONF_PIVOT_Y)
):
lv.img_set_pivot(
w.obj, await size.process(pivot_x), await size.process(pivot_y)
)
if (cf_angle := config.get(CONF_ANGLE)) is not None:
lv.img_set_angle(w.obj, await lv_angle.process(cf_angle))
if (img_zoom := config.get(CONF_ZOOM)) is not None:
lv.img_set_zoom(w.obj, await zoom.process(img_zoom))
if (offset := config.get(CONF_OFFSET_X)) is not None:
lv.img_set_offset_x(w.obj, await size.process(offset))
if (offset := config.get(CONF_OFFSET_Y)) is not None:
lv.img_set_offset_y(w.obj, await size.process(offset))
if CONF_ANTIALIAS in config:
lv.img_set_antialias(w.obj, await lv_bool.process(config[CONF_ANTIALIAS]))
if mode := config.get(CONF_MODE):
await w.set_property("size_mode", mode)
await w.set_property(CONF_SRC, await lv_image.process(config.get(CONF_SRC)))
for prop, validator in BASE_IMG_SCHEMA.schema.items():
await w.set_property(prop, config, processor=validator)
img_spec = ImgType()
+2 -2
View File
@@ -11,8 +11,8 @@ from ..defines import (
)
from ..lv_validation import lv_bool, lv_text
from ..schemas import TEXT_SCHEMA
from ..types import LvText, WidgetType
from . import Widget
from ..types import LvText
from . import Widget, WidgetType
CONF_LABEL = "label"
+3 -3
View File
@@ -2,7 +2,7 @@ import esphome.config_validation as cv
from esphome.const import CONF_BRIGHTNESS, CONF_COLOR, CONF_LED
from ..defines import CONF_MAIN
from ..lv_validation import lv_brightness, lv_color
from ..lv_validation import lv_color, lv_percentage
from ..lvcode import lv
from ..types import LvType
from . import Widget, WidgetType
@@ -10,7 +10,7 @@ from . import Widget, WidgetType
LED_SCHEMA = cv.Schema(
{
cv.Optional(CONF_COLOR): lv_color,
cv.Optional(CONF_BRIGHTNESS): lv_brightness,
cv.Optional(CONF_BRIGHTNESS): lv_percentage,
}
)
@@ -23,7 +23,7 @@ class LedType(WidgetType):
if (color := config.get(CONF_COLOR)) is not None:
lv.led_set_color(w.obj, await lv_color.process(color))
if (brightness := config.get(CONF_BRIGHTNESS)) is not None:
lv.led_set_brightness(w.obj, await lv_brightness.process(brightness))
lv.led_set_brightness(w.obj, await lv_percentage.process(brightness))
led_spec = LedType()
+2 -4
View File
@@ -14,6 +14,7 @@ CONF_POINTS = "points"
CONF_POINT_LIST_ID = "point_list_id"
lv_point_t = cg.global_ns.struct("lv_point_t")
lv_point_precise_t = cg.global_ns.struct("lv_point_precise_t")
LINE_SCHEMA = {
@@ -23,10 +24,7 @@ LINE_SCHEMA = {
async def process_coord(coord):
if isinstance(coord, Lambda):
coord = call_lambda(await cg.process_lambda(coord, [], return_type=lv_coord_t))
if not coord.endswith("()"):
coord = f"static_cast<lv_coord_t>({coord})"
return cg.RawExpression(coord)
return call_lambda(await cg.process_lambda(coord, [], return_type=lv_coord_t))
return cg.safe_exp(coord)
+2 -2
View File
@@ -11,8 +11,8 @@ from ..defines import (
)
from ..lv_validation import animated, lv_int
from ..lvcode import lv
from ..types import LvNumber, NumberType
from . import Widget
from ..types import LvNumber
from . import NumberType, Widget
# Note this file cannot be called "bar.py" because that name is disallowed.
+438 -147
View File
@@ -1,9 +1,11 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components.image import get_image_metadata
import esphome.config_validation as cv
from esphome.const import (
CONF_COLOR,
CONF_COUNT,
CONF_HEIGHT,
CONF_ID,
CONF_ITEMS,
CONF_LENGTH,
@@ -13,22 +15,39 @@ from esphome.const import (
CONF_ROTATION,
CONF_VALUE,
CONF_WIDTH,
CONF_X,
)
from esphome.cpp_generator import MockObj
from esphome.cpp_types import nullptr
from .. import obj_spec, set_obj_properties
from ..automation import action_to_code
from ..defines import (
CHILD_ALIGNMENTS,
CONF_ALIGN,
CONF_CONTAINER,
CONF_END_VALUE,
CONF_INDICATOR,
CONF_LINE_WIDTH,
CONF_MAIN,
CONF_OPA,
CONF_PIVOT_X,
CONF_PIVOT_Y,
CONF_RADIUS,
CONF_SCALE,
CONF_SRC,
CONF_START_VALUE,
CONF_TICKS,
LV_OBJ_FLAG,
LV_PART,
LV_SCALE_MODE,
get_remapped_uses,
get_warnings,
)
from ..helpers import add_lv_use, lvgl_components_required
from ..helpers import add_lv_use
from ..lv_validation import (
LV_OPA,
LV_RADIUS,
get_end_value,
get_start_value,
lv_angle_degrees,
@@ -36,105 +55,168 @@ from ..lv_validation import (
lv_color,
lv_float,
lv_image,
lv_int,
opacity,
padding,
pixels,
pixels_or_percent,
pixels_or_percent_validator,
requires_component,
size,
)
from ..lvcode import LocalVariable, lv, lv_assign, lv_expr, lv_obj
from ..types import LvType, ObjUpdateAction
from . import Widget, WidgetType, get_widgets
from ..lvcode import LambdaContext, LocalVariable, lv, lv_add, lv_expr, lv_obj
from ..schemas import STATE_SCHEMA
from ..styles import LVStyle
from ..types import (
LV_EVENT,
LvCompound,
LvType,
ObjUpdateAction,
lv_event_t,
lv_img_t,
lv_obj_t,
)
from . import Widget, WidgetType, get_widgets, widget_to_code
from .arc import CONF_ARC
from .img import CONF_IMAGE
from .line import CONF_LINE
from .obj import obj_spec
CONF_ANGLE_RANGE = "angle_range"
CONF_COLOR_END = "color_end"
CONF_COLOR_START = "color_start"
CONF_DRAW_TICKS_ON_TOP = "draw_ticks_on_top"
CONF_IMAGE_ID = "image_id"
CONF_INDICATORS = "indicators"
CONF_LINE_ID = "line_id"
CONF_LABEL_GAP = "label_gap"
CONF_MAJOR = "major"
CONF_METER = "meter"
CONF_PIVOT = "pivot"
CONF_R_MOD = "r_mod"
CONF_RADIAL_OFFSET = "radial_offset"
CONF_SCALES = "scales"
CONF_STRIDE = "stride"
CONF_TICK_STYLE = "tick_style"
# LVGL 9.4 Migration: Use scale widget instead of removed meter widget
#
# The lv_meter widget was removed in LVGL 9.4 and replaced with the more
# flexible lv_scale widget. This implementation emulates meter functionality
# using the scale widget with the following mappings:
#
# - lv_meter -> lv_scale (set to LV_SCALE_MODE_ROUND_OUTER for circular meters)
# - lv_meter_scale -> scale configuration (range, ticks, etc.)
# - lv_meter_indicator -> lv_scale_section (colored ranges on the scale)
#
# For compatibility, keep meter types but map to scale
lv_scale_t = LvType("lv_obj_t")
lv_meter_t = LvType("lv_meter_t")
lv_meter_indicator_t = cg.global_ns.struct("lv_meter_indicator_t")
lv_meter_indicator_t_ptr = lv_meter_indicator_t.operator("ptr")
def pixels(value):
"""A size in one axis in pixels"""
if isinstance(value, str) and value.lower().endswith("px"):
return cv.int_(value[:-2])
return cv.int_(value)
lv_scale_section_t = LvType("lv_scale_section_t")
lv_meter_indicator_t = LvType("lv_meter_indicator_t")
lv_meter_indicator_ticks_t = LvType(
"lv_scale_section_t", parents=(lv_meter_indicator_t,)
)
lv_meter_indicator_arc_t = LvType("lv_scale_section_t", parents=(lv_meter_indicator_t,))
lv_meter_indicator_line_t = LvType(
"IndicatorLine",
parents=(
LvCompound,
lv_meter_indicator_t,
),
)
lv_meter_indicator_image_t = LvType("lv_image_t", parents=(lv_meter_indicator_t,))
DEFAULT_LABEL_GAP = 10 # Default label gap for major ticks added by LVGL
INDICATOR_LINE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_WIDTH, default=4): size,
cv.Optional(CONF_WIDTH, default=4): cv.int_,
cv.Optional(CONF_COLOR, default=0): lv_color,
cv.Optional(CONF_R_MOD, default=0): size,
cv.Optional(CONF_VALUE): lv_float,
cv.Optional(CONF_OPA): opacity,
cv.Optional(CONF_R_MOD): padding,
cv.Optional(CONF_LENGTH): pixels_or_percent_validator,
cv.Optional(CONF_RADIAL_OFFSET, 0): pixels_or_percent_validator,
cv.Optional(CONF_VALUE, default=0.0): lv_float,
cv.Optional(CONF_OPA, default=1.0): opacity,
}
)
).add_extra(cv.has_at_most_one_key(CONF_R_MOD, CONF_LENGTH))
class ScaleType(WidgetType):
"""
Will migrate to scale.py in due course
"""
def __init__(self):
super().__init__(
CONF_SCALE,
lv_scale_t,
(CONF_MAIN, CONF_ITEMS, CONF_INDICATOR),
{},
is_mock=True,
)
scale_spec = ScaleType()
INDICATOR_IMG_SCHEMA = cv.Schema(
{
cv.Required(CONF_SRC): lv_image,
cv.Required(CONF_PIVOT_X): pixels,
cv.Required(CONF_PIVOT_Y): pixels,
cv.Optional(CONF_PIVOT_X, default=0): pixels,
cv.Optional(CONF_PIVOT_Y): pixels,
cv.Optional(CONF_VALUE): lv_float,
cv.Optional(CONF_OPA): opacity,
cv.Optional(CONF_OPA, default=1.0): opacity,
}
)
INDICATOR_ARC_SCHEMA = cv.Schema(
{
cv.Optional(CONF_WIDTH, default=4): size,
cv.Optional(CONF_WIDTH, default=4): cv.int_,
cv.Optional(CONF_COLOR, default=0): lv_color,
cv.Optional(CONF_R_MOD, default=0): size,
cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float,
cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float,
cv.Optional(CONF_R_MOD): padding,
cv.Optional(CONF_VALUE): lv_float,
cv.Optional(CONF_START_VALUE): lv_float,
cv.Optional(CONF_END_VALUE): lv_float,
cv.Optional(CONF_OPA): opacity,
}
)
).add_extra(cv.has_at_most_one_key(CONF_VALUE, CONF_START_VALUE))
INDICATOR_TICKS_SCHEMA = cv.Schema(
{
cv.Optional(CONF_WIDTH, default=4): size,
cv.Optional(CONF_WIDTH, default=4): cv.int_,
cv.Optional(CONF_COLOR_START, default=0): lv_color,
cv.Optional(CONF_COLOR_END): lv_color,
cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float,
cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float,
cv.Optional(CONF_VALUE): lv_float,
cv.Optional(CONF_START_VALUE): lv_float,
cv.Optional(CONF_END_VALUE): lv_float,
cv.Optional(CONF_LOCAL, default=False): lv_bool,
}
)
).add_extra(cv.has_at_most_one_key(CONF_VALUE, CONF_START_VALUE))
INDICATOR_SCHEMA = cv.Schema(
{
cv.Exclusive(CONF_LINE, CONF_INDICATORS): INDICATOR_LINE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(lv_meter_indicator_t),
cv.GenerateID(): cv.declare_id(lv_meter_indicator_line_t),
}
),
cv.Exclusive(CONF_IMAGE, CONF_INDICATORS): cv.All(
INDICATOR_IMG_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(lv_meter_indicator_t),
cv.GenerateID(): cv.declare_id(lv_meter_indicator_image_t),
cv.GenerateID(CONF_IMAGE_ID): cv.declare_id(lv_img_t),
}
),
requires_component("image"),
),
cv.Exclusive(CONF_ARC, CONF_INDICATORS): INDICATOR_ARC_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(lv_meter_indicator_t),
cv.GenerateID(): cv.declare_id(lv_meter_indicator_arc_t),
}
),
cv.Exclusive(CONF_TICK_STYLE, CONF_INDICATORS): INDICATOR_TICKS_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(lv_meter_indicator_t),
cv.GenerateID(): cv.declare_id(lv_meter_indicator_ticks_t),
}
),
}
@@ -142,32 +224,96 @@ INDICATOR_SCHEMA = cv.Schema(
SCALE_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(lv_scale_t),
cv.Optional(CONF_TICKS): cv.Schema(
{
cv.Optional(CONF_COUNT, default=12): cv.positive_int,
cv.Optional(CONF_WIDTH, default=2): size,
cv.Optional(CONF_WIDTH, default=2): cv.positive_int,
cv.Optional(CONF_LENGTH, default=10): size,
cv.Optional(CONF_RADIAL_OFFSET, default=0): size,
cv.Optional(CONF_COLOR, default=0x808080): lv_color,
cv.Optional(CONF_MAJOR): cv.Schema(
{
cv.Optional(CONF_STRIDE, default=3): cv.positive_int,
cv.Optional(CONF_WIDTH, default=5): size,
cv.Optional(CONF_LENGTH, default="15%"): size,
cv.Optional(CONF_RADIAL_OFFSET, default=0): size,
cv.Optional(CONF_COLOR, default=0): lv_color,
cv.Optional(CONF_LABEL_GAP, default=4): size,
}
),
}
),
cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_,
cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_,
cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360),
cv.Optional(CONF_ROTATION): lv_angle_degrees,
cv.Optional(CONF_RANGE_FROM, default=0.0): lv_int,
cv.Optional(CONF_RANGE_TO, default=100.0): lv_int,
cv.Optional(CONF_ANGLE_RANGE, default=270): lv_angle_degrees,
cv.Optional(CONF_ROTATION, default=0): lv_angle_degrees,
cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA),
cv.Optional(CONF_DRAW_TICKS_ON_TOP, default=True): bool,
}
)
METER_SCHEMA = {cv.Optional(CONF_SCALES): cv.ensure_list(SCALE_SCHEMA)}
METER_SCHEMA = {
cv.Optional(CONF_PIVOT): STATE_SCHEMA,
cv.Optional(CONF_INDICATOR): STATE_SCHEMA,
cv.Optional(CONF_SCALES): cv.ensure_list(SCALE_SCHEMA),
}
LIGHT_STYLE = LVStyle(
"lv_meter_light",
{
"bg_opa": 1.0,
"bg_color": 0xEEEEEE,
"line_width": 1,
"line_color": 0xEEEEEE,
"arc_width": 2,
"arc_color": 0xEEEEEE,
"pad_all": 10,
"border_width": 2,
"border_color": 0xEEEEEE,
"radius": "LV_RADIUS_CIRCLE",
},
)
PIVOT_STYLE = {
CONF_RADIUS: LV_RADIUS.CIRCLE,
CONF_ALIGN: CHILD_ALIGNMENTS.CENTER,
"bg_color": 0x000000,
"bg_opa": 1.0,
CONF_WIDTH: 15,
CONF_HEIGHT: 15,
}
line_indicator_type = WidgetType(
CONF_INDICATOR,
lv_meter_indicator_line_t,
(CONF_MAIN,),
lv_name=CONF_LINE,
is_mock=True,
)
class SectionType(WidgetType):
def __init__(self):
super().__init__(
"scale_section",
lv_meter_indicator_arc_t,
(CONF_MAIN,),
is_mock=True,
lv_name="scale_section",
)
arc_indicator_type = SectionType()
image_indicator_type = WidgetType(
CONF_INDICATOR,
lv_meter_indicator_image_t,
(CONF_MAIN,),
lv_name=CONF_IMAGE,
is_mock=True,
)
class MeterType(WidgetType):
@@ -175,111 +321,240 @@ class MeterType(WidgetType):
super().__init__(
CONF_METER,
lv_meter_t,
# Note that mapping from 8.x to 9.x, indicator styling is applied to needles, and tick styling
# is migrated to indicator
(CONF_MAIN, CONF_INDICATOR, CONF_TICKS, CONF_ITEMS),
METER_SCHEMA,
lv_name=CONF_CONTAINER,
)
async def to_code(self, w: Widget, config):
"""For a meter object, create and set parameters"""
def get_uses(self):
return CONF_SCALE, CONF_LINE
lvgl_components_required.add(CONF_METER)
def validate(self, value):
return cv.has_at_most_one_key(CONF_INDICATOR, CONF_PIVOT)(value)
async def on_create(self, var: MockObj, config: dict):
# Remove theme styling from outer container
lv.obj_add_style(var, await LIGHT_STYLE.get_var(), LV_PART.MAIN)
async def create_to_code(self, config: dict, parent: MockObj):
"""For a meter object using scale widget, create and set parameters"""
add_lv_use(*self.get_uses())
outer_config = config.copy()
indicator_config = {CONF_INDICATOR: outer_config.pop(CONF_TICKS, {})}
w = await super().create_to_code(outer_config, parent)
var = w.obj
# LVGL 9.4 scale widget setup
# Background style will be applied.
for scale_conf in config.get(CONF_SCALES, ()):
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
if CONF_ROTATION in scale_conf:
rotation = await lv_angle_degrees.process(scale_conf[CONF_ROTATION])
with LocalVariable(
"meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var)
) as meter_var:
lv.meter_set_scale_range(
var,
meter_var,
scale_conf[CONF_RANGE_FROM],
scale_conf[CONF_RANGE_TO],
scale_conf[CONF_ANGLE_RANGE],
rotation,
)
if ticks := scale_conf.get(CONF_TICKS):
color = await lv_color.process(ticks[CONF_COLOR])
lv.meter_set_scale_ticks(
var,
meter_var,
ticks[CONF_COUNT],
await size.process(ticks[CONF_WIDTH]),
await size.process(ticks[CONF_LENGTH]),
color,
scale_var = cg.Pvariable(scale_conf[CONF_ID], lv_expr.scale_create(var))
percent100 = await pixels_or_percent.process(1.0)
lv_obj.set_style_height(scale_var, percent100, LV_PART.MAIN)
lv_obj.set_style_width(scale_var, percent100, LV_PART.MAIN)
lv_obj.set_style_align(scale_var, CHILD_ALIGNMENTS.CENTER, LV_PART.MAIN)
lv_obj.set_style_bg_opa(scale_var, LV_OPA.TRANSP, LV_PART.MAIN)
lv_obj.set_style_radius(scale_var, LV_RADIUS.CIRCLE, 0)
await set_obj_properties(Widget(scale_var, scale_spec), indicator_config)
lv.scale_set_mode(scale_var, LV_SCALE_MODE.ROUND_INNER)
# Set the scale range
range_from = await lv_int.process(scale_conf[CONF_RANGE_FROM])
range_to = await lv_int.process(scale_conf[CONF_RANGE_TO])
lv.scale_set_range(scale_var, range_from, range_to)
angle_range = await lv_angle_degrees.process(scale_conf[CONF_ANGLE_RANGE])
rotation = await lv_angle_degrees.process(scale_conf[CONF_ROTATION])
# Set angle range
lv.scale_set_angle_range(
scale_var,
angle_range,
)
# Set rotation if specified
if rotation:
lv.scale_set_rotation(scale_var, rotation)
# Handle indicators as sections
for indicator in scale_conf.get(CONF_INDICATORS, ()):
(t, v) = next(iter(indicator.items()))
iid = v[CONF_ID]
# Enable getting the meter to which this belongs.
# Set section range based on indicator values
start_value = await get_start_value(v) or scale_conf[CONF_RANGE_FROM]
end_value = await get_end_value(v) or scale_conf[CONF_RANGE_TO]
# Create and apply styles based on indicator type
if t == CONF_ARC:
props = {
"arc_width": v[CONF_WIDTH],
"arc_color": v[CONF_COLOR],
"arc_rounded": v.get("arc_rounded", False),
}
if (opa := v.get(CONF_OPA)) is not None:
props["arc_opa"] = opa
if CONF_R_MOD in v:
get_warnings().add(
"The 'r_mod' indicator property is not supported in LVGL 9.x and will be ignored."
)
arc_style = LVStyle(f"meter_arc_{iid.id}", props)
tvar = cg.Pvariable(iid, lv_expr.scale_add_section(scale_var))
lv.scale_section_set_style(
tvar, LV_PART.MAIN, await arc_style.get_var()
)
if CONF_MAJOR in ticks:
major = ticks[CONF_MAJOR]
lv.meter_set_scale_major_ticks(
var,
meter_var,
major[CONF_STRIDE],
await size.process(major[CONF_WIDTH]),
await size.process(major[CONF_LENGTH]),
await lv_color.process(major[CONF_COLOR]),
await size.process(major[CONF_LABEL_GAP]),
)
for indicator in scale_conf.get(CONF_INDICATORS, ()):
(t, v) = next(iter(indicator.items()))
iid = v[CONF_ID]
ivar = cg.Pvariable(iid, cg.nullptr, type_=lv_meter_indicator_t)
# Enable getting the meter to which this belongs.
wid = Widget.create(iid, var, obj_spec, v)
wid.obj = ivar
if t == CONF_LINE:
color = await lv_color.process(v[CONF_COLOR])
lv_assign(
ivar,
lv_expr.meter_add_needle_line(
var,
meter_var,
await size.process(v[CONF_WIDTH]),
color,
await size.process(v[CONF_R_MOD]),
),
)
if t == CONF_ARC:
color = await lv_color.process(v[CONF_COLOR])
lv_assign(
ivar,
lv_expr.meter_add_arc(
var,
meter_var,
await size.process(v[CONF_WIDTH]),
color,
await size.process(v[CONF_R_MOD]),
),
)
if t == CONF_TICK_STYLE:
color_start = await lv_color.process(v[CONF_COLOR_START])
color_end = await lv_color.process(
v.get(CONF_COLOR_END) or color_start
)
lv_assign(
ivar,
lv_expr.meter_add_scale_lines(
var,
meter_var,
lw = Widget(tvar, arc_indicator_type)
await set_indicator_values(lw, v)
if t == CONF_TICK_STYLE:
# No object created for this
color_start = await lv_color.process(v[CONF_COLOR_START])
color_end = await lv_color.process(v[CONF_COLOR_END])
local = v[CONF_LOCAL]
if color_start and color_end:
async with LambdaContext(
[(lv_event_t.operator("ptr"), "e")]
) as lambda_:
lv.scale_draw_event_cb(
lambda_.get_parameter(0),
start_value,
end_value,
color_start,
color_end,
v[CONF_LOCAL],
await size.process(v[CONF_WIDTH]),
),
local,
)
lv_obj.add_event_cb(
scale_var,
await lambda_.get_lambda(),
LV_EVENT.DRAW_TASK_ADDED,
nullptr,
)
if t == CONF_IMAGE:
add_lv_use("img")
lv_assign(
ivar,
lv_expr.meter_add_needle_img(
var,
meter_var,
await lv_image.process(v[CONF_SRC]),
v[CONF_PIVOT_X],
v[CONF_PIVOT_Y],
),
)
await set_indicator_values(var, ivar, v)
lv.obj_add_flag(scale_var, LV_OBJ_FLAG.SEND_DRAW_TASK_EVENTS)
if t == CONF_LINE:
# Needle represented by a line
if CONF_LENGTH in v:
length = v[CONF_LENGTH]
elif r_mod := v.get(CONF_R_MOD):
get_remapped_uses().add(CONF_R_MOD)
length = -abs(r_mod)
else:
length = 1.0
props = {
CONF_ID: v[CONF_ID],
CONF_OPA: v[CONF_OPA],
CONF_LINE_WIDTH: v[CONF_WIDTH],
"line_color": v[CONF_COLOR],
"line_rounded": True,
CONF_ALIGN: CHILD_ALIGNMENTS.TOP_LEFT,
CONF_LENGTH: length,
CONF_RADIAL_OFFSET: v[CONF_RADIAL_OFFSET],
}
lw = await widget_to_code(props, line_indicator_type, scale_var)
await set_indicator_values(lw, v)
if t == CONF_IMAGE:
add_lv_use(CONF_IMAGE)
src = v[CONF_SRC]
src_data = get_image_metadata(src.id)
pivot_x = await pixels.process(v[CONF_PIVOT_X])
pivot_y = await pixels.process(
v.get(CONF_PIVOT_Y, src_data.height // 2)
)
props = {
CONF_X: src_data.width // 2 - pivot_x,
"transform_pivot_x": pivot_x,
"transform_pivot_y": pivot_y,
CONF_SRC: src,
CONF_OPA: v[CONF_OPA],
CONF_ID: v[CONF_ID],
CONF_ALIGN: CHILD_ALIGNMENTS.CENTER,
}
iw = await widget_to_code(props, image_indicator_type, scale_var)
await iw.set_property(CONF_SRC, await lv_image.process(src))
await set_indicator_values(iw, v)
if ticks := scale_conf.get(CONF_TICKS):
# Set total tick count
lv.scale_set_total_tick_count(scale_var, ticks[CONF_COUNT])
lv.scale_set_draw_ticks_on_top(
scale_var, scale_conf[CONF_DRAW_TICKS_ON_TOP]
)
# Set tick styling
lv_obj.set_style_length(
scale_var, await size.process(ticks[CONF_LENGTH]), LV_PART.ITEMS
)
lv_obj.set_style_line_width(
scale_var, await size.process(ticks[CONF_WIDTH]), LV_PART.ITEMS
)
lv_obj.set_style_radial_offset(
scale_var,
await size.process(ticks[CONF_RADIAL_OFFSET]),
LV_PART.ITEMS,
)
lv_obj.set_style_line_color(
scale_var,
await lv_color.process(ticks[CONF_COLOR]),
LV_PART.ITEMS,
)
# Hide the scale line
lv.obj_set_style_arc_opa(scale_var, LV_OPA.TRANSP, LV_PART.MAIN)
if CONF_MAJOR in ticks:
major = ticks[CONF_MAJOR]
# Set major tick frequency
lv.scale_set_major_tick_every(scale_var, major[CONF_STRIDE])
# Enable labels for major ticks
lv.scale_set_label_show(scale_var, True)
# Set major tick styling
lv_obj.set_style_length(
scale_var,
await size.process(major[CONF_LENGTH]),
LV_PART.INDICATOR,
)
lv_obj.set_style_radial_offset(
scale_var,
await size.process(ticks[CONF_RADIAL_OFFSET]),
LV_PART.INDICATOR,
)
lv_obj.set_style_line_width(
scale_var,
await size.process(major[CONF_WIDTH]),
LV_PART.INDICATOR,
)
lv_obj.set_style_line_color(
scale_var,
await lv_color.process(major[CONF_COLOR]),
LV_PART.INDICATOR,
)
# Set label gap (padding)
label_gap = await size.process(major[CONF_LABEL_GAP])
if isinstance(label_gap, int):
label_gap -= DEFAULT_LABEL_GAP
lv_obj.set_style_pad_radial(
scale_var,
label_gap,
LV_PART.INDICATOR,
)
else:
lv.scale_set_major_tick_every(scale_var, 0)
else:
lv.scale_set_total_tick_count(scale_var, 0)
# Add a pivot
# Get the default style
pivot_style = PIVOT_STYLE.copy()
pivot_style.update(config.get(CONF_INDICATOR, config.get(CONF_PIVOT, {})))
with LocalVariable("pivot", lv_obj_t, lv_expr.container_create(var)) as pivot:
pw = Widget(pivot, obj_spec, pivot_style)
await set_obj_properties(pw, pivot_style)
meter_spec = MeterType()
@@ -303,23 +578,39 @@ async def indicator_update_to_code(config, action_id, template_arg, args):
widget = await get_widgets(config)
async def set_value(w: Widget):
await set_indicator_values(w.var, w.obj, config)
await set_indicator_values(w, config)
return await action_to_code(
widget, set_value, action_id, template_arg, args, config
)
async def set_indicator_values(meter, indicator, config):
async def set_indicator_values(indicator: Widget, config):
"""Update scale section values (replaces meter indicator values)"""
start_value = await get_start_value(config)
end_value = await get_end_value(config)
if start_value is not None:
if end_value is None:
lv.meter_set_indicator_value(meter, indicator, start_value)
else:
lv.meter_set_indicator_start_value(meter, indicator, start_value)
if end_value is not None:
lv.meter_set_indicator_end_value(meter, indicator, end_value)
if (opa := config.get(CONF_OPA)) is not None:
lv_assign(indicator.opa, await opacity.process(opa))
lv_obj.invalidate(meter)
if indicator.type is arc_indicator_type:
# For scale sections, we update the range
if start_value is not None and end_value is not None:
lv.scale_section_set_range(indicator.obj, start_value, end_value)
elif start_value is not None:
# If only start value, use it as both start and end (single point)
lv.scale_section_set_range(indicator.obj, start_value, start_value)
elif end_value is not None:
# If only end value, assume range from 0 to end_value
lv.scale_section_set_range(indicator.obj, 0, end_value)
return
if start_value is None:
return
if indicator.type is line_indicator_type:
# Line needle
lv_add(indicator.var.set_value(start_value))
return
if indicator.type is image_indicator_type:
# Needle represented by an image
lv_obj.set_style_transform_rotation(
indicator.obj,
lv.get_needle_angle_for_value(indicator.obj, start_value) * 10,
LV_PART.MAIN,
)
+100 -73
View File
@@ -1,7 +1,7 @@
from esphome import config_validation as cv
from esphome.const import CONF_BUTTON, CONF_ID, CONF_ITEMS, CONF_TEXT
from esphome import codegen as cg, config_validation as cv
from esphome.const import CONF_BUTTON, CONF_ID, CONF_TEXT
from esphome.core import ID
from esphome.cpp_generator import new_Pvariable, static_const_array
from esphome.cpp_generator import MockObjClass
from esphome.cpp_types import nullptr
from ..defines import (
@@ -9,40 +9,82 @@ from ..defines import (
CONF_BUTTON_STYLE,
CONF_BUTTONS,
CONF_CLOSE_BUTTON,
CONF_HEADER_BUTTONS,
CONF_MAIN,
CONF_MSGBOXES,
CONF_SRC,
CONF_TITLE,
LV_OBJ_FLAG,
TYPE_FLEX,
add_warning,
literal,
)
from ..helpers import add_lv_use, lvgl_components_required
from ..lv_validation import lv_bool, lv_pct, lv_text
from ..lvcode import (
EVENT_ARG,
LambdaContext,
LocalVariable,
lv,
lv_add,
lv_assign,
lv_expr,
lv_obj,
lv_Pvariable,
)
from ..schemas import STYLE_SCHEMA, STYLED_TEXT_SCHEMA, container_schema, part_schema
from ..types import LV_EVENT, char_ptr, lv_obj_t
from . import Widget, add_widgets, set_obj_properties
from .button import button_spec
from .buttonmatrix import (
BUTTONMATRIX_BUTTON_SCHEMA,
CONF_BUTTON_TEXT_LIST_ID,
buttonmatrix_spec,
get_button_data,
lv_buttonmatrix_t,
set_btn_data,
from ..helpers import add_lv_use
from ..lv_validation import lv_bool, lv_image, lv_text, pixels_or_percent
from ..lvcode import EVENT_ARG, LambdaContext, LocalVariable, lv, lv_expr, lv_obj
from ..schemas import (
STYLE_SCHEMA,
STYLED_TEXT_SCHEMA,
TEXT_SCHEMA,
container_schema,
part_schema,
)
from ..styles import LVStyle
from ..types import LV_EVENT, lv_obj_t
from . import Widget, WidgetType, add_widgets, set_obj_properties, widget_to_code
from .button import button_spec, lv_button_t
from .label import CONF_LABEL
from .obj import obj_spec
CONF_MSGBOX = "msgbox"
OUTER_STYLE = LVStyle(
"msgbox_outer",
{
"bg_opa": 128,
"bg_color": "black",
"border_width": 0,
"pad_all": 0,
"radius": 0,
},
)
class FooterButtonType(WidgetType):
def __init__(self):
super().__init__(
CONF_BUTTON, lv_button_t, (CONF_MAIN,), TEXT_SCHEMA, is_mock=True
)
async def obj_creator(self, parent: MockObjClass, config: dict):
return lv_expr.msgbox_add_footer_button(parent, config[CONF_TEXT])
footer_button_spec = FooterButtonType()
class HeaderButtonType(WidgetType):
def __init__(self):
super().__init__(
CONF_BUTTON,
lv_button_t,
(CONF_MAIN,),
cv.Schema(
{
cv.Required(CONF_SRC): lv_image,
}
),
is_mock=True,
)
async def obj_creator(self, parent: MockObjClass, config: dict):
return lv_expr.msgbox_add_header_button(
parent, await lv_image.process(config[CONF_SRC])
)
header_button_spec = HeaderButtonType()
MSGBOX_SCHEMA = container_schema(
obj_spec,
STYLE_SCHEMA.extend(
@@ -50,10 +92,14 @@ MSGBOX_SCHEMA = container_schema(
cv.GenerateID(CONF_ID): cv.declare_id(lv_obj_t),
cv.Required(CONF_TITLE): STYLED_TEXT_SCHEMA,
cv.Optional(CONF_BODY, default=""): STYLED_TEXT_SCHEMA,
cv.Optional(CONF_BUTTONS): cv.ensure_list(BUTTONMATRIX_BUTTON_SCHEMA),
cv.Optional(CONF_BUTTON_STYLE): part_schema(buttonmatrix_spec.parts),
cv.Optional(CONF_BUTTONS): cv.ensure_list(
container_schema(footer_button_spec)
),
cv.Optional(CONF_HEADER_BUTTONS): cv.ensure_list(
container_schema(header_button_spec)
),
cv.Optional(CONF_CLOSE_BUTTON, default=True): lv_bool,
cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr),
cv.Optional(CONF_BUTTON_STYLE): part_schema(button_spec.parts),
}
),
)
@@ -62,7 +108,9 @@ MSGBOX_SCHEMA = container_schema(
async def msgbox_to_code(top_layer, conf):
"""
Construct a message box. This consists of a full-screen translucent background enclosing a centered container
with an optional title, body, close button and a button matrix. And any other widgets the user cares to add
with an optional title, body, close button and a set of footer buttons.
Header buttons can be added - they can be image buttons only.
The body of the message box may have any widgets the user wants to add.
:param conf: The config data
:return: code to add to the init lambda
"""
@@ -71,60 +119,42 @@ async def msgbox_to_code(top_layer, conf):
CONF_BUTTON,
CONF_LABEL,
CONF_MSGBOX,
*buttonmatrix_spec.get_uses(),
*button_spec.get_uses(),
)
lvgl_components_required.add("BUTTONMATRIX")
messagebox_id = conf[CONF_ID]
outer_id = f"{messagebox_id.id}_outer"
outer = lv_Pvariable(lv_obj_t, messagebox_id.id + "_outer")
buttonmatrix = new_Pvariable(
ID(
f"{messagebox_id.id}_buttonmatrix_",
is_declaration=True,
type=lv_buttonmatrix_t,
if CONF_BUTTON_STYLE in conf:
add_warning(
"'button_style' for msgbox is deprecated - style the buttons directly."
)
)
msgbox = lv_Pvariable(lv_obj_t, messagebox_id.id)
outer_widget = Widget.create(outer_id, outer, obj_spec, conf)
messagebox_id = conf[CONF_ID]
outer_id = ID(f"{messagebox_id.id}_outer", type=lv_obj_t)
outer = cg.Pvariable(outer_id, lv_expr.obj_create(top_layer))
outer_widget = Widget.create(outer_id.id, outer, obj_spec, conf)
msgbox = cg.Pvariable(messagebox_id, lv_expr.msgbox_create(outer))
outer_widget.move_to_foreground = True
msgbox_widget = Widget.create(messagebox_id, msgbox, obj_spec, conf)
msgbox_widget.outer = outer_widget
buttonmatrix_widget = Widget.create(
str(buttonmatrix), buttonmatrix, buttonmatrix_spec, conf
)
text_list, ctrl_list, width_list, _ = await get_button_data(
(conf,), buttonmatrix_widget
)
text_id = conf[CONF_BUTTON_TEXT_LIST_ID]
text_list = static_const_array(text_id, text_list)
text = await lv_text.process(conf[CONF_BODY].get(CONF_TEXT, ""))
title = await lv_text.process(conf[CONF_TITLE].get(CONF_TEXT, ""))
close_button = conf[CONF_CLOSE_BUTTON]
lv_assign(outer, lv_expr.obj_create(top_layer))
lv_obj.set_width(outer, lv_pct(100))
lv_obj.set_height(outer, lv_pct(100))
lv_obj.set_style_bg_opa(outer, 128, 0)
lv_obj.set_style_bg_color(outer, literal("lv_color_black()"), 0)
lv_obj.set_style_border_width(outer, 0, 0)
lv_obj.set_style_pad_all(outer, 0, 0)
lv_obj.set_style_radius(outer, 0, 0)
outer_widget.add_flag("LV_OBJ_FLAG_HIDDEN")
lv_assign(
msgbox, lv_expr.msgbox_create(outer, title, text, text_list, close_button)
)
percent100 = await pixels_or_percent.process(1.0)
lv_obj.set_size(outer, percent100, percent100)
outer_widget.add_style(await OUTER_STYLE.get_var())
outer_widget.add_flag(LV_OBJ_FLAG.HIDDEN)
lv.msgbox_add_title(msgbox, title)
lv.msgbox_add_text(msgbox, text)
lv_obj.set_style_align(msgbox, literal("LV_ALIGN_CENTER"), 0)
lv_add(buttonmatrix.set_obj(lv_expr.msgbox_get_btns(msgbox)))
if button_style := conf.get(CONF_BUTTON_STYLE):
button_style = {CONF_ITEMS: button_style}
await set_obj_properties(buttonmatrix_widget, button_style)
await set_obj_properties(msgbox_widget, conf)
await add_widgets(msgbox_widget, conf)
for button in conf.get(CONF_BUTTONS, ()):
await widget_to_code(button, footer_button_spec, msgbox)
for button in conf.get(CONF_HEADER_BUTTONS, ()):
await widget_to_code(button, header_button_spec, msgbox)
async with LambdaContext(EVENT_ARG, where=messagebox_id) as close_action:
outer_widget.add_flag("LV_OBJ_FLAG_HIDDEN")
outer_widget.add_flag(LV_OBJ_FLAG.HIDDEN)
if close_button:
with LocalVariable(
"close_btn_", lv_obj_t, lv_expr.msgbox_get_close_btn(msgbox)
"close_btn_", lv_obj_t, lv_expr.msgbox_add_close_button(msgbox)
) as close_btn:
lv_obj.remove_event_cb(close_btn, nullptr)
lv_obj.add_event_cb(
@@ -138,9 +168,6 @@ async def msgbox_to_code(top_layer, conf):
outer, await close_action.get_lambda(), LV_EVENT.CLICKED, nullptr
)
if len(ctrl_list) != 0 or len(width_list) != 0:
set_btn_data(buttonmatrix.obj, ctrl_list, width_list)
async def msgboxes_to_code(lv_component, config):
top_layer = lv.disp_get_layer_top(lv_component.get_disp())
+2 -1
View File
@@ -1,5 +1,6 @@
from ..defines import CONF_MAIN, CONF_OBJ, CONF_SCROLLBAR
from ..types import WidgetType, lv_obj_t
from ..types import lv_obj_t
from . import WidgetType
class ObjType(WidgetType):
+30 -17
View File
@@ -1,14 +1,15 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_SIZE, CONF_TEXT
from esphome.cpp_generator import MockObjClass
from ..defines import CONF_MAIN
from ..lv_validation import lv_color, lv_text
from ..lvcode import LocalVariable, lv, lv_expr
from ..defines import CONF_MAIN, get_color_formats
from ..lv_validation import color, lv_color, lv_int, lv_text
from ..lvcode import LocalVariable, lv
from ..schemas import TEXT_SCHEMA
from ..types import WidgetType, lv_obj_t
from . import Widget
from ..types import lv_obj_t
from . import Widget, WidgetType
from .canvas import CONF_CANVAS
from .img import CONF_IMAGE
CONF_QRCODE = "qrcode"
CONF_DARK_COLOR = "dark_color"
@@ -16,9 +17,16 @@ CONF_LIGHT_COLOR = "light_color"
QRCODE_SCHEMA = {
**TEXT_SCHEMA,
cv.Optional(CONF_DARK_COLOR, default="black"): lv_color,
cv.Optional(CONF_LIGHT_COLOR, default="white"): lv_color,
cv.Required(CONF_SIZE): cv.int_,
cv.Optional(CONF_DARK_COLOR, default="black"): color,
cv.Optional(CONF_LIGHT_COLOR, default="white"): color,
cv.Required(CONF_SIZE): lv_int,
}
QRCODE_MODIFY_SCHEMA = {
**TEXT_SCHEMA,
cv.Optional(CONF_DARK_COLOR): lv_color,
cv.Optional(CONF_LIGHT_COLOR): lv_color,
cv.Optional(CONF_SIZE): lv_int,
}
@@ -29,20 +37,25 @@ class QrCodeType(WidgetType):
lv_obj_t,
(CONF_MAIN,),
QRCODE_SCHEMA,
modify_schema=TEXT_SCHEMA,
modify_schema=QRCODE_MODIFY_SCHEMA,
)
def get_uses(self):
return "canvas", "img", "label"
async def obj_creator(self, parent: MockObjClass, config: dict):
dark_color = await lv_color.process(config[CONF_DARK_COLOR])
light_color = await lv_color.process(config[CONF_LIGHT_COLOR])
size = config[CONF_SIZE]
return lv_expr.call("qrcode_create", parent, size, dark_color, light_color)
return CONF_CANVAS, CONF_IMAGE
async def to_code(self, w: Widget, config):
get_color_formats().add("ARGB8888")
await w.set_property(
CONF_LIGHT_COLOR, await lv_color.process(config.get(CONF_LIGHT_COLOR))
)
await w.set_property(
CONF_DARK_COLOR, await lv_color.process(config.get(CONF_DARK_COLOR))
)
await w.set_property(CONF_SIZE, await lv_int.process(config.get(CONF_SIZE)))
if (value := config.get(CONF_TEXT)) is not None:
if isinstance(value, str):
lv.qrcode_update(w.obj, value, len(value))
return
value = await lv_text.process(value)
with LocalVariable("qr_text", cg.std_string, value, modifier="") as str_obj:
lv.qrcode_update(w.obj, str_obj.c_str(), str_obj.size())
+8 -6
View File
@@ -2,18 +2,18 @@ import esphome.config_validation as cv
from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE
from ..defines import (
BAR_MODES,
CONF_ANIMATED,
CONF_INDICATOR,
CONF_KNOB,
CONF_MAIN,
SLIDER_MODES,
literal,
)
from ..helpers import add_lv_use
from ..lv_validation import animated, get_start_value, lv_float
from ..lvcode import lv
from ..types import LvNumber, NumberType
from . import Widget
from ..types import LvNumber
from . import NumberType, Widget
from .label import CONF_LABEL
from .lv_bar import CONF_BAR
CONF_SLIDER = "slider"
@@ -29,7 +29,7 @@ SLIDER_SCHEMA = cv.Schema(
cv.Optional(CONF_VALUE): lv_float,
cv.Optional(CONF_MIN_VALUE, default=0): cv.int_,
cv.Optional(CONF_MAX_VALUE, default=100): cv.int_,
cv.Optional(CONF_MODE, default="NORMAL"): BAR_MODES.one_of,
cv.Optional(CONF_MODE, default="NORMAL"): SLIDER_MODES.one_of,
cv.Optional(CONF_ANIMATED, default=True): animated,
}
)
@@ -49,8 +49,10 @@ class SliderType(NumberType):
def animated(self):
return True
def get_uses(self):
return (CONF_BAR, CONF_LABEL)
async def to_code(self, w: Widget, config):
add_lv_use(CONF_BAR)
if CONF_MIN_VALUE in config:
# not modify case
lv.slider_set_range(w.obj, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE])
+10 -11
View File
@@ -1,9 +1,8 @@
import esphome.config_validation as cv
from esphome.cpp_generator import MockObjClass
from ..defines import CONF_ARC_LENGTH, CONF_INDICATOR, CONF_MAIN, CONF_SPIN_TIME
from ..lv_validation import lv_angle_degrees, lv_milliseconds
from ..lvcode import lv_expr
from ..lvcode import lv
from ..types import LvType
from . import Widget, WidgetType
from .arc import CONF_ARC
@@ -12,8 +11,10 @@ CONF_SPINNER = "spinner"
SPINNER_SCHEMA = cv.Schema(
{
cv.Required(CONF_ARC_LENGTH): lv_angle_degrees,
cv.Required(CONF_SPIN_TIME): lv_milliseconds,
cv.Optional(CONF_ARC_LENGTH, default=200): cv.All(
lv_angle_degrees, cv.int_range(min=0, max=360)
),
cv.Optional(CONF_SPIN_TIME, default="2s"): lv_milliseconds,
}
)
@@ -25,19 +26,17 @@ class SpinnerType(WidgetType):
LvType("lv_spinner_t"),
(CONF_MAIN, CONF_INDICATOR),
SPINNER_SCHEMA,
{},
)
async def to_code(self, w: Widget, config):
return []
spin_time = await lv_milliseconds.process(config.get(CONF_SPIN_TIME))
arc_length = int(config[CONF_ARC_LENGTH])
if arc_length < 180:
arc_length += 180
lv.spinner_set_anim_params(w.obj, spin_time, arc_length)
def get_uses(self):
return (CONF_ARC,)
async def obj_creator(self, parent: MockObjClass, config: dict):
spin_time = await lv_milliseconds.process(config[CONF_SPIN_TIME])
arc_length = await lv_angle_degrees.process(config[CONF_ARC_LENGTH])
return lv_expr.call("spinner_create", parent, spin_time, arc_length)
spinner_spec = SpinnerType()
+29 -16
View File
@@ -1,8 +1,14 @@
from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INDEX, CONF_NAME, CONF_POSITION, CONF_SIZE
from esphome.cpp_generator import MockObjClass
from esphome.const import (
CONF_ID,
CONF_INDEX,
CONF_ITEMS,
CONF_NAME,
CONF_POSITION,
CONF_SIZE,
)
from ..automation import action_to_code
from ..defines import (
@@ -15,10 +21,11 @@ from ..defines import (
literal,
)
from ..lv_validation import animated, lv_int, size
from ..lvcode import LocalVariable, lv, lv_assign, lv_expr
from ..lvcode import LocalVariable, lv, lv_assign, lv_expr, lv_obj
from ..schemas import container_schema, part_schema
from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr
from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties
from .button import button_spec
from .buttonmatrix import buttonmatrix_spec
from .obj import obj_spec
@@ -69,6 +76,10 @@ class TabviewType(WidgetType):
return "btnmatrix", TYPE_FLEX
async def to_code(self, w: Widget, config: dict):
await w.set_property(
"tab_bar_position", await DIRECTIONS.process(config[CONF_POSITION])
)
await w.set_property("tab_bar_size", await size.process(config[CONF_SIZE]))
for tab_conf in config[CONF_TABS]:
w_id = tab_conf[CONF_ID]
tab_obj = cg.Pvariable(w_id, cg.nullptr, type_=lv_tab_t)
@@ -76,25 +87,27 @@ class TabviewType(WidgetType):
lv_assign(tab_obj, lv_expr.tabview_add_tab(w.obj, tab_conf[CONF_NAME]))
await set_obj_properties(tab_widget, tab_conf)
await add_widgets(tab_widget, tab_conf)
if button_style := config.get(CONF_TAB_STYLE):
tab_style = config.get(CONF_TAB_STYLE, {})
tab_items_style = tab_style.get(CONF_ITEMS, {})
if tab_style:
with LocalVariable(
"tabview_btnmatrix", lv_obj_t, rhs=lv_expr.tabview_get_tab_btns(w.obj)
) as btnmatrix_obj:
await set_obj_properties(Widget(btnmatrix_obj, obj_spec), button_style)
"tabview_bar", lv_obj_t, rhs=lv_expr.tabview_get_tab_bar(w.obj)
) as bar_obj:
tab_bar = Widget(bar_obj, obj_spec)
await set_obj_properties(tab_bar, tab_style)
if tab_items_style:
for index, tab_conf in enumerate(config[CONF_TABS]):
await set_obj_properties(
Widget(lv_obj.get_child(bar_obj, index), button_spec),
tab_items_style,
)
if content_style := config.get(CONF_CONTENT_STYLE):
with LocalVariable(
"tabview_content", lv_obj_t, rhs=lv_expr.tabview_get_content(w.obj)
) as content_obj:
await set_obj_properties(Widget(content_obj, obj_spec), content_style)
async def obj_creator(self, parent: MockObjClass, config: dict):
return lv_expr.call(
"tabview_create",
parent,
await DIRECTIONS.process(config[CONF_POSITION]),
await size.process(config[CONF_SIZE]),
)
tabview_spec = TabviewType()
@@ -117,6 +130,6 @@ async def tabview_select(config, action_id, template_arg, args):
async def do_select(w: Widget):
lv.tabview_set_act(w.obj, index, literal(config[CONF_ANIMATED]))
lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr)
lv.obj_send_event(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr)
return await action_to_code(widget, do_select, action_id, template_arg, args)
+8 -6
View File
@@ -15,7 +15,7 @@ from ..defines import (
TILE_DIRECTIONS,
literal,
)
from ..lv_validation import animated, lv_int, lv_pct
from ..lv_validation import animated, lv_int, pixels_or_percent
from ..lvcode import lv, lv_assign, lv_expr, lv_obj, lv_Pvariable
from ..schemas import container_schema
from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr
@@ -68,17 +68,19 @@ class TileviewType(WidgetType):
w_id = tile_conf[CONF_ID]
tile_obj = lv_Pvariable(lv_obj_t, w_id)
tile = Widget.create(w_id, tile_obj, tile_spec, tile_conf)
dirs = tile_conf[CONF_DIR]
if isinstance(dirs, list):
dirs = "|".join(dirs)
dirs = await TILE_DIRECTIONS.process(tile_conf[CONF_DIR])
row_pos = tile_conf[CONF_ROW]
col_pos = tile_conf[CONF_COLUMN]
lv_assign(
tile_obj,
lv_expr.tileview_add_tile(w.obj, col_pos, row_pos, literal(dirs)),
lv_expr.tileview_add_tile(w.obj, col_pos, row_pos, dirs),
)
# Bugfix for LVGL 8.x
lv_obj.set_pos(tile_obj, lv_pct(col_pos * 100), lv_pct(row_pos * 100))
lv_obj.set_pos(
tile_obj,
await pixels_or_percent.process(float(col_pos)),
await pixels_or_percent.process(float(row_pos)),
)
await set_obj_properties(tile, tile_conf)
await add_widgets(tile, tile_conf)
if tiles:
@@ -23,27 +23,52 @@ static ProgmemStr alarm_state_to_mqtt_str(AlarmControlPanelState state) {
MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel)
: alarm_control_panel_(alarm_control_panel) {}
static bool apply_command(AlarmControlPanelCall &call, const char *state) {
if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("ARM_AWAY")) == 0) {
call.arm_away();
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("ARM_HOME")) == 0) {
call.arm_home();
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("ARM_NIGHT")) == 0) {
call.arm_night();
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("ARM_VACATION")) == 0) {
call.arm_vacation();
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("ARM_CUSTOM_BYPASS")) == 0) {
call.arm_custom_bypass();
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("DISARM")) == 0) {
call.disarm();
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("PENDING")) == 0) {
call.pending();
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("TRIGGERED")) == 0) {
call.triggered();
} else {
return false;
}
return true;
}
void MQTTAlarmControlPanelComponent::setup() {
this->alarm_control_panel_->add_on_state_callback([this]() { this->publish_state(); });
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
auto call = this->alarm_control_panel_->make_call();
if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_AWAY")) == 0) {
call.arm_away();
} else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_HOME")) == 0) {
call.arm_home();
} else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_NIGHT")) == 0) {
call.arm_night();
} else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_VACATION")) == 0) {
call.arm_vacation();
} else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_CUSTOM_BYPASS")) == 0) {
call.arm_custom_bypass();
} else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("DISARM")) == 0) {
call.disarm();
} else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("PENDING")) == 0) {
call.pending();
} else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("TRIGGERED")) == 0) {
call.triggered();
} else {
if (!payload.empty() && payload[0] == '{') {
// JSON payload: {"state": "DISARM", "code": "1234"}
JsonDocument doc = json::parse_json(payload);
JsonObject root = doc.as<JsonObject>();
if (!root.isNull()) {
const char *state = root[ESPHOME_F("state")];
if (state == nullptr) {
ESP_LOGW(TAG, "'%s': JSON payload missing 'state' key", this->friendly_name_().c_str());
} else if (!apply_command(call, state)) {
ESP_LOGW(TAG, "'%s': Received unknown state in JSON payload: %s", this->friendly_name_().c_str(), state);
} else {
const char *code = root[ESPHOME_F("code")];
if (code != nullptr) {
call.set_code(code);
}
}
}
} else if (!apply_command(call, payload.c_str())) {
ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name_().c_str(), payload.c_str());
}
call.perform();
@@ -5,12 +5,14 @@ import esphome.codegen as cg
from esphome.components import runtime_image
from esphome.components.const import CONF_REQUEST_HEADERS
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
from esphome.components.image import CONF_TRANSPARENCY, add_metadata
import esphome.config_validation as cv
from esphome.const import (
CONF_BUFFER_SIZE,
CONF_ID,
CONF_ON_ERROR,
CONF_TRIGGER_ID,
CONF_TYPE,
CONF_URL,
)
from esphome.core import Lambda
@@ -131,6 +133,13 @@ async def online_image_action_to_code(config, action_id, template_arg, args):
async def to_code(config):
# Use the enhanced helper function to get all runtime image parameters
settings = await runtime_image.process_runtime_image_config(config)
add_metadata(
config[CONF_ID],
settings.width,
settings.height,
config[CONF_TYPE],
config[CONF_TRANSPARENCY],
)
url = config[CONF_URL]
var = cg.new_Pvariable(
+2 -1
View File
@@ -181,7 +181,8 @@ async def process_runtime_image_config(config: dict) -> RuntimeImageSettings:
transparent = get_transparency_enum(config.get(CONF_TRANSPARENCY, "OPAQUE"))
# Get byte order (True for big endian, False for little endian)
byte_order_big_endian = config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN"
# If unspecified, use little endian
byte_order_big_endian = config.get(CONF_BYTE_ORDER) == "BIG_ENDIAN"
# Get placeholder if specified
placeholder = None
@@ -106,7 +106,7 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
break;
}
case image::IMAGE_TYPE_RGB565: {
uint32_t pos = this->get_position_(x, y);
const size_t pos = (x + y * this->buffer_width_) * 2;
Color mapped_color = color;
this->map_chroma_key(mapped_color);
uint16_t rgb565 = display::ColorUtil::color_to_565(mapped_color);
@@ -118,7 +118,8 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
this->buffer_[pos + 1] = static_cast<uint8_t>((rgb565 >> 8) & 0xFF);
}
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
this->buffer_[pos + 2] = color.w;
const size_t alpha_pos = pos / 2 + this->buffer_width_ * this->buffer_height_ * 2;
this->buffer_[alpha_pos] = color.w;
}
break;
}
@@ -283,6 +284,10 @@ size_t RuntimeImage::resize_buffer_(int width, int height) {
}
size_t RuntimeImage::get_buffer_size_(int width, int height) const {
if (this->get_type() == image::IMAGE_TYPE_RGB565 && this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
// Add extra alpha channel for RGB565 with alpha
return width * height * 3;
}
return (this->get_bpp() * width + 7u) / 8u * height;
}
@@ -457,6 +457,9 @@ void SpeakerSourceMediaPlayer::process_control_queue_() {
if (ps.playlist_index < ps.playlist.size()) {
this->queue_play_current_(pipeline, ps.playlist_delay_ms);
} else if (ps.repeat_mode == REPEAT_ALL && !ps.playlist.empty()) {
if (!ps.shuffle_indices.empty()) {
this->shuffle_playlist_(pipeline);
}
ps.playlist_index = 0;
this->queue_play_current_(pipeline, ps.playlist_delay_ms);
}
+1
View File
@@ -92,6 +92,7 @@
#define USE_LVGL_MSGBOX
#define USE_LVGL_ROLLER
#define USE_LVGL_ROTARY_ENCODER
#define USE_LVGL_SCALE
#define USE_LVGL_SLIDER
#define USE_LVGL_SPAN
#define USE_LVGL_SPINBOX
+2
View File
@@ -77,3 +77,5 @@ dependencies:
- if: "idf_version >=6.0.0 && target in [esp32s2, esp32s3, esp32p4]"
esp32async/asynctcp:
version: 3.4.91
lvgl/lvgl:
version: 9.5.0
+51 -1
View File
@@ -24,6 +24,7 @@ except ImportError:
from esphome import core
from esphome.config_helpers import Extend, Remove
from esphome.const import CONF_DEFAULTS
from esphome.core import (
CORE,
DocumentRange,
@@ -88,6 +89,47 @@ def make_data_base(
return value
class ConfigContext:
"""This is a mixin class that holds substitution vars that should be applied
to the tagged node and its children. During configuration loading, context vars can
be added to nodes using `add_context` function, which applies the mixin storing
the captured values and unevaluated expressions.
The substitution pass then recreates the effective context by merging the context vars
from this node and parent nodes.
"""
@property
def vars(self) -> dict[str, Any]:
return self._context_vars
def set_context(self, vars: dict[str, Any]) -> None:
# pylint: disable=attribute-defined-outside-init
self._context_vars = vars
def add_context(value: Any, context_vars: dict[str, Any] | None) -> Any:
"""Tags a list/string/dict value with context vars that must be applied to it and its children
during the substitution pass. If no vars are given, no tagging is done.
If the value is already tagged, the new context vars are merged with existing ones,
with new vars taking precedence. Returns the value tagged with ConfigContext. Returns
the original value if value is not a list/string/dict.
"""
if isinstance(value, dict) and CONF_DEFAULTS in value:
context_vars = {
**value.pop(CONF_DEFAULTS),
**(context_vars or {}),
}
if isinstance(value, ConfigContext):
value.set_context({**value.vars, **(context_vars or {})})
return value
if context_vars and isinstance(value, (dict, list, str)):
value = add_class_to_obj(value, ConfigContext)
value.set_context(context_vars)
return value
def _add_data_ref(fn):
@functools.wraps(fn)
def wrapped(loader, node):
@@ -455,7 +497,7 @@ def parse_yaml(
def substitute_vars(config, vars):
from esphome.components import substitutions
from esphome.const import CONF_DEFAULTS, CONF_SUBSTITUTIONS
from esphome.const import CONF_SUBSTITUTIONS
org_subs = None
result = config
@@ -612,6 +654,12 @@ class ESPHomeDumper(yaml.SafeDumper):
return self.represent_secret(value.value)
return self.represent_scalar(tag="!lambda", value=value.value, style="|")
def represent_extend(self, value):
return self.represent_scalar(tag="!extend", value=value.value)
def represent_remove(self, value):
return self.represent_scalar(tag="!remove", value=value.value)
def represent_id(self, value):
if is_secret(value.id):
return self.represent_secret(value.id)
@@ -638,6 +686,8 @@ ESPHomeDumper.add_multi_representer(_BaseNetwork, ESPHomeDumper.represent_string
ESPHomeDumper.add_multi_representer(MACAddress, ESPHomeDumper.represent_stringify)
ESPHomeDumper.add_multi_representer(TimePeriod, ESPHomeDumper.represent_stringify)
ESPHomeDumper.add_multi_representer(Lambda, ESPHomeDumper.represent_lambda)
ESPHomeDumper.add_multi_representer(Extend, ESPHomeDumper.represent_extend)
ESPHomeDumper.add_multi_representer(Remove, ESPHomeDumper.represent_remove)
ESPHomeDumper.add_multi_representer(core.ID, ESPHomeDumper.represent_id)
ESPHomeDumper.add_multi_representer(uuid.UUID, ESPHomeDumper.represent_stringify)
ESPHomeDumper.add_multi_representer(Path, ESPHomeDumper.represent_stringify)
+4 -1
View File
@@ -42,7 +42,6 @@ lib_deps_base =
https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps
; 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
lib_deps =
${common.lib_deps_base}
@@ -120,6 +119,7 @@ lib_deps =
ESP8266mDNS ; mdns (Arduino built-in)
DNSServer ; captive_portal (Arduino built-in)
droscy/esp_wireguard@0.4.2 ; wireguard
lvgl/lvgl@9.5.0 ; lvgl
build_flags =
${common:arduino.build_flags}
@@ -204,6 +204,7 @@ lib_deps =
ayushsharma82/RPAsyncTCP@1.3.2 ; async_tcp
bblanchon/ArduinoJson@7.4.2 ; json
ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base
lvgl/lvgl@9.5.0 ; lvgl
build_flags =
${common:arduino.build_flags}
-DUSE_RP2040
@@ -221,6 +222,7 @@ lib_deps =
bblanchon/ArduinoJson@7.4.2 ; json
ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base
droscy/esp_wireguard@0.4.2 ; wireguard
lvgl/lvgl@9.5.0 ; lvgl
build_flags =
${common:arduino.build_flags}
-DUSE_LIBRETINY
@@ -242,6 +244,7 @@ build_flags =
lib_deps =
${common.lib_deps_base}
bblanchon/ArduinoJson@7.4.2 ; json
lvgl/lvgl@9.5.0 ; lvgl
; All the actual environments are defined below.
+2
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import argparse
from functools import partial
import os
from pathlib import Path
import sys
@@ -35,6 +36,7 @@ PLATFORMIO_OPTIONS = {
def run_tests(selected_components: list[str]) -> int:
os.environ["ASAN_OPTIONS"] = "detect_leaks=0"
return build_and_run(
selected_components=selected_components,
tests_dir=COMPONENTS_TESTS_DIR,
+57 -18
View File
@@ -42,6 +42,11 @@ lvgl:
disp_bg_color: color_id
disp_bg_image: cat_image
disp_bg_opa: cover
bottom_layer:
widgets:
- obj:
bg_color: 0x000000
bg_opa: cover
theme:
obj:
border_width: 1
@@ -49,12 +54,14 @@ lvgl:
gradients:
- id: color_bar
direction: hor
dither: err_diff
# dither: err_diff
stops:
- color: 0xFF0000
position: 0
opa: 100%
- color: 0xFFFF00
position: 42
opa: 80%
- color: 0x00FF00
position: 84
- color: 0x00FFFF
@@ -156,6 +163,16 @@ lvgl:
offset_x: !lambda return 20;
offset_y: !lambda return 20;
antialias: !lambda return true;
- id: msgbox_with_header_buttons
title: Header Buttons Test
body:
text: Testing header buttons
header_buttons:
- src: cat_image
on_click:
logger.log: Header button clicked
buttons:
- text: OK
- id: simple_msgbox
title: Simple
@@ -199,11 +216,11 @@ lvgl:
text: Unloaded
- lvgl.label.update:
id: msgbox_label
text: "" # Empty text
text: "" # Empty text
on_all_events:
logger.log:
format: "Event %s"
args: ['lv_event_code_name_for(event->code).c_str()']
args: ['lv_event_code_name_for(event).c_str()']
skip: true
layout:
type: Flex
@@ -241,7 +258,7 @@ lvgl:
on_all_events:
- logger.log:
format: "Event %s"
args: ['lv_event_code_name_for(event->code).c_str()']
args: ['lv_event_code_name_for(event).c_str()']
- lvgl.animimg.update:
id: anim_img
src: !lambda "return {dog_image, cat_image};"
@@ -306,7 +323,6 @@ lvgl:
anim_time: 1s
bg_color: light_blue
bg_grad_color: light_blue
bg_dither_mode: ordered
bg_grad_dir: hor
bg_grad_stop: 128
bg_image_opa: transp
@@ -354,10 +370,18 @@ lvgl:
text_line_space: 4
text_opa: cover
transform_angle: 180
transform_rotation: 90
transform_height: 100
transform_pivot_x: 50%
transform_pivot_y: 50%
transform_zoom: 0.5
transform_scale: 2.0
transform_scale_x: 1.5
transform_scale_y: 0.8
transform_skew_x: 10
transform_skew_y: 20
shadow_offset_x: 3
shadow_offset_y: 3
translate_x: 10
translate_y: 10
max_height: 100
@@ -549,14 +573,16 @@ lvgl:
arc_length: 120
spin_time: 2s
align: left_mid
- spinner:
align: right_mid
send_draw_task_events: true
- image:
id: lv_image
src: cat_image
align: top_left
y: "50"
mode: real
zoom: 2.0
angle: 45
scale: 2.0
rotation: 45
- tileview:
id: tileview_id
scrollbar_mode: active
@@ -661,8 +687,11 @@ lvgl:
src: cat_image
x: 100
y: 100
angle: 90
zoom: 2.0
rotation: 90
scale_x: 2.0
scale_y: 1.5
skew_x: 10
skew_y: 5
pivot_x: 25
pivot_y: 25
- lvgl.canvas.draw_line:
@@ -710,6 +739,9 @@ lvgl:
text:
format: "A string with a number %d"
args: ['(int)(random_uint32() % 1000)']
size: 120
dark_color: navy
light_color: white
- slider:
min_value: 0
@@ -928,7 +960,6 @@ lvgl:
grid_cell_row_pos: 0
grid_cell_column_pos: 0
src: !lambda return dog_image;
mode: virtual
on_click:
then:
- lvgl.tabview.select:
@@ -1023,10 +1054,18 @@ lvgl:
text_color: 0xFFFFFF
scales:
- ticks:
width: !lambda return 1;
width: 1
count: 61
length: 20%
radial_offset: 5
color: 0xFFFFFF
major:
stride: 5
width: 2
length: 8
color: 0xC0C0C0
radial_offset: 3
label_gap: 6
range_from: 0
range_to: 60
angle_range: 360
@@ -1037,15 +1076,15 @@ lvgl:
end_value: 60
color_start: 0x0000bd
color_end: 0xbd0000
width: !lambda return 1;
width: 1
- line:
opa: 50%
id: minute_hand
color: 0xFF0000
r_mod: !lambda return -1;
width: !lambda return 3;
-
angle_range: 330
length: 99%
radial_offset: 2
width: 1
- angle_range: 330
rotation: 300
range_from: 1
range_to: 12
@@ -1069,7 +1108,7 @@ lvgl:
value: 180
width: 4
color: 0xA0A0A0
r_mod: -20
length: 80%
opa: 0%
- id: page3
layout: Horizontal
+126
View File
@@ -6,6 +6,7 @@ import pytest
from esphome import core, yaml_util
from esphome.components import substitutions
from esphome.config_helpers import Extend, Remove
from esphome.core import EsphomeError
from esphome.util import OrderedDict
@@ -306,3 +307,128 @@ def test_dump_sort_keys() -> None:
# nested keys should also be sorted
assert "a_key:" in sorted_dump
assert sorted_dump.index("a_key:") < sorted_dump.index("z_key:")
@pytest.mark.parametrize(
"data",
[
{
"key1": "value1",
"key2": 42,
},
[1, 2, 3],
"simple string",
],
)
def test_config_context_mixin(data) -> None:
"""Test that ConfigContext mixin correctly stores and retrieves context vars in a dict."""
context_vars = {
"var1": "context_value1",
"var2": 100,
}
# Add context to the data
tagged_data = yaml_util.add_context(data, context_vars)
# Check that tagged_data has ConfigContext and correct vars
assert isinstance(tagged_data, type(data))
assert isinstance(tagged_data, yaml_util.ConfigContext)
assert tagged_data.vars == context_vars
# Check that original data is preserved
assert tagged_data == data
def test_config_context_mixin_no_context() -> None:
"""Test that add_context does not tag data when no context vars are provided."""
data = {"key": "value"}
# Add context with None
tagged_data = yaml_util.add_context(data, None)
# Should return original data without tagging
assert tagged_data is data
assert not isinstance(tagged_data, yaml_util.ConfigContext)
def test_config_context_mixin_merge_contexts() -> None:
"""Test that add_context merges new context vars with existing ones."""
data = {"key": "value"}
initial_context = {
"var1": "initial_value",
}
# First, add initial context
tagged_data = yaml_util.add_context(data, initial_context)
assert isinstance(tagged_data, yaml_util.ConfigContext)
assert tagged_data.vars == initial_context
# Now, add more context vars
new_context = {
"var2": "new_value",
"var1": "overridden_value", # This should override the initial var1
}
merged_tagged_data = yaml_util.add_context(tagged_data, new_context)
# Check that merged_tagged_data has merged context vars
expected_context = {
"var1": "overridden_value",
"var2": "new_value",
}
assert isinstance(merged_tagged_data, yaml_util.ConfigContext)
assert merged_tagged_data.vars == expected_context
# Check that original data is preserved
assert merged_tagged_data == data
@pytest.mark.parametrize("data", [42, 3.14, True, None])
def test_config_context_non_taggable(data) -> None:
"""Test that add_context ignores non-string scalar values."""
context_vars = {
"var1": "context_value",
}
# Add context to the scalar data
tagged_data = yaml_util.add_context(data, context_vars)
# Check that tagged_data has ConfigContext and correct vars
assert not isinstance(tagged_data, yaml_util.ConfigContext)
# Check that original data is preserved
assert tagged_data == data
def test_config_context_defaults_only() -> None:
"""Test that defaults: key is popped and used as context vars when no explicit vars given."""
data = {"defaults": {"x": "1", "y": "2"}, "key": "value"}
tagged = yaml_util.add_context(data, None)
assert isinstance(tagged, yaml_util.ConfigContext)
assert tagged.vars == {"x": "1", "y": "2"}
assert "defaults" not in tagged
def test_config_context_defaults_explicit_vars_override() -> None:
"""Test that explicit vars take precedence over defaults: values."""
data = {"defaults": {"x": "default_x", "z": "default_z"}, "key": "value"}
tagged = yaml_util.add_context(data, {"x": "explicit_x", "w": "explicit_w"})
assert isinstance(tagged, yaml_util.ConfigContext)
assert tagged.vars == {"x": "explicit_x", "z": "default_z", "w": "explicit_w"}
assert "defaults" not in tagged
def test_represent_extend() -> None:
"""Test that Extend objects are dumped as plain !extend scalars."""
assert yaml_util.dump({"key": Extend("my_id")}) == "key: !extend 'my_id'\n"
def test_represent_remove() -> None:
"""Test that Remove objects are dumped as plain !remove scalars."""
assert yaml_util.dump({"key": Remove("my_id")}) == "key: !remove 'my_id'\n"