mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[mipi][mipi_spi][mipi_dsi][mipi_rgb] Transform cleanup (#17405)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
262ee421f6
commit
f0afd9e660
@@ -31,11 +31,16 @@ from esphome.const import (
|
||||
CONF_TRANSFORM,
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.core import TimePeriod
|
||||
from esphome.core import CORE, TimePeriod
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
|
||||
LOGGER = cv.logging.getLogger(__name__)
|
||||
|
||||
CONF_TRANSFORMS = "transforms"
|
||||
|
||||
# All axis transforms a model may support, in the order they appear in the schema.
|
||||
ALL_TRANSFORMS = (CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY)
|
||||
|
||||
ColorOrder = display_ns.enum("ColorMode")
|
||||
|
||||
NOP = 0x00
|
||||
@@ -302,7 +307,8 @@ class DriverChip:
|
||||
"""
|
||||
A class representing a MIPI DBI driver chip model.
|
||||
The parameters supplied as defaults will be used to provide default values for the display configuration.
|
||||
Setting swap_xy to cv.UNDEFINED will indicate that the model does not support swapping X and Y axes.
|
||||
Pass a ``transforms`` set to restrict which axis transforms (mirror_x, mirror_y, swap_xy) the model
|
||||
supports; by default all three are available.
|
||||
"""
|
||||
|
||||
models: dict[str, Self] = {}
|
||||
@@ -387,11 +393,15 @@ class DriverChip:
|
||||
"""
|
||||
Return the available transforms for this model.
|
||||
"""
|
||||
if (transforms := self.get_default(CONF_TRANSFORMS, None)) is not None:
|
||||
return transforms
|
||||
if self.get_default("no_transform", False):
|
||||
return set()
|
||||
if self.get_default(CONF_SWAP_XY) != cv.UNDEFINED:
|
||||
return {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY}
|
||||
return {CONF_MIRROR_X, CONF_MIRROR_Y}
|
||||
raise ValueError(
|
||||
"Setting 'swap_xy' to 'cv.UNDEFINED' is no longer supported; set 'transforms' instead"
|
||||
)
|
||||
|
||||
def has_hardware_transform(self, config) -> bool:
|
||||
"""
|
||||
@@ -533,17 +543,31 @@ class DriverChip:
|
||||
transform[CONF_TRANSFORM] = self.rotation_as_transform(config)
|
||||
return transform
|
||||
|
||||
def swap_xy_schema(self):
|
||||
uses_swap = self.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED
|
||||
def transform_schema(self):
|
||||
"""
|
||||
Build the schema for the ``transform`` config option of this model.
|
||||
|
||||
def validator(value):
|
||||
if value:
|
||||
raise cv.Invalid("Axis swapping not supported by this model")
|
||||
return cv.boolean(value)
|
||||
Each transform the model supports is a required boolean. A transform the model does not
|
||||
support may be omitted or set to ``false``; setting it to ``true`` reports a clear error
|
||||
naming the unsupported transform instead of a generic "extra keys not allowed".
|
||||
"""
|
||||
supported = self.transforms
|
||||
|
||||
if uses_swap:
|
||||
return {cv.Required(CONF_SWAP_XY): cv.boolean}
|
||||
return {cv.Optional(CONF_SWAP_XY, default=False): validator}
|
||||
def unsupported(name):
|
||||
def validator(value):
|
||||
if cv.boolean(value):
|
||||
raise cv.Invalid(f"'{name}' is not supported by this model")
|
||||
return False
|
||||
|
||||
return validator
|
||||
|
||||
schema = {}
|
||||
for name in ALL_TRANSFORMS:
|
||||
if name in supported:
|
||||
schema[cv.Required(name)] = cv.boolean
|
||||
else:
|
||||
schema[cv.Optional(name, default=False)] = unsupported(name)
|
||||
return cv.Any(cv.Schema(schema), cv.one_of(CONF_DISABLED, lower=True))
|
||||
|
||||
def get_madctl(self, transform: dict, config: dict) -> int:
|
||||
"""
|
||||
@@ -618,6 +642,31 @@ class DriverChip:
|
||||
# or the delay flag inserted where needed
|
||||
return flatten_sequence(sequence)
|
||||
|
||||
def check_requirements(self) -> None:
|
||||
"""
|
||||
Raise a friendly error if any component this model requires is not configured.
|
||||
|
||||
This runs during schema validation (before ID references are resolved) so that a
|
||||
model whose default pins live on a pin expander reports the missing expander clearly
|
||||
instead of a cryptic "Couldn't find ID" from the unresolved pin reference.
|
||||
"""
|
||||
requirements = self.get_default("requires", set())
|
||||
if not requirements:
|
||||
return
|
||||
# ``raw_config`` is populated before any component schema runs during a real
|
||||
# validation, so presence of a required component is simply a top-level key.
|
||||
# When it is absent (e.g. a unit test that invokes the schema directly) there
|
||||
# is no config to check against, so skip.
|
||||
global_config = CORE.raw_config
|
||||
if global_config is None:
|
||||
return
|
||||
missing = {x for x in requirements if x not in global_config}
|
||||
if missing:
|
||||
reqstr = ", ".join(f"'{x}'" for x in sorted(missing))
|
||||
raise cv.Invalid(
|
||||
f"{self.name} requires component{'s' if len(missing) > 1 else ''} {reqstr} to be configured"
|
||||
)
|
||||
|
||||
|
||||
def requires_buffer(config) -> bool:
|
||||
"""
|
||||
|
||||
@@ -41,24 +41,21 @@ from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
CONF_COLOR_ORDER,
|
||||
CONF_DIMENSIONS,
|
||||
CONF_DISABLED,
|
||||
CONF_ENABLE_PIN,
|
||||
CONF_ID,
|
||||
CONF_INIT_SEQUENCE,
|
||||
CONF_INVERT_COLORS,
|
||||
CONF_LAMBDA,
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_MODEL,
|
||||
CONF_RESET_PIN,
|
||||
CONF_ROTATION,
|
||||
CONF_SWAP_XY,
|
||||
CONF_TRANSFORM,
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.final_validate import full_config
|
||||
|
||||
from . import mipi_dsi_ns, models
|
||||
from .models import DsiDriverChip
|
||||
|
||||
# Currently only ESP32-P4 is supported, so esp_ldo and psram are required
|
||||
DEPENDENCIES = ["esp32", "esp_ldo", "psram"]
|
||||
@@ -73,7 +70,7 @@ ColorBitness = display.display_ns.enum("ColorBitness")
|
||||
CONF_LANE_BIT_RATE = "lane_bit_rate"
|
||||
CONF_LANES = "lanes"
|
||||
|
||||
DriverChip("CUSTOM")
|
||||
DsiDriverChip("CUSTOM")
|
||||
|
||||
# Import all models dynamically from the models package
|
||||
|
||||
@@ -90,19 +87,7 @@ COLOR_DEPTHS = {
|
||||
|
||||
def model_schema(config):
|
||||
model = MODELS[config[CONF_MODEL].upper()]
|
||||
model.defaults[CONF_SWAP_XY] = cv.UNDEFINED
|
||||
transform = cv.Any(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_MIRROR_X): cv.boolean,
|
||||
cv.Required(CONF_MIRROR_Y): cv.boolean,
|
||||
cv.Optional(CONF_SWAP_XY): cv.invalid(
|
||||
"Axis swapping not supported by DSI displays"
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.one_of(CONF_DISABLED, lower=True),
|
||||
)
|
||||
transform = model.transform_schema()
|
||||
# CUSTOM model will need to provide a custom init sequence
|
||||
iseqconf = (
|
||||
cv.Required(CONF_INIT_SEQUENCE)
|
||||
@@ -172,6 +157,7 @@ def _config_schema(config):
|
||||
)(config)
|
||||
config = model_schema(config)(config)
|
||||
model = MODELS[config[CONF_MODEL].upper()]
|
||||
model.check_requirements()
|
||||
width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
|
||||
model.get_dimensions(config)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from esphome.components.mipi import DriverChip
|
||||
from esphome.const import CONF_SWAP_XY
|
||||
|
||||
|
||||
class DsiDriverChip(DriverChip):
|
||||
"""A driver chip for MIPI DSI displays."""
|
||||
|
||||
@property
|
||||
def transforms(self) -> set[str]:
|
||||
"""
|
||||
Return the set of transformations supported by this driver chip.
|
||||
DSI displays do not support axis swapping, so this method removes CONF_SWAP_XY
|
||||
"""
|
||||
return super().transforms - {CONF_SWAP_XY}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from esphome.components.mipi import DriverChip
|
||||
import esphome.config_validation as cv
|
||||
from . import DsiDriverChip
|
||||
|
||||
# fmt: off
|
||||
DriverChip(
|
||||
DsiDriverChip(
|
||||
"JC1060P470",
|
||||
width=1024,
|
||||
height=600,
|
||||
@@ -14,7 +13,6 @@ DriverChip(
|
||||
vsync_front_porch=12,
|
||||
pclk_frequency="54MHz",
|
||||
lane_bit_rate="750Mbps",
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order="RGB",
|
||||
initsequence=[
|
||||
(0x30, 0x00), (0xF7, 0x49, 0x61, 0x02, 0x00), (0x30, 0x01), (0x04, 0x0C), (0x05, 0x00), (0x06, 0x00),
|
||||
@@ -46,7 +44,7 @@ DriverChip(
|
||||
# * Horizontal Timing (hsync_pulse_width=12, hsync_back_porch=42, hsync_front_porch=42)
|
||||
# * Vertical Timing (vsync_pulse_width=2, vsync_back_porch=8, vsync_front_porch=166)
|
||||
# ----------------------------------------------------------------------------------------------------------------------
|
||||
DriverChip(
|
||||
DsiDriverChip(
|
||||
"JC4880P443",
|
||||
width=480,
|
||||
height=800,
|
||||
@@ -58,7 +56,6 @@ DriverChip(
|
||||
vsync_front_porch=166,
|
||||
pclk_frequency="34MHz",
|
||||
lane_bit_rate="500Mbps",
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order="RGB",
|
||||
reset_pin=5,
|
||||
initsequence=[
|
||||
@@ -111,7 +108,7 @@ DriverChip(
|
||||
# * Horizontal Timing (hsync_pulse_width=20, hsync_back_porch=20, hsync_front_porch=40)
|
||||
# * Vertical Timing (vsync_pulse_width=4, vsync_back_porch=8, vsync_front_porch=20)
|
||||
# ----------------------------------------------------------------------------------------------------------------------
|
||||
DriverChip(
|
||||
DsiDriverChip(
|
||||
"JC8012P4A1",
|
||||
width=800,
|
||||
height=1280,
|
||||
@@ -123,7 +120,6 @@ DriverChip(
|
||||
vsync_front_porch=20,
|
||||
pclk_frequency="60MHz",
|
||||
lane_bit_rate="1Gbps",
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order="RGB",
|
||||
reset_pin=27,
|
||||
initsequence=[
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from esphome.components.mipi import DriverChip
|
||||
import esphome.config_validation as cv
|
||||
from . import DsiDriverChip
|
||||
|
||||
# fmt: off
|
||||
DriverChip(
|
||||
DsiDriverChip(
|
||||
"M5STACK-TAB5",
|
||||
height=1280,
|
||||
width=720,
|
||||
@@ -14,7 +13,6 @@ DriverChip(
|
||||
vsync_front_porch=20,
|
||||
pclk_frequency="60MHz",
|
||||
lane_bit_rate="730Mbps",
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order="RGB",
|
||||
initsequence=[
|
||||
(0xFF, 0x98, 0x81, 0x01), # Select Page 1
|
||||
@@ -56,7 +54,7 @@ DriverChip(
|
||||
],
|
||||
)
|
||||
|
||||
DriverChip(
|
||||
DsiDriverChip(
|
||||
"M5STACK-TAB5-V2",
|
||||
height=1280,
|
||||
width=720,
|
||||
@@ -68,7 +66,6 @@ DriverChip(
|
||||
vsync_front_porch=220,
|
||||
pclk_frequency="80MHz",
|
||||
lane_bit_rate="960Mbps",
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order="RGB",
|
||||
initsequence=[
|
||||
(0x01,),
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from esphome.components.mipi import DriverChip
|
||||
import esphome.config_validation as cv
|
||||
from . import DsiDriverChip
|
||||
|
||||
# Standalone display
|
||||
# Product page: https://www.seeedstudio.com/reTerminal-D1001-p-6729.html
|
||||
DriverChip(
|
||||
DsiDriverChip(
|
||||
"SEEED-RETERMINAL-D1001",
|
||||
height=1280,
|
||||
width=800,
|
||||
@@ -15,10 +14,10 @@ DriverChip(
|
||||
vsync_front_porch=30,
|
||||
pclk_frequency="80MHz",
|
||||
lane_bit_rate="1.5Gbps",
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order="RGB",
|
||||
enable_pin=[{"xl9535": None, "number": 0}, {"xl9535": None, "number": 7}],
|
||||
reset_pin={"xl9535": None, "number": 2},
|
||||
requires={"psram", "xl9535"},
|
||||
initsequence=(
|
||||
(0xE0, 0x00),
|
||||
(0xE1, 0x93),
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from esphome.components.mipi import DriverChip
|
||||
import esphome.config_validation as cv
|
||||
from . import DsiDriverChip
|
||||
|
||||
# fmt: off
|
||||
|
||||
# Source for parameters and initsequence:
|
||||
# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365_10_1
|
||||
# Product page: https://www.waveshare.com/wiki/ESP32-P4-Nano-StartPage
|
||||
JD9365_10_1_DSI_TOUCH_A = DriverChip(
|
||||
JD9365_10_1_DSI_TOUCH_A = DsiDriverChip(
|
||||
"WAVESHARE-P4-NANO-10.1",
|
||||
height=1280,
|
||||
width=800,
|
||||
@@ -18,7 +17,6 @@ JD9365_10_1_DSI_TOUCH_A = DriverChip(
|
||||
vsync_front_porch=30,
|
||||
pclk_frequency="80MHz",
|
||||
lane_bit_rate="1.5Gbps",
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order="RGB",
|
||||
initsequence=[
|
||||
(0xE0, 0x00), # select userpage
|
||||
@@ -65,7 +63,7 @@ JD9365_10_1_DSI_TOUCH_A.extend(
|
||||
# Source for parameters and initsequence:
|
||||
# https://github.com/espressif/esp-iot-solution/tree/master/components/display/lcd/esp_lcd_st7703
|
||||
# Product page: https://www.waveshare.com/wiki/ESP32-P4-86-Panel-ETH-2RO
|
||||
DriverChip(
|
||||
DsiDriverChip(
|
||||
"WAVESHARE-P4-86-PANEL",
|
||||
height=720,
|
||||
width=720,
|
||||
@@ -77,7 +75,6 @@ DriverChip(
|
||||
vsync_front_porch=20,
|
||||
pclk_frequency="38MHz",
|
||||
lane_bit_rate="480Mbps",
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order="RGB",
|
||||
reset_pin=27,
|
||||
initsequence=[
|
||||
@@ -109,7 +106,7 @@ DriverChip(
|
||||
# Source for parameters and initsequence:
|
||||
# https://github.com/espressif/esp-iot-solution/tree/master/components/display/lcd/esp_lcd_ek79007
|
||||
# Product page: https://www.waveshare.com/wiki/ESP32-P4-WIFI6-Touch-LCD-7B
|
||||
DriverChip(
|
||||
DsiDriverChip(
|
||||
"WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-7B",
|
||||
height=600,
|
||||
width=1024,
|
||||
@@ -139,7 +136,7 @@ DriverChip(
|
||||
# Source for parameters and initsequence:
|
||||
# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365
|
||||
# Product page: https://www.waveshare.com/wiki/ESP32-P4-WIFI6-Touch-LCD-3.4C
|
||||
JD9365_3_4_DSI_TOUCH_C = DriverChip(
|
||||
JD9365_3_4_DSI_TOUCH_C = DsiDriverChip(
|
||||
"WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-3.4C",
|
||||
height=800,
|
||||
width=800,
|
||||
@@ -151,7 +148,6 @@ JD9365_3_4_DSI_TOUCH_C = DriverChip(
|
||||
vsync_front_porch=24,
|
||||
pclk_frequency="80MHz",
|
||||
lane_bit_rate="1.5Gbps",
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order="RGB",
|
||||
initsequence=[
|
||||
(0xE0, 0x00), # select userpage
|
||||
@@ -197,7 +193,7 @@ JD9365_3_4_DSI_TOUCH_C.extend(
|
||||
# Source for parameters and initsequence:
|
||||
# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365
|
||||
# Product page: https://www.waveshare.com/wiki/ESP32-P4-WIFI6-Touch-LCD-4C
|
||||
JD9365_4_DSI_TOUCH_C = DriverChip(
|
||||
JD9365_4_DSI_TOUCH_C = DsiDriverChip(
|
||||
"WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-4C",
|
||||
height=720,
|
||||
width=720,
|
||||
@@ -209,7 +205,6 @@ JD9365_4_DSI_TOUCH_C = DriverChip(
|
||||
vsync_front_porch=24,
|
||||
pclk_frequency="80MHz",
|
||||
lane_bit_rate="1.5Gbps",
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order="RGB",
|
||||
initsequence=[
|
||||
(0xE0, 0x00), # select userpage
|
||||
@@ -255,7 +250,7 @@ JD9365_4_DSI_TOUCH_C.extend(
|
||||
# Source for parameters and initsequence:
|
||||
# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365
|
||||
# Product page: https://www.waveshare.com/wiki/8-DSI-TOUCH-A
|
||||
DriverChip(
|
||||
DsiDriverChip(
|
||||
"WAVESHARE-8-DSI-TOUCH-A",
|
||||
height=1280,
|
||||
width=800,
|
||||
@@ -267,7 +262,6 @@ DriverChip(
|
||||
vsync_front_porch=30,
|
||||
pclk_frequency="80MHz",
|
||||
lane_bit_rate="1.5Gbps",
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order="RGB",
|
||||
initsequence=[
|
||||
(0xE0, 0x00), # select userpage
|
||||
@@ -304,7 +298,7 @@ DriverChip(
|
||||
# Source for parameters and initsequence:
|
||||
# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_ili9881c
|
||||
# Product page: https://www.waveshare.com/wiki/7-DSI-TOUCH-A
|
||||
DriverChip(
|
||||
DsiDriverChip(
|
||||
"WAVESHARE-7-DSI-TOUCH-A",
|
||||
height=1280,
|
||||
width=720,
|
||||
|
||||
@@ -18,6 +18,8 @@ from esphome.components.mipi import (
|
||||
CONF_HSYNC_BACK_PORCH,
|
||||
CONF_HSYNC_FRONT_PORCH,
|
||||
CONF_HSYNC_PULSE_WIDTH,
|
||||
CONF_PCLK_FREQUENCY,
|
||||
CONF_PCLK_INVERTED,
|
||||
CONF_PCLK_PIN,
|
||||
CONF_PIXEL_MODE,
|
||||
CONF_USE_AXIS_FLIPS,
|
||||
@@ -34,9 +36,11 @@ from esphome.components.mipi import (
|
||||
power_of_two,
|
||||
requires_buffer,
|
||||
)
|
||||
from esphome.components.rpi_dpi_rgb.display import (
|
||||
CONF_PCLK_FREQUENCY,
|
||||
CONF_PCLK_INVERTED,
|
||||
from esphome.components.spi import (
|
||||
CONF_SPI_MODE,
|
||||
SPI_DATA_RATE_SCHEMA,
|
||||
SPI_MODE_OPTIONS,
|
||||
SPIComponent,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -48,7 +52,6 @@ from esphome.const import (
|
||||
CONF_DATA_RATE,
|
||||
CONF_DC_PIN,
|
||||
CONF_DIMENSIONS,
|
||||
CONF_DISABLED,
|
||||
CONF_ENABLE_PIN,
|
||||
CONF_GREEN,
|
||||
CONF_HSYNC_PIN,
|
||||
@@ -57,8 +60,6 @@ from esphome.const import (
|
||||
CONF_INIT_SEQUENCE,
|
||||
CONF_INVERT_COLORS,
|
||||
CONF_LAMBDA,
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_MODEL,
|
||||
CONF_NUMBER,
|
||||
CONF_RED,
|
||||
@@ -72,10 +73,10 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.final_validate import full_config
|
||||
|
||||
from ..spi import CONF_SPI_MODE, SPI_DATA_RATE_SCHEMA, SPI_MODE_OPTIONS, SPIComponent
|
||||
from . import models
|
||||
from .models import RgbDriverChip
|
||||
|
||||
DEPENDENCIES = ["esp32", "psram"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
|
||||
mipi_rgb_ns = cg.esphome_ns.namespace("mipi_rgb")
|
||||
mipi_rgb = mipi_rgb_ns.class_("MipiRgb", display.Display, cg.Component)
|
||||
@@ -86,7 +87,7 @@ ColorOrder = display.display_ns.enum("ColorMode")
|
||||
|
||||
DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema
|
||||
|
||||
DriverChip("CUSTOM")
|
||||
RgbDriverChip("CUSTOM")
|
||||
|
||||
# Import all models dynamically from the models package
|
||||
|
||||
@@ -120,16 +121,7 @@ def data_pin_set(length):
|
||||
|
||||
def model_schema(config):
|
||||
model = MODELS[config[CONF_MODEL].upper()]
|
||||
transform = cv.Any(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_MIRROR_X): cv.boolean,
|
||||
cv.Required(CONF_MIRROR_Y): cv.boolean,
|
||||
**model.swap_xy_schema(),
|
||||
}
|
||||
),
|
||||
cv.one_of(CONF_DISABLED, lower=True),
|
||||
)
|
||||
transform = model.transform_schema()
|
||||
# RPI model does not use an init sequence, indicates with empty list
|
||||
if model.initsequence is None:
|
||||
# Custom model requires an init sequence
|
||||
@@ -235,6 +227,7 @@ def _config_schema(config):
|
||||
only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4]),
|
||||
)(config)
|
||||
model = MODELS[config[CONF_MODEL].upper()]
|
||||
model.check_requirements()
|
||||
width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
|
||||
model.get_dimensions(config)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from esphome.components.mipi import DriverChip
|
||||
from esphome.const import CONF_SWAP_XY
|
||||
|
||||
|
||||
class RgbDriverChip(DriverChip):
|
||||
"""A driver chip for MIPI RGB displays."""
|
||||
|
||||
@property
|
||||
def transforms(self) -> set[str]:
|
||||
"""
|
||||
Return the set of transformations supported by this driver chip.
|
||||
RGB displays do not support axis swapping, so this method removes CONF_SWAP_XY
|
||||
"""
|
||||
return super().transforms - {CONF_SWAP_XY}
|
||||
@@ -5,6 +5,7 @@ st7701s.extend(
|
||||
width=480,
|
||||
height=480,
|
||||
data_rate="2MHz",
|
||||
requires={"psram"},
|
||||
cs_pin=39,
|
||||
de_pin=18,
|
||||
hsync_pin=16,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from esphome.config_validation import UNDEFINED
|
||||
|
||||
from .st7701s import ST7701S
|
||||
|
||||
# fmt: off
|
||||
@@ -8,10 +6,10 @@ ST7701S(
|
||||
width=480,
|
||||
height=480,
|
||||
invert_colors=False,
|
||||
swap_xy=UNDEFINED,
|
||||
spi_mode="MODE3",
|
||||
cs_pin={"xl9535": None, "number": 17},
|
||||
reset_pin={"xl9535": None, "number": 5},
|
||||
requires={"psram", "xl9535"},
|
||||
hsync_pin=39,
|
||||
vsync_pin=40,
|
||||
pclk_pin=41,
|
||||
@@ -57,9 +55,9 @@ t_rgb = ST7701S(
|
||||
height=480,
|
||||
pixel_mode="18bit",
|
||||
invert_colors=False,
|
||||
swap_xy=UNDEFINED,
|
||||
spi_mode="MODE3",
|
||||
cs_pin={"xl9535": None, "number": 3},
|
||||
requires={"psram", "xl9535"},
|
||||
de_pin=45,
|
||||
hsync_pin=47,
|
||||
vsync_pin=41,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from esphome.components.mipi import DriverChip
|
||||
from esphome.config_validation import UNDEFINED
|
||||
from . import RgbDriverChip
|
||||
|
||||
# A driver chip for Raspberry Pi MIPI RGB displays. These require no init sequence
|
||||
DriverChip(
|
||||
RgbDriverChip(
|
||||
"RPI",
|
||||
swap_xy=UNDEFINED,
|
||||
initsequence=(),
|
||||
)
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
from esphome.components.mipi import (
|
||||
MADCTL,
|
||||
MADCTL_ML,
|
||||
MADCTL_XFLIP,
|
||||
MODE_BGR,
|
||||
DriverChip,
|
||||
)
|
||||
from esphome.config_validation import UNDEFINED
|
||||
from esphome.components.mipi import MADCTL, MADCTL_ML, MADCTL_XFLIP, MODE_BGR
|
||||
from esphome.const import CONF_COLOR_ORDER, CONF_HEIGHT, CONF_MIRROR_X, CONF_MIRROR_Y
|
||||
|
||||
from . import RgbDriverChip
|
||||
|
||||
SDIR_CMD = 0xC7
|
||||
|
||||
|
||||
class ST7701S(DriverChip):
|
||||
class ST7701S(RgbDriverChip):
|
||||
# The ST7701s does not use the standard MADCTL bits for x/y mirroring
|
||||
def add_madctl(self, sequence: list, config: dict):
|
||||
transform = self.get_transform(config)
|
||||
@@ -45,7 +40,6 @@ st7701s = ST7701S(
|
||||
"ST7701S",
|
||||
width=480,
|
||||
height=864,
|
||||
swap_xy=UNDEFINED,
|
||||
hsync_front_porch=20,
|
||||
hsync_back_porch=10,
|
||||
hsync_pulse_width=10,
|
||||
@@ -85,6 +79,7 @@ st7701s.extend(
|
||||
height=480,
|
||||
invert_colors=True,
|
||||
pixel_mode="18bit",
|
||||
requires={"psram"},
|
||||
cs_pin=1,
|
||||
de_pin={
|
||||
"number": 45,
|
||||
@@ -117,6 +112,7 @@ st7701s.extend(
|
||||
vsync_pulse_width=8,
|
||||
vsync_back_porch=20,
|
||||
cs_pin={"pca9554": None, "number": 4},
|
||||
requires={"psram", "pca9554"},
|
||||
de_pin=18,
|
||||
hsync_pin=16,
|
||||
vsync_pin=17,
|
||||
@@ -134,6 +130,7 @@ st7701s.extend(
|
||||
width=480,
|
||||
height=480,
|
||||
pixel_mode="18bit",
|
||||
requires={"psram"},
|
||||
cs_pin=18,
|
||||
reset_pin=8,
|
||||
de_pin=17,
|
||||
@@ -177,6 +174,7 @@ st7701s.extend(
|
||||
width=480,
|
||||
height=480,
|
||||
pixel_mode="18bit",
|
||||
requires={"psram"},
|
||||
cs_pin=21,
|
||||
de_pin=39,
|
||||
vsync_pin=48,
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
from esphome.components.mipi import DriverChip
|
||||
from esphome.config_validation import UNDEFINED
|
||||
from . import RgbDriverChip
|
||||
|
||||
# fmt: off
|
||||
sunton = DriverChip(
|
||||
sunton = RgbDriverChip(
|
||||
"ESP32-8048S070",
|
||||
swap_xy=UNDEFINED,
|
||||
initsequence=(),
|
||||
width=800,
|
||||
height=480,
|
||||
pclk_frequency="12.5MHz",
|
||||
requires={"psram"},
|
||||
de_pin=41,
|
||||
hsync_pin=39,
|
||||
vsync_pin=40,
|
||||
@@ -28,7 +27,6 @@ sunton = DriverChip(
|
||||
|
||||
sunton.extend(
|
||||
"ESP32-8048S050",
|
||||
swap_xy=UNDEFINED,
|
||||
initsequence=(),
|
||||
width=800,
|
||||
height=480,
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
from esphome.components.mipi import DriverChip, delay
|
||||
from esphome.config_validation import UNDEFINED
|
||||
from esphome.components.mipi import delay
|
||||
|
||||
from . import RgbDriverChip
|
||||
from .st7701s import st7701s
|
||||
|
||||
# fmt: off
|
||||
wave_4_3 = DriverChip(
|
||||
wave_4_3 = RgbDriverChip(
|
||||
"ESP32-S3-TOUCH-LCD-4.3",
|
||||
swap_xy=UNDEFINED,
|
||||
initsequence=(),
|
||||
width=800,
|
||||
height=480,
|
||||
pclk_frequency="16MHz",
|
||||
reset_pin={"ch422g": None, "number": 3},
|
||||
enable_pin={"ch422g": None, "number": 2},
|
||||
requires={"psram", "ch422g"},
|
||||
de_pin=5,
|
||||
hsync_pin={"number": 46, "ignore_strapping_warning": True},
|
||||
vsync_pin={"number": 3, "ignore_strapping_warning": True},
|
||||
@@ -69,6 +69,7 @@ st7701s.extend(
|
||||
pclk_pin=41,
|
||||
pclk_frequency="12MHz",
|
||||
pclk_inverted=False,
|
||||
requires={"psram"},
|
||||
data_pins={
|
||||
"red": [46, 3, 8, 18, 17],
|
||||
"green": [14, 13, 12, 11, 10, 9],
|
||||
@@ -80,6 +81,7 @@ st7701s.extend(
|
||||
"WAVESHARE-3.16-320X820",
|
||||
width=320,
|
||||
height=820,
|
||||
requires={"psram"},
|
||||
de_pin=40,
|
||||
hsync_pin=38,
|
||||
vsync_pin=39,
|
||||
|
||||
@@ -41,14 +41,11 @@ from esphome.const import (
|
||||
CONF_DATA_RATE,
|
||||
CONF_DC_PIN,
|
||||
CONF_DIMENSIONS,
|
||||
CONF_DISABLED,
|
||||
CONF_ENABLE_PIN,
|
||||
CONF_ID,
|
||||
CONF_INIT_SEQUENCE,
|
||||
CONF_INVERT_COLORS,
|
||||
CONF_LAMBDA,
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_MODEL,
|
||||
CONF_RESET_PIN,
|
||||
CONF_ROTATION,
|
||||
@@ -138,16 +135,7 @@ def denominator(config):
|
||||
def model_schema(config):
|
||||
model = MODELS[config[CONF_MODEL]]
|
||||
bus_mode = config[CONF_BUS_MODE]
|
||||
transform = cv.Any(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_MIRROR_X): cv.boolean,
|
||||
cv.Required(CONF_MIRROR_Y): cv.boolean,
|
||||
**model.swap_xy_schema(),
|
||||
}
|
||||
),
|
||||
cv.one_of(CONF_DISABLED, lower=True),
|
||||
)
|
||||
transform = model.transform_schema()
|
||||
# CUSTOM model will need to provide a custom init sequence
|
||||
iseqconf = (
|
||||
cv.Required(CONF_INIT_SEQUENCE)
|
||||
@@ -265,6 +253,7 @@ def customise_schema(config):
|
||||
extra=ALLOW_EXTRA,
|
||||
)(config)
|
||||
model = MODELS[config[CONF_MODEL]]
|
||||
model.check_requirements()
|
||||
bus_modes = (TYPE_SINGLE, TYPE_QUAD, TYPE_OCTAL)
|
||||
config = cv.Schema(
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ ST7789V.extend(
|
||||
mirror_x=True,
|
||||
mirror_y=True,
|
||||
data_rate="80MHz",
|
||||
requires={"psram"},
|
||||
)
|
||||
|
||||
ST7789V.extend(
|
||||
@@ -25,4 +26,5 @@ ST7789V.extend(
|
||||
dc_pin=39,
|
||||
reset_pin=40,
|
||||
invert_colors=True,
|
||||
requires={"psram"},
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ from esphome.components.mipi import (
|
||||
delay,
|
||||
)
|
||||
from esphome.components.spi import TYPE_QUAD
|
||||
from esphome.config_validation import UNDEFINED
|
||||
from esphome.const import CONF_MIRROR_X, CONF_MIRROR_Y
|
||||
|
||||
DriverChip(
|
||||
"T-DISPLAY-S3-AMOLED",
|
||||
@@ -29,6 +29,7 @@ DriverChip(
|
||||
brightness=0xD0,
|
||||
color_order=MODE_RGB,
|
||||
no_slpout=True, # SLPOUT is in the init sequence, early
|
||||
requires={"psram"},
|
||||
initsequence=(SLPOUT,),
|
||||
)
|
||||
|
||||
@@ -43,6 +44,7 @@ DriverChip(
|
||||
data_rate="40MHz",
|
||||
brightness=0xD0,
|
||||
color_order=MODE_RGB,
|
||||
requires={"psram"},
|
||||
initsequence=(
|
||||
(PAGESEL, 4),
|
||||
(0x6A, 0x00),
|
||||
@@ -90,6 +92,7 @@ T4_S3_AMOLED = RM690B0.extend(
|
||||
reset_pin=13,
|
||||
enable_pin=9,
|
||||
bus_mode=TYPE_QUAD,
|
||||
requires={"psram"},
|
||||
)
|
||||
|
||||
CO5300 = DriverChip(
|
||||
@@ -98,7 +101,7 @@ CO5300 = DriverChip(
|
||||
color_order=MODE_RGB,
|
||||
bus_mode=TYPE_QUAD,
|
||||
no_slpout=True,
|
||||
swap_xy=UNDEFINED,
|
||||
transforms={CONF_MIRROR_X, CONF_MIRROR_Y},
|
||||
width=480,
|
||||
height=480,
|
||||
initsequence=(
|
||||
|
||||
@@ -314,6 +314,7 @@ DriverChip(
|
||||
data_rate="40MHz",
|
||||
dc_pin=4,
|
||||
cs_pin=5,
|
||||
requires={"psram"},
|
||||
# reset_pin={CONF_INVERTED: True, CONF_NUMBER: 48},
|
||||
initsequence=(
|
||||
(0xEF, 0x03, 0x80, 0x02),
|
||||
@@ -379,6 +380,7 @@ DriverChip(
|
||||
cs_pin=5,
|
||||
dc_pin=4,
|
||||
reset_pin=48,
|
||||
requires={"psram"},
|
||||
initsequence=(
|
||||
(0xEF, 0x03, 0x80, 0x02),
|
||||
(0xCF, 0x00, 0xC1, 0x30),
|
||||
@@ -711,6 +713,7 @@ ST7796.extend(
|
||||
reset_pin=4,
|
||||
dc_pin={"number": 0, "ignore_strapping_warning": True},
|
||||
invert_colors=True,
|
||||
requires={"psram"},
|
||||
)
|
||||
|
||||
ST7789V.extend(
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
from esphome.components.mipi import MODE_RGB, DriverChip
|
||||
from esphome.components.spi import TYPE_QUAD
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_IGNORE_STRAPPING_WARNING, CONF_NUMBER
|
||||
from esphome.const import (
|
||||
CONF_IGNORE_STRAPPING_WARNING,
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_NUMBER,
|
||||
)
|
||||
|
||||
AXS15231 = DriverChip(
|
||||
"AXS15231",
|
||||
draw_rounding=8,
|
||||
swap_xy=cv.UNDEFINED,
|
||||
transforms={CONF_MIRROR_X, CONF_MIRROR_Y},
|
||||
color_order=MODE_RGB,
|
||||
bus_mode=TYPE_QUAD,
|
||||
initsequence=(
|
||||
@@ -22,6 +26,7 @@ AXS15231.extend(
|
||||
height=480,
|
||||
cs_pin={CONF_NUMBER: 45, CONF_IGNORE_STRAPPING_WARNING: True},
|
||||
data_rate="40MHz",
|
||||
requires={"psram"},
|
||||
)
|
||||
|
||||
DriverChip(
|
||||
@@ -36,6 +41,7 @@ DriverChip(
|
||||
color_order=MODE_RGB,
|
||||
bus_mode=TYPE_QUAD,
|
||||
data_rate="40MHz",
|
||||
requires={"psram"},
|
||||
initsequence=(
|
||||
(0xF0, 0x08),
|
||||
(0xF2, 0x08),
|
||||
@@ -267,6 +273,7 @@ DriverChip(
|
||||
color_order=MODE_RGB,
|
||||
bus_mode=TYPE_QUAD,
|
||||
data_rate="40MHz",
|
||||
requires={"psram"},
|
||||
initsequence=(
|
||||
(0xF0, 0x28),
|
||||
(0xF2, 0x28),
|
||||
@@ -495,6 +502,7 @@ DriverChip(
|
||||
color_order=MODE_RGB,
|
||||
bus_mode=TYPE_QUAD,
|
||||
data_rate="20MHz",
|
||||
requires={"psram"},
|
||||
initsequence=(
|
||||
(0xFF, 0xA5),
|
||||
(0x41, 0x03),
|
||||
|
||||
@@ -10,4 +10,5 @@ ST7789V.extend(
|
||||
cs_pin=22,
|
||||
dc_pin=21,
|
||||
reset_pin=18,
|
||||
requires={"psram"},
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@ ST7789V.extend(
|
||||
dc_pin=13,
|
||||
reset_pin=9,
|
||||
data_rate="80MHz",
|
||||
requires={"psram"},
|
||||
)
|
||||
|
||||
ST7789V.extend(
|
||||
@@ -42,6 +43,7 @@ ST7789V.extend(
|
||||
enable_pin=[9, 15],
|
||||
data_rate="10MHz",
|
||||
bus_mode=TYPE_OCTAL,
|
||||
requires={"psram"},
|
||||
)
|
||||
|
||||
ST7796.extend(
|
||||
@@ -55,4 +57,5 @@ ST7796.extend(
|
||||
dc_pin=9,
|
||||
backlight_pin=48,
|
||||
invert_colors=True,
|
||||
requires={"psram"},
|
||||
)
|
||||
|
||||
@@ -49,6 +49,7 @@ ILI9341.extend(
|
||||
invert_colors=True,
|
||||
pixel_mode="18bit",
|
||||
data_rate="40MHz",
|
||||
requires={"psram"},
|
||||
)
|
||||
|
||||
GC9107 = ST7789V.extend(
|
||||
@@ -68,4 +69,5 @@ GC9107.extend(
|
||||
reset_pin=48,
|
||||
dc_pin=42,
|
||||
cs_pin=14,
|
||||
requires={"psram"},
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ from esphome.components.mipi import (
|
||||
PWSET,
|
||||
DriverChip,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_MIRROR_X, CONF_MIRROR_Y
|
||||
|
||||
from .amoled import CO5300
|
||||
from .ili import ILI9488_A, ST7789V
|
||||
@@ -155,7 +155,7 @@ ST7789P = DriverChip(
|
||||
|
||||
ILI9488_A.extend(
|
||||
"PICO-RESTOUCH-LCD-3.5",
|
||||
swap_xy=cv.UNDEFINED,
|
||||
transforms={CONF_MIRROR_X, CONF_MIRROR_Y},
|
||||
spi_16=True,
|
||||
pixel_mode="16bit",
|
||||
mirror_x=True,
|
||||
@@ -175,6 +175,7 @@ CO5300.extend(
|
||||
offset_width=6,
|
||||
cs_pin=12,
|
||||
reset_pin=39,
|
||||
requires={"psram"},
|
||||
)
|
||||
|
||||
# Waveshare ESP32-S3 Touch AMOLED 2.16" (CO5300 controller)
|
||||
@@ -189,6 +190,7 @@ CO5300.extend(
|
||||
cs_pin=12,
|
||||
reset_pin=39,
|
||||
data_rate="40MHz",
|
||||
requires={"psram"},
|
||||
)
|
||||
|
||||
AXS15231.extend(
|
||||
@@ -198,6 +200,7 @@ AXS15231.extend(
|
||||
data_rate="80MHz",
|
||||
cs_pin=9,
|
||||
reset_pin=21,
|
||||
requires={"psram"},
|
||||
)
|
||||
|
||||
# Waveshare 1.83-v2
|
||||
@@ -281,6 +284,7 @@ ST7789V.extend(
|
||||
offset_height=40,
|
||||
invert_colors=True,
|
||||
data_rate="40MHz",
|
||||
requires={"psram"},
|
||||
)
|
||||
|
||||
CO5300.extend(
|
||||
@@ -291,4 +295,5 @@ CO5300.extend(
|
||||
cs_pin=9,
|
||||
reset_pin=21,
|
||||
enable_pin=1,
|
||||
requires={"psram"},
|
||||
)
|
||||
|
||||
@@ -574,6 +574,9 @@ class EsphomeCore:
|
||||
self.build_path: Path | None = None
|
||||
# The validated configuration, this is None until the config has been validated
|
||||
self.config: ConfigType | None = None
|
||||
# The raw configuration as read from YAML (after packages/substitutions),
|
||||
# available during validation before the config is fully validated
|
||||
self.raw_config: ConfigType | None = None
|
||||
# YAML frontmatter loaded from user YAML files. Frontmatter is a leading
|
||||
# YAML document separated by `---` from the actual configuration. It is
|
||||
# ignored by config validation and code generation, but kept here so it
|
||||
@@ -650,6 +653,7 @@ class EsphomeCore:
|
||||
self.config_path = None
|
||||
self.build_path = None
|
||||
self.config = None
|
||||
self.raw_config = None
|
||||
self.frontmatter = {}
|
||||
self.event_loop = _FakeEventLoop()
|
||||
self.task_counter = 0
|
||||
|
||||
@@ -24,6 +24,9 @@ spi:
|
||||
mosi_pin: 6
|
||||
clk_pin: 7
|
||||
|
||||
psram:
|
||||
mode: quad
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
id: lcd_display
|
||||
|
||||
@@ -19,6 +19,9 @@ spi:
|
||||
mosi_pin: 6
|
||||
clk_pin: 7
|
||||
|
||||
psram:
|
||||
mode: quad
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
id: lcd_display
|
||||
|
||||
@@ -4,6 +4,9 @@ esphome:
|
||||
esp32:
|
||||
board: esp32s3box
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
|
||||
image:
|
||||
defaults:
|
||||
type: rgb565
|
||||
|
||||
@@ -71,6 +71,18 @@ def test_configuration_errors(set_core_config: SetCoreConfigCallable) -> None:
|
||||
}
|
||||
)
|
||||
|
||||
# DSI displays cannot swap axes; enabling swap_xy reports a clear error.
|
||||
with pytest.raises(cv.Invalid, match="'swap_xy' is not supported by this model"):
|
||||
CONFIG_SCHEMA(
|
||||
{
|
||||
"model": "custom",
|
||||
"init_sequence": [[0xA0, 0x01]],
|
||||
"lane_bit_rate": "1.5Gbps",
|
||||
"dimensions": {"width": 320, "height": 240},
|
||||
"transform": {"mirror_x": True, "mirror_y": True, "swap_xy": True},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_configuration_success(set_core_config: SetCoreConfigCallable) -> None:
|
||||
"""Test successful configuration validation."""
|
||||
@@ -116,6 +128,33 @@ def test_configuration_success(set_core_config: SetCoreConfigCallable) -> None:
|
||||
CONFIG_SCHEMA(config)
|
||||
|
||||
|
||||
def test_metadata_records_rotation(set_core_config: SetCoreConfigCallable) -> None:
|
||||
"""A configured display rotation is recorded in the metadata.
|
||||
|
||||
LVGL relies on this to flag a rotation set in the display config (see the
|
||||
mipi_spi tests for the end-to-end LVGL rejection).
|
||||
"""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32-p4-evboard", KEY_VARIANT: VARIANT_ESP32P4},
|
||||
)
|
||||
|
||||
from esphome.components.display import get_display_metadata
|
||||
from esphome.components.mipi_dsi.display import CONFIG_SCHEMA
|
||||
|
||||
base = {
|
||||
"model": "custom",
|
||||
"init_sequence": [[0xA0, 0x01]],
|
||||
"lane_bit_rate": "1.5Gbps",
|
||||
"dimensions": {"width": 320, "height": 240},
|
||||
}
|
||||
config = CONFIG_SCHEMA({**base, "id": "rotated", "rotation": 90})
|
||||
assert get_display_metadata(config["id"]).rotation == 90
|
||||
|
||||
config = CONFIG_SCHEMA({**base, "id": "unrotated"})
|
||||
assert get_display_metadata(config["id"]).rotation == 0
|
||||
|
||||
|
||||
def test_code_generation(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_fixture_path: Callable[[str], Path],
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Tests for mipi_rgb configuration validation, in particular the per-model
|
||||
``requires`` component check (see esphome.components.mipi.DriverChip.check_requirements)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32S3
|
||||
from esphome.components.mipi_rgb.display import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
|
||||
|
||||
# Importing pca9554 registers its pin schema with pins.PIN_SCHEMA_REGISTRY so that
|
||||
# models (e.g. SEEED-INDICATOR-D1) that reference pca9554-backed pins in their
|
||||
# defaults can be validated by the mipi_rgb CONFIG_SCHEMA in this test.
|
||||
import esphome.components.pca9554 # noqa: F401
|
||||
from esphome.const import PlatformFramework
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
def _validated(config: ConfigType) -> ConfigType:
|
||||
"""Run the component config schema followed by the final validation."""
|
||||
config = CONFIG_SCHEMA(config)
|
||||
FINAL_VALIDATE_SCHEMA(config)
|
||||
return config
|
||||
|
||||
|
||||
def test_model_requires_psram(set_core_config: SetCoreConfigCallable) -> None:
|
||||
"""A model known to have PSRAM on its board rejects a config without it.
|
||||
|
||||
RGB parallel displays always need a full framebuffer, so every model in this
|
||||
component is expected to carry ``requires={"psram", ...}``. This board has no
|
||||
other requirements, so its check is exercised in isolation here.
|
||||
"""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
|
||||
)
|
||||
CORE.raw_config = {}
|
||||
|
||||
with pytest.raises(
|
||||
cv.Invalid,
|
||||
match=r"ESP32-8048S070 requires component 'psram' to be configured",
|
||||
):
|
||||
_validated({"model": "ESP32-8048S070"})
|
||||
|
||||
|
||||
def test_model_requires_psram_satisfied(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Any,
|
||||
) -> None:
|
||||
"""The same board model validates once PSRAM is configured."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
|
||||
)
|
||||
set_component_config("psram", True)
|
||||
CORE.raw_config = {"psram": True}
|
||||
|
||||
config = _validated({"model": "ESP32-8048S070"})
|
||||
assert config["model"] == "ESP32-8048S070"
|
||||
|
||||
|
||||
def test_model_requires_psram_and_expander(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Any,
|
||||
) -> None:
|
||||
"""A model that also depends on an I2C GPIO expander lists both when missing."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
|
||||
)
|
||||
# Only satisfy one of the two requirements.
|
||||
set_component_config("psram", True)
|
||||
CORE.raw_config = {"psram": True}
|
||||
|
||||
with pytest.raises(
|
||||
cv.Invalid,
|
||||
match=r"SEEED-INDICATOR-D1 requires component 'pca9554' to be configured",
|
||||
):
|
||||
_validated(
|
||||
{
|
||||
"model": "SEEED-INDICATOR-D1",
|
||||
"spi_id": "spi_bus",
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,137 @@
|
||||
"""Tests for mipi_rgb configuration validation."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
|
||||
# Importing these registers their pin schemas with pins.PIN_SCHEMA_REGISTRY so that
|
||||
# models referencing IO-expander-backed pins in their defaults (e.g. the LilyGO
|
||||
# T-RGB boards via xl9535, SEEED-INDICATOR-D1 via pca9554, or the Waveshare panels
|
||||
# via ch422g) can be validated by the mipi_rgb CONFIG_SCHEMA in this test.
|
||||
import esphome.components.ch422g # noqa: F401
|
||||
from esphome.components.display import get_display_metadata
|
||||
from esphome.components.esp32 import KEY_BOARD, VARIANT_ESP32S3
|
||||
import esphome.components.pca9554 # noqa: F401
|
||||
import esphome.components.xl9535 # noqa: F401
|
||||
from esphome.const import (
|
||||
CONF_BLUE,
|
||||
CONF_DIMENSIONS,
|
||||
CONF_GREEN,
|
||||
CONF_HEIGHT,
|
||||
CONF_INIT_SEQUENCE,
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_RED,
|
||||
CONF_SWAP_XY,
|
||||
CONF_WIDTH,
|
||||
KEY_VARIANT,
|
||||
PlatformFramework,
|
||||
)
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
# A generic set of data pins so that models without a default pin assignment
|
||||
# (e.g. CUSTOM and RPI) still validate.
|
||||
DATA_PINS = {
|
||||
CONF_RED: [1, 2, 3, 4, 5],
|
||||
CONF_GREEN: [6, 7, 8, 9, 10, 11],
|
||||
CONF_BLUE: [12, 13, 14, 15, 16],
|
||||
}
|
||||
|
||||
|
||||
def _set_s3(set_core_config: SetCoreConfigCallable) -> None:
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={
|
||||
KEY_BOARD: "esp32-s3-devkitc-1",
|
||||
KEY_VARIANT: VARIANT_ESP32S3,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_configuration_success(set_core_config: SetCoreConfigCallable) -> None:
|
||||
"""Every predefined model validates once required defaults are supplied."""
|
||||
_set_s3(set_core_config)
|
||||
|
||||
from esphome.components.mipi_rgb.display import CONFIG_SCHEMA, MODELS
|
||||
|
||||
for name, model in MODELS.items():
|
||||
config = {"model": name, "data_pins": DATA_PINS, "pclk_pin": 21}
|
||||
if model.initsequence is None:
|
||||
config[CONF_INIT_SEQUENCE] = [[0xA0, 0x01]]
|
||||
if not model.get_default(CONF_WIDTH):
|
||||
config[CONF_DIMENSIONS] = {CONF_WIDTH: 480, CONF_HEIGHT: 480}
|
||||
CONFIG_SCHEMA(config)
|
||||
|
||||
|
||||
def test_transform_matches_model_support(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""The transform schema only accepts the axes a model actually supports."""
|
||||
_set_s3(set_core_config)
|
||||
|
||||
from esphome.components.mipi_rgb.display import CONFIG_SCHEMA, MODELS
|
||||
|
||||
# ESP32-8048S070 supports both mirror axes but not swap_xy (RGB displays
|
||||
# never support axis swapping).
|
||||
model = MODELS["ESP32-8048S070"]
|
||||
assert model.transforms == {CONF_MIRROR_X, CONF_MIRROR_Y}
|
||||
|
||||
base = {"model": "ESP32-8048S070", "data_pins": DATA_PINS, "pclk_pin": 21}
|
||||
CONFIG_SCHEMA({**base, "transform": {"mirror_x": True, "mirror_y": False}})
|
||||
|
||||
# An unsupported axis may be explicitly disabled (a harmless no-op)...
|
||||
CONFIG_SCHEMA(
|
||||
{**base, "transform": {"mirror_x": True, "mirror_y": False, "swap_xy": False}}
|
||||
)
|
||||
|
||||
# ...but enabling it reports a clear, model-specific error.
|
||||
with pytest.raises(cv.Invalid, match="'swap_xy' is not supported by this model"):
|
||||
CONFIG_SCHEMA(
|
||||
{
|
||||
**base,
|
||||
"transform": {"mirror_x": True, "mirror_y": False, "swap_xy": True},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_st7701s_only_supports_mirror_x(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""ST7701S panels shorter than full height only expose mirror_x.
|
||||
|
||||
mirror_y only works at full height (864px), so the LilyGO 480px panels must
|
||||
reject a mirror_y transform.
|
||||
"""
|
||||
_set_s3(set_core_config)
|
||||
|
||||
from esphome.components.mipi_rgb.display import CONFIG_SCHEMA, MODELS
|
||||
|
||||
model = MODELS["T-RGB-2.1"]
|
||||
assert model.transforms == {CONF_MIRROR_X}
|
||||
assert CONF_SWAP_XY not in model.transforms
|
||||
|
||||
base = {"model": "T-RGB-2.1"}
|
||||
CONFIG_SCHEMA({**base, "transform": {"mirror_x": True}})
|
||||
|
||||
with pytest.raises(cv.Invalid, match="'mirror_y' is not supported by this model"):
|
||||
CONFIG_SCHEMA({**base, "transform": {"mirror_x": True, "mirror_y": True}})
|
||||
|
||||
|
||||
def test_metadata_records_rotation(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""A configured display rotation is recorded in the metadata.
|
||||
|
||||
LVGL relies on this to flag a rotation set in the display config (see the
|
||||
mipi_spi tests for the end-to-end LVGL rejection).
|
||||
"""
|
||||
_set_s3(set_core_config)
|
||||
|
||||
from esphome.components.mipi_rgb.display import CONFIG_SCHEMA
|
||||
|
||||
base = {"model": "ESP32-8048S070", "data_pins": DATA_PINS, "pclk_pin": 21}
|
||||
config = CONFIG_SCHEMA({**base, "id": "rotated", "rotation": 90})
|
||||
assert get_display_metadata(config["id"]).rotation == 90
|
||||
|
||||
config = CONFIG_SCHEMA({**base, "id": "unrotated"})
|
||||
assert get_display_metadata(config["id"]).rotation == 0
|
||||
@@ -3,6 +3,9 @@
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.const import BYTE_ORDER_BIG
|
||||
from esphome.components.display import get_all_display_metadata, get_display_metadata
|
||||
from esphome.components.esp32 import (
|
||||
@@ -13,6 +16,7 @@ from esphome.components.esp32 import (
|
||||
)
|
||||
from esphome.components.mipi_spi.display import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
|
||||
from esphome.const import PlatformFramework
|
||||
from esphome.core import ID
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
@@ -23,6 +27,18 @@ def validated_config(config):
|
||||
return config
|
||||
|
||||
|
||||
def _lvgl_config(display_id: str) -> dict:
|
||||
"""Build a minimal LVGL config dict referencing the given display id."""
|
||||
return {
|
||||
"displays": [ID(display_id, True)],
|
||||
"log_level": "WARN",
|
||||
"color_depth": 16,
|
||||
"transparency_key": 0x000400,
|
||||
"draw_rounding": 2,
|
||||
"buffer_size": 0,
|
||||
}
|
||||
|
||||
|
||||
def test_metadata_native_quad_default_test_card(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
@@ -91,7 +107,7 @@ def test_metadata_no_swap_xy_not_full_hardware_rotation(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
|
||||
)
|
||||
# JC3248W535 has swap_xy=cv.UNDEFINED -> transforms={mirror_x, mirror_y} only
|
||||
# JC3248W535 has transforms={mirror_x, mirror_y} only
|
||||
config = CONFIG_SCHEMA({"model": "JC3248W535", "id": "jc3248w535"})
|
||||
meta = get_display_metadata(config["id"])
|
||||
assert meta is not None
|
||||
@@ -166,3 +182,69 @@ def test_metadata_via_code_generation_lvgl(
|
||||
assert meta.height == 160
|
||||
assert meta.has_hardware_rotation is True
|
||||
assert meta.byte_order == BYTE_ORDER_BIG
|
||||
|
||||
|
||||
def test_metadata_records_rotation(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""A configured display rotation is recorded in the metadata."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
config = CONFIG_SCHEMA(
|
||||
{"model": "ST7735", "dc_pin": 18, "id": "rotated", "rotation": 90}
|
||||
)
|
||||
meta = get_display_metadata(config["id"])
|
||||
assert meta is not None
|
||||
assert meta.rotation == 90
|
||||
|
||||
|
||||
def test_metadata_rotation_defaults_to_zero(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""A display without a rotation reports rotation 0 in its metadata."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
config = CONFIG_SCHEMA({"model": "ST7735", "dc_pin": 18, "id": "unrotated"})
|
||||
meta = get_display_metadata(config["id"])
|
||||
assert meta is not None
|
||||
assert meta.rotation == 0
|
||||
|
||||
|
||||
def test_rotation_flagged_when_used_with_lvgl(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""A display with a rotation is rejected when driven by LVGL.
|
||||
|
||||
LVGL manages its own rotation, so a rotation set in the display config must be
|
||||
flagged and the user directed to configure it in the LVGL block instead. This
|
||||
exercises the full chain: the mipi_spi schema records the rotation in the
|
||||
display metadata, and LVGL's final validation reports it.
|
||||
"""
|
||||
from esphome.components.lvgl import final_validation
|
||||
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
CONFIG_SCHEMA({"model": "ST7735", "dc_pin": 18, "id": "rotated", "rotation": 90})
|
||||
with pytest.raises(cv.Invalid, match="rotation.*not compatible with LVGL"):
|
||||
final_validation([_lvgl_config("rotated")])
|
||||
|
||||
|
||||
def test_no_rotation_accepted_with_lvgl(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""A display without a rotation validates cleanly when driven by LVGL."""
|
||||
from esphome.components.lvgl import final_validation
|
||||
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
CONFIG_SCHEMA({"model": "ST7735", "dc_pin": 18, "id": "unrotated"})
|
||||
# Should not raise.
|
||||
final_validation([_lvgl_config("unrotated")])
|
||||
|
||||
@@ -6,10 +6,13 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.display import CONF_SHOW_TEST_CARD
|
||||
from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32
|
||||
from esphome.components.mipi import DriverChip
|
||||
from esphome.components.mipi_spi.display import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
|
||||
from esphome.const import CONF_BUFFER_SIZE, PlatformFramework
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
@@ -183,3 +186,77 @@ def test_buffer_size_selected_when_lvgl_with_test_card(
|
||||
)
|
||||
|
||||
assert config[CONF_BUFFER_SIZE] == pytest.approx(1.0 / 4)
|
||||
|
||||
|
||||
def test_requires_missing_single_component_raises() -> None:
|
||||
"""A model that requires a single component raises when it is absent."""
|
||||
CORE.raw_config = {}
|
||||
chip = DriverChip("TEST-REQUIRES-PSRAM", requires={"psram"})
|
||||
|
||||
with pytest.raises(
|
||||
cv.Invalid,
|
||||
match=r"TEST-REQUIRES-PSRAM requires component 'psram' to be configured",
|
||||
):
|
||||
chip.check_requirements()
|
||||
|
||||
|
||||
def test_requires_missing_multiple_components_raises() -> None:
|
||||
"""A model that requires several components lists all the missing ones, pluralized."""
|
||||
CORE.raw_config = {}
|
||||
chip = DriverChip("TEST-REQUIRES-MULTI", requires={"psram", "pca9554"})
|
||||
|
||||
with pytest.raises(
|
||||
cv.Invalid,
|
||||
match=r"TEST-REQUIRES-MULTI requires components '.*' to be configured",
|
||||
) as excinfo:
|
||||
chip.check_requirements()
|
||||
assert "psram" in str(excinfo.value)
|
||||
assert "pca9554" in str(excinfo.value)
|
||||
|
||||
|
||||
def test_requires_satisfied_does_not_raise() -> None:
|
||||
"""No error is raised once all the required components are configured."""
|
||||
CORE.raw_config = {"psram": True, "pca9554": []}
|
||||
chip = DriverChip("TEST-REQUIRES-SATISFIED", requires={"psram", "pca9554"})
|
||||
|
||||
chip.check_requirements() # Should not raise
|
||||
|
||||
|
||||
def test_requires_absent_does_not_raise() -> None:
|
||||
"""Models without a requires set are unaffected by the check."""
|
||||
CORE.raw_config = {}
|
||||
chip = DriverChip("TEST-REQUIRES-NONE")
|
||||
|
||||
chip.check_requirements() # Should not raise
|
||||
|
||||
|
||||
def test_predefined_model_requires_psram(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""A predefined board model known to have PSRAM rejects a config without it."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
CORE.raw_config = {}
|
||||
|
||||
with pytest.raises(
|
||||
cv.Invalid, match=r"S3BOX requires component 'psram' to be configured"
|
||||
):
|
||||
_validated({"model": "s3box"})
|
||||
|
||||
|
||||
def test_predefined_model_requires_psram_satisfied(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Any,
|
||||
) -> None:
|
||||
"""The same board model validates once PSRAM is configured."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
set_component_config("psram", True)
|
||||
CORE.raw_config = {"psram": True}
|
||||
|
||||
config = _validated({"model": "s3box"})
|
||||
assert config["model"] == "S3BOX"
|
||||
|
||||
@@ -136,7 +136,7 @@ def test_dimension_validation(
|
||||
"model": "JC3248W535",
|
||||
"transform": {"mirror_x": False, "mirror_y": True, "swap_xy": True},
|
||||
},
|
||||
"Axis swapping not supported by this model",
|
||||
"'swap_xy' is not supported by this model",
|
||||
id="axis_swapping_not_supported",
|
||||
),
|
||||
pytest.param(
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -222,6 +223,7 @@ class TestNewModelVariants:
|
||||
def test_m5core2_with_native_dimensions(
|
||||
self,
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""Test M5CORE2 variant with reset native_width and native_height."""
|
||||
set_core_config(
|
||||
@@ -231,6 +233,8 @@ class TestNewModelVariants:
|
||||
KEY_VARIANT: VARIANT_ESP32S3,
|
||||
},
|
||||
)
|
||||
# M5CORE2 has PSRAM on board and requires it to be configured
|
||||
set_component_config("psram", True)
|
||||
|
||||
# M5CORE2 should validate successfully
|
||||
config = validated_config({"model": "M5CORE2"})
|
||||
|
||||
@@ -24,6 +24,9 @@ spi:
|
||||
mosi_pin: 6
|
||||
clk_pin: 7
|
||||
|
||||
psram:
|
||||
mode: quad
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
id: lcd_display
|
||||
|
||||
@@ -23,6 +23,9 @@ spi:
|
||||
mosi_pin: 6
|
||||
clk_pin: 7
|
||||
|
||||
psram:
|
||||
mode: quad
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
id: lcd_display
|
||||
|
||||
Reference in New Issue
Block a user