From f3f79c301b529cada8e00680e435e4d3399b6b0c Mon Sep 17 00:00:00 2001 From: Guillaume Rischard Date: Tue, 6 Oct 2026 22:24:08 -0400 Subject: [PATCH] [mipi_spi] Add Waveshare ESP32-S3-Touch-LCD-1.46 SPD2010 model (#19056) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/mipi_spi/models/spd2010.py | 418 ++++++++++++++++++ tests/component_tests/conftest.py | 14 +- .../mipi_spi/fixtures/spd2010.yaml | 23 + tests/component_tests/mipi_spi/test_init.py | 116 +++++ 4 files changed, 569 insertions(+), 2 deletions(-) create mode 100644 esphome/components/mipi_spi/models/spd2010.py create mode 100644 tests/component_tests/mipi_spi/fixtures/spd2010.yaml diff --git a/esphome/components/mipi_spi/models/spd2010.py b/esphome/components/mipi_spi/models/spd2010.py new file mode 100644 index 0000000000..b5893f179a --- /dev/null +++ b/esphome/components/mipi_spi/models/spd2010.py @@ -0,0 +1,418 @@ +# SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +# Adapted from esp_lcd_spd2010 2.0.0~1 for ESPHome. +# https://github.com/espressif/esp-iot-solution/tree/master/components/display/lcd/esp_lcd_spd2010 + +from esphome.components.mipi import MODE_RGB, DriverChip, delay +from esphome.components.spi import TYPE_QUAD +from esphome.const import CONF_MIRROR_X, CONF_MIRROR_Y + +# fmt: off +DriverChip( + "WAVESHARE-ESP32-S3-TOUCH-LCD-1.46", + width=412, + height=412, + bus_mode=TYPE_QUAD, + spi_mode="MODE0", + data_rate="40MHz", + cs_pin=21, + reset_pin={"pca9554": None, "number": 1}, + color_order=MODE_RGB, + draw_rounding=4, + transforms={CONF_MIRROR_X, CONF_MIRROR_Y}, + use_axis_flips=True, + reset_delay=20, + requires={"pca9554"}, + initsequence=( + delay(100), + (0xFF, 0x20, 0x10, 0x10), + (0x0C, 0x11), + (0x10, 0x02), + (0x11, 0x11), + (0x15, 0x42), + (0x16, 0x11), + (0x1A, 0x02), + (0x1B, 0x11), + (0x61, 0x80), + (0x62, 0x80), + (0x54, 0x44), + (0x58, 0x88), + (0x5C, 0xCC), + (0x20, 0x80), + (0x21, 0x81), + (0x22, 0x31), + (0x23, 0x20), + (0x24, 0x11), + (0x25, 0x11), + (0x26, 0x12), + (0x27, 0x12), + (0x30, 0x80), + (0x31, 0x81), + (0x32, 0x31), + (0x33, 0x20), + (0x34, 0x11), + (0x35, 0x11), + (0x36, 0x12), + (0x37, 0x12), + (0x41, 0x11), + (0x42, 0x22), + (0x43, 0x33), + (0x49, 0x11), + (0x4A, 0x22), + (0x4B, 0x33), + (0xFF, 0x20, 0x10, 0x15), + (0x00, 0x00), + (0x01, 0x00), + (0x02, 0x00), + (0x03, 0x00), + (0x04, 0x10), + (0x05, 0x0C), + (0x06, 0x23), + (0x07, 0x22), + (0x08, 0x21), + (0x09, 0x20), + (0x0A, 0x33), + (0x0B, 0x32), + (0x0C, 0x34), + (0x0D, 0x35), + (0x0E, 0x01), + (0x0F, 0x01), + (0x20, 0x00), + (0x21, 0x00), + (0x22, 0x00), + (0x23, 0x00), + (0x24, 0x0C), + (0x25, 0x10), + (0x26, 0x20), + (0x27, 0x21), + (0x28, 0x22), + (0x29, 0x23), + (0x2A, 0x33), + (0x2B, 0x32), + (0x2C, 0x34), + (0x2D, 0x35), + (0x2E, 0x01), + (0x2F, 0x01), + (0xFF, 0x20, 0x10, 0x16), + (0x00, 0x00), + (0x01, 0x00), + (0x02, 0x00), + (0x03, 0x00), + (0x04, 0x08), + (0x05, 0x04), + (0x06, 0x19), + (0x07, 0x18), + (0x08, 0x17), + (0x09, 0x16), + (0x0A, 0x33), + (0x0B, 0x32), + (0x0C, 0x34), + (0x0D, 0x35), + (0x0E, 0x01), + (0x0F, 0x01), + (0x20, 0x00), + (0x21, 0x00), + (0x22, 0x00), + (0x23, 0x00), + (0x24, 0x04), + (0x25, 0x08), + (0x26, 0x16), + (0x27, 0x17), + (0x28, 0x18), + (0x29, 0x19), + (0x2A, 0x33), + (0x2B, 0x32), + (0x2C, 0x34), + (0x2D, 0x35), + (0x2E, 0x01), + (0x2F, 0x01), + (0xFF, 0x20, 0x10, 0x12), + (0x00, 0x99), + (0x2A, 0x28), + (0x2B, 0x0F), + (0x2C, 0x16), + (0x2D, 0x28), + (0x2E, 0x0F), + (0xFF, 0x20, 0x10, 0xA0), + (0x08, 0xDC), + (0xFF, 0x20, 0x10, 0x45), + (0x01, 0x9C), + (0x03, 0x9C), + (0xFF, 0x20, 0x10, 0x42), + (0x05, 0x2C), + (0xFF, 0x20, 0x10, 0x11), + (0x50, 0x01), + (0xFF, 0x20, 0x10, 0x40), + (0x86, 0x00), + (0xFF, 0x20, 0x10, 0x12), + (0x0D, 0x66), + (0xFF, 0x20, 0x10, 0x17), + (0x39, 0x3C), + (0xFF, 0x20, 0x10, 0x31), + (0x38, 0x03), + (0x39, 0xF0), + (0x36, 0x03), + (0x37, 0xE8), + (0x34, 0x03), + (0x35, 0xCF), + (0x32, 0x03), + (0x33, 0xBA), + (0x30, 0x03), + (0x31, 0xA2), + (0x2E, 0x03), + (0x2F, 0x95), + (0x2C, 0x03), + (0x2D, 0x7E), + (0x2A, 0x03), + (0x2B, 0x62), + (0x28, 0x03), + (0x29, 0x44), + (0x26, 0x02), + (0x27, 0xFC), + (0x24, 0x02), + (0x25, 0xD0), + (0x22, 0x02), + (0x23, 0x98), + (0x20, 0x02), + (0x21, 0x6F), + (0x1E, 0x02), + (0x1F, 0x32), + (0x1C, 0x01), + (0x1D, 0xF6), + (0x1A, 0x01), + (0x1B, 0xB8), + (0x18, 0x01), + (0x19, 0x6E), + (0x16, 0x01), + (0x17, 0x41), + (0x14, 0x00), + (0x15, 0xFD), + (0x12, 0x00), + (0x13, 0xCF), + (0x10, 0x00), + (0x11, 0x98), + (0x0E, 0x00), + (0x0F, 0x89), + (0x0C, 0x00), + (0x0D, 0x79), + (0x0A, 0x00), + (0x0B, 0x67), + (0x08, 0x00), + (0x09, 0x55), + (0x06, 0x00), + (0x07, 0x3F), + (0x04, 0x00), + (0x05, 0x28), + (0x02, 0x00), + (0x03, 0x0E), + (0xFF, 0x20, 0x10, 0x32), + (0x38, 0x03), + (0x39, 0xF0), + (0x36, 0x03), + (0x37, 0xE8), + (0x34, 0x03), + (0x35, 0xCF), + (0x32, 0x03), + (0x33, 0xBA), + (0x30, 0x03), + (0x31, 0xA2), + (0x2E, 0x03), + (0x2F, 0x95), + (0x2C, 0x03), + (0x2D, 0x7E), + (0x2A, 0x03), + (0x2B, 0x62), + (0x28, 0x03), + (0x29, 0x44), + (0x26, 0x02), + (0x27, 0xFC), + (0x24, 0x02), + (0x25, 0xD0), + (0x22, 0x02), + (0x23, 0x98), + (0x20, 0x02), + (0x21, 0x6F), + (0x1E, 0x02), + (0x1F, 0x32), + (0x1C, 0x01), + (0x1D, 0xF6), + (0x1A, 0x01), + (0x1B, 0xB8), + (0x18, 0x01), + (0x19, 0x6E), + (0x16, 0x01), + (0x17, 0x41), + (0x14, 0x00), + (0x15, 0xFD), + (0x12, 0x00), + (0x13, 0xCF), + (0x10, 0x00), + (0x11, 0x98), + (0x0E, 0x00), + (0x0F, 0x89), + (0x0C, 0x00), + (0x0D, 0x79), + (0x0A, 0x00), + (0x0B, 0x67), + (0x08, 0x00), + (0x09, 0x55), + (0x06, 0x00), + (0x07, 0x3F), + (0x04, 0x00), + (0x05, 0x28), + (0x02, 0x00), + (0x03, 0x0E), + (0xFF, 0x20, 0x10, 0x11), + (0x60, 0x01), + (0x65, 0x03), + (0x66, 0x38), + (0x67, 0x04), + (0x68, 0x34), + (0x69, 0x03), + (0x61, 0x03), + (0x62, 0x38), + (0x63, 0x04), + (0x64, 0x34), + (0x0A, 0x11), + (0x0B, 0x20), + (0x0C, 0x20), + (0x55, 0x06), + (0xFF, 0x20, 0x10, 0x42), + (0x05, 0x3D), + (0x06, 0x03), + (0xFF, 0x20, 0x10, 0x12), + (0x1F, 0xDC), + (0xFF, 0x20, 0x10, 0x17), + (0x11, 0xAA), + (0x16, 0x12), + (0x0B, 0xC3), + (0x10, 0x0E), + (0x14, 0xAA), + (0x18, 0xA0), + (0x1A, 0x80), + (0x1F, 0x80), + (0xFF, 0x20, 0x10, 0x11), + (0x30, 0xEE), + (0xFF, 0x20, 0x10, 0x12), + (0x15, 0x0F), + (0xFF, 0x20, 0x10, 0x2D), + (0x01, 0x3E), + (0xFF, 0x20, 0x10, 0x40), + (0x83, 0xC4), + (0xFF, 0x20, 0x10, 0x12), + (0x00, 0xCC), + (0x36, 0xA0), + (0x2A, 0x2D), + (0x2B, 0x1E), + (0x2C, 0x26), + (0x2D, 0x2D), + (0x2E, 0x1E), + (0x1F, 0xE6), + (0xFF, 0x20, 0x10, 0xA0), + (0x08, 0xE6), + (0xFF, 0x20, 0x10, 0x12), + (0x10, 0x0F), + (0xFF, 0x20, 0x10, 0x18), + (0x01, 0x01), + (0x00, 0x1E), + (0xFF, 0x20, 0x10, 0x43), + (0x03, 0x04), + (0xFF, 0x20, 0x10, 0x18), + (0x3A, 0x01), + (0xFF, 0x20, 0x10, 0x50), + (0x05, 0x08), + (0xFF, 0x20, 0x10, 0x50), + (0x00, 0xA6), + (0x01, 0xA6), + (0xFF, 0x20, 0x10, 0x50), + (0x08, 0x55), + (0xFF, 0x20, 0x10, 0x10), + (0x0B, 0x43), + (0x0C, 0x12), + (0x10, 0x01), + (0x11, 0x12), + (0x15, 0x00), + (0x16, 0x00), + (0x1A, 0x00), + (0x1B, 0x00), + (0x61, 0x00), + (0x62, 0x00), + (0x51, 0x11), + (0x55, 0x55), + (0x58, 0x00), + (0x5C, 0x00), + (0xFF, 0x20, 0x10, 0x10), + (0x20, 0x81), + (0x21, 0x82), + (0x22, 0x72), + (0x30, 0x00), + (0x31, 0x00), + (0x32, 0x00), + (0xFF, 0x20, 0x10, 0x10), + (0x44, 0x44), + (0x45, 0x55), + (0x46, 0x66), + (0x47, 0x77), + (0x49, 0x00), + (0x4A, 0x00), + (0x4B, 0x00), + (0xFF, 0x20, 0x10, 0x17), + (0x37, 0x00), + (0xFF, 0x20, 0x10, 0x15), + (0x04, 0x08), + (0x05, 0x04), + (0x06, 0x1C), + (0x07, 0x1A), + (0x08, 0x18), + (0x09, 0x16), + (0x24, 0x05), + (0x25, 0x09), + (0x26, 0x17), + (0x27, 0x19), + (0x28, 0x1B), + (0x29, 0x1D), + (0xFF, 0x20, 0x10, 0x16), + (0x04, 0x09), + (0x05, 0x05), + (0x06, 0x1D), + (0x07, 0x1B), + (0x08, 0x19), + (0x09, 0x17), + (0x24, 0x04), + (0x25, 0x08), + (0x26, 0x16), + (0x27, 0x18), + (0x28, 0x1A), + (0x29, 0x1C), + (0xFF, 0x20, 0x10, 0x18), + (0x1F, 0x02), + (0xFF, 0x20, 0x10, 0x11), + (0x15, 0x99), + (0x16, 0x99), + (0x1C, 0x88), + (0x1D, 0x88), + (0x1E, 0x88), + (0x13, 0xF0), + (0x14, 0x34), + (0xFF, 0x20, 0x10, 0x12), + (0x12, 0x89), + (0x06, 0x06), + (0x18, 0x00), + (0xFF, 0x20, 0x10, 0x11), + (0x0A, 0x00), + (0x0B, 0xF0), + (0x0C, 0xF0), + (0x6A, 0x10), + (0xFF, 0x20, 0x10, 0x11), + (0x08, 0x70), + (0x09, 0x00), + (0xFF, 0x20, 0x10, 0x00), + (0x35, 0x00), + (0xFF, 0x20, 0x10, 0x12), + (0x21, 0x70), + (0xFF, 0x20, 0x10, 0x2D), + (0x02, 0x00), + (0xFF, 0x20, 0x10, 0x00), + ), +) +# fmt: on diff --git a/tests/component_tests/conftest.py b/tests/component_tests/conftest.py index b5eceeedf6..b4b40f9fc6 100644 --- a/tests/component_tests/conftest.py +++ b/tests/component_tests/conftest.py @@ -119,17 +119,19 @@ def set_component_config() -> Callable[[str, Any], None]: @pytest.fixture def choose_variant_with_pins() -> Generator[Callable[[list], None]]: - """Set the ESP32 variant to the first one on which all the given pins are valid. + """Set the ESP32 variant to the first one on which all internal pins are valid. For ESP32 only, since the other platforms do not have variants. The core configuration must already have been set up for an ESP32 target. + Pins supplied by external components are validated by the caller's component + schema and do not affect ESP32 variant selection. Using local imports to avoid importing when ESP32 is not the target. """ from esphome import config_validation as cv from esphome.components.esp32 import KEY_ESP32, KEY_VARIANT, VARIANTS from esphome.components.esp32.gpio import validate_gpio_pin from esphome.const import CONF_INPUT, CONF_OUTPUT - from esphome.pins import gpio_pin_schema + from esphome.pins import PIN_SCHEMA_REGISTRY, gpio_pin_schema def chooser(pins: list) -> None: for variant in VARIANTS: @@ -137,6 +139,14 @@ def choose_variant_with_pins() -> Generator[Callable[[list], None]]: CORE.data[KEY_ESP32][KEY_VARIANT] = variant for pin in pins: if pin is not None: + # Pins provided by external components do not constrain + # the ESP32 variant. Their own schemas are exercised by + # the component configuration test. + if ( + isinstance(pin, dict) + and PIN_SCHEMA_REGISTRY.get_key(pin) != CORE.target_platform + ): + continue pin = gpio_pin_schema( { CONF_INPUT: True, diff --git a/tests/component_tests/mipi_spi/fixtures/spd2010.yaml b/tests/component_tests/mipi_spi/fixtures/spd2010.yaml new file mode 100644 index 0000000000..25aeae763a --- /dev/null +++ b/tests/component_tests/mipi_spi/fixtures/spd2010.yaml @@ -0,0 +1,23 @@ +esphome: + name: waveshare-spd2010 + +esp32: + board: esp32-s3-devkitc-1 + framework: + type: esp-idf + +i2c: + sda: 11 + scl: 10 + +pca9554: + id: board_expander + +spi: + type: quad + clk_pin: 40 + data_pins: [46, 45, 42, 41] + +display: + - platform: mipi_spi + model: WAVESHARE-ESP32-S3-TOUCH-LCD-1.46 diff --git a/tests/component_tests/mipi_spi/test_init.py b/tests/component_tests/mipi_spi/test_init.py index cacdc0c106..65b1b844ae 100644 --- a/tests/component_tests/mipi_spi/test_init.py +++ b/tests/component_tests/mipi_spi/test_init.py @@ -22,6 +22,10 @@ from esphome.components.mipi_spi.display import ( MODELS, dimension_schema, ) + +# Register the external pin schema used by the SPD2010 model's reset pin. +import esphome.components.pca9554 # noqa: F401 +from esphome.config import validate_config from esphome.const import ( CONF_DC_PIN, CONF_DIMENSIONS, @@ -397,6 +401,118 @@ def test_lvgl_generation( assert "set_auto_clear(false);" in main_cpp +def test_spd2010_expander_default_generation( + generate_main: Callable[[str | Path], str], + component_fixture_path: Callable[[str], Path], +) -> None: + """The SPD2010 default reset pin resolves through a configured PCA9554.""" + main_cpp = generate_main(component_fixture_path("spd2010.yaml")) + + assert "new(pca9554_pca9554gpiopin_id) pca9554::PCA9554GPIOPin();" in main_cpp + assert "pca9554_pca9554gpiopin_id->set_parent(board_expander);" in main_cpp + assert "pca9554_pca9554gpiopin_id->set_pin(1);" in main_cpp + assert "mipi_spi_mipispi_id->set_reset_pin(pca9554_pca9554gpiopin_id);" in main_cpp + + +def test_spd2010_default_reset_requires_expander() -> None: + """A registered pin schema alone cannot satisfy the default expander reference.""" + result = validate_config( + { + "esphome": {"name": "missing-expander"}, + "esp32": { + "board": "esp32-s3-devkitc-1", + "framework": {"type": "esp-idf"}, + }, + "spi": { + "type": "quad", + "clk_pin": 40, + "data_pins": [46, 45, 42, 41], + }, + "display": [ + { + "platform": "mipi_spi", + "model": "WAVESHARE-ESP32-S3-TOUCH-LCD-1.46", + } + ], + }, + {}, + ) + assert len(result.errors) == 1 + error = result.errors[0] + assert ( + "WAVESHARE-ESP32-S3-TOUCH-LCD-1.46 requires component 'pca9554' " + "to be configured" in str(error) + ) + assert error.path == ["display", 0] + + +def test_spd2010_rejects_invalid_expander_pin() -> None: + """The reset pin must exist on the configured PCA9554 variant.""" + result = validate_config( + { + "esphome": {"name": "invalid-expander-pin"}, + "esp32": { + "board": "esp32-s3-devkitc-1", + "framework": {"type": "esp-idf"}, + }, + "i2c": {"sda": 11, "scl": 10}, + "pca9554": {"id": "board_expander"}, + "spi": { + "type": "quad", + "clk_pin": 40, + "data_pins": [46, 45, 42, 41], + }, + "display": [ + { + "platform": "mipi_spi", + "model": "WAVESHARE-ESP32-S3-TOUCH-LCD-1.46", + "reset_pin": { + "pca9554": "board_expander", + "number": 8, + }, + } + ], + }, + {}, + ) + assert len(result.errors) == 1 + error = result.errors[0] + assert "Pin number must be in range 0-7" in str(error) + assert error.path == ["display", 0] + + +def test_spd2010_rejects_invalid_internal_pin() -> None: + """An invalid internal reset pin is still checked by the ESP32 schema.""" + result = validate_config( + { + "esphome": {"name": "invalid-internal-pin"}, + "esp32": { + "board": "esp32-s3-devkitc-1", + "framework": {"type": "esp-idf"}, + }, + "i2c": {"sda": 11, "scl": 10}, + "pca9554": {"id": "board_expander"}, + "spi": { + "type": "quad", + "clk_pin": 40, + "data_pins": [46, 45, 42, 41], + }, + "display": [ + { + "platform": "mipi_spi", + "model": "WAVESHARE-ESP32-S3-TOUCH-LCD-1.46", + "reset_pin": 99, + } + ], + }, + {}, + ) + assert len(result.errors) == 1 + error = result.errors[0] + assert "Invalid pin number: 99" in str(error) + assert error.path == ["display", 0, "reset_pin"] + + # A 10ms delay (flattened to {10, 0xFF}, where 0xFF is the delay marker byte) is # always prepended to the init sequence, since both a software and a hardware reset # need to settle before further commands. A custom model has no reset_pin default