LVGL stage 2 (#7129)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
Clyde Stubbs
2024-07-30 09:41:34 +10:00
committed by GitHub
parent 12e840ee88
commit 7c1aa771aa
18 changed files with 503 additions and 183 deletions
+23 -15
View File
@@ -16,13 +16,20 @@ from esphome.final_validate import full_config
from esphome.helpers import write_file_if_changed
from . import defines as df, helpers, lv_validation as lvalid
from .btn import btn_spec
from .label import label_spec
from .lvcode import ConstantLiteral, LvContext
# from .menu import menu_spec
from .obj import obj_spec
from .schemas import WIDGET_TYPES, any_widget_schema, obj_schema
from .types import FontEngine, LvglComponent, lv_disp_t_ptr, lv_font_t, lvgl_ns
from .schemas import any_widget_schema, obj_schema
from .touchscreens import touchscreen_schema, touchscreens_to_code
from .types import (
WIDGET_TYPES,
FontEngine,
LvglComponent,
lv_disp_t_ptr,
lv_font_t,
lvgl_ns,
)
from .widget import LvScrActType, Widget, add_widgets, set_obj_properties
DOMAIN = "lvgl"
@@ -31,11 +38,8 @@ AUTO_LOAD = ("key_provider",)
CODEOWNERS = ("@clydebarrow",)
LOGGER = logging.getLogger(__name__)
for widg in (
label_spec,
obj_spec,
):
WIDGET_TYPES[widg.name] = widg
for w_type in (label_spec, obj_spec, btn_spec):
WIDGET_TYPES[w_type.name] = w_type
lv_scr_act_spec = LvScrActType()
lv_scr_act = Widget.create(
@@ -93,7 +97,7 @@ def final_validation(config):
"Using auto_clear_enabled: true in display config not compatible with LVGL"
)
buffer_frac = config[CONF_BUFFER_SIZE]
if not CORE.is_host and buffer_frac > 0.5 and "psram" not in global_config:
if CORE.is_esp32 and buffer_frac > 0.5 and "psram" not in global_config:
LOGGER.warning("buffer_size: may need to be reduced without PSRAM")
@@ -132,7 +136,7 @@ async def to_code(config):
cg.add_global(lvgl_ns.using)
lv_component = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(lv_component, config)
Widget.create(config[CONF_ID], lv_component, WIDGET_TYPES[df.CONF_OBJ], config)
Widget.create(config[CONF_ID], lv_component, obj_spec, config)
for display in config[df.CONF_DISPLAYS]:
cg.add(lv_component.add_display(await cg.get_variable(display)))
@@ -152,7 +156,7 @@ async def to_code(config):
await cg.get_variable(font)
cg.new_Pvariable(ID(f"{font}_engine", True, type=FontEngine), MockObj(font))
default_font = config[df.CONF_DEFAULT_FONT]
if default_font not in helpers.lv_fonts_used:
if not lvalid.is_lv_font(default_font):
add_define(
"LV_FONT_CUSTOM_DECLARE", f"LV_FONT_DECLARE(*{df.DEFAULT_ESPHOME_FONT})"
)
@@ -161,12 +165,15 @@ async def to_code(config):
True,
type=lv_font_t.operator("ptr").operator("const"),
)
cg.new_variable(globfont_id, MockObj(default_font))
cg.new_variable(
globfont_id, MockObj(await lvalid.lv_font.process(default_font))
)
add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT)
else:
add_define("LV_FONT_DEFAULT", default_font)
add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font))
with LvContext():
await touchscreens_to_code(lv_component, config)
await set_obj_properties(lv_scr_act, config)
await add_widgets(lv_scr_act, config)
Widget.set_completed()
@@ -190,7 +197,7 @@ FINAL_VALIDATE_SCHEMA = final_validation
CONFIG_SCHEMA = (
cv.polling_component_schema("1s")
.extend(obj_schema("obj"))
.extend(obj_schema(obj_spec))
.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(LvglComponent),
@@ -207,6 +214,7 @@ CONFIG_SCHEMA = (
),
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(WIDGET_SCHEMA),
cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color,
cv.GenerateID(df.CONF_TOUCHSCREENS): touchscreen_schema,
}
)
).add_extra(cv.has_at_least_one_key(CONF_PAGES, df.CONF_WIDGETS))
+25
View File
@@ -0,0 +1,25 @@
from esphome.const import CONF_BUTTON
from esphome.cpp_generator import MockObjClass
from .defines import CONF_MAIN
from .types import LvBoolean, WidgetType
class BtnType(WidgetType):
def __init__(self):
super().__init__(CONF_BUTTON, LvBoolean("lv_btn_t"), (CONF_MAIN,))
async def to_code(self, w, config):
return []
def obj_creator(self, parent: MockObjClass, config: dict):
"""
LVGL 8 calls buttons `btn`
"""
return f"lv_btn_create({parent})"
def get_uses(self):
return ("btn",)
btn_spec = BtnType()
+2 -7
View File
@@ -446,6 +446,7 @@ CONF_TILE_ID = "tile_id"
CONF_TILES = "tiles"
CONF_TITLE = "title"
CONF_TOP_LAYER = "top_layer"
CONF_TOUCHSCREENS = "touchscreens"
CONF_TRANSPARENCY_KEY = "transparency_key"
CONF_THEME = "theme"
CONF_VISIBLE_ROW_COUNT = "visible_row_count"
@@ -474,14 +475,8 @@ LV_KEYS = LvConstant(
)
# list of widgets and the parts allowed
WIDGET_PARTS = {
CONF_LABEL: (CONF_MAIN, CONF_SCROLLBAR, CONF_SELECTED),
CONF_OBJ: (CONF_MAIN,),
}
DEFAULT_ESPHOME_FONT = "esphome_lv_default_font"
def join_enums(enums, prefix=""):
return "|".join(f"(int){prefix}{e.upper()}" for e in enums)
return ConstantLiteral("|".join(f"(int){prefix}{e.upper()}" for e in enums))
-1
View File
@@ -22,7 +22,6 @@ def add_lv_use(*names):
lv_fonts_used = set()
esphome_fonts_used = set()
REQUIRED_COMPONENTS = {}
lvgl_components_required = set()
+14 -7
View File
@@ -1,16 +1,27 @@
import esphome.config_validation as cv
from .defines import CONF_LABEL, CONF_LONG_MODE, CONF_RECOLOR, CONF_TEXT, LV_LONG_MODES
from .defines import (
CONF_LABEL,
CONF_LONG_MODE,
CONF_MAIN,
CONF_RECOLOR,
CONF_SCROLLBAR,
CONF_SELECTED,
CONF_TEXT,
LV_LONG_MODES,
)
from .lv_validation import lv_bool, lv_text
from .schemas import TEXT_SCHEMA
from .types import lv_label_t
from .widget import Widget, WidgetType
from .types import LvText, WidgetType
from .widget import Widget
class LabelType(WidgetType):
def __init__(self):
super().__init__(
CONF_LABEL,
LvText("lv_label_t"),
(CONF_MAIN, CONF_SCROLLBAR, CONF_SELECTED),
TEXT_SCHEMA.extend(
{
cv.Optional(CONF_RECOLOR): lv_bool,
@@ -19,10 +30,6 @@ class LabelType(WidgetType):
),
)
@property
def w_type(self):
return lv_label_t
async def to_code(self, w: Widget, config):
"""For a text object, create and set text"""
if value := config.get(CONF_TEXT):
+65 -14
View File
@@ -8,6 +8,7 @@ import esphome.config_validation as cv
from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT
from esphome.core import HexInt
from esphome.cpp_generator import MockObj
from esphome.cpp_types import uint32
from esphome.helpers import cpp_string_escape
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
@@ -23,6 +24,28 @@ from .lvcode import ConstantLiteral, lv_expr
from .types import lv_font_t
def literal_mapper(value, args=()):
if isinstance(value, str):
return ConstantLiteral(value)
return value
opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER")
@schema_extractor("one_of")
def opacity_validator(value):
if value == SCHEMA_EXTRACT:
return opacity_consts.choices
value = cv.Any(cv.percentage, opacity_consts.one_of)(value)
if isinstance(value, float):
return int(value * 255)
return value
opacity = LValidator(opacity_validator, uint32, retmapper=literal_mapper)
@schema_extractor("one_of")
def color(value):
if value == SCHEMA_EXTRACT:
@@ -43,16 +66,24 @@ def color_retmapper(value):
return lv_expr.color_from(MockObj(value))
def pixels_or_percent(value):
lv_color = LValidator(color, ty.lv_color_t, retmapper=color_retmapper)
def pixels_or_percent_validator(value):
"""A length in one axis - either a number (pixels) or a percentage"""
if value == SCHEMA_EXTRACT:
return ["pixels", "..%"]
if isinstance(value, int):
return str(cv.int_(value))
return cv.int_(value)
# Will throw an exception if not a percentage.
return f"lv_pct({int(cv.percentage(value) * 100)})"
pixels_or_percent = LValidator(
pixels_or_percent_validator, uint32, retmapper=literal_mapper
)
def zoom(value):
value = cv.float_range(0.1, 10.0)(value)
return int(value * 256)
@@ -68,7 +99,7 @@ def angle(value):
@schema_extractor("one_of")
def size(value):
def size_validator(value):
"""A size in one axis - one of "size_content", a number (pixels) or a percentage"""
if value == SCHEMA_EXTRACT:
return ["size_content", "pixels", "..%"]
@@ -79,28 +110,42 @@ def size(value):
return "LV_SIZE_CONTENT"
raise cv.Invalid("must be 'size_content', a pixel position or a percentage")
if isinstance(value, int):
return str(cv.int_(value))
return cv.int_(value)
# Will throw an exception if not a percentage.
return f"lv_pct({int(cv.percentage(value) * 100)})"
size = LValidator(size_validator, uint32, retmapper=literal_mapper)
radius_consts = LvConstant("LV_RADIUS_", "CIRCLE")
@schema_extractor("one_of")
def opacity(value):
consts = LvConstant("LV_OPA_", "TRANSP", "COVER")
def radius_validator(value):
if value == SCHEMA_EXTRACT:
return consts.choices
value = cv.Any(cv.percentage, consts.one_of)(value)
return radius_consts.choices
value = cv.Any(size, cv.percentage, radius_consts.one_of)(value)
if isinstance(value, float):
return int(value * 255)
return value
def id_name(value):
if value == SCHEMA_EXTRACT:
return "id"
return cv.validate_id_name(value)
radius = LValidator(radius_validator, uint32, retmapper=literal_mapper)
def stop_value(value):
return cv.int_range(0, 255)(value)
lv_color = LValidator(color, ty.lv_color_t, retmapper=color_retmapper)
lv_bool = LValidator(cv.boolean, cg.bool_, BinarySensor, "get_state()")
lv_bool = LValidator(
cv.boolean, cg.bool_, BinarySensor, "get_state()", retmapper=literal_mapper
)
def lvms_validator_(value):
@@ -145,26 +190,32 @@ lv_float = LValidator(cv.float_, cg.float_, Sensor, "get_state()")
lv_int = LValidator(cv.int_, cg.int_, Sensor, "get_state()")
def is_lv_font(font):
return isinstance(font, str) and font.lower() in LV_FONTS
class LvFont(LValidator):
def __init__(self):
def lv_builtin_font(value):
fontval = cv.one_of(*LV_FONTS, lower=True)(value)
lv_fonts_used.add(fontval)
return "&lv_font_" + fontval
return fontval
def validator(value):
if value == SCHEMA_EXTRACT:
return LV_FONTS
if isinstance(value, str) and value.lower() in LV_FONTS:
if is_lv_font(value):
return lv_builtin_font(value)
fontval = cv.use_id(Font)(value)
esphome_fonts_used.add(fontval)
return requires_component("font")(f"{fontval}_engine->get_lv_font()")
return requires_component("font")(fontval)
super().__init__(validator, lv_font_t)
async def process(self, value, args=()):
return ConstantLiteral(value)
if is_lv_font(value):
return ConstantLiteral(f"&lv_font_{value}")
return ConstantLiteral(f"{value}_engine->get_lv_font()")
lv_font = LvFont()
+6
View File
@@ -38,7 +38,9 @@ void LvglComponent::setup() {
auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8;
auto *buf = lv_custom_mem_alloc(buf_bytes);
if (buf == nullptr) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
ESP_LOGE(TAG, "Malloc failed to allocate %zu bytes", buf_bytes);
#endif
this->mark_failed();
this->status_set_error("Memory allocation failure");
return;
@@ -85,7 +87,9 @@ size_t lv_millis(void) { return esphome::millis(); }
void *lv_custom_mem_alloc(size_t size) {
auto *ptr = malloc(size); // NOLINT
if (ptr == nullptr) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
#endif
}
return ptr;
}
@@ -102,7 +106,9 @@ void *lv_custom_mem_alloc(size_t size) {
ptr = heap_caps_malloc(size, cap_bits);
}
if (ptr == nullptr) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
#endif
return nullptr;
}
#ifdef ESPHOME_LOG_HAS_VERBOSE
+53 -6
View File
@@ -18,23 +18,27 @@
#ifdef USE_LVGL_FONT
#include "esphome/components/font/font.h"
#endif
#ifdef USE_LVGL_TOUCHSCREEN
#include "esphome/components/touchscreen/touchscreen.h"
#endif // USE_LVGL_TOUCHSCREEN
namespace esphome {
namespace lvgl {
extern lv_event_code_t lv_custom_event; // NOLINT
#ifdef USE_LVGL_COLOR
static lv_color_t lv_color_from(Color color) { return lv_color_make(color.red, color.green, color.blue); }
#endif
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
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_888;
#else
#else // LV_COLOR_DEPTH
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_332;
#endif
#endif // LV_COLOR_DEPTH
// Parent class for things that wrap an LVGL object
class LvCompound {
class LvCompound final {
public:
virtual void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; }
lv_obj_t *obj{};
@@ -99,6 +103,14 @@ class LvglComponent : public PollingComponent {
void set_full_refresh(bool full_refresh) { this->full_refresh_ = full_refresh; }
void set_buffer_frac(size_t frac) { this->buffer_frac_ = frac; }
lv_disp_t *get_disp() { return this->disp_; }
void set_paused(bool paused, bool show_snow) {
this->paused_ = paused;
if (!paused && lv_scr_act() != nullptr) {
lv_disp_trig_activity(this->disp_); // resets the inactivity time
lv_obj_invalidate(lv_scr_act());
}
}
bool is_paused() const { return this->paused_; }
protected:
void draw_buffer_(const lv_area_t *area, const uint8_t *ptr);
@@ -107,13 +119,48 @@ class LvglComponent : public PollingComponent {
lv_disp_draw_buf_t draw_buf_{};
lv_disp_drv_t disp_drv_{};
lv_disp_t *disp_{};
bool paused_{};
std::vector<std::function<void(lv_disp_t *)>> init_lambdas_;
size_t buffer_frac_{1};
bool full_refresh_{};
};
#ifdef USE_LVGL_TOUCHSCREEN
class LVTouchListener : public touchscreen::TouchListener, public Parented<LvglComponent> {
public:
LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time) {
lv_indev_drv_init(&this->drv_);
this->drv_.long_press_repeat_time = long_press_repeat_time;
this->drv_.long_press_time = long_press_time;
this->drv_.type = LV_INDEV_TYPE_POINTER;
this->drv_.user_data = this;
this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) {
auto *l = static_cast<LVTouchListener *>(d->user_data);
if (l->touch_pressed_) {
data->point.x = l->touch_point_.x;
data->point.y = l->touch_point_.y;
data->state = LV_INDEV_STATE_PRESSED;
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
};
}
void update(const touchscreen::TouchPoints_t &tpoints) override {
this->touch_pressed_ = !this->parent_->is_paused() && !tpoints.empty();
if (this->touch_pressed_)
this->touch_point_ = tpoints[0];
}
void release() override { touch_pressed_ = false; }
lv_indev_drv_t *get_drv() { return &this->drv_; }
protected:
lv_indev_drv_t drv_{};
touchscreen::TouchPoint touch_point_{};
bool touch_pressed_{};
};
#endif // USE_LVGL_TOUCHSCREEN
} // namespace lvgl
} // namespace esphome
#endif
#endif // USE_LVGL
+3 -8
View File
@@ -1,6 +1,5 @@
from .defines import CONF_OBJ
from .types import lv_obj_t
from .widget import WidgetType
from .defines import CONF_MAIN, CONF_OBJ
from .types import WidgetType, lv_obj_t
class ObjType(WidgetType):
@@ -9,11 +8,7 @@ class ObjType(WidgetType):
"""
def __init__(self):
super().__init__(CONF_OBJ, schema={}, modify_schema={})
@property
def w_type(self):
return lv_obj_t
super().__init__(CONF_OBJ, lv_obj_t, (CONF_MAIN,), schema={}, modify_schema={})
async def to_code(self, w, config):
return []
+34 -30
View File
@@ -3,15 +3,9 @@ from esphome.const import CONF_ARGS, CONF_FORMAT, CONF_ID, CONF_STATE, CONF_TYPE
from esphome.schema_extractors import SCHEMA_EXTRACT
from . import defines as df, lv_validation as lvalid, types as ty
from .defines import WIDGET_PARTS
from .helpers import (
REQUIRED_COMPONENTS,
add_lv_use,
requires_component,
validate_printf,
)
from .helpers import add_lv_use, requires_component, validate_printf
from .lv_validation import lv_font
from .types import WIDGET_TYPES, get_widget_type
from .types import WIDGET_TYPES, WidgetType
# A schema for text properties
TEXT_SCHEMA = cv.Schema(
@@ -46,9 +40,9 @@ STYLE_PROPS = {
"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_stop": lvalid.stop_value,
"bg_img_opa": lvalid.opacity,
"bg_img_recolor": lvalid.lv_color,
"bg_img_recolor_opa": lvalid.opacity,
"bg_image_opa": lvalid.opacity,
"bg_image_recolor": lvalid.lv_color,
"bg_image_recolor_opa": lvalid.opacity,
"bg_main_stop": lvalid.stop_value,
"bg_opa": lvalid.opacity,
"border_color": lvalid.lv_color,
@@ -60,8 +54,8 @@ STYLE_PROPS = {
"border_width": cv.positive_int,
"clip_corner": lvalid.lv_bool,
"height": lvalid.size,
"img_recolor": lvalid.lv_color,
"img_recolor_opa": lvalid.opacity,
"image_recolor": lvalid.lv_color,
"image_recolor_opa": lvalid.opacity,
"line_width": cv.positive_int,
"line_dash_width": cv.positive_int,
"line_dash_gap": cv.positive_int,
@@ -108,12 +102,21 @@ STYLE_PROPS = {
"max_width": lvalid.pixels_or_percent,
"min_height": lvalid.pixels_or_percent,
"min_width": lvalid.pixels_or_percent,
"radius": cv.Any(lvalid.size, df.LvConstant("LV_RADIUS_", "CIRCLE").one_of),
"radius": lvalid.radius,
"width": lvalid.size,
"x": lvalid.pixels_or_percent,
"y": lvalid.pixels_or_percent,
}
STYLE_REMAP = {
"bg_image_opa": "bg_img_opa",
"bg_image_recolor": "bg_img_recolor",
"bg_image_recolor_opa": "bg_img_recolor_opa",
"bg_image_src": "bg_img_src",
"image_recolor": "img_recolor",
"image_recolor_opa": "img_recolor_opa",
}
# Complete object style schema
STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend(
{
@@ -132,25 +135,23 @@ SET_STATE_SCHEMA = cv.Schema(
{cv.Optional(state): lvalid.lv_bool for state in df.STATES}
)
# Setting object flags
FLAG_SCHEMA = cv.Schema({cv.Optional(flag): cv.boolean 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)
def part_schema(widget_type):
def part_schema(widget_type: WidgetType):
"""
Generate a schema for the various parts (e.g. main:, indicator:) of a widget type
:param widget_type: The type of widget to generate for
:return:
"""
parts = WIDGET_PARTS.get(widget_type)
if parts is None:
parts = (df.CONF_MAIN,)
parts = widget_type.parts
return cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}).extend(
STATE_SCHEMA
)
def obj_schema(widget_type: str):
def obj_schema(widget_type: WidgetType):
"""
Create a schema for a widget type itself i.e. no allowance for children
:param widget_type:
@@ -187,13 +188,12 @@ STYLED_TEXT_SCHEMA = cv.maybe_simple_value(
STYLE_SCHEMA.extend(TEXT_SCHEMA), key=df.CONF_TEXT
)
ALL_STYLES = {
**STYLE_PROPS,
}
def container_validator(schema, widget_type):
def container_validator(schema, widget_type: WidgetType):
"""
Create a validator for a container given the widget type
:param schema: Base schema to extend
@@ -203,13 +203,16 @@ def container_validator(schema, widget_type):
def validator(value):
result = schema
if w_sch := WIDGET_TYPES[widget_type].schema:
if w_sch := widget_type.schema:
result = result.extend(w_sch)
if value and (layout := value.get(df.CONF_LAYOUT)):
if not isinstance(layout, dict):
raise cv.Invalid("Layout value must be a dict")
ltype = layout.get(CONF_TYPE)
add_lv_use(ltype)
result = result.extend(
{cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema())}
)
if value == SCHEMA_EXTRACT:
return result
return result(value)
@@ -217,7 +220,7 @@ def container_validator(schema, widget_type):
return validator
def container_schema(widget_type, 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
the extras passed in, plus any defined for the specific widget being specified.
@@ -225,15 +228,16 @@ def container_schema(widget_type, extras=None):
:param extras: Additional options to be made available, e.g. layout properties for children
:return: The schema for this type of widget.
"""
lv_type = get_widget_type(widget_type)
schema = obj_schema(widget_type).extend({cv.GenerateID(): cv.declare_id(lv_type)})
schema = obj_schema(widget_type).extend(
{cv.GenerateID(): cv.declare_id(widget_type.w_type)}
)
if extras:
schema = schema.extend(extras)
# Delayed evaluation for recursion
return container_validator(schema, widget_type)
def widget_schema(widget_type, extras=None):
def widget_schema(widget_type: WidgetType, extras=None):
"""
Create a schema for a given widget type
:param widget_type: The name of the widget
@@ -241,9 +245,9 @@ def widget_schema(widget_type, extras=None):
:return:
"""
validator = container_schema(widget_type, extras=extras)
if required := REQUIRED_COMPONENTS.get(widget_type):
if required := widget_type.required_component:
validator = cv.All(validator, requires_component(required))
return cv.Exclusive(widget_type, df.CONF_WIDGETS), validator
return cv.Exclusive(widget_type.name, df.CONF_WIDGETS), validator
# All widget schemas must be defined before this is called.
@@ -257,4 +261,4 @@ def any_widget_schema(extras=None):
:param extras: Additional schema to be applied to each generated one
:return:
"""
return cv.Any(dict(widget_schema(wt, extras) for wt in WIDGET_PARTS))
return cv.Any(dict(widget_schema(wt, extras) for wt in WIDGET_TYPES.values()))
+46
View File
@@ -0,0 +1,46 @@
import esphome.codegen as cg
from esphome.components.touchscreen import CONF_TOUCHSCREEN_ID, Touchscreen
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.core import CORE, TimePeriod
from .defines import (
CONF_LONG_PRESS_REPEAT_TIME,
CONF_LONG_PRESS_TIME,
CONF_TOUCHSCREENS,
)
from .helpers import lvgl_components_required
from .lv_validation import lv_milliseconds
from .lvcode import lv
from .types import LVTouchListener
PRESS_TIME = cv.All(lv_milliseconds, cv.Range(max=TimePeriod(milliseconds=65535)))
CONF_TOUCHSCREEN = "touchscreen"
TOUCHSCREENS_CONFIG = cv.maybe_simple_value(
{
cv.Required(CONF_TOUCHSCREEN_ID): cv.use_id(Touchscreen),
cv.Optional(CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME,
cv.Optional(CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME,
cv.GenerateID(): cv.declare_id(LVTouchListener),
},
key=CONF_TOUCHSCREEN_ID,
)
def touchscreen_schema(config):
value = cv.ensure_list(TOUCHSCREENS_CONFIG)(config)
if value or CONF_TOUCHSCREEN not in CORE.loaded_integrations:
return value
return [TOUCHSCREENS_CONFIG(config)]
async def touchscreens_to_code(var, config):
for tconf in config.get(CONF_TOUCHSCREENS) or ():
lvgl_components_required.add(CONF_TOUCHSCREEN)
touchscreen = await cg.get_variable(tconf[CONF_TOUCHSCREEN_ID])
lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds
lprt = tconf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds
listener = cg.new_Pvariable(tconf[CONF_ID], lpt, lprt)
await cg.register_parented(listener, var)
lv.indev_drv_register(listener.get_drv())
cg.add(touchscreen.register_listener(listener))
+87 -25
View File
@@ -1,7 +1,22 @@
from esphome import codegen as cg
from esphome.core import ID
from esphome.cpp_generator import MockObjClass
from .defines import CONF_TEXT
class LvType(cg.MockObjClass):
def __init__(self, *args, **kwargs):
parens = kwargs.pop("parents", ())
super().__init__(*args, parents=parens + (lv_obj_base_t,))
self.args = kwargs.pop("largs", [(lv_obj_t_ptr, "obj")])
self.value = kwargs.pop("lvalue", lambda w: w.obj)
self.has_on_value = kwargs.pop("has_on_value", False)
self.value_property = None
def get_arg_type(self):
return self.args[0][0] if len(self.args) else None
from .defines import CONF_LABEL, CONF_OBJ, CONF_TEXT
uint16_t_ptr = cg.uint16.operator("ptr")
lvgl_ns = cg.esphome_ns.namespace("lvgl")
@@ -18,25 +33,15 @@ lv_obj_base_t = cg.global_ns.class_("lv_obj_t", lv_pseudo_button_t)
lv_obj_t_ptr = lv_obj_base_t.operator("ptr")
lv_disp_t_ptr = cg.global_ns.struct("lv_disp_t").operator("ptr")
lv_color_t = cg.global_ns.struct("lv_color_t")
LVTouchListener = lvgl_ns.class_("LVTouchListener")
LVEncoderListener = lvgl_ns.class_("LVEncoderListener")
lv_obj_t = LvType("lv_obj_t")
# this will be populated later, in __init__.py to avoid circular imports.
WIDGET_TYPES: dict = {}
class LvType(cg.MockObjClass):
def __init__(self, *args, **kwargs):
parens = kwargs.pop("parents", ())
super().__init__(*args, parents=parens + (lv_obj_base_t,))
self.args = kwargs.pop("largs", [(lv_obj_t_ptr, "obj")])
self.value = kwargs.pop("lvalue", lambda w: w.obj)
self.has_on_value = kwargs.pop("has_on_value", False)
self.value_property = None
def get_arg_type(self):
return self.args[0][0] if len(self.args) else None
class LvText(LvType):
def __init__(self, *args, **kwargs):
super().__init__(
@@ -48,17 +53,74 @@ class LvText(LvType):
self.value_property = CONF_TEXT
lv_obj_t = LvType("lv_obj_t")
lv_label_t = LvText("lv_label_t")
LV_TYPES = {
CONF_LABEL: lv_label_t,
CONF_OBJ: lv_obj_t,
}
def get_widget_type(typestr: str) -> LvType:
return LV_TYPES[typestr]
class LvBoolean(LvType):
def __init__(self, *args, **kwargs):
super().__init__(
*args,
largs=[(cg.bool_, "x")],
lvalue=lambda w: w.is_checked(),
has_on_value=True,
**kwargs,
)
CUSTOM_EVENT = ID("lv_custom_event", False, type=lv_event_code_t)
class WidgetType:
"""
Describes a type of Widget, e.g. "bar" or "line"
"""
def __init__(self, name, w_type, parts, schema=None, modify_schema=None):
"""
: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
"""
self.name = name
self.w_type = w_type
self.parts = parts
self.schema = schema or {}
if modify_schema is None:
self.modify_schema = schema
else:
self.modify_schema = modify_schema
@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
:return: Generated code as a list of text lines
"""
raise NotImplementedError(f"No to_code defined for {self.name}")
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 f"lv_{self.name}_create({parent})"
def get_uses(self):
"""
Get a list of other widgets used by this one
:return:
"""
return ()
+8 -65
View File
@@ -21,78 +21,19 @@ from .defines import (
)
from .helpers import add_lv_use
from .lvcode import ConstantLiteral, add_line_marks, lv, lv_add, lv_assign, lv_obj
from .schemas import ALL_STYLES
from .types import WIDGET_TYPES, LvCompound, lv_obj_t
from .schemas import ALL_STYLES, STYLE_REMAP
from .types import WIDGET_TYPES, WidgetType, lv_obj_t
EVENT_LAMB = "event_lamb__"
class WidgetType:
"""
Describes a type of Widget, e.g. "bar" or "line"
"""
def __init__(self, name, schema=None, modify_schema=None):
"""
:param name: The widget name, e.g. "bar"
:param schema: The config schema for defining a widget
:param modify_schema: A schema to update the widget
"""
self.name = name
self.schema = schema or {}
if modify_schema is None:
self.modify_schema = schema
else:
self.modify_schema = modify_schema
@property
def animated(self):
return False
@property
def w_type(self):
"""
Get the type associated with this widget
:return:
"""
return lv_obj_t
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
:return: Generated code as a list of text lines
"""
raise NotImplementedError(f"No to_code defined for {self.name}")
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 f"lv_{self.name}_create({parent})"
def get_uses(self):
"""
Get a list of other widgets used by this one
:return:
"""
return ()
class LvScrActType(WidgetType):
"""
A "widget" representing the active screen.
"""
def __init__(self):
super().__init__("lv_scr_act()")
super().__init__("lv_scr_act()", lv_obj_t, ())
def obj_creator(self, parent: MockObjClass, config: dict):
return []
@@ -263,7 +204,9 @@ async def set_obj_properties(w: Widget, config):
}.items():
if isinstance(ALL_STYLES[prop], LValidator):
value = await ALL_STYLES[prop].process(value)
w.set_style(prop, value, lv_state)
# Remapping for backwards compatibility of style names
prop_r = STYLE_REMAP.get(prop, prop)
w.set_style(prop_r, value, lv_state)
flag_clr = set()
flag_set = set()
props = parts[CONF_MAIN][CONF_DEFAULT]
@@ -291,10 +234,10 @@ async def set_obj_properties(w: Widget, config):
else:
clears.add(key)
if adds:
adds = ConstantLiteral(join_enums(adds, "LV_STATE_"))
adds = join_enums(adds, "LV_STATE_")
w.add_state(adds)
if clears:
clears = ConstantLiteral(join_enums(clears, "LV_STATE_"))
clears = join_enums(clears, "LV_STATE_")
w.clear_state(clears)
for key, value in lambs.items():
lamb = await cg.process_lambda(value, [], return_type=cg.bool_)