[LVGL] Add color gradients (#7427)
Some checks are pending
CI for docker images / Build docker containers (aarch64, docker) (push) Waiting to run
CI for docker images / Build docker containers (aarch64, ha-addon) (push) Waiting to run
CI for docker images / Build docker containers (aarch64, lint) (push) Waiting to run
CI for docker images / Build docker containers (amd64, docker) (push) Waiting to run
CI for docker images / Build docker containers (amd64, ha-addon) (push) Waiting to run
CI for docker images / Build docker containers (amd64, lint) (push) Waiting to run
CI for docker images / Build docker containers (armv7, docker) (push) Waiting to run
CI for docker images / Build docker containers (armv7, ha-addon) (push) Waiting to run
CI for docker images / Build docker containers (armv7, lint) (push) Waiting to run
CI / Check pylint (push) Blocked by required conditions
CI / Create common environment (push) Waiting to run
CI / Check black (push) Blocked by required conditions
CI / Check flake8 (push) Blocked by required conditions
CI / Check pyupgrade (push) Blocked by required conditions
CI / Run script/ci-custom (push) Blocked by required conditions
CI / Run pytest (macOS-latest, 3.11) (push) Blocked by required conditions
CI / Run pytest (ubuntu-latest, 3.10) (push) Blocked by required conditions
CI / Check clang-format (push) Blocked by required conditions
CI / Run pytest (ubuntu-latest, 3.11) (push) Blocked by required conditions
CI / Run pytest (ubuntu-latest, 3.12) (push) Blocked by required conditions
CI / Run pytest (ubuntu-latest, 3.9) (push) Blocked by required conditions
CI / Run pytest (windows-latest, 3.11) (push) Blocked by required conditions
CI / Run script/clang-tidy for ESP32 Arduino 1/4 (push) Blocked by required conditions
CI / Run script/clang-tidy for ESP32 Arduino 2/4 (push) Blocked by required conditions
CI / Run script/clang-tidy for ESP32 Arduino 3/4 (push) Blocked by required conditions
CI / Run script/clang-tidy for ESP32 Arduino 4/4 (push) Blocked by required conditions
CI / Run script/clang-tidy for ESP32 IDF (push) Blocked by required conditions
CI / Run script/clang-tidy for ESP8266 (push) Blocked by required conditions
CI / list-components (push) Blocked by required conditions
CI / Component test ${{ matrix.file }} (push) Blocked by required conditions
CI / Split components for testing into 20 groups maximum (push) Blocked by required conditions
CI / Test split components (push) Blocked by required conditions
CI / CI Status (push) Blocked by required conditions
YAML lint / yamllint (push) Waiting to run

This commit is contained in:
Clyde Stubbs 2024-09-10 11:24:18 +10:00 committed by GitHub
parent dcfad31770
commit c8aed15157
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 190 additions and 44 deletions

View file

@ -22,8 +22,9 @@ from esphome.helpers import write_file_if_changed
from . import defines as df, helpers, lv_validation as lvalid from . import defines as df, helpers, lv_validation as lvalid
from .automation import disp_update, focused_widgets, update_to_code from .automation import disp_update, focused_widgets, update_to_code
from .defines import CONF_ADJUSTABLE, CONF_SKIP from .defines import add_define
from .encoders import ENCODERS_CONFIG, encoders_to_code, initial_focus_to_code from .encoders import ENCODERS_CONFIG, encoders_to_code, initial_focus_to_code
from .gradient import GRADIENT_SCHEMA, gradients_to_code
from .lv_validation import lv_bool, lv_images_used from .lv_validation import lv_bool, lv_images_used
from .lvcode import LvContext, LvglComponent from .lvcode import LvContext, LvglComponent
from .schemas import ( from .schemas import (
@ -128,17 +129,6 @@ for w_type in WIDGET_TYPES.values():
)(update_to_code) )(update_to_code)
lv_defines = {} # Dict of #defines to provide as build flags
def add_define(macro, value="1"):
if macro in lv_defines and lv_defines[macro] != value:
LOGGER.error(
"Redefinition of %s - was %s now %s", macro, lv_defines[macro], value
)
lv_defines[macro] = value
def as_macro(macro, value): def as_macro(macro, value):
if value is None: if value is None:
return f"#define {macro}" return f"#define {macro}"
@ -153,14 +143,14 @@ LV_CONF_H_FORMAT = """\
def generate_lv_conf_h(): def generate_lv_conf_h():
definitions = [as_macro(m, v) for m, v in lv_defines.items()] definitions = [as_macro(m, v) for m, v in df.lv_defines.items()]
definitions.sort() definitions.sort()
return LV_CONF_H_FORMAT.format("\n".join(definitions)) return LV_CONF_H_FORMAT.format("\n".join(definitions))
def final_validation(config): def final_validation(config):
if pages := config.get(CONF_PAGES): if pages := config.get(CONF_PAGES):
if all(p[CONF_SKIP] for p in pages): if all(p[df.CONF_SKIP] for p in pages):
raise cv.Invalid("At least one page must not be skipped") raise cv.Invalid("At least one page must not be skipped")
global_config = full_config.get() global_config = full_config.get()
for display_id in config[df.CONF_DISPLAYS]: for display_id in config[df.CONF_DISPLAYS]:
@ -185,7 +175,7 @@ def final_validation(config):
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])
if CONF_ADJUSTABLE in widget_conf and not widget_conf[CONF_ADJUSTABLE]: if df.CONF_ADJUSTABLE in widget_conf and not widget_conf[df.CONF_ADJUSTABLE]:
raise cv.Invalid( raise cv.Invalid(
"A non adjustable arc may not be focused", "A non adjustable arc may not be focused",
path, path,
@ -268,6 +258,7 @@ async def to_code(config):
await encoders_to_code(lv_component, config) await encoders_to_code(lv_component, config)
await theme_to_code(config) await theme_to_code(config)
await styles_to_code(config) await styles_to_code(config)
await gradients_to_code(config)
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)
@ -351,6 +342,7 @@ CONFIG_SCHEMA = (
cv.Optional(df.CONF_THEME): cv.Schema( cv.Optional(df.CONF_THEME): cv.Schema(
{cv.Optional(name): obj_schema(w) for name, w in WIDGET_TYPES.items()} {cv.Optional(name): obj_schema(w) for name, w in WIDGET_TYPES.items()}
), ),
cv.Optional(df.CONF_GRADIENTS): GRADIENT_SCHEMA,
cv.Optional(df.CONF_TOUCHSCREENS, default=None): touchscreen_schema, cv.Optional(df.CONF_TOUCHSCREENS, default=None): touchscreen_schema,
cv.Optional(df.CONF_ENCODERS, default=None): ENCODERS_CONFIG, cv.Optional(df.CONF_ENCODERS, default=None): ENCODERS_CONFIG,
cv.GenerateID(df.CONF_DEFAULT_GROUP): cv.declare_id(lv_group_t), cv.GenerateID(df.CONF_DEFAULT_GROUP): cv.declare_id(lv_group_t),

View file

@ -4,6 +4,8 @@ Constants already defined in esphome.const are not duplicated here and must be i
""" """
import logging
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 Lambda from esphome.core import Lambda
@ -13,8 +15,19 @@ from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from .helpers import requires_component from .helpers import requires_component
LOGGER = logging.getLogger(__name__)
lvgl_ns = cg.esphome_ns.namespace("lvgl") lvgl_ns = cg.esphome_ns.namespace("lvgl")
lv_defines = {} # Dict of #defines to provide as build flags
def add_define(macro, value="1"):
if macro in lv_defines and lv_defines[macro] != value:
LOGGER.error(
"Redefinition of %s - was %s now %s", macro, lv_defines[macro], value
)
lv_defines[macro] = value
def literal(arg): def literal(arg):
if isinstance(arg, str): if isinstance(arg, str):
@ -173,6 +186,9 @@ LV_ANIM = LvConstant(
"OUT_BOTTOM", "OUT_BOTTOM",
) )
LV_GRAD_DIR = LvConstant("LV_GRAD_DIR_", "NONE", "HOR", "VER")
LV_DITHER = LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF")
LOG_LEVELS = ( LOG_LEVELS = (
"TRACE", "TRACE",
"INFO", "INFO",
@ -406,6 +422,7 @@ CONF_FLEX_ALIGN_TRACK = "flex_align_track"
CONF_FLEX_GROW = "flex_grow" CONF_FLEX_GROW = "flex_grow"
CONF_FREEZE = "freeze" CONF_FREEZE = "freeze"
CONF_FULL_REFRESH = "full_refresh" CONF_FULL_REFRESH = "full_refresh"
CONF_GRADIENTS = "gradients"
CONF_GRID_CELL_ROW_POS = "grid_cell_row_pos" CONF_GRID_CELL_ROW_POS = "grid_cell_row_pos"
CONF_GRID_CELL_COLUMN_POS = "grid_cell_column_pos" CONF_GRID_CELL_COLUMN_POS = "grid_cell_column_pos"
CONF_GRID_CELL_ROW_SPAN = "grid_cell_row_span" CONF_GRID_CELL_ROW_SPAN = "grid_cell_row_span"

View file

@ -0,0 +1,61 @@
from esphome import config_validation as cv
import esphome.codegen as cg
from esphome.const import (
CONF_COLOR,
CONF_DIRECTION,
CONF_DITHER,
CONF_ID,
CONF_POSITION,
)
from esphome.cpp_generator import MockObj
from .defines import CONF_GRADIENTS, LV_DITHER, LV_GRAD_DIR, add_define
from .lv_validation import lv_color, lv_fraction
from .lvcode import lv_assign
from .types import lv_gradient_t
CONF_STOPS = "stops"
def min_stops(value):
if len(value) < 2:
raise cv.Invalid("Must have at least 2 stops")
return value
GRADIENT_SCHEMA = cv.ensure_list(
cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(lv_gradient_t),
cv.Optional(CONF_DIRECTION, default="NONE"): LV_GRAD_DIR.one_of,
cv.Optional(CONF_DITHER, default="NONE"): LV_DITHER.one_of,
cv.Required(CONF_STOPS): cv.All(
[
cv.Schema(
{
cv.Required(CONF_COLOR): lv_color,
cv.Required(CONF_POSITION): lv_fraction,
}
)
],
min_stops,
),
}
)
)
async def gradients_to_code(config):
max_stops = 2
for gradient in config.get(CONF_GRADIENTS, ()):
var = MockObj(cg.new_Pvariable(gradient[CONF_ID]), "->")
max_stops = max(max_stops, len(gradient[CONF_STOPS]))
lv_assign(var.dir, await LV_GRAD_DIR.process(gradient[CONF_DIRECTION]))
lv_assign(var.dither, await LV_DITHER.process(gradient[CONF_DITHER]))
lv_assign(var.stops_count, len(gradient[CONF_STOPS]))
for index, stop in enumerate(gradient[CONF_STOPS]):
lv_assign(var.stops[index].color, await lv_color.process(stop[CONF_COLOR]))
lv_assign(
var.stops[index].frac, await lv_fraction.process(stop[CONF_POSITION])
)
add_define("LV_GRADIENT_MAX_STOPS", max_stops)

View file

@ -1,12 +1,19 @@
from typing import Union from typing import Union
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.color import ColorStruct from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw
from esphome.components.font import Font from esphome.components.font import Font
from esphome.components.image import Image_ from esphome.components.image import Image_
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT, CONF_TIME, CONF_VALUE from esphome.const import (
from esphome.core import HexInt, Lambda CONF_ARGS,
CONF_COLOR,
CONF_FORMAT,
CONF_ID,
CONF_TIME,
CONF_VALUE,
)
from esphome.core import CORE, ID, Lambda
from esphome.cpp_generator import MockObj from esphome.cpp_generator import MockObj
from esphome.cpp_types import ESPTime, uint32 from esphome.cpp_types import ESPTime, uint32
from esphome.helpers import cpp_string_escape from esphome.helpers import cpp_string_escape
@ -23,14 +30,9 @@ from .defines import (
call_lambda, call_lambda,
literal, literal,
) )
from .helpers import ( from .helpers import esphome_fonts_used, lv_fonts_used, requires_component
esphome_fonts_used,
lv_fonts_used,
lvgl_components_required,
requires_component,
)
from .lvcode import lv_expr from .lvcode import lv_expr
from .types import lv_font_t, lv_img_t from .types import lv_font_t, lv_gradient_t, lv_img_t
opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER") opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER")
@ -59,11 +61,17 @@ def color_retmapper(value):
if isinstance(value, cv.Lambda): if isinstance(value, cv.Lambda):
return cv.returning_lambda(value) return cv.returning_lambda(value)
if isinstance(value, int): if isinstance(value, int):
hexval = HexInt(value) return literal(
return lv_expr.color_hex(hexval) f"lv_color_make({(value >> 16) & 0xFF}, {(value >> 8) & 0xFF}, {value & 0xFF})"
# Must be an id )
lvgl_components_required.add(CONF_COLOR) if isinstance(value, ID):
return lv_expr.color_from(MockObj(value)) cval = [x for x in CORE.config[CONF_COLOR] if x[CONF_ID] == value][0]
if CONF_HEX in cval:
r, g, b = cval[CONF_HEX]
else:
r, g, b, _ = from_rgbw(cval)
return literal(f"lv_color_make({r}, {g}, {b})")
assert False
def option_string(value): def option_string(value):
@ -132,7 +140,7 @@ radius_consts = LvConstant("LV_RADIUS_", "CIRCLE")
@schema_extractor("one_of") @schema_extractor("one_of")
def radius_validator(value): def fraction_validator(value):
if value == SCHEMA_EXTRACT: if value == SCHEMA_EXTRACT:
return radius_consts.choices return radius_consts.choices
value = cv.Any(size, cv.percentage, radius_consts.one_of)(value) value = cv.Any(size, cv.percentage, radius_consts.one_of)(value)
@ -141,7 +149,7 @@ def radius_validator(value):
return value return value
radius = LValidator(radius_validator, uint32, retmapper=literal) lv_fraction = LValidator(fraction_validator, uint32, retmapper=literal)
def id_name(value): def id_name(value):
@ -242,6 +250,21 @@ lv_int = LValidator(cv.int_, cg.int_)
lv_brightness = LValidator(cv.percentage, cg.float_, retmapper=lambda x: int(x * 255)) lv_brightness = LValidator(cv.percentage, cg.float_, retmapper=lambda x: int(x * 255))
def gradient_mapper(value):
return MockObj(value)
def gradient_validator(value):
return cv.use_id(lv_gradient_t)(value)
lv_gradient = LValidator(
validator=gradient_validator,
rtype=lv_gradient_t,
retmapper=gradient_mapper,
)
def is_lv_font(font): def is_lv_font(font):
return isinstance(font, str) and font.lower() in LV_FONTS return isinstance(font, str) and font.lower() in LV_FONTS

View file

@ -184,6 +184,7 @@ class LvContext(LambdaContext):
self.lv_component = lv_component self.lv_component = lv_component
async def add_init_lambda(self): async def add_init_lambda(self):
if self.code_list:
cg.add(self.lv_component.add_init_lambda(await self.get_lambda())) cg.add(self.lv_component.add_init_lambda(await self.get_lambda()))
LvContext.added_lambda_count += 1 LvContext.added_lambda_count += 1

View file

@ -42,9 +42,6 @@ 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(uint8_t event_code);
extern bool lv_is_pre_initialise(); extern bool lv_is_pre_initialise();
#ifdef USE_LVGL_COLOR
inline lv_color_t lv_color_from(Color color) { return lv_color_make(color.red, color.green, color.blue); }
#endif // USE_LVGL_COLOR
#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

View file

@ -17,9 +17,9 @@ from esphome.core import TimePeriod
from esphome.schema_extractors import SCHEMA_EXTRACT from esphome.schema_extractors import SCHEMA_EXTRACT
from . import defines as df, lv_validation as lvalid from . import defines as df, lv_validation as lvalid
from .defines import CONF_TIME_FORMAT from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR
from .helpers import add_lv_use, requires_component, validate_printf from .helpers import add_lv_use, requires_component, validate_printf
from .lv_validation import lv_color, lv_font, lv_image from .lv_validation import lv_color, lv_font, lv_gradient, lv_image
from .lvcode import LvglComponent, lv_event_t_ptr from .lvcode import LvglComponent, lv_event_t_ptr
from .types import ( from .types import (
LVEncoderListener, LVEncoderListener,
@ -94,9 +94,10 @@ STYLE_PROPS = {
"arc_width": cv.positive_int, "arc_width": cv.positive_int,
"anim_time": lvalid.lv_milliseconds, "anim_time": lvalid.lv_milliseconds,
"bg_color": lvalid.lv_color, "bg_color": lvalid.lv_color,
"bg_grad": lv_gradient,
"bg_grad_color": lvalid.lv_color, "bg_grad_color": lvalid.lv_color,
"bg_dither_mode": df.LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF").one_of, "bg_dither_mode": df.LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF").one_of,
"bg_grad_dir": df.LvConstant("LV_GRAD_DIR_", "NONE", "HOR", "VER").one_of, "bg_grad_dir": LV_GRAD_DIR.one_of,
"bg_grad_stop": lvalid.stop_value, "bg_grad_stop": lvalid.stop_value,
"bg_image_opa": lvalid.opacity, "bg_image_opa": lvalid.opacity,
"bg_image_recolor": lvalid.lv_color, "bg_image_recolor": lvalid.lv_color,
@ -160,7 +161,7 @@ STYLE_PROPS = {
"max_width": lvalid.pixels_or_percent, "max_width": lvalid.pixels_or_percent,
"min_height": lvalid.pixels_or_percent, "min_height": lvalid.pixels_or_percent,
"min_width": lvalid.pixels_or_percent, "min_width": lvalid.pixels_or_percent,
"radius": lvalid.radius, "radius": lvalid.lv_fraction,
"width": lvalid.size, "width": lvalid.size,
"x": lvalid.pixels_or_percent, "x": lvalid.pixels_or_percent,
"y": lvalid.pixels_or_percent, "y": lvalid.pixels_or_percent,

View file

@ -59,6 +59,7 @@ LVEncoderListener = lvgl_ns.class_("LVEncoderListener")
lv_obj_t = LvType("lv_obj_t") 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_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="")

View file

@ -5,6 +5,7 @@ from esphome.const import (
CONF_COLOR, CONF_COLOR,
CONF_COUNT, CONF_COUNT,
CONF_ID, CONF_ID,
CONF_ITEMS,
CONF_LENGTH, CONF_LENGTH,
CONF_LOCAL, CONF_LOCAL,
CONF_RANGE_FROM, CONF_RANGE_FROM,
@ -17,6 +18,7 @@ from esphome.const import (
from ..automation import action_to_code from ..automation import action_to_code
from ..defines import ( from ..defines import (
CONF_END_VALUE, CONF_END_VALUE,
CONF_INDICATOR,
CONF_MAIN, CONF_MAIN,
CONF_PIVOT_X, CONF_PIVOT_X,
CONF_PIVOT_Y, CONF_PIVOT_Y,
@ -165,7 +167,12 @@ METER_SCHEMA = {cv.Optional(CONF_SCALES): cv.ensure_list(SCALE_SCHEMA)}
class MeterType(WidgetType): class MeterType(WidgetType):
def __init__(self): def __init__(self):
super().__init__(CONF_METER, lv_meter_t, (CONF_MAIN,), METER_SCHEMA) super().__init__(
CONF_METER,
lv_meter_t,
(CONF_MAIN, CONF_INDICATOR, CONF_TICKS, CONF_ITEMS),
METER_SCHEMA,
)
async def to_code(self, w: Widget, config): async def to_code(self, w: Widget, config):
"""For a meter object, create and set parameters""" """For a meter object, create and set parameters"""

View file

@ -1,12 +1,32 @@
lvgl: lvgl:
log_level: TRACE log_level: TRACE
bg_color: light_blue bg_color: light_blue
disp_bg_color: 0xffff00 disp_bg_color: color_id
disp_bg_image: cat_image disp_bg_image: cat_image
theme: theme:
obj: obj:
border_width: 1 border_width: 1
gradients:
- id: color_bar
direction: hor
dither: err_diff
stops:
- color: 0xFF0000
position: 0
- color: 0xFFFF00
position: 42
- color: 0x00FF00
position: 84
- color: 0x00FFFF
position: 127
- color: 0x0000FF
position: 169
- color: 0xFF00FF
position: 212
- color: 0xFF0000
position: 255
style_definitions: style_definitions:
- id: style_test - id: style_test
bg_color: 0x2F8CD8 bg_color: 0x2F8CD8
@ -31,7 +51,7 @@ lvgl:
- id: date_style - id: date_style
text_font: roboto10 text_font: roboto10
align: center align: center
text_color: 0x000000 text_color: color_id2
bg_opa: cover bg_opa: cover
radius: 4 radius: 4
pad_all: 2 pad_all: 2
@ -386,6 +406,22 @@ lvgl:
- id: page2 - id: page2
widgets: widgets:
- slider:
min_value: 0
max_value: 255
bg_opa: cover
bg_grad: color_bar
radius: 0
indicator:
bg_opa: transp
knob:
radius: 1
width: 4
height: 10%
bg_color: 0x000000
width: 100%
height: 10%
align: top_mid
- button: - button:
styles: spin_button styles: spin_button
id: spin_up id: spin_up
@ -586,3 +622,13 @@ image:
color: color:
- id: light_blue - id: light_blue
hex: "3340FF" hex: "3340FF"
- id: color_id
red: 0.5
green: 0.5
blue: 0.5
white: 0.5
- id: color_id2
red_int: 0xFF
green_int: 123
blue_int: 64
white_int: 255