[lvgl] Migrate to library v9.5.0 (#12312)

Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
Clyde Stubbs
2026-03-19 17:31:33 +10:00
committed by GitHub
parent 9d6f2f71e8
commit 2341d510d3
54 changed files with 2312 additions and 1197 deletions

View File

@@ -1 +1 @@
8e48e836c6fc196d3da000d46eb09db243b87fe33518a74e49c8e009d756074a
44c877ff43765562ac8298902bf2208799643b77facf09c1c0c3c8c4e17187eb

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(

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;
}

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_{};

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]:

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;

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;

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,

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):

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)

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):

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)

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):

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],
)

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_{};

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,
)

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):

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);
}

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

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);

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())

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

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);

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)

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_{};
};

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))

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 {

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))

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))

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)

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

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)
)

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

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()

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()

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"

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()

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)

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.

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,
)

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())

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):

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())

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])

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()

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)

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:

View File

@@ -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(

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

View File

@@ -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;
}

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

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

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.

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