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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 503 additions and 183 deletions

View file

@ -16,13 +16,20 @@ from esphome.final_validate import full_config
from esphome.helpers import write_file_if_changed 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 .btn import btn_spec
from .label import label_spec from .label import label_spec
from .lvcode import ConstantLiteral, LvContext from .lvcode import ConstantLiteral, LvContext
# from .menu import menu_spec
from .obj import obj_spec from .obj import obj_spec
from .schemas import WIDGET_TYPES, any_widget_schema, obj_schema from .schemas import any_widget_schema, obj_schema
from .types import FontEngine, LvglComponent, lv_disp_t_ptr, lv_font_t, lvgl_ns 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 from .widget import LvScrActType, Widget, add_widgets, set_obj_properties
DOMAIN = "lvgl" DOMAIN = "lvgl"
@ -31,11 +38,8 @@ AUTO_LOAD = ("key_provider",)
CODEOWNERS = ("@clydebarrow",) CODEOWNERS = ("@clydebarrow",)
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
for widg in ( for w_type in (label_spec, obj_spec, btn_spec):
label_spec, WIDGET_TYPES[w_type.name] = w_type
obj_spec,
):
WIDGET_TYPES[widg.name] = widg
lv_scr_act_spec = LvScrActType() lv_scr_act_spec = LvScrActType()
lv_scr_act = Widget.create( 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" "Using auto_clear_enabled: true in display config not compatible with LVGL"
) )
buffer_frac = config[CONF_BUFFER_SIZE] 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") 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) cg.add_global(lvgl_ns.using)
lv_component = cg.new_Pvariable(config[CONF_ID]) lv_component = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(lv_component, config) 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]: for display in config[df.CONF_DISPLAYS]:
cg.add(lv_component.add_display(await cg.get_variable(display))) 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) await cg.get_variable(font)
cg.new_Pvariable(ID(f"{font}_engine", True, type=FontEngine), MockObj(font)) cg.new_Pvariable(ID(f"{font}_engine", True, type=FontEngine), MockObj(font))
default_font = config[df.CONF_DEFAULT_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( 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})"
) )
@ -161,12 +165,15 @@ async def to_code(config):
True, True,
type=lv_font_t.operator("ptr").operator("const"), 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) add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT)
else: else:
add_define("LV_FONT_DEFAULT", default_font) add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font))
with LvContext(): with LvContext():
await touchscreens_to_code(lv_component, 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)
Widget.set_completed() Widget.set_completed()
@ -190,7 +197,7 @@ FINAL_VALIDATE_SCHEMA = final_validation
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
cv.polling_component_schema("1s") cv.polling_component_schema("1s")
.extend(obj_schema("obj")) .extend(obj_schema(obj_spec))
.extend( .extend(
{ {
cv.GenerateID(CONF_ID): cv.declare_id(LvglComponent), 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_WIDGETS): cv.ensure_list(WIDGET_SCHEMA),
cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color, 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)) ).add_extra(cv.has_at_least_one_key(CONF_PAGES, df.CONF_WIDGETS))

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()

View file

@ -446,6 +446,7 @@ CONF_TILE_ID = "tile_id"
CONF_TILES = "tiles" CONF_TILES = "tiles"
CONF_TITLE = "title" CONF_TITLE = "title"
CONF_TOP_LAYER = "top_layer" CONF_TOP_LAYER = "top_layer"
CONF_TOUCHSCREENS = "touchscreens"
CONF_TRANSPARENCY_KEY = "transparency_key" CONF_TRANSPARENCY_KEY = "transparency_key"
CONF_THEME = "theme" CONF_THEME = "theme"
CONF_VISIBLE_ROW_COUNT = "visible_row_count" 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" DEFAULT_ESPHOME_FONT = "esphome_lv_default_font"
def join_enums(enums, prefix=""): 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))

View file

@ -22,7 +22,6 @@ def add_lv_use(*names):
lv_fonts_used = set() lv_fonts_used = set()
esphome_fonts_used = set() esphome_fonts_used = set()
REQUIRED_COMPONENTS = {}
lvgl_components_required = set() lvgl_components_required = set()

View file

@ -1,16 +1,27 @@
import esphome.config_validation as cv 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 .lv_validation import lv_bool, lv_text
from .schemas import TEXT_SCHEMA from .schemas import TEXT_SCHEMA
from .types import lv_label_t from .types import LvText, WidgetType
from .widget import Widget, WidgetType from .widget import Widget
class LabelType(WidgetType): class LabelType(WidgetType):
def __init__(self): def __init__(self):
super().__init__( super().__init__(
CONF_LABEL, CONF_LABEL,
LvText("lv_label_t"),
(CONF_MAIN, CONF_SCROLLBAR, CONF_SELECTED),
TEXT_SCHEMA.extend( TEXT_SCHEMA.extend(
{ {
cv.Optional(CONF_RECOLOR): lv_bool, 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): async def to_code(self, w: Widget, config):
"""For a text object, create and set text""" """For a text object, create and set text"""
if value := config.get(CONF_TEXT): if value := config.get(CONF_TEXT):

View file

@ -8,6 +8,7 @@ import esphome.config_validation as cv
from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT
from esphome.core import HexInt from esphome.core import HexInt
from esphome.cpp_generator import MockObj from esphome.cpp_generator import MockObj
from esphome.cpp_types import uint32
from esphome.helpers import cpp_string_escape from esphome.helpers import cpp_string_escape
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor 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 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") @schema_extractor("one_of")
def color(value): def color(value):
if value == SCHEMA_EXTRACT: if value == SCHEMA_EXTRACT:
@ -43,16 +66,24 @@ def color_retmapper(value):
return lv_expr.color_from(MockObj(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""" """A length in one axis - either a number (pixels) or a percentage"""
if value == SCHEMA_EXTRACT: if value == SCHEMA_EXTRACT:
return ["pixels", "..%"] return ["pixels", "..%"]
if isinstance(value, int): if isinstance(value, int):
return str(cv.int_(value)) return cv.int_(value)
# Will throw an exception if not a percentage. # Will throw an exception if not a percentage.
return f"lv_pct({int(cv.percentage(value) * 100)})" return f"lv_pct({int(cv.percentage(value) * 100)})"
pixels_or_percent = LValidator(
pixels_or_percent_validator, uint32, retmapper=literal_mapper
)
def zoom(value): def zoom(value):
value = cv.float_range(0.1, 10.0)(value) value = cv.float_range(0.1, 10.0)(value)
return int(value * 256) return int(value * 256)
@ -68,7 +99,7 @@ def angle(value):
@schema_extractor("one_of") @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""" """A size in one axis - one of "size_content", a number (pixels) or a percentage"""
if value == SCHEMA_EXTRACT: if value == SCHEMA_EXTRACT:
return ["size_content", "pixels", "..%"] return ["size_content", "pixels", "..%"]
@ -79,28 +110,42 @@ def size(value):
return "LV_SIZE_CONTENT" return "LV_SIZE_CONTENT"
raise cv.Invalid("must be 'size_content', a pixel position or a percentage") raise cv.Invalid("must be 'size_content', a pixel position or a percentage")
if isinstance(value, int): if isinstance(value, int):
return str(cv.int_(value)) return cv.int_(value)
# Will throw an exception if not a percentage. # Will throw an exception if not a percentage.
return f"lv_pct({int(cv.percentage(value) * 100)})" 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") @schema_extractor("one_of")
def opacity(value): def radius_validator(value):
consts = LvConstant("LV_OPA_", "TRANSP", "COVER")
if value == SCHEMA_EXTRACT: if value == SCHEMA_EXTRACT:
return consts.choices return radius_consts.choices
value = cv.Any(cv.percentage, consts.one_of)(value) value = cv.Any(size, cv.percentage, radius_consts.one_of)(value)
if isinstance(value, float): if isinstance(value, float):
return int(value * 255) return int(value * 255)
return value 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): def stop_value(value):
return cv.int_range(0, 255)(value) return cv.int_range(0, 255)(value)
lv_color = LValidator(color, ty.lv_color_t, retmapper=color_retmapper) lv_bool = LValidator(
lv_bool = LValidator(cv.boolean, cg.bool_, BinarySensor, "get_state()") cv.boolean, cg.bool_, BinarySensor, "get_state()", retmapper=literal_mapper
)
def lvms_validator_(value): 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()") 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): class LvFont(LValidator):
def __init__(self): def __init__(self):
def lv_builtin_font(value): def lv_builtin_font(value):
fontval = cv.one_of(*LV_FONTS, lower=True)(value) fontval = cv.one_of(*LV_FONTS, lower=True)(value)
lv_fonts_used.add(fontval) lv_fonts_used.add(fontval)
return "&lv_font_" + fontval return fontval
def validator(value): def validator(value):
if value == SCHEMA_EXTRACT: if value == SCHEMA_EXTRACT:
return LV_FONTS return LV_FONTS
if isinstance(value, str) and value.lower() in LV_FONTS: if is_lv_font(value):
return lv_builtin_font(value) return lv_builtin_font(value)
fontval = cv.use_id(Font)(value) fontval = cv.use_id(Font)(value)
esphome_fonts_used.add(fontval) 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) super().__init__(validator, lv_font_t)
async def process(self, value, args=()): 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() lv_font = LvFont()

View file

@ -38,7 +38,9 @@ void LvglComponent::setup() {
auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8; auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8;
auto *buf = lv_custom_mem_alloc(buf_bytes); auto *buf = lv_custom_mem_alloc(buf_bytes);
if (buf == nullptr) { if (buf == nullptr) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
ESP_LOGE(TAG, "Malloc failed to allocate %zu bytes", buf_bytes); ESP_LOGE(TAG, "Malloc failed to allocate %zu bytes", buf_bytes);
#endif
this->mark_failed(); this->mark_failed();
this->status_set_error("Memory allocation failure"); this->status_set_error("Memory allocation failure");
return; return;
@ -85,7 +87,9 @@ size_t lv_millis(void) { return esphome::millis(); }
void *lv_custom_mem_alloc(size_t size) { void *lv_custom_mem_alloc(size_t size) {
auto *ptr = malloc(size); // NOLINT auto *ptr = malloc(size); // NOLINT
if (ptr == nullptr) { if (ptr == nullptr) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
#endif
} }
return ptr; return ptr;
} }
@ -102,7 +106,9 @@ void *lv_custom_mem_alloc(size_t size) {
ptr = heap_caps_malloc(size, cap_bits); ptr = heap_caps_malloc(size, cap_bits);
} }
if (ptr == nullptr) { if (ptr == nullptr) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size);
#endif
return nullptr; return nullptr;
} }
#ifdef ESPHOME_LOG_HAS_VERBOSE #ifdef ESPHOME_LOG_HAS_VERBOSE

View file

@ -18,23 +18,27 @@
#ifdef USE_LVGL_FONT #ifdef USE_LVGL_FONT
#include "esphome/components/font/font.h" #include "esphome/components/font/font.h"
#endif #endif
#ifdef USE_LVGL_TOUCHSCREEN
#include "esphome/components/touchscreen/touchscreen.h"
#endif // USE_LVGL_TOUCHSCREEN
namespace esphome { namespace esphome {
namespace lvgl { namespace lvgl {
extern lv_event_code_t lv_custom_event; // NOLINT extern lv_event_code_t lv_custom_event; // NOLINT
#ifdef USE_LVGL_COLOR #ifdef USE_LVGL_COLOR
static lv_color_t lv_color_from(Color color) { return lv_color_make(color.red, color.green, color.blue); } inline lv_color_t lv_color_from(Color color) { return lv_color_make(color.red, color.green, color.blue); }
#endif #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
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_888; 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; 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 // Parent class for things that wrap an LVGL object
class LvCompound { class LvCompound final {
public: public:
virtual void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; } virtual void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; }
lv_obj_t *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_full_refresh(bool full_refresh) { this->full_refresh_ = full_refresh; }
void set_buffer_frac(size_t frac) { this->buffer_frac_ = frac; } void set_buffer_frac(size_t frac) { this->buffer_frac_ = frac; }
lv_disp_t *get_disp() { return this->disp_; } 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: protected:
void draw_buffer_(const lv_area_t *area, const uint8_t *ptr); 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_draw_buf_t draw_buf_{};
lv_disp_drv_t disp_drv_{}; lv_disp_drv_t disp_drv_{};
lv_disp_t *disp_{}; lv_disp_t *disp_{};
bool paused_{};
std::vector<std::function<void(lv_disp_t *)>> init_lambdas_; std::vector<std::function<void(lv_disp_t *)>> init_lambdas_;
size_t buffer_frac_{1}; size_t buffer_frac_{1};
bool full_refresh_{}; 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 lvgl
} // namespace esphome } // namespace esphome
#endif #endif // USE_LVGL

View file

@ -1,6 +1,5 @@
from .defines import CONF_OBJ from .defines import CONF_MAIN, CONF_OBJ
from .types import lv_obj_t from .types import WidgetType, lv_obj_t
from .widget import WidgetType
class ObjType(WidgetType): class ObjType(WidgetType):
@ -9,11 +8,7 @@ class ObjType(WidgetType):
""" """
def __init__(self): def __init__(self):
super().__init__(CONF_OBJ, schema={}, modify_schema={}) super().__init__(CONF_OBJ, lv_obj_t, (CONF_MAIN,), schema={}, modify_schema={})
@property
def w_type(self):
return lv_obj_t
async def to_code(self, w, config): async def to_code(self, w, config):
return [] return []

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 esphome.schema_extractors import SCHEMA_EXTRACT
from . import defines as df, lv_validation as lvalid, types as ty from . import defines as df, lv_validation as lvalid, types as ty
from .defines import WIDGET_PARTS from .helpers import add_lv_use, requires_component, validate_printf
from .helpers import (
REQUIRED_COMPONENTS,
add_lv_use,
requires_component,
validate_printf,
)
from .lv_validation import lv_font 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 # A schema for text properties
TEXT_SCHEMA = cv.Schema( TEXT_SCHEMA = cv.Schema(
@ -46,9 +40,9 @@ STYLE_PROPS = {
"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": df.LvConstant("LV_GRAD_DIR_", "NONE", "HOR", "VER").one_of,
"bg_grad_stop": lvalid.stop_value, "bg_grad_stop": lvalid.stop_value,
"bg_img_opa": lvalid.opacity, "bg_image_opa": lvalid.opacity,
"bg_img_recolor": lvalid.lv_color, "bg_image_recolor": lvalid.lv_color,
"bg_img_recolor_opa": lvalid.opacity, "bg_image_recolor_opa": lvalid.opacity,
"bg_main_stop": lvalid.stop_value, "bg_main_stop": lvalid.stop_value,
"bg_opa": lvalid.opacity, "bg_opa": lvalid.opacity,
"border_color": lvalid.lv_color, "border_color": lvalid.lv_color,
@ -60,8 +54,8 @@ STYLE_PROPS = {
"border_width": cv.positive_int, "border_width": cv.positive_int,
"clip_corner": lvalid.lv_bool, "clip_corner": lvalid.lv_bool,
"height": lvalid.size, "height": lvalid.size,
"img_recolor": lvalid.lv_color, "image_recolor": lvalid.lv_color,
"img_recolor_opa": lvalid.opacity, "image_recolor_opa": lvalid.opacity,
"line_width": cv.positive_int, "line_width": cv.positive_int,
"line_dash_width": cv.positive_int, "line_dash_width": cv.positive_int,
"line_dash_gap": cv.positive_int, "line_dash_gap": cv.positive_int,
@ -108,12 +102,21 @@ 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": cv.Any(lvalid.size, df.LvConstant("LV_RADIUS_", "CIRCLE").one_of), "radius": lvalid.radius,
"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,
} }
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 # 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(
{ {
@ -132,25 +135,23 @@ SET_STATE_SCHEMA = cv.Schema(
{cv.Optional(state): lvalid.lv_bool for state in df.STATES} {cv.Optional(state): lvalid.lv_bool for state in df.STATES}
) )
# Setting object flags # 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) 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 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 :param widget_type: The type of widget to generate for
:return: :return:
""" """
parts = WIDGET_PARTS.get(widget_type) parts = widget_type.parts
if parts is None:
parts = (df.CONF_MAIN,)
return cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}).extend( return cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}).extend(
STATE_SCHEMA 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 Create a schema for a widget type itself i.e. no allowance for children
:param widget_type: :param widget_type:
@ -187,13 +188,12 @@ STYLED_TEXT_SCHEMA = cv.maybe_simple_value(
STYLE_SCHEMA.extend(TEXT_SCHEMA), key=df.CONF_TEXT STYLE_SCHEMA.extend(TEXT_SCHEMA), key=df.CONF_TEXT
) )
ALL_STYLES = { ALL_STYLES = {
**STYLE_PROPS, **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 Create a validator for a container given the widget type
:param schema: Base schema to extend :param schema: Base schema to extend
@ -203,13 +203,16 @@ def container_validator(schema, widget_type):
def validator(value): def validator(value):
result = schema result = schema
if w_sch := WIDGET_TYPES[widget_type].schema: if w_sch := widget_type.schema:
result = result.extend(w_sch) result = result.extend(w_sch)
if value and (layout := value.get(df.CONF_LAYOUT)): if value and (layout := value.get(df.CONF_LAYOUT)):
if not isinstance(layout, dict): if not isinstance(layout, dict):
raise cv.Invalid("Layout value must be a dict") raise cv.Invalid("Layout value must be a dict")
ltype = layout.get(CONF_TYPE) ltype = layout.get(CONF_TYPE)
add_lv_use(ltype) add_lv_use(ltype)
result = result.extend(
{cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema())}
)
if value == SCHEMA_EXTRACT: if value == SCHEMA_EXTRACT:
return result return result
return result(value) return result(value)
@ -217,7 +220,7 @@ def container_validator(schema, widget_type):
return validator 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 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. 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 :param extras: Additional options to be made available, e.g. layout properties for children
:return: The schema for this type of widget. :return: The schema for this type of widget.
""" """
lv_type = get_widget_type(widget_type) schema = obj_schema(widget_type).extend(
schema = obj_schema(widget_type).extend({cv.GenerateID(): cv.declare_id(lv_type)}) {cv.GenerateID(): cv.declare_id(widget_type.w_type)}
)
if extras: if extras:
schema = schema.extend(extras) schema = schema.extend(extras)
# Delayed evaluation for recursion # Delayed evaluation for recursion
return container_validator(schema, widget_type) 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 Create a schema for a given widget type
:param widget_type: The name of the widget :param widget_type: The name of the widget
@ -241,9 +245,9 @@ def widget_schema(widget_type, extras=None):
:return: :return:
""" """
validator = container_schema(widget_type, extras=extras) 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)) 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. # 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 :param extras: Additional schema to be applied to each generated one
:return: :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()))

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))

View file

@ -1,7 +1,22 @@
from esphome import codegen as cg from esphome import codegen as cg
from esphome.core import ID 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") uint16_t_ptr = cg.uint16.operator("ptr")
lvgl_ns = cg.esphome_ns.namespace("lvgl") 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_obj_t_ptr = lv_obj_base_t.operator("ptr")
lv_disp_t_ptr = cg.global_ns.struct("lv_disp_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") 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. # this will be populated later, in __init__.py to avoid circular imports.
WIDGET_TYPES: dict = {} 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): class LvText(LvType):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__( super().__init__(
@ -48,17 +53,74 @@ class LvText(LvType):
self.value_property = CONF_TEXT self.value_property = CONF_TEXT
lv_obj_t = LvType("lv_obj_t") class LvBoolean(LvType):
lv_label_t = LvText("lv_label_t") def __init__(self, *args, **kwargs):
super().__init__(
LV_TYPES = { *args,
CONF_LABEL: lv_label_t, largs=[(cg.bool_, "x")],
CONF_OBJ: lv_obj_t, lvalue=lambda w: w.is_checked(),
} has_on_value=True,
**kwargs,
)
def get_widget_type(typestr: str) -> LvType:
return LV_TYPES[typestr]
CUSTOM_EVENT = ID("lv_custom_event", False, type=lv_event_code_t) 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 ()

View file

@ -21,78 +21,19 @@ from .defines import (
) )
from .helpers import add_lv_use from .helpers import add_lv_use
from .lvcode import ConstantLiteral, add_line_marks, lv, lv_add, lv_assign, lv_obj from .lvcode import ConstantLiteral, add_line_marks, lv, lv_add, lv_assign, lv_obj
from .schemas import ALL_STYLES from .schemas import ALL_STYLES, STYLE_REMAP
from .types import WIDGET_TYPES, LvCompound, lv_obj_t from .types import WIDGET_TYPES, WidgetType, lv_obj_t
EVENT_LAMB = "event_lamb__" 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): class LvScrActType(WidgetType):
""" """
A "widget" representing the active screen. A "widget" representing the active screen.
""" """
def __init__(self): def __init__(self):
super().__init__("lv_scr_act()") super().__init__("lv_scr_act()", lv_obj_t, ())
def obj_creator(self, parent: MockObjClass, config: dict): def obj_creator(self, parent: MockObjClass, config: dict):
return [] return []
@ -263,7 +204,9 @@ async def set_obj_properties(w: Widget, config):
}.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)
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_clr = set()
flag_set = set() flag_set = set()
props = parts[CONF_MAIN][CONF_DEFAULT] props = parts[CONF_MAIN][CONF_DEFAULT]
@ -291,10 +234,10 @@ async def set_obj_properties(w: Widget, config):
else: else:
clears.add(key) clears.add(key)
if adds: if adds:
adds = ConstantLiteral(join_enums(adds, "LV_STATE_")) adds = join_enums(adds, "LV_STATE_")
w.add_state(adds) w.add_state(adds)
if clears: if clears:
clears = ConstantLiteral(join_enums(clears, "LV_STATE_")) clears = join_enums(clears, "LV_STATE_")
w.clear_state(clears) w.clear_state(clears)
for key, value in lambs.items(): for key, value in lambs.items():
lamb = await cg.process_lambda(value, [], return_type=cg.bool_) lamb = await cg.process_lambda(value, [], return_type=cg.bool_)

View file

@ -41,6 +41,7 @@
#define USE_LVGL #define USE_LVGL
#define USE_LVGL_FONT #define USE_LVGL_FONT
#define USE_LVGL_IMAGE #define USE_LVGL_IMAGE
#define USE_LVGL_TOUCHSCREEN
#define USE_MDNS #define USE_MDNS
#define USE_MEDIA_PLAYER #define USE_MEDIA_PLAYER
#define USE_MQTT #define USE_MQTT

View file

@ -0,0 +1,10 @@
touchscreen:
- platform: ft63x6
id: tft_touch
display: tft_display
update_interval: 50ms
threshold: 1
calibration:
x_max: 240
y_max: 320

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 596.64 116.88">
<defs>
<style>
.cls-1 {
fill: #1d2126;
stroke-width: 0px;
}
</style>
</defs>
<g id="Layer_1-2" data-name="Layer 1">
<g>
<g>
<path class="cls-1" d="M211.37,84.7v11.25h-43.5V25.87h43.5v11.34h-31.59v18.14h28.22v10.55h-28.22v18.8h31.59Z"/>
<path class="cls-1" d="M245.41,24.6c6.41,0,11.73,1.56,15.98,4.69,4.25,3.12,6.86,7.3,7.83,12.52l-11.2,3.05c-.63-3-2.09-5.31-4.38-6.94-2.3-1.62-5.13-2.44-8.51-2.44-3.63,0-6.5.87-8.62,2.6-2.13,1.73-3.19,4.04-3.19,6.91,0,4.53,2.81,7.47,8.44,8.81l11.44,2.86c5.91,1.53,10.33,3.96,13.27,7.29,2.94,3.33,4.41,7.43,4.41,12.3,0,6.25-2.27,11.3-6.8,15.16-4.53,3.86-10.59,5.79-18.19,5.79-6.84,0-12.63-1.61-17.34-4.83-4.66-3.34-7.41-7.75-8.25-13.22l11.2-2.91c.41,3.09,1.95,5.52,4.62,7.27,2.67,1.75,6.07,2.62,10.2,2.62s7.09-.84,9.35-2.51c2.27-1.67,3.4-3.94,3.4-6.82,0-4.5-2.81-7.47-8.44-8.91l-11.44-2.77c-5.88-1.34-10.29-3.73-13.24-7.15s-4.43-7.6-4.43-12.54c0-6.19,2.2-11.21,6.59-15.07,4.39-3.86,10.16-5.79,17.32-5.79Z"/>
<path class="cls-1" d="M332.92,48.7c0,6.88-2.09,12.44-6.26,16.69-4.17,4.25-9.79,6.38-16.85,6.38h-15.75v24.19h-11.91V25.87h27.75c7.12,0,12.74,2.06,16.85,6.16,4.11,4.11,6.16,9.66,6.16,16.66ZM320.45,48.42c0-3.53-1.1-6.4-3.3-8.6s-5.32-3.3-9.35-3.3h-13.73v24.8h13.55c4.12,0,7.3-1.17,9.52-3.52,2.22-2.34,3.33-5.47,3.33-9.38Z"/>
<path class="cls-1" d="M343.56,25.87h11.91v29.3l30.89.09v-29.39h12v70.08h-12v-30.14l-30.89-.09v30.23h-11.91V25.87Z"/>
<path class="cls-1" d="M434.17,47.29c7.25,0,13.16,2.33,17.72,6.98,4.56,4.66,6.84,10.64,6.84,17.95s-2.28,13.25-6.84,17.91c-4.56,4.66-10.47,6.98-17.72,6.98s-13.25-2.33-17.81-6.98c-4.56-4.66-6.84-10.62-6.84-17.91s2.28-13.34,6.84-17.98c4.56-4.64,10.5-6.96,17.81-6.96ZM434.17,86.9c3.87,0,7.02-1.37,9.45-4.1,2.42-2.73,3.63-6.29,3.63-10.66s-1.21-7.91-3.63-10.62c-2.42-2.7-5.57-4.05-9.45-4.05s-7.17,1.35-9.61,4.05c-2.44,2.7-3.66,6.24-3.66,10.62s1.22,7.93,3.66,10.66c2.44,2.73,5.64,4.1,9.61,4.1Z"/>
<path class="cls-1" d="M540.67,65.81v30.14h-11.02v-28.41c0-3.28-.84-5.84-2.53-7.69-1.69-1.84-3.98-2.77-6.89-2.77-3.09,0-5.56,1.01-7.41,3.02-1.84,2.02-2.77,4.84-2.77,8.46v27.38h-11.16v-28.41c0-3.28-.82-5.84-2.46-7.69-1.64-1.84-3.91-2.77-6.82-2.77-3.09,0-5.58,1.01-7.45,3.02s-2.81,4.84-2.81,8.46v27.38h-11.34v-47.34h10.55l.38,4.55c2.75-4.03,7.23-6.05,13.45-6.05,3.62,0,6.77.75,9.42,2.25,2.66,1.5,4.67,3.69,6.05,6.56,1.16-2.75,3.07-4.91,5.74-6.47,2.67-1.56,5.85-2.34,9.54-2.34,5.37,0,9.64,1.66,12.8,4.97,3.16,3.31,4.73,7.89,4.73,13.73Z"/>
<path class="cls-1" d="M596.64,76.45h-36.28c.38,3.56,1.72,6.28,4.03,8.16,2.31,1.88,5.25,2.81,8.81,2.81,5.78,0,9.83-2.41,12.14-7.22l9.47,3.75c-1.78,4.16-4.59,7.41-8.41,9.75-3.83,2.34-8.23,3.52-13.2,3.52-7,0-12.68-2.29-17.04-6.87-4.36-4.58-6.54-10.59-6.54-18.02s2.2-13.48,6.59-18.14c4.39-4.66,10.13-6.98,17.23-6.98s12.58,2.3,16.83,6.89c4.25,4.59,6.38,10.64,6.38,18.14v4.22ZM560.55,68.48h24.56c-.22-3.84-1.38-6.77-3.49-8.79s-4.95-3.02-8.51-3.02-6.41,1.02-8.62,3.07c-2.22,2.05-3.53,4.96-3.94,8.74Z"/>
</g>
<path class="cls-1" d="M114.7,51.58L65.3,2.19c-2.92-2.92-7.69-2.92-10.61,0L5.3,51.58c-2.92,2.92-5.3,8.68-5.3,12.8v45c0,4.12,3.38,7.5,7.5,7.5h29.5V42.21c0-1.66,1.34-3,3-3h40c1.66,0,3,1.34,3,3v12c0,1.66-1.34,3-3,3h-25v6h25c1.66,0,3,1.34,3,3v12c0,1.66-1.34,3-3,3h-25v6h25c1.66,0,3,1.34,3,3v12c0,1.66-1.34,3-3,3h-28c-1.66,0-3-1.34-3-3s1.34-3,3-3h25v-6h-25c-1.66,0-3-1.34-3-3v-12c0-1.66,1.34-3,3-3h25v-6h-25c-1.66,0-3-1.34-3-3v-12c0-1.66,1.34-3,3-3h25v-6h-34v71.67h69.5c4.12,0,7.5-3.38,7.5-7.5v-45c0-4.12-2.39-9.89-5.3-12.8Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -1,9 +1,10 @@
color:
- id: light_blue
hex: "3340FF"
lvgl: lvgl:
log_level: TRACE
bg_color: light_blue bg_color: light_blue
touchscreens:
- touchscreen_id: tft_touch
long_press_repeat_time: 200ms
long_press_time: 500ms
widgets: widgets:
- label: - label:
text: Hello world text: Hello world
@ -17,8 +18,101 @@ lvgl:
text_color: 0xFFFFFF text_color: 0xFFFFFF
align: bottom_mid align: bottom_mid
text_font: space16 text_font: space16
- obj:
align: center
arc_opa: COVER
arc_color: 0xFF0000
arc_rounded: false
arc_width: 3
anim_time: 1s
bg_color: light_blue
bg_grad_color: light_blue
bg_dither_mode: ordered
bg_grad_dir: hor
bg_grad_stop: 128
bg_image_opa: transp
bg_image_recolor: light_blue
bg_image_recolor_opa: 50%
bg_main_stop: 0
bg_opa: 20%
border_color: 0x00FF00
border_opa: cover
border_post: true
border_side: [bottom, left]
border_width: 4
clip_corner: false
height: 50%
image_recolor: light_blue
image_recolor_opa: cover
line_width: 10
line_dash_width: 10
line_dash_gap: 10
line_rounded: false
line_color: light_blue
opa: cover
opa_layered: cover
outline_color: light_blue
outline_opa: cover
outline_pad: 10px
outline_width: 10px
pad_all: 10px
pad_bottom: 10px
pad_column: 10px
pad_left: 10px
pad_right: 10px
pad_row: 10px
pad_top: 10px
shadow_color: light_blue
shadow_ofs_x: 5
shadow_ofs_y: 5
shadow_opa: cover
shadow_spread: 5
shadow_width: 10
text_align: auto
text_color: light_blue
text_decor: [underline, strikethrough]
text_font: montserrat_18
text_letter_space: 4
text_line_space: 4
text_opa: cover
transform_angle: 180
transform_height: 100
transform_pivot_x: 50%
transform_pivot_y: 50%
transform_zoom: 0.5
translate_x: 10
translate_y: 10
max_height: 100
max_width: 200
min_height: 20%
min_width: 20%
radius: circle
width: 10px
x: 100
y: 120
- button:
width: 20%
height: 10%
pressed:
bg_color: light_blue
widgets:
- label:
text: Button
font: font:
- file: "gfonts://Roboto" - file: "gfonts://Roboto"
id: space16 id: space16
bpp: 4 bpp: 4
image:
- id: cat_img
resize: 256x48
file: $component_dir/logo-text.svg
- id: dog_img
file: $component_dir/logo-text.svg
resize: 256x48
type: TRANSPARENT_BINARY
color:
- id: light_blue
hex: "3340FF"

View file

@ -19,7 +19,9 @@ display:
mirror_y: true mirror_y: true
data_rate: 80MHz data_rate: 80MHz
cs_pin: GPIO20 cs_pin: GPIO20
dc_pin: GPIO15 dc_pin:
number: GPIO15
ignore_strapping_warning: true
auto_clear_enabled: false auto_clear_enabled: false
invert_colors: false invert_colors: false
update_interval: never update_interval: never