mirror of
https://github.com/esphome/esphome.git
synced 2026-10-06 02:51:29 +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:
co-authored by
pre-commit-ci-lite[bot]
Jonathan Swoboda
parent
9d6f2f71e8
commit
2341d510d3
+1
-1
@@ -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.
|
# get the codepoints from glyphsets and flatten to a set of chrs.
|
||||||
|
cg.add_define("USE_FONT")
|
||||||
point_set: set[str] = {
|
point_set: set[str] = {
|
||||||
chr(x)
|
chr(x)
|
||||||
for x in flatten(
|
for x in flatten(
|
||||||
|
|||||||
@@ -9,13 +9,87 @@ namespace font {
|
|||||||
static const char *const TAG = "font";
|
static const char *const TAG = "font";
|
||||||
|
|
||||||
#ifdef USE_LVGL_FONT
|
#ifdef USE_LVGL_FONT
|
||||||
const uint8_t *Font::get_glyph_bitmap(const lv_font_t *font, uint32_t 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};
|
||||||
auto *fe = (Font *) font->dsc;
|
|
||||||
const auto *gd = fe->get_glyph_data_(unicode_letter);
|
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) {
|
if (gd == nullptr) {
|
||||||
return 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) {
|
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_w = gd->width;
|
||||||
dsc->box_h = gd->height;
|
dsc->box_h = gd->height;
|
||||||
dsc->is_placeholder = 0;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ class Font
|
|||||||
uint8_t bpp_; // bits per pixel
|
uint8_t bpp_; // bits per pixel
|
||||||
#ifdef USE_LVGL_FONT
|
#ifdef USE_LVGL_FONT
|
||||||
lv_font_t lv_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);
|
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);
|
const Glyph *get_glyph_data_(uint32_t unicode_letter);
|
||||||
uint32_t last_letter_{};
|
uint32_t last_letter_{};
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from esphome.const import (
|
|||||||
CONF_URL,
|
CONF_URL,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, HexInt
|
from esphome.core import CORE, HexInt
|
||||||
|
from esphome.final_validate import full_config
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -84,7 +85,7 @@ class ImageEncoder:
|
|||||||
|
|
||||||
def __init__(self, width, height, transparency, dither, invert_alpha):
|
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 height: The image height in pixels
|
||||||
:param transparency: Transparency type
|
:param transparency: Transparency type
|
||||||
:param dither: Dither method
|
:param dither: Dither method
|
||||||
@@ -93,11 +94,12 @@ class ImageEncoder:
|
|||||||
self.transparency = transparency
|
self.transparency = transparency
|
||||||
self.width = width
|
self.width = width
|
||||||
self.height = height
|
self.height = height
|
||||||
self.data = [0 for _ in range(width * height)]
|
self.data = [0] * width * height
|
||||||
self.dither = dither
|
self.dither = dither
|
||||||
self.index = 0
|
self.index = 0
|
||||||
self.invert_alpha = invert_alpha
|
self.invert_alpha = invert_alpha
|
||||||
self.path = ""
|
self.path = ""
|
||||||
|
self.big_endian = False
|
||||||
|
|
||||||
def convert(self, image, path):
|
def convert(self, image, path):
|
||||||
"""
|
"""
|
||||||
@@ -119,12 +121,21 @@ class ImageEncoder:
|
|||||||
:return:
|
: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
|
@classmethod
|
||||||
def is_endian(cls) -> bool:
|
def is_endian(cls) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if the image encoder supports endianness configuration
|
Check if the image encoder supports endianness configuration
|
||||||
"""
|
"""
|
||||||
return getattr(cls, "set_big_endian", None) is not None
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_options(cls) -> list[str]:
|
def get_options(cls) -> list[str]:
|
||||||
@@ -212,18 +223,21 @@ class ImageGrayscale(ImageEncoder):
|
|||||||
|
|
||||||
class ImageRGB565(ImageEncoder):
|
class ImageRGB565(ImageEncoder):
|
||||||
def __init__(self, width, height, transparency, dither, invert_alpha):
|
def __init__(self, width, height, transparency, dither, invert_alpha):
|
||||||
stride = 3 if transparency == CONF_ALPHA_CHANNEL else 2
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
width * stride,
|
width * 2,
|
||||||
height,
|
height,
|
||||||
transparency,
|
transparency,
|
||||||
dither,
|
dither,
|
||||||
invert_alpha,
|
invert_alpha,
|
||||||
)
|
)
|
||||||
self.big_endian = True
|
self.alpha = [0] * width * height
|
||||||
|
|
||||||
def set_big_endian(self, big_endian: bool) -> None:
|
@classmethod
|
||||||
self.big_endian = big_endian
|
def is_endian(cls) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the image encoder supports endianness configuration
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
def convert(self, image, path):
|
def convert(self, image, path):
|
||||||
return image.convert("RGBA")
|
return image.convert("RGBA")
|
||||||
@@ -233,6 +247,9 @@ class ImageRGB565(ImageEncoder):
|
|||||||
r = r >> 3
|
r = r >> 3
|
||||||
g = g >> 2
|
g = g >> 2
|
||||||
b = b >> 3
|
b = b >> 3
|
||||||
|
if self.invert_alpha:
|
||||||
|
a ^= 0xFF
|
||||||
|
self.alpha[self.index // 2] = a
|
||||||
if self.transparency == CONF_CHROMA_KEY:
|
if self.transparency == CONF_CHROMA_KEY:
|
||||||
if r == 0 and g == 1 and b == 0:
|
if r == 0 and g == 1 and b == 0:
|
||||||
g = 0
|
g = 0
|
||||||
@@ -251,11 +268,10 @@ class ImageRGB565(ImageEncoder):
|
|||||||
self.index += 1
|
self.index += 1
|
||||||
self.data[self.index] = rgb >> 8
|
self.data[self.index] = rgb >> 8
|
||||||
self.index += 1
|
self.index += 1
|
||||||
|
|
||||||
|
def end_image(self):
|
||||||
if self.transparency == CONF_ALPHA_CHANNEL:
|
if self.transparency == CONF_ALPHA_CHANNEL:
|
||||||
if self.invert_alpha:
|
self.data.extend(self.alpha)
|
||||||
a ^= 0xFF
|
|
||||||
self.data[self.index] = a
|
|
||||||
self.index += 1
|
|
||||||
|
|
||||||
|
|
||||||
class ImageRGB(ImageEncoder):
|
class ImageRGB(ImageEncoder):
|
||||||
@@ -281,11 +297,11 @@ class ImageRGB(ImageEncoder):
|
|||||||
r = 0
|
r = 0
|
||||||
g = 1
|
g = 1
|
||||||
b = 0
|
b = 0
|
||||||
self.data[self.index] = r
|
self.data[self.index] = b
|
||||||
self.index += 1
|
self.index += 1
|
||||||
self.data[self.index] = g
|
self.data[self.index] = g
|
||||||
self.index += 1
|
self.index += 1
|
||||||
self.data[self.index] = b
|
self.data[self.index] = r
|
||||||
self.index += 1
|
self.index += 1
|
||||||
if self.transparency == CONF_ALPHA_CHANNEL:
|
if self.transparency == CONF_ALPHA_CHANNEL:
|
||||||
if self.invert_alpha:
|
if self.invert_alpha:
|
||||||
@@ -655,6 +671,24 @@ def _config_schema(value):
|
|||||||
CONFIG_SCHEMA = _config_schema
|
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):
|
async def write_image(config, all_frames=False):
|
||||||
path = Path(config[CONF_FILE])
|
path = Path(config[CONF_FILE])
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
@@ -720,6 +754,7 @@ async def write_image(config, all_frames=False):
|
|||||||
for col in range(width):
|
for col in range(width):
|
||||||
encoder.encode(pixels[row * width + col])
|
encoder.encode(pixels[row * width + col])
|
||||||
encoder.end_row()
|
encoder.end_row()
|
||||||
|
encoder.end_image()
|
||||||
|
|
||||||
rhs = [HexInt(x) for x in encoder.data]
|
rhs = [HexInt(x) for x in encoder.data]
|
||||||
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
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
|
return prog_arr, width, height, image_type, trans_value, frame_count
|
||||||
|
|
||||||
|
|
||||||
async def _image_to_code(entry):
|
def add_metadata(id: str, width: int, height: int, image_type: str, transparency):
|
||||||
"""
|
all_metadata = CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {})
|
||||||
Convert a single image entry to code and return its metadata.
|
all_metadata[str(id)] = ImageMetaData(
|
||||||
:param entry: The config entry for the image.
|
width=width, height=height, image_type=image_type, transparency=transparency
|
||||||
: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],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_define("USE_IMAGE")
|
cg.add_define("USE_IMAGE")
|
||||||
# By now the config will be a simple list.
|
# By now the config will be a simple list.
|
||||||
# Use a subkey to allow for other data in the future
|
for entry in config:
|
||||||
CORE.data[DOMAIN] = {
|
prog_arr, width, height, image_type, trans_value, _ = await write_image(entry)
|
||||||
KEY_METADATA: {
|
cg.new_Pvariable(
|
||||||
entry[CONF_ID].id: await _image_to_code(entry) for entry in config
|
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]:
|
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
|
#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.
|
// lazily construct lvgl image_dsc.
|
||||||
if (this->dsc_.data != this->data_start_) {
|
if (this->dsc_.data != this->data_start_) {
|
||||||
this->dsc_.data = this->data_start_;
|
this->dsc_.data = this->data_start_;
|
||||||
this->dsc_.header.always_zero = 0;
|
this->dsc_.header.reserved_2 = 0;
|
||||||
this->dsc_.header.reserved = 0;
|
this->dsc_.header.stride = this->get_width_stride();
|
||||||
this->dsc_.header.w = this->width_;
|
this->dsc_.header.w = this->width_;
|
||||||
this->dsc_.header.h = this->height_;
|
this->dsc_.header.h = this->height_;
|
||||||
this->dsc_.data_size = this->get_width_stride() * this->get_height();
|
this->dsc_.data_size = this->get_width_stride() * this->get_height();
|
||||||
switch (this->get_type()) {
|
switch (this->get_type()) {
|
||||||
case IMAGE_TYPE_BINARY:
|
case IMAGE_TYPE_BINARY:
|
||||||
this->dsc_.header.cf = LV_IMG_CF_ALPHA_1BIT;
|
this->dsc_.header.cf = LV_COLOR_FORMAT_A1;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case IMAGE_TYPE_GRAYSCALE:
|
case IMAGE_TYPE_GRAYSCALE:
|
||||||
this->dsc_.header.cf = LV_IMG_CF_ALPHA_8BIT;
|
this->dsc_.header.cf = LV_COLOR_FORMAT_A8;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case IMAGE_TYPE_RGB:
|
case IMAGE_TYPE_RGB:
|
||||||
@@ -138,7 +138,7 @@ lv_img_dsc_t *Image::get_lv_img_dsc() {
|
|||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
this->dsc_.header.cf =
|
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
|
#endif
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -146,14 +146,10 @@ lv_img_dsc_t *Image::get_lv_img_dsc() {
|
|||||||
#if LV_COLOR_DEPTH == 16
|
#if LV_COLOR_DEPTH == 16
|
||||||
switch (this->transparency_) {
|
switch (this->transparency_) {
|
||||||
case TRANSPARENCY_ALPHA_CHANNEL:
|
case TRANSPARENCY_ALPHA_CHANNEL:
|
||||||
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;
|
this->dsc_.header.cf = LV_COLOR_FORMAT_RGB565A8;
|
||||||
break;
|
|
||||||
case TRANSPARENCY_CHROMA_KEY:
|
|
||||||
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED;
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR;
|
this->dsc_.header.cf = LV_COLOR_FORMAT_RGB565;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
this->dsc_.header.cf =
|
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 {
|
Color Image::get_rgb_pixel_(int x, int y) const {
|
||||||
const uint32_t pos = (x + y * this->width_) * this->bpp_ / 8;
|
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),
|
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 + 2), 0xFF);
|
progmem_read_byte(this->data_start_ + pos + 0), 0xFF);
|
||||||
|
|
||||||
switch (this->transparency_) {
|
switch (this->transparency_) {
|
||||||
case TRANSPARENCY_CHROMA_KEY:
|
case TRANSPARENCY_CHROMA_KEY:
|
||||||
@@ -200,7 +196,7 @@ Color Image::get_rgb565_pixel_(int x, int y) const {
|
|||||||
auto a = 0xFF;
|
auto a = 0xFF;
|
||||||
switch (this->transparency_) {
|
switch (this->transparency_) {
|
||||||
case TRANSPARENCY_ALPHA_CHANNEL:
|
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;
|
break;
|
||||||
case TRANSPARENCY_CHROMA_KEY:
|
case TRANSPARENCY_CHROMA_KEY:
|
||||||
if (rgb565 == 0x0020)
|
if (rgb565 == 0x0020)
|
||||||
@@ -239,7 +235,7 @@ Image::Image(const uint8_t *data_start, int width, int height, ImageType type, T
|
|||||||
this->bpp_ = 8;
|
this->bpp_ = 8;
|
||||||
break;
|
break;
|
||||||
case IMAGE_TYPE_RGB565:
|
case IMAGE_TYPE_RGB565:
|
||||||
this->bpp_ = transparency == TRANSPARENCY_ALPHA_CHANNEL ? 24 : 16;
|
this->bpp_ = 16;
|
||||||
break;
|
break;
|
||||||
case IMAGE_TYPE_RGB:
|
case IMAGE_TYPE_RGB:
|
||||||
this->bpp_ = this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? 32 : 24;
|
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; }
|
bool has_transparency() const { return this->transparency_ != TRANSPARENCY_OPAQUE; }
|
||||||
|
|
||||||
#ifdef USE_LVGL
|
#ifdef USE_LVGL
|
||||||
lv_img_dsc_t *get_lv_img_dsc();
|
lv_image_dsc_t *get_lv_image_dsc();
|
||||||
#endif
|
#endif
|
||||||
protected:
|
protected:
|
||||||
bool get_binary_pixel_(int x, int y) const;
|
bool get_binary_pixel_(int x, int y) const;
|
||||||
|
|||||||
@@ -1,12 +1,30 @@
|
|||||||
import importlib
|
import importlib
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import pkgutil
|
import pkgutil
|
||||||
|
|
||||||
from esphome.automation import build_automation, validate_automation
|
from esphome.automation import build_automation, validate_automation
|
||||||
import esphome.codegen as cg
|
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.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
|
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
@@ -21,7 +39,6 @@ from esphome.const import (
|
|||||||
CONF_PAGES,
|
CONF_PAGES,
|
||||||
CONF_TIMEOUT,
|
CONF_TIMEOUT,
|
||||||
CONF_TRIGGER_ID,
|
CONF_TRIGGER_ID,
|
||||||
CONF_TYPE,
|
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, ID, Lambda
|
from esphome.core import CORE, ID, Lambda
|
||||||
from esphome.cpp_generator import MockObj
|
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 esphome.yaml_util import load_yaml
|
||||||
|
|
||||||
from . import defines as df, helpers, lv_validation as lvalid, widgets
|
from . import defines as df, helpers, lv_validation as lvalid, widgets
|
||||||
from .automation import disp_update, focused_widgets, refreshed_widgets
|
from .automation import focused_widgets, layers_to_code, lvgl_update, refreshed_widgets
|
||||||
from .defines import add_define
|
|
||||||
from .encoders import (
|
from .encoders import (
|
||||||
ENCODERS_CONFIG,
|
ENCODERS_CONFIG,
|
||||||
encoders_to_code,
|
encoders_to_code,
|
||||||
@@ -45,12 +61,13 @@ from .lvcode import LvContext, LvglComponent, lvgl_static
|
|||||||
from .schemas import (
|
from .schemas import (
|
||||||
DISP_BG_SCHEMA,
|
DISP_BG_SCHEMA,
|
||||||
FULL_STYLE_SCHEMA,
|
FULL_STYLE_SCHEMA,
|
||||||
|
STYLE_REMAP,
|
||||||
WIDGET_TYPES,
|
WIDGET_TYPES,
|
||||||
any_widget_schema,
|
any_widget_schema,
|
||||||
container_schema,
|
container_schema,
|
||||||
obj_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 .touchscreens import touchscreen_schema, touchscreens_to_code
|
||||||
from .trigger import add_on_boot_triggers, generate_triggers
|
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
|
from .types import IdleTrigger, PlainTrigger, lv_font_t, lv_group_t, lv_style_t, lvgl_ns
|
||||||
@@ -58,7 +75,7 @@ from .widgets import (
|
|||||||
LvScrActType,
|
LvScrActType,
|
||||||
Widget,
|
Widget,
|
||||||
add_widgets,
|
add_widgets,
|
||||||
get_scr_act,
|
get_screen_active,
|
||||||
set_obj_properties,
|
set_obj_properties,
|
||||||
styles_used,
|
styles_used,
|
||||||
)
|
)
|
||||||
@@ -84,7 +101,6 @@ DOMAIN = "lvgl"
|
|||||||
DEPENDENCIES = ["display"]
|
DEPENDENCIES = ["display"]
|
||||||
AUTO_LOAD = ["key_provider"]
|
AUTO_LOAD = ["key_provider"]
|
||||||
CODEOWNERS = ["@clydebarrow"]
|
CODEOWNERS = ["@clydebarrow"]
|
||||||
LOGGER = logging.getLogger(__name__)
|
|
||||||
HELLO_WORLD_FILE = "hello_world.yaml"
|
HELLO_WORLD_FILE = "hello_world.yaml"
|
||||||
|
|
||||||
|
|
||||||
@@ -102,6 +118,7 @@ def as_macro(macro, value):
|
|||||||
return f"#define {macro} {value}"
|
return f"#define {macro} {value}"
|
||||||
|
|
||||||
|
|
||||||
|
LVGL_VERSION = "9.5.0"
|
||||||
LV_CONF_FILENAME = "lv_conf.h"
|
LV_CONF_FILENAME = "lv_conf.h"
|
||||||
LV_CONF_H_FORMAT = """\
|
LV_CONF_H_FORMAT = """\
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -110,7 +127,17 @@ LV_CONF_H_FORMAT = """\
|
|||||||
|
|
||||||
|
|
||||||
def generate_lv_conf_h():
|
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()
|
definitions.sort()
|
||||||
return LV_CONF_H_FORMAT.format("\n".join(definitions))
|
return LV_CONF_H_FORMAT.format("\n".join(definitions))
|
||||||
|
|
||||||
@@ -133,7 +160,7 @@ def multi_conf_validate(configs: list[dict]):
|
|||||||
for item in (
|
for item in (
|
||||||
CONF_LOG_LEVEL,
|
CONF_LOG_LEVEL,
|
||||||
CONF_COLOR_DEPTH,
|
CONF_COLOR_DEPTH,
|
||||||
df.CONF_BYTE_ORDER,
|
CONF_BYTE_ORDER,
|
||||||
df.CONF_TRANSPARENCY_KEY,
|
df.CONF_TRANSPARENCY_KEY,
|
||||||
):
|
):
|
||||||
if base_config[item] != config[item]:
|
if base_config[item] != config[item]:
|
||||||
@@ -166,14 +193,7 @@ def final_validation(config_list):
|
|||||||
)
|
)
|
||||||
buffer_frac = config[CONF_BUFFER_SIZE]
|
buffer_frac = config[CONF_BUFFER_SIZE]
|
||||||
if CORE.is_esp32 and buffer_frac > 0.5 and PSRAM_DOMAIN not in global_config:
|
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")
|
df.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
|
|
||||||
)
|
|
||||||
for w in focused_widgets:
|
for w in focused_widgets:
|
||||||
path = global_config.get_path_for_id(w)
|
path = global_config.get_path_for_id(w)
|
||||||
widget_conf = global_config.get_config_for_path(path[:-1])
|
widget_conf = global_config.get_config_for_path(path[:-1])
|
||||||
@@ -205,39 +225,48 @@ def final_validation(config_list):
|
|||||||
async def to_code(configs):
|
async def to_code(configs):
|
||||||
config_0 = configs[0]
|
config_0 = configs[0]
|
||||||
# Global configuration
|
# 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")
|
cg.add_define("USE_LVGL")
|
||||||
# suppress default enabling of extra widgets
|
# suppress default enabling of extra widgets
|
||||||
add_define("_LV_KCONFIG_PRESENT")
|
# cg.add_define("LV_KCONFIG_PRESENT")
|
||||||
# Always enable - lots of things use it.
|
# Always enable - lots of things use it.
|
||||||
add_define("LV_DRAW_COMPLEX", "1")
|
df.add_define("LV_DRAW_SW_COMPLEX", "1")
|
||||||
add_define("LV_TICK_CUSTOM", "1")
|
|
||||||
add_define("LV_TICK_CUSTOM_INCLUDE", '"esphome/components/lvgl/lvgl_hal.h"')
|
|
||||||
add_define("LV_TICK_CUSTOM_SYS_TIME_EXPR", "(lv_millis())")
|
|
||||||
add_define("LV_MEM_CUSTOM", "1")
|
|
||||||
add_define("LV_MEM_CUSTOM_ALLOC", "lv_custom_mem_alloc")
|
|
||||||
add_define("LV_MEM_CUSTOM_FREE", "lv_custom_mem_free")
|
|
||||||
add_define("LV_MEM_CUSTOM_REALLOC", "lv_custom_mem_realloc")
|
|
||||||
add_define("LV_MEM_CUSTOM_INCLUDE", '"esphome/components/lvgl/lvgl_hal.h"')
|
|
||||||
|
|
||||||
add_define(
|
df.add_define(
|
||||||
"LV_LOG_LEVEL",
|
"LV_LOG_LEVEL",
|
||||||
f"LV_LOG_LEVEL_{df.LV_LOG_LEVELS[config_0[CONF_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(
|
cg.add_define(
|
||||||
"LVGL_LOG_LEVEL",
|
"LVGL_LOG_LEVEL",
|
||||||
cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[CONF_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:
|
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:
|
if config_0[CONF_COLOR_DEPTH] == 16:
|
||||||
add_define(
|
df.add_define(
|
||||||
"LV_COLOR_16_SWAP",
|
"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",
|
"LV_COLOR_CHROMA_KEY",
|
||||||
await lvalid.lv_color.process(config_0[df.CONF_TRANSPARENCY_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)
|
await cg.get_variable(font)
|
||||||
default_font = config_0[df.CONF_DEFAULT_FONT]
|
default_font = config_0[df.CONF_DEFAULT_FONT]
|
||||||
if not lvalid.is_lv_font(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})"
|
"LV_FONT_CUSTOM_DECLARE", f"LV_FONT_DECLARE(*{df.DEFAULT_ESPHOME_FONT})"
|
||||||
)
|
)
|
||||||
globfont_id = ID(
|
globfont_id = ID(
|
||||||
@@ -262,9 +291,9 @@ async def to_code(configs):
|
|||||||
MockObj(await lvalid.lv_font.process(default_font), "->").get_lv_font(),
|
MockObj(await lvalid.lv_font.process(default_font), "->").get_lv_font(),
|
||||||
static=False,
|
static=False,
|
||||||
)
|
)
|
||||||
add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT)
|
df.add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT)
|
||||||
else:
|
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())
|
cg.add(lvgl_static.esphome_lvgl_init())
|
||||||
default_group = get_default_group(config_0)
|
default_group = get_default_group(config_0)
|
||||||
|
|
||||||
@@ -293,8 +322,9 @@ async def to_code(configs):
|
|||||||
await cg.register_component(lv_component, config)
|
await cg.register_component(lv_component, config)
|
||||||
Widget.create(config[CONF_ID], lv_component, LvScrActType(), 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():
|
async with LvContext():
|
||||||
|
cg.add(lv_component.set_big_endian(config[CONF_BYTE_ORDER] == "big_endian"))
|
||||||
await touchscreens_to_code(lv_component, config)
|
await touchscreens_to_code(lv_component, config)
|
||||||
await encoders_to_code(lv_component, config, default_group)
|
await encoders_to_code(lv_component, config, default_group)
|
||||||
await keypads_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 set_obj_properties(lv_scr_act, config)
|
||||||
await add_widgets(lv_scr_act, config)
|
await add_widgets(lv_scr_act, config)
|
||||||
await add_pages(lv_component, 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 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.
|
# Set this directly since we are limited in how many methods can be added to the Widget class.
|
||||||
Widget.widgets_completed = True
|
Widget.widgets_completed = True
|
||||||
async with LvContext():
|
async with LvContext():
|
||||||
@@ -336,15 +367,53 @@ async def to_code(configs):
|
|||||||
# This must be done after all widgets are created
|
# This must be done after all widgets are created
|
||||||
for comp in helpers.lvgl_components_required:
|
for comp in helpers.lvgl_components_required:
|
||||||
cg.add_define(f"USE_LVGL_{comp.upper()}")
|
cg.add_define(f"USE_LVGL_{comp.upper()}")
|
||||||
if {"transform_angle", "transform_zoom"} & styles_used:
|
lv_image_formats = df.get_color_formats().copy()
|
||||||
add_define("LV_COLOR_SCREEN_TRANSP", "1")
|
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:
|
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()}")
|
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)
|
lv_conf_h_file = CORE.relative_src_path(LV_CONF_FILENAME)
|
||||||
write_file_if_changed(lv_conf_h_file, generate_lv_conf_h())
|
write_file_if_changed(lv_conf_h_file, generate_lv_conf_h())
|
||||||
cg.add_build_flag("-DLV_CONF_H=1")
|
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):
|
def display_schema(config):
|
||||||
@@ -357,7 +426,9 @@ def display_schema(config):
|
|||||||
|
|
||||||
def add_hello_world(config):
|
def add_hello_world(config):
|
||||||
if df.CONF_WIDGETS not in config and CONF_PAGES not in 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
|
hello_world_path = Path(__file__).parent / HELLO_WORLD_FILE
|
||||||
config[df.CONF_WIDGETS] = any_widget_schema()(load_yaml(hello_world_path))
|
config[df.CONF_WIDGETS] = any_widget_schema()(load_yaml(hello_world_path))
|
||||||
return config
|
return config
|
||||||
@@ -395,8 +466,8 @@ LVGL_SCHEMA = cv.All(
|
|||||||
cv.Optional(CONF_LOG_LEVEL, default="WARN"): cv.one_of(
|
cv.Optional(CONF_LOG_LEVEL, default="WARN"): cv.one_of(
|
||||||
*df.LV_LOG_LEVELS, upper=True
|
*df.LV_LOG_LEVELS, upper=True
|
||||||
),
|
),
|
||||||
cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of(
|
cv.Optional(CONF_BYTE_ORDER, default="big_endian"): cv.one_of(
|
||||||
"big_endian", "little_endian"
|
"big_endian", "little_endian", lower=True
|
||||||
),
|
),
|
||||||
cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list(
|
cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list(
|
||||||
cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)}).extend(
|
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_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA),
|
||||||
cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool,
|
cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool,
|
||||||
cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec),
|
cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec),
|
||||||
|
cv.Optional(df.CONF_BOTTOM_LAYER): container_schema(obj_spec),
|
||||||
cv.Optional(
|
cv.Optional(
|
||||||
df.CONF_TRANSPARENCY_KEY, default=0x000400
|
df.CONF_TRANSPARENCY_KEY, default=0x000400
|
||||||
): lvalid.lv_color,
|
): lvalid.lv_color,
|
||||||
|
|||||||
@@ -10,18 +10,21 @@ from esphome.cpp_generator import TemplateArguments, get_variable
|
|||||||
from esphome.cpp_types import nullptr
|
from esphome.cpp_types import nullptr
|
||||||
|
|
||||||
from .defines import (
|
from .defines import (
|
||||||
CONF_DISP_BG_COLOR,
|
CONF_BG_OPA,
|
||||||
CONF_DISP_BG_IMAGE,
|
CONF_BOTTOM_LAYER,
|
||||||
CONF_DISP_BG_OPA,
|
|
||||||
CONF_EDITING,
|
CONF_EDITING,
|
||||||
CONF_FREEZE,
|
CONF_FREEZE,
|
||||||
CONF_LVGL_ID,
|
CONF_LVGL_ID,
|
||||||
|
CONF_MAIN,
|
||||||
|
CONF_OBJ,
|
||||||
|
CONF_SCROLLBAR,
|
||||||
CONF_SHOW_SNOW,
|
CONF_SHOW_SNOW,
|
||||||
|
CONF_TOP_LAYER,
|
||||||
PARTS,
|
PARTS,
|
||||||
literal,
|
StaticCastExpression,
|
||||||
static_cast,
|
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 (
|
from .lvcode import (
|
||||||
LVGL_COMP_ARG,
|
LVGL_COMP_ARG,
|
||||||
UPDATE_EVENT,
|
UPDATE_EVENT,
|
||||||
@@ -42,13 +45,13 @@ from .schemas import (
|
|||||||
LIST_ACTION_SCHEMA,
|
LIST_ACTION_SCHEMA,
|
||||||
LVGL_SCHEMA,
|
LVGL_SCHEMA,
|
||||||
base_update_schema,
|
base_update_schema,
|
||||||
|
part_schema,
|
||||||
)
|
)
|
||||||
from .types import (
|
from .types import (
|
||||||
LV_STATE,
|
LV_STATE,
|
||||||
LvglAction,
|
LvglAction,
|
||||||
LvglCondition,
|
LvglCondition,
|
||||||
ObjUpdateAction,
|
ObjUpdateAction,
|
||||||
lv_disp_t,
|
|
||||||
lv_group_t,
|
lv_group_t,
|
||||||
lv_obj_base_t,
|
lv_obj_base_t,
|
||||||
lv_obj_t,
|
lv_obj_t,
|
||||||
@@ -56,7 +59,9 @@ from .types import (
|
|||||||
)
|
)
|
||||||
from .widgets import (
|
from .widgets import (
|
||||||
Widget,
|
Widget,
|
||||||
get_scr_act,
|
WidgetType,
|
||||||
|
add_widgets,
|
||||||
|
get_screen_active,
|
||||||
get_widgets,
|
get_widgets,
|
||||||
set_obj_properties,
|
set_obj_properties,
|
||||||
wait_for_widgets,
|
wait_for_widgets,
|
||||||
@@ -67,6 +72,41 @@ focused_widgets = set()
|
|||||||
refreshed_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(
|
async def action_to_code(
|
||||||
widgets: list[Widget],
|
widgets: list[Widget],
|
||||||
action: Callable[[Widget], Any],
|
action: Callable[[Widget], Any],
|
||||||
@@ -151,25 +191,6 @@ async def lvgl_is_idle(config, condition_id, template_arg, args):
|
|||||||
return var
|
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(
|
@automation.register_action(
|
||||||
"lvgl.widget.redraw",
|
"lvgl.widget.redraw",
|
||||||
ObjUpdateAction,
|
ObjUpdateAction,
|
||||||
@@ -187,7 +208,7 @@ async def disp_update(disp, config: dict):
|
|||||||
async def obj_invalidate_to_code(config, action_id, template_arg, args):
|
async def obj_invalidate_to_code(config, action_id, template_arg, args):
|
||||||
if CONF_LVGL_ID in config:
|
if CONF_LVGL_ID in config:
|
||||||
lv_comp = await cg.get_variable(config[CONF_LVGL_ID])
|
lv_comp = await cg.get_variable(config[CONF_LVGL_ID])
|
||||||
widgets = [get_scr_act(lv_comp)]
|
widgets = [get_screen_active(lv_comp)]
|
||||||
else:
|
else:
|
||||||
widgets = await get_widgets(config)
|
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)
|
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(
|
@automation.register_action(
|
||||||
"lvgl.update",
|
"lvgl.update",
|
||||||
LvglAction,
|
LvglAction,
|
||||||
DISP_BG_SCHEMA.extend(LVGL_SCHEMA).add_extra(
|
part_schema(layer_spec.parts)
|
||||||
cv.has_at_least_one_key(CONF_DISP_BG_COLOR, CONF_DISP_BG_IMAGE)
|
.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,
|
synchronous=True,
|
||||||
)
|
)
|
||||||
async def lvgl_update_to_code(config, action_id, template_arg, args):
|
async def lvgl_update_to_code(config, action_id, template_arg, args):
|
||||||
widgets = await get_widgets(config, CONF_LVGL_ID)
|
widgets = await get_widgets(config, CONF_LVGL_ID)
|
||||||
w = widgets[0]
|
w = widgets[0]
|
||||||
disp = literal(f"{w.obj}->get_disp()")
|
|
||||||
async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context:
|
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())
|
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
|
||||||
await cg.register_parented(var, w.var)
|
await cg.register_parented(var, w.var)
|
||||||
return var
|
return var
|
||||||
@@ -336,7 +367,7 @@ async def widget_focus(config, action_id, template_arg, args):
|
|||||||
widget = await get_widgets(config)
|
widget = await get_widgets(config)
|
||||||
if widget:
|
if widget:
|
||||||
widget = widget[0]
|
widget = widget[0]
|
||||||
group = static_cast(
|
group = StaticCastExpression(
|
||||||
lv_group_t.operator("ptr"), lv_expr.obj_get_group(widget.obj)
|
lv_group_t.operator("ptr"), lv_expr.obj_get_group(widget.obj)
|
||||||
)
|
)
|
||||||
elif group := config.get(CONF_GROUP):
|
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 import codegen as cg, config_validation as cv
|
||||||
from esphome.const import CONF_ITEMS
|
from esphome.const import CONF_ITEMS
|
||||||
from esphome.core import CORE, ID, Lambda
|
from esphome.core import CORE, ID, Lambda
|
||||||
from esphome.cpp_generator import LambdaExpression, MockObj
|
from esphome.cpp_generator import (
|
||||||
from esphome.cpp_types import uint32
|
CallExpression,
|
||||||
|
LambdaExpression,
|
||||||
|
MockObj,
|
||||||
|
MockObjClass,
|
||||||
|
)
|
||||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||||
from esphome.types import Expression, SafeExpType
|
from esphome.types import Expression, SafeExpType
|
||||||
|
|
||||||
@@ -21,8 +25,11 @@ LOGGER = logging.getLogger(__name__)
|
|||||||
lvgl_ns = cg.esphome_ns.namespace("lvgl")
|
lvgl_ns = cg.esphome_ns.namespace("lvgl")
|
||||||
|
|
||||||
DOMAIN = "lvgl"
|
DOMAIN = "lvgl"
|
||||||
|
KEY_COLOR_FORMATS = "color_formats"
|
||||||
KEY_LV_DEFINES = "lv_defines"
|
KEY_LV_DEFINES = "lv_defines"
|
||||||
|
KEY_REMAPPED_USES = "remapped_uses"
|
||||||
KEY_UPDATED_WIDGETS = "updated_widgets"
|
KEY_UPDATED_WIDGETS = "updated_widgets"
|
||||||
|
KEY_WARNINGS = "warnings"
|
||||||
|
|
||||||
|
|
||||||
def get_data(key, default=None):
|
def get_data(key, default=None):
|
||||||
@@ -33,10 +40,37 @@ def get_data(key, default=None):
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return CORE.data.setdefault(DOMAIN, {}).setdefault(
|
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"):
|
def add_define(macro, value="1"):
|
||||||
lv_defines = get_data(KEY_LV_DEFINES)
|
lv_defines = get_data(KEY_LV_DEFINES)
|
||||||
value = str(value)
|
value = str(value)
|
||||||
@@ -47,27 +81,43 @@ def add_define(macro, value="1"):
|
|||||||
lv_defines[macro] = value
|
lv_defines[macro] = value
|
||||||
|
|
||||||
|
|
||||||
|
def is_defined(macro):
|
||||||
|
return macro in get_data(KEY_LV_DEFINES)
|
||||||
|
|
||||||
|
|
||||||
def literal(arg) -> MockObj:
|
def literal(arg) -> MockObj:
|
||||||
if isinstance(arg, str):
|
if isinstance(arg, str):
|
||||||
return MockObj(arg)
|
return MockObj(arg)
|
||||||
return arg
|
return arg
|
||||||
|
|
||||||
|
|
||||||
def static_cast(type, value):
|
def addr(arg) -> MockObj:
|
||||||
return literal(f"static_cast<{type}>({value})")
|
return MockObj(f"&{arg}")
|
||||||
|
|
||||||
|
|
||||||
def call_lambda(lamb: LambdaExpression):
|
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()
|
expr = lamb.content.strip()
|
||||||
if expr.startswith("return") and expr.endswith(";"):
|
if expr.startswith("return") and expr.endswith(";"):
|
||||||
return expr[6:-1].strip()
|
# Convert a lambda returning a simple expression to just that expression
|
||||||
# If lambda has parameters, call it with those parameter names
|
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")
|
# Parameter names come from hardcoded component code (like "x", "it", "event")
|
||||||
# not from user input, so they're safe to use directly
|
# not from user input, so they're safe to use directly
|
||||||
if lamb.parameters and lamb.parameters.parameters:
|
if lamb.parameters and lamb.parameters.parameters:
|
||||||
param_names = ", ".join(str(param.id) for param in lamb.parameters.parameters)
|
return CallExpression(
|
||||||
return f"{lamb}({param_names})"
|
lamb, *[MockObj(x.id) for x in lamb.parameters.parameters]
|
||||||
return f"{lamb}()"
|
)
|
||||||
|
return CallExpression(lamb)
|
||||||
|
|
||||||
|
|
||||||
class LValidator:
|
class LValidator:
|
||||||
@@ -76,7 +126,7 @@ class LValidator:
|
|||||||
has `process()` to convert a value during code generation
|
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.validator = validator
|
||||||
self.rtype = rtype
|
self.rtype = rtype
|
||||||
self.retmapper = retmapper
|
self.retmapper = retmapper
|
||||||
@@ -99,10 +149,9 @@ class LValidator:
|
|||||||
from .lvcode import get_lambda_context_args
|
from .lvcode import get_lambda_context_args
|
||||||
|
|
||||||
args = args or get_lambda_context_args()
|
args = args or get_lambda_context_args()
|
||||||
return cg.RawExpression(
|
|
||||||
call_lambda(
|
return call_lambda(
|
||||||
await cg.process_lambda(value, args, return_type=self.rtype)
|
await cg.process_lambda(value, args, return_type=self.rtype)
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if self.retmapper is not None:
|
if self.retmapper is not None:
|
||||||
return self.retmapper(value)
|
return self.retmapper(value)
|
||||||
@@ -112,6 +161,8 @@ class LValidator:
|
|||||||
value = [
|
value = [
|
||||||
await cg.get_variable(x) if isinstance(x, ID) else x for x in 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)
|
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.
|
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.prefix = prefix
|
||||||
self.choices = choices
|
self.choices = tuple(x.upper() for x in choices)
|
||||||
prefixed_choices = [prefix + v for v 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)
|
prefixed_validator = cv.one_of(*prefixed_choices, upper=True)
|
||||||
|
|
||||||
@schema_extractor("one_of")
|
@schema_extractor("one_of")
|
||||||
@@ -136,24 +188,30 @@ class LvConstant(LValidator):
|
|||||||
return prefixed_validator(value)
|
return prefixed_validator(value)
|
||||||
return self.prefix + cv.one_of(*choices, upper=True)(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.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(
|
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):
|
def mapper(self, value):
|
||||||
if not isinstance(value, list):
|
if not isinstance(value, list):
|
||||||
value = [value]
|
value = [value]
|
||||||
return literal(
|
value = [
|
||||||
"|".join(
|
(
|
||||||
[
|
str(v).upper()
|
||||||
str(v) if str(v).startswith(self.prefix) else self.prefix + str(v)
|
if str(v).startswith(self.prefix)
|
||||||
for v in value
|
else self.prefix + str(v).upper()
|
||||||
]
|
)
|
||||||
).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):
|
def extend(self, *choices):
|
||||||
"""
|
"""
|
||||||
@@ -161,7 +219,14 @@ class LvConstant(LValidator):
|
|||||||
:param choices: The extra choices
|
:param choices: The extra choices
|
||||||
:return: A new LVConstant instance
|
: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
|
# Parts
|
||||||
@@ -277,11 +342,13 @@ PARTS = (
|
|||||||
CONF_KNOB,
|
CONF_KNOB,
|
||||||
CONF_SELECTED,
|
CONF_SELECTED,
|
||||||
CONF_ITEMS,
|
CONF_ITEMS,
|
||||||
CONF_TICKS,
|
# CONF_TICKS,
|
||||||
CONF_CURSOR,
|
CONF_CURSOR,
|
||||||
CONF_TEXTAREA_PLACEHOLDER,
|
CONF_TEXTAREA_PLACEHOLDER,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
LV_PART = LvConstant("LV_PART_", *(p.upper() for p in PARTS))
|
||||||
|
|
||||||
KEYBOARD_MODES = LvConstant(
|
KEYBOARD_MODES = LvConstant(
|
||||||
"LV_KEYBOARD_MODE_",
|
"LV_KEYBOARD_MODE_",
|
||||||
"TEXT_LOWER",
|
"TEXT_LOWER",
|
||||||
@@ -359,6 +426,7 @@ OBJ_FLAGS = (
|
|||||||
"overflow_visible",
|
"overflow_visible",
|
||||||
"layout_1",
|
"layout_1",
|
||||||
"layout_2",
|
"layout_2",
|
||||||
|
"send_draw_task_events",
|
||||||
"widget_1",
|
"widget_1",
|
||||||
"widget_2",
|
"widget_2",
|
||||||
"user_1",
|
"user_1",
|
||||||
@@ -366,12 +434,14 @@ OBJ_FLAGS = (
|
|||||||
"user_3",
|
"user_3",
|
||||||
"user_4",
|
"user_4",
|
||||||
)
|
)
|
||||||
|
LV_OBJ_FLAG = LvConstant("LV_OBJ_FLAG_", *OBJ_FLAGS)
|
||||||
|
|
||||||
ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL")
|
ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL")
|
||||||
BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE")
|
BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE")
|
||||||
|
SLIDER_MODES = LvConstant("LV_SLIDER_MODE_", "NORMAL", "SYMMETRICAL", "RANGE")
|
||||||
|
|
||||||
BUTTONMATRIX_CTRLS = LvConstant(
|
BUTTONMATRIX_CTRLS = LvConstant(
|
||||||
"LV_BTNMATRIX_CTRL_",
|
"LV_BUTTONMATRIX_CTRL_",
|
||||||
"HIDDEN",
|
"HIDDEN",
|
||||||
"NO_REPEAT",
|
"NO_REPEAT",
|
||||||
"DISABLED",
|
"DISABLED",
|
||||||
@@ -434,12 +504,16 @@ CONF_ACCEPTED_CHARS = "accepted_chars"
|
|||||||
CONF_ADJUSTABLE = "adjustable"
|
CONF_ADJUSTABLE = "adjustable"
|
||||||
CONF_ALIGN = "align"
|
CONF_ALIGN = "align"
|
||||||
CONF_ALIGN_TO = "align_to"
|
CONF_ALIGN_TO = "align_to"
|
||||||
|
CONF_ANGLE_RANGE = "angle_range"
|
||||||
CONF_ANIMATED = "animated"
|
CONF_ANIMATED = "animated"
|
||||||
CONF_ANIMATION = "animation"
|
CONF_ANIMATION = "animation"
|
||||||
|
CONF_ANIMATIONS = "animations"
|
||||||
CONF_ANTIALIAS = "antialias"
|
CONF_ANTIALIAS = "antialias"
|
||||||
CONF_ARC_LENGTH = "arc_length"
|
CONF_ARC_LENGTH = "arc_length"
|
||||||
CONF_AUTO_START = "auto_start"
|
CONF_AUTO_START = "auto_start"
|
||||||
CONF_BACKGROUND_STYLE = "background_style"
|
CONF_BACKGROUND_STYLE = "background_style"
|
||||||
|
CONF_BG_OPA = "bg_opa"
|
||||||
|
CONF_BOTTOM_LAYER = "bottom_layer"
|
||||||
CONF_BUTTON_STYLE = "button_style"
|
CONF_BUTTON_STYLE = "button_style"
|
||||||
CONF_DECIMAL_PLACES = "decimal_places"
|
CONF_DECIMAL_PLACES = "decimal_places"
|
||||||
CONF_COLUMN = "column"
|
CONF_COLUMN = "column"
|
||||||
@@ -449,9 +523,11 @@ CONF_DISP_BG_IMAGE = "disp_bg_image"
|
|||||||
CONF_DISP_BG_OPA = "disp_bg_opa"
|
CONF_DISP_BG_OPA = "disp_bg_opa"
|
||||||
CONF_BODY = "body"
|
CONF_BODY = "body"
|
||||||
CONF_BUTTONS = "buttons"
|
CONF_BUTTONS = "buttons"
|
||||||
CONF_BYTE_ORDER = "byte_order"
|
|
||||||
CONF_CHANGE_RATE = "change_rate"
|
CONF_CHANGE_RATE = "change_rate"
|
||||||
CONF_CLOSE_BUTTON = "close_button"
|
CONF_CLOSE_BUTTON = "close_button"
|
||||||
|
CONF_COLOR_DEPTH = "color_depth"
|
||||||
|
CONF_COLOR_END = "color_end"
|
||||||
|
CONF_COLOR_START = "color_start"
|
||||||
CONF_CONTAINER = "container"
|
CONF_CONTAINER = "container"
|
||||||
CONF_CONTROL = "control"
|
CONF_CONTROL = "control"
|
||||||
CONF_DEFAULT_FONT = "default_font"
|
CONF_DEFAULT_FONT = "default_font"
|
||||||
@@ -483,8 +559,10 @@ CONF_GRID_COLUMN_ALIGN = "grid_column_align"
|
|||||||
CONF_GRID_COLUMNS = "grid_columns"
|
CONF_GRID_COLUMNS = "grid_columns"
|
||||||
CONF_GRID_ROW_ALIGN = "grid_row_align"
|
CONF_GRID_ROW_ALIGN = "grid_row_align"
|
||||||
CONF_GRID_ROWS = "grid_rows"
|
CONF_GRID_ROWS = "grid_rows"
|
||||||
|
CONF_HEADER_BUTTONS = "header_buttons"
|
||||||
CONF_HEADER_MODE = "header_mode"
|
CONF_HEADER_MODE = "header_mode"
|
||||||
CONF_HOME = "home"
|
CONF_HOME = "home"
|
||||||
|
CONF_INDICATORS = "indicators"
|
||||||
CONF_INITIAL_FOCUS = "initial_focus"
|
CONF_INITIAL_FOCUS = "initial_focus"
|
||||||
CONF_SELECTED_DIGIT = "selected_digit"
|
CONF_SELECTED_DIGIT = "selected_digit"
|
||||||
CONF_KEY_CODE = "key_code"
|
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_LONG_PRESS_REPEAT_TIME = "long_press_repeat_time"
|
||||||
CONF_LVGL_ID = "lvgl_id"
|
CONF_LVGL_ID = "lvgl_id"
|
||||||
CONF_LONG_MODE = "long_mode"
|
CONF_LONG_MODE = "long_mode"
|
||||||
|
CONF_MAJOR_TICKS_STYLE = "major_ticks_style"
|
||||||
CONF_MSGBOXES = "msgboxes"
|
CONF_MSGBOXES = "msgboxes"
|
||||||
CONF_OBJ = "obj"
|
CONF_OBJ = "obj"
|
||||||
CONF_ONE_CHECKED = "one_checked"
|
CONF_ONE_CHECKED = "one_checked"
|
||||||
@@ -517,12 +596,15 @@ CONF_PIVOT_Y = "pivot_y"
|
|||||||
CONF_PLACEHOLDER_TEXT = "placeholder_text"
|
CONF_PLACEHOLDER_TEXT = "placeholder_text"
|
||||||
CONF_POINTS = "points"
|
CONF_POINTS = "points"
|
||||||
CONF_PREVIOUS = "previous"
|
CONF_PREVIOUS = "previous"
|
||||||
|
CONF_RADIUS = "radius"
|
||||||
CONF_REPEAT_COUNT = "repeat_count"
|
CONF_REPEAT_COUNT = "repeat_count"
|
||||||
CONF_RECOLOR = "recolor"
|
CONF_RECOLOR = "recolor"
|
||||||
CONF_RESUME_ON_INPUT = "resume_on_input"
|
CONF_RESUME_ON_INPUT = "resume_on_input"
|
||||||
CONF_RIGHT_BUTTON = "right_button"
|
CONF_RIGHT_BUTTON = "right_button"
|
||||||
CONF_ROLLOVER = "rollover"
|
CONF_ROLLOVER = "rollover"
|
||||||
CONF_ROOT_BACK_BTN = "root_back_btn"
|
CONF_ROOT_BACK_BTN = "root_back_btn"
|
||||||
|
CONF_ROWS = "rows"
|
||||||
|
CONF_SCALE = "scale"
|
||||||
CONF_SCALE_LINES = "scale_lines"
|
CONF_SCALE_LINES = "scale_lines"
|
||||||
CONF_SCROLLBAR_MODE = "scrollbar_mode"
|
CONF_SCROLLBAR_MODE = "scrollbar_mode"
|
||||||
CONF_SCROLL_DIR = "scroll_dir"
|
CONF_SCROLL_DIR = "scroll_dir"
|
||||||
@@ -536,6 +618,7 @@ CONF_SRC = "src"
|
|||||||
CONF_START_ANGLE = "start_angle"
|
CONF_START_ANGLE = "start_angle"
|
||||||
CONF_START_VALUE = "start_value"
|
CONF_START_VALUE = "start_value"
|
||||||
CONF_STATES = "states"
|
CONF_STATES = "states"
|
||||||
|
CONF_STRIDE = "stride"
|
||||||
CONF_STYLE = "style"
|
CONF_STYLE = "style"
|
||||||
CONF_STYLES = "styles"
|
CONF_STYLES = "styles"
|
||||||
CONF_STYLE_DEFINITIONS = "style_definitions"
|
CONF_STYLE_DEFINITIONS = "style_definitions"
|
||||||
@@ -544,6 +627,7 @@ CONF_SKIP = "skip"
|
|||||||
CONF_SYMBOL = "symbol"
|
CONF_SYMBOL = "symbol"
|
||||||
CONF_TAB_ID = "tab_id"
|
CONF_TAB_ID = "tab_id"
|
||||||
CONF_TABS = "tabs"
|
CONF_TABS = "tabs"
|
||||||
|
CONF_TICK_STYLE = "tick_style"
|
||||||
CONF_TIME_FORMAT = "time_format"
|
CONF_TIME_FORMAT = "time_format"
|
||||||
CONF_TILE = "tile"
|
CONF_TILE = "tile"
|
||||||
CONF_TILE_ID = "tile_id"
|
CONF_TILE_ID = "tile_id"
|
||||||
@@ -551,6 +635,8 @@ CONF_TILES = "tiles"
|
|||||||
CONF_TITLE = "title"
|
CONF_TITLE = "title"
|
||||||
CONF_TOP_LAYER = "top_layer"
|
CONF_TOP_LAYER = "top_layer"
|
||||||
CONF_TOUCHSCREENS = "touchscreens"
|
CONF_TOUCHSCREENS = "touchscreens"
|
||||||
|
CONF_TRANSFORM_ROTATION = "transform_rotation"
|
||||||
|
CONF_TRANSFORM_SCALE = "transform_scale"
|
||||||
CONF_TRANSPARENCY_KEY = "transparency_key"
|
CONF_TRANSPARENCY_KEY = "transparency_key"
|
||||||
CONF_THEME = "theme"
|
CONF_THEME = "theme"
|
||||||
CONF_UPDATE_ON_RELEASE = "update_on_release"
|
CONF_UPDATE_ON_RELEASE = "update_on_release"
|
||||||
@@ -578,6 +664,16 @@ LV_KEYS = LvConstant(
|
|||||||
"END",
|
"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"
|
DEFAULT_ESPHOME_FONT = "esphome_lv_default_font"
|
||||||
|
|
||||||
@@ -590,3 +686,29 @@ def join_enums(enums, prefix=""):
|
|||||||
if prefix:
|
if prefix:
|
||||||
return literal("|".join(f"{prefix}{e.upper()}" for e in enums))
|
return literal("|".join(f"{prefix}{e.upper()}" for e in enums))
|
||||||
return literal("|".join(f"(int){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())
|
lv_assign(group, lv_expr.group_create())
|
||||||
else:
|
else:
|
||||||
group = default_group
|
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):
|
async def initial_focus_to_code(config):
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ from esphome.const import (
|
|||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_POSITION,
|
CONF_POSITION,
|
||||||
)
|
)
|
||||||
|
from esphome.core import ID
|
||||||
from esphome.cpp_generator import MockObj
|
from esphome.cpp_generator import MockObj
|
||||||
|
|
||||||
from .defines import CONF_GRADIENTS, LV_DITHER, LV_GRAD_DIR, add_define
|
from .defines import CONF_GRADIENTS, CONF_OPA, LV_DITHER, add_define, add_warning
|
||||||
from .lv_validation import lv_color, lv_fraction
|
from .lv_validation import lv_color, lv_percentage, opacity
|
||||||
from .lvcode import lv_assign
|
from .lvcode import lv
|
||||||
from .types import lv_gradient_t
|
from .types import lv_color_t, lv_gradient_t, lv_opa_t
|
||||||
|
|
||||||
CONF_STOPS = "stops"
|
CONF_STOPS = "stops"
|
||||||
|
|
||||||
@@ -27,14 +28,17 @@ GRADIENT_SCHEMA = cv.ensure_list(
|
|||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
cv.GenerateID(CONF_ID): cv.declare_id(lv_gradient_t),
|
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.Optional(CONF_DITHER, default="NONE"): LV_DITHER.one_of,
|
||||||
cv.Required(CONF_STOPS): cv.All(
|
cv.Required(CONF_STOPS): cv.All(
|
||||||
[
|
[
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_COLOR): lv_color,
|
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):
|
async def gradients_to_code(config):
|
||||||
max_stops = 2
|
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, ()):
|
for gradient in config.get(CONF_GRADIENTS, ()):
|
||||||
var = MockObj(cg.new_Pvariable(gradient[CONF_ID]), "->")
|
var = MockObj(cg.new_Pvariable(gradient[CONF_ID]), "->")
|
||||||
max_stops = max(max_stops, len(gradient[CONF_STOPS]))
|
idbase = gradient[CONF_ID].id
|
||||||
lv_assign(var.dir, await LV_GRAD_DIR.process(gradient[CONF_DIRECTION]))
|
stops = gradient[CONF_STOPS]
|
||||||
lv_assign(var.dither, await LV_DITHER.process(gradient[CONF_DITHER]))
|
max_stops = max(max_stops, len(stops))
|
||||||
lv_assign(var.stops_count, len(gradient[CONF_STOPS]))
|
if gradient[CONF_DIRECTION].startswith("VER"):
|
||||||
for index, stop in enumerate(gradient[CONF_STOPS]):
|
lv.grad_vertical_init(var)
|
||||||
lv_assign(var.stops[index].color, await lv_color.process(stop[CONF_COLOR]))
|
else:
|
||||||
lv_assign(
|
lv.grad_horizontal_init(var)
|
||||||
var.stops[index].frac, await lv_fraction.process(stop[CONF_POSITION])
|
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)
|
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())
|
lv_assign(group, lv_expr.group_create())
|
||||||
else:
|
else:
|
||||||
group = default_group
|
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):
|
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 = {
|
GRID_CELL_SCHEMA = {
|
||||||
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
|
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_COLUMN_POS): cv.positive_int,
|
||||||
cv.Optional(CONF_GRID_CELL_ROW_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, default=1): cv.positive_int,
|
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_X_ALIGN): grid_alignments,
|
||||||
cv.Optional(CONF_GRID_CELL_Y_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_ROW_POS): cv.positive_int,
|
||||||
cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
|
cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
|
||||||
cv.Optional(
|
cv.Optional(CONF_GRID_CELL_ROW_SPAN): cv.int_range(min=1),
|
||||||
CONF_GRID_CELL_ROW_SPAN, default=1
|
cv.Optional(CONF_GRID_CELL_COLUMN_SPAN): cv.int_range(min=1),
|
||||||
): cv.positive_int,
|
|
||||||
cv.Optional(
|
|
||||||
CONF_GRID_CELL_COLUMN_SPAN, default=1
|
|
||||||
): cv.positive_int,
|
|
||||||
cv.Optional(
|
cv.Optional(
|
||||||
CONF_GRID_CELL_X_ALIGN, default="center"
|
CONF_GRID_CELL_X_ALIGN, default="center"
|
||||||
): grid_alignments,
|
): grid_alignments,
|
||||||
@@ -231,8 +227,8 @@ class GridLayout(Layout):
|
|||||||
{
|
{
|
||||||
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
|
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_COLUMN_POS): cv.positive_int,
|
||||||
cv.Optional(CONF_GRID_CELL_ROW_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, default=1): cv.positive_int,
|
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_X_ALIGN): grid_alignments,
|
||||||
cv.Optional(CONF_GRID_CELL_Y_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_ROW_POS] = row
|
||||||
w[CONF_GRID_CELL_COLUMN_POS] = column
|
w[CONF_GRID_CELL_COLUMN_POS] = column
|
||||||
|
|
||||||
for i in range(w[CONF_GRID_CELL_ROW_SPAN]):
|
row_span = w.get(CONF_GRID_CELL_ROW_SPAN, 1)
|
||||||
for j in range(w[CONF_GRID_CELL_COLUMN_SPAN]):
|
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:
|
if row + i >= rows or column + j >= columns:
|
||||||
raise cv.Invalid(
|
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}",
|
f"exceeds grid size {rows}x{columns}",
|
||||||
[CONF_WIDGETS, index],
|
[CONF_WIDGETS, index],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class LVLight : public light::LightOutput {
|
|||||||
void set_value_(lv_color_t value) {
|
void set_value_(lv_color_t value) {
|
||||||
lv_led_set_color(this->obj_, value);
|
lv_led_set_color(this->obj_, value);
|
||||||
lv_led_on(this->obj_);
|
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_{};
|
lv_obj_t *obj_{};
|
||||||
optional<lv_color_t> initial_value_{};
|
optional<lv_color_t> initial_value_{};
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from .defines import (
|
|||||||
LV_FONTS,
|
LV_FONTS,
|
||||||
LValidator,
|
LValidator,
|
||||||
LvConstant,
|
LvConstant,
|
||||||
|
StaticCastExpression,
|
||||||
call_lambda,
|
call_lambda,
|
||||||
literal,
|
literal,
|
||||||
)
|
)
|
||||||
@@ -40,22 +41,28 @@ from .helpers import (
|
|||||||
lv_fonts_used,
|
lv_fonts_used,
|
||||||
requires_component,
|
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")
|
@schema_extractor("one_of")
|
||||||
def opacity_validator(value):
|
def opacity_validator(value):
|
||||||
if value == SCHEMA_EXTRACT:
|
if value == SCHEMA_EXTRACT:
|
||||||
return opacity_consts.choices
|
return LV_OPA.choices
|
||||||
value = cv.Any(cv.percentage, opacity_consts.one_of)(value)
|
value = cv.Any(cv.percentage, LV_OPA.one_of)(value)
|
||||||
if isinstance(value, float):
|
if value == str(LV_OPA.COVER):
|
||||||
return int(value * 255)
|
value = 1.0
|
||||||
return value
|
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 = {
|
COLOR_NAMES = {
|
||||||
"aliceblue": 0xF0F8FF,
|
"aliceblue": 0xF0F8FF,
|
||||||
@@ -244,7 +251,17 @@ def option_string(value):
|
|||||||
return 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):
|
def pixels_or_percent_validator(value):
|
||||||
@@ -252,16 +269,17 @@ def pixels_or_percent_validator(value):
|
|||||||
if value == SCHEMA_EXTRACT:
|
if value == SCHEMA_EXTRACT:
|
||||||
return ["pixels", "..%"]
|
return ["pixels", "..%"]
|
||||||
if isinstance(value, str) and value.lower().endswith("px"):
|
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):
|
if isinstance(value, str) and re.match(r"^lv_pct\((\d+)\)$", value):
|
||||||
return value
|
return int(value[6:-1]) / 100.0
|
||||||
value = cv.Any(cv.int_, cv.percentage)(value)
|
return cv.Any(cv.int_, cv.possibly_negative_percentage)(value)
|
||||||
if isinstance(value, int):
|
|
||||||
return value
|
|
||||||
return f"lv_pct({int(value * 100)})"
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
def pixels_validator(value):
|
||||||
@@ -282,15 +300,11 @@ def padding_validator(value):
|
|||||||
padding = LValidator(padding_validator, int32, retmapper=literal)
|
padding = LValidator(padding_validator, int32, retmapper=literal)
|
||||||
|
|
||||||
|
|
||||||
def zoom_validator(value):
|
def scale_validator(value):
|
||||||
return cv.float_range(0.1, 10.0)(value)
|
return cv.float_range(0.1, 10.0)(value)
|
||||||
|
|
||||||
|
|
||||||
def zoom_retmapper(value):
|
scale = LValidator(scale_validator, uint32, retmapper=lambda x: int(x * 256))
|
||||||
return int(value * 256)
|
|
||||||
|
|
||||||
|
|
||||||
zoom = LValidator(zoom_validator, uint32, retmapper=zoom_retmapper)
|
|
||||||
|
|
||||||
|
|
||||||
def angle(value):
|
def angle(value):
|
||||||
@@ -321,17 +335,23 @@ def size_validator(value):
|
|||||||
return pixels_or_percent_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")
|
@schema_extractor("one_of")
|
||||||
def fraction_validator(value):
|
def fraction_validator(value):
|
||||||
if value == SCHEMA_EXTRACT:
|
if value == SCHEMA_EXTRACT:
|
||||||
return radius_consts.choices
|
return LV_RADIUS.choices
|
||||||
value = cv.Any(size, cv.percentage, radius_consts.one_of)(value)
|
value = cv.Any(size, cv.percentage, LV_RADIUS.one_of)(value)
|
||||||
if isinstance(value, float):
|
if isinstance(value, float):
|
||||||
return int(value * 255)
|
return int(value * 255)
|
||||||
return value
|
return value
|
||||||
@@ -374,12 +394,6 @@ lv_image_list = LValidator(
|
|||||||
lv_bool = LValidator(cv.boolean, cg.bool_, retmapper=literal)
|
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):
|
def lvms_validator_(value):
|
||||||
if value == "never":
|
if value == "never":
|
||||||
value = "2147483647ms"
|
value = "2147483647ms"
|
||||||
@@ -424,30 +438,28 @@ class TextValidator(LValidator):
|
|||||||
if time_format := value.get(CONF_TIME_FORMAT):
|
if time_format := value.get(CONF_TIME_FORMAT):
|
||||||
source = value[CONF_TIME]
|
source = value[CONF_TIME]
|
||||||
if isinstance(source, Lambda):
|
if isinstance(source, Lambda):
|
||||||
time_format = cpp_string_escape(time_format)
|
source = MockObj(
|
||||||
return cg.RawExpression(
|
|
||||||
call_lambda(
|
call_lambda(
|
||||||
await cg.process_lambda(source, args, return_type=ESPTime)
|
await cg.process_lambda(source, args, return_type=ESPTime)
|
||||||
)
|
)
|
||||||
+ f".strftime({time_format}).c_str()"
|
|
||||||
)
|
)
|
||||||
# must be an ID
|
# must be an ID
|
||||||
source = await cg.get_variable(source)
|
else:
|
||||||
return source.now().strftime(time_format).c_str()
|
source = (await cg.get_variable(source)).now()
|
||||||
|
return source.strftime(time_format).c_str()
|
||||||
if isinstance(value, Lambda):
|
if isinstance(value, Lambda):
|
||||||
value = call_lambda(
|
value = call_lambda(
|
||||||
await cg.process_lambda(value, args, return_type=self.rtype)
|
await cg.process_lambda(value, args, return_type=self.rtype)
|
||||||
)
|
)
|
||||||
|
textvalue = str(value)
|
||||||
|
|
||||||
# Was the lambda call reduced to a string?
|
# Was the lambda call reduced to a string?
|
||||||
if value.endswith("c_str()") or (
|
if textvalue.endswith("c_str()") or (
|
||||||
value.endswith('"') and value.startswith('"')
|
textvalue.endswith('"') and textvalue.startswith('"')
|
||||||
):
|
):
|
||||||
pass
|
return value
|
||||||
else:
|
# Either a std::string or a lambda call returning that. We need const char*
|
||||||
# Either a std::string or a lambda call returning that. We need const char*
|
return MockObj(f"({value}).c_str()")
|
||||||
value = f"({value}).c_str()"
|
|
||||||
return cg.RawExpression(value)
|
|
||||||
return await super().process(value, args)
|
return await super().process(value, args)
|
||||||
|
|
||||||
|
|
||||||
@@ -455,21 +467,24 @@ lv_text = TextValidator()
|
|||||||
lv_float = LValidator(cv.float_, cg.float_)
|
lv_float = LValidator(cv.float_, cg.float_)
|
||||||
lv_int = LValidator(cv.int_, cg.int_)
|
lv_int = LValidator(cv.int_, cg.int_)
|
||||||
lv_positive_int = LValidator(cv.positive_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):
|
def _percentage_validator(value):
|
||||||
return MockObj(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):
|
lv_percentage = LValidator(
|
||||||
return cv.use_id(lv_gradient_t)(value)
|
_percentage_validator, cg.float_, retmapper=lambda x: int(x * 255)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
lv_gradient = LValidator(
|
lv_gradient = LValidator(
|
||||||
validator=gradient_validator,
|
validator=cv.use_id(lv_gradient_t),
|
||||||
rtype=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]]:
|
def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]:
|
||||||
return self.parameters
|
return self.parameters
|
||||||
|
|
||||||
|
def get_parameter(self, index: int):
|
||||||
|
return literal(self.parameters[index][1])
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
await super().__aenter__()
|
await super().__aenter__()
|
||||||
add_line_marks(self.where)
|
add_line_marks(self.where)
|
||||||
@@ -250,10 +253,14 @@ class MockLv:
|
|||||||
A mock object that can be used to generate LVGL calls.
|
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):
|
def __init__(self, base):
|
||||||
self.base = base
|
self.base = base
|
||||||
|
|
||||||
def __getattr__(self, attr: str) -> "MockLv":
|
def __getattr__(self, attr: str) -> "MockLv":
|
||||||
|
attr = MockLv.ATTR_MAP.get(attr, attr)
|
||||||
return MockLv(f"{self.base}{attr}")
|
return MockLv(f"{self.base}{attr}")
|
||||||
|
|
||||||
def append(self, expression):
|
def append(self, expression):
|
||||||
@@ -307,6 +314,7 @@ class ReturnStatement(ExpressionStatement):
|
|||||||
|
|
||||||
class LvExpr(MockLv):
|
class LvExpr(MockLv):
|
||||||
def __getattr__(self, attr: str) -> "MockLv":
|
def __getattr__(self, attr: str) -> "MockLv":
|
||||||
|
attr = MockLv.ATTR_MAP.get(attr, attr)
|
||||||
return LvExpr(f"{self.base}{attr}")
|
return LvExpr(f"{self.base}{attr}")
|
||||||
|
|
||||||
def append(self, expression):
|
def append(self, expression):
|
||||||
|
|||||||
@@ -2,16 +2,21 @@
|
|||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "lvgl_hal.h"
|
|
||||||
#include "lvgl_esphome.h"
|
#include "lvgl_esphome.h"
|
||||||
|
|
||||||
|
#include "core/lv_global.h"
|
||||||
|
#include "core/lv_obj_class_private.h"
|
||||||
|
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
|
|
||||||
namespace esphome {
|
static void *lv_alloc_draw_buf(size_t size, bool internal);
|
||||||
namespace lvgl {
|
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 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[] = {
|
static const char *const EVENT_NAMES[] = {
|
||||||
"NONE",
|
"NONE",
|
||||||
@@ -61,7 +66,14 @@ static const char *const EVENT_NAMES[] = {
|
|||||||
"GET_SELF_SIZE",
|
"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])) {
|
if (event_code < sizeof(EVENT_NAMES) / sizeof(EVENT_NAMES[0])) {
|
||||||
return EVENT_NAMES[event_code];
|
return EVENT_NAMES[event_code];
|
||||||
}
|
}
|
||||||
@@ -71,11 +83,12 @@ std::string lv_event_code_name_for(uint8_t event_code) {
|
|||||||
return buf;
|
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
|
// cater for display driver chips with special requirements for bounds of partial
|
||||||
// draw areas. Extend the draw area to satisfy:
|
// draw areas. Extend the draw area to satisfy:
|
||||||
// * Coordinates must be a multiple of draw_rounding
|
// * Coordinates must be a multiple of draw_rounding
|
||||||
auto *comp = static_cast<LvglComponent *>(disp_drv->user_data);
|
|
||||||
auto draw_rounding = comp->draw_rounding;
|
auto draw_rounding = comp->draw_rounding;
|
||||||
// round down the start coordinates
|
// round down the start coordinates
|
||||||
area->x1 = area->x1 / draw_rounding * draw_rounding;
|
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;
|
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) {
|
void LvglComponent::render_end_cb(lv_event_t *event) {
|
||||||
ESP_LOGVV(TAG, "Draw end: %" PRIu32 " pixels in %" PRIu32 " ms", px, time);
|
auto *comp = static_cast<LvglComponent *>(lv_event_get_user_data(event));
|
||||||
auto *comp = static_cast<LvglComponent *>(disp_drv->user_data);
|
|
||||||
comp->draw_end_();
|
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");
|
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_();
|
comp->draw_start_();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,16 +118,15 @@ void LvglComponent::dump_config() {
|
|||||||
" Buffer size: %zu%%\n"
|
" Buffer size: %zu%%\n"
|
||||||
" Rotation: %d\n"
|
" Rotation: %d\n"
|
||||||
" Draw rounding: %d",
|
" Draw rounding: %d",
|
||||||
this->disp_drv_.hor_res, this->disp_drv_.ver_res, 100 / this->buffer_frac_, this->rotation,
|
this->width_, this->height_, 100 / this->buffer_frac_, this->rotation, (int) this->draw_rounding);
|
||||||
(int) this->draw_rounding);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void LvglComponent::set_paused(bool paused, bool show_snow) {
|
void LvglComponent::set_paused(bool paused, bool show_snow) {
|
||||||
this->paused_ = paused;
|
this->paused_ = paused;
|
||||||
this->show_snow_ = show_snow;
|
this->show_snow_ = show_snow;
|
||||||
if (!paused && lv_scr_act() != nullptr) {
|
if (!paused && lv_screen_active() != nullptr) {
|
||||||
lv_disp_trig_activity(this->disp_); // resets the inactivity time
|
lv_display_trigger_activity(this->disp_); // resets the inactivity time
|
||||||
lv_obj_invalidate(lv_scr_act());
|
lv_obj_invalidate(lv_screen_active());
|
||||||
}
|
}
|
||||||
if (paused && this->pause_callback_ != nullptr)
|
if (paused && this->pause_callback_ != nullptr)
|
||||||
this->pause_callback_->trigger();
|
this->pause_callback_->trigger();
|
||||||
@@ -125,6 +136,14 @@ void LvglComponent::set_paused(bool paused, bool show_snow) {
|
|||||||
|
|
||||||
void LvglComponent::esphome_lvgl_init() {
|
void LvglComponent::esphome_lvgl_init() {
|
||||||
lv_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_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());
|
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) {
|
void LvglComponent::add_page(LvPageType *page) {
|
||||||
this->pages_.push_back(page);
|
this->pages_.push_back(page);
|
||||||
page->set_parent(this);
|
page->set_parent(this);
|
||||||
lv_disp_set_default(this->disp_);
|
lv_display_set_default(this->disp_);
|
||||||
page->setup(this->pages_.size() - 1);
|
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_; }
|
size_t LvglComponent::get_current_page() const { return this->current_page_; }
|
||||||
bool LvPageType::is_showing() const { return this->parent_->get_current_page() == this->index; }
|
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 width = lv_area_get_width(area);
|
||||||
auto height = lv_area_get_height(area);
|
auto height = lv_area_get_height(area);
|
||||||
auto height_rounded = (height + this->draw_rounding - 1) / this->draw_rounding * this->draw_rounding;
|
auto height_rounded = (height + this->draw_rounding - 1) / this->draw_rounding * this->draw_rounding;
|
||||||
auto x1 = area->x1;
|
auto x1 = area->x1;
|
||||||
auto y1 = area->y1;
|
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) {
|
switch (this->rotation) {
|
||||||
case display::DISPLAY_ROTATION_90_DEGREES:
|
case display::DISPLAY_ROTATION_90_DEGREES:
|
||||||
for (lv_coord_t x = height; x-- != 0;) {
|
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;
|
y1 = x1;
|
||||||
x1 = this->disp_drv_.ver_res - area->y1 - height;
|
x1 = this->height_ - area->y1 - height;
|
||||||
height = width;
|
height = width;
|
||||||
width = height_rounded;
|
width = height_rounded;
|
||||||
break;
|
break;
|
||||||
@@ -213,8 +232,8 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
|
|||||||
dst[y * width + x] = *ptr++;
|
dst[y * width + x] = *ptr++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
x1 = this->disp_drv_.hor_res - x1 - width;
|
x1 = this->width_ - x1 - width;
|
||||||
y1 = this->disp_drv_.ver_res - y1 - height;
|
y1 = this->height_ - y1 - height;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case display::DISPLAY_ROTATION_270_DEGREES:
|
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;
|
x1 = y1;
|
||||||
y1 = this->disp_drv_.hor_res - area->x1 - width;
|
y1 = this->width_ - area->x1 - width;
|
||||||
height = width;
|
height = width;
|
||||||
width = height_rounded;
|
width = height_rounded;
|
||||||
break;
|
break;
|
||||||
@@ -234,20 +253,19 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
for (auto *display : this->displays_) {
|
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,
|
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()) {
|
if (!this->is_paused()) {
|
||||||
auto now = millis();
|
auto now = millis();
|
||||||
this->draw_buffer_(area, color_p);
|
this->draw_buffer_(area, reinterpret_cast<lv_color_data *>(color_p));
|
||||||
ESP_LOGVV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area),
|
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_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)) {
|
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
|
#ifdef USE_LVGL_TOUCHSCREEN
|
||||||
LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time, LvglComponent *parent) {
|
LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time, LvglComponent *parent) {
|
||||||
this->set_parent(parent);
|
this->set_parent(parent);
|
||||||
lv_indev_drv_init(&this->drv_);
|
this->drv_ = lv_indev_create();
|
||||||
this->drv_.disp = parent->get_disp();
|
lv_indev_set_type(this->drv_, LV_INDEV_TYPE_POINTER);
|
||||||
this->drv_.long_press_repeat_time = long_press_repeat_time;
|
lv_indev_set_disp(this->drv_, parent->get_disp());
|
||||||
this->drv_.long_press_time = long_press_time;
|
lv_indev_set_long_press_time(this->drv_, long_press_time);
|
||||||
this->drv_.type = LV_INDEV_TYPE_POINTER;
|
// long press repeat time TBD
|
||||||
this->drv_.user_data = this;
|
lv_indev_set_user_data(this->drv_, this);
|
||||||
this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) {
|
lv_indev_set_read_cb(this->drv_, [](lv_indev_t *d, lv_indev_data_t *data) {
|
||||||
auto *l = static_cast<LVTouchListener *>(d->user_data);
|
auto *l = static_cast<LVTouchListener *>(lv_indev_get_user_data(d));
|
||||||
if (l->touch_pressed_) {
|
if (l->touch_pressed_) {
|
||||||
data->point.x = l->touch_point_.x;
|
data->point.x = l->touch_point_.x;
|
||||||
data->point.y = l->touch_point_.y;
|
data->point.y = l->touch_point_.y;
|
||||||
@@ -279,7 +297,7 @@ LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_r
|
|||||||
} else {
|
} else {
|
||||||
data->state = LV_INDEV_STATE_RELEASED;
|
data->state = LV_INDEV_STATE_RELEASED;
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) {
|
void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) {
|
||||||
@@ -289,21 +307,78 @@ void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) {
|
|||||||
}
|
}
|
||||||
#endif // USE_LVGL_TOUCHSCREEN
|
#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
|
#ifdef USE_LVGL_KEY_LISTENER
|
||||||
LVEncoderListener::LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt) {
|
LVEncoderListener::LVEncoderListener(lv_indev_type_t type, uint16_t long_press_time, uint16_t long_press_repeat_time) {
|
||||||
lv_indev_drv_init(&this->drv_);
|
this->drv_ = lv_indev_create();
|
||||||
this->drv_.type = type;
|
lv_indev_set_type(this->drv_, type);
|
||||||
this->drv_.user_data = this;
|
lv_indev_set_long_press_time(this->drv_, long_press_time);
|
||||||
this->drv_.long_press_time = lpt;
|
lv_indev_set_long_press_repeat_time(this->drv_, long_press_repeat_time);
|
||||||
this->drv_.long_press_repeat_time = lprt;
|
lv_indev_set_user_data(this->drv_, this);
|
||||||
this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) {
|
lv_indev_set_read_cb(this->drv_, [](lv_indev_t *d, lv_indev_data_t *data) {
|
||||||
auto *l = static_cast<LVEncoderListener *>(d->user_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->state = l->pressed_ ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
|
||||||
data->key = l->key_;
|
data->key = l->key_;
|
||||||
data->enc_diff = (int16_t) (l->count_ - l->last_count_);
|
data->enc_diff = (int16_t) (l->count_ - l->last_count_);
|
||||||
l->last_count_ = l->count_;
|
l->last_count_ = l->count_;
|
||||||
data->continue_reading = false;
|
data->continue_reading = false;
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
#endif // USE_LVGL_KEY_LISTENER
|
#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);
|
auto index = std::find(this->options_.begin(), this->options_.end(), text);
|
||||||
if (index != this->options_.end()) {
|
if (index != this->options_.end()) {
|
||||||
this->set_selected_index(index - this->options_.begin(), anim);
|
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;
|
index = options.size() - 1;
|
||||||
this->options_ = std::move(options);
|
this->options_ = std::move(options);
|
||||||
this->set_option_string(join_string(this->options_).c_str());
|
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);
|
this->set_selected_index(index, LV_ANIM_OFF);
|
||||||
}
|
}
|
||||||
#endif // USE_LVGL_DROPDOWN || LV_USE_ROLLER
|
#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_add_event_cb(
|
||||||
lv_obj,
|
lv_obj,
|
||||||
[](lv_event_t *event) {
|
[](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)
|
if (self->key_callback_.size() == 0)
|
||||||
return;
|
return;
|
||||||
auto key_idx = lv_btnmatrix_get_selected_btn(self->obj);
|
auto key_idx = lv_buttonmatrix_get_selected_button(self->obj);
|
||||||
if (key_idx == LV_BTNMATRIX_BTN_NONE)
|
if (key_idx == LV_BUTTONMATRIX_BUTTON_NONE)
|
||||||
return;
|
return;
|
||||||
if (self->key_map_.count(key_idx) != 0) {
|
if (self->key_map_.count(key_idx) != 0) {
|
||||||
self->send_key_(self->key_map_[key_idx]);
|
self->send_key_(self->key_map_[key_idx]);
|
||||||
return;
|
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);
|
auto len = strlen(str);
|
||||||
while (len--)
|
while (len--)
|
||||||
self->send_key_(*str++);
|
self->send_key_(*str++);
|
||||||
@@ -376,14 +451,14 @@ void LvKeyboardType::set_obj(lv_obj_t *lv_obj) {
|
|||||||
lv_obj_add_event_cb(
|
lv_obj_add_event_cb(
|
||||||
lv_obj,
|
lv_obj,
|
||||||
[](lv_event_t *event) {
|
[](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)
|
if (self->key_callback_.size() == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
auto key_idx = lv_btnmatrix_get_selected_btn(self->obj);
|
auto key_idx = lv_buttonmatrix_get_selected_button(self->obj);
|
||||||
if (key_idx == LV_BTNMATRIX_BTN_NONE)
|
if (key_idx == LV_BUTTONMATRIX_BUTTON_NONE)
|
||||||
return;
|
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)
|
if (txt == nullptr)
|
||||||
return;
|
return;
|
||||||
for (const auto *kb_special_key : KB_SPECIAL_KEYS) {
|
for (const auto *kb_special_key : KB_SPECIAL_KEYS) {
|
||||||
@@ -419,33 +494,29 @@ bool LvglComponent::is_paused() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void LvglComponent::write_random_() {
|
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)
|
if (iterations <= 0)
|
||||||
iterations = 1;
|
iterations = 1;
|
||||||
while (iterations-- != 0) {
|
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;
|
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;
|
row = row / this->draw_rounding * this->draw_rounding;
|
||||||
auto size = ((random_uint32() % 32) / this->draw_rounding + 2) * this->draw_rounding - 1;
|
// size will be between 8 and 32, and a multiple of draw_rounding
|
||||||
// clamp size so the square fits within the draw buffer
|
int32_t size = (random_uint32() % 25 + 8) / this->draw_rounding * this->draw_rounding;
|
||||||
if ((size + 1) * (size + 1) > this->draw_buf_.size)
|
lv_area_t area{col, row, col + size - 1, row + size - 1};
|
||||||
size = static_cast<decltype(size)>(sqrtf(this->draw_buf_.size)) - 1;
|
// clip to display bounds just in case
|
||||||
lv_area_t area;
|
if (area.x2 >= this->width_)
|
||||||
area.x1 = col;
|
area.x2 = this->width_ - 1;
|
||||||
area.y1 = row;
|
if (area.y2 >= this->height_)
|
||||||
area.x2 = col + size;
|
area.y2 = this->height_ - 1;
|
||||||
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;
|
|
||||||
|
|
||||||
|
// 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;
|
size_t line_len = lv_area_get_width(&area) * lv_area_get_height(&area) / 2;
|
||||||
for (size_t i = 0; i != line_len; i++) {
|
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),
|
full_refresh_(full_refresh),
|
||||||
resume_on_input_(resume_on_input),
|
resume_on_input_(resume_on_input),
|
||||||
update_when_display_idle_(update_when_display_idle) {
|
update_when_display_idle_(update_when_display_idle) {
|
||||||
lv_disp_draw_buf_init(&this->draw_buf_, nullptr, nullptr, 0);
|
this->disp_ = lv_display_create(240, 240);
|
||||||
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_);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void LvglComponent::setup() {
|
void LvglComponent::setup() {
|
||||||
auto *display = this->displays_[0];
|
auto *display = this->displays_[0];
|
||||||
auto rounding = this->draw_rounding;
|
auto rounding = this->draw_rounding;
|
||||||
// cater for displays with dimensions that don't divide by the required 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 width = (display->get_width() + rounding - 1) / rounding * rounding;
|
||||||
auto height = (display->get_height() + rounding - 1) / rounding * rounding;
|
auto height = (display->get_height() + rounding - 1) / rounding * rounding;
|
||||||
auto frac = this->buffer_frac_;
|
auto frac = this->buffer_frac_;
|
||||||
if (frac == 0)
|
if (frac == 0)
|
||||||
frac = 1;
|
frac = 1;
|
||||||
size_t buffer_pixels = width * height / frac;
|
auto buf_bytes = clamp_at_least(width * height / frac * LV_COLOR_DEPTH / 8, MIN_BUFFER_SIZE);
|
||||||
auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8;
|
|
||||||
void *buffer = nullptr;
|
void *buffer = nullptr;
|
||||||
|
// for small buffers, try to allocate in internal memory first to improve performance
|
||||||
if (this->buffer_frac_ >= MIN_BUFFER_FRAC / 2)
|
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)
|
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 specific buffer size not set and can't get 100%, try for a smaller one
|
||||||
if (buffer == nullptr && this->buffer_frac_ == 0) {
|
if (buffer == nullptr && this->buffer_frac_ == 0) {
|
||||||
frac = MIN_BUFFER_FRAC;
|
frac = MIN_BUFFER_FRAC;
|
||||||
buffer_pixels /= MIN_BUFFER_FRAC;
|
|
||||||
buf_bytes /= 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;
|
this->buffer_frac_ = frac;
|
||||||
if (buffer == nullptr) {
|
if (buffer == nullptr) {
|
||||||
@@ -516,13 +581,17 @@ void LvglComponent::setup() {
|
|||||||
this->mark_failed();
|
this->mark_failed();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buffer_pixels);
|
this->draw_buf_ = static_cast<uint8_t *>(buffer);
|
||||||
this->disp_drv_.hor_res = display->get_width();
|
lv_display_set_resolution(this->disp_, this->width_, this->height_);
|
||||||
this->disp_drv_.ver_res = display->get_height();
|
lv_display_set_color_format(this->disp_, LV_COLOR_FORMAT_RGB565);
|
||||||
lv_disp_drv_update(this->disp_, &this->disp_drv_);
|
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();
|
this->rotation = display->get_rotation();
|
||||||
if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) {
|
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) {
|
if (this->rotate_buf_ == nullptr) {
|
||||||
this->status_set_error(LOG_STR("Memory allocation failure"));
|
this->status_set_error(LOG_STR("Memory allocation failure"));
|
||||||
this->mark_failed();
|
this->mark_failed();
|
||||||
@@ -530,26 +599,28 @@ void LvglComponent::setup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this->draw_start_callback_ != nullptr) {
|
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_) {
|
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
|
#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, ')');
|
auto next = strchr(buf, ')');
|
||||||
if (next != nullptr)
|
if (next != nullptr)
|
||||||
buf = next + 1;
|
buf = next + 1;
|
||||||
while (isspace(*buf))
|
while (isspace(*buf))
|
||||||
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
|
#endif
|
||||||
// Rotation will be handled by our drawing function, so reset the display rotation.
|
// Rotation will be handled by our drawing function, so reset the display rotation.
|
||||||
for (auto *disp : this->displays_)
|
for (auto *disp : this->displays_)
|
||||||
disp->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);
|
disp->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);
|
||||||
this->show_page(0, LV_SCR_LOAD_ANIM_NONE, 0);
|
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() {
|
void LvglComponent::update() {
|
||||||
@@ -557,7 +628,7 @@ void LvglComponent::update() {
|
|||||||
if (this->is_paused()) {
|
if (this->is_paused()) {
|
||||||
return;
|
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() {
|
void LvglComponent::loop() {
|
||||||
@@ -565,41 +636,129 @@ void LvglComponent::loop() {
|
|||||||
if (this->paused_ && this->show_snow_)
|
if (this->paused_ && this->show_snow_)
|
||||||
this->write_random_();
|
this->write_random_();
|
||||||
} else {
|
} 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
|
#ifdef USE_LVGL_ANIMIMG
|
||||||
void lv_animimg_stop(lv_obj_t *obj) {
|
void lv_animimg_stop(lv_obj_t *obj) {
|
||||||
auto *animg = (lv_animimg_t *) obj;
|
int32_t duration = lv_animimg_get_duration(obj);
|
||||||
int32_t duration = animg->anim.time;
|
|
||||||
lv_animimg_set_duration(obj, 0);
|
lv_animimg_set_duration(obj, 0);
|
||||||
lv_animimg_start(obj);
|
lv_animimg_start(obj);
|
||||||
lv_animimg_set_duration(obj, duration);
|
lv_animimg_set_duration(obj, duration);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
void LvglComponent::static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
|
void LvglComponent::static_flush_cb(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p) {
|
||||||
reinterpret_cast<LvglComponent *>(disp_drv->user_data)->flush_cb_(disp_drv, area, 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)
|
#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
|
auto *ptr = malloc(size); // NOLINT
|
||||||
if (ptr == nullptr) {
|
if (ptr == nullptr) {
|
||||||
ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
|
ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
|
||||||
}
|
}
|
||||||
return ptr;
|
return ptr;
|
||||||
}
|
}
|
||||||
void lv_custom_mem_free(void *ptr) { return free(ptr); } // NOLINT
|
void lv_free_core(void *ptr) { return free(ptr); } // NOLINT
|
||||||
void *lv_custom_mem_realloc(void *ptr, size_t size) { return realloc(ptr, size); } // NOLINT
|
void *lv_realloc_core(void *ptr, size_t size) { return realloc(ptr, size); } // NOLINT
|
||||||
#else
|
|
||||||
|
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
|
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;
|
void *ptr;
|
||||||
ptr = heap_caps_malloc(size, cap_bits);
|
ptr = heap_caps_malloc(size, cap_bits);
|
||||||
if (ptr == nullptr) {
|
if (ptr == nullptr) {
|
||||||
@@ -614,14 +773,14 @@ void *lv_custom_mem_alloc(size_t size) {
|
|||||||
return ptr;
|
return ptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void lv_custom_mem_free(void *ptr) {
|
void lv_free_core(void *ptr) {
|
||||||
ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr);
|
ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr);
|
||||||
if (ptr == nullptr)
|
if (ptr == nullptr)
|
||||||
return;
|
return;
|
||||||
heap_caps_free(ptr);
|
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);
|
ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size);
|
||||||
return heap_caps_realloc(ptr, size, cap_bits);
|
return heap_caps_realloc(ptr, size, cap_bits);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
#ifdef USE_BINARY_SENSOR
|
#ifdef USE_BINARY_SENSOR
|
||||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||||
#endif // USE_BINARY_SENSOR
|
#endif // USE_BINARY_SENSOR
|
||||||
#ifdef USE_LVGL_IMAGE
|
#ifdef USE_IMAGE
|
||||||
#include "esphome/components/image/image.h"
|
#include "esphome/components/image/image.h"
|
||||||
#endif // USE_LVGL_IMAGE
|
#endif // USE_LVGL_IMAGE
|
||||||
#ifdef USE_LVGL_ROTARY_ENCODER
|
#ifdef USE_LVGL_ROTARY_ENCODER
|
||||||
@@ -19,16 +19,17 @@
|
|||||||
#include "esphome/components/display/display.h"
|
#include "esphome/components/display/display.h"
|
||||||
#include "esphome/components/display/display_color_utils.h"
|
#include "esphome/components/display/display_color_utils.h"
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/log.h"
|
|
||||||
|
#include <list>
|
||||||
#include <lvgl.h>
|
#include <lvgl.h>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#ifdef USE_LVGL_FONT
|
#ifdef USE_FONT
|
||||||
#include "esphome/components/font/font.h"
|
#include "esphome/components/font/font.h"
|
||||||
#endif // USE_LVGL_FONT
|
#endif // USE_LVGL_FONT
|
||||||
#ifdef USE_LVGL_TOUCHSCREEN
|
#ifdef USE_TOUCHSCREEN
|
||||||
#include "esphome/components/touchscreen/touchscreen.h"
|
#include "esphome/components/touchscreen/touchscreen.h"
|
||||||
#endif // USE_LVGL_TOUCHSCREEN
|
#endif // USE_LVGL_TOUCHSCREEN
|
||||||
|
|
||||||
@@ -36,12 +37,24 @@
|
|||||||
#include "esphome/components/key_provider/key_provider.h"
|
#include "esphome/components/key_provider/key_provider.h"
|
||||||
#endif // USE_LVGL_BUTTONMATRIX
|
#endif // USE_LVGL_BUTTONMATRIX
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::lvgl {
|
||||||
namespace 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_api_event; // NOLINT
|
||||||
extern lv_event_code_t lv_update_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
|
#if LV_COLOR_DEPTH == 16
|
||||||
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_565;
|
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_565;
|
||||||
#elif LV_COLOR_DEPTH == 32
|
#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;
|
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_332;
|
||||||
#endif // LV_COLOR_DEPTH
|
#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) {
|
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);
|
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());
|
lv_style_set_text_font(style, font->get_lv_font());
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_LVGL_IMAGE
|
#ifdef USE_IMAGE
|
||||||
// Shortcut / overload, so that the source of an image can easily be updated
|
// Shortcut / overload, so that the source of an image can easily be updated
|
||||||
// from within a lambda.
|
// from within a lambda.
|
||||||
inline void lv_img_set_src(lv_obj_t *obj, esphome::image::Image *image) {
|
inline void lv_image_set_src(lv_obj_t *obj, esphome::image::Image *image) {
|
||||||
lv_img_set_src(obj, image->get_lv_img_dsc());
|
lv_image_set_src(obj, image->get_lv_image_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_obj_set_style_bg_img_src(lv_obj_t *obj, esphome::image::Image *image, lv_style_selector_t 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_img_src(obj, image->get_lv_img_dsc(), 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
|
#endif // USE_LVGL_IMAGE
|
||||||
#ifdef USE_LVGL_ANIMIMG
|
#ifdef USE_LVGL_ANIMIMG
|
||||||
inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images) {
|
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) {
|
if (dsc == nullptr) {
|
||||||
// object will be lazily allocated but never freed.
|
// 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);
|
lv_obj_set_user_data(img, dsc);
|
||||||
}
|
}
|
||||||
dsc->clear();
|
dsc->clear();
|
||||||
for (auto &image : images) {
|
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());
|
lv_animimg_set_src(img, (const void **) dsc->data(), dsc->size());
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // USE_LVGL_ANIMIMG
|
#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
|
// Parent class for things that wrap an LVGL object
|
||||||
class LvCompound {
|
class LvCompound {
|
||||||
public:
|
public:
|
||||||
@@ -130,7 +130,7 @@ class LvPageType : public Parented<LvglComponent> {
|
|||||||
|
|
||||||
using LvLambdaType = std::function<void(lv_obj_t *)>;
|
using LvLambdaType = std::function<void(lv_obj_t *)>;
|
||||||
using set_value_lambda_t = std::function<void(float)>;
|
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 *()>;
|
using text_lambda_t = std::function<const char *()>;
|
||||||
|
|
||||||
template<typename... Ts> class ObjUpdateAction : public Action<Ts...> {
|
template<typename... Ts> class ObjUpdateAction : public Action<Ts...> {
|
||||||
@@ -152,7 +152,7 @@ class LvglComponent : public PollingComponent {
|
|||||||
public:
|
public:
|
||||||
LvglComponent(std::vector<display::Display *> displays, float buffer_frac, bool full_refresh, int draw_rounding,
|
LvglComponent(std::vector<display::Display *> displays, float buffer_frac, bool full_refresh, int draw_rounding,
|
||||||
bool resume_on_input, bool update_when_display_idle);
|
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; }
|
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||||
void setup() override;
|
void setup() override;
|
||||||
@@ -160,11 +160,11 @@ class LvglComponent : public PollingComponent {
|
|||||||
void loop() override;
|
void loop() override;
|
||||||
template<typename F> void add_on_idle_callback(F &&callback) { this->idle_callbacks_.add(std::forward<F>(callback)); }
|
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_end_cb(lv_event_t *event);
|
||||||
static void render_start_cb(lv_disp_drv_t *disp_drv);
|
static void render_start_cb(lv_event_t *event);
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
lv_disp_t *get_disp() { return this->disp_; }
|
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.
|
// Pause or resume the display.
|
||||||
// @param paused If true, pause the display. If false, 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.
|
// @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);
|
||||||
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);
|
lv_event_code_t event3);
|
||||||
|
|
||||||
void add_page(LvPageType *page);
|
void add_page(LvPageType *page);
|
||||||
void show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time);
|
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_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 show_prev_page(lv_scr_load_anim_t anim, uint32_t time);
|
||||||
void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; }
|
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;
|
size_t get_current_page() const;
|
||||||
void set_focus_mark(lv_group_t *group) { this->focus_marks_[group] = lv_group_get_focused(group); }
|
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) {
|
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 draw_start_() const { this->draw_start_callback_->trigger(); }
|
||||||
|
|
||||||
void write_random_();
|
void write_random_();
|
||||||
void draw_buffer_(const lv_area_t *area, lv_color_t *ptr);
|
void draw_buffer_(const lv_area_t *area, lv_color_data *ptr);
|
||||||
void flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p);
|
void flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p);
|
||||||
|
|
||||||
std::vector<display::Display *> displays_{};
|
std::vector<display::Display *> displays_{};
|
||||||
size_t buffer_frac_{1};
|
size_t buffer_frac_{1};
|
||||||
bool full_refresh_{};
|
bool full_refresh_{};
|
||||||
bool resume_on_input_{};
|
bool resume_on_input_{};
|
||||||
bool update_when_display_idle_{};
|
bool update_when_display_idle_{};
|
||||||
|
|
||||||
lv_disp_draw_buf_t draw_buf_{};
|
uint8_t *draw_buf_{};
|
||||||
lv_disp_drv_t disp_drv_{};
|
lv_display_t *disp_{};
|
||||||
lv_disp_t *disp_{};
|
uint16_t width_{};
|
||||||
|
uint16_t height_{};
|
||||||
bool paused_{};
|
bool paused_{};
|
||||||
std::vector<LvPageType *> pages_{};
|
std::vector<LvPageType *> pages_{};
|
||||||
size_t current_page_{0};
|
size_t current_page_{0};
|
||||||
bool show_snow_{};
|
bool show_snow_{};
|
||||||
bool page_wrap_{true};
|
bool page_wrap_{true};
|
||||||
|
bool big_endian_{};
|
||||||
std::map<lv_group_t *, lv_obj_t *> focus_marks_{};
|
std::map<lv_group_t *, lv_obj_t *> focus_marks_{};
|
||||||
|
|
||||||
CallbackManager<void(uint32_t)> idle_callbacks_{};
|
CallbackManager<void(uint32_t)> idle_callbacks_{};
|
||||||
@@ -239,7 +244,7 @@ class LvglComponent : public PollingComponent {
|
|||||||
Trigger<> *resume_callback_{};
|
Trigger<> *resume_callback_{};
|
||||||
Trigger<> *draw_start_callback_{};
|
Trigger<> *draw_start_callback_{};
|
||||||
Trigger<> *draw_end_callback_{};
|
Trigger<> *draw_end_callback_{};
|
||||||
lv_color_t *rotate_buf_{};
|
void *rotate_buf_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
class IdleTrigger : public Trigger<> {
|
class IdleTrigger : public Trigger<> {
|
||||||
@@ -278,19 +283,37 @@ class LVTouchListener : public touchscreen::TouchListener, public Parented<LvglC
|
|||||||
touch_pressed_ = false;
|
touch_pressed_ = false;
|
||||||
this->parent_->maybe_wakeup();
|
this->parent_->maybe_wakeup();
|
||||||
}
|
}
|
||||||
lv_indev_drv_t *get_drv() { return &this->drv_; }
|
lv_indev_t *get_drv() { return this->drv_; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
lv_indev_drv_t drv_{};
|
lv_indev_t *drv_{};
|
||||||
touchscreen::TouchPoint touch_point_{};
|
touchscreen::TouchPoint touch_point_{};
|
||||||
bool touch_pressed_{};
|
bool touch_pressed_{};
|
||||||
};
|
};
|
||||||
#endif // USE_LVGL_TOUCHSCREEN
|
#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
|
#ifdef USE_LVGL_KEY_LISTENER
|
||||||
class LVEncoderListener : public Parented<LvglComponent> {
|
class LVEncoderListener : public Parented<LvglComponent> {
|
||||||
public:
|
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
|
#ifdef USE_BINARY_SENSOR
|
||||||
void add_button(binary_sensor::BinarySensor *button, lv_key_t key) {
|
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:
|
protected:
|
||||||
lv_indev_drv_t drv_{};
|
lv_indev_t *drv_{};
|
||||||
bool pressed_{};
|
bool pressed_{};
|
||||||
int32_t count_{};
|
int32_t count_{};
|
||||||
int32_t last_count_{};
|
int32_t last_count_{};
|
||||||
@@ -336,14 +359,13 @@ class LVEncoderListener : public Parented<LvglComponent> {
|
|||||||
#ifdef USE_LVGL_LINE
|
#ifdef USE_LVGL_LINE
|
||||||
class LvLineType : public LvCompound {
|
class LvLineType : public LvCompound {
|
||||||
public:
|
public:
|
||||||
std::vector<lv_point_t> get_points() { return this->points_; }
|
void set_points(FixedVector<lv_point_precise_t> points) {
|
||||||
void set_points(std::vector<lv_point_t> points) {
|
|
||||||
this->points_ = std::move(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:
|
protected:
|
||||||
std::vector<lv_point_t> points_{};
|
FixedVector<lv_point_precise_t> points_{};
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
#if defined(USE_LVGL_DROPDOWN) || defined(LV_USE_ROLLER)
|
#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 {
|
class LvButtonMatrixType : public key_provider::KeyProvider, public LvCompound {
|
||||||
public:
|
public:
|
||||||
void set_obj(lv_obj_t *lv_obj) override;
|
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; }
|
void set_key(size_t idx, uint8_t key) { this->key_map_[idx] = key; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@@ -406,5 +428,4 @@ class LvKeyboardType : public key_provider::KeyProvider, public LvCompound {
|
|||||||
void set_obj(lv_obj_t *lv_obj) override;
|
void set_obj(lv_obj_t *lv_obj) override;
|
||||||
};
|
};
|
||||||
#endif // USE_LVGL_KEYBOARD
|
#endif // USE_LVGL_KEYBOARD
|
||||||
} // namespace lvgl
|
} // namespace esphome::lvgl
|
||||||
} // namespace esphome
|
|
||||||
|
|||||||
@@ -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(),
|
await value.get_lambda(),
|
||||||
event_code,
|
event_code,
|
||||||
config[CONF_RESTORE_VALUE],
|
config[CONF_RESTORE_VALUE],
|
||||||
max_value=widget.get_max(),
|
max_value=widget.type.get_max(widget.config),
|
||||||
min_value=widget.get_min(),
|
min_value=widget.type.get_min(widget.config),
|
||||||
step=widget.get_step(),
|
step=widget.type.get_step(widget.config),
|
||||||
)
|
)
|
||||||
async with LambdaContext(EVENT_ARG) as event:
|
async with LambdaContext(EVENT_ARG) as event:
|
||||||
event.add(var.on_value())
|
event.add(var.on_value())
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from .defines import (
|
|||||||
CONF_SCROLLBAR_MODE,
|
CONF_SCROLLBAR_MODE,
|
||||||
CONF_TIME_FORMAT,
|
CONF_TIME_FORMAT,
|
||||||
LV_GRAD_DIR,
|
LV_GRAD_DIR,
|
||||||
|
get_remapped_uses,
|
||||||
)
|
)
|
||||||
from .helpers import CONF_IF_NAN, requires_component, validate_printf
|
from .helpers import CONF_IF_NAN, requires_component, validate_printf
|
||||||
from .layout import (
|
from .layout import (
|
||||||
@@ -42,16 +43,17 @@ from .lvcode import LvglComponent, lv_event_t_ptr
|
|||||||
from .types import (
|
from .types import (
|
||||||
LVEncoderListener,
|
LVEncoderListener,
|
||||||
LvType,
|
LvType,
|
||||||
WidgetType,
|
|
||||||
lv_group_t,
|
lv_group_t,
|
||||||
lv_obj_t,
|
lv_obj_t,
|
||||||
lv_pseudo_button_t,
|
lv_pseudo_button_t,
|
||||||
lv_style_t,
|
lv_style_t,
|
||||||
)
|
)
|
||||||
|
from .widgets import WidgetType
|
||||||
|
|
||||||
# this will be populated later, in __init__.py to avoid circular imports.
|
# this will be populated later, in __init__.py to avoid circular imports.
|
||||||
WIDGET_TYPES: dict = {}
|
WIDGET_TYPES: dict = {}
|
||||||
|
|
||||||
|
|
||||||
TIME_TEXT_SCHEMA = cv.Schema(
|
TIME_TEXT_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_TIME_FORMAT): cv.string,
|
cv.Required(CONF_TIME_FORMAT): cv.string,
|
||||||
@@ -176,23 +178,28 @@ STYLE_PROPS = {
|
|||||||
"height": lvalid.size,
|
"height": lvalid.size,
|
||||||
"image_recolor": lvalid.lv_color,
|
"image_recolor": lvalid.lv_color,
|
||||||
"image_recolor_opa": lvalid.opacity,
|
"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_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": lvalid.opacity,
|
||||||
"opa_layered": lvalid.opacity,
|
"opa_layered": lvalid.opacity,
|
||||||
"outline_color": lvalid.lv_color,
|
"outline_color": lvalid.lv_color,
|
||||||
"outline_opa": lvalid.opacity,
|
"outline_opa": lvalid.opacity,
|
||||||
"outline_pad": lvalid.padding,
|
"outline_pad": lvalid.padding,
|
||||||
"outline_width": lvalid.pixels,
|
"outline_width": lvalid.pixels,
|
||||||
|
"length": lvalid.pixels_or_percent,
|
||||||
"pad_all": lvalid.padding,
|
"pad_all": lvalid.padding,
|
||||||
"pad_bottom": lvalid.padding,
|
"pad_bottom": lvalid.padding,
|
||||||
"pad_left": lvalid.padding,
|
"pad_left": lvalid.padding,
|
||||||
"pad_right": lvalid.padding,
|
"pad_right": lvalid.padding,
|
||||||
"pad_top": lvalid.padding,
|
"pad_top": lvalid.padding,
|
||||||
|
"radial_offset": lvalid.size,
|
||||||
"shadow_color": lvalid.lv_color,
|
"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_x": lvalid.lv_int,
|
||||||
"shadow_ofs_y": lvalid.lv_int,
|
"shadow_ofs_y": lvalid.lv_int,
|
||||||
"shadow_opa": lvalid.opacity,
|
"shadow_opa": lvalid.opacity,
|
||||||
@@ -213,7 +220,13 @@ STYLE_PROPS = {
|
|||||||
"transform_height": lvalid.pixels_or_percent,
|
"transform_height": lvalid.pixels_or_percent,
|
||||||
"transform_pivot_x": lvalid.pixels_or_percent,
|
"transform_pivot_x": lvalid.pixels_or_percent,
|
||||||
"transform_pivot_y": 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_x": lvalid.pixels_or_percent,
|
||||||
"translate_y": lvalid.pixels_or_percent,
|
"translate_y": lvalid.pixels_or_percent,
|
||||||
"max_height": lvalid.pixels_or_percent,
|
"max_height": lvalid.pixels_or_percent,
|
||||||
@@ -227,15 +240,23 @@ STYLE_PROPS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
STYLE_REMAP = {
|
STYLE_REMAP = {
|
||||||
"bg_image_opa": "bg_img_opa",
|
"transform_angle": "transform_rotation",
|
||||||
"bg_image_recolor": "bg_img_recolor",
|
"transform_zoom": "transform_scale",
|
||||||
"bg_image_recolor_opa": "bg_img_recolor_opa",
|
"zoom": "scale",
|
||||||
"bg_image_src": "bg_img_src",
|
"angle": "rotation",
|
||||||
"bg_image_tiled": "bg_img_tiled",
|
"shadow_ofs_x": "shadow_offset_x",
|
||||||
"image_recolor": "img_recolor",
|
"shadow_ofs_y": "shadow_offset_y",
|
||||||
"image_recolor_opa": "img_recolor_opa",
|
"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
|
# Complete object style schema
|
||||||
STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend(
|
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
|
# Setting object flags
|
||||||
FLAG_SCHEMA = cv.Schema({cv.Optional(flag): lvalid.lv_bool for flag in df.OBJ_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):
|
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):
|
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
|
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)
|
container_validator, requires_component(required)
|
||||||
)
|
)
|
||||||
# Apply custom validation
|
# Apply custom validation
|
||||||
value = widget_type.validate(value or {})
|
|
||||||
path = [key] if is_dict else [index, key]
|
path = [key] if is_dict else [index, key]
|
||||||
with prepend_path(path):
|
with prepend_path(path):
|
||||||
|
value = widget_type.validate(value or {})
|
||||||
result.append({key: container_validator(value)})
|
result.append({key: container_validator(value)})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/preferences.h"
|
#include "esphome/core/preferences.h"
|
||||||
#include "../lvgl.h"
|
#include "esphome/components/lvgl/lvgl_esphome.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace lvgl {
|
namespace lvgl {
|
||||||
@@ -28,12 +28,12 @@ class LVGLSelect : public select::Select, public Component {
|
|||||||
lv_obj_add_event_cb(
|
lv_obj_add_event_cb(
|
||||||
this->widget_->obj,
|
this->widget_->obj,
|
||||||
[](lv_event_t *e) {
|
[](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_();
|
it->set_options_();
|
||||||
},
|
},
|
||||||
LV_EVENT_REFRESH, this);
|
LV_EVENT_REFRESH, this);
|
||||||
auto lamb = [](lv_event_t *e) {
|
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();
|
self->publish();
|
||||||
};
|
};
|
||||||
lv_obj_add_event_cb(this->widget_->obj, lamb, LV_EVENT_VALUE_CHANGED, this);
|
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 .helpers import add_lv_use
|
||||||
from .lvcode import LambdaContext, LocalVariable, lv
|
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 .types import ObjUpdateAction, lv_obj_t, lv_style_t
|
||||||
from .widgets import (
|
from .widgets import (
|
||||||
Widget,
|
Widget,
|
||||||
@@ -26,6 +26,10 @@ from .widgets import (
|
|||||||
from .widgets.obj import obj_spec
|
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):
|
async def style_set(svar, style):
|
||||||
for prop, validator in ALL_STYLES.items():
|
for prop, validator in ALL_STYLES.items():
|
||||||
if (value := style.get(prop)) is not None:
|
if (value := style.get(prop)) is not None:
|
||||||
@@ -33,22 +37,44 @@ async def style_set(svar, style):
|
|||||||
value = await validator.process(value)
|
value = await validator.process(value)
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
value = "|".join(value)
|
value = "|".join(value)
|
||||||
remapped_prop = STYLE_REMAP.get(prop, prop)
|
lv.call(f"style_set_{remap_property(prop)}", svar, literal(value))
|
||||||
lv.call(f"style_set_{remapped_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)
|
style_id = ID(id_name, True, lv_style_t)
|
||||||
svar = cg.new_Pvariable(style_id)
|
svar = cg.new_Pvariable(style_id)
|
||||||
lv.style_init(svar)
|
lv.style_init(svar)
|
||||||
await style_set(svar, style)
|
if style:
|
||||||
|
await style_set(svar, style)
|
||||||
return svar
|
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):
|
async def styles_to_code(config):
|
||||||
"""Convert styles to C__ code."""
|
"""Convert styles to C__ code."""
|
||||||
for style in config.get(CONF_STYLE_DEFINITIONS, ()):
|
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(
|
@automation.register_action(
|
||||||
@@ -81,8 +107,7 @@ async def theme_to_code(config):
|
|||||||
for part, states in collect_parts(style).items():
|
for part, states in collect_parts(style).items():
|
||||||
styles[part] = {
|
styles[part] = {
|
||||||
state: await create_style(
|
state: await create_style(
|
||||||
props,
|
"_lv_theme_style_" + w_name + "_" + part + "_" + state, props
|
||||||
"_lv_theme_style_" + w_name + "_" + part + "_" + state,
|
|
||||||
)
|
)
|
||||||
for state, props in states.items()
|
for state, props in states.items()
|
||||||
}
|
}
|
||||||
@@ -90,7 +115,7 @@ async def theme_to_code(config):
|
|||||||
|
|
||||||
|
|
||||||
async def add_top_layer(lv_component, 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):
|
if top_conf := config.get(CONF_TOP_LAYER):
|
||||||
with LocalVariable("top_layer", lv_obj_t, top_layer) as top_layer_obj:
|
with LocalVariable("top_layer", lv_obj_t, top_layer) as top_layer_obj:
|
||||||
top_w = Widget(top_layer_obj, obj_spec, top_conf)
|
top_w = Widget(top_layer_obj, obj_spec, top_conf)
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include "esphome/components/text/text.h"
|
#include "esphome/components/text/text.h"
|
||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/preferences.h"
|
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace lvgl {
|
namespace lvgl {
|
||||||
|
|
||||||
class LVGLText : public text::Text {
|
class LVGLText : public text::Text {
|
||||||
public:
|
public:
|
||||||
void set_control_lambda(std::function<void(const std::string)> control_lambda) {
|
void set_control_lambda(const std::function<void(std::string)> &control_lambda) {
|
||||||
this->control_lambda_ = std::move(control_lambda);
|
this->control_lambda_ = control_lambda;
|
||||||
if (this->initial_state_.has_value()) {
|
if (this->initial_state_.has_value()) {
|
||||||
this->control_lambda_(this->initial_state_.value());
|
this->control_lambda_(this->initial_state_.value());
|
||||||
this->initial_state_.reset();
|
this->initial_state_.reset();
|
||||||
@@ -28,7 +25,7 @@ class LVGLText : public text::Text {
|
|||||||
this->initial_state_ = value;
|
this->initial_state_ = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::function<void(const std::string)> control_lambda_{};
|
std::function<void(std::string)> control_lambda_{};
|
||||||
optional<std::string> initial_state_{};
|
optional<std::string> initial_state_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from .defines import (
|
|||||||
CONF_TOUCHSCREENS,
|
CONF_TOUCHSCREENS,
|
||||||
)
|
)
|
||||||
from .helpers import lvgl_components_required
|
from .helpers import lvgl_components_required
|
||||||
from .lvcode import lv
|
|
||||||
from .schemas import PRESS_TIME
|
from .schemas import PRESS_TIME
|
||||||
from .types import LVTouchListener
|
from .types import LVTouchListener
|
||||||
|
|
||||||
@@ -40,5 +39,4 @@ async def touchscreens_to_code(lv_component, config):
|
|||||||
lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds
|
lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds
|
||||||
lprt = tconf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds
|
lprt = tconf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds
|
||||||
listener = cg.new_Pvariable(tconf[CONF_ID], lpt, lprt, lv_component)
|
listener = cg.new_Pvariable(tconf[CONF_ID], lpt, lprt, lv_component)
|
||||||
lv.indev_drv_register(listener.get_drv())
|
|
||||||
cg.add(touchscreen.register_listener(listener))
|
cg.add(touchscreen.register_listener(listener))
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ from .lvcode import (
|
|||||||
lvgl_static,
|
lvgl_static,
|
||||||
)
|
)
|
||||||
from .types import LV_EVENT
|
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):
|
async def add_on_boot_triggers(triggers):
|
||||||
@@ -48,7 +48,7 @@ async def generate_triggers():
|
|||||||
|
|
||||||
for w in widget_map.values():
|
for w in widget_map.values():
|
||||||
if isinstance(w.type, LvScrActType):
|
if isinstance(w.type, LvScrActType):
|
||||||
w = get_scr_act(w.var)
|
w = get_screen_active(w.var)
|
||||||
|
|
||||||
if w.config:
|
if w.config:
|
||||||
for event, conf in {
|
for event, conf in {
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
import sys
|
|
||||||
|
|
||||||
from esphome import automation, codegen as cg
|
from esphome import automation, codegen as cg
|
||||||
from esphome.automation import register_action
|
from esphome.const import CONF_TEXT, CONF_VALUE
|
||||||
from esphome.config_validation import Schema
|
from esphome.cpp_generator import MockObj
|
||||||
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.cpp_types import esphome_ns
|
from esphome.cpp_types import esphome_ns
|
||||||
|
|
||||||
from .defines import lvgl_ns
|
from .defines import lvgl_ns
|
||||||
from .lvcode import lv_expr
|
|
||||||
|
|
||||||
|
|
||||||
class LvType(cg.MockObjClass):
|
class LvType(cg.MockObjClass):
|
||||||
@@ -26,6 +20,10 @@ class LvType(cg.MockObjClass):
|
|||||||
return None
|
return None
|
||||||
return [arg[0] for arg in self.args]
|
return [arg[0] for arg in self.args]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.base.removeprefix("lv_").removesuffix("_t")
|
||||||
|
|
||||||
|
|
||||||
class LvNumber(LvType):
|
class LvNumber(LvType):
|
||||||
def __init__(self, *args):
|
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_obj_t_ptr = lv_obj_base_t.operator("ptr")
|
||||||
lv_disp_t = cg.global_ns.struct("lv_disp_t")
|
lv_disp_t = cg.global_ns.struct("lv_disp_t")
|
||||||
lv_color_t = cg.global_ns.struct("lv_color_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")
|
lv_group_t = cg.global_ns.struct("lv_group_t")
|
||||||
LVTouchListener = lvgl_ns.class_("LVTouchListener")
|
LVTouchListener = lvgl_ns.class_("LVTouchListener")
|
||||||
LVEncoderListener = lvgl_ns.class_("LVEncoderListener")
|
LVEncoderListener = lvgl_ns.class_("LVEncoderListener")
|
||||||
@@ -70,10 +69,11 @@ lv_obj_t = LvType("lv_obj_t")
|
|||||||
lv_page_t = LvType("LvPageType", parents=(LvCompound,))
|
lv_page_t = LvType("LvPageType", parents=(LvCompound,))
|
||||||
lv_img_t = LvType("lv_img_t")
|
lv_img_t = LvType("lv_img_t")
|
||||||
lv_gradient_t = LvType("lv_grad_dsc_t")
|
lv_gradient_t = LvType("lv_grad_dsc_t")
|
||||||
|
lv_event_t = LvType("lv_event_t")
|
||||||
|
|
||||||
LV_EVENT = MockObj(base="LV_EVENT_", op="")
|
LV_EVENT = MockObj(base="LV_EVENT_", op="")
|
||||||
LV_STATE = MockObj(base="LV_STATE_", 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):
|
class LvText(LvType):
|
||||||
@@ -93,7 +93,7 @@ class LvBoolean(LvType):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
*args,
|
*args,
|
||||||
largs=[(cg.bool_, "x")],
|
largs=[(cg.bool_, "x")],
|
||||||
lvalue=lambda w: w.is_checked(),
|
lvalue=lambda w: w.has_state(LV_STATE.CHECKED),
|
||||||
has_on_value=True,
|
has_on_value=True,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
@@ -110,137 +110,3 @@ class LvSelect(LvType):
|
|||||||
parents=parens,
|
parents=parens,
|
||||||
**kwargs,
|
**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 typing import Any
|
||||||
|
|
||||||
from esphome import codegen as cg, config_validation as cv
|
from esphome import codegen as cg, config_validation as cv
|
||||||
from esphome.config_validation import Invalid
|
from esphome.automation import register_action
|
||||||
from esphome.const import CONF_DEFAULT, CONF_GROUP, CONF_ID, CONF_STATE, CONF_TYPE
|
from esphome.config_validation import Invalid, Schema
|
||||||
from esphome.core import ID, TimePeriod
|
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.coroutine import FakeAwaitable
|
||||||
from esphome.cpp_generator import MockObj
|
from esphome.cpp_generator import MockObj
|
||||||
|
|
||||||
@@ -21,6 +30,7 @@ from ..defines import (
|
|||||||
CONF_MAIN,
|
CONF_MAIN,
|
||||||
CONF_PAD_COLUMN,
|
CONF_PAD_COLUMN,
|
||||||
CONF_PAD_ROW,
|
CONF_PAD_ROW,
|
||||||
|
CONF_SCALE,
|
||||||
CONF_STYLES,
|
CONF_STYLES,
|
||||||
CONF_WIDGETS,
|
CONF_WIDGETS,
|
||||||
OBJ_FLAGS,
|
OBJ_FLAGS,
|
||||||
@@ -44,8 +54,15 @@ from ..lvcode import (
|
|||||||
lv_obj,
|
lv_obj,
|
||||||
lv_Pvariable,
|
lv_Pvariable,
|
||||||
)
|
)
|
||||||
from ..schemas import ALL_STYLES, OBJ_PROPERTIES, STYLE_REMAP, WIDGET_TYPES
|
from ..types import (
|
||||||
from ..types import LV_STATE, LvType, WidgetType, lv_coord_t, lv_obj_t, lv_obj_t_ptr
|
LV_STATE,
|
||||||
|
LvCompound,
|
||||||
|
LvType,
|
||||||
|
ObjUpdateAction,
|
||||||
|
lv_coord_t,
|
||||||
|
lv_obj_t,
|
||||||
|
lv_obj_t_ptr,
|
||||||
|
)
|
||||||
|
|
||||||
EVENT_LAMB = "event_lamb__"
|
EVENT_LAMB = "event_lamb__"
|
||||||
|
|
||||||
@@ -53,6 +70,171 @@ theme_widget_map = {}
|
|||||||
styles_used = set()
|
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:
|
class Widget:
|
||||||
"""
|
"""
|
||||||
Represents a Widget.
|
Represents a Widget.
|
||||||
@@ -74,19 +256,25 @@ class Widget:
|
|||||||
self.obj = var
|
self.obj = var
|
||||||
self.outer = None
|
self.outer = None
|
||||||
self.move_to_foreground = False
|
self.move_to_foreground = False
|
||||||
|
# Properties for linear equations
|
||||||
|
self.slope = None
|
||||||
|
self.y_int = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create(name, var, wtype: WidgetType, config: dict = None):
|
def create(name, var, wtype: WidgetType, config: dict = None):
|
||||||
w = Widget(var, wtype, config)
|
w = Widget(var, wtype, config)
|
||||||
if name is not None:
|
widget_map[name] = w
|
||||||
widget_map[name] = w
|
|
||||||
return w
|
return w
|
||||||
|
|
||||||
def add_state(self, state):
|
def add_state(self, state):
|
||||||
|
if "|" in state:
|
||||||
|
state = f"(lv_state_t)({state})"
|
||||||
return lv_obj.add_state(self.obj, literal(state))
|
return lv_obj.add_state(self.obj, literal(state))
|
||||||
|
|
||||||
def clear_state(self, 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):
|
def has_state(self, state):
|
||||||
return (lv_expr.obj_get_state(self.obj) & literal(state)) != 0
|
return (lv_expr.obj_get_state(self.obj) & literal(state)) != 0
|
||||||
@@ -98,12 +286,23 @@ class Widget:
|
|||||||
return self.has_state(LV_STATE.CHECKED)
|
return self.has_state(LV_STATE.CHECKED)
|
||||||
|
|
||||||
def add_flag(self, flag):
|
def add_flag(self, flag):
|
||||||
|
if "|" in flag:
|
||||||
|
flag = f"(lv_obj_flag_t)({flag})"
|
||||||
return lv_obj.add_flag(self.obj, literal(flag))
|
return lv_obj.add_flag(self.obj, literal(flag))
|
||||||
|
|
||||||
def clear_flag(self, 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.
|
Set a property of the widget.
|
||||||
:param prop: The property name
|
:param prop: The property name
|
||||||
@@ -111,18 +310,28 @@ class Widget:
|
|||||||
:param animated: If the change should be animated
|
:param animated: If the change should be animated
|
||||||
:param lv_name: The base type of the widget e.g. "obj"
|
:param lv_name: The base type of the widget e.g. "obj"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from ..schemas import ALL_STYLES, remap_property
|
||||||
|
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
value = value.get(prop)
|
value = value.get(prop)
|
||||||
if isinstance(ALL_STYLES.get(prop), LValidator):
|
if value is None:
|
||||||
value = await ALL_STYLES[prop].process(value)
|
return
|
||||||
else:
|
if not processor and isinstance(ALL_STYLES.get(prop), LValidator):
|
||||||
value = literal(value)
|
processor = ALL_STYLES[prop]
|
||||||
if value is None:
|
if isinstance(processor, LValidator):
|
||||||
|
processor = processor.process
|
||||||
|
if processor:
|
||||||
|
value = await processor(value)
|
||||||
|
elif value is None:
|
||||||
return
|
return
|
||||||
|
prop = remap_property(prop)
|
||||||
if isinstance(value, TimePeriod):
|
if isinstance(value, TimePeriod):
|
||||||
value = value.total_milliseconds
|
value = value.total_milliseconds
|
||||||
if isinstance(value, str):
|
elif isinstance(value, str):
|
||||||
value = literal(value)
|
value = literal(value)
|
||||||
|
elif isinstance(value, ID):
|
||||||
|
value = MockObj(value)
|
||||||
lv_name = lv_name or self.type.lv_name
|
lv_name = lv_name or self.type.lv_name
|
||||||
if animated is None or self.type.animated is not True:
|
if animated is None or self.type.animated is not True:
|
||||||
lv.call(f"{lv_name}_set_{prop}", self.obj, value)
|
lv.call(f"{lv_name}_set_{prop}", self.obj, value)
|
||||||
@@ -138,10 +347,12 @@ class Widget:
|
|||||||
ltype = ltype or self.__type_base()
|
ltype = ltype or self.__type_base()
|
||||||
return cg.RawExpression(f"lv_{ltype}_get_{prop}({self.obj})")
|
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:
|
if value is None:
|
||||||
return
|
return
|
||||||
styles_used.add(prop)
|
styles_used.add(prop)
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = literal(value)
|
||||||
lv.call(f"obj_set_style_{prop}", self.obj, value, state)
|
lv.call(f"obj_set_style_{prop}", self.obj, value, state)
|
||||||
|
|
||||||
def __type_base(self):
|
def __type_base(self):
|
||||||
@@ -189,15 +400,6 @@ class Widget:
|
|||||||
"""
|
"""
|
||||||
return
|
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):
|
def get_scale(self):
|
||||||
return self.type.get_scale(self.config)
|
return self.type.get_scale(self.config)
|
||||||
|
|
||||||
@@ -212,14 +414,14 @@ class LvScrActType(WidgetType):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
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):
|
async def to_code(self, w, config: dict):
|
||||||
return []
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_scr_act(lv_comp: MockObj) -> Widget:
|
def get_screen_active(lv_comp: MockObj) -> Widget:
|
||||||
return Widget.create(None, lv_comp.get_scr_act(), LvScrActType(), {})
|
return Widget(lv_comp.get_screen_active(), LvScrActType(), {})
|
||||||
|
|
||||||
|
|
||||||
def get_widget_generator(wid):
|
def get_widget_generator(wid):
|
||||||
@@ -271,10 +473,17 @@ def collect_props(config):
|
|||||||
:param config:
|
:param config:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from ..schemas import ALL_STYLES
|
||||||
|
|
||||||
props = {}
|
props = {}
|
||||||
for prop in [*ALL_STYLES, *OBJ_FLAGS, CONF_STYLES, CONF_GROUP]:
|
for prop in [*ALL_STYLES, *OBJ_FLAGS, CONF_STYLES, CONF_GROUP]:
|
||||||
if prop in config:
|
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
|
return props
|
||||||
|
|
||||||
|
|
||||||
@@ -304,34 +513,41 @@ def collect_parts(config):
|
|||||||
return parts
|
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):
|
async def set_obj_properties(w: Widget, config):
|
||||||
"""Generate a list of C++ statements to apply properties to an lv_obj_t"""
|
"""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):
|
if layout := config.get(CONF_LAYOUT):
|
||||||
layout_type: str = layout[CONF_TYPE]
|
layout_type: str = layout[CONF_TYPE]
|
||||||
add_lv_use(layout_type)
|
add_lv_use(layout_type)
|
||||||
lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}"))
|
lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}"))
|
||||||
if (pad_row := layout.get(CONF_PAD_ROW)) is not None:
|
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:
|
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:
|
if layout_type == TYPE_GRID:
|
||||||
wid = config[CONF_ID]
|
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}"
|
rows = "{" + ",".join(rows) + ", LV_GRID_TEMPLATE_LAST}"
|
||||||
row_id = ID(f"{wid}_row_dsc", is_declaration=True, type=lv_coord_t)
|
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))
|
row_array = cg.static_const_array(row_id, cg.RawExpression(rows))
|
||||||
w.set_style("grid_row_dsc_array", row_array, 0)
|
w.set_style("grid_row_dsc_array", row_array)
|
||||||
columns = [str(x) for x in layout[CONF_GRID_COLUMNS]]
|
columns = [_size_to_str(x) for x in layout[CONF_GRID_COLUMNS]]
|
||||||
columns = "{" + ",".join(columns) + ", LV_GRID_TEMPLATE_LAST}"
|
columns = "{" + ",".join(columns) + ", LV_GRID_TEMPLATE_LAST}"
|
||||||
column_id = ID(f"{wid}_column_dsc", is_declaration=True, type=lv_coord_t)
|
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))
|
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(
|
w.set_style(
|
||||||
CONF_GRID_COLUMN_ALIGN, literal(layout.get(CONF_GRID_COLUMN_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)), 0
|
|
||||||
)
|
)
|
||||||
|
w.set_style(CONF_GRID_ROW_ALIGN, literal(layout.get(CONF_GRID_ROW_ALIGN)))
|
||||||
if layout_type == TYPE_FLEX:
|
if layout_type == TYPE_FLEX:
|
||||||
lv_obj.set_flex_flow(w.obj, literal(layout[CONF_FLEX_FLOW]))
|
lv_obj.set_flex_flow(w.obj, literal(layout[CONF_FLEX_FLOW]))
|
||||||
main = literal(layout[CONF_FLEX_ALIGN_MAIN])
|
main = literal(layout[CONF_FLEX_ALIGN_MAIN])
|
||||||
@@ -353,13 +569,13 @@ async def set_obj_properties(w: Widget, config):
|
|||||||
else:
|
else:
|
||||||
lv_state = join_enums((state, part))
|
lv_state = join_enums((state, part))
|
||||||
for style_id in props.get(CONF_STYLES, ()):
|
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 {
|
for prop, value in {
|
||||||
k: v for k, v in props.items() if k in ALL_STYLES
|
k: v for k, v in props.items() if k in ALL_STYLES
|
||||||
}.items():
|
}.items():
|
||||||
if isinstance(ALL_STYLES[prop], LValidator):
|
if isinstance(ALL_STYLES[prop], LValidator):
|
||||||
value = await ALL_STYLES[prop].process(value)
|
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)
|
w.set_style(prop_r, value, lv_state)
|
||||||
if group := config.get(CONF_GROUP):
|
if group := config.get(CONF_GROUP):
|
||||||
group = await cg.get_variable(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)
|
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.
|
Converts a Widget definition to C code.
|
||||||
:param w_cnfig: The widget configuration
|
: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
|
:param parent: The parent to which the widget should be added
|
||||||
:return:
|
: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)
|
from ..schemas import WIDGET_TYPES
|
||||||
if theme := theme_widget_map.get(w_type):
|
|
||||||
for part, states in theme.items():
|
spec: WidgetType = (
|
||||||
part = "LV_PART_" + part.upper()
|
w_type if isinstance(w_type, WidgetType) else WIDGET_TYPES[w_type]
|
||||||
for state, style in states.items():
|
)
|
||||||
state = "LV_STATE_" + state.upper()
|
return await spec.create_to_code(w_cnfig, parent)
|
||||||
if state == "LV_STATE_DEFAULT":
|
|
||||||
lv_state = literal(part)
|
|
||||||
elif part == "LV_PART_MAIN":
|
class NumberType(WidgetType):
|
||||||
lv_state = literal(state)
|
def get_max(self, config: dict):
|
||||||
else:
|
return int(config.get(CONF_MAX_VALUE, 100))
|
||||||
lv_state = join_enums((state, part))
|
|
||||||
lv.obj_add_style(w.obj, style, lv_state)
|
def get_min(self, config: dict):
|
||||||
await set_obj_properties(w, w_cnfig)
|
return int(config.get(CONF_MIN_VALUE, 0))
|
||||||
await add_widgets(w, w_cnfig)
|
|
||||||
await spec.to_code(w, w_cnfig)
|
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ from ..defines import (
|
|||||||
CONF_KNOB,
|
CONF_KNOB,
|
||||||
CONF_MAIN,
|
CONF_MAIN,
|
||||||
CONF_START_ANGLE,
|
CONF_START_ANGLE,
|
||||||
literal,
|
LV_OBJ_FLAG,
|
||||||
|
LV_PART,
|
||||||
)
|
)
|
||||||
from ..lv_validation import (
|
from ..lv_validation import (
|
||||||
get_start_value,
|
get_start_value,
|
||||||
@@ -28,8 +29,8 @@ from ..lv_validation import (
|
|||||||
lv_positive_int,
|
lv_positive_int,
|
||||||
)
|
)
|
||||||
from ..lvcode import lv, lv_expr, lv_obj
|
from ..lvcode import lv, lv_expr, lv_obj
|
||||||
from ..types import LvNumber, NumberType
|
from ..types import LvNumber
|
||||||
from . import Widget
|
from . import NumberType, Widget
|
||||||
|
|
||||||
CONF_ARC = "arc"
|
CONF_ARC = "arc"
|
||||||
ARC_SCHEMA = cv.Schema(
|
ARC_SCHEMA = cv.Schema(
|
||||||
@@ -71,39 +72,17 @@ class ArcType(NumberType):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def to_code(self, w: Widget, config):
|
async def to_code(self, w: Widget, config):
|
||||||
if CONF_MIN_VALUE in config and CONF_MAX_VALUE in config:
|
for prop, validator in ARC_MODIFY_SCHEMA.schema.items():
|
||||||
max_value = await lv_int.process(config[CONF_MAX_VALUE])
|
if prop != CONF_VALUE:
|
||||||
min_value = await lv_int.process(config[CONF_MIN_VALUE])
|
# start_angle and end_angle are mapped to bg_start_angle and bg_end_angle
|
||||||
lv.arc_set_range(w.obj, min_value, max_value)
|
prop = str(prop)
|
||||||
elif CONF_MIN_VALUE in config:
|
if prop.endswith("_angle"):
|
||||||
max_value = w.get_property(CONF_MAX_VALUE)
|
prop = "bg_" + prop
|
||||||
min_value = await lv_int.process(config[CONF_MIN_VALUE])
|
await w.set_property(prop, config, processor=validator)
|
||||||
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)),
|
|
||||||
)
|
|
||||||
|
|
||||||
if CONF_ADJUSTABLE in config:
|
if CONF_ADJUSTABLE in config:
|
||||||
if not config[CONF_ADJUSTABLE]:
|
if not config[CONF_ADJUSTABLE]:
|
||||||
lv_obj.remove_style(w.obj, nullptr, literal("LV_PART_KNOB"))
|
lv_obj.remove_style(w.obj, nullptr, LV_PART.KNOB)
|
||||||
w.clear_flag("LV_OBJ_FLAG_CLICKABLE")
|
w.clear_flag(LV_OBJ_FLAG.CLICKABLE)
|
||||||
elif CONF_GROUP not in config:
|
elif CONF_GROUP not in config:
|
||||||
# For some reason arc does not get automatically added to the default group
|
# For some reason arc does not get automatically added to the default group
|
||||||
lv.group_add_obj(lv_expr.group_get_default(), w.obj)
|
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 ..lv_validation import lv_text
|
||||||
from ..lvcode import lv, lv_expr
|
from ..lvcode import lv, lv_expr
|
||||||
from ..schemas import TEXT_SCHEMA
|
from ..schemas import TEXT_SCHEMA
|
||||||
from ..types import LvBoolean, WidgetType
|
from ..types import LvBoolean
|
||||||
from . import Widget
|
from . import Widget, WidgetType
|
||||||
from .label import label_spec
|
from .label import label_spec
|
||||||
|
|
||||||
lv_button_t = LvBoolean("lv_btn_t")
|
lv_button_t = LvBoolean("lv_button_t")
|
||||||
|
|
||||||
|
|
||||||
class ButtonType(WidgetType):
|
class ButtonType(WidgetType):
|
||||||
@@ -30,7 +30,7 @@ class ButtonType(WidgetType):
|
|||||||
def get_uses(self):
|
def get_uses(self):
|
||||||
return ("btn",)
|
return ("btn",)
|
||||||
|
|
||||||
def on_create(self, var: MockObj, config: dict):
|
async def on_create(self, var: MockObj, config: dict):
|
||||||
if CONF_TEXT in config:
|
if CONF_TEXT in config:
|
||||||
lv.label_create(var)
|
lv.label_create(var)
|
||||||
return var
|
return var
|
||||||
|
|||||||
@@ -118,15 +118,15 @@ class MatrixButton(Widget):
|
|||||||
|
|
||||||
def has_state(self, state):
|
def has_state(self, state):
|
||||||
state = self.map_ctrls(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):
|
def add_state(self, state):
|
||||||
state = self.map_ctrls(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):
|
def clear_state(self, state):
|
||||||
state = self.map_ctrls(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):
|
def is_pressed(self):
|
||||||
return self.is_selected() & self.parent.has_state(LV_STATE.PRESSED)
|
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 "")
|
text_list.append(button_conf.get(CONF_TEXT) or "")
|
||||||
key_list.append(button_conf.get(CONF_KEY_CODE) or 0)
|
key_list.append(button_conf.get(CONF_KEY_CODE) or 0)
|
||||||
width_list.append(button_conf[CONF_WIDTH])
|
width_list.append(button_conf[CONF_WIDTH])
|
||||||
ctrl = ["LV_BTNMATRIX_CTRL_CLICK_TRIG"]
|
ctrl = ["CLICK_TRIG"]
|
||||||
for item in button_conf.get(CONF_CONTROL, ()):
|
for item in button_conf.get(CONF_CONTROL, ()):
|
||||||
ctrl.extend([k for k, v in item.items() if v])
|
ctrl.extend([k for k, v in item.items() if v])
|
||||||
ctrl_list.append(await BUTTONMATRIX_CTRLS.process(ctrl))
|
ctrl_list.append(await BUTTONMATRIX_CTRLS.process(ctrl))
|
||||||
@@ -187,7 +187,7 @@ class ButtonMatrixType(WidgetType):
|
|||||||
(CONF_MAIN, CONF_ITEMS),
|
(CONF_MAIN, CONF_ITEMS),
|
||||||
BUTTONMATRIX_SCHEMA,
|
BUTTONMATRIX_SCHEMA,
|
||||||
{},
|
{},
|
||||||
lv_name="btnmatrix",
|
lv_name="buttonmatrix",
|
||||||
)
|
)
|
||||||
|
|
||||||
async def to_code(self, w: Widget, config):
|
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 = config[CONF_BUTTON_TEXT_LIST_ID]
|
||||||
text_id = cg.static_const_array(text_id, text_list)
|
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)
|
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):
|
for index, key in enumerate(key_list):
|
||||||
if key != 0:
|
if key != 0:
|
||||||
lv_add(w.var.set_key(index, key))
|
lv_add(w.var.set_key(index, key))
|
||||||
|
|
||||||
def get_uses(self):
|
def get_uses(self):
|
||||||
return ("btnmatrix",)
|
return ("buttonmatrix",)
|
||||||
|
|
||||||
|
|
||||||
def set_btn_data(obj, ctrl_list, width_list):
|
def set_btn_data(obj, ctrl_list, width_list):
|
||||||
for index, ctrl in enumerate(ctrl_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):
|
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()
|
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):
|
async def do_button_update(w):
|
||||||
if (width := config.get(CONF_WIDTH)) is not None:
|
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):
|
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):
|
if controls := config.get(CONF_CONTROL):
|
||||||
adds = []
|
adds = []
|
||||||
clrs = []
|
clrs = []
|
||||||
for item in controls:
|
for item in controls:
|
||||||
adds.extend(
|
adds.extend([f"{k.upper()}" for k, v in item.items() if v])
|
||||||
[f"LV_BTNMATRIX_CTRL_{k.upper()}" for k, v in item.items() if v]
|
clrs.extend([f"{k.upper()}" for k, v in item.items() if not v])
|
||||||
)
|
|
||||||
clrs.extend(
|
|
||||||
[f"LV_BTNMATRIX_CTRL_{k.upper()}" for k, v in item.items() if not v]
|
|
||||||
)
|
|
||||||
if adds:
|
if adds:
|
||||||
lv.btnmatrix_set_btn_ctrl(
|
lv.buttonmatrix_set_button_ctrl(
|
||||||
w.obj, w.index, await BUTTONMATRIX_CTRLS.process(adds)
|
w.obj, w.index, await BUTTONMATRIX_CTRLS.process(adds)
|
||||||
)
|
)
|
||||||
if clrs:
|
if clrs:
|
||||||
lv.btnmatrix_clear_btn_ctrl(
|
lv.buttonmatrix_clear_button_ctrl(
|
||||||
w.obj, w.index, await BUTTONMATRIX_CTRLS.process(clrs)
|
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 import automation, codegen as cg, config_validation as cv
|
||||||
from esphome.components.display_menu_base import CONF_LABEL
|
from esphome.components.display_menu_base import CONF_LABEL
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
@@ -9,7 +28,7 @@ from esphome.const import (
|
|||||||
CONF_X,
|
CONF_X,
|
||||||
CONF_Y,
|
CONF_Y,
|
||||||
)
|
)
|
||||||
from esphome.cpp_generator import Literal, MockObj
|
from esphome.cpp_types import FixedVector
|
||||||
|
|
||||||
from ..automation import action_to_code
|
from ..automation import action_to_code
|
||||||
from ..defines import (
|
from ..defines import (
|
||||||
@@ -19,8 +38,11 @@ from ..defines import (
|
|||||||
CONF_PIVOT_X,
|
CONF_PIVOT_X,
|
||||||
CONF_PIVOT_Y,
|
CONF_PIVOT_Y,
|
||||||
CONF_POINTS,
|
CONF_POINTS,
|
||||||
|
CONF_RADIUS,
|
||||||
CONF_SRC,
|
CONF_SRC,
|
||||||
CONF_START_ANGLE,
|
CONF_START_ANGLE,
|
||||||
|
addr,
|
||||||
|
get_color_formats,
|
||||||
literal,
|
literal,
|
||||||
)
|
)
|
||||||
from ..lv_validation import (
|
from ..lv_validation import (
|
||||||
@@ -34,18 +56,20 @@ from ..lv_validation import (
|
|||||||
size,
|
size,
|
||||||
)
|
)
|
||||||
from ..lvcode import LocalVariable, lv, lv_assign, lv_expr
|
from ..lvcode import LocalVariable, lv, lv_assign, lv_expr
|
||||||
from ..schemas import STYLE_PROPS, STYLE_REMAP, TEXT_SCHEMA, point_schema
|
from ..schemas import STYLE_PROPS, TEXT_SCHEMA, point_schema, remap_property
|
||||||
from ..types import LvType, ObjUpdateAction, WidgetType
|
from ..types import LvType, ObjUpdateAction
|
||||||
from . import Widget, get_widgets
|
from . import Widget, WidgetType, get_widgets
|
||||||
from .line import lv_point_t, process_coord
|
from .img import CONF_IMAGE
|
||||||
|
from .line import lv_point_precise_t, process_coord
|
||||||
|
|
||||||
CONF_CANVAS = "canvas"
|
CONF_CANVAS = "canvas"
|
||||||
CONF_BUFFER_ID = "buffer_id"
|
CONF_BUFFER_ID = "buffer_id"
|
||||||
CONF_MAX_WIDTH = "max_width"
|
CONF_MAX_WIDTH = "max_width"
|
||||||
CONF_TRANSPARENT = "transparent"
|
CONF_TRANSPARENT = "transparent"
|
||||||
CONF_RADIUS = "radius"
|
CONF_DRAW_BUF_ID = "draw_buf_id"
|
||||||
|
|
||||||
lv_canvas_t = LvType("lv_canvas_t")
|
lv_canvas_t = LvType("lv_canvas_t")
|
||||||
|
lv_draw_buf_t = LvType("lv_draw_buf_t")
|
||||||
|
|
||||||
|
|
||||||
class CanvasType(WidgetType):
|
class CanvasType(WidgetType):
|
||||||
@@ -59,32 +83,44 @@ class CanvasType(WidgetType):
|
|||||||
cv.Required(CONF_WIDTH): size,
|
cv.Required(CONF_WIDTH): size,
|
||||||
cv.Required(CONF_HEIGHT): size,
|
cv.Required(CONF_HEIGHT): size,
|
||||||
cv.Optional(CONF_TRANSPARENT, default=False): cv.boolean,
|
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):
|
def get_uses(self):
|
||||||
return "img", CONF_LABEL
|
return CONF_IMAGE, CONF_LABEL
|
||||||
|
|
||||||
async def to_code(self, w: Widget, config):
|
async def to_code(self, w: Widget, config):
|
||||||
width = config[CONF_WIDTH]
|
width = config[CONF_WIDTH]
|
||||||
height = config[CONF_HEIGHT]
|
height = config[CONF_HEIGHT]
|
||||||
use_alpha = "_ALPHA" if config[CONF_TRANSPARENT] else ""
|
# LVGL 9.4: Use LV_COLOR_FORMAT instead of LV_IMG_CF
|
||||||
buf_size = literal(
|
# RGB565 is 16-bit (2 bytes per pixel), ARGB8888 is 32-bit (4 bytes per pixel)
|
||||||
f"LV_CANVAS_BUF_SIZE_TRUE_COLOR{use_alpha}({width}, {height})"
|
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:
|
lv.draw_buf_set_flag(draw_buf, literal("LV_IMAGE_FLAGS_MODIFIABLE"))
|
||||||
cg.add(cg.RawExpression(f"memset({buf}, 0, {buf_size});"))
|
lv.canvas_set_draw_buf(w.obj, draw_buf)
|
||||||
lv.canvas_set_buffer(
|
|
||||||
w.obj,
|
|
||||||
buf,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
literal(f"LV_IMG_CF_TRUE_COLOR{use_alpha}"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
canvas_spec = CanvasType()
|
CanvasType()
|
||||||
|
|
||||||
|
|
||||||
@automation.register_action(
|
@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.GenerateID(CONF_ID): cv.use_id(lv_canvas_t),
|
||||||
cv.Required(CONF_COLOR): lv_color,
|
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),
|
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):
|
async def canvas_set_pixel(config, action_id, template_arg, args):
|
||||||
widget = await get_widgets(config)
|
widget = await get_widgets(config)
|
||||||
color = await lv_color.process(config[CONF_COLOR])
|
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 = [
|
points = [
|
||||||
(
|
(
|
||||||
await pixels.process(p[CONF_X]),
|
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):
|
async def do_set_pixels(w: Widget):
|
||||||
if isinstance(color, MockObj):
|
# LVGL 9.4: lv_canvas_set_px combines color and opacity
|
||||||
for point in points:
|
# Could optimize this for lambda values
|
||||||
x, y = point
|
for point in points:
|
||||||
lv.canvas_set_px_color(w.obj, x, y, color)
|
x, y = point
|
||||||
else:
|
lv.canvas_set_px(w.obj, x, y, color, opa)
|
||||||
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)
|
|
||||||
|
|
||||||
return await action_to_code(
|
return await action_to_code(
|
||||||
widget, do_set_pixels, action_id, template_arg, args, config
|
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))
|
y = await pixels.process(config.get(CONF_Y))
|
||||||
|
|
||||||
async def action_func(w: Widget):
|
async def action_func(w: Widget):
|
||||||
with LocalVariable("dsc", f"lv_draw_{dsc_type}_dsc_t", modifier="") as dsc:
|
# LVGL 9.4: Create a layer for drawing on canvas
|
||||||
dsc_addr = literal(f"&{dsc}")
|
with LocalVariable("layer", "lv_layer_t", modifier="") as layer:
|
||||||
lv.call(f"draw_{dsc_type}_dsc_init", dsc_addr)
|
lv.canvas_init_layer(w.obj, addr(layer))
|
||||||
if CONF_OPA in config:
|
with LocalVariable("dsc", f"lv_draw_{dsc_type}_dsc_t", modifier="") as dsc:
|
||||||
opa = await opacity.process(config[CONF_OPA])
|
lv.call(f"draw_{dsc_type}_dsc_init", addr(dsc))
|
||||||
lv_assign(dsc.opa, opa)
|
if CONF_OPA in config:
|
||||||
for prop, validator in props.items():
|
opa = await opacity.process(config[CONF_OPA])
|
||||||
if prop in config:
|
lv_assign(dsc.opa, opa)
|
||||||
value = await validator.process(config[prop])
|
for prop, validator in props.items():
|
||||||
mapped_prop = STYLE_REMAP.get(prop, prop)
|
if prop in config:
|
||||||
lv_assign(getattr(dsc, mapped_prop), value)
|
value = await validator.process(config[prop])
|
||||||
await do_draw(w, x, y, dsc_addr)
|
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(
|
return await action_to_code(
|
||||||
widget, action_func, action_id, template_arg, args, config
|
widget, action_func, action_id, template_arg, args, config
|
||||||
@@ -212,6 +237,8 @@ RECT_PROPS = {
|
|||||||
"outline_opa",
|
"outline_opa",
|
||||||
"shadow_color",
|
"shadow_color",
|
||||||
"shadow_width",
|
"shadow_width",
|
||||||
|
"shadow_offset_x",
|
||||||
|
"shadow_offset_y",
|
||||||
"shadow_ofs_x",
|
"shadow_ofs_x",
|
||||||
"shadow_ofs_y",
|
"shadow_ofs_y",
|
||||||
"shadow_spread",
|
"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(
|
@automation.register_action(
|
||||||
"lvgl.canvas.draw_rectangle",
|
"lvgl.canvas.draw_rectangle",
|
||||||
ObjUpdateAction,
|
ObjUpdateAction,
|
||||||
@@ -237,8 +282,14 @@ async def canvas_draw_rect(config, action_id, template_arg, args):
|
|||||||
width = await pixels.process(config[CONF_WIDTH])
|
width = await pixels.process(config[CONF_WIDTH])
|
||||||
height = await pixels.process(config[CONF_HEIGHT])
|
height = await pixels.process(config[CONF_HEIGHT])
|
||||||
|
|
||||||
async def do_draw_rect(w: Widget, x, y, dsc_addr):
|
async def do_draw_rect(layer, x, y, dsc):
|
||||||
lv.canvas_draw_rect(w.obj, x, y, width, height, dsc_addr)
|
# 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(
|
return await draw_to_code(
|
||||||
config, "rect", RECT_PROPS, do_draw_rect, action_id, template_arg, args
|
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])
|
text = await lv_text.process(config[CONF_TEXT])
|
||||||
max_width = await pixels.process(config[CONF_MAX_WIDTH])
|
max_width = await pixels.process(config[CONF_MAX_WIDTH])
|
||||||
|
|
||||||
async def do_draw_text(w: Widget, x, y, dsc_addr):
|
async def do_draw_text(layer, x, y, dsc):
|
||||||
lv.canvas_draw_text(w.obj, x, y, max_width, dsc_addr, text)
|
# 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(
|
return await draw_to_code(
|
||||||
config, "label", TEXT_PROPS, do_draw_text, action_id, template_arg, args
|
config, "label", TEXT_PROPS, do_draw_text, action_id, template_arg, args
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
IMG_PROPS = {
|
IMG_PROPS = (
|
||||||
"angle": STYLE_PROPS["transform_angle"],
|
"angle",
|
||||||
"zoom": STYLE_PROPS["transform_zoom"],
|
"rotation",
|
||||||
"recolor": STYLE_PROPS["image_recolor"],
|
"scale_x",
|
||||||
"recolor_opa": STYLE_PROPS["image_recolor_opa"],
|
"scale_y",
|
||||||
"opa": STYLE_PROPS["opa"],
|
"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(
|
@automation.register_action(
|
||||||
@@ -303,9 +388,9 @@ IMG_PROPS = {
|
|||||||
cv.Required(CONF_SRC): lv_image,
|
cv.Required(CONF_SRC): lv_image,
|
||||||
cv.Optional(CONF_PIVOT_X, default=0): pixels,
|
cv.Optional(CONF_PIVOT_X, default=0): pixels,
|
||||||
cv.Optional(CONF_PIVOT_Y, 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,
|
synchronous=True,
|
||||||
)
|
)
|
||||||
async def canvas_draw_image(config, action_id, template_arg, args):
|
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_x = await pixels.process(config[CONF_PIVOT_X])
|
||||||
pivot_y = await pixels.process(config[CONF_PIVOT_Y])
|
pivot_y = await pixels.process(config[CONF_PIVOT_Y])
|
||||||
|
|
||||||
async def do_draw_image(w: Widget, x, y, dsc_addr):
|
async def do_draw_image(layer, x, y, dsc):
|
||||||
dsc = MockObj(f"(*{dsc_addr})")
|
# LVGL 9.4: Use lv_draw_image with area
|
||||||
|
lv_assign(dsc.src, src.get_lv_image_dsc())
|
||||||
if pivot_x or pivot_y:
|
if pivot_x or pivot_y:
|
||||||
# pylint :disable=no-member
|
# pylint :disable=no-member
|
||||||
lv_assign(dsc.pivot, literal(f"{{{pivot_x}, {pivot_y}}}"))
|
lv_assign(dsc.pivot.x, pivot_x)
|
||||||
lv.canvas_draw_img(w.obj, x, y, src, dsc_addr)
|
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(
|
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]
|
for p in config[CONF_POINTS]
|
||||||
]
|
]
|
||||||
|
|
||||||
async def do_draw_line(w: Widget, x, y, dsc_addr):
|
async def do_draw_line(layer, _x, _y, dsc):
|
||||||
with LocalVariable(
|
_draw_line(layer, dsc, points)
|
||||||
"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)
|
|
||||||
|
|
||||||
return await draw_to_code(
|
return await draw_to_code(
|
||||||
config, "line", LINE_PROPS, do_draw_line, action_id, template_arg, args
|
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])]
|
[await process_coord(p[CONF_X]), await process_coord(p[CONF_Y])]
|
||||||
for p in config[CONF_POINTS]
|
for p in config[CONF_POINTS]
|
||||||
]
|
]
|
||||||
|
# Close the polygon
|
||||||
|
points.append(points[0])
|
||||||
|
|
||||||
async def do_draw_polygon(w: Widget, x, y, dsc_addr):
|
async def do_draw_polygon(layer, x, y, dsc):
|
||||||
with LocalVariable(
|
# LVGL 9.4: Draw polygon using line drawing in a closed path
|
||||||
"points", cg.std_vector.template(lv_point_t), points, modifier=""
|
# Note: This draws outline only. For filled polygons, would need different approach
|
||||||
) as points_var:
|
# Convert rect descriptor to line descriptor for polygon outline
|
||||||
lv.canvas_draw_polygon(
|
with LocalVariable("line_dsc", "lv_draw_line_dsc_t", modifier="") as line_dsc:
|
||||||
w.obj, points_var.data(), points_var.size(), dsc_addr
|
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(
|
return await draw_to_code(
|
||||||
config, "rect", RECT_PROPS, do_draw_polygon, action_id, template_arg, args
|
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])
|
start_angle = await lv_angle_degrees.process(config[CONF_START_ANGLE])
|
||||||
end_angle = await lv_angle_degrees.process(config[CONF_END_ANGLE])
|
end_angle = await lv_angle_degrees.process(config[CONF_END_ANGLE])
|
||||||
|
|
||||||
async def do_draw_arc(w: Widget, x, y, dsc_addr):
|
async def do_draw_arc(layer, x, y, dsc):
|
||||||
lv.canvas_draw_arc(w.obj, x, y, radius, start_angle, end_angle, dsc_addr)
|
# 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(
|
return await draw_to_code(
|
||||||
config, "arc", ARC_PROPS, do_draw_arc, action_id, template_arg, args
|
config, "arc", ARC_PROPS, do_draw_arc, action_id, template_arg, args
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_HEIGHT, CONF_WIDTH
|
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 ..lv_validation import size
|
||||||
from ..lvcode import lv
|
from ..types import lv_obj_t
|
||||||
from ..types import WidgetType, lv_obj_t
|
from . import WidgetType
|
||||||
|
|
||||||
CONTAINER_SCHEMA = cv.Schema(
|
CONTAINER_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
@@ -28,12 +27,9 @@ class ContainerType(WidgetType):
|
|||||||
(CONF_MAIN, CONF_SCROLLBAR),
|
(CONF_MAIN, CONF_SCROLLBAR),
|
||||||
schema=CONTAINER_SCHEMA,
|
schema=CONTAINER_SCHEMA,
|
||||||
modify_schema={},
|
modify_schema={},
|
||||||
lv_name=CONF_OBJ,
|
lv_name=CONF_CONTAINER,
|
||||||
)
|
)
|
||||||
self.styles = {}
|
self.styles = {}
|
||||||
|
|
||||||
def on_create(self, var: MockObj, config: dict):
|
|
||||||
lv.obj_remove_style_all(var)
|
|
||||||
|
|
||||||
|
|
||||||
container_spec = ContainerType()
|
container_spec = ContainerType()
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
import esphome.config_validation as cv
|
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 (
|
from ..defines import (
|
||||||
CONF_ANTIALIAS,
|
CONF_ANTIALIAS,
|
||||||
CONF_MAIN,
|
CONF_MAIN,
|
||||||
CONF_PIVOT_X,
|
CONF_PIVOT_X,
|
||||||
CONF_PIVOT_Y,
|
CONF_PIVOT_Y,
|
||||||
|
CONF_SCALE,
|
||||||
CONF_SRC,
|
CONF_SRC,
|
||||||
CONF_ZOOM,
|
CONF_ZOOM,
|
||||||
LvConstant,
|
|
||||||
)
|
)
|
||||||
from ..lv_validation import lv_angle, lv_bool, lv_image, size, zoom
|
from ..lv_validation import lv_angle, lv_bool, lv_image, scale, size
|
||||||
from ..lvcode import lv
|
|
||||||
from ..types import lv_img_t
|
from ..types import lv_img_t
|
||||||
from . import Widget, WidgetType
|
from . import Widget, WidgetType
|
||||||
from .label import CONF_LABEL
|
from .label import CONF_LABEL
|
||||||
@@ -22,14 +27,14 @@ BASE_IMG_SCHEMA = cv.Schema(
|
|||||||
{
|
{
|
||||||
cv.Optional(CONF_PIVOT_X): size,
|
cv.Optional(CONF_PIVOT_X): size,
|
||||||
cv.Optional(CONF_PIVOT_Y): size,
|
cv.Optional(CONF_PIVOT_Y): size,
|
||||||
cv.Optional(CONF_ANGLE): lv_angle,
|
cv.Exclusive(CONF_ANGLE, CONF_ROTATION): lv_angle,
|
||||||
cv.Optional(CONF_ZOOM): zoom,
|
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_X): size,
|
||||||
cv.Optional(CONF_OFFSET_Y): size,
|
cv.Optional(CONF_OFFSET_Y): size,
|
||||||
cv.Optional(CONF_ANTIALIAS): lv_bool,
|
cv.Optional(CONF_ANTIALIAS): lv_bool,
|
||||||
cv.Optional(CONF_MODE): LvConstant(
|
cv.Optional(CONF_MODE): cv.invalid(f"{CONF_MODE} is not supported in LVGL 9.x"),
|
||||||
"LV_IMG_SIZE_MODE_", "VIRTUAL", "REAL"
|
|
||||||
).one_of,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,33 +59,15 @@ class ImgType(WidgetType):
|
|||||||
(CONF_MAIN,),
|
(CONF_MAIN,),
|
||||||
IMG_SCHEMA,
|
IMG_SCHEMA,
|
||||||
IMG_MODIFY_SCHEMA,
|
IMG_MODIFY_SCHEMA,
|
||||||
lv_name="img",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_uses(self):
|
def get_uses(self):
|
||||||
return "img", CONF_LABEL
|
return CONF_IMAGE, CONF_LABEL
|
||||||
|
|
||||||
async def to_code(self, w: Widget, config):
|
async def to_code(self, w: Widget, config):
|
||||||
if src := config.get(CONF_SRC):
|
await w.set_property(CONF_SRC, await lv_image.process(config.get(CONF_SRC)))
|
||||||
lv.img_set_src(w.obj, await lv_image.process(src))
|
for prop, validator in BASE_IMG_SCHEMA.schema.items():
|
||||||
if (pivot_x := config.get(CONF_PIVOT_X)) and (
|
await w.set_property(prop, config, processor=validator)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
img_spec = ImgType()
|
img_spec = ImgType()
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ from ..defines import (
|
|||||||
)
|
)
|
||||||
from ..lv_validation import lv_bool, lv_text
|
from ..lv_validation import lv_bool, lv_text
|
||||||
from ..schemas import TEXT_SCHEMA
|
from ..schemas import TEXT_SCHEMA
|
||||||
from ..types import LvText, WidgetType
|
from ..types import LvText
|
||||||
from . import Widget
|
from . import Widget, WidgetType
|
||||||
|
|
||||||
CONF_LABEL = "label"
|
CONF_LABEL = "label"
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import esphome.config_validation as cv
|
|||||||
from esphome.const import CONF_BRIGHTNESS, CONF_COLOR, CONF_LED
|
from esphome.const import CONF_BRIGHTNESS, CONF_COLOR, CONF_LED
|
||||||
|
|
||||||
from ..defines import CONF_MAIN
|
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 ..lvcode import lv
|
||||||
from ..types import LvType
|
from ..types import LvType
|
||||||
from . import Widget, WidgetType
|
from . import Widget, WidgetType
|
||||||
@@ -10,7 +10,7 @@ from . import Widget, WidgetType
|
|||||||
LED_SCHEMA = cv.Schema(
|
LED_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
cv.Optional(CONF_COLOR): lv_color,
|
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:
|
if (color := config.get(CONF_COLOR)) is not None:
|
||||||
lv.led_set_color(w.obj, await lv_color.process(color))
|
lv.led_set_color(w.obj, await lv_color.process(color))
|
||||||
if (brightness := config.get(CONF_BRIGHTNESS)) is not None:
|
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()
|
led_spec = LedType()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ CONF_POINTS = "points"
|
|||||||
CONF_POINT_LIST_ID = "point_list_id"
|
CONF_POINT_LIST_ID = "point_list_id"
|
||||||
|
|
||||||
lv_point_t = cg.global_ns.struct("lv_point_t")
|
lv_point_t = cg.global_ns.struct("lv_point_t")
|
||||||
|
lv_point_precise_t = cg.global_ns.struct("lv_point_precise_t")
|
||||||
|
|
||||||
|
|
||||||
LINE_SCHEMA = {
|
LINE_SCHEMA = {
|
||||||
@@ -23,10 +24,7 @@ LINE_SCHEMA = {
|
|||||||
|
|
||||||
async def process_coord(coord):
|
async def process_coord(coord):
|
||||||
if isinstance(coord, Lambda):
|
if isinstance(coord, Lambda):
|
||||||
coord = call_lambda(await cg.process_lambda(coord, [], return_type=lv_coord_t))
|
return 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 cg.safe_exp(coord)
|
return cg.safe_exp(coord)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ from ..defines import (
|
|||||||
)
|
)
|
||||||
from ..lv_validation import animated, lv_int
|
from ..lv_validation import animated, lv_int
|
||||||
from ..lvcode import lv
|
from ..lvcode import lv
|
||||||
from ..types import LvNumber, NumberType
|
from ..types import LvNumber
|
||||||
from . import Widget
|
from . import NumberType, Widget
|
||||||
|
|
||||||
# Note this file cannot be called "bar.py" because that name is disallowed.
|
# Note this file cannot be called "bar.py" because that name is disallowed.
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
from esphome import automation
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
|
from esphome.components.image import get_image_metadata
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_COLOR,
|
CONF_COLOR,
|
||||||
CONF_COUNT,
|
CONF_COUNT,
|
||||||
|
CONF_HEIGHT,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_ITEMS,
|
CONF_ITEMS,
|
||||||
CONF_LENGTH,
|
CONF_LENGTH,
|
||||||
@@ -13,22 +15,39 @@ from esphome.const import (
|
|||||||
CONF_ROTATION,
|
CONF_ROTATION,
|
||||||
CONF_VALUE,
|
CONF_VALUE,
|
||||||
CONF_WIDTH,
|
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 ..automation import action_to_code
|
||||||
from ..defines import (
|
from ..defines import (
|
||||||
|
CHILD_ALIGNMENTS,
|
||||||
|
CONF_ALIGN,
|
||||||
|
CONF_CONTAINER,
|
||||||
CONF_END_VALUE,
|
CONF_END_VALUE,
|
||||||
CONF_INDICATOR,
|
CONF_INDICATOR,
|
||||||
|
CONF_LINE_WIDTH,
|
||||||
CONF_MAIN,
|
CONF_MAIN,
|
||||||
CONF_OPA,
|
CONF_OPA,
|
||||||
CONF_PIVOT_X,
|
CONF_PIVOT_X,
|
||||||
CONF_PIVOT_Y,
|
CONF_PIVOT_Y,
|
||||||
|
CONF_RADIUS,
|
||||||
|
CONF_SCALE,
|
||||||
CONF_SRC,
|
CONF_SRC,
|
||||||
CONF_START_VALUE,
|
CONF_START_VALUE,
|
||||||
CONF_TICKS,
|
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 (
|
from ..lv_validation import (
|
||||||
|
LV_OPA,
|
||||||
|
LV_RADIUS,
|
||||||
get_end_value,
|
get_end_value,
|
||||||
get_start_value,
|
get_start_value,
|
||||||
lv_angle_degrees,
|
lv_angle_degrees,
|
||||||
@@ -36,105 +55,168 @@ from ..lv_validation import (
|
|||||||
lv_color,
|
lv_color,
|
||||||
lv_float,
|
lv_float,
|
||||||
lv_image,
|
lv_image,
|
||||||
|
lv_int,
|
||||||
opacity,
|
opacity,
|
||||||
|
padding,
|
||||||
|
pixels,
|
||||||
|
pixels_or_percent,
|
||||||
|
pixels_or_percent_validator,
|
||||||
requires_component,
|
requires_component,
|
||||||
size,
|
size,
|
||||||
)
|
)
|
||||||
from ..lvcode import LocalVariable, lv, lv_assign, lv_expr, lv_obj
|
from ..lvcode import LambdaContext, LocalVariable, lv, lv_add, lv_expr, lv_obj
|
||||||
from ..types import LvType, ObjUpdateAction
|
from ..schemas import STATE_SCHEMA
|
||||||
from . import Widget, WidgetType, get_widgets
|
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 .arc import CONF_ARC
|
||||||
from .img import CONF_IMAGE
|
from .img import CONF_IMAGE
|
||||||
from .line import CONF_LINE
|
from .line import CONF_LINE
|
||||||
from .obj import obj_spec
|
|
||||||
|
|
||||||
CONF_ANGLE_RANGE = "angle_range"
|
CONF_ANGLE_RANGE = "angle_range"
|
||||||
CONF_COLOR_END = "color_end"
|
CONF_COLOR_END = "color_end"
|
||||||
CONF_COLOR_START = "color_start"
|
CONF_COLOR_START = "color_start"
|
||||||
|
CONF_DRAW_TICKS_ON_TOP = "draw_ticks_on_top"
|
||||||
|
CONF_IMAGE_ID = "image_id"
|
||||||
CONF_INDICATORS = "indicators"
|
CONF_INDICATORS = "indicators"
|
||||||
|
CONF_LINE_ID = "line_id"
|
||||||
CONF_LABEL_GAP = "label_gap"
|
CONF_LABEL_GAP = "label_gap"
|
||||||
CONF_MAJOR = "major"
|
CONF_MAJOR = "major"
|
||||||
CONF_METER = "meter"
|
CONF_METER = "meter"
|
||||||
|
CONF_PIVOT = "pivot"
|
||||||
CONF_R_MOD = "r_mod"
|
CONF_R_MOD = "r_mod"
|
||||||
|
CONF_RADIAL_OFFSET = "radial_offset"
|
||||||
CONF_SCALES = "scales"
|
CONF_SCALES = "scales"
|
||||||
CONF_STRIDE = "stride"
|
CONF_STRIDE = "stride"
|
||||||
CONF_TICK_STYLE = "tick_style"
|
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_t = LvType("lv_meter_t")
|
||||||
lv_meter_indicator_t = cg.global_ns.struct("lv_meter_indicator_t")
|
lv_scale_section_t = LvType("lv_scale_section_t")
|
||||||
lv_meter_indicator_t_ptr = lv_meter_indicator_t.operator("ptr")
|
lv_meter_indicator_t = LvType("lv_meter_indicator_t")
|
||||||
|
lv_meter_indicator_ticks_t = LvType(
|
||||||
|
"lv_scale_section_t", parents=(lv_meter_indicator_t,)
|
||||||
def pixels(value):
|
)
|
||||||
"""A size in one axis in pixels"""
|
lv_meter_indicator_arc_t = LvType("lv_scale_section_t", parents=(lv_meter_indicator_t,))
|
||||||
if isinstance(value, str) and value.lower().endswith("px"):
|
lv_meter_indicator_line_t = LvType(
|
||||||
return cv.int_(value[:-2])
|
"IndicatorLine",
|
||||||
return cv.int_(value)
|
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(
|
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_COLOR, default=0): lv_color,
|
||||||
cv.Optional(CONF_R_MOD, default=0): size,
|
cv.Optional(CONF_R_MOD): padding,
|
||||||
cv.Optional(CONF_VALUE): lv_float,
|
cv.Optional(CONF_LENGTH): pixels_or_percent_validator,
|
||||||
cv.Optional(CONF_OPA): opacity,
|
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(
|
INDICATOR_IMG_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_SRC): lv_image,
|
cv.Required(CONF_SRC): lv_image,
|
||||||
cv.Required(CONF_PIVOT_X): pixels,
|
cv.Optional(CONF_PIVOT_X, default=0): pixels,
|
||||||
cv.Required(CONF_PIVOT_Y): pixels,
|
cv.Optional(CONF_PIVOT_Y): pixels,
|
||||||
cv.Optional(CONF_VALUE): lv_float,
|
cv.Optional(CONF_VALUE): lv_float,
|
||||||
cv.Optional(CONF_OPA): opacity,
|
cv.Optional(CONF_OPA, default=1.0): opacity,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
INDICATOR_ARC_SCHEMA = cv.Schema(
|
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_COLOR, default=0): lv_color,
|
||||||
cv.Optional(CONF_R_MOD, default=0): size,
|
cv.Optional(CONF_R_MOD): padding,
|
||||||
cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float,
|
cv.Optional(CONF_VALUE): lv_float,
|
||||||
cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float,
|
cv.Optional(CONF_START_VALUE): lv_float,
|
||||||
cv.Optional(CONF_END_VALUE): lv_float,
|
cv.Optional(CONF_END_VALUE): lv_float,
|
||||||
cv.Optional(CONF_OPA): opacity,
|
cv.Optional(CONF_OPA): opacity,
|
||||||
}
|
}
|
||||||
)
|
).add_extra(cv.has_at_most_one_key(CONF_VALUE, CONF_START_VALUE))
|
||||||
|
|
||||||
INDICATOR_TICKS_SCHEMA = cv.Schema(
|
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_START, default=0): lv_color,
|
||||||
cv.Optional(CONF_COLOR_END): lv_color,
|
cv.Optional(CONF_COLOR_END): lv_color,
|
||||||
cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float,
|
cv.Optional(CONF_VALUE): lv_float,
|
||||||
cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float,
|
cv.Optional(CONF_START_VALUE): lv_float,
|
||||||
cv.Optional(CONF_END_VALUE): lv_float,
|
cv.Optional(CONF_END_VALUE): lv_float,
|
||||||
cv.Optional(CONF_LOCAL, default=False): lv_bool,
|
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(
|
INDICATOR_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
cv.Exclusive(CONF_LINE, CONF_INDICATORS): INDICATOR_LINE_SCHEMA.extend(
|
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(
|
cv.Exclusive(CONF_IMAGE, CONF_INDICATORS): cv.All(
|
||||||
INDICATOR_IMG_SCHEMA.extend(
|
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"),
|
requires_component("image"),
|
||||||
),
|
),
|
||||||
cv.Exclusive(CONF_ARC, CONF_INDICATORS): INDICATOR_ARC_SCHEMA.extend(
|
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.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(
|
SCALE_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(lv_scale_t),
|
||||||
cv.Optional(CONF_TICKS): cv.Schema(
|
cv.Optional(CONF_TICKS): cv.Schema(
|
||||||
{
|
{
|
||||||
cv.Optional(CONF_COUNT, default=12): cv.positive_int,
|
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_LENGTH, default=10): size,
|
||||||
|
cv.Optional(CONF_RADIAL_OFFSET, default=0): size,
|
||||||
cv.Optional(CONF_COLOR, default=0x808080): lv_color,
|
cv.Optional(CONF_COLOR, default=0x808080): lv_color,
|
||||||
cv.Optional(CONF_MAJOR): cv.Schema(
|
cv.Optional(CONF_MAJOR): cv.Schema(
|
||||||
{
|
{
|
||||||
cv.Optional(CONF_STRIDE, default=3): cv.positive_int,
|
cv.Optional(CONF_STRIDE, default=3): cv.positive_int,
|
||||||
cv.Optional(CONF_WIDTH, default=5): size,
|
cv.Optional(CONF_WIDTH, default=5): size,
|
||||||
cv.Optional(CONF_LENGTH, default="15%"): 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_COLOR, default=0): lv_color,
|
||||||
cv.Optional(CONF_LABEL_GAP, default=4): size,
|
cv.Optional(CONF_LABEL_GAP, default=4): size,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_,
|
cv.Optional(CONF_RANGE_FROM, default=0.0): lv_int,
|
||||||
cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_,
|
cv.Optional(CONF_RANGE_TO, default=100.0): lv_int,
|
||||||
cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360),
|
cv.Optional(CONF_ANGLE_RANGE, default=270): lv_angle_degrees,
|
||||||
cv.Optional(CONF_ROTATION): lv_angle_degrees,
|
cv.Optional(CONF_ROTATION, default=0): lv_angle_degrees,
|
||||||
cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA),
|
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):
|
class MeterType(WidgetType):
|
||||||
@@ -175,111 +321,240 @@ class MeterType(WidgetType):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
CONF_METER,
|
CONF_METER,
|
||||||
lv_meter_t,
|
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),
|
(CONF_MAIN, CONF_INDICATOR, CONF_TICKS, CONF_ITEMS),
|
||||||
METER_SCHEMA,
|
METER_SCHEMA,
|
||||||
|
lv_name=CONF_CONTAINER,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def to_code(self, w: Widget, config):
|
def get_uses(self):
|
||||||
"""For a meter object, create and set parameters"""
|
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
|
var = w.obj
|
||||||
|
|
||||||
|
# LVGL 9.4 scale widget setup
|
||||||
|
# Background style will be applied.
|
||||||
for scale_conf in config.get(CONF_SCALES, ()):
|
for scale_conf in config.get(CONF_SCALES, ()):
|
||||||
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
|
scale_var = cg.Pvariable(scale_conf[CONF_ID], lv_expr.scale_create(var))
|
||||||
if CONF_ROTATION in scale_conf:
|
percent100 = await pixels_or_percent.process(1.0)
|
||||||
rotation = await lv_angle_degrees.process(scale_conf[CONF_ROTATION])
|
lv_obj.set_style_height(scale_var, percent100, LV_PART.MAIN)
|
||||||
with LocalVariable(
|
lv_obj.set_style_width(scale_var, percent100, LV_PART.MAIN)
|
||||||
"meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var)
|
lv_obj.set_style_align(scale_var, CHILD_ALIGNMENTS.CENTER, LV_PART.MAIN)
|
||||||
) as meter_var:
|
lv_obj.set_style_bg_opa(scale_var, LV_OPA.TRANSP, LV_PART.MAIN)
|
||||||
lv.meter_set_scale_range(
|
lv_obj.set_style_radius(scale_var, LV_RADIUS.CIRCLE, 0)
|
||||||
var,
|
await set_obj_properties(Widget(scale_var, scale_spec), indicator_config)
|
||||||
meter_var,
|
|
||||||
scale_conf[CONF_RANGE_FROM],
|
lv.scale_set_mode(scale_var, LV_SCALE_MODE.ROUND_INNER)
|
||||||
scale_conf[CONF_RANGE_TO],
|
# Set the scale range
|
||||||
scale_conf[CONF_ANGLE_RANGE],
|
range_from = await lv_int.process(scale_conf[CONF_RANGE_FROM])
|
||||||
rotation,
|
range_to = await lv_int.process(scale_conf[CONF_RANGE_TO])
|
||||||
)
|
lv.scale_set_range(scale_var, range_from, range_to)
|
||||||
if ticks := scale_conf.get(CONF_TICKS):
|
|
||||||
color = await lv_color.process(ticks[CONF_COLOR])
|
angle_range = await lv_angle_degrees.process(scale_conf[CONF_ANGLE_RANGE])
|
||||||
lv.meter_set_scale_ticks(
|
rotation = await lv_angle_degrees.process(scale_conf[CONF_ROTATION])
|
||||||
var,
|
# Set angle range
|
||||||
meter_var,
|
lv.scale_set_angle_range(
|
||||||
ticks[CONF_COUNT],
|
scale_var,
|
||||||
await size.process(ticks[CONF_WIDTH]),
|
angle_range,
|
||||||
await size.process(ticks[CONF_LENGTH]),
|
)
|
||||||
color,
|
|
||||||
|
# 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:
|
lw = Widget(tvar, arc_indicator_type)
|
||||||
major = ticks[CONF_MAJOR]
|
await set_indicator_values(lw, v)
|
||||||
lv.meter_set_scale_major_ticks(
|
|
||||||
var,
|
if t == CONF_TICK_STYLE:
|
||||||
meter_var,
|
# No object created for this
|
||||||
major[CONF_STRIDE],
|
color_start = await lv_color.process(v[CONF_COLOR_START])
|
||||||
await size.process(major[CONF_WIDTH]),
|
color_end = await lv_color.process(v[CONF_COLOR_END])
|
||||||
await size.process(major[CONF_LENGTH]),
|
local = v[CONF_LOCAL]
|
||||||
await lv_color.process(major[CONF_COLOR]),
|
if color_start and color_end:
|
||||||
await size.process(major[CONF_LABEL_GAP]),
|
async with LambdaContext(
|
||||||
)
|
[(lv_event_t.operator("ptr"), "e")]
|
||||||
for indicator in scale_conf.get(CONF_INDICATORS, ()):
|
) as lambda_:
|
||||||
(t, v) = next(iter(indicator.items()))
|
lv.scale_draw_event_cb(
|
||||||
iid = v[CONF_ID]
|
lambda_.get_parameter(0),
|
||||||
ivar = cg.Pvariable(iid, cg.nullptr, type_=lv_meter_indicator_t)
|
start_value,
|
||||||
# Enable getting the meter to which this belongs.
|
end_value,
|
||||||
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,
|
|
||||||
color_start,
|
color_start,
|
||||||
color_end,
|
color_end,
|
||||||
v[CONF_LOCAL],
|
local,
|
||||||
await size.process(v[CONF_WIDTH]),
|
)
|
||||||
),
|
lv_obj.add_event_cb(
|
||||||
|
scale_var,
|
||||||
|
await lambda_.get_lambda(),
|
||||||
|
LV_EVENT.DRAW_TASK_ADDED,
|
||||||
|
nullptr,
|
||||||
)
|
)
|
||||||
if t == CONF_IMAGE:
|
lv.obj_add_flag(scale_var, LV_OBJ_FLAG.SEND_DRAW_TASK_EVENTS)
|
||||||
add_lv_use("img")
|
|
||||||
lv_assign(
|
if t == CONF_LINE:
|
||||||
ivar,
|
# Needle represented by a line
|
||||||
lv_expr.meter_add_needle_img(
|
if CONF_LENGTH in v:
|
||||||
var,
|
length = v[CONF_LENGTH]
|
||||||
meter_var,
|
elif r_mod := v.get(CONF_R_MOD):
|
||||||
await lv_image.process(v[CONF_SRC]),
|
get_remapped_uses().add(CONF_R_MOD)
|
||||||
v[CONF_PIVOT_X],
|
length = -abs(r_mod)
|
||||||
v[CONF_PIVOT_Y],
|
else:
|
||||||
),
|
length = 1.0
|
||||||
)
|
props = {
|
||||||
await set_indicator_values(var, ivar, v)
|
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()
|
meter_spec = MeterType()
|
||||||
@@ -303,23 +578,39 @@ async def indicator_update_to_code(config, action_id, template_arg, args):
|
|||||||
widget = await get_widgets(config)
|
widget = await get_widgets(config)
|
||||||
|
|
||||||
async def set_value(w: Widget):
|
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(
|
return await action_to_code(
|
||||||
widget, set_value, action_id, template_arg, args, config
|
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)
|
start_value = await get_start_value(config)
|
||||||
end_value = await get_end_value(config)
|
end_value = await get_end_value(config)
|
||||||
if start_value is not None:
|
if indicator.type is arc_indicator_type:
|
||||||
if end_value is None:
|
# For scale sections, we update the range
|
||||||
lv.meter_set_indicator_value(meter, indicator, start_value)
|
if start_value is not None and end_value is not None:
|
||||||
else:
|
lv.scale_section_set_range(indicator.obj, start_value, end_value)
|
||||||
lv.meter_set_indicator_start_value(meter, indicator, start_value)
|
elif start_value is not None:
|
||||||
if end_value is not None:
|
# If only start value, use it as both start and end (single point)
|
||||||
lv.meter_set_indicator_end_value(meter, indicator, end_value)
|
lv.scale_section_set_range(indicator.obj, start_value, start_value)
|
||||||
if (opa := config.get(CONF_OPA)) is not None:
|
elif end_value is not None:
|
||||||
lv_assign(indicator.opa, await opacity.process(opa))
|
# If only end value, assume range from 0 to end_value
|
||||||
lv_obj.invalidate(meter)
|
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 import codegen as cg, config_validation as cv
|
||||||
from esphome.const import CONF_BUTTON, CONF_ID, CONF_ITEMS, CONF_TEXT
|
from esphome.const import CONF_BUTTON, CONF_ID, CONF_TEXT
|
||||||
from esphome.core import ID
|
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 esphome.cpp_types import nullptr
|
||||||
|
|
||||||
from ..defines import (
|
from ..defines import (
|
||||||
@@ -9,40 +9,82 @@ from ..defines import (
|
|||||||
CONF_BUTTON_STYLE,
|
CONF_BUTTON_STYLE,
|
||||||
CONF_BUTTONS,
|
CONF_BUTTONS,
|
||||||
CONF_CLOSE_BUTTON,
|
CONF_CLOSE_BUTTON,
|
||||||
|
CONF_HEADER_BUTTONS,
|
||||||
|
CONF_MAIN,
|
||||||
CONF_MSGBOXES,
|
CONF_MSGBOXES,
|
||||||
|
CONF_SRC,
|
||||||
CONF_TITLE,
|
CONF_TITLE,
|
||||||
|
LV_OBJ_FLAG,
|
||||||
TYPE_FLEX,
|
TYPE_FLEX,
|
||||||
|
add_warning,
|
||||||
literal,
|
literal,
|
||||||
)
|
)
|
||||||
from ..helpers import add_lv_use, lvgl_components_required
|
from ..helpers import add_lv_use
|
||||||
from ..lv_validation import lv_bool, lv_pct, lv_text
|
from ..lv_validation import lv_bool, lv_image, lv_text, pixels_or_percent
|
||||||
from ..lvcode import (
|
from ..lvcode import EVENT_ARG, LambdaContext, LocalVariable, lv, lv_expr, lv_obj
|
||||||
EVENT_ARG,
|
from ..schemas import (
|
||||||
LambdaContext,
|
STYLE_SCHEMA,
|
||||||
LocalVariable,
|
STYLED_TEXT_SCHEMA,
|
||||||
lv,
|
TEXT_SCHEMA,
|
||||||
lv_add,
|
container_schema,
|
||||||
lv_assign,
|
part_schema,
|
||||||
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 ..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 .label import CONF_LABEL
|
||||||
from .obj import obj_spec
|
from .obj import obj_spec
|
||||||
|
|
||||||
CONF_MSGBOX = "msgbox"
|
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(
|
MSGBOX_SCHEMA = container_schema(
|
||||||
obj_spec,
|
obj_spec,
|
||||||
STYLE_SCHEMA.extend(
|
STYLE_SCHEMA.extend(
|
||||||
@@ -50,10 +92,14 @@ MSGBOX_SCHEMA = container_schema(
|
|||||||
cv.GenerateID(CONF_ID): cv.declare_id(lv_obj_t),
|
cv.GenerateID(CONF_ID): cv.declare_id(lv_obj_t),
|
||||||
cv.Required(CONF_TITLE): STYLED_TEXT_SCHEMA,
|
cv.Required(CONF_TITLE): STYLED_TEXT_SCHEMA,
|
||||||
cv.Optional(CONF_BODY, default=""): STYLED_TEXT_SCHEMA,
|
cv.Optional(CONF_BODY, default=""): STYLED_TEXT_SCHEMA,
|
||||||
cv.Optional(CONF_BUTTONS): cv.ensure_list(BUTTONMATRIX_BUTTON_SCHEMA),
|
cv.Optional(CONF_BUTTONS): cv.ensure_list(
|
||||||
cv.Optional(CONF_BUTTON_STYLE): part_schema(buttonmatrix_spec.parts),
|
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.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):
|
async def msgbox_to_code(top_layer, conf):
|
||||||
"""
|
"""
|
||||||
Construct a message box. This consists of a full-screen translucent background enclosing a centered container
|
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
|
:param conf: The config data
|
||||||
:return: code to add to the init lambda
|
:return: code to add to the init lambda
|
||||||
"""
|
"""
|
||||||
@@ -71,60 +119,42 @@ async def msgbox_to_code(top_layer, conf):
|
|||||||
CONF_BUTTON,
|
CONF_BUTTON,
|
||||||
CONF_LABEL,
|
CONF_LABEL,
|
||||||
CONF_MSGBOX,
|
CONF_MSGBOX,
|
||||||
*buttonmatrix_spec.get_uses(),
|
|
||||||
*button_spec.get_uses(),
|
*button_spec.get_uses(),
|
||||||
)
|
)
|
||||||
lvgl_components_required.add("BUTTONMATRIX")
|
if CONF_BUTTON_STYLE in conf:
|
||||||
messagebox_id = conf[CONF_ID]
|
add_warning(
|
||||||
outer_id = f"{messagebox_id.id}_outer"
|
"'button_style' for msgbox is deprecated - style the buttons directly."
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
)
|
messagebox_id = conf[CONF_ID]
|
||||||
msgbox = lv_Pvariable(lv_obj_t, messagebox_id.id)
|
outer_id = ID(f"{messagebox_id.id}_outer", type=lv_obj_t)
|
||||||
outer_widget = Widget.create(outer_id, outer, obj_spec, conf)
|
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
|
outer_widget.move_to_foreground = True
|
||||||
msgbox_widget = Widget.create(messagebox_id, msgbox, obj_spec, conf)
|
msgbox_widget = Widget.create(messagebox_id, msgbox, obj_spec, conf)
|
||||||
msgbox_widget.outer = outer_widget
|
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, ""))
|
text = await lv_text.process(conf[CONF_BODY].get(CONF_TEXT, ""))
|
||||||
title = await lv_text.process(conf[CONF_TITLE].get(CONF_TEXT, ""))
|
title = await lv_text.process(conf[CONF_TITLE].get(CONF_TEXT, ""))
|
||||||
close_button = conf[CONF_CLOSE_BUTTON]
|
close_button = conf[CONF_CLOSE_BUTTON]
|
||||||
lv_assign(outer, lv_expr.obj_create(top_layer))
|
percent100 = await pixels_or_percent.process(1.0)
|
||||||
lv_obj.set_width(outer, lv_pct(100))
|
lv_obj.set_size(outer, percent100, percent100)
|
||||||
lv_obj.set_height(outer, lv_pct(100))
|
outer_widget.add_style(await OUTER_STYLE.get_var())
|
||||||
lv_obj.set_style_bg_opa(outer, 128, 0)
|
outer_widget.add_flag(LV_OBJ_FLAG.HIDDEN)
|
||||||
lv_obj.set_style_bg_color(outer, literal("lv_color_black()"), 0)
|
lv.msgbox_add_title(msgbox, title)
|
||||||
lv_obj.set_style_border_width(outer, 0, 0)
|
lv.msgbox_add_text(msgbox, text)
|
||||||
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)
|
|
||||||
)
|
|
||||||
lv_obj.set_style_align(msgbox, literal("LV_ALIGN_CENTER"), 0)
|
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 set_obj_properties(msgbox_widget, conf)
|
||||||
await add_widgets(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:
|
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:
|
if close_button:
|
||||||
with LocalVariable(
|
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:
|
) as close_btn:
|
||||||
lv_obj.remove_event_cb(close_btn, nullptr)
|
lv_obj.remove_event_cb(close_btn, nullptr)
|
||||||
lv_obj.add_event_cb(
|
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
|
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):
|
async def msgboxes_to_code(lv_component, config):
|
||||||
top_layer = lv.disp_get_layer_top(lv_component.get_disp())
|
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 ..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):
|
class ObjType(WidgetType):
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_SIZE, CONF_TEXT
|
from esphome.const import CONF_SIZE, CONF_TEXT
|
||||||
from esphome.cpp_generator import MockObjClass
|
|
||||||
|
|
||||||
from ..defines import CONF_MAIN
|
from ..defines import CONF_MAIN, get_color_formats
|
||||||
from ..lv_validation import lv_color, lv_text
|
from ..lv_validation import color, lv_color, lv_int, lv_text
|
||||||
from ..lvcode import LocalVariable, lv, lv_expr
|
from ..lvcode import LocalVariable, lv
|
||||||
from ..schemas import TEXT_SCHEMA
|
from ..schemas import TEXT_SCHEMA
|
||||||
from ..types import WidgetType, lv_obj_t
|
from ..types import lv_obj_t
|
||||||
from . import Widget
|
from . import Widget, WidgetType
|
||||||
|
from .canvas import CONF_CANVAS
|
||||||
|
from .img import CONF_IMAGE
|
||||||
|
|
||||||
CONF_QRCODE = "qrcode"
|
CONF_QRCODE = "qrcode"
|
||||||
CONF_DARK_COLOR = "dark_color"
|
CONF_DARK_COLOR = "dark_color"
|
||||||
@@ -16,9 +17,16 @@ CONF_LIGHT_COLOR = "light_color"
|
|||||||
|
|
||||||
QRCODE_SCHEMA = {
|
QRCODE_SCHEMA = {
|
||||||
**TEXT_SCHEMA,
|
**TEXT_SCHEMA,
|
||||||
cv.Optional(CONF_DARK_COLOR, default="black"): lv_color,
|
cv.Optional(CONF_DARK_COLOR, default="black"): color,
|
||||||
cv.Optional(CONF_LIGHT_COLOR, default="white"): lv_color,
|
cv.Optional(CONF_LIGHT_COLOR, default="white"): color,
|
||||||
cv.Required(CONF_SIZE): cv.int_,
|
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,
|
lv_obj_t,
|
||||||
(CONF_MAIN,),
|
(CONF_MAIN,),
|
||||||
QRCODE_SCHEMA,
|
QRCODE_SCHEMA,
|
||||||
modify_schema=TEXT_SCHEMA,
|
modify_schema=QRCODE_MODIFY_SCHEMA,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_uses(self):
|
def get_uses(self):
|
||||||
return "canvas", "img", "label"
|
return CONF_CANVAS, CONF_IMAGE
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
async def to_code(self, w: Widget, config):
|
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 (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)
|
value = await lv_text.process(value)
|
||||||
with LocalVariable("qr_text", cg.std_string, value, modifier="") as str_obj:
|
with LocalVariable("qr_text", cg.std_string, value, modifier="") as str_obj:
|
||||||
lv.qrcode_update(w.obj, str_obj.c_str(), str_obj.size())
|
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 esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE
|
||||||
|
|
||||||
from ..defines import (
|
from ..defines import (
|
||||||
BAR_MODES,
|
|
||||||
CONF_ANIMATED,
|
CONF_ANIMATED,
|
||||||
CONF_INDICATOR,
|
CONF_INDICATOR,
|
||||||
CONF_KNOB,
|
CONF_KNOB,
|
||||||
CONF_MAIN,
|
CONF_MAIN,
|
||||||
|
SLIDER_MODES,
|
||||||
literal,
|
literal,
|
||||||
)
|
)
|
||||||
from ..helpers import add_lv_use
|
|
||||||
from ..lv_validation import animated, get_start_value, lv_float
|
from ..lv_validation import animated, get_start_value, lv_float
|
||||||
from ..lvcode import lv
|
from ..lvcode import lv
|
||||||
from ..types import LvNumber, NumberType
|
from ..types import LvNumber
|
||||||
from . import Widget
|
from . import NumberType, Widget
|
||||||
|
from .label import CONF_LABEL
|
||||||
from .lv_bar import CONF_BAR
|
from .lv_bar import CONF_BAR
|
||||||
|
|
||||||
CONF_SLIDER = "slider"
|
CONF_SLIDER = "slider"
|
||||||
@@ -29,7 +29,7 @@ SLIDER_SCHEMA = cv.Schema(
|
|||||||
cv.Optional(CONF_VALUE): lv_float,
|
cv.Optional(CONF_VALUE): lv_float,
|
||||||
cv.Optional(CONF_MIN_VALUE, default=0): cv.int_,
|
cv.Optional(CONF_MIN_VALUE, default=0): cv.int_,
|
||||||
cv.Optional(CONF_MAX_VALUE, default=100): 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,
|
cv.Optional(CONF_ANIMATED, default=True): animated,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -49,8 +49,10 @@ class SliderType(NumberType):
|
|||||||
def animated(self):
|
def animated(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def get_uses(self):
|
||||||
|
return (CONF_BAR, CONF_LABEL)
|
||||||
|
|
||||||
async def to_code(self, w: Widget, config):
|
async def to_code(self, w: Widget, config):
|
||||||
add_lv_use(CONF_BAR)
|
|
||||||
if CONF_MIN_VALUE in config:
|
if CONF_MIN_VALUE in config:
|
||||||
# not modify case
|
# not modify case
|
||||||
lv.slider_set_range(w.obj, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE])
|
lv.slider_set_range(w.obj, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE])
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import esphome.config_validation as cv
|
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 ..defines import CONF_ARC_LENGTH, CONF_INDICATOR, CONF_MAIN, CONF_SPIN_TIME
|
||||||
from ..lv_validation import lv_angle_degrees, lv_milliseconds
|
from ..lv_validation import lv_angle_degrees, lv_milliseconds
|
||||||
from ..lvcode import lv_expr
|
from ..lvcode import lv
|
||||||
from ..types import LvType
|
from ..types import LvType
|
||||||
from . import Widget, WidgetType
|
from . import Widget, WidgetType
|
||||||
from .arc import CONF_ARC
|
from .arc import CONF_ARC
|
||||||
@@ -12,8 +11,10 @@ CONF_SPINNER = "spinner"
|
|||||||
|
|
||||||
SPINNER_SCHEMA = cv.Schema(
|
SPINNER_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_ARC_LENGTH): lv_angle_degrees,
|
cv.Optional(CONF_ARC_LENGTH, default=200): cv.All(
|
||||||
cv.Required(CONF_SPIN_TIME): lv_milliseconds,
|
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"),
|
LvType("lv_spinner_t"),
|
||||||
(CONF_MAIN, CONF_INDICATOR),
|
(CONF_MAIN, CONF_INDICATOR),
|
||||||
SPINNER_SCHEMA,
|
SPINNER_SCHEMA,
|
||||||
{},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def to_code(self, w: Widget, config):
|
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):
|
def get_uses(self):
|
||||||
return (CONF_ARC,)
|
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()
|
spinner_spec = SpinnerType()
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
from esphome import automation
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ID, CONF_INDEX, CONF_NAME, CONF_POSITION, CONF_SIZE
|
from esphome.const import (
|
||||||
from esphome.cpp_generator import MockObjClass
|
CONF_ID,
|
||||||
|
CONF_INDEX,
|
||||||
|
CONF_ITEMS,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_POSITION,
|
||||||
|
CONF_SIZE,
|
||||||
|
)
|
||||||
|
|
||||||
from ..automation import action_to_code
|
from ..automation import action_to_code
|
||||||
from ..defines import (
|
from ..defines import (
|
||||||
@@ -15,10 +21,11 @@ from ..defines import (
|
|||||||
literal,
|
literal,
|
||||||
)
|
)
|
||||||
from ..lv_validation import animated, lv_int, size
|
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 ..schemas import container_schema, part_schema
|
||||||
from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr
|
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 . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties
|
||||||
|
from .button import button_spec
|
||||||
from .buttonmatrix import buttonmatrix_spec
|
from .buttonmatrix import buttonmatrix_spec
|
||||||
from .obj import obj_spec
|
from .obj import obj_spec
|
||||||
|
|
||||||
@@ -69,6 +76,10 @@ class TabviewType(WidgetType):
|
|||||||
return "btnmatrix", TYPE_FLEX
|
return "btnmatrix", TYPE_FLEX
|
||||||
|
|
||||||
async def to_code(self, w: Widget, config: dict):
|
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]:
|
for tab_conf in config[CONF_TABS]:
|
||||||
w_id = tab_conf[CONF_ID]
|
w_id = tab_conf[CONF_ID]
|
||||||
tab_obj = cg.Pvariable(w_id, cg.nullptr, type_=lv_tab_t)
|
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]))
|
lv_assign(tab_obj, lv_expr.tabview_add_tab(w.obj, tab_conf[CONF_NAME]))
|
||||||
await set_obj_properties(tab_widget, tab_conf)
|
await set_obj_properties(tab_widget, tab_conf)
|
||||||
await add_widgets(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(
|
with LocalVariable(
|
||||||
"tabview_btnmatrix", lv_obj_t, rhs=lv_expr.tabview_get_tab_btns(w.obj)
|
"tabview_bar", lv_obj_t, rhs=lv_expr.tabview_get_tab_bar(w.obj)
|
||||||
) as btnmatrix_obj:
|
) as bar_obj:
|
||||||
await set_obj_properties(Widget(btnmatrix_obj, obj_spec), button_style)
|
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):
|
if content_style := config.get(CONF_CONTENT_STYLE):
|
||||||
with LocalVariable(
|
with LocalVariable(
|
||||||
"tabview_content", lv_obj_t, rhs=lv_expr.tabview_get_content(w.obj)
|
"tabview_content", lv_obj_t, rhs=lv_expr.tabview_get_content(w.obj)
|
||||||
) as content_obj:
|
) as content_obj:
|
||||||
await set_obj_properties(Widget(content_obj, obj_spec), content_style)
|
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()
|
tabview_spec = TabviewType()
|
||||||
|
|
||||||
@@ -117,6 +130,6 @@ async def tabview_select(config, action_id, template_arg, args):
|
|||||||
|
|
||||||
async def do_select(w: Widget):
|
async def do_select(w: Widget):
|
||||||
lv.tabview_set_act(w.obj, index, literal(config[CONF_ANIMATED]))
|
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)
|
return await action_to_code(widget, do_select, action_id, template_arg, args)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from ..defines import (
|
|||||||
TILE_DIRECTIONS,
|
TILE_DIRECTIONS,
|
||||||
literal,
|
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 ..lvcode import lv, lv_assign, lv_expr, lv_obj, lv_Pvariable
|
||||||
from ..schemas import container_schema
|
from ..schemas import container_schema
|
||||||
from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr
|
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]
|
w_id = tile_conf[CONF_ID]
|
||||||
tile_obj = lv_Pvariable(lv_obj_t, w_id)
|
tile_obj = lv_Pvariable(lv_obj_t, w_id)
|
||||||
tile = Widget.create(w_id, tile_obj, tile_spec, tile_conf)
|
tile = Widget.create(w_id, tile_obj, tile_spec, tile_conf)
|
||||||
dirs = tile_conf[CONF_DIR]
|
dirs = await TILE_DIRECTIONS.process(tile_conf[CONF_DIR])
|
||||||
if isinstance(dirs, list):
|
|
||||||
dirs = "|".join(dirs)
|
|
||||||
row_pos = tile_conf[CONF_ROW]
|
row_pos = tile_conf[CONF_ROW]
|
||||||
col_pos = tile_conf[CONF_COLUMN]
|
col_pos = tile_conf[CONF_COLUMN]
|
||||||
lv_assign(
|
lv_assign(
|
||||||
tile_obj,
|
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
|
# 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 set_obj_properties(tile, tile_conf)
|
||||||
await add_widgets(tile, tile_conf)
|
await add_widgets(tile, tile_conf)
|
||||||
if tiles:
|
if tiles:
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import esphome.codegen as cg
|
|||||||
from esphome.components import runtime_image
|
from esphome.components import runtime_image
|
||||||
from esphome.components.const import CONF_REQUEST_HEADERS
|
from esphome.components.const import CONF_REQUEST_HEADERS
|
||||||
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
|
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
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_BUFFER_SIZE,
|
CONF_BUFFER_SIZE,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_ON_ERROR,
|
CONF_ON_ERROR,
|
||||||
CONF_TRIGGER_ID,
|
CONF_TRIGGER_ID,
|
||||||
|
CONF_TYPE,
|
||||||
CONF_URL,
|
CONF_URL,
|
||||||
)
|
)
|
||||||
from esphome.core import Lambda
|
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):
|
async def to_code(config):
|
||||||
# Use the enhanced helper function to get all runtime image parameters
|
# Use the enhanced helper function to get all runtime image parameters
|
||||||
settings = await runtime_image.process_runtime_image_config(config)
|
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]
|
url = config[CONF_URL]
|
||||||
var = cg.new_Pvariable(
|
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"))
|
transparent = get_transparency_enum(config.get(CONF_TRANSPARENCY, "OPAQUE"))
|
||||||
|
|
||||||
# Get byte order (True for big endian, False for little endian)
|
# 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
|
# Get placeholder if specified
|
||||||
placeholder = None
|
placeholder = None
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case image::IMAGE_TYPE_RGB565: {
|
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;
|
Color mapped_color = color;
|
||||||
this->map_chroma_key(mapped_color);
|
this->map_chroma_key(mapped_color);
|
||||||
uint16_t rgb565 = display::ColorUtil::color_to_565(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);
|
this->buffer_[pos + 1] = static_cast<uint8_t>((rgb565 >> 8) & 0xFF);
|
||||||
}
|
}
|
||||||
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
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;
|
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 {
|
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;
|
return (this->get_bpp() * width + 7u) / 8u * height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,7 @@
|
|||||||
#define USE_LVGL_MSGBOX
|
#define USE_LVGL_MSGBOX
|
||||||
#define USE_LVGL_ROLLER
|
#define USE_LVGL_ROLLER
|
||||||
#define USE_LVGL_ROTARY_ENCODER
|
#define USE_LVGL_ROTARY_ENCODER
|
||||||
|
#define USE_LVGL_SCALE
|
||||||
#define USE_LVGL_SLIDER
|
#define USE_LVGL_SLIDER
|
||||||
#define USE_LVGL_SPAN
|
#define USE_LVGL_SPAN
|
||||||
#define USE_LVGL_SPINBOX
|
#define USE_LVGL_SPINBOX
|
||||||
|
|||||||
@@ -77,3 +77,5 @@ dependencies:
|
|||||||
- if: "idf_version >=6.0.0 && target in [esp32s2, esp32s3, esp32p4]"
|
- if: "idf_version >=6.0.0 && target in [esp32s2, esp32s3, esp32p4]"
|
||||||
esp32async/asynctcp:
|
esp32async/asynctcp:
|
||||||
version: 3.4.91
|
version: 3.4.91
|
||||||
|
lvgl/lvgl:
|
||||||
|
version: 9.5.0
|
||||||
|
|||||||
+4
-1
@@ -42,7 +42,6 @@ lib_deps_base =
|
|||||||
https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps
|
https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps
|
||||||
; This is using the repository until a new release is published to PlatformIO
|
; 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
|
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 =
|
lib_deps =
|
||||||
${common.lib_deps_base}
|
${common.lib_deps_base}
|
||||||
@@ -120,6 +119,7 @@ lib_deps =
|
|||||||
ESP8266mDNS ; mdns (Arduino built-in)
|
ESP8266mDNS ; mdns (Arduino built-in)
|
||||||
DNSServer ; captive_portal (Arduino built-in)
|
DNSServer ; captive_portal (Arduino built-in)
|
||||||
droscy/esp_wireguard@0.4.2 ; wireguard
|
droscy/esp_wireguard@0.4.2 ; wireguard
|
||||||
|
lvgl/lvgl@9.5.0 ; lvgl
|
||||||
|
|
||||||
build_flags =
|
build_flags =
|
||||||
${common:arduino.build_flags}
|
${common:arduino.build_flags}
|
||||||
@@ -204,6 +204,7 @@ lib_deps =
|
|||||||
ayushsharma82/RPAsyncTCP@1.3.2 ; async_tcp
|
ayushsharma82/RPAsyncTCP@1.3.2 ; async_tcp
|
||||||
bblanchon/ArduinoJson@7.4.2 ; json
|
bblanchon/ArduinoJson@7.4.2 ; json
|
||||||
ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base
|
ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base
|
||||||
|
lvgl/lvgl@9.5.0 ; lvgl
|
||||||
build_flags =
|
build_flags =
|
||||||
${common:arduino.build_flags}
|
${common:arduino.build_flags}
|
||||||
-DUSE_RP2040
|
-DUSE_RP2040
|
||||||
@@ -221,6 +222,7 @@ lib_deps =
|
|||||||
bblanchon/ArduinoJson@7.4.2 ; json
|
bblanchon/ArduinoJson@7.4.2 ; json
|
||||||
ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base
|
ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base
|
||||||
droscy/esp_wireguard@0.4.2 ; wireguard
|
droscy/esp_wireguard@0.4.2 ; wireguard
|
||||||
|
lvgl/lvgl@9.5.0 ; lvgl
|
||||||
build_flags =
|
build_flags =
|
||||||
${common:arduino.build_flags}
|
${common:arduino.build_flags}
|
||||||
-DUSE_LIBRETINY
|
-DUSE_LIBRETINY
|
||||||
@@ -242,6 +244,7 @@ build_flags =
|
|||||||
lib_deps =
|
lib_deps =
|
||||||
${common.lib_deps_base}
|
${common.lib_deps_base}
|
||||||
bblanchon/ArduinoJson@7.4.2 ; json
|
bblanchon/ArduinoJson@7.4.2 ; json
|
||||||
|
lvgl/lvgl@9.5.0 ; lvgl
|
||||||
|
|
||||||
; All the actual environments are defined below.
|
; All the actual environments are defined below.
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ lvgl:
|
|||||||
disp_bg_color: color_id
|
disp_bg_color: color_id
|
||||||
disp_bg_image: cat_image
|
disp_bg_image: cat_image
|
||||||
disp_bg_opa: cover
|
disp_bg_opa: cover
|
||||||
|
bottom_layer:
|
||||||
|
widgets:
|
||||||
|
- obj:
|
||||||
|
bg_color: 0x000000
|
||||||
|
bg_opa: cover
|
||||||
theme:
|
theme:
|
||||||
obj:
|
obj:
|
||||||
border_width: 1
|
border_width: 1
|
||||||
@@ -49,12 +54,14 @@ lvgl:
|
|||||||
gradients:
|
gradients:
|
||||||
- id: color_bar
|
- id: color_bar
|
||||||
direction: hor
|
direction: hor
|
||||||
dither: err_diff
|
# dither: err_diff
|
||||||
stops:
|
stops:
|
||||||
- color: 0xFF0000
|
- color: 0xFF0000
|
||||||
position: 0
|
position: 0
|
||||||
|
opa: 100%
|
||||||
- color: 0xFFFF00
|
- color: 0xFFFF00
|
||||||
position: 42
|
position: 42
|
||||||
|
opa: 80%
|
||||||
- color: 0x00FF00
|
- color: 0x00FF00
|
||||||
position: 84
|
position: 84
|
||||||
- color: 0x00FFFF
|
- color: 0x00FFFF
|
||||||
@@ -156,6 +163,16 @@ lvgl:
|
|||||||
offset_x: !lambda return 20;
|
offset_x: !lambda return 20;
|
||||||
offset_y: !lambda return 20;
|
offset_y: !lambda return 20;
|
||||||
antialias: !lambda return true;
|
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
|
- id: simple_msgbox
|
||||||
title: Simple
|
title: Simple
|
||||||
|
|
||||||
@@ -199,11 +216,11 @@ lvgl:
|
|||||||
text: Unloaded
|
text: Unloaded
|
||||||
- lvgl.label.update:
|
- lvgl.label.update:
|
||||||
id: msgbox_label
|
id: msgbox_label
|
||||||
text: "" # Empty text
|
text: "" # Empty text
|
||||||
on_all_events:
|
on_all_events:
|
||||||
logger.log:
|
logger.log:
|
||||||
format: "Event %s"
|
format: "Event %s"
|
||||||
args: ['lv_event_code_name_for(event->code).c_str()']
|
args: ['lv_event_code_name_for(event).c_str()']
|
||||||
skip: true
|
skip: true
|
||||||
layout:
|
layout:
|
||||||
type: Flex
|
type: Flex
|
||||||
@@ -241,7 +258,7 @@ lvgl:
|
|||||||
on_all_events:
|
on_all_events:
|
||||||
- logger.log:
|
- logger.log:
|
||||||
format: "Event %s"
|
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:
|
- lvgl.animimg.update:
|
||||||
id: anim_img
|
id: anim_img
|
||||||
src: !lambda "return {dog_image, cat_image};"
|
src: !lambda "return {dog_image, cat_image};"
|
||||||
@@ -306,7 +323,6 @@ lvgl:
|
|||||||
anim_time: 1s
|
anim_time: 1s
|
||||||
bg_color: light_blue
|
bg_color: light_blue
|
||||||
bg_grad_color: light_blue
|
bg_grad_color: light_blue
|
||||||
bg_dither_mode: ordered
|
|
||||||
bg_grad_dir: hor
|
bg_grad_dir: hor
|
||||||
bg_grad_stop: 128
|
bg_grad_stop: 128
|
||||||
bg_image_opa: transp
|
bg_image_opa: transp
|
||||||
@@ -354,10 +370,18 @@ lvgl:
|
|||||||
text_line_space: 4
|
text_line_space: 4
|
||||||
text_opa: cover
|
text_opa: cover
|
||||||
transform_angle: 180
|
transform_angle: 180
|
||||||
|
transform_rotation: 90
|
||||||
transform_height: 100
|
transform_height: 100
|
||||||
transform_pivot_x: 50%
|
transform_pivot_x: 50%
|
||||||
transform_pivot_y: 50%
|
transform_pivot_y: 50%
|
||||||
transform_zoom: 0.5
|
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_x: 10
|
||||||
translate_y: 10
|
translate_y: 10
|
||||||
max_height: 100
|
max_height: 100
|
||||||
@@ -549,14 +573,16 @@ lvgl:
|
|||||||
arc_length: 120
|
arc_length: 120
|
||||||
spin_time: 2s
|
spin_time: 2s
|
||||||
align: left_mid
|
align: left_mid
|
||||||
|
- spinner:
|
||||||
|
align: right_mid
|
||||||
|
send_draw_task_events: true
|
||||||
- image:
|
- image:
|
||||||
id: lv_image
|
id: lv_image
|
||||||
src: cat_image
|
src: cat_image
|
||||||
align: top_left
|
align: top_left
|
||||||
y: "50"
|
y: "50"
|
||||||
mode: real
|
scale: 2.0
|
||||||
zoom: 2.0
|
rotation: 45
|
||||||
angle: 45
|
|
||||||
- tileview:
|
- tileview:
|
||||||
id: tileview_id
|
id: tileview_id
|
||||||
scrollbar_mode: active
|
scrollbar_mode: active
|
||||||
@@ -661,8 +687,11 @@ lvgl:
|
|||||||
src: cat_image
|
src: cat_image
|
||||||
x: 100
|
x: 100
|
||||||
y: 100
|
y: 100
|
||||||
angle: 90
|
rotation: 90
|
||||||
zoom: 2.0
|
scale_x: 2.0
|
||||||
|
scale_y: 1.5
|
||||||
|
skew_x: 10
|
||||||
|
skew_y: 5
|
||||||
pivot_x: 25
|
pivot_x: 25
|
||||||
pivot_y: 25
|
pivot_y: 25
|
||||||
- lvgl.canvas.draw_line:
|
- lvgl.canvas.draw_line:
|
||||||
@@ -710,6 +739,9 @@ lvgl:
|
|||||||
text:
|
text:
|
||||||
format: "A string with a number %d"
|
format: "A string with a number %d"
|
||||||
args: ['(int)(random_uint32() % 1000)']
|
args: ['(int)(random_uint32() % 1000)']
|
||||||
|
size: 120
|
||||||
|
dark_color: navy
|
||||||
|
light_color: white
|
||||||
|
|
||||||
- slider:
|
- slider:
|
||||||
min_value: 0
|
min_value: 0
|
||||||
@@ -928,7 +960,6 @@ lvgl:
|
|||||||
grid_cell_row_pos: 0
|
grid_cell_row_pos: 0
|
||||||
grid_cell_column_pos: 0
|
grid_cell_column_pos: 0
|
||||||
src: !lambda return dog_image;
|
src: !lambda return dog_image;
|
||||||
mode: virtual
|
|
||||||
on_click:
|
on_click:
|
||||||
then:
|
then:
|
||||||
- lvgl.tabview.select:
|
- lvgl.tabview.select:
|
||||||
@@ -1023,10 +1054,18 @@ lvgl:
|
|||||||
text_color: 0xFFFFFF
|
text_color: 0xFFFFFF
|
||||||
scales:
|
scales:
|
||||||
- ticks:
|
- ticks:
|
||||||
width: !lambda return 1;
|
width: 1
|
||||||
count: 61
|
count: 61
|
||||||
length: 20%
|
length: 20%
|
||||||
|
radial_offset: 5
|
||||||
color: 0xFFFFFF
|
color: 0xFFFFFF
|
||||||
|
major:
|
||||||
|
stride: 5
|
||||||
|
width: 2
|
||||||
|
length: 8
|
||||||
|
color: 0xC0C0C0
|
||||||
|
radial_offset: 3
|
||||||
|
label_gap: 6
|
||||||
range_from: 0
|
range_from: 0
|
||||||
range_to: 60
|
range_to: 60
|
||||||
angle_range: 360
|
angle_range: 360
|
||||||
@@ -1037,15 +1076,15 @@ lvgl:
|
|||||||
end_value: 60
|
end_value: 60
|
||||||
color_start: 0x0000bd
|
color_start: 0x0000bd
|
||||||
color_end: 0xbd0000
|
color_end: 0xbd0000
|
||||||
width: !lambda return 1;
|
width: 1
|
||||||
- line:
|
- line:
|
||||||
opa: 50%
|
opa: 50%
|
||||||
id: minute_hand
|
id: minute_hand
|
||||||
color: 0xFF0000
|
color: 0xFF0000
|
||||||
r_mod: !lambda return -1;
|
length: 99%
|
||||||
width: !lambda return 3;
|
radial_offset: 2
|
||||||
-
|
width: 1
|
||||||
angle_range: 330
|
- angle_range: 330
|
||||||
rotation: 300
|
rotation: 300
|
||||||
range_from: 1
|
range_from: 1
|
||||||
range_to: 12
|
range_to: 12
|
||||||
@@ -1069,7 +1108,7 @@ lvgl:
|
|||||||
value: 180
|
value: 180
|
||||||
width: 4
|
width: 4
|
||||||
color: 0xA0A0A0
|
color: 0xA0A0A0
|
||||||
r_mod: -20
|
length: 80%
|
||||||
opa: 0%
|
opa: 0%
|
||||||
- id: page3
|
- id: page3
|
||||||
layout: Horizontal
|
layout: Horizontal
|
||||||
|
|||||||
Reference in New Issue
Block a user