mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 12:35:25 +00:00
[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:
@@ -1 +1 @@
|
||||
8e48e836c6fc196d3da000d46eb09db243b87fe33518a74e49c8e009d756074a
|
||||
44c877ff43765562ac8298902bf2208799643b77facf09c1c0c3c8c4e17187eb
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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_{};
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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],
|
||||
)
|
||||
|
||||
@@ -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_{};
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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_{};
|
||||
};
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -181,7 +181,8 @@ async def process_runtime_image_config(config: dict) -> RuntimeImageSettings:
|
||||
transparent = get_transparency_enum(config.get(CONF_TRANSPARENCY, "OPAQUE"))
|
||||
|
||||
# Get byte order (True for big endian, False for little endian)
|
||||
byte_order_big_endian = config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN"
|
||||
# If unspecified, use little endian
|
||||
byte_order_big_endian = config.get(CONF_BYTE_ORDER) == "BIG_ENDIAN"
|
||||
|
||||
# Get placeholder if specified
|
||||
placeholder = None
|
||||
|
||||
@@ -106,7 +106,7 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
|
||||
break;
|
||||
}
|
||||
case image::IMAGE_TYPE_RGB565: {
|
||||
uint32_t pos = this->get_position_(x, y);
|
||||
const size_t pos = (x + y * this->buffer_width_) * 2;
|
||||
Color mapped_color = color;
|
||||
this->map_chroma_key(mapped_color);
|
||||
uint16_t rgb565 = display::ColorUtil::color_to_565(mapped_color);
|
||||
@@ -118,7 +118,8 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
|
||||
this->buffer_[pos + 1] = static_cast<uint8_t>((rgb565 >> 8) & 0xFF);
|
||||
}
|
||||
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||
this->buffer_[pos + 2] = color.w;
|
||||
const size_t alpha_pos = pos / 2 + this->buffer_width_ * this->buffer_height_ * 2;
|
||||
this->buffer_[alpha_pos] = color.w;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -283,6 +284,10 @@ size_t RuntimeImage::resize_buffer_(int width, int height) {
|
||||
}
|
||||
|
||||
size_t RuntimeImage::get_buffer_size_(int width, int height) const {
|
||||
if (this->get_type() == image::IMAGE_TYPE_RGB565 && this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||
// Add extra alpha channel for RGB565 with alpha
|
||||
return width * height * 3;
|
||||
}
|
||||
return (this->get_bpp() * width + 7u) / 8u * height;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user