From 8a08c688f628a0a2302db29dd52646168ea6fe99 Mon Sep 17 00:00:00 2001 From: schrob <83939986+schdro@users.noreply.github.com> Date: Thu, 12 Feb 2026 13:25:51 +0100 Subject: [PATCH 1/6] [mipi_spi] Add Waveshare 1.83 v2 panel (#13680) --- .../components/mipi_spi/models/waveshare.py | 83 ++++++++++++++++++- tests/components/mipi_spi/common.yaml | 24 +++++- 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/esphome/components/mipi_spi/models/waveshare.py b/esphome/components/mipi_spi/models/waveshare.py index e4e090da2e..cc86101f5e 100644 --- a/esphome/components/mipi_spi/models/waveshare.py +++ b/esphome/components/mipi_spi/models/waveshare.py @@ -1,4 +1,17 @@ -from esphome.components.mipi import DriverChip +from esphome.components.mipi import ( + ETMOD, + FRMCTR2, + GMCTRN1, + GMCTRP1, + IFCTR, + MODE_RGB, + PWCTR1, + PWCTR3, + PWCTR4, + PWCTR5, + PWSET, + DriverChip, +) import esphome.config_validation as cv from .amoled import CO5300 @@ -129,6 +142,16 @@ DriverChip( ), ), ) +ST7789P = DriverChip( + "ST7789P", + # Max supported dimensions + width=240, + height=320, + # SPI: RGB layout + color_order=MODE_RGB, + invert_colors=True, + draw_rounding=1, +) ILI9488_A.extend( "PICO-RESTOUCH-LCD-3.5", @@ -162,3 +185,61 @@ AXS15231.extend( cs_pin=9, reset_pin=21, ) + +# Waveshare 1.83-v2 +# +# Do not use on 1.83-v1: Vendor warning on different chip! +ST7789P.extend( + "WAVESHARE-1.83-V2", + # Panel size smaller than ST7789 max allowed + width=240, + height=284, + # Vendor specific init derived from vendor sample code + # "LCD_1.83_Code_Rev2/ESP32/LCD_1in83/LCD_Driver.cpp" + # Compatible MIT license, see esphome/LICENSE file. + initsequence=( + (FRMCTR2, 0x0C, 0x0C, 0x00, 0x33, 0x33), + (ETMOD, 0x35), + (0xBB, 0x19), + (PWCTR1, 0x2C), + (PWCTR3, 0x01), + (PWCTR4, 0x12), + (PWCTR5, 0x20), + (IFCTR, 0x0F), + (PWSET, 0xA4, 0xA1), + ( + GMCTRP1, + 0xD0, + 0x04, + 0x0D, + 0x11, + 0x13, + 0x2B, + 0x3F, + 0x54, + 0x4C, + 0x18, + 0x0D, + 0x0B, + 0x1F, + 0x23, + ), + ( + GMCTRN1, + 0xD0, + 0x04, + 0x0C, + 0x11, + 0x13, + 0x2C, + 0x3F, + 0x44, + 0x51, + 0x2F, + 0x1F, + 0x1F, + 0x20, + 0x23, + ), + ), +) diff --git a/tests/components/mipi_spi/common.yaml b/tests/components/mipi_spi/common.yaml index 692a9f436e..a867b726ed 100644 --- a/tests/components/mipi_spi/common.yaml +++ b/tests/components/mipi_spi/common.yaml @@ -3,9 +3,15 @@ display: spi_16: true pixel_mode: 18bit model: ili9488 - dc_pin: ${dc_pin} - cs_pin: ${cs_pin} - reset_pin: ${reset_pin} + dc_pin: + allow_other_uses: true + number: ${dc_pin} + cs_pin: + allow_other_uses: true + number: ${cs_pin} + reset_pin: + allow_other_uses: true + number: ${reset_pin} data_rate: 20MHz invert_colors: true show_test_card: true @@ -24,3 +30,15 @@ display: height: 200 enable_pin: ${enable_pin} bus_mode: single + + - platform: mipi_spi + model: WAVESHARE-1.83-V2 + dc_pin: + allow_other_uses: true + number: ${dc_pin} + cs_pin: + allow_other_uses: true + number: ${cs_pin} + reset_pin: + allow_other_uses: true + number: ${reset_pin} From 7b251dcc310a7751f85c22de33d493288c0f700a Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Thu, 12 Feb 2026 13:23:59 -0300 Subject: [PATCH 2/6] [schema-gen] fix Windows: ensure UTF-8 encoding when reading component files (#13952) --- script/build_language_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/build_language_schema.py b/script/build_language_schema.py index c9501cb193..bea540dc63 100755 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -369,7 +369,7 @@ def get_logger_tags(): "api.service", ] for file in CORE_COMPONENTS_PATH.rglob("*.cpp"): - data = file.read_text() + data = file.read_text(encoding="utf-8") match = pattern.search(data) if match: tags.append(match.group(1)) From 9aa98ed6c65d7f2f025bdc9c5eb3ebe78e9bb71d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 10:26:10 -0600 Subject: [PATCH 3/6] [uart] Remove redundant mutex, fix flush race, conditional event queue (#13955) --- .../uart/uart_component_esp_idf.cpp | 49 ++++++++++--------- .../components/uart/uart_component_esp_idf.h | 17 +++++-- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 19b9a4077f..6c242220a6 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -90,7 +90,6 @@ void IDFUARTComponent::setup() { return; } this->uart_num_ = static_cast(next_uart_num++); - this->lock_ = xSemaphoreCreateMutex(); #if (SOC_UART_LP_NUM >= 1) size_t fifo_len = ((this->uart_num_ < SOC_UART_HP_NUM) ? SOC_UART_FIFO_LEN : SOC_LP_UART_FIFO_LEN); @@ -102,11 +101,7 @@ void IDFUARTComponent::setup() { this->rx_buffer_size_ = fifo_len * 2; } - xSemaphoreTake(this->lock_, portMAX_DELAY); - this->load_settings(false); - - xSemaphoreGive(this->lock_); } void IDFUARTComponent::load_settings(bool dump_config) { @@ -126,13 +121,20 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } } +#ifdef USE_UART_WAKE_LOOP_ON_RX + constexpr int event_queue_size = 20; + QueueHandle_t *event_queue_ptr = &this->uart_event_queue_; +#else + constexpr int event_queue_size = 0; + QueueHandle_t *event_queue_ptr = nullptr; +#endif err = uart_driver_install(this->uart_num_, // UART number this->rx_buffer_size_, // RX ring buffer size - 0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will - // block task until all data has been sent out - 20, // event queue size/depth - &this->uart_event_queue_, // event queue - 0 // Flags used to allocate the interrupt + 0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will + // block task until all data has been sent out + event_queue_size, // event queue size/depth + event_queue_ptr, // event queue + 0 // Flags used to allocate the interrupt ); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); @@ -282,9 +284,7 @@ void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) { } void IDFUARTComponent::write_array(const uint8_t *data, size_t len) { - xSemaphoreTake(this->lock_, portMAX_DELAY); int32_t write_len = uart_write_bytes(this->uart_num_, data, len); - xSemaphoreGive(this->lock_); if (write_len != (int32_t) len) { ESP_LOGW(TAG, "uart_write_bytes failed: %d != %zu", write_len, len); this->mark_failed(); @@ -299,7 +299,6 @@ void IDFUARTComponent::write_array(const uint8_t *data, size_t len) { bool IDFUARTComponent::peek_byte(uint8_t *data) { if (!this->check_read_timeout_()) return false; - xSemaphoreTake(this->lock_, portMAX_DELAY); if (this->has_peek_) { *data = this->peek_byte_; } else { @@ -311,7 +310,6 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) { this->peek_byte_ = *data; } } - xSemaphoreGive(this->lock_); return true; } @@ -320,7 +318,6 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { int32_t read_len = 0; if (!this->check_read_timeout_(len)) return false; - xSemaphoreTake(this->lock_, portMAX_DELAY); if (this->has_peek_) { length_to_read--; *data = this->peek_byte_; @@ -329,7 +326,6 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { } if (length_to_read > 0) read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS); - xSemaphoreGive(this->lock_); #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { this->debug_callback_.call(UART_DIRECTION_RX, data[i]); @@ -342,9 +338,7 @@ size_t IDFUARTComponent::available() { size_t available = 0; esp_err_t err; - xSemaphoreTake(this->lock_, portMAX_DELAY); err = uart_get_buffered_data_len(this->uart_num_, &available); - xSemaphoreGive(this->lock_); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_get_buffered_data_len failed: %s", esp_err_to_name(err)); @@ -358,9 +352,7 @@ size_t IDFUARTComponent::available() { void IDFUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing"); - xSemaphoreTake(this->lock_, portMAX_DELAY); uart_wait_tx_done(this->uart_num_, portMAX_DELAY); - xSemaphoreGive(this->lock_); } void IDFUARTComponent::check_logger_conflict() {} @@ -384,6 +376,13 @@ void IDFUARTComponent::start_rx_event_task_() { ESP_LOGV(TAG, "RX event task started"); } +// FreeRTOS task that relays UART ISR events to the main loop. +// This task exists because wake_loop_threadsafe() is not ISR-safe (it uses a +// UDP loopback socket), so we need a task as an ISR-to-main-loop trampoline. +// IMPORTANT: This task must NOT call any UART wrapper methods (read_array, +// write_array, peek_byte, etc.) or touch has_peek_/peek_byte_ — all reading +// is done by the main loop. This task only reads from the event queue and +// calls App.wake_loop_threadsafe(). void IDFUARTComponent::rx_event_task_func(void *param) { auto *self = static_cast(param); uart_event_t event; @@ -405,8 +404,14 @@ void IDFUARTComponent::rx_event_task_func(void *param) { case UART_FIFO_OVF: case UART_BUFFER_FULL: - ESP_LOGW(TAG, "FIFO overflow or ring buffer full - clearing"); - uart_flush_input(self->uart_num_); + // Don't call uart_flush_input() here — this task does not own the read side. + // ESP-IDF examples flush on overflow because the same task handles both events + // and reads, so flush and read are serialized. Here, reads happen on the main + // loop, so flushing from this task races with read_array() and can destroy data + // mid-read. The driver self-heals without an explicit flush: uart_read_bytes() + // calls uart_check_buf_full() after each chunk, which moves stashed FIFO bytes + // into the ring buffer and re-enables RX interrupts once space is freed. + ESP_LOGW(TAG, "FIFO overflow or ring buffer full"); #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); #endif diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 1ecb02d7ab..1517eab509 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -8,6 +8,13 @@ namespace esphome::uart { +/// ESP-IDF UART driver wrapper. +/// +/// Thread safety: All public methods must only be called from the main loop. +/// The ESP-IDF UART driver API does not guarantee thread safety, and ESPHome's +/// peek byte state (has_peek_/peek_byte_) is not synchronized. The rx_event_task +/// (when enabled) must not call any of these methods — it communicates with the +/// main loop exclusively via App.wake_loop_threadsafe(). class IDFUARTComponent : public UARTComponent, public Component { public: void setup() override; @@ -26,7 +33,9 @@ class IDFUARTComponent : public UARTComponent, public Component { void flush() override; uint8_t get_hw_serial_number() { return this->uart_num_; } +#ifdef USE_UART_WAKE_LOOP_ON_RX QueueHandle_t *get_uart_event_queue() { return &this->uart_event_queue_; } +#endif /** * Load the UART with the current settings. @@ -46,18 +55,20 @@ class IDFUARTComponent : public UARTComponent, public Component { protected: void check_logger_conflict() override; uart_port_t uart_num_; - QueueHandle_t uart_event_queue_; uart_config_t get_config_(); - SemaphoreHandle_t lock_; bool has_peek_{false}; uint8_t peek_byte_; #ifdef USE_UART_WAKE_LOOP_ON_RX - // RX notification support + // RX notification support — runs on a separate FreeRTOS task. + // IMPORTANT: rx_event_task_func must NOT call any UART wrapper methods (read_array, + // write_array, etc.) or touch has_peek_/peek_byte_. It must only read from the + // event queue and call App.wake_loop_threadsafe(). void start_rx_event_task_(); static void rx_event_task_func(void *param); + QueueHandle_t uart_event_queue_; TaskHandle_t rx_event_task_handle_{nullptr}; #endif // USE_UART_WAKE_LOOP_ON_RX }; From 725e774fe79aac107288af979bf8ff7da7479feb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 10:26:36 -0600 Subject: [PATCH 4/6] [web_server] Guard icon JSON field with USE_ENTITY_ICON (#13948) --- esphome/components/web_server/web_server.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index dfd602be6b..7da8b49c6d 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -557,7 +557,9 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J root[ESPHOME_F("device")] = device_name; } #endif +#ifdef USE_ENTITY_ICON root[ESPHOME_F("icon")] = obj->get_icon_ref(); +#endif root[ESPHOME_F("entity_category")] = obj->get_entity_category(); bool is_disabled = obj->is_disabled_by_default(); if (is_disabled) From 60fef5e656396593f2a2867241ebf7c576549917 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 10:26:54 -0600 Subject: [PATCH 5/6] [analyze_memory] Fix mDNS packet buffer miscategorized as wifi_config (#13949) Co-authored-by: Claude Opus 4.6 --- esphome/analyze_memory/const.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/analyze_memory/const.py b/esphome/analyze_memory/const.py index 66866615a6..3bdf555ae3 100644 --- a/esphome/analyze_memory/const.py +++ b/esphome/analyze_memory/const.py @@ -256,7 +256,7 @@ SYMBOL_PATTERNS = { "ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"], # Order matters! More specific categories must come before general ones. # mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern - "mdns_lib": ["mdns"], + "mdns_lib": ["mdns", "packet$"], # memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols "memory_mgmt": [ "mem_", @@ -794,7 +794,6 @@ SYMBOL_PATTERNS = { "s_dp", "s_ni", "s_reg_dump", - "packet$", "d_mult_table", "K", "fcstab", From 932067c3324410d7daf098bb6b3455371e56fb1b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Feb 2026 10:47:23 -0600 Subject: [PATCH 6/6] [wifi] Deprecate wifi_ssid() in favor of wifi_ssid_to() wifi_ssid() returns std::string which allocates on the heap. All internal callers have already been migrated to the buffer-based wifi_ssid_to() alternative. Mark as deprecated with a 6-month removal window (2026.9.0). --- esphome/components/wifi/wifi_component.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ac28a1bc81..53ff0d9cad 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -502,6 +502,8 @@ class WiFiComponent : public Component { } network::IPAddresses wifi_sta_ip_addresses(); + // Remove before 2026.9.0 + ESPDEPRECATED("Use wifi_ssid_to() instead. Removed in 2026.9.0", "2026.3.0") std::string wifi_ssid(); /// Write SSID to buffer without heap allocation. /// Returns pointer to buffer, or empty string if not connected.