Skip to content

Commit

Permalink
[LVGL] Add color gradients (esphome#7427)
Browse files Browse the repository at this point in the history
  • Loading branch information
clydebarrow authored Sep 10, 2024
1 parent dcfad31 commit c8aed15
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 44 deletions.
22 changes: 7 additions & 15 deletions esphome/components/lvgl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@

from . import defines as df, helpers, lv_validation as lvalid
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 .gradient import GRADIENT_SCHEMA, gradients_to_code
from .lv_validation import lv_bool, lv_images_used
from .lvcode import LvContext, LvglComponent
from .schemas import (
Expand Down Expand Up @@ -128,17 +129,6 @@
)(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):
if value is None:
return f"#define {macro}"
Expand All @@ -153,14 +143,14 @@ def as_macro(macro, value):


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()
return LV_CONF_H_FORMAT.format("\n".join(definitions))


def final_validation(config):
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")
global_config = full_config.get()
for display_id in config[df.CONF_DISPLAYS]:
Expand All @@ -185,7 +175,7 @@ def final_validation(config):
for w in focused_widgets:
path = global_config.get_path_for_id(w)
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(
"A non adjustable arc may not be focused",
path,
Expand Down Expand Up @@ -268,6 +258,7 @@ async def to_code(config):
await encoders_to_code(lv_component, config)
await theme_to_code(config)
await styles_to_code(config)
await gradients_to_code(config)
await set_obj_properties(lv_scr_act, config)
await add_widgets(lv_scr_act, config)
await add_pages(lv_component, config)
Expand Down Expand Up @@ -351,6 +342,7 @@ def display_schema(config):
cv.Optional(df.CONF_THEME): cv.Schema(
{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_ENCODERS, default=None): ENCODERS_CONFIG,
cv.GenerateID(df.CONF_DEFAULT_GROUP): cv.declare_id(lv_group_t),
Expand Down
17 changes: 17 additions & 0 deletions esphome/components/lvgl/defines.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"""

import logging

from esphome import codegen as cg, config_validation as cv
from esphome.const import CONF_ITEMS
from esphome.core import Lambda
Expand All @@ -13,8 +15,19 @@

from .helpers import requires_component

LOGGER = logging.getLogger(__name__)
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):
if isinstance(arg, str):
Expand Down Expand Up @@ -173,6 +186,9 @@ def extend(self, *choices):
"OUT_BOTTOM",
)

LV_GRAD_DIR = LvConstant("LV_GRAD_DIR_", "NONE", "HOR", "VER")
LV_DITHER = LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF")

LOG_LEVELS = (
"TRACE",
"INFO",
Expand Down Expand Up @@ -406,6 +422,7 @@ def extend(self, *choices):
CONF_FLEX_GROW = "flex_grow"
CONF_FREEZE = "freeze"
CONF_FULL_REFRESH = "full_refresh"
CONF_GRADIENTS = "gradients"
CONF_GRID_CELL_ROW_POS = "grid_cell_row_pos"
CONF_GRID_CELL_COLUMN_POS = "grid_cell_column_pos"
CONF_GRID_CELL_ROW_SPAN = "grid_cell_row_span"
Expand Down
61 changes: 61 additions & 0 deletions esphome/components/lvgl/gradient.py
Original file line number Diff line number Diff line change
@@ -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)
57 changes: 40 additions & 17 deletions esphome/components/lvgl/lv_validation.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
from typing import Union

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.image import Image_
import esphome.config_validation as cv
from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT, CONF_TIME, CONF_VALUE
from esphome.core import HexInt, Lambda
from esphome.const import (
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_types import ESPTime, uint32
from esphome.helpers import cpp_string_escape
Expand All @@ -23,14 +30,9 @@
call_lambda,
literal,
)
from .helpers import (
esphome_fonts_used,
lv_fonts_used,
lvgl_components_required,
requires_component,
)
from .helpers import esphome_fonts_used, lv_fonts_used, requires_component
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")

Expand Down Expand Up @@ -59,11 +61,17 @@ def color_retmapper(value):
if isinstance(value, cv.Lambda):
return cv.returning_lambda(value)
if isinstance(value, int):
hexval = HexInt(value)
return lv_expr.color_hex(hexval)
# Must be an id
lvgl_components_required.add(CONF_COLOR)
return lv_expr.color_from(MockObj(value))
return literal(
f"lv_color_make({(value >> 16) & 0xFF}, {(value >> 8) & 0xFF}, {value & 0xFF})"
)
if isinstance(value, ID):
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):
Expand Down Expand Up @@ -132,7 +140,7 @@ def pixels_validator(value):


@schema_extractor("one_of")
def radius_validator(value):
def fraction_validator(value):
if value == SCHEMA_EXTRACT:
return radius_consts.choices
value = cv.Any(size, cv.percentage, radius_consts.one_of)(value)
Expand All @@ -141,7 +149,7 @@ def radius_validator(value):
return value


radius = LValidator(radius_validator, uint32, retmapper=literal)
lv_fraction = LValidator(fraction_validator, uint32, retmapper=literal)


def id_name(value):
Expand Down Expand Up @@ -242,6 +250,21 @@ async def process(self, value, args=()):
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):
return isinstance(font, str) and font.lower() in LV_FONTS

Expand Down
5 changes: 3 additions & 2 deletions esphome/components/lvgl/lvcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,9 @@ def __init__(self, lv_component, args=None):
self.lv_component = lv_component

async def add_init_lambda(self):
cg.add(self.lv_component.add_init_lambda(await self.get_lambda()))
LvContext.added_lambda_count += 1
if self.code_list:
cg.add(self.lv_component.add_init_lambda(await self.get_lambda()))
LvContext.added_lambda_count += 1

async def __aexit__(self, exc_type, exc_val, exc_tb):
await super().__aexit__(exc_type, exc_val, exc_tb)
Expand Down
3 changes: 0 additions & 3 deletions esphome/components/lvgl/lvgl_esphome.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,6 @@ extern lv_event_code_t lv_api_event; // NOLINT
extern lv_event_code_t lv_update_event; // NOLINT
extern std::string lv_event_code_name_for(uint8_t event_code);
extern 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
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_565;
#elif LV_COLOR_DEPTH == 32
Expand Down
9 changes: 5 additions & 4 deletions esphome/components/lvgl/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
from esphome.schema_extractors import SCHEMA_EXTRACT

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 .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 .types import (
LVEncoderListener,
Expand Down Expand Up @@ -94,9 +94,10 @@
"arc_width": cv.positive_int,
"anim_time": lvalid.lv_milliseconds,
"bg_color": lvalid.lv_color,
"bg_grad": lv_gradient,
"bg_grad_color": lvalid.lv_color,
"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_image_opa": lvalid.opacity,
"bg_image_recolor": lvalid.lv_color,
Expand Down Expand Up @@ -160,7 +161,7 @@
"max_width": lvalid.pixels_or_percent,
"min_height": lvalid.pixels_or_percent,
"min_width": lvalid.pixels_or_percent,
"radius": lvalid.radius,
"radius": lvalid.lv_fraction,
"width": lvalid.size,
"x": lvalid.pixels_or_percent,
"y": lvalid.pixels_or_percent,
Expand Down
1 change: 1 addition & 0 deletions esphome/components/lvgl/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def __init__(self, *args):
lv_obj_t = LvType("lv_obj_t")
lv_page_t = LvType("LvPageType", parents=(LvCompound,))
lv_img_t = LvType("lv_img_t")
lv_gradient_t = LvType("lv_grad_dsc_t")

LV_EVENT = MockObj(base="LV_EVENT_", op="")
LV_STATE = MockObj(base="LV_STATE_", op="")
Expand Down
9 changes: 8 additions & 1 deletion esphome/components/lvgl/widgets/meter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
CONF_COLOR,
CONF_COUNT,
CONF_ID,
CONF_ITEMS,
CONF_LENGTH,
CONF_LOCAL,
CONF_RANGE_FROM,
Expand All @@ -17,6 +18,7 @@
from ..automation import action_to_code
from ..defines import (
CONF_END_VALUE,
CONF_INDICATOR,
CONF_MAIN,
CONF_PIVOT_X,
CONF_PIVOT_Y,
Expand Down Expand Up @@ -165,7 +167,12 @@ def pixels(value):

class MeterType(WidgetType):
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):
"""For a meter object, create and set parameters"""
Expand Down
Loading

0 comments on commit c8aed15

Please sign in to comment.