Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 54 additions & 13 deletions esphome/components/display/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from esphome import automation, core
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components.const import KEY_METADATA
from esphome.components.const import BYTE_ORDER_BIG, KEY_METADATA
import esphome.config_validation as cv
from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
Expand All @@ -18,8 +18,7 @@
CONF_UPDATE_INTERVAL,
SCHEDULER_DONT_RUN,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import MockObj
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority

DOMAIN = "display"
IS_PLATFORM_COMPONENT = True
Expand Down Expand Up @@ -159,29 +158,71 @@ async def setup_display_core_(var, config):
class DisplayMetaData:
width: int = 0
height: int = 0
has_writer: bool = False
has_hardware_rotation: bool = False
byte_order: str = BYTE_ORDER_BIG
has_writer: bool = False
rotation: int = 0
draw_rounding: int = 0


def _get_metadata_list() -> list[tuple]:
"""Get the raw metadata list. Each entry is (id, DisplayMetaData)."""
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, [])


def get_all_display_metadata() -> dict[str, DisplayMetaData]:
"""Get all display metadata."""
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {})
"""Get all display metadata as a dict keyed by resolved ID strings.

Must not be called before IDs have been finalised.
"""
entries = _get_metadata_list()
assert all(id_.id is not None for id_, _ in entries), (
"get_all_display_metadata called before display IDs have been resolved"
)
return {id_.id: meta for id_, meta in entries}

def get_display_metadata(display_id: str) -> DisplayMetaData | None:
"""Get display metadata by ID for use by other components."""
return get_all_display_metadata().get(display_id, DisplayMetaData())

def get_display_metadata(display_id: str) -> DisplayMetaData:
"""Get display metadata by ID string.

Must not be called before IDs have been finalised.
"""
for id_, meta in _get_metadata_list():
assert id_.id is not None, (
"get_display_metadata called before display IDs have been resolved"
)
if id_.id == display_id:
return meta
return DisplayMetaData()


def add_metadata(
id: str | MockObj,
id: ID,
width: int,
height: int,
has_writer: bool,
has_hardware_rotation: bool = False,
byte_order: str = BYTE_ORDER_BIG,
has_writer: bool = False,
rotation: int = 0,
draw_rounding: int = 0,
):
get_all_display_metadata()[str(id)] = DisplayMetaData(
width, height, has_writer, has_hardware_rotation
entries = _get_metadata_list()
assert not any(existing_id is id for existing_id, _ in entries), (
f"Duplicate display metadata for ID {id}"
)
entries.append(
(
id,
DisplayMetaData(
width=width,
height=height,
has_hardware_rotation=has_hardware_rotation,
byte_order=byte_order,
has_writer=has_writer,
rotation=rotation,
draw_rounding=draw_rounding,
),
)
)


Expand Down
48 changes: 33 additions & 15 deletions esphome/components/lvgl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from esphome.automation import Trigger, build_automation, validate_automation
import esphome.codegen as cg
from esphome.components.const import (
BYTE_ORDER_BIG,
CONF_BYTE_ORDER,
CONF_COLOR_DEPTH,
CONF_DRAW_ROUNDING,
Expand All @@ -30,12 +31,10 @@
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
import esphome.config_validation as cv
from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_BUFFER_SIZE,
CONF_ESPHOME,
CONF_GROUP,
CONF_ID,
CONF_LAMBDA,
CONF_LOG_LEVEL,
CONF_ON_IDLE,
CONF_PAGES,
Expand Down Expand Up @@ -214,31 +213,50 @@ def multi_conf_validate(configs: list[dict]):


def final_validation(config_list):
global_config = full_config.get()
# Resolve byte_order from display metadata before multi-config validation
for config in config_list:
display_byte_orders = set()
for display_id in config[df.CONF_DISPLAYS]:
meta = get_display_metadata(display_id.id)
if meta.byte_order:
display_byte_orders.add(meta.byte_order)
if len(display_byte_orders) > 1:
raise cv.Invalid(
"All displays configured for an LVGL instance must use the same byte_order"
)
if display_byte_orders:
display_order = next(iter(display_byte_orders))
if CONF_BYTE_ORDER in config:
if config[CONF_BYTE_ORDER] != display_order:
LOGGER.warning(
"LVGL byte_order '%s' does not match display byte_order '%s'",
config[CONF_BYTE_ORDER],
display_order,
)
else:
config[CONF_BYTE_ORDER] = display_order
if CONF_BYTE_ORDER not in config:
config[CONF_BYTE_ORDER] = BYTE_ORDER_BIG
if len(config_list) != 1:
multi_conf_validate(config_list)
global_config = full_config.get()
for config in config_list:
if (pages := config.get(CONF_PAGES)) and all(p[df.CONF_SKIP] for p in pages):
raise cv.Invalid("At least one page must not be skipped")
for display_id in config[df.CONF_DISPLAYS]:
path = global_config.get_path_for_id(display_id)[:-1]
display = global_config.get_config_for_path(path)
if CONF_LAMBDA in display or CONF_PAGES in display:
meta = get_display_metadata(str(display_id))
if meta.has_writer:
raise cv.Invalid(
"Using lambda: or pages: in display config is not compatible with LVGL"
"Using lambda:, pages:, or auto_clear_enabled: true in display config is not compatible with LVGL"
)
# treating 0 as false is intended here.
if display.get(CONF_ROTATION):
if meta.rotation:
raise cv.Invalid(
"use of 'rotation' in the display config is not compatible with LVGL, please set rotation in the LVGL config instead"
)
if display.get(CONF_AUTO_CLEAR_ENABLED) is True:
raise cv.Invalid(
"Using auto_clear_enabled: true in display config not compatible with LVGL"
)
if draw_rounding := display.get(CONF_DRAW_ROUNDING):
if meta.draw_rounding:
config[CONF_DRAW_ROUNDING] = max(
draw_rounding, config[CONF_DRAW_ROUNDING]
meta.draw_rounding, config[CONF_DRAW_ROUNDING]
)
buffer_frac = config[CONF_BUFFER_SIZE]
if CORE.is_esp32 and buffer_frac > 0.5 and PSRAM_DOMAIN not in global_config:
Expand Down Expand Up @@ -583,7 +601,7 @@ def _theme_schema(value: dict) -> dict:
cv.Optional(CONF_LOG_LEVEL, default="WARN"): cv.one_of(
*df.LV_LOG_LEVELS, upper=True
),
cv.Optional(CONF_BYTE_ORDER, default="big_endian"): cv.one_of(
cv.Optional(CONF_BYTE_ORDER): cv.one_of(
"big_endian", "little_endian", lower=True
),
cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list(
Expand Down
17 changes: 16 additions & 1 deletion esphome/components/mipi_dsi/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
)
import esphome.config_validation as cv
from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_COLOR_ORDER,
CONF_DIMENSIONS,
CONF_DISABLED,
Expand Down Expand Up @@ -167,7 +168,21 @@ def _config_schema(config):
},
extra=cv.ALLOW_EXTRA,
)(config)
return model_schema(config)(config)
config = model_schema(config)(config)
model = MODELS[config[CONF_MODEL].upper()]
width, height, _offset_width, _offset_height = model.get_dimensions(config)
display.add_metadata(
config[CONF_ID],
width,
height,
model.rotation_as_transform(config),
byte_order=config[CONF_BYTE_ORDER],
has_writer=requires_buffer(config)
or config.get(CONF_AUTO_CLEAR_ENABLED) is True,
rotation=config.get(CONF_ROTATION, 0),
draw_rounding=config.get(CONF_DRAW_ROUNDING, 0),
)
return config


def _final_validate(config):
Expand Down
17 changes: 16 additions & 1 deletion esphome/components/mipi_rgb/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
)
import esphome.config_validation as cv
from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_BLUE,
CONF_COLOR_ORDER,
CONF_CS_PIN,
Expand Down Expand Up @@ -226,11 +227,25 @@ def _config_schema(config):
extra=cv.ALLOW_EXTRA,
)(config)
schema = model_schema(config)
return cv.All(
config = cv.All(
schema,
cv.only_on_esp32,
only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4]),
)(config)
model = MODELS[config[CONF_MODEL].upper()]
width, height, _offset_width, _offset_height = model.get_dimensions(config)
display.add_metadata(
config[CONF_ID],
width,
height,
model.rotation_as_transform(config),
byte_order=config[CONF_BYTE_ORDER],
has_writer=requires_buffer(config)
or config.get(CONF_AUTO_CLEAR_ENABLED) is True,
rotation=config.get(CONF_ROTATION, 0),
draw_rounding=config.get(CONF_DRAW_ROUNDING, 0),
)
return config


CONFIG_SCHEMA = _config_schema
Expand Down
28 changes: 24 additions & 4 deletions esphome/components/mipi_spi/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import esphome.config_validation as cv
from esphome.config_validation import ALLOW_EXTRA
from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_BRIGHTNESS,
CONF_BUFFER_SIZE,
CONF_COLOR_ORDER,
Expand All @@ -47,6 +48,7 @@
CONF_MIRROR_Y,
CONF_MODEL,
CONF_RESET_PIN,
CONF_ROTATION,
CONF_SWAP_XY,
CONF_TRANSFORM,
CONF_WIDTH,
Expand Down Expand Up @@ -267,6 +269,28 @@ def customise_schema(config):
if bus_mode != TYPE_QUAD and CONF_DC_PIN not in config:
raise cv.Invalid(f"DC pin is required in {bus_mode} mode")
denominator(config)
model = MODELS[config[CONF_MODEL]]
has_hardware_transform = config.get(
CONF_TRANSFORM
) != CONF_DISABLED and model.transforms == {
CONF_MIRROR_X,
CONF_MIRROR_Y,
CONF_SWAP_XY,
}
width, height, _offset_width, _offset_height = model.get_dimensions(
config, not has_hardware_transform
)
display.add_metadata(
config[CONF_ID],
width,
height,
has_hardware_transform,
byte_order=config[CONF_BYTE_ORDER],
has_writer=requires_buffer(config)
or config.get(CONF_AUTO_CLEAR_ENABLED) is True,
rotation=config.get(CONF_ROTATION, 0),
draw_rounding=config.get(CONF_DRAW_ROUNDING, 0),
)
return config


Expand Down Expand Up @@ -338,7 +362,6 @@ def get_instance(config):
buffer_type = cg.uint8 if color_depth == 8 else cg.uint16
frac = denominator(config)
madctl = model.get_madctl(model.get_base_transform(config), config)
has_writer = requires_buffer(config)
templateargs = [
buffer_type,
bufferpixels,
Expand All @@ -352,9 +375,6 @@ def get_instance(config):
madctl,
has_hardware_transform,
]
display.add_metadata(
config[CONF_ID], width, height, has_writer, has_hardware_transform
)
# If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi
if requires_buffer(config):
templateargs.extend(
Expand Down
Loading