From 410b6353fefbfe1e9e093a7bd2a6111260d985bf Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 May 2025 18:17:54 +1200 Subject: [PATCH 01/15] [switch] Fix schema generation (#8774) --- .../dfrobot_sen0395/switch/__init__.py | 43 ++++++------ esphome/components/switch/__init__.py | 70 ++++++++----------- tests/components/dfrobot_sen0395/common.yaml | 14 ++++ 3 files changed, 65 insertions(+), 62 deletions(-) diff --git a/esphome/components/dfrobot_sen0395/switch/__init__.py b/esphome/components/dfrobot_sen0395/switch/__init__.py index f854d08398..8e492080de 100644 --- a/esphome/components/dfrobot_sen0395/switch/__init__.py +++ b/esphome/components/dfrobot_sen0395/switch/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import switch import esphome.config_validation as cv from esphome.const import CONF_TYPE, ENTITY_CATEGORY_CONFIG +from esphome.cpp_generator import MockObjClass from .. import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component @@ -26,32 +27,30 @@ Sen0395StartAfterBootSwitch = dfrobot_sen0395_ns.class_( "Sen0395StartAfterBootSwitch", DfrobotSen0395Switch ) -_SWITCH_SCHEMA = ( - switch.switch_schema( - entity_category=ENTITY_CATEGORY_CONFIG, + +def _switch_schema(class_: MockObjClass) -> cv.Schema: + return ( + switch.switch_schema( + class_, + entity_category=ENTITY_CATEGORY_CONFIG, + ) + .extend( + { + cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id( + DfrobotSen0395Component + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) ) - .extend( - { - cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id(DfrobotSen0395Component), - } - ) - .extend(cv.COMPONENT_SCHEMA) -) + CONFIG_SCHEMA = cv.typed_schema( { - "sensor_active": _SWITCH_SCHEMA.extend( - {cv.GenerateID(): cv.declare_id(Sen0395PowerSwitch)} - ), - "turn_on_led": _SWITCH_SCHEMA.extend( - {cv.GenerateID(): cv.declare_id(Sen0395LedSwitch)} - ), - "presence_via_uart": _SWITCH_SCHEMA.extend( - {cv.GenerateID(): cv.declare_id(Sen0395UartPresenceSwitch)} - ), - "start_after_boot": _SWITCH_SCHEMA.extend( - {cv.GenerateID(): cv.declare_id(Sen0395StartAfterBootSwitch)} - ), + "sensor_active": _switch_schema(Sen0395PowerSwitch), + "turn_on_led": _switch_schema(Sen0395LedSwitch), + "presence_via_uart": _switch_schema(Sen0395UartPresenceSwitch), + "start_after_boot": _switch_schema(Sen0395StartAfterBootSwitch), } ) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 1c65aa8dfc..e7445051e0 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -72,6 +72,9 @@ _SWITCH_SCHEMA = ( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSwitchComponent), cv.Optional(CONF_INVERTED): cv.boolean, + cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum( + RESTORE_MODES, upper=True, space="_" + ), cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOnTrigger), @@ -89,54 +92,41 @@ _SWITCH_SCHEMA = ( def switch_schema( - class_: MockObjClass = cv.UNDEFINED, + class_: MockObjClass, *, - entity_category: str = cv.UNDEFINED, - device_class: str = cv.UNDEFINED, - icon: str = cv.UNDEFINED, block_inverted: bool = False, - default_restore_mode: str = "ALWAYS_OFF", + default_restore_mode: str = cv.UNDEFINED, + device_class: str = cv.UNDEFINED, + entity_category: str = cv.UNDEFINED, + icon: str = cv.UNDEFINED, ): - schema = _SWITCH_SCHEMA.extend( - { - cv.Optional(CONF_RESTORE_MODE, default=default_restore_mode): cv.enum( - RESTORE_MODES, upper=True, space="_" - ), - } - ) - if class_ is not cv.UNDEFINED: - schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) - if entity_category is not cv.UNDEFINED: - schema = schema.extend( - { - cv.Optional( - CONF_ENTITY_CATEGORY, default=entity_category - ): cv.entity_category - } - ) - if device_class is not cv.UNDEFINED: - schema = schema.extend( - { - cv.Optional( - CONF_DEVICE_CLASS, default=device_class - ): validate_device_class - } - ) - if icon is not cv.UNDEFINED: - schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) + schema = {cv.GenerateID(): cv.declare_id(class_)} + + for key, default, validator in [ + (CONF_DEVICE_CLASS, device_class, validate_device_class), + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + (CONF_ICON, icon, cv.icon), + ( + CONF_RESTORE_MODE, + default_restore_mode, + cv.enum(RESTORE_MODES, upper=True, space="_") + if default_restore_mode is not cv.UNDEFINED + else cv.UNDEFINED, + ), + ]: + if default is not cv.UNDEFINED: + schema[cv.Optional(key, default=default)] = validator + if block_inverted: - schema = schema.extend( - { - cv.Optional(CONF_INVERTED): cv.invalid( - "Inverted is not supported for this platform!" - ) - } + schema[cv.Optional(CONF_INVERTED)] = cv.invalid( + "Inverted is not supported for this platform!" ) - return schema + + return _SWITCH_SCHEMA.extend(schema) # Remove before 2025.11.0 -SWITCH_SCHEMA = switch_schema() +SWITCH_SCHEMA = switch_schema(Switch) SWITCH_SCHEMA.add_extra(cv.deprecated_schema_constant("switch")) diff --git a/tests/components/dfrobot_sen0395/common.yaml b/tests/components/dfrobot_sen0395/common.yaml index 69bcebf182..8c349911d3 100644 --- a/tests/components/dfrobot_sen0395/common.yaml +++ b/tests/components/dfrobot_sen0395/common.yaml @@ -26,3 +26,17 @@ dfrobot_sen0395: binary_sensor: - platform: dfrobot_sen0395 id: mmwave_detected + +switch: + - platform: dfrobot_sen0395 + type: sensor_active + id: mmwave_sensor_active + - platform: dfrobot_sen0395 + type: turn_on_led + id: mmwave_turn_on_led + - platform: dfrobot_sen0395 + type: presence_via_uart + id: mmwave_presence_via_uart + - platform: dfrobot_sen0395 + type: start_after_boot + id: mmwave_start_after_boot From c5654b4cb2d96ae8848d100939caff9bdb5d80d9 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Tue, 13 May 2025 08:24:38 +0200 Subject: [PATCH 02/15] [esp32] improve `gpio` (#8709) --- esphome/components/esp32/gpio.py | 31 +++++++++++------------ esphome/components/esp32/gpio_esp32.py | 3 +-- esphome/components/esp32/gpio_esp32_c2.py | 2 +- esphome/components/esp32/gpio_esp32_c3.py | 2 +- esphome/components/esp32/gpio_esp32_c6.py | 2 +- esphome/components/esp32/gpio_esp32_h2.py | 2 +- 6 files changed, 20 insertions(+), 22 deletions(-) diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index df01769a66..2bb10ce6ec 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -1,6 +1,6 @@ from dataclasses import dataclass import logging -from typing import Any +from typing import Any, Callable from esphome import pins import esphome.codegen as cg @@ -64,8 +64,7 @@ def _lookup_pin(value): def _translate_pin(value): if isinstance(value, dict) or value is None: raise cv.Invalid( - "This variable only supports pin numbers, not full pin schemas " - "(with inverted and mode)." + "This variable only supports pin numbers, not full pin schemas (with inverted and mode)." ) if isinstance(value, int) and not isinstance(value, bool): return value @@ -82,30 +81,22 @@ def _translate_pin(value): @dataclass class ESP32ValidationFunctions: - pin_validation: Any - usage_validation: Any + pin_validation: Callable[[Any], Any] + usage_validation: Callable[[Any], Any] _esp32_validations = { VARIANT_ESP32: ESP32ValidationFunctions( pin_validation=esp32_validate_gpio_pin, usage_validation=esp32_validate_supports ), - VARIANT_ESP32S2: ESP32ValidationFunctions( - pin_validation=esp32_s2_validate_gpio_pin, - usage_validation=esp32_s2_validate_supports, + VARIANT_ESP32C2: ESP32ValidationFunctions( + pin_validation=esp32_c2_validate_gpio_pin, + usage_validation=esp32_c2_validate_supports, ), VARIANT_ESP32C3: ESP32ValidationFunctions( pin_validation=esp32_c3_validate_gpio_pin, usage_validation=esp32_c3_validate_supports, ), - VARIANT_ESP32S3: ESP32ValidationFunctions( - pin_validation=esp32_s3_validate_gpio_pin, - usage_validation=esp32_s3_validate_supports, - ), - VARIANT_ESP32C2: ESP32ValidationFunctions( - pin_validation=esp32_c2_validate_gpio_pin, - usage_validation=esp32_c2_validate_supports, - ), VARIANT_ESP32C6: ESP32ValidationFunctions( pin_validation=esp32_c6_validate_gpio_pin, usage_validation=esp32_c6_validate_supports, @@ -114,6 +105,14 @@ _esp32_validations = { pin_validation=esp32_h2_validate_gpio_pin, usage_validation=esp32_h2_validate_supports, ), + VARIANT_ESP32S2: ESP32ValidationFunctions( + pin_validation=esp32_s2_validate_gpio_pin, + usage_validation=esp32_s2_validate_supports, + ), + VARIANT_ESP32S3: ESP32ValidationFunctions( + pin_validation=esp32_s3_validate_gpio_pin, + usage_validation=esp32_s3_validate_supports, + ), } diff --git a/esphome/components/esp32/gpio_esp32.py b/esphome/components/esp32/gpio_esp32.py index e4d3b6aaf3..973d2dc0ef 100644 --- a/esphome/components/esp32/gpio_esp32.py +++ b/esphome/components/esp32/gpio_esp32.py @@ -31,8 +31,7 @@ def esp32_validate_gpio_pin(value): ) if 9 <= value <= 10: _LOGGER.warning( - "Pin %s (9-10) might already be used by the " - "flash interface in QUAD IO flash mode.", + "Pin %s (9-10) might already be used by the flash interface in QUAD IO flash mode.", value, ) if value in (24, 28, 29, 30, 31): diff --git a/esphome/components/esp32/gpio_esp32_c2.py b/esphome/components/esp32/gpio_esp32_c2.py index abdcb1b655..32a24050ca 100644 --- a/esphome/components/esp32/gpio_esp32_c2.py +++ b/esphome/components/esp32/gpio_esp32_c2.py @@ -22,7 +22,7 @@ def esp32_c2_validate_supports(value): is_input = mode[CONF_INPUT] if num < 0 or num > 20: - raise cv.Invalid(f"Invalid pin number: {value} (must be 0-20)") + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-20)") if is_input: # All ESP32 pins support input mode diff --git a/esphome/components/esp32/gpio_esp32_c3.py b/esphome/components/esp32/gpio_esp32_c3.py index 5b9ec0ebd9..c1427cc02a 100644 --- a/esphome/components/esp32/gpio_esp32_c3.py +++ b/esphome/components/esp32/gpio_esp32_c3.py @@ -35,7 +35,7 @@ def esp32_c3_validate_supports(value): is_input = mode[CONF_INPUT] if num < 0 or num > 21: - raise cv.Invalid(f"Invalid pin number: {value} (must be 0-21)") + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-21)") if is_input: # All ESP32 pins support input mode diff --git a/esphome/components/esp32/gpio_esp32_c6.py b/esphome/components/esp32/gpio_esp32_c6.py index bc735f85c4..d466adb994 100644 --- a/esphome/components/esp32/gpio_esp32_c6.py +++ b/esphome/components/esp32/gpio_esp32_c6.py @@ -36,7 +36,7 @@ def esp32_c6_validate_supports(value): is_input = mode[CONF_INPUT] if num < 0 or num > 23: - raise cv.Invalid(f"Invalid pin number: {value} (must be 0-23)") + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-23)") if is_input: # All ESP32 pins support input mode pass diff --git a/esphome/components/esp32/gpio_esp32_h2.py b/esphome/components/esp32/gpio_esp32_h2.py index 7413bf4db5..7c3a658b17 100644 --- a/esphome/components/esp32/gpio_esp32_h2.py +++ b/esphome/components/esp32/gpio_esp32_h2.py @@ -45,7 +45,7 @@ def esp32_h2_validate_supports(value): is_input = mode[CONF_INPUT] if num < 0 or num > 27: - raise cv.Invalid(f"Invalid pin number: {value} (must be 0-27)") + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-27)") if is_input: # All ESP32 pins support input mode pass From 6f8ee659196ec4493e17a091bc205814c4025eed Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 May 2025 18:34:26 +1200 Subject: [PATCH 03/15] [text_sensor] Fix schema generation (#8773) --- esphome/components/text_sensor/__init__.py | 38 +++++++++------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 92b08aa6d0..888b65745f 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -156,32 +156,24 @@ _TEXT_SENSOR_SCHEMA = ( def text_sensor_schema( class_: MockObjClass = cv.UNDEFINED, *, - icon: str = cv.UNDEFINED, - entity_category: str = cv.UNDEFINED, device_class: str = cv.UNDEFINED, + entity_category: str = cv.UNDEFINED, + icon: str = cv.UNDEFINED, ) -> cv.Schema: - schema = _TEXT_SENSOR_SCHEMA + schema = {} + if class_ is not cv.UNDEFINED: - schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) - if icon is not cv.UNDEFINED: - schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) - if device_class is not cv.UNDEFINED: - schema = schema.extend( - { - cv.Optional( - CONF_DEVICE_CLASS, default=device_class - ): validate_device_class - } - ) - if entity_category is not cv.UNDEFINED: - schema = schema.extend( - { - cv.Optional( - CONF_ENTITY_CATEGORY, default=entity_category - ): cv.entity_category - } - ) - return schema + schema[cv.GenerateID()] = cv.declare_id(class_) + + for key, default, validator in [ + (CONF_ICON, icon, cv.icon), + (CONF_DEVICE_CLASS, device_class, validate_device_class), + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + ]: + if default is not cv.UNDEFINED: + schema[cv.Optional(key, default=default)] = validator + + return _TEXT_SENSOR_SCHEMA.extend(schema) # Remove before 2025.11.0 From 032949bc779880241421b8ee5c56af16adc81ddf Mon Sep 17 00:00:00 2001 From: Mischa Siekmann <45062894+gnumpi@users.noreply.github.com> Date: Tue, 13 May 2025 14:35:19 +0200 Subject: [PATCH 04/15] [audio] Fix: Decoder stops unnecessarily after a potential failure is detected. (#8776) --- esphome/components/audio/audio_decoder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index 60489d7d78..c74b028c4b 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -171,7 +171,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { bytes_available_before_processing = this->input_transfer_buffer_->available(); - if ((this->potentially_failed_count_ > 10) && (bytes_read == 0)) { + if ((this->potentially_failed_count_ > 0) && (bytes_read == 0)) { // Failed to decode in last attempt and there is no new data if ((this->input_transfer_buffer_->free() == 0) && first_loop_iteration) { From 0aa7911b1be979dc7c80fa855d484bd66366d0ca Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Tue, 13 May 2025 13:58:15 -0700 Subject: [PATCH 05/15] [esp32][esp8266] use low-level pin control for ISR gpio (#8743) Co-authored-by: Samuel Sieb --- esphome/components/esp32/gpio.cpp | 110 ++++++++++++++++++++++------ esphome/components/esp8266/gpio.cpp | 77 +++++++++++++++++-- 2 files changed, 155 insertions(+), 32 deletions(-) diff --git a/esphome/components/esp32/gpio.cpp b/esphome/components/esp32/gpio.cpp index 7896597d3e..b554b6d09c 100644 --- a/esphome/components/esp32/gpio.cpp +++ b/esphome/components/esp32/gpio.cpp @@ -2,42 +2,66 @@ #include "gpio.h" #include "esphome/core/log.h" +#include "driver/gpio.h" +#include "driver/rtc_io.h" +#include "hal/gpio_hal.h" +#include "soc/soc_caps.h" +#include "soc/gpio_periph.h" #include +#if (SOC_RTCIO_PIN_COUNT > 0) +#include "hal/rtc_io_hal.h" +#endif + +#ifndef SOC_GPIO_SUPPORT_RTC_INDEPENDENT +#define SOC_GPIO_SUPPORT_RTC_INDEPENDENT 0 // NOLINT +#endif + namespace esphome { namespace esp32 { static const char *const TAG = "esp32"; +static const gpio_hal_context_t GPIO_HAL = {.dev = GPIO_HAL_GET_HW(GPIO_PORT_0)}; + bool ESP32InternalGPIOPin::isr_service_installed = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static gpio_mode_t IRAM_ATTR flags_to_mode(gpio::Flags flags) { +static gpio_mode_t flags_to_mode(gpio::Flags flags) { flags = (gpio::Flags)(flags & ~(gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN)); - if (flags == gpio::FLAG_INPUT) { + if (flags == gpio::FLAG_INPUT) return GPIO_MODE_INPUT; - } else if (flags == gpio::FLAG_OUTPUT) { + if (flags == gpio::FLAG_OUTPUT) return GPIO_MODE_OUTPUT; - } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { + if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) return GPIO_MODE_OUTPUT_OD; - } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { + if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) return GPIO_MODE_INPUT_OUTPUT_OD; - } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT)) { + if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT)) return GPIO_MODE_INPUT_OUTPUT; - } else { - // unsupported or gpio::FLAG_NONE - return GPIO_MODE_DISABLE; - } + // unsupported or gpio::FLAG_NONE + return GPIO_MODE_DISABLE; } struct ISRPinArg { gpio_num_t pin; + gpio::Flags flags; bool inverted; +#if defined(USE_ESP32_VARIANT_ESP32) + bool use_rtc; + int rtc_pin; +#endif }; ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const { auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) - arg->pin = pin_; + arg->pin = this->pin_; + arg->flags = gpio::FLAG_NONE; arg->inverted = inverted_; +#if defined(USE_ESP32_VARIANT_ESP32) + arg->use_rtc = rtc_gpio_is_valid_gpio(this->pin_); + if (arg->use_rtc) + arg->rtc_pin = rtc_io_number_get(this->pin_); +#endif return ISRInternalGPIOPin((void *) arg); } @@ -90,6 +114,7 @@ void ESP32InternalGPIOPin::setup() { if (flags_ & gpio::FLAG_OUTPUT) { gpio_set_drive_capability(pin_, drive_strength_); } + ESP_LOGD(TAG, "rtc: %d", SOC_GPIO_SUPPORT_RTC_INDEPENDENT); } void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) { @@ -115,28 +140,65 @@ void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(pin_); } using namespace esp32; bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { - auto *arg = reinterpret_cast(arg_); - return bool(gpio_get_level(arg->pin)) != arg->inverted; + auto *arg = reinterpret_cast(this->arg_); + return bool(gpio_hal_get_level(&GPIO_HAL, arg->pin)) != arg->inverted; } + void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { - auto *arg = reinterpret_cast(arg_); - gpio_set_level(arg->pin, value != arg->inverted ? 1 : 0); + auto *arg = reinterpret_cast(this->arg_); + gpio_hal_set_level(&GPIO_HAL, arg->pin, value != arg->inverted); } + void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { // not supported } + void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { auto *arg = reinterpret_cast(arg_); - gpio_set_direction(arg->pin, flags_to_mode(flags)); - gpio_pull_mode_t pull_mode = GPIO_FLOATING; - if ((flags & gpio::FLAG_PULLUP) && (flags & gpio::FLAG_PULLDOWN)) { - pull_mode = GPIO_PULLUP_PULLDOWN; - } else if (flags & gpio::FLAG_PULLUP) { - pull_mode = GPIO_PULLUP_ONLY; - } else if (flags & gpio::FLAG_PULLDOWN) { - pull_mode = GPIO_PULLDOWN_ONLY; + gpio::Flags diff = (gpio::Flags)(flags ^ arg->flags); + if (diff & gpio::FLAG_OUTPUT) { + if (flags & gpio::FLAG_OUTPUT) { + gpio_hal_output_enable(&GPIO_HAL, arg->pin); + if (flags & gpio::FLAG_OPEN_DRAIN) + gpio_hal_od_enable(&GPIO_HAL, arg->pin); + } else { + gpio_hal_output_disable(&GPIO_HAL, arg->pin); + } } - gpio_set_pull_mode(arg->pin, pull_mode); + if (diff & gpio::FLAG_INPUT) { + if (flags & gpio::FLAG_INPUT) { + gpio_hal_input_enable(&GPIO_HAL, arg->pin); +#if defined(USE_ESP32_VARIANT_ESP32) + if (arg->use_rtc) { + if (flags & gpio::FLAG_PULLUP) { + rtcio_hal_pullup_enable(arg->rtc_pin); + } else { + rtcio_hal_pullup_disable(arg->rtc_pin); + } + if (flags & gpio::FLAG_PULLDOWN) { + rtcio_hal_pulldown_enable(arg->rtc_pin); + } else { + rtcio_hal_pulldown_disable(arg->rtc_pin); + } + } else +#endif + { + if (flags & gpio::FLAG_PULLUP) { + gpio_hal_pullup_en(&GPIO_HAL, arg->pin); + } else { + gpio_hal_pullup_dis(&GPIO_HAL, arg->pin); + } + if (flags & gpio::FLAG_PULLDOWN) { + gpio_hal_pulldown_en(&GPIO_HAL, arg->pin); + } else { + gpio_hal_pulldown_dis(&GPIO_HAL, arg->pin); + } + } + } else { + gpio_hal_input_disable(&GPIO_HAL, arg->pin); + } + } + arg->flags = flags; } } // namespace esphome diff --git a/esphome/components/esp8266/gpio.cpp b/esphome/components/esp8266/gpio.cpp index a24f217756..9f23e8e67e 100644 --- a/esphome/components/esp8266/gpio.cpp +++ b/esphome/components/esp8266/gpio.cpp @@ -8,7 +8,7 @@ namespace esp8266 { static const char *const TAG = "esp8266"; -static int IRAM_ATTR flags_to_mode(gpio::Flags flags, uint8_t pin) { +static int flags_to_mode(gpio::Flags flags, uint8_t pin) { if (flags == gpio::FLAG_INPUT) { // NOLINT(bugprone-branch-clone) return INPUT; } else if (flags == gpio::FLAG_OUTPUT) { @@ -34,12 +34,36 @@ static int IRAM_ATTR flags_to_mode(gpio::Flags flags, uint8_t pin) { struct ISRPinArg { uint8_t pin; bool inverted; + volatile uint32_t *in_reg; + volatile uint32_t *out_set_reg; + volatile uint32_t *out_clr_reg; + volatile uint32_t *mode_set_reg; + volatile uint32_t *mode_clr_reg; + volatile uint32_t *func_reg; + uint32_t mask; }; ISRInternalGPIOPin ESP8266GPIOPin::to_isr() const { auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) - arg->pin = pin_; - arg->inverted = inverted_; + arg->pin = this->pin_; + arg->inverted = this->inverted_; + if (this->pin_ < 16) { + arg->in_reg = &GPI; + arg->out_set_reg = &GPOS; + arg->out_clr_reg = &GPOC; + arg->mode_set_reg = &GPES; + arg->mode_clr_reg = &GPEC; + arg->func_reg = &GPF(this->pin_); + arg->mask = 1 << this->pin_; + } else { + arg->in_reg = &GP16I; + arg->out_set_reg = &GP16O; + arg->out_clr_reg = nullptr; + arg->mode_set_reg = &GP16E; + arg->mode_clr_reg = nullptr; + arg->func_reg = &GPF16; + arg->mask = 1; + } return ISRInternalGPIOPin((void *) arg); } @@ -88,20 +112,57 @@ void ESP8266GPIOPin::detach_interrupt() const { detachInterrupt(pin_); } using namespace esp8266; bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { - auto *arg = reinterpret_cast(arg_); - return bool(digitalRead(arg->pin)) != arg->inverted; // NOLINT + auto *arg = reinterpret_cast(this->arg_); + return bool(*arg->in_reg & arg->mask) != arg->inverted; } + void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { auto *arg = reinterpret_cast(arg_); - digitalWrite(arg->pin, value != arg->inverted ? 1 : 0); // NOLINT + if (arg->pin < 16) { + if (value != arg->inverted) { + *arg->out_set_reg = arg->mask; + } else { + *arg->out_clr_reg = arg->mask; + } + } else { + if (value != arg->inverted) { + *arg->out_set_reg |= 1; + } else { + *arg->out_set_reg &= ~1; + } + } } + void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { auto *arg = reinterpret_cast(arg_); GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, 1UL << arg->pin); } + void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { - auto *arg = reinterpret_cast(arg_); - pinMode(arg->pin, flags_to_mode(flags, arg->pin)); // NOLINT + auto *arg = reinterpret_cast(this->arg_); + if (arg->pin < 16) { + if (flags & gpio::FLAG_OUTPUT) { + *arg->mode_set_reg = arg->mask; + } else { + *arg->mode_clr_reg = arg->mask; + } + if (flags & gpio::FLAG_PULLUP) { + *arg->func_reg |= 1 << GPFPU; + } else { + *arg->func_reg &= ~(1 << GPFPU); + } + } else { + if (flags & gpio::FLAG_OUTPUT) { + *arg->mode_set_reg |= 1; + } else { + *arg->mode_set_reg &= ~1; + } + if (flags & gpio::FLAG_PULLDOWN) { + *arg->func_reg |= 1 << GP16FPD; + } else { + *arg->func_reg &= ~(1 << GP16FPD); + } + } } } // namespace esphome From 4ea63af7966d8e275c93fabb07ed6e0f581095a3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 09:21:19 +1200 Subject: [PATCH 06/15] [online_image] Support 24 bit bmp images (#8612) --- esphome/components/online_image/bmp_image.cpp | 59 +++++++++++++++---- esphome/components/online_image/bmp_image.h | 2 + 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/esphome/components/online_image/bmp_image.cpp b/esphome/components/online_image/bmp_image.cpp index af9019a4d2..f55c9f1813 100644 --- a/esphome/components/online_image/bmp_image.cpp +++ b/esphome/components/online_image/bmp_image.cpp @@ -62,6 +62,13 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { case 1: this->width_bytes_ = (this->width_ % 8 == 0) ? (this->width_ / 8) : (this->width_ / 8 + 1); break; + case 24: + this->width_bytes_ = this->width_ * 3; + if (this->width_bytes_ % 4 != 0) { + this->padding_bytes_ = 4 - (this->width_bytes_ % 4); + this->width_bytes_ += this->padding_bytes_; + } + break; default: ESP_LOGE(TAG, "Unsupported bits per pixel: %d", this->bits_per_pixel_); return DECODE_ERROR_UNSUPPORTED_FORMAT; @@ -78,18 +85,48 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { this->current_index_ = this->data_offset_; index = this->data_offset_; } - while (index < size) { - size_t paint_index = this->current_index_ - this->data_offset_; - - uint8_t current_byte = buffer[index]; - for (uint8_t i = 0; i < 8; i++) { - size_t x = (paint_index * 8) % this->width_ + i; - size_t y = (this->height_ - 1) - (paint_index / this->width_bytes_); - Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF; - this->draw(x, y, 1, 1, c); + switch (this->bits_per_pixel_) { + case 1: { + while (index < size) { + uint8_t current_byte = buffer[index]; + for (uint8_t i = 0; i < 8; i++) { + size_t x = (this->paint_index_ % this->width_) + i; + size_t y = (this->height_ - 1) - (this->paint_index_ / this->width_); + Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF; + this->draw(x, y, 1, 1, c); + } + this->paint_index_ += 8; + this->current_index_++; + index++; + } + break; } - this->current_index_++; - index++; + case 24: { + while (index < size) { + if (index + 2 >= size) { + this->decoded_bytes_ += index; + return index; + } + uint8_t b = buffer[index]; + uint8_t g = buffer[index + 1]; + uint8_t r = buffer[index + 2]; + size_t x = this->paint_index_ % this->width_; + size_t y = (this->height_ - 1) - (this->paint_index_ / this->width_); + Color c = Color(r, g, b); + this->draw(x, y, 1, 1, c); + this->paint_index_++; + this->current_index_ += 3; + index += 3; + if (x == this->width_ - 1 && this->padding_bytes_ > 0) { + index += this->padding_bytes_; + this->current_index_ += this->padding_bytes_; + } + } + break; + } + default: + ESP_LOGE(TAG, "Unsupported bits per pixel: %d", this->bits_per_pixel_); + return DECODE_ERROR_UNSUPPORTED_FORMAT; } this->decoded_bytes_ += size; return size; diff --git a/esphome/components/online_image/bmp_image.h b/esphome/components/online_image/bmp_image.h index 61192f6a46..916ffea1ad 100644 --- a/esphome/components/online_image/bmp_image.h +++ b/esphome/components/online_image/bmp_image.h @@ -24,6 +24,7 @@ class BmpDecoder : public ImageDecoder { protected: size_t current_index_{0}; + size_t paint_index_{0}; ssize_t width_{0}; ssize_t height_{0}; uint16_t bits_per_pixel_{0}; @@ -32,6 +33,7 @@ class BmpDecoder : public ImageDecoder { uint32_t color_table_entries_{0}; size_t width_bytes_{0}; size_t data_offset_{0}; + uint8_t padding_bytes_{0}; }; } // namespace online_image From 183659f52766c94a5692266e892bf7680d03f93e Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 14 May 2025 07:22:58 +1000 Subject: [PATCH 07/15] [mipi_spi] New display driver for MIPI DBI devices (#8383) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/mipi_spi/__init__.py | 15 + esphome/components/mipi_spi/display.py | 474 +++++++++++ esphome/components/mipi_spi/mipi_spi.cpp | 481 +++++++++++ esphome/components/mipi_spi/mipi_spi.h | 171 ++++ .../components/mipi_spi/models/__init__.py | 65 ++ esphome/components/mipi_spi/models/amoled.py | 72 ++ .../components/mipi_spi/models/commands.py | 82 ++ esphome/components/mipi_spi/models/cyd.py | 10 + esphome/components/mipi_spi/models/ili.py | 749 ++++++++++++++++++ esphome/components/mipi_spi/models/jc.py | 260 ++++++ esphome/components/mipi_spi/models/lanbon.py | 15 + esphome/components/mipi_spi/models/lilygo.py | 60 ++ .../components/mipi_spi/models/waveshare.py | 139 ++++ esphome/components/spi/spi.h | 6 + tests/components/mipi_spi/common.yaml | 38 + .../test-esp32-2432s028.esp32-s3-idf.yaml | 41 + .../test-jc3248w535.esp32-s3-idf.yaml | 41 + .../test-jc3636w518.esp32-s3-idf.yaml | 19 + ...est-pico-restouch-lcd-35.esp32-s3-idf.yaml | 9 + .../mipi_spi/test-s3box.esp32-s3-idf.yaml | 41 + .../mipi_spi/test-s3boxlite.esp32-s3-idf.yaml | 41 + ...t-display-s3-amoled-plus.esp32-s3-idf.yaml | 9 + ...test-t-display-s3-amoled.esp32-s3-idf.yaml | 15 + .../test-t-display-s3-pro.esp32-s3-idf.yaml | 9 + .../test-t-display-s3.esp32-s3-idf.yaml | 37 + .../mipi_spi/test-t-display.esp32-s3-idf.yaml | 41 + .../mipi_spi/test-t-embed.esp32-s3-idf.yaml | 9 + .../mipi_spi/test-t4-s3.esp32-s3-idf.yaml | 41 + .../test-wt32-sc01-plus.esp32-s3-idf.yaml | 37 + tests/components/mipi_spi/test.esp32-ard.yaml | 15 + .../mipi_spi/test.esp32-c3-ard.yaml | 10 + .../mipi_spi/test.esp32-c3-idf.yaml | 10 + tests/components/mipi_spi/test.esp32-idf.yaml | 15 + .../components/mipi_spi/test.rp2040-ard.yaml | 10 + 35 files changed, 3088 insertions(+) create mode 100644 esphome/components/mipi_spi/__init__.py create mode 100644 esphome/components/mipi_spi/display.py create mode 100644 esphome/components/mipi_spi/mipi_spi.cpp create mode 100644 esphome/components/mipi_spi/mipi_spi.h create mode 100644 esphome/components/mipi_spi/models/__init__.py create mode 100644 esphome/components/mipi_spi/models/amoled.py create mode 100644 esphome/components/mipi_spi/models/commands.py create mode 100644 esphome/components/mipi_spi/models/cyd.py create mode 100644 esphome/components/mipi_spi/models/ili.py create mode 100644 esphome/components/mipi_spi/models/jc.py create mode 100644 esphome/components/mipi_spi/models/lanbon.py create mode 100644 esphome/components/mipi_spi/models/lilygo.py create mode 100644 esphome/components/mipi_spi/models/waveshare.py create mode 100644 tests/components/mipi_spi/common.yaml create mode 100644 tests/components/mipi_spi/test-esp32-2432s028.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-jc3248w535.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-jc3636w518.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-pico-restouch-lcd-35.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-s3box.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-s3boxlite.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-t-display-s3-amoled-plus.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-t-display-s3-amoled.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-t-display-s3-pro.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-t-display-s3.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-t-display.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-t-embed.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-t4-s3.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test-wt32-sc01-plus.esp32-s3-idf.yaml create mode 100644 tests/components/mipi_spi/test.esp32-ard.yaml create mode 100644 tests/components/mipi_spi/test.esp32-c3-ard.yaml create mode 100644 tests/components/mipi_spi/test.esp32-c3-idf.yaml create mode 100644 tests/components/mipi_spi/test.esp32-idf.yaml create mode 100644 tests/components/mipi_spi/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index e6c149012a..ddd0494a3c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -282,6 +282,7 @@ esphome/components/microphone/* @jesserockz @kahrendt esphome/components/mics_4514/* @jesserockz esphome/components/midea/* @dudanov esphome/components/midea_ir/* @dudanov +esphome/components/mipi_spi/* @clydebarrow esphome/components/mitsubishi/* @RubyBailey esphome/components/mixer/speaker/* @kahrendt esphome/components/mlx90393/* @functionpointer diff --git a/esphome/components/mipi_spi/__init__.py b/esphome/components/mipi_spi/__init__.py new file mode 100644 index 0000000000..46b0206a1f --- /dev/null +++ b/esphome/components/mipi_spi/__init__.py @@ -0,0 +1,15 @@ +CODEOWNERS = ["@clydebarrow"] + +DOMAIN = "mipi_spi" + +CONF_DRAW_FROM_ORIGIN = "draw_from_origin" +CONF_SPI_16 = "spi_16" +CONF_PIXEL_MODE = "pixel_mode" +CONF_COLOR_DEPTH = "color_depth" +CONF_BUS_MODE = "bus_mode" +CONF_USE_AXIS_FLIPS = "use_axis_flips" +CONF_NATIVE_WIDTH = "native_width" +CONF_NATIVE_HEIGHT = "native_height" + +MODE_RGB = "RGB" +MODE_BGR = "BGR" diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py new file mode 100644 index 0000000000..e9ed97a2a2 --- /dev/null +++ b/esphome/components/mipi_spi/display.py @@ -0,0 +1,474 @@ +import logging + +from esphome import pins +import esphome.codegen as cg +from esphome.components import display, spi +from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE +import esphome.config_validation as cv +from esphome.config_validation import ALLOW_EXTRA +from esphome.const import ( + CONF_BRIGHTNESS, + CONF_COLOR_ORDER, + CONF_CS_PIN, + CONF_DATA_RATE, + CONF_DC_PIN, + CONF_DIMENSIONS, + CONF_ENABLE_PIN, + CONF_HEIGHT, + CONF_ID, + CONF_INIT_SEQUENCE, + CONF_INVERT_COLORS, + CONF_LAMBDA, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_MODEL, + CONF_OFFSET_HEIGHT, + CONF_OFFSET_WIDTH, + CONF_RESET_PIN, + CONF_ROTATION, + CONF_SWAP_XY, + CONF_TRANSFORM, + CONF_WIDTH, +) +from esphome.core import TimePeriod + +from ..const import CONF_DRAW_ROUNDING +from ..lvgl.defines import CONF_COLOR_DEPTH +from . import ( + CONF_BUS_MODE, + CONF_DRAW_FROM_ORIGIN, + CONF_NATIVE_HEIGHT, + CONF_NATIVE_WIDTH, + CONF_PIXEL_MODE, + CONF_SPI_16, + CONF_USE_AXIS_FLIPS, + DOMAIN, + MODE_BGR, + MODE_RGB, +) +from .models import ( + DELAY_FLAG, + MADCTL_BGR, + MADCTL_MV, + MADCTL_MX, + MADCTL_MY, + MADCTL_XFLIP, + MADCTL_YFLIP, + DriverChip, + amoled, + cyd, + ili, + jc, + lanbon, + lilygo, + waveshare, +) +from .models.commands import BRIGHTNESS, DISPON, INVOFF, INVON, MADCTL, PIXFMT, SLPOUT + +DEPENDENCIES = ["spi"] + +LOGGER = logging.getLogger(DOMAIN) +mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi") +MipiSpi = mipi_spi_ns.class_( + "MipiSpi", display.Display, display.DisplayBuffer, cg.Component, spi.SPIDevice +) +ColorOrder = display.display_ns.enum("ColorMode") +ColorBitness = display.display_ns.enum("ColorBitness") +Model = mipi_spi_ns.enum("Model") + +COLOR_ORDERS = { + MODE_RGB: ColorOrder.COLOR_ORDER_RGB, + MODE_BGR: ColorOrder.COLOR_ORDER_BGR, +} + +COLOR_DEPTHS = { + 8: ColorBitness.COLOR_BITNESS_332, + 16: ColorBitness.COLOR_BITNESS_565, +} +DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema + + +DriverChip("CUSTOM", initsequence={}) + +MODELS = DriverChip.models +# These statements are noops, but serve to suppress linting of side-effect-only imports +for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare): + pass + +PixelMode = mipi_spi_ns.enum("PixelMode") + +PIXEL_MODE_18BIT = "18bit" +PIXEL_MODE_16BIT = "16bit" + +PIXEL_MODES = { + PIXEL_MODE_16BIT: 0x55, + PIXEL_MODE_18BIT: 0x66, +} + + +def validate_dimension(rounding): + def validator(value): + value = cv.positive_int(value) + if value % rounding != 0: + raise cv.Invalid(f"Dimensions and offsets must be divisible by {rounding}") + return value + + return validator + + +def map_sequence(value): + """ + The format is a repeated sequence of [CMD, ] where is s a sequence of bytes. The length is inferred + from the length of the sequence and should not be explicit. + A delay can be inserted by specifying "- delay N" where N is in ms + """ + if isinstance(value, str) and value.lower().startswith("delay "): + value = value.lower()[6:] + delay = cv.All( + cv.positive_time_period_milliseconds, + cv.Range(TimePeriod(milliseconds=1), TimePeriod(milliseconds=255)), + )(value) + return DELAY_FLAG, delay.total_milliseconds + if isinstance(value, int): + return (value,) + value = cv.All(cv.ensure_list(cv.int_range(0, 255)), cv.Length(1, 254))(value) + return tuple(value) + + +def power_of_two(value): + value = cv.int_range(1, 128)(value) + if value & (value - 1) != 0: + raise cv.Invalid("value must be a power of two") + return value + + +def dimension_schema(rounding): + return cv.Any( + cv.dimensions, + cv.Schema( + { + cv.Required(CONF_WIDTH): validate_dimension(rounding), + cv.Required(CONF_HEIGHT): validate_dimension(rounding), + cv.Optional(CONF_OFFSET_HEIGHT, default=0): validate_dimension( + rounding + ), + cv.Optional(CONF_OFFSET_WIDTH, default=0): validate_dimension(rounding), + } + ), + ) + + +def model_schema(bus_mode, model: DriverChip, swapsies: bool): + transform = cv.Schema( + { + cv.Required(CONF_MIRROR_X): cv.boolean, + cv.Required(CONF_MIRROR_Y): cv.boolean, + } + ) + if model.get_default(CONF_SWAP_XY, False) == cv.UNDEFINED: + transform = transform.extend( + { + cv.Optional(CONF_SWAP_XY): cv.invalid( + "Axis swapping not supported by this model" + ) + } + ) + else: + transform = transform.extend( + { + cv.Required(CONF_SWAP_XY): cv.boolean, + } + ) + # CUSTOM model will need to provide a custom init sequence + iseqconf = ( + cv.Required(CONF_INIT_SEQUENCE) + if model.initsequence is None + else cv.Optional(CONF_INIT_SEQUENCE) + ) + # Dimensions are optional if the model has a default width and the transform is not overridden + cv_dimensions = ( + cv.Optional if model.get_default(CONF_WIDTH) and not swapsies else cv.Required + ) + pixel_modes = PIXEL_MODES if bus_mode == TYPE_SINGLE else (PIXEL_MODE_16BIT,) + color_depth = ( + ("16", "8", "16bit", "8bit") if bus_mode == TYPE_SINGLE else ("16", "16bit") + ) + schema = ( + display.FULL_DISPLAY_SCHEMA.extend( + spi.spi_device_schema( + cs_pin_required=False, + default_mode="MODE3" if bus_mode == TYPE_OCTAL else "MODE0", + default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000), + mode=bus_mode, + ) + ) + .extend( + { + model.option(pin, cv.UNDEFINED): pins.gpio_output_pin_schema + for pin in (CONF_RESET_PIN, CONF_CS_PIN, CONF_DC_PIN) + } + ) + .extend( + { + cv.GenerateID(): cv.declare_id(MipiSpi), + cv_dimensions(CONF_DIMENSIONS): dimension_schema( + model.get_default(CONF_DRAW_ROUNDING, 1) + ), + model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list( + pins.gpio_output_pin_schema + ), + model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum( + COLOR_ORDERS, upper=True + ), + model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True), + model.option(CONF_DRAW_ROUNDING, 2): power_of_two, + model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.Any( + cv.one_of(*pixel_modes, lower=True), + cv.int_range(0, 255, min_included=True, max_included=True), + ), + cv.Optional(CONF_TRANSFORM): transform, + cv.Optional(CONF_BUS_MODE, default=bus_mode): cv.one_of( + bus_mode, lower=True + ), + cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), + iseqconf: cv.ensure_list(map_sequence), + } + ) + .extend( + { + model.option(x): cv.boolean + for x in [ + CONF_DRAW_FROM_ORIGIN, + CONF_SPI_16, + CONF_INVERT_COLORS, + CONF_USE_AXIS_FLIPS, + ] + } + ) + ) + if brightness := model.get_default(CONF_BRIGHTNESS): + schema = schema.extend( + { + cv.Optional(CONF_BRIGHTNESS, default=brightness): cv.int_range( + 0, 0xFF, min_included=True, max_included=True + ), + } + ) + if bus_mode != TYPE_SINGLE: + return cv.All(schema, cv.only_with_esp_idf) + return schema + + +def rotation_as_transform(model, config): + """ + Check if a rotation can be implemented in hardware using the MADCTL register. + A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y. + """ + rotation = config.get(CONF_ROTATION, 0) + return rotation and ( + model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180 + ) + + +def config_schema(config): + # First get the model and bus mode + config = cv.Schema( + { + cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), + }, + extra=ALLOW_EXTRA, + )(config) + model = MODELS[config[CONF_MODEL]] + bus_modes = model.modes + config = cv.Schema( + { + model.option(CONF_BUS_MODE, TYPE_SINGLE): cv.one_of(*bus_modes, lower=True), + cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), + }, + extra=ALLOW_EXTRA, + )(config) + bus_mode = config.get(CONF_BUS_MODE, model.modes[0]) + swapsies = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True + config = model_schema(bus_mode, model, swapsies)(config) + # Check for invalid combinations of MADCTL config + if init_sequence := config.get(CONF_INIT_SEQUENCE): + if MADCTL in [x[0] for x in init_sequence] and CONF_TRANSFORM in config: + raise cv.Invalid( + f"transform is not supported when MADCTL ({MADCTL:#X}) is in the init sequence" + ) + + if bus_mode == TYPE_QUAD and CONF_DC_PIN in config: + raise cv.Invalid("DC pin is not supported in quad mode") + if config[CONF_PIXEL_MODE] == PIXEL_MODE_18BIT and bus_mode != TYPE_SINGLE: + raise cv.Invalid("18-bit pixel mode is not supported on a quad or octal bus") + if bus_mode != TYPE_QUAD and CONF_DC_PIN not in config: + raise cv.Invalid(f"DC pin is required in {bus_mode} mode") + return config + + +CONFIG_SCHEMA = config_schema + + +def get_transform(model, config): + can_transform = rotation_as_transform(model, config) + transform = config.get( + CONF_TRANSFORM, + { + CONF_MIRROR_X: model.get_default(CONF_MIRROR_X, False), + CONF_MIRROR_Y: model.get_default(CONF_MIRROR_Y, False), + CONF_SWAP_XY: model.get_default(CONF_SWAP_XY, False), + }, + ) + + # Can we use the MADCTL register to set the rotation? + if can_transform and CONF_TRANSFORM not in config: + rotation = config[CONF_ROTATION] + if rotation == 180: + transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] + transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + elif rotation == 90: + transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] + transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] + else: + transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] + transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + transform[CONF_TRANSFORM] = True + return transform + + +def get_sequence(model, config): + """ + Create the init sequence for the display. + Use the default sequence from the model, if any, and append any custom sequence provided in the config. + Append SLPOUT (if not already in the sequence) and DISPON to the end of the sequence + Pixel format, color order, and orientation will be set. + """ + sequence = list(model.initsequence) + custom_sequence = config.get(CONF_INIT_SEQUENCE, []) + sequence.extend(custom_sequence) + # Ensure each command is a tuple + sequence = [x if isinstance(x, tuple) else (x,) for x in sequence] + commands = [x[0] for x in sequence] + # Set pixel format if not already in the custom sequence + if PIXFMT not in commands: + pixel_mode = config[CONF_PIXEL_MODE] + if not isinstance(pixel_mode, int): + pixel_mode = PIXEL_MODES[pixel_mode] + sequence.append((PIXFMT, pixel_mode)) + # Does the chip use the flipping bits for mirroring rather than the reverse order bits? + use_flip = config[CONF_USE_AXIS_FLIPS] + if MADCTL not in commands: + madctl = 0 + transform = get_transform(model, config) + if transform.get(CONF_TRANSFORM): + LOGGER.info("Using hardware transform to implement rotation") + if transform.get(CONF_MIRROR_X): + madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX + if transform.get(CONF_MIRROR_Y): + madctl |= MADCTL_YFLIP if use_flip else MADCTL_MY + if transform.get(CONF_SWAP_XY) is True: # Exclude Undefined + madctl |= MADCTL_MV + if config[CONF_COLOR_ORDER] == MODE_BGR: + madctl |= MADCTL_BGR + sequence.append((MADCTL, madctl)) + if INVON not in commands and INVOFF not in commands: + if config[CONF_INVERT_COLORS]: + sequence.append((INVON,)) + else: + sequence.append((INVOFF,)) + if BRIGHTNESS not in commands: + if brightness := config.get( + CONF_BRIGHTNESS, model.get_default(CONF_BRIGHTNESS) + ): + sequence.append((BRIGHTNESS, brightness)) + if SLPOUT not in commands: + sequence.append((SLPOUT,)) + sequence.append((DISPON,)) + + # Flatten the sequence into a list of bytes, with the length of each command + # or the delay flag inserted where needed + return sum( + tuple( + (x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:] + for x in sequence + ), + (), + ) + + +async def to_code(config): + model = MODELS[config[CONF_MODEL]] + transform = get_transform(model, config) + if CONF_DIMENSIONS in config: + # Explicit dimensions, just use as is + dimensions = config[CONF_DIMENSIONS] + if isinstance(dimensions, dict): + width = dimensions[CONF_WIDTH] + height = dimensions[CONF_HEIGHT] + offset_width = dimensions[CONF_OFFSET_WIDTH] + offset_height = dimensions[CONF_OFFSET_HEIGHT] + else: + (width, height) = dimensions + offset_width = 0 + offset_height = 0 + else: + # Default dimensions, use model defaults and transform if needed + width = model.get_default(CONF_WIDTH) + height = model.get_default(CONF_HEIGHT) + offset_width = model.get_default(CONF_OFFSET_WIDTH, 0) + offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0) + + # if mirroring axes and there are offsets, also mirror the offsets to cater for situations where + # the offset is asymmetric + if transform[CONF_MIRROR_X]: + native_width = model.get_default( + CONF_NATIVE_WIDTH, width + offset_width * 2 + ) + offset_width = native_width - width - offset_width + if transform[CONF_MIRROR_Y]: + native_height = model.get_default( + CONF_NATIVE_HEIGHT, height + offset_height * 2 + ) + offset_height = native_height - height - offset_height + # Swap default dimensions if swap_xy is set + if transform[CONF_SWAP_XY] is True: + width, height = height, width + offset_height, offset_width = offset_width, offset_height + + color_depth = config[CONF_COLOR_DEPTH] + if color_depth.endswith("bit"): + color_depth = color_depth[:-3] + color_depth = COLOR_DEPTHS[int(color_depth)] + + var = cg.new_Pvariable( + config[CONF_ID], width, height, offset_width, offset_height, color_depth + ) + cg.add(var.set_init_sequence(get_sequence(model, config))) + if rotation_as_transform(model, config): + if CONF_TRANSFORM in config: + LOGGER.warning("Use of 'transform' with 'rotation' is not recommended") + else: + config[CONF_ROTATION] = 0 + cg.add(var.set_model(config[CONF_MODEL])) + cg.add(var.set_draw_from_origin(config[CONF_DRAW_FROM_ORIGIN])) + cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING])) + cg.add(var.set_spi_16(config[CONF_SPI_16])) + if enable_pin := config.get(CONF_ENABLE_PIN): + enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin] + cg.add(var.set_enable_pins(enable)) + + if reset_pin := config.get(CONF_RESET_PIN): + reset = await cg.gpio_pin_expression(reset_pin) + cg.add(var.set_reset_pin(reset)) + + if dc_pin := config.get(CONF_DC_PIN): + dc_pin = await cg.gpio_pin_expression(dc_pin) + cg.add(var.set_dc_pin(dc_pin)) + + if lamb := config.get(CONF_LAMBDA): + lambda_ = await cg.process_lambda( + lamb, [(display.DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) + await display.register_display(var, config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/mipi_spi/mipi_spi.cpp b/esphome/components/mipi_spi/mipi_spi.cpp new file mode 100644 index 0000000000..2d393ac349 --- /dev/null +++ b/esphome/components/mipi_spi/mipi_spi.cpp @@ -0,0 +1,481 @@ +#include "mipi_spi.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mipi_spi { + +void MipiSpi::setup() { + ESP_LOGCONFIG(TAG, "Setting up MIPI SPI"); + this->spi_setup(); + if (this->dc_pin_ != nullptr) { + this->dc_pin_->setup(); + this->dc_pin_->digital_write(false); + } + for (auto *pin : this->enable_pins_) { + pin->setup(); + pin->digital_write(true); + } + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(5); + this->reset_pin_->digital_write(false); + delay(5); + this->reset_pin_->digital_write(true); + } + this->bus_width_ = this->parent_->get_bus_width(); + + // need to know when the display is ready for SLPOUT command - will be 120ms after reset + auto when = millis() + 120; + delay(10); + size_t index = 0; + auto &vec = this->init_sequence_; + while (index != vec.size()) { + if (vec.size() - index < 2) { + ESP_LOGE(TAG, "Malformed init sequence"); + this->mark_failed(); + return; + } + uint8_t cmd = vec[index++]; + uint8_t x = vec[index++]; + if (x == DELAY_FLAG) { + ESP_LOGD(TAG, "Delay %dms", cmd); + delay(cmd); + } else { + uint8_t num_args = x & 0x7F; + if (vec.size() - index < num_args) { + ESP_LOGE(TAG, "Malformed init sequence"); + this->mark_failed(); + return; + } + auto arg_byte = vec[index]; + switch (cmd) { + case SLEEP_OUT: { + // are we ready, boots? + int duration = when - millis(); + if (duration > 0) { + ESP_LOGD(TAG, "Sleep %dms", duration); + delay(duration); + } + } break; + + case INVERT_ON: + this->invert_colors_ = true; + break; + case MADCTL_CMD: + this->madctl_ = arg_byte; + break; + case PIXFMT: + this->pixel_mode_ = arg_byte & 0x11 ? PIXEL_MODE_16 : PIXEL_MODE_18; + break; + case BRIGHTNESS: + this->brightness_ = arg_byte; + break; + + default: + break; + } + const auto *ptr = vec.data() + index; + ESP_LOGD(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte); + this->write_command_(cmd, ptr, num_args); + index += num_args; + if (cmd == SLEEP_OUT) + delay(10); + } + } + this->setup_complete_ = true; + if (this->draw_from_origin_) + check_buffer_(); + ESP_LOGCONFIG(TAG, "MIPI SPI setup complete"); +} + +void MipiSpi::update() { + if (!this->setup_complete_ || this->is_failed()) { + return; + } + this->do_update_(); + if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_) + return; + ESP_LOGV(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, this->y_high_); + // Some chips require that the drawing window be aligned on certain boundaries + auto dr = this->draw_rounding_; + this->x_low_ = this->x_low_ / dr * dr; + this->y_low_ = this->y_low_ / dr * dr; + this->x_high_ = (this->x_high_ + dr) / dr * dr - 1; + this->y_high_ = (this->y_high_ + dr) / dr * dr - 1; + if (this->draw_from_origin_) { + this->x_low_ = 0; + this->y_low_ = 0; + this->x_high_ = this->width_ - 1; + } + int w = this->x_high_ - this->x_low_ + 1; + int h = this->y_high_ - this->y_low_ + 1; + this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_, this->y_low_, + this->width_ - w - this->x_low_); + // invalidate watermarks + this->x_low_ = this->width_; + this->y_low_ = this->height_; + this->x_high_ = 0; + this->y_high_ = 0; +} + +void MipiSpi::fill(Color color) { + if (!this->check_buffer_()) + return; + this->x_low_ = 0; + this->y_low_ = 0; + this->x_high_ = this->get_width_internal() - 1; + this->y_high_ = this->get_height_internal() - 1; + switch (this->color_depth_) { + case display::COLOR_BITNESS_332: { + auto new_color = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); + memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_); + break; + } + default: { + auto new_color = display::ColorUtil::color_to_565(color); + if (((uint8_t) (new_color >> 8)) == ((uint8_t) new_color)) { + // Upper and lower is equal can use quicker memset operation. Takes ~20ms. + memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_); + } else { + auto *ptr_16 = reinterpret_cast(this->buffer_); + auto len = this->buffer_bytes_ / 2; + while (len--) { + *ptr_16++ = new_color; + } + } + } + } +} + +void MipiSpi::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) { + return; + } + if (!this->check_buffer_()) + return; + size_t pos = (y * this->width_) + x; + switch (this->color_depth_) { + case display::COLOR_BITNESS_332: { + uint8_t new_color = display::ColorUtil::color_to_332(color); + if (this->buffer_[pos] == new_color) + return; + this->buffer_[pos] = new_color; + break; + } + + case display::COLOR_BITNESS_565: { + auto *ptr_16 = reinterpret_cast(this->buffer_); + uint8_t hi_byte = static_cast(color.r & 0xF8) | (color.g >> 5); + uint8_t lo_byte = static_cast((color.g & 0x1C) << 3) | (color.b >> 3); + uint16_t new_color = hi_byte | (lo_byte << 8); // big endian + if (ptr_16[pos] == new_color) + return; + ptr_16[pos] = new_color; + break; + } + default: + return; + } + // low and high watermark may speed up drawing from buffer + if (x < this->x_low_) + this->x_low_ = x; + if (y < this->y_low_) + this->y_low_ = y; + if (x > this->x_high_) + this->x_high_ = x; + if (y > this->y_high_) + this->y_high_ = y; +} + +void MipiSpi::reset_params_() { + if (!this->is_ready()) + return; + this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF); + if (this->brightness_.has_value()) + this->write_command_(BRIGHTNESS, this->brightness_.value()); +} + +void MipiSpi::write_init_sequence_() { + size_t index = 0; + auto &vec = this->init_sequence_; + while (index != vec.size()) { + if (vec.size() - index < 2) { + ESP_LOGE(TAG, "Malformed init sequence"); + this->mark_failed(); + return; + } + uint8_t cmd = vec[index++]; + uint8_t x = vec[index++]; + if (x == DELAY_FLAG) { + ESP_LOGV(TAG, "Delay %dms", cmd); + delay(cmd); + } else { + uint8_t num_args = x & 0x7F; + if (vec.size() - index < num_args) { + ESP_LOGE(TAG, "Malformed init sequence"); + this->mark_failed(); + return; + } + const auto *ptr = vec.data() + index; + this->write_command_(cmd, ptr, num_args); + index += num_args; + } + } + this->setup_complete_ = true; + ESP_LOGCONFIG(TAG, "MIPI SPI setup complete"); +} + +void MipiSpi::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { + ESP_LOGVV(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2); + uint8_t buf[4]; + x1 += this->offset_width_; + x2 += this->offset_width_; + y1 += this->offset_height_; + y2 += this->offset_height_; + put16_be(buf, y1); + put16_be(buf + 2, y2); + this->write_command_(RASET, buf, sizeof buf); + put16_be(buf, x1); + put16_be(buf + 2, x2); + this->write_command_(CASET, buf, sizeof buf); +} + +void MipiSpi::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, + display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) { + if (!this->setup_complete_ || this->is_failed()) + return; + if (w <= 0 || h <= 0) + return; + if (bitness != this->color_depth_ || big_endian != (this->bit_order_ == spi::BIT_ORDER_MSB_FIRST)) { + Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad); + return; + } + if (this->draw_from_origin_) { + auto stride = x_offset + w + x_pad; + for (int y = 0; y != h; y++) { + memcpy(this->buffer_ + ((y + y_start) * this->width_ + x_start) * 2, + ptr + ((y + y_offset) * stride + x_offset) * 2, w * 2); + } + ptr = this->buffer_; + w = this->width_; + h += y_start; + x_start = 0; + y_start = 0; + x_offset = 0; + y_offset = 0; + } + this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad); +} + +void MipiSpi::write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride) { + stride -= w; + uint8_t transfer_buffer[6 * 256]; + size_t idx = 0; // index into transfer_buffer + while (h-- != 0) { + for (auto x = w; x-- != 0;) { + auto color_val = *ptr++; + // deal with byte swapping + transfer_buffer[idx++] = (color_val & 0xF8); // Blue + transfer_buffer[idx++] = ((color_val & 0x7) << 5) | ((color_val & 0xE000) >> 11); // Green + transfer_buffer[idx++] = (color_val >> 5) & 0xF8; // Red + if (idx == sizeof(transfer_buffer)) { + this->write_array(transfer_buffer, idx); + idx = 0; + } + } + ptr += stride; + } + if (idx != 0) + this->write_array(transfer_buffer, idx); +} + +void MipiSpi::write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) { + stride -= w; + uint8_t transfer_buffer[6 * 256]; + size_t idx = 0; // index into transfer_buffer + while (h-- != 0) { + for (auto x = w; x-- != 0;) { + auto color_val = *ptr++; + transfer_buffer[idx++] = color_val & 0xE0; // Red + transfer_buffer[idx++] = (color_val << 3) & 0xE0; // Green + transfer_buffer[idx++] = color_val << 6; // Blue + if (idx == sizeof(transfer_buffer)) { + this->write_array(transfer_buffer, idx); + idx = 0; + } + } + ptr += stride; + } + if (idx != 0) + this->write_array(transfer_buffer, idx); +} + +void MipiSpi::write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) { + stride -= w; + uint8_t transfer_buffer[6 * 256]; + size_t idx = 0; // index into transfer_buffer + while (h-- != 0) { + for (auto x = w; x-- != 0;) { + auto color_val = *ptr++; + transfer_buffer[idx++] = (color_val & 0xE0) | ((color_val & 0x1C) >> 2); + transfer_buffer[idx++] = (color_val & 0x3) << 3; + if (idx == sizeof(transfer_buffer)) { + this->write_array(transfer_buffer, idx); + idx = 0; + } + } + ptr += stride; + } + if (idx != 0) + this->write_array(transfer_buffer, idx); +} + +void MipiSpi::write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, + int x_pad) { + this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1); + auto stride = x_offset + w + x_pad; + const auto *offset_ptr = ptr; + if (this->color_depth_ == display::COLOR_BITNESS_332) { + offset_ptr += y_offset * stride + x_offset; + } else { + stride *= 2; + offset_ptr += y_offset * stride + x_offset * 2; + } + + switch (this->bus_width_) { + case 4: + this->enable(); + if (x_offset == 0 && x_pad == 0 && y_offset == 0) { + // we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't + // bother + this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w * h * 2, 4); + } else { + this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, nullptr, 0, 4); + for (int y = 0; y != h; y++) { + this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 4); + offset_ptr += stride; + } + } + break; + + case 8: + this->write_command_(WDATA); + this->enable(); + if (x_offset == 0 && x_pad == 0 && y_offset == 0) { + this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h * 2, 8); + } else { + for (int y = 0; y != h; y++) { + this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 8); + offset_ptr += stride; + } + } + break; + + default: + this->write_command_(WDATA); + this->enable(); + + if (this->color_depth_ == display::COLOR_BITNESS_565) { + // Source buffer is 16-bit RGB565 + if (this->pixel_mode_ == PIXEL_MODE_18) { + // Convert RGB565 to RGB666 + this->write_18_from_16_bit_(reinterpret_cast(offset_ptr), w, h, stride / 2); + } else { + // Direct RGB565 output + if (x_offset == 0 && x_pad == 0 && y_offset == 0) { + this->write_array(ptr, w * h * 2); + } else { + for (int y = 0; y != h; y++) { + this->write_array(offset_ptr, w * 2); + offset_ptr += stride; + } + } + } + } else { + // Source buffer is 8-bit RGB332 + if (this->pixel_mode_ == PIXEL_MODE_18) { + // Convert RGB332 to RGB666 + this->write_18_from_8_bit_(offset_ptr, w, h, stride); + } else { + this->write_16_from_8_bit_(offset_ptr, w, h, stride); + } + break; + } + } + this->disable(); +} + +void MipiSpi::write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) { + ESP_LOGV(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str()); + if (this->bus_width_ == 4) { + this->enable(); + this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len); + this->disable(); + } else if (this->bus_width_ == 8) { + this->dc_pin_->digital_write(false); + this->enable(); + this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8); + this->disable(); + this->dc_pin_->digital_write(true); + if (len != 0) { + this->enable(); + this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8); + this->disable(); + } + } else { + this->dc_pin_->digital_write(false); + this->enable(); + this->write_byte(cmd); + this->disable(); + this->dc_pin_->digital_write(true); + if (len != 0) { + if (this->spi_16_) { + for (size_t i = 0; i != len; i++) { + this->enable(); + this->write_byte(0); + this->write_byte(bytes[i]); + this->disable(); + } + } else { + this->enable(); + this->write_array(bytes, len); + this->disable(); + } + } + } +} + +void MipiSpi::dump_config() { + ESP_LOGCONFIG(TAG, "MIPI_SPI Display"); + ESP_LOGCONFIG(TAG, " Model: %s", this->model_); + ESP_LOGCONFIG(TAG, " Width: %u", this->width_); + ESP_LOGCONFIG(TAG, " Height: %u", this->height_); + if (this->offset_width_ != 0) + ESP_LOGCONFIG(TAG, " Offset width: %u", this->offset_width_); + if (this->offset_height_ != 0) + ESP_LOGCONFIG(TAG, " Offset height: %u", this->offset_height_); + ESP_LOGCONFIG(TAG, " Swap X/Y: %s", YESNO(this->madctl_ & MADCTL_MV)); + ESP_LOGCONFIG(TAG, " Mirror X: %s", YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP))); + ESP_LOGCONFIG(TAG, " Mirror Y: %s", YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP))); + ESP_LOGCONFIG(TAG, " Color depth: %d bits", this->color_depth_ == display::COLOR_BITNESS_565 ? 16 : 8); + ESP_LOGCONFIG(TAG, " Invert colors: %s", YESNO(this->invert_colors_)); + ESP_LOGCONFIG(TAG, " Color order: %s", this->madctl_ & MADCTL_BGR ? "BGR" : "RGB"); + ESP_LOGCONFIG(TAG, " Pixel mode: %s", this->pixel_mode_ == PIXEL_MODE_18 ? "18bit" : "16bit"); + if (this->brightness_.has_value()) + ESP_LOGCONFIG(TAG, " Brightness: %u", this->brightness_.value()); + if (this->spi_16_) + ESP_LOGCONFIG(TAG, " SPI 16bit: YES"); + ESP_LOGCONFIG(TAG, " Draw rounding: %u", this->draw_rounding_); + if (this->draw_from_origin_) + ESP_LOGCONFIG(TAG, " Draw from origin: YES"); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + ESP_LOGCONFIG(TAG, " SPI Mode: %d", this->mode_); + ESP_LOGCONFIG(TAG, " SPI Data rate: %dMHz", static_cast(this->data_rate_ / 1000000)); + ESP_LOGCONFIG(TAG, " SPI Bus width: %d", this->bus_width_); +} + +} // namespace mipi_spi +} // namespace esphome diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h new file mode 100644 index 0000000000..052ebe3a6b --- /dev/null +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -0,0 +1,171 @@ +#pragma once + +#include + +#include "esphome/components/spi/spi.h" +#include "esphome/components/display/display.h" +#include "esphome/components/display/display_buffer.h" +#include "esphome/components/display/display_color_utils.h" + +namespace esphome { +namespace mipi_spi { + +constexpr static const char *const TAG = "display.mipi_spi"; +static const uint8_t SW_RESET_CMD = 0x01; +static const uint8_t SLEEP_OUT = 0x11; +static const uint8_t NORON = 0x13; +static const uint8_t INVERT_OFF = 0x20; +static const uint8_t INVERT_ON = 0x21; +static const uint8_t ALL_ON = 0x23; +static const uint8_t WRAM = 0x24; +static const uint8_t MIPI = 0x26; +static const uint8_t DISPLAY_ON = 0x29; +static const uint8_t RASET = 0x2B; +static const uint8_t CASET = 0x2A; +static const uint8_t WDATA = 0x2C; +static const uint8_t TEON = 0x35; +static const uint8_t MADCTL_CMD = 0x36; +static const uint8_t PIXFMT = 0x3A; +static const uint8_t BRIGHTNESS = 0x51; +static const uint8_t SWIRE1 = 0x5A; +static const uint8_t SWIRE2 = 0x5B; +static const uint8_t PAGESEL = 0xFE; + +static const uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top +static const uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left +static const uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes +static const uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order +static const uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order +static const uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally +static const uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically + +static const uint8_t DELAY_FLAG = 0xFF; +// store a 16 bit value in a buffer, big endian. +static inline void put16_be(uint8_t *buf, uint16_t value) { + buf[0] = value >> 8; + buf[1] = value; +} + +enum PixelMode { + PIXEL_MODE_16, + PIXEL_MODE_18, +}; + +class MipiSpi : public display::DisplayBuffer, + public spi::SPIDevice { + public: + MipiSpi(size_t width, size_t height, int16_t offset_width, int16_t offset_height, display::ColorBitness color_depth) + : width_(width), + height_(height), + offset_width_(offset_width), + offset_height_(offset_height), + color_depth_(color_depth) {} + void set_model(const char *model) { this->model_ = model; } + void update() override; + void setup() override; + display::ColorOrder get_color_mode() { + return this->madctl_ & MADCTL_BGR ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB; + } + + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void set_enable_pins(std::vector enable_pins) { this->enable_pins_ = std::move(enable_pins); } + void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } + void set_invert_colors(bool invert_colors) { + this->invert_colors_ = invert_colors; + this->reset_params_(); + } + void set_brightness(uint8_t brightness) { + this->brightness_ = brightness; + this->reset_params_(); + } + + void set_draw_from_origin(bool draw_from_origin) { this->draw_from_origin_ = draw_from_origin; } + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } + void dump_config() override; + + int get_width_internal() override { return this->width_; } + int get_height_internal() override { return this->height_; } + bool can_proceed() override { return this->setup_complete_; } + void set_init_sequence(const std::vector &sequence) { this->init_sequence_ = sequence; } + void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; } + void set_spi_16(bool spi_16) { this->spi_16_ = spi_16; } + + protected: + bool check_buffer_() { + if (this->is_failed()) + return false; + if (this->buffer_ != nullptr) + return true; + auto bytes_per_pixel = this->color_depth_ == display::COLOR_BITNESS_565 ? 2 : 1; + this->init_internal_(this->width_ * this->height_ * bytes_per_pixel); + if (this->buffer_ == nullptr) { + this->mark_failed(); + return false; + } + this->buffer_bytes_ = this->width_ * this->height_ * bytes_per_pixel; + return true; + } + void fill(Color color) override; + void draw_absolute_pixel_internal(int x, int y, Color color) override; + void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, + display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override; + void write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride); + void write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride); + void write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride); + void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, + int x_pad); + /** + * the RM67162 in quad SPI mode seems to work like this (not in the datasheet, this is deduced from the + * sample code.) + * + * Immediately after enabling /CS send 4 bytes in single-dataline SPI mode: + * 0: either 0x2 or 0x32. The first indicates that any subsequent data bytes after the initial 4 will be + * sent in 1-dataline SPI. The second indicates quad mode. + * 1: 0x00 + * 2: The command (register address) byte. + * 3: 0x00 + * + * This is followed by zero or more data bytes in either 1-wire or 4-wire mode, depending on the first byte. + * At the conclusion of the write, de-assert /CS. + * + * @param cmd + * @param bytes + * @param len + */ + void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len); + + void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); } + void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); } + void reset_params_(); + void write_init_sequence_(); + void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2); + + GPIOPin *reset_pin_{nullptr}; + std::vector enable_pins_{}; + GPIOPin *dc_pin_{nullptr}; + uint16_t x_low_{1}; + uint16_t y_low_{1}; + uint16_t x_high_{0}; + uint16_t y_high_{0}; + bool setup_complete_{}; + + bool invert_colors_{}; + size_t width_; + size_t height_; + int16_t offset_width_; + int16_t offset_height_; + size_t buffer_bytes_{0}; + display::ColorBitness color_depth_; + PixelMode pixel_mode_{PIXEL_MODE_16}; + uint8_t bus_width_{}; + bool spi_16_{}; + uint8_t madctl_{}; + bool draw_from_origin_{false}; + unsigned draw_rounding_{2}; + optional brightness_{}; + const char *model_{"Unknown"}; + std::vector init_sequence_{}; +}; +} // namespace mipi_spi +} // namespace esphome diff --git a/esphome/components/mipi_spi/models/__init__.py b/esphome/components/mipi_spi/models/__init__.py new file mode 100644 index 0000000000..e9726032d4 --- /dev/null +++ b/esphome/components/mipi_spi/models/__init__.py @@ -0,0 +1,65 @@ +from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE +import esphome.config_validation as cv +from esphome.const import CONF_HEIGHT, CONF_OFFSET_HEIGHT, CONF_OFFSET_WIDTH, CONF_WIDTH + +from .. import CONF_NATIVE_HEIGHT, CONF_NATIVE_WIDTH + +MADCTL_MY = 0x80 # Bit 7 Bottom to top +MADCTL_MX = 0x40 # Bit 6 Right to left +MADCTL_MV = 0x20 # Bit 5 Reverse Mode +MADCTL_ML = 0x10 # Bit 4 LCD refresh Bottom to top +MADCTL_RGB = 0x00 # Bit 3 Red-Green-Blue pixel order +MADCTL_BGR = 0x08 # Bit 3 Blue-Green-Red pixel order +MADCTL_MH = 0x04 # Bit 2 LCD refresh right to left + +# These bits are used instead of the above bits on some chips, where using MX and MY results in incorrect +# partial updates. +MADCTL_XFLIP = 0x02 # Mirror the display horizontally +MADCTL_YFLIP = 0x01 # Mirror the display vertically + +DELAY_FLAG = 0xFFF # Special flag to indicate a delay + + +def delay(ms): + return DELAY_FLAG, ms + + +class DriverChip: + models = {} + + def __init__( + self, + name: str, + modes=(TYPE_SINGLE, TYPE_QUAD, TYPE_OCTAL), + initsequence=None, + **defaults, + ): + name = name.upper() + self.name = name + self.modes = modes + self.initsequence = initsequence + self.defaults = defaults + DriverChip.models[name] = self + + def extend(self, name, **kwargs): + defaults = self.defaults.copy() + if ( + CONF_WIDTH in defaults + and CONF_OFFSET_WIDTH in kwargs + and CONF_NATIVE_WIDTH not in defaults + ): + defaults[CONF_NATIVE_WIDTH] = defaults[CONF_WIDTH] + if ( + CONF_HEIGHT in defaults + and CONF_OFFSET_HEIGHT in kwargs + and CONF_NATIVE_HEIGHT not in defaults + ): + defaults[CONF_NATIVE_HEIGHT] = defaults[CONF_HEIGHT] + defaults.update(kwargs) + return DriverChip(name, self.modes, initsequence=self.initsequence, **defaults) + + def get_default(self, key, fallback=False): + return self.defaults.get(key, fallback) + + def option(self, name, fallback=False): + return cv.Optional(name, default=self.get_default(name, fallback)) diff --git a/esphome/components/mipi_spi/models/amoled.py b/esphome/components/mipi_spi/models/amoled.py new file mode 100644 index 0000000000..14277b243f --- /dev/null +++ b/esphome/components/mipi_spi/models/amoled.py @@ -0,0 +1,72 @@ +from esphome.components.spi import TYPE_QUAD + +from .. import MODE_RGB +from . import DriverChip, delay +from .commands import MIPI, NORON, PAGESEL, PIXFMT, SLPOUT, SWIRE1, SWIRE2, TEON, WRAM + +DriverChip( + "T-DISPLAY-S3-AMOLED", + width=240, + height=536, + cs_pin=6, + reset_pin=17, + enable_pin=38, + bus_mode=TYPE_QUAD, + brightness=0xD0, + color_order=MODE_RGB, + initsequence=(SLPOUT,), # Requires early SLPOUT +) + +DriverChip( + name="T-DISPLAY-S3-AMOLED-PLUS", + width=240, + height=536, + cs_pin=6, + reset_pin=17, + dc_pin=7, + enable_pin=38, + data_rate="40MHz", + brightness=0xD0, + color_order=MODE_RGB, + initsequence=( + (PAGESEL, 4), + (0x6A, 0x00), + (PAGESEL, 0x05), + (PAGESEL, 0x07), + (0x07, 0x4F), + (PAGESEL, 0x01), + (0x2A, 0x02), + (0x2B, 0x73), + (PAGESEL, 0x0A), + (0x29, 0x10), + (PAGESEL, 0x00), + (0x53, 0x20), + (TEON, 0x00), + (PIXFMT, 0x75), + (0xC4, 0x80), + ), +) + +RM690B0 = DriverChip( + "RM690B0", + brightness=0xD0, + color_order=MODE_RGB, + width=480, + height=600, + initsequence=( + (PAGESEL, 0x20), + (MIPI, 0x0A), + (WRAM, 0x80), + (SWIRE1, 0x51), + (SWIRE2, 0x2E), + (PAGESEL, 0x00), + (0xC2, 0x00), + delay(10), + (TEON, 0x00), + (NORON,), + ), +) + +T4_S3_AMOLED = RM690B0.extend("T4-S3", width=450, offset_width=16, bus_mode=TYPE_QUAD) + +models = {} diff --git a/esphome/components/mipi_spi/models/commands.py b/esphome/components/mipi_spi/models/commands.py new file mode 100644 index 0000000000..032a6e6b2b --- /dev/null +++ b/esphome/components/mipi_spi/models/commands.py @@ -0,0 +1,82 @@ +# MIPI DBI commands + +NOP = 0x00 +SWRESET = 0x01 +RDDID = 0x04 +RDDST = 0x09 +RDMODE = 0x0A +RDMADCTL = 0x0B +RDPIXFMT = 0x0C +RDIMGFMT = 0x0D +RDSELFDIAG = 0x0F +SLEEP_IN = 0x10 +SLPIN = 0x10 +SLEEP_OUT = 0x11 +SLPOUT = 0x11 +PTLON = 0x12 +NORON = 0x13 +INVERT_OFF = 0x20 +INVOFF = 0x20 +INVERT_ON = 0x21 +INVON = 0x21 +ALL_ON = 0x23 +WRAM = 0x24 +GAMMASET = 0x26 +MIPI = 0x26 +DISPOFF = 0x28 +DISPON = 0x29 +CASET = 0x2A +PASET = 0x2B +RASET = 0x2B +RAMWR = 0x2C +WDATA = 0x2C +RAMRD = 0x2E +PTLAR = 0x30 +VSCRDEF = 0x33 +TEON = 0x35 +MADCTL = 0x36 +MADCTL_CMD = 0x36 +VSCRSADD = 0x37 +IDMOFF = 0x38 +IDMON = 0x39 +COLMOD = 0x3A +PIXFMT = 0x3A +GETSCANLINE = 0x45 +BRIGHTNESS = 0x51 +WRDISBV = 0x51 +RDDISBV = 0x52 +WRCTRLD = 0x53 +SWIRE1 = 0x5A +SWIRE2 = 0x5B +IFMODE = 0xB0 +FRMCTR1 = 0xB1 +FRMCTR2 = 0xB2 +FRMCTR3 = 0xB3 +INVCTR = 0xB4 +DFUNCTR = 0xB6 +ETMOD = 0xB7 +PWCTR1 = 0xC0 +PWCTR2 = 0xC1 +PWCTR3 = 0xC2 +PWCTR4 = 0xC3 +PWCTR5 = 0xC4 +VMCTR1 = 0xC5 +IFCTR = 0xC6 +VMCTR2 = 0xC7 +GMCTR = 0xC8 +SETEXTC = 0xC8 +PWSET = 0xD0 +VMCTR = 0xD1 +PWSETN = 0xD2 +RDID4 = 0xD3 +RDINDEX = 0xD9 +RDID1 = 0xDA +RDID2 = 0xDB +RDID3 = 0xDC +RDIDX = 0xDD +GMCTRP1 = 0xE0 +GMCTRN1 = 0xE1 +CSCON = 0xF0 +PWCTR6 = 0xF6 +ADJCTL3 = 0xF7 +PAGESEL = 0xFE diff --git a/esphome/components/mipi_spi/models/cyd.py b/esphome/components/mipi_spi/models/cyd.py new file mode 100644 index 0000000000..a25ecf33a8 --- /dev/null +++ b/esphome/components/mipi_spi/models/cyd.py @@ -0,0 +1,10 @@ +from .ili import ILI9341 + +ILI9341.extend( + "ESP32-2432S028", + data_rate="40MHz", + cs_pin=15, + dc_pin=2, +) + +models = {} diff --git a/esphome/components/mipi_spi/models/ili.py b/esphome/components/mipi_spi/models/ili.py new file mode 100644 index 0000000000..cc12b38f5d --- /dev/null +++ b/esphome/components/mipi_spi/models/ili.py @@ -0,0 +1,749 @@ +from esphome.components.spi import TYPE_OCTAL + +from .. import MODE_RGB +from . import DriverChip, delay +from .commands import ( + ADJCTL3, + CSCON, + DFUNCTR, + ETMOD, + FRMCTR1, + FRMCTR2, + FRMCTR3, + GAMMASET, + GMCTR, + GMCTRN1, + GMCTRP1, + IDMOFF, + IFCTR, + IFMODE, + INVCTR, + NORON, + PWCTR1, + PWCTR2, + PWCTR3, + PWCTR4, + PWCTR5, + PWSET, + PWSETN, + SETEXTC, + SWRESET, + VMCTR, + VMCTR1, + VMCTR2, + VSCRSADD, +) + +DriverChip( + "M5CORE", + width=320, + height=240, + cs_pin=14, + dc_pin=27, + reset_pin=33, + initsequence=( + (SETEXTC, 0xFF, 0x93, 0x42), + (PWCTR1, 0x12, 0x12), + (PWCTR2, 0x03), + (VMCTR1, 0xF2), + (IFMODE, 0xE0), + (0xF6, 0x01, 0x00, 0x00), + ( + GMCTRP1, + 0x00, + 0x0C, + 0x11, + 0x04, + 0x11, + 0x08, + 0x37, + 0x89, + 0x4C, + 0x06, + 0x0C, + 0x0A, + 0x2E, + 0x34, + 0x0F, + ), + ( + GMCTRN1, + 0x00, + 0x0B, + 0x11, + 0x05, + 0x13, + 0x09, + 0x33, + 0x67, + 0x48, + 0x07, + 0x0E, + 0x0B, + 0x2E, + 0x33, + 0x0F, + ), + (DFUNCTR, 0x08, 0x82, 0x1D, 0x04), + (IDMOFF,), + ), +) +ILI9341 = DriverChip( + "ILI9341", + mirror_x=True, + width=240, + height=320, + initsequence=( + (0xEF, 0x03, 0x80, 0x02), + (0xCF, 0x00, 0xC1, 0x30), + (0xED, 0x64, 0x03, 0x12, 0x81), + (0xE8, 0x85, 0x00, 0x78), + (0xCB, 0x39, 0x2C, 0x00, 0x34, 0x02), + (0xF7, 0x20), + (0xEA, 0x00, 0x00), + (PWCTR1, 0x23), + (PWCTR2, 0x10), + (VMCTR1, 0x3E, 0x28), + (VMCTR2, 0x86), + (VSCRSADD, 0x00), + (FRMCTR1, 0x00, 0x18), + (DFUNCTR, 0x08, 0x82, 0x27), + (0xF2, 0x00), + (GAMMASET, 0x01), + ( + GMCTRP1, + 0x0F, + 0x31, + 0x2B, + 0x0C, + 0x0E, + 0x08, + 0x4E, + 0xF1, + 0x37, + 0x07, + 0x10, + 0x03, + 0x0E, + 0x09, + 0x00, + ), + ( + GMCTRN1, + 0x00, + 0x0E, + 0x14, + 0x03, + 0x11, + 0x07, + 0x31, + 0xC1, + 0x48, + 0x08, + 0x0F, + 0x0C, + 0x31, + 0x36, + 0x0F, + ), + ), +) +DriverChip( + "ILI9481", + mirror_x=True, + width=320, + height=480, + use_axis_flips=True, + initsequence=( + (PWSET, 0x07, 0x42, 0x18), + (VMCTR, 0x00, 0x07, 0x10), + (PWSETN, 0x01, 0x02), + (PWCTR1, 0x10, 0x3B, 0x00, 0x02, 0x11), + (VMCTR1, 0x03), + (IFCTR, 0x83), + (GMCTR, 0x32, 0x36, 0x45, 0x06, 0x16, 0x37, 0x75, 0x77, 0x54, 0x0C, 0x00), + ), +) +DriverChip( + "ILI9486", + mirror_x=True, + width=320, + height=480, + initsequence=( + (PWCTR3, 0x44), + (VMCTR1, 0x00, 0x00, 0x00, 0x00), + ( + GMCTRP1, + 0x0F, + 0x1F, + 0x1C, + 0x0C, + 0x0F, + 0x08, + 0x48, + 0x98, + 0x37, + 0x0A, + 0x13, + 0x04, + 0x11, + 0x0D, + 0x00, + ), + ( + GMCTRN1, + 0x0F, + 0x32, + 0x2E, + 0x0B, + 0x0D, + 0x05, + 0x47, + 0x75, + 0x37, + 0x06, + 0x10, + 0x03, + 0x24, + 0x20, + 0x00, + ), + ), +) +DriverChip( + "ILI9488", + width=320, + height=480, + pixel_mode="18bit", + initsequence=( + ( + GMCTRP1, + 0x0F, + 0x24, + 0x1C, + 0x0A, + 0x0F, + 0x08, + 0x43, + 0x88, + 0x32, + 0x0F, + 0x10, + 0x06, + 0x0F, + 0x07, + 0x00, + ), + ( + GMCTRN1, + 0x0F, + 0x38, + 0x30, + 0x09, + 0x0F, + 0x0F, + 0x4E, + 0x77, + 0x3C, + 0x07, + 0x10, + 0x05, + 0x23, + 0x1B, + 0x00, + ), + (PWCTR1, 0x17, 0x15), + (PWCTR2, 0x41), + (VMCTR1, 0x00, 0x12, 0x80), + (IFMODE, 0x00), + (FRMCTR1, 0xA0), + (INVCTR, 0x02), + (0xE9, 0x00), + (ADJCTL3, 0xA9, 0x51, 0x2C, 0x82), + ), +) +ILI9488_A = DriverChip( + "ILI9488_A", + width=320, + height=480, + invert_colors=False, + pixel_mode="18bit", + mirror_x=True, + initsequence=( + ( + GMCTRP1, + 0x00, + 0x03, + 0x09, + 0x08, + 0x16, + 0x0A, + 0x3F, + 0x78, + 0x4C, + 0x09, + 0x0A, + 0x08, + 0x16, + 0x1A, + 0x0F, + ), + ( + GMCTRN1, + 0x00, + 0x16, + 0x19, + 0x03, + 0x0F, + 0x05, + 0x32, + 0x45, + 0x46, + 0x04, + 0x0E, + 0x0D, + 0x35, + 0x37, + 0x0F, + ), + (PWCTR1, 0x17, 0x15), + (PWCTR2, 0x41), + (VMCTR1, 0x00, 0x12, 0x80), + (IFMODE, 0x00), + (FRMCTR1, 0xA0), + (INVCTR, 0x02), + (DFUNCTR, 0x02, 0x02), + (0xE9, 0x00), + (ADJCTL3, 0xA9, 0x51, 0x2C, 0x82), + ), +) +ST7796 = DriverChip( + "ST7796", + mirror_x=True, + width=320, + height=480, + initsequence=( + (SWRESET,), + (CSCON, 0xC3), + (CSCON, 0x96), + (VMCTR1, 0x1C), + (IFMODE, 0x80), + (INVCTR, 0x01), + (DFUNCTR, 0x80, 0x02, 0x3B), + (ETMOD, 0xC6), + (CSCON, 0x69), + (CSCON, 0x3C), + ), +) +DriverChip( + "S3BOX", + width=320, + height=240, + mirror_x=True, + mirror_y=True, + invert_colors=False, + data_rate="40MHz", + dc_pin=4, + cs_pin=5, + # reset_pin={CONF_INVERTED: True, CONF_NUMBER: 48}, + initsequence=( + (0xEF, 0x03, 0x80, 0x02), + (0xCF, 0x00, 0xC1, 0x30), + (0xED, 0x64, 0x03, 0x12, 0x81), + (0xE8, 0x85, 0x00, 0x78), + (0xCB, 0x39, 0x2C, 0x00, 0x34, 0x02), + (0xF7, 0x20), + (0xEA, 0x00, 0x00), + (PWCTR1, 0x23), + (PWCTR2, 0x10), + (VMCTR1, 0x3E, 0x28), + (VMCTR2, 0x86), + (VSCRSADD, 0x00), + (FRMCTR1, 0x00, 0x18), + (DFUNCTR, 0x08, 0x82, 0x27), + (0xF2, 0x00), + (GAMMASET, 0x01), + ( + GMCTRP1, + 0x0F, + 0x31, + 0x2B, + 0x0C, + 0x0E, + 0x08, + 0x4E, + 0xF1, + 0x37, + 0x07, + 0x10, + 0x03, + 0x0E, + 0x09, + 0x00, + ), + ( + GMCTRN1, + 0x00, + 0x0E, + 0x14, + 0x03, + 0x11, + 0x07, + 0x31, + 0xC1, + 0x48, + 0x08, + 0x0F, + 0x0C, + 0x31, + 0x36, + 0x0F, + ), + ), +) +DriverChip( + "S3BOXLITE", + mirror_x=True, + color_order=MODE_RGB, + width=320, + height=240, + cs_pin=5, + dc_pin=4, + reset_pin=48, + initsequence=( + (0xEF, 0x03, 0x80, 0x02), + (0xCF, 0x00, 0xC1, 0x30), + (0xED, 0x64, 0x03, 0x12, 0x81), + (0xE8, 0x85, 0x00, 0x78), + (0xCB, 0x39, 0x2C, 0x00, 0x34, 0x02), + (0xF7, 0x20), + (0xEA, 0x00, 0x00), + (PWCTR1, 0x23), + (PWCTR2, 0x10), + (VMCTR1, 0x3E, 0x28), + (VMCTR2, 0x86), + (VSCRSADD, 0x00), + (FRMCTR1, 0x00, 0x18), + (DFUNCTR, 0x08, 0x82, 0x27), + (0xF2, 0x00), + (GAMMASET, 0x01), + ( + GMCTRP1, + 0xF0, + 0x09, + 0x0B, + 0x06, + 0x04, + 0x15, + 0x2F, + 0x54, + 0x42, + 0x3C, + 0x17, + 0x14, + 0x18, + 0x1B, + ), + ( + GMCTRN1, + 0xE0, + 0x09, + 0x0B, + 0x06, + 0x04, + 0x03, + 0x2B, + 0x43, + 0x42, + 0x3B, + 0x16, + 0x14, + 0x17, + 0x1B, + ), + ), +) +ST7789V = DriverChip( + "ST7789V", + width=240, + height=320, + initsequence=( + (DFUNCTR, 0x0A, 0x82), + (FRMCTR2, 0x0C, 0x0C, 0x00, 0x33, 0x33), + (ETMOD, 0x35), + (0xBB, 0x28), + (PWCTR1, 0x0C), + (PWCTR3, 0x01, 0xFF), + (PWCTR4, 0x10), + (PWCTR5, 0x20), + (IFCTR, 0x0F), + (PWSET, 0xA4, 0xA1), + ( + GMCTRP1, + 0xD0, + 0x00, + 0x02, + 0x07, + 0x0A, + 0x28, + 0x32, + 0x44, + 0x42, + 0x06, + 0x0E, + 0x12, + 0x14, + 0x17, + ), + ( + GMCTRN1, + 0xD0, + 0x00, + 0x02, + 0x07, + 0x0A, + 0x28, + 0x31, + 0x54, + 0x47, + 0x0E, + 0x1C, + 0x17, + 0x1B, + 0x1E, + ), + ), +) +DriverChip( + "GC9A01A", + mirror_x=True, + width=240, + height=240, + initsequence=( + (0xEF,), + (0xEB, 0x14), + (0xFE,), + (0xEF,), + (0xEB, 0x14), + (0x84, 0x40), + (0x85, 0xFF), + (0x86, 0xFF), + (0x87, 0xFF), + (0x88, 0x0A), + (0x89, 0x21), + (0x8A, 0x00), + (0x8B, 0x80), + (0x8C, 0x01), + (0x8D, 0x01), + (0x8E, 0xFF), + (0x8F, 0xFF), + (0xB6, 0x00, 0x00), + (0x90, 0x08, 0x08, 0x08, 0x08), + (0xBD, 0x06), + (0xBC, 0x00), + (0xFF, 0x60, 0x01, 0x04), + (0xC3, 0x13), + (0xC4, 0x13), + (0xF9, 0x22), + (0xBE, 0x11), + (0xE1, 0x10, 0x0E), + (0xDF, 0x21, 0x0C, 0x02), + (0xF0, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A), + (0xF1, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F), + (0xF2, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A), + (0xF3, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F), + (0xED, 0x1B, 0x0B), + (0xAE, 0x77), + (0xCD, 0x63), + (0xE8, 0x34), + ( + 0x62, + 0x18, + 0x0D, + 0x71, + 0xED, + 0x70, + 0x70, + 0x18, + 0x0F, + 0x71, + 0xEF, + 0x70, + 0x70, + ), + ( + 0x63, + 0x18, + 0x11, + 0x71, + 0xF1, + 0x70, + 0x70, + 0x18, + 0x13, + 0x71, + 0xF3, + 0x70, + 0x70, + ), + (0x64, 0x28, 0x29, 0xF1, 0x01, 0xF1, 0x00, 0x07), + (0x66, 0x3C, 0x00, 0xCD, 0x67, 0x45, 0x45, 0x10, 0x00, 0x00, 0x00), + (0x67, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x01, 0x54, 0x10, 0x32, 0x98), + (0x74, 0x10, 0x85, 0x80, 0x00, 0x00, 0x4E, 0x00), + (0x98, 0x3E, 0x07), + (0x35,), + ), +) +DriverChip( + "GC9D01N", + width=160, + height=160, + initsequence=( + (0xFE,), + (0xEF,), + (0x80, 0xFF), + (0x81, 0xFF), + (0x82, 0xFF), + (0x83, 0xFF), + (0x84, 0xFF), + (0x85, 0xFF), + (0x86, 0xFF), + (0x87, 0xFF), + (0x88, 0xFF), + (0x89, 0xFF), + (0x8A, 0xFF), + (0x8B, 0xFF), + (0x8C, 0xFF), + (0x8D, 0xFF), + (0x8E, 0xFF), + (0x8F, 0xFF), + (0x3A, 0x05), + (0xEC, 0x01), + (0x74, 0x02, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00), + (0x98, 0x3E), + (0x99, 0x3E), + (0xB5, 0x0D, 0x0D), + (0x60, 0x38, 0x0F, 0x79, 0x67), + (0x61, 0x38, 0x11, 0x79, 0x67), + (0x64, 0x38, 0x17, 0x71, 0x5F, 0x79, 0x67), + (0x65, 0x38, 0x13, 0x71, 0x5B, 0x79, 0x67), + (0x6A, 0x00, 0x00), + (0x6C, 0x22, 0x02, 0x22, 0x02, 0x22, 0x22, 0x50), + ( + 0x6E, + 0x03, + 0x03, + 0x01, + 0x01, + 0x00, + 0x00, + 0x0F, + 0x0F, + 0x0D, + 0x0D, + 0x0B, + 0x0B, + 0x09, + 0x09, + 0x00, + 0x00, + 0x00, + 0x00, + 0x0A, + 0x0A, + 0x0C, + 0x0C, + 0x0E, + 0x0E, + 0x10, + 0x10, + 0x00, + 0x00, + 0x02, + 0x02, + 0x04, + 0x04, + ), + (0xBF, 0x01), + (0xF9, 0x40), + (0x9B, 0x3B, 0x93, 0x33, 0x7F, 0x00), + (0x7E, 0x30), + (0x70, 0x0D, 0x02, 0x08, 0x0D, 0x02, 0x08), + (0x71, 0x0D, 0x02, 0x08), + (0x91, 0x0E, 0x09), + (0xC3, 0x19, 0xC4, 0x19, 0xC9, 0x3C), + (0xF0, 0x53, 0x15, 0x0A, 0x04, 0x00, 0x3E), + (0xF1, 0x56, 0xA8, 0x7F, 0x33, 0x34, 0x5F), + (0xF2, 0x53, 0x15, 0x0A, 0x04, 0x00, 0x3A), + (0xF3, 0x52, 0xA4, 0x7F, 0x33, 0x34, 0xDF), + ), +) +DriverChip( + "ST7735", + color_order=MODE_RGB, + width=128, + height=160, + initsequence=( + SWRESET, + delay(10), + (FRMCTR1, 0x01, 0x2C, 0x2D), + (FRMCTR2, 0x01, 0x2C, 0x2D), + (FRMCTR3, 0x01, 0x2C, 0x2D, 0x01, 0x2C, 0x2D), + (INVCTR, 0x07), + (PWCTR1, 0xA2, 0x02, 0x84), + (PWCTR2, 0xC5), + (PWCTR3, 0x0A, 0x00), + (PWCTR4, 0x8A, 0x2A), + (PWCTR5, 0x8A, 0xEE), + (VMCTR1, 0x0E), + ( + GMCTRP1, + 0x02, + 0x1C, + 0x07, + 0x12, + 0x37, + 0x32, + 0x29, + 0x2D, + 0x29, + 0x25, + 0x2B, + 0x39, + 0x00, + 0x01, + 0x03, + 0x10, + ), + ( + GMCTRN1, + 0x03, + 0x1D, + 0x07, + 0x06, + 0x2E, + 0x2C, + 0x29, + 0x2D, + 0x2E, + 0x2E, + 0x37, + 0x3F, + 0x00, + 0x00, + 0x02, + 0x10, + ), + NORON, + ), +) +ST7796.extend( + "WT32-SC01-PLUS", + bus_mode=TYPE_OCTAL, + mirror_x=True, + reset_pin=4, + dc_pin=0, + invert_colors=True, +) + +models = {} diff --git a/esphome/components/mipi_spi/models/jc.py b/esphome/components/mipi_spi/models/jc.py new file mode 100644 index 0000000000..449c5b87ae --- /dev/null +++ b/esphome/components/mipi_spi/models/jc.py @@ -0,0 +1,260 @@ +from esphome.components.spi import TYPE_QUAD +import esphome.config_validation as cv +from esphome.const import CONF_IGNORE_STRAPPING_WARNING, CONF_NUMBER + +from .. import MODE_RGB +from . import DriverChip + +AXS15231 = DriverChip( + "AXS15231", + draw_rounding=8, + swap_xy=cv.UNDEFINED, + color_order=MODE_RGB, + bus_mode=TYPE_QUAD, + initsequence=( + (0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A, 0xA5), + (0xC1, 0x33), + (0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + ), +) + +AXS15231.extend( + "JC3248W535", + width=320, + height=480, + cs_pin={CONF_NUMBER: 45, CONF_IGNORE_STRAPPING_WARNING: True}, + data_rate="40MHz", +) + +DriverChip( + "JC3636W518", + height=360, + width=360, + offset_height=1, + draw_rounding=1, + cs_pin=10, + reset_pin=47, + invert_colors=True, + color_order=MODE_RGB, + bus_mode=TYPE_QUAD, + data_rate="40MHz", + initsequence=( + (0xF0, 0x08), + (0xF2, 0x08), + (0x9B, 0x51), + (0x86, 0x53), + (0xF2, 0x80), + (0xF0, 0x00), + (0xF0, 0x01), + (0xF1, 0x01), + (0xB0, 0x54), + (0xB1, 0x3F), + (0xB2, 0x2A), + (0xB4, 0x46), + (0xB5, 0x34), + (0xB6, 0xD5), + (0xB7, 0x30), + (0xBA, 0x00), + (0xBB, 0x08), + (0xBC, 0x08), + (0xBD, 0x00), + (0xC0, 0x80), + (0xC1, 0x10), + (0xC2, 0x37), + (0xC3, 0x80), + (0xC4, 0x10), + (0xC5, 0x37), + (0xC6, 0xA9), + (0xC7, 0x41), + (0xC8, 0x51), + (0xC9, 0xA9), + (0xCA, 0x41), + (0xCB, 0x51), + (0xD0, 0x91), + (0xD1, 0x68), + (0xD2, 0x69), + (0xF5, 0x00, 0xA5), + (0xDD, 0x3F), + (0xDE, 0x3F), + (0xF1, 0x10), + (0xF0, 0x00), + (0xF0, 0x02), + ( + 0xE0, + 0x70, + 0x09, + 0x12, + 0x0C, + 0x0B, + 0x27, + 0x38, + 0x54, + 0x4E, + 0x19, + 0x15, + 0x15, + 0x2C, + 0x2F, + ), + ( + 0xE1, + 0x70, + 0x08, + 0x11, + 0x0C, + 0x0B, + 0x27, + 0x38, + 0x43, + 0x4C, + 0x18, + 0x14, + 0x14, + 0x2B, + 0x2D, + ), + (0xF0, 0x10), + (0xF3, 0x10), + (0xE0, 0x08), + (0xE1, 0x00), + (0xE2, 0x00), + (0xE3, 0x00), + (0xE4, 0xE0), + (0xE5, 0x06), + (0xE6, 0x21), + (0xE7, 0x00), + (0xE8, 0x05), + (0xE9, 0x82), + (0xEA, 0xDF), + (0xEB, 0x89), + (0xEC, 0x20), + (0xED, 0x14), + (0xEE, 0xFF), + (0xEF, 0x00), + (0xF8, 0xFF), + (0xF9, 0x00), + (0xFA, 0x00), + (0xFB, 0x30), + (0xFC, 0x00), + (0xFD, 0x00), + (0xFE, 0x00), + (0xFF, 0x00), + (0x60, 0x42), + (0x61, 0xE0), + (0x62, 0x40), + (0x63, 0x40), + (0x64, 0x02), + (0x65, 0x00), + (0x66, 0x40), + (0x67, 0x03), + (0x68, 0x00), + (0x69, 0x00), + (0x6A, 0x00), + (0x6B, 0x00), + (0x70, 0x42), + (0x71, 0xE0), + (0x72, 0x40), + (0x73, 0x40), + (0x74, 0x02), + (0x75, 0x00), + (0x76, 0x40), + (0x77, 0x03), + (0x78, 0x00), + (0x79, 0x00), + (0x7A, 0x00), + (0x7B, 0x00), + (0x80, 0x48), + (0x81, 0x00), + (0x82, 0x05), + (0x83, 0x02), + (0x84, 0xDD), + (0x85, 0x00), + (0x86, 0x00), + (0x87, 0x00), + (0x88, 0x48), + (0x89, 0x00), + (0x8A, 0x07), + (0x8B, 0x02), + (0x8C, 0xDF), + (0x8D, 0x00), + (0x8E, 0x00), + (0x8F, 0x00), + (0x90, 0x48), + (0x91, 0x00), + (0x92, 0x09), + (0x93, 0x02), + (0x94, 0xE1), + (0x95, 0x00), + (0x96, 0x00), + (0x97, 0x00), + (0x98, 0x48), + (0x99, 0x00), + (0x9A, 0x0B), + (0x9B, 0x02), + (0x9C, 0xE3), + (0x9D, 0x00), + (0x9E, 0x00), + (0x9F, 0x00), + (0xA0, 0x48), + (0xA1, 0x00), + (0xA2, 0x04), + (0xA3, 0x02), + (0xA4, 0xDC), + (0xA5, 0x00), + (0xA6, 0x00), + (0xA7, 0x00), + (0xA8, 0x48), + (0xA9, 0x00), + (0xAA, 0x06), + (0xAB, 0x02), + (0xAC, 0xDE), + (0xAD, 0x00), + (0xAE, 0x00), + (0xAF, 0x00), + (0xB0, 0x48), + (0xB1, 0x00), + (0xB2, 0x08), + (0xB3, 0x02), + (0xB4, 0xE0), + (0xB5, 0x00), + (0xB6, 0x00), + (0xB7, 0x00), + (0xB8, 0x48), + (0xB9, 0x00), + (0xBA, 0x0A), + (0xBB, 0x02), + (0xBC, 0xE2), + (0xBD, 0x00), + (0xBE, 0x00), + (0xBF, 0x00), + (0xC0, 0x12), + (0xC1, 0xAA), + (0xC2, 0x65), + (0xC3, 0x74), + (0xC4, 0x47), + (0xC5, 0x56), + (0xC6, 0x00), + (0xC7, 0x88), + (0xC8, 0x99), + (0xC9, 0x33), + (0xD0, 0x21), + (0xD1, 0xAA), + (0xD2, 0x65), + (0xD3, 0x74), + (0xD4, 0x47), + (0xD5, 0x56), + (0xD6, 0x00), + (0xD7, 0x88), + (0xD8, 0x99), + (0xD9, 0x33), + (0xF3, 0x01), + (0xF0, 0x00), + (0xF0, 0x01), + (0xF1, 0x01), + (0xA0, 0x0B), + (0xA3, 0x2A), + (0xA5, 0xC3), + ), +) + +models = {} diff --git a/esphome/components/mipi_spi/models/lanbon.py b/esphome/components/mipi_spi/models/lanbon.py new file mode 100644 index 0000000000..6f9aa58674 --- /dev/null +++ b/esphome/components/mipi_spi/models/lanbon.py @@ -0,0 +1,15 @@ +from .ili import ST7789V + +ST7789V.extend( + "LANBON-L8", + width=240, + height=320, + mirror_x=True, + mirror_y=True, + data_rate="80MHz", + cs_pin=22, + dc_pin=21, + reset_pin=18, +) + +models = {} diff --git a/esphome/components/mipi_spi/models/lilygo.py b/esphome/components/mipi_spi/models/lilygo.py new file mode 100644 index 0000000000..dd6f9c02f7 --- /dev/null +++ b/esphome/components/mipi_spi/models/lilygo.py @@ -0,0 +1,60 @@ +from esphome.components.spi import TYPE_OCTAL + +from .. import MODE_BGR +from .ili import ST7789V, ST7796 + +ST7789V.extend( + "T-EMBED", + width=170, + height=320, + offset_width=35, + color_order=MODE_BGR, + invert_colors=True, + draw_rounding=1, + cs_pin=10, + dc_pin=13, + reset_pin=9, + data_rate="80MHz", +) + +ST7789V.extend( + "T-DISPLAY", + height=240, + width=135, + offset_width=52, + offset_height=40, + draw_rounding=1, + cs_pin=5, + dc_pin=16, + invert_colors=True, +) +ST7789V.extend( + "T-DISPLAY-S3", + height=320, + width=170, + offset_width=35, + color_order=MODE_BGR, + invert_colors=True, + draw_rounding=1, + dc_pin=7, + cs_pin=6, + reset_pin=5, + enable_pin=[9, 15], + data_rate="10MHz", + bus_mode=TYPE_OCTAL, +) + +ST7796.extend( + "T-DISPLAY-S3-PRO", + width=222, + height=480, + offset_width=49, + draw_rounding=1, + cs_pin=39, + reset_pin=47, + dc_pin=9, + backlight_pin=48, + invert_colors=True, +) + +models = {} diff --git a/esphome/components/mipi_spi/models/waveshare.py b/esphome/components/mipi_spi/models/waveshare.py new file mode 100644 index 0000000000..6d14f56fc6 --- /dev/null +++ b/esphome/components/mipi_spi/models/waveshare.py @@ -0,0 +1,139 @@ +from . import DriverChip +from .ili import ILI9488_A + +DriverChip( + "WAVESHARE-4-TFT", + width=320, + height=480, + invert_colors=True, + spi_16=True, + initsequence=( + ( + 0xF9, + 0x00, + 0x08, + ), + ( + 0xC0, + 0x19, + 0x1A, + ), + ( + 0xC1, + 0x45, + 0x00, + ), + ( + 0xC2, + 0x33, + ), + ( + 0xC5, + 0x00, + 0x28, + ), + ( + 0xB1, + 0xA0, + 0x11, + ), + ( + 0xB4, + 0x02, + ), + ( + 0xB6, + 0x00, + 0x42, + 0x3B, + ), + ( + 0xB7, + 0x07, + ), + ( + 0xE0, + 0x1F, + 0x25, + 0x22, + 0x0B, + 0x06, + 0x0A, + 0x4E, + 0xC6, + 0x39, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ), + ( + 0xE1, + 0x1F, + 0x3F, + 0x3F, + 0x0F, + 0x1F, + 0x0F, + 0x46, + 0x49, + 0x31, + 0x05, + 0x09, + 0x03, + 0x1C, + 0x1A, + 0x00, + ), + ( + 0xF1, + 0x36, + 0x04, + 0x00, + 0x3C, + 0x0F, + 0x0F, + 0xA4, + 0x02, + ), + ( + 0xF2, + 0x18, + 0xA3, + 0x12, + 0x02, + 0x32, + 0x12, + 0xFF, + 0x32, + 0x00, + ), + ( + 0xF4, + 0x40, + 0x00, + 0x08, + 0x91, + 0x04, + ), + ( + 0xF8, + 0x21, + 0x04, + ), + ), +) + +ILI9488_A.extend( + "PICO-RESTOUCH-LCD-3.5", + spi_16=True, + pixel_mode="16bit", + mirror_x=True, + dc_pin=33, + cs_pin=34, + reset_pin=40, + data_rate="20MHz", + invert_colors=True, +) diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 7cdffafdb5..f96d3da251 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -355,6 +355,12 @@ class SPIComponent : public Component { void setup() override; void dump_config() override; + size_t get_bus_width() const { + if (this->data_pins_.empty()) { + return 1; + } + return this->data_pins_.size(); + } protected: GPIOPin *clk_pin_{nullptr}; diff --git a/tests/components/mipi_spi/common.yaml b/tests/components/mipi_spi/common.yaml new file mode 100644 index 0000000000..e4b1e2b30c --- /dev/null +++ b/tests/components/mipi_spi/common.yaml @@ -0,0 +1,38 @@ +spi: + - id: spi_single + clk_pin: + number: ${clk_pin} + allow_other_uses: true + mosi_pin: + number: ${mosi_pin} + +display: + - platform: mipi_spi + spi_16: true + pixel_mode: 18bit + model: ili9488 + dc_pin: ${dc_pin} + cs_pin: ${cs_pin} + reset_pin: ${reset_pin} + data_rate: 20MHz + invert_colors: true + show_test_card: true + spi_mode: mode0 + draw_rounding: 8 + use_axis_flips: true + init_sequence: + - [0xd0, 1, 2, 3] + - delay 10ms + transform: + swap_xy: true + mirror_x: false + mirror_y: true + dimensions: + width: 100 + height: 200 + enable_pin: + - number: ${clk_pin} + allow_other_uses: true + - number: ${enable_pin} + bus_mode: single + diff --git a/tests/components/mipi_spi/test-esp32-2432s028.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-esp32-2432s028.esp32-s3-idf.yaml new file mode 100644 index 0000000000..a28776798c --- /dev/null +++ b/tests/components/mipi_spi/test-esp32-2432s028.esp32-s3-idf.yaml @@ -0,0 +1,41 @@ +spi: + - id: quad_spi + type: quad + interface: spi3 + clk_pin: + number: 47 + data_pins: + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: octal_spi + type: octal + interface: hardware + clk_pin: + number: 0 + data_pins: + - 36 + - 37 + - 38 + - 39 + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 9 + +display: + - platform: mipi_spi + model: ESP32-2432S028 diff --git a/tests/components/mipi_spi/test-jc3248w535.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-jc3248w535.esp32-s3-idf.yaml new file mode 100644 index 0000000000..02b8f78d58 --- /dev/null +++ b/tests/components/mipi_spi/test-jc3248w535.esp32-s3-idf.yaml @@ -0,0 +1,41 @@ +spi: + - id: quad_spi + type: quad + interface: spi3 + clk_pin: + number: 47 + data_pins: + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: octal_spi + type: octal + interface: hardware + clk_pin: + number: 0 + data_pins: + - 36 + - 37 + - 38 + - 39 + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 9 + +display: + - platform: mipi_spi + model: JC3248W535 diff --git a/tests/components/mipi_spi/test-jc3636w518.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-jc3636w518.esp32-s3-idf.yaml new file mode 100644 index 0000000000..147d4833ac --- /dev/null +++ b/tests/components/mipi_spi/test-jc3636w518.esp32-s3-idf.yaml @@ -0,0 +1,19 @@ +spi: + - id: quad_spi + type: quad + interface: spi3 + clk_pin: + number: 36 + data_pins: + - number: 40 + - number: 41 + - number: 42 + - number: 43 + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 9 + +display: + - platform: mipi_spi + model: JC3636W518 diff --git a/tests/components/mipi_spi/test-pico-restouch-lcd-35.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-pico-restouch-lcd-35.esp32-s3-idf.yaml new file mode 100644 index 0000000000..8d96f31fd5 --- /dev/null +++ b/tests/components/mipi_spi/test-pico-restouch-lcd-35.esp32-s3-idf.yaml @@ -0,0 +1,9 @@ +spi: + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 9 + +display: + - platform: mipi_spi + model: Pico-ResTouch-LCD-3.5 diff --git a/tests/components/mipi_spi/test-s3box.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-s3box.esp32-s3-idf.yaml new file mode 100644 index 0000000000..98f6955bf3 --- /dev/null +++ b/tests/components/mipi_spi/test-s3box.esp32-s3-idf.yaml @@ -0,0 +1,41 @@ +spi: + - id: quad_spi + type: quad + interface: spi3 + clk_pin: + number: 47 + data_pins: + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: octal_spi + type: octal + interface: hardware + clk_pin: + number: 0 + data_pins: + - 36 + - 37 + - 38 + - 39 + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 9 + +display: + - platform: mipi_spi + model: S3BOX diff --git a/tests/components/mipi_spi/test-s3boxlite.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-s3boxlite.esp32-s3-idf.yaml new file mode 100644 index 0000000000..11ad869d54 --- /dev/null +++ b/tests/components/mipi_spi/test-s3boxlite.esp32-s3-idf.yaml @@ -0,0 +1,41 @@ +spi: + - id: quad_spi + type: quad + interface: spi3 + clk_pin: + number: 47 + data_pins: + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: octal_spi + type: octal + interface: hardware + clk_pin: + number: 0 + data_pins: + - 36 + - 37 + - 38 + - 39 + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 9 + +display: + - platform: mipi_spi + model: S3BOXLITE diff --git a/tests/components/mipi_spi/test-t-display-s3-amoled-plus.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-display-s3-amoled-plus.esp32-s3-idf.yaml new file mode 100644 index 0000000000..dc328f950c --- /dev/null +++ b/tests/components/mipi_spi/test-t-display-s3-amoled-plus.esp32-s3-idf.yaml @@ -0,0 +1,9 @@ +spi: + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 9 + +display: + - platform: mipi_spi + model: T-DISPLAY-S3-AMOLED-PLUS diff --git a/tests/components/mipi_spi/test-t-display-s3-amoled.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-display-s3-amoled.esp32-s3-idf.yaml new file mode 100644 index 0000000000..f0432270dc --- /dev/null +++ b/tests/components/mipi_spi/test-t-display-s3-amoled.esp32-s3-idf.yaml @@ -0,0 +1,15 @@ +spi: + - id: quad_spi + type: quad + interface: spi3 + clk_pin: + number: 47 + data_pins: + - number: 40 + - number: 41 + - number: 42 + - number: 43 + +display: + - platform: mipi_spi + model: T-DISPLAY-S3-AMOLED diff --git a/tests/components/mipi_spi/test-t-display-s3-pro.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-display-s3-pro.esp32-s3-idf.yaml new file mode 100644 index 0000000000..5cda38e096 --- /dev/null +++ b/tests/components/mipi_spi/test-t-display-s3-pro.esp32-s3-idf.yaml @@ -0,0 +1,9 @@ +spi: + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 40 + +display: + - platform: mipi_spi + model: T-DISPLAY-S3-PRO diff --git a/tests/components/mipi_spi/test-t-display-s3.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-display-s3.esp32-s3-idf.yaml new file mode 100644 index 0000000000..144bde8366 --- /dev/null +++ b/tests/components/mipi_spi/test-t-display-s3.esp32-s3-idf.yaml @@ -0,0 +1,37 @@ +spi: + - id: quad_spi + type: quad + interface: spi3 + clk_pin: + number: 47 + data_pins: + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: octal_spi + type: octal + interface: hardware + clk_pin: + number: 0 + data_pins: + - 36 + - 37 + - 38 + - 39 + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + +display: + - platform: mipi_spi + model: T-DISPLAY-S3 diff --git a/tests/components/mipi_spi/test-t-display.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-display.esp32-s3-idf.yaml new file mode 100644 index 0000000000..39339b5ae2 --- /dev/null +++ b/tests/components/mipi_spi/test-t-display.esp32-s3-idf.yaml @@ -0,0 +1,41 @@ +spi: + - id: quad_spi + type: quad + interface: spi3 + clk_pin: + number: 47 + data_pins: + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: octal_spi + type: octal + interface: hardware + clk_pin: + number: 0 + data_pins: + - 36 + - 37 + - 38 + - 39 + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 9 + +display: + - platform: mipi_spi + model: T-DISPLAY diff --git a/tests/components/mipi_spi/test-t-embed.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-embed.esp32-s3-idf.yaml new file mode 100644 index 0000000000..6c9edb25b3 --- /dev/null +++ b/tests/components/mipi_spi/test-t-embed.esp32-s3-idf.yaml @@ -0,0 +1,9 @@ +spi: + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 40 + +display: + - platform: mipi_spi + model: T-EMBED diff --git a/tests/components/mipi_spi/test-t4-s3.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t4-s3.esp32-s3-idf.yaml new file mode 100644 index 0000000000..46eaedb7cb --- /dev/null +++ b/tests/components/mipi_spi/test-t4-s3.esp32-s3-idf.yaml @@ -0,0 +1,41 @@ +spi: + - id: quad_spi + type: quad + interface: spi3 + clk_pin: + number: 47 + data_pins: + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: octal_spi + type: octal + interface: hardware + clk_pin: + number: 0 + data_pins: + - 36 + - 37 + - 38 + - 39 + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: spi_id_3 + interface: any + clk_pin: 8 + mosi_pin: 9 + +display: + - platform: mipi_spi + model: T4-S3 diff --git a/tests/components/mipi_spi/test-wt32-sc01-plus.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-wt32-sc01-plus.esp32-s3-idf.yaml new file mode 100644 index 0000000000..3efb05ec89 --- /dev/null +++ b/tests/components/mipi_spi/test-wt32-sc01-plus.esp32-s3-idf.yaml @@ -0,0 +1,37 @@ +spi: + - id: quad_spi + type: quad + interface: spi3 + clk_pin: + number: 47 + data_pins: + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + - id: octal_spi + type: octal + interface: hardware + clk_pin: + number: 9 + data_pins: + - 36 + - 37 + - 38 + - 39 + - allow_other_uses: true + number: 40 + - allow_other_uses: true + number: 41 + - allow_other_uses: true + number: 42 + - allow_other_uses: true + number: 43 + +display: + - platform: mipi_spi + model: WT32-SC01-PLUS diff --git a/tests/components/mipi_spi/test.esp32-ard.yaml b/tests/components/mipi_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..a5ef77dabc --- /dev/null +++ b/tests/components/mipi_spi/test.esp32-ard.yaml @@ -0,0 +1,15 @@ +substitutions: + clk_pin: GPIO16 + mosi_pin: GPIO17 + miso_pin: GPIO15 + dc_pin: GPIO14 + cs_pin: GPIO13 + enable_pin: GPIO19 + reset_pin: GPIO20 + +display: + - platform: mipi_spi + model: LANBON-L8 + +packages: + display: !include common.yaml diff --git a/tests/components/mipi_spi/test.esp32-c3-ard.yaml b/tests/components/mipi_spi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..c17748c569 --- /dev/null +++ b/tests/components/mipi_spi/test.esp32-c3-ard.yaml @@ -0,0 +1,10 @@ +substitutions: + clk_pin: GPIO6 + mosi_pin: GPIO7 + miso_pin: GPIO5 + dc_pin: GPIO21 + cs_pin: GPIO18 + enable_pin: GPIO19 + reset_pin: GPIO20 + +<<: !include common.yaml diff --git a/tests/components/mipi_spi/test.esp32-c3-idf.yaml b/tests/components/mipi_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c17748c569 --- /dev/null +++ b/tests/components/mipi_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,10 @@ +substitutions: + clk_pin: GPIO6 + mosi_pin: GPIO7 + miso_pin: GPIO5 + dc_pin: GPIO21 + cs_pin: GPIO18 + enable_pin: GPIO19 + reset_pin: GPIO20 + +<<: !include common.yaml diff --git a/tests/components/mipi_spi/test.esp32-idf.yaml b/tests/components/mipi_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..653ccb4910 --- /dev/null +++ b/tests/components/mipi_spi/test.esp32-idf.yaml @@ -0,0 +1,15 @@ +substitutions: + clk_pin: GPIO16 + mosi_pin: GPIO17 + miso_pin: GPIO15 + dc_pin: GPIO21 + cs_pin: GPIO18 + enable_pin: GPIO19 + reset_pin: GPIO20 + +packages: + display: !include common.yaml + +display: + - platform: mipi_spi + model: m5core diff --git a/tests/components/mipi_spi/test.rp2040-ard.yaml b/tests/components/mipi_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..5d7333853b --- /dev/null +++ b/tests/components/mipi_spi/test.rp2040-ard.yaml @@ -0,0 +1,10 @@ +substitutions: + clk_pin: GPIO2 + mosi_pin: GPIO3 + miso_pin: GPIO4 + dc_pin: GPIO14 + cs_pin: GPIO13 + enable_pin: GPIO19 + reset_pin: GPIO20 + +<<: !include common.yaml From 28e29efd98e780a4b8d2804e8c9d29f77c852a4e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 09:54:26 +1200 Subject: [PATCH 08/15] Bump version to 2025.6.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 0f811aa870..8c2a1066bd 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2025.5.0-dev" +__version__ = "2025.6.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 06302441955a48dd12fab1df9c251c74aa27843b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 09:54:26 +1200 Subject: [PATCH 09/15] Bump version to 2025.5.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 0f811aa870..0974a673ec 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2025.5.0-dev" +__version__ = "2025.5.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From f28a373898924ee6e0ca1b3053887313a32b29b5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 11:48:54 +1200 Subject: [PATCH 10/15] [media_player] Deprecate ``MEDIA_PLAYER_SCHEMA`` (#8784) --- .../i2s_audio/media_player/__init__.py | 19 ++++++----- esphome/components/media_player/__init__.py | 34 ++++++++++++++++++- .../speaker/media_player/__init__.py | 6 ++-- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index bed25b011f..51001e9444 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -2,7 +2,7 @@ from esphome import pins import esphome.codegen as cg from esphome.components import esp32, media_player import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_MODE +from esphome.const import CONF_MODE from .. import ( CONF_I2S_AUDIO_ID, @@ -57,16 +57,17 @@ def validate_esp32_variant(config): CONFIG_SCHEMA = cv.All( cv.typed_schema( { - "internal": media_player.MEDIA_PLAYER_SCHEMA.extend( + "internal": media_player.media_player_schema(I2SAudioMediaPlayer) + .extend( { - cv.GenerateID(): cv.declare_id(I2SAudioMediaPlayer), cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True), } - ).extend(cv.COMPONENT_SCHEMA), - "external": media_player.MEDIA_PLAYER_SCHEMA.extend( + ) + .extend(cv.COMPONENT_SCHEMA), + "external": media_player.media_player_schema(I2SAudioMediaPlayer) + .extend( { - cv.GenerateID(): cv.declare_id(I2SAudioMediaPlayer), cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), cv.Required( CONF_I2S_DOUT_PIN @@ -79,7 +80,8 @@ CONFIG_SCHEMA = cv.All( *I2C_COMM_FMT_OPTIONS, lower=True ), } - ).extend(cv.COMPONENT_SCHEMA), + ) + .extend(cv.COMPONENT_SCHEMA), }, key=CONF_DAC_TYPE, ), @@ -97,9 +99,8 @@ FINAL_VALIDATE_SCHEMA = _final_validate async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await media_player.new_media_player(config) await cg.register_component(var, config) - await media_player.register_media_player(var, config) await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index 14fe1fdb6a..2f5fe0c03e 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -2,6 +2,8 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ICON, CONF_ID, CONF_ON_IDLE, CONF_ON_STATE, @@ -10,6 +12,7 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.coroutine import coroutine_with_priority +from esphome.cpp_generator import MockObjClass from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@jesserockz"] @@ -103,7 +106,13 @@ async def register_media_player(var, config): await setup_media_player_core_(var, config) -MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( +async def new_media_player(config, *args): + var = cg.new_Pvariable(config[CONF_ID], *args) + await register_media_player(var, config) + return var + + +_MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( { cv.Optional(CONF_ON_STATE): automation.validate_automation( { @@ -134,6 +143,29 @@ MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( ) +def media_player_schema( + class_: MockObjClass, + *, + entity_category: str = cv.UNDEFINED, + icon: str = cv.UNDEFINED, +) -> cv.Schema: + schema = {cv.GenerateID(CONF_ID): cv.declare_id(class_)} + + for key, default, validator in [ + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + (CONF_ICON, icon, cv.icon), + ]: + if default is not cv.UNDEFINED: + schema[cv.Optional(key, default=default)] = validator + + return _MEDIA_PLAYER_SCHEMA.extend(schema) + + +# Remove before 2025.11.0 +MEDIA_PLAYER_SCHEMA = media_player_schema(MediaPlayer) +MEDIA_PLAYER_SCHEMA.add_extra(cv.deprecated_schema_constant("media_player")) + + MEDIA_PLAYER_ACTION_SCHEMA = automation.maybe_simple_id( cv.Schema( { diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 35d763b1f8..cedafe214d 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -271,9 +271,8 @@ PIPELINE_SCHEMA = cv.Schema( ) CONFIG_SCHEMA = cv.All( - media_player.MEDIA_PLAYER_SCHEMA.extend( + media_player.media_player_schema(SpeakerMediaPlayer).extend( { - cv.GenerateID(): cv.declare_id(SpeakerMediaPlayer), cv.Required(CONF_ANNOUNCEMENT_PIPELINE): PIPELINE_SCHEMA, cv.Optional(CONF_MEDIA_PIPELINE): PIPELINE_SCHEMA, cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range( @@ -343,9 +342,8 @@ async def to_code(config): # Allocate wifi buffers in PSRAM esp32.add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) - var = cg.new_Pvariable(config[CONF_ID]) + var = await media_player.new_media_player(config) await cg.register_component(var, config) - await media_player.register_media_player(var, config) cg.add_define("USE_OTA_STATE_CALLBACK") From a835ab48bc9e4f6ca5a909ae46eb9f2563130bc3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 13:25:21 +1200 Subject: [PATCH 11/15] [schema] Get component name if available for deprecation warning (#8785) --- esphome/config_validation.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 88a805591d..2eabcc8568 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -2077,14 +2077,20 @@ def rename_key(old_key, new_key): # Remove before 2025.11.0 def deprecated_schema_constant(entity_type: str): def validator(config): + type: str = "unknown" + if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID): + type = str(id.type).split("::", maxsplit=1)[0] _LOGGER.warning( "Using `%s.%s_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. " "Please use `%s.%s_schema(...)` instead. " - "If you are seeing this, report an issue to the external_component author and ask them to update it.", + "If you are seeing this, report an issue to the external_component author and ask them to update it. " + "https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. " + "Component using this schema: %s", entity_type, entity_type.upper(), entity_type, entity_type, + type, ) return config From 42c355e6d77b42813875042363d5bb278e7516d0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 13:30:11 +1200 Subject: [PATCH 12/15] [fan] Update components to use ``fan_schema(...)`` (#8786) --- esphome/components/bedjet/fan/__init__.py | 13 ++------ esphome/components/binary/fan/__init__.py | 29 ++++++++--------- esphome/components/copy/fan/__init__.py | 20 ++++++------ esphome/components/hbridge/fan/__init__.py | 36 +++++++++++---------- esphome/components/speed/fan/__init__.py | 33 ++++++++++--------- esphome/components/template/fan/__init__.py | 26 ++++++++------- 6 files changed, 76 insertions(+), 81 deletions(-) diff --git a/esphome/components/bedjet/fan/__init__.py b/esphome/components/bedjet/fan/__init__.py index fdf0636153..a4a611fefc 100644 --- a/esphome/components/bedjet/fan/__init__.py +++ b/esphome/components/bedjet/fan/__init__.py @@ -1,31 +1,22 @@ -import logging - import esphome.codegen as cg from esphome.components import fan import esphome.config_validation as cv -from esphome.const import CONF_ID from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child -_LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@jhansche"] DEPENDENCIES = ["bedjet"] BedJetFan = bedjet_ns.class_("BedJetFan", fan.Fan, cg.PollingComponent) CONFIG_SCHEMA = ( - fan.FAN_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(BedJetFan), - } - ) + fan.fan_schema(BedJetFan) .extend(cv.polling_component_schema("60s")) .extend(BEDJET_CLIENT_SCHEMA) ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await fan.new_fan(config) await cg.register_component(var, config) - await fan.register_fan(var, config) await register_bedjet_child(var, config) diff --git a/esphome/components/binary/fan/__init__.py b/esphome/components/binary/fan/__init__.py index a504ef642c..dadcf52372 100644 --- a/esphome/components/binary/fan/__init__.py +++ b/esphome/components/binary/fan/__init__.py @@ -1,31 +1,28 @@ import esphome.codegen as cg from esphome.components import fan, output import esphome.config_validation as cv -from esphome.const import ( - CONF_DIRECTION_OUTPUT, - CONF_OSCILLATION_OUTPUT, - CONF_OUTPUT, - CONF_OUTPUT_ID, -) +from esphome.const import CONF_DIRECTION_OUTPUT, CONF_OSCILLATION_OUTPUT, CONF_OUTPUT from .. import binary_ns BinaryFan = binary_ns.class_("BinaryFan", fan.Fan, cg.Component) -CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( - { - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(BinaryFan), - cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), - cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), - cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + fan.fan_schema(BinaryFan) + .extend( + { + cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + var = await fan.new_fan(config) await cg.register_component(var, config) - await fan.register_fan(var, config) output_ = await cg.get_variable(config[CONF_OUTPUT]) cg.add(var.set_output(output_)) diff --git a/esphome/components/copy/fan/__init__.py b/esphome/components/copy/fan/__init__.py index 04872fb029..a208e5f80a 100644 --- a/esphome/components/copy/fan/__init__.py +++ b/esphome/components/copy/fan/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import fan import esphome.config_validation as cv -from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, CONF_SOURCE_ID +from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_SOURCE_ID from esphome.core.entity_helpers import inherit_property_from from .. import copy_ns @@ -9,12 +9,15 @@ from .. import copy_ns CopyFan = copy_ns.class_("CopyFan", fan.Fan, cg.Component) -CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(CopyFan), - cv.Required(CONF_SOURCE_ID): cv.use_id(fan.Fan), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + fan.fan_schema(CopyFan) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(fan.Fan), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) FINAL_VALIDATE_SCHEMA = cv.All( inherit_property_from(CONF_ICON, CONF_SOURCE_ID), @@ -23,8 +26,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await fan.register_fan(var, config) + var = await fan.new_fan(config) await cg.register_component(var, config) source = await cg.get_variable(config[CONF_SOURCE_ID]) diff --git a/esphome/components/hbridge/fan/__init__.py b/esphome/components/hbridge/fan/__init__.py index 4309a64359..31a20a8981 100644 --- a/esphome/components/hbridge/fan/__init__.py +++ b/esphome/components/hbridge/fan/__init__.py @@ -30,25 +30,28 @@ DECAY_MODE_OPTIONS = { # Actions BrakeAction = hbridge_ns.class_("BrakeAction", automation.Action) -CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( - { - cv.GenerateID(CONF_ID): cv.declare_id(HBridgeFan), - cv.Required(CONF_PIN_A): cv.use_id(output.FloatOutput), - cv.Required(CONF_PIN_B): cv.use_id(output.FloatOutput), - cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum( - DECAY_MODE_OPTIONS, upper=True - ), - cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), - cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput), - cv.Optional(CONF_PRESET_MODES): validate_preset_modes, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + fan.fan_schema(HBridgeFan) + .extend( + { + cv.Required(CONF_PIN_A): cv.use_id(output.FloatOutput), + cv.Required(CONF_PIN_B): cv.use_id(output.FloatOutput), + cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum( + DECAY_MODE_OPTIONS, upper=True + ), + cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), + cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput), + cv.Optional(CONF_PRESET_MODES): validate_preset_modes, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) @automation.register_action( "fan.hbridge.brake", BrakeAction, - maybe_simple_id({cv.Required(CONF_ID): cv.use_id(HBridgeFan)}), + maybe_simple_id({cv.GenerateID(): cv.use_id(HBridgeFan)}), ) async def fan_hbridge_brake_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -56,13 +59,12 @@ async def fan_hbridge_brake_to_code(config, action_id, template_arg, args): async def to_code(config): - var = cg.new_Pvariable( - config[CONF_ID], + var = await fan.new_fan( + config, config[CONF_SPEED_COUNT], config[CONF_DECAY_MODE], ) await cg.register_component(var, config) - await fan.register_fan(var, config) pin_a_ = await cg.get_variable(config[CONF_PIN_A]) cg.add(var.set_pin_a(pin_a_)) pin_b_ = await cg.get_variable(config[CONF_PIN_B]) diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py index fe43ac6a3a..3c495f3160 100644 --- a/esphome/components/speed/fan/__init__.py +++ b/esphome/components/speed/fan/__init__.py @@ -6,7 +6,6 @@ from esphome.const import ( CONF_DIRECTION_OUTPUT, CONF_OSCILLATION_OUTPUT, CONF_OUTPUT, - CONF_OUTPUT_ID, CONF_PRESET_MODES, CONF_SPEED, CONF_SPEED_COUNT, @@ -16,25 +15,27 @@ from .. import speed_ns SpeedFan = speed_ns.class_("SpeedFan", cg.Component, fan.Fan) -CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( - { - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(SpeedFan), - cv.Required(CONF_OUTPUT): cv.use_id(output.FloatOutput), - cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), - cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), - cv.Optional(CONF_SPEED): cv.invalid( - "Configuring individual speeds is deprecated." - ), - cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), - cv.Optional(CONF_PRESET_MODES): validate_preset_modes, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + fan.fan_schema(SpeedFan) + .extend( + { + cv.Required(CONF_OUTPUT): cv.use_id(output.FloatOutput), + cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_SPEED): cv.invalid( + "Configuring individual speeds is deprecated." + ), + cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), + cv.Optional(CONF_PRESET_MODES): validate_preset_modes, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_OUTPUT_ID], config[CONF_SPEED_COUNT]) + var = await fan.new_fan(config, config[CONF_SPEED_COUNT]) await cg.register_component(var, config) - await fan.register_fan(var, config) output_ = await cg.get_variable(config[CONF_OUTPUT]) cg.add(var.set_output(output_)) diff --git a/esphome/components/template/fan/__init__.py b/esphome/components/template/fan/__init__.py index c885866d40..72b20e1efe 100644 --- a/esphome/components/template/fan/__init__.py +++ b/esphome/components/template/fan/__init__.py @@ -2,7 +2,7 @@ import esphome.codegen as cg from esphome.components import fan from esphome.components.fan import validate_preset_modes import esphome.config_validation as cv -from esphome.const import CONF_OUTPUT_ID, CONF_PRESET_MODES, CONF_SPEED_COUNT +from esphome.const import CONF_PRESET_MODES, CONF_SPEED_COUNT from .. import template_ns @@ -13,21 +13,23 @@ TemplateFan = template_ns.class_("TemplateFan", cg.Component, fan.Fan) CONF_HAS_DIRECTION = "has_direction" CONF_HAS_OSCILLATING = "has_oscillating" -CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( - { - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TemplateFan), - cv.Optional(CONF_HAS_DIRECTION, default=False): cv.boolean, - cv.Optional(CONF_HAS_OSCILLATING, default=False): cv.boolean, - cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1), - cv.Optional(CONF_PRESET_MODES): validate_preset_modes, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + fan.fan_schema(TemplateFan) + .extend( + { + cv.Optional(CONF_HAS_DIRECTION, default=False): cv.boolean, + cv.Optional(CONF_HAS_OSCILLATING, default=False): cv.boolean, + cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1), + cv.Optional(CONF_PRESET_MODES): validate_preset_modes, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + var = await fan.new_fan(config) await cg.register_component(var, config) - await fan.register_fan(var, config) cg.add(var.set_has_direction(config[CONF_HAS_DIRECTION])) cg.add(var.set_has_oscillating(config[CONF_HAS_OSCILLATING])) From 5570a788fd2d846e14c8518058298c173f28f7e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 May 2025 21:23:32 -0500 Subject: [PATCH 13/15] Bump aioesphomeapi from 30.2.0 to 31.0.0 (#8779) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9547cd0ef0..985b9bf519 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.8.1 click==8.1.7 esphome-dashboard==20250415.0 -aioesphomeapi==30.2.0 +aioesphomeapi==31.0.0 zeroconf==0.147.0 puremagic==1.29 ruamel.yaml==0.18.10 # dashboard_import From ddb986b4fa36ff2931c8848460b59b8d7f6169b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 May 2025 23:34:33 -0500 Subject: [PATCH 14/15] Improve batching of BLE advertisements for better airtime efficiency (#8778) --- .../bluetooth_proxy/bluetooth_proxy.cpp | 77 ++++++++++++++----- .../bluetooth_proxy/bluetooth_proxy.h | 1 + .../esp32_ble_tracker/esp32_ble_tracker.cpp | 4 +- .../esp32_ble_tracker/esp32_ble_tracker.h | 2 +- 4 files changed, 61 insertions(+), 23 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 9c8bd4009f..915d2882d3 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -51,35 +51,60 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) return true; } +static constexpr size_t FLUSH_BATCH_SIZE = 8; +static std::vector &get_batch_buffer() { + static std::vector batch_buffer; + return batch_buffer; +} + bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_) return false; - api::BluetoothLERawAdvertisementsResponse resp; - // Pre-allocate the advertisements vector to avoid reallocations - resp.advertisements.reserve(count); + // Get the batch buffer reference + auto &batch_buffer = get_batch_buffer(); + // Reserve additional capacity if needed + size_t new_size = batch_buffer.size() + count; + if (batch_buffer.capacity() < new_size) { + batch_buffer.reserve(new_size); + } + + // Add new advertisements to the batch buffer for (size_t i = 0; i < count; i++) { auto &result = advertisements[i]; - api::BluetoothLERawAdvertisement adv; + uint8_t length = result.adv_data_len + result.scan_rsp_len; + + batch_buffer.emplace_back(); + auto &adv = batch_buffer.back(); adv.address = esp32_ble::ble_addr_to_uint64(result.bda); adv.rssi = result.rssi; adv.address_type = result.ble_addr_type; + adv.data.assign(&result.ble_adv[0], &result.ble_adv[length]); - uint8_t length = result.adv_data_len + result.scan_rsp_len; - adv.data.reserve(length); - // Use a bulk insert instead of individual push_backs - adv.data.insert(adv.data.end(), &result.ble_adv[0], &result.ble_adv[length]); - - resp.advertisements.push_back(std::move(adv)); - - ESP_LOGV(TAG, "Proxying raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0], + ESP_LOGV(TAG, "Queuing raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0], result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi); } - ESP_LOGV(TAG, "Proxying %d packets", count); - this->api_connection_->send_bluetooth_le_raw_advertisements_response(resp); + + // Only send if we've accumulated a good batch size to maximize batching efficiency + // https://github.com/esphome/backlog/issues/21 + if (batch_buffer.size() >= FLUSH_BATCH_SIZE) { + this->flush_pending_advertisements(); + } + return true; } + +void BluetoothProxy::flush_pending_advertisements() { + auto &batch_buffer = get_batch_buffer(); + if (batch_buffer.empty() || !api::global_api_server->is_connected() || this->api_connection_ == nullptr) + return; + + api::BluetoothLERawAdvertisementsResponse resp; + resp.advertisements.swap(batch_buffer); + this->api_connection_->send_bluetooth_le_raw_advertisements_response(resp); +} + void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { api::BluetoothLEAdvertisementResponse resp; resp.address = device.address_uint64(); @@ -91,28 +116,28 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi // Pre-allocate vectors based on known sizes auto service_uuids = device.get_service_uuids(); resp.service_uuids.reserve(service_uuids.size()); - for (auto uuid : service_uuids) { - resp.service_uuids.push_back(uuid.to_string()); + for (auto &uuid : service_uuids) { + resp.service_uuids.emplace_back(uuid.to_string()); } // Pre-allocate service data vector auto service_datas = device.get_service_datas(); resp.service_data.reserve(service_datas.size()); for (auto &data : service_datas) { - api::BluetoothServiceData service_data; + resp.service_data.emplace_back(); + auto &service_data = resp.service_data.back(); service_data.uuid = data.uuid.to_string(); service_data.data.assign(data.data.begin(), data.data.end()); - resp.service_data.push_back(std::move(service_data)); } // Pre-allocate manufacturer data vector auto manufacturer_datas = device.get_manufacturer_datas(); resp.manufacturer_data.reserve(manufacturer_datas.size()); for (auto &data : manufacturer_datas) { - api::BluetoothServiceData manufacturer_data; + resp.manufacturer_data.emplace_back(); + auto &manufacturer_data = resp.manufacturer_data.back(); manufacturer_data.uuid = data.uuid.to_string(); manufacturer_data.data.assign(data.data.begin(), data.data.end()); - resp.manufacturer_data.push_back(std::move(manufacturer_data)); } this->api_connection_->send_bluetooth_le_advertisement(resp); @@ -148,6 +173,18 @@ void BluetoothProxy::loop() { } return; } + + // Flush any pending BLE advertisements that have been accumulated but not yet sent + if (this->raw_advertisements_) { + static uint32_t last_flush_time = 0; + uint32_t now = millis(); + + // Flush accumulated advertisements every 100ms + if (now - last_flush_time >= 100) { + this->flush_pending_advertisements(); + last_flush_time = now; + } + } for (auto *connection : this->connections_) { if (connection->send_service_ == connection->service_count_) { connection->send_service_ = DONE_SENDING_SERVICES; diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index de24165fe8..f75e73e796 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -56,6 +56,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com void dump_config() override; void setup() override; void loop() override; + void flush_pending_advertisements(); esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; void register_connection(BluetoothConnection *connection) { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index be45b177ff..1a6071c9fe 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -122,7 +122,7 @@ void ESP32BLETracker::loop() { if (this->scanner_state_ == ScannerState::RUNNING && this->scan_result_index_ && // if it looks like we have a scan result we will take the lock - xSemaphoreTake(this->scan_result_lock_, 5L / portTICK_PERIOD_MS)) { + xSemaphoreTake(this->scan_result_lock_, 0)) { uint32_t index = this->scan_result_index_; if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); @@ -447,7 +447,7 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_ void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { ESP_LOGV(TAG, "gap_scan_result - event %d", param.search_evt); if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { - if (xSemaphoreTake(this->scan_result_lock_, 0L)) { + if (xSemaphoreTake(this->scan_result_lock_, 0)) { if (this->scan_result_index_ < ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { this->scan_result_buffer_[this->scan_result_index_++] = param; } diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 2e45d9602c..eea73a7d26 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -290,7 +290,7 @@ class ESP32BLETracker : public Component, #ifdef USE_PSRAM const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 32; #else - const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 16; + const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 20; #endif // USE_PSRAM esp_ble_gap_cb_param_t::ble_scan_result_evt_param *scan_result_buffer_; esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; From a12bd78ceb0ebcd2c8af978946b7dddcb102a9c2 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 May 2025 16:35:30 +1200 Subject: [PATCH 15/15] Fix release to pypi (#8789) --- .github/workflows/release.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 88704953ce..41e9186987 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,16 +56,14 @@ jobs: uses: actions/setup-python@v5.6.0 with: python-version: "3.x" - - name: Set up python environment - env: - ESPHOME_NO_VENV: 1 - run: script/setup - name: Build run: |- pip3 install build python3 -m build - name: Publish uses: pypa/gh-action-pypi-publish@v1.12.4 + with: + skip-existing: true deploy-docker: name: Build ESPHome ${{ matrix.platform.arch }}